@electric-sql/experimental 0.1.2-beta.4 → 1.0.1
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/index.cjs +40 -30
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.legacy-esm.js +40 -30
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +40 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/bigint-utils.ts +11 -0
- package/src/multi-shape-stream.ts +39 -28
package/dist/cjs/index.cjs
CHANGED
|
@@ -108,6 +108,17 @@ function matchBy(column, value) {
|
|
|
108
108
|
return (message) => message.value[column] === value;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// src/bigint-utils.ts
|
|
112
|
+
function bigIntMax(nums) {
|
|
113
|
+
return BigInt(nums.reduce((m, e) => e > m ? e : m));
|
|
114
|
+
}
|
|
115
|
+
function bigIntMin(nums) {
|
|
116
|
+
return BigInt(nums.reduce((m, e) => e < m ? e : m));
|
|
117
|
+
}
|
|
118
|
+
function bigIntCompare(a, b) {
|
|
119
|
+
return a > b ? 1 : a < b ? -1 : 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
111
122
|
// src/multi-shape-stream.ts
|
|
112
123
|
var import_client2 = require("@electric-sql/client");
|
|
113
124
|
var _shapes, _started, _checkForUpdatesTimeout, _lastDataLsns, _lastUpToDateLsns, _subscribers, _MultiShapeStream_instances, start_fn, scheduleCheckForUpdates_fn, checkForUpdates_fn, onError_fn, shapeEntries_fn;
|
|
@@ -140,10 +151,10 @@ var MultiShapeStream = class {
|
|
|
140
151
|
])
|
|
141
152
|
));
|
|
142
153
|
__privateSet(this, _lastDataLsns, Object.fromEntries(
|
|
143
|
-
Object.entries(shapes).map(([key]) => [key, -
|
|
154
|
+
Object.entries(shapes).map(([key]) => [key, BigInt(-1)])
|
|
144
155
|
));
|
|
145
156
|
__privateSet(this, _lastUpToDateLsns, Object.fromEntries(
|
|
146
|
-
Object.entries(shapes).map(([key]) => [key, -
|
|
157
|
+
Object.entries(shapes).map(([key]) => [key, BigInt(-1)])
|
|
147
158
|
));
|
|
148
159
|
if (start) __privateMethod(this, _MultiShapeStream_instances, start_fn).call(this);
|
|
149
160
|
}
|
|
@@ -181,14 +192,12 @@ var MultiShapeStream = class {
|
|
|
181
192
|
}
|
|
182
193
|
/** Unix time at which we last synced. Undefined when `isLoading` is true. */
|
|
183
194
|
lastSyncedAt() {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
)
|
|
191
|
-
);
|
|
195
|
+
const shapeEntries = __privateMethod(this, _MultiShapeStream_instances, shapeEntries_fn).call(this);
|
|
196
|
+
if (shapeEntries.length === 0) return;
|
|
197
|
+
return shapeEntries.reduce((minLastSyncedAt, [_, shape]) => {
|
|
198
|
+
var _a;
|
|
199
|
+
return Math.min(minLastSyncedAt, (_a = shape.lastSyncedAt()) != null ? _a : Infinity);
|
|
200
|
+
}, Infinity);
|
|
192
201
|
}
|
|
193
202
|
/** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */
|
|
194
203
|
lastSynced() {
|
|
@@ -223,23 +232,21 @@ start_fn = function() {
|
|
|
223
232
|
}
|
|
224
233
|
shape.subscribe(
|
|
225
234
|
(messages) => __async(this, null, function* () {
|
|
226
|
-
const upToDateLsns = messages.filter(import_client2.isControlMessage).map(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
});
|
|
235
|
+
const upToDateLsns = messages.filter(import_client2.isControlMessage).map(
|
|
236
|
+
({ headers }) => typeof headers.global_last_seen_lsn === `string` ? BigInt(headers.global_last_seen_lsn) : BigInt(0)
|
|
237
|
+
);
|
|
230
238
|
if (upToDateLsns.length > 0) {
|
|
231
|
-
const maxUpToDateLsn =
|
|
239
|
+
const maxUpToDateLsn = bigIntMax(upToDateLsns);
|
|
232
240
|
const lastMaxUpToDateLsn = __privateGet(this, _lastUpToDateLsns)[key];
|
|
233
241
|
if (maxUpToDateLsn > lastMaxUpToDateLsn) {
|
|
234
242
|
__privateGet(this, _lastUpToDateLsns)[key] = maxUpToDateLsn;
|
|
235
243
|
}
|
|
236
244
|
}
|
|
237
|
-
const dataLsns = messages.filter(import_client2.isChangeMessage).map(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
});
|
|
245
|
+
const dataLsns = messages.filter(import_client2.isChangeMessage).map(
|
|
246
|
+
({ headers }) => typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)
|
|
247
|
+
);
|
|
241
248
|
if (dataLsns.length > 0) {
|
|
242
|
-
const maxDataLsn =
|
|
249
|
+
const maxDataLsn = bigIntMax(dataLsns);
|
|
243
250
|
const lastMaxDataLsn = __privateGet(this, _lastDataLsns)[key];
|
|
244
251
|
if (maxDataLsn > lastMaxDataLsn) {
|
|
245
252
|
__privateGet(this, _lastDataLsns)[key] = maxDataLsn;
|
|
@@ -267,7 +274,7 @@ scheduleCheckForUpdates_fn = function() {
|
|
|
267
274
|
};
|
|
268
275
|
checkForUpdates_fn = function() {
|
|
269
276
|
return __async(this, null, function* () {
|
|
270
|
-
const maxDataLsn =
|
|
277
|
+
const maxDataLsn = bigIntMax(Object.values(__privateGet(this, _lastDataLsns)));
|
|
271
278
|
const refreshPromises = __privateMethod(this, _MultiShapeStream_instances, shapeEntries_fn).call(this).filter(([key]) => {
|
|
272
279
|
const lastUpToDateLsn = __privateGet(this, _lastUpToDateLsns)[key];
|
|
273
280
|
return lastUpToDateLsn < maxDataLsn;
|
|
@@ -298,7 +305,7 @@ var _TransactionalMultiShapeStream = class _TransactionalMultiShapeStream extend
|
|
|
298
305
|
__privateAdd(this, _changeMessages, /* @__PURE__ */ new Map());
|
|
299
306
|
__privateAdd(this, _completeLsns);
|
|
300
307
|
__privateSet(this, _completeLsns, Object.fromEntries(
|
|
301
|
-
Object.entries(options.shapes).map(([key]) => [key, -
|
|
308
|
+
Object.entries(options.shapes).map(([key]) => [key, BigInt(-1)])
|
|
302
309
|
));
|
|
303
310
|
}
|
|
304
311
|
_publish(messages) {
|
|
@@ -308,7 +315,7 @@ var _TransactionalMultiShapeStream = class _TransactionalMultiShapeStream extend
|
|
|
308
315
|
const lsnsToPublish = [...__privateGet(this, _changeMessages).keys()].filter(
|
|
309
316
|
(lsn) => lsn <= lowestCompleteLsn
|
|
310
317
|
);
|
|
311
|
-
const messagesToPublish = lsnsToPublish.sort((a, b) => a
|
|
318
|
+
const messagesToPublish = lsnsToPublish.sort((a, b) => bigIntCompare(a, b)).map(
|
|
312
319
|
(lsn) => {
|
|
313
320
|
var _a;
|
|
314
321
|
return (_a = __privateGet(this, _changeMessages).get(lsn)) == null ? void 0 : _a.sort((a, b) => {
|
|
@@ -334,7 +341,7 @@ _changeMessages = new WeakMap();
|
|
|
334
341
|
_completeLsns = new WeakMap();
|
|
335
342
|
_TransactionalMultiShapeStream_instances = new WeakSet();
|
|
336
343
|
getLowestCompleteLsn_fn = function() {
|
|
337
|
-
return
|
|
344
|
+
return bigIntMin(Object.values(__privateGet(this, _completeLsns)));
|
|
338
345
|
};
|
|
339
346
|
accumulate_fn = function(messages) {
|
|
340
347
|
const isUpToDate = this.isUpToDate;
|
|
@@ -342,24 +349,27 @@ accumulate_fn = function(messages) {
|
|
|
342
349
|
var _a;
|
|
343
350
|
const { shape, headers } = message;
|
|
344
351
|
if ((0, import_client2.isChangeMessage)(message)) {
|
|
345
|
-
const lsn = typeof headers.lsn === `
|
|
352
|
+
const lsn = typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0);
|
|
346
353
|
if (!__privateGet(this, _changeMessages).has(lsn)) {
|
|
347
354
|
__privateGet(this, _changeMessages).set(lsn, []);
|
|
348
355
|
}
|
|
349
356
|
(_a = __privateGet(this, _changeMessages).get(lsn)) == null ? void 0 : _a.push(message);
|
|
350
357
|
if (isUpToDate && // All shapes must be up to date
|
|
351
358
|
typeof headers.last === `boolean` && headers.last === true) {
|
|
352
|
-
__privateGet(this, _completeLsns)[shape] =
|
|
359
|
+
__privateGet(this, _completeLsns)[shape] = bigIntMax([
|
|
360
|
+
__privateGet(this, _completeLsns)[shape],
|
|
361
|
+
lsn
|
|
362
|
+
]);
|
|
353
363
|
}
|
|
354
364
|
} else if ((0, import_client2.isControlMessage)(message)) {
|
|
355
365
|
if (headers.control === `up-to-date`) {
|
|
356
|
-
if (typeof headers.global_last_seen_lsn !== `
|
|
366
|
+
if (typeof headers.global_last_seen_lsn !== `string`) {
|
|
357
367
|
throw new Error(`global_last_seen_lsn is not a number`);
|
|
358
368
|
}
|
|
359
|
-
__privateGet(this, _completeLsns)[shape] =
|
|
369
|
+
__privateGet(this, _completeLsns)[shape] = bigIntMax([
|
|
360
370
|
__privateGet(this, _completeLsns)[shape],
|
|
361
|
-
headers.global_last_seen_lsn
|
|
362
|
-
);
|
|
371
|
+
BigInt(headers.global_last_seen_lsn)
|
|
372
|
+
]);
|
|
363
373
|
}
|
|
364
374
|
}
|
|
365
375
|
});
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/index.ts","../../src/match.ts","../../src/multi-shape-stream.ts"],"sourcesContent":["export * from './match'\nexport * from './multi-shape-stream'\n","import {\n isChangeMessage,\n type ShapeStreamInterface,\n type ChangeMessage,\n type GetExtensions,\n type Operation,\n type Row,\n type Value,\n type Message,\n} from '@electric-sql/client'\n\nexport function matchStream<T extends Row<unknown>>(\n stream: ShapeStreamInterface<T>,\n operations: Array<Operation>,\n matchFn: (message: ChangeMessage<T>) => boolean,\n timeout = 60000 // ms\n): Promise<ChangeMessage<T>> {\n return new Promise<ChangeMessage<T>>((resolve, reject) => {\n const unsubscribe: () => void = stream.subscribe(\n (messages: Array<unknown>) => {\n const message = messages\n .filter((msg): msg is ChangeMessage<T> =>\n isChangeMessage(msg as Message<Row<never>>)\n )\n .find((message) => {\n const operation: Operation = message.headers.operation\n\n return operations.includes(operation) && matchFn(message)\n })\n\n if (message) {\n return finish(message)\n }\n }\n )\n\n const timeoutId: NodeJS.Timeout = setTimeout(() => {\n const msg: string = `matchStream timed out after ${timeout}ms`\n\n console.error(msg)\n\n reject(msg)\n }, timeout)\n\n function finish(message: ChangeMessage<T>): void {\n clearTimeout(timeoutId)\n\n unsubscribe()\n\n return resolve(message)\n }\n })\n}\n\nexport function matchBy<T extends Row<unknown>>(\n column: string,\n value: Value<GetExtensions<T>>\n): (message: ChangeMessage<T>) => boolean {\n return (message: ChangeMessage<T>) => message.value[column] === value\n}\n","import {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n} from '@electric-sql/client'\nimport type {\n ChangeMessage,\n ControlMessage,\n FetchError,\n MaybePromise,\n Row,\n ShapeStreamOptions,\n} from '@electric-sql/client'\n\ninterface MultiShapeStreamOptions<\n TShapeRows extends {\n [K: string]: Row<unknown>\n } = {\n [K: string]: Row<unknown>\n },\n> {\n shapes: {\n [K in keyof TShapeRows]:\n | ShapeStreamOptions<TShapeRows[K]>\n | ShapeStream<TShapeRows[K]>\n }\n start?: boolean\n checkForUpdatesAfterMs?: number // milliseconds\n}\n\ninterface MultiShapeChangeMessage<\n T extends Row<unknown>,\n ShapeNames extends string,\n> extends ChangeMessage<T> {\n shape: ShapeNames\n}\n\ninterface MultiShapeControlMessage<ShapeNames extends string>\n extends ControlMessage {\n shape: ShapeNames\n}\n\ntype MultiShapeMessage<T extends Row<unknown>, ShapeNames extends string> =\n | MultiShapeChangeMessage<T, ShapeNames>\n | MultiShapeControlMessage<ShapeNames>\n\nexport type MultiShapeMessages<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> = {\n [K in keyof TShapeRows & string]: MultiShapeMessage<TShapeRows[K], K>\n}[keyof TShapeRows & string]\n\nexport interface MultiShapeStreamInterface<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> {\n shapes: { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n checkForUpdatesAfterMs?: number\n\n subscribe(\n callback: (\n messages: MultiShapeMessages<TShapeRows>[]\n ) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ): () => void\n unsubscribeAll(): void\n\n lastSyncedAt(): number | undefined\n lastSynced(): number\n isConnected(): boolean\n isLoading(): boolean\n\n isUpToDate: boolean\n}\n\n/**\n * A multi-shape stream is a stream that can subscribe to multiple shapes.\n * It ensures that all shapes will receive at least an `up-to-date` message from\n * Electric within the `checkForUpdatesAfterMs` interval.\n *\n * @constructor\n * @param {MultiShapeStreamOptions} options - configure the multi-shape stream\n * @example\n * ```ts\n * const multiShapeStream = new MultiShapeStream({\n * shapes: {\n * shape1: {\n * url: 'http://localhost:3000/v1/shape1',\n * },\n * shape2: {\n * url: 'http://localhost:3000/v1/shape2',\n * },\n * },\n * })\n *\n * multiShapeStream.subscribe((msgs) => {\n * console.log(msgs)\n * })\n *\n * // or with ShapeStream instances\n * const multiShapeStream = new MultiShapeStream({\n * shapes: {\n * shape1: new ShapeStream({ url: 'http://localhost:3000/v1/shape1' }),\n * shape2: new ShapeStream({ url: 'http://localhost:3000/v1/shape2' }),\n * },\n * })\n * ```\n */\n\nexport class MultiShapeStream<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> implements MultiShapeStreamInterface<TShapeRows>\n{\n #shapes: { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n #started = false\n checkForUpdatesAfterMs?: number\n\n #checkForUpdatesTimeout?: ReturnType<typeof setTimeout> | undefined\n\n // We keep track of the last lsn of data and up-to-date messages for each shape\n // so that we can skip checkForUpdates if the lsn of the up-to-date message is\n // greater than the last lsn of data.\n #lastDataLsns: { [K in keyof TShapeRows]: number }\n #lastUpToDateLsns: { [K in keyof TShapeRows]: number }\n\n readonly #subscribers = new Map<\n number,\n [\n (messages: MultiShapeMessages<TShapeRows>[]) => MaybePromise<void>,\n ((error: Error) => void) | undefined,\n ]\n >()\n\n constructor(options: MultiShapeStreamOptions<TShapeRows>) {\n const {\n start = true, // By default we start the multi-shape stream\n checkForUpdatesAfterMs = 100, // Force a check for updates after 100ms\n shapes,\n } = options\n this.checkForUpdatesAfterMs = checkForUpdatesAfterMs\n this.#shapes = Object.fromEntries(\n Object.entries(shapes).map(([key, shape]) => [\n key,\n shape instanceof ShapeStream\n ? shape\n : new ShapeStream<TShapeRows[typeof key]>({\n ...shape,\n start: false,\n }),\n ])\n ) as { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n this.#lastDataLsns = Object.fromEntries(\n Object.entries(shapes).map(([key]) => [key, -Infinity])\n ) as { [K in keyof TShapeRows]: number }\n this.#lastUpToDateLsns = Object.fromEntries(\n Object.entries(shapes).map(([key]) => [key, -Infinity])\n ) as { [K in keyof TShapeRows]: number }\n if (start) this.#start()\n }\n\n #start() {\n if (this.#started) throw new Error(`Cannot start multi-shape stream twice`)\n for (const [key, shape] of this.#shapeEntries()) {\n if (shape.hasStarted()) {\n // The multi-shape stream needs to be started together as a whole, and so we\n // have to check that a shape is not already started.\n throw new Error(`Shape ${key} already started`)\n }\n shape.subscribe(\n async (messages) => {\n // Whats the max lsn of the up-to-date messages?\n const upToDateLsns = messages\n .filter(isControlMessage)\n .map(({ headers }) => (headers.global_last_seen_lsn as number) ?? 0)\n if (upToDateLsns.length > 0) {\n const maxUpToDateLsn = Math.max(...upToDateLsns)\n const lastMaxUpToDateLsn = this.#lastUpToDateLsns[key]\n if (maxUpToDateLsn > lastMaxUpToDateLsn) {\n this.#lastUpToDateLsns[key] = maxUpToDateLsn\n }\n }\n\n // Whats the max lsn of the data messages?\n const dataLsns = messages\n .filter(isChangeMessage)\n .map(({ headers }) => (headers.lsn as number) ?? 0)\n if (dataLsns.length > 0) {\n const maxDataLsn = Math.max(...dataLsns)\n const lastMaxDataLsn = this.#lastDataLsns[key]\n if (maxDataLsn > lastMaxDataLsn) {\n this.#lastDataLsns[key] = maxDataLsn\n }\n // There is new data, so we need to schedule a check for updates on\n // other shapes\n this.#scheduleCheckForUpdates()\n }\n\n // Publish the messages to the multi-shape stream subscribers\n const multiShapeMessages = messages.map(\n (message) =>\n ({\n ...message,\n shape: key,\n }) as MultiShapeMessages<TShapeRows>\n )\n await this._publish(multiShapeMessages)\n },\n (error) => this.#onError(error)\n )\n }\n this.#started = true\n }\n\n #scheduleCheckForUpdates() {\n this.#checkForUpdatesTimeout ??= setTimeout(() => {\n this.#checkForUpdates()\n this.#checkForUpdatesTimeout = undefined\n }, this.checkForUpdatesAfterMs)\n }\n\n async #checkForUpdates() {\n const maxDataLsn = Math.max(...Object.values(this.#lastDataLsns))\n const refreshPromises = this.#shapeEntries()\n .filter(([key]) => {\n // We only need to refresh shapes that have not seen an up-to-date message\n // lower than the max lsn of the data messages we have received.\n const lastUpToDateLsn = this.#lastUpToDateLsns[key]\n return lastUpToDateLsn < maxDataLsn\n })\n .map(([_, shape]) => {\n return shape.forceDisconnectAndRefresh()\n })\n await Promise.all(refreshPromises)\n }\n\n #onError(error: Error) {\n // TODO: we probably want to disconnect all shapes here on the first error\n this.#subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n protected async _publish(\n messages: MultiShapeMessages<TShapeRows>[]\n ): Promise<void> {\n await Promise.all(\n Array.from(this.#subscribers.values()).map(async ([callback, __]) => {\n try {\n await callback(messages)\n } catch (err) {\n queueMicrotask(() => {\n throw err\n })\n }\n })\n )\n }\n\n /**\n * Returns an array of the shape entries.\n * Ensures that the shape entries are typed, as `Object.entries`\n * will not type the entries correctly.\n */\n #shapeEntries() {\n return Object.entries(this.#shapes) as [\n keyof TShapeRows & string,\n ShapeStream<TShapeRows[string]>,\n ][]\n }\n\n /**\n * The ShapeStreams that are being subscribed to.\n */\n get shapes() {\n return this.#shapes\n }\n\n subscribe(\n callback: (\n messages: MultiShapeMessages<TShapeRows>[]\n ) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n\n this.#subscribers.set(subscriptionId, [callback, onError])\n if (!this.#started) this.#start()\n\n return () => {\n this.#subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.#subscribers.clear()\n }\n\n /** Unix time at which we last synced. Undefined when `isLoading` is true. */\n lastSyncedAt(): number | undefined {\n // Min of all the lastSyncedAt values\n return Math.min(\n ...this.#shapeEntries().map(\n ([_, shape]) => shape.lastSyncedAt() ?? Infinity\n )\n )\n }\n\n /** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */\n lastSynced(): number {\n const lastSyncedAt = this.lastSyncedAt()\n if (lastSyncedAt === undefined) return Infinity\n return Date.now() - lastSyncedAt\n }\n\n /** Indicates if we are connected to the Electric sync service. */\n isConnected(): boolean {\n return this.#shapeEntries().every(([_, shape]) => shape.isConnected())\n }\n\n /** True during initial fetch. False afterwise. */\n isLoading(): boolean {\n return this.#shapeEntries().some(([_, shape]) => shape.isLoading())\n }\n\n get isUpToDate() {\n return this.#shapeEntries().every(([_, shape]) => shape.isUpToDate)\n }\n}\n\n/**\n * A transactional multi-shape stream is a multi-shape stream that emits the\n * messages in transactional batches, ensuring that all shapes will receive\n * at least an `up-to-date` message from Electric within the `checkForUpdatesAfterMs`\n * interval.\n * It uses the `lsn` metadata to infer transaction boundaries, and the `op_position`\n * metadata to sort the messages within a transaction.\n *\n * @constructor\n * @param {MultiShapeStreamOptions} options - configure the multi-shape stream\n * @example\n * ```ts\n * const transactionalMultiShapeStream = new TransactionalMultiShapeStream({\n * shapes: {\n * shape1: {\n * url: 'http://localhost:3000/v1/shape1',\n * },\n * shape2: {\n * url: 'http://localhost:3000/v1/shape2',\n * },\n * },\n * })\n *\n * transactionalMultiShapeStream.subscribe((msgs) => {\n * console.log(msgs)\n * })\n *\n * // or with ShapeStream instances\n * const transactionalMultiShapeStream = new TransactionalMultiShapeStream({\n * shapes: {\n * shape1: new ShapeStream({ url: 'http://localhost:3000/v1/shape1' }),\n * shape2: new ShapeStream({ url: 'http://localhost:3000/v1/shape2' }),\n * },\n * })\n * ```\n */\n\nexport class TransactionalMultiShapeStream<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> extends MultiShapeStream<TShapeRows> {\n #changeMessages = new Map<number, MultiShapeMessage<Row<unknown>, string>[]>()\n #completeLsns: {\n [K in keyof TShapeRows]: number\n }\n\n constructor(options: MultiShapeStreamOptions<TShapeRows>) {\n super(options)\n this.#completeLsns = Object.fromEntries(\n Object.entries(options.shapes).map(([key]) => [key, -Infinity])\n ) as { [K in keyof TShapeRows]: number }\n }\n\n #getLowestCompleteLsn() {\n return Math.min(...Object.values(this.#completeLsns))\n }\n\n protected async _publish(\n messages: MultiShapeMessages<TShapeRows>[]\n ): Promise<void> {\n this.#accumulate(messages)\n const lowestCompleteLsn = this.#getLowestCompleteLsn()\n const lsnsToPublish = [...this.#changeMessages.keys()].filter(\n (lsn) => lsn <= lowestCompleteLsn\n )\n const messagesToPublish = lsnsToPublish\n .sort((a, b) => a - b)\n .map((lsn) =>\n this.#changeMessages.get(lsn)?.sort((a, b) => {\n const { headers: aHeaders } = a\n const { headers: bHeaders } = b\n if (\n typeof aHeaders.op_position !== `number` ||\n typeof bHeaders.op_position !== `number`\n ) {\n return 0 // op_position is not present on the snapshot message\n }\n return aHeaders.op_position - bHeaders.op_position\n })\n )\n .filter((messages) => messages !== undefined)\n .flat() as MultiShapeMessages<TShapeRows>[]\n lsnsToPublish.forEach((lsn) => {\n this.#changeMessages.delete(lsn)\n })\n if (messagesToPublish.length > 0) {\n await super._publish(messagesToPublish)\n }\n }\n\n #accumulate(messages: MultiShapeMessages<TShapeRows>[]) {\n const isUpToDate = this.isUpToDate\n messages.forEach((message) => {\n const { shape, headers } = message\n if (isChangeMessage(message)) {\n // The snapshot message does not have an lsn, so we use 0\n const lsn = typeof headers.lsn === `number` ? headers.lsn : 0\n if (!this.#changeMessages.has(lsn)) {\n this.#changeMessages.set(lsn, [])\n }\n this.#changeMessages.get(lsn)?.push(message)\n if (\n isUpToDate && // All shapes must be up to date\n typeof headers.last === `boolean` &&\n headers.last === true\n ) {\n this.#completeLsns[shape] = Math.max(this.#completeLsns[shape], lsn)\n }\n } else if (isControlMessage(message)) {\n if (headers.control === `up-to-date`) {\n if (typeof headers.global_last_seen_lsn !== `number`) {\n throw new Error(`global_last_seen_lsn is not a number`)\n }\n this.#completeLsns[shape] = Math.max(\n this.#completeLsns[shape],\n headers.global_last_seen_lsn\n )\n }\n }\n })\n }\n}\n"],"mappings":"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","names":["message","import_client","messages"]}
|
|
1
|
+
{"version":3,"sources":["../../src/index.ts","../../src/match.ts","../../src/bigint-utils.ts","../../src/multi-shape-stream.ts"],"sourcesContent":["export * from './match'\nexport * from './multi-shape-stream'\n","import {\n isChangeMessage,\n type ShapeStreamInterface,\n type ChangeMessage,\n type GetExtensions,\n type Operation,\n type Row,\n type Value,\n type Message,\n} from '@electric-sql/client'\n\nexport function matchStream<T extends Row<unknown>>(\n stream: ShapeStreamInterface<T>,\n operations: Array<Operation>,\n matchFn: (message: ChangeMessage<T>) => boolean,\n timeout = 60000 // ms\n): Promise<ChangeMessage<T>> {\n return new Promise<ChangeMessage<T>>((resolve, reject) => {\n const unsubscribe: () => void = stream.subscribe(\n (messages: Array<unknown>) => {\n const message = messages\n .filter((msg): msg is ChangeMessage<T> =>\n isChangeMessage(msg as Message<Row<never>>)\n )\n .find((message) => {\n const operation: Operation = message.headers.operation\n\n return operations.includes(operation) && matchFn(message)\n })\n\n if (message) {\n return finish(message)\n }\n }\n )\n\n const timeoutId: NodeJS.Timeout = setTimeout(() => {\n const msg: string = `matchStream timed out after ${timeout}ms`\n\n console.error(msg)\n\n reject(msg)\n }, timeout)\n\n function finish(message: ChangeMessage<T>): void {\n clearTimeout(timeoutId)\n\n unsubscribe()\n\n return resolve(message)\n }\n })\n}\n\nexport function matchBy<T extends Row<unknown>>(\n column: string,\n value: Value<GetExtensions<T>>\n): (message: ChangeMessage<T>) => boolean {\n return (message: ChangeMessage<T>) => message.value[column] === value\n}\n","export function bigIntMax(nums: Array<bigint | number>): bigint {\n return BigInt(nums.reduce((m, e) => (e > m ? e : m)))\n}\n\nexport function bigIntMin(nums: Array<bigint | number>): bigint {\n return BigInt(nums.reduce((m, e) => (e < m ? e : m)))\n}\n\nexport function bigIntCompare(a: bigint, b: bigint): 1 | -1 | 0 {\n return a > b ? 1 : a < b ? -1 : 0\n}\n","import { bigIntCompare, bigIntMax, bigIntMin } from './bigint-utils'\nimport {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n} from '@electric-sql/client'\nimport type {\n ChangeMessage,\n ControlMessage,\n FetchError,\n MaybePromise,\n Row,\n ShapeStreamOptions,\n} from '@electric-sql/client'\n\ninterface MultiShapeStreamOptions<\n TShapeRows extends {\n [K: string]: Row<unknown>\n } = {\n [K: string]: Row<unknown>\n },\n> {\n shapes: {\n [K in keyof TShapeRows]:\n | ShapeStreamOptions<TShapeRows[K]>\n | ShapeStream<TShapeRows[K]>\n }\n start?: boolean\n checkForUpdatesAfterMs?: number // milliseconds\n}\n\ninterface MultiShapeChangeMessage<\n T extends Row<unknown>,\n ShapeNames extends string,\n> extends ChangeMessage<T> {\n shape: ShapeNames\n}\n\ninterface MultiShapeControlMessage<ShapeNames extends string>\n extends ControlMessage {\n shape: ShapeNames\n}\n\ntype MultiShapeMessage<T extends Row<unknown>, ShapeNames extends string> =\n | MultiShapeChangeMessage<T, ShapeNames>\n | MultiShapeControlMessage<ShapeNames>\n\nexport type MultiShapeMessages<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> = {\n [K in keyof TShapeRows & string]: MultiShapeMessage<TShapeRows[K], K>\n}[keyof TShapeRows & string]\n\nexport interface MultiShapeStreamInterface<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> {\n shapes: { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n checkForUpdatesAfterMs?: number\n\n subscribe(\n callback: (\n messages: MultiShapeMessages<TShapeRows>[]\n ) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ): () => void\n unsubscribeAll(): void\n\n lastSyncedAt(): number | undefined\n lastSynced(): number\n isConnected(): boolean\n isLoading(): boolean\n\n isUpToDate: boolean\n}\n\n/**\n * A multi-shape stream is a stream that can subscribe to multiple shapes.\n * It ensures that all shapes will receive at least an `up-to-date` message from\n * Electric within the `checkForUpdatesAfterMs` interval.\n *\n * @constructor\n * @param {MultiShapeStreamOptions} options - configure the multi-shape stream\n * @example\n * ```ts\n * const multiShapeStream = new MultiShapeStream({\n * shapes: {\n * shape1: {\n * url: 'http://localhost:3000/v1/shape1',\n * },\n * shape2: {\n * url: 'http://localhost:3000/v1/shape2',\n * },\n * },\n * })\n *\n * multiShapeStream.subscribe((msgs) => {\n * console.log(msgs)\n * })\n *\n * // or with ShapeStream instances\n * const multiShapeStream = new MultiShapeStream({\n * shapes: {\n * shape1: new ShapeStream({ url: 'http://localhost:3000/v1/shape1' }),\n * shape2: new ShapeStream({ url: 'http://localhost:3000/v1/shape2' }),\n * },\n * })\n * ```\n */\n\nexport class MultiShapeStream<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> implements MultiShapeStreamInterface<TShapeRows>\n{\n #shapes: { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n #started = false\n checkForUpdatesAfterMs?: number\n\n #checkForUpdatesTimeout?: ReturnType<typeof setTimeout> | undefined\n\n // We keep track of the last lsn of data and up-to-date messages for each shape\n // so that we can skip checkForUpdates if the lsn of the up-to-date message is\n // greater than the last lsn of data.\n #lastDataLsns: { [K in keyof TShapeRows]: bigint }\n #lastUpToDateLsns: { [K in keyof TShapeRows]: bigint }\n\n readonly #subscribers = new Map<\n number,\n [\n (messages: MultiShapeMessages<TShapeRows>[]) => MaybePromise<void>,\n ((error: Error) => void) | undefined,\n ]\n >()\n\n constructor(options: MultiShapeStreamOptions<TShapeRows>) {\n const {\n start = true, // By default we start the multi-shape stream\n checkForUpdatesAfterMs = 100, // Force a check for updates after 100ms\n shapes,\n } = options\n this.checkForUpdatesAfterMs = checkForUpdatesAfterMs\n this.#shapes = Object.fromEntries(\n Object.entries(shapes).map(([key, shape]) => [\n key,\n shape instanceof ShapeStream\n ? shape\n : new ShapeStream<TShapeRows[typeof key]>({\n ...shape,\n start: false,\n }),\n ])\n ) as { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n this.#lastDataLsns = Object.fromEntries(\n Object.entries(shapes).map(([key]) => [key, BigInt(-1)])\n ) as { [K in keyof TShapeRows]: bigint }\n this.#lastUpToDateLsns = Object.fromEntries(\n Object.entries(shapes).map(([key]) => [key, BigInt(-1)])\n ) as { [K in keyof TShapeRows]: bigint }\n if (start) this.#start()\n }\n\n #start() {\n if (this.#started) throw new Error(`Cannot start multi-shape stream twice`)\n for (const [key, shape] of this.#shapeEntries()) {\n if (shape.hasStarted()) {\n // The multi-shape stream needs to be started together as a whole, and so we\n // have to check that a shape is not already started.\n throw new Error(`Shape ${key} already started`)\n }\n shape.subscribe(\n async (messages) => {\n // Whats the max lsn of the up-to-date messages?\n const upToDateLsns = messages\n .filter(isControlMessage)\n .map(({ headers }) =>\n typeof headers.global_last_seen_lsn === `string`\n ? BigInt(headers.global_last_seen_lsn)\n : BigInt(0)\n )\n if (upToDateLsns.length > 0) {\n const maxUpToDateLsn = bigIntMax(upToDateLsns)\n const lastMaxUpToDateLsn = this.#lastUpToDateLsns[key]\n if (maxUpToDateLsn > lastMaxUpToDateLsn) {\n this.#lastUpToDateLsns[key] = maxUpToDateLsn\n }\n }\n\n // Whats the max lsn of the data messages?\n const dataLsns = messages\n .filter(isChangeMessage)\n .map(({ headers }) =>\n typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)\n )\n if (dataLsns.length > 0) {\n const maxDataLsn = bigIntMax(dataLsns)\n const lastMaxDataLsn = this.#lastDataLsns[key]\n if (maxDataLsn > lastMaxDataLsn) {\n this.#lastDataLsns[key] = maxDataLsn\n }\n // There is new data, so we need to schedule a check for updates on\n // other shapes\n this.#scheduleCheckForUpdates()\n }\n\n // Publish the messages to the multi-shape stream subscribers\n const multiShapeMessages = messages.map(\n (message) =>\n ({\n ...message,\n shape: key,\n }) as MultiShapeMessages<TShapeRows>\n )\n await this._publish(multiShapeMessages)\n },\n (error) => this.#onError(error)\n )\n }\n this.#started = true\n }\n\n #scheduleCheckForUpdates() {\n this.#checkForUpdatesTimeout ??= setTimeout(() => {\n this.#checkForUpdates()\n this.#checkForUpdatesTimeout = undefined\n }, this.checkForUpdatesAfterMs)\n }\n\n async #checkForUpdates() {\n const maxDataLsn = bigIntMax(Object.values(this.#lastDataLsns))\n const refreshPromises = this.#shapeEntries()\n .filter(([key]) => {\n // We only need to refresh shapes that have not seen an up-to-date message\n // lower than the max lsn of the data messages we have received.\n const lastUpToDateLsn = this.#lastUpToDateLsns[key]\n return lastUpToDateLsn < maxDataLsn\n })\n .map(([_, shape]) => {\n return shape.forceDisconnectAndRefresh()\n })\n await Promise.all(refreshPromises)\n }\n\n #onError(error: Error) {\n // TODO: we probably want to disconnect all shapes here on the first error\n this.#subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n protected async _publish(\n messages: MultiShapeMessages<TShapeRows>[]\n ): Promise<void> {\n await Promise.all(\n Array.from(this.#subscribers.values()).map(async ([callback, __]) => {\n try {\n await callback(messages)\n } catch (err) {\n queueMicrotask(() => {\n throw err\n })\n }\n })\n )\n }\n\n /**\n * Returns an array of the shape entries.\n * Ensures that the shape entries are typed, as `Object.entries`\n * will not type the entries correctly.\n */\n #shapeEntries() {\n return Object.entries(this.#shapes) as [\n keyof TShapeRows & string,\n ShapeStream<TShapeRows[string]>,\n ][]\n }\n\n /**\n * The ShapeStreams that are being subscribed to.\n */\n get shapes() {\n return this.#shapes\n }\n\n subscribe(\n callback: (\n messages: MultiShapeMessages<TShapeRows>[]\n ) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n\n this.#subscribers.set(subscriptionId, [callback, onError])\n if (!this.#started) this.#start()\n\n return () => {\n this.#subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.#subscribers.clear()\n }\n\n /** Unix time at which we last synced. Undefined when `isLoading` is true. */\n lastSyncedAt(): number | undefined {\n // Min of all the lastSyncedAt values\n const shapeEntries = this.#shapeEntries()\n if (shapeEntries.length === 0) return\n return shapeEntries.reduce((minLastSyncedAt, [_, shape]) => {\n return Math.min(minLastSyncedAt, shape.lastSyncedAt() ?? Infinity)\n }, Infinity)\n }\n\n /** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */\n lastSynced(): number {\n const lastSyncedAt = this.lastSyncedAt()\n if (lastSyncedAt === undefined) return Infinity\n return Date.now() - lastSyncedAt\n }\n\n /** Indicates if we are connected to the Electric sync service. */\n isConnected(): boolean {\n return this.#shapeEntries().every(([_, shape]) => shape.isConnected())\n }\n\n /** True during initial fetch. False afterwise. */\n isLoading(): boolean {\n return this.#shapeEntries().some(([_, shape]) => shape.isLoading())\n }\n\n get isUpToDate() {\n return this.#shapeEntries().every(([_, shape]) => shape.isUpToDate)\n }\n}\n\n/**\n * A transactional multi-shape stream is a multi-shape stream that emits the\n * messages in transactional batches, ensuring that all shapes will receive\n * at least an `up-to-date` message from Electric within the `checkForUpdatesAfterMs`\n * interval.\n * It uses the `lsn` metadata to infer transaction boundaries, and the `op_position`\n * metadata to sort the messages within a transaction.\n *\n * @constructor\n * @param {MultiShapeStreamOptions} options - configure the multi-shape stream\n * @example\n * ```ts\n * const transactionalMultiShapeStream = new TransactionalMultiShapeStream({\n * shapes: {\n * shape1: {\n * url: 'http://localhost:3000/v1/shape1',\n * },\n * shape2: {\n * url: 'http://localhost:3000/v1/shape2',\n * },\n * },\n * })\n *\n * transactionalMultiShapeStream.subscribe((msgs) => {\n * console.log(msgs)\n * })\n *\n * // or with ShapeStream instances\n * const transactionalMultiShapeStream = new TransactionalMultiShapeStream({\n * shapes: {\n * shape1: new ShapeStream({ url: 'http://localhost:3000/v1/shape1' }),\n * shape2: new ShapeStream({ url: 'http://localhost:3000/v1/shape2' }),\n * },\n * })\n * ```\n */\n\nexport class TransactionalMultiShapeStream<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> extends MultiShapeStream<TShapeRows> {\n #changeMessages = new Map<bigint, MultiShapeMessage<Row<unknown>, string>[]>()\n #completeLsns: {\n [K in keyof TShapeRows]: bigint\n }\n\n constructor(options: MultiShapeStreamOptions<TShapeRows>) {\n super(options)\n this.#completeLsns = Object.fromEntries(\n Object.entries(options.shapes).map(([key]) => [key, BigInt(-1)])\n ) as { [K in keyof TShapeRows]: bigint }\n }\n\n #getLowestCompleteLsn() {\n return bigIntMin(Object.values(this.#completeLsns))\n }\n\n protected async _publish(\n messages: MultiShapeMessages<TShapeRows>[]\n ): Promise<void> {\n this.#accumulate(messages)\n const lowestCompleteLsn = this.#getLowestCompleteLsn()\n const lsnsToPublish = [...this.#changeMessages.keys()].filter(\n (lsn) => lsn <= lowestCompleteLsn\n )\n const messagesToPublish = lsnsToPublish\n .sort((a, b) => bigIntCompare(a, b))\n .map((lsn) =>\n this.#changeMessages.get(lsn)?.sort((a, b) => {\n const { headers: aHeaders } = a\n const { headers: bHeaders } = b\n if (\n typeof aHeaders.op_position !== `number` ||\n typeof bHeaders.op_position !== `number`\n ) {\n return 0 // op_position is not present on the snapshot message\n }\n return aHeaders.op_position - bHeaders.op_position\n })\n )\n .filter((messages) => messages !== undefined)\n .flat() as MultiShapeMessages<TShapeRows>[]\n lsnsToPublish.forEach((lsn) => {\n this.#changeMessages.delete(lsn)\n })\n if (messagesToPublish.length > 0) {\n await super._publish(messagesToPublish)\n }\n }\n\n #accumulate(messages: MultiShapeMessages<TShapeRows>[]) {\n const isUpToDate = this.isUpToDate\n messages.forEach((message) => {\n const { shape, headers } = message\n if (isChangeMessage(message)) {\n // The snapshot message does not have an lsn, so we use 0\n const lsn =\n typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)\n if (!this.#changeMessages.has(lsn)) {\n this.#changeMessages.set(lsn, [])\n }\n this.#changeMessages.get(lsn)?.push(message)\n if (\n isUpToDate && // All shapes must be up to date\n typeof headers.last === `boolean` &&\n headers.last === true\n ) {\n this.#completeLsns[shape] = bigIntMax([\n this.#completeLsns[shape],\n lsn,\n ])\n }\n } else if (isControlMessage(message)) {\n if (headers.control === `up-to-date`) {\n if (typeof headers.global_last_seen_lsn !== `string`) {\n throw new Error(`global_last_seen_lsn is not a number`)\n }\n this.#completeLsns[shape] = bigIntMax([\n this.#completeLsns[shape],\n BigInt(headers.global_last_seen_lsn),\n ])\n }\n }\n })\n }\n}\n"],"mappings":"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","names":["message","import_client","messages"]}
|
package/dist/index.browser.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
1
|
+
var X=Object.defineProperty,Y=Object.defineProperties;var Z=Object.getOwnPropertyDescriptors;var L=Object.getOwnPropertySymbols,ee=Object.getPrototypeOf,se=Object.prototype.hasOwnProperty,te=Object.prototype.propertyIsEnumerable,ne=Reflect.get;var B=t=>{throw TypeError(t)};var D=(t,e,s)=>e in t?X(t,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):t[e]=s,K=(t,e)=>{for(var s in e||(e={}))se.call(e,s)&&D(t,s,e[s]);if(L)for(var s of L(e))te.call(e,s)&&D(t,s,e[s]);return t},E=(t,e)=>Y(t,Z(e));var v=(t,e,s)=>e.has(t)||B("Cannot "+s);var a=(t,e,s)=>(v(t,e,"read from private field"),s?s.call(t):e.get(t)),g=(t,e,s)=>e.has(t)?B("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,s),S=(t,e,s,n)=>(v(t,e,"write to private field"),n?n.call(t,s):e.set(t,s),s),u=(t,e,s)=>(v(t,e,"access private method"),s);var P=(t,e,s)=>ne(ee(t),s,e);var y=(t,e,s)=>new Promise((n,h)=>{var r=p=>{try{i(s.next(p))}catch(l){h(l)}},o=p=>{try{i(s.throw(p))}catch(l){h(l)}},i=p=>p.done?n(p.value):Promise.resolve(p.value).then(r,o);i((s=s.apply(t,e)).next())});import{isChangeMessage as ae}from"@electric-sql/client";function re(t,e,s,n=6e4){return new Promise((h,r)=>{let o=t.subscribe(l=>{let C=l.filter(b=>ae(b)).find(b=>{let W=b.headers.operation;return e.includes(W)&&s(b)});if(C)return p(C)}),i=setTimeout(()=>{let l=`matchStream timed out after ${n}ms`;console.error(l),r(l)},n);function p(l){return clearTimeout(i),o(),h(l)}})}function he(t,e){return s=>s.value[t]===e}function R(t){return BigInt(t.reduce((e,s)=>s>e?s:e))}function j(t){return BigInt(t.reduce((e,s)=>s<e?s:e))}function F(t,e){return t>e?1:t<e?-1:0}import{ShapeStream as N,isChangeMessage as G,isControlMessage as H}from"@electric-sql/client";var k,x,I,M,T,d,c,O,V,$,J,w,A=class{constructor(e){g(this,c);g(this,k);g(this,x,!1);g(this,I);g(this,M);g(this,T);g(this,d,new Map);let{start:s=!0,checkForUpdatesAfterMs:n=100,shapes:h}=e;this.checkForUpdatesAfterMs=n,S(this,k,Object.fromEntries(Object.entries(h).map(([r,o])=>[r,o instanceof N?o:new N(E(K({},o),{start:!1}))]))),S(this,M,Object.fromEntries(Object.entries(h).map(([r])=>[r,BigInt(-1)]))),S(this,T,Object.fromEntries(Object.entries(h).map(([r])=>[r,BigInt(-1)]))),s&&u(this,c,O).call(this)}_publish(e){return y(this,null,function*(){yield Promise.all(Array.from(a(this,d).values()).map(h=>y(this,[h],function*([s,n]){try{yield s(e)}catch(r){queueMicrotask(()=>{throw r})}})))})}get shapes(){return a(this,k)}subscribe(e,s){let n=Math.random();return a(this,d).set(n,[e,s]),a(this,x)||u(this,c,O).call(this),()=>{a(this,d).delete(n)}}unsubscribeAll(){a(this,d).clear()}lastSyncedAt(){let e=u(this,c,w).call(this);if(e.length!==0)return e.reduce((s,[n,h])=>{var r;return Math.min(s,(r=h.lastSyncedAt())!=null?r:1/0)},1/0)}lastSynced(){let e=this.lastSyncedAt();return e===void 0?1/0:Date.now()-e}isConnected(){return u(this,c,w).call(this).every(([e,s])=>s.isConnected())}isLoading(){return u(this,c,w).call(this).some(([e,s])=>s.isLoading())}get isUpToDate(){return u(this,c,w).call(this).every(([e,s])=>s.isUpToDate)}};k=new WeakMap,x=new WeakMap,I=new WeakMap,M=new WeakMap,T=new WeakMap,d=new WeakMap,c=new WeakSet,O=function(){if(a(this,x))throw new Error("Cannot start multi-shape stream twice");for(let[e,s]of u(this,c,w).call(this)){if(s.hasStarted())throw new Error(`Shape ${e} already started`);s.subscribe(n=>y(this,null,function*(){let h=n.filter(H).map(({headers:i})=>typeof i.global_last_seen_lsn=="string"?BigInt(i.global_last_seen_lsn):BigInt(0));if(h.length>0){let i=R(h),p=a(this,T)[e];i>p&&(a(this,T)[e]=i)}let r=n.filter(G).map(({headers:i})=>typeof i.lsn=="string"?BigInt(i.lsn):BigInt(0));if(r.length>0){let i=R(r),p=a(this,M)[e];i>p&&(a(this,M)[e]=i),u(this,c,V).call(this)}let o=n.map(i=>E(K({},i),{shape:e}));yield this._publish(o)}),n=>u(this,c,J).call(this,n))}S(this,x,!0)},V=function(){var e;(e=a(this,I))!=null||S(this,I,setTimeout(()=>{u(this,c,$).call(this),S(this,I,void 0)},this.checkForUpdatesAfterMs))},$=function(){return y(this,null,function*(){let e=R(Object.values(a(this,M))),s=u(this,c,w).call(this).filter(([n])=>a(this,T)[n]<e).map(([n,h])=>h.forceDisconnectAndRefresh());yield Promise.all(s)})},J=function(e){a(this,d).forEach(([s,n])=>{n==null||n(e)})},w=function(){return Object.entries(a(this,k))};var f,m,_,z,Q,U=class U extends A{constructor(s){super(s);g(this,_);g(this,f,new Map);g(this,m);S(this,m,Object.fromEntries(Object.entries(s.shapes).map(([n])=>[n,BigInt(-1)])))}_publish(s){return y(this,null,function*(){u(this,_,Q).call(this,s);let n=u(this,_,z).call(this),h=[...a(this,f).keys()].filter(o=>o<=n),r=h.sort((o,i)=>F(o,i)).map(o=>{var i;return(i=a(this,f).get(o))==null?void 0:i.sort((p,l)=>{let{headers:C}=p,{headers:b}=l;return typeof C.op_position!="number"||typeof b.op_position!="number"?0:C.op_position-b.op_position})}).filter(o=>o!==void 0).flat();h.forEach(o=>{a(this,f).delete(o)}),r.length>0&&(yield P(U.prototype,this,"_publish").call(this,r))})}};f=new WeakMap,m=new WeakMap,_=new WeakSet,z=function(){return j(Object.values(a(this,m)))},Q=function(s){let n=this.isUpToDate;s.forEach(h=>{var i;let{shape:r,headers:o}=h;if(G(h)){let p=typeof o.lsn=="string"?BigInt(o.lsn):BigInt(0);a(this,f).has(p)||a(this,f).set(p,[]),(i=a(this,f).get(p))==null||i.push(h),n&&typeof o.last=="boolean"&&o.last===!0&&(a(this,m)[r]=R([a(this,m)[r],p]))}else if(H(h)&&o.control==="up-to-date"){if(typeof o.global_last_seen_lsn!="string")throw new Error("global_last_seen_lsn is not a number");a(this,m)[r]=R([a(this,m)[r],BigInt(o.global_last_seen_lsn)])}})};var q=U;export{A as MultiShapeStream,q as TransactionalMultiShapeStream,he as matchBy,re as matchStream};
|
|
2
2
|
//# sourceMappingURL=index.browser.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/match.ts","../src/multi-shape-stream.ts"],"sourcesContent":["import {\n isChangeMessage,\n type ShapeStreamInterface,\n type ChangeMessage,\n type GetExtensions,\n type Operation,\n type Row,\n type Value,\n type Message,\n} from '@electric-sql/client'\n\nexport function matchStream<T extends Row<unknown>>(\n stream: ShapeStreamInterface<T>,\n operations: Array<Operation>,\n matchFn: (message: ChangeMessage<T>) => boolean,\n timeout = 60000 // ms\n): Promise<ChangeMessage<T>> {\n return new Promise<ChangeMessage<T>>((resolve, reject) => {\n const unsubscribe: () => void = stream.subscribe(\n (messages: Array<unknown>) => {\n const message = messages\n .filter((msg): msg is ChangeMessage<T> =>\n isChangeMessage(msg as Message<Row<never>>)\n )\n .find((message) => {\n const operation: Operation = message.headers.operation\n\n return operations.includes(operation) && matchFn(message)\n })\n\n if (message) {\n return finish(message)\n }\n }\n )\n\n const timeoutId: NodeJS.Timeout = setTimeout(() => {\n const msg: string = `matchStream timed out after ${timeout}ms`\n\n console.error(msg)\n\n reject(msg)\n }, timeout)\n\n function finish(message: ChangeMessage<T>): void {\n clearTimeout(timeoutId)\n\n unsubscribe()\n\n return resolve(message)\n }\n })\n}\n\nexport function matchBy<T extends Row<unknown>>(\n column: string,\n value: Value<GetExtensions<T>>\n): (message: ChangeMessage<T>) => boolean {\n return (message: ChangeMessage<T>) => message.value[column] === value\n}\n","import {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n} from '@electric-sql/client'\nimport type {\n ChangeMessage,\n ControlMessage,\n FetchError,\n MaybePromise,\n Row,\n ShapeStreamOptions,\n} from '@electric-sql/client'\n\ninterface MultiShapeStreamOptions<\n TShapeRows extends {\n [K: string]: Row<unknown>\n } = {\n [K: string]: Row<unknown>\n },\n> {\n shapes: {\n [K in keyof TShapeRows]:\n | ShapeStreamOptions<TShapeRows[K]>\n | ShapeStream<TShapeRows[K]>\n }\n start?: boolean\n checkForUpdatesAfterMs?: number // milliseconds\n}\n\ninterface MultiShapeChangeMessage<\n T extends Row<unknown>,\n ShapeNames extends string,\n> extends ChangeMessage<T> {\n shape: ShapeNames\n}\n\ninterface MultiShapeControlMessage<ShapeNames extends string>\n extends ControlMessage {\n shape: ShapeNames\n}\n\ntype MultiShapeMessage<T extends Row<unknown>, ShapeNames extends string> =\n | MultiShapeChangeMessage<T, ShapeNames>\n | MultiShapeControlMessage<ShapeNames>\n\nexport type MultiShapeMessages<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> = {\n [K in keyof TShapeRows & string]: MultiShapeMessage<TShapeRows[K], K>\n}[keyof TShapeRows & string]\n\nexport interface MultiShapeStreamInterface<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> {\n shapes: { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n checkForUpdatesAfterMs?: number\n\n subscribe(\n callback: (\n messages: MultiShapeMessages<TShapeRows>[]\n ) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ): () => void\n unsubscribeAll(): void\n\n lastSyncedAt(): number | undefined\n lastSynced(): number\n isConnected(): boolean\n isLoading(): boolean\n\n isUpToDate: boolean\n}\n\n/**\n * A multi-shape stream is a stream that can subscribe to multiple shapes.\n * It ensures that all shapes will receive at least an `up-to-date` message from\n * Electric within the `checkForUpdatesAfterMs` interval.\n *\n * @constructor\n * @param {MultiShapeStreamOptions} options - configure the multi-shape stream\n * @example\n * ```ts\n * const multiShapeStream = new MultiShapeStream({\n * shapes: {\n * shape1: {\n * url: 'http://localhost:3000/v1/shape1',\n * },\n * shape2: {\n * url: 'http://localhost:3000/v1/shape2',\n * },\n * },\n * })\n *\n * multiShapeStream.subscribe((msgs) => {\n * console.log(msgs)\n * })\n *\n * // or with ShapeStream instances\n * const multiShapeStream = new MultiShapeStream({\n * shapes: {\n * shape1: new ShapeStream({ url: 'http://localhost:3000/v1/shape1' }),\n * shape2: new ShapeStream({ url: 'http://localhost:3000/v1/shape2' }),\n * },\n * })\n * ```\n */\n\nexport class MultiShapeStream<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> implements MultiShapeStreamInterface<TShapeRows>\n{\n #shapes: { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n #started = false\n checkForUpdatesAfterMs?: number\n\n #checkForUpdatesTimeout?: ReturnType<typeof setTimeout> | undefined\n\n // We keep track of the last lsn of data and up-to-date messages for each shape\n // so that we can skip checkForUpdates if the lsn of the up-to-date message is\n // greater than the last lsn of data.\n #lastDataLsns: { [K in keyof TShapeRows]: number }\n #lastUpToDateLsns: { [K in keyof TShapeRows]: number }\n\n readonly #subscribers = new Map<\n number,\n [\n (messages: MultiShapeMessages<TShapeRows>[]) => MaybePromise<void>,\n ((error: Error) => void) | undefined,\n ]\n >()\n\n constructor(options: MultiShapeStreamOptions<TShapeRows>) {\n const {\n start = true, // By default we start the multi-shape stream\n checkForUpdatesAfterMs = 100, // Force a check for updates after 100ms\n shapes,\n } = options\n this.checkForUpdatesAfterMs = checkForUpdatesAfterMs\n this.#shapes = Object.fromEntries(\n Object.entries(shapes).map(([key, shape]) => [\n key,\n shape instanceof ShapeStream\n ? shape\n : new ShapeStream<TShapeRows[typeof key]>({\n ...shape,\n start: false,\n }),\n ])\n ) as { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n this.#lastDataLsns = Object.fromEntries(\n Object.entries(shapes).map(([key]) => [key, -Infinity])\n ) as { [K in keyof TShapeRows]: number }\n this.#lastUpToDateLsns = Object.fromEntries(\n Object.entries(shapes).map(([key]) => [key, -Infinity])\n ) as { [K in keyof TShapeRows]: number }\n if (start) this.#start()\n }\n\n #start() {\n if (this.#started) throw new Error(`Cannot start multi-shape stream twice`)\n for (const [key, shape] of this.#shapeEntries()) {\n if (shape.hasStarted()) {\n // The multi-shape stream needs to be started together as a whole, and so we\n // have to check that a shape is not already started.\n throw new Error(`Shape ${key} already started`)\n }\n shape.subscribe(\n async (messages) => {\n // Whats the max lsn of the up-to-date messages?\n const upToDateLsns = messages\n .filter(isControlMessage)\n .map(({ headers }) => (headers.global_last_seen_lsn as number) ?? 0)\n if (upToDateLsns.length > 0) {\n const maxUpToDateLsn = Math.max(...upToDateLsns)\n const lastMaxUpToDateLsn = this.#lastUpToDateLsns[key]\n if (maxUpToDateLsn > lastMaxUpToDateLsn) {\n this.#lastUpToDateLsns[key] = maxUpToDateLsn\n }\n }\n\n // Whats the max lsn of the data messages?\n const dataLsns = messages\n .filter(isChangeMessage)\n .map(({ headers }) => (headers.lsn as number) ?? 0)\n if (dataLsns.length > 0) {\n const maxDataLsn = Math.max(...dataLsns)\n const lastMaxDataLsn = this.#lastDataLsns[key]\n if (maxDataLsn > lastMaxDataLsn) {\n this.#lastDataLsns[key] = maxDataLsn\n }\n // There is new data, so we need to schedule a check for updates on\n // other shapes\n this.#scheduleCheckForUpdates()\n }\n\n // Publish the messages to the multi-shape stream subscribers\n const multiShapeMessages = messages.map(\n (message) =>\n ({\n ...message,\n shape: key,\n }) as MultiShapeMessages<TShapeRows>\n )\n await this._publish(multiShapeMessages)\n },\n (error) => this.#onError(error)\n )\n }\n this.#started = true\n }\n\n #scheduleCheckForUpdates() {\n this.#checkForUpdatesTimeout ??= setTimeout(() => {\n this.#checkForUpdates()\n this.#checkForUpdatesTimeout = undefined\n }, this.checkForUpdatesAfterMs)\n }\n\n async #checkForUpdates() {\n const maxDataLsn = Math.max(...Object.values(this.#lastDataLsns))\n const refreshPromises = this.#shapeEntries()\n .filter(([key]) => {\n // We only need to refresh shapes that have not seen an up-to-date message\n // lower than the max lsn of the data messages we have received.\n const lastUpToDateLsn = this.#lastUpToDateLsns[key]\n return lastUpToDateLsn < maxDataLsn\n })\n .map(([_, shape]) => {\n return shape.forceDisconnectAndRefresh()\n })\n await Promise.all(refreshPromises)\n }\n\n #onError(error: Error) {\n // TODO: we probably want to disconnect all shapes here on the first error\n this.#subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n protected async _publish(\n messages: MultiShapeMessages<TShapeRows>[]\n ): Promise<void> {\n await Promise.all(\n Array.from(this.#subscribers.values()).map(async ([callback, __]) => {\n try {\n await callback(messages)\n } catch (err) {\n queueMicrotask(() => {\n throw err\n })\n }\n })\n )\n }\n\n /**\n * Returns an array of the shape entries.\n * Ensures that the shape entries are typed, as `Object.entries`\n * will not type the entries correctly.\n */\n #shapeEntries() {\n return Object.entries(this.#shapes) as [\n keyof TShapeRows & string,\n ShapeStream<TShapeRows[string]>,\n ][]\n }\n\n /**\n * The ShapeStreams that are being subscribed to.\n */\n get shapes() {\n return this.#shapes\n }\n\n subscribe(\n callback: (\n messages: MultiShapeMessages<TShapeRows>[]\n ) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n\n this.#subscribers.set(subscriptionId, [callback, onError])\n if (!this.#started) this.#start()\n\n return () => {\n this.#subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.#subscribers.clear()\n }\n\n /** Unix time at which we last synced. Undefined when `isLoading` is true. */\n lastSyncedAt(): number | undefined {\n // Min of all the lastSyncedAt values\n return Math.min(\n ...this.#shapeEntries().map(\n ([_, shape]) => shape.lastSyncedAt() ?? Infinity\n )\n )\n }\n\n /** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */\n lastSynced(): number {\n const lastSyncedAt = this.lastSyncedAt()\n if (lastSyncedAt === undefined) return Infinity\n return Date.now() - lastSyncedAt\n }\n\n /** Indicates if we are connected to the Electric sync service. */\n isConnected(): boolean {\n return this.#shapeEntries().every(([_, shape]) => shape.isConnected())\n }\n\n /** True during initial fetch. False afterwise. */\n isLoading(): boolean {\n return this.#shapeEntries().some(([_, shape]) => shape.isLoading())\n }\n\n get isUpToDate() {\n return this.#shapeEntries().every(([_, shape]) => shape.isUpToDate)\n }\n}\n\n/**\n * A transactional multi-shape stream is a multi-shape stream that emits the\n * messages in transactional batches, ensuring that all shapes will receive\n * at least an `up-to-date` message from Electric within the `checkForUpdatesAfterMs`\n * interval.\n * It uses the `lsn` metadata to infer transaction boundaries, and the `op_position`\n * metadata to sort the messages within a transaction.\n *\n * @constructor\n * @param {MultiShapeStreamOptions} options - configure the multi-shape stream\n * @example\n * ```ts\n * const transactionalMultiShapeStream = new TransactionalMultiShapeStream({\n * shapes: {\n * shape1: {\n * url: 'http://localhost:3000/v1/shape1',\n * },\n * shape2: {\n * url: 'http://localhost:3000/v1/shape2',\n * },\n * },\n * })\n *\n * transactionalMultiShapeStream.subscribe((msgs) => {\n * console.log(msgs)\n * })\n *\n * // or with ShapeStream instances\n * const transactionalMultiShapeStream = new TransactionalMultiShapeStream({\n * shapes: {\n * shape1: new ShapeStream({ url: 'http://localhost:3000/v1/shape1' }),\n * shape2: new ShapeStream({ url: 'http://localhost:3000/v1/shape2' }),\n * },\n * })\n * ```\n */\n\nexport class TransactionalMultiShapeStream<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> extends MultiShapeStream<TShapeRows> {\n #changeMessages = new Map<number, MultiShapeMessage<Row<unknown>, string>[]>()\n #completeLsns: {\n [K in keyof TShapeRows]: number\n }\n\n constructor(options: MultiShapeStreamOptions<TShapeRows>) {\n super(options)\n this.#completeLsns = Object.fromEntries(\n Object.entries(options.shapes).map(([key]) => [key, -Infinity])\n ) as { [K in keyof TShapeRows]: number }\n }\n\n #getLowestCompleteLsn() {\n return Math.min(...Object.values(this.#completeLsns))\n }\n\n protected async _publish(\n messages: MultiShapeMessages<TShapeRows>[]\n ): Promise<void> {\n this.#accumulate(messages)\n const lowestCompleteLsn = this.#getLowestCompleteLsn()\n const lsnsToPublish = [...this.#changeMessages.keys()].filter(\n (lsn) => lsn <= lowestCompleteLsn\n )\n const messagesToPublish = lsnsToPublish\n .sort((a, b) => a - b)\n .map((lsn) =>\n this.#changeMessages.get(lsn)?.sort((a, b) => {\n const { headers: aHeaders } = a\n const { headers: bHeaders } = b\n if (\n typeof aHeaders.op_position !== `number` ||\n typeof bHeaders.op_position !== `number`\n ) {\n return 0 // op_position is not present on the snapshot message\n }\n return aHeaders.op_position - bHeaders.op_position\n })\n )\n .filter((messages) => messages !== undefined)\n .flat() as MultiShapeMessages<TShapeRows>[]\n lsnsToPublish.forEach((lsn) => {\n this.#changeMessages.delete(lsn)\n })\n if (messagesToPublish.length > 0) {\n await super._publish(messagesToPublish)\n }\n }\n\n #accumulate(messages: MultiShapeMessages<TShapeRows>[]) {\n const isUpToDate = this.isUpToDate\n messages.forEach((message) => {\n const { shape, headers } = message\n if (isChangeMessage(message)) {\n // The snapshot message does not have an lsn, so we use 0\n const lsn = typeof headers.lsn === `number` ? headers.lsn : 0\n if (!this.#changeMessages.has(lsn)) {\n this.#changeMessages.set(lsn, [])\n }\n this.#changeMessages.get(lsn)?.push(message)\n if (\n isUpToDate && // All shapes must be up to date\n typeof headers.last === `boolean` &&\n headers.last === true\n ) {\n this.#completeLsns[shape] = Math.max(this.#completeLsns[shape], lsn)\n }\n } else if (isControlMessage(message)) {\n if (headers.control === `up-to-date`) {\n if (typeof headers.global_last_seen_lsn !== `number`) {\n throw new Error(`global_last_seen_lsn is not a number`)\n }\n this.#completeLsns[shape] = Math.max(\n this.#completeLsns[shape],\n headers.global_last_seen_lsn\n )\n }\n }\n })\n }\n}\n"],"mappings":"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","names":["isChangeMessage","matchStream","stream","operations","matchFn","timeout","resolve","reject","unsubscribe","messages","message","msg","operation","finish","timeoutId","matchBy","column","value","ShapeStream","isChangeMessage","isControlMessage","_shapes","_started","_checkForUpdatesTimeout","_lastDataLsns","_lastUpToDateLsns","_subscribers","_MultiShapeStream_instances","start_fn","scheduleCheckForUpdates_fn","checkForUpdates_fn","onError_fn","shapeEntries_fn","MultiShapeStream","options","__privateAdd","start","checkForUpdatesAfterMs","shapes","__privateSet","key","shape","ShapeStream","__spreadProps","__spreadValues","__privateMethod","messages","__async","__privateGet","_0","callback","__","err","onError","subscriptionId","_","_a","lastSyncedAt","upToDateLsns","isControlMessage","headers","maxUpToDateLsn","lastMaxUpToDateLsn","dataLsns","isChangeMessage","maxDataLsn","lastMaxDataLsn","multiShapeMessages","message","error","refreshPromises","errorFn","_changeMessages","_completeLsns","_TransactionalMultiShapeStream_instances","getLowestCompleteLsn_fn","accumulate_fn","_TransactionalMultiShapeStream","lowestCompleteLsn","lsnsToPublish","lsn","messagesToPublish","a","b","aHeaders","bHeaders","__superGet","isUpToDate","TransactionalMultiShapeStream"]}
|
|
1
|
+
{"version":3,"sources":["../src/match.ts","../src/bigint-utils.ts","../src/multi-shape-stream.ts"],"sourcesContent":["import {\n isChangeMessage,\n type ShapeStreamInterface,\n type ChangeMessage,\n type GetExtensions,\n type Operation,\n type Row,\n type Value,\n type Message,\n} from '@electric-sql/client'\n\nexport function matchStream<T extends Row<unknown>>(\n stream: ShapeStreamInterface<T>,\n operations: Array<Operation>,\n matchFn: (message: ChangeMessage<T>) => boolean,\n timeout = 60000 // ms\n): Promise<ChangeMessage<T>> {\n return new Promise<ChangeMessage<T>>((resolve, reject) => {\n const unsubscribe: () => void = stream.subscribe(\n (messages: Array<unknown>) => {\n const message = messages\n .filter((msg): msg is ChangeMessage<T> =>\n isChangeMessage(msg as Message<Row<never>>)\n )\n .find((message) => {\n const operation: Operation = message.headers.operation\n\n return operations.includes(operation) && matchFn(message)\n })\n\n if (message) {\n return finish(message)\n }\n }\n )\n\n const timeoutId: NodeJS.Timeout = setTimeout(() => {\n const msg: string = `matchStream timed out after ${timeout}ms`\n\n console.error(msg)\n\n reject(msg)\n }, timeout)\n\n function finish(message: ChangeMessage<T>): void {\n clearTimeout(timeoutId)\n\n unsubscribe()\n\n return resolve(message)\n }\n })\n}\n\nexport function matchBy<T extends Row<unknown>>(\n column: string,\n value: Value<GetExtensions<T>>\n): (message: ChangeMessage<T>) => boolean {\n return (message: ChangeMessage<T>) => message.value[column] === value\n}\n","export function bigIntMax(nums: Array<bigint | number>): bigint {\n return BigInt(nums.reduce((m, e) => (e > m ? e : m)))\n}\n\nexport function bigIntMin(nums: Array<bigint | number>): bigint {\n return BigInt(nums.reduce((m, e) => (e < m ? e : m)))\n}\n\nexport function bigIntCompare(a: bigint, b: bigint): 1 | -1 | 0 {\n return a > b ? 1 : a < b ? -1 : 0\n}\n","import { bigIntCompare, bigIntMax, bigIntMin } from './bigint-utils'\nimport {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n} from '@electric-sql/client'\nimport type {\n ChangeMessage,\n ControlMessage,\n FetchError,\n MaybePromise,\n Row,\n ShapeStreamOptions,\n} from '@electric-sql/client'\n\ninterface MultiShapeStreamOptions<\n TShapeRows extends {\n [K: string]: Row<unknown>\n } = {\n [K: string]: Row<unknown>\n },\n> {\n shapes: {\n [K in keyof TShapeRows]:\n | ShapeStreamOptions<TShapeRows[K]>\n | ShapeStream<TShapeRows[K]>\n }\n start?: boolean\n checkForUpdatesAfterMs?: number // milliseconds\n}\n\ninterface MultiShapeChangeMessage<\n T extends Row<unknown>,\n ShapeNames extends string,\n> extends ChangeMessage<T> {\n shape: ShapeNames\n}\n\ninterface MultiShapeControlMessage<ShapeNames extends string>\n extends ControlMessage {\n shape: ShapeNames\n}\n\ntype MultiShapeMessage<T extends Row<unknown>, ShapeNames extends string> =\n | MultiShapeChangeMessage<T, ShapeNames>\n | MultiShapeControlMessage<ShapeNames>\n\nexport type MultiShapeMessages<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> = {\n [K in keyof TShapeRows & string]: MultiShapeMessage<TShapeRows[K], K>\n}[keyof TShapeRows & string]\n\nexport interface MultiShapeStreamInterface<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> {\n shapes: { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n checkForUpdatesAfterMs?: number\n\n subscribe(\n callback: (\n messages: MultiShapeMessages<TShapeRows>[]\n ) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ): () => void\n unsubscribeAll(): void\n\n lastSyncedAt(): number | undefined\n lastSynced(): number\n isConnected(): boolean\n isLoading(): boolean\n\n isUpToDate: boolean\n}\n\n/**\n * A multi-shape stream is a stream that can subscribe to multiple shapes.\n * It ensures that all shapes will receive at least an `up-to-date` message from\n * Electric within the `checkForUpdatesAfterMs` interval.\n *\n * @constructor\n * @param {MultiShapeStreamOptions} options - configure the multi-shape stream\n * @example\n * ```ts\n * const multiShapeStream = new MultiShapeStream({\n * shapes: {\n * shape1: {\n * url: 'http://localhost:3000/v1/shape1',\n * },\n * shape2: {\n * url: 'http://localhost:3000/v1/shape2',\n * },\n * },\n * })\n *\n * multiShapeStream.subscribe((msgs) => {\n * console.log(msgs)\n * })\n *\n * // or with ShapeStream instances\n * const multiShapeStream = new MultiShapeStream({\n * shapes: {\n * shape1: new ShapeStream({ url: 'http://localhost:3000/v1/shape1' }),\n * shape2: new ShapeStream({ url: 'http://localhost:3000/v1/shape2' }),\n * },\n * })\n * ```\n */\n\nexport class MultiShapeStream<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> implements MultiShapeStreamInterface<TShapeRows>\n{\n #shapes: { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n #started = false\n checkForUpdatesAfterMs?: number\n\n #checkForUpdatesTimeout?: ReturnType<typeof setTimeout> | undefined\n\n // We keep track of the last lsn of data and up-to-date messages for each shape\n // so that we can skip checkForUpdates if the lsn of the up-to-date message is\n // greater than the last lsn of data.\n #lastDataLsns: { [K in keyof TShapeRows]: bigint }\n #lastUpToDateLsns: { [K in keyof TShapeRows]: bigint }\n\n readonly #subscribers = new Map<\n number,\n [\n (messages: MultiShapeMessages<TShapeRows>[]) => MaybePromise<void>,\n ((error: Error) => void) | undefined,\n ]\n >()\n\n constructor(options: MultiShapeStreamOptions<TShapeRows>) {\n const {\n start = true, // By default we start the multi-shape stream\n checkForUpdatesAfterMs = 100, // Force a check for updates after 100ms\n shapes,\n } = options\n this.checkForUpdatesAfterMs = checkForUpdatesAfterMs\n this.#shapes = Object.fromEntries(\n Object.entries(shapes).map(([key, shape]) => [\n key,\n shape instanceof ShapeStream\n ? shape\n : new ShapeStream<TShapeRows[typeof key]>({\n ...shape,\n start: false,\n }),\n ])\n ) as { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n this.#lastDataLsns = Object.fromEntries(\n Object.entries(shapes).map(([key]) => [key, BigInt(-1)])\n ) as { [K in keyof TShapeRows]: bigint }\n this.#lastUpToDateLsns = Object.fromEntries(\n Object.entries(shapes).map(([key]) => [key, BigInt(-1)])\n ) as { [K in keyof TShapeRows]: bigint }\n if (start) this.#start()\n }\n\n #start() {\n if (this.#started) throw new Error(`Cannot start multi-shape stream twice`)\n for (const [key, shape] of this.#shapeEntries()) {\n if (shape.hasStarted()) {\n // The multi-shape stream needs to be started together as a whole, and so we\n // have to check that a shape is not already started.\n throw new Error(`Shape ${key} already started`)\n }\n shape.subscribe(\n async (messages) => {\n // Whats the max lsn of the up-to-date messages?\n const upToDateLsns = messages\n .filter(isControlMessage)\n .map(({ headers }) =>\n typeof headers.global_last_seen_lsn === `string`\n ? BigInt(headers.global_last_seen_lsn)\n : BigInt(0)\n )\n if (upToDateLsns.length > 0) {\n const maxUpToDateLsn = bigIntMax(upToDateLsns)\n const lastMaxUpToDateLsn = this.#lastUpToDateLsns[key]\n if (maxUpToDateLsn > lastMaxUpToDateLsn) {\n this.#lastUpToDateLsns[key] = maxUpToDateLsn\n }\n }\n\n // Whats the max lsn of the data messages?\n const dataLsns = messages\n .filter(isChangeMessage)\n .map(({ headers }) =>\n typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)\n )\n if (dataLsns.length > 0) {\n const maxDataLsn = bigIntMax(dataLsns)\n const lastMaxDataLsn = this.#lastDataLsns[key]\n if (maxDataLsn > lastMaxDataLsn) {\n this.#lastDataLsns[key] = maxDataLsn\n }\n // There is new data, so we need to schedule a check for updates on\n // other shapes\n this.#scheduleCheckForUpdates()\n }\n\n // Publish the messages to the multi-shape stream subscribers\n const multiShapeMessages = messages.map(\n (message) =>\n ({\n ...message,\n shape: key,\n }) as MultiShapeMessages<TShapeRows>\n )\n await this._publish(multiShapeMessages)\n },\n (error) => this.#onError(error)\n )\n }\n this.#started = true\n }\n\n #scheduleCheckForUpdates() {\n this.#checkForUpdatesTimeout ??= setTimeout(() => {\n this.#checkForUpdates()\n this.#checkForUpdatesTimeout = undefined\n }, this.checkForUpdatesAfterMs)\n }\n\n async #checkForUpdates() {\n const maxDataLsn = bigIntMax(Object.values(this.#lastDataLsns))\n const refreshPromises = this.#shapeEntries()\n .filter(([key]) => {\n // We only need to refresh shapes that have not seen an up-to-date message\n // lower than the max lsn of the data messages we have received.\n const lastUpToDateLsn = this.#lastUpToDateLsns[key]\n return lastUpToDateLsn < maxDataLsn\n })\n .map(([_, shape]) => {\n return shape.forceDisconnectAndRefresh()\n })\n await Promise.all(refreshPromises)\n }\n\n #onError(error: Error) {\n // TODO: we probably want to disconnect all shapes here on the first error\n this.#subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n protected async _publish(\n messages: MultiShapeMessages<TShapeRows>[]\n ): Promise<void> {\n await Promise.all(\n Array.from(this.#subscribers.values()).map(async ([callback, __]) => {\n try {\n await callback(messages)\n } catch (err) {\n queueMicrotask(() => {\n throw err\n })\n }\n })\n )\n }\n\n /**\n * Returns an array of the shape entries.\n * Ensures that the shape entries are typed, as `Object.entries`\n * will not type the entries correctly.\n */\n #shapeEntries() {\n return Object.entries(this.#shapes) as [\n keyof TShapeRows & string,\n ShapeStream<TShapeRows[string]>,\n ][]\n }\n\n /**\n * The ShapeStreams that are being subscribed to.\n */\n get shapes() {\n return this.#shapes\n }\n\n subscribe(\n callback: (\n messages: MultiShapeMessages<TShapeRows>[]\n ) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n\n this.#subscribers.set(subscriptionId, [callback, onError])\n if (!this.#started) this.#start()\n\n return () => {\n this.#subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.#subscribers.clear()\n }\n\n /** Unix time at which we last synced. Undefined when `isLoading` is true. */\n lastSyncedAt(): number | undefined {\n // Min of all the lastSyncedAt values\n const shapeEntries = this.#shapeEntries()\n if (shapeEntries.length === 0) return\n return shapeEntries.reduce((minLastSyncedAt, [_, shape]) => {\n return Math.min(minLastSyncedAt, shape.lastSyncedAt() ?? Infinity)\n }, Infinity)\n }\n\n /** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */\n lastSynced(): number {\n const lastSyncedAt = this.lastSyncedAt()\n if (lastSyncedAt === undefined) return Infinity\n return Date.now() - lastSyncedAt\n }\n\n /** Indicates if we are connected to the Electric sync service. */\n isConnected(): boolean {\n return this.#shapeEntries().every(([_, shape]) => shape.isConnected())\n }\n\n /** True during initial fetch. False afterwise. */\n isLoading(): boolean {\n return this.#shapeEntries().some(([_, shape]) => shape.isLoading())\n }\n\n get isUpToDate() {\n return this.#shapeEntries().every(([_, shape]) => shape.isUpToDate)\n }\n}\n\n/**\n * A transactional multi-shape stream is a multi-shape stream that emits the\n * messages in transactional batches, ensuring that all shapes will receive\n * at least an `up-to-date` message from Electric within the `checkForUpdatesAfterMs`\n * interval.\n * It uses the `lsn` metadata to infer transaction boundaries, and the `op_position`\n * metadata to sort the messages within a transaction.\n *\n * @constructor\n * @param {MultiShapeStreamOptions} options - configure the multi-shape stream\n * @example\n * ```ts\n * const transactionalMultiShapeStream = new TransactionalMultiShapeStream({\n * shapes: {\n * shape1: {\n * url: 'http://localhost:3000/v1/shape1',\n * },\n * shape2: {\n * url: 'http://localhost:3000/v1/shape2',\n * },\n * },\n * })\n *\n * transactionalMultiShapeStream.subscribe((msgs) => {\n * console.log(msgs)\n * })\n *\n * // or with ShapeStream instances\n * const transactionalMultiShapeStream = new TransactionalMultiShapeStream({\n * shapes: {\n * shape1: new ShapeStream({ url: 'http://localhost:3000/v1/shape1' }),\n * shape2: new ShapeStream({ url: 'http://localhost:3000/v1/shape2' }),\n * },\n * })\n * ```\n */\n\nexport class TransactionalMultiShapeStream<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> extends MultiShapeStream<TShapeRows> {\n #changeMessages = new Map<bigint, MultiShapeMessage<Row<unknown>, string>[]>()\n #completeLsns: {\n [K in keyof TShapeRows]: bigint\n }\n\n constructor(options: MultiShapeStreamOptions<TShapeRows>) {\n super(options)\n this.#completeLsns = Object.fromEntries(\n Object.entries(options.shapes).map(([key]) => [key, BigInt(-1)])\n ) as { [K in keyof TShapeRows]: bigint }\n }\n\n #getLowestCompleteLsn() {\n return bigIntMin(Object.values(this.#completeLsns))\n }\n\n protected async _publish(\n messages: MultiShapeMessages<TShapeRows>[]\n ): Promise<void> {\n this.#accumulate(messages)\n const lowestCompleteLsn = this.#getLowestCompleteLsn()\n const lsnsToPublish = [...this.#changeMessages.keys()].filter(\n (lsn) => lsn <= lowestCompleteLsn\n )\n const messagesToPublish = lsnsToPublish\n .sort((a, b) => bigIntCompare(a, b))\n .map((lsn) =>\n this.#changeMessages.get(lsn)?.sort((a, b) => {\n const { headers: aHeaders } = a\n const { headers: bHeaders } = b\n if (\n typeof aHeaders.op_position !== `number` ||\n typeof bHeaders.op_position !== `number`\n ) {\n return 0 // op_position is not present on the snapshot message\n }\n return aHeaders.op_position - bHeaders.op_position\n })\n )\n .filter((messages) => messages !== undefined)\n .flat() as MultiShapeMessages<TShapeRows>[]\n lsnsToPublish.forEach((lsn) => {\n this.#changeMessages.delete(lsn)\n })\n if (messagesToPublish.length > 0) {\n await super._publish(messagesToPublish)\n }\n }\n\n #accumulate(messages: MultiShapeMessages<TShapeRows>[]) {\n const isUpToDate = this.isUpToDate\n messages.forEach((message) => {\n const { shape, headers } = message\n if (isChangeMessage(message)) {\n // The snapshot message does not have an lsn, so we use 0\n const lsn =\n typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)\n if (!this.#changeMessages.has(lsn)) {\n this.#changeMessages.set(lsn, [])\n }\n this.#changeMessages.get(lsn)?.push(message)\n if (\n isUpToDate && // All shapes must be up to date\n typeof headers.last === `boolean` &&\n headers.last === true\n ) {\n this.#completeLsns[shape] = bigIntMax([\n this.#completeLsns[shape],\n lsn,\n ])\n }\n } else if (isControlMessage(message)) {\n if (headers.control === `up-to-date`) {\n if (typeof headers.global_last_seen_lsn !== `string`) {\n throw new Error(`global_last_seen_lsn is not a number`)\n }\n this.#completeLsns[shape] = bigIntMax([\n this.#completeLsns[shape],\n BigInt(headers.global_last_seen_lsn),\n ])\n }\n }\n })\n }\n}\n"],"mappings":"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","names":["isChangeMessage","matchStream","stream","operations","matchFn","timeout","resolve","reject","unsubscribe","messages","message","msg","operation","finish","timeoutId","matchBy","column","value","bigIntMax","nums","m","e","bigIntMin","bigIntCompare","a","b","ShapeStream","isChangeMessage","isControlMessage","_shapes","_started","_checkForUpdatesTimeout","_lastDataLsns","_lastUpToDateLsns","_subscribers","_MultiShapeStream_instances","start_fn","scheduleCheckForUpdates_fn","checkForUpdates_fn","onError_fn","shapeEntries_fn","MultiShapeStream","options","__privateAdd","start","checkForUpdatesAfterMs","shapes","__privateSet","key","shape","ShapeStream","__spreadProps","__spreadValues","__privateMethod","messages","__async","__privateGet","_0","callback","__","err","onError","subscriptionId","shapeEntries","minLastSyncedAt","_","_a","lastSyncedAt","upToDateLsns","isControlMessage","headers","maxUpToDateLsn","bigIntMax","lastMaxUpToDateLsn","dataLsns","isChangeMessage","maxDataLsn","lastMaxDataLsn","multiShapeMessages","message","error","refreshPromises","errorFn","_changeMessages","_completeLsns","_TransactionalMultiShapeStream_instances","getLowestCompleteLsn_fn","accumulate_fn","_TransactionalMultiShapeStream","lowestCompleteLsn","lsnsToPublish","lsn","messagesToPublish","a","b","bigIntCompare","aHeaders","bHeaders","__superGet","bigIntMin","isUpToDate","TransactionalMultiShapeStream"]}
|
package/dist/index.legacy-esm.js
CHANGED
|
@@ -61,6 +61,17 @@ function matchBy(column, value) {
|
|
|
61
61
|
return (message) => message.value[column] === value;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// src/bigint-utils.ts
|
|
65
|
+
function bigIntMax(nums) {
|
|
66
|
+
return BigInt(nums.reduce((m, e) => e > m ? e : m));
|
|
67
|
+
}
|
|
68
|
+
function bigIntMin(nums) {
|
|
69
|
+
return BigInt(nums.reduce((m, e) => e < m ? e : m));
|
|
70
|
+
}
|
|
71
|
+
function bigIntCompare(a, b) {
|
|
72
|
+
return a > b ? 1 : a < b ? -1 : 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
64
75
|
// src/multi-shape-stream.ts
|
|
65
76
|
import {
|
|
66
77
|
ShapeStream,
|
|
@@ -97,10 +108,10 @@ var MultiShapeStream = class {
|
|
|
97
108
|
])
|
|
98
109
|
));
|
|
99
110
|
__privateSet(this, _lastDataLsns, Object.fromEntries(
|
|
100
|
-
Object.entries(shapes).map(([key]) => [key, -
|
|
111
|
+
Object.entries(shapes).map(([key]) => [key, BigInt(-1)])
|
|
101
112
|
));
|
|
102
113
|
__privateSet(this, _lastUpToDateLsns, Object.fromEntries(
|
|
103
|
-
Object.entries(shapes).map(([key]) => [key, -
|
|
114
|
+
Object.entries(shapes).map(([key]) => [key, BigInt(-1)])
|
|
104
115
|
));
|
|
105
116
|
if (start) __privateMethod(this, _MultiShapeStream_instances, start_fn).call(this);
|
|
106
117
|
}
|
|
@@ -136,14 +147,12 @@ var MultiShapeStream = class {
|
|
|
136
147
|
}
|
|
137
148
|
/** Unix time at which we last synced. Undefined when `isLoading` is true. */
|
|
138
149
|
lastSyncedAt() {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
)
|
|
146
|
-
);
|
|
150
|
+
const shapeEntries = __privateMethod(this, _MultiShapeStream_instances, shapeEntries_fn).call(this);
|
|
151
|
+
if (shapeEntries.length === 0) return;
|
|
152
|
+
return shapeEntries.reduce((minLastSyncedAt, [_, shape]) => {
|
|
153
|
+
var _a;
|
|
154
|
+
return Math.min(minLastSyncedAt, (_a = shape.lastSyncedAt()) != null ? _a : Infinity);
|
|
155
|
+
}, Infinity);
|
|
147
156
|
}
|
|
148
157
|
/** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */
|
|
149
158
|
lastSynced() {
|
|
@@ -178,23 +187,21 @@ start_fn = function() {
|
|
|
178
187
|
}
|
|
179
188
|
shape.subscribe(
|
|
180
189
|
async (messages) => {
|
|
181
|
-
const upToDateLsns = messages.filter(isControlMessage).map(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
});
|
|
190
|
+
const upToDateLsns = messages.filter(isControlMessage).map(
|
|
191
|
+
({ headers }) => typeof headers.global_last_seen_lsn === `string` ? BigInt(headers.global_last_seen_lsn) : BigInt(0)
|
|
192
|
+
);
|
|
185
193
|
if (upToDateLsns.length > 0) {
|
|
186
|
-
const maxUpToDateLsn =
|
|
194
|
+
const maxUpToDateLsn = bigIntMax(upToDateLsns);
|
|
187
195
|
const lastMaxUpToDateLsn = __privateGet(this, _lastUpToDateLsns)[key];
|
|
188
196
|
if (maxUpToDateLsn > lastMaxUpToDateLsn) {
|
|
189
197
|
__privateGet(this, _lastUpToDateLsns)[key] = maxUpToDateLsn;
|
|
190
198
|
}
|
|
191
199
|
}
|
|
192
|
-
const dataLsns = messages.filter(isChangeMessage2).map(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
});
|
|
200
|
+
const dataLsns = messages.filter(isChangeMessage2).map(
|
|
201
|
+
({ headers }) => typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)
|
|
202
|
+
);
|
|
196
203
|
if (dataLsns.length > 0) {
|
|
197
|
-
const maxDataLsn =
|
|
204
|
+
const maxDataLsn = bigIntMax(dataLsns);
|
|
198
205
|
const lastMaxDataLsn = __privateGet(this, _lastDataLsns)[key];
|
|
199
206
|
if (maxDataLsn > lastMaxDataLsn) {
|
|
200
207
|
__privateGet(this, _lastDataLsns)[key] = maxDataLsn;
|
|
@@ -221,7 +228,7 @@ scheduleCheckForUpdates_fn = function() {
|
|
|
221
228
|
}, this.checkForUpdatesAfterMs));
|
|
222
229
|
};
|
|
223
230
|
checkForUpdates_fn = async function() {
|
|
224
|
-
const maxDataLsn =
|
|
231
|
+
const maxDataLsn = bigIntMax(Object.values(__privateGet(this, _lastDataLsns)));
|
|
225
232
|
const refreshPromises = __privateMethod(this, _MultiShapeStream_instances, shapeEntries_fn).call(this).filter(([key]) => {
|
|
226
233
|
const lastUpToDateLsn = __privateGet(this, _lastUpToDateLsns)[key];
|
|
227
234
|
return lastUpToDateLsn < maxDataLsn;
|
|
@@ -251,7 +258,7 @@ var TransactionalMultiShapeStream = class extends MultiShapeStream {
|
|
|
251
258
|
__privateAdd(this, _changeMessages, /* @__PURE__ */ new Map());
|
|
252
259
|
__privateAdd(this, _completeLsns);
|
|
253
260
|
__privateSet(this, _completeLsns, Object.fromEntries(
|
|
254
|
-
Object.entries(options.shapes).map(([key]) => [key, -
|
|
261
|
+
Object.entries(options.shapes).map(([key]) => [key, BigInt(-1)])
|
|
255
262
|
));
|
|
256
263
|
}
|
|
257
264
|
async _publish(messages) {
|
|
@@ -260,7 +267,7 @@ var TransactionalMultiShapeStream = class extends MultiShapeStream {
|
|
|
260
267
|
const lsnsToPublish = [...__privateGet(this, _changeMessages).keys()].filter(
|
|
261
268
|
(lsn) => lsn <= lowestCompleteLsn
|
|
262
269
|
);
|
|
263
|
-
const messagesToPublish = lsnsToPublish.sort((a, b) => a
|
|
270
|
+
const messagesToPublish = lsnsToPublish.sort((a, b) => bigIntCompare(a, b)).map(
|
|
264
271
|
(lsn) => {
|
|
265
272
|
var _a;
|
|
266
273
|
return (_a = __privateGet(this, _changeMessages).get(lsn)) == null ? void 0 : _a.sort((a, b) => {
|
|
@@ -285,7 +292,7 @@ _changeMessages = new WeakMap();
|
|
|
285
292
|
_completeLsns = new WeakMap();
|
|
286
293
|
_TransactionalMultiShapeStream_instances = new WeakSet();
|
|
287
294
|
getLowestCompleteLsn_fn = function() {
|
|
288
|
-
return
|
|
295
|
+
return bigIntMin(Object.values(__privateGet(this, _completeLsns)));
|
|
289
296
|
};
|
|
290
297
|
accumulate_fn = function(messages) {
|
|
291
298
|
const isUpToDate = this.isUpToDate;
|
|
@@ -293,24 +300,27 @@ accumulate_fn = function(messages) {
|
|
|
293
300
|
var _a;
|
|
294
301
|
const { shape, headers } = message;
|
|
295
302
|
if (isChangeMessage2(message)) {
|
|
296
|
-
const lsn = typeof headers.lsn === `
|
|
303
|
+
const lsn = typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0);
|
|
297
304
|
if (!__privateGet(this, _changeMessages).has(lsn)) {
|
|
298
305
|
__privateGet(this, _changeMessages).set(lsn, []);
|
|
299
306
|
}
|
|
300
307
|
(_a = __privateGet(this, _changeMessages).get(lsn)) == null ? void 0 : _a.push(message);
|
|
301
308
|
if (isUpToDate && // All shapes must be up to date
|
|
302
309
|
typeof headers.last === `boolean` && headers.last === true) {
|
|
303
|
-
__privateGet(this, _completeLsns)[shape] =
|
|
310
|
+
__privateGet(this, _completeLsns)[shape] = bigIntMax([
|
|
311
|
+
__privateGet(this, _completeLsns)[shape],
|
|
312
|
+
lsn
|
|
313
|
+
]);
|
|
304
314
|
}
|
|
305
315
|
} else if (isControlMessage(message)) {
|
|
306
316
|
if (headers.control === `up-to-date`) {
|
|
307
|
-
if (typeof headers.global_last_seen_lsn !== `
|
|
317
|
+
if (typeof headers.global_last_seen_lsn !== `string`) {
|
|
308
318
|
throw new Error(`global_last_seen_lsn is not a number`);
|
|
309
319
|
}
|
|
310
|
-
__privateGet(this, _completeLsns)[shape] =
|
|
320
|
+
__privateGet(this, _completeLsns)[shape] = bigIntMax([
|
|
311
321
|
__privateGet(this, _completeLsns)[shape],
|
|
312
|
-
headers.global_last_seen_lsn
|
|
313
|
-
);
|
|
322
|
+
BigInt(headers.global_last_seen_lsn)
|
|
323
|
+
]);
|
|
314
324
|
}
|
|
315
325
|
}
|
|
316
326
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/match.ts","../src/multi-shape-stream.ts"],"sourcesContent":["import {\n isChangeMessage,\n type ShapeStreamInterface,\n type ChangeMessage,\n type GetExtensions,\n type Operation,\n type Row,\n type Value,\n type Message,\n} from '@electric-sql/client'\n\nexport function matchStream<T extends Row<unknown>>(\n stream: ShapeStreamInterface<T>,\n operations: Array<Operation>,\n matchFn: (message: ChangeMessage<T>) => boolean,\n timeout = 60000 // ms\n): Promise<ChangeMessage<T>> {\n return new Promise<ChangeMessage<T>>((resolve, reject) => {\n const unsubscribe: () => void = stream.subscribe(\n (messages: Array<unknown>) => {\n const message = messages\n .filter((msg): msg is ChangeMessage<T> =>\n isChangeMessage(msg as Message<Row<never>>)\n )\n .find((message) => {\n const operation: Operation = message.headers.operation\n\n return operations.includes(operation) && matchFn(message)\n })\n\n if (message) {\n return finish(message)\n }\n }\n )\n\n const timeoutId: NodeJS.Timeout = setTimeout(() => {\n const msg: string = `matchStream timed out after ${timeout}ms`\n\n console.error(msg)\n\n reject(msg)\n }, timeout)\n\n function finish(message: ChangeMessage<T>): void {\n clearTimeout(timeoutId)\n\n unsubscribe()\n\n return resolve(message)\n }\n })\n}\n\nexport function matchBy<T extends Row<unknown>>(\n column: string,\n value: Value<GetExtensions<T>>\n): (message: ChangeMessage<T>) => boolean {\n return (message: ChangeMessage<T>) => message.value[column] === value\n}\n","import {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n} from '@electric-sql/client'\nimport type {\n ChangeMessage,\n ControlMessage,\n FetchError,\n MaybePromise,\n Row,\n ShapeStreamOptions,\n} from '@electric-sql/client'\n\ninterface MultiShapeStreamOptions<\n TShapeRows extends {\n [K: string]: Row<unknown>\n } = {\n [K: string]: Row<unknown>\n },\n> {\n shapes: {\n [K in keyof TShapeRows]:\n | ShapeStreamOptions<TShapeRows[K]>\n | ShapeStream<TShapeRows[K]>\n }\n start?: boolean\n checkForUpdatesAfterMs?: number // milliseconds\n}\n\ninterface MultiShapeChangeMessage<\n T extends Row<unknown>,\n ShapeNames extends string,\n> extends ChangeMessage<T> {\n shape: ShapeNames\n}\n\ninterface MultiShapeControlMessage<ShapeNames extends string>\n extends ControlMessage {\n shape: ShapeNames\n}\n\ntype MultiShapeMessage<T extends Row<unknown>, ShapeNames extends string> =\n | MultiShapeChangeMessage<T, ShapeNames>\n | MultiShapeControlMessage<ShapeNames>\n\nexport type MultiShapeMessages<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> = {\n [K in keyof TShapeRows & string]: MultiShapeMessage<TShapeRows[K], K>\n}[keyof TShapeRows & string]\n\nexport interface MultiShapeStreamInterface<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> {\n shapes: { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n checkForUpdatesAfterMs?: number\n\n subscribe(\n callback: (\n messages: MultiShapeMessages<TShapeRows>[]\n ) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ): () => void\n unsubscribeAll(): void\n\n lastSyncedAt(): number | undefined\n lastSynced(): number\n isConnected(): boolean\n isLoading(): boolean\n\n isUpToDate: boolean\n}\n\n/**\n * A multi-shape stream is a stream that can subscribe to multiple shapes.\n * It ensures that all shapes will receive at least an `up-to-date` message from\n * Electric within the `checkForUpdatesAfterMs` interval.\n *\n * @constructor\n * @param {MultiShapeStreamOptions} options - configure the multi-shape stream\n * @example\n * ```ts\n * const multiShapeStream = new MultiShapeStream({\n * shapes: {\n * shape1: {\n * url: 'http://localhost:3000/v1/shape1',\n * },\n * shape2: {\n * url: 'http://localhost:3000/v1/shape2',\n * },\n * },\n * })\n *\n * multiShapeStream.subscribe((msgs) => {\n * console.log(msgs)\n * })\n *\n * // or with ShapeStream instances\n * const multiShapeStream = new MultiShapeStream({\n * shapes: {\n * shape1: new ShapeStream({ url: 'http://localhost:3000/v1/shape1' }),\n * shape2: new ShapeStream({ url: 'http://localhost:3000/v1/shape2' }),\n * },\n * })\n * ```\n */\n\nexport class MultiShapeStream<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> implements MultiShapeStreamInterface<TShapeRows>\n{\n #shapes: { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n #started = false\n checkForUpdatesAfterMs?: number\n\n #checkForUpdatesTimeout?: ReturnType<typeof setTimeout> | undefined\n\n // We keep track of the last lsn of data and up-to-date messages for each shape\n // so that we can skip checkForUpdates if the lsn of the up-to-date message is\n // greater than the last lsn of data.\n #lastDataLsns: { [K in keyof TShapeRows]: number }\n #lastUpToDateLsns: { [K in keyof TShapeRows]: number }\n\n readonly #subscribers = new Map<\n number,\n [\n (messages: MultiShapeMessages<TShapeRows>[]) => MaybePromise<void>,\n ((error: Error) => void) | undefined,\n ]\n >()\n\n constructor(options: MultiShapeStreamOptions<TShapeRows>) {\n const {\n start = true, // By default we start the multi-shape stream\n checkForUpdatesAfterMs = 100, // Force a check for updates after 100ms\n shapes,\n } = options\n this.checkForUpdatesAfterMs = checkForUpdatesAfterMs\n this.#shapes = Object.fromEntries(\n Object.entries(shapes).map(([key, shape]) => [\n key,\n shape instanceof ShapeStream\n ? shape\n : new ShapeStream<TShapeRows[typeof key]>({\n ...shape,\n start: false,\n }),\n ])\n ) as { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n this.#lastDataLsns = Object.fromEntries(\n Object.entries(shapes).map(([key]) => [key, -Infinity])\n ) as { [K in keyof TShapeRows]: number }\n this.#lastUpToDateLsns = Object.fromEntries(\n Object.entries(shapes).map(([key]) => [key, -Infinity])\n ) as { [K in keyof TShapeRows]: number }\n if (start) this.#start()\n }\n\n #start() {\n if (this.#started) throw new Error(`Cannot start multi-shape stream twice`)\n for (const [key, shape] of this.#shapeEntries()) {\n if (shape.hasStarted()) {\n // The multi-shape stream needs to be started together as a whole, and so we\n // have to check that a shape is not already started.\n throw new Error(`Shape ${key} already started`)\n }\n shape.subscribe(\n async (messages) => {\n // Whats the max lsn of the up-to-date messages?\n const upToDateLsns = messages\n .filter(isControlMessage)\n .map(({ headers }) => (headers.global_last_seen_lsn as number) ?? 0)\n if (upToDateLsns.length > 0) {\n const maxUpToDateLsn = Math.max(...upToDateLsns)\n const lastMaxUpToDateLsn = this.#lastUpToDateLsns[key]\n if (maxUpToDateLsn > lastMaxUpToDateLsn) {\n this.#lastUpToDateLsns[key] = maxUpToDateLsn\n }\n }\n\n // Whats the max lsn of the data messages?\n const dataLsns = messages\n .filter(isChangeMessage)\n .map(({ headers }) => (headers.lsn as number) ?? 0)\n if (dataLsns.length > 0) {\n const maxDataLsn = Math.max(...dataLsns)\n const lastMaxDataLsn = this.#lastDataLsns[key]\n if (maxDataLsn > lastMaxDataLsn) {\n this.#lastDataLsns[key] = maxDataLsn\n }\n // There is new data, so we need to schedule a check for updates on\n // other shapes\n this.#scheduleCheckForUpdates()\n }\n\n // Publish the messages to the multi-shape stream subscribers\n const multiShapeMessages = messages.map(\n (message) =>\n ({\n ...message,\n shape: key,\n }) as MultiShapeMessages<TShapeRows>\n )\n await this._publish(multiShapeMessages)\n },\n (error) => this.#onError(error)\n )\n }\n this.#started = true\n }\n\n #scheduleCheckForUpdates() {\n this.#checkForUpdatesTimeout ??= setTimeout(() => {\n this.#checkForUpdates()\n this.#checkForUpdatesTimeout = undefined\n }, this.checkForUpdatesAfterMs)\n }\n\n async #checkForUpdates() {\n const maxDataLsn = Math.max(...Object.values(this.#lastDataLsns))\n const refreshPromises = this.#shapeEntries()\n .filter(([key]) => {\n // We only need to refresh shapes that have not seen an up-to-date message\n // lower than the max lsn of the data messages we have received.\n const lastUpToDateLsn = this.#lastUpToDateLsns[key]\n return lastUpToDateLsn < maxDataLsn\n })\n .map(([_, shape]) => {\n return shape.forceDisconnectAndRefresh()\n })\n await Promise.all(refreshPromises)\n }\n\n #onError(error: Error) {\n // TODO: we probably want to disconnect all shapes here on the first error\n this.#subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n protected async _publish(\n messages: MultiShapeMessages<TShapeRows>[]\n ): Promise<void> {\n await Promise.all(\n Array.from(this.#subscribers.values()).map(async ([callback, __]) => {\n try {\n await callback(messages)\n } catch (err) {\n queueMicrotask(() => {\n throw err\n })\n }\n })\n )\n }\n\n /**\n * Returns an array of the shape entries.\n * Ensures that the shape entries are typed, as `Object.entries`\n * will not type the entries correctly.\n */\n #shapeEntries() {\n return Object.entries(this.#shapes) as [\n keyof TShapeRows & string,\n ShapeStream<TShapeRows[string]>,\n ][]\n }\n\n /**\n * The ShapeStreams that are being subscribed to.\n */\n get shapes() {\n return this.#shapes\n }\n\n subscribe(\n callback: (\n messages: MultiShapeMessages<TShapeRows>[]\n ) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n\n this.#subscribers.set(subscriptionId, [callback, onError])\n if (!this.#started) this.#start()\n\n return () => {\n this.#subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.#subscribers.clear()\n }\n\n /** Unix time at which we last synced. Undefined when `isLoading` is true. */\n lastSyncedAt(): number | undefined {\n // Min of all the lastSyncedAt values\n return Math.min(\n ...this.#shapeEntries().map(\n ([_, shape]) => shape.lastSyncedAt() ?? Infinity\n )\n )\n }\n\n /** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */\n lastSynced(): number {\n const lastSyncedAt = this.lastSyncedAt()\n if (lastSyncedAt === undefined) return Infinity\n return Date.now() - lastSyncedAt\n }\n\n /** Indicates if we are connected to the Electric sync service. */\n isConnected(): boolean {\n return this.#shapeEntries().every(([_, shape]) => shape.isConnected())\n }\n\n /** True during initial fetch. False afterwise. */\n isLoading(): boolean {\n return this.#shapeEntries().some(([_, shape]) => shape.isLoading())\n }\n\n get isUpToDate() {\n return this.#shapeEntries().every(([_, shape]) => shape.isUpToDate)\n }\n}\n\n/**\n * A transactional multi-shape stream is a multi-shape stream that emits the\n * messages in transactional batches, ensuring that all shapes will receive\n * at least an `up-to-date` message from Electric within the `checkForUpdatesAfterMs`\n * interval.\n * It uses the `lsn` metadata to infer transaction boundaries, and the `op_position`\n * metadata to sort the messages within a transaction.\n *\n * @constructor\n * @param {MultiShapeStreamOptions} options - configure the multi-shape stream\n * @example\n * ```ts\n * const transactionalMultiShapeStream = new TransactionalMultiShapeStream({\n * shapes: {\n * shape1: {\n * url: 'http://localhost:3000/v1/shape1',\n * },\n * shape2: {\n * url: 'http://localhost:3000/v1/shape2',\n * },\n * },\n * })\n *\n * transactionalMultiShapeStream.subscribe((msgs) => {\n * console.log(msgs)\n * })\n *\n * // or with ShapeStream instances\n * const transactionalMultiShapeStream = new TransactionalMultiShapeStream({\n * shapes: {\n * shape1: new ShapeStream({ url: 'http://localhost:3000/v1/shape1' }),\n * shape2: new ShapeStream({ url: 'http://localhost:3000/v1/shape2' }),\n * },\n * })\n * ```\n */\n\nexport class TransactionalMultiShapeStream<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> extends MultiShapeStream<TShapeRows> {\n #changeMessages = new Map<number, MultiShapeMessage<Row<unknown>, string>[]>()\n #completeLsns: {\n [K in keyof TShapeRows]: number\n }\n\n constructor(options: MultiShapeStreamOptions<TShapeRows>) {\n super(options)\n this.#completeLsns = Object.fromEntries(\n Object.entries(options.shapes).map(([key]) => [key, -Infinity])\n ) as { [K in keyof TShapeRows]: number }\n }\n\n #getLowestCompleteLsn() {\n return Math.min(...Object.values(this.#completeLsns))\n }\n\n protected async _publish(\n messages: MultiShapeMessages<TShapeRows>[]\n ): Promise<void> {\n this.#accumulate(messages)\n const lowestCompleteLsn = this.#getLowestCompleteLsn()\n const lsnsToPublish = [...this.#changeMessages.keys()].filter(\n (lsn) => lsn <= lowestCompleteLsn\n )\n const messagesToPublish = lsnsToPublish\n .sort((a, b) => a - b)\n .map((lsn) =>\n this.#changeMessages.get(lsn)?.sort((a, b) => {\n const { headers: aHeaders } = a\n const { headers: bHeaders } = b\n if (\n typeof aHeaders.op_position !== `number` ||\n typeof bHeaders.op_position !== `number`\n ) {\n return 0 // op_position is not present on the snapshot message\n }\n return aHeaders.op_position - bHeaders.op_position\n })\n )\n .filter((messages) => messages !== undefined)\n .flat() as MultiShapeMessages<TShapeRows>[]\n lsnsToPublish.forEach((lsn) => {\n this.#changeMessages.delete(lsn)\n })\n if (messagesToPublish.length > 0) {\n await super._publish(messagesToPublish)\n }\n }\n\n #accumulate(messages: MultiShapeMessages<TShapeRows>[]) {\n const isUpToDate = this.isUpToDate\n messages.forEach((message) => {\n const { shape, headers } = message\n if (isChangeMessage(message)) {\n // The snapshot message does not have an lsn, so we use 0\n const lsn = typeof headers.lsn === `number` ? headers.lsn : 0\n if (!this.#changeMessages.has(lsn)) {\n this.#changeMessages.set(lsn, [])\n }\n this.#changeMessages.get(lsn)?.push(message)\n if (\n isUpToDate && // All shapes must be up to date\n typeof headers.last === `boolean` &&\n headers.last === true\n ) {\n this.#completeLsns[shape] = Math.max(this.#completeLsns[shape], lsn)\n }\n } else if (isControlMessage(message)) {\n if (headers.control === `up-to-date`) {\n if (typeof headers.global_last_seen_lsn !== `number`) {\n throw new Error(`global_last_seen_lsn is not a number`)\n }\n this.#completeLsns[shape] = Math.max(\n this.#completeLsns[shape],\n headers.global_last_seen_lsn\n )\n }\n }\n })\n }\n}\n"],"mappings":"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","names":["message","isChangeMessage","isChangeMessage","messages"]}
|
|
1
|
+
{"version":3,"sources":["../src/match.ts","../src/bigint-utils.ts","../src/multi-shape-stream.ts"],"sourcesContent":["import {\n isChangeMessage,\n type ShapeStreamInterface,\n type ChangeMessage,\n type GetExtensions,\n type Operation,\n type Row,\n type Value,\n type Message,\n} from '@electric-sql/client'\n\nexport function matchStream<T extends Row<unknown>>(\n stream: ShapeStreamInterface<T>,\n operations: Array<Operation>,\n matchFn: (message: ChangeMessage<T>) => boolean,\n timeout = 60000 // ms\n): Promise<ChangeMessage<T>> {\n return new Promise<ChangeMessage<T>>((resolve, reject) => {\n const unsubscribe: () => void = stream.subscribe(\n (messages: Array<unknown>) => {\n const message = messages\n .filter((msg): msg is ChangeMessage<T> =>\n isChangeMessage(msg as Message<Row<never>>)\n )\n .find((message) => {\n const operation: Operation = message.headers.operation\n\n return operations.includes(operation) && matchFn(message)\n })\n\n if (message) {\n return finish(message)\n }\n }\n )\n\n const timeoutId: NodeJS.Timeout = setTimeout(() => {\n const msg: string = `matchStream timed out after ${timeout}ms`\n\n console.error(msg)\n\n reject(msg)\n }, timeout)\n\n function finish(message: ChangeMessage<T>): void {\n clearTimeout(timeoutId)\n\n unsubscribe()\n\n return resolve(message)\n }\n })\n}\n\nexport function matchBy<T extends Row<unknown>>(\n column: string,\n value: Value<GetExtensions<T>>\n): (message: ChangeMessage<T>) => boolean {\n return (message: ChangeMessage<T>) => message.value[column] === value\n}\n","export function bigIntMax(nums: Array<bigint | number>): bigint {\n return BigInt(nums.reduce((m, e) => (e > m ? e : m)))\n}\n\nexport function bigIntMin(nums: Array<bigint | number>): bigint {\n return BigInt(nums.reduce((m, e) => (e < m ? e : m)))\n}\n\nexport function bigIntCompare(a: bigint, b: bigint): 1 | -1 | 0 {\n return a > b ? 1 : a < b ? -1 : 0\n}\n","import { bigIntCompare, bigIntMax, bigIntMin } from './bigint-utils'\nimport {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n} from '@electric-sql/client'\nimport type {\n ChangeMessage,\n ControlMessage,\n FetchError,\n MaybePromise,\n Row,\n ShapeStreamOptions,\n} from '@electric-sql/client'\n\ninterface MultiShapeStreamOptions<\n TShapeRows extends {\n [K: string]: Row<unknown>\n } = {\n [K: string]: Row<unknown>\n },\n> {\n shapes: {\n [K in keyof TShapeRows]:\n | ShapeStreamOptions<TShapeRows[K]>\n | ShapeStream<TShapeRows[K]>\n }\n start?: boolean\n checkForUpdatesAfterMs?: number // milliseconds\n}\n\ninterface MultiShapeChangeMessage<\n T extends Row<unknown>,\n ShapeNames extends string,\n> extends ChangeMessage<T> {\n shape: ShapeNames\n}\n\ninterface MultiShapeControlMessage<ShapeNames extends string>\n extends ControlMessage {\n shape: ShapeNames\n}\n\ntype MultiShapeMessage<T extends Row<unknown>, ShapeNames extends string> =\n | MultiShapeChangeMessage<T, ShapeNames>\n | MultiShapeControlMessage<ShapeNames>\n\nexport type MultiShapeMessages<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> = {\n [K in keyof TShapeRows & string]: MultiShapeMessage<TShapeRows[K], K>\n}[keyof TShapeRows & string]\n\nexport interface MultiShapeStreamInterface<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> {\n shapes: { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n checkForUpdatesAfterMs?: number\n\n subscribe(\n callback: (\n messages: MultiShapeMessages<TShapeRows>[]\n ) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ): () => void\n unsubscribeAll(): void\n\n lastSyncedAt(): number | undefined\n lastSynced(): number\n isConnected(): boolean\n isLoading(): boolean\n\n isUpToDate: boolean\n}\n\n/**\n * A multi-shape stream is a stream that can subscribe to multiple shapes.\n * It ensures that all shapes will receive at least an `up-to-date` message from\n * Electric within the `checkForUpdatesAfterMs` interval.\n *\n * @constructor\n * @param {MultiShapeStreamOptions} options - configure the multi-shape stream\n * @example\n * ```ts\n * const multiShapeStream = new MultiShapeStream({\n * shapes: {\n * shape1: {\n * url: 'http://localhost:3000/v1/shape1',\n * },\n * shape2: {\n * url: 'http://localhost:3000/v1/shape2',\n * },\n * },\n * })\n *\n * multiShapeStream.subscribe((msgs) => {\n * console.log(msgs)\n * })\n *\n * // or with ShapeStream instances\n * const multiShapeStream = new MultiShapeStream({\n * shapes: {\n * shape1: new ShapeStream({ url: 'http://localhost:3000/v1/shape1' }),\n * shape2: new ShapeStream({ url: 'http://localhost:3000/v1/shape2' }),\n * },\n * })\n * ```\n */\n\nexport class MultiShapeStream<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> implements MultiShapeStreamInterface<TShapeRows>\n{\n #shapes: { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n #started = false\n checkForUpdatesAfterMs?: number\n\n #checkForUpdatesTimeout?: ReturnType<typeof setTimeout> | undefined\n\n // We keep track of the last lsn of data and up-to-date messages for each shape\n // so that we can skip checkForUpdates if the lsn of the up-to-date message is\n // greater than the last lsn of data.\n #lastDataLsns: { [K in keyof TShapeRows]: bigint }\n #lastUpToDateLsns: { [K in keyof TShapeRows]: bigint }\n\n readonly #subscribers = new Map<\n number,\n [\n (messages: MultiShapeMessages<TShapeRows>[]) => MaybePromise<void>,\n ((error: Error) => void) | undefined,\n ]\n >()\n\n constructor(options: MultiShapeStreamOptions<TShapeRows>) {\n const {\n start = true, // By default we start the multi-shape stream\n checkForUpdatesAfterMs = 100, // Force a check for updates after 100ms\n shapes,\n } = options\n this.checkForUpdatesAfterMs = checkForUpdatesAfterMs\n this.#shapes = Object.fromEntries(\n Object.entries(shapes).map(([key, shape]) => [\n key,\n shape instanceof ShapeStream\n ? shape\n : new ShapeStream<TShapeRows[typeof key]>({\n ...shape,\n start: false,\n }),\n ])\n ) as { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n this.#lastDataLsns = Object.fromEntries(\n Object.entries(shapes).map(([key]) => [key, BigInt(-1)])\n ) as { [K in keyof TShapeRows]: bigint }\n this.#lastUpToDateLsns = Object.fromEntries(\n Object.entries(shapes).map(([key]) => [key, BigInt(-1)])\n ) as { [K in keyof TShapeRows]: bigint }\n if (start) this.#start()\n }\n\n #start() {\n if (this.#started) throw new Error(`Cannot start multi-shape stream twice`)\n for (const [key, shape] of this.#shapeEntries()) {\n if (shape.hasStarted()) {\n // The multi-shape stream needs to be started together as a whole, and so we\n // have to check that a shape is not already started.\n throw new Error(`Shape ${key} already started`)\n }\n shape.subscribe(\n async (messages) => {\n // Whats the max lsn of the up-to-date messages?\n const upToDateLsns = messages\n .filter(isControlMessage)\n .map(({ headers }) =>\n typeof headers.global_last_seen_lsn === `string`\n ? BigInt(headers.global_last_seen_lsn)\n : BigInt(0)\n )\n if (upToDateLsns.length > 0) {\n const maxUpToDateLsn = bigIntMax(upToDateLsns)\n const lastMaxUpToDateLsn = this.#lastUpToDateLsns[key]\n if (maxUpToDateLsn > lastMaxUpToDateLsn) {\n this.#lastUpToDateLsns[key] = maxUpToDateLsn\n }\n }\n\n // Whats the max lsn of the data messages?\n const dataLsns = messages\n .filter(isChangeMessage)\n .map(({ headers }) =>\n typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)\n )\n if (dataLsns.length > 0) {\n const maxDataLsn = bigIntMax(dataLsns)\n const lastMaxDataLsn = this.#lastDataLsns[key]\n if (maxDataLsn > lastMaxDataLsn) {\n this.#lastDataLsns[key] = maxDataLsn\n }\n // There is new data, so we need to schedule a check for updates on\n // other shapes\n this.#scheduleCheckForUpdates()\n }\n\n // Publish the messages to the multi-shape stream subscribers\n const multiShapeMessages = messages.map(\n (message) =>\n ({\n ...message,\n shape: key,\n }) as MultiShapeMessages<TShapeRows>\n )\n await this._publish(multiShapeMessages)\n },\n (error) => this.#onError(error)\n )\n }\n this.#started = true\n }\n\n #scheduleCheckForUpdates() {\n this.#checkForUpdatesTimeout ??= setTimeout(() => {\n this.#checkForUpdates()\n this.#checkForUpdatesTimeout = undefined\n }, this.checkForUpdatesAfterMs)\n }\n\n async #checkForUpdates() {\n const maxDataLsn = bigIntMax(Object.values(this.#lastDataLsns))\n const refreshPromises = this.#shapeEntries()\n .filter(([key]) => {\n // We only need to refresh shapes that have not seen an up-to-date message\n // lower than the max lsn of the data messages we have received.\n const lastUpToDateLsn = this.#lastUpToDateLsns[key]\n return lastUpToDateLsn < maxDataLsn\n })\n .map(([_, shape]) => {\n return shape.forceDisconnectAndRefresh()\n })\n await Promise.all(refreshPromises)\n }\n\n #onError(error: Error) {\n // TODO: we probably want to disconnect all shapes here on the first error\n this.#subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n protected async _publish(\n messages: MultiShapeMessages<TShapeRows>[]\n ): Promise<void> {\n await Promise.all(\n Array.from(this.#subscribers.values()).map(async ([callback, __]) => {\n try {\n await callback(messages)\n } catch (err) {\n queueMicrotask(() => {\n throw err\n })\n }\n })\n )\n }\n\n /**\n * Returns an array of the shape entries.\n * Ensures that the shape entries are typed, as `Object.entries`\n * will not type the entries correctly.\n */\n #shapeEntries() {\n return Object.entries(this.#shapes) as [\n keyof TShapeRows & string,\n ShapeStream<TShapeRows[string]>,\n ][]\n }\n\n /**\n * The ShapeStreams that are being subscribed to.\n */\n get shapes() {\n return this.#shapes\n }\n\n subscribe(\n callback: (\n messages: MultiShapeMessages<TShapeRows>[]\n ) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n\n this.#subscribers.set(subscriptionId, [callback, onError])\n if (!this.#started) this.#start()\n\n return () => {\n this.#subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.#subscribers.clear()\n }\n\n /** Unix time at which we last synced. Undefined when `isLoading` is true. */\n lastSyncedAt(): number | undefined {\n // Min of all the lastSyncedAt values\n const shapeEntries = this.#shapeEntries()\n if (shapeEntries.length === 0) return\n return shapeEntries.reduce((minLastSyncedAt, [_, shape]) => {\n return Math.min(minLastSyncedAt, shape.lastSyncedAt() ?? Infinity)\n }, Infinity)\n }\n\n /** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */\n lastSynced(): number {\n const lastSyncedAt = this.lastSyncedAt()\n if (lastSyncedAt === undefined) return Infinity\n return Date.now() - lastSyncedAt\n }\n\n /** Indicates if we are connected to the Electric sync service. */\n isConnected(): boolean {\n return this.#shapeEntries().every(([_, shape]) => shape.isConnected())\n }\n\n /** True during initial fetch. False afterwise. */\n isLoading(): boolean {\n return this.#shapeEntries().some(([_, shape]) => shape.isLoading())\n }\n\n get isUpToDate() {\n return this.#shapeEntries().every(([_, shape]) => shape.isUpToDate)\n }\n}\n\n/**\n * A transactional multi-shape stream is a multi-shape stream that emits the\n * messages in transactional batches, ensuring that all shapes will receive\n * at least an `up-to-date` message from Electric within the `checkForUpdatesAfterMs`\n * interval.\n * It uses the `lsn` metadata to infer transaction boundaries, and the `op_position`\n * metadata to sort the messages within a transaction.\n *\n * @constructor\n * @param {MultiShapeStreamOptions} options - configure the multi-shape stream\n * @example\n * ```ts\n * const transactionalMultiShapeStream = new TransactionalMultiShapeStream({\n * shapes: {\n * shape1: {\n * url: 'http://localhost:3000/v1/shape1',\n * },\n * shape2: {\n * url: 'http://localhost:3000/v1/shape2',\n * },\n * },\n * })\n *\n * transactionalMultiShapeStream.subscribe((msgs) => {\n * console.log(msgs)\n * })\n *\n * // or with ShapeStream instances\n * const transactionalMultiShapeStream = new TransactionalMultiShapeStream({\n * shapes: {\n * shape1: new ShapeStream({ url: 'http://localhost:3000/v1/shape1' }),\n * shape2: new ShapeStream({ url: 'http://localhost:3000/v1/shape2' }),\n * },\n * })\n * ```\n */\n\nexport class TransactionalMultiShapeStream<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> extends MultiShapeStream<TShapeRows> {\n #changeMessages = new Map<bigint, MultiShapeMessage<Row<unknown>, string>[]>()\n #completeLsns: {\n [K in keyof TShapeRows]: bigint\n }\n\n constructor(options: MultiShapeStreamOptions<TShapeRows>) {\n super(options)\n this.#completeLsns = Object.fromEntries(\n Object.entries(options.shapes).map(([key]) => [key, BigInt(-1)])\n ) as { [K in keyof TShapeRows]: bigint }\n }\n\n #getLowestCompleteLsn() {\n return bigIntMin(Object.values(this.#completeLsns))\n }\n\n protected async _publish(\n messages: MultiShapeMessages<TShapeRows>[]\n ): Promise<void> {\n this.#accumulate(messages)\n const lowestCompleteLsn = this.#getLowestCompleteLsn()\n const lsnsToPublish = [...this.#changeMessages.keys()].filter(\n (lsn) => lsn <= lowestCompleteLsn\n )\n const messagesToPublish = lsnsToPublish\n .sort((a, b) => bigIntCompare(a, b))\n .map((lsn) =>\n this.#changeMessages.get(lsn)?.sort((a, b) => {\n const { headers: aHeaders } = a\n const { headers: bHeaders } = b\n if (\n typeof aHeaders.op_position !== `number` ||\n typeof bHeaders.op_position !== `number`\n ) {\n return 0 // op_position is not present on the snapshot message\n }\n return aHeaders.op_position - bHeaders.op_position\n })\n )\n .filter((messages) => messages !== undefined)\n .flat() as MultiShapeMessages<TShapeRows>[]\n lsnsToPublish.forEach((lsn) => {\n this.#changeMessages.delete(lsn)\n })\n if (messagesToPublish.length > 0) {\n await super._publish(messagesToPublish)\n }\n }\n\n #accumulate(messages: MultiShapeMessages<TShapeRows>[]) {\n const isUpToDate = this.isUpToDate\n messages.forEach((message) => {\n const { shape, headers } = message\n if (isChangeMessage(message)) {\n // The snapshot message does not have an lsn, so we use 0\n const lsn =\n typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)\n if (!this.#changeMessages.has(lsn)) {\n this.#changeMessages.set(lsn, [])\n }\n this.#changeMessages.get(lsn)?.push(message)\n if (\n isUpToDate && // All shapes must be up to date\n typeof headers.last === `boolean` &&\n headers.last === true\n ) {\n this.#completeLsns[shape] = bigIntMax([\n this.#completeLsns[shape],\n lsn,\n ])\n }\n } else if (isControlMessage(message)) {\n if (headers.control === `up-to-date`) {\n if (typeof headers.global_last_seen_lsn !== `string`) {\n throw new Error(`global_last_seen_lsn is not a number`)\n }\n this.#completeLsns[shape] = bigIntMax([\n this.#completeLsns[shape],\n BigInt(headers.global_last_seen_lsn),\n ])\n }\n }\n })\n }\n}\n"],"mappings":"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","names":["message","isChangeMessage","isChangeMessage","messages"]}
|
package/dist/index.mjs
CHANGED
|
@@ -84,6 +84,17 @@ function matchBy(column, value) {
|
|
|
84
84
|
return (message) => message.value[column] === value;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// src/bigint-utils.ts
|
|
88
|
+
function bigIntMax(nums) {
|
|
89
|
+
return BigInt(nums.reduce((m, e) => e > m ? e : m));
|
|
90
|
+
}
|
|
91
|
+
function bigIntMin(nums) {
|
|
92
|
+
return BigInt(nums.reduce((m, e) => e < m ? e : m));
|
|
93
|
+
}
|
|
94
|
+
function bigIntCompare(a, b) {
|
|
95
|
+
return a > b ? 1 : a < b ? -1 : 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
// src/multi-shape-stream.ts
|
|
88
99
|
import {
|
|
89
100
|
ShapeStream,
|
|
@@ -120,10 +131,10 @@ var MultiShapeStream = class {
|
|
|
120
131
|
])
|
|
121
132
|
));
|
|
122
133
|
__privateSet(this, _lastDataLsns, Object.fromEntries(
|
|
123
|
-
Object.entries(shapes).map(([key]) => [key, -
|
|
134
|
+
Object.entries(shapes).map(([key]) => [key, BigInt(-1)])
|
|
124
135
|
));
|
|
125
136
|
__privateSet(this, _lastUpToDateLsns, Object.fromEntries(
|
|
126
|
-
Object.entries(shapes).map(([key]) => [key, -
|
|
137
|
+
Object.entries(shapes).map(([key]) => [key, BigInt(-1)])
|
|
127
138
|
));
|
|
128
139
|
if (start) __privateMethod(this, _MultiShapeStream_instances, start_fn).call(this);
|
|
129
140
|
}
|
|
@@ -161,14 +172,12 @@ var MultiShapeStream = class {
|
|
|
161
172
|
}
|
|
162
173
|
/** Unix time at which we last synced. Undefined when `isLoading` is true. */
|
|
163
174
|
lastSyncedAt() {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
)
|
|
171
|
-
);
|
|
175
|
+
const shapeEntries = __privateMethod(this, _MultiShapeStream_instances, shapeEntries_fn).call(this);
|
|
176
|
+
if (shapeEntries.length === 0) return;
|
|
177
|
+
return shapeEntries.reduce((minLastSyncedAt, [_, shape]) => {
|
|
178
|
+
var _a;
|
|
179
|
+
return Math.min(minLastSyncedAt, (_a = shape.lastSyncedAt()) != null ? _a : Infinity);
|
|
180
|
+
}, Infinity);
|
|
172
181
|
}
|
|
173
182
|
/** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */
|
|
174
183
|
lastSynced() {
|
|
@@ -203,23 +212,21 @@ start_fn = function() {
|
|
|
203
212
|
}
|
|
204
213
|
shape.subscribe(
|
|
205
214
|
(messages) => __async(this, null, function* () {
|
|
206
|
-
const upToDateLsns = messages.filter(isControlMessage).map(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
});
|
|
215
|
+
const upToDateLsns = messages.filter(isControlMessage).map(
|
|
216
|
+
({ headers }) => typeof headers.global_last_seen_lsn === `string` ? BigInt(headers.global_last_seen_lsn) : BigInt(0)
|
|
217
|
+
);
|
|
210
218
|
if (upToDateLsns.length > 0) {
|
|
211
|
-
const maxUpToDateLsn =
|
|
219
|
+
const maxUpToDateLsn = bigIntMax(upToDateLsns);
|
|
212
220
|
const lastMaxUpToDateLsn = __privateGet(this, _lastUpToDateLsns)[key];
|
|
213
221
|
if (maxUpToDateLsn > lastMaxUpToDateLsn) {
|
|
214
222
|
__privateGet(this, _lastUpToDateLsns)[key] = maxUpToDateLsn;
|
|
215
223
|
}
|
|
216
224
|
}
|
|
217
|
-
const dataLsns = messages.filter(isChangeMessage2).map(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
});
|
|
225
|
+
const dataLsns = messages.filter(isChangeMessage2).map(
|
|
226
|
+
({ headers }) => typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)
|
|
227
|
+
);
|
|
221
228
|
if (dataLsns.length > 0) {
|
|
222
|
-
const maxDataLsn =
|
|
229
|
+
const maxDataLsn = bigIntMax(dataLsns);
|
|
223
230
|
const lastMaxDataLsn = __privateGet(this, _lastDataLsns)[key];
|
|
224
231
|
if (maxDataLsn > lastMaxDataLsn) {
|
|
225
232
|
__privateGet(this, _lastDataLsns)[key] = maxDataLsn;
|
|
@@ -247,7 +254,7 @@ scheduleCheckForUpdates_fn = function() {
|
|
|
247
254
|
};
|
|
248
255
|
checkForUpdates_fn = function() {
|
|
249
256
|
return __async(this, null, function* () {
|
|
250
|
-
const maxDataLsn =
|
|
257
|
+
const maxDataLsn = bigIntMax(Object.values(__privateGet(this, _lastDataLsns)));
|
|
251
258
|
const refreshPromises = __privateMethod(this, _MultiShapeStream_instances, shapeEntries_fn).call(this).filter(([key]) => {
|
|
252
259
|
const lastUpToDateLsn = __privateGet(this, _lastUpToDateLsns)[key];
|
|
253
260
|
return lastUpToDateLsn < maxDataLsn;
|
|
@@ -278,7 +285,7 @@ var _TransactionalMultiShapeStream = class _TransactionalMultiShapeStream extend
|
|
|
278
285
|
__privateAdd(this, _changeMessages, /* @__PURE__ */ new Map());
|
|
279
286
|
__privateAdd(this, _completeLsns);
|
|
280
287
|
__privateSet(this, _completeLsns, Object.fromEntries(
|
|
281
|
-
Object.entries(options.shapes).map(([key]) => [key, -
|
|
288
|
+
Object.entries(options.shapes).map(([key]) => [key, BigInt(-1)])
|
|
282
289
|
));
|
|
283
290
|
}
|
|
284
291
|
_publish(messages) {
|
|
@@ -288,7 +295,7 @@ var _TransactionalMultiShapeStream = class _TransactionalMultiShapeStream extend
|
|
|
288
295
|
const lsnsToPublish = [...__privateGet(this, _changeMessages).keys()].filter(
|
|
289
296
|
(lsn) => lsn <= lowestCompleteLsn
|
|
290
297
|
);
|
|
291
|
-
const messagesToPublish = lsnsToPublish.sort((a, b) => a
|
|
298
|
+
const messagesToPublish = lsnsToPublish.sort((a, b) => bigIntCompare(a, b)).map(
|
|
292
299
|
(lsn) => {
|
|
293
300
|
var _a;
|
|
294
301
|
return (_a = __privateGet(this, _changeMessages).get(lsn)) == null ? void 0 : _a.sort((a, b) => {
|
|
@@ -314,7 +321,7 @@ _changeMessages = new WeakMap();
|
|
|
314
321
|
_completeLsns = new WeakMap();
|
|
315
322
|
_TransactionalMultiShapeStream_instances = new WeakSet();
|
|
316
323
|
getLowestCompleteLsn_fn = function() {
|
|
317
|
-
return
|
|
324
|
+
return bigIntMin(Object.values(__privateGet(this, _completeLsns)));
|
|
318
325
|
};
|
|
319
326
|
accumulate_fn = function(messages) {
|
|
320
327
|
const isUpToDate = this.isUpToDate;
|
|
@@ -322,24 +329,27 @@ accumulate_fn = function(messages) {
|
|
|
322
329
|
var _a;
|
|
323
330
|
const { shape, headers } = message;
|
|
324
331
|
if (isChangeMessage2(message)) {
|
|
325
|
-
const lsn = typeof headers.lsn === `
|
|
332
|
+
const lsn = typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0);
|
|
326
333
|
if (!__privateGet(this, _changeMessages).has(lsn)) {
|
|
327
334
|
__privateGet(this, _changeMessages).set(lsn, []);
|
|
328
335
|
}
|
|
329
336
|
(_a = __privateGet(this, _changeMessages).get(lsn)) == null ? void 0 : _a.push(message);
|
|
330
337
|
if (isUpToDate && // All shapes must be up to date
|
|
331
338
|
typeof headers.last === `boolean` && headers.last === true) {
|
|
332
|
-
__privateGet(this, _completeLsns)[shape] =
|
|
339
|
+
__privateGet(this, _completeLsns)[shape] = bigIntMax([
|
|
340
|
+
__privateGet(this, _completeLsns)[shape],
|
|
341
|
+
lsn
|
|
342
|
+
]);
|
|
333
343
|
}
|
|
334
344
|
} else if (isControlMessage(message)) {
|
|
335
345
|
if (headers.control === `up-to-date`) {
|
|
336
|
-
if (typeof headers.global_last_seen_lsn !== `
|
|
346
|
+
if (typeof headers.global_last_seen_lsn !== `string`) {
|
|
337
347
|
throw new Error(`global_last_seen_lsn is not a number`);
|
|
338
348
|
}
|
|
339
|
-
__privateGet(this, _completeLsns)[shape] =
|
|
349
|
+
__privateGet(this, _completeLsns)[shape] = bigIntMax([
|
|
340
350
|
__privateGet(this, _completeLsns)[shape],
|
|
341
|
-
headers.global_last_seen_lsn
|
|
342
|
-
);
|
|
351
|
+
BigInt(headers.global_last_seen_lsn)
|
|
352
|
+
]);
|
|
343
353
|
}
|
|
344
354
|
}
|
|
345
355
|
});
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/match.ts","../src/multi-shape-stream.ts"],"sourcesContent":["import {\n isChangeMessage,\n type ShapeStreamInterface,\n type ChangeMessage,\n type GetExtensions,\n type Operation,\n type Row,\n type Value,\n type Message,\n} from '@electric-sql/client'\n\nexport function matchStream<T extends Row<unknown>>(\n stream: ShapeStreamInterface<T>,\n operations: Array<Operation>,\n matchFn: (message: ChangeMessage<T>) => boolean,\n timeout = 60000 // ms\n): Promise<ChangeMessage<T>> {\n return new Promise<ChangeMessage<T>>((resolve, reject) => {\n const unsubscribe: () => void = stream.subscribe(\n (messages: Array<unknown>) => {\n const message = messages\n .filter((msg): msg is ChangeMessage<T> =>\n isChangeMessage(msg as Message<Row<never>>)\n )\n .find((message) => {\n const operation: Operation = message.headers.operation\n\n return operations.includes(operation) && matchFn(message)\n })\n\n if (message) {\n return finish(message)\n }\n }\n )\n\n const timeoutId: NodeJS.Timeout = setTimeout(() => {\n const msg: string = `matchStream timed out after ${timeout}ms`\n\n console.error(msg)\n\n reject(msg)\n }, timeout)\n\n function finish(message: ChangeMessage<T>): void {\n clearTimeout(timeoutId)\n\n unsubscribe()\n\n return resolve(message)\n }\n })\n}\n\nexport function matchBy<T extends Row<unknown>>(\n column: string,\n value: Value<GetExtensions<T>>\n): (message: ChangeMessage<T>) => boolean {\n return (message: ChangeMessage<T>) => message.value[column] === value\n}\n","import {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n} from '@electric-sql/client'\nimport type {\n ChangeMessage,\n ControlMessage,\n FetchError,\n MaybePromise,\n Row,\n ShapeStreamOptions,\n} from '@electric-sql/client'\n\ninterface MultiShapeStreamOptions<\n TShapeRows extends {\n [K: string]: Row<unknown>\n } = {\n [K: string]: Row<unknown>\n },\n> {\n shapes: {\n [K in keyof TShapeRows]:\n | ShapeStreamOptions<TShapeRows[K]>\n | ShapeStream<TShapeRows[K]>\n }\n start?: boolean\n checkForUpdatesAfterMs?: number // milliseconds\n}\n\ninterface MultiShapeChangeMessage<\n T extends Row<unknown>,\n ShapeNames extends string,\n> extends ChangeMessage<T> {\n shape: ShapeNames\n}\n\ninterface MultiShapeControlMessage<ShapeNames extends string>\n extends ControlMessage {\n shape: ShapeNames\n}\n\ntype MultiShapeMessage<T extends Row<unknown>, ShapeNames extends string> =\n | MultiShapeChangeMessage<T, ShapeNames>\n | MultiShapeControlMessage<ShapeNames>\n\nexport type MultiShapeMessages<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> = {\n [K in keyof TShapeRows & string]: MultiShapeMessage<TShapeRows[K], K>\n}[keyof TShapeRows & string]\n\nexport interface MultiShapeStreamInterface<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> {\n shapes: { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n checkForUpdatesAfterMs?: number\n\n subscribe(\n callback: (\n messages: MultiShapeMessages<TShapeRows>[]\n ) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ): () => void\n unsubscribeAll(): void\n\n lastSyncedAt(): number | undefined\n lastSynced(): number\n isConnected(): boolean\n isLoading(): boolean\n\n isUpToDate: boolean\n}\n\n/**\n * A multi-shape stream is a stream that can subscribe to multiple shapes.\n * It ensures that all shapes will receive at least an `up-to-date` message from\n * Electric within the `checkForUpdatesAfterMs` interval.\n *\n * @constructor\n * @param {MultiShapeStreamOptions} options - configure the multi-shape stream\n * @example\n * ```ts\n * const multiShapeStream = new MultiShapeStream({\n * shapes: {\n * shape1: {\n * url: 'http://localhost:3000/v1/shape1',\n * },\n * shape2: {\n * url: 'http://localhost:3000/v1/shape2',\n * },\n * },\n * })\n *\n * multiShapeStream.subscribe((msgs) => {\n * console.log(msgs)\n * })\n *\n * // or with ShapeStream instances\n * const multiShapeStream = new MultiShapeStream({\n * shapes: {\n * shape1: new ShapeStream({ url: 'http://localhost:3000/v1/shape1' }),\n * shape2: new ShapeStream({ url: 'http://localhost:3000/v1/shape2' }),\n * },\n * })\n * ```\n */\n\nexport class MultiShapeStream<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> implements MultiShapeStreamInterface<TShapeRows>\n{\n #shapes: { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n #started = false\n checkForUpdatesAfterMs?: number\n\n #checkForUpdatesTimeout?: ReturnType<typeof setTimeout> | undefined\n\n // We keep track of the last lsn of data and up-to-date messages for each shape\n // so that we can skip checkForUpdates if the lsn of the up-to-date message is\n // greater than the last lsn of data.\n #lastDataLsns: { [K in keyof TShapeRows]: number }\n #lastUpToDateLsns: { [K in keyof TShapeRows]: number }\n\n readonly #subscribers = new Map<\n number,\n [\n (messages: MultiShapeMessages<TShapeRows>[]) => MaybePromise<void>,\n ((error: Error) => void) | undefined,\n ]\n >()\n\n constructor(options: MultiShapeStreamOptions<TShapeRows>) {\n const {\n start = true, // By default we start the multi-shape stream\n checkForUpdatesAfterMs = 100, // Force a check for updates after 100ms\n shapes,\n } = options\n this.checkForUpdatesAfterMs = checkForUpdatesAfterMs\n this.#shapes = Object.fromEntries(\n Object.entries(shapes).map(([key, shape]) => [\n key,\n shape instanceof ShapeStream\n ? shape\n : new ShapeStream<TShapeRows[typeof key]>({\n ...shape,\n start: false,\n }),\n ])\n ) as { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n this.#lastDataLsns = Object.fromEntries(\n Object.entries(shapes).map(([key]) => [key, -Infinity])\n ) as { [K in keyof TShapeRows]: number }\n this.#lastUpToDateLsns = Object.fromEntries(\n Object.entries(shapes).map(([key]) => [key, -Infinity])\n ) as { [K in keyof TShapeRows]: number }\n if (start) this.#start()\n }\n\n #start() {\n if (this.#started) throw new Error(`Cannot start multi-shape stream twice`)\n for (const [key, shape] of this.#shapeEntries()) {\n if (shape.hasStarted()) {\n // The multi-shape stream needs to be started together as a whole, and so we\n // have to check that a shape is not already started.\n throw new Error(`Shape ${key} already started`)\n }\n shape.subscribe(\n async (messages) => {\n // Whats the max lsn of the up-to-date messages?\n const upToDateLsns = messages\n .filter(isControlMessage)\n .map(({ headers }) => (headers.global_last_seen_lsn as number) ?? 0)\n if (upToDateLsns.length > 0) {\n const maxUpToDateLsn = Math.max(...upToDateLsns)\n const lastMaxUpToDateLsn = this.#lastUpToDateLsns[key]\n if (maxUpToDateLsn > lastMaxUpToDateLsn) {\n this.#lastUpToDateLsns[key] = maxUpToDateLsn\n }\n }\n\n // Whats the max lsn of the data messages?\n const dataLsns = messages\n .filter(isChangeMessage)\n .map(({ headers }) => (headers.lsn as number) ?? 0)\n if (dataLsns.length > 0) {\n const maxDataLsn = Math.max(...dataLsns)\n const lastMaxDataLsn = this.#lastDataLsns[key]\n if (maxDataLsn > lastMaxDataLsn) {\n this.#lastDataLsns[key] = maxDataLsn\n }\n // There is new data, so we need to schedule a check for updates on\n // other shapes\n this.#scheduleCheckForUpdates()\n }\n\n // Publish the messages to the multi-shape stream subscribers\n const multiShapeMessages = messages.map(\n (message) =>\n ({\n ...message,\n shape: key,\n }) as MultiShapeMessages<TShapeRows>\n )\n await this._publish(multiShapeMessages)\n },\n (error) => this.#onError(error)\n )\n }\n this.#started = true\n }\n\n #scheduleCheckForUpdates() {\n this.#checkForUpdatesTimeout ??= setTimeout(() => {\n this.#checkForUpdates()\n this.#checkForUpdatesTimeout = undefined\n }, this.checkForUpdatesAfterMs)\n }\n\n async #checkForUpdates() {\n const maxDataLsn = Math.max(...Object.values(this.#lastDataLsns))\n const refreshPromises = this.#shapeEntries()\n .filter(([key]) => {\n // We only need to refresh shapes that have not seen an up-to-date message\n // lower than the max lsn of the data messages we have received.\n const lastUpToDateLsn = this.#lastUpToDateLsns[key]\n return lastUpToDateLsn < maxDataLsn\n })\n .map(([_, shape]) => {\n return shape.forceDisconnectAndRefresh()\n })\n await Promise.all(refreshPromises)\n }\n\n #onError(error: Error) {\n // TODO: we probably want to disconnect all shapes here on the first error\n this.#subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n protected async _publish(\n messages: MultiShapeMessages<TShapeRows>[]\n ): Promise<void> {\n await Promise.all(\n Array.from(this.#subscribers.values()).map(async ([callback, __]) => {\n try {\n await callback(messages)\n } catch (err) {\n queueMicrotask(() => {\n throw err\n })\n }\n })\n )\n }\n\n /**\n * Returns an array of the shape entries.\n * Ensures that the shape entries are typed, as `Object.entries`\n * will not type the entries correctly.\n */\n #shapeEntries() {\n return Object.entries(this.#shapes) as [\n keyof TShapeRows & string,\n ShapeStream<TShapeRows[string]>,\n ][]\n }\n\n /**\n * The ShapeStreams that are being subscribed to.\n */\n get shapes() {\n return this.#shapes\n }\n\n subscribe(\n callback: (\n messages: MultiShapeMessages<TShapeRows>[]\n ) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n\n this.#subscribers.set(subscriptionId, [callback, onError])\n if (!this.#started) this.#start()\n\n return () => {\n this.#subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.#subscribers.clear()\n }\n\n /** Unix time at which we last synced. Undefined when `isLoading` is true. */\n lastSyncedAt(): number | undefined {\n // Min of all the lastSyncedAt values\n return Math.min(\n ...this.#shapeEntries().map(\n ([_, shape]) => shape.lastSyncedAt() ?? Infinity\n )\n )\n }\n\n /** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */\n lastSynced(): number {\n const lastSyncedAt = this.lastSyncedAt()\n if (lastSyncedAt === undefined) return Infinity\n return Date.now() - lastSyncedAt\n }\n\n /** Indicates if we are connected to the Electric sync service. */\n isConnected(): boolean {\n return this.#shapeEntries().every(([_, shape]) => shape.isConnected())\n }\n\n /** True during initial fetch. False afterwise. */\n isLoading(): boolean {\n return this.#shapeEntries().some(([_, shape]) => shape.isLoading())\n }\n\n get isUpToDate() {\n return this.#shapeEntries().every(([_, shape]) => shape.isUpToDate)\n }\n}\n\n/**\n * A transactional multi-shape stream is a multi-shape stream that emits the\n * messages in transactional batches, ensuring that all shapes will receive\n * at least an `up-to-date` message from Electric within the `checkForUpdatesAfterMs`\n * interval.\n * It uses the `lsn` metadata to infer transaction boundaries, and the `op_position`\n * metadata to sort the messages within a transaction.\n *\n * @constructor\n * @param {MultiShapeStreamOptions} options - configure the multi-shape stream\n * @example\n * ```ts\n * const transactionalMultiShapeStream = new TransactionalMultiShapeStream({\n * shapes: {\n * shape1: {\n * url: 'http://localhost:3000/v1/shape1',\n * },\n * shape2: {\n * url: 'http://localhost:3000/v1/shape2',\n * },\n * },\n * })\n *\n * transactionalMultiShapeStream.subscribe((msgs) => {\n * console.log(msgs)\n * })\n *\n * // or with ShapeStream instances\n * const transactionalMultiShapeStream = new TransactionalMultiShapeStream({\n * shapes: {\n * shape1: new ShapeStream({ url: 'http://localhost:3000/v1/shape1' }),\n * shape2: new ShapeStream({ url: 'http://localhost:3000/v1/shape2' }),\n * },\n * })\n * ```\n */\n\nexport class TransactionalMultiShapeStream<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> extends MultiShapeStream<TShapeRows> {\n #changeMessages = new Map<number, MultiShapeMessage<Row<unknown>, string>[]>()\n #completeLsns: {\n [K in keyof TShapeRows]: number\n }\n\n constructor(options: MultiShapeStreamOptions<TShapeRows>) {\n super(options)\n this.#completeLsns = Object.fromEntries(\n Object.entries(options.shapes).map(([key]) => [key, -Infinity])\n ) as { [K in keyof TShapeRows]: number }\n }\n\n #getLowestCompleteLsn() {\n return Math.min(...Object.values(this.#completeLsns))\n }\n\n protected async _publish(\n messages: MultiShapeMessages<TShapeRows>[]\n ): Promise<void> {\n this.#accumulate(messages)\n const lowestCompleteLsn = this.#getLowestCompleteLsn()\n const lsnsToPublish = [...this.#changeMessages.keys()].filter(\n (lsn) => lsn <= lowestCompleteLsn\n )\n const messagesToPublish = lsnsToPublish\n .sort((a, b) => a - b)\n .map((lsn) =>\n this.#changeMessages.get(lsn)?.sort((a, b) => {\n const { headers: aHeaders } = a\n const { headers: bHeaders } = b\n if (\n typeof aHeaders.op_position !== `number` ||\n typeof bHeaders.op_position !== `number`\n ) {\n return 0 // op_position is not present on the snapshot message\n }\n return aHeaders.op_position - bHeaders.op_position\n })\n )\n .filter((messages) => messages !== undefined)\n .flat() as MultiShapeMessages<TShapeRows>[]\n lsnsToPublish.forEach((lsn) => {\n this.#changeMessages.delete(lsn)\n })\n if (messagesToPublish.length > 0) {\n await super._publish(messagesToPublish)\n }\n }\n\n #accumulate(messages: MultiShapeMessages<TShapeRows>[]) {\n const isUpToDate = this.isUpToDate\n messages.forEach((message) => {\n const { shape, headers } = message\n if (isChangeMessage(message)) {\n // The snapshot message does not have an lsn, so we use 0\n const lsn = typeof headers.lsn === `number` ? headers.lsn : 0\n if (!this.#changeMessages.has(lsn)) {\n this.#changeMessages.set(lsn, [])\n }\n this.#changeMessages.get(lsn)?.push(message)\n if (\n isUpToDate && // All shapes must be up to date\n typeof headers.last === `boolean` &&\n headers.last === true\n ) {\n this.#completeLsns[shape] = Math.max(this.#completeLsns[shape], lsn)\n }\n } else if (isControlMessage(message)) {\n if (headers.control === `up-to-date`) {\n if (typeof headers.global_last_seen_lsn !== `number`) {\n throw new Error(`global_last_seen_lsn is not a number`)\n }\n this.#completeLsns[shape] = Math.max(\n this.#completeLsns[shape],\n headers.global_last_seen_lsn\n )\n }\n }\n })\n }\n}\n"],"mappings":"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","names":["message","isChangeMessage","isChangeMessage","messages"]}
|
|
1
|
+
{"version":3,"sources":["../src/match.ts","../src/bigint-utils.ts","../src/multi-shape-stream.ts"],"sourcesContent":["import {\n isChangeMessage,\n type ShapeStreamInterface,\n type ChangeMessage,\n type GetExtensions,\n type Operation,\n type Row,\n type Value,\n type Message,\n} from '@electric-sql/client'\n\nexport function matchStream<T extends Row<unknown>>(\n stream: ShapeStreamInterface<T>,\n operations: Array<Operation>,\n matchFn: (message: ChangeMessage<T>) => boolean,\n timeout = 60000 // ms\n): Promise<ChangeMessage<T>> {\n return new Promise<ChangeMessage<T>>((resolve, reject) => {\n const unsubscribe: () => void = stream.subscribe(\n (messages: Array<unknown>) => {\n const message = messages\n .filter((msg): msg is ChangeMessage<T> =>\n isChangeMessage(msg as Message<Row<never>>)\n )\n .find((message) => {\n const operation: Operation = message.headers.operation\n\n return operations.includes(operation) && matchFn(message)\n })\n\n if (message) {\n return finish(message)\n }\n }\n )\n\n const timeoutId: NodeJS.Timeout = setTimeout(() => {\n const msg: string = `matchStream timed out after ${timeout}ms`\n\n console.error(msg)\n\n reject(msg)\n }, timeout)\n\n function finish(message: ChangeMessage<T>): void {\n clearTimeout(timeoutId)\n\n unsubscribe()\n\n return resolve(message)\n }\n })\n}\n\nexport function matchBy<T extends Row<unknown>>(\n column: string,\n value: Value<GetExtensions<T>>\n): (message: ChangeMessage<T>) => boolean {\n return (message: ChangeMessage<T>) => message.value[column] === value\n}\n","export function bigIntMax(nums: Array<bigint | number>): bigint {\n return BigInt(nums.reduce((m, e) => (e > m ? e : m)))\n}\n\nexport function bigIntMin(nums: Array<bigint | number>): bigint {\n return BigInt(nums.reduce((m, e) => (e < m ? e : m)))\n}\n\nexport function bigIntCompare(a: bigint, b: bigint): 1 | -1 | 0 {\n return a > b ? 1 : a < b ? -1 : 0\n}\n","import { bigIntCompare, bigIntMax, bigIntMin } from './bigint-utils'\nimport {\n ShapeStream,\n isChangeMessage,\n isControlMessage,\n} from '@electric-sql/client'\nimport type {\n ChangeMessage,\n ControlMessage,\n FetchError,\n MaybePromise,\n Row,\n ShapeStreamOptions,\n} from '@electric-sql/client'\n\ninterface MultiShapeStreamOptions<\n TShapeRows extends {\n [K: string]: Row<unknown>\n } = {\n [K: string]: Row<unknown>\n },\n> {\n shapes: {\n [K in keyof TShapeRows]:\n | ShapeStreamOptions<TShapeRows[K]>\n | ShapeStream<TShapeRows[K]>\n }\n start?: boolean\n checkForUpdatesAfterMs?: number // milliseconds\n}\n\ninterface MultiShapeChangeMessage<\n T extends Row<unknown>,\n ShapeNames extends string,\n> extends ChangeMessage<T> {\n shape: ShapeNames\n}\n\ninterface MultiShapeControlMessage<ShapeNames extends string>\n extends ControlMessage {\n shape: ShapeNames\n}\n\ntype MultiShapeMessage<T extends Row<unknown>, ShapeNames extends string> =\n | MultiShapeChangeMessage<T, ShapeNames>\n | MultiShapeControlMessage<ShapeNames>\n\nexport type MultiShapeMessages<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> = {\n [K in keyof TShapeRows & string]: MultiShapeMessage<TShapeRows[K], K>\n}[keyof TShapeRows & string]\n\nexport interface MultiShapeStreamInterface<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> {\n shapes: { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n checkForUpdatesAfterMs?: number\n\n subscribe(\n callback: (\n messages: MultiShapeMessages<TShapeRows>[]\n ) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ): () => void\n unsubscribeAll(): void\n\n lastSyncedAt(): number | undefined\n lastSynced(): number\n isConnected(): boolean\n isLoading(): boolean\n\n isUpToDate: boolean\n}\n\n/**\n * A multi-shape stream is a stream that can subscribe to multiple shapes.\n * It ensures that all shapes will receive at least an `up-to-date` message from\n * Electric within the `checkForUpdatesAfterMs` interval.\n *\n * @constructor\n * @param {MultiShapeStreamOptions} options - configure the multi-shape stream\n * @example\n * ```ts\n * const multiShapeStream = new MultiShapeStream({\n * shapes: {\n * shape1: {\n * url: 'http://localhost:3000/v1/shape1',\n * },\n * shape2: {\n * url: 'http://localhost:3000/v1/shape2',\n * },\n * },\n * })\n *\n * multiShapeStream.subscribe((msgs) => {\n * console.log(msgs)\n * })\n *\n * // or with ShapeStream instances\n * const multiShapeStream = new MultiShapeStream({\n * shapes: {\n * shape1: new ShapeStream({ url: 'http://localhost:3000/v1/shape1' }),\n * shape2: new ShapeStream({ url: 'http://localhost:3000/v1/shape2' }),\n * },\n * })\n * ```\n */\n\nexport class MultiShapeStream<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> implements MultiShapeStreamInterface<TShapeRows>\n{\n #shapes: { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n #started = false\n checkForUpdatesAfterMs?: number\n\n #checkForUpdatesTimeout?: ReturnType<typeof setTimeout> | undefined\n\n // We keep track of the last lsn of data and up-to-date messages for each shape\n // so that we can skip checkForUpdates if the lsn of the up-to-date message is\n // greater than the last lsn of data.\n #lastDataLsns: { [K in keyof TShapeRows]: bigint }\n #lastUpToDateLsns: { [K in keyof TShapeRows]: bigint }\n\n readonly #subscribers = new Map<\n number,\n [\n (messages: MultiShapeMessages<TShapeRows>[]) => MaybePromise<void>,\n ((error: Error) => void) | undefined,\n ]\n >()\n\n constructor(options: MultiShapeStreamOptions<TShapeRows>) {\n const {\n start = true, // By default we start the multi-shape stream\n checkForUpdatesAfterMs = 100, // Force a check for updates after 100ms\n shapes,\n } = options\n this.checkForUpdatesAfterMs = checkForUpdatesAfterMs\n this.#shapes = Object.fromEntries(\n Object.entries(shapes).map(([key, shape]) => [\n key,\n shape instanceof ShapeStream\n ? shape\n : new ShapeStream<TShapeRows[typeof key]>({\n ...shape,\n start: false,\n }),\n ])\n ) as { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }\n this.#lastDataLsns = Object.fromEntries(\n Object.entries(shapes).map(([key]) => [key, BigInt(-1)])\n ) as { [K in keyof TShapeRows]: bigint }\n this.#lastUpToDateLsns = Object.fromEntries(\n Object.entries(shapes).map(([key]) => [key, BigInt(-1)])\n ) as { [K in keyof TShapeRows]: bigint }\n if (start) this.#start()\n }\n\n #start() {\n if (this.#started) throw new Error(`Cannot start multi-shape stream twice`)\n for (const [key, shape] of this.#shapeEntries()) {\n if (shape.hasStarted()) {\n // The multi-shape stream needs to be started together as a whole, and so we\n // have to check that a shape is not already started.\n throw new Error(`Shape ${key} already started`)\n }\n shape.subscribe(\n async (messages) => {\n // Whats the max lsn of the up-to-date messages?\n const upToDateLsns = messages\n .filter(isControlMessage)\n .map(({ headers }) =>\n typeof headers.global_last_seen_lsn === `string`\n ? BigInt(headers.global_last_seen_lsn)\n : BigInt(0)\n )\n if (upToDateLsns.length > 0) {\n const maxUpToDateLsn = bigIntMax(upToDateLsns)\n const lastMaxUpToDateLsn = this.#lastUpToDateLsns[key]\n if (maxUpToDateLsn > lastMaxUpToDateLsn) {\n this.#lastUpToDateLsns[key] = maxUpToDateLsn\n }\n }\n\n // Whats the max lsn of the data messages?\n const dataLsns = messages\n .filter(isChangeMessage)\n .map(({ headers }) =>\n typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)\n )\n if (dataLsns.length > 0) {\n const maxDataLsn = bigIntMax(dataLsns)\n const lastMaxDataLsn = this.#lastDataLsns[key]\n if (maxDataLsn > lastMaxDataLsn) {\n this.#lastDataLsns[key] = maxDataLsn\n }\n // There is new data, so we need to schedule a check for updates on\n // other shapes\n this.#scheduleCheckForUpdates()\n }\n\n // Publish the messages to the multi-shape stream subscribers\n const multiShapeMessages = messages.map(\n (message) =>\n ({\n ...message,\n shape: key,\n }) as MultiShapeMessages<TShapeRows>\n )\n await this._publish(multiShapeMessages)\n },\n (error) => this.#onError(error)\n )\n }\n this.#started = true\n }\n\n #scheduleCheckForUpdates() {\n this.#checkForUpdatesTimeout ??= setTimeout(() => {\n this.#checkForUpdates()\n this.#checkForUpdatesTimeout = undefined\n }, this.checkForUpdatesAfterMs)\n }\n\n async #checkForUpdates() {\n const maxDataLsn = bigIntMax(Object.values(this.#lastDataLsns))\n const refreshPromises = this.#shapeEntries()\n .filter(([key]) => {\n // We only need to refresh shapes that have not seen an up-to-date message\n // lower than the max lsn of the data messages we have received.\n const lastUpToDateLsn = this.#lastUpToDateLsns[key]\n return lastUpToDateLsn < maxDataLsn\n })\n .map(([_, shape]) => {\n return shape.forceDisconnectAndRefresh()\n })\n await Promise.all(refreshPromises)\n }\n\n #onError(error: Error) {\n // TODO: we probably want to disconnect all shapes here on the first error\n this.#subscribers.forEach(([_, errorFn]) => {\n errorFn?.(error)\n })\n }\n\n protected async _publish(\n messages: MultiShapeMessages<TShapeRows>[]\n ): Promise<void> {\n await Promise.all(\n Array.from(this.#subscribers.values()).map(async ([callback, __]) => {\n try {\n await callback(messages)\n } catch (err) {\n queueMicrotask(() => {\n throw err\n })\n }\n })\n )\n }\n\n /**\n * Returns an array of the shape entries.\n * Ensures that the shape entries are typed, as `Object.entries`\n * will not type the entries correctly.\n */\n #shapeEntries() {\n return Object.entries(this.#shapes) as [\n keyof TShapeRows & string,\n ShapeStream<TShapeRows[string]>,\n ][]\n }\n\n /**\n * The ShapeStreams that are being subscribed to.\n */\n get shapes() {\n return this.#shapes\n }\n\n subscribe(\n callback: (\n messages: MultiShapeMessages<TShapeRows>[]\n ) => MaybePromise<void>,\n onError?: (error: FetchError | Error) => void\n ) {\n const subscriptionId = Math.random()\n\n this.#subscribers.set(subscriptionId, [callback, onError])\n if (!this.#started) this.#start()\n\n return () => {\n this.#subscribers.delete(subscriptionId)\n }\n }\n\n unsubscribeAll(): void {\n this.#subscribers.clear()\n }\n\n /** Unix time at which we last synced. Undefined when `isLoading` is true. */\n lastSyncedAt(): number | undefined {\n // Min of all the lastSyncedAt values\n const shapeEntries = this.#shapeEntries()\n if (shapeEntries.length === 0) return\n return shapeEntries.reduce((minLastSyncedAt, [_, shape]) => {\n return Math.min(minLastSyncedAt, shape.lastSyncedAt() ?? Infinity)\n }, Infinity)\n }\n\n /** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */\n lastSynced(): number {\n const lastSyncedAt = this.lastSyncedAt()\n if (lastSyncedAt === undefined) return Infinity\n return Date.now() - lastSyncedAt\n }\n\n /** Indicates if we are connected to the Electric sync service. */\n isConnected(): boolean {\n return this.#shapeEntries().every(([_, shape]) => shape.isConnected())\n }\n\n /** True during initial fetch. False afterwise. */\n isLoading(): boolean {\n return this.#shapeEntries().some(([_, shape]) => shape.isLoading())\n }\n\n get isUpToDate() {\n return this.#shapeEntries().every(([_, shape]) => shape.isUpToDate)\n }\n}\n\n/**\n * A transactional multi-shape stream is a multi-shape stream that emits the\n * messages in transactional batches, ensuring that all shapes will receive\n * at least an `up-to-date` message from Electric within the `checkForUpdatesAfterMs`\n * interval.\n * It uses the `lsn` metadata to infer transaction boundaries, and the `op_position`\n * metadata to sort the messages within a transaction.\n *\n * @constructor\n * @param {MultiShapeStreamOptions} options - configure the multi-shape stream\n * @example\n * ```ts\n * const transactionalMultiShapeStream = new TransactionalMultiShapeStream({\n * shapes: {\n * shape1: {\n * url: 'http://localhost:3000/v1/shape1',\n * },\n * shape2: {\n * url: 'http://localhost:3000/v1/shape2',\n * },\n * },\n * })\n *\n * transactionalMultiShapeStream.subscribe((msgs) => {\n * console.log(msgs)\n * })\n *\n * // or with ShapeStream instances\n * const transactionalMultiShapeStream = new TransactionalMultiShapeStream({\n * shapes: {\n * shape1: new ShapeStream({ url: 'http://localhost:3000/v1/shape1' }),\n * shape2: new ShapeStream({ url: 'http://localhost:3000/v1/shape2' }),\n * },\n * })\n * ```\n */\n\nexport class TransactionalMultiShapeStream<\n TShapeRows extends {\n [K: string]: Row<unknown>\n },\n> extends MultiShapeStream<TShapeRows> {\n #changeMessages = new Map<bigint, MultiShapeMessage<Row<unknown>, string>[]>()\n #completeLsns: {\n [K in keyof TShapeRows]: bigint\n }\n\n constructor(options: MultiShapeStreamOptions<TShapeRows>) {\n super(options)\n this.#completeLsns = Object.fromEntries(\n Object.entries(options.shapes).map(([key]) => [key, BigInt(-1)])\n ) as { [K in keyof TShapeRows]: bigint }\n }\n\n #getLowestCompleteLsn() {\n return bigIntMin(Object.values(this.#completeLsns))\n }\n\n protected async _publish(\n messages: MultiShapeMessages<TShapeRows>[]\n ): Promise<void> {\n this.#accumulate(messages)\n const lowestCompleteLsn = this.#getLowestCompleteLsn()\n const lsnsToPublish = [...this.#changeMessages.keys()].filter(\n (lsn) => lsn <= lowestCompleteLsn\n )\n const messagesToPublish = lsnsToPublish\n .sort((a, b) => bigIntCompare(a, b))\n .map((lsn) =>\n this.#changeMessages.get(lsn)?.sort((a, b) => {\n const { headers: aHeaders } = a\n const { headers: bHeaders } = b\n if (\n typeof aHeaders.op_position !== `number` ||\n typeof bHeaders.op_position !== `number`\n ) {\n return 0 // op_position is not present on the snapshot message\n }\n return aHeaders.op_position - bHeaders.op_position\n })\n )\n .filter((messages) => messages !== undefined)\n .flat() as MultiShapeMessages<TShapeRows>[]\n lsnsToPublish.forEach((lsn) => {\n this.#changeMessages.delete(lsn)\n })\n if (messagesToPublish.length > 0) {\n await super._publish(messagesToPublish)\n }\n }\n\n #accumulate(messages: MultiShapeMessages<TShapeRows>[]) {\n const isUpToDate = this.isUpToDate\n messages.forEach((message) => {\n const { shape, headers } = message\n if (isChangeMessage(message)) {\n // The snapshot message does not have an lsn, so we use 0\n const lsn =\n typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)\n if (!this.#changeMessages.has(lsn)) {\n this.#changeMessages.set(lsn, [])\n }\n this.#changeMessages.get(lsn)?.push(message)\n if (\n isUpToDate && // All shapes must be up to date\n typeof headers.last === `boolean` &&\n headers.last === true\n ) {\n this.#completeLsns[shape] = bigIntMax([\n this.#completeLsns[shape],\n lsn,\n ])\n }\n } else if (isControlMessage(message)) {\n if (headers.control === `up-to-date`) {\n if (typeof headers.global_last_seen_lsn !== `string`) {\n throw new Error(`global_last_seen_lsn is not a number`)\n }\n this.#completeLsns[shape] = bigIntMax([\n this.#completeLsns[shape],\n BigInt(headers.global_last_seen_lsn),\n ])\n }\n }\n })\n }\n}\n"],"mappings":"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","names":["message","isChangeMessage","isChangeMessage","messages"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-sql/experimental",
|
|
3
3
|
"description": "Experimental TypeScript features for ElectricSQL.",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "1.0.1",
|
|
5
5
|
"author": "ElectricSQL team and contributors.",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/electric-sql/electric/issues"
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"typescript": "^5.5.2",
|
|
24
24
|
"uuid": "^10.0.0",
|
|
25
25
|
"vitest": "^2.0.2",
|
|
26
|
-
"@electric-sql/client": "1.0.
|
|
26
|
+
"@electric-sql/client": "1.0.1"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"@electric-sql/client": "1.0.
|
|
29
|
+
"@electric-sql/client": "1.0.1"
|
|
30
30
|
},
|
|
31
31
|
"peerDependenciesMeta": {
|
|
32
32
|
"@electric-sql/client": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"src"
|
|
52
52
|
],
|
|
53
53
|
"homepage": "https://electric-sql.com",
|
|
54
|
-
"license": "Apache-2",
|
|
54
|
+
"license": "Apache-2.0",
|
|
55
55
|
"main": "dist/cjs/index.cjs",
|
|
56
56
|
"module": "dist/index.legacy-esm.js",
|
|
57
57
|
"optionalDependencies": {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function bigIntMax(nums: Array<bigint | number>): bigint {
|
|
2
|
+
return BigInt(nums.reduce((m, e) => (e > m ? e : m)))
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function bigIntMin(nums: Array<bigint | number>): bigint {
|
|
6
|
+
return BigInt(nums.reduce((m, e) => (e < m ? e : m)))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function bigIntCompare(a: bigint, b: bigint): 1 | -1 | 0 {
|
|
10
|
+
return a > b ? 1 : a < b ? -1 : 0
|
|
11
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bigIntCompare, bigIntMax, bigIntMin } from './bigint-utils'
|
|
1
2
|
import {
|
|
2
3
|
ShapeStream,
|
|
3
4
|
isChangeMessage,
|
|
@@ -125,8 +126,8 @@ export class MultiShapeStream<
|
|
|
125
126
|
// We keep track of the last lsn of data and up-to-date messages for each shape
|
|
126
127
|
// so that we can skip checkForUpdates if the lsn of the up-to-date message is
|
|
127
128
|
// greater than the last lsn of data.
|
|
128
|
-
#lastDataLsns: { [K in keyof TShapeRows]:
|
|
129
|
-
#lastUpToDateLsns: { [K in keyof TShapeRows]:
|
|
129
|
+
#lastDataLsns: { [K in keyof TShapeRows]: bigint }
|
|
130
|
+
#lastUpToDateLsns: { [K in keyof TShapeRows]: bigint }
|
|
130
131
|
|
|
131
132
|
readonly #subscribers = new Map<
|
|
132
133
|
number,
|
|
@@ -155,11 +156,11 @@ export class MultiShapeStream<
|
|
|
155
156
|
])
|
|
156
157
|
) as { [K in keyof TShapeRows]: ShapeStream<TShapeRows[K]> }
|
|
157
158
|
this.#lastDataLsns = Object.fromEntries(
|
|
158
|
-
Object.entries(shapes).map(([key]) => [key, -
|
|
159
|
-
) as { [K in keyof TShapeRows]:
|
|
159
|
+
Object.entries(shapes).map(([key]) => [key, BigInt(-1)])
|
|
160
|
+
) as { [K in keyof TShapeRows]: bigint }
|
|
160
161
|
this.#lastUpToDateLsns = Object.fromEntries(
|
|
161
|
-
Object.entries(shapes).map(([key]) => [key, -
|
|
162
|
-
) as { [K in keyof TShapeRows]:
|
|
162
|
+
Object.entries(shapes).map(([key]) => [key, BigInt(-1)])
|
|
163
|
+
) as { [K in keyof TShapeRows]: bigint }
|
|
163
164
|
if (start) this.#start()
|
|
164
165
|
}
|
|
165
166
|
|
|
@@ -176,9 +177,13 @@ export class MultiShapeStream<
|
|
|
176
177
|
// Whats the max lsn of the up-to-date messages?
|
|
177
178
|
const upToDateLsns = messages
|
|
178
179
|
.filter(isControlMessage)
|
|
179
|
-
.map(({ headers }) =>
|
|
180
|
+
.map(({ headers }) =>
|
|
181
|
+
typeof headers.global_last_seen_lsn === `string`
|
|
182
|
+
? BigInt(headers.global_last_seen_lsn)
|
|
183
|
+
: BigInt(0)
|
|
184
|
+
)
|
|
180
185
|
if (upToDateLsns.length > 0) {
|
|
181
|
-
const maxUpToDateLsn =
|
|
186
|
+
const maxUpToDateLsn = bigIntMax(upToDateLsns)
|
|
182
187
|
const lastMaxUpToDateLsn = this.#lastUpToDateLsns[key]
|
|
183
188
|
if (maxUpToDateLsn > lastMaxUpToDateLsn) {
|
|
184
189
|
this.#lastUpToDateLsns[key] = maxUpToDateLsn
|
|
@@ -188,9 +193,11 @@ export class MultiShapeStream<
|
|
|
188
193
|
// Whats the max lsn of the data messages?
|
|
189
194
|
const dataLsns = messages
|
|
190
195
|
.filter(isChangeMessage)
|
|
191
|
-
.map(({ headers }) =>
|
|
196
|
+
.map(({ headers }) =>
|
|
197
|
+
typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)
|
|
198
|
+
)
|
|
192
199
|
if (dataLsns.length > 0) {
|
|
193
|
-
const maxDataLsn =
|
|
200
|
+
const maxDataLsn = bigIntMax(dataLsns)
|
|
194
201
|
const lastMaxDataLsn = this.#lastDataLsns[key]
|
|
195
202
|
if (maxDataLsn > lastMaxDataLsn) {
|
|
196
203
|
this.#lastDataLsns[key] = maxDataLsn
|
|
@@ -224,7 +231,7 @@ export class MultiShapeStream<
|
|
|
224
231
|
}
|
|
225
232
|
|
|
226
233
|
async #checkForUpdates() {
|
|
227
|
-
const maxDataLsn =
|
|
234
|
+
const maxDataLsn = bigIntMax(Object.values(this.#lastDataLsns))
|
|
228
235
|
const refreshPromises = this.#shapeEntries()
|
|
229
236
|
.filter(([key]) => {
|
|
230
237
|
// We only need to refresh shapes that have not seen an up-to-date message
|
|
@@ -303,11 +310,11 @@ export class MultiShapeStream<
|
|
|
303
310
|
/** Unix time at which we last synced. Undefined when `isLoading` is true. */
|
|
304
311
|
lastSyncedAt(): number | undefined {
|
|
305
312
|
// Min of all the lastSyncedAt values
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
)
|
|
310
|
-
)
|
|
313
|
+
const shapeEntries = this.#shapeEntries()
|
|
314
|
+
if (shapeEntries.length === 0) return
|
|
315
|
+
return shapeEntries.reduce((minLastSyncedAt, [_, shape]) => {
|
|
316
|
+
return Math.min(minLastSyncedAt, shape.lastSyncedAt() ?? Infinity)
|
|
317
|
+
}, Infinity)
|
|
311
318
|
}
|
|
312
319
|
|
|
313
320
|
/** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */
|
|
@@ -374,20 +381,20 @@ export class TransactionalMultiShapeStream<
|
|
|
374
381
|
[K: string]: Row<unknown>
|
|
375
382
|
},
|
|
376
383
|
> extends MultiShapeStream<TShapeRows> {
|
|
377
|
-
#changeMessages = new Map<
|
|
384
|
+
#changeMessages = new Map<bigint, MultiShapeMessage<Row<unknown>, string>[]>()
|
|
378
385
|
#completeLsns: {
|
|
379
|
-
[K in keyof TShapeRows]:
|
|
386
|
+
[K in keyof TShapeRows]: bigint
|
|
380
387
|
}
|
|
381
388
|
|
|
382
389
|
constructor(options: MultiShapeStreamOptions<TShapeRows>) {
|
|
383
390
|
super(options)
|
|
384
391
|
this.#completeLsns = Object.fromEntries(
|
|
385
|
-
Object.entries(options.shapes).map(([key]) => [key, -
|
|
386
|
-
) as { [K in keyof TShapeRows]:
|
|
392
|
+
Object.entries(options.shapes).map(([key]) => [key, BigInt(-1)])
|
|
393
|
+
) as { [K in keyof TShapeRows]: bigint }
|
|
387
394
|
}
|
|
388
395
|
|
|
389
396
|
#getLowestCompleteLsn() {
|
|
390
|
-
return
|
|
397
|
+
return bigIntMin(Object.values(this.#completeLsns))
|
|
391
398
|
}
|
|
392
399
|
|
|
393
400
|
protected async _publish(
|
|
@@ -399,7 +406,7 @@ export class TransactionalMultiShapeStream<
|
|
|
399
406
|
(lsn) => lsn <= lowestCompleteLsn
|
|
400
407
|
)
|
|
401
408
|
const messagesToPublish = lsnsToPublish
|
|
402
|
-
.sort((a, b) => a
|
|
409
|
+
.sort((a, b) => bigIntCompare(a, b))
|
|
403
410
|
.map((lsn) =>
|
|
404
411
|
this.#changeMessages.get(lsn)?.sort((a, b) => {
|
|
405
412
|
const { headers: aHeaders } = a
|
|
@@ -429,7 +436,8 @@ export class TransactionalMultiShapeStream<
|
|
|
429
436
|
const { shape, headers } = message
|
|
430
437
|
if (isChangeMessage(message)) {
|
|
431
438
|
// The snapshot message does not have an lsn, so we use 0
|
|
432
|
-
const lsn =
|
|
439
|
+
const lsn =
|
|
440
|
+
typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)
|
|
433
441
|
if (!this.#changeMessages.has(lsn)) {
|
|
434
442
|
this.#changeMessages.set(lsn, [])
|
|
435
443
|
}
|
|
@@ -439,17 +447,20 @@ export class TransactionalMultiShapeStream<
|
|
|
439
447
|
typeof headers.last === `boolean` &&
|
|
440
448
|
headers.last === true
|
|
441
449
|
) {
|
|
442
|
-
this.#completeLsns[shape] =
|
|
450
|
+
this.#completeLsns[shape] = bigIntMax([
|
|
451
|
+
this.#completeLsns[shape],
|
|
452
|
+
lsn,
|
|
453
|
+
])
|
|
443
454
|
}
|
|
444
455
|
} else if (isControlMessage(message)) {
|
|
445
456
|
if (headers.control === `up-to-date`) {
|
|
446
|
-
if (typeof headers.global_last_seen_lsn !== `
|
|
457
|
+
if (typeof headers.global_last_seen_lsn !== `string`) {
|
|
447
458
|
throw new Error(`global_last_seen_lsn is not a number`)
|
|
448
459
|
}
|
|
449
|
-
this.#completeLsns[shape] =
|
|
460
|
+
this.#completeLsns[shape] = bigIntMax([
|
|
450
461
|
this.#completeLsns[shape],
|
|
451
|
-
headers.global_last_seen_lsn
|
|
452
|
-
)
|
|
462
|
+
BigInt(headers.global_last_seen_lsn),
|
|
463
|
+
])
|
|
453
464
|
}
|
|
454
465
|
}
|
|
455
466
|
})
|