@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.
- package/dist/cjs/electric.cjs +191 -41
- package/dist/cjs/electric.cjs.map +1 -1
- package/dist/cjs/electric.d.cts +116 -7
- package/dist/cjs/errors.cjs +16 -29
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +7 -10
- package/dist/cjs/index.cjs +2 -3
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/electric.d.ts +116 -7
- package/dist/esm/electric.js +188 -43
- package/dist/esm/electric.js.map +1 -1
- package/dist/esm/errors.d.ts +7 -10
- package/dist/esm/errors.js +16 -29
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +3 -4
- package/package.json +2 -2
- package/src/electric.ts +391 -74
- package/src/errors.ts +14 -27
package/dist/cjs/errors.cjs
CHANGED
|
@@ -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
|
|
23
|
-
constructor() {
|
|
24
|
-
super(
|
|
25
|
-
|
|
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
|
|
31
|
-
constructor() {
|
|
32
|
-
super(
|
|
33
|
-
|
|
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
|
package/dist/cjs/errors.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.cjs","sources":["../../src/errors.ts"],"sourcesContent":["import { TanStackDBError } from \"@tanstack/db\"\n\n// Electric DB Collection Errors\nexport class ElectricDBCollectionError extends TanStackDBError {\n constructor(message: string) {\n super(message)\n this.name = `ElectricDBCollectionError`\n }\n}\n\nexport class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {\n constructor(txIdType: string) {\n super(`Expected number in awaitTxId, received ${txIdType}
|
|
1
|
+
{"version":3,"file":"errors.cjs","sources":["../../src/errors.ts"],"sourcesContent":["import { TanStackDBError } from \"@tanstack/db\"\n\n// Electric DB Collection Errors\nexport class ElectricDBCollectionError extends TanStackDBError {\n constructor(message: string, collectionId?: string) {\n super(`${collectionId ? `[${collectionId}] ` : ``}${message}`)\n this.name = `ElectricDBCollectionError`\n }\n}\n\nexport class ExpectedNumberInAwaitTxIdError extends ElectricDBCollectionError {\n constructor(txIdType: string, collectionId?: string) {\n super(`Expected number in awaitTxId, received ${txIdType}`, collectionId)\n this.name = `ExpectedNumberInAwaitTxIdError`\n }\n}\n\nexport class TimeoutWaitingForTxIdError extends ElectricDBCollectionError {\n constructor(txId: number, collectionId?: string) {\n super(`Timeout waiting for txId: ${txId}`, collectionId)\n this.name = `TimeoutWaitingForTxIdError`\n }\n}\n\nexport class 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;;;;;;"}
|
package/dist/cjs/errors.d.cts
CHANGED
|
@@ -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
|
|
12
|
-
constructor();
|
|
11
|
+
export declare class TimeoutWaitingForMatchError extends ElectricDBCollectionError {
|
|
12
|
+
constructor(collectionId?: string);
|
|
13
13
|
}
|
|
14
|
-
export declare class
|
|
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
|
}
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -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
|
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/esm/electric.d.ts
CHANGED
|
@@ -1,33 +1,143 @@
|
|
|
1
|
-
import { BaseCollectionConfig, CollectionConfig,
|
|
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,
|
|
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 {};
|
package/dist/esm/electric.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
33
|
-
debug(
|
|
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(
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
134
|
-
|
|
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(
|
|
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(
|
|
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
|