@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.
- package/dist/bundle.cjs +510 -1129
- package/dist/bundle.cjs.map +1 -1
- package/dist/bundle.mjs +511 -1116
- package/dist/bundle.mjs.map +1 -1
- package/dist/bundle.node.cjs +508 -1129
- package/dist/bundle.node.cjs.map +1 -1
- package/dist/bundle.node.mjs +509 -1116
- package/dist/bundle.node.mjs.map +1 -1
- package/dist/index.d.cts +73 -433
- package/legacy/sync_protocol.d.ts +103 -0
- package/lib/client/AbstractPowerSyncDatabase.js +3 -3
- package/lib/client/AbstractPowerSyncDatabase.js.map +1 -1
- package/lib/client/ConnectionManager.js +1 -1
- package/lib/client/ConnectionManager.js.map +1 -1
- package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +6 -64
- package/lib/client/sync/bucket/BucketStorageAdapter.js +4 -0
- package/lib/client/sync/bucket/BucketStorageAdapter.js.map +1 -1
- package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +1 -28
- package/lib/client/sync/bucket/SqliteBucketStorage.js +0 -162
- package/lib/client/sync/bucket/SqliteBucketStorage.js.map +1 -1
- package/lib/client/sync/stream/AbstractRemote.d.ts +29 -18
- package/lib/client/sync/stream/AbstractRemote.js +155 -188
- package/lib/client/sync/stream/AbstractRemote.js.map +1 -1
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +13 -35
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +150 -448
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -1
- package/lib/client/sync/stream/JsonValue.d.ts +7 -0
- package/lib/client/sync/stream/JsonValue.js +2 -0
- package/lib/client/sync/stream/JsonValue.js.map +1 -0
- package/lib/client/sync/stream/core-instruction.d.ts +14 -9
- package/lib/client/sync/stream/core-instruction.js +3 -0
- package/lib/client/sync/stream/core-instruction.js.map +1 -1
- package/lib/db/DBAdapter.d.ts +9 -0
- package/lib/db/DBAdapter.js +8 -1
- package/lib/db/DBAdapter.js.map +1 -1
- package/lib/db/crud/SyncStatus.d.ts +3 -4
- package/lib/db/crud/SyncStatus.js +0 -4
- package/lib/db/crud/SyncStatus.js.map +1 -1
- package/lib/db/schema/RawTable.d.ts +0 -5
- package/lib/db/schema/Schema.d.ts +0 -2
- package/lib/db/schema/Schema.js +0 -2
- package/lib/db/schema/Schema.js.map +1 -1
- package/lib/index.d.ts +2 -6
- package/lib/index.js +1 -6
- 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/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 +15 -10
- package/src/client/AbstractPowerSyncDatabase.ts +3 -3
- package/src/client/ConnectionManager.ts +1 -1
- package/src/client/sync/bucket/BucketStorageAdapter.ts +6 -71
- package/src/client/sync/bucket/SqliteBucketStorage.ts +1 -197
- package/src/client/sync/stream/AbstractRemote.ts +183 -229
- package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +181 -510
- package/src/client/sync/stream/JsonValue.ts +8 -0
- package/src/client/sync/stream/core-instruction.ts +15 -5
- package/src/db/DBAdapter.ts +20 -2
- package/src/db/crud/SyncStatus.ts +4 -5
- package/src/db/schema/RawTable.ts +0 -5
- package/src/db/schema/Schema.ts +0 -2
- package/src/index.ts +2 -6
- package/src/utils/async.ts +0 -11
- package/src/utils/stream_transform.ts +252 -0
- package/lib/client/sync/bucket/OpType.d.ts +0 -16
- package/lib/client/sync/bucket/OpType.js +0 -23
- package/lib/client/sync/bucket/OpType.js.map +0 -1
- package/lib/client/sync/bucket/OplogEntry.d.ts +0 -23
- package/lib/client/sync/bucket/OplogEntry.js +0 -36
- package/lib/client/sync/bucket/OplogEntry.js.map +0 -1
- package/lib/client/sync/bucket/SyncDataBatch.d.ts +0 -6
- package/lib/client/sync/bucket/SyncDataBatch.js +0 -12
- package/lib/client/sync/bucket/SyncDataBatch.js.map +0 -1
- package/lib/client/sync/bucket/SyncDataBucket.d.ts +0 -40
- package/lib/client/sync/bucket/SyncDataBucket.js +0 -40
- package/lib/client/sync/bucket/SyncDataBucket.js.map +0 -1
- package/lib/client/sync/stream/streaming-sync-types.d.ts +0 -143
- package/lib/client/sync/stream/streaming-sync-types.js +0 -26
- package/lib/client/sync/stream/streaming-sync-types.js.map +0 -1
- 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/client/sync/bucket/OpType.ts +0 -23
- package/src/client/sync/bucket/OplogEntry.ts +0 -50
- package/src/client/sync/bucket/SyncDataBatch.ts +0 -11
- package/src/client/sync/bucket/SyncDataBucket.ts +0 -49
- package/src/client/sync/stream/streaming-sync-types.ts +0 -210
- 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
|
|
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 {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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:
|
|
43
|
+
data: unknown;
|
|
41
44
|
headers?: Record<string, string>;
|
|
42
|
-
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
|
-
|
|
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
|
-
*
|
|
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<
|
|
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 =
|
|
283
|
+
const mimeType = 'application/json';
|
|
292
284
|
|
|
293
285
|
function toBuffer(js: any): Buffer {
|
|
294
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
328
|
+
if (options.abortSignal.aborted) {
|
|
322
329
|
throw new AbortOperation('Connection request aborted');
|
|
323
330
|
} else {
|
|
324
|
-
options.abortSignal
|
|
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
|
-
|
|
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', (
|
|
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
|
-
|
|
373
|
+
pendingSocket = null;
|
|
388
374
|
} catch (ex) {
|
|
389
375
|
this.logger.error(`Failed to connect WebSocket`, ex);
|
|
390
|
-
|
|
391
|
-
|
|
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(() => (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
461
|
+
// Request another event (unless the downstream consumer is paused).
|
|
462
|
+
requestMore();
|
|
470
463
|
},
|
|
471
464
|
onComplete: () => {
|
|
472
|
-
|
|
465
|
+
abortRequest(); // this will also emit a done event
|
|
473
466
|
},
|
|
474
467
|
onExtension: () => {}
|
|
475
468
|
}
|
|
476
469
|
);
|
|
477
470
|
});
|
|
471
|
+
}
|
|
478
472
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
514
|
-
throw new AbortOperation('Abort request received before making
|
|
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
|
|
519
|
-
abortSignal
|
|
520
|
-
|
|
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
|
-
|
|
524
|
-
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
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
|
|
589
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
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
|
-
|
|
640
|
-
|
|
641
|
-
closeReader();
|
|
642
|
-
l?.();
|
|
643
|
-
}
|
|
644
|
-
});
|
|
585
|
+
return { isBson: responseIsBson, stream };
|
|
586
|
+
}
|
|
645
587
|
|
|
646
|
-
|
|
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
|
}
|