@tanstack/electric-db-collection 0.1.29 → 0.1.31

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.
@@ -2,51 +2,38 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const db = require("@tanstack/db");
4
4
  class ElectricDBCollectionError extends db.TanStackDBError {
5
- constructor(message) {
6
- super(message);
5
+ constructor(message, collectionId) {
6
+ super(`${collectionId ? `[${collectionId}] ` : ``}${message}`);
7
7
  this.name = `ElectricDBCollectionError`;
8
8
  }
9
9
  }
10
10
  class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {
11
- constructor(txIdType) {
12
- super(`Expected number in awaitTxId, received ${txIdType}`);
11
+ constructor(txIdType, collectionId) {
12
+ super(`Expected number in awaitTxId, received ${txIdType}`, collectionId);
13
13
  this.name = `ExpectedNumberInAwaitTxIdError`;
14
14
  }
15
15
  }
16
16
  class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {
17
- constructor(txId) {
18
- super(`Timeout waiting for txId: ${txId}`);
17
+ constructor(txId, collectionId) {
18
+ super(`Timeout waiting for txId: ${txId}`, collectionId);
19
19
  this.name = `TimeoutWaitingForTxIdError`;
20
20
  }
21
21
  }
22
- class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {
23
- constructor() {
24
- super(
25
- `Electric collection onInsert handler must return a txid or array of txids`
26
- );
27
- this.name = `ElectricInsertHandlerMustReturnTxIdError`;
22
+ class TimeoutWaitingForMatchError extends ElectricDBCollectionError {
23
+ constructor(collectionId) {
24
+ super(`Timeout waiting for custom match function`, collectionId);
25
+ this.name = `TimeoutWaitingForMatchError`;
28
26
  }
29
27
  }
30
- class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {
31
- constructor() {
32
- super(
33
- `Electric collection onUpdate handler must return a txid or array of txids`
34
- );
35
- this.name = `ElectricUpdateHandlerMustReturnTxIdError`;
36
- }
37
- }
38
- class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {
39
- constructor() {
40
- super(
41
- `Electric collection onDelete handler must return a txid or array of txids`
42
- );
43
- this.name = `ElectricDeleteHandlerMustReturnTxIdError`;
28
+ class StreamAbortedError extends ElectricDBCollectionError {
29
+ constructor(collectionId) {
30
+ super(`Stream aborted`, collectionId);
31
+ this.name = `StreamAbortedError`;
44
32
  }
45
33
  }
46
34
  exports.ElectricDBCollectionError = ElectricDBCollectionError;
47
- exports.ElectricDeleteHandlerMustReturnTxIdError = ElectricDeleteHandlerMustReturnTxIdError;
48
- exports.ElectricInsertHandlerMustReturnTxIdError = ElectricInsertHandlerMustReturnTxIdError;
49
- exports.ElectricUpdateHandlerMustReturnTxIdError = ElectricUpdateHandlerMustReturnTxIdError;
50
35
  exports.ExpectedNumberInAwaitTxIdError = ExpectedNumberInAwaitTxIdError;
36
+ exports.StreamAbortedError = StreamAbortedError;
37
+ exports.TimeoutWaitingForMatchError = TimeoutWaitingForMatchError;
51
38
  exports.TimeoutWaitingForTxIdError = TimeoutWaitingForTxIdError;
52
39
  //# sourceMappingURL=errors.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.cjs","sources":["../../src/errors.ts"],"sourcesContent":["import { TanStackDBError } from \"@tanstack/db\"\n\n// Electric DB Collection Errors\nexport class ElectricDBCollectionError extends TanStackDBError {\n constructor(message: string) {\n super(message)\n this.name = `ElectricDBCollectionError`\n }\n}\n\nexport class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {\n constructor(txIdType: string) {\n super(`Expected number in awaitTxId, received ${txIdType}`)\n this.name = `ExpectedNumberInAwaitTxIdError`\n }\n}\n\nexport class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {\n constructor(txId: number) {\n super(`Timeout waiting for txId: ${txId}`)\n this.name = `TimeoutWaitingForTxIdError`\n }\n}\n\nexport class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {\n constructor() {\n super(\n `Electric collection onInsert handler must return a txid or array of txids`\n )\n this.name = `ElectricInsertHandlerMustReturnTxIdError`\n }\n}\n\nexport class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {\n constructor() {\n super(\n `Electric collection onUpdate handler must return a txid or array of txids`\n )\n this.name = `ElectricUpdateHandlerMustReturnTxIdError`\n }\n}\n\nexport class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {\n constructor() {\n super(\n `Electric collection onDelete handler must return a txid or array of txids`\n )\n this.name = `ElectricDeleteHandlerMustReturnTxIdError`\n }\n}\n"],"names":["TanStackDBError"],"mappings":";;;AAGO,MAAM,kCAAkCA,GAAAA,gBAAgB;AAAA,EAC7D,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,uCAAuC,0BAA0B;AAAA,EAC5E,YAAY,UAAkB;AAC5B,UAAM,0CAA0C,QAAQ,EAAE;AAC1D,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,mCAAmC,0BAA0B;AAAA,EACxE,YAAY,MAAc;AACxB,UAAM,6BAA6B,IAAI,EAAE;AACzC,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,iDAAiD,0BAA0B;AAAA,EACtF,cAAc;AACZ;AAAA,MACE;AAAA,IAAA;AAEF,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,iDAAiD,0BAA0B;AAAA,EACtF,cAAc;AACZ;AAAA,MACE;AAAA,IAAA;AAEF,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,iDAAiD,0BAA0B;AAAA,EACtF,cAAc;AACZ;AAAA,MACE;AAAA,IAAA;AAEF,SAAK,OAAO;AAAA,EACd;AACF;;;;;;;"}
1
+ {"version":3,"file":"errors.cjs","sources":["../../src/errors.ts"],"sourcesContent":["import { TanStackDBError } from \"@tanstack/db\"\n\n// Electric DB Collection Errors\nexport class ElectricDBCollectionError extends TanStackDBError {\n constructor(message: string, collectionId?: string) {\n super(`${collectionId ? `[${collectionId}] ` : ``}${message}`)\n this.name = `ElectricDBCollectionError`\n }\n}\n\nexport class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {\n constructor(txIdType: string, collectionId?: string) {\n super(`Expected number in awaitTxId, received ${txIdType}`, collectionId)\n this.name = `ExpectedNumberInAwaitTxIdError`\n }\n}\n\nexport class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {\n constructor(txId: number, collectionId?: string) {\n super(`Timeout waiting for txId: ${txId}`, collectionId)\n this.name = `TimeoutWaitingForTxIdError`\n }\n}\n\nexport class TimeoutWaitingForMatchError extends ElectricDBCollectionError {\n constructor(collectionId?: string) {\n super(`Timeout waiting for custom match function`, collectionId)\n this.name = `TimeoutWaitingForMatchError`\n }\n}\n\nexport class StreamAbortedError extends ElectricDBCollectionError {\n constructor(collectionId?: string) {\n super(`Stream aborted`, collectionId)\n this.name = `StreamAbortedError`\n }\n}\n"],"names":["TanStackDBError"],"mappings":";;;AAGO,MAAM,kCAAkCA,GAAAA,gBAAgB;AAAA,EAC7D,YAAY,SAAiB,cAAuB;AAClD,UAAM,GAAG,eAAe,IAAI,YAAY,OAAO,EAAE,GAAG,OAAO,EAAE;AAC7D,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,uCAAuC,0BAA0B;AAAA,EAC5E,YAAY,UAAkB,cAAuB;AACnD,UAAM,0CAA0C,QAAQ,IAAI,YAAY;AACxE,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,mCAAmC,0BAA0B;AAAA,EACxE,YAAY,MAAc,cAAuB;AAC/C,UAAM,6BAA6B,IAAI,IAAI,YAAY;AACvD,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,oCAAoC,0BAA0B;AAAA,EACzE,YAAY,cAAuB;AACjC,UAAM,6CAA6C,YAAY;AAC/D,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,2BAA2B,0BAA0B;AAAA,EAChE,YAAY,cAAuB;AACjC,UAAM,kBAAkB,YAAY;AACpC,SAAK,OAAO;AAAA,EACd;AACF;;;;;;"}
@@ -1,19 +1,16 @@
1
1
  import { TanStackDBError } from '@tanstack/db';
2
2
  export declare class ElectricDBCollectionError extends TanStackDBError {
3
- constructor(message: string);
3
+ constructor(message: string, collectionId?: string);
4
4
  }
5
5
  export declare class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {
6
- constructor(txIdType: string);
6
+ constructor(txIdType: string, collectionId?: string);
7
7
  }
8
8
  export declare class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {
9
- constructor(txId: number);
9
+ constructor(txId: number, collectionId?: string);
10
10
  }
11
- export declare class ElectricInsertHandlerMustReturnTxIdError extends ElectricDBCollectionError {
12
- constructor();
11
+ export declare class TimeoutWaitingForMatchError extends ElectricDBCollectionError {
12
+ constructor(collectionId?: string);
13
13
  }
14
- export declare class ElectricUpdateHandlerMustReturnTxIdError extends ElectricDBCollectionError {
15
- constructor();
16
- }
17
- export declare class ElectricDeleteHandlerMustReturnTxIdError extends ElectricDBCollectionError {
18
- constructor();
14
+ export declare class StreamAbortedError extends ElectricDBCollectionError {
15
+ constructor(collectionId?: string);
19
16
  }
@@ -4,9 +4,8 @@ const electric = require("./electric.cjs");
4
4
  const errors = require("./errors.cjs");
5
5
  exports.electricCollectionOptions = electric.electricCollectionOptions;
6
6
  exports.ElectricDBCollectionError = errors.ElectricDBCollectionError;
7
- exports.ElectricDeleteHandlerMustReturnTxIdError = errors.ElectricDeleteHandlerMustReturnTxIdError;
8
- exports.ElectricInsertHandlerMustReturnTxIdError = errors.ElectricInsertHandlerMustReturnTxIdError;
9
- exports.ElectricUpdateHandlerMustReturnTxIdError = errors.ElectricUpdateHandlerMustReturnTxIdError;
10
7
  exports.ExpectedNumberInAwaitTxIdError = errors.ExpectedNumberInAwaitTxIdError;
8
+ exports.StreamAbortedError = errors.StreamAbortedError;
9
+ exports.TimeoutWaitingForMatchError = errors.TimeoutWaitingForMatchError;
11
10
  exports.TimeoutWaitingForTxIdError = errors.TimeoutWaitingForTxIdError;
12
11
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
@@ -1,33 +1,143 @@
1
- import { BaseCollectionConfig, CollectionConfig, Fn, UtilsRecord } from '@tanstack/db';
1
+ import { BaseCollectionConfig, CollectionConfig, DeleteMutationFnParams, InsertMutationFnParams, UpdateMutationFnParams, UtilsRecord } from '@tanstack/db';
2
2
  import { StandardSchemaV1 } from '@standard-schema/spec';
3
- import { GetExtensions, Row, ShapeStreamOptions } from '@electric-sql/client';
3
+ import { GetExtensions, Message, Row, ShapeStreamOptions } from '@electric-sql/client';
4
+ export { isChangeMessage, isControlMessage } from '@electric-sql/client';
4
5
  /**
5
6
  * Type representing a transaction ID in ElectricSQL
6
7
  */
7
8
  export type Txid = number;
9
+ /**
10
+ * Custom match function type - receives stream messages and returns boolean
11
+ * indicating if the mutation has been synchronized
12
+ */
13
+ export type MatchFunction<T extends Row<unknown>> = (message: Message<T>) => boolean;
14
+ /**
15
+ * Matching strategies for Electric synchronization
16
+ * Handlers can return:
17
+ * - Txid strategy: { txid: number | number[] } (recommended)
18
+ * - Void (no return value) - mutation completes without waiting
19
+ */
20
+ export type MatchingStrategy = {
21
+ txid: Txid | Array<Txid>;
22
+ } | void;
8
23
  type InferSchemaOutput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<T> extends Row<unknown> ? StandardSchemaV1.InferOutput<T> : Record<string, unknown> : Record<string, unknown>;
9
24
  /**
10
25
  * Configuration interface for Electric collection options
11
26
  * @template T - The type of items in the collection
12
27
  * @template TSchema - The schema type for validation
13
28
  */
14
- export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>, TSchema extends StandardSchemaV1 = never> extends BaseCollectionConfig<T, string | number, TSchema, Record<string, Fn>, {
15
- txid: Txid | Array<Txid>;
16
- }> {
29
+ export interface ElectricCollectionConfig<T extends Row<unknown> = Row<unknown>, TSchema extends StandardSchemaV1 = never> extends Omit<BaseCollectionConfig<T, string | number, TSchema, UtilsRecord, any>, `onInsert` | `onUpdate` | `onDelete`> {
17
30
  /**
18
31
  * Configuration options for the ElectricSQL ShapeStream
19
32
  */
20
33
  shapeOptions: ShapeStreamOptions<GetExtensions<T>>;
34
+ /**
35
+ * Optional asynchronous handler function called before an insert operation
36
+ * @param params Object containing transaction and collection information
37
+ * @returns Promise resolving to { txid } or void
38
+ * @example
39
+ * // Basic Electric insert handler with txid (recommended)
40
+ * onInsert: async ({ transaction }) => {
41
+ * const newItem = transaction.mutations[0].modified
42
+ * const result = await api.todos.create({
43
+ * data: newItem
44
+ * })
45
+ * return { txid: result.txid }
46
+ * }
47
+ *
48
+ * @example
49
+ * // Insert handler with multiple items - return array of txids
50
+ * onInsert: async ({ transaction }) => {
51
+ * const items = transaction.mutations.map(m => m.modified)
52
+ * const results = await Promise.all(
53
+ * items.map(item => api.todos.create({ data: item }))
54
+ * )
55
+ * return { txid: results.map(r => r.txid) }
56
+ * }
57
+ *
58
+ * @example
59
+ * // Use awaitMatch utility for custom matching
60
+ * onInsert: async ({ transaction, collection }) => {
61
+ * const newItem = transaction.mutations[0].modified
62
+ * await api.todos.create({ data: newItem })
63
+ * await collection.utils.awaitMatch(
64
+ * (message) => isChangeMessage(message) &&
65
+ * message.headers.operation === 'insert' &&
66
+ * message.value.name === newItem.name
67
+ * )
68
+ * }
69
+ */
70
+ onInsert?: (params: InsertMutationFnParams<T>) => Promise<MatchingStrategy>;
71
+ /**
72
+ * Optional asynchronous handler function called before an update operation
73
+ * @param params Object containing transaction and collection information
74
+ * @returns Promise resolving to { txid } or void
75
+ * @example
76
+ * // Basic Electric update handler with txid (recommended)
77
+ * onUpdate: async ({ transaction }) => {
78
+ * const { original, changes } = transaction.mutations[0]
79
+ * const result = await api.todos.update({
80
+ * where: { id: original.id },
81
+ * data: changes
82
+ * })
83
+ * return { txid: result.txid }
84
+ * }
85
+ *
86
+ * @example
87
+ * // Use awaitMatch utility for custom matching
88
+ * onUpdate: async ({ transaction, collection }) => {
89
+ * const { original, changes } = transaction.mutations[0]
90
+ * await api.todos.update({ where: { id: original.id }, data: changes })
91
+ * await collection.utils.awaitMatch(
92
+ * (message) => isChangeMessage(message) &&
93
+ * message.headers.operation === 'update' &&
94
+ * message.value.id === original.id
95
+ * )
96
+ * }
97
+ */
98
+ onUpdate?: (params: UpdateMutationFnParams<T>) => Promise<MatchingStrategy>;
99
+ /**
100
+ * Optional asynchronous handler function called before a delete operation
101
+ * @param params Object containing transaction and collection information
102
+ * @returns Promise resolving to { txid } or void
103
+ * @example
104
+ * // Basic Electric delete handler with txid (recommended)
105
+ * onDelete: async ({ transaction }) => {
106
+ * const mutation = transaction.mutations[0]
107
+ * const result = await api.todos.delete({
108
+ * id: mutation.original.id
109
+ * })
110
+ * return { txid: result.txid }
111
+ * }
112
+ *
113
+ * @example
114
+ * // Use awaitMatch utility for custom matching
115
+ * onDelete: async ({ transaction, collection }) => {
116
+ * const mutation = transaction.mutations[0]
117
+ * await api.todos.delete({ id: mutation.original.id })
118
+ * await collection.utils.awaitMatch(
119
+ * (message) => isChangeMessage(message) &&
120
+ * message.headers.operation === 'delete' &&
121
+ * message.value.id === mutation.original.id
122
+ * )
123
+ * }
124
+ */
125
+ onDelete?: (params: DeleteMutationFnParams<T>) => Promise<MatchingStrategy>;
21
126
  }
22
127
  /**
23
128
  * Type for the awaitTxId utility function
24
129
  */
25
130
  export type AwaitTxIdFn = (txId: Txid, timeout?: number) => Promise<boolean>;
131
+ /**
132
+ * Type for the awaitMatch utility function
133
+ */
134
+ export type AwaitMatchFn<T extends Row<unknown>> = (matchFn: MatchFunction<T>, timeout?: number) => Promise<boolean>;
26
135
  /**
27
136
  * Electric collection utilities type
28
137
  */
29
- export interface ElectricCollectionUtils extends UtilsRecord {
138
+ export interface ElectricCollectionUtils<T extends Row<unknown> = Row<unknown>> extends UtilsRecord {
30
139
  awaitTxId: AwaitTxIdFn;
140
+ awaitMatch: AwaitMatchFn<T>;
31
141
  }
32
142
  /**
33
143
  * Creates Electric collection options for use with a standard Collection
@@ -52,4 +162,3 @@ export declare function electricCollectionOptions<T extends Row<unknown>>(config
52
162
  utils: ElectricCollectionUtils;
53
163
  schema?: never;
54
164
  };
55
- export {};
@@ -1,7 +1,8 @@
1
1
  import { ShapeStream, isChangeMessage, isVisibleInSnapshot, isControlMessage } from "@electric-sql/client";
2
+ import { isChangeMessage as isChangeMessage2, isControlMessage as isControlMessage2 } from "@electric-sql/client";
2
3
  import { Store } from "@tanstack/store";
3
4
  import DebugModule from "debug";
4
- import { ElectricInsertHandlerMustReturnTxIdError, ElectricUpdateHandlerMustReturnTxIdError, ElectricDeleteHandlerMustReturnTxIdError, ExpectedNumberInAwaitTxIdError, TimeoutWaitingForTxIdError } from "./errors.js";
5
+ import { StreamAbortedError, ExpectedNumberInAwaitTxIdError, TimeoutWaitingForTxIdError, TimeoutWaitingForMatchError } from "./errors.js";
5
6
  const debug = DebugModule.debug(`ts/db:electric`);
6
7
  function isUpToDateMessage(message) {
7
8
  return isControlMessage(message) && message.headers.control === `up-to-date`;
@@ -25,14 +26,48 @@ function hasTxids(message) {
25
26
  function electricCollectionOptions(config) {
26
27
  const seenTxids = new Store(/* @__PURE__ */ new Set([]));
27
28
  const seenSnapshots = new Store([]);
29
+ const pendingMatches = new Store(/* @__PURE__ */ new Map());
30
+ const currentBatchMessages = new Store([]);
31
+ const removePendingMatches = (matchIds) => {
32
+ if (matchIds.length > 0) {
33
+ pendingMatches.setState((current) => {
34
+ const newMatches = new Map(current);
35
+ matchIds.forEach((id) => newMatches.delete(id));
36
+ return newMatches;
37
+ });
38
+ }
39
+ };
40
+ const resolveMatchedPendingMatches = () => {
41
+ const matchesToResolve = [];
42
+ pendingMatches.state.forEach((match, matchId) => {
43
+ if (match.matched) {
44
+ clearTimeout(match.timeoutId);
45
+ match.resolve(true);
46
+ matchesToResolve.push(matchId);
47
+ debug(
48
+ `${config.id ? `[${config.id}] ` : ``}awaitMatch resolved on up-to-date for match %s`,
49
+ matchId
50
+ );
51
+ }
52
+ });
53
+ removePendingMatches(matchesToResolve);
54
+ };
28
55
  const sync = createElectricSync(config.shapeOptions, {
29
56
  seenTxids,
30
- seenSnapshots
57
+ seenSnapshots,
58
+ pendingMatches,
59
+ currentBatchMessages,
60
+ removePendingMatches,
61
+ resolveMatchedPendingMatches,
62
+ collectionId: config.id
31
63
  });
32
- const awaitTxId = async (txId, timeout = 3e4) => {
33
- debug(`awaitTxId called with txid %d`, txId);
64
+ const awaitTxId = async (txId, timeout = 5e3) => {
65
+ debug(
66
+ `${config.id ? `[${config.id}] ` : ``}awaitTxId called with txid %d`,
67
+ txId
68
+ );
34
69
  if (typeof txId !== `number`) {
35
- throw new ExpectedNumberInAwaitTxIdError(typeof txId);
70
+ throw new ExpectedNumberInAwaitTxIdError(typeof txId, config.id);
36
71
  }
37
72
  const hasTxid = seenTxids.state.has(txId);
38
73
  if (hasTxid) return true;
@@ -44,11 +79,14 @@ function electricCollectionOptions(config) {
44
79
  const timeoutId = setTimeout(() => {
45
80
  unsubscribeSeenTxids();
46
81
  unsubscribeSeenSnapshots();
47
- reject(new TimeoutWaitingForTxIdError(txId));
82
+ reject(new TimeoutWaitingForTxIdError(txId, config.id));
48
83
  }, timeout);
49
84
  const unsubscribeSeenTxids = seenTxids.subscribe(() => {
50
85
  if (seenTxids.state.has(txId)) {
51
- debug(`awaitTxId found match for txid %o`, txId);
86
+ debug(
87
+ `${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o`,
88
+ txId
89
+ );
52
90
  clearTimeout(timeoutId);
53
91
  unsubscribeSeenTxids();
54
92
  unsubscribeSeenSnapshots();
@@ -61,7 +99,7 @@ function electricCollectionOptions(config) {
61
99
  );
62
100
  if (visibleSnapshot) {
63
101
  debug(
64
- `awaitTxId found match for txid %o in snapshot %o`,
102
+ `${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o in snapshot %o`,
65
103
  txId,
66
104
  visibleSnapshot
67
105
  );
@@ -73,42 +111,96 @@ function electricCollectionOptions(config) {
73
111
  });
74
112
  });
75
113
  };
76
- const wrappedOnInsert = config.onInsert ? async (params) => {
77
- const handlerResult = await config.onInsert(params) ?? {};
78
- const txid = handlerResult.txid;
79
- if (!txid) {
80
- throw new ElectricInsertHandlerMustReturnTxIdError();
81
- }
82
- if (Array.isArray(txid)) {
83
- await Promise.all(txid.map((id) => awaitTxId(id)));
84
- } else {
85
- await awaitTxId(txid);
114
+ const awaitMatch = async (matchFn, timeout = 3e3) => {
115
+ debug(
116
+ `${config.id ? `[${config.id}] ` : ``}awaitMatch called with custom function`
117
+ );
118
+ return new Promise((resolve, reject) => {
119
+ const matchId = Math.random().toString(36);
120
+ const cleanupMatch = () => {
121
+ pendingMatches.setState((current) => {
122
+ const newMatches = new Map(current);
123
+ newMatches.delete(matchId);
124
+ return newMatches;
125
+ });
126
+ };
127
+ const onTimeout = () => {
128
+ cleanupMatch();
129
+ reject(new TimeoutWaitingForMatchError(config.id));
130
+ };
131
+ const timeoutId = setTimeout(onTimeout, timeout);
132
+ const checkMatch = (message) => {
133
+ if (matchFn(message)) {
134
+ debug(
135
+ `${config.id ? `[${config.id}] ` : ``}awaitMatch found matching message, waiting for up-to-date`
136
+ );
137
+ pendingMatches.setState((current) => {
138
+ const newMatches = new Map(current);
139
+ const existing = newMatches.get(matchId);
140
+ if (existing) {
141
+ newMatches.set(matchId, { ...existing, matched: true });
142
+ }
143
+ return newMatches;
144
+ });
145
+ return true;
146
+ }
147
+ return false;
148
+ };
149
+ for (const message of currentBatchMessages.state) {
150
+ if (matchFn(message)) {
151
+ debug(
152
+ `${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in current batch, waiting for up-to-date`
153
+ );
154
+ pendingMatches.setState((current) => {
155
+ const newMatches = new Map(current);
156
+ newMatches.set(matchId, {
157
+ matchFn: checkMatch,
158
+ resolve,
159
+ reject,
160
+ timeoutId,
161
+ matched: true
162
+ // Already matched
163
+ });
164
+ return newMatches;
165
+ });
166
+ return;
167
+ }
168
+ }
169
+ pendingMatches.setState((current) => {
170
+ const newMatches = new Map(current);
171
+ newMatches.set(matchId, {
172
+ matchFn: checkMatch,
173
+ resolve,
174
+ reject,
175
+ timeoutId,
176
+ matched: false
177
+ });
178
+ return newMatches;
179
+ });
180
+ });
181
+ };
182
+ const processMatchingStrategy = async (result) => {
183
+ if (result && `txid` in result) {
184
+ if (Array.isArray(result.txid)) {
185
+ await Promise.all(result.txid.map(awaitTxId));
186
+ } else {
187
+ await awaitTxId(result.txid);
188
+ }
86
189
  }
190
+ };
191
+ const wrappedOnInsert = config.onInsert ? async (params) => {
192
+ const handlerResult = await config.onInsert(params);
193
+ await processMatchingStrategy(handlerResult);
87
194
  return handlerResult;
88
195
  } : void 0;
89
196
  const wrappedOnUpdate = config.onUpdate ? async (params) => {
90
- const handlerResult = await config.onUpdate(params) ?? {};
91
- const txid = handlerResult.txid;
92
- if (!txid) {
93
- throw new ElectricUpdateHandlerMustReturnTxIdError();
94
- }
95
- if (Array.isArray(txid)) {
96
- await Promise.all(txid.map((id) => awaitTxId(id)));
97
- } else {
98
- await awaitTxId(txid);
99
- }
197
+ const handlerResult = await config.onUpdate(params);
198
+ await processMatchingStrategy(handlerResult);
100
199
  return handlerResult;
101
200
  } : void 0;
102
201
  const wrappedOnDelete = config.onDelete ? async (params) => {
103
202
  const handlerResult = await config.onDelete(params);
104
- if (!handlerResult.txid) {
105
- throw new ElectricDeleteHandlerMustReturnTxIdError();
106
- }
107
- if (Array.isArray(handlerResult.txid)) {
108
- await Promise.all(handlerResult.txid.map((id) => awaitTxId(id)));
109
- } else {
110
- await awaitTxId(handlerResult.txid);
111
- }
203
+ await processMatchingStrategy(handlerResult);
112
204
  return handlerResult;
113
205
  } : void 0;
114
206
  const {
@@ -125,13 +217,22 @@ function electricCollectionOptions(config) {
125
217
  onUpdate: wrappedOnUpdate,
126
218
  onDelete: wrappedOnDelete,
127
219
  utils: {
128
- awaitTxId
220
+ awaitTxId,
221
+ awaitMatch
129
222
  }
130
223
  };
131
224
  }
132
225
  function createElectricSync(shapeOptions, options) {
133
- const { seenTxids } = options;
134
- const { seenSnapshots } = options;
226
+ const {
227
+ seenTxids,
228
+ seenSnapshots,
229
+ pendingMatches,
230
+ currentBatchMessages,
231
+ removePendingMatches,
232
+ resolveMatchedPendingMatches,
233
+ collectionId
234
+ } = options;
235
+ const MAX_BATCH_MESSAGES = 1e3;
135
236
  const relationSchema = new Store(void 0);
136
237
  const getSyncMetadata = () => {
137
238
  const schema = relationSchema.state || `public`;
@@ -158,6 +259,15 @@ function createElectricSync(shapeOptions, options) {
158
259
  abortController.abort();
159
260
  }
160
261
  }
262
+ abortController.signal.addEventListener(`abort`, () => {
263
+ pendingMatches.setState((current) => {
264
+ current.forEach((match) => {
265
+ clearTimeout(match.timeoutId);
266
+ match.reject(new StreamAbortedError());
267
+ });
268
+ return /* @__PURE__ */ new Map();
269
+ });
270
+ });
161
271
  const stream = new ShapeStream({
162
272
  ...shapeOptions,
163
273
  signal: abortController.signal,
@@ -182,9 +292,34 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
182
292
  unsubscribeStream = stream.subscribe((messages) => {
183
293
  let hasUpToDate = false;
184
294
  for (const message of messages) {
295
+ if (isChangeMessage(message)) {
296
+ currentBatchMessages.setState((currentBuffer) => {
297
+ const newBuffer = [...currentBuffer, message];
298
+ if (newBuffer.length > MAX_BATCH_MESSAGES) {
299
+ newBuffer.splice(0, newBuffer.length - MAX_BATCH_MESSAGES);
300
+ }
301
+ return newBuffer;
302
+ });
303
+ }
185
304
  if (hasTxids(message)) {
186
305
  message.headers.txids?.forEach((txid) => newTxids.add(txid));
187
306
  }
307
+ const matchesToRemove = [];
308
+ pendingMatches.state.forEach((match, matchId) => {
309
+ if (!match.matched) {
310
+ try {
311
+ match.matchFn(message);
312
+ } catch (err) {
313
+ clearTimeout(match.timeoutId);
314
+ match.reject(
315
+ err instanceof Error ? err : new Error(String(err))
316
+ );
317
+ matchesToRemove.push(matchId);
318
+ debug(`matchFn error: %o`, err);
319
+ }
320
+ }
321
+ });
322
+ removePendingMatches(matchesToRemove);
188
323
  if (isChangeMessage(message)) {
189
324
  const schema = message.headers.schema;
190
325
  if (schema && typeof schema === `string`) {
@@ -208,7 +343,7 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
208
343
  hasUpToDate = true;
209
344
  } else if (isMustRefetchMessage(message)) {
210
345
  debug(
211
- `Received must-refetch message, starting transaction with truncate`
346
+ `${collectionId ? `[${collectionId}] ` : ``}Received must-refetch message, starting transaction with truncate`
212
347
  );
213
348
  if (!transactionStarted) {
214
349
  begin();
@@ -219,6 +354,7 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
219
354
  }
220
355
  }
221
356
  if (hasUpToDate) {
357
+ currentBatchMessages.setState(() => []);
222
358
  if (transactionStarted) {
223
359
  commit();
224
360
  transactionStarted = false;
@@ -227,7 +363,10 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
227
363
  seenTxids.setState((currentTxids) => {
228
364
  const clonedSeen = new Set(currentTxids);
229
365
  if (newTxids.size > 0) {
230
- debug(`new txids synced from pg %O`, Array.from(newTxids));
366
+ debug(
367
+ `${collectionId ? `[${collectionId}] ` : ``}new txids synced from pg %O`,
368
+ Array.from(newTxids)
369
+ );
231
370
  }
232
371
  newTxids.forEach((txid) => clonedSeen.add(txid));
233
372
  newTxids.clear();
@@ -236,11 +375,15 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
236
375
  seenSnapshots.setState((currentSnapshots) => {
237
376
  const seen = [...currentSnapshots, ...newSnapshots];
238
377
  newSnapshots.forEach(
239
- (snapshot) => debug(`new snapshot synced from pg %o`, snapshot)
378
+ (snapshot) => debug(
379
+ `${collectionId ? `[${collectionId}] ` : ``}new snapshot synced from pg %o`,
380
+ snapshot
381
+ )
240
382
  );
241
383
  newSnapshots.length = 0;
242
384
  return seen;
243
385
  });
386
+ resolveMatchedPendingMatches();
244
387
  }
245
388
  });
246
389
  return () => {
@@ -253,6 +396,8 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
253
396
  };
254
397
  }
255
398
  export {
256
- electricCollectionOptions
399
+ electricCollectionOptions,
400
+ isChangeMessage2 as isChangeMessage,
401
+ isControlMessage2 as isControlMessage
257
402
  };
258
403
  //# sourceMappingURL=electric.js.map