@tanstack/electric-db-collection 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,12 +16,9 @@ function hasTxids(message) {
16
16
  }
17
17
  function electricCollectionOptions(config) {
18
18
  const seenTxids = new store.Store(/* @__PURE__ */ new Set([]));
19
- const sync = createElectricSync(
20
- config.shapeOptions,
21
- {
22
- seenTxids
23
- }
24
- );
19
+ const sync = createElectricSync(config.shapeOptions, {
20
+ seenTxids
21
+ });
25
22
  const awaitTxId = async (txId, timeout = 3e4) => {
26
23
  debug(`awaitTxId called with txid %d`, txId);
27
24
  if (typeof txId !== `number`) {
@@ -1 +1 @@
1
- {"version":3,"file":"electric.cjs","sources":["../../src/electric.ts"],"sourcesContent":["import {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n} from \"@electric-sql/client\"\nimport { Store } from \"@tanstack/store\"\nimport DebugModule from \"debug\"\nimport {\n ElectricDeleteHandlerMustReturnTxIdError,\n ElectricInsertHandlerMustReturnTxIdError,\n ElectricUpdateHandlerMustReturnTxIdError,\n ExpectedNumberInAwaitTxIdError,\n TimeoutWaitingForTxIdError,\n} from \"./errors\"\nimport type {\n CollectionConfig,\n DeleteMutationFnParams,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type {\n ControlMessage,\n GetExtensions,\n Message,\n Row,\n ShapeStreamOptions,\n} from \"@electric-sql/client\"\n\nconst debug = DebugModule.debug(`ts/db:electric`)\n\n/**\n * Type representing a transaction ID in ElectricSQL\n */\nexport type Txid = number\n\n// The `InferSchemaOutput` and `ResolveType` are copied from the `@tanstack/db` package\n// but we modified `InferSchemaOutput` slightly to restrict the schema output to `Row<unknown>`\n// This is needed in order for `GetExtensions` to be able to infer the parser extensions type from the schema\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends Row<unknown>\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\ntype ResolveType<\n TExplicit extends Row<unknown> = Row<unknown>,\n TSchema extends StandardSchemaV1 = never,\n TFallback extends object = Record<string, unknown>,\n> =\n unknown extends GetExtensions<TExplicit>\n ? [TSchema] extends [never]\n ? TFallback\n : InferSchemaOutput<TSchema>\n : TExplicit\n\n/**\n * Configuration interface for Electric collection options\n * @template TExplicit - The explicit type of items in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TFallback - The fallback type if no explicit or schema type is provided\n *\n * @remarks\n * Type resolution follows a priority order:\n * 1. If you provide an explicit type via generic parameter, it will be used\n * 2. If no explicit type is provided but a schema is, the schema's output type will be inferred\n * 3. If neither explicit type nor schema is provided, the fallback type will be used\n *\n * You should provide EITHER an explicit type OR a schema, but not both, as they would conflict.\n */\nexport interface ElectricCollectionConfig<\n TExplicit extends Row<unknown> = Row<unknown>,\n TSchema extends StandardSchemaV1 = never,\n TFallback extends Row<unknown> = Row<unknown>,\n> {\n /**\n * Configuration options for the ElectricSQL ShapeStream\n */\n shapeOptions: ShapeStreamOptions<\n GetExtensions<ResolveType<TExplicit, TSchema, TFallback>>\n >\n\n /**\n * All standard Collection configuration properties\n */\n id?: string\n schema?: TSchema\n getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>[`getKey`]\n sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>[`sync`]\n\n /**\n * Optional asynchronous handler function called before an insert operation\n * Must return an object containing a txid number or array of txids\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to an object with txid or txids\n * @example\n * // Basic Electric insert handler - MUST return { txid: number }\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * const result = await api.todos.create({\n * data: newItem\n * })\n * return { txid: result.txid } // Required for Electric sync matching\n * }\n *\n * @example\n * // Insert handler with multiple items - return array of txids\n * onInsert: async ({ transaction }) => {\n * const items = transaction.mutations.map(m => m.modified)\n * const results = await Promise.all(\n * items.map(item => api.todos.create({ data: item }))\n * )\n * return { txid: results.map(r => r.txid) } // Array of txids\n * }\n *\n * @example\n * // Insert handler with error handling\n * onInsert: async ({ transaction }) => {\n * try {\n * const newItem = transaction.mutations[0].modified\n * const result = await api.createTodo(newItem)\n * return { txid: result.txid }\n * } catch (error) {\n * console.error('Insert failed:', error)\n * throw error // This will cause the transaction to fail\n * }\n * }\n *\n * @example\n * // Insert handler with batch operation - single txid\n * onInsert: async ({ transaction }) => {\n * const items = transaction.mutations.map(m => m.modified)\n * const result = await api.todos.createMany({\n * data: items\n * })\n * return { txid: result.txid } // Single txid for batch operation\n * }\n */\n onInsert?: (\n params: InsertMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>\n ) => Promise<{ txid: Txid | Array<Txid> }>\n\n /**\n * Optional asynchronous handler function called before an update operation\n * Must return an object containing a txid number or array of txids\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to an object with txid or txids\n * @example\n * // Basic Electric update handler - MUST return { txid: number }\n * onUpdate: async ({ transaction }) => {\n * const { original, changes } = transaction.mutations[0]\n * const result = await api.todos.update({\n * where: { id: original.id },\n * data: changes // Only the changed fields\n * })\n * return { txid: result.txid } // Required for Electric sync matching\n * }\n *\n * @example\n * // Update handler with multiple items - return array of txids\n * onUpdate: async ({ transaction }) => {\n * const updates = await Promise.all(\n * transaction.mutations.map(m =>\n * api.todos.update({\n * where: { id: m.original.id },\n * data: m.changes\n * })\n * )\n * )\n * return { txid: updates.map(u => u.txid) } // Array of txids\n * }\n *\n * @example\n * // Update handler with optimistic rollback\n * onUpdate: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * try {\n * const result = await api.updateTodo(mutation.original.id, mutation.changes)\n * return { txid: result.txid }\n * } catch (error) {\n * // Transaction will automatically rollback optimistic changes\n * console.error('Update failed, rolling back:', error)\n * throw error\n * }\n * }\n */\n onUpdate?: (\n params: UpdateMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>\n ) => Promise<{ txid: Txid | Array<Txid> }>\n\n /**\n * Optional asynchronous handler function called before a delete operation\n * Must return an object containing a txid number or array of txids\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to an object with txid or txids\n * @example\n * // Basic Electric delete handler - MUST return { txid: number }\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * const result = await api.todos.delete({\n * id: mutation.original.id\n * })\n * return { txid: result.txid } // Required for Electric sync matching\n * }\n *\n * @example\n * // Delete handler with multiple items - return array of txids\n * onDelete: async ({ transaction }) => {\n * const deletes = await Promise.all(\n * transaction.mutations.map(m =>\n * api.todos.delete({\n * where: { id: m.key }\n * })\n * )\n * )\n * return { txid: deletes.map(d => d.txid) } // Array of txids\n * }\n *\n * @example\n * // Delete handler with batch operation - single txid\n * onDelete: async ({ transaction }) => {\n * const idsToDelete = transaction.mutations.map(m => m.original.id)\n * const result = await api.todos.deleteMany({\n * ids: idsToDelete\n * })\n * return { txid: result.txid } // Single txid for batch operation\n * }\n *\n * @example\n * // Delete handler with optimistic rollback\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * try {\n * const result = await api.deleteTodo(mutation.original.id)\n * return { txid: result.txid }\n * } catch (error) {\n * // Transaction will automatically rollback optimistic changes\n * console.error('Delete failed, rolling back:', error)\n * throw error\n * }\n * }\n *\n */\n onDelete?: (\n params: DeleteMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>\n ) => Promise<{ txid: Txid | Array<Txid> }>\n}\n\nfunction isUpToDateMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { up_to_date: true } {\n return isControlMessage(message) && message.headers.control === `up-to-date`\n}\n\nfunction isMustRefetchMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { headers: { control: `must-refetch` } } {\n return isControlMessage(message) && message.headers.control === `must-refetch`\n}\n\n// Check if a message contains txids in its headers\nfunction hasTxids<T extends Row<unknown>>(\n message: Message<T>\n): message is Message<T> & { headers: { txids?: Array<Txid> } } {\n return `txids` in message.headers && Array.isArray(message.headers.txids)\n}\n\n/**\n * Type for the awaitTxId utility function\n */\nexport type AwaitTxIdFn = (txId: Txid, timeout?: number) => Promise<boolean>\n\n/**\n * Electric collection utilities type\n */\nexport interface ElectricCollectionUtils extends UtilsRecord {\n awaitTxId: AwaitTxIdFn\n}\n\n/**\n * Creates Electric collection options for use with a standard Collection\n *\n * @template TExplicit - The explicit type of items in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TFallback - The fallback type if no explicit or schema type is provided\n * @param config - Configuration options for the Electric collection\n * @returns Collection options with utilities\n */\nexport function electricCollectionOptions<\n TExplicit extends Row<unknown> = Row<unknown>,\n TSchema extends StandardSchemaV1 = never,\n TFallback extends Row<unknown> = Row<unknown>,\n>(config: ElectricCollectionConfig<TExplicit, TSchema, TFallback>) {\n const seenTxids = new Store<Set<Txid>>(new Set([]))\n const sync = createElectricSync<ResolveType<TExplicit, TSchema, TFallback>>(\n config.shapeOptions,\n {\n seenTxids,\n }\n )\n\n /**\n * Wait for a specific transaction ID to be synced\n * @param txId The transaction ID to wait for as a number\n * @param timeout Optional timeout in milliseconds (defaults to 30000ms)\n * @returns Promise that resolves when the txId is synced\n */\n const awaitTxId: AwaitTxIdFn = async (\n txId: Txid,\n timeout: number = 30000\n ): Promise<boolean> => {\n debug(`awaitTxId called with txid %d`, txId)\n if (typeof txId !== `number`) {\n throw new ExpectedNumberInAwaitTxIdError(typeof txId)\n }\n\n const hasTxid = seenTxids.state.has(txId)\n if (hasTxid) return true\n\n return new Promise((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n unsubscribe()\n reject(new TimeoutWaitingForTxIdError(txId))\n }, timeout)\n\n const unsubscribe = seenTxids.subscribe(() => {\n if (seenTxids.state.has(txId)) {\n debug(`awaitTxId found match for txid %o`, txId)\n clearTimeout(timeoutId)\n unsubscribe()\n resolve(true)\n }\n })\n })\n }\n\n // Create wrapper handlers for direct persistence operations that handle txid awaiting\n const wrappedOnInsert = config.onInsert\n ? async (\n params: InsertMutationFnParams<\n ResolveType<TExplicit, TSchema, TFallback>\n >\n ) => {\n // Runtime check (that doesn't follow type)\n // eslint-disable-next-line\n const handlerResult = (await config.onInsert!(params)) ?? {}\n const txid = (handlerResult as { txid?: Txid | Array<Txid> }).txid\n\n if (!txid) {\n throw new ElectricInsertHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = config.onUpdate\n ? async (\n params: UpdateMutationFnParams<\n ResolveType<TExplicit, TSchema, TFallback>\n >\n ) => {\n // Runtime check (that doesn't follow type)\n // eslint-disable-next-line\n const handlerResult = (await config.onUpdate!(params)) ?? {}\n const txid = (handlerResult as { txid?: Txid | Array<Txid> }).txid\n\n if (!txid) {\n throw new ElectricUpdateHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = config.onDelete\n ? async (\n params: DeleteMutationFnParams<\n ResolveType<TExplicit, TSchema, TFallback>\n >\n ) => {\n const handlerResult = await config.onDelete!(params)\n if (!handlerResult.txid) {\n throw new ElectricDeleteHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(handlerResult.txid)) {\n await Promise.all(handlerResult.txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(handlerResult.txid)\n }\n\n return handlerResult\n }\n : undefined\n\n // Extract standard Collection config properties\n const {\n shapeOptions: _shapeOptions,\n onInsert: _onInsert,\n onUpdate: _onUpdate,\n onDelete: _onDelete,\n ...restConfig\n } = config\n\n return {\n ...restConfig,\n sync,\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n awaitTxId,\n },\n }\n}\n\n/**\n * Internal function to create ElectricSQL sync configuration\n */\nfunction createElectricSync<T extends Row<unknown>>(\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>,\n options: {\n seenTxids: Store<Set<Txid>>\n }\n): SyncConfig<T> {\n const { seenTxids } = options\n\n // Store for the relation schema information\n const relationSchema = new Store<string | undefined>(undefined)\n\n /**\n * Get the sync metadata for insert operations\n * @returns Record containing relation information\n */\n const getSyncMetadata = (): Record<string, unknown> => {\n // Use the stored schema if available, otherwise default to 'public'\n const schema = relationSchema.state || `public`\n\n return {\n relation: shapeOptions.params?.table\n ? [schema, shapeOptions.params.table]\n : undefined,\n }\n }\n\n let unsubscribeStream: () => void\n\n return {\n sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {\n const { begin, write, commit, markReady, truncate, collection } = params\n\n // Abort controller for the stream - wraps the signal if provided\n const abortController = new AbortController()\n\n if (shapeOptions.signal) {\n shapeOptions.signal.addEventListener(\n `abort`,\n () => {\n abortController.abort()\n },\n {\n once: true,\n }\n )\n if (shapeOptions.signal.aborted) {\n abortController.abort()\n }\n }\n\n const stream = new ShapeStream({\n ...shapeOptions,\n signal: abortController.signal,\n onError: (errorParams) => {\n // Just immediately mark ready if there's an error to avoid blocking\n // apps waiting for `.preload()` to finish.\n // Note that Electric sends a 409 error on a `must-refetch` message, but the\n // ShapeStream handled this and it will not reach this handler, therefor\n // this markReady will not be triggers by a `must-refetch`.\n markReady()\n\n if (shapeOptions.onError) {\n return shapeOptions.onError(errorParams)\n } else {\n console.error(\n `An error occurred while syncing collection: ${collection.id}, \\n` +\n `it has been marked as ready to avoid blocking apps waiting for '.preload()' to finish. \\n` +\n `You can provide an 'onError' handler on the shapeOptions to handle this error, and this message will not be logged.`,\n errorParams\n )\n }\n\n return\n },\n })\n let transactionStarted = false\n const newTxids = new Set<Txid>()\n\n unsubscribeStream = stream.subscribe((messages: Array<Message<T>>) => {\n let hasUpToDate = false\n\n for (const message of messages) {\n // Check for txids in the message and add them to our store\n if (hasTxids(message)) {\n message.headers.txids?.forEach((txid) => newTxids.add(txid))\n }\n\n if (isChangeMessage(message)) {\n // Check if the message contains schema information\n const schema = message.headers.schema\n if (schema && typeof schema === `string`) {\n // Store the schema for future use if it's a valid string\n relationSchema.setState(() => schema)\n }\n\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n write({\n type: message.headers.operation,\n value: message.value,\n // Include the primary key and relation info in the metadata\n metadata: {\n ...message.headers,\n },\n })\n } else if (isUpToDateMessage(message)) {\n hasUpToDate = true\n } else if (isMustRefetchMessage(message)) {\n debug(\n `Received must-refetch message, starting transaction with truncate`\n )\n\n // Start a transaction and truncate the collection\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n truncate()\n\n // Reset hasUpToDate so we continue accumulating changes until next up-to-date\n hasUpToDate = false\n }\n }\n\n if (hasUpToDate) {\n // Commit transaction if one was started\n if (transactionStarted) {\n commit()\n transactionStarted = false\n }\n\n // Mark the collection as ready now that sync is up to date\n markReady()\n\n // Always commit txids when we receive up-to-date, regardless of transaction state\n seenTxids.setState((currentTxids) => {\n const clonedSeen = new Set<Txid>(currentTxids)\n if (newTxids.size > 0) {\n debug(`new txids synced from pg %O`, Array.from(newTxids))\n }\n newTxids.forEach((txid) => clonedSeen.add(txid))\n newTxids.clear()\n return clonedSeen\n })\n }\n })\n\n // Return the unsubscribe function\n return () => {\n // Unsubscribe from the stream\n unsubscribeStream()\n // Abort the abort controller to stop the stream\n abortController.abort()\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata,\n }\n}\n"],"names":["isControlMessage","Store","ExpectedNumberInAwaitTxIdError","TimeoutWaitingForTxIdError","ElectricInsertHandlerMustReturnTxIdError","ElectricUpdateHandlerMustReturnTxIdError","ElectricDeleteHandlerMustReturnTxIdError","ShapeStream","isChangeMessage"],"mappings":"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"}
1
+ {"version":3,"file":"electric.cjs","sources":["../../src/electric.ts"],"sourcesContent":["import {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n} from \"@electric-sql/client\"\nimport { Store } from \"@tanstack/store\"\nimport DebugModule from \"debug\"\nimport {\n ElectricDeleteHandlerMustReturnTxIdError,\n ElectricInsertHandlerMustReturnTxIdError,\n ElectricUpdateHandlerMustReturnTxIdError,\n ExpectedNumberInAwaitTxIdError,\n TimeoutWaitingForTxIdError,\n} from \"./errors\"\nimport type {\n BaseCollectionConfig,\n CollectionConfig,\n DeleteMutationFnParams,\n Fn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type {\n ControlMessage,\n GetExtensions,\n Message,\n Row,\n ShapeStreamOptions,\n} from \"@electric-sql/client\"\n\nconst debug = DebugModule.debug(`ts/db:electric`)\n\n/**\n * Type representing a transaction ID in ElectricSQL\n */\nexport type Txid = number\n\n// The `InferSchemaOutput` and `ResolveType` are copied from the `@tanstack/db` package\n// but we modified `InferSchemaOutput` slightly to restrict the schema output to `Row<unknown>`\n// This is needed in order for `GetExtensions` to be able to infer the parser extensions type from the schema\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends Row<unknown>\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Configuration interface for Electric collection options\n * @template T - The type of items in the collection\n * @template TSchema - The schema type for validation\n */\nexport interface ElectricCollectionConfig<\n T extends Row<unknown> = Row<unknown>,\n TSchema extends StandardSchemaV1 = never,\n> extends BaseCollectionConfig<\n T,\n string | number,\n TSchema,\n Record<string, Fn>,\n { txid: Txid | Array<Txid> }\n > {\n /**\n * Configuration options for the ElectricSQL ShapeStream\n */\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>\n}\n\nfunction isUpToDateMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { up_to_date: true } {\n return isControlMessage(message) && message.headers.control === `up-to-date`\n}\n\nfunction isMustRefetchMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { headers: { control: `must-refetch` } } {\n return isControlMessage(message) && message.headers.control === `must-refetch`\n}\n\n// Check if a message contains txids in its headers\nfunction hasTxids<T extends Row<unknown>>(\n message: Message<T>\n): message is Message<T> & { headers: { txids?: Array<Txid> } } {\n return `txids` in message.headers && Array.isArray(message.headers.txids)\n}\n\n/**\n * Type for the awaitTxId utility function\n */\nexport type AwaitTxIdFn = (txId: Txid, timeout?: number) => Promise<boolean>\n\n/**\n * Electric collection utilities type\n */\nexport interface ElectricCollectionUtils extends UtilsRecord {\n awaitTxId: AwaitTxIdFn\n}\n\n/**\n * Creates Electric collection options for use with a standard Collection\n *\n * @template T - The explicit type of items in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TFallback - The fallback type if no explicit or schema type is provided\n * @param config - Configuration options for the Electric collection\n * @returns Collection options with utilities\n */\n\n// Overload for when schema is provided\nexport function electricCollectionOptions<T extends StandardSchemaV1>(\n config: ElectricCollectionConfig<InferSchemaOutput<T>, T> & {\n schema: T\n }\n): CollectionConfig<InferSchemaOutput<T>, string | number, T> & {\n id?: string\n utils: ElectricCollectionUtils\n schema: T\n}\n\n// Overload for when no schema is provided\nexport function electricCollectionOptions<T extends Row<unknown>>(\n config: ElectricCollectionConfig<T> & {\n schema?: never // prohibit schema\n }\n): CollectionConfig<T, string | number> & {\n id?: string\n utils: ElectricCollectionUtils\n schema?: never // no schema in the result\n}\n\nexport function electricCollectionOptions(\n config: ElectricCollectionConfig<any, any>\n): CollectionConfig<any, string | number, any> & {\n id?: string\n utils: ElectricCollectionUtils\n schema?: any\n} {\n const seenTxids = new Store<Set<Txid>>(new Set([]))\n const sync = createElectricSync<any>(config.shapeOptions, {\n seenTxids,\n })\n\n /**\n * Wait for a specific transaction ID to be synced\n * @param txId The transaction ID to wait for as a number\n * @param timeout Optional timeout in milliseconds (defaults to 30000ms)\n * @returns Promise that resolves when the txId is synced\n */\n const awaitTxId: AwaitTxIdFn = async (\n txId: Txid,\n timeout: number = 30000\n ): Promise<boolean> => {\n debug(`awaitTxId called with txid %d`, txId)\n if (typeof txId !== `number`) {\n throw new ExpectedNumberInAwaitTxIdError(typeof txId)\n }\n\n const hasTxid = seenTxids.state.has(txId)\n if (hasTxid) return true\n\n return new Promise((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n unsubscribe()\n reject(new TimeoutWaitingForTxIdError(txId))\n }, timeout)\n\n const unsubscribe = seenTxids.subscribe(() => {\n if (seenTxids.state.has(txId)) {\n debug(`awaitTxId found match for txid %o`, txId)\n clearTimeout(timeoutId)\n unsubscribe()\n resolve(true)\n }\n })\n })\n }\n\n // Create wrapper handlers for direct persistence operations that handle txid awaiting\n const wrappedOnInsert = config.onInsert\n ? async (params: InsertMutationFnParams<any>) => {\n // Runtime check (that doesn't follow type)\n\n const handlerResult = (await config.onInsert!(params)) ?? {}\n const txid = (handlerResult as { txid?: Txid | Array<Txid> }).txid\n\n if (!txid) {\n throw new ElectricInsertHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = config.onUpdate\n ? async (params: UpdateMutationFnParams<any>) => {\n // Runtime check (that doesn't follow type)\n\n const handlerResult = (await config.onUpdate!(params)) ?? {}\n const txid = (handlerResult as { txid?: Txid | Array<Txid> }).txid\n\n if (!txid) {\n throw new ElectricUpdateHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = config.onDelete\n ? async (params: DeleteMutationFnParams<any>) => {\n const handlerResult = await config.onDelete!(params)\n if (!handlerResult.txid) {\n throw new ElectricDeleteHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(handlerResult.txid)) {\n await Promise.all(handlerResult.txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(handlerResult.txid)\n }\n\n return handlerResult\n }\n : undefined\n\n // Extract standard Collection config properties\n const {\n shapeOptions: _shapeOptions,\n onInsert: _onInsert,\n onUpdate: _onUpdate,\n onDelete: _onDelete,\n ...restConfig\n } = config\n\n return {\n ...restConfig,\n sync,\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n awaitTxId,\n },\n }\n}\n\n/**\n * Internal function to create ElectricSQL sync configuration\n */\nfunction createElectricSync<T extends Row<unknown>>(\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>,\n options: {\n seenTxids: Store<Set<Txid>>\n }\n): SyncConfig<T> {\n const { seenTxids } = options\n\n // Store for the relation schema information\n const relationSchema = new Store<string | undefined>(undefined)\n\n /**\n * Get the sync metadata for insert operations\n * @returns Record containing relation information\n */\n const getSyncMetadata = (): Record<string, unknown> => {\n // Use the stored schema if available, otherwise default to 'public'\n const schema = relationSchema.state || `public`\n\n return {\n relation: shapeOptions.params?.table\n ? [schema, shapeOptions.params.table]\n : undefined,\n }\n }\n\n let unsubscribeStream: () => void\n\n return {\n sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {\n const { begin, write, commit, markReady, truncate, collection } = params\n\n // Abort controller for the stream - wraps the signal if provided\n const abortController = new AbortController()\n\n if (shapeOptions.signal) {\n shapeOptions.signal.addEventListener(\n `abort`,\n () => {\n abortController.abort()\n },\n {\n once: true,\n }\n )\n if (shapeOptions.signal.aborted) {\n abortController.abort()\n }\n }\n\n const stream = new ShapeStream({\n ...shapeOptions,\n signal: abortController.signal,\n onError: (errorParams) => {\n // Just immediately mark ready if there's an error to avoid blocking\n // apps waiting for `.preload()` to finish.\n // Note that Electric sends a 409 error on a `must-refetch` message, but the\n // ShapeStream handled this and it will not reach this handler, therefor\n // this markReady will not be triggers by a `must-refetch`.\n markReady()\n\n if (shapeOptions.onError) {\n return shapeOptions.onError(errorParams)\n } else {\n console.error(\n `An error occurred while syncing collection: ${collection.id}, \\n` +\n `it has been marked as ready to avoid blocking apps waiting for '.preload()' to finish. \\n` +\n `You can provide an 'onError' handler on the shapeOptions to handle this error, and this message will not be logged.`,\n errorParams\n )\n }\n\n return\n },\n })\n let transactionStarted = false\n const newTxids = new Set<Txid>()\n\n unsubscribeStream = stream.subscribe((messages: Array<Message<T>>) => {\n let hasUpToDate = false\n\n for (const message of messages) {\n // Check for txids in the message and add them to our store\n if (hasTxids(message)) {\n message.headers.txids?.forEach((txid) => newTxids.add(txid))\n }\n\n if (isChangeMessage(message)) {\n // Check if the message contains schema information\n const schema = message.headers.schema\n if (schema && typeof schema === `string`) {\n // Store the schema for future use if it's a valid string\n relationSchema.setState(() => schema)\n }\n\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n write({\n type: message.headers.operation,\n value: message.value,\n // Include the primary key and relation info in the metadata\n metadata: {\n ...message.headers,\n },\n })\n } else if (isUpToDateMessage(message)) {\n hasUpToDate = true\n } else if (isMustRefetchMessage(message)) {\n debug(\n `Received must-refetch message, starting transaction with truncate`\n )\n\n // Start a transaction and truncate the collection\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n truncate()\n\n // Reset hasUpToDate so we continue accumulating changes until next up-to-date\n hasUpToDate = false\n }\n }\n\n if (hasUpToDate) {\n // Commit transaction if one was started\n if (transactionStarted) {\n commit()\n transactionStarted = false\n }\n\n // Mark the collection as ready now that sync is up to date\n markReady()\n\n // Always commit txids when we receive up-to-date, regardless of transaction state\n seenTxids.setState((currentTxids) => {\n const clonedSeen = new Set<Txid>(currentTxids)\n if (newTxids.size > 0) {\n debug(`new txids synced from pg %O`, Array.from(newTxids))\n }\n newTxids.forEach((txid) => clonedSeen.add(txid))\n newTxids.clear()\n return clonedSeen\n })\n }\n })\n\n // Return the unsubscribe function\n return () => {\n // Unsubscribe from the stream\n unsubscribeStream()\n // Abort the abort controller to stop the stream\n abortController.abort()\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata,\n }\n}\n"],"names":["isControlMessage","Store","ExpectedNumberInAwaitTxIdError","TimeoutWaitingForTxIdError","ElectricInsertHandlerMustReturnTxIdError","ElectricUpdateHandlerMustReturnTxIdError","ElectricDeleteHandlerMustReturnTxIdError","ShapeStream","isChangeMessage"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { CollectionConfig, DeleteMutationFnParams, InsertMutationFnParams, SyncConfig, UpdateMutationFnParams, UtilsRecord } from '@tanstack/db';
1
+ import { BaseCollectionConfig, CollectionConfig, Fn, UtilsRecord } from '@tanstack/db';
2
2
  import { StandardSchemaV1 } from '@standard-schema/spec';
3
3
  import { GetExtensions, Row, ShapeStreamOptions } from '@electric-sql/client';
4
4
  /**
@@ -6,187 +6,18 @@ import { GetExtensions, Row, ShapeStreamOptions } from '@electric-sql/client';
6
6
  */
7
7
  export type Txid = number;
8
8
  type InferSchemaOutput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<T> extends Row<unknown> ? StandardSchemaV1.InferOutput<T> : Record<string, unknown> : Record<string, unknown>;
9
- 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;
10
9
  /**
11
10
  * Configuration interface for Electric collection options
12
- * @template TExplicit - The explicit type of items in the collection (highest priority)
13
- * @template TSchema - The schema type for validation and type inference (second priority)
14
- * @template TFallback - The fallback type if no explicit or schema type is provided
15
- *
16
- * @remarks
17
- * Type resolution follows a priority order:
18
- * 1. If you provide an explicit type via generic parameter, it will be used
19
- * 2. If no explicit type is provided but a schema is, the schema's output type will be inferred
20
- * 3. If neither explicit type nor schema is provided, the fallback type will be used
21
- *
22
- * You should provide EITHER an explicit type OR a schema, but not both, as they would conflict.
11
+ * @template T - The type of items in the collection
12
+ * @template TSchema - The schema type for validation
23
13
  */
24
- export interface ElectricCollectionConfig<TExplicit extends Row<unknown> = Row<unknown>, TSchema extends StandardSchemaV1 = never, TFallback extends Row<unknown> = Row<unknown>> {
14
+ export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>, TSchema extends StandardSchemaV1 = never> extends BaseCollectionConfig<T, string | number, TSchema, Record<string, Fn>, {
15
+ txid: Txid | Array<Txid>;
16
+ }> {
25
17
  /**
26
18
  * Configuration options for the ElectricSQL ShapeStream
27
19
  */
28
- shapeOptions: ShapeStreamOptions<GetExtensions<ResolveType<TExplicit, TSchema, TFallback>>>;
29
- /**
30
- * All standard Collection configuration properties
31
- */
32
- id?: string;
33
- schema?: TSchema;
34
- getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>[`getKey`];
35
- sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>[`sync`];
36
- /**
37
- * Optional asynchronous handler function called before an insert operation
38
- * Must return an object containing a txid number or array of txids
39
- * @param params Object containing transaction and collection information
40
- * @returns Promise resolving to an object with txid or txids
41
- * @example
42
- * // Basic Electric insert handler - MUST return { txid: number }
43
- * onInsert: async ({ transaction }) => {
44
- * const newItem = transaction.mutations[0].modified
45
- * const result = await api.todos.create({
46
- * data: newItem
47
- * })
48
- * return { txid: result.txid } // Required for Electric sync matching
49
- * }
50
- *
51
- * @example
52
- * // Insert handler with multiple items - return array of txids
53
- * onInsert: async ({ transaction }) => {
54
- * const items = transaction.mutations.map(m => m.modified)
55
- * const results = await Promise.all(
56
- * items.map(item => api.todos.create({ data: item }))
57
- * )
58
- * return { txid: results.map(r => r.txid) } // Array of txids
59
- * }
60
- *
61
- * @example
62
- * // Insert handler with error handling
63
- * onInsert: async ({ transaction }) => {
64
- * try {
65
- * const newItem = transaction.mutations[0].modified
66
- * const result = await api.createTodo(newItem)
67
- * return { txid: result.txid }
68
- * } catch (error) {
69
- * console.error('Insert failed:', error)
70
- * throw error // This will cause the transaction to fail
71
- * }
72
- * }
73
- *
74
- * @example
75
- * // Insert handler with batch operation - single txid
76
- * onInsert: async ({ transaction }) => {
77
- * const items = transaction.mutations.map(m => m.modified)
78
- * const result = await api.todos.createMany({
79
- * data: items
80
- * })
81
- * return { txid: result.txid } // Single txid for batch operation
82
- * }
83
- */
84
- onInsert?: (params: InsertMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
85
- txid: Txid | Array<Txid>;
86
- }>;
87
- /**
88
- * Optional asynchronous handler function called before an update operation
89
- * Must return an object containing a txid number or array of txids
90
- * @param params Object containing transaction and collection information
91
- * @returns Promise resolving to an object with txid or txids
92
- * @example
93
- * // Basic Electric update handler - MUST return { txid: number }
94
- * onUpdate: async ({ transaction }) => {
95
- * const { original, changes } = transaction.mutations[0]
96
- * const result = await api.todos.update({
97
- * where: { id: original.id },
98
- * data: changes // Only the changed fields
99
- * })
100
- * return { txid: result.txid } // Required for Electric sync matching
101
- * }
102
- *
103
- * @example
104
- * // Update handler with multiple items - return array of txids
105
- * onUpdate: async ({ transaction }) => {
106
- * const updates = await Promise.all(
107
- * transaction.mutations.map(m =>
108
- * api.todos.update({
109
- * where: { id: m.original.id },
110
- * data: m.changes
111
- * })
112
- * )
113
- * )
114
- * return { txid: updates.map(u => u.txid) } // Array of txids
115
- * }
116
- *
117
- * @example
118
- * // Update handler with optimistic rollback
119
- * onUpdate: async ({ transaction }) => {
120
- * const mutation = transaction.mutations[0]
121
- * try {
122
- * const result = await api.updateTodo(mutation.original.id, mutation.changes)
123
- * return { txid: result.txid }
124
- * } catch (error) {
125
- * // Transaction will automatically rollback optimistic changes
126
- * console.error('Update failed, rolling back:', error)
127
- * throw error
128
- * }
129
- * }
130
- */
131
- onUpdate?: (params: UpdateMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
132
- txid: Txid | Array<Txid>;
133
- }>;
134
- /**
135
- * Optional asynchronous handler function called before a delete operation
136
- * Must return an object containing a txid number or array of txids
137
- * @param params Object containing transaction and collection information
138
- * @returns Promise resolving to an object with txid or txids
139
- * @example
140
- * // Basic Electric delete handler - MUST return { txid: number }
141
- * onDelete: async ({ transaction }) => {
142
- * const mutation = transaction.mutations[0]
143
- * const result = await api.todos.delete({
144
- * id: mutation.original.id
145
- * })
146
- * return { txid: result.txid } // Required for Electric sync matching
147
- * }
148
- *
149
- * @example
150
- * // Delete handler with multiple items - return array of txids
151
- * onDelete: async ({ transaction }) => {
152
- * const deletes = await Promise.all(
153
- * transaction.mutations.map(m =>
154
- * api.todos.delete({
155
- * where: { id: m.key }
156
- * })
157
- * )
158
- * )
159
- * return { txid: deletes.map(d => d.txid) } // Array of txids
160
- * }
161
- *
162
- * @example
163
- * // Delete handler with batch operation - single txid
164
- * onDelete: async ({ transaction }) => {
165
- * const idsToDelete = transaction.mutations.map(m => m.original.id)
166
- * const result = await api.todos.deleteMany({
167
- * ids: idsToDelete
168
- * })
169
- * return { txid: result.txid } // Single txid for batch operation
170
- * }
171
- *
172
- * @example
173
- * // Delete handler with optimistic rollback
174
- * onDelete: async ({ transaction }) => {
175
- * const mutation = transaction.mutations[0]
176
- * try {
177
- * const result = await api.deleteTodo(mutation.original.id)
178
- * return { txid: result.txid }
179
- * } catch (error) {
180
- * // Transaction will automatically rollback optimistic changes
181
- * console.error('Delete failed, rolling back:', error)
182
- * throw error
183
- * }
184
- * }
185
- *
186
- */
187
- onDelete?: (params: DeleteMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
188
- txid: Txid | Array<Txid>;
189
- }>;
20
+ shapeOptions: ShapeStreamOptions<GetExtensions<T>>;
190
21
  }
191
22
  /**
192
23
  * Type for the awaitTxId utility function
@@ -201,31 +32,24 @@ export interface ElectricCollectionUtils extends UtilsRecord {
201
32
  /**
202
33
  * Creates Electric collection options for use with a standard Collection
203
34
  *
204
- * @template TExplicit - The explicit type of items in the collection (highest priority)
35
+ * @template T - The explicit type of items in the collection (highest priority)
205
36
  * @template TSchema - The schema type for validation and type inference (second priority)
206
37
  * @template TFallback - The fallback type if no explicit or schema type is provided
207
38
  * @param config - Configuration options for the Electric collection
208
39
  * @returns Collection options with utilities
209
40
  */
210
- 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>): {
211
- sync: SyncConfig<ResolveType<TExplicit, TSchema, TFallback>, string | number>;
212
- onInsert: ((params: InsertMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
213
- txid: Txid | Array<Txid>;
214
- }>) | undefined;
215
- onUpdate: ((params: UpdateMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
216
- txid: Txid | Array<Txid>;
217
- }>) | undefined;
218
- onDelete: ((params: DeleteMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
219
- txid: Txid | Array<Txid>;
220
- }>) | undefined;
221
- utils: {
222
- awaitTxId: AwaitTxIdFn;
223
- };
224
- /**
225
- * All standard Collection configuration properties
226
- */
41
+ export declare function electricCollectionOptions<T extends StandardSchemaV1>(config: ElectricCollectionConfig<InferSchemaOutput<T>, T> & {
42
+ schema: T;
43
+ }): CollectionConfig<InferSchemaOutput<T>, string | number, T> & {
44
+ id?: string;
45
+ utils: ElectricCollectionUtils;
46
+ schema: T;
47
+ };
48
+ export declare function electricCollectionOptions<T extends Row<unknown>>(config: ElectricCollectionConfig<T> & {
49
+ schema?: never;
50
+ }): CollectionConfig<T, string | number> & {
227
51
  id?: string;
228
- schema?: TSchema | undefined;
229
- getKey: (item: ResolveType<TExplicit, TSchema, TFallback>) => string | number;
52
+ utils: ElectricCollectionUtils;
53
+ schema?: never;
230
54
  };
231
55
  export {};
@@ -1,4 +1,4 @@
1
- import { CollectionConfig, DeleteMutationFnParams, InsertMutationFnParams, SyncConfig, UpdateMutationFnParams, UtilsRecord } from '@tanstack/db';
1
+ import { BaseCollectionConfig, CollectionConfig, Fn, UtilsRecord } from '@tanstack/db';
2
2
  import { StandardSchemaV1 } from '@standard-schema/spec';
3
3
  import { GetExtensions, Row, ShapeStreamOptions } from '@electric-sql/client';
4
4
  /**
@@ -6,187 +6,18 @@ import { GetExtensions, Row, ShapeStreamOptions } from '@electric-sql/client';
6
6
  */
7
7
  export type Txid = number;
8
8
  type InferSchemaOutput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<T> extends Row<unknown> ? StandardSchemaV1.InferOutput<T> : Record<string, unknown> : Record<string, unknown>;
9
- 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;
10
9
  /**
11
10
  * Configuration interface for Electric collection options
12
- * @template TExplicit - The explicit type of items in the collection (highest priority)
13
- * @template TSchema - The schema type for validation and type inference (second priority)
14
- * @template TFallback - The fallback type if no explicit or schema type is provided
15
- *
16
- * @remarks
17
- * Type resolution follows a priority order:
18
- * 1. If you provide an explicit type via generic parameter, it will be used
19
- * 2. If no explicit type is provided but a schema is, the schema's output type will be inferred
20
- * 3. If neither explicit type nor schema is provided, the fallback type will be used
21
- *
22
- * You should provide EITHER an explicit type OR a schema, but not both, as they would conflict.
11
+ * @template T - The type of items in the collection
12
+ * @template TSchema - The schema type for validation
23
13
  */
24
- export interface ElectricCollectionConfig<TExplicit extends Row<unknown> = Row<unknown>, TSchema extends StandardSchemaV1 = never, TFallback extends Row<unknown> = Row<unknown>> {
14
+ export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>, TSchema extends StandardSchemaV1 = never> extends BaseCollectionConfig<T, string | number, TSchema, Record<string, Fn>, {
15
+ txid: Txid | Array<Txid>;
16
+ }> {
25
17
  /**
26
18
  * Configuration options for the ElectricSQL ShapeStream
27
19
  */
28
- shapeOptions: ShapeStreamOptions<GetExtensions<ResolveType<TExplicit, TSchema, TFallback>>>;
29
- /**
30
- * All standard Collection configuration properties
31
- */
32
- id?: string;
33
- schema?: TSchema;
34
- getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>[`getKey`];
35
- sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>[`sync`];
36
- /**
37
- * Optional asynchronous handler function called before an insert operation
38
- * Must return an object containing a txid number or array of txids
39
- * @param params Object containing transaction and collection information
40
- * @returns Promise resolving to an object with txid or txids
41
- * @example
42
- * // Basic Electric insert handler - MUST return { txid: number }
43
- * onInsert: async ({ transaction }) => {
44
- * const newItem = transaction.mutations[0].modified
45
- * const result = await api.todos.create({
46
- * data: newItem
47
- * })
48
- * return { txid: result.txid } // Required for Electric sync matching
49
- * }
50
- *
51
- * @example
52
- * // Insert handler with multiple items - return array of txids
53
- * onInsert: async ({ transaction }) => {
54
- * const items = transaction.mutations.map(m => m.modified)
55
- * const results = await Promise.all(
56
- * items.map(item => api.todos.create({ data: item }))
57
- * )
58
- * return { txid: results.map(r => r.txid) } // Array of txids
59
- * }
60
- *
61
- * @example
62
- * // Insert handler with error handling
63
- * onInsert: async ({ transaction }) => {
64
- * try {
65
- * const newItem = transaction.mutations[0].modified
66
- * const result = await api.createTodo(newItem)
67
- * return { txid: result.txid }
68
- * } catch (error) {
69
- * console.error('Insert failed:', error)
70
- * throw error // This will cause the transaction to fail
71
- * }
72
- * }
73
- *
74
- * @example
75
- * // Insert handler with batch operation - single txid
76
- * onInsert: async ({ transaction }) => {
77
- * const items = transaction.mutations.map(m => m.modified)
78
- * const result = await api.todos.createMany({
79
- * data: items
80
- * })
81
- * return { txid: result.txid } // Single txid for batch operation
82
- * }
83
- */
84
- onInsert?: (params: InsertMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
85
- txid: Txid | Array<Txid>;
86
- }>;
87
- /**
88
- * Optional asynchronous handler function called before an update operation
89
- * Must return an object containing a txid number or array of txids
90
- * @param params Object containing transaction and collection information
91
- * @returns Promise resolving to an object with txid or txids
92
- * @example
93
- * // Basic Electric update handler - MUST return { txid: number }
94
- * onUpdate: async ({ transaction }) => {
95
- * const { original, changes } = transaction.mutations[0]
96
- * const result = await api.todos.update({
97
- * where: { id: original.id },
98
- * data: changes // Only the changed fields
99
- * })
100
- * return { txid: result.txid } // Required for Electric sync matching
101
- * }
102
- *
103
- * @example
104
- * // Update handler with multiple items - return array of txids
105
- * onUpdate: async ({ transaction }) => {
106
- * const updates = await Promise.all(
107
- * transaction.mutations.map(m =>
108
- * api.todos.update({
109
- * where: { id: m.original.id },
110
- * data: m.changes
111
- * })
112
- * )
113
- * )
114
- * return { txid: updates.map(u => u.txid) } // Array of txids
115
- * }
116
- *
117
- * @example
118
- * // Update handler with optimistic rollback
119
- * onUpdate: async ({ transaction }) => {
120
- * const mutation = transaction.mutations[0]
121
- * try {
122
- * const result = await api.updateTodo(mutation.original.id, mutation.changes)
123
- * return { txid: result.txid }
124
- * } catch (error) {
125
- * // Transaction will automatically rollback optimistic changes
126
- * console.error('Update failed, rolling back:', error)
127
- * throw error
128
- * }
129
- * }
130
- */
131
- onUpdate?: (params: UpdateMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
132
- txid: Txid | Array<Txid>;
133
- }>;
134
- /**
135
- * Optional asynchronous handler function called before a delete operation
136
- * Must return an object containing a txid number or array of txids
137
- * @param params Object containing transaction and collection information
138
- * @returns Promise resolving to an object with txid or txids
139
- * @example
140
- * // Basic Electric delete handler - MUST return { txid: number }
141
- * onDelete: async ({ transaction }) => {
142
- * const mutation = transaction.mutations[0]
143
- * const result = await api.todos.delete({
144
- * id: mutation.original.id
145
- * })
146
- * return { txid: result.txid } // Required for Electric sync matching
147
- * }
148
- *
149
- * @example
150
- * // Delete handler with multiple items - return array of txids
151
- * onDelete: async ({ transaction }) => {
152
- * const deletes = await Promise.all(
153
- * transaction.mutations.map(m =>
154
- * api.todos.delete({
155
- * where: { id: m.key }
156
- * })
157
- * )
158
- * )
159
- * return { txid: deletes.map(d => d.txid) } // Array of txids
160
- * }
161
- *
162
- * @example
163
- * // Delete handler with batch operation - single txid
164
- * onDelete: async ({ transaction }) => {
165
- * const idsToDelete = transaction.mutations.map(m => m.original.id)
166
- * const result = await api.todos.deleteMany({
167
- * ids: idsToDelete
168
- * })
169
- * return { txid: result.txid } // Single txid for batch operation
170
- * }
171
- *
172
- * @example
173
- * // Delete handler with optimistic rollback
174
- * onDelete: async ({ transaction }) => {
175
- * const mutation = transaction.mutations[0]
176
- * try {
177
- * const result = await api.deleteTodo(mutation.original.id)
178
- * return { txid: result.txid }
179
- * } catch (error) {
180
- * // Transaction will automatically rollback optimistic changes
181
- * console.error('Delete failed, rolling back:', error)
182
- * throw error
183
- * }
184
- * }
185
- *
186
- */
187
- onDelete?: (params: DeleteMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
188
- txid: Txid | Array<Txid>;
189
- }>;
20
+ shapeOptions: ShapeStreamOptions<GetExtensions<T>>;
190
21
  }
191
22
  /**
192
23
  * Type for the awaitTxId utility function
@@ -201,31 +32,24 @@ export interface ElectricCollectionUtils extends UtilsRecord {
201
32
  /**
202
33
  * Creates Electric collection options for use with a standard Collection
203
34
  *
204
- * @template TExplicit - The explicit type of items in the collection (highest priority)
35
+ * @template T - The explicit type of items in the collection (highest priority)
205
36
  * @template TSchema - The schema type for validation and type inference (second priority)
206
37
  * @template TFallback - The fallback type if no explicit or schema type is provided
207
38
  * @param config - Configuration options for the Electric collection
208
39
  * @returns Collection options with utilities
209
40
  */
210
- 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>): {
211
- sync: SyncConfig<ResolveType<TExplicit, TSchema, TFallback>, string | number>;
212
- onInsert: ((params: InsertMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
213
- txid: Txid | Array<Txid>;
214
- }>) | undefined;
215
- onUpdate: ((params: UpdateMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
216
- txid: Txid | Array<Txid>;
217
- }>) | undefined;
218
- onDelete: ((params: DeleteMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>) => Promise<{
219
- txid: Txid | Array<Txid>;
220
- }>) | undefined;
221
- utils: {
222
- awaitTxId: AwaitTxIdFn;
223
- };
224
- /**
225
- * All standard Collection configuration properties
226
- */
41
+ export declare function electricCollectionOptions<T extends StandardSchemaV1>(config: ElectricCollectionConfig<InferSchemaOutput<T>, T> & {
42
+ schema: T;
43
+ }): CollectionConfig<InferSchemaOutput<T>, string | number, T> & {
44
+ id?: string;
45
+ utils: ElectricCollectionUtils;
46
+ schema: T;
47
+ };
48
+ export declare function electricCollectionOptions<T extends Row<unknown>>(config: ElectricCollectionConfig<T> & {
49
+ schema?: never;
50
+ }): CollectionConfig<T, string | number> & {
227
51
  id?: string;
228
- schema?: TSchema | undefined;
229
- getKey: (item: ResolveType<TExplicit, TSchema, TFallback>) => string | number;
52
+ utils: ElectricCollectionUtils;
53
+ schema?: never;
230
54
  };
231
55
  export {};
@@ -14,12 +14,9 @@ function hasTxids(message) {
14
14
  }
15
15
  function electricCollectionOptions(config) {
16
16
  const seenTxids = new Store(/* @__PURE__ */ new Set([]));
17
- const sync = createElectricSync(
18
- config.shapeOptions,
19
- {
20
- seenTxids
21
- }
22
- );
17
+ const sync = createElectricSync(config.shapeOptions, {
18
+ seenTxids
19
+ });
23
20
  const awaitTxId = async (txId, timeout = 3e4) => {
24
21
  debug(`awaitTxId called with txid %d`, txId);
25
22
  if (typeof txId !== `number`) {
@@ -1 +1 @@
1
- {"version":3,"file":"electric.js","sources":["../../src/electric.ts"],"sourcesContent":["import {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n} from \"@electric-sql/client\"\nimport { Store } from \"@tanstack/store\"\nimport DebugModule from \"debug\"\nimport {\n ElectricDeleteHandlerMustReturnTxIdError,\n ElectricInsertHandlerMustReturnTxIdError,\n ElectricUpdateHandlerMustReturnTxIdError,\n ExpectedNumberInAwaitTxIdError,\n TimeoutWaitingForTxIdError,\n} from \"./errors\"\nimport type {\n CollectionConfig,\n DeleteMutationFnParams,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type {\n ControlMessage,\n GetExtensions,\n Message,\n Row,\n ShapeStreamOptions,\n} from \"@electric-sql/client\"\n\nconst debug = DebugModule.debug(`ts/db:electric`)\n\n/**\n * Type representing a transaction ID in ElectricSQL\n */\nexport type Txid = number\n\n// The `InferSchemaOutput` and `ResolveType` are copied from the `@tanstack/db` package\n// but we modified `InferSchemaOutput` slightly to restrict the schema output to `Row<unknown>`\n// This is needed in order for `GetExtensions` to be able to infer the parser extensions type from the schema\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends Row<unknown>\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\ntype ResolveType<\n TExplicit extends Row<unknown> = Row<unknown>,\n TSchema extends StandardSchemaV1 = never,\n TFallback extends object = Record<string, unknown>,\n> =\n unknown extends GetExtensions<TExplicit>\n ? [TSchema] extends [never]\n ? TFallback\n : InferSchemaOutput<TSchema>\n : TExplicit\n\n/**\n * Configuration interface for Electric collection options\n * @template TExplicit - The explicit type of items in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TFallback - The fallback type if no explicit or schema type is provided\n *\n * @remarks\n * Type resolution follows a priority order:\n * 1. If you provide an explicit type via generic parameter, it will be used\n * 2. If no explicit type is provided but a schema is, the schema's output type will be inferred\n * 3. If neither explicit type nor schema is provided, the fallback type will be used\n *\n * You should provide EITHER an explicit type OR a schema, but not both, as they would conflict.\n */\nexport interface ElectricCollectionConfig<\n TExplicit extends Row<unknown> = Row<unknown>,\n TSchema extends StandardSchemaV1 = never,\n TFallback extends Row<unknown> = Row<unknown>,\n> {\n /**\n * Configuration options for the ElectricSQL ShapeStream\n */\n shapeOptions: ShapeStreamOptions<\n GetExtensions<ResolveType<TExplicit, TSchema, TFallback>>\n >\n\n /**\n * All standard Collection configuration properties\n */\n id?: string\n schema?: TSchema\n getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>[`getKey`]\n sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>[`sync`]\n\n /**\n * Optional asynchronous handler function called before an insert operation\n * Must return an object containing a txid number or array of txids\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to an object with txid or txids\n * @example\n * // Basic Electric insert handler - MUST return { txid: number }\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * const result = await api.todos.create({\n * data: newItem\n * })\n * return { txid: result.txid } // Required for Electric sync matching\n * }\n *\n * @example\n * // Insert handler with multiple items - return array of txids\n * onInsert: async ({ transaction }) => {\n * const items = transaction.mutations.map(m => m.modified)\n * const results = await Promise.all(\n * items.map(item => api.todos.create({ data: item }))\n * )\n * return { txid: results.map(r => r.txid) } // Array of txids\n * }\n *\n * @example\n * // Insert handler with error handling\n * onInsert: async ({ transaction }) => {\n * try {\n * const newItem = transaction.mutations[0].modified\n * const result = await api.createTodo(newItem)\n * return { txid: result.txid }\n * } catch (error) {\n * console.error('Insert failed:', error)\n * throw error // This will cause the transaction to fail\n * }\n * }\n *\n * @example\n * // Insert handler with batch operation - single txid\n * onInsert: async ({ transaction }) => {\n * const items = transaction.mutations.map(m => m.modified)\n * const result = await api.todos.createMany({\n * data: items\n * })\n * return { txid: result.txid } // Single txid for batch operation\n * }\n */\n onInsert?: (\n params: InsertMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>\n ) => Promise<{ txid: Txid | Array<Txid> }>\n\n /**\n * Optional asynchronous handler function called before an update operation\n * Must return an object containing a txid number or array of txids\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to an object with txid or txids\n * @example\n * // Basic Electric update handler - MUST return { txid: number }\n * onUpdate: async ({ transaction }) => {\n * const { original, changes } = transaction.mutations[0]\n * const result = await api.todos.update({\n * where: { id: original.id },\n * data: changes // Only the changed fields\n * })\n * return { txid: result.txid } // Required for Electric sync matching\n * }\n *\n * @example\n * // Update handler with multiple items - return array of txids\n * onUpdate: async ({ transaction }) => {\n * const updates = await Promise.all(\n * transaction.mutations.map(m =>\n * api.todos.update({\n * where: { id: m.original.id },\n * data: m.changes\n * })\n * )\n * )\n * return { txid: updates.map(u => u.txid) } // Array of txids\n * }\n *\n * @example\n * // Update handler with optimistic rollback\n * onUpdate: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * try {\n * const result = await api.updateTodo(mutation.original.id, mutation.changes)\n * return { txid: result.txid }\n * } catch (error) {\n * // Transaction will automatically rollback optimistic changes\n * console.error('Update failed, rolling back:', error)\n * throw error\n * }\n * }\n */\n onUpdate?: (\n params: UpdateMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>\n ) => Promise<{ txid: Txid | Array<Txid> }>\n\n /**\n * Optional asynchronous handler function called before a delete operation\n * Must return an object containing a txid number or array of txids\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to an object with txid or txids\n * @example\n * // Basic Electric delete handler - MUST return { txid: number }\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * const result = await api.todos.delete({\n * id: mutation.original.id\n * })\n * return { txid: result.txid } // Required for Electric sync matching\n * }\n *\n * @example\n * // Delete handler with multiple items - return array of txids\n * onDelete: async ({ transaction }) => {\n * const deletes = await Promise.all(\n * transaction.mutations.map(m =>\n * api.todos.delete({\n * where: { id: m.key }\n * })\n * )\n * )\n * return { txid: deletes.map(d => d.txid) } // Array of txids\n * }\n *\n * @example\n * // Delete handler with batch operation - single txid\n * onDelete: async ({ transaction }) => {\n * const idsToDelete = transaction.mutations.map(m => m.original.id)\n * const result = await api.todos.deleteMany({\n * ids: idsToDelete\n * })\n * return { txid: result.txid } // Single txid for batch operation\n * }\n *\n * @example\n * // Delete handler with optimistic rollback\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * try {\n * const result = await api.deleteTodo(mutation.original.id)\n * return { txid: result.txid }\n * } catch (error) {\n * // Transaction will automatically rollback optimistic changes\n * console.error('Delete failed, rolling back:', error)\n * throw error\n * }\n * }\n *\n */\n onDelete?: (\n params: DeleteMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>\n ) => Promise<{ txid: Txid | Array<Txid> }>\n}\n\nfunction isUpToDateMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { up_to_date: true } {\n return isControlMessage(message) && message.headers.control === `up-to-date`\n}\n\nfunction isMustRefetchMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { headers: { control: `must-refetch` } } {\n return isControlMessage(message) && message.headers.control === `must-refetch`\n}\n\n// Check if a message contains txids in its headers\nfunction hasTxids<T extends Row<unknown>>(\n message: Message<T>\n): message is Message<T> & { headers: { txids?: Array<Txid> } } {\n return `txids` in message.headers && Array.isArray(message.headers.txids)\n}\n\n/**\n * Type for the awaitTxId utility function\n */\nexport type AwaitTxIdFn = (txId: Txid, timeout?: number) => Promise<boolean>\n\n/**\n * Electric collection utilities type\n */\nexport interface ElectricCollectionUtils extends UtilsRecord {\n awaitTxId: AwaitTxIdFn\n}\n\n/**\n * Creates Electric collection options for use with a standard Collection\n *\n * @template TExplicit - The explicit type of items in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TFallback - The fallback type if no explicit or schema type is provided\n * @param config - Configuration options for the Electric collection\n * @returns Collection options with utilities\n */\nexport function electricCollectionOptions<\n TExplicit extends Row<unknown> = Row<unknown>,\n TSchema extends StandardSchemaV1 = never,\n TFallback extends Row<unknown> = Row<unknown>,\n>(config: ElectricCollectionConfig<TExplicit, TSchema, TFallback>) {\n const seenTxids = new Store<Set<Txid>>(new Set([]))\n const sync = createElectricSync<ResolveType<TExplicit, TSchema, TFallback>>(\n config.shapeOptions,\n {\n seenTxids,\n }\n )\n\n /**\n * Wait for a specific transaction ID to be synced\n * @param txId The transaction ID to wait for as a number\n * @param timeout Optional timeout in milliseconds (defaults to 30000ms)\n * @returns Promise that resolves when the txId is synced\n */\n const awaitTxId: AwaitTxIdFn = async (\n txId: Txid,\n timeout: number = 30000\n ): Promise<boolean> => {\n debug(`awaitTxId called with txid %d`, txId)\n if (typeof txId !== `number`) {\n throw new ExpectedNumberInAwaitTxIdError(typeof txId)\n }\n\n const hasTxid = seenTxids.state.has(txId)\n if (hasTxid) return true\n\n return new Promise((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n unsubscribe()\n reject(new TimeoutWaitingForTxIdError(txId))\n }, timeout)\n\n const unsubscribe = seenTxids.subscribe(() => {\n if (seenTxids.state.has(txId)) {\n debug(`awaitTxId found match for txid %o`, txId)\n clearTimeout(timeoutId)\n unsubscribe()\n resolve(true)\n }\n })\n })\n }\n\n // Create wrapper handlers for direct persistence operations that handle txid awaiting\n const wrappedOnInsert = config.onInsert\n ? async (\n params: InsertMutationFnParams<\n ResolveType<TExplicit, TSchema, TFallback>\n >\n ) => {\n // Runtime check (that doesn't follow type)\n // eslint-disable-next-line\n const handlerResult = (await config.onInsert!(params)) ?? {}\n const txid = (handlerResult as { txid?: Txid | Array<Txid> }).txid\n\n if (!txid) {\n throw new ElectricInsertHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = config.onUpdate\n ? async (\n params: UpdateMutationFnParams<\n ResolveType<TExplicit, TSchema, TFallback>\n >\n ) => {\n // Runtime check (that doesn't follow type)\n // eslint-disable-next-line\n const handlerResult = (await config.onUpdate!(params)) ?? {}\n const txid = (handlerResult as { txid?: Txid | Array<Txid> }).txid\n\n if (!txid) {\n throw new ElectricUpdateHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = config.onDelete\n ? async (\n params: DeleteMutationFnParams<\n ResolveType<TExplicit, TSchema, TFallback>\n >\n ) => {\n const handlerResult = await config.onDelete!(params)\n if (!handlerResult.txid) {\n throw new ElectricDeleteHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(handlerResult.txid)) {\n await Promise.all(handlerResult.txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(handlerResult.txid)\n }\n\n return handlerResult\n }\n : undefined\n\n // Extract standard Collection config properties\n const {\n shapeOptions: _shapeOptions,\n onInsert: _onInsert,\n onUpdate: _onUpdate,\n onDelete: _onDelete,\n ...restConfig\n } = config\n\n return {\n ...restConfig,\n sync,\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n awaitTxId,\n },\n }\n}\n\n/**\n * Internal function to create ElectricSQL sync configuration\n */\nfunction createElectricSync<T extends Row<unknown>>(\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>,\n options: {\n seenTxids: Store<Set<Txid>>\n }\n): SyncConfig<T> {\n const { seenTxids } = options\n\n // Store for the relation schema information\n const relationSchema = new Store<string | undefined>(undefined)\n\n /**\n * Get the sync metadata for insert operations\n * @returns Record containing relation information\n */\n const getSyncMetadata = (): Record<string, unknown> => {\n // Use the stored schema if available, otherwise default to 'public'\n const schema = relationSchema.state || `public`\n\n return {\n relation: shapeOptions.params?.table\n ? [schema, shapeOptions.params.table]\n : undefined,\n }\n }\n\n let unsubscribeStream: () => void\n\n return {\n sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {\n const { begin, write, commit, markReady, truncate, collection } = params\n\n // Abort controller for the stream - wraps the signal if provided\n const abortController = new AbortController()\n\n if (shapeOptions.signal) {\n shapeOptions.signal.addEventListener(\n `abort`,\n () => {\n abortController.abort()\n },\n {\n once: true,\n }\n )\n if (shapeOptions.signal.aborted) {\n abortController.abort()\n }\n }\n\n const stream = new ShapeStream({\n ...shapeOptions,\n signal: abortController.signal,\n onError: (errorParams) => {\n // Just immediately mark ready if there's an error to avoid blocking\n // apps waiting for `.preload()` to finish.\n // Note that Electric sends a 409 error on a `must-refetch` message, but the\n // ShapeStream handled this and it will not reach this handler, therefor\n // this markReady will not be triggers by a `must-refetch`.\n markReady()\n\n if (shapeOptions.onError) {\n return shapeOptions.onError(errorParams)\n } else {\n console.error(\n `An error occurred while syncing collection: ${collection.id}, \\n` +\n `it has been marked as ready to avoid blocking apps waiting for '.preload()' to finish. \\n` +\n `You can provide an 'onError' handler on the shapeOptions to handle this error, and this message will not be logged.`,\n errorParams\n )\n }\n\n return\n },\n })\n let transactionStarted = false\n const newTxids = new Set<Txid>()\n\n unsubscribeStream = stream.subscribe((messages: Array<Message<T>>) => {\n let hasUpToDate = false\n\n for (const message of messages) {\n // Check for txids in the message and add them to our store\n if (hasTxids(message)) {\n message.headers.txids?.forEach((txid) => newTxids.add(txid))\n }\n\n if (isChangeMessage(message)) {\n // Check if the message contains schema information\n const schema = message.headers.schema\n if (schema && typeof schema === `string`) {\n // Store the schema for future use if it's a valid string\n relationSchema.setState(() => schema)\n }\n\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n write({\n type: message.headers.operation,\n value: message.value,\n // Include the primary key and relation info in the metadata\n metadata: {\n ...message.headers,\n },\n })\n } else if (isUpToDateMessage(message)) {\n hasUpToDate = true\n } else if (isMustRefetchMessage(message)) {\n debug(\n `Received must-refetch message, starting transaction with truncate`\n )\n\n // Start a transaction and truncate the collection\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n truncate()\n\n // Reset hasUpToDate so we continue accumulating changes until next up-to-date\n hasUpToDate = false\n }\n }\n\n if (hasUpToDate) {\n // Commit transaction if one was started\n if (transactionStarted) {\n commit()\n transactionStarted = false\n }\n\n // Mark the collection as ready now that sync is up to date\n markReady()\n\n // Always commit txids when we receive up-to-date, regardless of transaction state\n seenTxids.setState((currentTxids) => {\n const clonedSeen = new Set<Txid>(currentTxids)\n if (newTxids.size > 0) {\n debug(`new txids synced from pg %O`, Array.from(newTxids))\n }\n newTxids.forEach((txid) => clonedSeen.add(txid))\n newTxids.clear()\n return clonedSeen\n })\n }\n })\n\n // Return the unsubscribe function\n return () => {\n // Unsubscribe from the stream\n unsubscribeStream()\n // Abort the abort controller to stop the stream\n abortController.abort()\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata,\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"electric.js","sources":["../../src/electric.ts"],"sourcesContent":["import {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n} from \"@electric-sql/client\"\nimport { Store } from \"@tanstack/store\"\nimport DebugModule from \"debug\"\nimport {\n ElectricDeleteHandlerMustReturnTxIdError,\n ElectricInsertHandlerMustReturnTxIdError,\n ElectricUpdateHandlerMustReturnTxIdError,\n ExpectedNumberInAwaitTxIdError,\n TimeoutWaitingForTxIdError,\n} from \"./errors\"\nimport type {\n BaseCollectionConfig,\n CollectionConfig,\n DeleteMutationFnParams,\n Fn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type {\n ControlMessage,\n GetExtensions,\n Message,\n Row,\n ShapeStreamOptions,\n} from \"@electric-sql/client\"\n\nconst debug = DebugModule.debug(`ts/db:electric`)\n\n/**\n * Type representing a transaction ID in ElectricSQL\n */\nexport type Txid = number\n\n// The `InferSchemaOutput` and `ResolveType` are copied from the `@tanstack/db` package\n// but we modified `InferSchemaOutput` slightly to restrict the schema output to `Row<unknown>`\n// This is needed in order for `GetExtensions` to be able to infer the parser extensions type from the schema\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends Row<unknown>\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Configuration interface for Electric collection options\n * @template T - The type of items in the collection\n * @template TSchema - The schema type for validation\n */\nexport interface ElectricCollectionConfig<\n T extends Row<unknown> = Row<unknown>,\n TSchema extends StandardSchemaV1 = never,\n> extends BaseCollectionConfig<\n T,\n string | number,\n TSchema,\n Record<string, Fn>,\n { txid: Txid | Array<Txid> }\n > {\n /**\n * Configuration options for the ElectricSQL ShapeStream\n */\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>\n}\n\nfunction isUpToDateMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { up_to_date: true } {\n return isControlMessage(message) && message.headers.control === `up-to-date`\n}\n\nfunction isMustRefetchMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { headers: { control: `must-refetch` } } {\n return isControlMessage(message) && message.headers.control === `must-refetch`\n}\n\n// Check if a message contains txids in its headers\nfunction hasTxids<T extends Row<unknown>>(\n message: Message<T>\n): message is Message<T> & { headers: { txids?: Array<Txid> } } {\n return `txids` in message.headers && Array.isArray(message.headers.txids)\n}\n\n/**\n * Type for the awaitTxId utility function\n */\nexport type AwaitTxIdFn = (txId: Txid, timeout?: number) => Promise<boolean>\n\n/**\n * Electric collection utilities type\n */\nexport interface ElectricCollectionUtils extends UtilsRecord {\n awaitTxId: AwaitTxIdFn\n}\n\n/**\n * Creates Electric collection options for use with a standard Collection\n *\n * @template T - The explicit type of items in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TFallback - The fallback type if no explicit or schema type is provided\n * @param config - Configuration options for the Electric collection\n * @returns Collection options with utilities\n */\n\n// Overload for when schema is provided\nexport function electricCollectionOptions<T extends StandardSchemaV1>(\n config: ElectricCollectionConfig<InferSchemaOutput<T>, T> & {\n schema: T\n }\n): CollectionConfig<InferSchemaOutput<T>, string | number, T> & {\n id?: string\n utils: ElectricCollectionUtils\n schema: T\n}\n\n// Overload for when no schema is provided\nexport function electricCollectionOptions<T extends Row<unknown>>(\n config: ElectricCollectionConfig<T> & {\n schema?: never // prohibit schema\n }\n): CollectionConfig<T, string | number> & {\n id?: string\n utils: ElectricCollectionUtils\n schema?: never // no schema in the result\n}\n\nexport function electricCollectionOptions(\n config: ElectricCollectionConfig<any, any>\n): CollectionConfig<any, string | number, any> & {\n id?: string\n utils: ElectricCollectionUtils\n schema?: any\n} {\n const seenTxids = new Store<Set<Txid>>(new Set([]))\n const sync = createElectricSync<any>(config.shapeOptions, {\n seenTxids,\n })\n\n /**\n * Wait for a specific transaction ID to be synced\n * @param txId The transaction ID to wait for as a number\n * @param timeout Optional timeout in milliseconds (defaults to 30000ms)\n * @returns Promise that resolves when the txId is synced\n */\n const awaitTxId: AwaitTxIdFn = async (\n txId: Txid,\n timeout: number = 30000\n ): Promise<boolean> => {\n debug(`awaitTxId called with txid %d`, txId)\n if (typeof txId !== `number`) {\n throw new ExpectedNumberInAwaitTxIdError(typeof txId)\n }\n\n const hasTxid = seenTxids.state.has(txId)\n if (hasTxid) return true\n\n return new Promise((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n unsubscribe()\n reject(new TimeoutWaitingForTxIdError(txId))\n }, timeout)\n\n const unsubscribe = seenTxids.subscribe(() => {\n if (seenTxids.state.has(txId)) {\n debug(`awaitTxId found match for txid %o`, txId)\n clearTimeout(timeoutId)\n unsubscribe()\n resolve(true)\n }\n })\n })\n }\n\n // Create wrapper handlers for direct persistence operations that handle txid awaiting\n const wrappedOnInsert = config.onInsert\n ? async (params: InsertMutationFnParams<any>) => {\n // Runtime check (that doesn't follow type)\n\n const handlerResult = (await config.onInsert!(params)) ?? {}\n const txid = (handlerResult as { txid?: Txid | Array<Txid> }).txid\n\n if (!txid) {\n throw new ElectricInsertHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = config.onUpdate\n ? async (params: UpdateMutationFnParams<any>) => {\n // Runtime check (that doesn't follow type)\n\n const handlerResult = (await config.onUpdate!(params)) ?? {}\n const txid = (handlerResult as { txid?: Txid | Array<Txid> }).txid\n\n if (!txid) {\n throw new ElectricUpdateHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = config.onDelete\n ? async (params: DeleteMutationFnParams<any>) => {\n const handlerResult = await config.onDelete!(params)\n if (!handlerResult.txid) {\n throw new ElectricDeleteHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(handlerResult.txid)) {\n await Promise.all(handlerResult.txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(handlerResult.txid)\n }\n\n return handlerResult\n }\n : undefined\n\n // Extract standard Collection config properties\n const {\n shapeOptions: _shapeOptions,\n onInsert: _onInsert,\n onUpdate: _onUpdate,\n onDelete: _onDelete,\n ...restConfig\n } = config\n\n return {\n ...restConfig,\n sync,\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n awaitTxId,\n },\n }\n}\n\n/**\n * Internal function to create ElectricSQL sync configuration\n */\nfunction createElectricSync<T extends Row<unknown>>(\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>,\n options: {\n seenTxids: Store<Set<Txid>>\n }\n): SyncConfig<T> {\n const { seenTxids } = options\n\n // Store for the relation schema information\n const relationSchema = new Store<string | undefined>(undefined)\n\n /**\n * Get the sync metadata for insert operations\n * @returns Record containing relation information\n */\n const getSyncMetadata = (): Record<string, unknown> => {\n // Use the stored schema if available, otherwise default to 'public'\n const schema = relationSchema.state || `public`\n\n return {\n relation: shapeOptions.params?.table\n ? [schema, shapeOptions.params.table]\n : undefined,\n }\n }\n\n let unsubscribeStream: () => void\n\n return {\n sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {\n const { begin, write, commit, markReady, truncate, collection } = params\n\n // Abort controller for the stream - wraps the signal if provided\n const abortController = new AbortController()\n\n if (shapeOptions.signal) {\n shapeOptions.signal.addEventListener(\n `abort`,\n () => {\n abortController.abort()\n },\n {\n once: true,\n }\n )\n if (shapeOptions.signal.aborted) {\n abortController.abort()\n }\n }\n\n const stream = new ShapeStream({\n ...shapeOptions,\n signal: abortController.signal,\n onError: (errorParams) => {\n // Just immediately mark ready if there's an error to avoid blocking\n // apps waiting for `.preload()` to finish.\n // Note that Electric sends a 409 error on a `must-refetch` message, but the\n // ShapeStream handled this and it will not reach this handler, therefor\n // this markReady will not be triggers by a `must-refetch`.\n markReady()\n\n if (shapeOptions.onError) {\n return shapeOptions.onError(errorParams)\n } else {\n console.error(\n `An error occurred while syncing collection: ${collection.id}, \\n` +\n `it has been marked as ready to avoid blocking apps waiting for '.preload()' to finish. \\n` +\n `You can provide an 'onError' handler on the shapeOptions to handle this error, and this message will not be logged.`,\n errorParams\n )\n }\n\n return\n },\n })\n let transactionStarted = false\n const newTxids = new Set<Txid>()\n\n unsubscribeStream = stream.subscribe((messages: Array<Message<T>>) => {\n let hasUpToDate = false\n\n for (const message of messages) {\n // Check for txids in the message and add them to our store\n if (hasTxids(message)) {\n message.headers.txids?.forEach((txid) => newTxids.add(txid))\n }\n\n if (isChangeMessage(message)) {\n // Check if the message contains schema information\n const schema = message.headers.schema\n if (schema && typeof schema === `string`) {\n // Store the schema for future use if it's a valid string\n relationSchema.setState(() => schema)\n }\n\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n write({\n type: message.headers.operation,\n value: message.value,\n // Include the primary key and relation info in the metadata\n metadata: {\n ...message.headers,\n },\n })\n } else if (isUpToDateMessage(message)) {\n hasUpToDate = true\n } else if (isMustRefetchMessage(message)) {\n debug(\n `Received must-refetch message, starting transaction with truncate`\n )\n\n // Start a transaction and truncate the collection\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n truncate()\n\n // Reset hasUpToDate so we continue accumulating changes until next up-to-date\n hasUpToDate = false\n }\n }\n\n if (hasUpToDate) {\n // Commit transaction if one was started\n if (transactionStarted) {\n commit()\n transactionStarted = false\n }\n\n // Mark the collection as ready now that sync is up to date\n markReady()\n\n // Always commit txids when we receive up-to-date, regardless of transaction state\n seenTxids.setState((currentTxids) => {\n const clonedSeen = new Set<Txid>(currentTxids)\n if (newTxids.size > 0) {\n debug(`new txids synced from pg %O`, Array.from(newTxids))\n }\n newTxids.forEach((txid) => clonedSeen.add(txid))\n newTxids.clear()\n return clonedSeen\n })\n }\n })\n\n // Return the unsubscribe function\n return () => {\n // Unsubscribe from the stream\n unsubscribeStream()\n // Abort the abort controller to stop the stream\n abortController.abort()\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata,\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@tanstack/electric-db-collection",
3
3
  "description": "ElectricSQL collection for TanStack DB",
4
- "version": "0.1.19",
4
+ "version": "0.1.21",
5
5
  "dependencies": {
6
6
  "@electric-sql/client": "1.0.9",
7
7
  "@standard-schema/spec": "^1.0.0",
8
8
  "@tanstack/store": "^0.7.5",
9
9
  "debug": "^4.4.1",
10
- "@tanstack/db": "0.2.4"
10
+ "@tanstack/db": "0.3.0"
11
11
  },
12
12
  "devDependencies": {
13
13
  "@types/debug": "^4.1.12",
package/src/electric.ts CHANGED
@@ -13,8 +13,10 @@ import {
13
13
  TimeoutWaitingForTxIdError,
14
14
  } from "./errors"
15
15
  import type {
16
+ BaseCollectionConfig,
16
17
  CollectionConfig,
17
18
  DeleteMutationFnParams,
19
+ Fn,
18
20
  InsertMutationFnParams,
19
21
  SyncConfig,
20
22
  UpdateMutationFnParams,
@@ -45,207 +47,25 @@ type InferSchemaOutput<T> = T extends StandardSchemaV1
45
47
  : Record<string, unknown>
46
48
  : Record<string, unknown>
47
49
 
48
- type ResolveType<
49
- TExplicit extends Row<unknown> = Row<unknown>,
50
- TSchema extends StandardSchemaV1 = never,
51
- TFallback extends object = Record<string, unknown>,
52
- > =
53
- unknown extends GetExtensions<TExplicit>
54
- ? [TSchema] extends [never]
55
- ? TFallback
56
- : InferSchemaOutput<TSchema>
57
- : TExplicit
58
-
59
50
  /**
60
51
  * Configuration interface for Electric collection options
61
- * @template TExplicit - The explicit type of items in the collection (highest priority)
62
- * @template TSchema - The schema type for validation and type inference (second priority)
63
- * @template TFallback - The fallback type if no explicit or schema type is provided
64
- *
65
- * @remarks
66
- * Type resolution follows a priority order:
67
- * 1. If you provide an explicit type via generic parameter, it will be used
68
- * 2. If no explicit type is provided but a schema is, the schema's output type will be inferred
69
- * 3. If neither explicit type nor schema is provided, the fallback type will be used
70
- *
71
- * You should provide EITHER an explicit type OR a schema, but not both, as they would conflict.
52
+ * @template T - The type of items in the collection
53
+ * @template TSchema - The schema type for validation
72
54
  */
73
55
  export interface ElectricCollectionConfig<
74
- TExplicit extends Row<unknown> = Row<unknown>,
56
+ T extends Row<unknown> = Row<unknown>,
75
57
  TSchema extends StandardSchemaV1 = never,
76
- TFallback extends Row<unknown> = Row<unknown>,
77
- > {
58
+ > extends BaseCollectionConfig<
59
+ T,
60
+ string | number,
61
+ TSchema,
62
+ Record<string, Fn>,
63
+ { txid: Txid | Array<Txid> }
64
+ > {
78
65
  /**
79
66
  * Configuration options for the ElectricSQL ShapeStream
80
67
  */
81
- shapeOptions: ShapeStreamOptions<
82
- GetExtensions<ResolveType<TExplicit, TSchema, TFallback>>
83
- >
84
-
85
- /**
86
- * All standard Collection configuration properties
87
- */
88
- id?: string
89
- schema?: TSchema
90
- getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>[`getKey`]
91
- sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TFallback>>[`sync`]
92
-
93
- /**
94
- * Optional asynchronous handler function called before an insert operation
95
- * Must return an object containing a txid number or array of txids
96
- * @param params Object containing transaction and collection information
97
- * @returns Promise resolving to an object with txid or txids
98
- * @example
99
- * // Basic Electric insert handler - MUST return { txid: number }
100
- * onInsert: async ({ transaction }) => {
101
- * const newItem = transaction.mutations[0].modified
102
- * const result = await api.todos.create({
103
- * data: newItem
104
- * })
105
- * return { txid: result.txid } // Required for Electric sync matching
106
- * }
107
- *
108
- * @example
109
- * // Insert handler with multiple items - return array of txids
110
- * onInsert: async ({ transaction }) => {
111
- * const items = transaction.mutations.map(m => m.modified)
112
- * const results = await Promise.all(
113
- * items.map(item => api.todos.create({ data: item }))
114
- * )
115
- * return { txid: results.map(r => r.txid) } // Array of txids
116
- * }
117
- *
118
- * @example
119
- * // Insert handler with error handling
120
- * onInsert: async ({ transaction }) => {
121
- * try {
122
- * const newItem = transaction.mutations[0].modified
123
- * const result = await api.createTodo(newItem)
124
- * return { txid: result.txid }
125
- * } catch (error) {
126
- * console.error('Insert failed:', error)
127
- * throw error // This will cause the transaction to fail
128
- * }
129
- * }
130
- *
131
- * @example
132
- * // Insert handler with batch operation - single txid
133
- * onInsert: async ({ transaction }) => {
134
- * const items = transaction.mutations.map(m => m.modified)
135
- * const result = await api.todos.createMany({
136
- * data: items
137
- * })
138
- * return { txid: result.txid } // Single txid for batch operation
139
- * }
140
- */
141
- onInsert?: (
142
- params: InsertMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>
143
- ) => Promise<{ txid: Txid | Array<Txid> }>
144
-
145
- /**
146
- * Optional asynchronous handler function called before an update operation
147
- * Must return an object containing a txid number or array of txids
148
- * @param params Object containing transaction and collection information
149
- * @returns Promise resolving to an object with txid or txids
150
- * @example
151
- * // Basic Electric update handler - MUST return { txid: number }
152
- * onUpdate: async ({ transaction }) => {
153
- * const { original, changes } = transaction.mutations[0]
154
- * const result = await api.todos.update({
155
- * where: { id: original.id },
156
- * data: changes // Only the changed fields
157
- * })
158
- * return { txid: result.txid } // Required for Electric sync matching
159
- * }
160
- *
161
- * @example
162
- * // Update handler with multiple items - return array of txids
163
- * onUpdate: async ({ transaction }) => {
164
- * const updates = await Promise.all(
165
- * transaction.mutations.map(m =>
166
- * api.todos.update({
167
- * where: { id: m.original.id },
168
- * data: m.changes
169
- * })
170
- * )
171
- * )
172
- * return { txid: updates.map(u => u.txid) } // Array of txids
173
- * }
174
- *
175
- * @example
176
- * // Update handler with optimistic rollback
177
- * onUpdate: async ({ transaction }) => {
178
- * const mutation = transaction.mutations[0]
179
- * try {
180
- * const result = await api.updateTodo(mutation.original.id, mutation.changes)
181
- * return { txid: result.txid }
182
- * } catch (error) {
183
- * // Transaction will automatically rollback optimistic changes
184
- * console.error('Update failed, rolling back:', error)
185
- * throw error
186
- * }
187
- * }
188
- */
189
- onUpdate?: (
190
- params: UpdateMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>
191
- ) => Promise<{ txid: Txid | Array<Txid> }>
192
-
193
- /**
194
- * Optional asynchronous handler function called before a delete operation
195
- * Must return an object containing a txid number or array of txids
196
- * @param params Object containing transaction and collection information
197
- * @returns Promise resolving to an object with txid or txids
198
- * @example
199
- * // Basic Electric delete handler - MUST return { txid: number }
200
- * onDelete: async ({ transaction }) => {
201
- * const mutation = transaction.mutations[0]
202
- * const result = await api.todos.delete({
203
- * id: mutation.original.id
204
- * })
205
- * return { txid: result.txid } // Required for Electric sync matching
206
- * }
207
- *
208
- * @example
209
- * // Delete handler with multiple items - return array of txids
210
- * onDelete: async ({ transaction }) => {
211
- * const deletes = await Promise.all(
212
- * transaction.mutations.map(m =>
213
- * api.todos.delete({
214
- * where: { id: m.key }
215
- * })
216
- * )
217
- * )
218
- * return { txid: deletes.map(d => d.txid) } // Array of txids
219
- * }
220
- *
221
- * @example
222
- * // Delete handler with batch operation - single txid
223
- * onDelete: async ({ transaction }) => {
224
- * const idsToDelete = transaction.mutations.map(m => m.original.id)
225
- * const result = await api.todos.deleteMany({
226
- * ids: idsToDelete
227
- * })
228
- * return { txid: result.txid } // Single txid for batch operation
229
- * }
230
- *
231
- * @example
232
- * // Delete handler with optimistic rollback
233
- * onDelete: async ({ transaction }) => {
234
- * const mutation = transaction.mutations[0]
235
- * try {
236
- * const result = await api.deleteTodo(mutation.original.id)
237
- * return { txid: result.txid }
238
- * } catch (error) {
239
- * // Transaction will automatically rollback optimistic changes
240
- * console.error('Delete failed, rolling back:', error)
241
- * throw error
242
- * }
243
- * }
244
- *
245
- */
246
- onDelete?: (
247
- params: DeleteMutationFnParams<ResolveType<TExplicit, TSchema, TFallback>>
248
- ) => Promise<{ txid: Txid | Array<Txid> }>
68
+ shapeOptions: ShapeStreamOptions<GetExtensions<T>>
249
69
  }
250
70
 
251
71
  function isUpToDateMessage<T extends Row<unknown>>(
@@ -282,24 +102,46 @@ export interface ElectricCollectionUtils extends UtilsRecord {
282
102
  /**
283
103
  * Creates Electric collection options for use with a standard Collection
284
104
  *
285
- * @template TExplicit - The explicit type of items in the collection (highest priority)
105
+ * @template T - The explicit type of items in the collection (highest priority)
286
106
  * @template TSchema - The schema type for validation and type inference (second priority)
287
107
  * @template TFallback - The fallback type if no explicit or schema type is provided
288
108
  * @param config - Configuration options for the Electric collection
289
109
  * @returns Collection options with utilities
290
110
  */
291
- export function electricCollectionOptions<
292
- TExplicit extends Row<unknown> = Row<unknown>,
293
- TSchema extends StandardSchemaV1 = never,
294
- TFallback extends Row<unknown> = Row<unknown>,
295
- >(config: ElectricCollectionConfig<TExplicit, TSchema, TFallback>) {
111
+
112
+ // Overload for when schema is provided
113
+ export function electricCollectionOptions<T extends StandardSchemaV1>(
114
+ config: ElectricCollectionConfig<InferSchemaOutput<T>, T> & {
115
+ schema: T
116
+ }
117
+ ): CollectionConfig<InferSchemaOutput<T>, string | number, T> & {
118
+ id?: string
119
+ utils: ElectricCollectionUtils
120
+ schema: T
121
+ }
122
+
123
+ // Overload for when no schema is provided
124
+ export function electricCollectionOptions<T extends Row<unknown>>(
125
+ config: ElectricCollectionConfig<T> & {
126
+ schema?: never // prohibit schema
127
+ }
128
+ ): CollectionConfig<T, string | number> & {
129
+ id?: string
130
+ utils: ElectricCollectionUtils
131
+ schema?: never // no schema in the result
132
+ }
133
+
134
+ export function electricCollectionOptions(
135
+ config: ElectricCollectionConfig<any, any>
136
+ ): CollectionConfig<any, string | number, any> & {
137
+ id?: string
138
+ utils: ElectricCollectionUtils
139
+ schema?: any
140
+ } {
296
141
  const seenTxids = new Store<Set<Txid>>(new Set([]))
297
- const sync = createElectricSync<ResolveType<TExplicit, TSchema, TFallback>>(
298
- config.shapeOptions,
299
- {
300
- seenTxids,
301
- }
302
- )
142
+ const sync = createElectricSync<any>(config.shapeOptions, {
143
+ seenTxids,
144
+ })
303
145
 
304
146
  /**
305
147
  * Wait for a specific transaction ID to be synced
@@ -338,13 +180,9 @@ export function electricCollectionOptions<
338
180
 
339
181
  // Create wrapper handlers for direct persistence operations that handle txid awaiting
340
182
  const wrappedOnInsert = config.onInsert
341
- ? async (
342
- params: InsertMutationFnParams<
343
- ResolveType<TExplicit, TSchema, TFallback>
344
- >
345
- ) => {
183
+ ? async (params: InsertMutationFnParams<any>) => {
346
184
  // Runtime check (that doesn't follow type)
347
- // eslint-disable-next-line
185
+
348
186
  const handlerResult = (await config.onInsert!(params)) ?? {}
349
187
  const txid = (handlerResult as { txid?: Txid | Array<Txid> }).txid
350
188
 
@@ -364,13 +202,9 @@ export function electricCollectionOptions<
364
202
  : undefined
365
203
 
366
204
  const wrappedOnUpdate = config.onUpdate
367
- ? async (
368
- params: UpdateMutationFnParams<
369
- ResolveType<TExplicit, TSchema, TFallback>
370
- >
371
- ) => {
205
+ ? async (params: UpdateMutationFnParams<any>) => {
372
206
  // Runtime check (that doesn't follow type)
373
- // eslint-disable-next-line
207
+
374
208
  const handlerResult = (await config.onUpdate!(params)) ?? {}
375
209
  const txid = (handlerResult as { txid?: Txid | Array<Txid> }).txid
376
210
 
@@ -390,11 +224,7 @@ export function electricCollectionOptions<
390
224
  : undefined
391
225
 
392
226
  const wrappedOnDelete = config.onDelete
393
- ? async (
394
- params: DeleteMutationFnParams<
395
- ResolveType<TExplicit, TSchema, TFallback>
396
- >
397
- ) => {
227
+ ? async (params: DeleteMutationFnParams<any>) => {
398
228
  const handlerResult = await config.onDelete!(params)
399
229
  if (!handlerResult.txid) {
400
230
  throw new ElectricDeleteHandlerMustReturnTxIdError()