/** * The Aggregate API uses keys and IDs, where the keys are for sorting and * IDs are for tie-breaking uniqueness. The component's BTree API uses * positions, which are unique keys. */ import type { Value } from "convex/values"; import type { Key } from "../component/btree.js"; export type Bound = { key: K; id?: ID; inclusive: boolean; }; export type SideBounds = { lower?: Bound; upper?: Bound; }; // e.g. TuplePrefix<[string, number]> = [] | [string] | [string, number] export type TuplePrefix< K extends unknown[], P extends unknown[] = [], > = P["length"] extends K["length"] ? P : P | TuplePrefix; export type Bounds = | SideBounds | (K extends unknown[] ? { prefix: TuplePrefix> } : never) | { eq: K; }; // IDs are strings so in the Convex ordering, null < IDs < arrays. const BEFORE_ALL_IDS = null; const AFTER_ALL_IDS: never[] = []; // First a key, which is exploded with explodeKey. // Then the ID, or BEFORE_ALL_IDS or AFTER_ALL_IDS. // Then a value to be inclusive or exclusive. export type Position = [Key, string | null | never[], "" | null | never[]]; function explodeKey(key: K): Key { if (Array.isArray(key)) { const exploded = [""]; for (const item of key) { exploded.push(item); exploded.push(""); } return exploded; } return key; } function implodeKey(k: Key): Key { if (Array.isArray(k)) { const imploded: Value[] = []; for (let i = 1; i < k.length; i += 2) { imploded.push(k[i]); } return imploded; } return k; } export function keyToPosition( key: K, id: ID, ): Position { return [explodeKey(key), id, ""]; } export function positionToKey( position: Position, ): { key: K; id: ID } { return { key: implodeKey(position[0]) as K, id: position[1] as ID }; } export function boundsToPositions( bounds?: Bounds, ): { k1?: Position; k2?: Position } { if (bounds === undefined) { return {}; } if ("eq" in bounds) { return { k1: boundToPosition("lower", { key: bounds.eq, inclusive: true }), k2: boundToPosition("upper", { key: bounds.eq, inclusive: true }), }; } if ("prefix" in bounds) { const prefix: Key[] = bounds.prefix; const exploded: Key = []; for (const item of prefix) { exploded.push(""); exploded.push(item); } return { k1: [exploded.concat([BEFORE_ALL_IDS]), BEFORE_ALL_IDS, BEFORE_ALL_IDS], k2: [exploded.concat([AFTER_ALL_IDS]), AFTER_ALL_IDS, AFTER_ALL_IDS], }; } return { k1: boundToPosition("lower", bounds.lower), k2: boundToPosition("upper", bounds.upper), }; } export function boundToPosition( direction: "lower" | "upper", bound: Bound, ): Position; export function boundToPosition(direction: "lower" | "upper"): undefined; export function boundToPosition( direction: "lower" | "upper", bound?: Bound, ): Position | undefined; export function boundToPosition( direction: "lower" | "upper", bound?: Bound, ): Position | undefined { if (bound === undefined) { return undefined; } if (direction === "lower") { return [ explodeKey(bound.key), bound.id ?? (bound.inclusive ? BEFORE_ALL_IDS : AFTER_ALL_IDS), bound.inclusive ? BEFORE_ALL_IDS : AFTER_ALL_IDS, ]; } else { return [ explodeKey(bound.key), bound.id ?? (bound.inclusive ? AFTER_ALL_IDS : BEFORE_ALL_IDS), bound.inclusive ? AFTER_ALL_IDS : BEFORE_ALL_IDS, ]; } }