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