fraci
Version:
Fractional indexing that's robust, performant, and secure, with first-class support for Drizzle ORM and Prisma ORM.
1 lines • 140 kB
Source Map (JSON)
{"version":3,"sources":["../src/prisma.ts","../src/prisma/extension.ts","../src/lib/decimal-binary.ts","../src/lib/decimal-string.ts","../src/lib/errors.ts","../src/lib/fractional-indexing-binary.ts","../src/lib/fractional-indexing-string.ts","../src/lib/utils.ts","../src/factory.ts","../src/prisma/common.ts","../src/prisma/constants.ts","../src/prisma/schema.ts"],"sourcesContent":["/**\n * The Prisma ORM integration for the fraci library.\n *\n * @module fraci/prisma\n */\n\nexport * from \"./prisma/index.js\";\n","import { Prisma } from \"@prisma/client/extension.js\";\nimport {\n createFraciCache,\n DEFAULT_MAX_LENGTH,\n DEFAULT_MAX_RETRIES,\n fraciBinary,\n fraciString,\n type AnyFraci,\n type Fraci,\n} from \"../factory.js\";\nimport { FraciError } from \"../lib/errors.js\";\nimport type { AnyFractionalIndex, FractionalIndex } from \"../lib/types.js\";\nimport type { FraciOf } from \"../types.js\";\nimport {\n isIndexConflictError,\n type PrismaClientConflictError,\n} from \"./common.js\";\nimport { EXTENSION_NAME } from \"./constants.js\";\nimport type {\n AllModelFieldName,\n BinaryModelFieldName,\n ModelKey,\n ModelScalarPayload,\n QueryArgs,\n StringModelFieldName,\n} from \"./prisma-types.js\";\nimport type { PrismaFraciFieldOptions, PrismaFraciOptions } from \"./schema.js\";\n\ntype AnyPrismaClient = any;\n\n/**\n * A brand for Prisma models and fields.\n *\n * @template Model - The model name\n * @template Field - The field name\n */\ntype PrismaBrand<Model extends string, Field extends string> = {\n readonly __prisma__: { model: Model; field: Field };\n};\n\n/**\n * A tuple of two fractional indices, used for generating a new index between them.\n *\n * @template FI - The fractional index type\n */\ntype Indices<FI extends AnyFractionalIndex> = [a: FI | null, b: FI | null];\n\n/**\n * Type representing the enhanced fractional indexing utility for Prisma ORM.\n * This type extends the base fractional indexing utility with additional methods for retrieving indices.\n *\n * This is an internal type used to define the methods for the Prisma extension.\n *\n * @template Client - The Prisma client type\n * @template Model - The model name\n * @template Where - The type of the required fields for the `where` argument of the `findMany` method\n * @template FI - The fractional index type\n *\n * @see {@link Fraci} - The base fractional indexing utility type\n */\ntype FraciForPrismaInternal<\n Client,\n Model extends ModelKey<Client>,\n Where,\n FI extends AnyFractionalIndex,\n> = FraciOf<FI> & {\n /**\n * Checks if the error is a conflict error for the fractional index.\n *\n * @param error - The error to check.\n * @returns `true` if the error is a conflict error for the fractional index, or `false` otherwise.\n */\n isIndexConflictError(error: unknown): error is PrismaClientConflictError;\n /**\n * Retrieves the existing indices to generate a new fractional index for the item after the specified item.\n *\n * @param where - The `where` argument of the `findMany` method. Must have the fields specified in the {@link PrismaFraciFieldOptions.group group} property of the field options.\n * @param cursor - The cursor (selector) of the item. If `null`, this method returns the indices to generate a new fractional index for the first item.\n * @param client - The Prisma client to use. Should be specified when using transactions. If not specified, the client used to create the extension is used.\n * @returns The indices to generate a new fractional index for the item after the specified item, or `undefined` if the item specified by the `cursor` does not exist.\n */\n indicesForAfter: {\n (\n where: Where & QueryArgs<Client, Model>[\"where\"],\n cursor: QueryArgs<Client, Model>[\"cursor\"],\n client?: Client,\n ): Promise<Indices<FI> | undefined>;\n (\n where: Where & QueryArgs<Client, Model>[\"where\"],\n cursor: null,\n client?: Client,\n ): Promise<Indices<FI>>;\n };\n /**\n * Retrieves the existing indices to generate a new fractional index for the item before the specified item.\n *\n * @param where - The `where` argument of the `findMany` method. Must have the fields specified in the {@link PrismaFraciFieldOptions.group group} property of the field options.\n * @param cursor - The cursor (selector) of the item. If `null`, this method returns the indices to generate a new fractional index for the last item.\n * @param client - The Prisma client to use. Should be specified when using transactions. If not specified, the client used to create the extension is used.\n * @returns The indices to generate a new fractional index for the item before the specified item, or `undefined` if the item specified by the `cursor` does not exist.\n */\n indicesForBefore: {\n (\n where: Where & QueryArgs<Client>[\"where\"],\n cursor: QueryArgs<Client>[\"cursor\"],\n client?: Client,\n ): Promise<Indices<FI> | undefined>;\n (\n where: Where & QueryArgs<Client>[\"where\"],\n cursor: null,\n client?: Client,\n ): Promise<Indices<FI>>;\n };\n /**\n * Retrieves the existing indices to generate a new fractional index for the first item.\n * Equivalent to {@link FraciForPrismaInternal.indicesForAfter `indicesForAfter(where, null, client)`}.\n *\n * @param where - The `where` argument of the `findMany` method. Must have the fields specified in the {@link PrismaFraciOptions.group group} property of the field options.\n * @param client - The Prisma client to use. Should be specified when using transactions. If not specified, the client used to create the extension is used.\n * @returns The indices to generate a new fractional index for the first item.\n */\n indicesForFirst(\n where: Where & QueryArgs<Client, Model>[\"where\"],\n client?: Client,\n ): Promise<Indices<FI>>;\n /**\n * Retrieves the existing indices to generate a new fractional index for the last item.\n * Equivalent to {@link FraciForPrismaInternal.indicesForBefore `indicesForBefore(where, null, client)`}.\n *\n * @param where - The `where` argument of the `findMany` method. Must have the fields specified in the {@link PrismaFraciOptions.group group} property of the field options.\n * @param client - The Prisma client to use. Should be specified when using transactions. If not specified, the client used to create the extension is used.\n * @returns The indices to generate a new fractional index for the last item.\n */\n indicesForLast(\n where: Where & QueryArgs<Client, Model>[\"where\"],\n client?: Client,\n ): Promise<Indices<FI>>;\n};\n\n/**\n * Type representing the enhanced fractional indexing utility for Prisma ORM.\n * This type extends the base fractional indexing utility with additional methods for retrieving indices.\n *\n * @template Client - The Prisma client type\n * @template Options - The field options type\n * @template Model - The model name\n * @template Field - The field name\n *\n * @see {@link Fraci} - The main fractional indexing utility type\n */\ntype FraciForPrismaByFieldOptions<\n Client,\n Options extends PrismaFraciFieldOptions,\n Model extends ModelKey<Client>,\n Field extends\n | BinaryModelFieldName<Client, Model>\n | StringModelFieldName<Client, Model>,\n> = FraciForPrismaInternal<\n Client,\n Model,\n Pick<\n ModelScalarPayload<Client, Model>,\n Extract<Options[\"group\"][number], AllModelFieldName<Client, Model>>\n >,\n Field extends BinaryModelFieldName<Client, Model>\n ? Options extends { readonly type: \"binary\" }\n ? FractionalIndex<Options, PrismaBrand<Model, Field>>\n : never\n : Options extends {\n readonly lengthBase: string;\n readonly digitBase: string;\n }\n ? FractionalIndex<\n {\n readonly type: \"string\";\n readonly lengthBase: Options[\"lengthBase\"];\n readonly digitBase: Options[\"digitBase\"];\n },\n PrismaBrand<Model, Field>\n >\n : never\n>;\n\n/**\n * Type representing the enhanced fractional indexing utility for Prisma ORM.\n * This type extends the base fractional indexing utility with additional methods for retrieving indices.\n *\n * @template Client - The Prisma client type\n * @template Options - The options type\n * @template QualifiedField - The qualified field name\n *\n * @see {@link Fraci} - The main fractional indexing utility type\n */\nexport type FraciForPrisma<\n Client,\n Options extends PrismaFraciOptions<Client>,\n QualifiedField extends keyof Options[\"fields\"],\n> = Options[\"fields\"][QualifiedField] extends PrismaFraciFieldOptions\n ? QualifiedField extends `${infer M extends ModelKey<Client>}.${infer F}`\n ? F extends BinaryModelFieldName<Client, M>\n ? FraciForPrismaByFieldOptions<\n Client,\n Options[\"fields\"][QualifiedField],\n M,\n F\n >\n : F extends StringModelFieldName<Client, M>\n ? FraciForPrismaByFieldOptions<\n Client,\n Options[\"fields\"][QualifiedField],\n M,\n F\n >\n : never\n : never\n : never;\n\n/**\n * A union of the pairs of the key and value of the {@link PrismaFraciOptions.fields fields} property of the options.\n *\n * @template Client - The Prisma client type\n * @template Options - The options type\n *\n * @example [\"article.fi\", { group: [\"userId\"], lengthBase: \"0123456789\", digitBase: \"0123456789\" }] | [\"photo.fi\", { group: [\"userId\"], lengthBase: \"0123456789\", digitBase: \"0123456789\" }] | ...\n */\ntype FieldsUnion<Client, Options extends PrismaFraciOptions<Client>> = {\n [K in keyof Options[\"fields\"]]: [K, Options[\"fields\"][K]];\n}[keyof Options[\"fields\"]];\n\n/**\n * The field information for each model.\n *\n * @template Client - The Prisma client type\n * @template Options - The options type\n */\ntype PerModelFieldInfo<Client, Options extends PrismaFraciOptions<Client>> = {\n [M in ModelKey<Client>]: {\n [F in\n | BinaryModelFieldName<Client, M>\n | StringModelFieldName<Client, M> as `${M}.${F}` extends FieldsUnion<\n Client,\n Options\n >[0]\n ? F\n : never]: {\n readonly helper: Options[\"fields\"][`${M}.${F}`] extends PrismaFraciFieldOptions\n ? FraciForPrismaByFieldOptions<\n Client,\n Options[\"fields\"][`${M}.${F}`],\n M,\n F\n >\n : never;\n };\n };\n};\n\n/**\n * [model component](https://www.prisma.io/docs/orm/prisma-client/client-extensions/model) of the Prisma extension.\n *\n * @template Client - The Prisma client type\n * @template Options - The options type\n */\ntype PrismaFraciExtensionModel<\n Client,\n Options extends PrismaFraciOptions<Client>,\n> = {\n [M in keyof PerModelFieldInfo<Client, Options>]: {\n fraci<F extends keyof PerModelFieldInfo<Client, Options>[M]>(\n field: F,\n ): PerModelFieldInfo<Client, Options>[M][F][\"helper\"];\n };\n};\n\n/**\n * The type of our Prisma extension.\n *\n * @template Client - The Prisma client type\n * @template Options - The options type\n */\nexport type PrismaFraciExtension<\n Client,\n Options extends PrismaFraciOptions<Client>,\n> = {\n name: typeof EXTENSION_NAME;\n model: PrismaFraciExtensionModel<Client, Options>;\n};\n\n/**\n * {@link AnyFraci} for Prisma.\n */\ntype AnyFraciForPrisma = FraciForPrismaInternal<\n any,\n string,\n any,\n AnyFractionalIndex\n>;\n\n/**\n * Creates a Prisma extension for fractional indexing.\n *\n * @template Client - The Prisma client type\n * @template Options - The options type\n *\n * @param _clientOrConstructor - The Prisma client or constructor. Only used for type inference and not used at runtime.\n * @param options - The options for the fractional indexing extension\n * @returns The Prisma extension.\n * @throws {FraciError} Throws a {@link FraciError} when field information for a specified model.field cannot be retrieved\n * @throws {FraciError} Throws a {@link FraciError} when the digit or length base strings are invalid\n *\n * @see {@link FraciForPrisma} - The enhanced fractional indexing utility for Prisma ORM\n * @see {@link FraciError} - The custom error class for the Fraci library\n */\nexport function prismaFraci<\n Client,\n const Options extends PrismaFraciOptions<Client>,\n>(\n _clientOrConstructor:\n | (new (...args: any) => Client)\n | ((...args: any) => Client)\n | Client,\n {\n fields,\n maxLength = DEFAULT_MAX_LENGTH,\n maxRetries = DEFAULT_MAX_RETRIES,\n }: Options,\n) {\n return Prisma.defineExtension((client) => {\n // Create a shared cache for better performance across multiple fields\n const cache = createFraciCache();\n\n // Map to store helper instances for each model.field combination\n const helperMap = new Map<string, AnyFraciForPrisma>();\n\n // Process each field configuration from the options\n for (const [modelAndField, config] of Object.entries(fields) as [\n string,\n PrismaFraciFieldOptions,\n ][]) {\n // Split the \"model.field\" string into separate parts\n const [model, field] = modelAndField.split(\".\", 2) as [\n ModelKey<Client>,\n string,\n ];\n\n // Get the actual model name from Prisma metadata\n const { modelName } = (client as any)[model]?.fields?.[field] ?? {};\n if (!modelName) {\n if (globalThis.__DEV__) {\n console.error(`FraciError: [INITIALIZATION_FAILED] Could not get field information for ${model}.${field}.\nMake sure that\n- The model and field names are correct and exist in the Prisma schema\n- The Prisma client is generated with the correct schema\n- The Prisma version is compatible with the extension`);\n }\n\n throw new FraciError(\n \"INITIALIZATION_FAILED\",\n `Could not get field information for ${model}.${field}`,\n );\n }\n\n // Create the base fractional indexing helper\n const helper =\n config.type === \"binary\"\n ? fraciBinary({\n maxLength,\n maxRetries,\n })\n : fraciString(\n {\n ...config,\n maxLength,\n maxRetries,\n },\n cache,\n );\n\n /**\n * Internal function to retrieve indices for positioning items.\n * This function queries the database to find the appropriate indices\n * for inserting an item before or after a specified cursor position.\n *\n * @param where - The where clause to filter items by group\n * @param cursor - The cursor position, or null for first/last position\n * @param direction - The direction to search for indices (asc/desc)\n * @param tuple - A function to create a tuple from two indices\n * @param pClient - The Prisma client to use\n * @returns A tuple of indices, or undefined if the cursor doesn't exist\n */\n const indicesFor = async (\n where: any,\n cursor: any,\n direction: \"asc\" | \"desc\",\n tuple: <T>(a: T, b: T) => [T, T],\n pClient: AnyPrismaClient = client,\n ): Promise<any> => {\n // Case 1: No cursor provided - get the first/last item in the group\n if (!cursor) {\n const item = await pClient[model].findFirst({\n where, // Filter by group conditions\n select: { [field]: true }, // Only select the fractional index field\n orderBy: { [field]: direction }, // Order by the fractional index in appropriate direction\n });\n\n // We should always return a tuple of two indices if `cursor` is `null`.\n // For after: [null, firstItem]\n // For before: [lastItem, null]\n return tuple(null, item?.[field] ?? null);\n }\n\n // Case 2: Cursor provided - find items adjacent to the cursor\n const items = await pClient[model].findMany({\n cursor, // Start from the cursor position\n where, // Filter by group conditions\n select: { [field]: true }, // Only select the fractional index field\n orderBy: { [field]: direction }, // Order by the fractional index in appropriate direction\n take: 2, // Get the cursor item and the adjacent item\n });\n\n return items.length < 1\n ? // Return undefined if cursor not found\n undefined\n : // Return the indices in the appropriate order based on direction\n tuple(items[0][field], items[1]?.[field] ?? null);\n };\n\n // Function to find indices for inserting an item after a specified cursor\n const indicesForAfter = (\n where: any,\n cursor: any,\n pClient?: AnyPrismaClient,\n ): Promise<any> =>\n indicesFor(where, cursor, \"asc\", (a, b) => [a, b], pClient);\n\n // Function to find indices for inserting an item before a specified cursor\n const indicesForBefore = (\n where: any,\n cursor: any,\n pClient?: AnyPrismaClient,\n ): Promise<any> =>\n indicesFor(where, cursor, \"desc\", (a, b) => [b, a], pClient);\n\n // Create an enhanced helper with Prisma-specific methods\n const helperEx: AnyFraciForPrisma = {\n ...(helper as AnyFraci), // Include all methods from the base fraci helper\n isIndexConflictError: (\n error: unknown,\n ): error is PrismaClientConflictError =>\n isIndexConflictError(error, modelName, field),\n indicesForAfter,\n indicesForBefore,\n indicesForFirst: (where: any, pClient?: AnyPrismaClient) =>\n indicesForAfter(where, null, pClient),\n indicesForLast: (where: any, pClient?: AnyPrismaClient) =>\n indicesForBefore(where, null, pClient),\n };\n\n // Store the helper in the map with a unique key combining model and field\n helperMap.set(`${model}\\0${field}`, helperEx);\n }\n\n // Create the extension model object that will be attached to each Prisma model\n const extensionModel = Object.create(null) as Record<\n ModelKey<Client>,\n unknown\n >;\n\n // Iterate through all models in the Prisma client\n for (const model of Object.keys(client) as ModelKey<Client>[]) {\n // Skip internal Prisma properties that start with $ or _\n if (model.startsWith(\"$\") || model.startsWith(\"_\")) {\n continue;\n }\n\n // Add the fraci method to each model\n extensionModel[model] = {\n // This method retrieves the appropriate helper for the specified field\n fraci(field: string) {\n return helperMap.get(`${model}\\0${field}`)!;\n },\n };\n }\n\n // Register the extension with Prisma\n return client.$extends({\n name: EXTENSION_NAME,\n model: extensionModel,\n } as unknown as PrismaFraciExtension<Client, Options>);\n });\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","/**\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","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 gene