UNPKG

@tanstack/electric-db-collection

Version:

ElectricSQL collection for TanStack DB

216 lines (215 loc) 9.25 kB
import { BaseCollectionConfig, CollectionConfig, DeleteMutationFnParams, InsertMutationFnParams, SyncMode, UpdateMutationFnParams, UtilsRecord } from '@tanstack/db'; import { StandardSchemaV1 } from '@standard-schema/spec'; import { GetExtensions, Message, Row, ShapeStreamOptions } from '@electric-sql/client'; export { isChangeMessage, isControlMessage } from '@electric-sql/client'; /** * Symbol for internal test hooks (hidden from public API) */ export declare const ELECTRIC_TEST_HOOKS: unique symbol; /** * Internal test hooks interface (for testing only) */ export interface ElectricTestHooks { /** * Called before marking collection ready after first up-to-date in progressive mode * Allows tests to pause and validate snapshot phase before atomic swap completes */ beforeMarkingReady?: () => Promise<void>; } /** * Type representing a transaction ID in ElectricSQL */ export type Txid = number; /** * Custom match function type - receives stream messages and returns boolean * indicating if the mutation has been synchronized */ export type MatchFunction<T extends Row<unknown>> = (message: Message<T>) => boolean; /** * Matching strategies for Electric synchronization * Handlers can return: * - Txid strategy: { txid: number | number[], timeout?: number } (recommended) * - Void (no return value) - mutation completes without waiting * * The optional timeout property specifies how long to wait for the txid(s) in milliseconds. * If not specified, defaults to 5000ms. */ export type MatchingStrategy = { txid: Txid | Array<Txid>; timeout?: number; } | void; type InferSchemaOutput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<T> extends Row<unknown> ? StandardSchemaV1.InferOutput<T> : Record<string, unknown> : Record<string, unknown>; /** * The mode of sync to use for the collection. * @default `eager` * @description * - `eager`: * - syncs all data immediately on preload * - collection will be marked as ready once the sync is complete * - there is no incremental sync * - `on-demand`: * - syncs data in incremental snapshots when the collection is queried * - collection will be marked as ready immediately after the first snapshot is synced * - `progressive`: * - syncs all data for the collection in the background * - uses incremental snapshots during the initial sync to provide a fast path to the data required for queries * - collection will be marked as ready once the full sync is complete */ export type ElectricSyncMode = SyncMode | `progressive`; /** * Configuration interface for Electric collection options * @template T - The type of items in the collection * @template TSchema - The schema type for validation */ export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>, TSchema extends StandardSchemaV1 = never> extends Omit<BaseCollectionConfig<T, string | number, TSchema, ElectricCollectionUtils<T>, any>, `onInsert` | `onUpdate` | `onDelete` | `syncMode`> { /** * Configuration options for the ElectricSQL ShapeStream */ shapeOptions: ShapeStreamOptions<GetExtensions<T>>; syncMode?: ElectricSyncMode; /** * Internal test hooks (for testing only) * Hidden via Symbol to prevent accidental usage in production */ [ELECTRIC_TEST_HOOKS]?: ElectricTestHooks; /** * Optional asynchronous handler function called before an insert operation * @param params Object containing transaction and collection information * @returns Promise resolving to { txid, timeout? } or void * @example * // Basic Electric insert handler with txid (recommended) * onInsert: async ({ transaction }) => { * const newItem = transaction.mutations[0].modified * const result = await api.todos.create({ * data: newItem * }) * return { txid: result.txid } * } * * @example * // Insert handler with custom timeout * onInsert: async ({ transaction }) => { * const newItem = transaction.mutations[0].modified * const result = await api.todos.create({ * data: newItem * }) * return { txid: result.txid, timeout: 10000 } // Wait up to 10 seconds * } * * @example * // Insert handler with multiple items - return array of txids * onInsert: async ({ transaction }) => { * const items = transaction.mutations.map(m => m.modified) * const results = await Promise.all( * items.map(item => api.todos.create({ data: item })) * ) * return { txid: results.map(r => r.txid) } * } * * @example * // Use awaitMatch utility for custom matching * onInsert: async ({ transaction, collection }) => { * const newItem = transaction.mutations[0].modified * await api.todos.create({ data: newItem }) * await collection.utils.awaitMatch( * (message) => isChangeMessage(message) && * message.headers.operation === 'insert' && * message.value.name === newItem.name * ) * } */ onInsert?: (params: InsertMutationFnParams<T, string | number, ElectricCollectionUtils<T>>) => Promise<MatchingStrategy>; /** * Optional asynchronous handler function called before an update operation * @param params Object containing transaction and collection information * @returns Promise resolving to { txid, timeout? } or void * @example * // Basic Electric update handler with txid (recommended) * onUpdate: async ({ transaction }) => { * const { original, changes } = transaction.mutations[0] * const result = await api.todos.update({ * where: { id: original.id }, * data: changes * }) * return { txid: result.txid } * } * * @example * // Use awaitMatch utility for custom matching * onUpdate: async ({ transaction, collection }) => { * const { original, changes } = transaction.mutations[0] * await api.todos.update({ where: { id: original.id }, data: changes }) * await collection.utils.awaitMatch( * (message) => isChangeMessage(message) && * message.headers.operation === 'update' && * message.value.id === original.id * ) * } */ onUpdate?: (params: UpdateMutationFnParams<T, string | number, ElectricCollectionUtils<T>>) => Promise<MatchingStrategy>; /** * Optional asynchronous handler function called before a delete operation * @param params Object containing transaction and collection information * @returns Promise resolving to { txid, timeout? } or void * @example * // Basic Electric delete handler with txid (recommended) * onDelete: async ({ transaction }) => { * const mutation = transaction.mutations[0] * const result = await api.todos.delete({ * id: mutation.original.id * }) * return { txid: result.txid } * } * * @example * // Use awaitMatch utility for custom matching * onDelete: async ({ transaction, collection }) => { * const mutation = transaction.mutations[0] * await api.todos.delete({ id: mutation.original.id }) * await collection.utils.awaitMatch( * (message) => isChangeMessage(message) && * message.headers.operation === 'delete' && * message.value.id === mutation.original.id * ) * } */ onDelete?: (params: DeleteMutationFnParams<T, string | number, ElectricCollectionUtils<T>>) => Promise<MatchingStrategy>; } /** * Type for the awaitTxId utility function */ export type AwaitTxIdFn = (txId: Txid, timeout?: number) => Promise<boolean>; /** * Type for the awaitMatch utility function */ export type AwaitMatchFn<T extends Row<unknown>> = (matchFn: MatchFunction<T>, timeout?: number) => Promise<boolean>; /** * Electric collection utilities type */ export interface ElectricCollectionUtils<T extends Row<unknown> = Row<unknown>> extends UtilsRecord { awaitTxId: AwaitTxIdFn; awaitMatch: AwaitMatchFn<T>; } /** * Creates Electric collection options for use with a standard Collection * * @template T - The explicit type of items in the collection (highest priority) * @template TSchema - The schema type for validation and type inference (second priority) * @template TFallback - The fallback type if no explicit or schema type is provided * @param config - Configuration options for the Electric collection * @returns Collection options with utilities */ export declare function electricCollectionOptions<T extends StandardSchemaV1>(config: ElectricCollectionConfig<InferSchemaOutput<T>, T> & { schema: T; }): Omit<CollectionConfig<InferSchemaOutput<T>, string | number, T>, `utils`> & { id?: string; utils: ElectricCollectionUtils<InferSchemaOutput<T>>; schema: T; }; export declare function electricCollectionOptions<T extends Row<unknown>>(config: ElectricCollectionConfig<T> & { schema?: never; }): Omit<CollectionConfig<T, string | number>, `utils`> & { id?: string; utils: ElectricCollectionUtils<T>; schema?: never; };