@electric-sql/experimental 0.1.2-beta.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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(...args) {
113
+ return BigInt(args.reduce((m, e) => e > m ? e : m));
114
+ }
115
+ function bigIntMin(...args) {
116
+ return BigInt(args.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, -Infinity])
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, -Infinity])
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
  }
@@ -223,23 +234,21 @@ start_fn = function() {
223
234
  }
224
235
  shape.subscribe(
225
236
  (messages) => __async(this, null, function* () {
226
- const upToDateLsns = messages.filter(import_client2.isControlMessage).map(({ headers }) => {
227
- var _a;
228
- return (_a = headers.global_last_seen_lsn) != null ? _a : 0;
229
- });
237
+ const upToDateLsns = messages.filter(import_client2.isControlMessage).map(
238
+ ({ headers }) => typeof headers.global_last_seen_lsn === `string` ? BigInt(headers.global_last_seen_lsn) : BigInt(0)
239
+ );
230
240
  if (upToDateLsns.length > 0) {
231
- const maxUpToDateLsn = Math.max(...upToDateLsns);
241
+ const maxUpToDateLsn = bigIntMax(...upToDateLsns);
232
242
  const lastMaxUpToDateLsn = __privateGet(this, _lastUpToDateLsns)[key];
233
243
  if (maxUpToDateLsn > lastMaxUpToDateLsn) {
234
244
  __privateGet(this, _lastUpToDateLsns)[key] = maxUpToDateLsn;
235
245
  }
236
246
  }
237
- const dataLsns = messages.filter(import_client2.isChangeMessage).map(({ headers }) => {
238
- var _a;
239
- return (_a = headers.lsn) != null ? _a : 0;
240
- });
247
+ const dataLsns = messages.filter(import_client2.isChangeMessage).map(
248
+ ({ headers }) => typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)
249
+ );
241
250
  if (dataLsns.length > 0) {
242
- const maxDataLsn = Math.max(...dataLsns);
251
+ const maxDataLsn = bigIntMax(...dataLsns);
243
252
  const lastMaxDataLsn = __privateGet(this, _lastDataLsns)[key];
244
253
  if (maxDataLsn > lastMaxDataLsn) {
245
254
  __privateGet(this, _lastDataLsns)[key] = maxDataLsn;
@@ -267,7 +276,7 @@ scheduleCheckForUpdates_fn = function() {
267
276
  };
268
277
  checkForUpdates_fn = function() {
269
278
  return __async(this, null, function* () {
270
- const maxDataLsn = Math.max(...Object.values(__privateGet(this, _lastDataLsns)));
279
+ const maxDataLsn = bigIntMax(...Object.values(__privateGet(this, _lastDataLsns)));
271
280
  const refreshPromises = __privateMethod(this, _MultiShapeStream_instances, shapeEntries_fn).call(this).filter(([key]) => {
272
281
  const lastUpToDateLsn = __privateGet(this, _lastUpToDateLsns)[key];
273
282
  return lastUpToDateLsn < maxDataLsn;
@@ -298,7 +307,7 @@ var _TransactionalMultiShapeStream = class _TransactionalMultiShapeStream extend
298
307
  __privateAdd(this, _changeMessages, /* @__PURE__ */ new Map());
299
308
  __privateAdd(this, _completeLsns);
300
309
  __privateSet(this, _completeLsns, Object.fromEntries(
301
- Object.entries(options.shapes).map(([key]) => [key, -Infinity])
310
+ Object.entries(options.shapes).map(([key]) => [key, BigInt(-1)])
302
311
  ));
303
312
  }
304
313
  _publish(messages) {
@@ -308,7 +317,7 @@ var _TransactionalMultiShapeStream = class _TransactionalMultiShapeStream extend
308
317
  const lsnsToPublish = [...__privateGet(this, _changeMessages).keys()].filter(
309
318
  (lsn) => lsn <= lowestCompleteLsn
310
319
  );
311
- const messagesToPublish = lsnsToPublish.sort((a, b) => a - b).map(
320
+ const messagesToPublish = lsnsToPublish.sort((a, b) => bigIntCompare(a, b)).map(
312
321
  (lsn) => {
313
322
  var _a;
314
323
  return (_a = __privateGet(this, _changeMessages).get(lsn)) == null ? void 0 : _a.sort((a, b) => {
@@ -334,7 +343,7 @@ _changeMessages = new WeakMap();
334
343
  _completeLsns = new WeakMap();
335
344
  _TransactionalMultiShapeStream_instances = new WeakSet();
336
345
  getLowestCompleteLsn_fn = function() {
337
- return Math.min(...Object.values(__privateGet(this, _completeLsns)));
346
+ return bigIntMin(...Object.values(__privateGet(this, _completeLsns)));
338
347
  };
339
348
  accumulate_fn = function(messages) {
340
349
  const isUpToDate = this.isUpToDate;
@@ -342,23 +351,23 @@ accumulate_fn = function(messages) {
342
351
  var _a;
343
352
  const { shape, headers } = message;
344
353
  if ((0, import_client2.isChangeMessage)(message)) {
345
- const lsn = typeof headers.lsn === `number` ? headers.lsn : 0;
354
+ const lsn = typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0);
346
355
  if (!__privateGet(this, _changeMessages).has(lsn)) {
347
356
  __privateGet(this, _changeMessages).set(lsn, []);
348
357
  }
349
358
  (_a = __privateGet(this, _changeMessages).get(lsn)) == null ? void 0 : _a.push(message);
350
359
  if (isUpToDate && // All shapes must be up to date
351
360
  typeof headers.last === `boolean` && headers.last === true) {
352
- __privateGet(this, _completeLsns)[shape] = Math.max(__privateGet(this, _completeLsns)[shape], lsn);
361
+ __privateGet(this, _completeLsns)[shape] = bigIntMax(__privateGet(this, _completeLsns)[shape], lsn);
353
362
  }
354
363
  } else if ((0, import_client2.isControlMessage)(message)) {
355
364
  if (headers.control === `up-to-date`) {
356
- if (typeof headers.global_last_seen_lsn !== `number`) {
365
+ if (typeof headers.global_last_seen_lsn !== `string`) {
357
366
  throw new Error(`global_last_seen_lsn is not a number`);
358
367
  }
359
- __privateGet(this, _completeLsns)[shape] = Math.max(
368
+ __privateGet(this, _completeLsns)[shape] = bigIntMax(
360
369
  __privateGet(this, _completeLsns)[shape],
361
- headers.global_last_seen_lsn
370
+ BigInt(headers.global_last_seen_lsn)
362
371
  );
363
372
  }
364
373
  }
@@ -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(...args: Array<bigint | number>): bigint {\n return BigInt(args.reduce((m, e) => (e > m ? e : m)))\n}\n\nexport function bigIntMin(...args: Array<bigint | number>): bigint {\n return BigInt(args.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 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<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(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 !== `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"]}
@@ -1,2 +1,2 @@
1
- var z=Object.defineProperty,Q=Object.defineProperties;var W=Object.getOwnPropertyDescriptors;var A=Object.getOwnPropertySymbols,X=Object.getPrototypeOf,Y=Object.prototype.hasOwnProperty,Z=Object.prototype.propertyIsEnumerable,ee=Reflect.get;var P=t=>{throw TypeError(t)};var D=(t,e,s)=>e in t?z(t,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):t[e]=s,K=(t,e)=>{for(var s in e||(e={}))Y.call(e,s)&&D(t,s,e[s]);if(A)for(var s of A(e))Z.call(e,s)&&D(t,s,e[s]);return t},E=(t,e)=>Q(t,W(e));var v=(t,e,s)=>e.has(t)||P("Cannot "+s);var n=(t,e,s)=>(v(t,e,"read from private field"),s?s.call(t):e.get(t)),m=(t,e,s)=>e.has(t)?P("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,s),d=(t,e,s,a)=>(v(t,e,"write to private field"),a?a.call(t,s):e.set(t,s),s),c=(t,e,s)=>(v(t,e,"access private method"),s);var j=(t,e,s)=>ee(X(t),s,e);var y=(t,e,s)=>new Promise((a,h)=>{var p=r=>{try{i(s.next(r))}catch(l){h(l)}},o=r=>{try{i(s.throw(r))}catch(l){h(l)}},i=r=>r.done?a(r.value):Promise.resolve(r.value).then(p,o);i((s=s.apply(t,e)).next())});import{isChangeMessage as se}from"@electric-sql/client";function ne(t,e,s,a=6e4){return new Promise((h,p)=>{let o=t.subscribe(l=>{let _=l.filter(w=>se(w)).find(w=>{let J=w.headers.operation;return e.includes(J)&&s(w)});if(_)return r(_)}),i=setTimeout(()=>{let l=`matchStream timed out after ${a}ms`;console.error(l),p(l)},a);function r(l){return clearTimeout(i),o(),h(l)}})}function oe(t,e){return s=>s.value[t]===e}import{ShapeStream as I,isChangeMessage as N,isControlMessage as q}from"@electric-sql/client";var R,k,C,b,T,g,u,U,G,H,V,M,O=class{constructor(e){m(this,u);m(this,R);m(this,k,!1);m(this,C);m(this,b);m(this,T);m(this,g,new Map);let{start:s=!0,checkForUpdatesAfterMs:a=100,shapes:h}=e;this.checkForUpdatesAfterMs=a,d(this,R,Object.fromEntries(Object.entries(h).map(([p,o])=>[p,o instanceof I?o:new I(E(K({},o),{start:!1}))]))),d(this,b,Object.fromEntries(Object.entries(h).map(([p])=>[p,-1/0]))),d(this,T,Object.fromEntries(Object.entries(h).map(([p])=>[p,-1/0]))),s&&c(this,u,U).call(this)}_publish(e){return y(this,null,function*(){yield Promise.all(Array.from(n(this,g).values()).map(h=>y(this,[h],function*([s,a]){try{yield s(e)}catch(p){queueMicrotask(()=>{throw p})}})))})}get shapes(){return n(this,R)}subscribe(e,s){let a=Math.random();return n(this,g).set(a,[e,s]),n(this,k)||c(this,u,U).call(this),()=>{n(this,g).delete(a)}}unsubscribeAll(){n(this,g).clear()}lastSyncedAt(){return Math.min(...c(this,u,M).call(this).map(([e,s])=>{var a;return(a=s.lastSyncedAt())!=null?a:1/0}))}lastSynced(){let e=this.lastSyncedAt();return e===void 0?1/0:Date.now()-e}isConnected(){return c(this,u,M).call(this).every(([e,s])=>s.isConnected())}isLoading(){return c(this,u,M).call(this).some(([e,s])=>s.isLoading())}get isUpToDate(){return c(this,u,M).call(this).every(([e,s])=>s.isUpToDate)}};R=new WeakMap,k=new WeakMap,C=new WeakMap,b=new WeakMap,T=new WeakMap,g=new WeakMap,u=new WeakSet,U=function(){if(n(this,k))throw new Error("Cannot start multi-shape stream twice");for(let[e,s]of c(this,u,M).call(this)){if(s.hasStarted())throw new Error(`Shape ${e} already started`);s.subscribe(a=>y(this,null,function*(){let h=a.filter(q).map(({headers:i})=>{var r;return(r=i.global_last_seen_lsn)!=null?r:0});if(h.length>0){let i=Math.max(...h),r=n(this,T)[e];i>r&&(n(this,T)[e]=i)}let p=a.filter(N).map(({headers:i})=>{var r;return(r=i.lsn)!=null?r:0});if(p.length>0){let i=Math.max(...p),r=n(this,b)[e];i>r&&(n(this,b)[e]=i),c(this,u,G).call(this)}let o=a.map(i=>E(K({},i),{shape:e}));yield this._publish(o)}),a=>c(this,u,V).call(this,a))}d(this,k,!0)},G=function(){var e;(e=n(this,C))!=null||d(this,C,setTimeout(()=>{c(this,u,H).call(this),d(this,C,void 0)},this.checkForUpdatesAfterMs))},H=function(){return y(this,null,function*(){let e=Math.max(...Object.values(n(this,b))),s=c(this,u,M).call(this).filter(([a])=>n(this,T)[a]<e).map(([a,h])=>h.forceDisconnectAndRefresh());yield Promise.all(s)})},V=function(e){n(this,g).forEach(([s,a])=>{a==null||a(e)})},M=function(){return Object.entries(n(this,R))};var f,S,x,$,B,L=class L extends O{constructor(s){super(s);m(this,x);m(this,f,new Map);m(this,S);d(this,S,Object.fromEntries(Object.entries(s.shapes).map(([a])=>[a,-1/0])))}_publish(s){return y(this,null,function*(){c(this,x,B).call(this,s);let a=c(this,x,$).call(this),h=[...n(this,f).keys()].filter(o=>o<=a),p=h.sort((o,i)=>o-i).map(o=>{var i;return(i=n(this,f).get(o))==null?void 0:i.sort((r,l)=>{let{headers:_}=r,{headers:w}=l;return typeof _.op_position!="number"||typeof w.op_position!="number"?0:_.op_position-w.op_position})}).filter(o=>o!==void 0).flat();h.forEach(o=>{n(this,f).delete(o)}),p.length>0&&(yield j(L.prototype,this,"_publish").call(this,p))})}};f=new WeakMap,S=new WeakMap,x=new WeakSet,$=function(){return Math.min(...Object.values(n(this,S)))},B=function(s){let a=this.isUpToDate;s.forEach(h=>{var i;let{shape:p,headers:o}=h;if(N(h)){let r=typeof o.lsn=="number"?o.lsn:0;n(this,f).has(r)||n(this,f).set(r,[]),(i=n(this,f).get(r))==null||i.push(h),a&&typeof o.last=="boolean"&&o.last===!0&&(n(this,S)[p]=Math.max(n(this,S)[p],r))}else if(q(h)&&o.control==="up-to-date"){if(typeof o.global_last_seen_lsn!="number")throw new Error("global_last_seen_lsn is not a number");n(this,S)[p]=Math.max(n(this,S)[p],o.global_last_seen_lsn)}})};var F=L;export{O as MultiShapeStream,F as TransactionalMultiShapeStream,oe as matchBy,ne as matchStream};
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,r)=>{var h=p=>{try{i(s.next(p))}catch(l){r(l)}},o=p=>{try{i(s.throw(p))}catch(l){r(l)}},i=p=>p.done?n(p.value):Promise.resolve(p.value).then(h,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((r,h)=>{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),h(l)},n);function p(l){return clearTimeout(i),o(),r(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:r}=e;this.checkForUpdatesAfterMs=n,S(this,k,Object.fromEntries(Object.entries(r).map(([h,o])=>[h,o instanceof N?o:new N(E(K({},o),{start:!1}))]))),S(this,M,Object.fromEntries(Object.entries(r).map(([h])=>[h,BigInt(-1)]))),S(this,T,Object.fromEntries(Object.entries(r).map(([h])=>[h,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(r=>y(this,[r],function*([s,n]){try{yield s(e)}catch(h){queueMicrotask(()=>{throw h})}})))})}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(){return Math.min(...u(this,c,w).call(this).map(([e,s])=>{var n;return(n=s.lastSyncedAt())!=null?n: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 r=n.filter(H).map(({headers:i})=>typeof i.global_last_seen_lsn=="string"?BigInt(i.global_last_seen_lsn):BigInt(0));if(r.length>0){let i=R(...r),p=a(this,T)[e];i>p&&(a(this,T)[e]=i)}let h=n.filter(G).map(({headers:i})=>typeof i.lsn=="string"?BigInt(i.lsn):BigInt(0));if(h.length>0){let i=R(...h),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,r])=>r.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),r=[...a(this,f).keys()].filter(o=>o<=n),h=r.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();r.forEach(o=>{a(this,f).delete(o)}),h.length>0&&(yield P(U.prototype,this,"_publish").call(this,h))})}};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(r=>{var i;let{shape:h,headers:o}=r;if(G(r)){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(r),n&&typeof o.last=="boolean"&&o.last===!0&&(a(this,m)[h]=R(a(this,m)[h],p))}else if(H(r)&&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)[h]=R(a(this,m)[h],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(...args: Array<bigint | number>): bigint {\n return BigInt(args.reduce((m, e) => (e > m ? e : m)))\n}\n\nexport function bigIntMin(...args: Array<bigint | number>): bigint {\n return BigInt(args.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 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<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(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 !== `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","args","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","_","_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"]}
@@ -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(...args) {
66
+ return BigInt(args.reduce((m, e) => e > m ? e : m));
67
+ }
68
+ function bigIntMin(...args) {
69
+ return BigInt(args.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, -Infinity])
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, -Infinity])
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
  }
@@ -178,23 +189,21 @@ start_fn = function() {
178
189
  }
179
190
  shape.subscribe(
180
191
  async (messages) => {
181
- const upToDateLsns = messages.filter(isControlMessage).map(({ headers }) => {
182
- var _a;
183
- return (_a = headers.global_last_seen_lsn) != null ? _a : 0;
184
- });
192
+ const upToDateLsns = messages.filter(isControlMessage).map(
193
+ ({ headers }) => typeof headers.global_last_seen_lsn === `string` ? BigInt(headers.global_last_seen_lsn) : BigInt(0)
194
+ );
185
195
  if (upToDateLsns.length > 0) {
186
- const maxUpToDateLsn = Math.max(...upToDateLsns);
196
+ const maxUpToDateLsn = bigIntMax(...upToDateLsns);
187
197
  const lastMaxUpToDateLsn = __privateGet(this, _lastUpToDateLsns)[key];
188
198
  if (maxUpToDateLsn > lastMaxUpToDateLsn) {
189
199
  __privateGet(this, _lastUpToDateLsns)[key] = maxUpToDateLsn;
190
200
  }
191
201
  }
192
- const dataLsns = messages.filter(isChangeMessage2).map(({ headers }) => {
193
- var _a;
194
- return (_a = headers.lsn) != null ? _a : 0;
195
- });
202
+ const dataLsns = messages.filter(isChangeMessage2).map(
203
+ ({ headers }) => typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)
204
+ );
196
205
  if (dataLsns.length > 0) {
197
- const maxDataLsn = Math.max(...dataLsns);
206
+ const maxDataLsn = bigIntMax(...dataLsns);
198
207
  const lastMaxDataLsn = __privateGet(this, _lastDataLsns)[key];
199
208
  if (maxDataLsn > lastMaxDataLsn) {
200
209
  __privateGet(this, _lastDataLsns)[key] = maxDataLsn;
@@ -221,7 +230,7 @@ scheduleCheckForUpdates_fn = function() {
221
230
  }, this.checkForUpdatesAfterMs));
222
231
  };
223
232
  checkForUpdates_fn = async function() {
224
- const maxDataLsn = Math.max(...Object.values(__privateGet(this, _lastDataLsns)));
233
+ const maxDataLsn = bigIntMax(...Object.values(__privateGet(this, _lastDataLsns)));
225
234
  const refreshPromises = __privateMethod(this, _MultiShapeStream_instances, shapeEntries_fn).call(this).filter(([key]) => {
226
235
  const lastUpToDateLsn = __privateGet(this, _lastUpToDateLsns)[key];
227
236
  return lastUpToDateLsn < maxDataLsn;
@@ -251,7 +260,7 @@ var TransactionalMultiShapeStream = class extends MultiShapeStream {
251
260
  __privateAdd(this, _changeMessages, /* @__PURE__ */ new Map());
252
261
  __privateAdd(this, _completeLsns);
253
262
  __privateSet(this, _completeLsns, Object.fromEntries(
254
- Object.entries(options.shapes).map(([key]) => [key, -Infinity])
263
+ Object.entries(options.shapes).map(([key]) => [key, BigInt(-1)])
255
264
  ));
256
265
  }
257
266
  async _publish(messages) {
@@ -260,7 +269,7 @@ var TransactionalMultiShapeStream = class extends MultiShapeStream {
260
269
  const lsnsToPublish = [...__privateGet(this, _changeMessages).keys()].filter(
261
270
  (lsn) => lsn <= lowestCompleteLsn
262
271
  );
263
- const messagesToPublish = lsnsToPublish.sort((a, b) => a - b).map(
272
+ const messagesToPublish = lsnsToPublish.sort((a, b) => bigIntCompare(a, b)).map(
264
273
  (lsn) => {
265
274
  var _a;
266
275
  return (_a = __privateGet(this, _changeMessages).get(lsn)) == null ? void 0 : _a.sort((a, b) => {
@@ -285,7 +294,7 @@ _changeMessages = new WeakMap();
285
294
  _completeLsns = new WeakMap();
286
295
  _TransactionalMultiShapeStream_instances = new WeakSet();
287
296
  getLowestCompleteLsn_fn = function() {
288
- return Math.min(...Object.values(__privateGet(this, _completeLsns)));
297
+ return bigIntMin(...Object.values(__privateGet(this, _completeLsns)));
289
298
  };
290
299
  accumulate_fn = function(messages) {
291
300
  const isUpToDate = this.isUpToDate;
@@ -293,23 +302,23 @@ accumulate_fn = function(messages) {
293
302
  var _a;
294
303
  const { shape, headers } = message;
295
304
  if (isChangeMessage2(message)) {
296
- const lsn = typeof headers.lsn === `number` ? headers.lsn : 0;
305
+ const lsn = typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0);
297
306
  if (!__privateGet(this, _changeMessages).has(lsn)) {
298
307
  __privateGet(this, _changeMessages).set(lsn, []);
299
308
  }
300
309
  (_a = __privateGet(this, _changeMessages).get(lsn)) == null ? void 0 : _a.push(message);
301
310
  if (isUpToDate && // All shapes must be up to date
302
311
  typeof headers.last === `boolean` && headers.last === true) {
303
- __privateGet(this, _completeLsns)[shape] = Math.max(__privateGet(this, _completeLsns)[shape], lsn);
312
+ __privateGet(this, _completeLsns)[shape] = bigIntMax(__privateGet(this, _completeLsns)[shape], lsn);
304
313
  }
305
314
  } else if (isControlMessage(message)) {
306
315
  if (headers.control === `up-to-date`) {
307
- if (typeof headers.global_last_seen_lsn !== `number`) {
316
+ if (typeof headers.global_last_seen_lsn !== `string`) {
308
317
  throw new Error(`global_last_seen_lsn is not a number`);
309
318
  }
310
- __privateGet(this, _completeLsns)[shape] = Math.max(
319
+ __privateGet(this, _completeLsns)[shape] = bigIntMax(
311
320
  __privateGet(this, _completeLsns)[shape],
312
- headers.global_last_seen_lsn
321
+ BigInt(headers.global_last_seen_lsn)
313
322
  );
314
323
  }
315
324
  }
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA,EACE;AAAA,OAQK;AAEA,SAAS,YACd,QACA,YACA,SACA,UAAU,KACiB;AAC3B,SAAO,IAAI,QAA0B,CAAC,SAAS,WAAW;AACxD,UAAM,cAA0B,OAAO;AAAA,MACrC,CAAC,aAA6B;AAC5B,cAAM,UAAU,SACb;AAAA,UAAO,CAAC,QACP,gBAAgB,GAA0B;AAAA,QAC5C,EACC,KAAK,CAACA,aAAY;AACjB,gBAAM,YAAuBA,SAAQ,QAAQ;AAE7C,iBAAO,WAAW,SAAS,SAAS,KAAK,QAAQA,QAAO;AAAA,QAC1D,CAAC;AAEH,YAAI,SAAS;AACX,iBAAO,OAAO,OAAO;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AAEA,UAAM,YAA4B,WAAW,MAAM;AACjD,YAAM,MAAc,+BAA+B,OAAO;AAE1D,cAAQ,MAAM,GAAG;AAEjB,aAAO,GAAG;AAAA,IACZ,GAAG,OAAO;AAEV,aAAS,OAAO,SAAiC;AAC/C,mBAAa,SAAS;AAEtB,kBAAY;AAEZ,aAAO,QAAQ,OAAO;AAAA,IACxB;AAAA,EACF,CAAC;AACH;AAEO,SAAS,QACd,QACA,OACwC;AACxC,SAAO,CAAC,YAA8B,QAAQ,MAAM,MAAM,MAAM;AAClE;;;AC3DA;AAAA,EACE;AAAA,EACA,mBAAAC;AAAA,EACA;AAAA,OACK;AAJP;AAgHO,IAAM,mBAAN,MAKP;AAAA,EAqBE,YAAY,SAA8C;AA1BrD;AAML;AACA,iCAAW;AAGX;AAKA;AAAA;AAAA;AAAA;AACA;AAEA,uBAAS,cAAe,oBAAI,IAM1B;AAGA,UAAM;AAAA,MACJ,QAAQ;AAAA;AAAA,MACR,yBAAyB;AAAA;AAAA,MACzB;AAAA,IACF,IAAI;AACJ,SAAK,yBAAyB;AAC9B,uBAAK,SAAU,OAAO;AAAA,MACpB,OAAO,QAAQ,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM;AAAA,QAC3C;AAAA,QACA,iBAAiB,cACb,QACA,IAAI,YAAoC,iCACnC,QADmC;AAAA,UAEtC,OAAO;AAAA,QACT,EAAC;AAAA,MACP,CAAC;AAAA,IACH;AACA,uBAAK,eAAgB,OAAO;AAAA,MAC1B,OAAO,QAAQ,MAAM,EAAE,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,SAAS,CAAC;AAAA,IACxD;AACA,uBAAK,mBAAoB,OAAO;AAAA,MAC9B,OAAO,QAAQ,MAAM,EAAE,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,SAAS,CAAC;AAAA,IACxD;AACA,QAAI,MAAO,uBAAK,uCAAL;AAAA,EACb;AAAA,EAoFA,MAAgB,SACd,UACe;AACf,UAAM,QAAQ;AAAA,MACZ,MAAM,KAAK,mBAAK,cAAa,OAAO,CAAC,EAAE,IAAI,OAAO,CAAC,UAAU,EAAE,MAAM;AACnE,YAAI;AACF,gBAAM,SAAS,QAAQ;AAAA,QACzB,SAAS,KAAK;AACZ,yBAAe,MAAM;AACnB,kBAAM;AAAA,UACR,CAAC;AAAA,QACH;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAiBA,IAAI,SAAS;AACX,WAAO,mBAAK;AAAA,EACd;AAAA,EAEA,UACE,UAGA,SACA;AACA,UAAM,iBAAiB,KAAK,OAAO;AAEnC,uBAAK,cAAa,IAAI,gBAAgB,CAAC,UAAU,OAAO,CAAC;AACzD,QAAI,CAAC,mBAAK,UAAU,uBAAK,uCAAL;AAEpB,WAAO,MAAM;AACX,yBAAK,cAAa,OAAO,cAAc;AAAA,IACzC;AAAA,EACF;AAAA,EAEA,iBAAuB;AACrB,uBAAK,cAAa,MAAM;AAAA,EAC1B;AAAA;AAAA,EAGA,eAAmC;AAEjC,WAAO,KAAK;AAAA,MACV,GAAG,sBAAK,8CAAL,WAAqB;AAAA,QACtB,CAAC,CAAC,GAAG,KAAK,MAAG;AAnTrB;AAmTwB,6BAAM,aAAa,MAAnB,YAAwB;AAAA;AAAA,MAC1C;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,aAAqB;AACnB,UAAM,eAAe,KAAK,aAAa;AACvC,QAAI,iBAAiB,OAAW,QAAO;AACvC,WAAO,KAAK,IAAI,IAAI;AAAA,EACtB;AAAA;AAAA,EAGA,cAAuB;AACrB,WAAO,sBAAK,8CAAL,WAAqB,MAAM,CAAC,CAAC,GAAG,KAAK,MAAM,MAAM,YAAY,CAAC;AAAA,EACvE;AAAA;AAAA,EAGA,YAAqB;AACnB,WAAO,sBAAK,8CAAL,WAAqB,KAAK,CAAC,CAAC,GAAG,KAAK,MAAM,MAAM,UAAU,CAAC;AAAA,EACpE;AAAA,EAEA,IAAI,aAAa;AACf,WAAO,sBAAK,8CAAL,WAAqB,MAAM,CAAC,CAAC,GAAG,KAAK,MAAM,MAAM,UAAU;AAAA,EACpE;AACF;AAtNE;AACA;AAGA;AAKA;AACA;AAES;AAlBJ;AAqDL,WAAM,WAAG;AACP,MAAI,mBAAK,UAAU,OAAM,IAAI,MAAM,uCAAuC;AAC1E,aAAW,CAAC,KAAK,KAAK,KAAK,sBAAK,8CAAL,YAAsB;AAC/C,QAAI,MAAM,WAAW,GAAG;AAGtB,YAAM,IAAI,MAAM,SAAS,GAAG,kBAAkB;AAAA,IAChD;AACA,UAAM;AAAA,MACJ,OAAO,aAAa;AAElB,cAAM,eAAe,SAClB,OAAO,gBAAgB,EACvB,IAAI,CAAC,EAAE,QAAQ,MAAG;AAlL/B;AAkLmC,+BAAQ,yBAAR,YAA2C;AAAA,SAAC;AACrE,YAAI,aAAa,SAAS,GAAG;AAC3B,gBAAM,iBAAiB,KAAK,IAAI,GAAG,YAAY;AAC/C,gBAAM,qBAAqB,mBAAK,mBAAkB,GAAG;AACrD,cAAI,iBAAiB,oBAAoB;AACvC,+BAAK,mBAAkB,GAAG,IAAI;AAAA,UAChC;AAAA,QACF;AAGA,cAAM,WAAW,SACd,OAAOC,gBAAe,EACtB,IAAI,CAAC,EAAE,QAAQ,MAAG;AA9L/B;AA8LmC,+BAAQ,QAAR,YAA0B;AAAA,SAAC;AACpD,YAAI,SAAS,SAAS,GAAG;AACvB,gBAAM,aAAa,KAAK,IAAI,GAAG,QAAQ;AACvC,gBAAM,iBAAiB,mBAAK,eAAc,GAAG;AAC7C,cAAI,aAAa,gBAAgB;AAC/B,+BAAK,eAAc,GAAG,IAAI;AAAA,UAC5B;AAGA,gCAAK,yDAAL;AAAA,QACF;AAGA,cAAM,qBAAqB,SAAS;AAAA,UAClC,CAAC,YACE,iCACI,UADJ;AAAA,YAEC,OAAO;AAAA,UACT;AAAA,QACJ;AACA,cAAM,KAAK,SAAS,kBAAkB;AAAA,MACxC;AAAA,MACA,CAAC,UAAU,sBAAK,yCAAL,WAAc;AAAA,IAC3B;AAAA,EACF;AACA,qBAAK,UAAW;AAClB;AAEA,6BAAwB,WAAG;AA1N7B;AA2NI,2BAAK,6BAAL,+BAAK,yBAA4B,WAAW,MAAM;AAChD,0BAAK,iDAAL;AACA,uBAAK,yBAA0B;AAAA,EACjC,GAAG,KAAK,sBAAsB;AAChC;AAEM,qBAAgB,iBAAG;AACvB,QAAM,aAAa,KAAK,IAAI,GAAG,OAAO,OAAO,mBAAK,cAAa,CAAC;AAChE,QAAM,kBAAkB,sBAAK,8CAAL,WACrB,OAAO,CAAC,CAAC,GAAG,MAAM;AAGjB,UAAM,kBAAkB,mBAAK,mBAAkB,GAAG;AAClD,WAAO,kBAAkB;AAAA,EAC3B,CAAC,EACA,IAAI,CAAC,CAAC,GAAG,KAAK,MAAM;AACnB,WAAO,MAAM,0BAA0B;AAAA,EACzC,CAAC;AACH,QAAM,QAAQ,IAAI,eAAe;AACnC;AAEA,aAAQ,SAAC,OAAc;AAErB,qBAAK,cAAa,QAAQ,CAAC,CAAC,GAAG,OAAO,MAAM;AAC1C,uCAAU;AAAA,EACZ,CAAC;AACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAuBA,kBAAa,WAAG;AACd,SAAO,OAAO,QAAQ,mBAAK,QAAO;AAIpC;AAjRF;AAmXO,IAAM,gCAAN,cAIG,iBAA6B;AAAA,EAMrC,YAAY,SAA8C;AACxD,UAAM,OAAO;AAXV;AAKL,wCAAkB,oBAAI,IAAuD;AAC7E;AAME,uBAAK,eAAgB,OAAO;AAAA,MAC1B,OAAO,QAAQ,QAAQ,MAAM,EAAE,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,SAAS,CAAC;AAAA,IAChE;AAAA,EACF;AAAA,EAMA,MAAgB,SACd,UACe;AACf,0BAAK,yDAAL,WAAiB;AACjB,UAAM,oBAAoB,sBAAK,mEAAL;AAC1B,UAAM,gBAAgB,CAAC,GAAG,mBAAK,iBAAgB,KAAK,CAAC,EAAE;AAAA,MACrD,CAAC,QAAQ,OAAO;AAAA,IAClB;AACA,UAAM,oBAAoB,cACvB,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,EACpB;AAAA,MAAI,CAAC,QAAK;AAlZjB;AAmZQ,wCAAK,iBAAgB,IAAI,GAAG,MAA5B,mBAA+B,KAAK,CAAC,GAAG,MAAM;AAC5C,gBAAM,EAAE,SAAS,SAAS,IAAI;AAC9B,gBAAM,EAAE,SAAS,SAAS,IAAI;AAC9B,cACE,OAAO,SAAS,gBAAgB,YAChC,OAAO,SAAS,gBAAgB,UAChC;AACA,mBAAO;AAAA,UACT;AACA,iBAAO,SAAS,cAAc,SAAS;AAAA,QACzC;AAAA;AAAA,IACF,EACC,OAAO,CAACC,cAAaA,cAAa,MAAS,EAC3C,KAAK;AACR,kBAAc,QAAQ,CAAC,QAAQ;AAC7B,yBAAK,iBAAgB,OAAO,GAAG;AAAA,IACjC,CAAC;AACD,QAAI,kBAAkB,SAAS,GAAG;AAChC,YAAM,MAAM,SAAS,iBAAiB;AAAA,IACxC;AAAA,EACF;AAiCF;AAhFE;AACA;AANK;AAiBL,0BAAqB,WAAG;AACtB,SAAO,KAAK,IAAI,GAAG,OAAO,OAAO,mBAAK,cAAa,CAAC;AACtD;AAmCA,gBAAW,SAAC,UAA4C;AACtD,QAAM,aAAa,KAAK;AACxB,WAAS,QAAQ,CAAC,YAAY;AA3alC;AA4aM,UAAM,EAAE,OAAO,QAAQ,IAAI;AAC3B,QAAID,iBAAgB,OAAO,GAAG;AAE5B,YAAM,MAAM,OAAO,QAAQ,QAAQ,WAAW,QAAQ,MAAM;AAC5D,UAAI,CAAC,mBAAK,iBAAgB,IAAI,GAAG,GAAG;AAClC,2BAAK,iBAAgB,IAAI,KAAK,CAAC,CAAC;AAAA,MAClC;AACA,+BAAK,iBAAgB,IAAI,GAAG,MAA5B,mBAA+B,KAAK;AACpC,UACE;AAAA,MACA,OAAO,QAAQ,SAAS,aACxB,QAAQ,SAAS,MACjB;AACA,2BAAK,eAAc,KAAK,IAAI,KAAK,IAAI,mBAAK,eAAc,KAAK,GAAG,GAAG;AAAA,MACrE;AAAA,IACF,WAAW,iBAAiB,OAAO,GAAG;AACpC,UAAI,QAAQ,YAAY,cAAc;AACpC,YAAI,OAAO,QAAQ,yBAAyB,UAAU;AACpD,gBAAM,IAAI,MAAM,sCAAsC;AAAA,QACxD;AACA,2BAAK,eAAc,KAAK,IAAI,KAAK;AAAA,UAC/B,mBAAK,eAAc,KAAK;AAAA,UACxB,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;","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(...args: Array<bigint | number>): bigint {\n return BigInt(args.reduce((m, e) => (e > m ? e : m)))\n}\n\nexport function bigIntMin(...args: Array<bigint | number>): bigint {\n return BigInt(args.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 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<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(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 !== `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(...args) {
89
+ return BigInt(args.reduce((m, e) => e > m ? e : m));
90
+ }
91
+ function bigIntMin(...args) {
92
+ return BigInt(args.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, -Infinity])
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, -Infinity])
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
  }
@@ -203,23 +214,21 @@ start_fn = function() {
203
214
  }
204
215
  shape.subscribe(
205
216
  (messages) => __async(this, null, function* () {
206
- const upToDateLsns = messages.filter(isControlMessage).map(({ headers }) => {
207
- var _a;
208
- return (_a = headers.global_last_seen_lsn) != null ? _a : 0;
209
- });
217
+ const upToDateLsns = messages.filter(isControlMessage).map(
218
+ ({ headers }) => typeof headers.global_last_seen_lsn === `string` ? BigInt(headers.global_last_seen_lsn) : BigInt(0)
219
+ );
210
220
  if (upToDateLsns.length > 0) {
211
- const maxUpToDateLsn = Math.max(...upToDateLsns);
221
+ const maxUpToDateLsn = bigIntMax(...upToDateLsns);
212
222
  const lastMaxUpToDateLsn = __privateGet(this, _lastUpToDateLsns)[key];
213
223
  if (maxUpToDateLsn > lastMaxUpToDateLsn) {
214
224
  __privateGet(this, _lastUpToDateLsns)[key] = maxUpToDateLsn;
215
225
  }
216
226
  }
217
- const dataLsns = messages.filter(isChangeMessage2).map(({ headers }) => {
218
- var _a;
219
- return (_a = headers.lsn) != null ? _a : 0;
220
- });
227
+ const dataLsns = messages.filter(isChangeMessage2).map(
228
+ ({ headers }) => typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)
229
+ );
221
230
  if (dataLsns.length > 0) {
222
- const maxDataLsn = Math.max(...dataLsns);
231
+ const maxDataLsn = bigIntMax(...dataLsns);
223
232
  const lastMaxDataLsn = __privateGet(this, _lastDataLsns)[key];
224
233
  if (maxDataLsn > lastMaxDataLsn) {
225
234
  __privateGet(this, _lastDataLsns)[key] = maxDataLsn;
@@ -247,7 +256,7 @@ scheduleCheckForUpdates_fn = function() {
247
256
  };
248
257
  checkForUpdates_fn = function() {
249
258
  return __async(this, null, function* () {
250
- const maxDataLsn = Math.max(...Object.values(__privateGet(this, _lastDataLsns)));
259
+ const maxDataLsn = bigIntMax(...Object.values(__privateGet(this, _lastDataLsns)));
251
260
  const refreshPromises = __privateMethod(this, _MultiShapeStream_instances, shapeEntries_fn).call(this).filter(([key]) => {
252
261
  const lastUpToDateLsn = __privateGet(this, _lastUpToDateLsns)[key];
253
262
  return lastUpToDateLsn < maxDataLsn;
@@ -278,7 +287,7 @@ var _TransactionalMultiShapeStream = class _TransactionalMultiShapeStream extend
278
287
  __privateAdd(this, _changeMessages, /* @__PURE__ */ new Map());
279
288
  __privateAdd(this, _completeLsns);
280
289
  __privateSet(this, _completeLsns, Object.fromEntries(
281
- Object.entries(options.shapes).map(([key]) => [key, -Infinity])
290
+ Object.entries(options.shapes).map(([key]) => [key, BigInt(-1)])
282
291
  ));
283
292
  }
284
293
  _publish(messages) {
@@ -288,7 +297,7 @@ var _TransactionalMultiShapeStream = class _TransactionalMultiShapeStream extend
288
297
  const lsnsToPublish = [...__privateGet(this, _changeMessages).keys()].filter(
289
298
  (lsn) => lsn <= lowestCompleteLsn
290
299
  );
291
- const messagesToPublish = lsnsToPublish.sort((a, b) => a - b).map(
300
+ const messagesToPublish = lsnsToPublish.sort((a, b) => bigIntCompare(a, b)).map(
292
301
  (lsn) => {
293
302
  var _a;
294
303
  return (_a = __privateGet(this, _changeMessages).get(lsn)) == null ? void 0 : _a.sort((a, b) => {
@@ -314,7 +323,7 @@ _changeMessages = new WeakMap();
314
323
  _completeLsns = new WeakMap();
315
324
  _TransactionalMultiShapeStream_instances = new WeakSet();
316
325
  getLowestCompleteLsn_fn = function() {
317
- return Math.min(...Object.values(__privateGet(this, _completeLsns)));
326
+ return bigIntMin(...Object.values(__privateGet(this, _completeLsns)));
318
327
  };
319
328
  accumulate_fn = function(messages) {
320
329
  const isUpToDate = this.isUpToDate;
@@ -322,23 +331,23 @@ accumulate_fn = function(messages) {
322
331
  var _a;
323
332
  const { shape, headers } = message;
324
333
  if (isChangeMessage2(message)) {
325
- const lsn = typeof headers.lsn === `number` ? headers.lsn : 0;
334
+ const lsn = typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0);
326
335
  if (!__privateGet(this, _changeMessages).has(lsn)) {
327
336
  __privateGet(this, _changeMessages).set(lsn, []);
328
337
  }
329
338
  (_a = __privateGet(this, _changeMessages).get(lsn)) == null ? void 0 : _a.push(message);
330
339
  if (isUpToDate && // All shapes must be up to date
331
340
  typeof headers.last === `boolean` && headers.last === true) {
332
- __privateGet(this, _completeLsns)[shape] = Math.max(__privateGet(this, _completeLsns)[shape], lsn);
341
+ __privateGet(this, _completeLsns)[shape] = bigIntMax(__privateGet(this, _completeLsns)[shape], lsn);
333
342
  }
334
343
  } else if (isControlMessage(message)) {
335
344
  if (headers.control === `up-to-date`) {
336
- if (typeof headers.global_last_seen_lsn !== `number`) {
345
+ if (typeof headers.global_last_seen_lsn !== `string`) {
337
346
  throw new Error(`global_last_seen_lsn is not a number`);
338
347
  }
339
- __privateGet(this, _completeLsns)[shape] = Math.max(
348
+ __privateGet(this, _completeLsns)[shape] = bigIntMax(
340
349
  __privateGet(this, _completeLsns)[shape],
341
- headers.global_last_seen_lsn
350
+ BigInt(headers.global_last_seen_lsn)
342
351
  );
343
352
  }
344
353
  }
@@ -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(...args: Array<bigint | number>): bigint {\n return BigInt(args.reduce((m, e) => (e > m ? e : m)))\n}\n\nexport function bigIntMin(...args: Array<bigint | number>): bigint {\n return BigInt(args.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 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<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(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 !== `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.2-beta.4",
4
+ "version": "1.0.0",
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.0-beta.5"
26
+ "@electric-sql/client": "1.0.0"
27
27
  },
28
28
  "peerDependencies": {
29
- "@electric-sql/client": "1.0.0-beta.5"
29
+ "@electric-sql/client": "1.0.0"
30
30
  },
31
31
  "peerDependenciesMeta": {
32
32
  "@electric-sql/client": {
@@ -0,0 +1,11 @@
1
+ export function bigIntMax(...args: Array<bigint | number>): bigint {
2
+ return BigInt(args.reduce((m, e) => (e > m ? e : m)))
3
+ }
4
+
5
+ export function bigIntMin(...args: Array<bigint | number>): bigint {
6
+ return BigInt(args.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]: number }
129
- #lastUpToDateLsns: { [K in keyof TShapeRows]: number }
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, -Infinity])
159
- ) as { [K in keyof TShapeRows]: number }
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, -Infinity])
162
- ) as { [K in keyof TShapeRows]: number }
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 }) => (headers.global_last_seen_lsn as number) ?? 0)
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 = Math.max(...upToDateLsns)
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 }) => (headers.lsn as number) ?? 0)
196
+ .map(({ headers }) =>
197
+ typeof headers.lsn === `string` ? BigInt(headers.lsn) : BigInt(0)
198
+ )
192
199
  if (dataLsns.length > 0) {
193
- const maxDataLsn = Math.max(...dataLsns)
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 = Math.max(...Object.values(this.#lastDataLsns))
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
@@ -374,20 +381,20 @@ export class TransactionalMultiShapeStream<
374
381
  [K: string]: Row<unknown>
375
382
  },
376
383
  > extends MultiShapeStream<TShapeRows> {
377
- #changeMessages = new Map<number, MultiShapeMessage<Row<unknown>, string>[]>()
384
+ #changeMessages = new Map<bigint, MultiShapeMessage<Row<unknown>, string>[]>()
378
385
  #completeLsns: {
379
- [K in keyof TShapeRows]: number
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, -Infinity])
386
- ) as { [K in keyof TShapeRows]: number }
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 Math.min(...Object.values(this.#completeLsns))
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 - b)
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 = typeof headers.lsn === `number` ? headers.lsn : 0
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,16 +447,16 @@ export class TransactionalMultiShapeStream<
439
447
  typeof headers.last === `boolean` &&
440
448
  headers.last === true
441
449
  ) {
442
- this.#completeLsns[shape] = Math.max(this.#completeLsns[shape], lsn)
450
+ this.#completeLsns[shape] = bigIntMax(this.#completeLsns[shape], lsn)
443
451
  }
444
452
  } else if (isControlMessage(message)) {
445
453
  if (headers.control === `up-to-date`) {
446
- if (typeof headers.global_last_seen_lsn !== `number`) {
454
+ if (typeof headers.global_last_seen_lsn !== `string`) {
447
455
  throw new Error(`global_last_seen_lsn is not a number`)
448
456
  }
449
- this.#completeLsns[shape] = Math.max(
457
+ this.#completeLsns[shape] = bigIntMax(
450
458
  this.#completeLsns[shape],
451
- headers.global_last_seen_lsn
459
+ BigInt(headers.global_last_seen_lsn)
452
460
  )
453
461
  }
454
462
  }