@tanstack/db-collections
Version:
A collection for (aspirationally) every way of loading your data
232 lines (231 loc) • 10.1 kB
TypeScript
import { CollectionConfig, DeleteMutationFnParams, InsertMutationFnParams, SyncConfig, UpdateMutationFnParams, UtilsRecord } from '@tanstack/db';
import { StandardSchemaV1 } from '@standard-schema/spec';
import { GetExtensions, Row, ShapeStreamOptions } from '@electric-sql/client';
/**
* Type representing a transaction ID in Electric SQL
*/
export type Txid = number;
type InferSchemaOutput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<T> extends Row<unknown> ? StandardSchemaV1.InferOutput<T> : Record<string, unknown> : Record<string, unknown>;
type ResolveType<TExplicit extends Row<unknown> = Row<unknown>, TSchema extends StandardSchemaV1 = never, TFallback extends object = Record<string, unknown>> = unknown extends GetExtensions<TExplicit> ? [TSchema] extends [never] ? TFallback : InferSchemaOutput<TSchema> : TExplicit;
/**
* Configuration interface for Electric collection options
* @template TExplicit - 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
*
* @remarks
* Type resolution follows a priority order:
* 1. If you provide an explicit type via generic parameter, it will be used
* 2. If no explicit type is provided but a schema is, the schema's output type will be inferred
* 3. If neither explicit type nor schema is provided, the fallback type will be used
*
* You should provide EITHER an explicit type OR a schema, but not both, as they would conflict.
*/
export interface ElectricCollectionConfig<TExplicit extends Row<unknown> = Row<unknown>, TSchema extends StandardSchemaV1 = never, TFallback extends Row<unknown> = Row<unknown>> {
/**
* Configuration options for the ElectricSQL ShapeStream
*/
shapeOptions: ShapeStreamOptions<GetExtensions<ResolveType<TExplicit, TSchema, TFallback>>>;
/**
* All standard Collection configuration properties
*/
id?: string;
schema?: TSchema;
getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>[`getKey`];
sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>[`sync`];
/**
* Optional asynchronous handler function called before an insert operation
* Must return an object containing a txid number or array of txids
* @param params Object containing transaction and collection information
* @returns Promise resolving to an object with txid or txids
* @example
* // Basic Electric insert handler - MUST return { txid: number }
* onInsert: async ({ transaction }) => {
* const newItem = transaction.mutations[0].modified
* const result = await api.todos.create({
* data: newItem
* })
* return { txid: result.txid } // Required for Electric sync matching
* }
*
* @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) } // Array of txids
* }
*
* @example
* // Insert handler with error handling
* onInsert: async ({ transaction }) => {
* try {
* const newItem = transaction.mutations[0].modified
* const result = await api.createTodo(newItem)
* return { txid: result.txid }
* } catch (error) {
* console.error('Insert failed:', error)
* throw error // This will cause the transaction to fail
* }
* }
*
* @example
* // Insert handler with batch operation - single txid
* onInsert: async ({ transaction }) => {
* const items = transaction.mutations.map(m => m.modified)
* const result = await api.todos.createMany({
* data: items
* })
* return { txid: result.txid } // Single txid for batch operation
* }
*/
onInsert?: (params: InsertMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
txid: Txid | Array<Txid>;
}>;
/**
* Optional asynchronous handler function called before an update operation
* Must return an object containing a txid number or array of txids
* @param params Object containing transaction and collection information
* @returns Promise resolving to an object with txid or txids
* @example
* // Basic Electric update handler - MUST return { txid: number }
* onUpdate: async ({ transaction }) => {
* const { original, changes } = transaction.mutations[0]
* const result = await api.todos.update({
* where: { id: original.id },
* data: changes // Only the changed fields
* })
* return { txid: result.txid } // Required for Electric sync matching
* }
*
* @example
* // Update handler with multiple items - return array of txids
* onUpdate: async ({ transaction }) => {
* const updates = await Promise.all(
* transaction.mutations.map(m =>
* api.todos.update({
* where: { id: m.original.id },
* data: m.changes
* })
* )
* )
* return { txid: updates.map(u => u.txid) } // Array of txids
* }
*
* @example
* // Update handler with optimistic rollback
* onUpdate: async ({ transaction }) => {
* const mutation = transaction.mutations[0]
* try {
* const result = await api.updateTodo(mutation.original.id, mutation.changes)
* return { txid: result.txid }
* } catch (error) {
* // Transaction will automatically rollback optimistic changes
* console.error('Update failed, rolling back:', error)
* throw error
* }
* }
*/
onUpdate?: (params: UpdateMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
txid: Txid | Array<Txid>;
}>;
/**
* Optional asynchronous handler function called before a delete operation
* Must return an object containing a txid number or array of txids
* @param params Object containing transaction and collection information
* @returns Promise resolving to an object with txid or txids
* @example
* // Basic Electric delete handler - MUST return { txid: number }
* onDelete: async ({ transaction }) => {
* const mutation = transaction.mutations[0]
* const result = await api.todos.delete({
* id: mutation.original.id
* })
* return { txid: result.txid } // Required for Electric sync matching
* }
*
* @example
* // Delete handler with multiple items - return array of txids
* onDelete: async ({ transaction }) => {
* const deletes = await Promise.all(
* transaction.mutations.map(m =>
* api.todos.delete({
* where: { id: m.key }
* })
* )
* )
* return { txid: deletes.map(d => d.txid) } // Array of txids
* }
*
* @example
* // Delete handler with batch operation - single txid
* onDelete: async ({ transaction }) => {
* const idsToDelete = transaction.mutations.map(m => m.original.id)
* const result = await api.todos.deleteMany({
* ids: idsToDelete
* })
* return { txid: result.txid } // Single txid for batch operation
* }
*
* @example
* // Delete handler with optimistic rollback
* onDelete: async ({ transaction }) => {
* const mutation = transaction.mutations[0]
* try {
* const result = await api.deleteTodo(mutation.original.id)
* return { txid: result.txid }
* } catch (error) {
* // Transaction will automatically rollback optimistic changes
* console.error('Delete failed, rolling back:', error)
* throw error
* }
* }
*
*/
onDelete?: (params: DeleteMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
txid: Txid | Array<Txid>;
}>;
}
/**
* Type for the awaitTxId utility function
*/
export type AwaitTxIdFn = (txId: Txid, timeout?: number) => Promise<boolean>;
/**
* Electric collection utilities type
*/
export interface ElectricCollectionUtils extends UtilsRecord {
awaitTxId: AwaitTxIdFn;
}
/**
* Creates Electric collection options for use with a standard Collection
*
* @template TExplicit - 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<TExplicit extends Row<unknown> = Row<unknown>, TSchema extends StandardSchemaV1 = never, TFallback extends Row<unknown> = Row<unknown>>(config: ElectricCollectionConfig<TExplicit, TSchema, TFallback>): {
sync: SyncConfig<ResolveType<TExplicit, TSchema, TFallback>, string | number>;
onInsert: ((params: InsertMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
txid: Txid | Array<Txid>;
}>) | undefined;
onUpdate: ((params: UpdateMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
txid: Txid | Array<Txid>;
}>) | undefined;
onDelete: ((params: DeleteMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
txid: Txid | Array<Txid>;
}>) | undefined;
utils: {
awaitTxId: AwaitTxIdFn;
};
/**
* All standard Collection configuration properties
*/
id?: string;
schema?: TSchema | undefined;
getKey: (item: ResolveType<TExplicit, TSchema, TFallback>) => string | number;
};
export {};