@tanstack/electric-db-collection 0.2.13 → 0.2.15
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 +17 -2
- package/dist/cjs/electric.cjs.map +1 -1
- package/dist/cjs/index.cjs +9 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/sql-compiler.cjs +18 -1
- package/dist/cjs/sql-compiler.cjs.map +1 -1
- package/dist/esm/electric.js +17 -2
- package/dist/esm/electric.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +4 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/sql-compiler.js +18 -1
- package/dist/esm/sql-compiler.js.map +1 -1
- package/package.json +2 -2
- package/src/electric.ts +32 -6
- package/src/index.ts +2 -0
- package/src/sql-compiler.ts +39 -2
package/dist/cjs/electric.cjs
CHANGED
|
@@ -115,6 +115,7 @@ function electricCollectionOptions(config) {
|
|
|
115
115
|
const finalSyncMode = internalSyncMode === `progressive` ? `on-demand` : internalSyncMode;
|
|
116
116
|
const pendingMatches = new store.Store(/* @__PURE__ */ new Map());
|
|
117
117
|
const currentBatchMessages = new store.Store([]);
|
|
118
|
+
const batchCommitted = new store.Store(false);
|
|
118
119
|
const removePendingMatches = (matchIds) => {
|
|
119
120
|
if (matchIds.length > 0) {
|
|
120
121
|
pendingMatches.setState((current) => {
|
|
@@ -145,6 +146,7 @@ function electricCollectionOptions(config) {
|
|
|
145
146
|
syncMode: internalSyncMode,
|
|
146
147
|
pendingMatches,
|
|
147
148
|
currentBatchMessages,
|
|
149
|
+
batchCommitted,
|
|
148
150
|
removePendingMatches,
|
|
149
151
|
resolveMatchedPendingMatches,
|
|
150
152
|
collectionId: config.id,
|
|
@@ -237,6 +239,14 @@ function electricCollectionOptions(config) {
|
|
|
237
239
|
};
|
|
238
240
|
for (const message of currentBatchMessages.state) {
|
|
239
241
|
if (matchFn(message)) {
|
|
242
|
+
if (batchCommitted.state) {
|
|
243
|
+
debug(
|
|
244
|
+
`${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in committed batch, resolving immediately`
|
|
245
|
+
);
|
|
246
|
+
clearTimeout(timeoutId);
|
|
247
|
+
resolve(true);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
240
250
|
debug(
|
|
241
251
|
`${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in current batch, waiting for up-to-date`
|
|
242
252
|
);
|
|
@@ -248,7 +258,7 @@ function electricCollectionOptions(config) {
|
|
|
248
258
|
reject,
|
|
249
259
|
timeoutId,
|
|
250
260
|
matched: true
|
|
251
|
-
// Already matched
|
|
261
|
+
// Already matched, will resolve on up-to-date
|
|
252
262
|
});
|
|
253
263
|
return newMatches;
|
|
254
264
|
});
|
|
@@ -320,6 +330,7 @@ function createElectricSync(shapeOptions, options) {
|
|
|
320
330
|
syncMode,
|
|
321
331
|
pendingMatches,
|
|
322
332
|
currentBatchMessages,
|
|
333
|
+
batchCommitted,
|
|
323
334
|
removePendingMatches,
|
|
324
335
|
resolveMatchedPendingMatches,
|
|
325
336
|
collectionId,
|
|
@@ -413,6 +424,8 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
413
424
|
unsubscribeStream = stream.subscribe((messages) => {
|
|
414
425
|
let hasUpToDate = false;
|
|
415
426
|
let hasSnapshotEnd = false;
|
|
427
|
+
currentBatchMessages.setState(() => []);
|
|
428
|
+
batchCommitted.setState(() => false);
|
|
416
429
|
for (const message of messages) {
|
|
417
430
|
if (client.isChangeMessage(message)) {
|
|
418
431
|
currentBatchMessages.setState((currentBuffer) => {
|
|
@@ -523,7 +536,6 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
523
536
|
transactionStarted = false;
|
|
524
537
|
}
|
|
525
538
|
}
|
|
526
|
-
currentBatchMessages.setState(() => []);
|
|
527
539
|
if (hasUpToDate || hasSnapshotEnd && syncMode === `on-demand`) {
|
|
528
540
|
wrappedMarkReady(isBufferingInitialSync());
|
|
529
541
|
}
|
|
@@ -553,6 +565,9 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
553
565
|
newSnapshots.length = 0;
|
|
554
566
|
return seen;
|
|
555
567
|
});
|
|
568
|
+
if (hasUpToDate || hasSnapshotEnd && syncMode === `on-demand`) {
|
|
569
|
+
batchCommitted.setState(() => true);
|
|
570
|
+
}
|
|
556
571
|
resolveMatchedPendingMatches();
|
|
557
572
|
}
|
|
558
573
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"electric.cjs","sources":["../../src/electric.ts"],"sourcesContent":["import {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n isVisibleInSnapshot,\n} from '@electric-sql/client'\nimport { Store } from '@tanstack/store'\nimport DebugModule from 'debug'\nimport { DeduplicatedLoadSubset, and } 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 * Symbol for internal test hooks (hidden from public API)\n */\nexport const ELECTRIC_TEST_HOOKS = Symbol(`electricTestHooks`)\n\n/**\n * Internal test hooks interface (for testing only)\n */\nexport interface ElectricTestHooks {\n /**\n * Called before marking collection ready after first up-to-date in progressive mode\n * Allows tests to pause and validate snapshot phase before atomic swap completes\n */\n beforeMarkingReady?: () => Promise<void>\n}\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<\n T,\n string | number,\n TSchema,\n ElectricCollectionUtils<T>,\n any\n >,\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 * Internal test hooks (for testing only)\n * Hidden via Symbol to prevent accidental usage in production\n */\n [ELECTRIC_TEST_HOOKS]?: ElectricTestHooks\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?: (\n params: InsertMutationFnParams<\n T,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => 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?: (\n params: UpdateMutationFnParams<\n T,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => 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?: (\n params: DeleteMutationFnParams<\n T,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => 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 * Creates a deduplicated loadSubset handler for progressive/on-demand modes\n * Returns null for eager mode, or a DeduplicatedLoadSubset instance for other modes.\n * Handles fetching snapshots in progressive mode during buffering phase,\n * and requesting snapshots in on-demand mode.\n *\n * When cursor expressions are provided (whereFrom/whereCurrent), makes two\n * requestSnapshot calls:\n * - One for whereFrom (rows > cursor) with limit\n * - One for whereCurrent (rows = cursor, for tie-breaking) without limit\n */\nfunction createLoadSubsetDedupe<T extends Row<unknown>>({\n stream,\n syncMode,\n isBufferingInitialSync,\n begin,\n write,\n commit,\n collectionId,\n}: {\n stream: ShapeStream<T>\n syncMode: ElectricSyncMode\n isBufferingInitialSync: () => boolean\n begin: () => void\n write: (mutation: {\n type: `insert` | `update` | `delete`\n value: T\n metadata: Record<string, unknown>\n }) => void\n commit: () => void\n collectionId?: string\n}): DeduplicatedLoadSubset | null {\n // Eager mode doesn't need subset loading\n if (syncMode === `eager`) {\n return null\n }\n\n const loadSubset = async (opts: LoadSubsetOptions) => {\n // In progressive mode, use fetchSnapshot during snapshot phase\n if (isBufferingInitialSync()) {\n // Progressive mode snapshot phase: fetch and apply immediately\n const snapshotParams = compileSQL<T>(opts)\n try {\n const { data: rows } = await stream.fetchSnapshot(snapshotParams)\n\n // Check again if we're still buffering - we might have received up-to-date\n // and completed the atomic swap while waiting for the snapshot\n if (!isBufferingInitialSync()) {\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Ignoring snapshot - sync completed while fetching`,\n )\n return\n }\n\n // Apply snapshot data in a sync transaction (only if we have data)\n if (rows.length > 0) {\n begin()\n for (const row of rows) {\n write({\n type: `insert`,\n value: row.value,\n metadata: {\n ...row.headers,\n },\n })\n }\n commit()\n\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Applied snapshot with ${rows.length} rows`,\n )\n }\n } catch (error) {\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Error fetching snapshot: %o`,\n error,\n )\n throw error\n }\n } else if (syncMode === `progressive`) {\n // Progressive mode after full sync complete: no need to load more\n return\n } else {\n // On-demand mode: use requestSnapshot\n // When cursor is provided, make two calls:\n // 1. whereCurrent (all ties, no limit)\n // 2. whereFrom (rows > cursor, with limit)\n const { cursor, where, orderBy, limit } = opts\n\n if (cursor) {\n // Make parallel requests for cursor-based pagination\n const promises: Array<Promise<unknown>> = []\n\n // Request 1: All rows matching whereCurrent (ties at boundary, no limit)\n // Combine main where with cursor.whereCurrent\n const whereCurrentOpts: LoadSubsetOptions = {\n where: where ? and(where, cursor.whereCurrent) : cursor.whereCurrent,\n orderBy,\n // No limit - get all ties\n }\n const whereCurrentParams = compileSQL<T>(whereCurrentOpts)\n promises.push(stream.requestSnapshot(whereCurrentParams))\n\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Requesting cursor.whereCurrent snapshot (all ties)`,\n )\n\n // Request 2: Rows matching whereFrom (rows > cursor, with limit)\n // Combine main where with cursor.whereFrom\n const whereFromOpts: LoadSubsetOptions = {\n where: where ? and(where, cursor.whereFrom) : cursor.whereFrom,\n orderBy,\n limit,\n }\n const whereFromParams = compileSQL<T>(whereFromOpts)\n promises.push(stream.requestSnapshot(whereFromParams))\n\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Requesting cursor.whereFrom snapshot (with limit ${limit})`,\n )\n\n // Wait for both requests to complete\n await Promise.all(promises)\n } else {\n // No cursor - standard single request\n const snapshotParams = compileSQL<T>(opts)\n await stream.requestSnapshot(snapshotParams)\n }\n }\n }\n\n return new DeduplicatedLoadSubset({ loadSubset })\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<\n 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): Omit<CollectionConfig<InferSchemaOutput<T>, string | number, T>, `utils`> & {\n id?: string\n utils: ElectricCollectionUtils<InferSchemaOutput<T>>\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): Omit<CollectionConfig<T, string | number>, `utils`> & {\n id?: string\n utils: ElectricCollectionUtils<T>\n schema?: never // no schema in the result\n}\n\nexport function electricCollectionOptions<T extends Row<unknown>>(\n config: ElectricCollectionConfig<T, any>,\n): Omit<\n CollectionConfig<T, string | number, any, ElectricCollectionUtils<T>>,\n `utils`\n> & {\n id?: string\n utils: ElectricCollectionUtils<T>\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<T>(config.shapeOptions, {\n seenTxids,\n seenSnapshots,\n syncMode: internalSyncMode,\n pendingMatches,\n currentBatchMessages,\n removePendingMatches,\n resolveMatchedPendingMatches,\n collectionId: config.id,\n testHooks: config[ELECTRIC_TEST_HOOKS],\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 (\n params: InsertMutationFnParams<\n any,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => {\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 (\n params: UpdateMutationFnParams<\n any,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => {\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 (\n params: DeleteMutationFnParams<\n any,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => {\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 },\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 testHooks?: ElectricTestHooks\n },\n): SyncConfig<T> {\n const {\n seenTxids,\n seenSnapshots,\n syncMode,\n pendingMatches,\n currentBatchMessages,\n removePendingMatches,\n resolveMatchedPendingMatches,\n collectionId,\n testHooks,\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 // Wrap markReady to wait for test hook in progressive mode\n let progressiveReadyGate: Promise<void> | null = null\n const wrappedMarkReady = (isBuffering: boolean) => {\n // Only create gate if we're in buffering phase (first up-to-date)\n if (\n isBuffering &&\n syncMode === `progressive` &&\n testHooks?.beforeMarkingReady\n ) {\n // Create a new gate promise for this sync cycle\n progressiveReadyGate = testHooks.beforeMarkingReady()\n progressiveReadyGate.then(() => {\n markReady()\n })\n } else {\n // No hook, not buffering, or already past first up-to-date\n markReady()\n }\n }\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 // Progressive mode state\n // Helper to determine if we're buffering the initial sync\n const isBufferingInitialSync = () =>\n syncMode === `progressive` && !hasReceivedUpToDate\n const bufferedMessages: Array<Message<T>> = [] // Buffer change messages during initial sync\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 = createLoadSubsetDedupe({\n stream,\n syncMode,\n isBufferingInitialSync,\n begin,\n write,\n commit,\n collectionId,\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 // Skip during buffered initial sync in progressive mode (txids will be extracted during atomic swap)\n if (hasTxids(message) && !isBufferingInitialSync()) {\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 // In buffered initial sync of progressive mode, buffer messages instead of writing\n if (isBufferingInitialSync()) {\n bufferedMessages.push(message)\n } else {\n // Normal processing: write changes immediately\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 }\n } else if (isSnapshotEndMessage(message)) {\n // Skip snapshot-end tracking during buffered initial sync (will be extracted during atomic swap)\n if (!isBufferingInitialSync()) {\n newSnapshots.push(parseSnapshotMessage(message))\n }\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 (isBufferingInitialSync will reflect this)\n bufferedMessages.length = 0 // Clear buffered messages\n }\n }\n\n if (hasUpToDate || hasSnapshotEnd) {\n // PROGRESSIVE MODE: Atomic swap on first up-to-date\n if (isBufferingInitialSync() && hasUpToDate) {\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Performing atomic swap with ${bufferedMessages.length} buffered messages`,\n )\n\n // Start atomic swap transaction\n begin()\n\n // Truncate to clear all snapshot data\n truncate()\n\n // Apply all buffered change messages and extract txids/snapshots\n for (const bufferedMsg of bufferedMessages) {\n if (isChangeMessage(bufferedMsg)) {\n write({\n type: bufferedMsg.headers.operation,\n value: bufferedMsg.value,\n metadata: {\n ...bufferedMsg.headers,\n },\n })\n\n // Extract txids from buffered messages (will be committed to store after transaction)\n if (hasTxids(bufferedMsg)) {\n bufferedMsg.headers.txids?.forEach((txid) =>\n newTxids.add(txid),\n )\n }\n } else if (isSnapshotEndMessage(bufferedMsg)) {\n // Extract snapshots from buffered messages (will be committed to store after transaction)\n newSnapshots.push(parseSnapshotMessage(bufferedMsg))\n }\n }\n\n // Commit the atomic swap\n commit()\n\n // Exit buffering phase by marking that we've received up-to-date\n // isBufferingInitialSync() will now return false\n bufferedMessages.length = 0\n\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Atomic swap complete, now in normal sync mode`,\n )\n } else {\n // Normal mode or on-demand: commit transaction if one was started\n // In eager mode, only commit on snapshot-end if we've already received\n // the first up-to-date, because the snapshot-end in the log could be from\n // a significant period before the stream is actually up to date\n const shouldCommit =\n hasUpToDate || syncMode === `on-demand` || hasReceivedUpToDate\n\n if (transactionStarted && shouldCommit) {\n commit()\n transactionStarted = false\n }\n }\n\n // Clear the current batch buffer since we're now up-to-date\n currentBatchMessages.setState(() => [])\n\n if (hasUpToDate || (hasSnapshotEnd && syncMode === `on-demand`)) {\n // Mark the collection as ready now that sync is up to date\n wrappedMarkReady(isBufferingInitialSync())\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":["isControlMessage","compileSQL","and","DeduplicatedLoadSubset","Store","ExpectedNumberInAwaitTxIdError","isVisibleInSnapshot","TimeoutWaitingForTxIdError","TimeoutWaitingForMatchError","StreamAbortedError","ShapeStream","isChangeMessage"],"mappings":";;;;;;;;AAwCA,MAAM,QAAQ,YAAY,MAAM,gBAAgB;AAKzC,MAAM,sBAAsB,OAAO,mBAAmB;AAmO7D,SAAS,kBACP,SACkD;AAClD,SAAOA,OAAAA,iBAAiB,OAAO,KAAK,QAAQ,QAAQ,YAAY;AAClE;AAEA,SAAS,qBACP,SACsE;AACtE,SAAOA,OAAAA,iBAAiB,OAAO,KAAK,QAAQ,QAAQ,YAAY;AAClE;AAEA,SAAS,qBACP,SAC+B;AAC/B,SAAOA,OAAAA,iBAAiB,OAAO,KAAK,QAAQ,QAAQ,YAAY;AAClE;AAEA,SAAS,qBAAqB,SAA+C;AAC3E,SAAO;AAAA,IACL,MAAM,QAAQ,QAAQ;AAAA,IACtB,MAAM,QAAQ,QAAQ;AAAA,IACtB,UAAU,QAAQ,QAAQ;AAAA,EAAA;AAE9B;AAGA,SAAS,SACP,SAC8D;AAC9D,SAAO,WAAW,QAAQ,WAAW,MAAM,QAAQ,QAAQ,QAAQ,KAAK;AAC1E;AAaA,SAAS,uBAA+C;AAAA,EACtD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAYkC;AAEhC,MAAI,aAAa,SAAS;AACxB,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,OAAO,SAA4B;AAEpD,QAAI,0BAA0B;AAE5B,YAAM,iBAAiBC,YAAAA,WAAc,IAAI;AACzC,UAAI;AACF,cAAM,EAAE,MAAM,KAAA,IAAS,MAAM,OAAO,cAAc,cAAc;AAIhE,YAAI,CAAC,0BAA0B;AAC7B;AAAA,YACE,GAAG,eAAe,IAAI,YAAY,OAAO,EAAE;AAAA,UAAA;AAE7C;AAAA,QACF;AAGA,YAAI,KAAK,SAAS,GAAG;AACnB,gBAAA;AACA,qBAAW,OAAO,MAAM;AACtB,kBAAM;AAAA,cACJ,MAAM;AAAA,cACN,OAAO,IAAI;AAAA,cACX,UAAU;AAAA,gBACR,GAAG,IAAI;AAAA,cAAA;AAAA,YACT,CACD;AAAA,UACH;AACA,iBAAA;AAEA;AAAA,YACE,GAAG,eAAe,IAAI,YAAY,OAAO,EAAE,yBAAyB,KAAK,MAAM;AAAA,UAAA;AAAA,QAEnF;AAAA,MACF,SAAS,OAAO;AACd;AAAA,UACE,GAAG,eAAe,IAAI,YAAY,OAAO,EAAE;AAAA,UAC3C;AAAA,QAAA;AAEF,cAAM;AAAA,MACR;AAAA,IACF,WAAW,aAAa,eAAe;AAErC;AAAA,IACF,OAAO;AAKL,YAAM,EAAE,QAAQ,OAAO,SAAS,UAAU;AAE1C,UAAI,QAAQ;AAEV,cAAM,WAAoC,CAAA;AAI1C,cAAM,mBAAsC;AAAA,UAC1C,OAAO,QAAQC,OAAI,OAAO,OAAO,YAAY,IAAI,OAAO;AAAA,UACxD;AAAA;AAAA,QAAA;AAGF,cAAM,qBAAqBD,YAAAA,WAAc,gBAAgB;AACzD,iBAAS,KAAK,OAAO,gBAAgB,kBAAkB,CAAC;AAExD;AAAA,UACE,GAAG,eAAe,IAAI,YAAY,OAAO,EAAE;AAAA,QAAA;AAK7C,cAAM,gBAAmC;AAAA,UACvC,OAAO,QAAQC,OAAI,OAAO,OAAO,SAAS,IAAI,OAAO;AAAA,UACrD;AAAA,UACA;AAAA,QAAA;AAEF,cAAM,kBAAkBD,YAAAA,WAAc,aAAa;AACnD,iBAAS,KAAK,OAAO,gBAAgB,eAAe,CAAC;AAErD;AAAA,UACE,GAAG,eAAe,IAAI,YAAY,OAAO,EAAE,oDAAoD,KAAK;AAAA,QAAA;AAItG,cAAM,QAAQ,IAAI,QAAQ;AAAA,MAC5B,OAAO;AAEL,cAAM,iBAAiBA,YAAAA,WAAc,IAAI;AACzC,cAAM,OAAO,gBAAgB,cAAc;AAAA,MAC7C;AAAA,IACF;AAAA,EACF;AAEA,SAAO,IAAIE,GAAAA,uBAAuB,EAAE,YAAY;AAClD;AAyDO,SAAS,0BACd,QAQA;AACA,QAAM,YAAY,IAAIC,MAAAA,0BAAqB,IAAI,CAAA,CAAE,CAAC;AAClD,QAAM,gBAAgB,IAAIA,MAAAA,MAA+B,EAAE;AAC3D,QAAM,mBAAmB,OAAO,YAAY;AAC5C,QAAM,gBACJ,qBAAqB,gBAAgB,cAAc;AACrD,QAAM,iBAAiB,IAAIA,YAWzB,oBAAI,KAAK;AAGX,QAAM,uBAAuB,IAAIA,MAAAA,MAA2B,EAAE;AAK9D,QAAM,uBAAuB,CAAC,aAA4B;AACxD,QAAI,SAAS,SAAS,GAAG;AACvB,qBAAe,SAAS,CAAC,YAAY;AACnC,cAAM,aAAa,IAAI,IAAI,OAAO;AAClC,iBAAS,QAAQ,CAAC,OAAO,WAAW,OAAO,EAAE,CAAC;AAC9C,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,EACF;AAKA,QAAM,+BAA+B,MAAM;AACzC,UAAM,mBAAkC,CAAA;AACxC,mBAAe,MAAM,QAAQ,CAAC,OAAO,YAAY;AAC/C,UAAI,MAAM,SAAS;AACjB,qBAAa,MAAM,SAAS;AAC5B,cAAM,QAAQ,IAAI;AAClB,yBAAiB,KAAK,OAAO;AAC7B;AAAA,UACE,GAAG,OAAO,KAAK,IAAI,OAAO,EAAE,OAAO,EAAE;AAAA,UACrC;AAAA,QAAA;AAAA,MAEJ;AAAA,IACF,CAAC;AACD,yBAAqB,gBAAgB;AAAA,EACvC;AACA,QAAM,OAAO,mBAAsB,OAAO,cAAc;AAAA,IACtD;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc,OAAO;AAAA,IACrB,WAAW,OAAO,mBAAmB;AAAA,EAAA,CACtC;AAQD,QAAM,YAAyB,OAC7B,MACA,UAAkB,QACG;AACrB;AAAA,MACE,GAAG,OAAO,KAAK,IAAI,OAAO,EAAE,OAAO,EAAE;AAAA,MACrC;AAAA,IAAA;AAEF,QAAI,OAAO,SAAS,UAAU;AAC5B,YAAM,IAAIC,OAAAA,+BAA+B,OAAO,MAAM,OAAO,EAAE;AAAA,IACjE;AAGA,UAAM,UAAU,UAAU,MAAM,IAAI,IAAI;AACxC,QAAI,QAAS,QAAO;AAGpB,UAAM,cAAc,cAAc,MAAM;AAAA,MAAK,CAAC,aAC5CC,2BAAoB,MAAM,QAAQ;AAAA,IAAA;AAEpC,QAAI,YAAa,QAAO;AAExB,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,YAAY,WAAW,MAAM;AACjC,6BAAA;AACA,iCAAA;AACA,eAAO,IAAIC,OAAAA,2BAA2B,MAAM,OAAO,EAAE,CAAC;AAAA,MACxD,GAAG,OAAO;AAEV,YAAM,uBAAuB,UAAU,UAAU,MAAM;AACrD,YAAI,UAAU,MAAM,IAAI,IAAI,GAAG;AAC7B;AAAA,YACE,GAAG,OAAO,KAAK,IAAI,OAAO,EAAE,OAAO,EAAE;AAAA,YACrC;AAAA,UAAA;AAEF,uBAAa,SAAS;AACtB,+BAAA;AACA,mCAAA;AACA,kBAAQ,IAAI;AAAA,QACd;AAAA,MACF,CAAC;AAED,YAAM,2BAA2B,cAAc,UAAU,MAAM;AAC7D,cAAM,kBAAkB,cAAc,MAAM;AAAA,UAAK,CAAC,aAChDD,2BAAoB,MAAM,QAAQ;AAAA,QAAA;AAEpC,YAAI,iBAAiB;AACnB;AAAA,YACE,GAAG,OAAO,KAAK,IAAI,OAAO,EAAE,OAAO,EAAE;AAAA,YACrC;AAAA,YACA;AAAA,UAAA;AAEF,uBAAa,SAAS;AACtB,mCAAA;AACA,+BAAA;AACA,kBAAQ,IAAI;AAAA,QACd;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAQA,QAAM,aAAgC,OACpC,SACA,UAAkB,QACG;AACrB;AAAA,MACE,GAAG,OAAO,KAAK,IAAI,OAAO,EAAE,OAAO,EAAE;AAAA,IAAA;AAGvC,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,UAAU,KAAK,OAAA,EAAS,SAAS,EAAE;AAEzC,YAAM,eAAe,MAAM;AACzB,uBAAe,SAAS,CAAC,YAAY;AACnC,gBAAM,aAAa,IAAI,IAAI,OAAO;AAClC,qBAAW,OAAO,OAAO;AACzB,iBAAO;AAAA,QACT,CAAC;AAAA,MACH;AAEA,YAAM,YAAY,MAAM;AACtB,qBAAA;AACA,eAAO,IAAIE,OAAAA,4BAA4B,OAAO,EAAE,CAAC;AAAA,MACnD;AAEA,YAAM,YAAY,WAAW,WAAW,OAAO;AAI/C,YAAM,aAAa,CAAC,YAA0B;AAC5C,YAAI,QAAQ,OAAO,GAAG;AACpB;AAAA,YACE,GAAG,OAAO,KAAK,IAAI,OAAO,EAAE,OAAO,EAAE;AAAA,UAAA;AAGvC,yBAAe,SAAS,CAAC,YAAY;AACnC,kBAAM,aAAa,IAAI,IAAI,OAAO;AAClC,kBAAM,WAAW,WAAW,IAAI,OAAO;AACvC,gBAAI,UAAU;AACZ,yBAAW,IAAI,SAAS,EAAE,GAAG,UAAU,SAAS,MAAM;AAAA,YACxD;AACA,mBAAO;AAAA,UACT,CAAC;AACD,iBAAO;AAAA,QACT;AACA,eAAO;AAAA,MACT;AAGA,iBAAW,WAAW,qBAAqB,OAAO;AAChD,YAAI,QAAQ,OAAO,GAAG;AACpB;AAAA,YACE,GAAG,OAAO,KAAK,IAAI,OAAO,EAAE,OAAO,EAAE;AAAA,UAAA;AAGvC,yBAAe,SAAS,CAAC,YAAY;AACnC,kBAAM,aAAa,IAAI,IAAI,OAAO;AAClC,uBAAW,IAAI,SAAS;AAAA,cACtB,SAAS;AAAA,cACT;AAAA,cACA;AAAA,cACA;AAAA,cACA,SAAS;AAAA;AAAA,YAAA,CACV;AACD,mBAAO;AAAA,UACT,CAAC;AACD;AAAA,QACF;AAAA,MACF;AAIA,qBAAe,SAAS,CAAC,YAAY;AACnC,cAAM,aAAa,IAAI,IAAI,OAAO;AAClC,mBAAW,IAAI,SAAS;AAAA,UACtB,SAAS;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA,SAAS;AAAA,QAAA,CACV;AACD,eAAO;AAAA,MACT,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAKA,QAAM,0BAA0B,OAC9B,WACkB;AAElB,QAAI,UAAU,UAAU,QAAQ;AAC9B,YAAM,UAAU,OAAO;AAEvB,UAAI,MAAM,QAAQ,OAAO,IAAI,GAAG;AAC9B,cAAM,QAAQ,IAAI,OAAO,KAAK,IAAI,CAAC,SAAS,UAAU,MAAM,OAAO,CAAC,CAAC;AAAA,MACvE,OAAO;AACL,cAAM,UAAU,OAAO,MAAM,OAAO;AAAA,MACtC;AAAA,IACF;AAAA,EAEF;AAGA,QAAM,kBAAkB,OAAO,WAC3B,OACE,WAKG;AACH,UAAM,gBAAgB,MAAM,OAAO,SAAU,MAAM;AACnD,UAAM,wBAAwB,aAAa;AAC3C,WAAO;AAAA,EACT,IACA;AAEJ,QAAM,kBAAkB,OAAO,WAC3B,OACE,WAKG;AACH,UAAM,gBAAgB,MAAM,OAAO,SAAU,MAAM;AACnD,UAAM,wBAAwB,aAAa;AAC3C,WAAO;AAAA,EACT,IACA;AAEJ,QAAM,kBAAkB,OAAO,WAC3B,OACE,WAKG;AACH,UAAM,gBAAgB,MAAM,OAAO,SAAU,MAAM;AACnD,UAAM,wBAAwB,aAAa;AAC3C,WAAO;AAAA,EACT,IACA;AAGJ,QAAM;AAAA,IACJ,cAAc;AAAA,IACd,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,GAAG;AAAA,EAAA,IACD;AAEJ,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU;AAAA,IACV;AAAA,IACA,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,OAAO;AAAA,MACL;AAAA,MACA;AAAA,IAAA;AAAA,EACF;AAEJ;AAKA,SAAS,mBACP,cACA,SAsBe;AACf,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,IACE;AACJ,QAAM,qBAAqB;AAG3B,QAAM,iBAAiB,IAAIJ,MAAAA,MAA0B,MAAS;AAM9D,QAAM,kBAAkB,MAA+B;AAErD,UAAM,SAAS,eAAe,SAAS;AAEvC,WAAO;AAAA,MACL,UAAU,aAAa,QAAQ,QAC3B,CAAC,QAAQ,aAAa,OAAO,KAAK,IAClC;AAAA,IAAA;AAAA,EAER;AAEA,MAAI;AAEJ,SAAO;AAAA,IACL,MAAM,CAAC,WAAiD;AACtD,YAAM,EAAE,OAAO,OAAO,QAAQ,WAAW,UAAU,eAAe;AAGlE,UAAI,uBAA6C;AACjD,YAAM,mBAAmB,CAAC,gBAAyB;AAEjD,YACE,eACA,aAAa,iBACb,WAAW,oBACX;AAEA,iCAAuB,UAAU,mBAAA;AACjC,+BAAqB,KAAK,MAAM;AAC9B,sBAAA;AAAA,UACF,CAAC;AAAA,QACH,OAAO;AAEL,oBAAA;AAAA,QACF;AAAA,MACF;AAGA,YAAM,kBAAkB,IAAI,gBAAA;AAE5B,UAAI,aAAa,QAAQ;AACvB,qBAAa,OAAO;AAAA,UAClB;AAAA,UACA,MAAM;AACJ,4BAAgB,MAAA;AAAA,UAClB;AAAA,UACA;AAAA,YACE,MAAM;AAAA,UAAA;AAAA,QACR;AAEF,YAAI,aAAa,OAAO,SAAS;AAC/B,0BAAgB,MAAA;AAAA,QAClB;AAAA,MACF;AAGA,sBAAgB,OAAO,iBAAiB,SAAS,MAAM;AACrD,uBAAe,SAAS,CAAC,YAAY;AACnC,kBAAQ,QAAQ,CAAC,UAAU;AACzB,yBAAa,MAAM,SAAS;AAC5B,kBAAM,OAAO,IAAIK,OAAAA,oBAAoB;AAAA,UACvC,CAAC;AACD,qCAAW,IAAA;AAAA,QACb,CAAC;AAAA,MACH,CAAC;AAED,YAAM,SAAS,IAAIC,mBAAY;AAAA,QAC7B,GAAG;AAAA;AAAA,QAEH,KAAK,aAAa,cAAc,iBAAiB;AAAA;AAAA;AAAA,QAGjD,QACE,aAAa,WAAW,aAAa,cAAc,QAAQ;AAAA,QAC7D,QAAQ,gBAAgB;AAAA,QACxB,SAAS,CAAC,gBAAgB;AAMxB,oBAAA;AAEA,cAAI,aAAa,SAAS;AACxB,mBAAO,aAAa,QAAQ,WAAW;AAAA,UACzC,OAAO;AACL,oBAAQ;AAAA,cACN,+CAA+C,WAAW,EAAE;AAAA;AAAA;AAAA,cAG5D;AAAA,YAAA;AAAA,UAEJ;AAEA;AAAA,QACF;AAAA,MAAA,CACD;AACD,UAAI,qBAAqB;AACzB,YAAM,+BAAe,IAAA;AACrB,YAAM,eAAwC,CAAA;AAC9C,UAAI,sBAAsB;AAI1B,YAAM,yBAAyB,MAC7B,aAAa,iBAAiB,CAAC;AACjC,YAAM,mBAAsC,CAAA;AAK5C,YAAM,mBAAmB,uBAAuB;AAAA,QAC9C;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA,CACD;AAED,0BAAoB,OAAO,UAAU,CAAC,aAAgC;AACpE,YAAI,cAAc;AAClB,YAAI,iBAAiB;AAErB,mBAAW,WAAW,UAAU;AAE9B,cAAIC,OAAAA,gBAAgB,OAAO,GAAG;AAC5B,iCAAqB,SAAS,CAAC,kBAAkB;AAC/C,oBAAM,YAAY,CAAC,GAAG,eAAe,OAAO;AAE5C,kBAAI,UAAU,SAAS,oBAAoB;AACzC,0BAAU,OAAO,GAAG,UAAU,SAAS,kBAAkB;AAAA,cAC3D;AACA,qBAAO;AAAA,YACT,CAAC;AAAA,UACH;AAIA,cAAI,SAAS,OAAO,KAAK,CAAC,0BAA0B;AAClD,oBAAQ,QAAQ,OAAO,QAAQ,CAAC,SAAS,SAAS,IAAI,IAAI,CAAC;AAAA,UAC7D;AAIA,gBAAM,kBAAiC,CAAA;AACvC,yBAAe,MAAM,QAAQ,CAAC,OAAO,YAAY;AAC/C,gBAAI,CAAC,MAAM,SAAS;AAClB,kBAAI;AACF,sBAAM,QAAQ,OAAO;AAAA,cACvB,SAAS,KAAK;AAEZ,6BAAa,MAAM,SAAS;AAC5B,sBAAM;AAAA,kBACJ,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAAA,gBAAA;AAEpD,gCAAgB,KAAK,OAAO;AAC5B,sBAAM,qBAAqB,GAAG;AAAA,cAChC;AAAA,YACF;AAAA,UACF,CAAC;AAGD,+BAAqB,eAAe;AAEpC,cAAIA,OAAAA,gBAAgB,OAAO,GAAG;AAE5B,kBAAM,SAAS,QAAQ,QAAQ;AAC/B,gBAAI,UAAU,OAAO,WAAW,UAAU;AAExC,6BAAe,SAAS,MAAM,MAAM;AAAA,YACtC;AAGA,gBAAI,0BAA0B;AAC5B,+BAAiB,KAAK,OAAO;AAAA,YAC/B,OAAO;AAEL,kBAAI,CAAC,oBAAoB;AACvB,sBAAA;AACA,qCAAqB;AAAA,cACvB;AAEA,oBAAM;AAAA,gBACJ,MAAM,QAAQ,QAAQ;AAAA,gBACtB,OAAO,QAAQ;AAAA;AAAA,gBAEf,UAAU;AAAA,kBACR,GAAG,QAAQ;AAAA,gBAAA;AAAA,cACb,CACD;AAAA,YACH;AAAA,UACF,WAAW,qBAAqB,OAAO,GAAG;AAExC,gBAAI,CAAC,0BAA0B;AAC7B,2BAAa,KAAK,qBAAqB,OAAO,CAAC;AAAA,YACjD;AACA,6BAAiB;AAAA,UACnB,WAAW,kBAAkB,OAAO,GAAG;AACrC,0BAAc;AAAA,UAChB,WAAW,qBAAqB,OAAO,GAAG;AACxC;AAAA,cACE,GAAG,eAAe,IAAI,YAAY,OAAO,EAAE;AAAA,YAAA;AAI7C,gBAAI,CAAC,oBAAoB;AACvB,oBAAA;AACA,mCAAqB;AAAA,YACvB;AAEA,qBAAA;AAIA,8BAAkB,MAAA;AAGlB,0BAAc;AACd,6BAAiB;AACjB,kCAAsB;AACtB,6BAAiB,SAAS;AAAA,UAC5B;AAAA,QACF;AAEA,YAAI,eAAe,gBAAgB;AAEjC,cAAI,uBAAA,KAA4B,aAAa;AAC3C;AAAA,cACE,GAAG,eAAe,IAAI,YAAY,OAAO,EAAE,iDAAiD,iBAAiB,MAAM;AAAA,YAAA;AAIrH,kBAAA;AAGA,qBAAA;AAGA,uBAAW,eAAe,kBAAkB;AAC1C,kBAAIA,OAAAA,gBAAgB,WAAW,GAAG;AAChC,sBAAM;AAAA,kBACJ,MAAM,YAAY,QAAQ;AAAA,kBAC1B,OAAO,YAAY;AAAA,kBACnB,UAAU;AAAA,oBACR,GAAG,YAAY;AAAA,kBAAA;AAAA,gBACjB,CACD;AAGD,oBAAI,SAAS,WAAW,GAAG;AACzB,8BAAY,QAAQ,OAAO;AAAA,oBAAQ,CAAC,SAClC,SAAS,IAAI,IAAI;AAAA,kBAAA;AAAA,gBAErB;AAAA,cACF,WAAW,qBAAqB,WAAW,GAAG;AAE5C,6BAAa,KAAK,qBAAqB,WAAW,CAAC;AAAA,cACrD;AAAA,YACF;AAGA,mBAAA;AAIA,6BAAiB,SAAS;AAE1B;AAAA,cACE,GAAG,eAAe,IAAI,YAAY,OAAO,EAAE;AAAA,YAAA;AAAA,UAE/C,OAAO;AAKL,kBAAM,eACJ,eAAe,aAAa,eAAe;AAE7C,gBAAI,sBAAsB,cAAc;AACtC,qBAAA;AACA,mCAAqB;AAAA,YACvB;AAAA,UACF;AAGA,+BAAqB,SAAS,MAAM,EAAE;AAEtC,cAAI,eAAgB,kBAAkB,aAAa,aAAc;AAE/D,6BAAiB,wBAAwB;AAAA,UAC3C;AAGA,cAAI,aAAa;AACf,kCAAsB;AAAA,UACxB;AAGA,oBAAU,SAAS,CAAC,iBAAiB;AACnC,kBAAM,aAAa,IAAI,IAAU,YAAY;AAC7C,gBAAI,SAAS,OAAO,GAAG;AACrB;AAAA,gBACE,GAAG,eAAe,IAAI,YAAY,OAAO,EAAE;AAAA,gBAC3C,MAAM,KAAK,QAAQ;AAAA,cAAA;AAAA,YAEvB;AACA,qBAAS,QAAQ,CAAC,SAAS,WAAW,IAAI,IAAI,CAAC;AAC/C,qBAAS,MAAA;AACT,mBAAO;AAAA,UACT,CAAC;AAGD,wBAAc,SAAS,CAAC,qBAAqB;AAC3C,kBAAM,OAAO,CAAC,GAAG,kBAAkB,GAAG,YAAY;AAClD,yBAAa;AAAA,cAAQ,CAAC,aACpB;AAAA,gBACE,GAAG,eAAe,IAAI,YAAY,OAAO,EAAE;AAAA,gBAC3C;AAAA,cAAA;AAAA,YACF;AAEF,yBAAa,SAAS;AACtB,mBAAO;AAAA,UACT,CAAC;AAGD,uCAAA;AAAA,QACF;AAAA,MACF,CAAC;AAID,aAAO;AAAA,QACL,YAAY,kBAAkB;AAAA,QAC9B,SAAS,MAAM;AAEb,4BAAA;AAEA,0BAAgB,MAAA;AAEhB,4BAAkB,MAAA;AAAA,QACpB;AAAA,MAAA;AAAA,IAEJ;AAAA;AAAA,IAEA;AAAA,EAAA;AAEJ;;;;;;;;;;;"}
|
|
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 { DeduplicatedLoadSubset, and } 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 * Symbol for internal test hooks (hidden from public API)\n */\nexport const ELECTRIC_TEST_HOOKS = Symbol(`electricTestHooks`)\n\n/**\n * Internal test hooks interface (for testing only)\n */\nexport interface ElectricTestHooks {\n /**\n * Called before marking collection ready after first up-to-date in progressive mode\n * Allows tests to pause and validate snapshot phase before atomic swap completes\n */\n beforeMarkingReady?: () => Promise<void>\n}\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<\n T,\n string | number,\n TSchema,\n ElectricCollectionUtils<T>,\n any\n >,\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 * Internal test hooks (for testing only)\n * Hidden via Symbol to prevent accidental usage in production\n */\n [ELECTRIC_TEST_HOOKS]?: ElectricTestHooks\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?: (\n params: InsertMutationFnParams<\n T,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => 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?: (\n params: UpdateMutationFnParams<\n T,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => 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?: (\n params: DeleteMutationFnParams<\n T,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => 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 * Creates a deduplicated loadSubset handler for progressive/on-demand modes\n * Returns null for eager mode, or a DeduplicatedLoadSubset instance for other modes.\n * Handles fetching snapshots in progressive mode during buffering phase,\n * and requesting snapshots in on-demand mode.\n *\n * When cursor expressions are provided (whereFrom/whereCurrent), makes two\n * requestSnapshot calls:\n * - One for whereFrom (rows > cursor) with limit\n * - One for whereCurrent (rows = cursor, for tie-breaking) without limit\n */\nfunction createLoadSubsetDedupe<T extends Row<unknown>>({\n stream,\n syncMode,\n isBufferingInitialSync,\n begin,\n write,\n commit,\n collectionId,\n}: {\n stream: ShapeStream<T>\n syncMode: ElectricSyncMode\n isBufferingInitialSync: () => boolean\n begin: () => void\n write: (mutation: {\n type: `insert` | `update` | `delete`\n value: T\n metadata: Record<string, unknown>\n }) => void\n commit: () => void\n collectionId?: string\n}): DeduplicatedLoadSubset | null {\n // Eager mode doesn't need subset loading\n if (syncMode === `eager`) {\n return null\n }\n\n const loadSubset = async (opts: LoadSubsetOptions) => {\n // In progressive mode, use fetchSnapshot during snapshot phase\n if (isBufferingInitialSync()) {\n // Progressive mode snapshot phase: fetch and apply immediately\n const snapshotParams = compileSQL<T>(opts)\n try {\n const { data: rows } = await stream.fetchSnapshot(snapshotParams)\n\n // Check again if we're still buffering - we might have received up-to-date\n // and completed the atomic swap while waiting for the snapshot\n if (!isBufferingInitialSync()) {\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Ignoring snapshot - sync completed while fetching`,\n )\n return\n }\n\n // Apply snapshot data in a sync transaction (only if we have data)\n if (rows.length > 0) {\n begin()\n for (const row of rows) {\n write({\n type: `insert`,\n value: row.value,\n metadata: {\n ...row.headers,\n },\n })\n }\n commit()\n\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Applied snapshot with ${rows.length} rows`,\n )\n }\n } catch (error) {\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Error fetching snapshot: %o`,\n error,\n )\n throw error\n }\n } else if (syncMode === `progressive`) {\n // Progressive mode after full sync complete: no need to load more\n return\n } else {\n // On-demand mode: use requestSnapshot\n // When cursor is provided, make two calls:\n // 1. whereCurrent (all ties, no limit)\n // 2. whereFrom (rows > cursor, with limit)\n const { cursor, where, orderBy, limit } = opts\n\n if (cursor) {\n // Make parallel requests for cursor-based pagination\n const promises: Array<Promise<unknown>> = []\n\n // Request 1: All rows matching whereCurrent (ties at boundary, no limit)\n // Combine main where with cursor.whereCurrent\n const whereCurrentOpts: LoadSubsetOptions = {\n where: where ? and(where, cursor.whereCurrent) : cursor.whereCurrent,\n orderBy,\n // No limit - get all ties\n }\n const whereCurrentParams = compileSQL<T>(whereCurrentOpts)\n promises.push(stream.requestSnapshot(whereCurrentParams))\n\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Requesting cursor.whereCurrent snapshot (all ties)`,\n )\n\n // Request 2: Rows matching whereFrom (rows > cursor, with limit)\n // Combine main where with cursor.whereFrom\n const whereFromOpts: LoadSubsetOptions = {\n where: where ? and(where, cursor.whereFrom) : cursor.whereFrom,\n orderBy,\n limit,\n }\n const whereFromParams = compileSQL<T>(whereFromOpts)\n promises.push(stream.requestSnapshot(whereFromParams))\n\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Requesting cursor.whereFrom snapshot (with limit ${limit})`,\n )\n\n // Wait for both requests to complete\n await Promise.all(promises)\n } else {\n // No cursor - standard single request\n const snapshotParams = compileSQL<T>(opts)\n await stream.requestSnapshot(snapshotParams)\n }\n }\n }\n\n return new DeduplicatedLoadSubset({ loadSubset })\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<\n 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): Omit<CollectionConfig<InferSchemaOutput<T>, string | number, T>, `utils`> & {\n id?: string\n utils: ElectricCollectionUtils<InferSchemaOutput<T>>\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): Omit<CollectionConfig<T, string | number>, `utils`> & {\n id?: string\n utils: ElectricCollectionUtils<T>\n schema?: never // no schema in the result\n}\n\nexport function electricCollectionOptions<T extends Row<unknown>>(\n config: ElectricCollectionConfig<T, any>,\n): Omit<\n CollectionConfig<T, string | number, any, ElectricCollectionUtils<T>>,\n `utils`\n> & {\n id?: string\n utils: ElectricCollectionUtils<T>\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 // Track whether the current batch has been committed (up-to-date received)\n // This allows awaitMatch to resolve immediately for messages from committed batches\n const batchCommitted = new Store<boolean>(false)\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<T>(config.shapeOptions, {\n seenTxids,\n seenSnapshots,\n syncMode: internalSyncMode,\n pendingMatches,\n currentBatchMessages,\n batchCommitted,\n removePendingMatches,\n resolveMatchedPendingMatches,\n collectionId: config.id,\n testHooks: config[ELECTRIC_TEST_HOOKS],\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 // If batch is committed (up-to-date already received), resolve immediately\n // just like awaitTxId does when it finds a txid in seenTxids\n if (batchCommitted.state) {\n debug(\n `${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in committed batch, resolving immediately`,\n )\n clearTimeout(timeoutId)\n resolve(true)\n return\n }\n\n // If batch is not yet committed, register match and wait for up-to-date\n debug(\n `${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in current batch, waiting for up-to-date`,\n )\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, will resolve on up-to-date\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 (\n params: InsertMutationFnParams<\n any,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => {\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 (\n params: UpdateMutationFnParams<\n any,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => {\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 (\n params: DeleteMutationFnParams<\n any,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => {\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 },\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 batchCommitted: Store<boolean>\n removePendingMatches: (matchIds: Array<string>) => void\n resolveMatchedPendingMatches: () => void\n collectionId?: string\n testHooks?: ElectricTestHooks\n },\n): SyncConfig<T> {\n const {\n seenTxids,\n seenSnapshots,\n syncMode,\n pendingMatches,\n currentBatchMessages,\n batchCommitted,\n removePendingMatches,\n resolveMatchedPendingMatches,\n collectionId,\n testHooks,\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 // Wrap markReady to wait for test hook in progressive mode\n let progressiveReadyGate: Promise<void> | null = null\n const wrappedMarkReady = (isBuffering: boolean) => {\n // Only create gate if we're in buffering phase (first up-to-date)\n if (\n isBuffering &&\n syncMode === `progressive` &&\n testHooks?.beforeMarkingReady\n ) {\n // Create a new gate promise for this sync cycle\n progressiveReadyGate = testHooks.beforeMarkingReady()\n progressiveReadyGate.then(() => {\n markReady()\n })\n } else {\n // No hook, not buffering, or already past first up-to-date\n markReady()\n }\n }\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 // Progressive mode state\n // Helper to determine if we're buffering the initial sync\n const isBufferingInitialSync = () =>\n syncMode === `progressive` && !hasReceivedUpToDate\n const bufferedMessages: Array<Message<T>> = [] // Buffer change messages during initial sync\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 = createLoadSubsetDedupe({\n stream,\n syncMode,\n isBufferingInitialSync,\n begin,\n write,\n commit,\n collectionId,\n })\n\n unsubscribeStream = stream.subscribe((messages: Array<Message<T>>) => {\n let hasUpToDate = false\n let hasSnapshotEnd = false\n\n // Clear the current batch buffer at the START of processing a new batch\n // This preserves messages from the previous batch until new ones arrive,\n // allowing awaitMatch to find messages even if called after up-to-date\n currentBatchMessages.setState(() => [])\n batchCommitted.setState(() => 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 // Skip during buffered initial sync in progressive mode (txids will be extracted during atomic swap)\n if (hasTxids(message) && !isBufferingInitialSync()) {\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 // In buffered initial sync of progressive mode, buffer messages instead of writing\n if (isBufferingInitialSync()) {\n bufferedMessages.push(message)\n } else {\n // Normal processing: write changes immediately\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 }\n } else if (isSnapshotEndMessage(message)) {\n // Skip snapshot-end tracking during buffered initial sync (will be extracted during atomic swap)\n if (!isBufferingInitialSync()) {\n newSnapshots.push(parseSnapshotMessage(message))\n }\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 (isBufferingInitialSync will reflect this)\n bufferedMessages.length = 0 // Clear buffered messages\n }\n }\n\n if (hasUpToDate || hasSnapshotEnd) {\n // PROGRESSIVE MODE: Atomic swap on first up-to-date\n if (isBufferingInitialSync() && hasUpToDate) {\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Performing atomic swap with ${bufferedMessages.length} buffered messages`,\n )\n\n // Start atomic swap transaction\n begin()\n\n // Truncate to clear all snapshot data\n truncate()\n\n // Apply all buffered change messages and extract txids/snapshots\n for (const bufferedMsg of bufferedMessages) {\n if (isChangeMessage(bufferedMsg)) {\n write({\n type: bufferedMsg.headers.operation,\n value: bufferedMsg.value,\n metadata: {\n ...bufferedMsg.headers,\n },\n })\n\n // Extract txids from buffered messages (will be committed to store after transaction)\n if (hasTxids(bufferedMsg)) {\n bufferedMsg.headers.txids?.forEach((txid) =>\n newTxids.add(txid),\n )\n }\n } else if (isSnapshotEndMessage(bufferedMsg)) {\n // Extract snapshots from buffered messages (will be committed to store after transaction)\n newSnapshots.push(parseSnapshotMessage(bufferedMsg))\n }\n }\n\n // Commit the atomic swap\n commit()\n\n // Exit buffering phase by marking that we've received up-to-date\n // isBufferingInitialSync() will now return false\n bufferedMessages.length = 0\n\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Atomic swap complete, now in normal sync mode`,\n )\n } else {\n // Normal mode or on-demand: commit transaction if one was started\n // In eager mode, only commit on snapshot-end if we've already received\n // the first up-to-date, because the snapshot-end in the log could be from\n // a significant period before the stream is actually up to date\n const shouldCommit =\n hasUpToDate || syncMode === `on-demand` || hasReceivedUpToDate\n\n if (transactionStarted && shouldCommit) {\n commit()\n transactionStarted = false\n }\n }\n\n if (hasUpToDate || (hasSnapshotEnd && syncMode === `on-demand`)) {\n // Mark the collection as ready now that sync is up to date\n wrappedMarkReady(isBufferingInitialSync())\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 or snapshot-end in on-demand mode\n // Set batchCommitted BEFORE resolving to avoid timing window where late awaitMatch\n // calls could register as \"matched\" after resolver pass already ran\n if (hasUpToDate || (hasSnapshotEnd && syncMode === `on-demand`)) {\n batchCommitted.setState(() => true)\n }\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":["isControlMessage","compileSQL","and","DeduplicatedLoadSubset","Store","ExpectedNumberInAwaitTxIdError","isVisibleInSnapshot","TimeoutWaitingForTxIdError","TimeoutWaitingForMatchError","StreamAbortedError","ShapeStream","isChangeMessage"],"mappings":"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"}
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -2,10 +2,19 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const electric = require("./electric.cjs");
|
|
4
4
|
const errors = require("./errors.cjs");
|
|
5
|
+
const client = require("@electric-sql/client");
|
|
5
6
|
exports.electricCollectionOptions = electric.electricCollectionOptions;
|
|
6
7
|
exports.ElectricDBCollectionError = errors.ElectricDBCollectionError;
|
|
7
8
|
exports.ExpectedNumberInAwaitTxIdError = errors.ExpectedNumberInAwaitTxIdError;
|
|
8
9
|
exports.StreamAbortedError = errors.StreamAbortedError;
|
|
9
10
|
exports.TimeoutWaitingForMatchError = errors.TimeoutWaitingForMatchError;
|
|
10
11
|
exports.TimeoutWaitingForTxIdError = errors.TimeoutWaitingForTxIdError;
|
|
12
|
+
Object.defineProperty(exports, "isChangeMessage", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: () => client.isChangeMessage
|
|
15
|
+
});
|
|
16
|
+
Object.defineProperty(exports, "isControlMessage", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
get: () => client.isControlMessage
|
|
19
|
+
});
|
|
11
20
|
//# sourceMappingURL=index.cjs.map
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;"}
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { electricCollectionOptions, type ElectricCollectionConfig, type ElectricCollectionUtils, type Txid, type AwaitTxIdFn, } from './electric.cjs';
|
|
1
|
+
export { electricCollectionOptions, isChangeMessage, isControlMessage, type ElectricCollectionConfig, type ElectricCollectionUtils, type Txid, type AwaitTxIdFn, } from './electric.cjs';
|
|
2
2
|
export * from './errors.cjs';
|
|
@@ -73,9 +73,22 @@ function compileOrderByClause(clause, params) {
|
|
|
73
73
|
}
|
|
74
74
|
return sql;
|
|
75
75
|
}
|
|
76
|
+
function isNullValue(exp) {
|
|
77
|
+
return exp.type === `val` && (exp.value === null || exp.value === void 0);
|
|
78
|
+
}
|
|
76
79
|
function compileFunction(exp, params = []) {
|
|
77
80
|
const { name, args } = exp;
|
|
78
81
|
const opName = getOpName(name);
|
|
82
|
+
if (isComparisonOp(name)) {
|
|
83
|
+
const nullArgIndex = args.findIndex(
|
|
84
|
+
(arg) => isNullValue(arg)
|
|
85
|
+
);
|
|
86
|
+
if (nullArgIndex !== -1) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Cannot use null/undefined value with '${name}' operator. Comparisons with null always evaluate to UNKNOWN in SQL. Use isNull() or isUndefined() to check for null values, or filter out null values before building the query.`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
79
92
|
const compiledArgs = args.map(
|
|
80
93
|
(arg) => compileBasicExpression(arg, params)
|
|
81
94
|
);
|
|
@@ -107,7 +120,7 @@ function compileFunction(exp, params = []) {
|
|
|
107
120
|
throw new Error(`Binary operator ${name} expects 2 arguments`);
|
|
108
121
|
}
|
|
109
122
|
const [lhs, rhs] = compiledArgs;
|
|
110
|
-
if (
|
|
123
|
+
if (isBooleanComparisonOp(name)) {
|
|
111
124
|
const lhsArg = args[0];
|
|
112
125
|
const rhsArg = args[1];
|
|
113
126
|
if (rhsArg && rhsArg.type === `val` && typeof rhsArg.value === `boolean`) {
|
|
@@ -202,6 +215,10 @@ function isBinaryOp(name) {
|
|
|
202
215
|
return binaryOps.includes(name);
|
|
203
216
|
}
|
|
204
217
|
function isComparisonOp(name) {
|
|
218
|
+
const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`];
|
|
219
|
+
return comparisonOps.includes(name);
|
|
220
|
+
}
|
|
221
|
+
function isBooleanComparisonOp(name) {
|
|
205
222
|
return [`gt`, `gte`, `lt`, `lte`].includes(name);
|
|
206
223
|
}
|
|
207
224
|
function getOpName(name) {
|
|
@@ -1 +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\n // Special case for comparison operators with boolean values\n // PostgreSQL doesn't support < > <= >= on booleans\n // Transform to equivalent equality checks or constant expressions\n if (isComparisonOp(name)) {\n const lhsArg = args[0]\n const rhsArg = args[1]\n\n // Check if RHS is a boolean literal value\n if (\n rhsArg &&\n rhsArg.type === `val` &&\n typeof rhsArg.value === `boolean`\n ) {\n const boolValue = rhsArg.value\n // Remove the boolean param we just added since we'll transform the expression\n params.pop()\n\n // Transform based on operator and boolean value\n // Boolean ordering: false < true\n if (name === `lt`) {\n if (boolValue === true) {\n // lt(col, true) → col = false (only false is less than true)\n params.push(false)\n return `${lhs} = $${params.length}`\n } else {\n // lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `gt`) {\n if (boolValue === false) {\n // gt(col, false) → col = true (only true is greater than false)\n params.push(true)\n return `${lhs} = $${params.length}`\n } else {\n // gt(col, true) → nothing is greater than true\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === true) {\n // lte(col, true) → everything is ≤ true\n return `true`\n } else {\n // lte(col, false) → col = false\n params.push(false)\n return `${lhs} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === false) {\n // gte(col, false) → everything is ≥ false\n return `true`\n } else {\n // gte(col, true) → col = true\n params.push(true)\n return `${lhs} = $${params.length}`\n }\n }\n }\n\n // Check if LHS is a boolean literal value (less common but handle it)\n if (\n lhsArg &&\n lhsArg.type === `val` &&\n typeof lhsArg.value === `boolean`\n ) {\n const boolValue = lhsArg.value\n // Remove params for this expression and rebuild\n params.pop() // remove RHS\n params.pop() // remove LHS (boolean)\n\n // Recompile RHS to get fresh param\n const rhsCompiled = compileBasicExpression(rhsArg!, params)\n\n // Transform: flip the comparison (val op col → col flipped_op val)\n if (name === `lt`) {\n // lt(true, col) → gt(col, true) → col > true → nothing is greater than true\n if (boolValue === true) {\n return `false`\n } else {\n // lt(false, col) → gt(col, false) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gt`) {\n // gt(true, col) → lt(col, true) → col = false\n if (boolValue === true) {\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n } else {\n // gt(false, col) → lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === false) {\n // lte(false, col) → gte(col, false) → everything\n return `true`\n } else {\n // lte(true, col) → gte(col, true) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === true) {\n // gte(true, col) → lte(col, true) → everything\n return `true`\n } else {\n // gte(false, col) → lte(col, false) → col = false\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n }\n }\n }\n }\n\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 = [\n `eq`,\n `gt`,\n `gte`,\n `lt`,\n `lte`,\n `and`,\n `or`,\n `in`,\n `like`,\n `ilike`,\n ]\n return binaryOps.includes(name)\n}\n\n/**\n * Checks if the operator is a comparison operator (excluding eq)\n * These operators don't work on booleans in PostgreSQL without casting\n */\nfunction isComparisonOp(name: string): boolean {\n return [`gt`, `gte`, `lt`, `lte`].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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"}
|
|
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\n/**\n * Check if a BasicExpression represents a null/undefined value\n */\nfunction isNullValue(exp: IR.BasicExpression<unknown>): boolean {\n return exp.type === `val` && (exp.value === null || exp.value === undefined)\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 // Handle comparison operators with null/undefined values\n // These would create invalid queries with missing params (e.g., \"col = $1\" with empty params)\n // In SQL, all comparisons with NULL return UNKNOWN, so these are almost always mistakes\n if (isComparisonOp(name)) {\n const nullArgIndex = args.findIndex((arg: IR.BasicExpression) =>\n isNullValue(arg),\n )\n\n if (nullArgIndex !== -1) {\n // All comparison operators (including eq) throw an error for null values\n // Users should use isNull() or isUndefined() to check for null values\n throw new Error(\n `Cannot use null/undefined value with '${name}' operator. ` +\n `Comparisons with null always evaluate to UNKNOWN in SQL. ` +\n `Use isNull() or isUndefined() to check for null values, ` +\n `or filter out null values before building the query.`,\n )\n }\n }\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\n // Special case for comparison operators with boolean values\n // PostgreSQL doesn't support < > <= >= on booleans\n // Transform to equivalent equality checks or constant expressions\n if (isBooleanComparisonOp(name)) {\n const lhsArg = args[0]\n const rhsArg = args[1]\n\n // Check if RHS is a boolean literal value\n if (\n rhsArg &&\n rhsArg.type === `val` &&\n typeof rhsArg.value === `boolean`\n ) {\n const boolValue = rhsArg.value\n // Remove the boolean param we just added since we'll transform the expression\n params.pop()\n\n // Transform based on operator and boolean value\n // Boolean ordering: false < true\n if (name === `lt`) {\n if (boolValue === true) {\n // lt(col, true) → col = false (only false is less than true)\n params.push(false)\n return `${lhs} = $${params.length}`\n } else {\n // lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `gt`) {\n if (boolValue === false) {\n // gt(col, false) → col = true (only true is greater than false)\n params.push(true)\n return `${lhs} = $${params.length}`\n } else {\n // gt(col, true) → nothing is greater than true\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === true) {\n // lte(col, true) → everything is ≤ true\n return `true`\n } else {\n // lte(col, false) → col = false\n params.push(false)\n return `${lhs} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === false) {\n // gte(col, false) → everything is ≥ false\n return `true`\n } else {\n // gte(col, true) → col = true\n params.push(true)\n return `${lhs} = $${params.length}`\n }\n }\n }\n\n // Check if LHS is a boolean literal value (less common but handle it)\n if (\n lhsArg &&\n lhsArg.type === `val` &&\n typeof lhsArg.value === `boolean`\n ) {\n const boolValue = lhsArg.value\n // Remove params for this expression and rebuild\n params.pop() // remove RHS\n params.pop() // remove LHS (boolean)\n\n // Recompile RHS to get fresh param\n const rhsCompiled = compileBasicExpression(rhsArg!, params)\n\n // Transform: flip the comparison (val op col → col flipped_op val)\n if (name === `lt`) {\n // lt(true, col) → gt(col, true) → col > true → nothing is greater than true\n if (boolValue === true) {\n return `false`\n } else {\n // lt(false, col) → gt(col, false) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gt`) {\n // gt(true, col) → lt(col, true) → col = false\n if (boolValue === true) {\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n } else {\n // gt(false, col) → lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === false) {\n // lte(false, col) → gte(col, false) → everything\n return `true`\n } else {\n // lte(true, col) → gte(col, true) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === true) {\n // gte(true, col) → lte(col, true) → everything\n return `true`\n } else {\n // gte(false, col) → lte(col, false) → col = false\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n }\n }\n }\n }\n\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 = [\n `eq`,\n `gt`,\n `gte`,\n `lt`,\n `lte`,\n `and`,\n `or`,\n `in`,\n `like`,\n `ilike`,\n ]\n return binaryOps.includes(name)\n}\n\n/**\n * Check if operator is a comparison operator that takes two values\n * These operators cannot accept null/undefined as values\n * (null comparisons in SQL always evaluate to UNKNOWN)\n */\nfunction isComparisonOp(name: string): boolean {\n const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`]\n return comparisonOps.includes(name)\n}\n\n/**\n * Checks if the operator is a comparison operator (excluding eq)\n * These operators don't work on booleans in PostgreSQL without casting\n */\nfunction isBooleanComparisonOp(name: string): boolean {\n return [`gt`, `gte`, `lt`, `lte`].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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"}
|
package/dist/esm/electric.js
CHANGED
|
@@ -114,6 +114,7 @@ function electricCollectionOptions(config) {
|
|
|
114
114
|
const finalSyncMode = internalSyncMode === `progressive` ? `on-demand` : internalSyncMode;
|
|
115
115
|
const pendingMatches = new Store(/* @__PURE__ */ new Map());
|
|
116
116
|
const currentBatchMessages = new Store([]);
|
|
117
|
+
const batchCommitted = new Store(false);
|
|
117
118
|
const removePendingMatches = (matchIds) => {
|
|
118
119
|
if (matchIds.length > 0) {
|
|
119
120
|
pendingMatches.setState((current) => {
|
|
@@ -144,6 +145,7 @@ function electricCollectionOptions(config) {
|
|
|
144
145
|
syncMode: internalSyncMode,
|
|
145
146
|
pendingMatches,
|
|
146
147
|
currentBatchMessages,
|
|
148
|
+
batchCommitted,
|
|
147
149
|
removePendingMatches,
|
|
148
150
|
resolveMatchedPendingMatches,
|
|
149
151
|
collectionId: config.id,
|
|
@@ -236,6 +238,14 @@ function electricCollectionOptions(config) {
|
|
|
236
238
|
};
|
|
237
239
|
for (const message of currentBatchMessages.state) {
|
|
238
240
|
if (matchFn(message)) {
|
|
241
|
+
if (batchCommitted.state) {
|
|
242
|
+
debug(
|
|
243
|
+
`${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in committed batch, resolving immediately`
|
|
244
|
+
);
|
|
245
|
+
clearTimeout(timeoutId);
|
|
246
|
+
resolve(true);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
239
249
|
debug(
|
|
240
250
|
`${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in current batch, waiting for up-to-date`
|
|
241
251
|
);
|
|
@@ -247,7 +257,7 @@ function electricCollectionOptions(config) {
|
|
|
247
257
|
reject,
|
|
248
258
|
timeoutId,
|
|
249
259
|
matched: true
|
|
250
|
-
// Already matched
|
|
260
|
+
// Already matched, will resolve on up-to-date
|
|
251
261
|
});
|
|
252
262
|
return newMatches;
|
|
253
263
|
});
|
|
@@ -319,6 +329,7 @@ function createElectricSync(shapeOptions, options) {
|
|
|
319
329
|
syncMode,
|
|
320
330
|
pendingMatches,
|
|
321
331
|
currentBatchMessages,
|
|
332
|
+
batchCommitted,
|
|
322
333
|
removePendingMatches,
|
|
323
334
|
resolveMatchedPendingMatches,
|
|
324
335
|
collectionId,
|
|
@@ -412,6 +423,8 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
412
423
|
unsubscribeStream = stream.subscribe((messages) => {
|
|
413
424
|
let hasUpToDate = false;
|
|
414
425
|
let hasSnapshotEnd = false;
|
|
426
|
+
currentBatchMessages.setState(() => []);
|
|
427
|
+
batchCommitted.setState(() => false);
|
|
415
428
|
for (const message of messages) {
|
|
416
429
|
if (isChangeMessage(message)) {
|
|
417
430
|
currentBatchMessages.setState((currentBuffer) => {
|
|
@@ -522,7 +535,6 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
522
535
|
transactionStarted = false;
|
|
523
536
|
}
|
|
524
537
|
}
|
|
525
|
-
currentBatchMessages.setState(() => []);
|
|
526
538
|
if (hasUpToDate || hasSnapshotEnd && syncMode === `on-demand`) {
|
|
527
539
|
wrappedMarkReady(isBufferingInitialSync());
|
|
528
540
|
}
|
|
@@ -552,6 +564,9 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
552
564
|
newSnapshots.length = 0;
|
|
553
565
|
return seen;
|
|
554
566
|
});
|
|
567
|
+
if (hasUpToDate || hasSnapshotEnd && syncMode === `on-demand`) {
|
|
568
|
+
batchCommitted.setState(() => true);
|
|
569
|
+
}
|
|
555
570
|
resolveMatchedPendingMatches();
|
|
556
571
|
}
|
|
557
572
|
});
|
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 { DeduplicatedLoadSubset, and } 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 * Symbol for internal test hooks (hidden from public API)\n */\nexport const ELECTRIC_TEST_HOOKS = Symbol(`electricTestHooks`)\n\n/**\n * Internal test hooks interface (for testing only)\n */\nexport interface ElectricTestHooks {\n /**\n * Called before marking collection ready after first up-to-date in progressive mode\n * Allows tests to pause and validate snapshot phase before atomic swap completes\n */\n beforeMarkingReady?: () => Promise<void>\n}\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<\n T,\n string | number,\n TSchema,\n ElectricCollectionUtils<T>,\n any\n >,\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 * Internal test hooks (for testing only)\n * Hidden via Symbol to prevent accidental usage in production\n */\n [ELECTRIC_TEST_HOOKS]?: ElectricTestHooks\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?: (\n params: InsertMutationFnParams<\n T,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => 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?: (\n params: UpdateMutationFnParams<\n T,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => 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?: (\n params: DeleteMutationFnParams<\n T,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => 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 * Creates a deduplicated loadSubset handler for progressive/on-demand modes\n * Returns null for eager mode, or a DeduplicatedLoadSubset instance for other modes.\n * Handles fetching snapshots in progressive mode during buffering phase,\n * and requesting snapshots in on-demand mode.\n *\n * When cursor expressions are provided (whereFrom/whereCurrent), makes two\n * requestSnapshot calls:\n * - One for whereFrom (rows > cursor) with limit\n * - One for whereCurrent (rows = cursor, for tie-breaking) without limit\n */\nfunction createLoadSubsetDedupe<T extends Row<unknown>>({\n stream,\n syncMode,\n isBufferingInitialSync,\n begin,\n write,\n commit,\n collectionId,\n}: {\n stream: ShapeStream<T>\n syncMode: ElectricSyncMode\n isBufferingInitialSync: () => boolean\n begin: () => void\n write: (mutation: {\n type: `insert` | `update` | `delete`\n value: T\n metadata: Record<string, unknown>\n }) => void\n commit: () => void\n collectionId?: string\n}): DeduplicatedLoadSubset | null {\n // Eager mode doesn't need subset loading\n if (syncMode === `eager`) {\n return null\n }\n\n const loadSubset = async (opts: LoadSubsetOptions) => {\n // In progressive mode, use fetchSnapshot during snapshot phase\n if (isBufferingInitialSync()) {\n // Progressive mode snapshot phase: fetch and apply immediately\n const snapshotParams = compileSQL<T>(opts)\n try {\n const { data: rows } = await stream.fetchSnapshot(snapshotParams)\n\n // Check again if we're still buffering - we might have received up-to-date\n // and completed the atomic swap while waiting for the snapshot\n if (!isBufferingInitialSync()) {\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Ignoring snapshot - sync completed while fetching`,\n )\n return\n }\n\n // Apply snapshot data in a sync transaction (only if we have data)\n if (rows.length > 0) {\n begin()\n for (const row of rows) {\n write({\n type: `insert`,\n value: row.value,\n metadata: {\n ...row.headers,\n },\n })\n }\n commit()\n\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Applied snapshot with ${rows.length} rows`,\n )\n }\n } catch (error) {\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Error fetching snapshot: %o`,\n error,\n )\n throw error\n }\n } else if (syncMode === `progressive`) {\n // Progressive mode after full sync complete: no need to load more\n return\n } else {\n // On-demand mode: use requestSnapshot\n // When cursor is provided, make two calls:\n // 1. whereCurrent (all ties, no limit)\n // 2. whereFrom (rows > cursor, with limit)\n const { cursor, where, orderBy, limit } = opts\n\n if (cursor) {\n // Make parallel requests for cursor-based pagination\n const promises: Array<Promise<unknown>> = []\n\n // Request 1: All rows matching whereCurrent (ties at boundary, no limit)\n // Combine main where with cursor.whereCurrent\n const whereCurrentOpts: LoadSubsetOptions = {\n where: where ? and(where, cursor.whereCurrent) : cursor.whereCurrent,\n orderBy,\n // No limit - get all ties\n }\n const whereCurrentParams = compileSQL<T>(whereCurrentOpts)\n promises.push(stream.requestSnapshot(whereCurrentParams))\n\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Requesting cursor.whereCurrent snapshot (all ties)`,\n )\n\n // Request 2: Rows matching whereFrom (rows > cursor, with limit)\n // Combine main where with cursor.whereFrom\n const whereFromOpts: LoadSubsetOptions = {\n where: where ? and(where, cursor.whereFrom) : cursor.whereFrom,\n orderBy,\n limit,\n }\n const whereFromParams = compileSQL<T>(whereFromOpts)\n promises.push(stream.requestSnapshot(whereFromParams))\n\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Requesting cursor.whereFrom snapshot (with limit ${limit})`,\n )\n\n // Wait for both requests to complete\n await Promise.all(promises)\n } else {\n // No cursor - standard single request\n const snapshotParams = compileSQL<T>(opts)\n await stream.requestSnapshot(snapshotParams)\n }\n }\n }\n\n return new DeduplicatedLoadSubset({ loadSubset })\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<\n 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): Omit<CollectionConfig<InferSchemaOutput<T>, string | number, T>, `utils`> & {\n id?: string\n utils: ElectricCollectionUtils<InferSchemaOutput<T>>\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): Omit<CollectionConfig<T, string | number>, `utils`> & {\n id?: string\n utils: ElectricCollectionUtils<T>\n schema?: never // no schema in the result\n}\n\nexport function electricCollectionOptions<T extends Row<unknown>>(\n config: ElectricCollectionConfig<T, any>,\n): Omit<\n CollectionConfig<T, string | number, any, ElectricCollectionUtils<T>>,\n `utils`\n> & {\n id?: string\n utils: ElectricCollectionUtils<T>\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<T>(config.shapeOptions, {\n seenTxids,\n seenSnapshots,\n syncMode: internalSyncMode,\n pendingMatches,\n currentBatchMessages,\n removePendingMatches,\n resolveMatchedPendingMatches,\n collectionId: config.id,\n testHooks: config[ELECTRIC_TEST_HOOKS],\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 (\n params: InsertMutationFnParams<\n any,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => {\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 (\n params: UpdateMutationFnParams<\n any,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => {\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 (\n params: DeleteMutationFnParams<\n any,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => {\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 },\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 testHooks?: ElectricTestHooks\n },\n): SyncConfig<T> {\n const {\n seenTxids,\n seenSnapshots,\n syncMode,\n pendingMatches,\n currentBatchMessages,\n removePendingMatches,\n resolveMatchedPendingMatches,\n collectionId,\n testHooks,\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 // Wrap markReady to wait for test hook in progressive mode\n let progressiveReadyGate: Promise<void> | null = null\n const wrappedMarkReady = (isBuffering: boolean) => {\n // Only create gate if we're in buffering phase (first up-to-date)\n if (\n isBuffering &&\n syncMode === `progressive` &&\n testHooks?.beforeMarkingReady\n ) {\n // Create a new gate promise for this sync cycle\n progressiveReadyGate = testHooks.beforeMarkingReady()\n progressiveReadyGate.then(() => {\n markReady()\n })\n } else {\n // No hook, not buffering, or already past first up-to-date\n markReady()\n }\n }\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 // Progressive mode state\n // Helper to determine if we're buffering the initial sync\n const isBufferingInitialSync = () =>\n syncMode === `progressive` && !hasReceivedUpToDate\n const bufferedMessages: Array<Message<T>> = [] // Buffer change messages during initial sync\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 = createLoadSubsetDedupe({\n stream,\n syncMode,\n isBufferingInitialSync,\n begin,\n write,\n commit,\n collectionId,\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 // Skip during buffered initial sync in progressive mode (txids will be extracted during atomic swap)\n if (hasTxids(message) && !isBufferingInitialSync()) {\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 // In buffered initial sync of progressive mode, buffer messages instead of writing\n if (isBufferingInitialSync()) {\n bufferedMessages.push(message)\n } else {\n // Normal processing: write changes immediately\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 }\n } else if (isSnapshotEndMessage(message)) {\n // Skip snapshot-end tracking during buffered initial sync (will be extracted during atomic swap)\n if (!isBufferingInitialSync()) {\n newSnapshots.push(parseSnapshotMessage(message))\n }\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 (isBufferingInitialSync will reflect this)\n bufferedMessages.length = 0 // Clear buffered messages\n }\n }\n\n if (hasUpToDate || hasSnapshotEnd) {\n // PROGRESSIVE MODE: Atomic swap on first up-to-date\n if (isBufferingInitialSync() && hasUpToDate) {\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Performing atomic swap with ${bufferedMessages.length} buffered messages`,\n )\n\n // Start atomic swap transaction\n begin()\n\n // Truncate to clear all snapshot data\n truncate()\n\n // Apply all buffered change messages and extract txids/snapshots\n for (const bufferedMsg of bufferedMessages) {\n if (isChangeMessage(bufferedMsg)) {\n write({\n type: bufferedMsg.headers.operation,\n value: bufferedMsg.value,\n metadata: {\n ...bufferedMsg.headers,\n },\n })\n\n // Extract txids from buffered messages (will be committed to store after transaction)\n if (hasTxids(bufferedMsg)) {\n bufferedMsg.headers.txids?.forEach((txid) =>\n newTxids.add(txid),\n )\n }\n } else if (isSnapshotEndMessage(bufferedMsg)) {\n // Extract snapshots from buffered messages (will be committed to store after transaction)\n newSnapshots.push(parseSnapshotMessage(bufferedMsg))\n }\n }\n\n // Commit the atomic swap\n commit()\n\n // Exit buffering phase by marking that we've received up-to-date\n // isBufferingInitialSync() will now return false\n bufferedMessages.length = 0\n\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Atomic swap complete, now in normal sync mode`,\n )\n } else {\n // Normal mode or on-demand: commit transaction if one was started\n // In eager mode, only commit on snapshot-end if we've already received\n // the first up-to-date, because the snapshot-end in the log could be from\n // a significant period before the stream is actually up to date\n const shouldCommit =\n hasUpToDate || syncMode === `on-demand` || hasReceivedUpToDate\n\n if (transactionStarted && shouldCommit) {\n commit()\n transactionStarted = false\n }\n }\n\n // Clear the current batch buffer since we're now up-to-date\n currentBatchMessages.setState(() => [])\n\n if (hasUpToDate || (hasSnapshotEnd && syncMode === `on-demand`)) {\n // Mark the collection as ready now that sync is up to date\n wrappedMarkReady(isBufferingInitialSync())\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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"}
|
|
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, and } 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 * Symbol for internal test hooks (hidden from public API)\n */\nexport const ELECTRIC_TEST_HOOKS = Symbol(`electricTestHooks`)\n\n/**\n * Internal test hooks interface (for testing only)\n */\nexport interface ElectricTestHooks {\n /**\n * Called before marking collection ready after first up-to-date in progressive mode\n * Allows tests to pause and validate snapshot phase before atomic swap completes\n */\n beforeMarkingReady?: () => Promise<void>\n}\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<\n T,\n string | number,\n TSchema,\n ElectricCollectionUtils<T>,\n any\n >,\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 * Internal test hooks (for testing only)\n * Hidden via Symbol to prevent accidental usage in production\n */\n [ELECTRIC_TEST_HOOKS]?: ElectricTestHooks\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?: (\n params: InsertMutationFnParams<\n T,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => 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?: (\n params: UpdateMutationFnParams<\n T,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => 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?: (\n params: DeleteMutationFnParams<\n T,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => 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 * Creates a deduplicated loadSubset handler for progressive/on-demand modes\n * Returns null for eager mode, or a DeduplicatedLoadSubset instance for other modes.\n * Handles fetching snapshots in progressive mode during buffering phase,\n * and requesting snapshots in on-demand mode.\n *\n * When cursor expressions are provided (whereFrom/whereCurrent), makes two\n * requestSnapshot calls:\n * - One for whereFrom (rows > cursor) with limit\n * - One for whereCurrent (rows = cursor, for tie-breaking) without limit\n */\nfunction createLoadSubsetDedupe<T extends Row<unknown>>({\n stream,\n syncMode,\n isBufferingInitialSync,\n begin,\n write,\n commit,\n collectionId,\n}: {\n stream: ShapeStream<T>\n syncMode: ElectricSyncMode\n isBufferingInitialSync: () => boolean\n begin: () => void\n write: (mutation: {\n type: `insert` | `update` | `delete`\n value: T\n metadata: Record<string, unknown>\n }) => void\n commit: () => void\n collectionId?: string\n}): DeduplicatedLoadSubset | null {\n // Eager mode doesn't need subset loading\n if (syncMode === `eager`) {\n return null\n }\n\n const loadSubset = async (opts: LoadSubsetOptions) => {\n // In progressive mode, use fetchSnapshot during snapshot phase\n if (isBufferingInitialSync()) {\n // Progressive mode snapshot phase: fetch and apply immediately\n const snapshotParams = compileSQL<T>(opts)\n try {\n const { data: rows } = await stream.fetchSnapshot(snapshotParams)\n\n // Check again if we're still buffering - we might have received up-to-date\n // and completed the atomic swap while waiting for the snapshot\n if (!isBufferingInitialSync()) {\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Ignoring snapshot - sync completed while fetching`,\n )\n return\n }\n\n // Apply snapshot data in a sync transaction (only if we have data)\n if (rows.length > 0) {\n begin()\n for (const row of rows) {\n write({\n type: `insert`,\n value: row.value,\n metadata: {\n ...row.headers,\n },\n })\n }\n commit()\n\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Applied snapshot with ${rows.length} rows`,\n )\n }\n } catch (error) {\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Error fetching snapshot: %o`,\n error,\n )\n throw error\n }\n } else if (syncMode === `progressive`) {\n // Progressive mode after full sync complete: no need to load more\n return\n } else {\n // On-demand mode: use requestSnapshot\n // When cursor is provided, make two calls:\n // 1. whereCurrent (all ties, no limit)\n // 2. whereFrom (rows > cursor, with limit)\n const { cursor, where, orderBy, limit } = opts\n\n if (cursor) {\n // Make parallel requests for cursor-based pagination\n const promises: Array<Promise<unknown>> = []\n\n // Request 1: All rows matching whereCurrent (ties at boundary, no limit)\n // Combine main where with cursor.whereCurrent\n const whereCurrentOpts: LoadSubsetOptions = {\n where: where ? and(where, cursor.whereCurrent) : cursor.whereCurrent,\n orderBy,\n // No limit - get all ties\n }\n const whereCurrentParams = compileSQL<T>(whereCurrentOpts)\n promises.push(stream.requestSnapshot(whereCurrentParams))\n\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Requesting cursor.whereCurrent snapshot (all ties)`,\n )\n\n // Request 2: Rows matching whereFrom (rows > cursor, with limit)\n // Combine main where with cursor.whereFrom\n const whereFromOpts: LoadSubsetOptions = {\n where: where ? and(where, cursor.whereFrom) : cursor.whereFrom,\n orderBy,\n limit,\n }\n const whereFromParams = compileSQL<T>(whereFromOpts)\n promises.push(stream.requestSnapshot(whereFromParams))\n\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Requesting cursor.whereFrom snapshot (with limit ${limit})`,\n )\n\n // Wait for both requests to complete\n await Promise.all(promises)\n } else {\n // No cursor - standard single request\n const snapshotParams = compileSQL<T>(opts)\n await stream.requestSnapshot(snapshotParams)\n }\n }\n }\n\n return new DeduplicatedLoadSubset({ loadSubset })\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<\n 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): Omit<CollectionConfig<InferSchemaOutput<T>, string | number, T>, `utils`> & {\n id?: string\n utils: ElectricCollectionUtils<InferSchemaOutput<T>>\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): Omit<CollectionConfig<T, string | number>, `utils`> & {\n id?: string\n utils: ElectricCollectionUtils<T>\n schema?: never // no schema in the result\n}\n\nexport function electricCollectionOptions<T extends Row<unknown>>(\n config: ElectricCollectionConfig<T, any>,\n): Omit<\n CollectionConfig<T, string | number, any, ElectricCollectionUtils<T>>,\n `utils`\n> & {\n id?: string\n utils: ElectricCollectionUtils<T>\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 // Track whether the current batch has been committed (up-to-date received)\n // This allows awaitMatch to resolve immediately for messages from committed batches\n const batchCommitted = new Store<boolean>(false)\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<T>(config.shapeOptions, {\n seenTxids,\n seenSnapshots,\n syncMode: internalSyncMode,\n pendingMatches,\n currentBatchMessages,\n batchCommitted,\n removePendingMatches,\n resolveMatchedPendingMatches,\n collectionId: config.id,\n testHooks: config[ELECTRIC_TEST_HOOKS],\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 // If batch is committed (up-to-date already received), resolve immediately\n // just like awaitTxId does when it finds a txid in seenTxids\n if (batchCommitted.state) {\n debug(\n `${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in committed batch, resolving immediately`,\n )\n clearTimeout(timeoutId)\n resolve(true)\n return\n }\n\n // If batch is not yet committed, register match and wait for up-to-date\n debug(\n `${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in current batch, waiting for up-to-date`,\n )\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, will resolve on up-to-date\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 (\n params: InsertMutationFnParams<\n any,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => {\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 (\n params: UpdateMutationFnParams<\n any,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => {\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 (\n params: DeleteMutationFnParams<\n any,\n string | number,\n ElectricCollectionUtils<T>\n >,\n ) => {\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 },\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 batchCommitted: Store<boolean>\n removePendingMatches: (matchIds: Array<string>) => void\n resolveMatchedPendingMatches: () => void\n collectionId?: string\n testHooks?: ElectricTestHooks\n },\n): SyncConfig<T> {\n const {\n seenTxids,\n seenSnapshots,\n syncMode,\n pendingMatches,\n currentBatchMessages,\n batchCommitted,\n removePendingMatches,\n resolveMatchedPendingMatches,\n collectionId,\n testHooks,\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 // Wrap markReady to wait for test hook in progressive mode\n let progressiveReadyGate: Promise<void> | null = null\n const wrappedMarkReady = (isBuffering: boolean) => {\n // Only create gate if we're in buffering phase (first up-to-date)\n if (\n isBuffering &&\n syncMode === `progressive` &&\n testHooks?.beforeMarkingReady\n ) {\n // Create a new gate promise for this sync cycle\n progressiveReadyGate = testHooks.beforeMarkingReady()\n progressiveReadyGate.then(() => {\n markReady()\n })\n } else {\n // No hook, not buffering, or already past first up-to-date\n markReady()\n }\n }\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 // Progressive mode state\n // Helper to determine if we're buffering the initial sync\n const isBufferingInitialSync = () =>\n syncMode === `progressive` && !hasReceivedUpToDate\n const bufferedMessages: Array<Message<T>> = [] // Buffer change messages during initial sync\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 = createLoadSubsetDedupe({\n stream,\n syncMode,\n isBufferingInitialSync,\n begin,\n write,\n commit,\n collectionId,\n })\n\n unsubscribeStream = stream.subscribe((messages: Array<Message<T>>) => {\n let hasUpToDate = false\n let hasSnapshotEnd = false\n\n // Clear the current batch buffer at the START of processing a new batch\n // This preserves messages from the previous batch until new ones arrive,\n // allowing awaitMatch to find messages even if called after up-to-date\n currentBatchMessages.setState(() => [])\n batchCommitted.setState(() => 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 // Skip during buffered initial sync in progressive mode (txids will be extracted during atomic swap)\n if (hasTxids(message) && !isBufferingInitialSync()) {\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 // In buffered initial sync of progressive mode, buffer messages instead of writing\n if (isBufferingInitialSync()) {\n bufferedMessages.push(message)\n } else {\n // Normal processing: write changes immediately\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 }\n } else if (isSnapshotEndMessage(message)) {\n // Skip snapshot-end tracking during buffered initial sync (will be extracted during atomic swap)\n if (!isBufferingInitialSync()) {\n newSnapshots.push(parseSnapshotMessage(message))\n }\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 (isBufferingInitialSync will reflect this)\n bufferedMessages.length = 0 // Clear buffered messages\n }\n }\n\n if (hasUpToDate || hasSnapshotEnd) {\n // PROGRESSIVE MODE: Atomic swap on first up-to-date\n if (isBufferingInitialSync() && hasUpToDate) {\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Performing atomic swap with ${bufferedMessages.length} buffered messages`,\n )\n\n // Start atomic swap transaction\n begin()\n\n // Truncate to clear all snapshot data\n truncate()\n\n // Apply all buffered change messages and extract txids/snapshots\n for (const bufferedMsg of bufferedMessages) {\n if (isChangeMessage(bufferedMsg)) {\n write({\n type: bufferedMsg.headers.operation,\n value: bufferedMsg.value,\n metadata: {\n ...bufferedMsg.headers,\n },\n })\n\n // Extract txids from buffered messages (will be committed to store after transaction)\n if (hasTxids(bufferedMsg)) {\n bufferedMsg.headers.txids?.forEach((txid) =>\n newTxids.add(txid),\n )\n }\n } else if (isSnapshotEndMessage(bufferedMsg)) {\n // Extract snapshots from buffered messages (will be committed to store after transaction)\n newSnapshots.push(parseSnapshotMessage(bufferedMsg))\n }\n }\n\n // Commit the atomic swap\n commit()\n\n // Exit buffering phase by marking that we've received up-to-date\n // isBufferingInitialSync() will now return false\n bufferedMessages.length = 0\n\n debug(\n `${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Atomic swap complete, now in normal sync mode`,\n )\n } else {\n // Normal mode or on-demand: commit transaction if one was started\n // In eager mode, only commit on snapshot-end if we've already received\n // the first up-to-date, because the snapshot-end in the log could be from\n // a significant period before the stream is actually up to date\n const shouldCommit =\n hasUpToDate || syncMode === `on-demand` || hasReceivedUpToDate\n\n if (transactionStarted && shouldCommit) {\n commit()\n transactionStarted = false\n }\n }\n\n if (hasUpToDate || (hasSnapshotEnd && syncMode === `on-demand`)) {\n // Mark the collection as ready now that sync is up to date\n wrappedMarkReady(isBufferingInitialSync())\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 or snapshot-end in on-demand mode\n // Set batchCommitted BEFORE resolving to avoid timing window where late awaitMatch\n // calls could register as \"matched\" after resolver pass already ran\n if (hasUpToDate || (hasSnapshotEnd && syncMode === `on-demand`)) {\n batchCommitted.setState(() => true)\n }\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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"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { electricCollectionOptions, type ElectricCollectionConfig, type ElectricCollectionUtils, type Txid, type AwaitTxIdFn, } from './electric.js';
|
|
1
|
+
export { electricCollectionOptions, isChangeMessage, isControlMessage, type ElectricCollectionConfig, type ElectricCollectionUtils, type Txid, type AwaitTxIdFn, } from './electric.js';
|
|
2
2
|
export * from './errors.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { electricCollectionOptions } from "./electric.js";
|
|
2
2
|
import { ElectricDBCollectionError, ExpectedNumberInAwaitTxIdError, StreamAbortedError, TimeoutWaitingForMatchError, TimeoutWaitingForTxIdError } from "./errors.js";
|
|
3
|
+
import { isChangeMessage, isControlMessage } from "@electric-sql/client";
|
|
3
4
|
export {
|
|
4
5
|
ElectricDBCollectionError,
|
|
5
6
|
ExpectedNumberInAwaitTxIdError,
|
|
6
7
|
StreamAbortedError,
|
|
7
8
|
TimeoutWaitingForMatchError,
|
|
8
9
|
TimeoutWaitingForTxIdError,
|
|
9
|
-
electricCollectionOptions
|
|
10
|
+
electricCollectionOptions,
|
|
11
|
+
isChangeMessage,
|
|
12
|
+
isControlMessage
|
|
10
13
|
};
|
|
11
14
|
//# sourceMappingURL=index.js.map
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;"}
|
package/dist/esm/sql-compiler.js
CHANGED
|
@@ -71,9 +71,22 @@ function compileOrderByClause(clause, params) {
|
|
|
71
71
|
}
|
|
72
72
|
return sql;
|
|
73
73
|
}
|
|
74
|
+
function isNullValue(exp) {
|
|
75
|
+
return exp.type === `val` && (exp.value === null || exp.value === void 0);
|
|
76
|
+
}
|
|
74
77
|
function compileFunction(exp, params = []) {
|
|
75
78
|
const { name, args } = exp;
|
|
76
79
|
const opName = getOpName(name);
|
|
80
|
+
if (isComparisonOp(name)) {
|
|
81
|
+
const nullArgIndex = args.findIndex(
|
|
82
|
+
(arg) => isNullValue(arg)
|
|
83
|
+
);
|
|
84
|
+
if (nullArgIndex !== -1) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Cannot use null/undefined value with '${name}' operator. Comparisons with null always evaluate to UNKNOWN in SQL. Use isNull() or isUndefined() to check for null values, or filter out null values before building the query.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
77
90
|
const compiledArgs = args.map(
|
|
78
91
|
(arg) => compileBasicExpression(arg, params)
|
|
79
92
|
);
|
|
@@ -105,7 +118,7 @@ function compileFunction(exp, params = []) {
|
|
|
105
118
|
throw new Error(`Binary operator ${name} expects 2 arguments`);
|
|
106
119
|
}
|
|
107
120
|
const [lhs, rhs] = compiledArgs;
|
|
108
|
-
if (
|
|
121
|
+
if (isBooleanComparisonOp(name)) {
|
|
109
122
|
const lhsArg = args[0];
|
|
110
123
|
const rhsArg = args[1];
|
|
111
124
|
if (rhsArg && rhsArg.type === `val` && typeof rhsArg.value === `boolean`) {
|
|
@@ -200,6 +213,10 @@ function isBinaryOp(name) {
|
|
|
200
213
|
return binaryOps.includes(name);
|
|
201
214
|
}
|
|
202
215
|
function isComparisonOp(name) {
|
|
216
|
+
const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`];
|
|
217
|
+
return comparisonOps.includes(name);
|
|
218
|
+
}
|
|
219
|
+
function isBooleanComparisonOp(name) {
|
|
203
220
|
return [`gt`, `gte`, `lt`, `lte`].includes(name);
|
|
204
221
|
}
|
|
205
222
|
function getOpName(name) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql-compiler.js","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\n // Special case for comparison operators with boolean values\n // PostgreSQL doesn't support < > <= >= on booleans\n // Transform to equivalent equality checks or constant expressions\n if (isComparisonOp(name)) {\n const lhsArg = args[0]\n const rhsArg = args[1]\n\n // Check if RHS is a boolean literal value\n if (\n rhsArg &&\n rhsArg.type === `val` &&\n typeof rhsArg.value === `boolean`\n ) {\n const boolValue = rhsArg.value\n // Remove the boolean param we just added since we'll transform the expression\n params.pop()\n\n // Transform based on operator and boolean value\n // Boolean ordering: false < true\n if (name === `lt`) {\n if (boolValue === true) {\n // lt(col, true) → col = false (only false is less than true)\n params.push(false)\n return `${lhs} = $${params.length}`\n } else {\n // lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `gt`) {\n if (boolValue === false) {\n // gt(col, false) → col = true (only true is greater than false)\n params.push(true)\n return `${lhs} = $${params.length}`\n } else {\n // gt(col, true) → nothing is greater than true\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === true) {\n // lte(col, true) → everything is ≤ true\n return `true`\n } else {\n // lte(col, false) → col = false\n params.push(false)\n return `${lhs} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === false) {\n // gte(col, false) → everything is ≥ false\n return `true`\n } else {\n // gte(col, true) → col = true\n params.push(true)\n return `${lhs} = $${params.length}`\n }\n }\n }\n\n // Check if LHS is a boolean literal value (less common but handle it)\n if (\n lhsArg &&\n lhsArg.type === `val` &&\n typeof lhsArg.value === `boolean`\n ) {\n const boolValue = lhsArg.value\n // Remove params for this expression and rebuild\n params.pop() // remove RHS\n params.pop() // remove LHS (boolean)\n\n // Recompile RHS to get fresh param\n const rhsCompiled = compileBasicExpression(rhsArg!, params)\n\n // Transform: flip the comparison (val op col → col flipped_op val)\n if (name === `lt`) {\n // lt(true, col) → gt(col, true) → col > true → nothing is greater than true\n if (boolValue === true) {\n return `false`\n } else {\n // lt(false, col) → gt(col, false) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gt`) {\n // gt(true, col) → lt(col, true) → col = false\n if (boolValue === true) {\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n } else {\n // gt(false, col) → lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === false) {\n // lte(false, col) → gte(col, false) → everything\n return `true`\n } else {\n // lte(true, col) → gte(col, true) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === true) {\n // gte(true, col) → lte(col, true) → everything\n return `true`\n } else {\n // gte(false, col) → lte(col, false) → col = false\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n }\n }\n }\n }\n\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 = [\n `eq`,\n `gt`,\n `gte`,\n `lt`,\n `lte`,\n `and`,\n `or`,\n `in`,\n `like`,\n `ilike`,\n ]\n return binaryOps.includes(name)\n}\n\n/**\n * Checks if the operator is a comparison operator (excluding eq)\n * These operators don't work on booleans in PostgreSQL without casting\n */\nfunction isComparisonOp(name: string): boolean {\n return [`gt`, `gte`, `lt`, `lte`].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":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sql-compiler.js","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\n/**\n * Check if a BasicExpression represents a null/undefined value\n */\nfunction isNullValue(exp: IR.BasicExpression<unknown>): boolean {\n return exp.type === `val` && (exp.value === null || exp.value === undefined)\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 // Handle comparison operators with null/undefined values\n // These would create invalid queries with missing params (e.g., \"col = $1\" with empty params)\n // In SQL, all comparisons with NULL return UNKNOWN, so these are almost always mistakes\n if (isComparisonOp(name)) {\n const nullArgIndex = args.findIndex((arg: IR.BasicExpression) =>\n isNullValue(arg),\n )\n\n if (nullArgIndex !== -1) {\n // All comparison operators (including eq) throw an error for null values\n // Users should use isNull() or isUndefined() to check for null values\n throw new Error(\n `Cannot use null/undefined value with '${name}' operator. ` +\n `Comparisons with null always evaluate to UNKNOWN in SQL. ` +\n `Use isNull() or isUndefined() to check for null values, ` +\n `or filter out null values before building the query.`,\n )\n }\n }\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\n // Special case for comparison operators with boolean values\n // PostgreSQL doesn't support < > <= >= on booleans\n // Transform to equivalent equality checks or constant expressions\n if (isBooleanComparisonOp(name)) {\n const lhsArg = args[0]\n const rhsArg = args[1]\n\n // Check if RHS is a boolean literal value\n if (\n rhsArg &&\n rhsArg.type === `val` &&\n typeof rhsArg.value === `boolean`\n ) {\n const boolValue = rhsArg.value\n // Remove the boolean param we just added since we'll transform the expression\n params.pop()\n\n // Transform based on operator and boolean value\n // Boolean ordering: false < true\n if (name === `lt`) {\n if (boolValue === true) {\n // lt(col, true) → col = false (only false is less than true)\n params.push(false)\n return `${lhs} = $${params.length}`\n } else {\n // lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `gt`) {\n if (boolValue === false) {\n // gt(col, false) → col = true (only true is greater than false)\n params.push(true)\n return `${lhs} = $${params.length}`\n } else {\n // gt(col, true) → nothing is greater than true\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === true) {\n // lte(col, true) → everything is ≤ true\n return `true`\n } else {\n // lte(col, false) → col = false\n params.push(false)\n return `${lhs} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === false) {\n // gte(col, false) → everything is ≥ false\n return `true`\n } else {\n // gte(col, true) → col = true\n params.push(true)\n return `${lhs} = $${params.length}`\n }\n }\n }\n\n // Check if LHS is a boolean literal value (less common but handle it)\n if (\n lhsArg &&\n lhsArg.type === `val` &&\n typeof lhsArg.value === `boolean`\n ) {\n const boolValue = lhsArg.value\n // Remove params for this expression and rebuild\n params.pop() // remove RHS\n params.pop() // remove LHS (boolean)\n\n // Recompile RHS to get fresh param\n const rhsCompiled = compileBasicExpression(rhsArg!, params)\n\n // Transform: flip the comparison (val op col → col flipped_op val)\n if (name === `lt`) {\n // lt(true, col) → gt(col, true) → col > true → nothing is greater than true\n if (boolValue === true) {\n return `false`\n } else {\n // lt(false, col) → gt(col, false) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gt`) {\n // gt(true, col) → lt(col, true) → col = false\n if (boolValue === true) {\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n } else {\n // gt(false, col) → lt(col, false) → nothing is less than false\n return `false`\n }\n } else if (name === `lte`) {\n if (boolValue === false) {\n // lte(false, col) → gte(col, false) → everything\n return `true`\n } else {\n // lte(true, col) → gte(col, true) → col = true\n params.push(true)\n return `${rhsCompiled} = $${params.length}`\n }\n } else if (name === `gte`) {\n if (boolValue === true) {\n // gte(true, col) → lte(col, true) → everything\n return `true`\n } else {\n // gte(false, col) → lte(col, false) → col = false\n params.push(false)\n return `${rhsCompiled} = $${params.length}`\n }\n }\n }\n }\n\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 = [\n `eq`,\n `gt`,\n `gte`,\n `lt`,\n `lte`,\n `and`,\n `or`,\n `in`,\n `like`,\n `ilike`,\n ]\n return binaryOps.includes(name)\n}\n\n/**\n * Check if operator is a comparison operator that takes two values\n * These operators cannot accept null/undefined as values\n * (null comparisons in SQL always evaluate to UNKNOWN)\n */\nfunction isComparisonOp(name: string): boolean {\n const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`]\n return comparisonOps.includes(name)\n}\n\n/**\n * Checks if the operator is a comparison operator (excluding eq)\n * These operators don't work on booleans in PostgreSQL without casting\n */\nfunction isBooleanComparisonOp(name: string): boolean {\n return [`gt`, `gte`, `lt`, `lte`].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":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/electric-db-collection",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "ElectricSQL collection for TanStack DB",
|
|
5
5
|
"author": "Kyle Mathews",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"src"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@electric-sql/client": "1.2.0",
|
|
42
|
+
"@electric-sql/client": "^1.2.0",
|
|
43
43
|
"@standard-schema/spec": "^1.0.0",
|
|
44
44
|
"@tanstack/store": "^0.8.0",
|
|
45
45
|
"debug": "^4.4.3",
|
package/src/electric.ts
CHANGED
|
@@ -523,6 +523,10 @@ export function electricCollectionOptions<T extends Row<unknown>>(
|
|
|
523
523
|
// Buffer messages since last up-to-date to handle race conditions
|
|
524
524
|
const currentBatchMessages = new Store<Array<Message<any>>>([])
|
|
525
525
|
|
|
526
|
+
// Track whether the current batch has been committed (up-to-date received)
|
|
527
|
+
// This allows awaitMatch to resolve immediately for messages from committed batches
|
|
528
|
+
const batchCommitted = new Store<boolean>(false)
|
|
529
|
+
|
|
526
530
|
/**
|
|
527
531
|
* Helper function to remove multiple matches from the pendingMatches store
|
|
528
532
|
*/
|
|
@@ -560,6 +564,7 @@ export function electricCollectionOptions<T extends Row<unknown>>(
|
|
|
560
564
|
syncMode: internalSyncMode,
|
|
561
565
|
pendingMatches,
|
|
562
566
|
currentBatchMessages,
|
|
567
|
+
batchCommitted,
|
|
563
568
|
removePendingMatches,
|
|
564
569
|
resolveMatchedPendingMatches,
|
|
565
570
|
collectionId: config.id,
|
|
@@ -689,10 +694,21 @@ export function electricCollectionOptions<T extends Row<unknown>>(
|
|
|
689
694
|
// Check against current batch messages first to handle race conditions
|
|
690
695
|
for (const message of currentBatchMessages.state) {
|
|
691
696
|
if (matchFn(message)) {
|
|
697
|
+
// If batch is committed (up-to-date already received), resolve immediately
|
|
698
|
+
// just like awaitTxId does when it finds a txid in seenTxids
|
|
699
|
+
if (batchCommitted.state) {
|
|
700
|
+
debug(
|
|
701
|
+
`${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in committed batch, resolving immediately`,
|
|
702
|
+
)
|
|
703
|
+
clearTimeout(timeoutId)
|
|
704
|
+
resolve(true)
|
|
705
|
+
return
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// If batch is not yet committed, register match and wait for up-to-date
|
|
692
709
|
debug(
|
|
693
710
|
`${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in current batch, waiting for up-to-date`,
|
|
694
711
|
)
|
|
695
|
-
// Register match as already matched
|
|
696
712
|
pendingMatches.setState((current) => {
|
|
697
713
|
const newMatches = new Map(current)
|
|
698
714
|
newMatches.set(matchId, {
|
|
@@ -700,7 +716,7 @@ export function electricCollectionOptions<T extends Row<unknown>>(
|
|
|
700
716
|
resolve,
|
|
701
717
|
reject,
|
|
702
718
|
timeoutId,
|
|
703
|
-
matched: true, // Already matched
|
|
719
|
+
matched: true, // Already matched, will resolve on up-to-date
|
|
704
720
|
})
|
|
705
721
|
return newMatches
|
|
706
722
|
})
|
|
@@ -831,6 +847,7 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
831
847
|
>
|
|
832
848
|
>
|
|
833
849
|
currentBatchMessages: Store<Array<Message<T>>>
|
|
850
|
+
batchCommitted: Store<boolean>
|
|
834
851
|
removePendingMatches: (matchIds: Array<string>) => void
|
|
835
852
|
resolveMatchedPendingMatches: () => void
|
|
836
853
|
collectionId?: string
|
|
@@ -843,6 +860,7 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
843
860
|
syncMode,
|
|
844
861
|
pendingMatches,
|
|
845
862
|
currentBatchMessages,
|
|
863
|
+
batchCommitted,
|
|
846
864
|
removePendingMatches,
|
|
847
865
|
resolveMatchedPendingMatches,
|
|
848
866
|
collectionId,
|
|
@@ -982,6 +1000,12 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
982
1000
|
let hasUpToDate = false
|
|
983
1001
|
let hasSnapshotEnd = false
|
|
984
1002
|
|
|
1003
|
+
// Clear the current batch buffer at the START of processing a new batch
|
|
1004
|
+
// This preserves messages from the previous batch until new ones arrive,
|
|
1005
|
+
// allowing awaitMatch to find messages even if called after up-to-date
|
|
1006
|
+
currentBatchMessages.setState(() => [])
|
|
1007
|
+
batchCommitted.setState(() => false)
|
|
1008
|
+
|
|
985
1009
|
for (const message of messages) {
|
|
986
1010
|
// Add message to current batch buffer (for race condition handling)
|
|
987
1011
|
if (isChangeMessage(message)) {
|
|
@@ -1143,9 +1167,6 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1143
1167
|
}
|
|
1144
1168
|
}
|
|
1145
1169
|
|
|
1146
|
-
// Clear the current batch buffer since we're now up-to-date
|
|
1147
|
-
currentBatchMessages.setState(() => [])
|
|
1148
|
-
|
|
1149
1170
|
if (hasUpToDate || (hasSnapshotEnd && syncMode === `on-demand`)) {
|
|
1150
1171
|
// Mark the collection as ready now that sync is up to date
|
|
1151
1172
|
wrappedMarkReady(isBufferingInitialSync())
|
|
@@ -1183,7 +1204,12 @@ function createElectricSync<T extends Row<unknown>>(
|
|
|
1183
1204
|
return seen
|
|
1184
1205
|
})
|
|
1185
1206
|
|
|
1186
|
-
// Resolve all matched pending matches on up-to-date
|
|
1207
|
+
// Resolve all matched pending matches on up-to-date or snapshot-end in on-demand mode
|
|
1208
|
+
// Set batchCommitted BEFORE resolving to avoid timing window where late awaitMatch
|
|
1209
|
+
// calls could register as "matched" after resolver pass already ran
|
|
1210
|
+
if (hasUpToDate || (hasSnapshotEnd && syncMode === `on-demand`)) {
|
|
1211
|
+
batchCommitted.setState(() => true)
|
|
1212
|
+
}
|
|
1187
1213
|
resolveMatchedPendingMatches()
|
|
1188
1214
|
}
|
|
1189
1215
|
})
|
package/src/index.ts
CHANGED
package/src/sql-compiler.ts
CHANGED
|
@@ -124,6 +124,13 @@ function compileOrderByClause(
|
|
|
124
124
|
return sql
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Check if a BasicExpression represents a null/undefined value
|
|
129
|
+
*/
|
|
130
|
+
function isNullValue(exp: IR.BasicExpression<unknown>): boolean {
|
|
131
|
+
return exp.type === `val` && (exp.value === null || exp.value === undefined)
|
|
132
|
+
}
|
|
133
|
+
|
|
127
134
|
function compileFunction(
|
|
128
135
|
exp: IR.Func<unknown>,
|
|
129
136
|
params: Array<unknown> = [],
|
|
@@ -132,6 +139,26 @@ function compileFunction(
|
|
|
132
139
|
|
|
133
140
|
const opName = getOpName(name)
|
|
134
141
|
|
|
142
|
+
// Handle comparison operators with null/undefined values
|
|
143
|
+
// These would create invalid queries with missing params (e.g., "col = $1" with empty params)
|
|
144
|
+
// In SQL, all comparisons with NULL return UNKNOWN, so these are almost always mistakes
|
|
145
|
+
if (isComparisonOp(name)) {
|
|
146
|
+
const nullArgIndex = args.findIndex((arg: IR.BasicExpression) =>
|
|
147
|
+
isNullValue(arg),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if (nullArgIndex !== -1) {
|
|
151
|
+
// All comparison operators (including eq) throw an error for null values
|
|
152
|
+
// Users should use isNull() or isUndefined() to check for null values
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Cannot use null/undefined value with '${name}' operator. ` +
|
|
155
|
+
`Comparisons with null always evaluate to UNKNOWN in SQL. ` +
|
|
156
|
+
`Use isNull() or isUndefined() to check for null values, ` +
|
|
157
|
+
`or filter out null values before building the query.`,
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
135
162
|
const compiledArgs = args.map((arg: IR.BasicExpression) =>
|
|
136
163
|
compileBasicExpression(arg, params),
|
|
137
164
|
)
|
|
@@ -176,7 +203,7 @@ function compileFunction(
|
|
|
176
203
|
// Special case for comparison operators with boolean values
|
|
177
204
|
// PostgreSQL doesn't support < > <= >= on booleans
|
|
178
205
|
// Transform to equivalent equality checks or constant expressions
|
|
179
|
-
if (
|
|
206
|
+
if (isBooleanComparisonOp(name)) {
|
|
180
207
|
const lhsArg = args[0]
|
|
181
208
|
const rhsArg = args[1]
|
|
182
209
|
|
|
@@ -312,11 +339,21 @@ function isBinaryOp(name: string): boolean {
|
|
|
312
339
|
return binaryOps.includes(name)
|
|
313
340
|
}
|
|
314
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Check if operator is a comparison operator that takes two values
|
|
344
|
+
* These operators cannot accept null/undefined as values
|
|
345
|
+
* (null comparisons in SQL always evaluate to UNKNOWN)
|
|
346
|
+
*/
|
|
347
|
+
function isComparisonOp(name: string): boolean {
|
|
348
|
+
const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`]
|
|
349
|
+
return comparisonOps.includes(name)
|
|
350
|
+
}
|
|
351
|
+
|
|
315
352
|
/**
|
|
316
353
|
* Checks if the operator is a comparison operator (excluding eq)
|
|
317
354
|
* These operators don't work on booleans in PostgreSQL without casting
|
|
318
355
|
*/
|
|
319
|
-
function
|
|
356
|
+
function isBooleanComparisonOp(name: string): boolean {
|
|
320
357
|
return [`gt`, `gte`, `lt`, `lte`].includes(name)
|
|
321
358
|
}
|
|
322
359
|
|