@powersync/common 0.0.0-dev-20250625140957 → 0.0.0-dev-20250710151329
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 +2 -2
- package/dist/bundle.mjs +2 -2
- package/lib/client/AbstractPowerSyncDatabase.d.ts +2 -3
- package/lib/client/AbstractPowerSyncDatabase.js +5 -7
- package/lib/client/ConnectionManager.js +0 -1
- package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +1 -1
- package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +1 -1
- package/lib/client/sync/stream/AbstractRemote.d.ts +1 -2
- package/lib/client/sync/stream/AbstractRemote.js +30 -8
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +50 -28
- package/lib/db/crud/SyncProgress.d.ts +1 -1
- package/lib/utils/DataStream.d.ts +13 -14
- package/lib/utils/DataStream.js +27 -29
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Mutex } from 'async-mutex';
|
|
2
|
-
import
|
|
2
|
+
import { ILogger } from 'js-logger';
|
|
3
3
|
import { DBAdapter, QueryResult, Transaction } from '../db/DBAdapter.js';
|
|
4
4
|
import { SyncStatus } from '../db/crud/SyncStatus.js';
|
|
5
5
|
import { UploadQueueStats } from '../db/crud/UploadQueueStatus.js';
|
|
@@ -84,7 +84,6 @@ export declare const DEFAULT_POWERSYNC_CLOSE_OPTIONS: PowerSyncCloseOptions;
|
|
|
84
84
|
export declare const DEFAULT_WATCH_THROTTLE_MS = 30;
|
|
85
85
|
export declare const DEFAULT_POWERSYNC_DB_OPTIONS: {
|
|
86
86
|
retryDelayMs: number;
|
|
87
|
-
logger: Logger.ILogger;
|
|
88
87
|
crudUploadThrottleMs: number;
|
|
89
88
|
};
|
|
90
89
|
export declare const DEFAULT_CRUD_BATCH_LIMIT = 100;
|
|
@@ -123,6 +122,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
123
122
|
protected _schema: Schema;
|
|
124
123
|
private _database;
|
|
125
124
|
protected runExclusiveMutex: Mutex;
|
|
125
|
+
logger: ILogger;
|
|
126
126
|
constructor(options: PowerSyncDatabaseOptionsWithDBAdapter);
|
|
127
127
|
constructor(options: PowerSyncDatabaseOptionsWithOpenFactory);
|
|
128
128
|
constructor(options: PowerSyncDatabaseOptionsWithSettings);
|
|
@@ -185,7 +185,6 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
185
185
|
* Cannot be used while connected - this should only be called before {@link AbstractPowerSyncDatabase.connect}.
|
|
186
186
|
*/
|
|
187
187
|
updateSchema(schema: Schema): Promise<void>;
|
|
188
|
-
get logger(): Logger.ILogger;
|
|
189
188
|
/**
|
|
190
189
|
* Wait for initialization to complete.
|
|
191
190
|
* While initializing is automatic, this helps to catch and report initialization errors.
|
|
@@ -27,7 +27,6 @@ export const DEFAULT_POWERSYNC_CLOSE_OPTIONS = {
|
|
|
27
27
|
export const DEFAULT_WATCH_THROTTLE_MS = 30;
|
|
28
28
|
export const DEFAULT_POWERSYNC_DB_OPTIONS = {
|
|
29
29
|
retryDelayMs: 5000,
|
|
30
|
-
logger: Logger.get('PowerSyncDatabase'),
|
|
31
30
|
crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
|
|
32
31
|
};
|
|
33
32
|
export const DEFAULT_CRUD_BATCH_LIMIT = 100;
|
|
@@ -70,6 +69,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
70
69
|
_schema;
|
|
71
70
|
_database;
|
|
72
71
|
runExclusiveMutex;
|
|
72
|
+
logger;
|
|
73
73
|
constructor(options) {
|
|
74
74
|
super();
|
|
75
75
|
this.options = options;
|
|
@@ -89,6 +89,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
89
89
|
else {
|
|
90
90
|
throw new Error('The provided `database` option is invalid.');
|
|
91
91
|
}
|
|
92
|
+
this.logger = options.logger ?? Logger.get(`PowerSyncDatabase[${this._database.name}]`);
|
|
92
93
|
this.bucketStorageAdapter = this.generateBucketStorageAdapter();
|
|
93
94
|
this.closed = false;
|
|
94
95
|
this.currentStatus = new SyncStatus({});
|
|
@@ -268,16 +269,13 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
268
269
|
schema.validate();
|
|
269
270
|
}
|
|
270
271
|
catch (ex) {
|
|
271
|
-
this.
|
|
272
|
+
this.logger.warn('Schema validation failed. Unexpected behaviour could occur', ex);
|
|
272
273
|
}
|
|
273
274
|
this._schema = schema;
|
|
274
275
|
await this.database.execute('SELECT powersync_replace_schema(?)', [JSON.stringify(this.schema.toJSON())]);
|
|
275
276
|
await this.database.refreshSchema();
|
|
276
277
|
this.iterateListeners(async (cb) => cb.schemaChanged?.(schema));
|
|
277
278
|
}
|
|
278
|
-
get logger() {
|
|
279
|
-
return this.options.logger;
|
|
280
|
-
}
|
|
281
279
|
/**
|
|
282
280
|
* Wait for initialization to complete.
|
|
283
281
|
* While initializing is automatic, this helps to catch and report initialization errors.
|
|
@@ -617,7 +615,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
617
615
|
* @param options Options for configuring watch behavior
|
|
618
616
|
*/
|
|
619
617
|
watchWithCallback(sql, parameters, handler, options) {
|
|
620
|
-
const { onResult, onError = (e) => this.
|
|
618
|
+
const { onResult, onError = (e) => this.logger.error(e) } = handler ?? {};
|
|
621
619
|
if (!onResult) {
|
|
622
620
|
throw new Error('onResult is required');
|
|
623
621
|
}
|
|
@@ -723,7 +721,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
723
721
|
* @returns A dispose function to stop watching for changes
|
|
724
722
|
*/
|
|
725
723
|
onChangeWithCallback(handler, options) {
|
|
726
|
-
const { onChange, onError = (e) => this.
|
|
724
|
+
const { onChange, onError = (e) => this.logger.error(e) } = handler ?? {};
|
|
727
725
|
if (!onChange) {
|
|
728
726
|
throw new Error('onChange is required');
|
|
729
727
|
}
|
|
@@ -136,7 +136,6 @@ export class ConnectionManager extends BaseObserver {
|
|
|
136
136
|
await this.disconnectingPromise;
|
|
137
137
|
this.logger.debug('Attempting to connect to PowerSync instance');
|
|
138
138
|
await this.syncStreamImplementation?.connect(appliedOptions);
|
|
139
|
-
this.syncStreamImplementation?.triggerCrudUpload();
|
|
140
139
|
}
|
|
141
140
|
/**
|
|
142
141
|
* Close the sync connection.
|
|
@@ -90,5 +90,5 @@ export interface BucketStorageAdapter extends BaseObserver<BucketStorageListener
|
|
|
90
90
|
/**
|
|
91
91
|
* Invokes the `powersync_control` function for the sync client.
|
|
92
92
|
*/
|
|
93
|
-
control(op: PowerSyncControlCommand, payload: string |
|
|
93
|
+
control(op: PowerSyncControlCommand, payload: string | Uint8Array | null): Promise<string>;
|
|
94
94
|
}
|
|
@@ -56,7 +56,7 @@ export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListe
|
|
|
56
56
|
* Set a target checkpoint.
|
|
57
57
|
*/
|
|
58
58
|
setTargetCheckpoint(checkpoint: Checkpoint): Promise<void>;
|
|
59
|
-
control(op: PowerSyncControlCommand, payload: string | ArrayBuffer | null): Promise<string>;
|
|
59
|
+
control(op: PowerSyncControlCommand, payload: string | Uint8Array | ArrayBuffer | null): Promise<string>;
|
|
60
60
|
hasMigratedSubkeys(): Promise<boolean>;
|
|
61
61
|
migrateToFixedSubkeys(): Promise<void>;
|
|
62
62
|
static _subkeyMigrationKey: string;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { BSON } from 'bson';
|
|
2
|
-
import { Buffer } from 'buffer';
|
|
3
2
|
import { type fetch } from 'cross-fetch';
|
|
4
3
|
import Logger, { ILogger } from 'js-logger';
|
|
5
4
|
import { DataStream } from '../../../utils/DataStream.js';
|
|
@@ -133,7 +132,7 @@ export declare abstract class AbstractRemote {
|
|
|
133
132
|
* @param bson A BSON encoder and decoder. When set, the data stream will be requested with a BSON payload
|
|
134
133
|
* (required for compatibility with older sync services).
|
|
135
134
|
*/
|
|
136
|
-
socketStreamRaw<T>(options: SocketSyncStreamOptions, map: (buffer:
|
|
135
|
+
socketStreamRaw<T>(options: SocketSyncStreamOptions, map: (buffer: Uint8Array) => T, bson?: typeof BSON): Promise<DataStream<T>>;
|
|
137
136
|
/**
|
|
138
137
|
* Connects to the sync/stream http endpoint, parsing lines as JSON.
|
|
139
138
|
*/
|
|
@@ -10,8 +10,12 @@ const POWERSYNC_JS_VERSION = PACKAGE.version;
|
|
|
10
10
|
const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
|
|
11
11
|
// Keep alive message is sent every period
|
|
12
12
|
const KEEP_ALIVE_MS = 20_000;
|
|
13
|
-
//
|
|
14
|
-
const
|
|
13
|
+
// One message of any type must be received in this period.
|
|
14
|
+
const SOCKET_TIMEOUT_MS = 30_000;
|
|
15
|
+
// One keepalive message must be received in this period.
|
|
16
|
+
// If there is a backlog of messages (for example on slow connections), keepalive messages could be delayed
|
|
17
|
+
// significantly. Therefore this is longer than the socket timeout.
|
|
18
|
+
const KEEP_ALIVE_LIFETIME_MS = 90_000;
|
|
15
19
|
export const DEFAULT_REMOTE_LOGGER = Logger.get('PowerSyncRemote');
|
|
16
20
|
export var FetchStrategy;
|
|
17
21
|
(function (FetchStrategy) {
|
|
@@ -208,12 +212,25 @@ export class AbstractRemote {
|
|
|
208
212
|
// headers with websockets on web. The browser userAgent is however added
|
|
209
213
|
// automatically as a header.
|
|
210
214
|
const userAgent = this.getUserAgent();
|
|
215
|
+
let keepAliveTimeout;
|
|
216
|
+
const resetTimeout = () => {
|
|
217
|
+
clearTimeout(keepAliveTimeout);
|
|
218
|
+
keepAliveTimeout = setTimeout(() => {
|
|
219
|
+
this.logger.error(`No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.`);
|
|
220
|
+
stream.close();
|
|
221
|
+
}, SOCKET_TIMEOUT_MS);
|
|
222
|
+
};
|
|
223
|
+
resetTimeout();
|
|
211
224
|
const url = this.options.socketUrlTransformer(request.url);
|
|
212
225
|
const connector = new RSocketConnector({
|
|
213
226
|
transport: new WebsocketClientTransport({
|
|
214
227
|
url,
|
|
215
228
|
wsCreator: (url) => {
|
|
216
|
-
|
|
229
|
+
const socket = this.createSocket(url);
|
|
230
|
+
socket.addEventListener('message', (event) => {
|
|
231
|
+
resetTimeout();
|
|
232
|
+
});
|
|
233
|
+
return socket;
|
|
217
234
|
}
|
|
218
235
|
}),
|
|
219
236
|
setup: {
|
|
@@ -236,16 +253,20 @@ export class AbstractRemote {
|
|
|
236
253
|
}
|
|
237
254
|
catch (ex) {
|
|
238
255
|
this.logger.error(`Failed to connect WebSocket`, ex);
|
|
256
|
+
clearTimeout(keepAliveTimeout);
|
|
239
257
|
throw ex;
|
|
240
258
|
}
|
|
259
|
+
resetTimeout();
|
|
241
260
|
const stream = new DataStream({
|
|
242
261
|
logger: this.logger,
|
|
243
262
|
pressure: {
|
|
244
263
|
lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
|
|
245
|
-
}
|
|
264
|
+
},
|
|
265
|
+
mapLine: map
|
|
246
266
|
});
|
|
247
267
|
let socketIsClosed = false;
|
|
248
268
|
const closeSocket = () => {
|
|
269
|
+
clearTimeout(keepAliveTimeout);
|
|
249
270
|
if (socketIsClosed) {
|
|
250
271
|
return;
|
|
251
272
|
}
|
|
@@ -307,7 +328,7 @@ export class AbstractRemote {
|
|
|
307
328
|
if (!data) {
|
|
308
329
|
return;
|
|
309
330
|
}
|
|
310
|
-
stream.enqueueData(
|
|
331
|
+
stream.enqueueData(data);
|
|
311
332
|
},
|
|
312
333
|
onComplete: () => {
|
|
313
334
|
stream.close();
|
|
@@ -418,7 +439,8 @@ export class AbstractRemote {
|
|
|
418
439
|
const decoder = new TextDecoder();
|
|
419
440
|
let buffer = '';
|
|
420
441
|
const stream = new DataStream({
|
|
421
|
-
logger: this.logger
|
|
442
|
+
logger: this.logger,
|
|
443
|
+
mapLine: mapLine
|
|
422
444
|
});
|
|
423
445
|
const l = stream.registerListener({
|
|
424
446
|
lowWater: async () => {
|
|
@@ -429,7 +451,7 @@ export class AbstractRemote {
|
|
|
429
451
|
if (done) {
|
|
430
452
|
const remaining = buffer.trim();
|
|
431
453
|
if (remaining.length != 0) {
|
|
432
|
-
stream.enqueueData(
|
|
454
|
+
stream.enqueueData(remaining);
|
|
433
455
|
}
|
|
434
456
|
stream.close();
|
|
435
457
|
await closeReader();
|
|
@@ -441,7 +463,7 @@ export class AbstractRemote {
|
|
|
441
463
|
for (var i = 0; i < lines.length - 1; i++) {
|
|
442
464
|
var l = lines[i].trim();
|
|
443
465
|
if (l.length > 0) {
|
|
444
|
-
stream.enqueueData(
|
|
466
|
+
stream.enqueueData(l);
|
|
445
467
|
didCompleteLine = true;
|
|
446
468
|
}
|
|
447
469
|
}
|
|
@@ -169,9 +169,12 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
169
169
|
}
|
|
170
170
|
async getWriteCheckpoint() {
|
|
171
171
|
const clientId = await this.options.adapter.getClientId();
|
|
172
|
+
this.logger.debug(`Creating write checkpoint for ${clientId}`);
|
|
172
173
|
let path = `/write-checkpoint2.json?client_id=${clientId}`;
|
|
173
174
|
const response = await this.options.remote.get(path);
|
|
174
|
-
|
|
175
|
+
const checkpoint = response['data']['write_checkpoint'];
|
|
176
|
+
this.logger.debug(`Got write checkpoint: ${checkpoint}`);
|
|
177
|
+
return checkpoint;
|
|
175
178
|
}
|
|
176
179
|
async _uploadAllCrud() {
|
|
177
180
|
return this.obtainLock({
|
|
@@ -182,17 +185,17 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
182
185
|
*/
|
|
183
186
|
let checkedCrudItem;
|
|
184
187
|
while (true) {
|
|
185
|
-
this.updateSyncStatus({
|
|
186
|
-
dataFlow: {
|
|
187
|
-
uploading: true
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
188
|
try {
|
|
191
189
|
/**
|
|
192
190
|
* This is the first item in the FIFO CRUD queue.
|
|
193
191
|
*/
|
|
194
192
|
const nextCrudItem = await this.options.adapter.nextCrudItem();
|
|
195
193
|
if (nextCrudItem) {
|
|
194
|
+
this.updateSyncStatus({
|
|
195
|
+
dataFlow: {
|
|
196
|
+
uploading: true
|
|
197
|
+
}
|
|
198
|
+
});
|
|
196
199
|
if (nextCrudItem.clientId == checkedCrudItem?.clientId) {
|
|
197
200
|
// This will force a higher log level than exceptions which are caught here.
|
|
198
201
|
this.logger.warn(`Potentially previously uploaded CRUD entries are still present in the upload queue.
|
|
@@ -210,6 +213,7 @@ The next upload iteration will be delayed.`);
|
|
|
210
213
|
}
|
|
211
214
|
else {
|
|
212
215
|
// Uploading is completed
|
|
216
|
+
this.logger.debug('Upload complete, updating write checkpoint');
|
|
213
217
|
await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
|
|
214
218
|
break;
|
|
215
219
|
}
|
|
@@ -247,23 +251,17 @@ The next upload iteration will be delayed.`);
|
|
|
247
251
|
const controller = new AbortController();
|
|
248
252
|
this.abortController = controller;
|
|
249
253
|
this.streamingSyncPromise = this.streamingSync(this.abortController.signal, options);
|
|
250
|
-
// Return a promise that resolves when the connection status is updated
|
|
254
|
+
// Return a promise that resolves when the connection status is updated to indicate that we're connected.
|
|
251
255
|
return new Promise((resolve) => {
|
|
252
256
|
const disposer = this.registerListener({
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (typeof update.connected == 'undefined') {
|
|
256
|
-
// only concern with connection updates
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
if (update.connected == false) {
|
|
260
|
-
/**
|
|
261
|
-
* This function does not reject if initial connect attempt failed.
|
|
262
|
-
* Connected can be false if the connection attempt was aborted or if the initial connection
|
|
263
|
-
* attempt failed.
|
|
264
|
-
*/
|
|
257
|
+
statusChanged: (status) => {
|
|
258
|
+
if (status.dataFlowStatus.downloadError != null) {
|
|
265
259
|
this.logger.warn('Initial connect attempt did not successfully connect to server');
|
|
266
260
|
}
|
|
261
|
+
else if (status.connecting) {
|
|
262
|
+
// Still connecting.
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
267
265
|
disposer();
|
|
268
266
|
resolve();
|
|
269
267
|
}
|
|
@@ -655,6 +653,7 @@ The next upload iteration will be delayed.`);
|
|
|
655
653
|
const adapter = this.options.adapter;
|
|
656
654
|
const remote = this.options.remote;
|
|
657
655
|
let receivingLines = null;
|
|
656
|
+
let hadSyncLine = false;
|
|
658
657
|
const abortController = new AbortController();
|
|
659
658
|
signal.addEventListener('abort', () => abortController.abort());
|
|
660
659
|
// Pending sync lines received from the service, as well as local events that trigger a powersync_control
|
|
@@ -668,20 +667,39 @@ The next upload iteration will be delayed.`);
|
|
|
668
667
|
data: instr.request
|
|
669
668
|
};
|
|
670
669
|
if (resolvedOptions.connectionMethod == SyncStreamConnectionMethod.HTTP) {
|
|
671
|
-
controlInvocations = await remote.postStreamRaw(syncOptions, (line) =>
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
670
|
+
controlInvocations = await remote.postStreamRaw(syncOptions, (line) => {
|
|
671
|
+
if (typeof line == 'string') {
|
|
672
|
+
return {
|
|
673
|
+
command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
|
|
674
|
+
payload: line
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
// Directly enqueued by us
|
|
679
|
+
return line;
|
|
680
|
+
}
|
|
681
|
+
});
|
|
675
682
|
}
|
|
676
683
|
else {
|
|
677
684
|
controlInvocations = await remote.socketStreamRaw({
|
|
678
685
|
...syncOptions,
|
|
679
686
|
fetchStrategy: resolvedOptions.fetchStrategy
|
|
680
|
-
}, (
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
687
|
+
}, (payload) => {
|
|
688
|
+
if (payload instanceof Uint8Array) {
|
|
689
|
+
return {
|
|
690
|
+
command: PowerSyncControlCommand.PROCESS_BSON_LINE,
|
|
691
|
+
payload: payload
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
// Directly enqueued by us
|
|
696
|
+
return payload;
|
|
697
|
+
}
|
|
698
|
+
});
|
|
684
699
|
}
|
|
700
|
+
// The rust client will set connected: true after the first sync line because that's when it gets invoked, but
|
|
701
|
+
// we're already connected here and can report that.
|
|
702
|
+
syncImplementation.updateSyncStatus({ connected: true });
|
|
685
703
|
try {
|
|
686
704
|
while (!controlInvocations.closed) {
|
|
687
705
|
const line = await controlInvocations.read();
|
|
@@ -689,6 +707,10 @@ The next upload iteration will be delayed.`);
|
|
|
689
707
|
return;
|
|
690
708
|
}
|
|
691
709
|
await control(line.command, line.payload);
|
|
710
|
+
if (!hadSyncLine) {
|
|
711
|
+
syncImplementation.triggerCrudUpload();
|
|
712
|
+
hadSyncLine = true;
|
|
713
|
+
}
|
|
692
714
|
}
|
|
693
715
|
}
|
|
694
716
|
finally {
|
|
@@ -726,7 +748,7 @@ The next upload iteration will be delayed.`);
|
|
|
726
748
|
return {
|
|
727
749
|
priority: status.priority,
|
|
728
750
|
hasSynced: status.has_synced ?? undefined,
|
|
729
|
-
lastSyncedAt: status?.last_synced_at != null ? new Date(status.last_synced_at) : undefined
|
|
751
|
+
lastSyncedAt: status?.last_synced_at != null ? new Date(status.last_synced_at * 1000) : undefined
|
|
730
752
|
};
|
|
731
753
|
}
|
|
732
754
|
const info = instruction.UpdateSyncStatus.status;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ILogger } from 'js-logger';
|
|
2
2
|
import { BaseListener, BaseObserver } from './BaseObserver.js';
|
|
3
|
-
export type DataStreamOptions = {
|
|
3
|
+
export type DataStreamOptions<ParsedData, SourceData> = {
|
|
4
|
+
mapLine?: (line: SourceData) => ParsedData;
|
|
4
5
|
/**
|
|
5
6
|
* Close the stream if any consumer throws an error
|
|
6
7
|
*/
|
|
@@ -28,13 +29,15 @@ export declare const DEFAULT_PRESSURE_LIMITS: {
|
|
|
28
29
|
* native JS streams or async iterators.
|
|
29
30
|
* This is handy for environments such as React Native which need polyfills for the above.
|
|
30
31
|
*/
|
|
31
|
-
export declare class DataStream<
|
|
32
|
-
protected options?: DataStreamOptions | undefined;
|
|
33
|
-
dataQueue:
|
|
32
|
+
export declare class DataStream<ParsedData, SourceData = any> extends BaseObserver<DataStreamListener<ParsedData>> {
|
|
33
|
+
protected options?: DataStreamOptions<ParsedData, SourceData> | undefined;
|
|
34
|
+
dataQueue: SourceData[];
|
|
34
35
|
protected isClosed: boolean;
|
|
35
36
|
protected processingPromise: Promise<void> | null;
|
|
37
|
+
protected notifyDataAdded: (() => void) | null;
|
|
36
38
|
protected logger: ILogger;
|
|
37
|
-
|
|
39
|
+
protected mapLine: (line: SourceData) => ParsedData;
|
|
40
|
+
constructor(options?: DataStreamOptions<ParsedData, SourceData> | undefined);
|
|
38
41
|
get highWatermark(): number;
|
|
39
42
|
get lowWatermark(): number;
|
|
40
43
|
get closed(): boolean;
|
|
@@ -42,22 +45,18 @@ export declare class DataStream<Data extends any = any> extends BaseObserver<Dat
|
|
|
42
45
|
/**
|
|
43
46
|
* Enqueues data for the consumers to read
|
|
44
47
|
*/
|
|
45
|
-
enqueueData(data:
|
|
48
|
+
enqueueData(data: SourceData): void;
|
|
46
49
|
/**
|
|
47
50
|
* Reads data once from the data stream
|
|
48
51
|
* @returns a Data payload or Null if the stream closed.
|
|
49
52
|
*/
|
|
50
|
-
read(): Promise<
|
|
53
|
+
read(): Promise<ParsedData | null>;
|
|
51
54
|
/**
|
|
52
55
|
* Executes a callback for each data item in the stream
|
|
53
56
|
*/
|
|
54
|
-
forEach(callback: DataStreamCallback<
|
|
55
|
-
protected processQueue(): Promise<void
|
|
56
|
-
/**
|
|
57
|
-
* Creates a new data stream which is a map of the original
|
|
58
|
-
*/
|
|
59
|
-
map<ReturnData>(callback: (data: Data) => ReturnData): DataStream<ReturnData>;
|
|
57
|
+
forEach(callback: DataStreamCallback<ParsedData>): () => void;
|
|
58
|
+
protected processQueue(): Promise<void> | undefined;
|
|
60
59
|
protected hasDataReader(): boolean;
|
|
61
60
|
protected _processQueue(): Promise<void>;
|
|
62
|
-
protected iterateAsyncErrored(cb: (l:
|
|
61
|
+
protected iterateAsyncErrored(cb: (l: Partial<DataStreamListener<ParsedData>>) => Promise<void>): Promise<void>;
|
|
63
62
|
}
|
package/lib/utils/DataStream.js
CHANGED
|
@@ -14,13 +14,16 @@ export class DataStream extends BaseObserver {
|
|
|
14
14
|
dataQueue;
|
|
15
15
|
isClosed;
|
|
16
16
|
processingPromise;
|
|
17
|
+
notifyDataAdded;
|
|
17
18
|
logger;
|
|
19
|
+
mapLine;
|
|
18
20
|
constructor(options) {
|
|
19
21
|
super();
|
|
20
22
|
this.options = options;
|
|
21
23
|
this.processingPromise = null;
|
|
22
24
|
this.isClosed = false;
|
|
23
25
|
this.dataQueue = [];
|
|
26
|
+
this.mapLine = options?.mapLine ?? ((line) => line);
|
|
24
27
|
this.logger = options?.logger ?? Logger.get('DataStream');
|
|
25
28
|
if (options?.closeOnError) {
|
|
26
29
|
const l = this.registerListener({
|
|
@@ -56,6 +59,7 @@ export class DataStream extends BaseObserver {
|
|
|
56
59
|
throw new Error('Cannot enqueue data into closed stream.');
|
|
57
60
|
}
|
|
58
61
|
this.dataQueue.push(data);
|
|
62
|
+
this.notifyDataAdded?.();
|
|
59
63
|
this.processQueue();
|
|
60
64
|
}
|
|
61
65
|
/**
|
|
@@ -96,10 +100,20 @@ export class DataStream extends BaseObserver {
|
|
|
96
100
|
data: callback
|
|
97
101
|
});
|
|
98
102
|
}
|
|
99
|
-
|
|
103
|
+
processQueue() {
|
|
100
104
|
if (this.processingPromise) {
|
|
101
105
|
return;
|
|
102
106
|
}
|
|
107
|
+
const promise = (this.processingPromise = this._processQueue());
|
|
108
|
+
promise.finally(() => {
|
|
109
|
+
return (this.processingPromise = null);
|
|
110
|
+
});
|
|
111
|
+
return promise;
|
|
112
|
+
}
|
|
113
|
+
hasDataReader() {
|
|
114
|
+
return Array.from(this.listeners.values()).some((l) => !!l.data);
|
|
115
|
+
}
|
|
116
|
+
async _processQueue() {
|
|
103
117
|
/**
|
|
104
118
|
* Allow listeners to mutate the queue before processing.
|
|
105
119
|
* This allows for operations such as dropping or compressing data
|
|
@@ -108,47 +122,31 @@ export class DataStream extends BaseObserver {
|
|
|
108
122
|
if (this.dataQueue.length >= this.highWatermark) {
|
|
109
123
|
await this.iterateAsyncErrored(async (l) => l.highWater?.());
|
|
110
124
|
}
|
|
111
|
-
return (this.processingPromise = this._processQueue());
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Creates a new data stream which is a map of the original
|
|
115
|
-
*/
|
|
116
|
-
map(callback) {
|
|
117
|
-
const stream = new DataStream(this.options);
|
|
118
|
-
const l = this.registerListener({
|
|
119
|
-
data: async (data) => {
|
|
120
|
-
stream.enqueueData(callback(data));
|
|
121
|
-
},
|
|
122
|
-
closed: () => {
|
|
123
|
-
stream.close();
|
|
124
|
-
l?.();
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
return stream;
|
|
128
|
-
}
|
|
129
|
-
hasDataReader() {
|
|
130
|
-
return Array.from(this.listeners.values()).some((l) => !!l.data);
|
|
131
|
-
}
|
|
132
|
-
async _processQueue() {
|
|
133
125
|
if (this.isClosed || !this.hasDataReader()) {
|
|
134
|
-
Promise.resolve().then(() => (this.processingPromise = null));
|
|
135
126
|
return;
|
|
136
127
|
}
|
|
137
128
|
if (this.dataQueue.length) {
|
|
138
129
|
const data = this.dataQueue.shift();
|
|
139
|
-
|
|
130
|
+
const mapped = this.mapLine(data);
|
|
131
|
+
await this.iterateAsyncErrored(async (l) => l.data?.(mapped));
|
|
140
132
|
}
|
|
141
133
|
if (this.dataQueue.length <= this.lowWatermark) {
|
|
142
|
-
|
|
134
|
+
const dataAdded = new Promise((resolve) => {
|
|
135
|
+
this.notifyDataAdded = resolve;
|
|
136
|
+
});
|
|
137
|
+
await Promise.race([this.iterateAsyncErrored(async (l) => l.lowWater?.()), dataAdded]);
|
|
138
|
+
this.notifyDataAdded = null;
|
|
143
139
|
}
|
|
144
|
-
this.
|
|
145
|
-
if (this.dataQueue.length) {
|
|
140
|
+
if (this.dataQueue.length > 0) {
|
|
146
141
|
// Next tick
|
|
147
142
|
setTimeout(() => this.processQueue());
|
|
148
143
|
}
|
|
149
144
|
}
|
|
150
145
|
async iterateAsyncErrored(cb) {
|
|
151
|
-
|
|
146
|
+
// Important: We need to copy the listeners, as calling a listener could result in adding another
|
|
147
|
+
// listener, resulting in infinite loops.
|
|
148
|
+
const listeners = Array.from(this.listeners.values());
|
|
149
|
+
for (let i of listeners) {
|
|
152
150
|
try {
|
|
153
151
|
await cb(i);
|
|
154
152
|
}
|