@tanstack/electric-db-collection 0.0.9 → 0.0.12

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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const client = require("@electric-sql/client");
4
4
  const store = require("@tanstack/store");
5
5
  const DebugModule = require("debug");
6
+ const errors = require("./errors.cjs");
6
7
  const debug = DebugModule.debug(`ts/db:electric`);
7
8
  function isUpToDateMessage(message) {
8
9
  return client.isControlMessage(message) && message.headers.control === `up-to-date`;
@@ -21,16 +22,14 @@ function electricCollectionOptions(config) {
21
22
  const awaitTxId = async (txId, timeout = 3e4) => {
22
23
  debug(`awaitTxId called with txid %d`, txId);
23
24
  if (typeof txId !== `number`) {
24
- throw new TypeError(
25
- `Expected number in awaitTxId, received ${typeof txId}`
26
- );
25
+ throw new errors.ExpectedNumberInAwaitTxIdError(typeof txId);
27
26
  }
28
27
  const hasTxid = seenTxids.state.has(txId);
29
28
  if (hasTxid) return true;
30
29
  return new Promise((resolve, reject) => {
31
30
  const timeoutId = setTimeout(() => {
32
31
  unsubscribe();
33
- reject(new Error(`Timeout waiting for txId: ${txId}`));
32
+ reject(new errors.TimeoutWaitingForTxIdError(txId));
34
33
  }, timeout);
35
34
  const unsubscribe = seenTxids.subscribe(() => {
36
35
  if (seenTxids.state.has(txId)) {
@@ -46,9 +45,7 @@ function electricCollectionOptions(config) {
46
45
  const handlerResult = await config.onInsert(params) ?? {};
47
46
  const txid = handlerResult.txid;
48
47
  if (!txid) {
49
- throw new Error(
50
- `Electric collection onInsert handler must return a txid or array of txids`
51
- );
48
+ throw new errors.ElectricInsertHandlerMustReturnTxIdError();
52
49
  }
53
50
  if (Array.isArray(txid)) {
54
51
  await Promise.all(txid.map((id) => awaitTxId(id)));
@@ -61,9 +58,7 @@ function electricCollectionOptions(config) {
61
58
  const handlerResult = await config.onUpdate(params) ?? {};
62
59
  const txid = handlerResult.txid;
63
60
  if (!txid) {
64
- throw new Error(
65
- `Electric collection onUpdate handler must return a txid or array of txids`
66
- );
61
+ throw new errors.ElectricUpdateHandlerMustReturnTxIdError();
67
62
  }
68
63
  if (Array.isArray(txid)) {
69
64
  await Promise.all(txid.map((id) => awaitTxId(id)));
@@ -75,9 +70,7 @@ function electricCollectionOptions(config) {
75
70
  const wrappedOnDelete = config.onDelete ? async (params) => {
76
71
  const handlerResult = await config.onDelete(params);
77
72
  if (!handlerResult.txid) {
78
- throw new Error(
79
- `Electric collection onDelete handler must return a txid or array of txids`
80
- );
73
+ throw new errors.ElectricDeleteHandlerMustReturnTxIdError();
81
74
  }
82
75
  if (Array.isArray(handlerResult.txid)) {
83
76
  await Promise.all(handlerResult.txid.map((id) => awaitTxId(id)));
@@ -130,10 +123,10 @@ function createElectricSync(shapeOptions, options) {
130
123
  const stream = new client.ShapeStream({
131
124
  ...shapeOptions,
132
125
  signal: abortController.signal,
133
- onError: (params2) => {
126
+ onError: (errorParams) => {
134
127
  markReady();
135
128
  if (shapeOptions.onError) {
136
- return shapeOptions.onError(params2);
129
+ return shapeOptions.onError(errorParams);
137
130
  }
138
131
  return;
139
132
  }
@@ -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 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 Electric SQL\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\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 TypeError(\n `Expected number in awaitTxId, received ${typeof txId}`\n )\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 Error(`Timeout waiting for txId: ${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 Error(\n `Electric collection onInsert handler must return a txid or array of txids`\n )\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 Error(\n `Electric collection onUpdate handler must return a txid or array of txids`\n )\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 Error(\n `Electric collection onDelete handler must return a txid or array of txids`\n )\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 // Abort controller for the stream - wraps the signal if provided\n const abortController = new AbortController()\n if (shapeOptions.signal) {\n shapeOptions.signal.addEventListener(`abort`, () => {\n abortController.abort()\n })\n if (shapeOptions.signal.aborted) {\n abortController.abort()\n }\n }\n\n let unsubscribeStream: () => void\n\n return {\n sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {\n const { begin, write, commit, markReady } = params\n const stream = new ShapeStream({\n ...shapeOptions,\n signal: abortController.signal,\n onError: (params) => {\n // Just immediately mark ready if there's an error to avoid blocking\n // apps waiting for `.preload()` to finish.\n markReady()\n\n if (shapeOptions.onError) {\n return shapeOptions.onError(params)\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 }\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","ShapeStream","params","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 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 Electric SQL\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\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 // Abort controller for the stream - wraps the signal if provided\n const abortController = new AbortController()\n if (shapeOptions.signal) {\n shapeOptions.signal.addEventListener(`abort`, () => {\n abortController.abort()\n })\n if (shapeOptions.signal.aborted) {\n abortController.abort()\n }\n }\n\n let unsubscribeStream: () => void\n\n return {\n sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {\n const { begin, write, commit, markReady } = params\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 markReady()\n\n if (shapeOptions.onError) {\n return shapeOptions.onError(errorParams)\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 }\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":";;;;;;AA+BA,MAAM,QAAQ,YAAY,MAAM,gBAAgB;AA2NhD,SAAS,kBACP,SACkD;AAClD,SAAOA,OAAAA,iBAAiB,OAAO,KAAK,QAAQ,QAAQ,YAAY;AAClE;AAGA,SAAS,SACP,SAC8D;AAC9D,SAAO,WAAW,QAAQ,WAAW,MAAM,QAAQ,QAAQ,QAAQ,KAAK;AAC1E;AAuBO,SAAS,0BAId,QAAiE;AACjE,QAAM,YAAY,IAAIC,MAAAA,0BAAqB,IAAI,CAAA,CAAE,CAAC;AAClD,QAAM,OAAO;AAAA,IACX,OAAO;AAAA,IACP;AAAA,MACE;AAAA,IAAA;AAAA,EACF;AASF,QAAM,YAAyB,OAC7B,MACA,UAAkB,QACG;AACrB,UAAM,iCAAiC,IAAI;AAC3C,QAAI,OAAO,SAAS,UAAU;AAC5B,YAAM,IAAIC,OAAAA,+BAA+B,OAAO,IAAI;AAAA,IACtD;AAEA,UAAM,UAAU,UAAU,MAAM,IAAI,IAAI;AACxC,QAAI,QAAS,QAAO;AAEpB,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,YAAY,WAAW,MAAM;AACjC,oBAAA;AACA,eAAO,IAAIC,kCAA2B,IAAI,CAAC;AAAA,MAC7C,GAAG,OAAO;AAEV,YAAM,cAAc,UAAU,UAAU,MAAM;AAC5C,YAAI,UAAU,MAAM,IAAI,IAAI,GAAG;AAC7B,gBAAM,qCAAqC,IAAI;AAC/C,uBAAa,SAAS;AACtB,sBAAA;AACA,kBAAQ,IAAI;AAAA,QACd;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAGA,QAAM,kBAAkB,OAAO,WAC3B,OACE,WAGG;AAGH,UAAM,gBAAiB,MAAM,OAAO,SAAU,MAAM,KAAM,CAAA;AAC1D,UAAM,OAAQ,cAAgD;AAE9D,QAAI,CAAC,MAAM;AACT,YAAM,IAAIC,OAAAA,yCAAA;AAAA,IACZ;AAGA,QAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,YAAM,QAAQ,IAAI,KAAK,IAAI,CAAC,OAAO,UAAU,EAAE,CAAC,CAAC;AAAA,IACnD,OAAO;AACL,YAAM,UAAU,IAAI;AAAA,IACtB;AAEA,WAAO;AAAA,EACT,IACA;AAEJ,QAAM,kBAAkB,OAAO,WAC3B,OACE,WAGG;AAGH,UAAM,gBAAiB,MAAM,OAAO,SAAU,MAAM,KAAM,CAAA;AAC1D,UAAM,OAAQ,cAAgD;AAE9D,QAAI,CAAC,MAAM;AACT,YAAM,IAAIC,OAAAA,yCAAA;AAAA,IACZ;AAGA,QAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,YAAM,QAAQ,IAAI,KAAK,IAAI,CAAC,OAAO,UAAU,EAAE,CAAC,CAAC;AAAA,IACnD,OAAO;AACL,YAAM,UAAU,IAAI;AAAA,IACtB;AAEA,WAAO;AAAA,EACT,IACA;AAEJ,QAAM,kBAAkB,OAAO,WAC3B,OACE,WAGG;AACH,UAAM,gBAAgB,MAAM,OAAO,SAAU,MAAM;AACnD,QAAI,CAAC,cAAc,MAAM;AACvB,YAAM,IAAIC,OAAAA,yCAAA;AAAA,IACZ;AAGA,QAAI,MAAM,QAAQ,cAAc,IAAI,GAAG;AACrC,YAAM,QAAQ,IAAI,cAAc,KAAK,IAAI,CAAC,OAAO,UAAU,EAAE,CAAC,CAAC;AAAA,IACjE,OAAO;AACL,YAAM,UAAU,cAAc,IAAI;AAAA,IACpC;AAEA,WAAO;AAAA,EACT,IACA;AAGJ,QAAM;AAAA,IACJ,cAAc;AAAA,IACd,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,GAAG;AAAA,EAAA,IACD;AAEJ,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,OAAO;AAAA,MACL;AAAA,IAAA;AAAA,EACF;AAEJ;AAKA,SAAS,mBACP,cACA,SAGe;AACf,QAAM,EAAE,cAAc;AAGtB,QAAM,iBAAiB,IAAIL,MAAAA,MAA0B,MAAS;AAM9D,QAAM,kBAAkB,MAA+B;;AAErD,UAAM,SAAS,eAAe,SAAS;AAEvC,WAAO;AAAA,MACL,YAAU,kBAAa,WAAb,mBAAqB,SAC3B,CAAC,QAAQ,aAAa,OAAO,KAAK,IAClC;AAAA,IAAA;AAAA,EAER;AAGA,QAAM,kBAAkB,IAAI,gBAAA;AAC5B,MAAI,aAAa,QAAQ;AACvB,iBAAa,OAAO,iBAAiB,SAAS,MAAM;AAClD,sBAAgB,MAAA;AAAA,IAClB,CAAC;AACD,QAAI,aAAa,OAAO,SAAS;AAC/B,sBAAgB,MAAA;AAAA,IAClB;AAAA,EACF;AAEA,MAAI;AAEJ,SAAO;AAAA,IACL,MAAM,CAAC,WAAiD;AACtD,YAAM,EAAE,OAAO,OAAO,QAAQ,cAAc;AAC5C,YAAM,SAAS,IAAIM,mBAAY;AAAA,QAC7B,GAAG;AAAA,QACH,QAAQ,gBAAgB;AAAA,QACxB,SAAS,CAAC,gBAAgB;AAGxB,oBAAA;AAEA,cAAI,aAAa,SAAS;AACxB,mBAAO,aAAa,QAAQ,WAAW;AAAA,UACzC;AAEA;AAAA,QACF;AAAA,MAAA,CACD;AACD,UAAI,qBAAqB;AACzB,YAAM,+BAAe,IAAA;AAErB,0BAAoB,OAAO,UAAU,CAAC,aAAgC;;AACpE,YAAI,cAAc;AAElB,mBAAW,WAAW,UAAU;AAE9B,cAAI,SAAS,OAAO,GAAG;AACrB,0BAAQ,QAAQ,UAAhB,mBAAuB,QAAQ,CAAC,SAAS,SAAS,IAAI,IAAI;AAAA,UAC5D;AAEA,cAAIC,OAAAA,gBAAgB,OAAO,GAAG;AAE5B,kBAAM,SAAS,QAAQ,QAAQ;AAC/B,gBAAI,UAAU,OAAO,WAAW,UAAU;AAExC,6BAAe,SAAS,MAAM,MAAM;AAAA,YACtC;AAEA,gBAAI,CAAC,oBAAoB;AACvB,oBAAA;AACA,mCAAqB;AAAA,YACvB;AAEA,kBAAM;AAAA,cACJ,MAAM,QAAQ,QAAQ;AAAA,cACtB,OAAO,QAAQ;AAAA;AAAA,cAEf,UAAU;AAAA,gBACR,GAAG,QAAQ;AAAA,cAAA;AAAA,YACb,CACD;AAAA,UACH,WAAW,kBAAkB,OAAO,GAAG;AACrC,0BAAc;AAAA,UAChB;AAAA,QACF;AAEA,YAAI,aAAa;AAEf,cAAI,oBAAoB;AACtB,mBAAA;AACA,iCAAqB;AAAA,UACvB;AAGA,oBAAA;AAGA,oBAAU,SAAS,CAAC,iBAAiB;AACnC,kBAAM,aAAa,IAAI,IAAU,YAAY;AAC7C,gBAAI,SAAS,OAAO,GAAG;AACrB,oBAAM,+BAA+B,MAAM,KAAK,QAAQ,CAAC;AAAA,YAC3D;AACA,qBAAS,QAAQ,CAAC,SAAS,WAAW,IAAI,IAAI,CAAC;AAC/C,qBAAS,MAAA;AACT,mBAAO;AAAA,UACT,CAAC;AAAA,QACH;AAAA,MACF,CAAC;AAGD,aAAO,MAAM;AAEX,0BAAA;AAEA,wBAAgB,MAAA;AAAA,MAClB;AAAA,IACF;AAAA;AAAA,IAEA;AAAA,EAAA;AAEJ;;"}
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const db = require("@tanstack/db");
4
+ class ElectricDBCollectionError extends db.TanStackDBError {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = `ElectricDBCollectionError`;
8
+ }
9
+ }
10
+ class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {
11
+ constructor(txIdType) {
12
+ super(`Expected number in awaitTxId, received ${txIdType}`);
13
+ this.name = `ExpectedNumberInAwaitTxIdError`;
14
+ }
15
+ }
16
+ class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {
17
+ constructor(txId) {
18
+ super(`Timeout waiting for txId: ${txId}`);
19
+ this.name = `TimeoutWaitingForTxIdError`;
20
+ }
21
+ }
22
+ class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {
23
+ constructor() {
24
+ super(
25
+ `Electric collection onInsert handler must return a txid or array of txids`
26
+ );
27
+ this.name = `ElectricInsertHandlerMustReturnTxIdError`;
28
+ }
29
+ }
30
+ class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {
31
+ constructor() {
32
+ super(
33
+ `Electric collection onUpdate handler must return a txid or array of txids`
34
+ );
35
+ this.name = `ElectricUpdateHandlerMustReturnTxIdError`;
36
+ }
37
+ }
38
+ class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {
39
+ constructor() {
40
+ super(
41
+ `Electric collection onDelete handler must return a txid or array of txids`
42
+ );
43
+ this.name = `ElectricDeleteHandlerMustReturnTxIdError`;
44
+ }
45
+ }
46
+ exports.ElectricDBCollectionError = ElectricDBCollectionError;
47
+ exports.ElectricDeleteHandlerMustReturnTxIdError = ElectricDeleteHandlerMustReturnTxIdError;
48
+ exports.ElectricInsertHandlerMustReturnTxIdError = ElectricInsertHandlerMustReturnTxIdError;
49
+ exports.ElectricUpdateHandlerMustReturnTxIdError = ElectricUpdateHandlerMustReturnTxIdError;
50
+ exports.ExpectedNumberInAwaitTxIdError = ExpectedNumberInAwaitTxIdError;
51
+ exports.TimeoutWaitingForTxIdError = TimeoutWaitingForTxIdError;
52
+ //# sourceMappingURL=errors.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.cjs","sources":["../../src/errors.ts"],"sourcesContent":["import { TanStackDBError } from \"@tanstack/db\"\n\n// Electric DB Collection Errors\nexport class ElectricDBCollectionError extends TanStackDBError {\n constructor(message: string) {\n super(message)\n this.name = `ElectricDBCollectionError`\n }\n}\n\nexport class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {\n constructor(txIdType: string) {\n super(`Expected number in awaitTxId, received ${txIdType}`)\n this.name = `ExpectedNumberInAwaitTxIdError`\n }\n}\n\nexport class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {\n constructor(txId: number) {\n super(`Timeout waiting for txId: ${txId}`)\n this.name = `TimeoutWaitingForTxIdError`\n }\n}\n\nexport class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {\n constructor() {\n super(\n `Electric collection onInsert handler must return a txid or array of txids`\n )\n this.name = `ElectricInsertHandlerMustReturnTxIdError`\n }\n}\n\nexport class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {\n constructor() {\n super(\n `Electric collection onUpdate handler must return a txid or array of txids`\n )\n this.name = `ElectricUpdateHandlerMustReturnTxIdError`\n }\n}\n\nexport class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {\n constructor() {\n super(\n `Electric collection onDelete handler must return a txid or array of txids`\n )\n this.name = `ElectricDeleteHandlerMustReturnTxIdError`\n }\n}\n"],"names":["TanStackDBError"],"mappings":";;;AAGO,MAAM,kCAAkCA,GAAAA,gBAAgB;AAAA,EAC7D,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,uCAAuC,0BAA0B;AAAA,EAC5E,YAAY,UAAkB;AAC5B,UAAM,0CAA0C,QAAQ,EAAE;AAC1D,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,mCAAmC,0BAA0B;AAAA,EACxE,YAAY,MAAc;AACxB,UAAM,6BAA6B,IAAI,EAAE;AACzC,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,iDAAiD,0BAA0B;AAAA,EACtF,cAAc;AACZ;AAAA,MACE;AAAA,IAAA;AAEF,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,iDAAiD,0BAA0B;AAAA,EACtF,cAAc;AACZ;AAAA,MACE;AAAA,IAAA;AAEF,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,iDAAiD,0BAA0B;AAAA,EACtF,cAAc;AACZ;AAAA,MACE;AAAA,IAAA;AAEF,SAAK,OAAO;AAAA,EACd;AACF;;;;;;;"}
@@ -0,0 +1,19 @@
1
+ import { TanStackDBError } from '@tanstack/db';
2
+ export declare class ElectricDBCollectionError extends TanStackDBError {
3
+ constructor(message: string);
4
+ }
5
+ export declare class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {
6
+ constructor(txIdType: string);
7
+ }
8
+ export declare class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {
9
+ constructor(txId: number);
10
+ }
11
+ export declare class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {
12
+ constructor();
13
+ }
14
+ export declare class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {
15
+ constructor();
16
+ }
17
+ export declare class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {
18
+ constructor();
19
+ }
@@ -1,5 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const electric = require("./electric.cjs");
4
+ const errors = require("./errors.cjs");
4
5
  exports.electricCollectionOptions = electric.electricCollectionOptions;
6
+ exports.ElectricDBCollectionError = errors.ElectricDBCollectionError;
7
+ exports.ElectricDeleteHandlerMustReturnTxIdError = errors.ElectricDeleteHandlerMustReturnTxIdError;
8
+ exports.ElectricInsertHandlerMustReturnTxIdError = errors.ElectricInsertHandlerMustReturnTxIdError;
9
+ exports.ElectricUpdateHandlerMustReturnTxIdError = errors.ElectricUpdateHandlerMustReturnTxIdError;
10
+ exports.ExpectedNumberInAwaitTxIdError = errors.ExpectedNumberInAwaitTxIdError;
11
+ exports.TimeoutWaitingForTxIdError = errors.TimeoutWaitingForTxIdError;
5
12
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;"}
@@ -1 +1,2 @@
1
1
  export { electricCollectionOptions, type ElectricCollectionConfig, type ElectricCollectionUtils, type Txid, } from './electric.cjs';
2
+ export * from './errors.cjs';
@@ -1,6 +1,7 @@
1
1
  import { ShapeStream, isChangeMessage, isControlMessage } from "@electric-sql/client";
2
2
  import { Store } from "@tanstack/store";
3
3
  import DebugModule from "debug";
4
+ import { ElectricInsertHandlerMustReturnTxIdError, ElectricUpdateHandlerMustReturnTxIdError, ElectricDeleteHandlerMustReturnTxIdError, ExpectedNumberInAwaitTxIdError, TimeoutWaitingForTxIdError } from "./errors.js";
4
5
  const debug = DebugModule.debug(`ts/db:electric`);
5
6
  function isUpToDateMessage(message) {
6
7
  return isControlMessage(message) && message.headers.control === `up-to-date`;
@@ -19,16 +20,14 @@ function electricCollectionOptions(config) {
19
20
  const awaitTxId = async (txId, timeout = 3e4) => {
20
21
  debug(`awaitTxId called with txid %d`, txId);
21
22
  if (typeof txId !== `number`) {
22
- throw new TypeError(
23
- `Expected number in awaitTxId, received ${typeof txId}`
24
- );
23
+ throw new ExpectedNumberInAwaitTxIdError(typeof txId);
25
24
  }
26
25
  const hasTxid = seenTxids.state.has(txId);
27
26
  if (hasTxid) return true;
28
27
  return new Promise((resolve, reject) => {
29
28
  const timeoutId = setTimeout(() => {
30
29
  unsubscribe();
31
- reject(new Error(`Timeout waiting for txId: ${txId}`));
30
+ reject(new TimeoutWaitingForTxIdError(txId));
32
31
  }, timeout);
33
32
  const unsubscribe = seenTxids.subscribe(() => {
34
33
  if (seenTxids.state.has(txId)) {
@@ -44,9 +43,7 @@ function electricCollectionOptions(config) {
44
43
  const handlerResult = await config.onInsert(params) ?? {};
45
44
  const txid = handlerResult.txid;
46
45
  if (!txid) {
47
- throw new Error(
48
- `Electric collection onInsert handler must return a txid or array of txids`
49
- );
46
+ throw new ElectricInsertHandlerMustReturnTxIdError();
50
47
  }
51
48
  if (Array.isArray(txid)) {
52
49
  await Promise.all(txid.map((id) => awaitTxId(id)));
@@ -59,9 +56,7 @@ function electricCollectionOptions(config) {
59
56
  const handlerResult = await config.onUpdate(params) ?? {};
60
57
  const txid = handlerResult.txid;
61
58
  if (!txid) {
62
- throw new Error(
63
- `Electric collection onUpdate handler must return a txid or array of txids`
64
- );
59
+ throw new ElectricUpdateHandlerMustReturnTxIdError();
65
60
  }
66
61
  if (Array.isArray(txid)) {
67
62
  await Promise.all(txid.map((id) => awaitTxId(id)));
@@ -73,9 +68,7 @@ function electricCollectionOptions(config) {
73
68
  const wrappedOnDelete = config.onDelete ? async (params) => {
74
69
  const handlerResult = await config.onDelete(params);
75
70
  if (!handlerResult.txid) {
76
- throw new Error(
77
- `Electric collection onDelete handler must return a txid or array of txids`
78
- );
71
+ throw new ElectricDeleteHandlerMustReturnTxIdError();
79
72
  }
80
73
  if (Array.isArray(handlerResult.txid)) {
81
74
  await Promise.all(handlerResult.txid.map((id) => awaitTxId(id)));
@@ -128,10 +121,10 @@ function createElectricSync(shapeOptions, options) {
128
121
  const stream = new ShapeStream({
129
122
  ...shapeOptions,
130
123
  signal: abortController.signal,
131
- onError: (params2) => {
124
+ onError: (errorParams) => {
132
125
  markReady();
133
126
  if (shapeOptions.onError) {
134
- return shapeOptions.onError(params2);
127
+ return shapeOptions.onError(errorParams);
135
128
  }
136
129
  return;
137
130
  }
@@ -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 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 Electric SQL\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\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 TypeError(\n `Expected number in awaitTxId, received ${typeof txId}`\n )\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 Error(`Timeout waiting for txId: ${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 Error(\n `Electric collection onInsert handler must return a txid or array of txids`\n )\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 Error(\n `Electric collection onUpdate handler must return a txid or array of txids`\n )\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 Error(\n `Electric collection onDelete handler must return a txid or array of txids`\n )\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 // Abort controller for the stream - wraps the signal if provided\n const abortController = new AbortController()\n if (shapeOptions.signal) {\n shapeOptions.signal.addEventListener(`abort`, () => {\n abortController.abort()\n })\n if (shapeOptions.signal.aborted) {\n abortController.abort()\n }\n }\n\n let unsubscribeStream: () => void\n\n return {\n sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {\n const { begin, write, commit, markReady } = params\n const stream = new ShapeStream({\n ...shapeOptions,\n signal: abortController.signal,\n onError: (params) => {\n // Just immediately mark ready if there's an error to avoid blocking\n // apps waiting for `.preload()` to finish.\n markReady()\n\n if (shapeOptions.onError) {\n return shapeOptions.onError(params)\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 }\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":["params"],"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 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 Electric SQL\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\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 // Abort controller for the stream - wraps the signal if provided\n const abortController = new AbortController()\n if (shapeOptions.signal) {\n shapeOptions.signal.addEventListener(`abort`, () => {\n abortController.abort()\n })\n if (shapeOptions.signal.aborted) {\n abortController.abort()\n }\n }\n\n let unsubscribeStream: () => void\n\n return {\n sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {\n const { begin, write, commit, markReady } = params\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 markReady()\n\n if (shapeOptions.onError) {\n return shapeOptions.onError(errorParams)\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 }\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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"}
@@ -0,0 +1,19 @@
1
+ import { TanStackDBError } from '@tanstack/db';
2
+ export declare class ElectricDBCollectionError extends TanStackDBError {
3
+ constructor(message: string);
4
+ }
5
+ export declare class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {
6
+ constructor(txIdType: string);
7
+ }
8
+ export declare class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {
9
+ constructor(txId: number);
10
+ }
11
+ export declare class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {
12
+ constructor();
13
+ }
14
+ export declare class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {
15
+ constructor();
16
+ }
17
+ export declare class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {
18
+ constructor();
19
+ }
@@ -0,0 +1,52 @@
1
+ import { TanStackDBError } from "@tanstack/db";
2
+ class ElectricDBCollectionError extends TanStackDBError {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = `ElectricDBCollectionError`;
6
+ }
7
+ }
8
+ class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {
9
+ constructor(txIdType) {
10
+ super(`Expected number in awaitTxId, received ${txIdType}`);
11
+ this.name = `ExpectedNumberInAwaitTxIdError`;
12
+ }
13
+ }
14
+ class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {
15
+ constructor(txId) {
16
+ super(`Timeout waiting for txId: ${txId}`);
17
+ this.name = `TimeoutWaitingForTxIdError`;
18
+ }
19
+ }
20
+ class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {
21
+ constructor() {
22
+ super(
23
+ `Electric collection onInsert handler must return a txid or array of txids`
24
+ );
25
+ this.name = `ElectricInsertHandlerMustReturnTxIdError`;
26
+ }
27
+ }
28
+ class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {
29
+ constructor() {
30
+ super(
31
+ `Electric collection onUpdate handler must return a txid or array of txids`
32
+ );
33
+ this.name = `ElectricUpdateHandlerMustReturnTxIdError`;
34
+ }
35
+ }
36
+ class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {
37
+ constructor() {
38
+ super(
39
+ `Electric collection onDelete handler must return a txid or array of txids`
40
+ );
41
+ this.name = `ElectricDeleteHandlerMustReturnTxIdError`;
42
+ }
43
+ }
44
+ export {
45
+ ElectricDBCollectionError,
46
+ ElectricDeleteHandlerMustReturnTxIdError,
47
+ ElectricInsertHandlerMustReturnTxIdError,
48
+ ElectricUpdateHandlerMustReturnTxIdError,
49
+ ExpectedNumberInAwaitTxIdError,
50
+ TimeoutWaitingForTxIdError
51
+ };
52
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sources":["../../src/errors.ts"],"sourcesContent":["import { TanStackDBError } from \"@tanstack/db\"\n\n// Electric DB Collection Errors\nexport class ElectricDBCollectionError extends TanStackDBError {\n constructor(message: string) {\n super(message)\n this.name = `ElectricDBCollectionError`\n }\n}\n\nexport class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {\n constructor(txIdType: string) {\n super(`Expected number in awaitTxId, received ${txIdType}`)\n this.name = `ExpectedNumberInAwaitTxIdError`\n }\n}\n\nexport class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {\n constructor(txId: number) {\n super(`Timeout waiting for txId: ${txId}`)\n this.name = `TimeoutWaitingForTxIdError`\n }\n}\n\nexport class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {\n constructor() {\n super(\n `Electric collection onInsert handler must return a txid or array of txids`\n )\n this.name = `ElectricInsertHandlerMustReturnTxIdError`\n }\n}\n\nexport class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {\n constructor() {\n super(\n `Electric collection onUpdate handler must return a txid or array of txids`\n )\n this.name = `ElectricUpdateHandlerMustReturnTxIdError`\n }\n}\n\nexport class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {\n constructor() {\n super(\n `Electric collection onDelete handler must return a txid or array of txids`\n )\n this.name = `ElectricDeleteHandlerMustReturnTxIdError`\n }\n}\n"],"names":[],"mappings":";AAGO,MAAM,kCAAkC,gBAAgB;AAAA,EAC7D,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,uCAAuC,0BAA0B;AAAA,EAC5E,YAAY,UAAkB;AAC5B,UAAM,0CAA0C,QAAQ,EAAE;AAC1D,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,mCAAmC,0BAA0B;AAAA,EACxE,YAAY,MAAc;AACxB,UAAM,6BAA6B,IAAI,EAAE;AACzC,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,iDAAiD,0BAA0B;AAAA,EACtF,cAAc;AACZ;AAAA,MACE;AAAA,IAAA;AAEF,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,iDAAiD,0BAA0B;AAAA,EACtF,cAAc;AACZ;AAAA,MACE;AAAA,IAAA;AAEF,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,iDAAiD,0BAA0B;AAAA,EACtF,cAAc;AACZ;AAAA,MACE;AAAA,IAAA;AAEF,SAAK,OAAO;AAAA,EACd;AACF;"}
@@ -1 +1,2 @@
1
1
  export { electricCollectionOptions, type ElectricCollectionConfig, type ElectricCollectionUtils, type Txid, } from './electric.js';
2
+ export * from './errors.js';
package/dist/esm/index.js CHANGED
@@ -1,5 +1,12 @@
1
1
  import { electricCollectionOptions } from "./electric.js";
2
+ import { ElectricDBCollectionError, ElectricDeleteHandlerMustReturnTxIdError, ElectricInsertHandlerMustReturnTxIdError, ElectricUpdateHandlerMustReturnTxIdError, ExpectedNumberInAwaitTxIdError, TimeoutWaitingForTxIdError } from "./errors.js";
2
3
  export {
4
+ ElectricDBCollectionError,
5
+ ElectricDeleteHandlerMustReturnTxIdError,
6
+ ElectricInsertHandlerMustReturnTxIdError,
7
+ ElectricUpdateHandlerMustReturnTxIdError,
8
+ ExpectedNumberInAwaitTxIdError,
9
+ TimeoutWaitingForTxIdError,
3
10
  electricCollectionOptions
4
11
  };
5
12
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@tanstack/electric-db-collection",
3
3
  "description": "Electric SQL collection for TanStack DB",
4
- "version": "0.0.9",
4
+ "version": "0.0.12",
5
5
  "dependencies": {
6
6
  "@electric-sql/client": "1.0.0",
7
7
  "@standard-schema/spec": "^1.0.0",
8
8
  "@tanstack/store": "^0.7.0",
9
9
  "debug": "^4.4.1",
10
- "@tanstack/db": "0.0.27"
10
+ "@tanstack/db": "0.0.30"
11
11
  },
12
12
  "devDependencies": {
13
13
  "@types/debug": "^4.1.12",
package/src/electric.ts CHANGED
@@ -5,6 +5,13 @@ import {
5
5
  } from "@electric-sql/client"
6
6
  import { Store } from "@tanstack/store"
7
7
  import DebugModule from "debug"
8
+ import {
9
+ ElectricDeleteHandlerMustReturnTxIdError,
10
+ ElectricInsertHandlerMustReturnTxIdError,
11
+ ElectricUpdateHandlerMustReturnTxIdError,
12
+ ExpectedNumberInAwaitTxIdError,
13
+ TimeoutWaitingForTxIdError,
14
+ } from "./errors"
8
15
  import type {
9
16
  CollectionConfig,
10
17
  DeleteMutationFnParams,
@@ -300,9 +307,7 @@ export function electricCollectionOptions<
300
307
  ): Promise<boolean> => {
301
308
  debug(`awaitTxId called with txid %d`, txId)
302
309
  if (typeof txId !== `number`) {
303
- throw new TypeError(
304
- `Expected number in awaitTxId, received ${typeof txId}`
305
- )
310
+ throw new ExpectedNumberInAwaitTxIdError(typeof txId)
306
311
  }
307
312
 
308
313
  const hasTxid = seenTxids.state.has(txId)
@@ -311,7 +316,7 @@ export function electricCollectionOptions<
311
316
  return new Promise((resolve, reject) => {
312
317
  const timeoutId = setTimeout(() => {
313
318
  unsubscribe()
314
- reject(new Error(`Timeout waiting for txId: ${txId}`))
319
+ reject(new TimeoutWaitingForTxIdError(txId))
315
320
  }, timeout)
316
321
 
317
322
  const unsubscribe = seenTxids.subscribe(() => {
@@ -338,9 +343,7 @@ export function electricCollectionOptions<
338
343
  const txid = (handlerResult as { txid?: Txid | Array<Txid> }).txid
339
344
 
340
345
  if (!txid) {
341
- throw new Error(
342
- `Electric collection onInsert handler must return a txid or array of txids`
343
- )
346
+ throw new ElectricInsertHandlerMustReturnTxIdError()
344
347
  }
345
348
 
346
349
  // Handle both single txid and array of txids
@@ -366,9 +369,7 @@ export function electricCollectionOptions<
366
369
  const txid = (handlerResult as { txid?: Txid | Array<Txid> }).txid
367
370
 
368
371
  if (!txid) {
369
- throw new Error(
370
- `Electric collection onUpdate handler must return a txid or array of txids`
371
- )
372
+ throw new ElectricUpdateHandlerMustReturnTxIdError()
372
373
  }
373
374
 
374
375
  // Handle both single txid and array of txids
@@ -390,9 +391,7 @@ export function electricCollectionOptions<
390
391
  ) => {
391
392
  const handlerResult = await config.onDelete!(params)
392
393
  if (!handlerResult.txid) {
393
- throw new Error(
394
- `Electric collection onDelete handler must return a txid or array of txids`
395
- )
394
+ throw new ElectricDeleteHandlerMustReturnTxIdError()
396
395
  }
397
396
 
398
397
  // Handle both single txid and array of txids
@@ -475,13 +474,13 @@ function createElectricSync<T extends Row<unknown>>(
475
474
  const stream = new ShapeStream({
476
475
  ...shapeOptions,
477
476
  signal: abortController.signal,
478
- onError: (params) => {
477
+ onError: (errorParams) => {
479
478
  // Just immediately mark ready if there's an error to avoid blocking
480
479
  // apps waiting for `.preload()` to finish.
481
480
  markReady()
482
481
 
483
482
  if (shapeOptions.onError) {
484
- return shapeOptions.onError(params)
483
+ return shapeOptions.onError(errorParams)
485
484
  }
486
485
 
487
486
  return
package/src/errors.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { TanStackDBError } from "@tanstack/db"
2
+
3
+ // Electric DB Collection Errors
4
+ export class ElectricDBCollectionError extends TanStackDBError {
5
+ constructor(message: string) {
6
+ super(message)
7
+ this.name = `ElectricDBCollectionError`
8
+ }
9
+ }
10
+
11
+ export class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {
12
+ constructor(txIdType: string) {
13
+ super(`Expected number in awaitTxId, received ${txIdType}`)
14
+ this.name = `ExpectedNumberInAwaitTxIdError`
15
+ }
16
+ }
17
+
18
+ export class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {
19
+ constructor(txId: number) {
20
+ super(`Timeout waiting for txId: ${txId}`)
21
+ this.name = `TimeoutWaitingForTxIdError`
22
+ }
23
+ }
24
+
25
+ export class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {
26
+ constructor() {
27
+ super(
28
+ `Electric collection onInsert handler must return a txid or array of txids`
29
+ )
30
+ this.name = `ElectricInsertHandlerMustReturnTxIdError`
31
+ }
32
+ }
33
+
34
+ export class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {
35
+ constructor() {
36
+ super(
37
+ `Electric collection onUpdate handler must return a txid or array of txids`
38
+ )
39
+ this.name = `ElectricUpdateHandlerMustReturnTxIdError`
40
+ }
41
+ }
42
+
43
+ export class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {
44
+ constructor() {
45
+ super(
46
+ `Electric collection onDelete handler must return a txid or array of txids`
47
+ )
48
+ this.name = `ElectricDeleteHandlerMustReturnTxIdError`
49
+ }
50
+ }
package/src/index.ts CHANGED
@@ -4,3 +4,5 @@ export {
4
4
  type ElectricCollectionUtils,
5
5
  type Txid,
6
6
  } from "./electric"
7
+
8
+ export * from "./errors"