diff --git a/README.md b/README.md
index 4e4029f..ef1bc37 100644
--- a/README.md
+++ b/README.md
@@ -1,193 +1,696 @@
-# TypeScript Library Template
+# bitf
-An opinionated production-ready TypeScript library template with automated builds, testing, and releases.
+`bitf` is a tiny and fast bit flags _(other terms: bit fields, optionsets)_ management library for TypeScript/JavaScript
-
+## What are bitflags?
-## Features
+Bitflags are a way to represent a set of boolean options using a single integer. Each bit in the number corresponds to a different option, allowing for efficient storage and manipulation of multiple flags at once. Concept of creating this library originates from the article ["Everything About Bitflags"](https://neg4n.dev/explore/everything-about-bitflags).
-- ๐ฆ **Dual Package Support** - Outputs CommonJS and ESM builds
-- ๐ก๏ธ **Type Safety** - Extremely strict TypeScript configuration
-- โ
**Build Validation** - Uses `@arethetypeswrong/cli` to check package exports
-- ๐งช **Automated Testing** - Vitest with coverage reporting
-- ๐จ **Code Quality** - Biome linting and formatting with pre-commit hooks
-- ๐ **Automated Releases** - Semantic versioning with changelog generation
-- โ๏ธ **CI/CD Pipeline** - GitHub Actions for testing and publishing
-- ๐ง **One-Click Setup** - Automated repository configuration with `init.sh` script
- - ๐๏ธ **Repository rulesets** - Branch protection with linear history and PR reviews
- - ๐ท **Feature cleanup** - Disable wikis, projects, squash/merge commits
- - ๐ **Merge restrictions** - Rebase-only workflow at repository and ruleset levels
- - ๐ **Admin bypass** - Repository administrators can bypass protection rules
- - ๐ **Actions verification** - Ensure GitHub Actions are enabled
- - ๐๏ธ **Secrets validation** - Check and guide setup of required secrets
+### Features
-## Tech Stack
+- Type safety via [Tagged](https://github.com/sindresorhus/type-fest/blob/main/source/tagged.d.ts) types.
+- Lightweight and fast, almost native bitwise performance with minimal abstraction layer.
+- No runtime dependencies.
+- Robust and ready-to-use on production.
+- Comprehensive tests suite with 100% test coverage.
+- `.describe()` iterator for better debugging and visualization of the bit flags.
+- Range guards while defining bit flags.
-- **TypeScript** - Strict configuration for type safety
-- **Rollup** - Builds both CommonJS and ESM formats
-- **Biome** - Fast linting and formatting
-- **Vitest** - Testing with coverage reports
-- **Husky** - Pre-commit hooks for code quality
-- **Semantic Release** - Automated versioning and releases
-- **pnpm** - Fast package management with Corepack
-- **GitHub Actions** - CI/CD pipeline
-
-## Setup
-
-### 1. Use the template
-
-Run this in your terminal _[GitHub CLI](https://cli.github.com) required_
+## Installation
```bash
-gh repo create my-typescript-library --clone --template neg4n/typescript-library-template --private && cd my-typescript-library
+npm i bitf
+# or
+yarn add bitf
+# or
+pnpm add bitf
+# or
+bun add bitf
+```
+
+## Usage Example
+
+
+This example shows a pizza ordering system where customers can customize their toppings. The restaurant starts with a default Pepperoni pizza and enforces three business rules:
+
+1. cheese must always be included (it's the base of toppings every pizza being sold)
+2. only one meat type is allowed per pizza (to ensure good taste combinations)
+3. customers get an automatic discount when they order a Hawaiian pizza (cheese + ham + pineapple)
+
+The code uses bitflags to efficiently track which toppings are selected and validate these rules, demonstrating how bitflags can handle complex combinations while enforcing business logic and detecting special cases for promotions without hundreds of lines of code of copying, modyfing and iterating over `Object`s and `Array`s of `Object`s and mapping boolean states and wasting the network bandwidth.
+
+```ts
+import { type Bitflag, bitflag, defineBitflags } from "../src/index";
+
+// This should probably live in a file shared between frontend/backend contexts
+const Toppings = defineBitflags({
+ CHEESE: 1 << 0,
+ PEPPERONI: 1 << 1,
+ MUSHROOMS: 1 << 2,
+ OREGANO: 1 << 3,
+ PINEAPPLE: 1 << 4,
+ BACON: 1 << 5,
+ HAM: 1 << 6,
+});
+
+// Can be mapped on frontend using InferBitflagsDefinitions and .describe() function
+type PizzaOrderPreferences = Readonly<{
+ desiredSize: "small" | "medium" | "large";
+ toppingsToAdd: Bitflag;
+ toppingsToRemove: Bitflag;
+}>;
+
+export async function configurePizzaOrder({
+ desiredSize,
+ toppingsToAdd,
+ toppingsToRemove,
+}: PizzaOrderPreferences) {
+ if (bitflag(toppingsToRemove).has(Toppings.CHEESE))
+ throw new Error("Cheese is always included in our pizzas!");
+
+ const defaultPizza = bitflag().add(Toppings.CHEESE, Toppings.PEPPERONI);
+ const pizzaAfterRemoval = processToppingsRemoval(
+ defaultPizza,
+ toppingsToRemove
+ );
+
+ validateMeatAddition(pizzaAfterRemoval, toppingsToAdd);
+
+ // ... some additional logic like checking the toppings availability in the restaurant inventory
+ // ... some additional logging using the .describe() function for comprehensive info
+
+ const finalPizza = bitflag(pizzaAfterRemoval).add(toppingsToAdd);
+
+ return {
+ size: desiredSize,
+ pizza: finalPizza,
+ metadata: {
+ hawaiianPizzaDiscount: bitflag(finalPizza).hasExact(
+ Toppings.CHEESE,
+ Toppings.HAM,
+ Toppings.PINEAPPLE
+ ),
+ },
+ };
+}
+
+function processToppingsRemoval(
+ currentPizza: Bitflag,
+ toppingsToRemove: Bitflag
+) {
+ if (toppingsToRemove) return bitflag(currentPizza).remove(toppingsToRemove);
+ return currentPizza;
+}
+
+function validateMeatAddition(currentPizza: Bitflag, toppingsToAdd: Bitflag) {
+ const currentHasMeat = bitflag(currentPizza).hasAny(
+ Toppings.PEPPERONI,
+ Toppings.BACON,
+ Toppings.HAM
+ );
+
+ const requestingMeat = bitflag(toppingsToAdd).hasAny(
+ Toppings.PEPPERONI,
+ Toppings.BACON,
+ Toppings.HAM
+ );
+
+ if (currentHasMeat && requestingMeat)
+ throw new Error("Only one type of meat is allowed per pizza!");
+}
```
+## Top-level API
+
+`bitf` library exposes the following:
+
+**Core runtime functionality:**
+
+- `bitflag` - the main function to perform bitwise operations on the flags.
+ - Bitwise operations abstraction
+ - `has` - checks if the specified flags are set
+ - `hasAny` - checks if any of the specified flags are set
+ - `hasExact` - checks if the specified flags are set exactly
+ - `add` - adds the specified flags
+ - `remove` - removes the specified flags
+ - `toggle` - toggles the specified flags
+ - `clear` - clears all flags
+ - Debugging and visualization
+ - `describe` - returns an iterator for describing the flags
+ - Interoperability between the library and other code
+ - `value` - returns the current value of the flags
+ - `valueOf` - returns the current value of the flags
+ - `toString` - returns the string representation of the flags
+- `defineBitflags` - utility to define type-safe set of bit flags
+
> [!NOTE]
-> Replace `my-typescript-library` with your new library name, you can also change the visiblity of the newly created repo by passing `--public` instead of `--private`! Read more about possible options in [GitHub CLI documentation](https://cli.github.com/manual/gh_repo_create)
+> All of the operations support passing multiple flags at once through variadic arguments.
-#### Setup via GitHub web interface
+**Utility functions**
+
+- `makeBitflag` - utility to create a `Bitflag` Tagged Type from a number if it is possible.
+- `isBitflag` - utility to check if number is within allowed range and to create a `Bitflag` Tagged type out of it
+- `unwrapBitflag` - utility to unwrap the Tagged type of `Bitflag` to be just `number`
+
+**Type utilities**
+
+- `Bitflag` - The tagged type for individual bitflag numbers
+- `BitflagsDefinitions` - The type for frozen bitflag definition objects returned by `defineBitflags`
+- `InferBitflagsDefinitions` - Type utility to extract the shape from bitflag definitions (similar to Zod's `z.infer`)
+
+## API Specifications
+
+- `bitflag(Bitflag | number)`
+
+ Bitflag is a factory function that returns object with a specific set of operations for managing the flags. It accepts any number or `Bitflag` Tagged Type as an argument and then allows you to perform various operations on it. It also supports methods like `toString()`, `value` getter and `valueOf()` for compatibility with other JavaScript APIs.
+
+ > [!IMPORTANT]
+ > The `bitflag` function's returned object's methods are **non-chainable** - each call to the bitwise operations returns just a number wrapped with the `Bitflag` Tagged Type. It does not return a new instance of the `bitflag` object.
+ >
+ > โ
Good
+ >
+ > ```ts
+ > const combinedFlags = bitflag(flags.NONE).add(
+ > flags.MY_OTHER_FLAG,
+ > flags.ANOTHER_FLAG
+ > );
+ >
+ > if (bitflag(combinedFlags).has(flags.ANOTHER_FLAG)) {
+ > console.log("has ANOTHER_FLAG");
+ > }
+ > ```
+ >
+ > โ Bad
+ >
+ > ```ts
+ > if (
+ > bitflag(flags.NONE)
+ > .add(flags.MY_OTHER_FLAG, flags.ANOTHER_FLAG)
+ > .has(flags.ANOTHER_FLAG)
+ > ) {
+ > console.log("has ANOTHER_FLAG");
+ > }
+ > ```
-If for some reason you can't run the mentioned commands in your terminal, click the "Use this template โพ" button below (or in the top right corner of the repository page)
+- `.add(...Bitflag[])`
+ Adds the specified flags to the current set. Returns a new number wrapped in `Bitflag` as the updated flags.
-
-
-
+ > [!TIP]
+ > Adding the same flag multiple times is idempotent - it won't change the result.
+
+ Usage Examples
-### 2. Minimal Setup
+ ```ts
+ bitflag(flags.MY_FLAG).add(flags.MY_OTHER_FLAG); // single defined
+ bitflag(flags.MY_FLAG).add(flags.MY_OTHER_FLAG, flags.ANOTHER_FLAG); // multiple defined
+ bitflag(flags.MY_FLAG).add(flags.MY_OTHER_FLAG, makeBitflag(1 << 2)); // mixed
+ ```
-Run the initialization script to automatically configure your repository:
+
-```bash
-# One-command setup
-./init.sh
-```
+- `.remove(...Bitflag[])`
+ Removes the specified flags from the current set. Returns a new number wrapped in `Bitflag` as the updated flags.
-This script will:
-- ๐ **Create repository rulesets** for branch protection (linear history, PR reviews)
-- ๐ซ **Disable unnecessary features** (wikis, projects, squash/merge commits)
-- โ๏ธ **Configure merge settings** (rebase-only workflow at repository and ruleset levels)
-- ๐ค **Grant admin bypass** permissions for repository administrators
-- ๐ง **Verify GitHub Actions** and validate repository configuration
-- ๐ **Check required secrets** and provide setup instructions
+ > [!TIP]
+ > Removing non-existent flags has no effect and won't change the result.
-### 3. Required Secrets
+
+ Usage Examples
-The script will guide you to set up these secrets if missing:
+ ```ts
+ bitflag(flags.READ | flags.WRITE).remove(flags.WRITE); // single defined
+ bitflag(flags.ALL).remove(flags.WRITE, flags.DELETE); // multiple defined
+ bitflag(flags.READ | flags.WRITE).remove(flags.WRITE, makeBitflag(1 << 3)); // mixed
+ ```
+
+
+
+- `.toggle(...Bitflag[])`
+ Toggles the specified flags in the current set - adds them if not present, removes them if present. Returns a new number wrapped in `Bitflag` as the updated flags.
-**NPM_TOKEN** (for publishing):
-```bash
-# Generate NPM token with OTP for enhanced security
-pnpm token create --otp= --registry=https://registry.npmjs.org/
+
+ Usage Examples
-# Set the token as repository secret
-gh secret set NPM_TOKEN --body "your-npm-token-here"
-```
+ ```ts
+ bitflag(flags.READ).toggle(flags.WRITE); // single defined
+ bitflag(flags.READ | flags.EXECUTE).toggle(flags.WRITE, flags.EXECUTE); // multiple defined
+ bitflag(flags.READ).toggle(flags.WRITE, makeBitflag(1 << 4)); // mixed
+ ```
-**ACTIONS_BRANCH_PROTECTION_BYPASS** (for automated releases):
-```bash
-# Create Personal Access Token with 'repo' permissions
-# Visit: https://github.com/settings/personal-access-tokens/new
+
+
+- `.has(...Bitflag[])`
+ Checks if all the specified flags are set in the current set. Returns `true` if all flags are present, `false` otherwise.
+
+ > [!TIP]
+ > Passing no arguments to `.has()` always returns `false`.
+
+
+ Usage Examples
+
+ ```ts
+ bitflag(flags.READ | flags.WRITE).has(flags.READ); // single defined
+ bitflag(flags.READ | flags.WRITE | flags.EXECUTE).has(
+ flags.READ,
+ flags.WRITE
+ ); // multiple defined
+ bitflag(flags.READ | flags.WRITE).has(flags.READ, makeBitflag(1 << 1)); // mixed
+ ```
+
+
+
+- `.hasAny(...Bitflag[])`
+ Checks if any of the specified flags are set in the current set. Returns `true` if at least one flag is present, `false` if none are present.
+
+ > [!TIP]
+ > Passing no arguments to `.hasAny()` always returns `false`.
+
+
+ Usage Examples
+
+ ```ts
+ bitflag(flags.READ | flags.WRITE).hasAny(flags.EXECUTE); // single defined
+ bitflag(flags.READ).hasAny(flags.EXECUTE, flags.DELETE); // multiple defined
+ bitflag(flags.READ).hasAny(flags.EXECUTE, makeBitflag(1 << 0)); // mixed
+ ```
+
+
+
+- `.hasExact(...Bitflag[])`
+ Checks if the current set matches exactly the specified flags - no more, no less. Returns `true` if the flags match exactly, `false` otherwise.
+
+ > [!TIP]
+ > Calling `.hasExact()` with no arguments checks if the current value is exactly zero.
+
+
+ Usage Examples
+
+ ```ts
+ bitflag(flags.READ | flags.WRITE).hasExact(flags.READ, flags.WRITE); // single defined exact match
+ bitflag(flags.NONE).hasExact(); // multiple defined (empty means zero flags)
+ bitflag(flags.READ).hasExact(makeBitflag(1 << 0)); // mixed
+ ```
+
+
+
+- `.clear()`
+ Clears all flags, setting the value to zero. Returns a new number wrapped in `Bitflag` with value 0.
+
+
+ Usage Examples
+
+ ```ts
+ bitflag(flags.ALL).clear(); // returns 0 as Bitflag
+ ```
+
+
+
+- `.describe(flagDefinitions?: BitflagsDefinitions)`
+ Returns an iterator that yields `FlagDescription` objects for each set bit, providing detailed information about the flags including name, value, and bit position visualization.
+
+ **FlagDescription Structure:**
+
+ ```ts
+ type FlagDescription = {
+ name: string; // Flag name or "BIT_X"/"UNKNOWN_BIT_X"
+ value: number; // Numeric value of this specific flag
+ decimal: string; // Decimal representation (e.g., "42")
+ hexadecimal: string; // Hexadecimal with 0x prefix (e.g., "0x2A")
+ binary: string; // Binary with 0b prefix (e.g., "0b101010")
+ unknown: boolean; // true if flag not found in the definitions provided via parameter
+ bitPosition: {
+ exact: number; // Highest bit position (-1 for zero)
+ remaining: number; // Remaining available bit positions
+ visual: string; // Visual bit representation with [1] markers
+ };
+ };
+ ```
+
+ Examples:
+
+
+ Basic Usage with Flag Definitions
+
+ ```ts
+ [...bitflag(flags.READ | flags.WRITE).describe(flags)];
+ ```
+
+ ```js
+ // Returns:
+ [
+ {
+ name: "READ",
+ value: 1,
+ decimal: "1",
+ hexadecimal: "0x1",
+ binary: "0b1",
+ unknown: false,
+ bitPosition: {
+ exact: 0,
+ remaining: 31,
+ visual: "(0)000000000000000000000000000000[1]",
+ },
+ },
+ {
+ name: "WRITE",
+ value: 2,
+ decimal: "2",
+ hexadecimal: "0x2",
+ binary: "0b10",
+ unknown: false,
+ bitPosition: {
+ exact: 1,
+ remaining: 30,
+ visual: "(0)00000000000000000000000000000[1]0",
+ },
+ },
+ ];
+ ```
+
+
+
+
+ Generic Bit Names (No Definitions)
+
+ ```ts
+ // Without definitions - shows generic BIT_X names
+ [...bitflag(5).describe()]; // value 5 = bits 0 and 2
+ ```
+
+ ```js
+ // Returns:
+ [
+ {
+ name: "BIT_0",
+ value: 1,
+ binary: "0b1",
+ unknown: false,
+ bitPosition: {
+ exact: 0,
+ remaining: 31,
+ visual: "(0)000000000000000000000000000000[1]",
+ },
+ },
+ {
+ name: "BIT_2",
+ value: 4,
+ binary: "0b100",
+ unknown: false,
+ bitPosition: {
+ exact: 2,
+ remaining: 29,
+ visual: "(0)0000000000000000000000000000[1]00",
+ },
+ },
+ ];
+ ```
+
+
+
+
+ Mixed Known and Unknown Flags
+
+ ```ts
+ // Mixed known and unknown flags
+ [...bitflag(flags.READ | makeBitflag(1 << 10)).describe(flags)];
+ ```
+
+ ```js
+ // Returns:
+ [
+ {
+ name: "READ",
+ value: 1,
+ decimal: "1",
+ hexadecimal: "0x1",
+ binary: "0b1",
+ unknown: false,
+ bitPosition: {
+ exact: 0,
+ remaining: 31,
+ visual: "(0)000000000000000000000000000000[1]",
+ },
+ },
+ {
+ name: "UNKNOWN_BIT_10",
+ value: 1024,
+ decimal: "1024",
+ hexadecimal: "0x400",
+ binary: "0b10000000000",
+ unknown: true,
+ bitPosition: {
+ exact: 10,
+ remaining: 21,
+ visual: "(0)00000000000000000000[1]0000000000",
+ },
+ },
+ ];
+ ```
+
+
+
+
+ Zero Value Special Case
+
+ ```ts
+ // Zero value special case
+ [...bitflag(0).describe()];
+ ```
+
+ ```js
+ // Returns:
+ [
+ {
+ name: "NONE",
+ value: 0,
+ decimal: "0",
+ hexadecimal: "0x0",
+ binary: "0b0",
+ unknown: false,
+ bitPosition: {
+ exact: -1,
+ remaining: 31,
+ visual: "(0)0000000000000000000000000000000",
+ },
+ },
+ ];
+ ```
+
+
+
+
+ High Bit Positions (15 & 30)
+
+ ```ts
+ // High bit positions (bit 15 and 30)
+ [...bitflag(makeBitflag((1 << 15) | (1 << 30))).describe()];
+ ```
+
+ ```js
+ // Returns:
+ [
+ {
+ name: "BIT_15",
+ value: 32768,
+ binary: "0b1000000000000000",
+ bitPosition: {
+ exact: 15,
+ remaining: 16,
+ visual: "(0)000000000000000[1]000000000000000",
+ },
+ },
+ {
+ name: "BIT_30",
+ value: 1073741824,
+ binary: "0b1000000000000000000000000000000",
+ bitPosition: {
+ exact: 30,
+ remaining: 1,
+ visual: "(0)[1]000000000000000000000000000000",
+ },
+ },
+ ];
+ ```
+
+
+
+ > [!TIP] > **Bit Position Visualization:** The `visual` field shows a 32-character representation where `[1]` indicates set bits and `0` shows unset bits. The format is `(0)[bit31][bit30]...[bit1][bit0]` with the sign bit always shown as `(0)`.
+
+ > [!TIP] > **Flag Resolution Order:** When using flag definitions, the iterator yields known flags first (in the order they match), then unknown bits as `UNKNOWN_BIT_X` in ascending bit order.
+
+ > [!TIP] > **Complex Flags:** Combined flags (like `READ_WRITE: (1<<0)|(1<<1)`) are detected when their exact bit pattern matches the current value, alongside their individual component flags.
+
+- `.value`
+ A getter that returns the current numeric value of the flags as a regular number.
+
+
+ Usage Examples
+
+ ```ts
+ bitflag(flags.READ | flags.WRITE).value; // returns 3
+ bitflag().value; // returns 0
+ bitflag(flags.ALL).value; // returns the combined numeric value
+ ```
+
+
+
+- `.valueOf()`
+ Returns the current numeric value of the flags, enabling implicit conversion to number in JavaScript operations.
+
+
+ Usage Examples
+
+ ```ts
+ bitflag(flags.READ).valueOf(); // explicit call
+ +bitflag(flags.WRITE); // implicit conversion
+ Number(bitflag(flags.EXECUTE)); // explicit conversion
+ ```
+
+
+
+- `.toString()`
+ Returns the string representation of the current numeric value of the flags.
-# Set the PAT as repository secret
-gh secret set ACTIONS_BRANCH_PROTECTION_BYPASS --body "your-pat-token-here"
-```
+
+ Usage Examples
-## Scripts
+ ```ts
+ bitflag(flags.READ).toString(); // returns "1"
+ String(bitflag(flags.READ | flags.WRITE)); // returns "3"
+ `Current flags: ${bitflag(flags.ALL)}`; // template literal usage
+ ```
-| Command | Description |
-|---------|-------------|
-| `pnpm dev` | Watch mode build |
-| `pnpm build` | Production build |
-| `pnpm build:check` | Build + package validation |
-| `pnpm test` | Run tests |
-| `pnpm test:watch` | Watch mode testing |
-| `pnpm test:coverage` | Generate coverage report |
-| `pnpm lint` | Check linting and formatting |
-| `pnpm lint:fix` | Fix linting and formatting issues |
-| `pnpm typecheck` | TypeScript type checking |
-| `pnpm release` | Create release (CI only) |
+
-## FAQ
+- `defineBitflags>(obj: T)`
-#### How do I modify the merging methods?
+ Utility function to define a type-safe set of bit flags. It validates that all values are non-negative integers within the 31-bit range and returns a frozen object with `Bitflag` Tagged Types.
-`typescript-library-template` sets **rebase-only** at both repository and main branch levels. Here's how to modify this:
+
+ Usage Examples
-##### **Current Setup**
-- **Repository**: Rebase merging only (squash/merge disabled)
-- **Main branch ruleset**: Requires rebase merging
+ ```ts
+ const flags = defineBitflags({
+ READ: 1 << 0,
+ WRITE: 1 << 1,
+ EXECUTE: 1 << 2,
+ }); // basic usage
-##### **To Change Merge Methods**
+ const complexFlags = defineBitflags({
+ NONE: 0,
+ READ: 1 << 0,
+ WRITE: 1 << 1,
+ READ_WRITE: (1 << 0) | (1 << 1),
+ }); // with combined flags
-**For repository-wide changes:**
-- **Settings > General > Pull Requests** - toggle merge methods
+ const permissions = defineBitflags({
+ VIEWER: 1,
+ EDITOR: 3,
+ ADMIN: 15,
+ }); // with arbitrary values
+ ```
-**For branch-specific changes:**
-- **Settings > Rules** - edit the main branch ruleset's "Require merge type"
+
-##### **Precedence Rules**
-1. Repository settings define what's **available**
-2. Rulesets add **restrictions** on top
-3. **Most restrictive wins** - if repository disallows a method but ruleset requires it, merging is **blocked**
+ > [!TIP]
+ > The returned object is frozen to prevent accidental modifications. All values must be within the range 0 to 0x7FFFFFFF (31-bit signed integer range).
-##### **Common Modifications**
-- **Allow all methods**: Enable squash/merge in repo settings + remove "Require merge type" from ruleset
-- **Squash-only**: Change repo settings to squash-only OR keep current repo settings + change ruleset to require squash
-- **Different rules per branch**: Create additional rulesets for other branch patterns
+- `makeBitflag(value: number)`
-> [!TIP]
-> Since `typescript-library-template` is rebase-only, you must enable other methods in repository settings before rulesets can use them.
+ Utility function to create a `Bitflag` Tagged Type from a number if it's within the valid range. Throws an error if the conversion would result in a data loss.
-#### How to solve pnpm lockfile error on my CI/CD?
+
+ Usage Examples
-If you're seeing this error in your CI/CD (GitHub Actions) pipeline:
+ ```ts
+ makeBitflag(5); // creates Bitflag from valid number
+ makeBitflag(0); // creates Bitflag for zero
+ makeBitflag(0x7fffffff); // creates Bitflag for maximum value
+ ```
-```
-[...]
+
-ERR_PNPM_OUTDATED_LOCKFILE Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with /package.json
+ > [!TIP]
+ > This function validates the input and throws a descriptive error for invalid values (negative numbers or values exceeding 31 bits). It is also the only function that `throws`
-[...]
-```
+- `isBitflag(value: unknown)`
-##### **Why This Happens**
-This template uses `--frozen-lockfile` flag to ensure consistent installations in CI/CD. The error occurs when your `package.json` has been modified but the `pnpm-lock.yaml` hasn't been updated to match.
+ Type guard utility to check if a value can be used as a `Bitflag`. Returns `true` if the value is a non-negative integer within the 31-bit range.
-##### **Solution**
-Run the following command locally:
-```bash
-pnpm install
-```
+
+ Usage Examples
-This will:
-1. Update your `pnpm-lock.yaml` to match your `package.json`
-2. Install any new dependencies
-3. Resolve version conflicts
+ ```ts
+ isBitflag(5); // returns true
+ isBitflag(-1); // returns false
+ isBitflag(1.5); // returns false
+ ```
-Then commit the updated lockfile:
-```bash
-git add pnpm-lock.yaml
-git commit -m "chore: update pnpm lockfile"
-```
+
+
+ > [!TIP]
+ > This function is useful for runtime validation before using values with the bitflag operations.
+
+- `unwrapBitflag(flag: Bitflag)`
+
+ Utility function to extract the numeric value from a `Bitflag` Tagged Type, converting it back to a regular number.
+
+
+ Usage Examples
+
+ ```ts
+ const flags = defineBitflags({ TEST: 5 });
+ unwrapBitflag(flags.TEST); // returns 5 as number
+ unwrapBitflag(bitflag(flags.TEST).add(makeBitflag(2))); // returns 7 as number
+ unwrapBitflag(bitflag().clear());// returns 0 as number
+ ```
+
+
+
+## Type Utilities
+
+- `Bitflag`
+
+ The tagged type for individual bitflag numbers. This ensures type safety by distinguishing bitflag numbers from regular numbers.
+
+ It is mostly used internally inside the library source. Export exists for complex cases where you would need this type.
+
+- `BitflagsDefinitions`
+
+ The type for frozen bitflag definition objects returned by `defineBitflags`. This represents the complete set of flag definitions.
+
+ It is mostly used internally inside the library source. Export exists for complex cases where you would need this type.
+
+
+- `InferBitflagsDefinitions`
+
+ Type utility to extract the shape from bitflag definitions. Converts `BitflagsDefinitions` to `Record`.
+
+
+ Usage Examples
-> [!TIP]
-> This is expected behavior and ensures your CI/CD uses the exact same dependency versions as your local environment.
+ ```ts
+ import { type InferBitflagsDefinitions } from "bitf";
-#### Why Linear History?
+ // Define flags
+ const UserPermissions = defineBitflags({
+ READ: 1 << 0,
+ WRITE: 1 << 1,
+ DELETE: 1 << 2,
+ });
-Linear history provides several benefits for library releases:
+ // Extract type shape
+ type UserPermissionsType = InferBitflagsDefinitions;
+ // Result: { READ: Bitflag; WRITE: Bitflag; DELETE: Bitflag }
+ ```
-- **Clean commit history** - Easy to track changes and debug issues
-- **Simplified releases** - Semantic release works better with linear commits
-- **Clear changelog** - Each commit represents a complete change
-- **Better debugging** - `git bisect` works more effectively
-- **Consistent workflow** - Forces proper PR review process
+
-## Contributing
+## Benchmarks
-See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow, commit conventions, and contribution guidelines.
+See more ["Everything About Bitflags - Benchmarks"](https://neg4n.dev/explore/everything-about-bitflags#benchmarks).
## License
-The MIT License
+The MIT License
\ No newline at end of file
diff --git a/benchmark/index.ts b/benchmark/index.ts
new file mode 100644
index 0000000..4bced46
--- /dev/null
+++ b/benchmark/index.ts
@@ -0,0 +1,6 @@
+import { runBenchmarks } from './runner'
+
+runBenchmarks().catch(error => {
+ console.error('Benchmark failed:', error)
+ process.exit(1)
+})
diff --git a/benchmark/json-flags.ts b/benchmark/json-flags.ts
new file mode 100644
index 0000000..75eb0a7
--- /dev/null
+++ b/benchmark/json-flags.ts
@@ -0,0 +1,225 @@
+export type JsonFlags = {
+ flags: Set
+ metadata?: Record
+}
+
+export type JsonFlagOperations = {
+ has(...flags: string[]): boolean
+ hasAny(...flags: string[]): boolean
+ hasExact(...flags: string[]): boolean
+ add(...flags: string[]): JsonFlags
+ remove(...flags: string[]): JsonFlags
+ toggle(...flags: string[]): JsonFlags
+ clear(): JsonFlags
+ value: JsonFlags
+ valueOf(): JsonFlags
+ toString(): string
+}
+
+export function bitToFlagName(bit: number): string {
+ return `feature_flag_${bit}`
+}
+
+export function numberToJsonFlags(value: number): JsonFlags {
+ const flagsArray: string[] = []
+
+ for (let bit = 0; bit < 31; bit++) {
+ const mask = 1 << bit
+ if (value & mask) {
+ flagsArray.push(bitToFlagName(bit))
+ }
+ }
+
+ return {
+ flags: new Set(flagsArray),
+ metadata: {
+ createdAt: Date.now(),
+ version: '1.0.0',
+ },
+ }
+}
+
+export function jsonFlag(initialValue: number = 0): JsonFlagOperations {
+ let state = numberToJsonFlags(initialValue)
+
+ return {
+ has(...flagNames: string[]): boolean {
+ if (!flagNames || flagNames.length === 0) {
+ return false
+ }
+
+ return flagNames.every(flag => {
+ if (typeof flag !== 'string') {
+ return false
+ }
+ return state.flags.has(flag)
+ })
+ },
+
+ hasAny(...flagNames: string[]): boolean {
+ if (!flagNames || flagNames.length === 0) {
+ return false
+ }
+
+ return flagNames.some(flag => {
+ if (typeof flag !== 'string') {
+ return false
+ }
+ return state.flags.has(flag)
+ })
+ },
+
+ hasExact(...flagNames: string[]): boolean {
+ const currentFlags = Array.from(state.flags)
+
+ if (flagNames.length === 0) {
+ return currentFlags.length === 0
+ }
+
+ if (currentFlags.length !== flagNames.length) {
+ return false
+ }
+
+ const targetSet = new Set(flagNames)
+ return currentFlags.every(flag => targetSet.has(flag))
+ },
+
+ add(...flagNames: string[]): JsonFlags {
+ const newFlags = new Set(state.flags)
+
+ flagNames.forEach(flag => {
+ if (typeof flag === 'string') {
+ newFlags.add(flag)
+ }
+ })
+
+ state = {
+ flags: newFlags,
+ metadata: {
+ ...state.metadata,
+ lastModified: Date.now(),
+ },
+ }
+
+ return {
+ flags: new Set(newFlags),
+ metadata: { ...state.metadata },
+ }
+ },
+
+ remove(...flagNames: string[]): JsonFlags {
+ const newFlags = new Set(state.flags)
+
+ flagNames.forEach(flag => {
+ if (typeof flag === 'string') {
+ newFlags.delete(flag)
+ }
+ })
+
+ state = {
+ flags: newFlags,
+ metadata: {
+ ...state.metadata,
+ lastModified: Date.now(),
+ },
+ }
+
+ return {
+ flags: new Set(newFlags),
+ metadata: { ...state.metadata },
+ }
+ },
+
+ toggle(...flagNames: string[]): JsonFlags {
+ const newFlags = new Set(state.flags)
+
+ flagNames.forEach(flag => {
+ if (typeof flag === 'string') {
+ if (newFlags.has(flag)) {
+ newFlags.delete(flag)
+ } else {
+ newFlags.add(flag)
+ }
+ }
+ })
+
+ state = {
+ flags: newFlags,
+ metadata: {
+ ...state.metadata,
+ lastModified: Date.now(),
+ toggleCount: (state.metadata?.toggleCount || 0) + 1,
+ },
+ }
+
+ return {
+ flags: new Set(newFlags),
+ metadata: { ...state.metadata },
+ }
+ },
+
+ clear(): JsonFlags {
+ state = {
+ flags: new Set(),
+ metadata: {
+ ...state.metadata,
+ clearedAt: Date.now(),
+ },
+ }
+
+ return {
+ flags: new Set(),
+ metadata: { ...state.metadata },
+ }
+ },
+
+ get value(): JsonFlags {
+ return {
+ flags: new Set(state.flags),
+ metadata: { ...state.metadata },
+ }
+ },
+
+ valueOf(): JsonFlags {
+ const flagsArray = Array.from(state.flags)
+ return {
+ flags: new Set(flagsArray),
+ metadata: JSON.parse(JSON.stringify(state.metadata)),
+ }
+ },
+
+ toString(): string {
+ return JSON.stringify(
+ {
+ flags: Array.from(state.flags),
+ metadata: state.metadata,
+ },
+ null,
+ 2
+ )
+ },
+ }
+}
+
+export function defineJsonFlags(bitflags: Record): Record {
+ const jsonFlags: Record = {}
+
+ Object.entries(bitflags).forEach(([key, value]) => {
+ if (key === 'NONE') {
+ jsonFlags[key] = 'NONE_FLAG'
+ } else if (key === 'ALL') {
+ jsonFlags[key] = 'ALL_FLAGS'
+ } else if (key.startsWith('GROUP_')) {
+ jsonFlags[key] = `group_${key.toLowerCase()}`
+ } else {
+ const bitPos = Math.log2(value)
+ if (Number.isInteger(bitPos) && bitPos >= 0 && bitPos < 31) {
+ jsonFlags[key] = bitToFlagName(bitPos)
+ } else {
+ jsonFlags[key] = `composite_${key.toLowerCase()}`
+ }
+ }
+ })
+
+ return Object.freeze(jsonFlags)
+}
diff --git a/benchmark/operations.ts b/benchmark/operations.ts
new file mode 100644
index 0000000..4b2f1fd
--- /dev/null
+++ b/benchmark/operations.ts
@@ -0,0 +1,275 @@
+import { Bench } from 'tinybench'
+import { bitflag, defineBitflags } from '../src/index'
+import { bitToFlagName, defineJsonFlags, jsonFlag } from './json-flags'
+
+const FLAGS = defineBitflags({
+ NONE: 0,
+ FLAG_0: 1 << 0,
+ FLAG_1: 1 << 1,
+ FLAG_2: 1 << 2,
+ FLAG_3: 1 << 3,
+ FLAG_4: 1 << 4,
+ FLAG_5: 1 << 5,
+ FLAG_6: 1 << 6,
+ FLAG_7: 1 << 7,
+ GROUP_LOW: (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3),
+ GROUP_HIGH: (1 << 4) | (1 << 5) | (1 << 6) | (1 << 7),
+ ALL: (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5) | (1 << 6) | (1 << 7),
+})
+
+const JSON_FLAGS = defineJsonFlags(FLAGS as any)
+
+function calculateCV(stdDev: number, mean: number): number {
+ return mean > 0 ? stdDev / mean : 0
+}
+
+function formatOps(ops: number): string {
+ if (ops >= 1_000_000_000) {
+ return `${(ops / 1_000_000_000).toFixed(2)}B`
+ } else if (ops >= 1_000_000) {
+ return `${(ops / 1_000_000).toFixed(2)}M`
+ } else if (ops >= 1_000) {
+ return `${(ops / 1_000).toFixed(2)}K`
+ }
+ return ops.toFixed(0)
+}
+
+type BenchmarkResult = {
+ name: string
+ ops: number
+ rme: number
+ samples: number
+ cv?: number
+}
+
+export async function runOperationsBenchmark() {
+ async function warmup() {
+ const warmupBench = new Bench({ time: 100 })
+
+ const bf = bitflag(FLAGS.FLAG_0)
+ const jf = jsonFlag(FLAGS.FLAG_0)
+
+ warmupBench
+ .add('warmup-bitflag', () => bf.has(FLAGS.FLAG_0))
+ .add('warmup-json', () => jf.has(JSON_FLAGS.FLAG_0))
+
+ await warmupBench.run()
+ }
+
+ console.log('Running warmup iterations...')
+ for (let i = 0; i < 5; i++) {
+ await warmup()
+ }
+
+ const bitfSingle = bitflag(FLAGS.FLAG_0)
+ const bitfMulti = bitflag(FLAGS.FLAG_0 | FLAGS.FLAG_1 | FLAGS.FLAG_2)
+ const bitfAll = bitflag(FLAGS.ALL)
+
+ const jsonSingle = jsonFlag(FLAGS.FLAG_0)
+ const jsonMulti = jsonFlag(FLAGS.FLAG_0 | FLAGS.FLAG_1 | FLAGS.FLAG_2)
+ const jsonAll = jsonFlag(FLAGS.ALL)
+
+ const bench = new Bench({
+ time: 500,
+ iterations: 100000,
+ })
+
+ bench
+ .add('bitf.has(single)', () => {
+ return bitfSingle.has(FLAGS.FLAG_0)
+ })
+ .add('bitf.has(double)', () => {
+ return bitfMulti.has(FLAGS.FLAG_0, FLAGS.FLAG_1)
+ })
+ .add('bitf.hasAny()', () => {
+ return bitfMulti.hasAny(FLAGS.FLAG_0, FLAGS.FLAG_5)
+ })
+ .add('bitf.hasExact()', () => {
+ return bitfMulti.hasExact(FLAGS.FLAG_0, FLAGS.FLAG_1, FLAGS.FLAG_2)
+ })
+
+ bench
+ .add('json.has(single)', () => {
+ return jsonSingle.has(JSON_FLAGS.FLAG_0)
+ })
+ .add('json.has(double)', () => {
+ return jsonMulti.has(JSON_FLAGS.FLAG_0, JSON_FLAGS.FLAG_1)
+ })
+ .add('json.hasAny()', () => {
+ return jsonMulti.hasAny(JSON_FLAGS.FLAG_0, JSON_FLAGS.FLAG_5)
+ })
+ .add('json.hasExact()', () => {
+ return jsonMulti.hasExact(JSON_FLAGS.FLAG_0, JSON_FLAGS.FLAG_1, JSON_FLAGS.FLAG_2)
+ })
+
+ bench
+ .add('bitf.add(single)', () => {
+ return bitfSingle.add(FLAGS.FLAG_5)
+ })
+ .add('bitf.add(multiple)', () => {
+ return bitfSingle.add(FLAGS.FLAG_5, FLAGS.FLAG_6)
+ })
+ .add('bitf.remove(single)', () => {
+ return bitfMulti.remove(FLAGS.FLAG_1)
+ })
+ .add('bitf.remove(multiple)', () => {
+ return bitfAll.remove(FLAGS.FLAG_0, FLAGS.FLAG_1)
+ })
+ .add('bitf.toggle()', () => {
+ return bitfMulti.toggle(FLAGS.FLAG_5)
+ })
+ .add('bitf.clear()', () => {
+ return bitfAll.clear()
+ })
+
+ bench
+ .add('json.add(single)', () => {
+ return jsonSingle.add(JSON_FLAGS.FLAG_5)
+ })
+ .add('json.add(multiple)', () => {
+ return jsonSingle.add(JSON_FLAGS.FLAG_5, JSON_FLAGS.FLAG_6)
+ })
+ .add('json.remove(single)', () => {
+ return jsonMulti.remove(JSON_FLAGS.FLAG_1)
+ })
+ .add('json.remove(multiple)', () => {
+ return jsonAll.remove(JSON_FLAGS.FLAG_0, JSON_FLAGS.FLAG_1)
+ })
+ .add('json.toggle()', () => {
+ return jsonMulti.toggle(JSON_FLAGS.FLAG_5)
+ })
+ .add('json.clear()', () => {
+ return jsonAll.clear()
+ })
+
+ bench
+ .add('bitf.value', () => {
+ return bitfMulti.value
+ })
+ .add('bitf.valueOf()', () => {
+ return bitfMulti.valueOf()
+ })
+
+ bench
+ .add('json.value', () => {
+ return jsonMulti.value
+ })
+ .add('json.valueOf()', () => {
+ return jsonMulti.valueOf()
+ })
+
+ console.log('\nRunning benchmarks (500ms each, max 100k iterations)...\n')
+ await bench.run()
+
+ const operations = [
+ 'has(single)',
+ 'has(double)',
+ 'hasAny()',
+ 'hasExact()',
+ 'add(single)',
+ 'add(multiple)',
+ 'remove(single)',
+ 'remove(multiple)',
+ 'toggle()',
+ 'clear()',
+ 'value',
+ 'valueOf()',
+ ]
+
+ const results: Map = new Map()
+
+ for (const task of bench.tasks) {
+ const [system, ...opParts] = task.name.split('.')
+ const operation = opParts.join('.')
+
+ if (!results.has(operation)) {
+ results.set(operation, {})
+ }
+
+ const result: BenchmarkResult = {
+ name: task.name,
+ ops: Math.round(task.result?.hz || 0),
+ rme: task.result?.rme || 0,
+ samples: task.result?.samples?.length || 0,
+ }
+
+ if (task.result?.sd && task.result?.mean) {
+ result.cv = calculateCV(task.result.sd, task.result.mean)
+ }
+
+ const entry = results.get(operation)!
+ if (system === 'bitf') {
+ entry.bitf = result
+ } else {
+ entry.json = result
+ }
+ }
+
+ console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ')
+ console.log(
+ 'โ BENCHMARK RESULTS โ'
+ )
+ console.log('โ โโโโโโโโโโโโโโโโโโโโโคโโโโโโโโโโโโโโโโคโโโโโโโโโโโโโโโโคโโโโโโโโโโคโโโโโโโโโโโโโโโโโโฃ')
+ console.log('โ Operation โ bitf ops/sec โ JSON ops/sec โ Ratio โ Samples โ')
+ console.log('โ โโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโชโโโโโโโโโโชโโโโโโโโโโโโโโโโโโฃ')
+
+ for (const op of operations) {
+ const result = results.get(op)
+ if (!result || !result.bitf || !result.json) continue
+
+ const ratio = result.bitf.ops / result.json.ops
+ const opName = op.padEnd(18)
+ const bitfOps = formatOps(result.bitf.ops).padStart(13)
+ const jsonOps = formatOps(result.json.ops).padStart(13)
+ const ratioStr = `${ratio.toFixed(1)}x`.padStart(7)
+ const samples = `${result.bitf.samples}/${result.json.samples}`.padStart(15)
+
+ console.log(`โ ${opName} โ ${bitfOps} โ ${jsonOps} โ ${ratioStr} โ ${samples} โ`)
+
+ if (result.bitf.rme > 3 || result.json.rme > 3) {
+ const warning = ` โ High RME: bitf=${result.bitf.rme.toFixed(2)}% json=${result.json.rme.toFixed(2)}%`
+ console.log(`โ ${warning.padEnd(82)} โ`)
+ }
+ }
+
+ console.log('โโโโโโโโโโโโโโโโโโโโโโงโโโโโโโโโโโโโโโโงโโโโโโโโโโโโโโโโงโโโโโโโโโโงโโโโโโโโโโโโโโโโโโ')
+
+ let totalRatio = 0
+ let count = 0
+ for (const result of results.values()) {
+ if (result.bitf && result.json) {
+ totalRatio += result.bitf.ops / result.json.ops
+ count++
+ }
+ }
+
+ console.log(
+ `\n๐ Average Performance Improvement: ${(totalRatio / count).toFixed(1)}x faster than JSON`
+ )
+
+ if (global.gc) {
+ console.log('\n๐พ Memory Usage Comparison:')
+
+ global.gc()
+ const bitfBefore = process.memoryUsage().heapUsed
+ const bitfInstances = Array.from({ length: 10000 }, () => bitflag(FLAGS.ALL))
+ const bitfAfter = process.memoryUsage().heapUsed
+ const bitfMemory = (bitfAfter - bitfBefore) / 10000
+
+ global.gc()
+ const jsonBefore = process.memoryUsage().heapUsed
+ const jsonInstances = Array.from({ length: 10000 }, () => jsonFlag(FLAGS.ALL))
+ const jsonAfter = process.memoryUsage().heapUsed
+ const jsonMemory = (jsonAfter - jsonBefore) / 10000
+
+ console.log(` bitf: ~${bitfMemory.toFixed(2)} bytes per instance`)
+ console.log(` JSON: ~${jsonMemory.toFixed(2)} bytes per instance`)
+ console.log(` Ratio: ${(jsonMemory / bitfMemory).toFixed(1)}x more memory for JSON`)
+ } else {
+ console.log('\n๐ก Tip: Run with --expose-gc flag for memory usage comparison')
+ }
+}
+
+if (import.meta.url === `file://${process.argv[1]}`) {
+ runOperationsBenchmark().catch(console.error)
+}
diff --git a/benchmark/runner.ts b/benchmark/runner.ts
new file mode 100644
index 0000000..6cfcda2
--- /dev/null
+++ b/benchmark/runner.ts
@@ -0,0 +1,102 @@
+import { runbitfSuite } from './suites/bitf-suite'
+import { runJsonSuite } from './suites/json-suite'
+import { displayComparison, displaySingleSuite } from './utils/display'
+
+export type RunnerOptions = {
+ suite?: 'bitf' | 'json' | 'both'
+ compare?: boolean
+ verbose?: boolean
+}
+
+export async function runBenchmarks(options: RunnerOptions = {}) {
+ const { suite = 'both', compare = suite === 'both' } = options
+
+ console.log('# bitf benchmarks')
+ console.log(`# Node ${process.version}`)
+
+ let bitfResults = null
+ let jsonResults = null
+
+ if (suite === 'bitf' || suite === 'both') {
+ try {
+ bitfResults = await runbitfSuite()
+
+ if (suite === 'bitf') {
+ displaySingleSuite(bitfResults)
+ }
+ } catch (error) {
+ console.error('โ bitf suite failed:', error)
+ if (suite === 'bitf') process.exit(1)
+ }
+ }
+
+ if (global.gc && suite === 'both') {
+ console.log('\n๐งน Running garbage collection...')
+ global.gc()
+ }
+
+ if (suite === 'json' || suite === 'both') {
+ try {
+ jsonResults = await runJsonSuite()
+
+ if (suite === 'json') {
+ displaySingleSuite(jsonResults)
+ }
+ } catch (error) {
+ console.error('โ JSON suite failed:', error)
+ if (suite === 'json') process.exit(1)
+ }
+ }
+
+ if (suite === 'both' && bitfResults && jsonResults) {
+ displayComparison(bitfResults, jsonResults)
+ }
+
+ if (global.gc && suite === 'both') {
+ console.log('\n๐พ Memory Usage (approximate):')
+
+ global.gc()
+ const bitfBefore = process.memoryUsage().heapUsed
+ const { bitflag } = await import('../src/index')
+ const { FLAGS } = await import('./utils/setup')
+ const bitfInstances = Array.from({ length: 1000 }, () => bitflag(FLAGS.ALL))
+ const bitfAfter = process.memoryUsage().heapUsed
+ const bitfMemory = (bitfAfter - bitfBefore) / 1000
+
+ global.gc()
+ const jsonBefore = process.memoryUsage().heapUsed
+ const { jsonFlag } = await import('./json-flags')
+ const jsonInstances = Array.from({ length: 1000 }, () => jsonFlag(FLAGS.ALL))
+ const jsonAfter = process.memoryUsage().heapUsed
+ const jsonMemory = (jsonAfter - jsonBefore) / 1000
+
+ console.log(` bitf: ~${bitfMemory.toFixed(0)} bytes per instance`)
+ console.log(` JSON: ~${jsonMemory.toFixed(0)} bytes per instance`)
+ console.log(` Ratio: ${(jsonMemory / bitfMemory).toFixed(1)}x more memory for JSON`)
+ } else if (suite === 'both') {
+ console.log('\n๐ก Tip: Run with --expose-gc flag for memory usage comparison')
+ console.log(' node --expose-gc node_modules/.bin/tsx benchmark/index.ts')
+ }
+}
+
+if (import.meta.url === `file://${process.argv[1]}`) {
+ const args = process.argv.slice(2)
+ const options: RunnerOptions = {}
+
+ for (const arg of args) {
+ if (arg === '--bitf' || arg === '-b') {
+ options.suite = 'bitf'
+ } else if (arg === '--json' || arg === '-j') {
+ options.suite = 'json'
+ } else if (arg === '--no-compare') {
+ options.compare = false
+ } else if (arg === '--verbose' || arg === '-v') {
+ options.verbose = true
+ }
+ }
+
+ runBenchmarks(options).catch(error => {
+ console.error('Benchmark failed:', error)
+ process.exit(1)
+ })
+}
diff --git a/benchmark/suites/bitf-suite.ts b/benchmark/suites/bitf-suite.ts
new file mode 100644
index 0000000..b4dc12e
--- /dev/null
+++ b/benchmark/suites/bitf-suite.ts
@@ -0,0 +1,93 @@
+import { Bench } from 'tinybench'
+import { bitflag } from '../../src/index'
+import { BENCHMARK_CONFIG, FLAGS, type OperationName, type SuiteResults } from '../utils/setup'
+
+export async function runbitfSuite(): Promise {
+ const startTime = Date.now()
+
+ console.log('\n๐ Running bitf benchmark suite...')
+
+ console.log(' Warming up...')
+ const warmupBench = new Bench({ time: BENCHMARK_CONFIG.warmupTime })
+ const warmupFlag = bitflag(FLAGS.FLAG_0)
+
+ warmupBench.add('warmup', () => warmupFlag.has(FLAGS.FLAG_0))
+
+ for (let i = 0; i < BENCHMARK_CONFIG.warmupIterations; i++) {
+ await warmupBench.run()
+ }
+
+ const singleFlag = bitflag(FLAGS.FLAG_0)
+ const multiFlags = bitflag(FLAGS.FLAG_0 | FLAGS.FLAG_1 | FLAGS.FLAG_2)
+ const allFlags = bitflag(FLAGS.ALL)
+
+ const bench = new Bench({
+ time: BENCHMARK_CONFIG.benchmarkTime,
+ })
+
+ bench
+ .add('has(single)', () => {
+ return singleFlag.has(FLAGS.FLAG_0)
+ })
+ .add('has(double)', () => {
+ return multiFlags.has(FLAGS.FLAG_0, FLAGS.FLAG_1)
+ })
+ .add('hasAny()', () => {
+ return multiFlags.hasAny(FLAGS.FLAG_0, FLAGS.FLAG_5)
+ })
+ .add('hasExact()', () => {
+ return multiFlags.hasExact(FLAGS.FLAG_0, FLAGS.FLAG_1, FLAGS.FLAG_2)
+ })
+
+ .add('add(single)', () => {
+ return singleFlag.add(FLAGS.FLAG_5)
+ })
+ .add('add(multiple)', () => {
+ return singleFlag.add(FLAGS.FLAG_5, FLAGS.FLAG_6)
+ })
+ .add('remove(single)', () => {
+ return multiFlags.remove(FLAGS.FLAG_1)
+ })
+ .add('remove(multiple)', () => {
+ return allFlags.remove(FLAGS.FLAG_0, FLAGS.FLAG_1)
+ })
+ .add('toggle()', () => {
+ return multiFlags.toggle(FLAGS.FLAG_5)
+ })
+ .add('clear()', () => {
+ return allFlags.clear()
+ })
+
+ .add('value', () => {
+ return multiFlags.value
+ })
+ .add('valueOf()', () => {
+ return multiFlags.valueOf()
+ })
+
+ console.log(' Running benchmarks...')
+ await bench.run()
+
+ const results = new Map()
+
+ for (const task of bench.tasks) {
+ const name = task.name as OperationName
+ results.set(name, {
+ name: task.name,
+ ops: Math.round(task.result?.hz || 0),
+ rme: task.result?.rme || 0,
+ samples: task.result?.samples?.length || 0,
+ mean: task.result?.mean,
+ sd: task.result?.sd,
+ })
+ }
+
+ const totalTime = Date.now() - startTime
+ console.log(` โ
bitf suite completed in ${(totalTime / 1000).toFixed(2)}s`)
+
+ return {
+ implementation: 'bitf',
+ results,
+ totalTime,
+ }
+}
diff --git a/benchmark/suites/json-suite.ts b/benchmark/suites/json-suite.ts
new file mode 100644
index 0000000..cd27892
--- /dev/null
+++ b/benchmark/suites/json-suite.ts
@@ -0,0 +1,95 @@
+import { Bench } from 'tinybench'
+import { defineJsonFlags, jsonFlag } from '../json-flags'
+import { BENCHMARK_CONFIG, FLAGS, type OperationName, type SuiteResults } from '../utils/setup'
+
+const JSON_FLAGS = defineJsonFlags(FLAGS as any)
+
+export async function runJsonSuite(): Promise {
+ const startTime = Date.now()
+
+ console.log('\n๐ง Running JSON benchmark suite...')
+
+ console.log(' Warming up...')
+ const warmupBench = new Bench({ time: BENCHMARK_CONFIG.warmupTime })
+ const warmupFlag = jsonFlag(FLAGS.FLAG_0)
+
+ warmupBench.add('warmup', () => warmupFlag.has(JSON_FLAGS.FLAG_0))
+
+ for (let i = 0; i < BENCHMARK_CONFIG.warmupIterations; i++) {
+ await warmupBench.run()
+ }
+
+ const singleFlag = jsonFlag(FLAGS.FLAG_0)
+ const multiFlags = jsonFlag(FLAGS.FLAG_0 | FLAGS.FLAG_1 | FLAGS.FLAG_2)
+ const allFlags = jsonFlag(FLAGS.ALL)
+
+ const bench = new Bench({
+ time: BENCHMARK_CONFIG.benchmarkTime,
+ })
+
+ bench
+ .add('has(single)', () => {
+ return singleFlag.has(JSON_FLAGS.FLAG_0)
+ })
+ .add('has(double)', () => {
+ return multiFlags.has(JSON_FLAGS.FLAG_0, JSON_FLAGS.FLAG_1)
+ })
+ .add('hasAny()', () => {
+ return multiFlags.hasAny(JSON_FLAGS.FLAG_0, JSON_FLAGS.FLAG_5)
+ })
+ .add('hasExact()', () => {
+ return multiFlags.hasExact(JSON_FLAGS.FLAG_0, JSON_FLAGS.FLAG_1, JSON_FLAGS.FLAG_2)
+ })
+
+ .add('add(single)', () => {
+ return singleFlag.add(JSON_FLAGS.FLAG_5)
+ })
+ .add('add(multiple)', () => {
+ return singleFlag.add(JSON_FLAGS.FLAG_5, JSON_FLAGS.FLAG_6)
+ })
+ .add('remove(single)', () => {
+ return multiFlags.remove(JSON_FLAGS.FLAG_1)
+ })
+ .add('remove(multiple)', () => {
+ return allFlags.remove(JSON_FLAGS.FLAG_0, JSON_FLAGS.FLAG_1)
+ })
+ .add('toggle()', () => {
+ return multiFlags.toggle(JSON_FLAGS.FLAG_5)
+ })
+ .add('clear()', () => {
+ return allFlags.clear()
+ })
+
+ .add('value', () => {
+ return multiFlags.value
+ })
+ .add('valueOf()', () => {
+ return multiFlags.valueOf()
+ })
+
+ console.log(' Running benchmarks...')
+ await bench.run()
+
+ const results = new Map()
+
+ for (const task of bench.tasks) {
+ const name = task.name as OperationName
+ results.set(name, {
+ name: task.name,
+ ops: Math.round(task.result?.hz || 0),
+ rme: task.result?.rme || 0,
+ samples: task.result?.samples?.length || 0,
+ mean: task.result?.mean,
+ sd: task.result?.sd,
+ })
+ }
+
+ const totalTime = Date.now() - startTime
+ console.log(` โ
JSON suite completed in ${(totalTime / 1000).toFixed(2)}s`)
+
+ return {
+ implementation: 'json',
+ results,
+ totalTime,
+ }
+}
diff --git a/benchmark/utils/display.ts b/benchmark/utils/display.ts
new file mode 100644
index 0000000..2091feb
--- /dev/null
+++ b/benchmark/utils/display.ts
@@ -0,0 +1,74 @@
+import { formatOps, formatRatio, formatSamples } from './format'
+import { OPERATIONS, OperationName, type SuiteResults } from './setup'
+
+export function displayComparison(bitfResults: SuiteResults, jsonResults: SuiteResults) {
+ console.log(
+ '\nโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'
+ )
+ console.log(
+ 'โ BENCHMARK RESULTS โ'
+ )
+ console.log('โ โโโโโโโโโโโโโโโโโโโโโคโโโโโโโโโโโโโโโโคโโโโโโโโโโโโโโโโคโโโโโโโโโโคโโโโโโโโโโโโโโโโโโฃ')
+ console.log('โ Operation โ bitf ops/sec โ JSON ops/sec โ Ratio โ Samples โ')
+ console.log('โ โโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโชโโโโโโโโโโชโโโโโโโโโโโโโโโโโโฃ')
+
+ let totalRatio = 0
+ let validComparisons = 0
+
+ for (const op of OPERATIONS) {
+ const bitfResult = bitfResults.results.get(op)
+ const jsonResult = jsonResults.results.get(op)
+
+ if (!bitfResult || !jsonResult) continue
+
+ const ratio = bitfResult.ops / jsonResult.ops
+ totalRatio += ratio
+ validComparisons++
+
+ const opName = op.padEnd(18)
+ const bitfOps = formatOps(bitfResult.ops).padStart(13)
+ const jsonOps = formatOps(jsonResult.ops).padStart(13)
+ const ratioStr = formatRatio(ratio).padStart(7)
+ const samples = formatSamples(bitfResult.samples, jsonResult.samples).padStart(15)
+
+ console.log(`โ ${opName} โ ${bitfOps} โ ${jsonOps} โ ${ratioStr} โ ${samples} โ`)
+
+ if (bitfResult.rme > 3 || jsonResult.rme > 3) {
+ const warning = ` โ High RME: bitf=${bitfResult.rme.toFixed(2)}% json=${jsonResult.rme.toFixed(2)}%`
+ console.log(`โ ${warning.padEnd(82)} โ`)
+ }
+ }
+
+ console.log('โโโโโโโโโโโโโโโโโโโโโโงโโโโโโโโโโโโโโโโงโโโโโโโโโโโโโโโโงโโโโโโโโโโงโโโโโโโโโโโโโโโโโโ')
+
+ if (validComparisons > 0) {
+ const avgRatio = totalRatio / validComparisons
+ console.log(`\n๐ Average Performance Improvement: ${formatRatio(avgRatio)} faster than JSON`)
+ }
+
+ console.log(`\nโฑ๏ธ Benchmark Time:`)
+ console.log(` bitf suite: ${(bitfResults.totalTime / 1000).toFixed(2)}s`)
+ console.log(` JSON suite: ${(jsonResults.totalTime / 1000).toFixed(2)}s`)
+}
+
+export function displaySingleSuite(results: SuiteResults) {
+ console.log(`\nโโโ ${results.implementation.toUpperCase()} Benchmark Results โโโ`)
+ console.log('โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโโ')
+ console.log('โ Operation โ Ops/sec โ RME % โ Samples โ')
+ console.log('โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโค')
+
+ for (const op of OPERATIONS) {
+ const result = results.results.get(op)
+ if (!result) continue
+
+ const opName = op.padEnd(18)
+ const ops = formatOps(result.ops).padStart(13)
+ const rme = result.rme.toFixed(2).padStart(8)
+ const samples = result.samples.toString().padStart(9)
+
+ console.log(`โ ${opName} โ ${ops} โ ${rme} โ ${samples} โ`)
+ }
+
+ console.log('โโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโโ')
+ console.log(`Total time: ${(results.totalTime / 1000).toFixed(2)}s`)
+}
diff --git a/benchmark/utils/format.ts b/benchmark/utils/format.ts
new file mode 100644
index 0000000..dca07d0
--- /dev/null
+++ b/benchmark/utils/format.ts
@@ -0,0 +1,36 @@
+export function formatOps(ops: number): string {
+ if (ops >= 1_000_000_000) {
+ return `${(ops / 1_000_000_000).toFixed(2)}B`
+ } else if (ops >= 1_000_000) {
+ return `${(ops / 1_000_000).toFixed(2)}M`
+ } else if (ops >= 1_000) {
+ return `${(ops / 1_000).toFixed(2)}K`
+ }
+ return ops.toFixed(0)
+}
+
+export function formatNumber(num: number): string {
+ return num.toLocaleString('en-US')
+}
+
+export function calculateCV(stdDev: number, mean: number): number {
+ return mean > 0 ? stdDev / mean : 0
+}
+
+export function formatRatio(ratio: number): string {
+ if (ratio >= 100) {
+ return `${ratio.toFixed(0)}x`
+ } else if (ratio >= 10) {
+ return `${ratio.toFixed(1)}x`
+ } else {
+ return `${ratio.toFixed(2)}x`
+ }
+}
+
+export function formatSamples(bitfSamples: number, jsonSamples: number): string {
+ const bitfStr =
+ bitfSamples >= 1000 ? `${(bitfSamples / 1000).toFixed(0)}k` : bitfSamples.toString()
+ const jsonStr =
+ jsonSamples >= 1000 ? `${(jsonSamples / 1000).toFixed(0)}k` : jsonSamples.toString()
+ return `${bitfStr}/${jsonStr}`
+}
diff --git a/benchmark/utils/setup.ts b/benchmark/utils/setup.ts
new file mode 100644
index 0000000..e703ab0
--- /dev/null
+++ b/benchmark/utils/setup.ts
@@ -0,0 +1,54 @@
+import { defineBitflags } from '../../src/index'
+
+export const FLAGS = defineBitflags({
+ NONE: 0,
+ FLAG_0: 1 << 0,
+ FLAG_1: 1 << 1,
+ FLAG_2: 1 << 2,
+ FLAG_3: 1 << 3,
+ FLAG_4: 1 << 4,
+ FLAG_5: 1 << 5,
+ FLAG_6: 1 << 6,
+ FLAG_7: 1 << 7,
+ GROUP_LOW: (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3),
+ GROUP_HIGH: (1 << 4) | (1 << 5) | (1 << 6) | (1 << 7),
+ ALL: (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5) | (1 << 6) | (1 << 7),
+})
+
+export const BENCHMARK_CONFIG = {
+ warmupTime: 50,
+ warmupIterations: 3,
+ benchmarkTime: 100,
+}
+
+export const OPERATIONS = [
+ 'has(single)',
+ 'has(double)',
+ 'hasAny()',
+ 'hasExact()',
+ 'add(single)',
+ 'add(multiple)',
+ 'remove(single)',
+ 'remove(multiple)',
+ 'toggle()',
+ 'clear()',
+ 'value',
+ 'valueOf()',
+] as const
+
+export type OperationName = (typeof OPERATIONS)[number]
+
+export type BenchmarkResult = {
+ name: string
+ ops: number
+ rme: number
+ samples: number
+ mean?: number
+ sd?: number
+}
+
+export type SuiteResults = {
+ implementation: 'bitf' | 'json'
+ results: Map
+ totalTime: number
+}
diff --git a/biome.json b/biome.json
index 976ed1f..49f32d1 100644
--- a/biome.json
+++ b/biome.json
@@ -6,7 +6,7 @@
},
"files": {
"ignoreUnknown": false,
- "includes": ["src/**/*", "test/**/*", "*.ts", "*.js", "*.json"],
+ "includes": ["src/**/*", "benchmark/**/*", "test/**/*", "*.ts", "*.js", "*.json"],
"experimentalScannerIgnores": ["dist/**", "node_modules/**"]
},
"formatter": {
diff --git a/examples/pizza.ts b/examples/pizza.ts
new file mode 100644
index 0000000..d854608
--- /dev/null
+++ b/examples/pizza.ts
@@ -0,0 +1,67 @@
+import {
+ type Bitflag,
+ bitflag,
+ defineBitflags,
+} from '../src/index'
+
+// This should probably live in a file shared between frontend/backend contexts
+const Toppings = defineBitflags({
+ CHEESE: 1 << 0,
+ PEPPERONI: 1 << 1,
+ MUSHROOMS: 1 << 2,
+ OREGANO: 1 << 3,
+ PINEAPPLE: 1 << 4,
+ BACON: 1 << 5,
+ HAM: 1 << 6,
+})
+
+// Can be mapped on frontend using InferBitflagsDefinitions and .describe() function
+type PizzaOrderPreferences = Readonly<{
+ desiredSize: 'small' | 'medium' | 'large'
+ toppingsToAdd: Bitflag
+ toppingsToRemove: Bitflag
+}>
+
+export async function configurePizzaOrder({
+ desiredSize,
+ toppingsToAdd,
+ toppingsToRemove,
+}: PizzaOrderPreferences) {
+ if (bitflag(toppingsToRemove).has(Toppings.CHEESE))
+ throw new Error('Cheese is always included in our pizzas!')
+
+ const defaultPizza = bitflag().add(Toppings.CHEESE, Toppings.PEPPERONI)
+ const pizzaAfterRemoval = processToppingsRemoval(defaultPizza, toppingsToRemove)
+
+ validateMeatAddition(pizzaAfterRemoval, toppingsToAdd)
+
+ // ... some additional logic like checking the toppings availability in the restaurant inventory
+ // ... some additional logging using the .describe() function for comprehensive info
+
+ const finalPizza = bitflag(pizzaAfterRemoval).add(toppingsToAdd)
+
+ return {
+ size: desiredSize,
+ pizza: finalPizza,
+ metadata: {
+ hawaiianPizzaDiscount: bitflag(finalPizza)
+ .hasExact(Toppings.CHEESE, Toppings.HAM, Toppings.PINEAPPLE)
+ }
+ }
+}
+
+function processToppingsRemoval(currentPizza: Bitflag, toppingsToRemove: Bitflag) {
+ if (toppingsToRemove) return bitflag(currentPizza).remove(toppingsToRemove)
+ return currentPizza
+}
+
+function validateMeatAddition(currentPizza: Bitflag, toppingsToAdd: Bitflag) {
+ const currentHasMeat = bitflag(currentPizza)
+ .hasAny(Toppings.PEPPERONI, Toppings.BACON, Toppings.HAM)
+
+ const requestingMeat = bitflag(toppingsToAdd)
+ .hasAny(Toppings.PEPPERONI, Toppings.BACON, Toppings.HAM)
+
+ if (currentHasMeat && requestingMeat)
+ throw new Error('Only one type of meat is allowed per pizza!')
+}
diff --git a/package.json b/package.json
index ad4b82c..7bf228d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
- "name": "typescript-library-template",
+ "name": "bitf",
"version": "0.0.1",
- "description": "A neg4n's template for creating TypeScript libraries",
+ "description": "A tiny utility to write and manage bit flags in JavaScript/TypeScript projects.",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.esm.js",
@@ -31,6 +31,11 @@
"test:coverage": "vitest --coverage",
"lint": "biome check .",
"lint:fix": "biome check --write .",
+ "bench": "tsx benchmark/index.ts",
+ "bench:bitf": "tsx benchmark/runner.ts --bitf",
+ "bench:json": "tsx benchmark/runner.ts --json",
+ "bench:gc": "node --expose-gc node_modules/.bin/tsx benchmark/index.ts",
+ "bench:watch": "tsx watch benchmark/index.ts",
"prepare": "husky",
"postinstall": "husky",
"prepublishOnly": "pnpm build",
@@ -70,7 +75,9 @@
"rollup": "^4.44.0",
"rollup-plugin-dts": "^6.2.1",
"semantic-release": "24.2.5",
+ "tinybench": "^5.0.0",
"tslib": "^2.8.1",
+ "tsx": "^4.20.4",
"type-fest": "^4.41.0",
"typescript": "^5.8.3",
"vitest": "3.2.4"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dbd915a..acc6523 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -46,7 +46,7 @@ importers:
version: 14.0.3(semantic-release@24.2.5(typescript@5.8.3))
'@vitest/coverage-v8':
specifier: 3.2.4
- version: 3.2.4(vitest@3.2.4(terser@5.43.1)(yaml@2.8.0))
+ version: 3.2.4(vitest@3.2.4(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.0))
husky:
specifier: 9.1.7
version: 9.1.7
@@ -62,9 +62,15 @@ importers:
semantic-release:
specifier: 24.2.5
version: 24.2.5(typescript@5.8.3)
+ tinybench:
+ specifier: ^5.0.0
+ version: 5.0.0
tslib:
specifier: ^2.8.1
version: 2.8.1
+ tsx:
+ specifier: ^4.20.4
+ version: 4.20.4
type-fest:
specifier: ^4.41.0
version: 4.41.0
@@ -73,7 +79,7 @@ importers:
version: 5.8.3
vitest:
specifier: 3.2.4
- version: 3.2.4(terser@5.43.1)(yaml@2.8.0)
+ version: 3.2.4(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.0)
packages:
@@ -1120,6 +1126,9 @@ packages:
resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
engines: {node: '>=18'}
+ get-tsconfig@4.10.1:
+ resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
+
git-log-parser@1.2.1:
resolution: {integrity: sha512-PI+sPDvHXNPl5WNOErAK05s3j0lgwUzMN6o8cyQrDaKfT3qd7TmNJKeXX+SknI5I0QhG5fVPAEwSY4tRGDtYoQ==}
@@ -1791,6 +1800,9 @@ packages:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'}
+ resolve-pkg-maps@1.0.0:
+ resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
resolve@1.22.10:
resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
engines: {node: '>= 0.4'}
@@ -2032,6 +2044,10 @@ packages:
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+ tinybench@5.0.0:
+ resolution: {integrity: sha512-iqp7HhNk6IQXuE5fyJsX4ENpnWcw9k+QS5hsLXZq47J8hH/cL/WjNr6Fr9kXMqCGYMhFSLX8xwoJl6rgB0Pz/A==}
+ engines: {node: '>=20.0.0'}
+
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
@@ -2062,6 +2078,11 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+ tsx@4.20.4:
+ resolution: {integrity: sha512-yyxBKfORQ7LuRt/BQKBXrpcq59ZvSW0XxwfjAt3w2/8PmdxaFzijtMhTawprSHhpzeM5BgU2hXHG3lklIERZXg==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+
type-fest@1.4.0:
resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==}
engines: {node: '>=10'}
@@ -2768,7 +2789,7 @@ snapshots:
'@types/resolve@1.20.2': {}
- '@vitest/coverage-v8@3.2.4(vitest@3.2.4(terser@5.43.1)(yaml@2.8.0))':
+ '@vitest/coverage-v8@3.2.4(vitest@3.2.4(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.0))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
@@ -2783,7 +2804,7 @@ snapshots:
std-env: 3.9.0
test-exclude: 7.0.1
tinyrainbow: 2.0.0
- vitest: 3.2.4(terser@5.43.1)(yaml@2.8.0)
+ vitest: 3.2.4(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.0)
transitivePeerDependencies:
- supports-color
@@ -2795,13 +2816,13 @@ snapshots:
chai: 5.2.0
tinyrainbow: 2.0.0
- '@vitest/mocker@3.2.4(vite@6.3.5(terser@5.43.1)(yaml@2.8.0))':
+ '@vitest/mocker@3.2.4(vite@6.3.5(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.0))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
- vite: 6.3.5(terser@5.43.1)(yaml@2.8.0)
+ vite: 6.3.5(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.0)
'@vitest/pretty-format@3.2.4':
dependencies:
@@ -3249,6 +3270,10 @@ snapshots:
'@sec-ant/readable-stream': 0.4.1
is-stream: 4.0.1
+ get-tsconfig@4.10.1:
+ dependencies:
+ resolve-pkg-maps: 1.0.0
+
git-log-parser@1.2.1:
dependencies:
argv-formatter: 1.0.0
@@ -3817,6 +3842,8 @@ snapshots:
resolve-from@5.0.0: {}
+ resolve-pkg-maps@1.0.0: {}
+
resolve@1.22.10:
dependencies:
is-core-module: 2.16.1
@@ -4102,6 +4129,8 @@ snapshots:
tinybench@2.9.0: {}
+ tinybench@5.0.0: {}
+
tinyexec@0.3.2: {}
tinyglobby@0.2.14:
@@ -4123,6 +4152,13 @@ snapshots:
tslib@2.8.1: {}
+ tsx@4.20.4:
+ dependencies:
+ esbuild: 0.25.5
+ get-tsconfig: 4.10.1
+ optionalDependencies:
+ fsevents: 2.3.3
+
type-fest@1.4.0: {}
type-fest@2.19.0: {}
@@ -4161,13 +4197,13 @@ snapshots:
validate-npm-package-name@5.0.1: {}
- vite-node@3.2.4(terser@5.43.1)(yaml@2.8.0):
+ vite-node@3.2.4(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.0):
dependencies:
cac: 6.7.14
debug: 4.4.1
es-module-lexer: 1.7.0
pathe: 2.0.3
- vite: 6.3.5(terser@5.43.1)(yaml@2.8.0)
+ vite: 6.3.5(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.0)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -4182,7 +4218,7 @@ snapshots:
- tsx
- yaml
- vite@6.3.5(terser@5.43.1)(yaml@2.8.0):
+ vite@6.3.5(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.0):
dependencies:
esbuild: 0.25.5
fdir: 6.4.6(picomatch@4.0.2)
@@ -4193,13 +4229,14 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
terser: 5.43.1
+ tsx: 4.20.4
yaml: 2.8.0
- vitest@3.2.4(terser@5.43.1)(yaml@2.8.0):
+ vitest@3.2.4(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.0):
dependencies:
'@types/chai': 5.2.2
'@vitest/expect': 3.2.4
- '@vitest/mocker': 3.2.4(vite@6.3.5(terser@5.43.1)(yaml@2.8.0))
+ '@vitest/mocker': 3.2.4(vite@6.3.5(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.0))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
@@ -4217,8 +4254,8 @@ snapshots:
tinyglobby: 0.2.14
tinypool: 1.1.1
tinyrainbow: 2.0.0
- vite: 6.3.5(terser@5.43.1)(yaml@2.8.0)
- vite-node: 3.2.4(terser@5.43.1)(yaml@2.8.0)
+ vite: 6.3.5(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.0)
+ vite-node: 3.2.4(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.0)
why-is-node-running: 2.3.0
transitivePeerDependencies:
- jiti
diff --git a/rollup.config.js b/rollup.config.js
index 32c59ca..6585bcc 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -8,7 +8,6 @@ import dts from 'rollup-plugin-dts'
const external = id => !id.startsWith('.') && !id.startsWith('/') && !id.includes('src/')
export default defineConfig([
- // ESM and CJS builds
{
input: 'src/index.ts',
external,
@@ -37,7 +36,6 @@ export default defineConfig([
}),
],
},
- // Minified ESM build
{
input: 'src/index.ts',
external,
@@ -70,7 +68,6 @@ export default defineConfig([
}),
],
},
- // Type definitions
{
input: 'src/index.ts',
external,
diff --git a/src/index.ts b/src/index.ts
index e94213b..56d824c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,17 +1,244 @@
-export function greet(name: string): string {
- return `Hello, ${name}!`
+import type { Tagged } from 'type-fest'
+
+export type Bitflag = Tagged
+
+export type BitflagsDefinitions> = {
+ readonly [K in keyof T]: Tagged
+}
+
+export type InferBitflagsDefinitions> =
+ T extends BitflagsDefinitions ? { [K in keyof U]: Bitflag } : never
+
+type BitPosition = {
+ exact: number
+ remaining: number
+ visual: string
+}
+
+type FlagDescription = {
+ name: string
+ value: number
+ decimal: string
+ hexadecimal: string
+ binary: string
+ unknown: boolean
+ bitPosition: BitPosition
+}
+
+type BitflagOperations = Record> = {
+ has(...flags: Bitflag[]): boolean
+ hasAny(...flags: Bitflag[]): boolean
+ hasExact(...flags: Bitflag[]): boolean
+ add(...flags: Bitflag[]): Bitflag
+ remove(...flags: Bitflag[]): Bitflag
+ toggle(...flags: Bitflag[]): Bitflag
+ clear(): Bitflag
+ describe(flagDefinitions?: BitflagsDefinitions): IterableIterator
+ value: number
+ valueOf(): number
+ toString(): string
+}
+
+export function defineBitflags>(obj: T): BitflagsDefinitions {
+ const frozen = Object.freeze(obj)
+
+ for (const [key, value] of Object.entries(frozen)) {
+ if (!Number.isInteger(value) || value < 0 || value > 0x7fffffff) {
+ throw new Error(
+ `Invalid bitflag value for "${key}": ${value}. Must be a non-negative integer within 31-bit range.`
+ )
+ }
+ }
+
+ return frozen as BitflagsDefinitions
+}
+
+function combineFlags(flags: Bitflag[]): number {
+ let result = 0
+ for (let i = 0; i < flags.length; i++) {
+ result |= flags[i] as unknown as number
+ }
+ return result
+}
+
+function createBitPosition(value: number): BitPosition {
+ if (value === 0) {
+ return {
+ exact: -1,
+ remaining: 31,
+ visual: '(0)0000000000000000000000000000000',
+ }
+ }
+
+ const bits = []
+ const exactPositions = []
+
+ for (let i = 30; i >= 0; i--) {
+ if (value & (1 << i)) {
+ bits.push('[1]')
+ exactPositions.push(i)
+ } else {
+ bits.push('0')
+ }
+ }
+
+ const visual = `(0)${bits.join('')}`
+ const maxPosition = exactPositions.length > 0 ? Math.max(...exactPositions) : 0
+
+ return {
+ exact: maxPosition,
+ remaining: 31 - maxPosition,
+ visual: visual,
+ }
+}
+
+export function bitflag = Record>(
+ flag: Bitflag | number = 0
+): BitflagOperations {
+ const value = (typeof flag === 'number' ? flag : (flag as unknown as number)) | 0
+
+ return {
+ has(...flags: Bitflag[]) {
+ if (flags.length === 0) return false
+ const combined = combineFlags(flags)
+ return (value & combined) === combined
+ },
+
+ hasAny(...flags: Bitflag[]) {
+ if (flags.length === 0) return false
+ const combined = combineFlags(flags)
+ return (value & combined) !== 0
+ },
+
+ hasExact(...flags: Bitflag[]) {
+ if (flags.length === 0) return value === 0
+ const combined = combineFlags(flags)
+ return value === combined
+ },
+
+ add(...flags: Bitflag[]) {
+ if (flags.length === 0) return value as Bitflag
+ const combined = combineFlags(flags)
+ return (value | combined) as Bitflag
+ },
+
+ remove(...flags: Bitflag[]) {
+ if (flags.length === 0) return value as Bitflag
+ const combined = combineFlags(flags)
+ return (value & ~combined) as Bitflag
+ },
+
+ toggle(...flags: Bitflag[]) {
+ if (flags.length === 0) return value as Bitflag
+ const combined = combineFlags(flags)
+ return (value ^ combined) as Bitflag
+ },
+
+ clear() {
+ return 0 as Bitflag
+ },
+
+ *describe(flagDefinitions?: BitflagsDefinitions): IterableIterator {
+ if (value === 0) {
+ yield {
+ name: 'NONE',
+ value: 0,
+ decimal: '0',
+ hexadecimal: '0x0',
+ binary: '0b0',
+ unknown: false,
+ bitPosition: createBitPosition(0),
+ }
+ return
+ }
+
+ const knownFlags: FlagDescription[] = []
+ let unknownBits = value
+
+ if (flagDefinitions) {
+ for (const [name, flagValue] of Object.entries(flagDefinitions)) {
+ const numValue = flagValue as unknown as number
+ if (numValue !== 0 && (value & numValue) === numValue) {
+ knownFlags.push({
+ name,
+ value: numValue,
+ decimal: numValue.toString(),
+ hexadecimal: `0x${numValue.toString(16).toUpperCase()}`,
+ binary: `0b${numValue.toString(2)}`,
+ unknown: false,
+ bitPosition: createBitPosition(numValue),
+ })
+ unknownBits &= ~numValue
+ }
+ }
+ }
+
+ if (!flagDefinitions || Object.keys(flagDefinitions).length === 0) {
+ for (let bit = 0; bit < 31; bit++) {
+ const mask = 1 << bit
+ if (value & mask) {
+ yield {
+ name: `BIT_${bit}`,
+ value: mask,
+ decimal: mask.toString(),
+ hexadecimal: `0x${mask.toString(16).toUpperCase()}`,
+ binary: `0b${mask.toString(2)}`,
+ unknown: false,
+ bitPosition: createBitPosition(mask),
+ }
+ }
+ }
+ } else {
+ for (const flag of knownFlags) {
+ yield flag
+ }
+
+ if (unknownBits !== 0) {
+ for (let bit = 0; bit < 31; bit++) {
+ const mask = 1 << bit
+ if (unknownBits & mask) {
+ yield {
+ name: `UNKNOWN_BIT_${bit}`,
+ value: mask,
+ decimal: mask.toString(),
+ hexadecimal: `0x${mask.toString(16).toUpperCase()}`,
+ binary: `0b${mask.toString(2)}`,
+ unknown: true,
+ bitPosition: createBitPosition(mask),
+ }
+ }
+ }
+ }
+ }
+ },
+
+ get value() {
+ return value
+ },
+
+ valueOf() {
+ return value
+ },
+
+ toString() {
+ return value.toString()
+ },
+ }
}
-export function add(a: number, b: number): number {
- return a + b
+export function makeBitflag(value: number): Bitflag {
+ if (isBitflag(value)) {
+ return value
+ }
+ throw new Error(
+ 'Value cannot be converted to Bitflag. Possible causes are: value exceeding 31 bits or value being a negative number.'
+ )
}
-export function multiply(a: number, b: number): number {
- return a * b
+export function isBitflag(value: unknown): value is Bitflag {
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 0x7fffffff
}
-export default {
- greet,
- add,
- multiply,
+export function unwrapBitflag(flag: Bitflag): number {
+ return flag as unknown as number
}
diff --git a/test/index.test.ts b/test/index.test.ts
index 61fc331..c8b5126 100644
--- a/test/index.test.ts
+++ b/test/index.test.ts
@@ -1,40 +1,941 @@
import { describe, expect, it } from 'vitest'
-import { add, greet, multiply } from '../src/index'
+import {
+ type Bitflag,
+ bitflag,
+ defineBitflags,
+ type InferBitflagsDefinitions,
+ isBitflag,
+ makeBitflag,
+ unwrapBitflag,
+} from '../src/index'
-describe('greet', () => {
- it('should return a greeting message', () => {
- expect(greet('World')).toBe('Hello, World!')
+describe('defineBitflags', () => {
+ it('should create a frozen bitflags object', () => {
+ const flags = defineBitflags({
+ READ: 1 << 0,
+ WRITE: 1 << 1,
+ EXECUTE: 1 << 2,
+ })
+
+ expect(flags.READ).toBe(1)
+ expect(flags.WRITE).toBe(2)
+ expect(flags.EXECUTE).toBe(4)
+ expect(Object.isFrozen(flags)).toBe(true)
+ })
+
+ it('should accept combined flags', () => {
+ const flags = defineBitflags({
+ READ: 1 << 0,
+ WRITE: 1 << 1,
+ READ_WRITE: (1 << 0) | (1 << 1),
+ })
+
+ expect(flags.READ).toBe(1)
+ expect(flags.WRITE).toBe(2)
+ expect(flags.READ_WRITE).toBe(3)
})
- it('should handle empty string', () => {
- expect(greet('')).toBe('Hello, !')
+ it('should handle zero flag', () => {
+ const flags = defineBitflags({
+ NONE: 0,
+ FLAG_A: 1 << 0,
+ FLAG_B: 1 << 1,
+ })
+
+ expect(flags.NONE).toBe(0)
+ expect(flags.FLAG_A).toBe(1)
+ expect(flags.FLAG_B).toBe(2)
})
})
-describe('add', () => {
- it('should add two positive numbers', () => {
- expect(add(2, 3)).toBe(5)
+describe('bitflag operations', () => {
+ const Permissions = defineBitflags({
+ NONE: 0,
+ READ: 1 << 0,
+ WRITE: 1 << 1,
+ EXECUTE: 1 << 2,
+ DELETE: 1 << 3,
+ ALL: (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3),
+ })
+
+ describe('has', () => {
+ it('should check if flag has specific permissions', () => {
+ const perms = bitflag(Permissions.READ | Permissions.WRITE)
+
+ expect(perms.has(Permissions.READ)).toBe(true)
+ expect(perms.has(Permissions.WRITE)).toBe(true)
+ expect(perms.has(Permissions.EXECUTE)).toBe(false)
+ expect(perms.has(Permissions.DELETE)).toBe(false)
+ })
+
+ it('should check multiple flags at once', () => {
+ const perms = bitflag(Permissions.READ | Permissions.WRITE | Permissions.EXECUTE)
+
+ expect(perms.has(Permissions.READ, Permissions.WRITE)).toBe(true)
+ expect(perms.has(Permissions.READ, Permissions.EXECUTE)).toBe(true)
+ expect(perms.has(Permissions.READ, Permissions.DELETE)).toBe(false)
+ expect(perms.has(Permissions.WRITE, Permissions.DELETE)).toBe(false)
+ })
+
+ it('should return false for empty arguments', () => {
+ const perms = bitflag(Permissions.READ)
+ expect(perms.has()).toBe(false)
+ })
+ })
+
+ describe('hasAny', () => {
+ it('should check if flag has any of the specified permissions', () => {
+ const perms = bitflag(Permissions.READ | Permissions.WRITE)
+
+ expect(perms.hasAny(Permissions.READ)).toBe(true)
+ expect(perms.hasAny(Permissions.EXECUTE)).toBe(false)
+ expect(perms.hasAny(Permissions.READ, Permissions.EXECUTE)).toBe(true)
+ expect(perms.hasAny(Permissions.EXECUTE, Permissions.DELETE)).toBe(false)
+ })
+
+ it('should return false for empty arguments', () => {
+ const perms = bitflag(Permissions.READ)
+ expect(perms.hasAny()).toBe(false)
+ })
})
- it('should add negative numbers', () => {
- expect(add(-1, -2)).toBe(-3)
+ describe('hasExact', () => {
+ it('should check if flags match exactly', () => {
+ const perms = bitflag(Permissions.READ | Permissions.WRITE)
+
+ expect(perms.hasExact(Permissions.READ, Permissions.WRITE)).toBe(true)
+ expect(perms.hasExact(Permissions.READ)).toBe(false)
+ expect(perms.hasExact(Permissions.READ, Permissions.WRITE, Permissions.EXECUTE)).toBe(false)
+ })
+
+ it('should handle zero flags', () => {
+ const noPerms = bitflag(Permissions.NONE)
+ expect(noPerms.hasExact()).toBe(true)
+ expect(noPerms.hasExact(Permissions.READ)).toBe(false)
+
+ const somePerms = bitflag(Permissions.READ)
+ expect(somePerms.hasExact()).toBe(false)
+ })
})
- it('should add zero', () => {
- expect(add(5, 0)).toBe(5)
+ describe('add', () => {
+ it('should add single flag', () => {
+ const perms = bitflag(Permissions.READ)
+ const updated = bitflag(perms.add(Permissions.WRITE))
+
+ expect(updated.has(Permissions.READ)).toBe(true)
+ expect(updated.has(Permissions.WRITE)).toBe(true)
+ expect(updated.has(Permissions.EXECUTE)).toBe(false)
+ })
+
+ it('should add multiple flags', () => {
+ const perms = bitflag(Permissions.READ)
+ const updated = bitflag(perms.add(Permissions.WRITE, Permissions.EXECUTE))
+
+ expect(updated.has(Permissions.READ, Permissions.WRITE, Permissions.EXECUTE)).toBe(true)
+ expect(updated.has(Permissions.DELETE)).toBe(false)
+ })
+
+ it('should be idempotent', () => {
+ const perms = bitflag(Permissions.READ | Permissions.WRITE)
+ const updated = bitflag(perms.add(Permissions.READ))
+
+ expect(updated.value).toBe(perms.value)
+ })
+
+ it('should return same value with no arguments', () => {
+ const perms = bitflag(Permissions.READ)
+ const updated = bitflag(perms.add())
+ expect(updated.value).toBe(perms.value)
+ })
+ })
+
+ describe('remove', () => {
+ it('should remove single flag', () => {
+ const perms = bitflag(Permissions.READ | Permissions.WRITE)
+ const updated = bitflag(perms.remove(Permissions.WRITE))
+
+ expect(updated.has(Permissions.READ)).toBe(true)
+ expect(updated.has(Permissions.WRITE)).toBe(false)
+ })
+
+ it('should remove multiple flags', () => {
+ const perms = bitflag(Permissions.ALL)
+ const updated = bitflag(perms.remove(Permissions.WRITE, Permissions.DELETE))
+
+ expect(updated.has(Permissions.READ)).toBe(true)
+ expect(updated.has(Permissions.EXECUTE)).toBe(true)
+ expect(updated.has(Permissions.WRITE)).toBe(false)
+ expect(updated.has(Permissions.DELETE)).toBe(false)
+ })
+
+ it('should handle removing non-existent flag', () => {
+ const perms = bitflag(Permissions.READ)
+ const updated = bitflag(perms.remove(Permissions.WRITE))
+
+ expect(updated.value).toBe(perms.value)
+ })
+
+ it('should return same value with no arguments', () => {
+ const perms = bitflag(Permissions.READ)
+ const updated = bitflag(perms.remove())
+ expect(updated.value).toBe(perms.value)
+ })
+ })
+
+ describe('toggle', () => {
+ it('should toggle single flag', () => {
+ const perms = bitflag(Permissions.READ)
+ const toggled = bitflag(perms.toggle(Permissions.WRITE))
+
+ expect(toggled.has(Permissions.READ)).toBe(true)
+ expect(toggled.has(Permissions.WRITE)).toBe(true)
+
+ const toggledAgain = bitflag(toggled.toggle(Permissions.WRITE))
+ expect(toggledAgain.has(Permissions.READ)).toBe(true)
+ expect(toggledAgain.has(Permissions.WRITE)).toBe(false)
+ })
+
+ it('should toggle multiple flags', () => {
+ const perms = bitflag(Permissions.READ | Permissions.EXECUTE)
+ const toggled = bitflag(perms.toggle(Permissions.WRITE, Permissions.EXECUTE))
+
+ expect(toggled.has(Permissions.READ)).toBe(true)
+ expect(toggled.has(Permissions.WRITE)).toBe(true)
+ expect(toggled.has(Permissions.EXECUTE)).toBe(false)
+ })
+
+ it('should return same value with no arguments', () => {
+ const perms = bitflag(Permissions.READ)
+ const toggled = bitflag(perms.toggle())
+ expect(toggled.value).toBe(perms.value)
+ })
+ })
+
+ describe('clear', () => {
+ it('should clear all flags', () => {
+ const perms = bitflag(Permissions.ALL)
+ const cleared = bitflag(perms.clear())
+
+ expect(cleared.value).toBe(0)
+ expect(cleared.has(Permissions.READ)).toBe(false)
+ expect(cleared.has(Permissions.WRITE)).toBe(false)
+ expect(cleared.has(Permissions.EXECUTE)).toBe(false)
+ expect(cleared.has(Permissions.DELETE)).toBe(false)
+ })
+ })
+
+ describe('describe', () => {
+ it('should describe flags with definitions', () => {
+ const perms = bitflag(Permissions.READ | Permissions.WRITE)
+ const descriptions = [...perms.describe(Permissions)]
+
+ expect(descriptions).toHaveLength(2)
+ expect(descriptions.find(d => d.name === 'READ')).toBeDefined()
+ expect(descriptions.find(d => d.name === 'WRITE')).toBeDefined()
+
+ const readDesc = descriptions.find(d => d.name === 'READ')
+ expect(readDesc).toBeDefined()
+ expect(readDesc?.value).toBe(1)
+ expect(readDesc?.decimal).toBe('1')
+ expect(readDesc?.hexadecimal).toBe('0x1')
+ expect(readDesc?.binary).toBe('0b1')
+ expect(readDesc?.unknown).toBe(false)
+ expect(readDesc?.bitPosition.exact).toBe(0)
+ })
+
+ it('should describe unknown flags', () => {
+ const unknownFlag = bitflag(1 << 10)
+ const descriptions = [...unknownFlag.describe(Permissions)]
+
+ expect(descriptions).toHaveLength(1)
+ expect(descriptions[0]?.name).toBe('UNKNOWN_BIT_10')
+ expect(descriptions[0]?.value).toBe(1024)
+ })
+
+ it('should describe flags without definitions', () => {
+ const perms = bitflag((1 << 0) | (1 << 2) | (1 << 5))
+ const descriptions = [...perms.describe()]
+
+ expect(descriptions).toHaveLength(3)
+ expect(descriptions.find(d => d.name === 'BIT_0')).toBeDefined()
+ expect(descriptions.find(d => d.name === 'BIT_2')).toBeDefined()
+ expect(descriptions.find(d => d.name === 'BIT_5')).toBeDefined()
+ })
+
+ it('should describe zero flag', () => {
+ const noPerms = bitflag(Permissions.NONE)
+ const descriptions = [...noPerms.describe()]
+
+ expect(descriptions).toHaveLength(1)
+ expect(descriptions[0]?.name).toBe('NONE')
+ expect(descriptions[0]?.value).toBe(0)
+ expect(descriptions[0]?.unknown).toBe(false)
+ expect(descriptions[0]?.bitPosition.exact).toBe(-1)
+ expect(descriptions[0]?.bitPosition.remaining).toBe(31)
+ expect(descriptions[0]?.bitPosition.visual).toBe('(0)0000000000000000000000000000000')
+ })
+
+ it('should be iterable', () => {
+ const perms = bitflag(Permissions.READ | Permissions.WRITE)
+ const names: string[] = []
+
+ for (const desc of perms.describe(Permissions)) {
+ names.push(desc.name)
+ }
+
+ expect(names).toContain('READ')
+ expect(names).toContain('WRITE')
+ })
+ })
+
+ describe('bitPosition and unknown properties', () => {
+ it('should provide correct bit position information for single bits', () => {
+ const flags = defineBitflags({
+ BIT_0: 1 << 0,
+ BIT_1: 1 << 1,
+ BIT_15: 1 << 15,
+ BIT_30: 1 << 30,
+ })
+
+ const bit0 = bitflag(flags.BIT_0)
+ const descriptions0 = [...bit0.describe(flags)]
+ expect(descriptions0[0]?.bitPosition.exact).toBe(0)
+ expect(descriptions0[0]?.bitPosition.remaining).toBe(31)
+ expect(descriptions0[0]?.bitPosition.visual).toBe('(0)000000000000000000000000000000[1]')
+ expect(descriptions0[0]?.unknown).toBe(false)
+
+ const bit1 = bitflag(flags.BIT_1)
+ const descriptions1 = [...bit1.describe(flags)]
+ expect(descriptions1[0]?.bitPosition.exact).toBe(1)
+ expect(descriptions1[0]?.bitPosition.remaining).toBe(30)
+ expect(descriptions1[0]?.bitPosition.visual).toBe('(0)00000000000000000000000000000[1]0')
+ expect(descriptions1[0]?.unknown).toBe(false)
+
+ const bit15 = bitflag(flags.BIT_15)
+ const descriptions15 = [...bit15.describe(flags)]
+ expect(descriptions15[0]?.bitPosition.exact).toBe(15)
+ expect(descriptions15[0]?.bitPosition.remaining).toBe(16)
+ expect(descriptions15[0]?.bitPosition.visual).toBe('(0)000000000000000[1]000000000000000')
+ expect(descriptions15[0]?.unknown).toBe(false)
+
+ const bit30 = bitflag(flags.BIT_30)
+ const descriptions30 = [...bit30.describe(flags)]
+ expect(descriptions30[0]?.bitPosition.exact).toBe(30)
+ expect(descriptions30[0]?.bitPosition.remaining).toBe(1)
+ expect(descriptions30[0]?.bitPosition.visual).toBe('(0)[1]000000000000000000000000000000')
+ expect(descriptions30[0]?.unknown).toBe(false)
+ })
+
+ it('should handle unknown flags correctly', () => {
+ const flags = defineBitflags({
+ KNOWN: 1 << 0,
+ })
+
+ const unknownFlag = bitflag((1 << 5) | (1 << 10))
+ const descriptions = [...unknownFlag.describe(flags)]
+
+ expect(descriptions).toHaveLength(2)
+
+ const bit5Desc = descriptions.find(d => d.name === 'UNKNOWN_BIT_5')
+ expect(bit5Desc?.unknown).toBe(true)
+ expect(bit5Desc?.bitPosition.exact).toBe(5)
+ expect(bit5Desc?.bitPosition.remaining).toBe(26)
+ expect(bit5Desc?.bitPosition.visual).toBe('(0)0000000000000000000000000[1]00000')
+
+ const bit10Desc = descriptions.find(d => d.name === 'UNKNOWN_BIT_10')
+ expect(bit10Desc?.unknown).toBe(true)
+ expect(bit10Desc?.bitPosition.exact).toBe(10)
+ expect(bit10Desc?.bitPosition.remaining).toBe(21)
+ expect(bit10Desc?.bitPosition.visual).toBe('(0)00000000000000000000[1]0000000000')
+ })
+
+ it('should handle multiple bits in combined flags', () => {
+ const flags = defineBitflags({
+ COMBINED: (1 << 2) | (1 << 7) | (1 << 20),
+ })
+
+ const combined = bitflag(flags.COMBINED)
+ const descriptions = [...combined.describe(flags)]
+
+ expect(descriptions).toHaveLength(1)
+ expect(descriptions[0]?.name).toBe('COMBINED')
+ expect(descriptions[0]?.unknown).toBe(false)
+ expect(descriptions[0]?.bitPosition.exact).toBe(20)
+ expect(descriptions[0]?.bitPosition.remaining).toBe(11)
+ expect(descriptions[0]?.bitPosition.visual).toBe('(0)0000000000[1]000000000000[1]0000[1]00')
+ })
+
+ it('should handle mixed known and unknown flags', () => {
+ const flags = defineBitflags({
+ KNOWN_A: 1 << 3,
+ KNOWN_B: 1 << 8,
+ })
+
+ const mixed = bitflag((1 << 3) | (1 << 8) | (1 << 15) | (1 << 25))
+ const descriptions = [...mixed.describe(flags)]
+
+ expect(descriptions).toHaveLength(4)
+
+ const knownA = descriptions.find(d => d.name === 'KNOWN_A')
+ expect(knownA?.unknown).toBe(false)
+ expect(knownA?.bitPosition.exact).toBe(3)
+
+ const knownB = descriptions.find(d => d.name === 'KNOWN_B')
+ expect(knownB?.unknown).toBe(false)
+ expect(knownB?.bitPosition.exact).toBe(8)
+
+ const unknown15 = descriptions.find(d => d.name === 'UNKNOWN_BIT_15')
+ expect(unknown15?.unknown).toBe(true)
+ expect(unknown15?.bitPosition.exact).toBe(15)
+
+ const unknown25 = descriptions.find(d => d.name === 'UNKNOWN_BIT_25')
+ expect(unknown25?.unknown).toBe(true)
+ expect(unknown25?.bitPosition.exact).toBe(25)
+ })
+
+ it('should handle describe without definitions correctly', () => {
+ const mixed = bitflag((1 << 0) | (1 << 10) | (1 << 29))
+ const descriptions = [...mixed.describe()]
+
+ expect(descriptions).toHaveLength(3)
+
+ const bit0 = descriptions.find(d => d.name === 'BIT_0')
+ expect(bit0?.unknown).toBe(false)
+ expect(bit0?.bitPosition.exact).toBe(0)
+ expect(bit0?.bitPosition.visual).toBe('(0)000000000000000000000000000000[1]')
+
+ const bit10 = descriptions.find(d => d.name === 'BIT_10')
+ expect(bit10?.unknown).toBe(false)
+ expect(bit10?.bitPosition.exact).toBe(10)
+
+ const bit29 = descriptions.find(d => d.name === 'BIT_29')
+ expect(bit29?.unknown).toBe(false)
+ expect(bit29?.bitPosition.exact).toBe(29)
+ expect(bit29?.bitPosition.visual).toBe('(0)0[1]00000000000000000000000000000')
+ })
+
+ it('should handle edge case with maximum value', () => {
+ const flags = defineBitflags({
+ MAX_VALUE: 0x7fffffff,
+ })
+
+ const maxFlag = bitflag(flags.MAX_VALUE)
+ const descriptions = [...maxFlag.describe(flags)]
+
+ expect(descriptions).toHaveLength(1)
+ expect(descriptions[0]?.name).toBe('MAX_VALUE')
+ expect(descriptions[0]?.unknown).toBe(false)
+ expect(descriptions[0]?.bitPosition.exact).toBe(30)
+ expect(descriptions[0]?.bitPosition.remaining).toBe(1)
+ expect(descriptions[0]?.bitPosition.visual).toBe(
+ '(0)[1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1]'
+ )
+ })
+
+ it('should handle bitflag parameters with non-number type path', () => {
+ const flags = defineBitflags({
+ TEST: 5,
+ })
+
+ const result = bitflag(makeBitflag(flags.TEST))
+ expect(result.value).toBe(5)
+ expect(result.has(flags.TEST)).toBe(true)
+ })
+ })
+
+ describe('value properties', () => {
+ it('should expose numeric value', () => {
+ const perms = bitflag(Permissions.READ | Permissions.WRITE)
+
+ expect(perms.value).toBe(3)
+ expect(perms.valueOf()).toBe(3)
+ expect(perms.toString()).toBe('3')
+ })
+
+ it('should handle default zero value', () => {
+ const perms = bitflag()
+
+ expect(perms.value).toBe(0)
+ expect(perms.valueOf()).toBe(0)
+ expect(perms.toString()).toBe('0')
+ })
+
+ it('should handle bitflag input instead of number', () => {
+ const flags = defineBitflags({
+ READ: 1 << 0,
+ WRITE: 1 << 1,
+ })
+
+ const original = bitflag(flags.READ)
+ const fromBitflag = bitflag(flags.READ)
+
+ expect(fromBitflag.value).toBe(1)
+ expect(fromBitflag.has(flags.READ)).toBe(true)
+ expect(fromBitflag.valueOf()).toBe(original.valueOf())
+ })
+
+ it('should handle non-number bitflag values', () => {
+ const flags = defineBitflags({
+ TEST: 5,
+ })
+
+ const bf1 = bitflag(5)
+ const bf2 = bitflag(bf1.add(flags.TEST))
+
+ expect(bf2.value).toBe(5)
+ expect(bf2.has(flags.TEST)).toBe(true)
+ })
+ })
+})
+
+describe('utility functions', () => {
+ describe('isBitflag', () => {
+ it('should validate valid bitflags', () => {
+ expect(isBitflag(0)).toBe(true)
+ expect(isBitflag(1)).toBe(true)
+ expect(isBitflag(255)).toBe(true)
+ expect(isBitflag(0x7fffffff)).toBe(true)
+ })
+
+ it('should reject invalid values', () => {
+ expect(isBitflag(-1)).toBe(false)
+ expect(isBitflag(1.5)).toBe(false)
+ expect(isBitflag(0x80000000)).toBe(false)
+ expect(isBitflag('1')).toBe(false)
+ expect(isBitflag(null)).toBe(false)
+ expect(isBitflag(undefined)).toBe(false)
+ expect(isBitflag({})).toBe(false)
+ })
+ })
+
+ describe('unwrapBitflag', () => {
+ it('should unwrap bitflag to number', () => {
+ const flags = defineBitflags({
+ FLAG_A: 1 << 0,
+ FLAG_B: 1 << 1,
+ })
+
+ expect(unwrapBitflag(flags.FLAG_A)).toBe(1)
+ expect(unwrapBitflag(flags.FLAG_B)).toBe(2)
+
+ const combined = bitflag(flags.FLAG_A).add(flags.FLAG_B)
+ expect(unwrapBitflag(combined)).toBe(3)
+ })
+ })
+})
+
+describe('defineBitflags error handling', () => {
+ it('should throw error for non-integer values', () => {
+ expect(() => {
+ defineBitflags({
+ INVALID: 1.5,
+ })
+ }).toThrow(
+ 'Invalid bitflag value for "INVALID": 1.5. Must be a non-negative integer within 31-bit range.'
+ )
+
+ expect(() => {
+ defineBitflags({
+ NAN_VALUE: NaN,
+ })
+ }).toThrow(
+ 'Invalid bitflag value for "NAN_VALUE": NaN. Must be a non-negative integer within 31-bit range.'
+ )
+
+ expect(() => {
+ defineBitflags({
+ INFINITY: Infinity,
+ })
+ }).toThrow(
+ 'Invalid bitflag value for "INFINITY": Infinity. Must be a non-negative integer within 31-bit range.'
+ )
+ })
+
+ it('should throw error for negative values', () => {
+ expect(() => {
+ defineBitflags({
+ NEGATIVE: -1,
+ })
+ }).toThrow(
+ 'Invalid bitflag value for "NEGATIVE": -1. Must be a non-negative integer within 31-bit range.'
+ )
+ })
+
+ it('should throw error for values exceeding 31-bit range', () => {
+ expect(() => {
+ defineBitflags({
+ TOO_LARGE: 0x80000000,
+ })
+ }).toThrow(
+ 'Invalid bitflag value for "TOO_LARGE": 2147483648. Must be a non-negative integer within 31-bit range.'
+ )
+
+ expect(() => {
+ defineBitflags({
+ WAY_TOO_LARGE: 0xffffffff,
+ })
+ }).toThrow(
+ 'Invalid bitflag value for "WAY_TOO_LARGE": 4294967295. Must be a non-negative integer within 31-bit range.'
+ )
+ })
+})
+
+describe('complex real-world scenarios', () => {
+ it('should handle permission cascading', () => {
+ const Permissions = defineBitflags({
+ NONE: 0,
+ READ: 1 << 0,
+ WRITE: 1 << 1,
+ EXECUTE: 1 << 2,
+ DELETE: 1 << 3,
+ ADMIN: 1 << 4,
+ SUPER_ADMIN: (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4),
+ EDITOR: (1 << 0) | (1 << 1),
+ VIEWER: 1 << 0,
+ })
+
+ const superAdmin = bitflag(Permissions.SUPER_ADMIN)
+ expect(
+ superAdmin.has(
+ Permissions.READ,
+ Permissions.WRITE,
+ Permissions.EXECUTE,
+ Permissions.DELETE,
+ Permissions.ADMIN
+ )
+ ).toBe(true)
+ expect(superAdmin.hasAny(Permissions.EDITOR)).toBe(true)
+ expect(superAdmin.hasAny(Permissions.VIEWER)).toBe(true)
+
+ const editor = bitflag(Permissions.EDITOR)
+ expect(editor.has(Permissions.READ, Permissions.WRITE)).toBe(true)
+ expect(editor.has(Permissions.EXECUTE)).toBe(false)
+ expect(editor.hasAny(Permissions.DELETE, Permissions.ADMIN)).toBe(false)
+ })
+
+ it('should handle state machine transitions', () => {
+ const States = defineBitflags({
+ IDLE: 0,
+ LOADING: 1 << 0,
+ SUCCESS: 1 << 1,
+ ERROR: 1 << 2,
+ RETRY: 1 << 3,
+ CACHED: 1 << 4,
+ LOADING_WITH_CACHE: (1 << 0) | (1 << 4),
+ ERROR_WITH_RETRY: (1 << 2) | (1 << 3),
+ })
+
+ let currentState = bitflag(States.IDLE)
+
+ currentState = bitflag(currentState.add(States.LOADING))
+ expect(currentState.has(States.LOADING)).toBe(true)
+ expect(currentState.hasExact(States.LOADING)).toBe(true)
+
+ const removedLoading = bitflag(currentState.remove(States.LOADING))
+ currentState = bitflag(removedLoading.add(States.SUCCESS, States.CACHED))
+ expect(currentState.has(States.SUCCESS, States.CACHED)).toBe(true)
+ expect(currentState.has(States.LOADING)).toBe(false)
+
+ const clearedState = bitflag(currentState.clear())
+ currentState = bitflag(clearedState.add(States.LOADING_WITH_CACHE))
+ expect(currentState.has(States.LOADING, States.CACHED)).toBe(true)
+ })
+
+ it('should handle feature flag combinations', () => {
+ const Features = defineBitflags({
+ NONE: 0,
+ DARK_MODE: 1 << 0,
+ NOTIFICATIONS: 1 << 1,
+ PREMIUM: 1 << 2,
+ BETA: 1 << 3,
+ ANALYTICS: 1 << 4,
+ PREMIUM_ONLY: (1 << 0) | (1 << 1) | (1 << 4),
+ BETA_FEATURES: (1 << 3) | (1 << 4),
+ })
+
+ const userFeatures = bitflag(Features.DARK_MODE | Features.NOTIFICATIONS)
+ const premiumUpgrade = bitflag(userFeatures.add(Features.PREMIUM, Features.ANALYTICS))
+
+ expect(premiumUpgrade.has(Features.PREMIUM_ONLY)).toBe(true)
+ expect(premiumUpgrade.hasAny(Features.BETA_FEATURES)).toBe(true)
+ expect(premiumUpgrade.has(Features.BETA)).toBe(false)
+
+ const betaUser = bitflag(premiumUpgrade.toggle(Features.BETA))
+ expect(betaUser.has(Features.BETA_FEATURES)).toBe(true)
+ expect(betaUser.hasExact(Features.BETA_FEATURES)).toBe(false)
+ })
+
+ it('should handle audit logging scenarios', () => {
+ const Actions = defineBitflags({
+ NONE: 0,
+ CREATE: 1 << 0,
+ READ: 1 << 1,
+ UPDATE: 1 << 2,
+ DELETE: 1 << 3,
+ ADMIN_ACTION: 1 << 4,
+ BULK_OPERATION: 1 << 5,
+ CRITICAL: (1 << 3) | (1 << 4),
+ MODIFICATION: (1 << 0) | (1 << 2) | (1 << 3) | (1 << 5),
+ })
+
+ const userAction = bitflag(Actions.UPDATE | Actions.BULK_OPERATION)
+ expect(userAction.hasAny(Actions.MODIFICATION)).toBe(true)
+ expect(userAction.has(Actions.CRITICAL)).toBe(false)
+
+ const adminAction = bitflag(Actions.DELETE | Actions.ADMIN_ACTION)
+ expect(adminAction.hasExact(Actions.CRITICAL)).toBe(true)
+ expect(adminAction.hasAny(Actions.MODIFICATION)).toBe(true)
+
+ const logEntry = [...adminAction.describe(Actions)]
+ expect(logEntry.length).toBe(3)
+ expect(logEntry.find(entry => entry.name === 'DELETE')).toBeDefined()
+ expect(logEntry.find(entry => entry.name === 'ADMIN_ACTION')).toBeDefined()
+ expect(logEntry.find(entry => entry.name === 'CRITICAL')).toBeDefined()
+ })
+})
+
+describe('32-bit boundary and edge cases', () => {
+ it('should handle exact 32-bit boundaries', () => {
+ const flags = defineBitflags({
+ BIT_0: 1 << 0,
+ BIT_15: 1 << 15,
+ BIT_30: 1 << 30,
+ MAX_VALUE: 0x7fffffff,
+ })
+
+ const bf30 = bitflag(flags.BIT_30)
+ expect(bf30.value).toBe(1073741824)
+ expect(bf30.toString()).toBe('1073741824')
+ expect(bf30.valueOf()).toBe(1073741824)
+
+ const maxBf = bitflag(flags.MAX_VALUE)
+ expect(maxBf.value).toBe(0x7fffffff)
+ expect(maxBf.has(flags.BIT_0, flags.BIT_15, flags.BIT_30)).toBe(true)
+ })
+
+ it('should handle all zeros edge cases', () => {
+ const flags = defineBitflags({
+ ZERO: 0,
+ ONE: 1,
+ TWO: 2,
+ })
+
+ const zeroFlag = bitflag(flags.ZERO)
+ expect(zeroFlag.hasExact()).toBe(true)
+ expect(zeroFlag.has()).toBe(false)
+ expect(zeroFlag.hasAny()).toBe(false)
+ expect(bitflag(zeroFlag.add()).value).toBe(0)
+ expect(bitflag(zeroFlag.remove()).value).toBe(0)
+ expect(bitflag(zeroFlag.toggle()).value).toBe(0)
+
+ const addedToZero = bitflag(zeroFlag.add(flags.ONE, flags.TWO))
+ expect(addedToZero.value).toBe(3)
+ expect(addedToZero.has(flags.ONE, flags.TWO)).toBe(true)
+ })
+
+ it('should handle all-bits-set scenarios', () => {
+ const allBits = 0x7fffffff
+ const maxFlags = defineBitflags({
+ ALL_BITS: allBits,
+ SINGLE: 1,
+ })
+
+ const bf = bitflag(maxFlags.ALL_BITS)
+ expect(bf.value).toBe(allBits)
+
+ const removed = bitflag(bf.remove(maxFlags.SINGLE))
+ expect(removed.value).toBe(allBits & ~1)
+ expect(removed.has(maxFlags.SINGLE)).toBe(false)
+
+ const toggled = bitflag(removed.toggle(maxFlags.SINGLE))
+ expect(toggled.value).toBe(allBits)
+ expect(toggled.hasExact(maxFlags.ALL_BITS)).toBe(true)
+ })
+
+ it('should handle mixed flag sources', () => {
+ const Flags1 = defineBitflags({
+ A: 1 << 0,
+ B: 1 << 1,
+ })
+
+ const Flags2 = defineBitflags({
+ C: 1 << 2,
+ D: 1 << 3,
+ })
+
+ const mixed = bitflag(Flags1.A | Flags1.B | Flags2.C)
+ expect(mixed.value).toBe(7)
+ expect(mixed.has(Flags1.A, Flags1.B)).toBe(true)
+ expect(mixed.has(Flags2.C)).toBe(true)
+ expect(mixed.has(Flags2.D)).toBe(false)
+
+ const descriptions1 = [...mixed.describe(Flags1)]
+ expect(descriptions1.length).toBe(3)
+ expect(descriptions1.find(d => d.name === 'A')).toBeDefined()
+ expect(descriptions1.find(d => d.name === 'B')).toBeDefined()
+ expect(descriptions1.find(d => d.name === 'UNKNOWN_BIT_2')).toBeDefined()
+ })
+})
+
+describe('production reliability tests', () => {
+ it('should maintain immutability across operations', () => {
+ const flags = defineBitflags({
+ READ: 1 << 0,
+ WRITE: 1 << 1,
+ })
+
+ const original = bitflag(flags.READ)
+ const originalValue = original.value
+
+ const added = bitflag(original.add(flags.WRITE))
+ const removed = bitflag(original.remove(flags.READ))
+ const toggled = bitflag(original.toggle(flags.WRITE))
+ const cleared = bitflag(original.clear())
+
+ expect(original.value).toBe(originalValue)
+ expect(added.value).not.toBe(original.value)
+ expect(removed.value).not.toBe(original.value)
+ expect(toggled.value).not.toBe(original.value)
+ expect(cleared.value).not.toBe(original.value)
+ })
+
+ it('should maintain consistency across multiple operations', () => {
+ const flags = defineBitflags({
+ A: 1,
+ B: 2,
+ C: 4,
+ D: 8,
+ })
+
+ for (let i = 0; i < 100; i++) {
+ const bf = bitflag(flags.A | flags.C)
+ expect(bf.value).toBe(5)
+ expect(bf.has(flags.A, flags.C)).toBe(true)
+ expect(bf.has(flags.B, flags.D)).toBe(false)
+ }
+ })
+
+ it('should handle iterator multiple times without side effects', () => {
+ const flags = defineBitflags({
+ X: 1 << 0,
+ Y: 1 << 1,
+ Z: 1 << 2,
+ })
+
+ const bf = bitflag(flags.X | flags.Y | flags.Z)
+
+ const firstRun = [...bf.describe(flags)]
+ const secondRun = [...bf.describe(flags)]
+ const thirdRun = [...bf.describe(flags)]
+
+ expect(firstRun.length).toBe(3)
+ expect(secondRun.length).toBe(3)
+ expect(thirdRun.length).toBe(3)
+
+ for (let i = 0; i < firstRun.length; i++) {
+ expect(firstRun[i]?.name).toBe(secondRun[i]?.name)
+ expect(secondRun[i]?.name).toBe(thirdRun[i]?.name)
+ expect(firstRun[i]?.value).toBe(secondRun[i]?.value)
+ expect(secondRun[i]?.value).toBe(thirdRun[i]?.value)
+ }
+ })
+
+ it('should handle large-scale flag operations efficiently', () => {
+ const flags = defineBitflags({
+ BIT_0: 1 << 0,
+ BIT_5: 1 << 5,
+ BIT_10: 1 << 10,
+ BIT_15: 1 << 15,
+ BIT_20: 1 << 20,
+ BIT_25: 1 << 25,
+ BIT_30: 1 << 30,
+ })
+
+ let bf = bitflag()
+ const startTime = Date.now()
+
+ for (let i = 0; i < 1000; i++) {
+ bf = bitflag(bf.add(flags.BIT_0))
+ bf = bitflag(bf.toggle(flags.BIT_5))
+ bf = bitflag(bf.remove(flags.BIT_10))
+ bf = bitflag(bf.add(flags.BIT_15, flags.BIT_20))
+ bf = bitflag(bf.toggle(flags.BIT_25, flags.BIT_30))
+ }
+
+ const endTime = Date.now()
+ expect(endTime - startTime).toBeLessThan(100)
+ expect(bf.has(flags.BIT_0)).toBe(true)
+ })
+
+ it('should handle concurrent-style operations correctly', () => {
+ const flags = defineBitflags({
+ TASK_A: 1 << 0,
+ TASK_B: 1 << 1,
+ TASK_C: 1 << 2,
+ RUNNING: 1 << 10,
+ COMPLETED: 1 << 11,
+ ERROR: 1 << 12,
+ })
+
+ const initialState = bitflag()
+
+ const task1 = bitflag(initialState.add(flags.TASK_A, flags.RUNNING))
+ const task2 = bitflag(task1.add(flags.TASK_B))
+ const removedRunning = bitflag(task2.remove(flags.RUNNING))
+ const task3 = bitflag(removedRunning.add(flags.COMPLETED))
+ const task4 = bitflag(task3.add(flags.TASK_C))
+ const task5 = bitflag(task4.toggle(flags.ERROR))
+
+ expect(task5.has(flags.TASK_A, flags.TASK_B, flags.TASK_C)).toBe(true)
+ expect(task5.has(flags.COMPLETED, flags.ERROR)).toBe(true)
+ expect(task5.has(flags.RUNNING)).toBe(false)
+
+ const finalDescriptions = [...task5.describe(flags)]
+ expect(finalDescriptions.length).toBe(5)
})
})
-describe('multiply', () => {
- it('should multiply two positive numbers', () => {
- expect(multiply(3, 4)).toBe(12)
+describe('edge cases and performance', () => {
+ it('should handle maximum 31-bit value', () => {
+ const maxFlag = 0x7fffffff
+ const flags = defineBitflags({
+ MAX: maxFlag,
+ })
+
+ const bf = bitflag(flags.MAX)
+ expect(bf.value).toBe(maxFlag)
+ expect(bf.has(flags.MAX)).toBe(true)
})
- it('should multiply by zero', () => {
- expect(multiply(5, 0)).toBe(0)
+ it('should optimize for SMI representation', () => {
+ const flags = defineBitflags({
+ BIT_30: 1 << 30,
+ })
+
+ const bf = bitflag(flags.BIT_30)
+ expect(bf.value).toBe(1073741824)
+ expect(bf.has(flags.BIT_30)).toBe(true)
})
- it('should multiply negative numbers', () => {
- expect(multiply(-2, 3)).toBe(-6)
+ it('should handle complex flag combinations', () => {
+ const Flags = defineBitflags({
+ A: 1 << 0,
+ B: 1 << 1,
+ C: 1 << 2,
+ D: 1 << 3,
+ E: 1 << 4,
+ GROUP_ABC: (1 << 0) | (1 << 1) | (1 << 2),
+ GROUP_DE: (1 << 3) | (1 << 4),
+ })
+
+ const bf = bitflag(Flags.GROUP_ABC)
+ expect(bf.has(Flags.A, Flags.B, Flags.C)).toBe(true)
+ expect(bf.has(Flags.D)).toBe(false)
+ expect(bf.hasAny(Flags.GROUP_DE)).toBe(false)
+
+ const updated = bitflag(bf.add(Flags.GROUP_DE))
+ expect(updated.has(Flags.GROUP_ABC)).toBe(true)
+ expect(updated.has(Flags.GROUP_DE)).toBe(true)
})
})
diff --git a/test/pizza.test.ts b/test/pizza.test.ts
new file mode 100644
index 0000000..03e66cf
--- /dev/null
+++ b/test/pizza.test.ts
@@ -0,0 +1,201 @@
+import { describe, expect, it } from 'vitest'
+import { configurePizzaOrder } from '../examples/pizza'
+import { bitflag, defineBitflags } from '../src/index'
+
+const Toppings = defineBitflags({
+ CHEESE: 1 << 0,
+ PEPPERONI: 1 << 1,
+ MUSHROOMS: 1 << 2,
+ OREGANO: 1 << 3,
+ PINEAPPLE: 1 << 4,
+ BACON: 1 << 5,
+ HAM: 1 << 6,
+})
+
+describe('Pizza Order Configuration', () => {
+ it('should create default pepperoni pizza with no modifications', async () => {
+ const result = await configurePizzaOrder({
+ desiredSize: 'medium',
+ toppingsToAdd: bitflag().clear(),
+ toppingsToRemove: bitflag().clear(),
+ })
+
+ const pizza = bitflag(result.pizza)
+ expect(pizza.has(Toppings.CHEESE)).toBe(true)
+ expect(pizza.has(Toppings.PEPPERONI)).toBe(true)
+ expect(pizza.hasExact(Toppings.CHEESE, Toppings.PEPPERONI)).toBe(true)
+ expect(result.size).toBe('medium')
+ expect(result.metadata.hawaiianPizzaDiscount).toBe(false)
+ })
+
+ it('should add non-meat toppings to default pepperoni pizza', async () => {
+ const result = await configurePizzaOrder({
+ desiredSize: 'large',
+ toppingsToAdd: bitflag().add(Toppings.MUSHROOMS, Toppings.OREGANO),
+ toppingsToRemove: bitflag().clear(),
+ })
+
+ const pizza = bitflag(result.pizza)
+ expect(pizza.has(Toppings.CHEESE)).toBe(true)
+ expect(pizza.has(Toppings.PEPPERONI)).toBe(true)
+ expect(pizza.has(Toppings.MUSHROOMS)).toBe(true)
+ expect(pizza.has(Toppings.OREGANO)).toBe(true)
+ expect(pizza.has(Toppings.BACON)).toBe(false)
+ expect(result.size).toBe('large')
+ })
+
+ it('should throw error when trying to add another meat to pepperoni pizza', async () => {
+ await expect(
+ configurePizzaOrder({
+ desiredSize: 'small',
+ toppingsToAdd: bitflag().add(Toppings.BACON),
+ toppingsToRemove: bitflag().clear(),
+ })
+ ).rejects.toThrow('Only one type of meat is allowed per pizza!')
+ })
+
+ it('should throw error when trying to add multiple meats at once', async () => {
+ await expect(
+ configurePizzaOrder({
+ desiredSize: 'medium',
+ toppingsToAdd: bitflag().add(Toppings.BACON, Toppings.HAM),
+ toppingsToRemove: bitflag().clear(),
+ })
+ ).rejects.toThrow('Only one type of meat is allowed per pizza!')
+ })
+
+ it('should allow replacing pepperoni with bacon', async () => {
+ const result = await configurePizzaOrder({
+ desiredSize: 'small',
+ toppingsToAdd: bitflag().add(Toppings.BACON),
+ toppingsToRemove: bitflag().add(Toppings.PEPPERONI),
+ })
+
+ const pizza = bitflag(result.pizza)
+ expect(pizza.has(Toppings.CHEESE)).toBe(true)
+ expect(pizza.has(Toppings.PEPPERONI)).toBe(false)
+ expect(pizza.has(Toppings.BACON)).toBe(true)
+ expect(result.size).toBe('small')
+ expect(result.metadata.hawaiianPizzaDiscount).toBe(false)
+ })
+
+ it('should allow replacing pepperoni with ham', async () => {
+ const result = await configurePizzaOrder({
+ desiredSize: 'large',
+ toppingsToAdd: bitflag().add(Toppings.HAM, Toppings.MUSHROOMS),
+ toppingsToRemove: bitflag().add(Toppings.PEPPERONI),
+ })
+
+ const pizza = bitflag(result.pizza)
+ expect(pizza.has(Toppings.CHEESE)).toBe(true)
+ expect(pizza.has(Toppings.PEPPERONI)).toBe(false)
+ expect(pizza.has(Toppings.HAM)).toBe(true)
+ expect(pizza.has(Toppings.MUSHROOMS)).toBe(true)
+ expect(result.size).toBe('large')
+ })
+
+ it('should allow removing pepperoni completely (no meat pizza)', async () => {
+ const result = await configurePizzaOrder({
+ desiredSize: 'medium',
+ toppingsToAdd: bitflag().add(Toppings.MUSHROOMS, Toppings.OREGANO),
+ toppingsToRemove: bitflag().add(Toppings.PEPPERONI),
+ })
+
+ const pizza = bitflag(result.pizza)
+ expect(pizza.has(Toppings.CHEESE)).toBe(true)
+ expect(pizza.has(Toppings.PEPPERONI)).toBe(false)
+ expect(pizza.has(Toppings.BACON)).toBe(false)
+ expect(pizza.has(Toppings.HAM)).toBe(false)
+ expect(pizza.has(Toppings.MUSHROOMS)).toBe(true)
+ expect(pizza.has(Toppings.OREGANO)).toBe(true)
+ })
+
+ it('should throw error when trying to remove cheese', async () => {
+ await expect(
+ configurePizzaOrder({
+ desiredSize: 'small',
+ toppingsToAdd: bitflag().clear(),
+ toppingsToRemove: bitflag().add(Toppings.CHEESE),
+ })
+ ).rejects.toThrow('Cheese is always included in our pizzas!')
+ })
+
+ it('should throw error when trying to remove cheese along with other toppings', async () => {
+ await expect(
+ configurePizzaOrder({
+ desiredSize: 'medium',
+ toppingsToAdd: bitflag().add(Toppings.MUSHROOMS),
+ toppingsToRemove: bitflag().add(Toppings.CHEESE, Toppings.PEPPERONI),
+ })
+ ).rejects.toThrow('Cheese is always included in our pizzas!')
+ })
+
+ it('should create Hawaiian pizza and apply discount', async () => {
+ const result = await configurePizzaOrder({
+ desiredSize: 'large',
+ toppingsToAdd: bitflag().add(Toppings.HAM, Toppings.PINEAPPLE),
+ toppingsToRemove: bitflag().add(Toppings.PEPPERONI),
+ })
+
+ const pizza = bitflag(result.pizza)
+ expect(pizza.has(Toppings.CHEESE)).toBe(true)
+ expect(pizza.has(Toppings.HAM)).toBe(true)
+ expect(pizza.has(Toppings.PINEAPPLE)).toBe(true)
+ expect(pizza.has(Toppings.PEPPERONI)).toBe(false)
+ expect(pizza.hasExact(Toppings.CHEESE, Toppings.HAM, Toppings.PINEAPPLE)).toBe(true)
+ expect(result.metadata.hawaiianPizzaDiscount).toBe(true)
+ })
+
+ it('should not apply Hawaiian discount for incomplete Hawaiian pizza', async () => {
+ const result = await configurePizzaOrder({
+ desiredSize: 'medium',
+ toppingsToAdd: bitflag().add(Toppings.HAM),
+ toppingsToRemove: bitflag().add(Toppings.PEPPERONI),
+ })
+
+ expect(result.metadata.hawaiianPizzaDiscount).toBe(false)
+ })
+
+ it('should not apply Hawaiian discount when Hawaiian has extra toppings', async () => {
+ const result = await configurePizzaOrder({
+ desiredSize: 'large',
+ toppingsToAdd: bitflag().add(Toppings.HAM, Toppings.PINEAPPLE, Toppings.MUSHROOMS),
+ toppingsToRemove: bitflag().add(Toppings.PEPPERONI),
+ })
+
+ const pizza = bitflag(result.pizza)
+ expect(pizza.has(Toppings.CHEESE)).toBe(true)
+ expect(pizza.has(Toppings.HAM)).toBe(true)
+ expect(pizza.has(Toppings.PINEAPPLE)).toBe(true)
+ expect(pizza.has(Toppings.MUSHROOMS)).toBe(true)
+ expect(result.metadata.hawaiianPizzaDiscount).toBe(false)
+ })
+
+ it('should handle complex topping changes correctly', async () => {
+ const result = await configurePizzaOrder({
+ desiredSize: 'medium',
+ toppingsToAdd: bitflag().add(Toppings.BACON, Toppings.MUSHROOMS, Toppings.OREGANO),
+ toppingsToRemove: bitflag().add(Toppings.PEPPERONI),
+ })
+
+ const pizza = bitflag(result.pizza)
+ expect(pizza.has(Toppings.CHEESE)).toBe(true)
+ expect(pizza.has(Toppings.PEPPERONI)).toBe(false)
+ expect(pizza.has(Toppings.BACON)).toBe(true)
+ expect(pizza.has(Toppings.MUSHROOMS)).toBe(true)
+ expect(pizza.has(Toppings.OREGANO)).toBe(true)
+ expect(pizza.has(Toppings.HAM)).toBe(false)
+ expect(pizza.has(Toppings.PINEAPPLE)).toBe(false)
+ })
+
+ it('should handle empty bitflags correctly', async () => {
+ const result = await configurePizzaOrder({
+ desiredSize: 'small',
+ toppingsToAdd: bitflag().clear(),
+ toppingsToRemove: bitflag().clear(),
+ })
+
+ const pizza = bitflag(result.pizza)
+ expect(pizza.hasExact(Toppings.CHEESE, Toppings.PEPPERONI)).toBe(true)
+ })
+})
diff --git a/vitest.config.ts b/vitest.config.ts
index 75cd464..ceabff4 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -16,6 +16,7 @@ export default defineConfig({
'node_modules/',
'dist/',
'test/',
+ 'benchmark/',
'**/*.d.ts',
'rollup.config.js',
'vitest.config.ts',