UNPKG

@convex-dev/aggregate

Version:

Convex component to calculate counts and sums of values for efficient aggregation.

333 lines 15.3 kB
import type { DocumentByName, GenericDataModel, GenericMutationCtx, GenericQueryCtx, TableNamesInDataModel } from "convex/server"; import type { Key } from "../component/btree.js"; import { type Bound, type Bounds } from "./positions.js"; import type { GenericId, Value as ConvexValue } from "convex/values"; import type { ComponentApi } from "../component/_generated/component.js"; export type RunQueryCtx = { runQuery: GenericQueryCtx<GenericDataModel>["runQuery"]; }; export type RunMutationCtx = { runMutation: GenericMutationCtx<GenericDataModel>["runMutation"]; }; export type Item<K extends Key, ID extends string> = { key: K; id: ID; sumValue: number; }; export type { Key, Bound, Bounds }; /** * Write data to be aggregated, and read aggregated data. * * The data structure is effectively a key-value store sorted by key, where the * value is an ID and an optional sumValue. * 1. The key can be any Convex value (number, string, array, etc.). * 2. The ID is a string which should be unique. * 3. The sumValue is a number which is aggregated by summing. If not provided, * it's assumed to be zero. * * Once values have been added to the data structure, you can query for the * count and sum of items between a range of keys. */ export declare class Aggregate<K extends Key, ID extends string, Namespace extends ConvexValue | undefined = undefined> { protected component: ComponentApi; constructor(component: ComponentApi); /** * Counts items between the given bounds. */ count(ctx: RunQueryCtx, ...opts: NamespacedOpts<{ bounds?: Bounds<K, ID>; }, Namespace>): Promise<number>; /** * Batch version of count() - counts items for multiple bounds in a single call. */ countBatch(ctx: RunQueryCtx, queries: NamespacedOptsBatch<{ bounds?: Bounds<K, ID>; }, Namespace>): Promise<number[]>; /** * Adds up the sumValue of items between the given bounds. */ sum(ctx: RunQueryCtx, ...opts: NamespacedOpts<{ bounds?: Bounds<K, ID>; }, Namespace>): Promise<number>; /** * Batch version of sum() - sums items for multiple bounds in a single call. */ sumBatch(ctx: RunQueryCtx, queries: NamespacedOptsBatch<{ bounds?: Bounds<K, ID>; }, Namespace>): Promise<number[]>; /** * Returns the item at the given offset/index/rank in the order of key, * within the bounds. Zero-indexed, so at(0) is the smallest key within the * bounds. * * If offset is negative, it counts from the end of the list, so at(-1) is the * item with the largest key within the bounds. */ at(ctx: RunQueryCtx, offset: number, ...opts: NamespacedOpts<{ bounds?: Bounds<K, ID>; }, Namespace>): Promise<Item<K, ID>>; /** * Batch version of at() - returns items at multiple offsets in a single call. */ atBatch(ctx: RunQueryCtx, queries: NamespacedOptsBatch<{ offset: number; bounds?: Bounds<K, ID>; }, Namespace>): Promise<Item<K, ID>[]>; /** * Returns the rank/offset/index of the given key, within the bounds. * Specifically, it returns the index of the first item with * * - key >= the given key if `order` is "asc" (default) * - key <= the given key if `order` is "desc" */ indexOf(ctx: RunQueryCtx, key: K, ...opts: NamespacedOpts<{ id?: ID; bounds?: Bounds<K, ID>; order?: "asc" | "desc"; }, Namespace>): Promise<number>; /** * @deprecated Use `indexOf` instead. */ offsetOf(ctx: RunQueryCtx, key: K, namespace: Namespace, id?: ID, bounds?: Bounds<K, ID>): Promise<number>; /** * @deprecated Use `indexOf` instead. */ offsetUntil(ctx: RunQueryCtx, key: K, namespace: Namespace, id?: ID, bounds?: Bounds<K, ID>): Promise<number>; /** * Gets the minimum item within the given bounds. */ min(ctx: RunQueryCtx, ...opts: NamespacedOpts<{ bounds?: Bounds<K, ID>; }, Namespace>): Promise<Item<K, ID> | null>; /** * Gets the maximum item within the given bounds. */ max(ctx: RunQueryCtx, ...opts: NamespacedOpts<{ bounds?: Bounds<K, ID>; }, Namespace>): Promise<Item<K, ID> | null>; /** * Gets a uniformly random item within the given bounds. */ random(ctx: RunQueryCtx, ...opts: NamespacedOpts<{ bounds?: Bounds<K, ID>; }, Namespace>): Promise<Item<K, ID> | null>; /** * Get a page of items between the given bounds, with a cursor to paginate. * Use `iter` to iterate over all items within the bounds. */ paginate(ctx: RunQueryCtx, ...opts: NamespacedOpts<{ bounds?: Bounds<K, ID>; cursor?: string; order?: "asc" | "desc"; pageSize?: number; }, Namespace>): Promise<{ page: Item<K, ID>[]; cursor: string; isDone: boolean; }>; /** * Example usage: * ```ts * for await (const item of aggregate.iter(ctx, bounds)) { * console.log(item); * } * ``` */ iter(ctx: RunQueryCtx, ...opts: NamespacedOpts<{ bounds?: Bounds<K, ID>; order?: "asc" | "desc"; pageSize?: number; }, Namespace>): AsyncGenerator<Item<K, ID>, void, undefined>; /** Write operations. See {@link DirectAggregate} for docstrings. */ _insert(ctx: RunMutationCtx, namespace: Namespace, key: K, id: ID, summand?: number): Promise<void>; _delete(ctx: RunMutationCtx, namespace: Namespace, key: K, id: ID): Promise<void>; _replace(ctx: RunMutationCtx, currentNamespace: Namespace, currentKey: K, newNamespace: Namespace, newKey: K, id: ID, summand?: number): Promise<void>; _insertIfDoesNotExist(ctx: RunMutationCtx, namespace: Namespace, key: K, id: ID, summand?: number): Promise<void>; _deleteIfExists(ctx: RunMutationCtx, namespace: Namespace, key: K, id: ID): Promise<void>; _replaceOrInsert(ctx: RunMutationCtx, currentNamespace: Namespace, currentKey: K, newNamespace: Namespace, newKey: K, id: ID, summand?: number): Promise<void>; /** * (re-)initialize the data structure, removing all items if it exists. * * Change the maxNodeSize if provided, otherwise keep it the same. * maxNodeSize is how you tune the data structure's width and depth. * Larger values can reduce write contention but increase read latency. * Default is 16. * Set rootLazy = false to eagerly compute aggregates on the root node, which * improves aggregation latency at the expense of making all writes contend * with each other, so it's only recommended for read-heavy workloads. * Default is true. */ clear(ctx: RunMutationCtx, ...opts: NamespacedOpts<{ maxNodeSize?: number; rootLazy?: boolean; }, Namespace>): Promise<void>; /** * If rootLazy is false (the default is true but it can be set to false by * `clear`), the aggregates data structure writes to a single root node on * every insert/delete/replace, which can cause contention. * * If your data structure has frequent writes, you can reduce contention by * calling makeRootLazy, which removes the frequent writes to the root node. * With a lazy root node, updates will only contend with other updates to the * same shard of the tree. The number of shards is determined by maxNodeSize, * so larger maxNodeSize can also help. */ makeRootLazy(ctx: RunMutationCtx, namespace: Namespace): Promise<void>; paginateNamespaces(ctx: RunQueryCtx, cursor?: string, pageSize?: number): Promise<{ page: Namespace[]; cursor: string; isDone: boolean; }>; iterNamespaces(ctx: RunQueryCtx, pageSize?: number): AsyncGenerator<Namespace, void, undefined>; clearAll(ctx: RunMutationCtx & RunQueryCtx, opts?: { maxNodeSize?: number; rootLazy?: boolean; }): Promise<void>; makeAllRootsLazy(ctx: RunMutationCtx & RunQueryCtx): Promise<void>; } export type DirectAggregateType<K extends Key, ID extends string, Namespace extends ConvexValue | undefined = undefined> = { Key: K; Id: ID; Namespace?: Namespace; }; type AnyDirectAggregateType = DirectAggregateType<Key, string, ConvexValue | undefined>; type DirectAggregateNamespace<T extends AnyDirectAggregateType> = "Namespace" extends keyof T ? T["Namespace"] : undefined; /** * A DirectAggregate is an Aggregate where you can insert, delete, and replace * items directly, and keys and IDs can be customized. * * Contrast with TableAggregate, which follows a table with Triggers and * computes keys and sumValues from the table's documents. */ export declare class DirectAggregate<T extends AnyDirectAggregateType> extends Aggregate<T["Key"], T["Id"], DirectAggregateNamespace<T>> { /** * Insert a new key into the data structure. * The id should be unique. * If not provided, the sumValue is assumed to be zero. * If the tree does not exist yet, it will be initialized with the default * maxNodeSize and lazyRoot=true. * If the [key, id] pair already exists, this will throw. */ insert(ctx: RunMutationCtx, args: NamespacedArgs<{ key: T["Key"]; id: T["Id"]; sumValue?: number; }, DirectAggregateNamespace<T>>): Promise<void>; /** * Delete the key with the given ID from the data structure. * Throws if the given key and ID do not exist. */ delete(ctx: RunMutationCtx, args: NamespacedArgs<{ key: T["Key"]; id: T["Id"]; }, DirectAggregateNamespace<T>>): Promise<void>; /** * Update an existing item in the data structure. * This is effectively a delete followed by an insert, but it's performed * atomically so it's impossible to view the data structure with the key missing. */ replace(ctx: RunMutationCtx, currentItem: NamespacedArgs<{ key: T["Key"]; id: T["Id"]; }, DirectAggregateNamespace<T>>, newItem: NamespacedArgs<{ key: T["Key"]; sumValue?: number; }, DirectAggregateNamespace<T>>): Promise<void>; /** * Equivalents to `insert`, `delete`, and `replace` where the item may or may not exist. * This can be useful for live backfills: * 1. Update live writes to use these methods to write into the new Aggregate. * 2. Run a background backfill, paginating over existing data, calling `insertIfDoesNotExist` on each item. * 3. Once the backfill is complete, use `insert`, `delete`, and `replace` for live writes. * 4. Begin using the Aggregate read methods. */ insertIfDoesNotExist(ctx: RunMutationCtx, args: NamespacedArgs<{ key: T["Key"]; id: T["Id"]; sumValue?: number; }, DirectAggregateNamespace<T>>): Promise<void>; deleteIfExists(ctx: RunMutationCtx, args: NamespacedArgs<{ key: T["Key"]; id: T["Id"]; }, DirectAggregateNamespace<T>>): Promise<void>; replaceOrInsert(ctx: RunMutationCtx, currentItem: NamespacedArgs<{ key: T["Key"]; id: T["Id"]; }, DirectAggregateNamespace<T>>, newItem: NamespacedArgs<{ key: T["Key"]; sumValue?: number; }, DirectAggregateNamespace<T>>): Promise<void>; } export type TableAggregateType<K extends Key, DataModel extends GenericDataModel, TableName extends TableNamesInDataModel<DataModel>, Namespace extends ConvexValue | undefined = undefined> = { Key: K; DataModel: DataModel; TableName: TableName; Namespace?: Namespace; }; type AnyTableAggregateType = TableAggregateType<Key, GenericDataModel, TableNamesInDataModel<GenericDataModel>, ConvexValue | undefined>; type TableAggregateNamespace<T extends AnyTableAggregateType> = "Namespace" extends keyof T ? T["Namespace"] : undefined; type TableAggregateDocument<T extends AnyTableAggregateType> = DocumentByName<T["DataModel"], T["TableName"]>; type TableAggregateId<T extends AnyTableAggregateType> = GenericId<T["TableName"]>; type TableAggregateTrigger<Ctx, T extends AnyTableAggregateType> = Trigger<Ctx, T["DataModel"], T["TableName"]>; export declare class TableAggregate<T extends AnyTableAggregateType> extends Aggregate<T["Key"], GenericId<T["TableName"]>, TableAggregateNamespace<T>> { private options; constructor(component: ComponentApi, options: { sortKey: (d: TableAggregateDocument<T>) => T["Key"]; sumValue?: (d: TableAggregateDocument<T>) => number; } & (undefined extends TableAggregateNamespace<T> ? { namespace?: (d: TableAggregateDocument<T>) => TableAggregateNamespace<T>; } : { namespace: (d: TableAggregateDocument<T>) => TableAggregateNamespace<T>; })); insert(ctx: RunMutationCtx, doc: TableAggregateDocument<T>): Promise<void>; delete(ctx: RunMutationCtx, doc: TableAggregateDocument<T>): Promise<void>; replace(ctx: RunMutationCtx, oldDoc: TableAggregateDocument<T>, newDoc: TableAggregateDocument<T>): Promise<void>; insertIfDoesNotExist(ctx: RunMutationCtx, doc: TableAggregateDocument<T>): Promise<void>; deleteIfExists(ctx: RunMutationCtx, doc: TableAggregateDocument<T>): Promise<void>; replaceOrInsert(ctx: RunMutationCtx, oldDoc: TableAggregateDocument<T>, newDoc: TableAggregateDocument<T>): Promise<void>; /** * Returns the rank/offset/index of the given document, within the bounds. * This differs from `indexOf` in that it take the document rather than key. * Specifically, it returns the index of the first item with * * - key >= the given doc's key if `order` is "asc" (default) * - key <= the given doc's key if `order` is "desc" */ indexOfDoc(ctx: RunQueryCtx, doc: TableAggregateDocument<T>, opts?: { id?: TableAggregateId<T>; bounds?: Bounds<T["Key"], TableAggregateId<T>>; order?: "asc" | "desc"; }): Promise<number>; trigger<Ctx extends RunMutationCtx>(): TableAggregateTrigger<Ctx, T>; idempotentTrigger<Ctx extends RunMutationCtx>(): TableAggregateTrigger<Ctx, T>; } export type Trigger<Ctx, DataModel extends GenericDataModel, TableName extends TableNamesInDataModel<DataModel>> = (ctx: Ctx, change: Change<DataModel, TableName>) => Promise<void>; export type Change<DataModel extends GenericDataModel, TableName extends TableNamesInDataModel<DataModel>> = { id: GenericId<TableName>; } & ({ operation: "insert"; oldDoc: null; newDoc: DocumentByName<DataModel, TableName>; } | { operation: "update"; oldDoc: DocumentByName<DataModel, TableName>; newDoc: DocumentByName<DataModel, TableName>; } | { operation: "delete"; oldDoc: DocumentByName<DataModel, TableName>; newDoc: null; }); export declare function btreeItemToAggregateItem<K extends Key, ID extends string>({ k, s, }: { k: unknown; s: number; }): Item<K, ID>; export type NamespacedArgs<Args, Namespace> = (Args & { namespace: Namespace; }) | (Namespace extends undefined ? Args : never); export type NamespacedOpts<Opts, Namespace> = [{ namespace: Namespace; } & Opts] | (undefined extends Namespace ? [Opts?] : never); export type NamespacedOptsBatch<Opts, Namespace> = Array<undefined extends Namespace ? Opts : { namespace: Namespace; } & Opts>; //# sourceMappingURL=index.d.ts.map