@tanstack/electric-db-collection 0.1.28 → 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.
- package/dist/cjs/electric.cjs +72 -14
- package/dist/cjs/electric.cjs.map +1 -1
- package/dist/cjs/errors.cjs +15 -12
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +6 -6
- package/dist/esm/electric.js +73 -15
- package/dist/esm/electric.js.map +1 -1
- package/dist/esm/errors.d.ts +6 -6
- package/dist/esm/errors.js +15 -12
- package/dist/esm/errors.js.map +1 -1
- package/package.json +3 -3
- package/src/electric.ts +108 -17
- package/src/errors.ts +15 -12
package/dist/cjs/electric.cjs
CHANGED
|
@@ -11,31 +11,72 @@ function isUpToDateMessage(message) {
|
|
|
11
11
|
function isMustRefetchMessage(message) {
|
|
12
12
|
return client.isControlMessage(message) && message.headers.control === `must-refetch`;
|
|
13
13
|
}
|
|
14
|
+
function isSnapshotEndMessage(message) {
|
|
15
|
+
return client.isControlMessage(message) && message.headers.control === `snapshot-end`;
|
|
16
|
+
}
|
|
17
|
+
function parseSnapshotMessage(message) {
|
|
18
|
+
return {
|
|
19
|
+
xmin: message.headers.xmin,
|
|
20
|
+
xmax: message.headers.xmax,
|
|
21
|
+
xip_list: message.headers.xip_list
|
|
22
|
+
};
|
|
23
|
+
}
|
|
14
24
|
function hasTxids(message) {
|
|
15
25
|
return `txids` in message.headers && Array.isArray(message.headers.txids);
|
|
16
26
|
}
|
|
17
27
|
function electricCollectionOptions(config) {
|
|
18
28
|
const seenTxids = new store.Store(/* @__PURE__ */ new Set([]));
|
|
29
|
+
const seenSnapshots = new store.Store([]);
|
|
19
30
|
const sync = createElectricSync(config.shapeOptions, {
|
|
20
|
-
seenTxids
|
|
31
|
+
seenTxids,
|
|
32
|
+
seenSnapshots,
|
|
33
|
+
collectionId: config.id
|
|
21
34
|
});
|
|
22
35
|
const awaitTxId = async (txId, timeout = 3e4) => {
|
|
23
|
-
debug(
|
|
36
|
+
debug(
|
|
37
|
+
`${config.id ? `[${config.id}] ` : ``}awaitTxId called with txid %d`,
|
|
38
|
+
txId
|
|
39
|
+
);
|
|
24
40
|
if (typeof txId !== `number`) {
|
|
25
|
-
throw new errors.ExpectedNumberInAwaitTxIdError(typeof txId);
|
|
41
|
+
throw new errors.ExpectedNumberInAwaitTxIdError(typeof txId, config.id);
|
|
26
42
|
}
|
|
27
43
|
const hasTxid = seenTxids.state.has(txId);
|
|
28
44
|
if (hasTxid) return true;
|
|
45
|
+
const hasSnapshot = seenSnapshots.state.some(
|
|
46
|
+
(snapshot) => client.isVisibleInSnapshot(txId, snapshot)
|
|
47
|
+
);
|
|
48
|
+
if (hasSnapshot) return true;
|
|
29
49
|
return new Promise((resolve, reject) => {
|
|
30
50
|
const timeoutId = setTimeout(() => {
|
|
31
|
-
|
|
32
|
-
|
|
51
|
+
unsubscribeSeenTxids();
|
|
52
|
+
unsubscribeSeenSnapshots();
|
|
53
|
+
reject(new errors.TimeoutWaitingForTxIdError(txId, config.id));
|
|
33
54
|
}, timeout);
|
|
34
|
-
const
|
|
55
|
+
const unsubscribeSeenTxids = seenTxids.subscribe(() => {
|
|
35
56
|
if (seenTxids.state.has(txId)) {
|
|
36
|
-
debug(
|
|
57
|
+
debug(
|
|
58
|
+
`${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o`,
|
|
59
|
+
txId
|
|
60
|
+
);
|
|
37
61
|
clearTimeout(timeoutId);
|
|
38
|
-
|
|
62
|
+
unsubscribeSeenTxids();
|
|
63
|
+
unsubscribeSeenSnapshots();
|
|
64
|
+
resolve(true);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const unsubscribeSeenSnapshots = seenSnapshots.subscribe(() => {
|
|
68
|
+
const visibleSnapshot = seenSnapshots.state.find(
|
|
69
|
+
(snapshot) => client.isVisibleInSnapshot(txId, snapshot)
|
|
70
|
+
);
|
|
71
|
+
if (visibleSnapshot) {
|
|
72
|
+
debug(
|
|
73
|
+
`${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o in snapshot %o`,
|
|
74
|
+
txId,
|
|
75
|
+
visibleSnapshot
|
|
76
|
+
);
|
|
77
|
+
clearTimeout(timeoutId);
|
|
78
|
+
unsubscribeSeenSnapshots();
|
|
79
|
+
unsubscribeSeenTxids();
|
|
39
80
|
resolve(true);
|
|
40
81
|
}
|
|
41
82
|
});
|
|
@@ -45,7 +86,7 @@ function electricCollectionOptions(config) {
|
|
|
45
86
|
const handlerResult = await config.onInsert(params) ?? {};
|
|
46
87
|
const txid = handlerResult.txid;
|
|
47
88
|
if (!txid) {
|
|
48
|
-
throw new errors.ElectricInsertHandlerMustReturnTxIdError();
|
|
89
|
+
throw new errors.ElectricInsertHandlerMustReturnTxIdError(config.id);
|
|
49
90
|
}
|
|
50
91
|
if (Array.isArray(txid)) {
|
|
51
92
|
await Promise.all(txid.map((id) => awaitTxId(id)));
|
|
@@ -58,7 +99,7 @@ function electricCollectionOptions(config) {
|
|
|
58
99
|
const handlerResult = await config.onUpdate(params) ?? {};
|
|
59
100
|
const txid = handlerResult.txid;
|
|
60
101
|
if (!txid) {
|
|
61
|
-
throw new errors.ElectricUpdateHandlerMustReturnTxIdError();
|
|
102
|
+
throw new errors.ElectricUpdateHandlerMustReturnTxIdError(config.id);
|
|
62
103
|
}
|
|
63
104
|
if (Array.isArray(txid)) {
|
|
64
105
|
await Promise.all(txid.map((id) => awaitTxId(id)));
|
|
@@ -70,7 +111,7 @@ function electricCollectionOptions(config) {
|
|
|
70
111
|
const wrappedOnDelete = config.onDelete ? async (params) => {
|
|
71
112
|
const handlerResult = await config.onDelete(params);
|
|
72
113
|
if (!handlerResult.txid) {
|
|
73
|
-
throw new errors.ElectricDeleteHandlerMustReturnTxIdError();
|
|
114
|
+
throw new errors.ElectricDeleteHandlerMustReturnTxIdError(config.id);
|
|
74
115
|
}
|
|
75
116
|
if (Array.isArray(handlerResult.txid)) {
|
|
76
117
|
await Promise.all(handlerResult.txid.map((id) => awaitTxId(id)));
|
|
@@ -98,7 +139,7 @@ function electricCollectionOptions(config) {
|
|
|
98
139
|
};
|
|
99
140
|
}
|
|
100
141
|
function createElectricSync(shapeOptions, options) {
|
|
101
|
-
const { seenTxids } = options;
|
|
142
|
+
const { seenTxids, seenSnapshots, collectionId } = options;
|
|
102
143
|
const relationSchema = new store.Store(void 0);
|
|
103
144
|
const getSyncMetadata = () => {
|
|
104
145
|
const schema = relationSchema.state || `public`;
|
|
@@ -145,6 +186,7 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
145
186
|
});
|
|
146
187
|
let transactionStarted = false;
|
|
147
188
|
const newTxids = /* @__PURE__ */ new Set();
|
|
189
|
+
const newSnapshots = [];
|
|
148
190
|
unsubscribeStream = stream.subscribe((messages) => {
|
|
149
191
|
let hasUpToDate = false;
|
|
150
192
|
for (const message of messages) {
|
|
@@ -168,11 +210,13 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
168
210
|
...message.headers
|
|
169
211
|
}
|
|
170
212
|
});
|
|
213
|
+
} else if (isSnapshotEndMessage(message)) {
|
|
214
|
+
newSnapshots.push(parseSnapshotMessage(message));
|
|
171
215
|
} else if (isUpToDateMessage(message)) {
|
|
172
216
|
hasUpToDate = true;
|
|
173
217
|
} else if (isMustRefetchMessage(message)) {
|
|
174
218
|
debug(
|
|
175
|
-
`Received must-refetch message, starting transaction with truncate`
|
|
219
|
+
`${collectionId ? `[${collectionId}] ` : ``}Received must-refetch message, starting transaction with truncate`
|
|
176
220
|
);
|
|
177
221
|
if (!transactionStarted) {
|
|
178
222
|
begin();
|
|
@@ -191,12 +235,26 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
191
235
|
seenTxids.setState((currentTxids) => {
|
|
192
236
|
const clonedSeen = new Set(currentTxids);
|
|
193
237
|
if (newTxids.size > 0) {
|
|
194
|
-
debug(
|
|
238
|
+
debug(
|
|
239
|
+
`${collectionId ? `[${collectionId}] ` : ``}new txids synced from pg %O`,
|
|
240
|
+
Array.from(newTxids)
|
|
241
|
+
);
|
|
195
242
|
}
|
|
196
243
|
newTxids.forEach((txid) => clonedSeen.add(txid));
|
|
197
244
|
newTxids.clear();
|
|
198
245
|
return clonedSeen;
|
|
199
246
|
});
|
|
247
|
+
seenSnapshots.setState((currentSnapshots) => {
|
|
248
|
+
const seen = [...currentSnapshots, ...newSnapshots];
|
|
249
|
+
newSnapshots.forEach(
|
|
250
|
+
(snapshot) => debug(
|
|
251
|
+
`${collectionId ? `[${collectionId}] ` : ``}new snapshot synced from pg %o`,
|
|
252
|
+
snapshot
|
|
253
|
+
)
|
|
254
|
+
);
|
|
255
|
+
newSnapshots.length = 0;
|
|
256
|
+
return seen;
|
|
257
|
+
});
|
|
200
258
|
}
|
|
201
259
|
});
|
|
202
260
|
return () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"electric.cjs","sources":["../../src/electric.ts"],"sourcesContent":["import {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n} from \"@electric-sql/client\"\nimport { Store } from \"@tanstack/store\"\nimport DebugModule from \"debug\"\nimport {\n ElectricDeleteHandlerMustReturnTxIdError,\n ElectricInsertHandlerMustReturnTxIdError,\n ElectricUpdateHandlerMustReturnTxIdError,\n ExpectedNumberInAwaitTxIdError,\n TimeoutWaitingForTxIdError,\n} from \"./errors\"\nimport type {\n BaseCollectionConfig,\n CollectionConfig,\n DeleteMutationFnParams,\n Fn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type {\n ControlMessage,\n GetExtensions,\n Message,\n Row,\n ShapeStreamOptions,\n} from \"@electric-sql/client\"\n\nconst debug = DebugModule.debug(`ts/db:electric`)\n\n/**\n * Type representing a transaction ID in ElectricSQL\n */\nexport type Txid = number\n\n// The `InferSchemaOutput` and `ResolveType` are copied from the `@tanstack/db` package\n// but we modified `InferSchemaOutput` slightly to restrict the schema output to `Row<unknown>`\n// This is needed in order for `GetExtensions` to be able to infer the parser extensions type from the schema\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends Row<unknown>\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Configuration interface for Electric collection options\n * @template T - The type of items in the collection\n * @template TSchema - The schema type for validation\n */\nexport interface ElectricCollectionConfig<\n T extends Row<unknown> = Row<unknown>,\n TSchema extends StandardSchemaV1 = never,\n> extends BaseCollectionConfig<\n T,\n string | number,\n TSchema,\n Record<string, Fn>,\n { txid: Txid | Array<Txid> }\n > {\n /**\n * Configuration options for the ElectricSQL ShapeStream\n */\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>\n}\n\nfunction isUpToDateMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { up_to_date: true } {\n return isControlMessage(message) && message.headers.control === `up-to-date`\n}\n\nfunction isMustRefetchMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { headers: { control: `must-refetch` } } {\n return isControlMessage(message) && message.headers.control === `must-refetch`\n}\n\n// Check if a message contains txids in its headers\nfunction hasTxids<T extends Row<unknown>>(\n message: Message<T>\n): message is Message<T> & { headers: { txids?: Array<Txid> } } {\n return `txids` in message.headers && Array.isArray(message.headers.txids)\n}\n\n/**\n * Type for the awaitTxId utility function\n */\nexport type AwaitTxIdFn = (txId: Txid, timeout?: number) => Promise<boolean>\n\n/**\n * Electric collection utilities type\n */\nexport interface ElectricCollectionUtils extends UtilsRecord {\n awaitTxId: AwaitTxIdFn\n}\n\n/**\n * Creates Electric collection options for use with a standard Collection\n *\n * @template T - The explicit type of items in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TFallback - The fallback type if no explicit or schema type is provided\n * @param config - Configuration options for the Electric collection\n * @returns Collection options with utilities\n */\n\n// Overload for when schema is provided\nexport function electricCollectionOptions<T extends StandardSchemaV1>(\n config: ElectricCollectionConfig<InferSchemaOutput<T>, T> & {\n schema: T\n }\n): CollectionConfig<InferSchemaOutput<T>, string | number, T> & {\n id?: string\n utils: ElectricCollectionUtils\n schema: T\n}\n\n// Overload for when no schema is provided\nexport function electricCollectionOptions<T extends Row<unknown>>(\n config: ElectricCollectionConfig<T> & {\n schema?: never // prohibit schema\n }\n): CollectionConfig<T, string | number> & {\n id?: string\n utils: ElectricCollectionUtils\n schema?: never // no schema in the result\n}\n\nexport function electricCollectionOptions(\n config: ElectricCollectionConfig<any, any>\n): CollectionConfig<any, string | number, any> & {\n id?: string\n utils: ElectricCollectionUtils\n schema?: any\n} {\n const seenTxids = new Store<Set<Txid>>(new Set([]))\n const sync = createElectricSync<any>(config.shapeOptions, {\n seenTxids,\n })\n\n /**\n * Wait for a specific transaction ID to be synced\n * @param txId The transaction ID to wait for as a number\n * @param timeout Optional timeout in milliseconds (defaults to 30000ms)\n * @returns Promise that resolves when the txId is synced\n */\n const awaitTxId: AwaitTxIdFn = async (\n txId: Txid,\n timeout: number = 30000\n ): Promise<boolean> => {\n debug(`awaitTxId called with txid %d`, txId)\n if (typeof txId !== `number`) {\n throw new ExpectedNumberInAwaitTxIdError(typeof txId)\n }\n\n const hasTxid = seenTxids.state.has(txId)\n if (hasTxid) return true\n\n return new Promise((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n unsubscribe()\n reject(new TimeoutWaitingForTxIdError(txId))\n }, timeout)\n\n const unsubscribe = seenTxids.subscribe(() => {\n if (seenTxids.state.has(txId)) {\n debug(`awaitTxId found match for txid %o`, txId)\n clearTimeout(timeoutId)\n unsubscribe()\n resolve(true)\n }\n })\n })\n }\n\n // Create wrapper handlers for direct persistence operations that handle txid awaiting\n const wrappedOnInsert = config.onInsert\n ? async (params: InsertMutationFnParams<any>) => {\n // Runtime check (that doesn't follow type)\n\n const handlerResult = (await config.onInsert!(params)) ?? {}\n const txid = (handlerResult as { txid?: Txid | Array<Txid> }).txid\n\n if (!txid) {\n throw new ElectricInsertHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = config.onUpdate\n ? async (params: UpdateMutationFnParams<any>) => {\n // Runtime check (that doesn't follow type)\n\n const handlerResult = (await config.onUpdate!(params)) ?? {}\n const txid = (handlerResult as { txid?: Txid | Array<Txid> }).txid\n\n if (!txid) {\n throw new ElectricUpdateHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = config.onDelete\n ? async (params: DeleteMutationFnParams<any>) => {\n const handlerResult = await config.onDelete!(params)\n if (!handlerResult.txid) {\n throw new ElectricDeleteHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(handlerResult.txid)) {\n await Promise.all(handlerResult.txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(handlerResult.txid)\n }\n\n return handlerResult\n }\n : undefined\n\n // Extract standard Collection config properties\n const {\n shapeOptions: _shapeOptions,\n onInsert: _onInsert,\n onUpdate: _onUpdate,\n onDelete: _onDelete,\n ...restConfig\n } = config\n\n return {\n ...restConfig,\n sync,\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n awaitTxId,\n },\n }\n}\n\n/**\n * Internal function to create ElectricSQL sync configuration\n */\nfunction createElectricSync<T extends Row<unknown>>(\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>,\n options: {\n seenTxids: Store<Set<Txid>>\n }\n): SyncConfig<T> {\n const { seenTxids } = options\n\n // Store for the relation schema information\n const relationSchema = new Store<string | undefined>(undefined)\n\n /**\n * Get the sync metadata for insert operations\n * @returns Record containing relation information\n */\n const getSyncMetadata = (): Record<string, unknown> => {\n // Use the stored schema if available, otherwise default to 'public'\n const schema = relationSchema.state || `public`\n\n return {\n relation: shapeOptions.params?.table\n ? [schema, shapeOptions.params.table]\n : undefined,\n }\n }\n\n let unsubscribeStream: () => void\n\n return {\n sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {\n const { begin, write, commit, markReady, truncate, collection } = params\n\n // Abort controller for the stream - wraps the signal if provided\n const abortController = new AbortController()\n\n if (shapeOptions.signal) {\n shapeOptions.signal.addEventListener(\n `abort`,\n () => {\n abortController.abort()\n },\n {\n once: true,\n }\n )\n if (shapeOptions.signal.aborted) {\n abortController.abort()\n }\n }\n\n const stream = new ShapeStream({\n ...shapeOptions,\n signal: abortController.signal,\n onError: (errorParams) => {\n // Just immediately mark ready if there's an error to avoid blocking\n // apps waiting for `.preload()` to finish.\n // Note that Electric sends a 409 error on a `must-refetch` message, but the\n // ShapeStream handled this and it will not reach this handler, therefor\n // this markReady will not be triggers by a `must-refetch`.\n markReady()\n\n if (shapeOptions.onError) {\n return shapeOptions.onError(errorParams)\n } else {\n console.error(\n `An error occurred while syncing collection: ${collection.id}, \\n` +\n `it has been marked as ready to avoid blocking apps waiting for '.preload()' to finish. \\n` +\n `You can provide an 'onError' handler on the shapeOptions to handle this error, and this message will not be logged.`,\n errorParams\n )\n }\n\n return\n },\n })\n let transactionStarted = false\n const newTxids = new Set<Txid>()\n\n unsubscribeStream = stream.subscribe((messages: Array<Message<T>>) => {\n let hasUpToDate = false\n\n for (const message of messages) {\n // Check for txids in the message and add them to our store\n if (hasTxids(message)) {\n message.headers.txids?.forEach((txid) => newTxids.add(txid))\n }\n\n if (isChangeMessage(message)) {\n // Check if the message contains schema information\n const schema = message.headers.schema\n if (schema && typeof schema === `string`) {\n // Store the schema for future use if it's a valid string\n relationSchema.setState(() => schema)\n }\n\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n write({\n type: message.headers.operation,\n value: message.value,\n // Include the primary key and relation info in the metadata\n metadata: {\n ...message.headers,\n },\n })\n } else if (isUpToDateMessage(message)) {\n hasUpToDate = true\n } else if (isMustRefetchMessage(message)) {\n debug(\n `Received must-refetch message, starting transaction with truncate`\n )\n\n // Start a transaction and truncate the collection\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n truncate()\n\n // Reset hasUpToDate so we continue accumulating changes until next up-to-date\n hasUpToDate = false\n }\n }\n\n if (hasUpToDate) {\n // Commit transaction if one was started\n if (transactionStarted) {\n commit()\n transactionStarted = false\n }\n\n // Mark the collection as ready now that sync is up to date\n markReady()\n\n // Always commit txids when we receive up-to-date, regardless of transaction state\n seenTxids.setState((currentTxids) => {\n const clonedSeen = new Set<Txid>(currentTxids)\n if (newTxids.size > 0) {\n debug(`new txids synced from pg %O`, Array.from(newTxids))\n }\n newTxids.forEach((txid) => clonedSeen.add(txid))\n newTxids.clear()\n return clonedSeen\n })\n }\n })\n\n // Return the unsubscribe function\n return () => {\n // Unsubscribe from the stream\n unsubscribeStream()\n // Abort the abort controller to stop the stream\n abortController.abort()\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata,\n }\n}\n"],"names":["isControlMessage","Store","ExpectedNumberInAwaitTxIdError","TimeoutWaitingForTxIdError","ElectricInsertHandlerMustReturnTxIdError","ElectricUpdateHandlerMustReturnTxIdError","ElectricDeleteHandlerMustReturnTxIdError","ShapeStream","isChangeMessage"],"mappings":"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"}
|
|
1
|
+
{"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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"}
|
package/dist/cjs/errors.cjs
CHANGED
|
@@ -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
|
}
|
package/dist/cjs/errors.cjs.map
CHANGED
|
@@ -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}
|
|
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;;;;;;;"}
|
package/dist/cjs/errors.d.cts
CHANGED
|
@@ -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
|
}
|
package/dist/esm/electric.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ShapeStream, isChangeMessage, isControlMessage } from "@electric-sql/client";
|
|
1
|
+
import { ShapeStream, isChangeMessage, isVisibleInSnapshot, isControlMessage } from "@electric-sql/client";
|
|
2
2
|
import { Store } from "@tanstack/store";
|
|
3
3
|
import DebugModule from "debug";
|
|
4
4
|
import { ElectricInsertHandlerMustReturnTxIdError, ElectricUpdateHandlerMustReturnTxIdError, ElectricDeleteHandlerMustReturnTxIdError, ExpectedNumberInAwaitTxIdError, TimeoutWaitingForTxIdError } from "./errors.js";
|
|
@@ -9,31 +9,72 @@ function isUpToDateMessage(message) {
|
|
|
9
9
|
function isMustRefetchMessage(message) {
|
|
10
10
|
return isControlMessage(message) && message.headers.control === `must-refetch`;
|
|
11
11
|
}
|
|
12
|
+
function isSnapshotEndMessage(message) {
|
|
13
|
+
return isControlMessage(message) && message.headers.control === `snapshot-end`;
|
|
14
|
+
}
|
|
15
|
+
function parseSnapshotMessage(message) {
|
|
16
|
+
return {
|
|
17
|
+
xmin: message.headers.xmin,
|
|
18
|
+
xmax: message.headers.xmax,
|
|
19
|
+
xip_list: message.headers.xip_list
|
|
20
|
+
};
|
|
21
|
+
}
|
|
12
22
|
function hasTxids(message) {
|
|
13
23
|
return `txids` in message.headers && Array.isArray(message.headers.txids);
|
|
14
24
|
}
|
|
15
25
|
function electricCollectionOptions(config) {
|
|
16
26
|
const seenTxids = new Store(/* @__PURE__ */ new Set([]));
|
|
27
|
+
const seenSnapshots = new Store([]);
|
|
17
28
|
const sync = createElectricSync(config.shapeOptions, {
|
|
18
|
-
seenTxids
|
|
29
|
+
seenTxids,
|
|
30
|
+
seenSnapshots,
|
|
31
|
+
collectionId: config.id
|
|
19
32
|
});
|
|
20
33
|
const awaitTxId = async (txId, timeout = 3e4) => {
|
|
21
|
-
debug(
|
|
34
|
+
debug(
|
|
35
|
+
`${config.id ? `[${config.id}] ` : ``}awaitTxId called with txid %d`,
|
|
36
|
+
txId
|
|
37
|
+
);
|
|
22
38
|
if (typeof txId !== `number`) {
|
|
23
|
-
throw new ExpectedNumberInAwaitTxIdError(typeof txId);
|
|
39
|
+
throw new ExpectedNumberInAwaitTxIdError(typeof txId, config.id);
|
|
24
40
|
}
|
|
25
41
|
const hasTxid = seenTxids.state.has(txId);
|
|
26
42
|
if (hasTxid) return true;
|
|
43
|
+
const hasSnapshot = seenSnapshots.state.some(
|
|
44
|
+
(snapshot) => isVisibleInSnapshot(txId, snapshot)
|
|
45
|
+
);
|
|
46
|
+
if (hasSnapshot) return true;
|
|
27
47
|
return new Promise((resolve, reject) => {
|
|
28
48
|
const timeoutId = setTimeout(() => {
|
|
29
|
-
|
|
30
|
-
|
|
49
|
+
unsubscribeSeenTxids();
|
|
50
|
+
unsubscribeSeenSnapshots();
|
|
51
|
+
reject(new TimeoutWaitingForTxIdError(txId, config.id));
|
|
31
52
|
}, timeout);
|
|
32
|
-
const
|
|
53
|
+
const unsubscribeSeenTxids = seenTxids.subscribe(() => {
|
|
33
54
|
if (seenTxids.state.has(txId)) {
|
|
34
|
-
debug(
|
|
55
|
+
debug(
|
|
56
|
+
`${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o`,
|
|
57
|
+
txId
|
|
58
|
+
);
|
|
35
59
|
clearTimeout(timeoutId);
|
|
36
|
-
|
|
60
|
+
unsubscribeSeenTxids();
|
|
61
|
+
unsubscribeSeenSnapshots();
|
|
62
|
+
resolve(true);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const unsubscribeSeenSnapshots = seenSnapshots.subscribe(() => {
|
|
66
|
+
const visibleSnapshot = seenSnapshots.state.find(
|
|
67
|
+
(snapshot) => isVisibleInSnapshot(txId, snapshot)
|
|
68
|
+
);
|
|
69
|
+
if (visibleSnapshot) {
|
|
70
|
+
debug(
|
|
71
|
+
`${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o in snapshot %o`,
|
|
72
|
+
txId,
|
|
73
|
+
visibleSnapshot
|
|
74
|
+
);
|
|
75
|
+
clearTimeout(timeoutId);
|
|
76
|
+
unsubscribeSeenSnapshots();
|
|
77
|
+
unsubscribeSeenTxids();
|
|
37
78
|
resolve(true);
|
|
38
79
|
}
|
|
39
80
|
});
|
|
@@ -43,7 +84,7 @@ function electricCollectionOptions(config) {
|
|
|
43
84
|
const handlerResult = await config.onInsert(params) ?? {};
|
|
44
85
|
const txid = handlerResult.txid;
|
|
45
86
|
if (!txid) {
|
|
46
|
-
throw new ElectricInsertHandlerMustReturnTxIdError();
|
|
87
|
+
throw new ElectricInsertHandlerMustReturnTxIdError(config.id);
|
|
47
88
|
}
|
|
48
89
|
if (Array.isArray(txid)) {
|
|
49
90
|
await Promise.all(txid.map((id) => awaitTxId(id)));
|
|
@@ -56,7 +97,7 @@ function electricCollectionOptions(config) {
|
|
|
56
97
|
const handlerResult = await config.onUpdate(params) ?? {};
|
|
57
98
|
const txid = handlerResult.txid;
|
|
58
99
|
if (!txid) {
|
|
59
|
-
throw new ElectricUpdateHandlerMustReturnTxIdError();
|
|
100
|
+
throw new ElectricUpdateHandlerMustReturnTxIdError(config.id);
|
|
60
101
|
}
|
|
61
102
|
if (Array.isArray(txid)) {
|
|
62
103
|
await Promise.all(txid.map((id) => awaitTxId(id)));
|
|
@@ -68,7 +109,7 @@ function electricCollectionOptions(config) {
|
|
|
68
109
|
const wrappedOnDelete = config.onDelete ? async (params) => {
|
|
69
110
|
const handlerResult = await config.onDelete(params);
|
|
70
111
|
if (!handlerResult.txid) {
|
|
71
|
-
throw new ElectricDeleteHandlerMustReturnTxIdError();
|
|
112
|
+
throw new ElectricDeleteHandlerMustReturnTxIdError(config.id);
|
|
72
113
|
}
|
|
73
114
|
if (Array.isArray(handlerResult.txid)) {
|
|
74
115
|
await Promise.all(handlerResult.txid.map((id) => awaitTxId(id)));
|
|
@@ -96,7 +137,7 @@ function electricCollectionOptions(config) {
|
|
|
96
137
|
};
|
|
97
138
|
}
|
|
98
139
|
function createElectricSync(shapeOptions, options) {
|
|
99
|
-
const { seenTxids } = options;
|
|
140
|
+
const { seenTxids, seenSnapshots, collectionId } = options;
|
|
100
141
|
const relationSchema = new Store(void 0);
|
|
101
142
|
const getSyncMetadata = () => {
|
|
102
143
|
const schema = relationSchema.state || `public`;
|
|
@@ -143,6 +184,7 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
143
184
|
});
|
|
144
185
|
let transactionStarted = false;
|
|
145
186
|
const newTxids = /* @__PURE__ */ new Set();
|
|
187
|
+
const newSnapshots = [];
|
|
146
188
|
unsubscribeStream = stream.subscribe((messages) => {
|
|
147
189
|
let hasUpToDate = false;
|
|
148
190
|
for (const message of messages) {
|
|
@@ -166,11 +208,13 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
166
208
|
...message.headers
|
|
167
209
|
}
|
|
168
210
|
});
|
|
211
|
+
} else if (isSnapshotEndMessage(message)) {
|
|
212
|
+
newSnapshots.push(parseSnapshotMessage(message));
|
|
169
213
|
} else if (isUpToDateMessage(message)) {
|
|
170
214
|
hasUpToDate = true;
|
|
171
215
|
} else if (isMustRefetchMessage(message)) {
|
|
172
216
|
debug(
|
|
173
|
-
`Received must-refetch message, starting transaction with truncate`
|
|
217
|
+
`${collectionId ? `[${collectionId}] ` : ``}Received must-refetch message, starting transaction with truncate`
|
|
174
218
|
);
|
|
175
219
|
if (!transactionStarted) {
|
|
176
220
|
begin();
|
|
@@ -189,12 +233,26 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
189
233
|
seenTxids.setState((currentTxids) => {
|
|
190
234
|
const clonedSeen = new Set(currentTxids);
|
|
191
235
|
if (newTxids.size > 0) {
|
|
192
|
-
debug(
|
|
236
|
+
debug(
|
|
237
|
+
`${collectionId ? `[${collectionId}] ` : ``}new txids synced from pg %O`,
|
|
238
|
+
Array.from(newTxids)
|
|
239
|
+
);
|
|
193
240
|
}
|
|
194
241
|
newTxids.forEach((txid) => clonedSeen.add(txid));
|
|
195
242
|
newTxids.clear();
|
|
196
243
|
return clonedSeen;
|
|
197
244
|
});
|
|
245
|
+
seenSnapshots.setState((currentSnapshots) => {
|
|
246
|
+
const seen = [...currentSnapshots, ...newSnapshots];
|
|
247
|
+
newSnapshots.forEach(
|
|
248
|
+
(snapshot) => debug(
|
|
249
|
+
`${collectionId ? `[${collectionId}] ` : ``}new snapshot synced from pg %o`,
|
|
250
|
+
snapshot
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
newSnapshots.length = 0;
|
|
254
|
+
return seen;
|
|
255
|
+
});
|
|
198
256
|
}
|
|
199
257
|
});
|
|
200
258
|
return () => {
|
package/dist/esm/electric.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"electric.js","sources":["../../src/electric.ts"],"sourcesContent":["import {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n} from \"@electric-sql/client\"\nimport { Store } from \"@tanstack/store\"\nimport DebugModule from \"debug\"\nimport {\n ElectricDeleteHandlerMustReturnTxIdError,\n ElectricInsertHandlerMustReturnTxIdError,\n ElectricUpdateHandlerMustReturnTxIdError,\n ExpectedNumberInAwaitTxIdError,\n TimeoutWaitingForTxIdError,\n} from \"./errors\"\nimport type {\n BaseCollectionConfig,\n CollectionConfig,\n DeleteMutationFnParams,\n Fn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type {\n ControlMessage,\n GetExtensions,\n Message,\n Row,\n ShapeStreamOptions,\n} from \"@electric-sql/client\"\n\nconst debug = DebugModule.debug(`ts/db:electric`)\n\n/**\n * Type representing a transaction ID in ElectricSQL\n */\nexport type Txid = number\n\n// The `InferSchemaOutput` and `ResolveType` are copied from the `@tanstack/db` package\n// but we modified `InferSchemaOutput` slightly to restrict the schema output to `Row<unknown>`\n// This is needed in order for `GetExtensions` to be able to infer the parser extensions type from the schema\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends Row<unknown>\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Configuration interface for Electric collection options\n * @template T - The type of items in the collection\n * @template TSchema - The schema type for validation\n */\nexport interface ElectricCollectionConfig<\n T extends Row<unknown> = Row<unknown>,\n TSchema extends StandardSchemaV1 = never,\n> extends BaseCollectionConfig<\n T,\n string | number,\n TSchema,\n Record<string, Fn>,\n { txid: Txid | Array<Txid> }\n > {\n /**\n * Configuration options for the ElectricSQL ShapeStream\n */\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>\n}\n\nfunction isUpToDateMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { up_to_date: true } {\n return isControlMessage(message) && message.headers.control === `up-to-date`\n}\n\nfunction isMustRefetchMessage<T extends Row<unknown>>(\n message: Message<T>\n): message is ControlMessage & { headers: { control: `must-refetch` } } {\n return isControlMessage(message) && message.headers.control === `must-refetch`\n}\n\n// Check if a message contains txids in its headers\nfunction hasTxids<T extends Row<unknown>>(\n message: Message<T>\n): message is Message<T> & { headers: { txids?: Array<Txid> } } {\n return `txids` in message.headers && Array.isArray(message.headers.txids)\n}\n\n/**\n * Type for the awaitTxId utility function\n */\nexport type AwaitTxIdFn = (txId: Txid, timeout?: number) => Promise<boolean>\n\n/**\n * Electric collection utilities type\n */\nexport interface ElectricCollectionUtils extends UtilsRecord {\n awaitTxId: AwaitTxIdFn\n}\n\n/**\n * Creates Electric collection options for use with a standard Collection\n *\n * @template T - The explicit type of items in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TFallback - The fallback type if no explicit or schema type is provided\n * @param config - Configuration options for the Electric collection\n * @returns Collection options with utilities\n */\n\n// Overload for when schema is provided\nexport function electricCollectionOptions<T extends StandardSchemaV1>(\n config: ElectricCollectionConfig<InferSchemaOutput<T>, T> & {\n schema: T\n }\n): CollectionConfig<InferSchemaOutput<T>, string | number, T> & {\n id?: string\n utils: ElectricCollectionUtils\n schema: T\n}\n\n// Overload for when no schema is provided\nexport function electricCollectionOptions<T extends Row<unknown>>(\n config: ElectricCollectionConfig<T> & {\n schema?: never // prohibit schema\n }\n): CollectionConfig<T, string | number> & {\n id?: string\n utils: ElectricCollectionUtils\n schema?: never // no schema in the result\n}\n\nexport function electricCollectionOptions(\n config: ElectricCollectionConfig<any, any>\n): CollectionConfig<any, string | number, any> & {\n id?: string\n utils: ElectricCollectionUtils\n schema?: any\n} {\n const seenTxids = new Store<Set<Txid>>(new Set([]))\n const sync = createElectricSync<any>(config.shapeOptions, {\n seenTxids,\n })\n\n /**\n * Wait for a specific transaction ID to be synced\n * @param txId The transaction ID to wait for as a number\n * @param timeout Optional timeout in milliseconds (defaults to 30000ms)\n * @returns Promise that resolves when the txId is synced\n */\n const awaitTxId: AwaitTxIdFn = async (\n txId: Txid,\n timeout: number = 30000\n ): Promise<boolean> => {\n debug(`awaitTxId called with txid %d`, txId)\n if (typeof txId !== `number`) {\n throw new ExpectedNumberInAwaitTxIdError(typeof txId)\n }\n\n const hasTxid = seenTxids.state.has(txId)\n if (hasTxid) return true\n\n return new Promise((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n unsubscribe()\n reject(new TimeoutWaitingForTxIdError(txId))\n }, timeout)\n\n const unsubscribe = seenTxids.subscribe(() => {\n if (seenTxids.state.has(txId)) {\n debug(`awaitTxId found match for txid %o`, txId)\n clearTimeout(timeoutId)\n unsubscribe()\n resolve(true)\n }\n })\n })\n }\n\n // Create wrapper handlers for direct persistence operations that handle txid awaiting\n const wrappedOnInsert = config.onInsert\n ? async (params: InsertMutationFnParams<any>) => {\n // Runtime check (that doesn't follow type)\n\n const handlerResult = (await config.onInsert!(params)) ?? {}\n const txid = (handlerResult as { txid?: Txid | Array<Txid> }).txid\n\n if (!txid) {\n throw new ElectricInsertHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = config.onUpdate\n ? async (params: UpdateMutationFnParams<any>) => {\n // Runtime check (that doesn't follow type)\n\n const handlerResult = (await config.onUpdate!(params)) ?? {}\n const txid = (handlerResult as { txid?: Txid | Array<Txid> }).txid\n\n if (!txid) {\n throw new ElectricUpdateHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(txid)) {\n await Promise.all(txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(txid)\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = config.onDelete\n ? async (params: DeleteMutationFnParams<any>) => {\n const handlerResult = await config.onDelete!(params)\n if (!handlerResult.txid) {\n throw new ElectricDeleteHandlerMustReturnTxIdError()\n }\n\n // Handle both single txid and array of txids\n if (Array.isArray(handlerResult.txid)) {\n await Promise.all(handlerResult.txid.map((id) => awaitTxId(id)))\n } else {\n await awaitTxId(handlerResult.txid)\n }\n\n return handlerResult\n }\n : undefined\n\n // Extract standard Collection config properties\n const {\n shapeOptions: _shapeOptions,\n onInsert: _onInsert,\n onUpdate: _onUpdate,\n onDelete: _onDelete,\n ...restConfig\n } = config\n\n return {\n ...restConfig,\n sync,\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n awaitTxId,\n },\n }\n}\n\n/**\n * Internal function to create ElectricSQL sync configuration\n */\nfunction createElectricSync<T extends Row<unknown>>(\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>,\n options: {\n seenTxids: Store<Set<Txid>>\n }\n): SyncConfig<T> {\n const { seenTxids } = options\n\n // Store for the relation schema information\n const relationSchema = new Store<string | undefined>(undefined)\n\n /**\n * Get the sync metadata for insert operations\n * @returns Record containing relation information\n */\n const getSyncMetadata = (): Record<string, unknown> => {\n // Use the stored schema if available, otherwise default to 'public'\n const schema = relationSchema.state || `public`\n\n return {\n relation: shapeOptions.params?.table\n ? [schema, shapeOptions.params.table]\n : undefined,\n }\n }\n\n let unsubscribeStream: () => void\n\n return {\n sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {\n const { begin, write, commit, markReady, truncate, collection } = params\n\n // Abort controller for the stream - wraps the signal if provided\n const abortController = new AbortController()\n\n if (shapeOptions.signal) {\n shapeOptions.signal.addEventListener(\n `abort`,\n () => {\n abortController.abort()\n },\n {\n once: true,\n }\n )\n if (shapeOptions.signal.aborted) {\n abortController.abort()\n }\n }\n\n const stream = new ShapeStream({\n ...shapeOptions,\n signal: abortController.signal,\n onError: (errorParams) => {\n // Just immediately mark ready if there's an error to avoid blocking\n // apps waiting for `.preload()` to finish.\n // Note that Electric sends a 409 error on a `must-refetch` message, but the\n // ShapeStream handled this and it will not reach this handler, therefor\n // this markReady will not be triggers by a `must-refetch`.\n markReady()\n\n if (shapeOptions.onError) {\n return shapeOptions.onError(errorParams)\n } else {\n console.error(\n `An error occurred while syncing collection: ${collection.id}, \\n` +\n `it has been marked as ready to avoid blocking apps waiting for '.preload()' to finish. \\n` +\n `You can provide an 'onError' handler on the shapeOptions to handle this error, and this message will not be logged.`,\n errorParams\n )\n }\n\n return\n },\n })\n let transactionStarted = false\n const newTxids = new Set<Txid>()\n\n unsubscribeStream = stream.subscribe((messages: Array<Message<T>>) => {\n let hasUpToDate = false\n\n for (const message of messages) {\n // Check for txids in the message and add them to our store\n if (hasTxids(message)) {\n message.headers.txids?.forEach((txid) => newTxids.add(txid))\n }\n\n if (isChangeMessage(message)) {\n // Check if the message contains schema information\n const schema = message.headers.schema\n if (schema && typeof schema === `string`) {\n // Store the schema for future use if it's a valid string\n relationSchema.setState(() => schema)\n }\n\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n write({\n type: message.headers.operation,\n value: message.value,\n // Include the primary key and relation info in the metadata\n metadata: {\n ...message.headers,\n },\n })\n } else if (isUpToDateMessage(message)) {\n hasUpToDate = true\n } else if (isMustRefetchMessage(message)) {\n debug(\n `Received must-refetch message, starting transaction with truncate`\n )\n\n // Start a transaction and truncate the collection\n if (!transactionStarted) {\n begin()\n transactionStarted = true\n }\n\n truncate()\n\n // Reset hasUpToDate so we continue accumulating changes until next up-to-date\n hasUpToDate = false\n }\n }\n\n if (hasUpToDate) {\n // Commit transaction if one was started\n if (transactionStarted) {\n commit()\n transactionStarted = false\n }\n\n // Mark the collection as ready now that sync is up to date\n markReady()\n\n // Always commit txids when we receive up-to-date, regardless of transaction state\n seenTxids.setState((currentTxids) => {\n const clonedSeen = new Set<Txid>(currentTxids)\n if (newTxids.size > 0) {\n debug(`new txids synced from pg %O`, Array.from(newTxids))\n }\n newTxids.forEach((txid) => clonedSeen.add(txid))\n newTxids.clear()\n return clonedSeen\n })\n }\n })\n\n // Return the unsubscribe function\n return () => {\n // Unsubscribe from the stream\n unsubscribeStream()\n // Abort the abort controller to stop the stream\n abortController.abort()\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata,\n }\n}\n"],"names":[],"mappings":"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"}
|
|
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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"}
|
package/dist/esm/errors.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/esm/errors.js
CHANGED
|
@@ -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
|
}
|
package/dist/esm/errors.js.map
CHANGED
|
@@ -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}
|
|
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.
|
|
4
|
+
"version": "0.1.30",
|
|
5
5
|
"dependencies": {
|
|
6
|
+
"@electric-sql/client": "^1.0.14",
|
|
6
7
|
"@standard-schema/spec": "^1.0.0",
|
|
7
|
-
"@electric-sql/client": "^1.0.10",
|
|
8
8
|
"@tanstack/store": "^0.7.7",
|
|
9
9
|
"debug": "^4.4.3",
|
|
10
|
-
"@tanstack/db": "0.4.
|
|
10
|
+
"@tanstack/db": "0.4.6"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"@types/debug": "^4.1.12",
|
package/src/electric.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
ShapeStream,
|
|
3
3
|
isChangeMessage,
|
|
4
4
|
isControlMessage,
|
|
5
|
+
isVisibleInSnapshot,
|
|
5
6
|
} from "@electric-sql/client"
|
|
6
7
|
import { Store } from "@tanstack/store"
|
|
7
8
|
import DebugModule from "debug"
|
|
@@ -27,6 +28,7 @@ import type {
|
|
|
27
28
|
ControlMessage,
|
|
28
29
|
GetExtensions,
|
|
29
30
|
Message,
|
|
31
|
+
PostgresSnapshot,
|
|
30
32
|
Row,
|
|
31
33
|
ShapeStreamOptions,
|
|
32
34
|
} from "@electric-sql/client"
|
|
@@ -38,6 +40,23 @@ const debug = DebugModule.debug(`ts/db:electric`)
|
|
|
38
40
|
*/
|
|
39
41
|
export type Txid = number
|
|
40
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Type representing the result of an insert, update, or delete handler
|
|
45
|
+
*/
|
|
46
|
+
type MaybeTxId =
|
|
47
|
+
| {
|
|
48
|
+
txid?: Txid | Array<Txid>
|
|
49
|
+
}
|
|
50
|
+
| undefined
|
|
51
|
+
| null
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Type representing a snapshot end message
|
|
55
|
+
*/
|
|
56
|
+
type SnapshotEndMessage = ControlMessage & {
|
|
57
|
+
headers: { control: `snapshot-end` }
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
// The `InferSchemaOutput` and `ResolveType` are copied from the `@tanstack/db` package
|
|
42
61
|
// but we modified `InferSchemaOutput` slightly to restrict the schema output to `Row<unknown>`
|
|
43
62
|
// This is needed in order for `GetExtensions` to be able to infer the parser extensions type from the schema
|
|
@@ -80,6 +99,20 @@ function isMustRefetchMessage<T extends Row<unknown>>(
|
|
|
80
99
|
return isControlMessage(message) && message.headers.control === `must-refetch`
|
|
81
100
|
}
|
|
82
101
|
|
|
102
|
+
function isSnapshotEndMessage<T extends Row<unknown>>(
|
|
103
|
+
message: Message<T>
|
|
104
|
+
): message is SnapshotEndMessage {
|
|
105
|
+
return isControlMessage(message) && message.headers.control === `snapshot-end`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseSnapshotMessage(message: SnapshotEndMessage): PostgresSnapshot {
|
|
109
|
+
return {
|
|
110
|
+
xmin: message.headers.xmin,
|
|
111
|
+
xmax: message.headers.xmax,
|
|
112
|
+
xip_list: message.headers.xip_list,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
83
116
|
// Check if a message contains txids in its headers
|
|
84
117
|
function hasTxids<T extends Row<unknown>>(
|
|
85
118
|
message: Message<T>
|
|
@@ -139,8 +172,11 @@ export function electricCollectionOptions(
|
|
|
139
172
|
schema?: any
|
|
140
173
|
} {
|
|
141
174
|
const seenTxids = new Store<Set<Txid>>(new Set([]))
|
|
175
|
+
const seenSnapshots = new Store<Array<PostgresSnapshot>>([])
|
|
142
176
|
const sync = createElectricSync<any>(config.shapeOptions, {
|
|
143
177
|
seenTxids,
|
|
178
|
+
seenSnapshots,
|
|
179
|
+
collectionId: config.id,
|
|
144
180
|
})
|
|
145
181
|
|
|
146
182
|
/**
|
|
@@ -153,25 +189,57 @@ export function electricCollectionOptions(
|
|
|
153
189
|
txId: Txid,
|
|
154
190
|
timeout: number = 30000
|
|
155
191
|
): Promise<boolean> => {
|
|
156
|
-
debug(
|
|
192
|
+
debug(
|
|
193
|
+
`${config.id ? `[${config.id}] ` : ``}awaitTxId called with txid %d`,
|
|
194
|
+
txId
|
|
195
|
+
)
|
|
157
196
|
if (typeof txId !== `number`) {
|
|
158
|
-
throw new ExpectedNumberInAwaitTxIdError(typeof txId)
|
|
197
|
+
throw new ExpectedNumberInAwaitTxIdError(typeof txId, config.id)
|
|
159
198
|
}
|
|
160
199
|
|
|
200
|
+
// First check if the txid is in the seenTxids store
|
|
161
201
|
const hasTxid = seenTxids.state.has(txId)
|
|
162
202
|
if (hasTxid) return true
|
|
163
203
|
|
|
204
|
+
// Then check if the txid is in any of the seen snapshots
|
|
205
|
+
const hasSnapshot = seenSnapshots.state.some((snapshot) =>
|
|
206
|
+
isVisibleInSnapshot(txId, snapshot)
|
|
207
|
+
)
|
|
208
|
+
if (hasSnapshot) return true
|
|
209
|
+
|
|
164
210
|
return new Promise((resolve, reject) => {
|
|
165
211
|
const timeoutId = setTimeout(() => {
|
|
166
|
-
|
|
167
|
-
|
|
212
|
+
unsubscribeSeenTxids()
|
|
213
|
+
unsubscribeSeenSnapshots()
|
|
214
|
+
reject(new TimeoutWaitingForTxIdError(txId, config.id))
|
|
168
215
|
}, timeout)
|
|
169
216
|
|
|
170
|
-
const
|
|
217
|
+
const unsubscribeSeenTxids = seenTxids.subscribe(() => {
|
|
171
218
|
if (seenTxids.state.has(txId)) {
|
|
172
|
-
debug(
|
|
219
|
+
debug(
|
|
220
|
+
`${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o`,
|
|
221
|
+
txId
|
|
222
|
+
)
|
|
223
|
+
clearTimeout(timeoutId)
|
|
224
|
+
unsubscribeSeenTxids()
|
|
225
|
+
unsubscribeSeenSnapshots()
|
|
226
|
+
resolve(true)
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
const unsubscribeSeenSnapshots = seenSnapshots.subscribe(() => {
|
|
231
|
+
const visibleSnapshot = seenSnapshots.state.find((snapshot) =>
|
|
232
|
+
isVisibleInSnapshot(txId, snapshot)
|
|
233
|
+
)
|
|
234
|
+
if (visibleSnapshot) {
|
|
235
|
+
debug(
|
|
236
|
+
`${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o in snapshot %o`,
|
|
237
|
+
txId,
|
|
238
|
+
visibleSnapshot
|
|
239
|
+
)
|
|
173
240
|
clearTimeout(timeoutId)
|
|
174
|
-
|
|
241
|
+
unsubscribeSeenSnapshots()
|
|
242
|
+
unsubscribeSeenTxids()
|
|
175
243
|
resolve(true)
|
|
176
244
|
}
|
|
177
245
|
})
|
|
@@ -183,11 +251,12 @@ export function electricCollectionOptions(
|
|
|
183
251
|
? async (params: InsertMutationFnParams<any>) => {
|
|
184
252
|
// Runtime check (that doesn't follow type)
|
|
185
253
|
|
|
186
|
-
const handlerResult =
|
|
187
|
-
|
|
254
|
+
const handlerResult =
|
|
255
|
+
((await config.onInsert!(params)) as MaybeTxId) ?? {}
|
|
256
|
+
const txid = handlerResult.txid
|
|
188
257
|
|
|
189
258
|
if (!txid) {
|
|
190
|
-
throw new ElectricInsertHandlerMustReturnTxIdError()
|
|
259
|
+
throw new ElectricInsertHandlerMustReturnTxIdError(config.id)
|
|
191
260
|
}
|
|
192
261
|
|
|
193
262
|
// Handle both single txid and array of txids
|
|
@@ -205,11 +274,12 @@ export function electricCollectionOptions(
|
|
|
205
274
|
? async (params: UpdateMutationFnParams<any>) => {
|
|
206
275
|
// Runtime check (that doesn't follow type)
|
|
207
276
|
|
|
208
|
-
const handlerResult =
|
|
209
|
-
|
|
277
|
+
const handlerResult =
|
|
278
|
+
((await config.onUpdate!(params)) as MaybeTxId) ?? {}
|
|
279
|
+
const txid = handlerResult.txid
|
|
210
280
|
|
|
211
281
|
if (!txid) {
|
|
212
|
-
throw new ElectricUpdateHandlerMustReturnTxIdError()
|
|
282
|
+
throw new ElectricUpdateHandlerMustReturnTxIdError(config.id)
|
|
213
283
|
}
|
|
214
284
|
|
|
215
285
|
// Handle both single txid and array of txids
|
|
@@ -227,7 +297,7 @@ export function electricCollectionOptions(
|
|
|
227
297
|
? async (params: DeleteMutationFnParams<any>) => {
|
|
228
298
|
const handlerResult = await config.onDelete!(params)
|
|
229
299
|
if (!handlerResult.txid) {
|
|
230
|
-
throw new ElectricDeleteHandlerMustReturnTxIdError()
|
|
300
|
+
throw new ElectricDeleteHandlerMustReturnTxIdError(config.id)
|
|
231
301
|
}
|
|
232
302
|
|
|
233
303
|
// Handle both single txid and array of txids
|
|
@@ -269,9 +339,11 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
269
339
|
shapeOptions: ShapeStreamOptions<GetExtensions<T>>,
|
|
270
340
|
options: {
|
|
271
341
|
seenTxids: Store<Set<Txid>>
|
|
342
|
+
seenSnapshots: Store<Array<PostgresSnapshot>>
|
|
343
|
+
collectionId?: string
|
|
272
344
|
}
|
|
273
345
|
): SyncConfig<T> {
|
|
274
|
-
const { seenTxids } = options
|
|
346
|
+
const { seenTxids, seenSnapshots, collectionId } = options
|
|
275
347
|
|
|
276
348
|
// Store for the relation schema information
|
|
277
349
|
const relationSchema = new Store<string | undefined>(undefined)
|
|
@@ -342,6 +414,7 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
342
414
|
})
|
|
343
415
|
let transactionStarted = false
|
|
344
416
|
const newTxids = new Set<Txid>()
|
|
417
|
+
const newSnapshots: Array<PostgresSnapshot> = []
|
|
345
418
|
|
|
346
419
|
unsubscribeStream = stream.subscribe((messages: Array<Message<T>>) => {
|
|
347
420
|
let hasUpToDate = false
|
|
@@ -373,11 +446,13 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
373
446
|
...message.headers,
|
|
374
447
|
},
|
|
375
448
|
})
|
|
449
|
+
} else if (isSnapshotEndMessage(message)) {
|
|
450
|
+
newSnapshots.push(parseSnapshotMessage(message))
|
|
376
451
|
} else if (isUpToDateMessage(message)) {
|
|
377
452
|
hasUpToDate = true
|
|
378
453
|
} else if (isMustRefetchMessage(message)) {
|
|
379
454
|
debug(
|
|
380
|
-
`Received must-refetch message, starting transaction with truncate`
|
|
455
|
+
`${collectionId ? `[${collectionId}] ` : ``}Received must-refetch message, starting transaction with truncate`
|
|
381
456
|
)
|
|
382
457
|
|
|
383
458
|
// Start a transaction and truncate the collection
|
|
@@ -407,12 +482,28 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
407
482
|
seenTxids.setState((currentTxids) => {
|
|
408
483
|
const clonedSeen = new Set<Txid>(currentTxids)
|
|
409
484
|
if (newTxids.size > 0) {
|
|
410
|
-
debug(
|
|
485
|
+
debug(
|
|
486
|
+
`${collectionId ? `[${collectionId}] ` : ``}new txids synced from pg %O`,
|
|
487
|
+
Array.from(newTxids)
|
|
488
|
+
)
|
|
411
489
|
}
|
|
412
490
|
newTxids.forEach((txid) => clonedSeen.add(txid))
|
|
413
491
|
newTxids.clear()
|
|
414
492
|
return clonedSeen
|
|
415
493
|
})
|
|
494
|
+
|
|
495
|
+
// Always commit snapshots when we receive up-to-date, regardless of transaction state
|
|
496
|
+
seenSnapshots.setState((currentSnapshots) => {
|
|
497
|
+
const seen = [...currentSnapshots, ...newSnapshots]
|
|
498
|
+
newSnapshots.forEach((snapshot) =>
|
|
499
|
+
debug(
|
|
500
|
+
`${collectionId ? `[${collectionId}] ` : ``}new snapshot synced from pg %o`,
|
|
501
|
+
snapshot
|
|
502
|
+
)
|
|
503
|
+
)
|
|
504
|
+
newSnapshots.length = 0
|
|
505
|
+
return seen
|
|
506
|
+
})
|
|
416
507
|
}
|
|
417
508
|
})
|
|
418
509
|
|
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
|
}
|