UNPKG

fraci

Version:

Fractional indexing that's robust, performant, and secure, with first-class support for Drizzle ORM and Prisma ORM.

1 lines 114 kB
{"version":3,"sources":["../src/index.ts","../src/bases.ts","../src/lib/errors.ts","../src/lib/decimal-binary.ts","../src/lib/decimal-string.ts","../src/lib/fractional-indexing-binary.ts","../src/lib/fractional-indexing-string.ts","../src/lib/utils.ts","../src/factory.ts"],"sourcesContent":["/**\n * The main module for the fraci library, providing core utilities and types.\n *\n * @module fraci\n */\n\nexport * from \"./bases.js\";\nexport * from \"./errors.js\";\nexport * from \"./factory.js\";\nexport type * from \"./types.js\";\n","// Note that characters must be unique and in ascending order of their character codes.\n\n/** Decimal */\nexport const BASE10 = \"0123456789\";\n\n/** Lowercase hex */\nexport const BASE16L = \"0123456789abcdef\";\n\n/** Uppercase hex */\nexport const BASE16U = \"0123456789ABCDEF\";\n\n/** Lowercase alphabets */\nexport const BASE26L = \"abcdefghijklmnopqrstuvwxyz\";\n\n/** Uppercase alphabets */\nexport const BASE26U = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\";\n\n/** Lowercase alphanumeric characters */\nexport const BASE36L = \"0123456789abcdefghijklmnopqrstuvwxyz\";\n\n/** Uppercase alphanumeric characters */\nexport const BASE36U = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\";\n\n/** Alphabets */\nexport const BASE52 = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n\n/** Alphanumeric characters */\nexport const BASE62 =\n \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n\n/** Characters used in Base64 URL */\nexport const BASE64URL =\n \"-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz\";\n\n/** HTML safe chars */\nexport const BASE88 =\n \"!#$%()*+,-./0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~\";\n\n/** All ASCII (excluding control chars and newlines) */\nexport const BASE95 =\n \" !\\\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\";\n","/**\n * Error codes for the Fraci library.\n *\n * These codes help identify specific error conditions that may occur during library operations.\n *\n * - `INITIALIZATION_FAILED`: Indicates that the library failed to initialize.\n * Currently seen when the base string does not meet the requirements, or when the specified model or field does not exist in the generated Prisma client.\n * - `INTERNAL_ERROR`: Indicates an internal error in the library. Please file an issue if you see this.\n * - `INVALID_FRACTIONAL_INDEX`: Indicates that an invalid fractional index was provided to `generateKeyBetween` or `generateNKeysBetween` functions.\n * - `MAX_LENGTH_EXCEEDED`: Indicates that the maximum length of the generated key was exceeded.\n * - `MAX_RETRIES_EXCEEDED`: Indicates that the maximum number of retries was exceeded when generating a key.\n *\n * @see {@link FraciError} - The custom error class for the Fraci library\n */\nexport type FraciErrorCode =\n | \"INITIALIZATION_FAILED\"\n | \"INTERNAL_ERROR\"\n | \"INVALID_FRACTIONAL_INDEX\"\n | \"MAX_LENGTH_EXCEEDED\"\n | \"MAX_RETRIES_EXCEEDED\";\n\n/**\n * Custom error class for the Fraci library.\n *\n * This class encapsulates errors that occur during fractional indexing operations,\n * providing structured error information through error codes and descriptive messages.\n * Use the utility functions {@link isFraciError} and {@link getFraciErrorCode} to safely work with these errors.\n *\n * @see {@link FraciErrorCode} - The error codes for the Fraci library\n * @see {@link isFraciError} - Type guard to check if an error is a FraciError\n * @see {@link getFraciErrorCode} - Function to extract the error code from a FraciError\n */\nexport class FraciError extends Error {\n readonly name: \"FraciError\";\n\n constructor(\n /**\n * The specific error code identifying the type of error.\n */\n readonly code: FraciErrorCode,\n /**\n * A descriptive message providing details about the error condition.\n */\n readonly message: string,\n ) {\n super(`[${code}] ${message}`);\n\n this.name = \"FraciError\";\n }\n}\n\n/**\n * Type guard that checks if the given error is an instance of {@link FraciError}.\n *\n * This is useful in error handling blocks to determine if an error originated from the Fraci library.\n *\n * @param error - The error to check\n * @returns `true` if the error is a {@link FraciError}, `false` otherwise\n *\n * @example\n * ```typescript\n * try {\n * // Some Fraci operation\n * } catch (error) {\n * if (isFraciError(error)) {\n * // Handle Fraci-specific error\n * } else {\n * // Handle other types of errors\n * }\n * }\n * ```\n *\n * @see {@link FraciError} - The custom error class for the Fraci library\n * @see {@link getFraciErrorCode} - Function to extract the error code from a {@link FraciError}\n */\nexport function isFraciError(error: unknown): error is FraciError {\n return error instanceof FraciError;\n}\n\n/**\n * Extracts the error code from a {@link FraciError}.\n *\n * This function safely extracts the error code without requiring type checking first.\n * If the error is not a {@link FraciError}, it returns `undefined`.\n *\n * @param error - The error to extract the code from\n * @returns The {@link FraciErrorCode} if the error is a {@link FraciError}, `undefined` otherwise\n *\n * @example\n * ```typescript\n * try {\n * // Some Fraci operation\n * } catch (error) {\n * switch (getFraciErrorCode(error)) {\n * case \"MAX_LENGTH_EXCEEDED\":\n * case \"MAX_RETRIES_EXCEEDED\":\n * // Handle specific error case\n * break;\n *\n * default:\n * // Handle other cases, including unknown errors\n * // or Fraci errors that are not handled above\n * break;\n * }\n * }\n * ```\n *\n * @see {@link FraciError} - The custom error class for the Fraci library\n * @see {@link FraciErrorCode} - The error codes for the Fraci library\n * @see {@link isFraciError} - Type guard to check if an error is a {@link FraciError}\n */\nexport function getFraciErrorCode(error: unknown): FraciErrorCode | undefined {\n return error instanceof FraciError ? error.code : undefined;\n}\n","export const INTEGER_ZERO = new Uint8Array([128, 0]);\n\nexport const INTEGER_MINUS_ONE = new Uint8Array([127, 255]);\n\n/**\n * Compares two Uint8Arrays.\n *\n * @param a - The first array\n * @param b - The second array\n * @returns A number indicating the comparison result\n * - Negative if a < b\n * - Zero if a == b\n * - Positive if a > b\n */\nexport function compare(a: Uint8Array, b: Uint8Array): number {\n const len = Math.min(a.length, b.length);\n let r = 0;\n for (let i = 0; !r && i < len; i++) {\n r = a[i] - b[i];\n }\n return r || a.length - b.length;\n}\n\n/**\n * Concatenates two Uint8Arrays.\n *\n * @param a - The first array\n * @param b - The second array\n * @returns The concatenated array\n */\nexport function concat(a: Uint8Array, b: Uint8Array): Uint8Array {\n const result = new Uint8Array(a.length + b.length);\n result.set(a);\n result.set(b, a.length);\n return result;\n}\n\n/**\n * Gets the signed length of the integer part from a binary fractional index.\n * This function extracts the length information encoded in the first byte of the index string.\n *\n * @param index - The fractional index binary\n * @returns The signed length of the integer part, or NaN if the first character is invalid\n */\nexport function getIntegerLengthSigned(index: Uint8Array): number {\n const [value] = index;\n return value - (value >= 128 ? 127 : 128);\n}\n\n/**\n * Gets the byte representing the length of the integer part.\n * Reverse operation of {@link getIntegerLengthSigned}.\n *\n * @param signedLength - The signed length of the integer part\n * @returns The byte representing the length of the integer part\n */\nexport function getIntegerLengthByte(signedLength: number): number {\n return signedLength + (signedLength < 0 ? 128 : 127);\n}\n\n/**\n * Checks if a binary fractional index represents the smallest possible integer.\n *\n * @param index - The fractional index binary to check\n * @returns A boolean indicating if the index represents the smallest integer\n */\nexport function isSmallestInteger(index: Uint8Array): boolean {\n return index.length === 129 && index.every((v) => v === 0);\n}\n\n/**\n * Splits a fractional index binary into its integer and fractional parts.\n * This function uses the length information encoded in the first character\n * to determine where to split the binary.\n *\n * @param index - The fractional index binary to split\n * @returns A tuple containing the integer and fractional parts, or undefined if the index is invalid\n */\nexport function splitParts(\n index: Uint8Array,\n): [integer: Uint8Array, fractional: Uint8Array] | undefined {\n // Get the encoded length from the first character and convert to absolute value\n // Add 1 because the length includes the length character itself\n const intLength = Math.abs(getIntegerLengthSigned(index)) + 1;\n\n // Validation: ensure the length is valid and the binary is long enough\n if (Number.isNaN(intLength) || index.length < intLength) {\n // Invalid length or binary too short\n return;\n }\n\n // Split the string into integer and fractional parts\n // The integer part includes the length character and the digits\n // The fractional part is everything after the integer part\n return [index.subarray(0, intLength), index.subarray(intLength)];\n}\n\n/**\n * Increments the integer part of a fractional index.\n * This function handles carrying and length changes when incrementing the integer.\n *\n * @param index - The fractional index binary whose integer part should be incremented\n * @returns\n * - A new binary with the incremented integer part\n * - null if the integer cannot be incremented (reached maximum value)\n * - undefined if the input is invalid\n */\nexport function incrementInteger(\n index: Uint8Array,\n): Uint8Array | null | undefined {\n if (!index.length) {\n return;\n }\n\n const intLengthSigned = getIntegerLengthSigned(index);\n\n // Extract the length character and the actual digits from the integer part\n const digits = index.slice(0, Math.abs(intLengthSigned) + 1);\n\n // Try to increment the rightmost digit first, with carrying if needed\n // This is similar to adding 1 to a number in the custom base system\n for (let i = digits.length - 1; i >= 1; i--) {\n // Increment the digit and check for overflow\n // Note that Uint8Array wraps around on overflow, which is what we want\n if (digits[i]++ < 255) {\n // The digit is not 255 before increment, meaning no overflow will occur\n // This is the common case for most increments\n return digits;\n }\n\n // Overflow occurred - carry to the next digit to the left\n }\n\n // Special case: transitioning from negative to zero\n // This is like going from -1 to 0 in decimal, which requires special handling\n if (intLengthSigned === -1) {\n // The integer is -1. We need to return 0.\n // This requires changing the length encoding character to represent positive length\n return INTEGER_ZERO.slice();\n }\n\n // If we get here, we've carried through all digits (like 999 + 1 = 1000)\n // We need to increase the length of the integer representation\n const newLenSigned = intLengthSigned + 1;\n if (newLenSigned > 128) {\n // Reached the limit of representable integers\n // This is an edge case where we can't represent a larger integer\n return null;\n }\n\n // Create a new integer with increased length (all digits are smallest digit)\n const newBinary = new Uint8Array(Math.abs(newLenSigned) + 1);\n newBinary[0] = getIntegerLengthByte(newLenSigned);\n return newBinary;\n}\n\n/**\n * Decrements the integer part of a fractional index.\n * This function handles borrowing and length changes when decrementing the integer.\n *\n * @param index - The fractional index string whose integer part should be decremented\n * @returns\n * - A new binary with the decremented integer part\n * - null if the integer cannot be decremented (reached minimum value)\n * - undefined if the input is invalid\n */\nexport function decrementInteger(\n index: Uint8Array,\n): Uint8Array | null | undefined {\n const intLengthSigned = getIntegerLengthSigned(index);\n if (Number.isNaN(intLengthSigned)) {\n return;\n }\n\n // Extract the length character and the actual digits from the integer part\n const digits = index.slice(0, Math.abs(intLengthSigned) + 1);\n\n // Try to decrement the rightmost digit first, with borrowing if needed\n // This is similar to subtracting 1 from a number in the custom base system\n for (let i = digits.length - 1; i >= 1; i--) {\n // Decrement the digit and check for underflow\n // Note that Uint8Array wraps around on underflow, which is what we want\n if (digits[i]--) {\n // The digit is non-zero before decrement, meaning no underflow will occur\n return digits;\n }\n\n // Underflow occurred - borrow from the next digit to the left\n }\n\n // Special case: transitioning from zero to negative integers\n // This is like going from 0 to -1 in decimal, which requires special handling\n if (intLengthSigned === 1) {\n // The integer is 0. We need to return -1.\n // This requires changing the length encoding character to represent negative length\n return INTEGER_MINUS_ONE.slice();\n }\n\n // If we get here, we've borrowed through all digits (like 1000 - 1 = 999)\n // We need to decrease the length of the integer representation\n const newLenSigned = intLengthSigned - 1;\n if (newLenSigned < -128) {\n // Reached the limit of representable integers\n // This is an edge case where we can't represent a smaller integer\n return null;\n }\n\n // Create a new integer with decreased length (all digits are largest digit)\n const newBinary = new Uint8Array(Math.abs(newLenSigned) + 1).fill(255);\n newBinary[0] = getIntegerLengthByte(newLenSigned);\n return newBinary;\n}\n\n/**\n * Calculates the midpoint between two fractional parts.\n * This function recursively finds a string that sorts between two fractional parts.\n * It handles various cases including when one of the inputs is null.\n *\n * @param a - The lower bound fractional part, or empty binary if there is no lower bound\n * @param b - The upper bound fractional part, or null if there is no upper bound\n * @returns A binary that sorts between a and b, or undefined if inputs are invalid\n */\nexport function getMidpointFractional(\n a: Uint8Array,\n b: Uint8Array | null,\n): Uint8Array | undefined {\n if (b != null && compare(a, b) >= 0) {\n // Precondition failed.\n return;\n }\n\n // Optimization: If a and b share a common prefix, preserve it\n if (b) {\n // Find the first position where a and b differ\n const prefixLength = b.findIndex((value, i) => value !== (a[i] ?? 0));\n\n // If they share a prefix, keep it and recursively find midpoint of the differing parts\n if (prefixLength > 0) {\n const suffix = getMidpointFractional(\n a.subarray(prefixLength),\n b.subarray(prefixLength),\n );\n if (!suffix) {\n return;\n }\n\n return concat(b.subarray(0, prefixLength), suffix);\n }\n }\n\n // At this point, we're handling the first differing digits\n const aDigit = a[0] ?? 0;\n const bDigit = b ? b[0] : 256;\n if (bDigit == null) {\n return;\n }\n\n // Case 1: Non-consecutive digits - we can simply use their average\n if (aDigit + 1 !== bDigit) {\n const mid = (aDigit + bDigit) >> 1; // Fast integer division by 2\n return new Uint8Array([mid]);\n }\n\n // Case 2: Consecutive digits with b having two or more digits\n if (b && b.length > 1) {\n // We can just use b's first digit (which is one more than a's first digit)\n return new Uint8Array([b[0]]);\n }\n\n // Case 3: Consecutive digits with b having length 1 or null\n // This is the most complex case requiring recursive construction\n // Example: midpoint('49', '5') becomes '495'\n // We take a's first digit, then recursively find midpoint of a's remainder and null\n const suffix = getMidpointFractional(a.subarray(1), null);\n if (!suffix) {\n return;\n }\n\n const result = new Uint8Array(1 + suffix.length);\n result[0] = aDigit;\n result.set(suffix, 1);\n return result;\n}\n","/**\n * Gets the signed length of the integer part from a fractional index.\n * This function extracts the length information encoded in the first character\n * of the index string.\n *\n * @param index - The fractional index string\n * @param lenBaseReverse - Map of length encoding characters to their numeric values\n * @returns The signed length of the integer part, or undefined if the first character is invalid\n */\nexport function getIntegerLengthSigned(\n index: string,\n lenBaseReverse: ReadonlyMap<string, number>,\n): number | undefined {\n return lenBaseReverse.get(index[0]);\n}\n\n/**\n * Splits a fractional index string into its integer and fractional parts.\n * This function uses the length information encoded in the first character\n * to determine where to split the string.\n *\n * @param index - The fractional index string to split\n * @param lenBaseReverse - Map of length encoding characters to their numeric values\n * @returns A tuple containing the integer and fractional parts, or undefined if the index is invalid\n */\nexport function splitParts(\n index: string,\n lenBaseReverse: ReadonlyMap<string, number>,\n): [integer: string, fractional: string] | undefined {\n // Get the encoded length from the first character and convert to absolute value\n // Add 1 because the length includes the length character itself\n const intLength =\n Math.abs(getIntegerLengthSigned(index, lenBaseReverse) ?? 0) + 1;\n\n // Validation: ensure the length is valid and the string is long enough\n if (intLength < 2 || index.length < intLength) {\n // Invalid length or string too short\n return;\n }\n\n // Split the string into integer and fractional parts\n // The integer part includes the length character and the digits\n // The fractional part is everything after the integer part\n return [index.slice(0, intLength), index.slice(intLength)];\n}\n\n/**\n * Generates a string representation of the integer zero.\n * This function creates a string that represents the integer zero\n * in the specified digit base and length encoding.\n *\n * @param digBaseForward - Array mapping digit positions to characters\n * @param lenBaseForward - Map of length values to their encoding characters\n * @returns A string representation of the integer zero\n */\nexport function getIntegerZero(\n digBaseForward: readonly string[],\n lenBaseForward: ReadonlyMap<number, string>,\n): string {\n return lenBaseForward.get(1)! + digBaseForward[0];\n}\n\n/**\n * Generates a string representation of the smallest possible integer.\n * This function finds the smallest length value in the length encoding map\n * and creates a string representing the smallest possible integer.\n *\n * @param digBaseForward - Array mapping digit positions to characters\n * @param lenBaseForward - Map of length values to their encoding characters\n * @returns A string representation of the smallest possible integer\n */\nexport function getSmallestInteger(\n digBaseForward: readonly string[],\n lenBaseForward: ReadonlyMap<number, string>,\n): string {\n // Find the smallest length value in the length encoding map\n // This will be the most negative value, representing the smallest possible integer\n const minKey = Math.min(...Array.from(lenBaseForward.keys()));\n\n // Get the character that encodes this smallest length\n const minLenChar = lenBaseForward.get(minKey)!;\n\n // Create a string with the length character followed by the smallest digit repeated\n // The number of repetitions is the absolute value of the length\n return `${minLenChar}${digBaseForward[0].repeat(Math.abs(minKey))}`;\n}\n\n/**\n * Increments the integer part of a fractional index.\n * This function handles carrying and length changes when incrementing the integer.\n *\n * @param index - The fractional index string whose integer part should be incremented\n * @param digBaseForward - Array mapping digit positions to characters\n * @param digBaseReverse - Map of digit characters to their numeric values\n * @param lenBaseForward - Map of length values to their encoding characters\n * @param lenBaseReverse - Map of length encoding characters to their numeric values\n * @returns\n * - A new string with the incremented integer part\n * - null if the integer cannot be incremented (reached maximum value)\n * - undefined if the input is invalid\n */\nexport function incrementInteger(\n index: string,\n digBaseForward: readonly string[],\n digBaseReverse: ReadonlyMap<string, number>,\n lenBaseForward: ReadonlyMap<number, string>,\n lenBaseReverse: ReadonlyMap<string, number>,\n): string | null | undefined {\n const intLengthSigned = getIntegerLengthSigned(index, lenBaseReverse);\n if (!intLengthSigned) {\n return;\n }\n\n const smallestDigit = digBaseForward[0];\n\n // Extract the length character and the actual digits from the integer part\n const [lenChar, ...digits] = index.slice(0, Math.abs(intLengthSigned) + 1);\n\n // Try to increment the rightmost digit first, with carrying if needed\n // This is similar to adding 1 to a number in the custom base system\n for (let i = digits.length - 1; i >= 0; i--) {\n const value = digBaseReverse.get(digits[i]);\n if (value == null) {\n // Invalid digit\n return;\n }\n\n if (value < digBaseForward.length - 1) {\n // No carrying needed - we can increment this digit and return\n // This is the common case for most increments\n digits[i] = digBaseForward[value + 1];\n return `${lenChar}${digits.join(\"\")}`;\n }\n\n // This digit is at max value (9 in decimal), set to smallest (0) and continue carrying\n // We need to carry to the next digit to the left\n digits[i] = smallestDigit;\n }\n\n // Special case: transitioning from negative integers to zero\n // This is like going from -1 to 0 in decimal, which requires special handling\n if (intLengthSigned === -1) {\n // The integer is -1. We need to return 0.\n // This requires changing the length encoding character\n return `${lenBaseForward.get(1)!}${smallestDigit}`;\n }\n\n // If we get here, we've carried through all digits (like 999 + 1 = 1000)\n // We need to increase the length of the integer representation\n const newLenSigned = intLengthSigned + 1;\n const newLenChar = lenBaseForward.get(newLenSigned);\n if (!newLenChar) {\n // Reached the limit of representable integers\n // This is an edge case where we can't represent a larger integer\n return null;\n }\n\n // Create a new integer with increased length (all digits are smallest digit)\n // For example, in decimal: 999 + 1 = 1000 (all zeros with a 1 at the start)\n // But in our system, we encode the length separately\n return `${newLenChar}${smallestDigit.repeat(Math.abs(newLenSigned))}`;\n}\n\n/**\n * Decrements the integer part of a fractional index.\n * This function handles borrowing and length changes when decrementing the integer.\n *\n * @param index - The fractional index string whose integer part should be decremented\n * @param digBaseForward - Array mapping digit positions to characters\n * @param digBaseReverse - Map of digit characters to their numeric values\n * @param lenBaseForward - Map of length values to their encoding characters\n * @param lenBaseReverse - Map of length encoding characters to their numeric values\n * @returns\n * - A new string with the decremented integer part\n * - null if the integer cannot be decremented (reached minimum value)\n * - undefined if the input is invalid\n */\nexport function decrementInteger(\n index: string,\n digBaseForward: readonly string[],\n digBaseReverse: ReadonlyMap<string, number>,\n lenBaseForward: ReadonlyMap<number, string>,\n lenBaseReverse: ReadonlyMap<string, number>,\n): string | null | undefined {\n const intLengthSigned = getIntegerLengthSigned(index, lenBaseReverse);\n if (!intLengthSigned) {\n return;\n }\n\n const largestDigit = digBaseForward[digBaseForward.length - 1];\n\n // Extract the length character and the actual digits from the integer part\n const [lenChar, ...digits] = index.slice(0, Math.abs(intLengthSigned) + 1);\n\n // Try to decrement the rightmost digit first, with borrowing if needed\n // This is similar to subtracting 1 from a number in the custom base system\n for (let i = digits.length - 1; i >= 0; i--) {\n const value = digBaseReverse.get(digits[i]);\n if (value == null) {\n // Invalid digit\n return;\n }\n\n if (value > 0) {\n // No borrowing needed - we can decrement this digit and return\n // This is the common case for most decrements\n digits[i] = digBaseForward[value - 1];\n return `${lenChar}${digits.join(\"\")}`;\n }\n\n // This digit is at min value (0 in decimal), set to largest (9) and continue borrowing\n // We need to borrow from the next digit to the left\n digits[i] = largestDigit;\n }\n\n // Special case: transitioning from zero to negative integers\n // This is like going from 0 to -1 in decimal, which requires special handling\n if (intLengthSigned === 1) {\n // The integer is 0. We need to return -1.\n // This requires changing the length encoding character to represent negative length\n return `${lenBaseForward.get(-1)!}${largestDigit}`;\n }\n\n // If we get here, we've borrowed through all digits (like 1000 - 1 = 999)\n // We need to decrease the length of the integer representation\n const newLenSigned = intLengthSigned - 1;\n const newLenChar = lenBaseForward.get(newLenSigned);\n if (!newLenChar) {\n // Reached the limit of representable integers\n // This is an edge case where we can't represent a smaller integer\n return null;\n }\n\n // Create a new integer with decreased length (all digits are largest digit)\n // For example, in decimal: 1000 - 1 = 999 (all nines)\n // But in our system, we encode the length separately\n return `${newLenChar}${largestDigit.repeat(Math.abs(newLenSigned))}`;\n}\n\n/**\n * Calculates the midpoint between two fractional parts.\n * This function recursively finds a string that sorts between two fractional parts.\n * It handles various cases including when one of the inputs is null.\n *\n * @param a - The lower bound fractional part, or empty string if there is no lower bound\n * @param b - The upper bound fractional part, or null if there is no upper bound\n * @param digBaseForward - Array mapping digit positions to characters\n * @param digBaseReverse - Map of digit characters to their numeric values\n * @returns A string that sorts between a and b, or undefined if inputs are invalid\n */\nexport function getMidpointFractional(\n a: string,\n b: string | null,\n digBaseForward: readonly string[],\n digBaseReverse: ReadonlyMap<string, number>,\n): string | undefined {\n if (b != null && b <= a) {\n // Precondition failed.\n return;\n }\n\n // Optimization: If a and b share a common prefix, preserve it\n if (b) {\n // Pad a with zeros to match b's length for comparison\n const aPadded = a.padEnd(b.length, digBaseForward[0]);\n\n // Find the first position where a and b differ\n const prefixLength = Array.prototype.findIndex.call(\n b,\n (char, i) => char !== aPadded[i],\n );\n\n // If they share a prefix, keep it and recursively find midpoint of the differing parts\n if (prefixLength > 0) {\n return `${b.slice(0, prefixLength)}${getMidpointFractional(\n a.slice(prefixLength),\n b.slice(prefixLength),\n digBaseForward,\n digBaseReverse,\n )}`;\n }\n }\n\n // At this point, we're handling the first differing digits\n const aDigit = a ? digBaseReverse.get(a[0]) : 0;\n const bDigit = b ? digBaseReverse.get(b[0]) : digBaseForward.length;\n if (aDigit == null || bDigit == null) {\n // Invalid digit.\n return;\n }\n\n // Case 1: Non-consecutive digits - we can simply use their average\n if (aDigit + 1 !== bDigit) {\n const mid = (aDigit + bDigit) >> 1; // Fast integer division by 2\n return digBaseForward[mid];\n }\n\n // Case 2: Consecutive digits with b having two or more digits\n if (b && b.length > 1) {\n // We can just use b's first digit (which is one more than a's first digit)\n return b[0];\n }\n\n // Case 3: Consecutive digits with b having length 1 or null\n // This is the most complex case requiring recursive construction\n // Example: midpoint('49', '5') becomes '495'\n // We take a's first digit, then recursively find midpoint of a's remainder and null\n return `${digBaseForward[aDigit]}${getMidpointFractional(\n a.slice(1),\n null,\n digBaseForward,\n digBaseReverse,\n )}`;\n}\n","import {\n INTEGER_ZERO,\n compare,\n concat,\n decrementInteger,\n getMidpointFractional,\n incrementInteger,\n isSmallestInteger,\n splitParts,\n} from \"./decimal-binary.js\";\nimport { FraciError } from \"./errors.js\";\n\n/**\n * Converts a Node.js Buffer to a Uint8Array if necessary.\n * Our library is not compatible with Node.js Buffers due to [the difference of the `slice` method](https://nodejs.org/api/buffer.html#bufslicestart-end).\n *\n * @param value - The value to convert to a Uint8Array\n * @returns The original value as a Uint8Array, or null if the value is null\n */\nfunction forceUint8Array(value: Uint8Array | null): Uint8Array | null {\n return value?.constructor.name === \"Buffer\"\n ? new Uint8Array(value.buffer, value.byteOffset, value.length)\n : value;\n}\n\n/**\n * Validates if a binary is a valid fractional index.\n * A valid fractional index must:\n * - Not be empty or equal to the smallest integer\n * - Have a valid integer part with valid digits\n * - Not have trailing zeros in the fractional part\n * - Contain only valid digits in both integer and fractional parts\n *\n * @param index - The string to validate as a fractional index\n * @returns True if the string is a valid fractional index, false otherwise\n */\nexport function isValidFractionalIndex(index: Uint8Array): boolean {\n if (!index.length || isSmallestInteger(index)) {\n // The smallest integer is not a valid fractional index. It must have a fractional part.\n return false;\n }\n\n const parts = splitParts(index);\n if (!parts) {\n // Invalid integer length character or the integer part is too short.\n return false;\n }\n\n const [, fractional] = parts;\n if (fractional?.at(-1) === 0) {\n // Trailing zeros are not allowed in the fractional part.\n return false;\n }\n\n // All bytes in a Uint8Array are valid by definition (0-255),\n // so we don't need to check each byte like in the string version\n\n return true;\n}\n\n/**\n * Ensures a value is not undefined, throwing an error if it is.\n * This is a utility function used to handle unexpected undefined values\n * that should have been validated earlier in the code.\n *\n * @param value - The value to check\n * @returns The original value if it's not undefined\n * @throws {FraciError} Throws a {@link FraciError} when the value is undefined (internal error)\n *\n * @see {@link FraciError} - The custom error class for the Fraci library\n */\nfunction ensureNotUndefined<T>(value: T | undefined): T {\n if (value === undefined) {\n // This should not happen as we should have validated the value before.\n if (globalThis.__DEV__) {\n console.error(\n \"FraciError: [INTERNAL_ERROR] Unexpected undefined. Please file an issue to report this error.\",\n );\n }\n\n throw new FraciError(\"INTERNAL_ERROR\", \"Unexpected undefined\");\n }\n return value;\n}\n\n/**\n * Generates a key between two existing keys without validation.\n * This internal function handles the core algorithm for creating a fractional index\n * between two existing indices. It assumes inputs are valid and doesn't perform validation.\n *\n * The function handles several cases:\n * - When both a and b are null (first key)\n * - When only a is null (key before b)\n * - When only b is null (key after a)\n * - When both a and b are provided (key between a and b)\n *\n * @param a - The lower bound key, or null if there is no lower bound\n * @param b - The upper bound key, or null if there is no upper bound\n * @returns A new key that sorts between a and b\n */\nfunction generateKeyBetweenUnsafe(\n a: Uint8Array | null,\n b: Uint8Array | null,\n): Uint8Array {\n // Strategy: Handle different cases based on bounds\n if (!a) {\n if (!b) {\n // Case: First key (no bounds)\n return INTEGER_ZERO.slice();\n }\n\n // Case: Key before first key\n const [bInt, bFrac] = ensureNotUndefined(splitParts(b));\n if (isSmallestInteger(bInt)) {\n // Edge case: b is already at the smallest possible integer\n // We can't decrement the integer part further, so we need to use a fractional part\n // that sorts before b's fractional part\n return concat(\n bInt,\n ensureNotUndefined(getMidpointFractional(new Uint8Array(), bFrac)),\n );\n }\n\n if (bFrac.length) {\n // Optimization: If b has a fractional part, we can use just its integer part\n // This creates a shorter key that still sorts correctly before b\n return bInt.slice();\n }\n\n // Standard case: Decrement the integer part of b\n const decremented = ensureNotUndefined(\n decrementInteger(bInt),\n ) as Uint8Array;\n if (!isSmallestInteger(decremented)) {\n return decremented;\n }\n\n // Edge case: If we hit the smallest integer, add the largest digit as fractional part\n // This ensures we still have a valid key that sorts before b\n const result = new Uint8Array(decremented.length + 1);\n result.set(decremented);\n result[decremented.length] = 255;\n return result;\n }\n\n if (!b) {\n // Case: Key after last key\n const aParts = ensureNotUndefined(splitParts(a));\n const [aInt, aFrac] = aParts;\n\n // Try to increment the integer part first (most efficient)\n const incremented = ensureNotUndefined(incrementInteger(aInt));\n if (incremented) {\n // If we can increment the integer part, use that result\n // This creates a shorter key than using fractional parts\n return incremented;\n }\n\n // Edge case: We've reached the largest possible integer representation\n // We need to use the fractional part method instead\n // Calculate a fractional part that sorts after a's fractional part\n return concat(aInt, ensureNotUndefined(getMidpointFractional(aFrac, null)));\n }\n\n // Case: Key between two existing keys\n const [aInt, aFrac] = ensureNotUndefined(splitParts(a));\n const [bInt, bFrac] = ensureNotUndefined(splitParts(b));\n\n // If both keys have the same integer part, we need to find a fractional part between them\n if (!compare(aInt, bInt)) {\n // Calculate the midpoint between the two fractional parts\n return concat(\n aInt,\n ensureNotUndefined(getMidpointFractional(aFrac, bFrac)),\n );\n }\n\n // Try to increment a's integer part\n const cInt = ensureNotUndefined(incrementInteger(aInt));\n\n // Two possible outcomes:\n return cInt && compare(cInt, bInt)\n ? // 1. If incrementing a's integer doesn't reach b's integer,\n // we can use the incremented value (shorter key)\n cInt\n : // 2. If incrementing a's integer equals b's integer or we can't increment,\n // we need to use a's integer with a fractional part that sorts after a's fractional part\n concat(aInt, ensureNotUndefined(getMidpointFractional(aFrac, null)));\n}\n\n/**\n * Generates a key between two existing keys with validation.\n * This function validates the input keys before generating a new key between them.\n * It returns undefined if either key is invalid or if b is less than or equal to a.\n *\n * @param a - The lower bound key, or null if there is no lower bound\n * @param b - The upper bound key, or null if there is no upper bound\n * @returns A new key that sorts between a and b, or undefined if inputs are invalid\n */\nexport function generateKeyBetween(\n a: Uint8Array | null,\n b: Uint8Array | null,\n): Uint8Array | undefined {\n return (a != null && !isValidFractionalIndex(a)) ||\n (b != null && !isValidFractionalIndex(b)) ||\n (a != null && b != null && compare(a, b) >= 0)\n ? undefined\n : generateKeyBetweenUnsafe(forceUint8Array(a), forceUint8Array(b));\n}\n\n/**\n * Generates multiple keys between two existing keys without validation.\n * This internal function creates n evenly distributed keys between a and b.\n * It uses a recursive divide-and-conquer approach for more even distribution.\n *\n * The function handles several cases:\n * - When n < 1 (returns empty array)\n * - When n = 1 (returns a single key between a and b)\n * - When b is null (generates n keys after a)\n * - When a is null (generates n keys before b)\n * - When both a and b are provided (generates n keys between a and b)\n *\n * @param a - The lower bound key, or null if there is no lower bound\n * @param b - The upper bound key, or null if there is no upper bound\n * @param n - Number of keys to generate\n * @returns An array of n new keys that sort between a and b\n */\nfunction generateNKeysBetweenUnsafe(\n a: Uint8Array | null,\n b: Uint8Array | null,\n n: number,\n): Uint8Array[] {\n if (n < 1) {\n return [];\n }\n\n if (n === 1) {\n return [generateKeyBetweenUnsafe(a, b)];\n }\n\n // Special case: Generate n keys after a (no upper bound)\n if (b == null) {\n let c = a;\n // Sequential generation - each new key is after the previous one\n return Array.from(\n { length: n },\n () => (c = generateKeyBetweenUnsafe(c, b)),\n );\n }\n\n // Special case: Generate n keys before b (no lower bound)\n if (a == null) {\n let c = b;\n // Sequential generation in reverse - each new key is before the previous one\n // Then reverse the array to get ascending order\n return Array.from(\n { length: n },\n () => (c = generateKeyBetweenUnsafe(a, c)),\n ).reverse();\n }\n\n // Divide-and-conquer approach for better distribution of keys\n const mid = n >> 1; // Fast integer division by 2\n\n // Find a midpoint key between a and b\n const c = generateKeyBetweenUnsafe(a, b);\n\n // Recursively generate keys in both halves and combine them\n // This creates a more balanced distribution than sequential generation\n return [\n ...generateNKeysBetweenUnsafe(a, c, mid),\n c,\n ...generateNKeysBetweenUnsafe(c, b, n - mid - 1),\n ];\n}\n\n/**\n * Generates multiple keys between two existing keys with validation.\n * This function validates the input keys before generating new keys between them.\n * It returns undefined if either key is invalid or if b is less than or equal to a.\n *\n * @param a - The lower bound key, or null if there is no lower bound\n * @param b - The upper bound key, or null if there is no upper bound\n * @param n - Number of keys to generate\n * @returns An array of n new keys that sort between a and b, or undefined if inputs are invalid\n */\nexport function generateNKeysBetween(\n a: Uint8Array | null,\n b: Uint8Array | null,\n n: number,\n): Uint8Array[] | undefined {\n return (a != null && !isValidFractionalIndex(a)) ||\n (b != null && !isValidFractionalIndex(b)) ||\n (a != null && b != null && compare(a, b) >= 0)\n ? undefined\n : generateNKeysBetweenUnsafe(forceUint8Array(a), forceUint8Array(b), n);\n}\n\n/**\n * Generates a suffix to avoid conflicts between fractional indices.\n * This function creates a unique suffix based on the count value,\n * converting it to the specified digit base. The suffix is used to\n * ensure uniqueness when multiple indices need to be generated between\n * the same bounds.\n *\n * @param count - The count value to convert to a suffix\n * @returns A binary suffix in the specified digit base\n */\nexport function avoidConflictSuffix(count: number): Uint8Array {\n const additionalFrac: number[] = [];\n\n // Convert a number to a binary representation\n // This works like converting to a different number base,\n // but we write the digits in reverse order.\n //\n // For example, in binary:\n // - The number 3 would become [3]\n // - The number 256 would become [0, 1]\n // - The number 1234 would become bytes representing 1234 in little-endian\n //\n // We do this reversed ordering to ensure the array doesn't end with zeros,\n // which we need to avoid in fractional indices.\n while (count > 0) {\n // Add the byte for the current remainder\n additionalFrac.push(count & 255);\n // Integer division to get the next byte\n count >>= 8;\n }\n\n // The result is a unique suffix for each count value\n return new Uint8Array(additionalFrac);\n}\n","import {\n decrementInteger,\n getIntegerZero,\n getMidpointFractional,\n incrementInteger,\n splitParts,\n} from \"./decimal-string.js\";\nimport { FraciError } from \"./errors.js\";\n\n/**\n * Validates if a string is a valid fractional index.\n * A valid fractional index must:\n * - Not be empty or equal to the smallest integer\n * - Have a valid integer part with valid digits\n * - Not have trailing zeros in the fractional part\n * - Contain only valid digits in both integer and fractional parts\n *\n * @param index - The string to validate as a fractional index\n * @param digBaseForward - Array mapping digit positions to characters\n * @param digBaseReverse - Map of digit characters to their numeric values\n * @param lenBaseReverse - Map of length encoding characters to their numeric values\n * @param smallestInteger - The smallest possible integer representation\n * @returns True if the string is a valid fractional index, false otherwise\n */\nexport function isValidFractionalIndex(\n index: string,\n digBaseForward: readonly string[],\n digBaseReverse: ReadonlyMap<string, number>,\n lenBaseReverse: ReadonlyMap<string, number>,\n smallestInteger: string,\n): boolean {\n if (!index || index === smallestInteger) {\n // The smallest integer is not a valid fractional index. It must have a fractional part.\n return false;\n }\n\n const parts = splitParts(index, lenBaseReverse);\n if (!parts) {\n // Invalid integer length character or the integer part is too short.\n return false;\n }\n\n const [integer, fractional] = parts;\n if (fractional.endsWith(digBaseForward[0])) {\n // Trailing zeros are not allowed in the fractional part.\n return false;\n }\n\n for (const char of integer.slice(1)) {\n if (!digBaseReverse.has(char)) {\n return false;\n }\n }\n\n for (const char of fractional) {\n if (!digBaseReverse.has(char)) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Ensures a value is not undefined, throwing an error if it is.\n * This is a utility function used to handle unexpected undefined values\n * that should have been validated earlier in the code.\n *\n * @param value - The value to check\n * @returns The original value if it's not undefined\n * @throws {FraciError} Throws a {@link FraciError} when the value is undefined (internal error)\n *\n * @see {@link FraciError} - The custom error class for the Fraci library\n */\nfunction ensureNotUndefined<T>(value: T | undefined): T {\n if (value === undefined) {\n // This should not happen as we should have validated the value before.\n if (globalThis.__DEV__) {\n console.error(\n \"FraciError: [INTERNAL_ERROR] Unexpected undefined. Please file an issue to report this error.\",\n );\n }\n\n throw new FraciError(\"INTERNAL_ERROR\", \"Unexpected undefined\");\n }\n return value;\n}\n\n/**\n * Generates a key between two existing keys without validation.\n * This internal function handles the core algorithm for creating a fractional index\n * between two existing indices. It assumes inputs are valid and doesn't perform validation.\n *\n * The function handles several cases:\n * - When both a and b are null (first key)\n * - When only a is null (key before b)\n * - When only b is null (key after a)\n * - When both a and b are provided (key between a and b)\n *\n * @param a - The lower bound key, or null if there is no lower bound\n * @param b - The upper bound key, or null if there is no upper bound\n * @param digBaseForward - Array mapping digit positions to characters\n * @param digBaseReverse - Map of digit characters to their numeric values\n * @param lenBaseForward - Map of length values to their encoding characters\n * @param lenBaseReverse - Map of length encoding characters to their numeric values\n * @param smallestInteger - The smallest possible integer representation\n * @returns A new key that sorts between a and b\n */\nfunction generateKeyBetweenUnsafe(\n a: string | null,\n b: string | null,\n digBaseForward: readonly string[],\n digBaseReverse: ReadonlyMap<string, number>,\n lenBaseForward: ReadonlyMap<number, string>,\n lenBaseReverse: ReadonlyMap<string, number>,\n smallestInteger: string,\n): string {\n // Strategy: Handle different cases based on bounds\n if (!a) {\n if (!b) {\n // Case: First key (no bounds)\n return getIntegerZero(digBaseForward, lenBaseForward);\n }\n\n // Case: Key before first key\n const [bInt, bFrac] = ensureNotUndefined(splitParts(b, lenBaseReverse));\n if (bInt === smallestInteger) {\n // Edge case: b is already at the smallest possible integer\n // We can't decrement the integer part further, so we need to use a fractional part\n // that sorts before b's fractional part\n return `${bInt}${ensureNotUndefined(\n getMidpointFractional(\"\", bFrac, digBaseForward, digBaseReverse),\n )}`;\n }\n\n if (bFrac) {\n // Optimization: If b has a fractional part, we can use just its integer part\n // This creates a shorter key that still sorts correctly before b\n return bInt;\n }\n\n // Standard case: Decrement the integer part of b\n const decremented = ensureNotUndefined(\n decrementInteger(\n bInt,\n digBaseForward,\n digBaseReverse,\n lenBaseForward,\n lenBaseReverse,\n ),\n ) as string;\n\n // Edge case: If we hit the smallest integer, add the largest digit as fractional part\n // This ensures we still have a valid key that sorts before b\n return decremented === smallestInteger\n ? `${decremented}${digBaseForward[digBaseForward.length - 1]}`\n : decremented;\n }\n\n if (!b) {\n // Case: Key after last key\n const aParts = ensureNotUndefined(splitParts(a, lenBaseReverse));\n const [aInt, aFrac] = aParts;\n\n // Try to increment the integer part first (most efficient)\n const incremented = ensureNotUndefined(\n incrementInteger(\n aInt,\n digBaseForward,\n digBaseReverse,\n lenBaseForward,\n lenBaseReverse,\n ),\n );\n\n if (incremented !== null) {\n // If we can increment the integer part, use that result\n // This creates a shorter key than using fractional parts\n return incremented;\n }\n\n // Edge case: We've reached the largest possible integer representation\n // We need to use the fractional part method instead\n // Calculate a fractional part that sorts after a's fractional part\n return `${aInt}${ensureNotUndefined(\n getMidpointFractional(aFrac, null, digBaseForward, digBaseReverse),\n )}`;\n }\n\n // Case: Key between two existing keys\n const aParts = ensureNotUndefined(splitParts(a, lenBaseReverse));\n const bParts = ensureNotUndefined(splitParts(b, lenBaseReverse));\n const [aInt, aFrac] = aParts;\n const [bInt, bFrac] = bParts;\n\n // If both keys have the same integer part, we need to find a fractional part between them\n if (aInt === bInt) {\n // Calculate the midpoint between the two fractional parts\n return `${aInt}${ensureNotUndefined(\n getMidpointFractional(aFrac, bFrac, digBaseForward, digBaseReverse),\n )}`;\n }\n\n // Try to increment a's integer part\n const cInt = ensureNotUndefined(\n incrementInteger(\n aInt,\n digBaseForward,\n digBaseReverse,\n lenBaseForward,\n lenBaseReverse,\n ),\n );\n\n // Two possible outcomes:\n return cInt !== null && cInt !== bInt\n ? // 1. If incrementing a's integer doesn't reach b's integer,\n // we can use the incremented value (shorter key)\n cInt\n : // 2. If incrementing a's integer equals b's integer or we can't increment,\n // we need to use a's integer with a fractional part that sorts after a's fractional part\n `${aInt}${ensureNotUndefined(\n getMidpointFractional(aFrac, null, digBaseForward, digBaseReverse),\n )}`;\n}\n\n/**\n * Generates a key between two existing keys with validation.\n * This function validates the input keys before generating a new key between them.\n * It returns undefined if either key is invalid or if b is less than or equal to a.\n *\n * @param a - The lower bound key, or null if there is no lower bound\n * @param b - The upper bound key, or null if there is no upper bound\n * @param digBaseForward - Array mapping digit positions to characters\n * @param digBaseReverse - Map of digit characters to their numeric values\n * @param lenBaseForward - Map of length values to their encoding characters\n * @param lenBaseReverse - Map of length encoding characters to their numeric values\n * @param smallestIn