@tanstack/electric-db-collection 0.1.44 → 0.2.0
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 +44 -7
- package/dist/cjs/electric.cjs.map +1 -1
- package/dist/cjs/electric.d.cts +38 -6
- package/dist/cjs/pg-serializer.cjs +35 -0
- package/dist/cjs/pg-serializer.cjs.map +1 -0
- package/dist/cjs/pg-serializer.d.cts +12 -0
- package/dist/cjs/sql-compiler.cjs +151 -0
- package/dist/cjs/sql-compiler.cjs.map +1 -0
- package/dist/cjs/sql-compiler.d.cts +6 -0
- package/dist/esm/electric.d.ts +38 -6
- package/dist/esm/electric.js +44 -7
- package/dist/esm/electric.js.map +1 -1
- package/dist/esm/pg-serializer.d.ts +12 -0
- package/dist/esm/pg-serializer.js +35 -0
- package/dist/esm/pg-serializer.js.map +1 -0
- package/dist/esm/sql-compiler.d.ts +6 -0
- package/dist/esm/sql-compiler.js +151 -0
- package/dist/esm/sql-compiler.js.map +1 -0
- package/package.json +7 -4
- package/src/electric.ts +110 -18
- package/src/pg-serializer.ts +58 -0
- package/src/sql-compiler.ts +220 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-compiler.cjs","sources":["../../src/sql-compiler.ts"],"sourcesContent":["import { serialize } from \"./pg-serializer\"\nimport type { SubsetParams } from \"@electric-sql/client\"\nimport type { IR, LoadSubsetOptions } from \"@tanstack/db\"\n\nexport type CompiledSqlRecord = Omit<SubsetParams, `params`> & {\n params?: Array<unknown>\n}\n\nexport function compileSQL<T>(options: LoadSubsetOptions): SubsetParams {\n const { where, orderBy, limit } = options\n\n const params: Array<T> = []\n const compiledSQL: CompiledSqlRecord = { params }\n\n if (where) {\n // TODO: this only works when the where expression's PropRefs directly reference a column of the collection\n // doesn't work if it goes through aliases because then we need to know the entire query to be able to follow the reference until the base collection (cf. followRef function)\n compiledSQL.where = compileBasicExpression(where, params)\n }\n\n if (orderBy) {\n compiledSQL.orderBy = compileOrderBy(orderBy, params)\n }\n\n if (limit) {\n compiledSQL.limit = limit\n }\n\n // WORKAROUND for Electric bug: Empty subset requests don't load data\n // Add dummy \"true = true\" predicate when there's no where clause\n // This is always true so doesn't filter data, just tricks Electric into loading\n if (!where) {\n compiledSQL.where = `true = true`\n }\n\n // Serialize the values in the params array into PG formatted strings\n // and transform the array into a Record<string, string>\n const paramsRecord = params.reduce(\n (acc, param, index) => {\n const serialized = serialize(param)\n // Only include non-empty values in params\n // Empty strings from null/undefined should be omitted\n if (serialized !== ``) {\n acc[`${index + 1}`] = serialized\n }\n return acc\n },\n {} as Record<string, string>\n )\n\n return {\n ...compiledSQL,\n params: paramsRecord,\n }\n}\n\n/**\n * Quote PostgreSQL identifiers to handle mixed case column names correctly.\n * Electric/Postgres requires quotes for case-sensitive identifiers.\n * @param name - The identifier to quote\n * @returns The quoted identifier\n */\nfunction quoteIdentifier(name: string): string {\n return `\"${name}\"`\n}\n\n/**\n * Compiles the expression to a SQL string and mutates the params array with the values.\n * @param exp - The expression to compile\n * @param params - The params array\n * @returns The compiled SQL string\n */\nfunction compileBasicExpression(\n exp: IR.BasicExpression<unknown>,\n params: Array<unknown>\n): string {\n switch (exp.type) {\n case `val`:\n params.push(exp.value)\n return `$${params.length}`\n case `ref`:\n // TODO: doesn't yet support JSON(B) values which could be accessed with nested props\n if (exp.path.length !== 1) {\n throw new Error(\n `Compiler can't handle nested properties: ${exp.path.join(`.`)}`\n )\n }\n return quoteIdentifier(exp.path[0]!)\n case `func`:\n return compileFunction(exp, params)\n default:\n throw new Error(`Unknown expression type`)\n }\n}\n\nfunction compileOrderBy(orderBy: IR.OrderBy, params: Array<unknown>): string {\n const compiledOrderByClauses = orderBy.map((clause: IR.OrderByClause) =>\n compileOrderByClause(clause, params)\n )\n return compiledOrderByClauses.join(`,`)\n}\n\nfunction compileOrderByClause(\n clause: IR.OrderByClause,\n params: Array<unknown>\n): string {\n // FIXME: We should handle stringSort and locale.\n // Correctly supporting them is tricky as it depends on Postgres' collation\n const { expression, compareOptions } = clause\n let sql = compileBasicExpression(expression, params)\n\n if (compareOptions.direction === `desc`) {\n sql = `${sql} DESC`\n }\n\n if (compareOptions.nulls === `first`) {\n sql = `${sql} NULLS FIRST`\n }\n\n if (compareOptions.nulls === `last`) {\n sql = `${sql} NULLS LAST`\n }\n\n return sql\n}\n\nfunction compileFunction(\n exp: IR.Func<unknown>,\n params: Array<unknown> = []\n): string {\n const { name, args } = exp\n\n const opName = getOpName(name)\n\n const compiledArgs = args.map((arg: IR.BasicExpression) =>\n compileBasicExpression(arg, params)\n )\n\n // Special case for IS NULL / IS NOT NULL - these are postfix operators\n if (name === `isNull` || name === `isUndefined`) {\n if (compiledArgs.length !== 1) {\n throw new Error(`${name} expects 1 argument`)\n }\n return `${compiledArgs[0]} ${opName}`\n }\n\n // Special case for NOT - unary prefix operator\n if (name === `not`) {\n if (compiledArgs.length !== 1) {\n throw new Error(`NOT expects 1 argument`)\n }\n // Check if the argument is IS NULL to generate IS NOT NULL\n const arg = args[0]\n if (arg && arg.type === `func`) {\n const funcArg = arg\n if (funcArg.name === `isNull` || funcArg.name === `isUndefined`) {\n const innerArg = compileBasicExpression(funcArg.args[0]!, params)\n return `${innerArg} IS NOT NULL`\n }\n }\n return `${opName} (${compiledArgs[0]})`\n }\n\n if (isBinaryOp(name)) {\n // Special handling for AND/OR which can be variadic\n if ((name === `and` || name === `or`) && compiledArgs.length > 2) {\n // Chain multiple arguments: (a AND b AND c) or (a OR b OR c)\n return compiledArgs.map((arg) => `(${arg})`).join(` ${opName} `)\n }\n\n if (compiledArgs.length !== 2) {\n throw new Error(`Binary operator ${name} expects 2 arguments`)\n }\n const [lhs, rhs] = compiledArgs\n // Special case for = ANY operator which needs parentheses around the array parameter\n if (name === `in`) {\n return `${lhs} ${opName}(${rhs})`\n }\n return `${lhs} ${opName} ${rhs}`\n }\n\n return `${opName}(${compiledArgs.join(`,`)})`\n}\n\nfunction isBinaryOp(name: string): boolean {\n const binaryOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `and`, `or`, `in`]\n return binaryOps.includes(name)\n}\n\nfunction getOpName(name: string): string {\n const opNames = {\n eq: `=`,\n gt: `>`,\n gte: `>=`,\n lt: `<`,\n lte: `<=`,\n add: `+`,\n and: `AND`,\n or: `OR`,\n not: `NOT`,\n isUndefined: `IS NULL`,\n isNull: `IS NULL`,\n in: `= ANY`, // Use = ANY syntax for array parameters\n like: `LIKE`,\n ilike: `ILIKE`,\n upper: `UPPER`,\n lower: `LOWER`,\n length: `LENGTH`,\n concat: `CONCAT`,\n coalesce: `COALESCE`,\n }\n\n const opName = opNames[name as keyof typeof opNames]\n\n if (!opName) {\n throw new Error(`Unknown operator/function: ${name}`)\n }\n\n return opName\n}\n"],"names":["serialize"],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SubsetParams } from '@electric-sql/client';
|
|
2
|
+
import { LoadSubsetOptions } from '@tanstack/db';
|
|
3
|
+
export type CompiledSqlRecord = Omit<SubsetParams, `params`> & {
|
|
4
|
+
params?: Array<unknown>;
|
|
5
|
+
};
|
|
6
|
+
export declare function compileSQL<T>(options: LoadSubsetOptions): SubsetParams;
|
package/dist/esm/electric.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseCollectionConfig, CollectionConfig, DeleteMutationFnParams, InsertMutationFnParams, UpdateMutationFnParams, UtilsRecord } from '@tanstack/db';
|
|
1
|
+
import { BaseCollectionConfig, CollectionConfig, DeleteMutationFnParams, InsertMutationFnParams, SyncMode, UpdateMutationFnParams, UtilsRecord } from '@tanstack/db';
|
|
2
2
|
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
3
3
|
import { GetExtensions, Message, Row, ShapeStreamOptions } from '@electric-sql/client';
|
|
4
4
|
export { isChangeMessage, isControlMessage } from '@electric-sql/client';
|
|
@@ -14,27 +14,49 @@ export type MatchFunction<T extends Row<unknown>> = (message: Message<T>) => boo
|
|
|
14
14
|
/**
|
|
15
15
|
* Matching strategies for Electric synchronization
|
|
16
16
|
* Handlers can return:
|
|
17
|
-
* - Txid strategy: { txid: number | number[] } (recommended)
|
|
17
|
+
* - Txid strategy: { txid: number | number[], timeout?: number } (recommended)
|
|
18
18
|
* - Void (no return value) - mutation completes without waiting
|
|
19
|
+
*
|
|
20
|
+
* The optional timeout property specifies how long to wait for the txid(s) in milliseconds.
|
|
21
|
+
* If not specified, defaults to 5000ms.
|
|
19
22
|
*/
|
|
20
23
|
export type MatchingStrategy = {
|
|
21
24
|
txid: Txid | Array<Txid>;
|
|
25
|
+
timeout?: number;
|
|
22
26
|
} | void;
|
|
23
27
|
type InferSchemaOutput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<T> extends Row<unknown> ? StandardSchemaV1.InferOutput<T> : Record<string, unknown> : Record<string, unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* The mode of sync to use for the collection.
|
|
30
|
+
* @default `eager`
|
|
31
|
+
* @description
|
|
32
|
+
* - `eager`:
|
|
33
|
+
* - syncs all data immediately on preload
|
|
34
|
+
* - collection will be marked as ready once the sync is complete
|
|
35
|
+
* - there is no incremental sync
|
|
36
|
+
* - `on-demand`:
|
|
37
|
+
* - syncs data in incremental snapshots when the collection is queried
|
|
38
|
+
* - collection will be marked as ready immediately after the first snapshot is synced
|
|
39
|
+
* - `progressive`:
|
|
40
|
+
* - syncs all data for the collection in the background
|
|
41
|
+
* - uses incremental snapshots during the initial sync to provide a fast path to the data required for queries
|
|
42
|
+
* - collection will be marked as ready once the full sync is complete
|
|
43
|
+
*/
|
|
44
|
+
export type ElectricSyncMode = SyncMode | `progressive`;
|
|
24
45
|
/**
|
|
25
46
|
* Configuration interface for Electric collection options
|
|
26
47
|
* @template T - The type of items in the collection
|
|
27
48
|
* @template TSchema - The schema type for validation
|
|
28
49
|
*/
|
|
29
|
-
export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>, TSchema extends StandardSchemaV1 = never> extends Omit<BaseCollectionConfig<T, string | number, TSchema, UtilsRecord, any>, `onInsert` | `onUpdate` | `onDelete`> {
|
|
50
|
+
export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>, TSchema extends StandardSchemaV1 = never> extends Omit<BaseCollectionConfig<T, string | number, TSchema, UtilsRecord, any>, `onInsert` | `onUpdate` | `onDelete` | `syncMode`> {
|
|
30
51
|
/**
|
|
31
52
|
* Configuration options for the ElectricSQL ShapeStream
|
|
32
53
|
*/
|
|
33
54
|
shapeOptions: ShapeStreamOptions<GetExtensions<T>>;
|
|
55
|
+
syncMode?: ElectricSyncMode;
|
|
34
56
|
/**
|
|
35
57
|
* Optional asynchronous handler function called before an insert operation
|
|
36
58
|
* @param params Object containing transaction and collection information
|
|
37
|
-
* @returns Promise resolving to { txid } or void
|
|
59
|
+
* @returns Promise resolving to { txid, timeout? } or void
|
|
38
60
|
* @example
|
|
39
61
|
* // Basic Electric insert handler with txid (recommended)
|
|
40
62
|
* onInsert: async ({ transaction }) => {
|
|
@@ -46,6 +68,16 @@ export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>,
|
|
|
46
68
|
* }
|
|
47
69
|
*
|
|
48
70
|
* @example
|
|
71
|
+
* // Insert handler with custom timeout
|
|
72
|
+
* onInsert: async ({ transaction }) => {
|
|
73
|
+
* const newItem = transaction.mutations[0].modified
|
|
74
|
+
* const result = await api.todos.create({
|
|
75
|
+
* data: newItem
|
|
76
|
+
* })
|
|
77
|
+
* return { txid: result.txid, timeout: 10000 } // Wait up to 10 seconds
|
|
78
|
+
* }
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
49
81
|
* // Insert handler with multiple items - return array of txids
|
|
50
82
|
* onInsert: async ({ transaction }) => {
|
|
51
83
|
* const items = transaction.mutations.map(m => m.modified)
|
|
@@ -71,7 +103,7 @@ export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>,
|
|
|
71
103
|
/**
|
|
72
104
|
* Optional asynchronous handler function called before an update operation
|
|
73
105
|
* @param params Object containing transaction and collection information
|
|
74
|
-
* @returns Promise resolving to { txid } or void
|
|
106
|
+
* @returns Promise resolving to { txid, timeout? } or void
|
|
75
107
|
* @example
|
|
76
108
|
* // Basic Electric update handler with txid (recommended)
|
|
77
109
|
* onUpdate: async ({ transaction }) => {
|
|
@@ -99,7 +131,7 @@ export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>,
|
|
|
99
131
|
/**
|
|
100
132
|
* Optional asynchronous handler function called before a delete operation
|
|
101
133
|
* @param params Object containing transaction and collection information
|
|
102
|
-
* @returns Promise resolving to { txid } or void
|
|
134
|
+
* @returns Promise resolving to { txid, timeout? } or void
|
|
103
135
|
* @example
|
|
104
136
|
* // Basic Electric delete handler with txid (recommended)
|
|
105
137
|
* onDelete: async ({ transaction }) => {
|
package/dist/esm/electric.js
CHANGED
|
@@ -2,7 +2,9 @@ import { ShapeStream, isChangeMessage, isVisibleInSnapshot, isControlMessage } f
|
|
|
2
2
|
import { isChangeMessage as isChangeMessage2, isControlMessage as isControlMessage2 } from "@electric-sql/client";
|
|
3
3
|
import { Store } from "@tanstack/store";
|
|
4
4
|
import DebugModule from "debug";
|
|
5
|
+
import { DeduplicatedLoadSubset } from "@tanstack/db";
|
|
5
6
|
import { StreamAbortedError, ExpectedNumberInAwaitTxIdError, TimeoutWaitingForTxIdError, TimeoutWaitingForMatchError } from "./errors.js";
|
|
7
|
+
import { compileSQL } from "./sql-compiler.js";
|
|
6
8
|
const debug = DebugModule.debug(`ts/db:electric`);
|
|
7
9
|
function isUpToDateMessage(message) {
|
|
8
10
|
return isControlMessage(message) && message.headers.control === `up-to-date`;
|
|
@@ -26,6 +28,8 @@ function hasTxids(message) {
|
|
|
26
28
|
function electricCollectionOptions(config) {
|
|
27
29
|
const seenTxids = new Store(/* @__PURE__ */ new Set([]));
|
|
28
30
|
const seenSnapshots = new Store([]);
|
|
31
|
+
const internalSyncMode = config.syncMode ?? `eager`;
|
|
32
|
+
const finalSyncMode = internalSyncMode === `progressive` ? `on-demand` : internalSyncMode;
|
|
29
33
|
const pendingMatches = new Store(/* @__PURE__ */ new Map());
|
|
30
34
|
const currentBatchMessages = new Store([]);
|
|
31
35
|
const removePendingMatches = (matchIds) => {
|
|
@@ -55,6 +59,7 @@ function electricCollectionOptions(config) {
|
|
|
55
59
|
const sync = createElectricSync(config.shapeOptions, {
|
|
56
60
|
seenTxids,
|
|
57
61
|
seenSnapshots,
|
|
62
|
+
syncMode: internalSyncMode,
|
|
58
63
|
pendingMatches,
|
|
59
64
|
currentBatchMessages,
|
|
60
65
|
removePendingMatches,
|
|
@@ -181,10 +186,11 @@ function electricCollectionOptions(config) {
|
|
|
181
186
|
};
|
|
182
187
|
const processMatchingStrategy = async (result) => {
|
|
183
188
|
if (result && `txid` in result) {
|
|
189
|
+
const timeout = result.timeout;
|
|
184
190
|
if (Array.isArray(result.txid)) {
|
|
185
|
-
await Promise.all(result.txid.map(awaitTxId));
|
|
191
|
+
await Promise.all(result.txid.map((txid) => awaitTxId(txid, timeout)));
|
|
186
192
|
} else {
|
|
187
|
-
await awaitTxId(result.txid);
|
|
193
|
+
await awaitTxId(result.txid, timeout);
|
|
188
194
|
}
|
|
189
195
|
}
|
|
190
196
|
};
|
|
@@ -212,6 +218,7 @@ function electricCollectionOptions(config) {
|
|
|
212
218
|
} = config;
|
|
213
219
|
return {
|
|
214
220
|
...restConfig,
|
|
221
|
+
syncMode: finalSyncMode,
|
|
215
222
|
sync,
|
|
216
223
|
onInsert: wrappedOnInsert,
|
|
217
224
|
onUpdate: wrappedOnUpdate,
|
|
@@ -226,6 +233,7 @@ function createElectricSync(shapeOptions, options) {
|
|
|
226
233
|
const {
|
|
227
234
|
seenTxids,
|
|
228
235
|
seenSnapshots,
|
|
236
|
+
syncMode,
|
|
229
237
|
pendingMatches,
|
|
230
238
|
currentBatchMessages,
|
|
231
239
|
removePendingMatches,
|
|
@@ -270,6 +278,11 @@ function createElectricSync(shapeOptions, options) {
|
|
|
270
278
|
});
|
|
271
279
|
const stream = new ShapeStream({
|
|
272
280
|
...shapeOptions,
|
|
281
|
+
// In on-demand mode, we only want to sync changes, so we set the log to `changes_only`
|
|
282
|
+
log: syncMode === `on-demand` ? `changes_only` : void 0,
|
|
283
|
+
// In on-demand mode, we only need the changes from the point of time the collection was created
|
|
284
|
+
// so we default to `now` when there is no saved offset.
|
|
285
|
+
offset: shapeOptions.offset ?? (syncMode === `on-demand` ? `now` : void 0),
|
|
273
286
|
signal: abortController.signal,
|
|
274
287
|
onError: (errorParams) => {
|
|
275
288
|
markReady();
|
|
@@ -289,8 +302,19 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
289
302
|
let transactionStarted = false;
|
|
290
303
|
const newTxids = /* @__PURE__ */ new Set();
|
|
291
304
|
const newSnapshots = [];
|
|
305
|
+
let hasReceivedUpToDate = false;
|
|
306
|
+
const loadSubsetDedupe = syncMode === `eager` ? null : new DeduplicatedLoadSubset({
|
|
307
|
+
loadSubset: async (opts) => {
|
|
308
|
+
if (syncMode === `progressive` && hasReceivedUpToDate) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const snapshotParams = compileSQL(opts);
|
|
312
|
+
await stream.requestSnapshot(snapshotParams);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
292
315
|
unsubscribeStream = stream.subscribe((messages) => {
|
|
293
316
|
let hasUpToDate = false;
|
|
317
|
+
let hasSnapshotEnd = false;
|
|
294
318
|
for (const message of messages) {
|
|
295
319
|
if (isChangeMessage(message)) {
|
|
296
320
|
currentBatchMessages.setState((currentBuffer) => {
|
|
@@ -339,6 +363,7 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
339
363
|
});
|
|
340
364
|
} else if (isSnapshotEndMessage(message)) {
|
|
341
365
|
newSnapshots.push(parseSnapshotMessage(message));
|
|
366
|
+
hasSnapshotEnd = true;
|
|
342
367
|
} else if (isUpToDateMessage(message)) {
|
|
343
368
|
hasUpToDate = true;
|
|
344
369
|
} else if (isMustRefetchMessage(message)) {
|
|
@@ -350,16 +375,24 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
350
375
|
transactionStarted = true;
|
|
351
376
|
}
|
|
352
377
|
truncate();
|
|
378
|
+
loadSubsetDedupe?.reset();
|
|
353
379
|
hasUpToDate = false;
|
|
380
|
+
hasSnapshotEnd = false;
|
|
381
|
+
hasReceivedUpToDate = false;
|
|
354
382
|
}
|
|
355
383
|
}
|
|
356
|
-
if (hasUpToDate) {
|
|
384
|
+
if (hasUpToDate || hasSnapshotEnd) {
|
|
357
385
|
currentBatchMessages.setState(() => []);
|
|
358
386
|
if (transactionStarted) {
|
|
359
387
|
commit();
|
|
360
388
|
transactionStarted = false;
|
|
361
389
|
}
|
|
362
|
-
|
|
390
|
+
if (hasUpToDate || hasSnapshotEnd && syncMode === `on-demand`) {
|
|
391
|
+
markReady();
|
|
392
|
+
}
|
|
393
|
+
if (hasUpToDate) {
|
|
394
|
+
hasReceivedUpToDate = true;
|
|
395
|
+
}
|
|
363
396
|
seenTxids.setState((currentTxids) => {
|
|
364
397
|
const clonedSeen = new Set(currentTxids);
|
|
365
398
|
if (newTxids.size > 0) {
|
|
@@ -386,9 +419,13 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
386
419
|
resolveMatchedPendingMatches();
|
|
387
420
|
}
|
|
388
421
|
});
|
|
389
|
-
return
|
|
390
|
-
|
|
391
|
-
|
|
422
|
+
return {
|
|
423
|
+
loadSubset: loadSubsetDedupe?.loadSubset,
|
|
424
|
+
cleanup: () => {
|
|
425
|
+
unsubscribeStream();
|
|
426
|
+
abortController.abort();
|
|
427
|
+
loadSubsetDedupe?.reset();
|
|
428
|
+
}
|
|
392
429
|
};
|
|
393
430
|
},
|
|
394
431
|
// Expose the getSyncMetadata function
|
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 isVisibleInSnapshot,\n} from \"@electric-sql/client\"\nimport { Store } from \"@tanstack/store\"\nimport DebugModule from \"debug\"\nimport {\n ExpectedNumberInAwaitTxIdError,\n StreamAbortedError,\n TimeoutWaitingForMatchError,\n TimeoutWaitingForTxIdError,\n} from \"./errors\"\nimport type {\n BaseCollectionConfig,\n CollectionConfig,\n DeleteMutationFnParams,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport type {\n ControlMessage,\n GetExtensions,\n Message,\n PostgresSnapshot,\n Row,\n ShapeStreamOptions,\n} from \"@electric-sql/client\"\n\n// Re-export for user convenience in custom match functions\nexport { isChangeMessage, isControlMessage } 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 * Custom match function type - receives stream messages and returns boolean\n * indicating if the mutation has been synchronized\n */\nexport type MatchFunction<T extends Row<unknown>> = (\n message: Message<T>\n) => boolean\n\n/**\n * Matching strategies for Electric synchronization\n * Handlers can return:\n * - Txid strategy: { txid: number | number[] } (recommended)\n * - Void (no return value) - mutation completes without waiting\n */\nexport type MatchingStrategy = { txid: Txid | Array<Txid> } | void\n\n/**\n * Type representing a snapshot end message\n */\ntype SnapshotEndMessage = ControlMessage & {\n headers: { control: `snapshot-end` }\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 Omit<\n BaseCollectionConfig<T, string | number, TSchema, UtilsRecord, any>,\n `onInsert` | `onUpdate` | `onDelete`\n > {\n /**\n * Configuration options for the ElectricSQL ShapeStream\n */\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>\n\n /**\n * Optional asynchronous handler function called before an insert operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to { txid } or void\n * @example\n * // Basic Electric insert handler with txid (recommended)\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * const result = await api.todos.create({\n * data: newItem\n * })\n * return { txid: result.txid }\n * }\n *\n * @example\n * // Insert handler with multiple items - return array of txids\n * onInsert: async ({ transaction }) => {\n * const items = transaction.mutations.map(m => m.modified)\n * const results = await Promise.all(\n * items.map(item => api.todos.create({ data: item }))\n * )\n * return { txid: results.map(r => r.txid) }\n * }\n *\n * @example\n * // Use awaitMatch utility for custom matching\n * onInsert: async ({ transaction, collection }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.todos.create({ data: newItem })\n * await collection.utils.awaitMatch(\n * (message) => isChangeMessage(message) &&\n * message.headers.operation === 'insert' &&\n * message.value.name === newItem.name\n * )\n * }\n */\n onInsert?: (params: InsertMutationFnParams<T>) => Promise<MatchingStrategy>\n\n /**\n * Optional asynchronous handler function called before an update operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to { txid } or void\n * @example\n * // Basic Electric update handler with txid (recommended)\n * onUpdate: async ({ transaction }) => {\n * const { original, changes } = transaction.mutations[0]\n * const result = await api.todos.update({\n * where: { id: original.id },\n * data: changes\n * })\n * return { txid: result.txid }\n * }\n *\n * @example\n * // Use awaitMatch utility for custom matching\n * onUpdate: async ({ transaction, collection }) => {\n * const { original, changes } = transaction.mutations[0]\n * await api.todos.update({ where: { id: original.id }, data: changes })\n * await collection.utils.awaitMatch(\n * (message) => isChangeMessage(message) &&\n * message.headers.operation === 'update' &&\n * message.value.id === original.id\n * )\n * }\n */\n onUpdate?: (params: UpdateMutationFnParams<T>) => Promise<MatchingStrategy>\n\n /**\n * Optional asynchronous handler function called before a delete operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to { txid } or void\n * @example\n * // Basic Electric delete handler with txid (recommended)\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * const result = await api.todos.delete({\n * id: mutation.original.id\n * })\n * return { txid: result.txid }\n * }\n *\n * @example\n * // Use awaitMatch utility for custom matching\n * onDelete: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.todos.delete({ id: mutation.original.id })\n * await collection.utils.awaitMatch(\n * (message) => isChangeMessage(message) &&\n * message.headers.operation === 'delete' &&\n * message.value.id === mutation.original.id\n * )\n * }\n */\n onDelete?: (params: DeleteMutationFnParams<T>) => Promise<MatchingStrategy>\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 * Type for the awaitMatch utility function\n */\nexport type AwaitMatchFn<T extends Row<unknown>> = (\n matchFn: MatchFunction<T>,\n timeout?: number\n) => Promise<boolean>\n\n/**\n * Electric collection utilities type\n */\nexport interface ElectricCollectionUtils<T extends Row<unknown> = Row<unknown>>\n extends UtilsRecord {\n awaitTxId: AwaitTxIdFn\n awaitMatch: AwaitMatchFn<T>\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 pendingMatches = new Store<\n Map<\n string,\n {\n matchFn: (message: Message<any>) => boolean\n resolve: (value: boolean) => void\n reject: (error: Error) => void\n timeoutId: ReturnType<typeof setTimeout>\n matched: boolean\n }\n >\n >(new Map())\n\n // Buffer messages since last up-to-date to handle race conditions\n const currentBatchMessages = new Store<Array<Message<any>>>([])\n\n /**\n * Helper function to remove multiple matches from the pendingMatches store\n */\n const removePendingMatches = (matchIds: Array<string>) => {\n if (matchIds.length > 0) {\n pendingMatches.setState((current) => {\n const newMatches = new Map(current)\n matchIds.forEach((id) => newMatches.delete(id))\n return newMatches\n })\n }\n }\n\n /**\n * Helper function to resolve and cleanup matched pending matches\n */\n const resolveMatchedPendingMatches = () => {\n const matchesToResolve: Array<string> = []\n pendingMatches.state.forEach((match, matchId) => {\n if (match.matched) {\n clearTimeout(match.timeoutId)\n match.resolve(true)\n matchesToResolve.push(matchId)\n debug(\n `${config.id ? `[${config.id}] ` : ``}awaitMatch resolved on up-to-date for match %s`,\n matchId\n )\n }\n })\n removePendingMatches(matchesToResolve)\n }\n const sync = createElectricSync<any>(config.shapeOptions, {\n seenTxids,\n seenSnapshots,\n pendingMatches,\n currentBatchMessages,\n removePendingMatches,\n resolveMatchedPendingMatches,\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 5000ms)\n * @returns Promise that resolves when the txId is synced\n */\n const awaitTxId: AwaitTxIdFn = async (\n txId: Txid,\n timeout: number = 5000\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 /**\n * Wait for a custom match function to find a matching message\n * @param matchFn Function that returns true when a message matches\n * @param timeout Optional timeout in milliseconds (defaults to 5000ms)\n * @returns Promise that resolves when a matching message is found\n */\n const awaitMatch: AwaitMatchFn<any> = async (\n matchFn: MatchFunction<any>,\n timeout: number = 3000\n ): Promise<boolean> => {\n debug(\n `${config.id ? `[${config.id}] ` : ``}awaitMatch called with custom function`\n )\n\n return new Promise((resolve, reject) => {\n const matchId = Math.random().toString(36)\n\n const cleanupMatch = () => {\n pendingMatches.setState((current) => {\n const newMatches = new Map(current)\n newMatches.delete(matchId)\n return newMatches\n })\n }\n\n const onTimeout = () => {\n cleanupMatch()\n reject(new TimeoutWaitingForMatchError(config.id))\n }\n\n const timeoutId = setTimeout(onTimeout, timeout)\n\n // We need access to the stream messages to check against the match function\n // This will be handled by the sync configuration\n const checkMatch = (message: Message<any>) => {\n if (matchFn(message)) {\n debug(\n `${config.id ? `[${config.id}] ` : ``}awaitMatch found matching message, waiting for up-to-date`\n )\n // Mark as matched but don't resolve yet - wait for up-to-date\n pendingMatches.setState((current) => {\n const newMatches = new Map(current)\n const existing = newMatches.get(matchId)\n if (existing) {\n newMatches.set(matchId, { ...existing, matched: true })\n }\n return newMatches\n })\n return true\n }\n return false\n }\n\n // Check against current batch messages first to handle race conditions\n for (const message of currentBatchMessages.state) {\n if (matchFn(message)) {\n debug(\n `${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in current batch, waiting for up-to-date`\n )\n // Register match as already matched\n pendingMatches.setState((current) => {\n const newMatches = new Map(current)\n newMatches.set(matchId, {\n matchFn: checkMatch,\n resolve,\n reject,\n timeoutId,\n matched: true, // Already matched\n })\n return newMatches\n })\n return\n }\n }\n\n // Store the match function for the sync process to use\n // We'll add this to a pending matches store\n pendingMatches.setState((current) => {\n const newMatches = new Map(current)\n newMatches.set(matchId, {\n matchFn: checkMatch,\n resolve,\n reject,\n timeoutId,\n matched: false,\n })\n return newMatches\n })\n })\n }\n\n /**\n * Process matching strategy and wait for synchronization\n */\n const processMatchingStrategy = async (\n result: MatchingStrategy\n ): Promise<void> => {\n // Only wait if result contains txid\n if (result && `txid` in result) {\n // Handle both single txid and array of txids\n if (Array.isArray(result.txid)) {\n await Promise.all(result.txid.map(awaitTxId))\n } else {\n await awaitTxId(result.txid)\n }\n }\n // If result is void/undefined, don't wait - mutation completes immediately\n }\n\n // Create wrapper handlers for direct persistence operations that handle different matching strategies\n const wrappedOnInsert = config.onInsert\n ? async (params: InsertMutationFnParams<any>) => {\n const handlerResult = await config.onInsert!(params)\n await processMatchingStrategy(handlerResult)\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = config.onUpdate\n ? async (params: UpdateMutationFnParams<any>) => {\n const handlerResult = await config.onUpdate!(params)\n await processMatchingStrategy(handlerResult)\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 await processMatchingStrategy(handlerResult)\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 awaitMatch,\n } as ElectricCollectionUtils<any>,\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 pendingMatches: Store<\n Map<\n string,\n {\n matchFn: (message: Message<T>) => boolean\n resolve: (value: boolean) => void\n reject: (error: Error) => void\n timeoutId: ReturnType<typeof setTimeout>\n matched: boolean\n }\n >\n >\n currentBatchMessages: Store<Array<Message<T>>>\n removePendingMatches: (matchIds: Array<string>) => void\n resolveMatchedPendingMatches: () => void\n collectionId?: string\n }\n): SyncConfig<T> {\n const {\n seenTxids,\n seenSnapshots,\n pendingMatches,\n currentBatchMessages,\n removePendingMatches,\n resolveMatchedPendingMatches,\n collectionId,\n } = options\n const MAX_BATCH_MESSAGES = 1000 // Safety limit for message buffer\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 // Cleanup pending matches on abort\n abortController.signal.addEventListener(`abort`, () => {\n pendingMatches.setState((current) => {\n current.forEach((match) => {\n clearTimeout(match.timeoutId)\n match.reject(new StreamAbortedError())\n })\n return new Map() // Clear all pending matches\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 // Add message to current batch buffer (for race condition handling)\n if (isChangeMessage(message)) {\n currentBatchMessages.setState((currentBuffer) => {\n const newBuffer = [...currentBuffer, message]\n // Limit buffer size for safety\n if (newBuffer.length > MAX_BATCH_MESSAGES) {\n newBuffer.splice(0, newBuffer.length - MAX_BATCH_MESSAGES)\n }\n return newBuffer\n })\n }\n\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 // Check pending matches against this message\n // Note: matchFn will mark matches internally, we don't resolve here\n const matchesToRemove: Array<string> = []\n pendingMatches.state.forEach((match, matchId) => {\n if (!match.matched) {\n try {\n match.matchFn(message)\n } catch (err) {\n // If matchFn throws, clean up and reject the promise\n clearTimeout(match.timeoutId)\n match.reject(\n err instanceof Error ? err : new Error(String(err))\n )\n matchesToRemove.push(matchId)\n debug(`matchFn error: %o`, err)\n }\n }\n })\n\n // Remove matches that errored\n removePendingMatches(matchesToRemove)\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 // Clear the current batch buffer since we're now up-to-date\n currentBatchMessages.setState(() => [])\n\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 // Resolve all matched pending matches on up-to-date\n resolveMatchedPendingMatches()\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 { DeduplicatedLoadSubset } from \"@tanstack/db\"\nimport {\n ExpectedNumberInAwaitTxIdError,\n StreamAbortedError,\n TimeoutWaitingForMatchError,\n TimeoutWaitingForTxIdError,\n} from \"./errors\"\nimport { compileSQL } from \"./sql-compiler\"\nimport type {\n BaseCollectionConfig,\n CollectionConfig,\n DeleteMutationFnParams,\n InsertMutationFnParams,\n LoadSubsetOptions,\n SyncConfig,\n SyncMode,\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\n// Re-export for user convenience in custom match functions\nexport { isChangeMessage, isControlMessage } 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 * Custom match function type - receives stream messages and returns boolean\n * indicating if the mutation has been synchronized\n */\nexport type MatchFunction<T extends Row<unknown>> = (\n message: Message<T>\n) => boolean\n\n/**\n * Matching strategies for Electric synchronization\n * Handlers can return:\n * - Txid strategy: { txid: number | number[], timeout?: number } (recommended)\n * - Void (no return value) - mutation completes without waiting\n *\n * The optional timeout property specifies how long to wait for the txid(s) in milliseconds.\n * If not specified, defaults to 5000ms.\n */\nexport type MatchingStrategy = {\n txid: Txid | Array<Txid>\n timeout?: number\n} | void\n\n/**\n * Type representing a snapshot end message\n */\ntype SnapshotEndMessage = ControlMessage & {\n headers: { control: `snapshot-end` }\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 * The mode of sync to use for the collection.\n * @default `eager`\n * @description\n * - `eager`:\n * - syncs all data immediately on preload\n * - collection will be marked as ready once the sync is complete\n * - there is no incremental sync\n * - `on-demand`:\n * - syncs data in incremental snapshots when the collection is queried\n * - collection will be marked as ready immediately after the first snapshot is synced\n * - `progressive`:\n * - syncs all data for the collection in the background\n * - uses incremental snapshots during the initial sync to provide a fast path to the data required for queries\n * - collection will be marked as ready once the full sync is complete\n */\nexport type ElectricSyncMode = SyncMode | `progressive`\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 Omit<\n BaseCollectionConfig<T, string | number, TSchema, UtilsRecord, any>,\n `onInsert` | `onUpdate` | `onDelete` | `syncMode`\n > {\n /**\n * Configuration options for the ElectricSQL ShapeStream\n */\n shapeOptions: ShapeStreamOptions<GetExtensions<T>>\n syncMode?: ElectricSyncMode\n\n /**\n * Optional asynchronous handler function called before an insert operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to { txid, timeout? } or void\n * @example\n * // Basic Electric insert handler with txid (recommended)\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * const result = await api.todos.create({\n * data: newItem\n * })\n * return { txid: result.txid }\n * }\n *\n * @example\n * // Insert handler with custom timeout\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * const result = await api.todos.create({\n * data: newItem\n * })\n * return { txid: result.txid, timeout: 10000 } // Wait up to 10 seconds\n * }\n *\n * @example\n * // Insert handler with multiple items - return array of txids\n * onInsert: async ({ transaction }) => {\n * const items = transaction.mutations.map(m => m.modified)\n * const results = await Promise.all(\n * items.map(item => api.todos.create({ data: item }))\n * )\n * return { txid: results.map(r => r.txid) }\n * }\n *\n * @example\n * // Use awaitMatch utility for custom matching\n * onInsert: async ({ transaction, collection }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.todos.create({ data: newItem })\n * await collection.utils.awaitMatch(\n * (message) => isChangeMessage(message) &&\n * message.headers.operation === 'insert' &&\n * message.value.name === newItem.name\n * )\n * }\n */\n onInsert?: (params: InsertMutationFnParams<T>) => Promise<MatchingStrategy>\n\n /**\n * Optional asynchronous handler function called before an update operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to { txid, timeout? } or void\n * @example\n * // Basic Electric update handler with txid (recommended)\n * onUpdate: async ({ transaction }) => {\n * const { original, changes } = transaction.mutations[0]\n * const result = await api.todos.update({\n * where: { id: original.id },\n * data: changes\n * })\n * return { txid: result.txid }\n * }\n *\n * @example\n * // Use awaitMatch utility for custom matching\n * onUpdate: async ({ transaction, collection }) => {\n * const { original, changes } = transaction.mutations[0]\n * await api.todos.update({ where: { id: original.id }, data: changes })\n * await collection.utils.awaitMatch(\n * (message) => isChangeMessage(message) &&\n * message.headers.operation === 'update' &&\n * message.value.id === original.id\n * )\n * }\n */\n onUpdate?: (params: UpdateMutationFnParams<T>) => Promise<MatchingStrategy>\n\n /**\n * Optional asynchronous handler function called before a delete operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to { txid, timeout? } or void\n * @example\n * // Basic Electric delete handler with txid (recommended)\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * const result = await api.todos.delete({\n * id: mutation.original.id\n * })\n * return { txid: result.txid }\n * }\n *\n * @example\n * // Use awaitMatch utility for custom matching\n * onDelete: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.todos.delete({ id: mutation.original.id })\n * await collection.utils.awaitMatch(\n * (message) => isChangeMessage(message) &&\n * message.headers.operation === 'delete' &&\n * message.value.id === mutation.original.id\n * )\n * }\n */\n onDelete?: (params: DeleteMutationFnParams<T>) => Promise<MatchingStrategy>\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 * Type for the awaitMatch utility function\n */\nexport type AwaitMatchFn<T extends Row<unknown>> = (\n matchFn: MatchFunction<T>,\n timeout?: number\n) => Promise<boolean>\n\n/**\n * Electric collection utilities type\n */\nexport interface ElectricCollectionUtils<T extends Row<unknown> = Row<unknown>>\n extends UtilsRecord {\n awaitTxId: AwaitTxIdFn\n awaitMatch: AwaitMatchFn<T>\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 internalSyncMode = config.syncMode ?? `eager`\n const finalSyncMode =\n internalSyncMode === `progressive` ? `on-demand` : internalSyncMode\n const pendingMatches = new Store<\n Map<\n string,\n {\n matchFn: (message: Message<any>) => boolean\n resolve: (value: boolean) => void\n reject: (error: Error) => void\n timeoutId: ReturnType<typeof setTimeout>\n matched: boolean\n }\n >\n >(new Map())\n\n // Buffer messages since last up-to-date to handle race conditions\n const currentBatchMessages = new Store<Array<Message<any>>>([])\n\n /**\n * Helper function to remove multiple matches from the pendingMatches store\n */\n const removePendingMatches = (matchIds: Array<string>) => {\n if (matchIds.length > 0) {\n pendingMatches.setState((current) => {\n const newMatches = new Map(current)\n matchIds.forEach((id) => newMatches.delete(id))\n return newMatches\n })\n }\n }\n\n /**\n * Helper function to resolve and cleanup matched pending matches\n */\n const resolveMatchedPendingMatches = () => {\n const matchesToResolve: Array<string> = []\n pendingMatches.state.forEach((match, matchId) => {\n if (match.matched) {\n clearTimeout(match.timeoutId)\n match.resolve(true)\n matchesToResolve.push(matchId)\n debug(\n `${config.id ? `[${config.id}] ` : ``}awaitMatch resolved on up-to-date for match %s`,\n matchId\n )\n }\n })\n removePendingMatches(matchesToResolve)\n }\n const sync = createElectricSync<any>(config.shapeOptions, {\n seenTxids,\n seenSnapshots,\n syncMode: internalSyncMode,\n pendingMatches,\n currentBatchMessages,\n removePendingMatches,\n resolveMatchedPendingMatches,\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 5000ms)\n * @returns Promise that resolves when the txId is synced\n */\n const awaitTxId: AwaitTxIdFn = async (\n txId: Txid,\n timeout: number = 5000\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 /**\n * Wait for a custom match function to find a matching message\n * @param matchFn Function that returns true when a message matches\n * @param timeout Optional timeout in milliseconds (defaults to 5000ms)\n * @returns Promise that resolves when a matching message is found\n */\n const awaitMatch: AwaitMatchFn<any> = async (\n matchFn: MatchFunction<any>,\n timeout: number = 3000\n ): Promise<boolean> => {\n debug(\n `${config.id ? `[${config.id}] ` : ``}awaitMatch called with custom function`\n )\n\n return new Promise((resolve, reject) => {\n const matchId = Math.random().toString(36)\n\n const cleanupMatch = () => {\n pendingMatches.setState((current) => {\n const newMatches = new Map(current)\n newMatches.delete(matchId)\n return newMatches\n })\n }\n\n const onTimeout = () => {\n cleanupMatch()\n reject(new TimeoutWaitingForMatchError(config.id))\n }\n\n const timeoutId = setTimeout(onTimeout, timeout)\n\n // We need access to the stream messages to check against the match function\n // This will be handled by the sync configuration\n const checkMatch = (message: Message<any>) => {\n if (matchFn(message)) {\n debug(\n `${config.id ? `[${config.id}] ` : ``}awaitMatch found matching message, waiting for up-to-date`\n )\n // Mark as matched but don't resolve yet - wait for up-to-date\n pendingMatches.setState((current) => {\n const newMatches = new Map(current)\n const existing = newMatches.get(matchId)\n if (existing) {\n newMatches.set(matchId, { ...existing, matched: true })\n }\n return newMatches\n })\n return true\n }\n return false\n }\n\n // Check against current batch messages first to handle race conditions\n for (const message of currentBatchMessages.state) {\n if (matchFn(message)) {\n debug(\n `${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in current batch, waiting for up-to-date`\n )\n // Register match as already matched\n pendingMatches.setState((current) => {\n const newMatches = new Map(current)\n newMatches.set(matchId, {\n matchFn: checkMatch,\n resolve,\n reject,\n timeoutId,\n matched: true, // Already matched\n })\n return newMatches\n })\n return\n }\n }\n\n // Store the match function for the sync process to use\n // We'll add this to a pending matches store\n pendingMatches.setState((current) => {\n const newMatches = new Map(current)\n newMatches.set(matchId, {\n matchFn: checkMatch,\n resolve,\n reject,\n timeoutId,\n matched: false,\n })\n return newMatches\n })\n })\n }\n\n /**\n * Process matching strategy and wait for synchronization\n */\n const processMatchingStrategy = async (\n result: MatchingStrategy\n ): Promise<void> => {\n // Only wait if result contains txid\n if (result && `txid` in result) {\n const timeout = result.timeout\n // Handle both single txid and array of txids\n if (Array.isArray(result.txid)) {\n await Promise.all(result.txid.map((txid) => awaitTxId(txid, timeout)))\n } else {\n await awaitTxId(result.txid, timeout)\n }\n }\n // If result is void/undefined, don't wait - mutation completes immediately\n }\n\n // Create wrapper handlers for direct persistence operations that handle different matching strategies\n const wrappedOnInsert = config.onInsert\n ? async (params: InsertMutationFnParams<any>) => {\n const handlerResult = await config.onInsert!(params)\n await processMatchingStrategy(handlerResult)\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = config.onUpdate\n ? async (params: UpdateMutationFnParams<any>) => {\n const handlerResult = await config.onUpdate!(params)\n await processMatchingStrategy(handlerResult)\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 await processMatchingStrategy(handlerResult)\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 syncMode: finalSyncMode,\n sync,\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n awaitTxId,\n awaitMatch,\n } as ElectricCollectionUtils<any>,\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 syncMode: ElectricSyncMode\n seenTxids: Store<Set<Txid>>\n seenSnapshots: Store<Array<PostgresSnapshot>>\n pendingMatches: Store<\n Map<\n string,\n {\n matchFn: (message: Message<T>) => boolean\n resolve: (value: boolean) => void\n reject: (error: Error) => void\n timeoutId: ReturnType<typeof setTimeout>\n matched: boolean\n }\n >\n >\n currentBatchMessages: Store<Array<Message<T>>>\n removePendingMatches: (matchIds: Array<string>) => void\n resolveMatchedPendingMatches: () => void\n collectionId?: string\n }\n): SyncConfig<T> {\n const {\n seenTxids,\n seenSnapshots,\n syncMode,\n pendingMatches,\n currentBatchMessages,\n removePendingMatches,\n resolveMatchedPendingMatches,\n collectionId,\n } = options\n const MAX_BATCH_MESSAGES = 1000 // Safety limit for message buffer\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 // Cleanup pending matches on abort\n abortController.signal.addEventListener(`abort`, () => {\n pendingMatches.setState((current) => {\n current.forEach((match) => {\n clearTimeout(match.timeoutId)\n match.reject(new StreamAbortedError())\n })\n return new Map() // Clear all pending matches\n })\n })\n\n const stream = new ShapeStream({\n ...shapeOptions,\n // In on-demand mode, we only want to sync changes, so we set the log to `changes_only`\n log: syncMode === `on-demand` ? `changes_only` : undefined,\n // In on-demand mode, we only need the changes from the point of time the collection was created\n // so we default to `now` when there is no saved offset.\n offset:\n shapeOptions.offset ?? (syncMode === `on-demand` ? `now` : undefined),\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 let hasReceivedUpToDate = false // Track if we've completed initial sync in progressive mode\n\n // Create deduplicated loadSubset wrapper for non-eager modes\n // This prevents redundant snapshot requests when multiple concurrent\n // live queries request overlapping or subset predicates\n const loadSubsetDedupe =\n syncMode === `eager`\n ? null\n : new DeduplicatedLoadSubset({\n loadSubset: async (opts: LoadSubsetOptions) => {\n // In progressive mode, stop requesting snapshots once full sync is complete\n if (syncMode === `progressive` && hasReceivedUpToDate) {\n return\n }\n const snapshotParams = compileSQL<T>(opts)\n await stream.requestSnapshot(snapshotParams)\n },\n })\n\n unsubscribeStream = stream.subscribe((messages: Array<Message<T>>) => {\n let hasUpToDate = false\n let hasSnapshotEnd = false\n\n for (const message of messages) {\n // Add message to current batch buffer (for race condition handling)\n if (isChangeMessage(message)) {\n currentBatchMessages.setState((currentBuffer) => {\n const newBuffer = [...currentBuffer, message]\n // Limit buffer size for safety\n if (newBuffer.length > MAX_BATCH_MESSAGES) {\n newBuffer.splice(0, newBuffer.length - MAX_BATCH_MESSAGES)\n }\n return newBuffer\n })\n }\n\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 // Check pending matches against this message\n // Note: matchFn will mark matches internally, we don't resolve here\n const matchesToRemove: Array<string> = []\n pendingMatches.state.forEach((match, matchId) => {\n if (!match.matched) {\n try {\n match.matchFn(message)\n } catch (err) {\n // If matchFn throws, clean up and reject the promise\n clearTimeout(match.timeoutId)\n match.reject(\n err instanceof Error ? err : new Error(String(err))\n )\n matchesToRemove.push(matchId)\n debug(`matchFn error: %o`, err)\n }\n }\n })\n\n // Remove matches that errored\n removePendingMatches(matchesToRemove)\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 hasSnapshotEnd = true\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 the loadSubset deduplication state since we're starting fresh\n // This ensures that previously loaded predicates don't prevent refetching after truncate\n loadSubsetDedupe?.reset()\n\n // Reset flags so we continue accumulating changes until next up-to-date\n hasUpToDate = false\n hasSnapshotEnd = false\n hasReceivedUpToDate = false // Reset for progressive mode - we're starting a new sync\n }\n }\n\n if (hasUpToDate || hasSnapshotEnd) {\n // Clear the current batch buffer since we're now up-to-date\n currentBatchMessages.setState(() => [])\n\n // Commit transaction if one was started\n if (transactionStarted) {\n commit()\n transactionStarted = false\n }\n\n if (hasUpToDate || (hasSnapshotEnd && syncMode === `on-demand`)) {\n // Mark the collection as ready now that sync is up to date\n markReady()\n }\n\n // Track that we've received the first up-to-date for progressive mode\n if (hasUpToDate) {\n hasReceivedUpToDate = true\n }\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 // Resolve all matched pending matches on up-to-date\n resolveMatchedPendingMatches()\n }\n })\n\n // Return the deduplicated loadSubset if available (on-demand or progressive mode)\n // The loadSubset method is auto-bound, so it can be safely returned directly\n return {\n loadSubset: loadSubsetDedupe?.loadSubset,\n cleanup: () => {\n // Unsubscribe from the stream\n unsubscribeStream()\n // Abort the abort controller to stop the stream\n abortController.abort()\n // Reset deduplication tracking so collection can load fresh data if restarted\n loadSubsetDedupe?.reset()\n },\n }\n },\n // Expose the getSyncMetadata function\n getSyncMetadata,\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize values for Electric SQL subset parameters.
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: Electric expects RAW values, NOT SQL-formatted literals.
|
|
5
|
+
* Electric handles all type casting and escaping on the server side.
|
|
6
|
+
* The params Record<string, string> contains the actual values as strings,
|
|
7
|
+
* and Electric will parse/cast them based on the column type in the WHERE clause.
|
|
8
|
+
*
|
|
9
|
+
* @param value - The value to serialize
|
|
10
|
+
* @returns The raw value as a string (no SQL formatting/quoting)
|
|
11
|
+
*/
|
|
12
|
+
export declare function serialize(value: unknown): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
function serialize(value) {
|
|
2
|
+
if (value === null || value === void 0) {
|
|
3
|
+
return ``;
|
|
4
|
+
}
|
|
5
|
+
if (typeof value === `string`) {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
if (typeof value === `number`) {
|
|
9
|
+
return value.toString();
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === `boolean`) {
|
|
12
|
+
return value ? `true` : `false`;
|
|
13
|
+
}
|
|
14
|
+
if (value instanceof Date) {
|
|
15
|
+
return value.toISOString();
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
const elements = value.map((item) => {
|
|
19
|
+
if (item === null || item === void 0) {
|
|
20
|
+
return `NULL`;
|
|
21
|
+
}
|
|
22
|
+
if (typeof item === `string`) {
|
|
23
|
+
const escaped = item.replace(/\\/g, `\\\\`).replace(/"/g, `\\"`);
|
|
24
|
+
return `"${escaped}"`;
|
|
25
|
+
}
|
|
26
|
+
return serialize(item);
|
|
27
|
+
});
|
|
28
|
+
return `{${elements.join(`,`)}}`;
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Cannot serialize value: ${JSON.stringify(value)}`);
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
serialize
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=pg-serializer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg-serializer.js","sources":["../../src/pg-serializer.ts"],"sourcesContent":["/**\n * Serialize values for Electric SQL subset parameters.\n *\n * IMPORTANT: Electric expects RAW values, NOT SQL-formatted literals.\n * Electric handles all type casting and escaping on the server side.\n * The params Record<string, string> contains the actual values as strings,\n * and Electric will parse/cast them based on the column type in the WHERE clause.\n *\n * @param value - The value to serialize\n * @returns The raw value as a string (no SQL formatting/quoting)\n */\nexport function serialize(value: unknown): string {\n // Handle null/undefined - return empty string\n // Electric interprets empty string as NULL in typed column context\n if (value === null || value === undefined) {\n return ``\n }\n\n // Handle strings - return as-is (NO quotes, Electric handles escaping)\n if (typeof value === `string`) {\n return value\n }\n\n // Handle numbers - convert to string\n if (typeof value === `number`) {\n return value.toString()\n }\n\n // Handle booleans - return as lowercase string\n if (typeof value === `boolean`) {\n return value ? `true` : `false`\n }\n\n // Handle dates - return ISO format (NO quotes)\n if (value instanceof Date) {\n return value.toISOString()\n }\n\n // Handle arrays - for = ANY() operator, serialize as Postgres array literal\n // Format: {val1,val2,val3} with proper escaping\n if (Array.isArray(value)) {\n // Postgres array literal format uses curly braces\n const elements = value.map((item) => {\n if (item === null || item === undefined) {\n return `NULL`\n }\n if (typeof item === `string`) {\n // Escape quotes and backslashes for Postgres array literals\n const escaped = item.replace(/\\\\/g, `\\\\\\\\`).replace(/\"/g, `\\\\\"`)\n return `\"${escaped}\"`\n }\n return serialize(item)\n })\n return `{${elements.join(`,`)}}`\n }\n\n throw new Error(`Cannot serialize value: ${JSON.stringify(value)}`)\n}\n"],"names":[],"mappings":"AAWO,SAAS,UAAU,OAAwB;AAGhD,MAAI,UAAU,QAAQ,UAAU,QAAW;AACzC,WAAO;AAAA,EACT;AAGA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO;AAAA,EACT;AAGA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,MAAM,SAAA;AAAA,EACf;AAGA,MAAI,OAAO,UAAU,WAAW;AAC9B,WAAO,QAAQ,SAAS;AAAA,EAC1B;AAGA,MAAI,iBAAiB,MAAM;AACzB,WAAO,MAAM,YAAA;AAAA,EACf;AAIA,MAAI,MAAM,QAAQ,KAAK,GAAG;AAExB,UAAM,WAAW,MAAM,IAAI,CAAC,SAAS;AACnC,UAAI,SAAS,QAAQ,SAAS,QAAW;AACvC,eAAO;AAAA,MACT;AACA,UAAI,OAAO,SAAS,UAAU;AAE5B,cAAM,UAAU,KAAK,QAAQ,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAK;AAC/D,eAAO,IAAI,OAAO;AAAA,MACpB;AACA,aAAO,UAAU,IAAI;AAAA,IACvB,CAAC;AACD,WAAO,IAAI,SAAS,KAAK,GAAG,CAAC;AAAA,EAC/B;AAEA,QAAM,IAAI,MAAM,2BAA2B,KAAK,UAAU,KAAK,CAAC,EAAE;AACpE;"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SubsetParams } from '@electric-sql/client';
|
|
2
|
+
import { LoadSubsetOptions } from '@tanstack/db';
|
|
3
|
+
export type CompiledSqlRecord = Omit<SubsetParams, `params`> & {
|
|
4
|
+
params?: Array<unknown>;
|
|
5
|
+
};
|
|
6
|
+
export declare function compileSQL<T>(options: LoadSubsetOptions): SubsetParams;
|