@tanstack/electric-db-collection 0.1.29 → 0.1.30

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.
@@ -29,12 +29,16 @@ function electricCollectionOptions(config) {
29
29
  const seenSnapshots = new store.Store([]);
30
30
  const sync = createElectricSync(config.shapeOptions, {
31
31
  seenTxids,
32
- seenSnapshots
32
+ seenSnapshots,
33
+ collectionId: config.id
33
34
  });
34
35
  const awaitTxId = async (txId, timeout = 3e4) => {
35
- debug(`awaitTxId called with txid %d`, txId);
36
+ debug(
37
+ `${config.id ? `[${config.id}] ` : ``}awaitTxId called with txid %d`,
38
+ txId
39
+ );
36
40
  if (typeof txId !== `number`) {
37
- throw new errors.ExpectedNumberInAwaitTxIdError(typeof txId);
41
+ throw new errors.ExpectedNumberInAwaitTxIdError(typeof txId, config.id);
38
42
  }
39
43
  const hasTxid = seenTxids.state.has(txId);
40
44
  if (hasTxid) return true;
@@ -46,11 +50,14 @@ function electricCollectionOptions(config) {
46
50
  const timeoutId = setTimeout(() => {
47
51
  unsubscribeSeenTxids();
48
52
  unsubscribeSeenSnapshots();
49
- reject(new errors.TimeoutWaitingForTxIdError(txId));
53
+ reject(new errors.TimeoutWaitingForTxIdError(txId, config.id));
50
54
  }, timeout);
51
55
  const unsubscribeSeenTxids = seenTxids.subscribe(() => {
52
56
  if (seenTxids.state.has(txId)) {
53
- debug(`awaitTxId found match for txid %o`, txId);
57
+ debug(
58
+ `${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o`,
59
+ txId
60
+ );
54
61
  clearTimeout(timeoutId);
55
62
  unsubscribeSeenTxids();
56
63
  unsubscribeSeenSnapshots();
@@ -63,7 +70,7 @@ function electricCollectionOptions(config) {
63
70
  );
64
71
  if (visibleSnapshot) {
65
72
  debug(
66
- `awaitTxId found match for txid %o in snapshot %o`,
73
+ `${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o in snapshot %o`,
67
74
  txId,
68
75
  visibleSnapshot
69
76
  );
@@ -79,7 +86,7 @@ function electricCollectionOptions(config) {
79
86
  const handlerResult = await config.onInsert(params) ?? {};
80
87
  const txid = handlerResult.txid;
81
88
  if (!txid) {
82
- throw new errors.ElectricInsertHandlerMustReturnTxIdError();
89
+ throw new errors.ElectricInsertHandlerMustReturnTxIdError(config.id);
83
90
  }
84
91
  if (Array.isArray(txid)) {
85
92
  await Promise.all(txid.map((id) => awaitTxId(id)));
@@ -92,7 +99,7 @@ function electricCollectionOptions(config) {
92
99
  const handlerResult = await config.onUpdate(params) ?? {};
93
100
  const txid = handlerResult.txid;
94
101
  if (!txid) {
95
- throw new errors.ElectricUpdateHandlerMustReturnTxIdError();
102
+ throw new errors.ElectricUpdateHandlerMustReturnTxIdError(config.id);
96
103
  }
97
104
  if (Array.isArray(txid)) {
98
105
  await Promise.all(txid.map((id) => awaitTxId(id)));
@@ -104,7 +111,7 @@ function electricCollectionOptions(config) {
104
111
  const wrappedOnDelete = config.onDelete ? async (params) => {
105
112
  const handlerResult = await config.onDelete(params);
106
113
  if (!handlerResult.txid) {
107
- throw new errors.ElectricDeleteHandlerMustReturnTxIdError();
114
+ throw new errors.ElectricDeleteHandlerMustReturnTxIdError(config.id);
108
115
  }
109
116
  if (Array.isArray(handlerResult.txid)) {
110
117
  await Promise.all(handlerResult.txid.map((id) => awaitTxId(id)));
@@ -132,8 +139,7 @@ function electricCollectionOptions(config) {
132
139
  };
133
140
  }
134
141
  function createElectricSync(shapeOptions, options) {
135
- const { seenTxids } = options;
136
- const { seenSnapshots } = options;
142
+ const { seenTxids, seenSnapshots, collectionId } = options;
137
143
  const relationSchema = new store.Store(void 0);
138
144
  const getSyncMetadata = () => {
139
145
  const schema = relationSchema.state || `public`;
@@ -210,7 +216,7 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
210
216
  hasUpToDate = true;
211
217
  } else if (isMustRefetchMessage(message)) {
212
218
  debug(
213
- `Received must-refetch message, starting transaction with truncate`
219
+ `${collectionId ? `[${collectionId}] ` : ``}Received must-refetch message, starting transaction with truncate`
214
220
  );
215
221
  if (!transactionStarted) {
216
222
  begin();
@@ -229,7 +235,10 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
229
235
  seenTxids.setState((currentTxids) => {
230
236
  const clonedSeen = new Set(currentTxids);
231
237
  if (newTxids.size > 0) {
232
- debug(`new txids synced from pg %O`, Array.from(newTxids));
238
+ debug(
239
+ `${collectionId ? `[${collectionId}] ` : ``}new txids synced from pg %O`,
240
+ Array.from(newTxids)
241
+ );
233
242
  }
234
243
  newTxids.forEach((txid) => clonedSeen.add(txid));
235
244
  newTxids.clear();
@@ -238,7 +247,10 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
238
247
  seenSnapshots.setState((currentSnapshots) => {
239
248
  const seen = [...currentSnapshots, ...newSnapshots];
240
249
  newSnapshots.forEach(
241
- (snapshot) => debug(`new snapshot synced from pg %o`, snapshot)
250
+ (snapshot) => debug(
251
+ `${collectionId ? `[${collectionId}] ` : ``}new snapshot synced from pg %o`,
252
+ snapshot
253
+ )
242
254
  );
243
255
  newSnapshots.length = 0;
244
256
  return seen;
@@ -1 +1 @@
1
- {"version":3,"file":"electric.cjs","sources":["../../src/electric.ts"],"sourcesContent":["import {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n isVisibleInSnapshot,\n} from \"@electric-sql/client\"\nimport { Store } from \"@tanstack/store\"\nimport DebugModule from \"debug\"\nimport {\n ElectricDeleteHandlerMustReturnTxIdError,\n ElectricInsertHandlerMustReturnTxIdError,\n ElectricUpdateHandlerMustReturnTxIdError,\n ExpectedNumberInAwaitTxIdError,\n TimeoutWaitingForTxIdError,\n} from \"./errors\"\nimport type {\n BaseCollectionConfig,\n CollectionConfig,\n DeleteMutationFnParams,\n Fn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type {\n ControlMessage,\n GetExtensions,\n Message,\n PostgresSnapshot,\n Row,\n ShapeStreamOptions,\n} from \"@electric-sql/client\"\n\nconst debug = DebugModule.debug(`ts/db:electric`)\n\n/**\n * Type representing a transaction ID in ElectricSQL\n */\nexport type Txid = number\n\n/**\n * Type representing the result of an insert, update, or delete handler\n */\ntype MaybeTxId =\n | {\n txid?: Txid | Array<Txid>\n }\n | undefined\n | null\n\n/**\n * Type representing a snapshot end message\n */\ntype SnapshotEndMessage = ControlMessage & {\n headers: { control: `snapshot-end` }\n}\n\n// The `InferSchemaOutput` and `ResolveType` are copied from the `@tanstack/db` package\n// but we modified `InferSchemaOutput` slightly to restrict the schema output to `Row<unknown>`\n// This is needed in order for `GetExtensions` to be able to infer the parser extensions type from the schema\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends Row<unknown>\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Configuration interface for Electric collection options\n * @template T - The type of items in the collection\n * @template TSchema - The schema type for validation\n */\nexport interface ElectricCollectionConfig<\n T extends Row<unknown> = Row<unknown>,\n TSchema extends StandardSchemaV1 = never,\n> extends BaseCollectionConfig<\n T,\n string | number,\n TSchema,\n Record<string, Fn>,\n { txid: Txid | Array<Txid> }\n > {\n /**\n * Configuration options for the ElectricSQL ShapeStream\n */\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>\n}\n\nfunction isUpToDateMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { up_to_date: true } {\n return isControlMessage(message) && message.headers.control === `up-to-date`\n}\n\nfunction isMustRefetchMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { headers: { control: `must-refetch` } } {\n return isControlMessage(message) && message.headers.control === `must-refetch`\n}\n\nfunction isSnapshotEndMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is SnapshotEndMessage {\n return isControlMessage(message) && message.headers.control === `snapshot-end`\n}\n\nfunction parseSnapshotMessage(message: SnapshotEndMessage): PostgresSnapshot {\n return {\n xmin: message.headers.xmin,\n xmax: message.headers.xmax,\n xip_list: message.headers.xip_list,\n }\n}\n\n// Check if a message contains txids in its headers\nfunction hasTxids<T extends Row<unknown>>(\n message: Message<T>\n): message is Message<T> & { headers: { txids?: Array<Txid> } } {\n return `txids` in message.headers && Array.isArray(message.headers.txids)\n}\n\n/**\n * Type for the awaitTxId utility function\n */\nexport type AwaitTxIdFn = (txId: Txid, timeout?: number) => Promise<boolean>\n\n/**\n * Electric collection utilities type\n */\nexport interface ElectricCollectionUtils extends UtilsRecord {\n awaitTxId: AwaitTxIdFn\n}\n\n/**\n * Creates Electric collection options for use with a standard Collection\n *\n * @template T - The explicit type of items in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TFallback - The fallback type if no explicit or schema type is provided\n * @param config - Configuration options for the Electric collection\n * @returns Collection options with utilities\n */\n\n// Overload for when schema is provided\nexport function electricCollectionOptions<T extends StandardSchemaV1>(\n config: ElectricCollectionConfig<InferSchemaOutput<T>, T> & {\n schema: T\n }\n): CollectionConfig<InferSchemaOutput<T>, string | number, T> & {\n id?: string\n utils: ElectricCollectionUtils\n schema: T\n}\n\n// Overload for when no schema is provided\nexport function electricCollectionOptions<T extends Row<unknown>>(\n config: ElectricCollectionConfig<T> & {\n schema?: never // prohibit schema\n }\n): CollectionConfig<T, string | number> & {\n id?: string\n utils: ElectricCollectionUtils\n schema?: never // no schema in the result\n}\n\nexport function electricCollectionOptions(\n config: ElectricCollectionConfig<any, any>\n): CollectionConfig<any, string | number, any> & {\n id?: string\n utils: ElectricCollectionUtils\n schema?: any\n} {\n const seenTxids = new Store<Set<Txid>>(new Set([]))\n const seenSnapshots = new Store<Array<PostgresSnapshot>>([])\n const sync = createElectricSync<any>(config.shapeOptions, {\n seenTxids,\n seenSnapshots,\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 // First check if the txid is in the seenTxids store\n const hasTxid = seenTxids.state.has(txId)\n if (hasTxid) return true\n\n // Then check if the txid is in any of the seen snapshots\n const hasSnapshot = seenSnapshots.state.some((snapshot) =>\n isVisibleInSnapshot(txId, snapshot)\n )\n if (hasSnapshot) return true\n\n return new Promise((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n unsubscribeSeenTxids()\n unsubscribeSeenSnapshots()\n reject(new TimeoutWaitingForTxIdError(txId))\n }, timeout)\n\n const unsubscribeSeenTxids = seenTxids.subscribe(() => {\n if (seenTxids.state.has(txId)) {\n debug(`awaitTxId found match for txid %o`, txId)\n clearTimeout(timeoutId)\n unsubscribeSeenTxids()\n unsubscribeSeenSnapshots()\n resolve(true)\n }\n })\n\n const unsubscribeSeenSnapshots = seenSnapshots.subscribe(() => {\n const visibleSnapshot = seenSnapshots.state.find((snapshot) =>\n isVisibleInSnapshot(txId, snapshot)\n )\n if (visibleSnapshot) {\n debug(\n `awaitTxId found match for txid %o in snapshot %o`,\n txId,\n visibleSnapshot\n )\n clearTimeout(timeoutId)\n unsubscribeSeenSnapshots()\n unsubscribeSeenTxids()\n resolve(true)\n }\n })\n })\n }\n\n // Create wrapper handlers for direct persistence operations that handle txid awaiting\n const wrappedOnInsert = config.onInsert\n ? async (params: InsertMutationFnParams<any>) => {\n // Runtime check (that doesn't follow type)\n\n const handlerResult =\n ((await config.onInsert!(params)) as MaybeTxId) ?? {}\n const txid = handlerResult.txid\n\n if (!txid) {\n throw new ElectricInsertHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = config.onUpdate\n ? async (params: UpdateMutationFnParams<any>) => {\n // Runtime check (that doesn't follow type)\n\n const handlerResult =\n ((await config.onUpdate!(params)) as MaybeTxId) ?? {}\n const txid = handlerResult.txid\n\n if (!txid) {\n throw new ElectricUpdateHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = config.onDelete\n ? async (params: DeleteMutationFnParams<any>) => {\n const handlerResult = await config.onDelete!(params)\n if (!handlerResult.txid) {\n throw new ElectricDeleteHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(handlerResult.txid)) {\n await Promise.all(handlerResult.txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(handlerResult.txid)\n }\n\n return handlerResult\n }\n : undefined\n\n // Extract standard Collection config properties\n const {\n shapeOptions: _shapeOptions,\n onInsert: _onInsert,\n onUpdate: _onUpdate,\n onDelete: _onDelete,\n ...restConfig\n } = config\n\n return {\n ...restConfig,\n sync,\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n awaitTxId,\n },\n }\n}\n\n/**\n * Internal function to create ElectricSQL sync configuration\n */\nfunction createElectricSync<T extends Row<unknown>>(\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>,\n options: {\n seenTxids: Store<Set<Txid>>\n seenSnapshots: Store<Array<PostgresSnapshot>>\n }\n): SyncConfig<T> {\n const { seenTxids } = options\n const { seenSnapshots } = options\n\n // Store for the relation schema information\n const relationSchema = new Store<string | undefined>(undefined)\n\n /**\n * Get the sync metadata for insert operations\n * @returns Record containing relation information\n */\n const getSyncMetadata = (): Record<string, unknown> => {\n // Use the stored schema if available, otherwise default to 'public'\n const schema = relationSchema.state || `public`\n\n return {\n relation: shapeOptions.params?.table\n ? [schema, shapeOptions.params.table]\n : undefined,\n }\n }\n\n let unsubscribeStream: () => void\n\n return {\n sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {\n const { begin, write, commit, markReady, truncate, collection } = params\n\n // Abort controller for the stream - wraps the signal if provided\n const abortController = new AbortController()\n\n if (shapeOptions.signal) {\n shapeOptions.signal.addEventListener(\n `abort`,\n () => {\n abortController.abort()\n },\n {\n once: true,\n }\n )\n if (shapeOptions.signal.aborted) {\n abortController.abort()\n }\n }\n\n const stream = new ShapeStream({\n ...shapeOptions,\n signal: abortController.signal,\n onError: (errorParams) => {\n // Just immediately mark ready if there's an error to avoid blocking\n // apps waiting for `.preload()` to finish.\n // Note that Electric sends a 409 error on a `must-refetch` message, but the\n // ShapeStream handled this and it will not reach this handler, therefor\n // this markReady will not be triggers by a `must-refetch`.\n markReady()\n\n if (shapeOptions.onError) {\n return shapeOptions.onError(errorParams)\n } else {\n console.error(\n `An error occurred while syncing collection: ${collection.id}, \\n` +\n `it has been marked as ready to avoid blocking apps waiting for '.preload()' to finish. \\n` +\n `You can provide an 'onError' handler on the shapeOptions to handle this error, and this message will not be logged.`,\n errorParams\n )\n }\n\n return\n },\n })\n let transactionStarted = false\n const newTxids = new Set<Txid>()\n const newSnapshots: Array<PostgresSnapshot> = []\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 (isSnapshotEndMessage(message)) {\n newSnapshots.push(parseSnapshotMessage(message))\n } else if (isUpToDateMessage(message)) {\n hasUpToDate = true\n } else if (isMustRefetchMessage(message)) {\n debug(\n `Received must-refetch message, starting transaction with truncate`\n )\n\n // Start a transaction and truncate the collection\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n truncate()\n\n // Reset hasUpToDate so we continue accumulating changes until next up-to-date\n hasUpToDate = false\n }\n }\n\n if (hasUpToDate) {\n // Commit transaction if one was started\n if (transactionStarted) {\n commit()\n transactionStarted = false\n }\n\n // Mark the collection as ready now that sync is up to date\n markReady()\n\n // Always commit txids when we receive up-to-date, regardless of transaction state\n seenTxids.setState((currentTxids) => {\n const clonedSeen = new Set<Txid>(currentTxids)\n if (newTxids.size > 0) {\n debug(`new txids synced from pg %O`, Array.from(newTxids))\n }\n newTxids.forEach((txid) => clonedSeen.add(txid))\n newTxids.clear()\n return clonedSeen\n })\n\n // Always commit snapshots when we receive up-to-date, regardless of transaction state\n seenSnapshots.setState((currentSnapshots) => {\n const seen = [...currentSnapshots, ...newSnapshots]\n newSnapshots.forEach((snapshot) =>\n debug(`new snapshot synced from pg %o`, snapshot)\n )\n newSnapshots.length = 0\n return seen\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","isVisibleInSnapshot","TimeoutWaitingForTxIdError","ElectricInsertHandlerMustReturnTxIdError","ElectricUpdateHandlerMustReturnTxIdError","ElectricDeleteHandlerMustReturnTxIdError","ShapeStream","isChangeMessage"],"mappings":"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"}
1
+ {"version":3,"file":"electric.cjs","sources":["../../src/electric.ts"],"sourcesContent":["import {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n isVisibleInSnapshot,\n} from \"@electric-sql/client\"\nimport { Store } from \"@tanstack/store\"\nimport DebugModule from \"debug\"\nimport {\n ElectricDeleteHandlerMustReturnTxIdError,\n ElectricInsertHandlerMustReturnTxIdError,\n ElectricUpdateHandlerMustReturnTxIdError,\n ExpectedNumberInAwaitTxIdError,\n TimeoutWaitingForTxIdError,\n} from \"./errors\"\nimport type {\n BaseCollectionConfig,\n CollectionConfig,\n DeleteMutationFnParams,\n Fn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type {\n ControlMessage,\n GetExtensions,\n Message,\n PostgresSnapshot,\n Row,\n ShapeStreamOptions,\n} from \"@electric-sql/client\"\n\nconst debug = DebugModule.debug(`ts/db:electric`)\n\n/**\n * Type representing a transaction ID in ElectricSQL\n */\nexport type Txid = number\n\n/**\n * Type representing the result of an insert, update, or delete handler\n */\ntype MaybeTxId =\n | {\n txid?: Txid | Array<Txid>\n }\n | undefined\n | null\n\n/**\n * Type representing a snapshot end message\n */\ntype SnapshotEndMessage = ControlMessage & {\n headers: { control: `snapshot-end` }\n}\n\n// The `InferSchemaOutput` and `ResolveType` are copied from the `@tanstack/db` package\n// but we modified `InferSchemaOutput` slightly to restrict the schema output to `Row<unknown>`\n// This is needed in order for `GetExtensions` to be able to infer the parser extensions type from the schema\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends Row<unknown>\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Configuration interface for Electric collection options\n * @template T - The type of items in the collection\n * @template TSchema - The schema type for validation\n */\nexport interface ElectricCollectionConfig<\n T extends Row<unknown> = Row<unknown>,\n TSchema extends StandardSchemaV1 = never,\n> extends BaseCollectionConfig<\n T,\n string | number,\n TSchema,\n Record<string, Fn>,\n { txid: Txid | Array<Txid> }\n > {\n /**\n * Configuration options for the ElectricSQL ShapeStream\n */\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>\n}\n\nfunction isUpToDateMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { up_to_date: true } {\n return isControlMessage(message) && message.headers.control === `up-to-date`\n}\n\nfunction isMustRefetchMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { headers: { control: `must-refetch` } } {\n return isControlMessage(message) && message.headers.control === `must-refetch`\n}\n\nfunction isSnapshotEndMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is SnapshotEndMessage {\n return isControlMessage(message) && message.headers.control === `snapshot-end`\n}\n\nfunction parseSnapshotMessage(message: SnapshotEndMessage): PostgresSnapshot {\n return {\n xmin: message.headers.xmin,\n xmax: message.headers.xmax,\n xip_list: message.headers.xip_list,\n }\n}\n\n// Check if a message contains txids in its headers\nfunction hasTxids<T extends Row<unknown>>(\n message: Message<T>\n): message is Message<T> & { headers: { txids?: Array<Txid> } } {\n return `txids` in message.headers && Array.isArray(message.headers.txids)\n}\n\n/**\n * Type for the awaitTxId utility function\n */\nexport type AwaitTxIdFn = (txId: Txid, timeout?: number) => Promise<boolean>\n\n/**\n * Electric collection utilities type\n */\nexport interface ElectricCollectionUtils extends UtilsRecord {\n awaitTxId: AwaitTxIdFn\n}\n\n/**\n * Creates Electric collection options for use with a standard Collection\n *\n * @template T - The explicit type of items in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TFallback - The fallback type if no explicit or schema type is provided\n * @param config - Configuration options for the Electric collection\n * @returns Collection options with utilities\n */\n\n// Overload for when schema is provided\nexport function electricCollectionOptions<T extends StandardSchemaV1>(\n config: ElectricCollectionConfig<InferSchemaOutput<T>, T> & {\n schema: T\n }\n): CollectionConfig<InferSchemaOutput<T>, string | number, T> & {\n id?: string\n utils: ElectricCollectionUtils\n schema: T\n}\n\n// Overload for when no schema is provided\nexport function electricCollectionOptions<T extends Row<unknown>>(\n config: ElectricCollectionConfig<T> & {\n schema?: never // prohibit schema\n }\n): CollectionConfig<T, string | number> & {\n id?: string\n utils: ElectricCollectionUtils\n schema?: never // no schema in the result\n}\n\nexport function electricCollectionOptions(\n config: ElectricCollectionConfig<any, any>\n): CollectionConfig<any, string | number, any> & {\n id?: string\n utils: ElectricCollectionUtils\n schema?: any\n} {\n const seenTxids = new Store<Set<Txid>>(new Set([]))\n const seenSnapshots = new Store<Array<PostgresSnapshot>>([])\n const sync = createElectricSync<any>(config.shapeOptions, {\n seenTxids,\n seenSnapshots,\n collectionId: config.id,\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(\n `${config.id ? `[${config.id}] ` : ``}awaitTxId called with txid %d`,\n txId\n )\n if (typeof txId !== `number`) {\n throw new ExpectedNumberInAwaitTxIdError(typeof txId, config.id)\n }\n\n // First check if the txid is in the seenTxids store\n const hasTxid = seenTxids.state.has(txId)\n if (hasTxid) return true\n\n // Then check if the txid is in any of the seen snapshots\n const hasSnapshot = seenSnapshots.state.some((snapshot) =>\n isVisibleInSnapshot(txId, snapshot)\n )\n if (hasSnapshot) return true\n\n return new Promise((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n unsubscribeSeenTxids()\n unsubscribeSeenSnapshots()\n reject(new TimeoutWaitingForTxIdError(txId, config.id))\n }, timeout)\n\n const unsubscribeSeenTxids = seenTxids.subscribe(() => {\n if (seenTxids.state.has(txId)) {\n debug(\n `${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o`,\n txId\n )\n clearTimeout(timeoutId)\n unsubscribeSeenTxids()\n unsubscribeSeenSnapshots()\n resolve(true)\n }\n })\n\n const unsubscribeSeenSnapshots = seenSnapshots.subscribe(() => {\n const visibleSnapshot = seenSnapshots.state.find((snapshot) =>\n isVisibleInSnapshot(txId, snapshot)\n )\n if (visibleSnapshot) {\n debug(\n `${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o in snapshot %o`,\n txId,\n visibleSnapshot\n )\n clearTimeout(timeoutId)\n unsubscribeSeenSnapshots()\n unsubscribeSeenTxids()\n resolve(true)\n }\n })\n })\n }\n\n // Create wrapper handlers for direct persistence operations that handle txid awaiting\n const wrappedOnInsert = config.onInsert\n ? async (params: InsertMutationFnParams<any>) => {\n // Runtime check (that doesn't follow type)\n\n const handlerResult =\n ((await config.onInsert!(params)) as MaybeTxId) ?? {}\n const txid = handlerResult.txid\n\n if (!txid) {\n throw new ElectricInsertHandlerMustReturnTxIdError(config.id)\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = config.onUpdate\n ? async (params: UpdateMutationFnParams<any>) => {\n // Runtime check (that doesn't follow type)\n\n const handlerResult =\n ((await config.onUpdate!(params)) as MaybeTxId) ?? {}\n const txid = handlerResult.txid\n\n if (!txid) {\n throw new ElectricUpdateHandlerMustReturnTxIdError(config.id)\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = config.onDelete\n ? async (params: DeleteMutationFnParams<any>) => {\n const handlerResult = await config.onDelete!(params)\n if (!handlerResult.txid) {\n throw new ElectricDeleteHandlerMustReturnTxIdError(config.id)\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 seenSnapshots: Store<Array<PostgresSnapshot>>\n collectionId?: string\n }\n): SyncConfig<T> {\n const { seenTxids, seenSnapshots, collectionId } = options\n\n // Store for the relation schema information\n const relationSchema = new Store<string | undefined>(undefined)\n\n /**\n * Get the sync metadata for insert operations\n * @returns Record containing relation information\n */\n const getSyncMetadata = (): Record<string, unknown> => {\n // Use the stored schema if available, otherwise default to 'public'\n const schema = relationSchema.state || `public`\n\n return {\n relation: shapeOptions.params?.table\n ? [schema, shapeOptions.params.table]\n : undefined,\n }\n }\n\n let unsubscribeStream: () => void\n\n return {\n sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {\n const { begin, write, commit, markReady, truncate, collection } = params\n\n // Abort controller for the stream - wraps the signal if provided\n const abortController = new AbortController()\n\n if (shapeOptions.signal) {\n shapeOptions.signal.addEventListener(\n `abort`,\n () => {\n abortController.abort()\n },\n {\n once: true,\n }\n )\n if (shapeOptions.signal.aborted) {\n abortController.abort()\n }\n }\n\n const stream = new ShapeStream({\n ...shapeOptions,\n signal: abortController.signal,\n onError: (errorParams) => {\n // Just immediately mark ready if there's an error to avoid blocking\n // apps waiting for `.preload()` to finish.\n // Note that Electric sends a 409 error on a `must-refetch` message, but the\n // ShapeStream handled this and it will not reach this handler, therefor\n // this markReady will not be triggers by a `must-refetch`.\n markReady()\n\n if (shapeOptions.onError) {\n return shapeOptions.onError(errorParams)\n } else {\n console.error(\n `An error occurred while syncing collection: ${collection.id}, \\n` +\n `it has been marked as ready to avoid blocking apps waiting for '.preload()' to finish. \\n` +\n `You can provide an 'onError' handler on the shapeOptions to handle this error, and this message will not be logged.`,\n errorParams\n )\n }\n\n return\n },\n })\n let transactionStarted = false\n const newTxids = new Set<Txid>()\n const newSnapshots: Array<PostgresSnapshot> = []\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 (isSnapshotEndMessage(message)) {\n newSnapshots.push(parseSnapshotMessage(message))\n } else if (isUpToDateMessage(message)) {\n hasUpToDate = true\n } else if (isMustRefetchMessage(message)) {\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Received must-refetch message, starting transaction with truncate`\n )\n\n // Start a transaction and truncate the collection\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n truncate()\n\n // Reset hasUpToDate so we continue accumulating changes until next up-to-date\n hasUpToDate = false\n }\n }\n\n if (hasUpToDate) {\n // Commit transaction if one was started\n if (transactionStarted) {\n commit()\n transactionStarted = false\n }\n\n // Mark the collection as ready now that sync is up to date\n markReady()\n\n // Always commit txids when we receive up-to-date, regardless of transaction state\n seenTxids.setState((currentTxids) => {\n const clonedSeen = new Set<Txid>(currentTxids)\n if (newTxids.size > 0) {\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}new txids synced from pg %O`,\n Array.from(newTxids)\n )\n }\n newTxids.forEach((txid) => clonedSeen.add(txid))\n newTxids.clear()\n return clonedSeen\n })\n\n // Always commit snapshots when we receive up-to-date, regardless of transaction state\n seenSnapshots.setState((currentSnapshots) => {\n const seen = [...currentSnapshots, ...newSnapshots]\n newSnapshots.forEach((snapshot) =>\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}new snapshot synced from pg %o`,\n snapshot\n )\n )\n newSnapshots.length = 0\n return seen\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","isVisibleInSnapshot","TimeoutWaitingForTxIdError","ElectricInsertHandlerMustReturnTxIdError","ElectricUpdateHandlerMustReturnTxIdError","ElectricDeleteHandlerMustReturnTxIdError","ShapeStream","isChangeMessage"],"mappings":"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"}
@@ -2,43 +2,46 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const db = require("@tanstack/db");
4
4
  class ElectricDBCollectionError extends db.TanStackDBError {
5
- constructor(message) {
6
- super(message);
5
+ constructor(message, collectionId) {
6
+ super(`${collectionId ? `[${collectionId}] ` : ``}${message}`);
7
7
  this.name = `ElectricDBCollectionError`;
8
8
  }
9
9
  }
10
10
  class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {
11
- constructor(txIdType) {
12
- super(`Expected number in awaitTxId, received ${txIdType}`);
11
+ constructor(txIdType, collectionId) {
12
+ super(`Expected number in awaitTxId, received ${txIdType}`, collectionId);
13
13
  this.name = `ExpectedNumberInAwaitTxIdError`;
14
14
  }
15
15
  }
16
16
  class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {
17
- constructor(txId) {
18
- super(`Timeout waiting for txId: ${txId}`);
17
+ constructor(txId, collectionId) {
18
+ super(`Timeout waiting for txId: ${txId}`, collectionId);
19
19
  this.name = `TimeoutWaitingForTxIdError`;
20
20
  }
21
21
  }
22
22
  class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {
23
- constructor() {
23
+ constructor(collectionId) {
24
24
  super(
25
- `Electric collection onInsert handler must return a txid or array of txids`
25
+ `Electric collection onInsert handler must return a txid or array of txids`,
26
+ collectionId
26
27
  );
27
28
  this.name = `ElectricInsertHandlerMustReturnTxIdError`;
28
29
  }
29
30
  }
30
31
  class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {
31
- constructor() {
32
+ constructor(collectionId) {
32
33
  super(
33
- `Electric collection onUpdate handler must return a txid or array of txids`
34
+ `Electric collection onUpdate handler must return a txid or array of txids`,
35
+ collectionId
34
36
  );
35
37
  this.name = `ElectricUpdateHandlerMustReturnTxIdError`;
36
38
  }
37
39
  }
38
40
  class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {
39
- constructor() {
41
+ constructor(collectionId) {
40
42
  super(
41
- `Electric collection onDelete handler must return a txid or array of txids`
43
+ `Electric collection onDelete handler must return a txid or array of txids`,
44
+ collectionId
42
45
  );
43
46
  this.name = `ElectricDeleteHandlerMustReturnTxIdError`;
44
47
  }
@@ -1 +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;;;;;;;"}
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, collectionId?: string) {\n super(`${collectionId ? `[${collectionId}] ` : ``}${message}`)\n this.name = `ElectricDBCollectionError`\n }\n}\n\nexport class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {\n constructor(txIdType: string, collectionId?: string) {\n super(`Expected number in awaitTxId, received ${txIdType}`, collectionId)\n this.name = `ExpectedNumberInAwaitTxIdError`\n }\n}\n\nexport class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {\n constructor(txId: number, collectionId?: string) {\n super(`Timeout waiting for txId: ${txId}`, collectionId)\n this.name = `TimeoutWaitingForTxIdError`\n }\n}\n\nexport class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {\n constructor(collectionId?: string) {\n super(\n `Electric collection onInsert handler must return a txid or array of txids`,\n collectionId\n )\n this.name = `ElectricInsertHandlerMustReturnTxIdError`\n }\n}\n\nexport class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {\n constructor(collectionId?: string) {\n super(\n `Electric collection onUpdate handler must return a txid or array of txids`,\n collectionId\n )\n this.name = `ElectricUpdateHandlerMustReturnTxIdError`\n }\n}\n\nexport class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {\n constructor(collectionId?: string) {\n super(\n `Electric collection onDelete handler must return a txid or array of txids`,\n collectionId\n )\n this.name = `ElectricDeleteHandlerMustReturnTxIdError`\n }\n}\n"],"names":["TanStackDBError"],"mappings":";;;AAGO,MAAM,kCAAkCA,GAAAA,gBAAgB;AAAA,EAC7D,YAAY,SAAiB,cAAuB;AAClD,UAAM,GAAG,eAAe,IAAI,YAAY,OAAO,EAAE,GAAG,OAAO,EAAE;AAC7D,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,uCAAuC,0BAA0B;AAAA,EAC5E,YAAY,UAAkB,cAAuB;AACnD,UAAM,0CAA0C,QAAQ,IAAI,YAAY;AACxE,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,mCAAmC,0BAA0B;AAAA,EACxE,YAAY,MAAc,cAAuB;AAC/C,UAAM,6BAA6B,IAAI,IAAI,YAAY;AACvD,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,iDAAiD,0BAA0B;AAAA,EACtF,YAAY,cAAuB;AACjC;AAAA,MACE;AAAA,MACA;AAAA,IAAA;AAEF,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,iDAAiD,0BAA0B;AAAA,EACtF,YAAY,cAAuB;AACjC;AAAA,MACE;AAAA,MACA;AAAA,IAAA;AAEF,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,iDAAiD,0BAA0B;AAAA,EACtF,YAAY,cAAuB;AACjC;AAAA,MACE;AAAA,MACA;AAAA,IAAA;AAEF,SAAK,OAAO;AAAA,EACd;AACF;;;;;;;"}
@@ -1,19 +1,19 @@
1
1
  import { TanStackDBError } from '@tanstack/db';
2
2
  export declare class ElectricDBCollectionError extends TanStackDBError {
3
- constructor(message: string);
3
+ constructor(message: string, collectionId?: string);
4
4
  }
5
5
  export declare class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {
6
- constructor(txIdType: string);
6
+ constructor(txIdType: string, collectionId?: string);
7
7
  }
8
8
  export declare class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {
9
- constructor(txId: number);
9
+ constructor(txId: number, collectionId?: string);
10
10
  }
11
11
  export declare class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {
12
- constructor();
12
+ constructor(collectionId?: string);
13
13
  }
14
14
  export declare class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {
15
- constructor();
15
+ constructor(collectionId?: string);
16
16
  }
17
17
  export declare class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {
18
- constructor();
18
+ constructor(collectionId?: string);
19
19
  }
@@ -27,12 +27,16 @@ function electricCollectionOptions(config) {
27
27
  const seenSnapshots = new Store([]);
28
28
  const sync = createElectricSync(config.shapeOptions, {
29
29
  seenTxids,
30
- seenSnapshots
30
+ seenSnapshots,
31
+ collectionId: config.id
31
32
  });
32
33
  const awaitTxId = async (txId, timeout = 3e4) => {
33
- debug(`awaitTxId called with txid %d`, txId);
34
+ debug(
35
+ `${config.id ? `[${config.id}] ` : ``}awaitTxId called with txid %d`,
36
+ txId
37
+ );
34
38
  if (typeof txId !== `number`) {
35
- throw new ExpectedNumberInAwaitTxIdError(typeof txId);
39
+ throw new ExpectedNumberInAwaitTxIdError(typeof txId, config.id);
36
40
  }
37
41
  const hasTxid = seenTxids.state.has(txId);
38
42
  if (hasTxid) return true;
@@ -44,11 +48,14 @@ function electricCollectionOptions(config) {
44
48
  const timeoutId = setTimeout(() => {
45
49
  unsubscribeSeenTxids();
46
50
  unsubscribeSeenSnapshots();
47
- reject(new TimeoutWaitingForTxIdError(txId));
51
+ reject(new TimeoutWaitingForTxIdError(txId, config.id));
48
52
  }, timeout);
49
53
  const unsubscribeSeenTxids = seenTxids.subscribe(() => {
50
54
  if (seenTxids.state.has(txId)) {
51
- debug(`awaitTxId found match for txid %o`, txId);
55
+ debug(
56
+ `${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o`,
57
+ txId
58
+ );
52
59
  clearTimeout(timeoutId);
53
60
  unsubscribeSeenTxids();
54
61
  unsubscribeSeenSnapshots();
@@ -61,7 +68,7 @@ function electricCollectionOptions(config) {
61
68
  );
62
69
  if (visibleSnapshot) {
63
70
  debug(
64
- `awaitTxId found match for txid %o in snapshot %o`,
71
+ `${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o in snapshot %o`,
65
72
  txId,
66
73
  visibleSnapshot
67
74
  );
@@ -77,7 +84,7 @@ function electricCollectionOptions(config) {
77
84
  const handlerResult = await config.onInsert(params) ?? {};
78
85
  const txid = handlerResult.txid;
79
86
  if (!txid) {
80
- throw new ElectricInsertHandlerMustReturnTxIdError();
87
+ throw new ElectricInsertHandlerMustReturnTxIdError(config.id);
81
88
  }
82
89
  if (Array.isArray(txid)) {
83
90
  await Promise.all(txid.map((id) => awaitTxId(id)));
@@ -90,7 +97,7 @@ function electricCollectionOptions(config) {
90
97
  const handlerResult = await config.onUpdate(params) ?? {};
91
98
  const txid = handlerResult.txid;
92
99
  if (!txid) {
93
- throw new ElectricUpdateHandlerMustReturnTxIdError();
100
+ throw new ElectricUpdateHandlerMustReturnTxIdError(config.id);
94
101
  }
95
102
  if (Array.isArray(txid)) {
96
103
  await Promise.all(txid.map((id) => awaitTxId(id)));
@@ -102,7 +109,7 @@ function electricCollectionOptions(config) {
102
109
  const wrappedOnDelete = config.onDelete ? async (params) => {
103
110
  const handlerResult = await config.onDelete(params);
104
111
  if (!handlerResult.txid) {
105
- throw new ElectricDeleteHandlerMustReturnTxIdError();
112
+ throw new ElectricDeleteHandlerMustReturnTxIdError(config.id);
106
113
  }
107
114
  if (Array.isArray(handlerResult.txid)) {
108
115
  await Promise.all(handlerResult.txid.map((id) => awaitTxId(id)));
@@ -130,8 +137,7 @@ function electricCollectionOptions(config) {
130
137
  };
131
138
  }
132
139
  function createElectricSync(shapeOptions, options) {
133
- const { seenTxids } = options;
134
- const { seenSnapshots } = options;
140
+ const { seenTxids, seenSnapshots, collectionId } = options;
135
141
  const relationSchema = new Store(void 0);
136
142
  const getSyncMetadata = () => {
137
143
  const schema = relationSchema.state || `public`;
@@ -208,7 +214,7 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
208
214
  hasUpToDate = true;
209
215
  } else if (isMustRefetchMessage(message)) {
210
216
  debug(
211
- `Received must-refetch message, starting transaction with truncate`
217
+ `${collectionId ? `[${collectionId}] ` : ``}Received must-refetch message, starting transaction with truncate`
212
218
  );
213
219
  if (!transactionStarted) {
214
220
  begin();
@@ -227,7 +233,10 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
227
233
  seenTxids.setState((currentTxids) => {
228
234
  const clonedSeen = new Set(currentTxids);
229
235
  if (newTxids.size > 0) {
230
- debug(`new txids synced from pg %O`, Array.from(newTxids));
236
+ debug(
237
+ `${collectionId ? `[${collectionId}] ` : ``}new txids synced from pg %O`,
238
+ Array.from(newTxids)
239
+ );
231
240
  }
232
241
  newTxids.forEach((txid) => clonedSeen.add(txid));
233
242
  newTxids.clear();
@@ -236,7 +245,10 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
236
245
  seenSnapshots.setState((currentSnapshots) => {
237
246
  const seen = [...currentSnapshots, ...newSnapshots];
238
247
  newSnapshots.forEach(
239
- (snapshot) => debug(`new snapshot synced from pg %o`, snapshot)
248
+ (snapshot) => debug(
249
+ `${collectionId ? `[${collectionId}] ` : ``}new snapshot synced from pg %o`,
250
+ snapshot
251
+ )
240
252
  );
241
253
  newSnapshots.length = 0;
242
254
  return seen;
@@ -1 +1 @@
1
- {"version":3,"file":"electric.js","sources":["../../src/electric.ts"],"sourcesContent":["import {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n isVisibleInSnapshot,\n} from \"@electric-sql/client\"\nimport { Store } from \"@tanstack/store\"\nimport DebugModule from \"debug\"\nimport {\n ElectricDeleteHandlerMustReturnTxIdError,\n ElectricInsertHandlerMustReturnTxIdError,\n ElectricUpdateHandlerMustReturnTxIdError,\n ExpectedNumberInAwaitTxIdError,\n TimeoutWaitingForTxIdError,\n} from \"./errors\"\nimport type {\n BaseCollectionConfig,\n CollectionConfig,\n DeleteMutationFnParams,\n Fn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type {\n ControlMessage,\n GetExtensions,\n Message,\n PostgresSnapshot,\n Row,\n ShapeStreamOptions,\n} from \"@electric-sql/client\"\n\nconst debug = DebugModule.debug(`ts/db:electric`)\n\n/**\n * Type representing a transaction ID in ElectricSQL\n */\nexport type Txid = number\n\n/**\n * Type representing the result of an insert, update, or delete handler\n */\ntype MaybeTxId =\n | {\n txid?: Txid | Array<Txid>\n }\n | undefined\n | null\n\n/**\n * Type representing a snapshot end message\n */\ntype SnapshotEndMessage = ControlMessage & {\n headers: { control: `snapshot-end` }\n}\n\n// The `InferSchemaOutput` and `ResolveType` are copied from the `@tanstack/db` package\n// but we modified `InferSchemaOutput` slightly to restrict the schema output to `Row<unknown>`\n// This is needed in order for `GetExtensions` to be able to infer the parser extensions type from the schema\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends Row<unknown>\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Configuration interface for Electric collection options\n * @template T - The type of items in the collection\n * @template TSchema - The schema type for validation\n */\nexport interface ElectricCollectionConfig<\n T extends Row<unknown> = Row<unknown>,\n TSchema extends StandardSchemaV1 = never,\n> extends BaseCollectionConfig<\n T,\n string | number,\n TSchema,\n Record<string, Fn>,\n { txid: Txid | Array<Txid> }\n > {\n /**\n * Configuration options for the ElectricSQL ShapeStream\n */\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>\n}\n\nfunction isUpToDateMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { up_to_date: true } {\n return isControlMessage(message) && message.headers.control === `up-to-date`\n}\n\nfunction isMustRefetchMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { headers: { control: `must-refetch` } } {\n return isControlMessage(message) && message.headers.control === `must-refetch`\n}\n\nfunction isSnapshotEndMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is SnapshotEndMessage {\n return isControlMessage(message) && message.headers.control === `snapshot-end`\n}\n\nfunction parseSnapshotMessage(message: SnapshotEndMessage): PostgresSnapshot {\n return {\n xmin: message.headers.xmin,\n xmax: message.headers.xmax,\n xip_list: message.headers.xip_list,\n }\n}\n\n// Check if a message contains txids in its headers\nfunction hasTxids<T extends Row<unknown>>(\n message: Message<T>\n): message is Message<T> & { headers: { txids?: Array<Txid> } } {\n return `txids` in message.headers && Array.isArray(message.headers.txids)\n}\n\n/**\n * Type for the awaitTxId utility function\n */\nexport type AwaitTxIdFn = (txId: Txid, timeout?: number) => Promise<boolean>\n\n/**\n * Electric collection utilities type\n */\nexport interface ElectricCollectionUtils extends UtilsRecord {\n awaitTxId: AwaitTxIdFn\n}\n\n/**\n * Creates Electric collection options for use with a standard Collection\n *\n * @template T - The explicit type of items in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TFallback - The fallback type if no explicit or schema type is provided\n * @param config - Configuration options for the Electric collection\n * @returns Collection options with utilities\n */\n\n// Overload for when schema is provided\nexport function electricCollectionOptions<T extends StandardSchemaV1>(\n config: ElectricCollectionConfig<InferSchemaOutput<T>, T> & {\n schema: T\n }\n): CollectionConfig<InferSchemaOutput<T>, string | number, T> & {\n id?: string\n utils: ElectricCollectionUtils\n schema: T\n}\n\n// Overload for when no schema is provided\nexport function electricCollectionOptions<T extends Row<unknown>>(\n config: ElectricCollectionConfig<T> & {\n schema?: never // prohibit schema\n }\n): CollectionConfig<T, string | number> & {\n id?: string\n utils: ElectricCollectionUtils\n schema?: never // no schema in the result\n}\n\nexport function electricCollectionOptions(\n config: ElectricCollectionConfig<any, any>\n): CollectionConfig<any, string | number, any> & {\n id?: string\n utils: ElectricCollectionUtils\n schema?: any\n} {\n const seenTxids = new Store<Set<Txid>>(new Set([]))\n const seenSnapshots = new Store<Array<PostgresSnapshot>>([])\n const sync = createElectricSync<any>(config.shapeOptions, {\n seenTxids,\n seenSnapshots,\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 // First check if the txid is in the seenTxids store\n const hasTxid = seenTxids.state.has(txId)\n if (hasTxid) return true\n\n // Then check if the txid is in any of the seen snapshots\n const hasSnapshot = seenSnapshots.state.some((snapshot) =>\n isVisibleInSnapshot(txId, snapshot)\n )\n if (hasSnapshot) return true\n\n return new Promise((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n unsubscribeSeenTxids()\n unsubscribeSeenSnapshots()\n reject(new TimeoutWaitingForTxIdError(txId))\n }, timeout)\n\n const unsubscribeSeenTxids = seenTxids.subscribe(() => {\n if (seenTxids.state.has(txId)) {\n debug(`awaitTxId found match for txid %o`, txId)\n clearTimeout(timeoutId)\n unsubscribeSeenTxids()\n unsubscribeSeenSnapshots()\n resolve(true)\n }\n })\n\n const unsubscribeSeenSnapshots = seenSnapshots.subscribe(() => {\n const visibleSnapshot = seenSnapshots.state.find((snapshot) =>\n isVisibleInSnapshot(txId, snapshot)\n )\n if (visibleSnapshot) {\n debug(\n `awaitTxId found match for txid %o in snapshot %o`,\n txId,\n visibleSnapshot\n )\n clearTimeout(timeoutId)\n unsubscribeSeenSnapshots()\n unsubscribeSeenTxids()\n resolve(true)\n }\n })\n })\n }\n\n // Create wrapper handlers for direct persistence operations that handle txid awaiting\n const wrappedOnInsert = config.onInsert\n ? async (params: InsertMutationFnParams<any>) => {\n // Runtime check (that doesn't follow type)\n\n const handlerResult =\n ((await config.onInsert!(params)) as MaybeTxId) ?? {}\n const txid = handlerResult.txid\n\n if (!txid) {\n throw new ElectricInsertHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = config.onUpdate\n ? async (params: UpdateMutationFnParams<any>) => {\n // Runtime check (that doesn't follow type)\n\n const handlerResult =\n ((await config.onUpdate!(params)) as MaybeTxId) ?? {}\n const txid = handlerResult.txid\n\n if (!txid) {\n throw new ElectricUpdateHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = config.onDelete\n ? async (params: DeleteMutationFnParams<any>) => {\n const handlerResult = await config.onDelete!(params)\n if (!handlerResult.txid) {\n throw new ElectricDeleteHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(handlerResult.txid)) {\n await Promise.all(handlerResult.txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(handlerResult.txid)\n }\n\n return handlerResult\n }\n : undefined\n\n // Extract standard Collection config properties\n const {\n shapeOptions: _shapeOptions,\n onInsert: _onInsert,\n onUpdate: _onUpdate,\n onDelete: _onDelete,\n ...restConfig\n } = config\n\n return {\n ...restConfig,\n sync,\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n awaitTxId,\n },\n }\n}\n\n/**\n * Internal function to create ElectricSQL sync configuration\n */\nfunction createElectricSync<T extends Row<unknown>>(\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>,\n options: {\n seenTxids: Store<Set<Txid>>\n seenSnapshots: Store<Array<PostgresSnapshot>>\n }\n): SyncConfig<T> {\n const { seenTxids } = options\n const { seenSnapshots } = options\n\n // Store for the relation schema information\n const relationSchema = new Store<string | undefined>(undefined)\n\n /**\n * Get the sync metadata for insert operations\n * @returns Record containing relation information\n */\n const getSyncMetadata = (): Record<string, unknown> => {\n // Use the stored schema if available, otherwise default to 'public'\n const schema = relationSchema.state || `public`\n\n return {\n relation: shapeOptions.params?.table\n ? [schema, shapeOptions.params.table]\n : undefined,\n }\n }\n\n let unsubscribeStream: () => void\n\n return {\n sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {\n const { begin, write, commit, markReady, truncate, collection } = params\n\n // Abort controller for the stream - wraps the signal if provided\n const abortController = new AbortController()\n\n if (shapeOptions.signal) {\n shapeOptions.signal.addEventListener(\n `abort`,\n () => {\n abortController.abort()\n },\n {\n once: true,\n }\n )\n if (shapeOptions.signal.aborted) {\n abortController.abort()\n }\n }\n\n const stream = new ShapeStream({\n ...shapeOptions,\n signal: abortController.signal,\n onError: (errorParams) => {\n // Just immediately mark ready if there's an error to avoid blocking\n // apps waiting for `.preload()` to finish.\n // Note that Electric sends a 409 error on a `must-refetch` message, but the\n // ShapeStream handled this and it will not reach this handler, therefor\n // this markReady will not be triggers by a `must-refetch`.\n markReady()\n\n if (shapeOptions.onError) {\n return shapeOptions.onError(errorParams)\n } else {\n console.error(\n `An error occurred while syncing collection: ${collection.id}, \\n` +\n `it has been marked as ready to avoid blocking apps waiting for '.preload()' to finish. \\n` +\n `You can provide an 'onError' handler on the shapeOptions to handle this error, and this message will not be logged.`,\n errorParams\n )\n }\n\n return\n },\n })\n let transactionStarted = false\n const newTxids = new Set<Txid>()\n const newSnapshots: Array<PostgresSnapshot> = []\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 (isSnapshotEndMessage(message)) {\n newSnapshots.push(parseSnapshotMessage(message))\n } else if (isUpToDateMessage(message)) {\n hasUpToDate = true\n } else if (isMustRefetchMessage(message)) {\n debug(\n `Received must-refetch message, starting transaction with truncate`\n )\n\n // Start a transaction and truncate the collection\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n truncate()\n\n // Reset hasUpToDate so we continue accumulating changes until next up-to-date\n hasUpToDate = false\n }\n }\n\n if (hasUpToDate) {\n // Commit transaction if one was started\n if (transactionStarted) {\n commit()\n transactionStarted = false\n }\n\n // Mark the collection as ready now that sync is up to date\n markReady()\n\n // Always commit txids when we receive up-to-date, regardless of transaction state\n seenTxids.setState((currentTxids) => {\n const clonedSeen = new Set<Txid>(currentTxids)\n if (newTxids.size > 0) {\n debug(`new txids synced from pg %O`, Array.from(newTxids))\n }\n newTxids.forEach((txid) => clonedSeen.add(txid))\n newTxids.clear()\n return clonedSeen\n })\n\n // Always commit snapshots when we receive up-to-date, regardless of transaction state\n seenSnapshots.setState((currentSnapshots) => {\n const seen = [...currentSnapshots, ...newSnapshots]\n newSnapshots.forEach((snapshot) =>\n debug(`new snapshot synced from pg %o`, snapshot)\n )\n newSnapshots.length = 0\n return seen\n })\n }\n })\n\n // Return the unsubscribe function\n return () => {\n // Unsubscribe from the stream\n unsubscribeStream()\n // Abort the abort controller to stop the stream\n abortController.abort()\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata,\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"electric.js","sources":["../../src/electric.ts"],"sourcesContent":["import {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n isVisibleInSnapshot,\n} from \"@electric-sql/client\"\nimport { Store } from \"@tanstack/store\"\nimport DebugModule from \"debug\"\nimport {\n ElectricDeleteHandlerMustReturnTxIdError,\n ElectricInsertHandlerMustReturnTxIdError,\n ElectricUpdateHandlerMustReturnTxIdError,\n ExpectedNumberInAwaitTxIdError,\n TimeoutWaitingForTxIdError,\n} from \"./errors\"\nimport type {\n BaseCollectionConfig,\n CollectionConfig,\n DeleteMutationFnParams,\n Fn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type {\n ControlMessage,\n GetExtensions,\n Message,\n PostgresSnapshot,\n Row,\n ShapeStreamOptions,\n} from \"@electric-sql/client\"\n\nconst debug = DebugModule.debug(`ts/db:electric`)\n\n/**\n * Type representing a transaction ID in ElectricSQL\n */\nexport type Txid = number\n\n/**\n * Type representing the result of an insert, update, or delete handler\n */\ntype MaybeTxId =\n | {\n txid?: Txid | Array<Txid>\n }\n | undefined\n | null\n\n/**\n * Type representing a snapshot end message\n */\ntype SnapshotEndMessage = ControlMessage & {\n headers: { control: `snapshot-end` }\n}\n\n// The `InferSchemaOutput` and `ResolveType` are copied from the `@tanstack/db` package\n// but we modified `InferSchemaOutput` slightly to restrict the schema output to `Row<unknown>`\n// This is needed in order for `GetExtensions` to be able to infer the parser extensions type from the schema\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends Row<unknown>\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Configuration interface for Electric collection options\n * @template T - The type of items in the collection\n * @template TSchema - The schema type for validation\n */\nexport interface ElectricCollectionConfig<\n T extends Row<unknown> = Row<unknown>,\n TSchema extends StandardSchemaV1 = never,\n> extends BaseCollectionConfig<\n T,\n string | number,\n TSchema,\n Record<string, Fn>,\n { txid: Txid | Array<Txid> }\n > {\n /**\n * Configuration options for the ElectricSQL ShapeStream\n */\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>\n}\n\nfunction isUpToDateMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { up_to_date: true } {\n return isControlMessage(message) && message.headers.control === `up-to-date`\n}\n\nfunction isMustRefetchMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { headers: { control: `must-refetch` } } {\n return isControlMessage(message) && message.headers.control === `must-refetch`\n}\n\nfunction isSnapshotEndMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is SnapshotEndMessage {\n return isControlMessage(message) && message.headers.control === `snapshot-end`\n}\n\nfunction parseSnapshotMessage(message: SnapshotEndMessage): PostgresSnapshot {\n return {\n xmin: message.headers.xmin,\n xmax: message.headers.xmax,\n xip_list: message.headers.xip_list,\n }\n}\n\n// Check if a message contains txids in its headers\nfunction hasTxids<T extends Row<unknown>>(\n message: Message<T>\n): message is Message<T> & { headers: { txids?: Array<Txid> } } {\n return `txids` in message.headers && Array.isArray(message.headers.txids)\n}\n\n/**\n * Type for the awaitTxId utility function\n */\nexport type AwaitTxIdFn = (txId: Txid, timeout?: number) => Promise<boolean>\n\n/**\n * Electric collection utilities type\n */\nexport interface ElectricCollectionUtils extends UtilsRecord {\n awaitTxId: AwaitTxIdFn\n}\n\n/**\n * Creates Electric collection options for use with a standard Collection\n *\n * @template T - The explicit type of items in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TFallback - The fallback type if no explicit or schema type is provided\n * @param config - Configuration options for the Electric collection\n * @returns Collection options with utilities\n */\n\n// Overload for when schema is provided\nexport function electricCollectionOptions<T extends StandardSchemaV1>(\n config: ElectricCollectionConfig<InferSchemaOutput<T>, T> & {\n schema: T\n }\n): CollectionConfig<InferSchemaOutput<T>, string | number, T> & {\n id?: string\n utils: ElectricCollectionUtils\n schema: T\n}\n\n// Overload for when no schema is provided\nexport function electricCollectionOptions<T extends Row<unknown>>(\n config: ElectricCollectionConfig<T> & {\n schema?: never // prohibit schema\n }\n): CollectionConfig<T, string | number> & {\n id?: string\n utils: ElectricCollectionUtils\n schema?: never // no schema in the result\n}\n\nexport function electricCollectionOptions(\n config: ElectricCollectionConfig<any, any>\n): CollectionConfig<any, string | number, any> & {\n id?: string\n utils: ElectricCollectionUtils\n schema?: any\n} {\n const seenTxids = new Store<Set<Txid>>(new Set([]))\n const seenSnapshots = new Store<Array<PostgresSnapshot>>([])\n const sync = createElectricSync<any>(config.shapeOptions, {\n seenTxids,\n seenSnapshots,\n collectionId: config.id,\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(\n `${config.id ? `[${config.id}] ` : ``}awaitTxId called with txid %d`,\n txId\n )\n if (typeof txId !== `number`) {\n throw new ExpectedNumberInAwaitTxIdError(typeof txId, config.id)\n }\n\n // First check if the txid is in the seenTxids store\n const hasTxid = seenTxids.state.has(txId)\n if (hasTxid) return true\n\n // Then check if the txid is in any of the seen snapshots\n const hasSnapshot = seenSnapshots.state.some((snapshot) =>\n isVisibleInSnapshot(txId, snapshot)\n )\n if (hasSnapshot) return true\n\n return new Promise((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n unsubscribeSeenTxids()\n unsubscribeSeenSnapshots()\n reject(new TimeoutWaitingForTxIdError(txId, config.id))\n }, timeout)\n\n const unsubscribeSeenTxids = seenTxids.subscribe(() => {\n if (seenTxids.state.has(txId)) {\n debug(\n `${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o`,\n txId\n )\n clearTimeout(timeoutId)\n unsubscribeSeenTxids()\n unsubscribeSeenSnapshots()\n resolve(true)\n }\n })\n\n const unsubscribeSeenSnapshots = seenSnapshots.subscribe(() => {\n const visibleSnapshot = seenSnapshots.state.find((snapshot) =>\n isVisibleInSnapshot(txId, snapshot)\n )\n if (visibleSnapshot) {\n debug(\n `${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o in snapshot %o`,\n txId,\n visibleSnapshot\n )\n clearTimeout(timeoutId)\n unsubscribeSeenSnapshots()\n unsubscribeSeenTxids()\n resolve(true)\n }\n })\n })\n }\n\n // Create wrapper handlers for direct persistence operations that handle txid awaiting\n const wrappedOnInsert = config.onInsert\n ? async (params: InsertMutationFnParams<any>) => {\n // Runtime check (that doesn't follow type)\n\n const handlerResult =\n ((await config.onInsert!(params)) as MaybeTxId) ?? {}\n const txid = handlerResult.txid\n\n if (!txid) {\n throw new ElectricInsertHandlerMustReturnTxIdError(config.id)\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = config.onUpdate\n ? async (params: UpdateMutationFnParams<any>) => {\n // Runtime check (that doesn't follow type)\n\n const handlerResult =\n ((await config.onUpdate!(params)) as MaybeTxId) ?? {}\n const txid = handlerResult.txid\n\n if (!txid) {\n throw new ElectricUpdateHandlerMustReturnTxIdError(config.id)\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = config.onDelete\n ? async (params: DeleteMutationFnParams<any>) => {\n const handlerResult = await config.onDelete!(params)\n if (!handlerResult.txid) {\n throw new ElectricDeleteHandlerMustReturnTxIdError(config.id)\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 seenSnapshots: Store<Array<PostgresSnapshot>>\n collectionId?: string\n }\n): SyncConfig<T> {\n const { seenTxids, seenSnapshots, collectionId } = options\n\n // Store for the relation schema information\n const relationSchema = new Store<string | undefined>(undefined)\n\n /**\n * Get the sync metadata for insert operations\n * @returns Record containing relation information\n */\n const getSyncMetadata = (): Record<string, unknown> => {\n // Use the stored schema if available, otherwise default to 'public'\n const schema = relationSchema.state || `public`\n\n return {\n relation: shapeOptions.params?.table\n ? [schema, shapeOptions.params.table]\n : undefined,\n }\n }\n\n let unsubscribeStream: () => void\n\n return {\n sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {\n const { begin, write, commit, markReady, truncate, collection } = params\n\n // Abort controller for the stream - wraps the signal if provided\n const abortController = new AbortController()\n\n if (shapeOptions.signal) {\n shapeOptions.signal.addEventListener(\n `abort`,\n () => {\n abortController.abort()\n },\n {\n once: true,\n }\n )\n if (shapeOptions.signal.aborted) {\n abortController.abort()\n }\n }\n\n const stream = new ShapeStream({\n ...shapeOptions,\n signal: abortController.signal,\n onError: (errorParams) => {\n // Just immediately mark ready if there's an error to avoid blocking\n // apps waiting for `.preload()` to finish.\n // Note that Electric sends a 409 error on a `must-refetch` message, but the\n // ShapeStream handled this and it will not reach this handler, therefor\n // this markReady will not be triggers by a `must-refetch`.\n markReady()\n\n if (shapeOptions.onError) {\n return shapeOptions.onError(errorParams)\n } else {\n console.error(\n `An error occurred while syncing collection: ${collection.id}, \\n` +\n `it has been marked as ready to avoid blocking apps waiting for '.preload()' to finish. \\n` +\n `You can provide an 'onError' handler on the shapeOptions to handle this error, and this message will not be logged.`,\n errorParams\n )\n }\n\n return\n },\n })\n let transactionStarted = false\n const newTxids = new Set<Txid>()\n const newSnapshots: Array<PostgresSnapshot> = []\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 (isSnapshotEndMessage(message)) {\n newSnapshots.push(parseSnapshotMessage(message))\n } else if (isUpToDateMessage(message)) {\n hasUpToDate = true\n } else if (isMustRefetchMessage(message)) {\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Received must-refetch message, starting transaction with truncate`\n )\n\n // Start a transaction and truncate the collection\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n truncate()\n\n // Reset hasUpToDate so we continue accumulating changes until next up-to-date\n hasUpToDate = false\n }\n }\n\n if (hasUpToDate) {\n // Commit transaction if one was started\n if (transactionStarted) {\n commit()\n transactionStarted = false\n }\n\n // Mark the collection as ready now that sync is up to date\n markReady()\n\n // Always commit txids when we receive up-to-date, regardless of transaction state\n seenTxids.setState((currentTxids) => {\n const clonedSeen = new Set<Txid>(currentTxids)\n if (newTxids.size > 0) {\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}new txids synced from pg %O`,\n Array.from(newTxids)\n )\n }\n newTxids.forEach((txid) => clonedSeen.add(txid))\n newTxids.clear()\n return clonedSeen\n })\n\n // Always commit snapshots when we receive up-to-date, regardless of transaction state\n seenSnapshots.setState((currentSnapshots) => {\n const seen = [...currentSnapshots, ...newSnapshots]\n newSnapshots.forEach((snapshot) =>\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}new snapshot synced from pg %o`,\n snapshot\n )\n )\n newSnapshots.length = 0\n return seen\n })\n }\n })\n\n // Return the unsubscribe function\n return () => {\n // Unsubscribe from the stream\n unsubscribeStream()\n // Abort the abort controller to stop the stream\n abortController.abort()\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata,\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1,19 +1,19 @@
1
1
  import { TanStackDBError } from '@tanstack/db';
2
2
  export declare class ElectricDBCollectionError extends TanStackDBError {
3
- constructor(message: string);
3
+ constructor(message: string, collectionId?: string);
4
4
  }
5
5
  export declare class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {
6
- constructor(txIdType: string);
6
+ constructor(txIdType: string, collectionId?: string);
7
7
  }
8
8
  export declare class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {
9
- constructor(txId: number);
9
+ constructor(txId: number, collectionId?: string);
10
10
  }
11
11
  export declare class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {
12
- constructor();
12
+ constructor(collectionId?: string);
13
13
  }
14
14
  export declare class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {
15
- constructor();
15
+ constructor(collectionId?: string);
16
16
  }
17
17
  export declare class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {
18
- constructor();
18
+ constructor(collectionId?: string);
19
19
  }
@@ -1,42 +1,45 @@
1
1
  import { TanStackDBError } from "@tanstack/db";
2
2
  class ElectricDBCollectionError extends TanStackDBError {
3
- constructor(message) {
4
- super(message);
3
+ constructor(message, collectionId) {
4
+ super(`${collectionId ? `[${collectionId}] ` : ``}${message}`);
5
5
  this.name = `ElectricDBCollectionError`;
6
6
  }
7
7
  }
8
8
  class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {
9
- constructor(txIdType) {
10
- super(`Expected number in awaitTxId, received ${txIdType}`);
9
+ constructor(txIdType, collectionId) {
10
+ super(`Expected number in awaitTxId, received ${txIdType}`, collectionId);
11
11
  this.name = `ExpectedNumberInAwaitTxIdError`;
12
12
  }
13
13
  }
14
14
  class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {
15
- constructor(txId) {
16
- super(`Timeout waiting for txId: ${txId}`);
15
+ constructor(txId, collectionId) {
16
+ super(`Timeout waiting for txId: ${txId}`, collectionId);
17
17
  this.name = `TimeoutWaitingForTxIdError`;
18
18
  }
19
19
  }
20
20
  class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {
21
- constructor() {
21
+ constructor(collectionId) {
22
22
  super(
23
- `Electric collection onInsert handler must return a txid or array of txids`
23
+ `Electric collection onInsert handler must return a txid or array of txids`,
24
+ collectionId
24
25
  );
25
26
  this.name = `ElectricInsertHandlerMustReturnTxIdError`;
26
27
  }
27
28
  }
28
29
  class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {
29
- constructor() {
30
+ constructor(collectionId) {
30
31
  super(
31
- `Electric collection onUpdate handler must return a txid or array of txids`
32
+ `Electric collection onUpdate handler must return a txid or array of txids`,
33
+ collectionId
32
34
  );
33
35
  this.name = `ElectricUpdateHandlerMustReturnTxIdError`;
34
36
  }
35
37
  }
36
38
  class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {
37
- constructor() {
39
+ constructor(collectionId) {
38
40
  super(
39
- `Electric collection onDelete handler must return a txid or array of txids`
41
+ `Electric collection onDelete handler must return a txid or array of txids`,
42
+ collectionId
40
43
  );
41
44
  this.name = `ElectricDeleteHandlerMustReturnTxIdError`;
42
45
  }
@@ -1 +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
+ {"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, collectionId?: string) {\n super(`${collectionId ? `[${collectionId}] ` : ``}${message}`)\n this.name = `ElectricDBCollectionError`\n }\n}\n\nexport class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {\n constructor(txIdType: string, collectionId?: string) {\n super(`Expected number in awaitTxId, received ${txIdType}`, collectionId)\n this.name = `ExpectedNumberInAwaitTxIdError`\n }\n}\n\nexport class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {\n constructor(txId: number, collectionId?: string) {\n super(`Timeout waiting for txId: ${txId}`, collectionId)\n this.name = `TimeoutWaitingForTxIdError`\n }\n}\n\nexport class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {\n constructor(collectionId?: string) {\n super(\n `Electric collection onInsert handler must return a txid or array of txids`,\n collectionId\n )\n this.name = `ElectricInsertHandlerMustReturnTxIdError`\n }\n}\n\nexport class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {\n constructor(collectionId?: string) {\n super(\n `Electric collection onUpdate handler must return a txid or array of txids`,\n collectionId\n )\n this.name = `ElectricUpdateHandlerMustReturnTxIdError`\n }\n}\n\nexport class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {\n constructor(collectionId?: string) {\n super(\n `Electric collection onDelete handler must return a txid or array of txids`,\n collectionId\n )\n this.name = `ElectricDeleteHandlerMustReturnTxIdError`\n }\n}\n"],"names":[],"mappings":";AAGO,MAAM,kCAAkC,gBAAgB;AAAA,EAC7D,YAAY,SAAiB,cAAuB;AAClD,UAAM,GAAG,eAAe,IAAI,YAAY,OAAO,EAAE,GAAG,OAAO,EAAE;AAC7D,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,uCAAuC,0BAA0B;AAAA,EAC5E,YAAY,UAAkB,cAAuB;AACnD,UAAM,0CAA0C,QAAQ,IAAI,YAAY;AACxE,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,mCAAmC,0BAA0B;AAAA,EACxE,YAAY,MAAc,cAAuB;AAC/C,UAAM,6BAA6B,IAAI,IAAI,YAAY;AACvD,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,iDAAiD,0BAA0B;AAAA,EACtF,YAAY,cAAuB;AACjC;AAAA,MACE;AAAA,MACA;AAAA,IAAA;AAEF,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,iDAAiD,0BAA0B;AAAA,EACtF,YAAY,cAAuB;AACjC;AAAA,MACE;AAAA,MACA;AAAA,IAAA;AAEF,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,iDAAiD,0BAA0B;AAAA,EACtF,YAAY,cAAuB;AACjC;AAAA,MACE;AAAA,MACA;AAAA,IAAA;AAEF,SAAK,OAAO;AAAA,EACd;AACF;"}
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@tanstack/electric-db-collection",
3
3
  "description": "ElectricSQL collection for TanStack DB",
4
- "version": "0.1.29",
4
+ "version": "0.1.30",
5
5
  "dependencies": {
6
6
  "@electric-sql/client": "^1.0.14",
7
7
  "@standard-schema/spec": "^1.0.0",
8
8
  "@tanstack/store": "^0.7.7",
9
9
  "debug": "^4.4.3",
10
- "@tanstack/db": "0.4.5"
10
+ "@tanstack/db": "0.4.6"
11
11
  },
12
12
  "devDependencies": {
13
13
  "@types/debug": "^4.1.12",
package/src/electric.ts CHANGED
@@ -176,6 +176,7 @@ export function electricCollectionOptions(
176
176
  const sync = createElectricSync<any>(config.shapeOptions, {
177
177
  seenTxids,
178
178
  seenSnapshots,
179
+ collectionId: config.id,
179
180
  })
180
181
 
181
182
  /**
@@ -188,9 +189,12 @@ export function electricCollectionOptions(
188
189
  txId: Txid,
189
190
  timeout: number = 30000
190
191
  ): Promise<boolean> => {
191
- debug(`awaitTxId called with txid %d`, txId)
192
+ debug(
193
+ `${config.id ? `[${config.id}] ` : ``}awaitTxId called with txid %d`,
194
+ txId
195
+ )
192
196
  if (typeof txId !== `number`) {
193
- throw new ExpectedNumberInAwaitTxIdError(typeof txId)
197
+ throw new ExpectedNumberInAwaitTxIdError(typeof txId, config.id)
194
198
  }
195
199
 
196
200
  // First check if the txid is in the seenTxids store
@@ -207,12 +211,15 @@ export function electricCollectionOptions(
207
211
  const timeoutId = setTimeout(() => {
208
212
  unsubscribeSeenTxids()
209
213
  unsubscribeSeenSnapshots()
210
- reject(new TimeoutWaitingForTxIdError(txId))
214
+ reject(new TimeoutWaitingForTxIdError(txId, config.id))
211
215
  }, timeout)
212
216
 
213
217
  const unsubscribeSeenTxids = seenTxids.subscribe(() => {
214
218
  if (seenTxids.state.has(txId)) {
215
- debug(`awaitTxId found match for txid %o`, txId)
219
+ debug(
220
+ `${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o`,
221
+ txId
222
+ )
216
223
  clearTimeout(timeoutId)
217
224
  unsubscribeSeenTxids()
218
225
  unsubscribeSeenSnapshots()
@@ -226,7 +233,7 @@ export function electricCollectionOptions(
226
233
  )
227
234
  if (visibleSnapshot) {
228
235
  debug(
229
- `awaitTxId found match for txid %o in snapshot %o`,
236
+ `${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o in snapshot %o`,
230
237
  txId,
231
238
  visibleSnapshot
232
239
  )
@@ -249,7 +256,7 @@ export function electricCollectionOptions(
249
256
  const txid = handlerResult.txid
250
257
 
251
258
  if (!txid) {
252
- throw new ElectricInsertHandlerMustReturnTxIdError()
259
+ throw new ElectricInsertHandlerMustReturnTxIdError(config.id)
253
260
  }
254
261
 
255
262
  // Handle both single txid and array of txids
@@ -272,7 +279,7 @@ export function electricCollectionOptions(
272
279
  const txid = handlerResult.txid
273
280
 
274
281
  if (!txid) {
275
- throw new ElectricUpdateHandlerMustReturnTxIdError()
282
+ throw new ElectricUpdateHandlerMustReturnTxIdError(config.id)
276
283
  }
277
284
 
278
285
  // Handle both single txid and array of txids
@@ -290,7 +297,7 @@ export function electricCollectionOptions(
290
297
  ? async (params: DeleteMutationFnParams<any>) => {
291
298
  const handlerResult = await config.onDelete!(params)
292
299
  if (!handlerResult.txid) {
293
- throw new ElectricDeleteHandlerMustReturnTxIdError()
300
+ throw new ElectricDeleteHandlerMustReturnTxIdError(config.id)
294
301
  }
295
302
 
296
303
  // Handle both single txid and array of txids
@@ -333,10 +340,10 @@ function createElectricSync<T extends Row<unknown>>(
333
340
  options: {
334
341
  seenTxids: Store<Set<Txid>>
335
342
  seenSnapshots: Store<Array<PostgresSnapshot>>
343
+ collectionId?: string
336
344
  }
337
345
  ): SyncConfig<T> {
338
- const { seenTxids } = options
339
- const { seenSnapshots } = options
346
+ const { seenTxids, seenSnapshots, collectionId } = options
340
347
 
341
348
  // Store for the relation schema information
342
349
  const relationSchema = new Store<string | undefined>(undefined)
@@ -445,7 +452,7 @@ function createElectricSync<T extends Row<unknown>>(
445
452
  hasUpToDate = true
446
453
  } else if (isMustRefetchMessage(message)) {
447
454
  debug(
448
- `Received must-refetch message, starting transaction with truncate`
455
+ `${collectionId ? `[${collectionId}] ` : ``}Received must-refetch message, starting transaction with truncate`
449
456
  )
450
457
 
451
458
  // Start a transaction and truncate the collection
@@ -475,7 +482,10 @@ function createElectricSync<T extends Row<unknown>>(
475
482
  seenTxids.setState((currentTxids) => {
476
483
  const clonedSeen = new Set<Txid>(currentTxids)
477
484
  if (newTxids.size > 0) {
478
- debug(`new txids synced from pg %O`, Array.from(newTxids))
485
+ debug(
486
+ `${collectionId ? `[${collectionId}] ` : ``}new txids synced from pg %O`,
487
+ Array.from(newTxids)
488
+ )
479
489
  }
480
490
  newTxids.forEach((txid) => clonedSeen.add(txid))
481
491
  newTxids.clear()
@@ -486,7 +496,10 @@ function createElectricSync<T extends Row<unknown>>(
486
496
  seenSnapshots.setState((currentSnapshots) => {
487
497
  const seen = [...currentSnapshots, ...newSnapshots]
488
498
  newSnapshots.forEach((snapshot) =>
489
- debug(`new snapshot synced from pg %o`, snapshot)
499
+ debug(
500
+ `${collectionId ? `[${collectionId}] ` : ``}new snapshot synced from pg %o`,
501
+ snapshot
502
+ )
490
503
  )
491
504
  newSnapshots.length = 0
492
505
  return seen
package/src/errors.ts CHANGED
@@ -2,48 +2,51 @@ import { TanStackDBError } from "@tanstack/db"
2
2
 
3
3
  // Electric DB Collection Errors
4
4
  export class ElectricDBCollectionError extends TanStackDBError {
5
- constructor(message: string) {
6
- super(message)
5
+ constructor(message: string, collectionId?: string) {
6
+ super(`${collectionId ? `[${collectionId}] ` : ``}${message}`)
7
7
  this.name = `ElectricDBCollectionError`
8
8
  }
9
9
  }
10
10
 
11
11
  export class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {
12
- constructor(txIdType: string) {
13
- super(`Expected number in awaitTxId, received ${txIdType}`)
12
+ constructor(txIdType: string, collectionId?: string) {
13
+ super(`Expected number in awaitTxId, received ${txIdType}`, collectionId)
14
14
  this.name = `ExpectedNumberInAwaitTxIdError`
15
15
  }
16
16
  }
17
17
 
18
18
  export class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {
19
- constructor(txId: number) {
20
- super(`Timeout waiting for txId: ${txId}`)
19
+ constructor(txId: number, collectionId?: string) {
20
+ super(`Timeout waiting for txId: ${txId}`, collectionId)
21
21
  this.name = `TimeoutWaitingForTxIdError`
22
22
  }
23
23
  }
24
24
 
25
25
  export class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {
26
- constructor() {
26
+ constructor(collectionId?: string) {
27
27
  super(
28
- `Electric collection onInsert handler must return a txid or array of txids`
28
+ `Electric collection onInsert handler must return a txid or array of txids`,
29
+ collectionId
29
30
  )
30
31
  this.name = `ElectricInsertHandlerMustReturnTxIdError`
31
32
  }
32
33
  }
33
34
 
34
35
  export class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {
35
- constructor() {
36
+ constructor(collectionId?: string) {
36
37
  super(
37
- `Electric collection onUpdate handler must return a txid or array of txids`
38
+ `Electric collection onUpdate handler must return a txid or array of txids`,
39
+ collectionId
38
40
  )
39
41
  this.name = `ElectricUpdateHandlerMustReturnTxIdError`
40
42
  }
41
43
  }
42
44
 
43
45
  export class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {
44
- constructor() {
46
+ constructor(collectionId?: string) {
45
47
  super(
46
- `Electric collection onDelete handler must return a txid or array of txids`
48
+ `Electric collection onDelete handler must return a txid or array of txids`,
49
+ collectionId
47
50
  )
48
51
  this.name = `ElectricDeleteHandlerMustReturnTxIdError`
49
52
  }