@powersync/common 1.51.0 → 1.53.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.
Files changed (91) hide show
  1. package/dist/bundle.cjs +510 -1129
  2. package/dist/bundle.cjs.map +1 -1
  3. package/dist/bundle.mjs +511 -1116
  4. package/dist/bundle.mjs.map +1 -1
  5. package/dist/bundle.node.cjs +508 -1129
  6. package/dist/bundle.node.cjs.map +1 -1
  7. package/dist/bundle.node.mjs +509 -1116
  8. package/dist/bundle.node.mjs.map +1 -1
  9. package/dist/index.d.cts +73 -433
  10. package/legacy/sync_protocol.d.ts +103 -0
  11. package/lib/client/AbstractPowerSyncDatabase.js +3 -3
  12. package/lib/client/AbstractPowerSyncDatabase.js.map +1 -1
  13. package/lib/client/ConnectionManager.js +1 -1
  14. package/lib/client/ConnectionManager.js.map +1 -1
  15. package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +6 -64
  16. package/lib/client/sync/bucket/BucketStorageAdapter.js +4 -0
  17. package/lib/client/sync/bucket/BucketStorageAdapter.js.map +1 -1
  18. package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +1 -28
  19. package/lib/client/sync/bucket/SqliteBucketStorage.js +0 -162
  20. package/lib/client/sync/bucket/SqliteBucketStorage.js.map +1 -1
  21. package/lib/client/sync/stream/AbstractRemote.d.ts +29 -18
  22. package/lib/client/sync/stream/AbstractRemote.js +155 -188
  23. package/lib/client/sync/stream/AbstractRemote.js.map +1 -1
  24. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +13 -35
  25. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +150 -448
  26. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -1
  27. package/lib/client/sync/stream/JsonValue.d.ts +7 -0
  28. package/lib/client/sync/stream/JsonValue.js +2 -0
  29. package/lib/client/sync/stream/JsonValue.js.map +1 -0
  30. package/lib/client/sync/stream/core-instruction.d.ts +14 -9
  31. package/lib/client/sync/stream/core-instruction.js +3 -0
  32. package/lib/client/sync/stream/core-instruction.js.map +1 -1
  33. package/lib/db/DBAdapter.d.ts +9 -0
  34. package/lib/db/DBAdapter.js +8 -1
  35. package/lib/db/DBAdapter.js.map +1 -1
  36. package/lib/db/crud/SyncStatus.d.ts +3 -4
  37. package/lib/db/crud/SyncStatus.js +0 -4
  38. package/lib/db/crud/SyncStatus.js.map +1 -1
  39. package/lib/db/schema/RawTable.d.ts +0 -5
  40. package/lib/db/schema/Schema.d.ts +0 -2
  41. package/lib/db/schema/Schema.js +0 -2
  42. package/lib/db/schema/Schema.js.map +1 -1
  43. package/lib/index.d.ts +2 -6
  44. package/lib/index.js +1 -6
  45. package/lib/index.js.map +1 -1
  46. package/lib/utils/async.d.ts +0 -9
  47. package/lib/utils/async.js +0 -9
  48. package/lib/utils/async.js.map +1 -1
  49. package/lib/utils/stream_transform.d.ts +39 -0
  50. package/lib/utils/stream_transform.js +206 -0
  51. package/lib/utils/stream_transform.js.map +1 -0
  52. package/package.json +15 -10
  53. package/src/client/AbstractPowerSyncDatabase.ts +3 -3
  54. package/src/client/ConnectionManager.ts +1 -1
  55. package/src/client/sync/bucket/BucketStorageAdapter.ts +6 -71
  56. package/src/client/sync/bucket/SqliteBucketStorage.ts +1 -197
  57. package/src/client/sync/stream/AbstractRemote.ts +183 -229
  58. package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +181 -510
  59. package/src/client/sync/stream/JsonValue.ts +8 -0
  60. package/src/client/sync/stream/core-instruction.ts +15 -5
  61. package/src/db/DBAdapter.ts +20 -2
  62. package/src/db/crud/SyncStatus.ts +4 -5
  63. package/src/db/schema/RawTable.ts +0 -5
  64. package/src/db/schema/Schema.ts +0 -2
  65. package/src/index.ts +2 -6
  66. package/src/utils/async.ts +0 -11
  67. package/src/utils/stream_transform.ts +252 -0
  68. package/lib/client/sync/bucket/OpType.d.ts +0 -16
  69. package/lib/client/sync/bucket/OpType.js +0 -23
  70. package/lib/client/sync/bucket/OpType.js.map +0 -1
  71. package/lib/client/sync/bucket/OplogEntry.d.ts +0 -23
  72. package/lib/client/sync/bucket/OplogEntry.js +0 -36
  73. package/lib/client/sync/bucket/OplogEntry.js.map +0 -1
  74. package/lib/client/sync/bucket/SyncDataBatch.d.ts +0 -6
  75. package/lib/client/sync/bucket/SyncDataBatch.js +0 -12
  76. package/lib/client/sync/bucket/SyncDataBatch.js.map +0 -1
  77. package/lib/client/sync/bucket/SyncDataBucket.d.ts +0 -40
  78. package/lib/client/sync/bucket/SyncDataBucket.js +0 -40
  79. package/lib/client/sync/bucket/SyncDataBucket.js.map +0 -1
  80. package/lib/client/sync/stream/streaming-sync-types.d.ts +0 -143
  81. package/lib/client/sync/stream/streaming-sync-types.js +0 -26
  82. package/lib/client/sync/stream/streaming-sync-types.js.map +0 -1
  83. package/lib/utils/DataStream.d.ts +0 -62
  84. package/lib/utils/DataStream.js +0 -169
  85. package/lib/utils/DataStream.js.map +0 -1
  86. package/src/client/sync/bucket/OpType.ts +0 -23
  87. package/src/client/sync/bucket/OplogEntry.ts +0 -50
  88. package/src/client/sync/bucket/SyncDataBatch.ts +0 -11
  89. package/src/client/sync/bucket/SyncDataBucket.ts +0 -49
  90. package/src/client/sync/stream/streaming-sync-types.ts +0 -210
  91. package/src/utils/DataStream.ts +0 -222
@@ -1,16 +1,18 @@
1
- import type { BSON } from 'bson';
2
1
  import { type fetch } from 'cross-fetch';
3
2
  import Logger, { ILogger } from 'js-logger';
4
- import { RSocket, RSocketConnector, Requestable } from 'rsocket-core';
3
+ import { Requestable, RSocket, RSocketConnector } from 'rsocket-core';
5
4
  import PACKAGE from '../../../../package.json' with { type: 'json' };
6
5
  import { AbortOperation } from '../../../utils/AbortOperation.js';
7
- import { DataStream } from '../../../utils/DataStream.js';
8
6
  import { PowerSyncCredentials } from '../../connection/PowerSyncCredentials.js';
9
7
  import { WebsocketClientTransport } from './WebsocketClientTransport.js';
10
- import { StreamingSyncRequest } from './streaming-sync-types.js';
11
-
12
-
13
- export type BSONImplementation = typeof BSON;
8
+ import {
9
+ doneResult,
10
+ extractBsonObjects,
11
+ extractJsonLines,
12
+ SimpleAsyncIterator
13
+ } from '../../../utils/stream_transform.js';
14
+ import { EventIterator } from 'event-iterator';
15
+ import type { Queue } from 'event-iterator/lib/event-iterator.js';
14
16
 
15
17
  export type RemoteConnector = {
16
18
  fetchCredentials: () => Promise<PowerSyncCredentials | null>;
@@ -20,6 +22,7 @@ export type RemoteConnector = {
20
22
  const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
21
23
  const POWERSYNC_JS_VERSION = PACKAGE.version;
22
24
 
25
+ const SYNC_QUEUE_REQUEST_HIGH_WATER = 10;
23
26
  const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
24
27
 
25
28
  // Keep alive message is sent every period
@@ -37,9 +40,9 @@ export const DEFAULT_REMOTE_LOGGER = Logger.get('PowerSyncRemote');
37
40
 
38
41
  export type SyncStreamOptions = {
39
42
  path: string;
40
- data: StreamingSyncRequest;
43
+ data: unknown;
41
44
  headers?: Record<string, string>;
42
- abortSignal?: AbortSignal;
45
+ abortSignal: AbortSignal;
43
46
  fetchOptions?: Request;
44
47
  };
45
48
 
@@ -258,16 +261,11 @@ export abstract class AbstractRemote {
258
261
  return res.json();
259
262
  }
260
263
 
261
- /**
262
- * Provides a BSON implementation. The import nature of this varies depending on the platform
263
- */
264
- abstract getBSON(): Promise<BSONImplementation>;
265
-
266
264
  /**
267
265
  * @returns A text decoder decoding UTF-8. This is a method to allow patching it for Hermes which doesn't support the
268
266
  * builtin, without forcing us to bundle a polyfill with `@powersync/common`.
269
267
  */
270
- protected createTextDecoder(): TextDecoder {
268
+ createTextDecoder(): TextDecoder {
271
269
  return new TextDecoder();
272
270
  }
273
271
 
@@ -276,92 +274,81 @@ export abstract class AbstractRemote {
276
274
  }
277
275
 
278
276
  /**
279
- * Returns a data stream of sync line data.
277
+ * Returns a data stream of sync line data, fetched via RSocket-over-WebSocket.
280
278
  *
281
- * @param map Maps received payload frames to the typed event value.
282
- * @param bson A BSON encoder and decoder. When set, the data stream will be requested with a BSON payload
283
- * (required for compatibility with older sync services).
279
+ * The only mechanism to abort the returned stream is to use the abort signal in {@link SocketSyncStreamOptions}.
284
280
  */
285
- async socketStreamRaw<T>(
286
- options: SocketSyncStreamOptions,
287
- map: (buffer: Uint8Array) => T,
288
- bson?: typeof BSON
289
- ): Promise<DataStream<T>> {
281
+ async socketStreamRaw(options: SocketSyncStreamOptions): Promise<SimpleAsyncIterator<Uint8Array>> {
290
282
  const { path, fetchStrategy = FetchStrategy.Buffered } = options;
291
- const mimeType = bson == null ? 'application/json' : 'application/bson';
283
+ const mimeType = 'application/json';
292
284
 
293
285
  function toBuffer(js: any): Buffer {
294
- let contents: any;
295
- if (bson != null) {
296
- contents = bson.serialize(js);
297
- } else {
298
- contents = JSON.stringify(js);
299
- }
300
-
301
- return Buffer.from(contents);
286
+ return Buffer.from(JSON.stringify(js));
302
287
  }
303
288
 
304
289
  const syncQueueRequestSize = fetchStrategy == FetchStrategy.Buffered ? 10 : 1;
305
290
  const request = await this.buildRequest(path);
291
+ const url = this.options.socketUrlTransformer(request.url);
306
292
 
307
293
  // Add the user agent in the setup payload - we can't set custom
308
294
  // headers with websockets on web. The browser userAgent is however added
309
295
  // automatically as a header.
310
296
  const userAgent = this.getUserAgent();
311
297
 
312
- const stream = new DataStream<T, Uint8Array>({
313
- logger: this.logger,
314
- pressure: {
315
- lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
316
- },
317
- mapLine: map
318
- });
298
+ // While we're connecting (a process that can't be aborted in RSocket), the WebSocket instance to close if we wanted
299
+ // to abort the connection.
300
+ let pendingSocket: WebSocket | null = null;
301
+ let keepAliveTimeout: any;
302
+ let rsocket: RSocket | null = null;
303
+ let queue: Queue<Uint8Array> | null = null;
304
+ let didClose = false;
305
+
306
+ const abortRequest = () => {
307
+ if (didClose) {
308
+ return;
309
+ }
310
+ didClose = true;
311
+
312
+ clearTimeout(keepAliveTimeout);
313
+
314
+ if (pendingSocket) {
315
+ pendingSocket.close();
316
+ }
317
+
318
+ if (rsocket) {
319
+ rsocket.close();
320
+ }
321
+
322
+ if (queue) {
323
+ queue.stop();
324
+ }
325
+ };
319
326
 
320
327
  // Handle upstream abort
321
- if (options.abortSignal?.aborted) {
328
+ if (options.abortSignal.aborted) {
322
329
  throw new AbortOperation('Connection request aborted');
323
330
  } else {
324
- options.abortSignal?.addEventListener(
325
- 'abort',
326
- () => {
327
- stream.close();
328
- },
329
- { once: true }
330
- );
331
+ options.abortSignal.addEventListener('abort', abortRequest);
331
332
  }
332
333
 
333
- let keepAliveTimeout: any;
334
334
  const resetTimeout = () => {
335
335
  clearTimeout(keepAliveTimeout);
336
336
  keepAliveTimeout = setTimeout(() => {
337
337
  this.logger.error(`No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.`);
338
- stream.close();
338
+ abortRequest();
339
339
  }, SOCKET_TIMEOUT_MS);
340
340
  };
341
341
  resetTimeout();
342
342
 
343
- // Typescript complains about this being `never` if it's not assigned here.
344
- // This is assigned in `wsCreator`.
345
- let disposeSocketConnectionTimeout = () => {};
346
-
347
- const url = this.options.socketUrlTransformer(request.url);
348
343
  const connector = new RSocketConnector({
349
344
  transport: new WebsocketClientTransport({
350
345
  url,
351
346
  wsCreator: (url) => {
352
- const socket = this.createSocket(url);
353
- disposeSocketConnectionTimeout = stream.registerListener({
354
- closed: () => {
355
- // Allow closing the underlying WebSocket if the stream was closed before the
356
- // RSocket connect completed. This should effectively abort the request.
357
- socket.close();
358
- }
359
- });
347
+ const socket = (pendingSocket = this.createSocket(url));
360
348
 
361
- socket.addEventListener('message', (event) => {
349
+ socket.addEventListener('message', () => {
362
350
  resetTimeout();
363
351
  });
364
-
365
352
  return socket;
366
353
  }
367
354
  }),
@@ -380,47 +367,50 @@ export abstract class AbstractRemote {
380
367
  }
381
368
  });
382
369
 
383
- let rsocket: RSocket;
384
370
  try {
385
371
  rsocket = await connector.connect();
386
372
  // The connection is established, we no longer need to monitor the initial timeout
387
- disposeSocketConnectionTimeout();
373
+ pendingSocket = null;
388
374
  } catch (ex) {
389
375
  this.logger.error(`Failed to connect WebSocket`, ex);
390
- clearTimeout(keepAliveTimeout);
391
- if (!stream.closed) {
392
- await stream.close();
393
- }
376
+ abortRequest();
377
+
394
378
  throw ex;
395
379
  }
396
380
 
397
381
  resetTimeout();
398
382
 
399
- let socketIsClosed = false;
400
- const closeSocket = () => {
401
- clearTimeout(keepAliveTimeout);
402
- if (socketIsClosed) {
403
- return;
404
- }
405
- socketIsClosed = true;
406
- rsocket.close();
407
- };
408
383
  // Helps to prevent double close scenarios
409
- rsocket.onClose(() => (socketIsClosed = true));
410
- // We initially request this amount and expect these to arrive eventually
411
- let pendingEventsCount = syncQueueRequestSize;
412
-
413
- const disposeClosedListener = stream.registerListener({
414
- closed: () => {
415
- closeSocket();
416
- disposeClosedListener();
417
- }
418
- });
384
+ rsocket.onClose(() => (rsocket = null));
419
385
 
420
- const socket = await new Promise<Requestable>((resolve, reject) => {
386
+ return await new Promise((resolve, reject) => {
421
387
  let connectionEstablished = false;
388
+ let pendingEventsCount = syncQueueRequestSize;
389
+ let paused = false;
390
+ let res: Requestable | null = null;
391
+
392
+ function requestMore() {
393
+ const delta = syncQueueRequestSize - pendingEventsCount;
394
+ if (!paused && delta > 0) {
395
+ res?.request(delta);
396
+ pendingEventsCount = syncQueueRequestSize;
397
+ }
398
+ }
422
399
 
423
- const res = rsocket.requestStream(
400
+ const events = new EventIterator<Uint8Array>(
401
+ (q) => {
402
+ queue = q;
403
+
404
+ q.on('highWater', () => (paused = true));
405
+ q.on('lowWater', () => {
406
+ paused = false;
407
+ requestMore();
408
+ });
409
+ },
410
+ { highWaterMark: SYNC_QUEUE_REQUEST_HIGH_WATER, lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER }
411
+ )[Symbol.asyncIterator]();
412
+
413
+ res = rsocket!.requestStream(
424
414
  {
425
415
  data: toBuffer(options.data),
426
416
  metadata: toBuffer({
@@ -447,7 +437,7 @@ export abstract class AbstractRemote {
447
437
  }
448
438
  // RSocket will close the RSocket stream automatically
449
439
  // Close the downstream stream as well - this will close the RSocket connection and WebSocket
450
- stream.close();
440
+ abortRequest();
451
441
  // Handles cases where the connection failed e.g. auth error or connection error
452
442
  if (!connectionEstablished) {
453
443
  reject(e);
@@ -457,48 +447,49 @@ export abstract class AbstractRemote {
457
447
  // The connection is active
458
448
  if (!connectionEstablished) {
459
449
  connectionEstablished = true;
460
- resolve(res);
450
+ resolve(events);
461
451
  }
462
452
  const { data } = payload;
453
+
454
+ if (data) {
455
+ queue!.push(data);
456
+ }
457
+
463
458
  // Less events are now pending
464
459
  pendingEventsCount--;
465
- if (!data) {
466
- return;
467
- }
468
460
 
469
- stream.enqueueData(data);
461
+ // Request another event (unless the downstream consumer is paused).
462
+ requestMore();
470
463
  },
471
464
  onComplete: () => {
472
- stream.close();
465
+ abortRequest(); // this will also emit a done event
473
466
  },
474
467
  onExtension: () => {}
475
468
  }
476
469
  );
477
470
  });
471
+ }
478
472
 
479
- const l = stream.registerListener({
480
- lowWater: async () => {
481
- // Request to fill up the queue
482
- const required = syncQueueRequestSize - pendingEventsCount;
483
- if (required > 0) {
484
- socket.request(syncQueueRequestSize - pendingEventsCount);
485
- pendingEventsCount = syncQueueRequestSize;
486
- }
487
- },
488
- closed: () => {
489
- l();
490
- }
491
- });
492
-
493
- return stream;
473
+ /**
474
+ * @returns Whether the HTTP implementation on this platform can receive streamed binary responses. This is true on
475
+ * all platforms except React Native (who would have guessed...), where we must not request BSON responses.
476
+ *
477
+ * @see https://github.com/react-native-community/fetch?tab=readme-ov-file#motivation
478
+ */
479
+ protected get supportsStreamingBinaryResponses(): boolean {
480
+ return true;
494
481
  }
495
482
 
496
483
  /**
497
- * Connects to the sync/stream http endpoint, mapping and emitting each received string line.
484
+ * Posts a `/sync/stream` request, asserts that it completes successfully and returns the streaming response as an
485
+ * async iterator of byte blobs.
486
+ *
487
+ * To cancel the async iterator, use the abort signal from {@link SyncStreamOptions} passed to this method.
498
488
  */
499
- async postStreamRaw<T>(options: SyncStreamOptions, mapLine: (line: string) => T): Promise<DataStream<T>> {
489
+ protected async fetchStreamRaw(
490
+ options: SyncStreamOptions
491
+ ): Promise<{ isBson: boolean; stream: SimpleAsyncIterator<Uint8Array> }> {
500
492
  const { data, path, headers, abortSignal } = options;
501
-
502
493
  const request = await this.buildRequest(path);
503
494
 
504
495
  /**
@@ -510,139 +501,102 @@ export abstract class AbstractRemote {
510
501
  * Aborting the active fetch request while it is being consumed seems to throw
511
502
  * an unhandled exception on the window level.
512
503
  */
513
- if (abortSignal?.aborted) {
514
- throw new AbortOperation('Abort request received before making postStreamRaw request');
504
+ if (abortSignal.aborted) {
505
+ throw new AbortOperation('Abort request received before making fetchStreamRaw request');
515
506
  }
516
507
 
517
508
  const controller = new AbortController();
518
- let requestResolved = false;
519
- abortSignal?.addEventListener('abort', () => {
520
- if (!requestResolved) {
509
+ let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
510
+ abortSignal.addEventListener('abort', () => {
511
+ const reason =
512
+ abortSignal.reason ??
513
+ new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.');
514
+
515
+ if (reader == null) {
521
516
  // Only abort via the abort controller if the request has not resolved yet
522
- controller.abort(
523
- abortSignal.reason ??
524
- new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.')
525
- );
517
+ controller.abort(reason);
518
+ } else {
519
+ reader.cancel(reason).catch(() => {
520
+ // Cancelling the reader might rethrow an exception we would have handled by throwing in next(). So we can
521
+ // ignore it here.
522
+ });
526
523
  }
527
524
  });
528
525
 
529
- const res = await this.fetch(request.url, {
530
- method: 'POST',
531
- headers: { ...headers, ...request.headers },
532
- body: JSON.stringify(data),
533
- signal: controller.signal,
534
- cache: 'no-store',
535
- ...(this.options.fetchOptions ?? {}),
536
- ...options.fetchOptions
537
- }).catch((ex) => {
526
+ let res: Response;
527
+ let responseIsBson = false;
528
+ try {
529
+ const ndJson = 'application/x-ndjson';
530
+ const bson = 'application/vnd.powersync.bson-stream';
531
+
532
+ res = await this.fetch(request.url, {
533
+ method: 'POST',
534
+ headers: {
535
+ ...headers,
536
+ ...request.headers,
537
+ accept: this.supportsStreamingBinaryResponses ? `${bson};q=0.9,${ndJson};q=0.8` : ndJson
538
+ },
539
+ body: JSON.stringify(data),
540
+ signal: controller.signal,
541
+ cache: 'no-store',
542
+ ...(this.options.fetchOptions ?? {}),
543
+ ...options.fetchOptions
544
+ });
545
+
546
+ if (!res.ok || !res.body) {
547
+ const text = await res.text();
548
+ this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
549
+ const error: any = new Error(`HTTP ${res.statusText}: ${text}`);
550
+ error.status = res.status;
551
+ throw error;
552
+ }
553
+
554
+ const contentType = res.headers.get('content-type');
555
+ responseIsBson = contentType == bson;
556
+ } catch (ex: any) {
538
557
  if (ex.name == 'AbortError') {
539
558
  throw new AbortOperation(`Pending fetch request to ${request.url} has been aborted.`);
540
559
  }
541
560
  throw ex;
542
- });
543
-
544
- if (!res) {
545
- throw new Error('Fetch request was aborted');
546
- }
547
-
548
- requestResolved = true;
549
-
550
- if (!res.ok || !res.body) {
551
- const text = await res.text();
552
- this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
553
- const error: any = new Error(`HTTP ${res.statusText}: ${text}`);
554
- error.status = res.status;
555
- throw error;
556
561
  }
557
562
 
558
- // Create a new stream splitting the response at line endings while also handling cancellations
559
- // by closing the reader.
560
- const reader = res.body.getReader();
561
- let readerReleased = false;
562
- // This will close the network request and read stream
563
- const closeReader = async () => {
564
- try {
565
- readerReleased = true;
566
- await reader.cancel();
567
- } catch (ex) {
568
- // an error will throw if the reader hasn't been used yet
569
- }
570
- reader.releaseLock();
571
- };
572
-
573
-
574
- const stream = new DataStream<T, string>({
575
- logger: this.logger,
576
- mapLine: mapLine,
577
- pressure: {
578
- highWaterMark: 20,
579
- lowWaterMark: 10
580
- }
581
- });
582
-
583
- abortSignal?.addEventListener('abort', () => {
584
- closeReader();
585
- stream.close();
586
- });
563
+ reader = res.body.getReader();
587
564
 
588
- const decoder = this.createTextDecoder();
589
- let buffer = '';
590
-
591
-
592
- const consumeStream = async () => {
593
- while (!stream.closed && !abortSignal?.aborted && !readerReleased) {
594
- const { done, value } = await reader.read();
595
- if (done) {
596
- const remaining = buffer.trim();
597
- if (remaining.length != 0) {
598
- stream.enqueueData(remaining);
599
- }
600
-
601
- stream.close();
602
- await closeReader();
603
- return;
565
+ const stream: SimpleAsyncIterator<Uint8Array> = {
566
+ next: async () => {
567
+ if (controller.signal.aborted) {
568
+ return doneResult;
604
569
  }
605
570
 
606
- const data = decoder.decode(value, { stream: true });
607
- buffer += data;
608
-
609
- const lines = buffer.split('\n');
610
- for (var i = 0; i < lines.length - 1; i++) {
611
- var l = lines[i].trim();
612
- if (l.length > 0) {
613
- stream.enqueueData(l);
571
+ try {
572
+ return await reader.read();
573
+ } catch (ex) {
574
+ if (controller.signal.aborted) {
575
+ // .read() completes with an error if we cancel the reader, which we do to disconnect. So this is just
576
+ // things working as intended, we can return a done event and consider the exception handled.
577
+ return doneResult;
614
578
  }
615
- }
616
579
 
617
- buffer = lines[lines.length - 1];
618
-
619
- // Implement backpressure by waiting for the low water mark to be reached
620
- if (stream.dataQueue.length > stream.highWatermark) {
621
- await new Promise<void>((resolve) => {
622
- const dispose = stream.registerListener({
623
- lowWater: async () => {
624
- resolve();
625
- dispose();
626
- },
627
- closed: () => {
628
- resolve();
629
- dispose();
630
- }
631
- })
632
- })
580
+ throw ex;
633
581
  }
634
582
  }
635
- }
636
-
637
- consumeStream().catch(ex => this.logger.error('Error consuming stream', ex));
583
+ };
638
584
 
639
- const l = stream.registerListener({
640
- closed: () => {
641
- closeReader();
642
- l?.();
643
- }
644
- });
585
+ return { isBson: responseIsBson, stream };
586
+ }
645
587
 
646
- return stream;
588
+ /**
589
+ * Posts a `/sync/stream` request.
590
+ *
591
+ * Depending on the `Content-Type` of the response, this returns strings for sync lines or encoded BSON documents as
592
+ * {@link Uint8Array}s.
593
+ */
594
+ async fetchStream(options: SyncStreamOptions): Promise<SimpleAsyncIterator<Uint8Array | string>> {
595
+ const { isBson, stream } = await this.fetchStreamRaw(options);
596
+ if (isBson) {
597
+ return extractBsonObjects(stream);
598
+ } else {
599
+ return extractJsonLines(stream, this.createTextDecoder());
600
+ }
647
601
  }
648
602
  }