@powersync/common 1.33.0 → 1.33.1
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 +22 -0
- package/dist/bundle.mjs +3 -3
- package/lib/client/ConnectionManager.js +0 -1
- package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +1 -9
- package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +1 -13
- package/lib/client/sync/bucket/SqliteBucketStorage.js +0 -42
- 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 +45 -27
- package/lib/utils/DataStream.d.ts +13 -14
- package/lib/utils/DataStream.js +27 -29
- package/package.json +4 -3
|
@@ -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.
|
|
@@ -82,14 +82,6 @@ export interface BucketStorageAdapter extends BaseObserver<BucketStorageListener
|
|
|
82
82
|
getCrudBatch(limit?: number): Promise<CrudBatch | null>;
|
|
83
83
|
hasCompletedSync(): Promise<boolean>;
|
|
84
84
|
updateLocalTarget(cb: () => Promise<string>): Promise<boolean>;
|
|
85
|
-
/**
|
|
86
|
-
* Exposed for tests only.
|
|
87
|
-
*/
|
|
88
|
-
autoCompact(): Promise<void>;
|
|
89
|
-
/**
|
|
90
|
-
* Exposed for tests only.
|
|
91
|
-
*/
|
|
92
|
-
forceCompact(): Promise<void>;
|
|
93
85
|
getMaxOpId(): string;
|
|
94
86
|
/**
|
|
95
87
|
* Get an unique client id.
|
|
@@ -98,5 +90,5 @@ export interface BucketStorageAdapter extends BaseObserver<BucketStorageListener
|
|
|
98
90
|
/**
|
|
99
91
|
* Invokes the `powersync_control` function for the sync client.
|
|
100
92
|
*/
|
|
101
|
-
control(op: PowerSyncControlCommand, payload: string |
|
|
93
|
+
control(op: PowerSyncControlCommand, payload: string | Uint8Array | null): Promise<string>;
|
|
102
94
|
}
|
|
@@ -11,14 +11,9 @@ export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListe
|
|
|
11
11
|
private mutex;
|
|
12
12
|
private logger;
|
|
13
13
|
tableNames: Set<string>;
|
|
14
|
-
private pendingBucketDeletes;
|
|
15
14
|
private _hasCompletedSync;
|
|
16
15
|
private updateListener;
|
|
17
16
|
private _clientId?;
|
|
18
|
-
/**
|
|
19
|
-
* Count up, and do a compact on startup.
|
|
20
|
-
*/
|
|
21
|
-
private compactCounter;
|
|
22
17
|
constructor(db: DBAdapter, mutex: Mutex, logger?: ILogger);
|
|
23
18
|
init(): Promise<void>;
|
|
24
19
|
dispose(): Promise<void>;
|
|
@@ -46,13 +41,6 @@ export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListe
|
|
|
46
41
|
*/
|
|
47
42
|
private updateObjectsFromBuckets;
|
|
48
43
|
validateChecksums(checkpoint: Checkpoint, priority: number | undefined): Promise<SyncLocalDatabaseResult>;
|
|
49
|
-
/**
|
|
50
|
-
* Force a compact, for tests.
|
|
51
|
-
*/
|
|
52
|
-
forceCompact(): Promise<void>;
|
|
53
|
-
autoCompact(): Promise<void>;
|
|
54
|
-
private deletePendingBuckets;
|
|
55
|
-
private clearRemoveOps;
|
|
56
44
|
updateLocalTarget(cb: () => Promise<string>): Promise<boolean>;
|
|
57
45
|
nextCrudItem(): Promise<CrudEntry | undefined>;
|
|
58
46
|
hasCrud(): Promise<boolean>;
|
|
@@ -68,7 +56,7 @@ export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListe
|
|
|
68
56
|
* Set a target checkpoint.
|
|
69
57
|
*/
|
|
70
58
|
setTargetCheckpoint(checkpoint: Checkpoint): Promise<void>;
|
|
71
|
-
control(op: PowerSyncControlCommand, payload: string | ArrayBuffer | null): Promise<string>;
|
|
59
|
+
control(op: PowerSyncControlCommand, payload: string | Uint8Array | ArrayBuffer | null): Promise<string>;
|
|
72
60
|
hasMigratedSubkeys(): Promise<boolean>;
|
|
73
61
|
migrateToFixedSubkeys(): Promise<void>;
|
|
74
62
|
static _subkeyMigrationKey: string;
|
|
@@ -4,27 +4,20 @@ import { BaseObserver } from '../../../utils/BaseObserver.js';
|
|
|
4
4
|
import { MAX_OP_ID } from '../../constants.js';
|
|
5
5
|
import { PSInternalTable } from './BucketStorageAdapter.js';
|
|
6
6
|
import { CrudEntry } from './CrudEntry.js';
|
|
7
|
-
const COMPACT_OPERATION_INTERVAL = 1_000;
|
|
8
7
|
export class SqliteBucketStorage extends BaseObserver {
|
|
9
8
|
db;
|
|
10
9
|
mutex;
|
|
11
10
|
logger;
|
|
12
11
|
tableNames;
|
|
13
|
-
pendingBucketDeletes;
|
|
14
12
|
_hasCompletedSync;
|
|
15
13
|
updateListener;
|
|
16
14
|
_clientId;
|
|
17
|
-
/**
|
|
18
|
-
* Count up, and do a compact on startup.
|
|
19
|
-
*/
|
|
20
|
-
compactCounter = COMPACT_OPERATION_INTERVAL;
|
|
21
15
|
constructor(db, mutex, logger = Logger.get('SqliteBucketStorage')) {
|
|
22
16
|
super();
|
|
23
17
|
this.db = db;
|
|
24
18
|
this.mutex = mutex;
|
|
25
19
|
this.logger = logger;
|
|
26
20
|
this._hasCompletedSync = false;
|
|
27
|
-
this.pendingBucketDeletes = true;
|
|
28
21
|
this.tableNames = new Set();
|
|
29
22
|
this.updateListener = db.registerListener({
|
|
30
23
|
tablesUpdated: (update) => {
|
|
@@ -72,16 +65,13 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
72
65
|
}
|
|
73
66
|
async saveSyncData(batch, fixedKeyFormat = false) {
|
|
74
67
|
await this.writeTransaction(async (tx) => {
|
|
75
|
-
let count = 0;
|
|
76
68
|
for (const b of batch.buckets) {
|
|
77
69
|
const result = await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
|
|
78
70
|
'save',
|
|
79
71
|
JSON.stringify({ buckets: [b.toJSON(fixedKeyFormat)] })
|
|
80
72
|
]);
|
|
81
73
|
this.logger.debug('saveSyncData', JSON.stringify(result));
|
|
82
|
-
count += b.data.length;
|
|
83
74
|
}
|
|
84
|
-
this.compactCounter += count;
|
|
85
75
|
});
|
|
86
76
|
}
|
|
87
77
|
async removeBuckets(buckets) {
|
|
@@ -97,7 +87,6 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
97
87
|
await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', ['delete_bucket', bucket]);
|
|
98
88
|
});
|
|
99
89
|
this.logger.debug('done deleting bucket');
|
|
100
|
-
this.pendingBucketDeletes = true;
|
|
101
90
|
}
|
|
102
91
|
async hasCompletedSync() {
|
|
103
92
|
if (this._hasCompletedSync) {
|
|
@@ -138,7 +127,6 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
138
127
|
this.logger.debug('Not at a consistent checkpoint - cannot update local db');
|
|
139
128
|
return { ready: false, checkpointValid: true };
|
|
140
129
|
}
|
|
141
|
-
await this.forceCompact();
|
|
142
130
|
return {
|
|
143
131
|
ready: true,
|
|
144
132
|
checkpointValid: true
|
|
@@ -209,36 +197,6 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
209
197
|
};
|
|
210
198
|
}
|
|
211
199
|
}
|
|
212
|
-
/**
|
|
213
|
-
* Force a compact, for tests.
|
|
214
|
-
*/
|
|
215
|
-
async forceCompact() {
|
|
216
|
-
this.compactCounter = COMPACT_OPERATION_INTERVAL;
|
|
217
|
-
this.pendingBucketDeletes = true;
|
|
218
|
-
await this.autoCompact();
|
|
219
|
-
}
|
|
220
|
-
async autoCompact() {
|
|
221
|
-
await this.deletePendingBuckets();
|
|
222
|
-
await this.clearRemoveOps();
|
|
223
|
-
}
|
|
224
|
-
async deletePendingBuckets() {
|
|
225
|
-
if (this.pendingBucketDeletes !== false) {
|
|
226
|
-
await this.writeTransaction(async (tx) => {
|
|
227
|
-
await tx.execute('INSERT INTO powersync_operations(op, data) VALUES (?, ?)', ['delete_pending_buckets', '']);
|
|
228
|
-
});
|
|
229
|
-
// Executed once after start-up, and again when there are pending deletes.
|
|
230
|
-
this.pendingBucketDeletes = false;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
async clearRemoveOps() {
|
|
234
|
-
if (this.compactCounter < COMPACT_OPERATION_INTERVAL) {
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
await this.writeTransaction(async (tx) => {
|
|
238
|
-
await tx.execute('INSERT INTO powersync_operations(op, data) VALUES (?, ?)', ['clear_remove_ops', '']);
|
|
239
|
-
});
|
|
240
|
-
this.compactCounter = 0;
|
|
241
|
-
}
|
|
242
200
|
async updateLocalTarget(cb) {
|
|
243
201
|
const rs1 = await this.db.getAll("SELECT target_op FROM ps_buckets WHERE name = '$local' AND target_op = CAST(? as INTEGER)", [MAX_OP_ID]);
|
|
244
202
|
if (!rs1.length) {
|
|
@@ -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
|
}
|
|
@@ -182,17 +182,17 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
182
182
|
*/
|
|
183
183
|
let checkedCrudItem;
|
|
184
184
|
while (true) {
|
|
185
|
-
this.updateSyncStatus({
|
|
186
|
-
dataFlow: {
|
|
187
|
-
uploading: true
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
185
|
try {
|
|
191
186
|
/**
|
|
192
187
|
* This is the first item in the FIFO CRUD queue.
|
|
193
188
|
*/
|
|
194
189
|
const nextCrudItem = await this.options.adapter.nextCrudItem();
|
|
195
190
|
if (nextCrudItem) {
|
|
191
|
+
this.updateSyncStatus({
|
|
192
|
+
dataFlow: {
|
|
193
|
+
uploading: true
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
196
|
if (nextCrudItem.clientId == checkedCrudItem?.clientId) {
|
|
197
197
|
// This will force a higher log level than exceptions which are caught here.
|
|
198
198
|
this.logger.warn(`Potentially previously uploaded CRUD entries are still present in the upload queue.
|
|
@@ -247,23 +247,17 @@ The next upload iteration will be delayed.`);
|
|
|
247
247
|
const controller = new AbortController();
|
|
248
248
|
this.abortController = controller;
|
|
249
249
|
this.streamingSyncPromise = this.streamingSync(this.abortController.signal, options);
|
|
250
|
-
// Return a promise that resolves when the connection status is updated
|
|
250
|
+
// Return a promise that resolves when the connection status is updated to indicate that we're connected.
|
|
251
251
|
return new Promise((resolve) => {
|
|
252
252
|
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
|
-
*/
|
|
253
|
+
statusChanged: (status) => {
|
|
254
|
+
if (status.dataFlowStatus.downloadError != null) {
|
|
265
255
|
this.logger.warn('Initial connect attempt did not successfully connect to server');
|
|
266
256
|
}
|
|
257
|
+
else if (status.connecting) {
|
|
258
|
+
// Still connecting.
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
267
261
|
disposer();
|
|
268
262
|
resolve();
|
|
269
263
|
}
|
|
@@ -655,6 +649,7 @@ The next upload iteration will be delayed.`);
|
|
|
655
649
|
const adapter = this.options.adapter;
|
|
656
650
|
const remote = this.options.remote;
|
|
657
651
|
let receivingLines = null;
|
|
652
|
+
let hadSyncLine = false;
|
|
658
653
|
const abortController = new AbortController();
|
|
659
654
|
signal.addEventListener('abort', () => abortController.abort());
|
|
660
655
|
// Pending sync lines received from the service, as well as local events that trigger a powersync_control
|
|
@@ -668,20 +663,39 @@ The next upload iteration will be delayed.`);
|
|
|
668
663
|
data: instr.request
|
|
669
664
|
};
|
|
670
665
|
if (resolvedOptions.connectionMethod == SyncStreamConnectionMethod.HTTP) {
|
|
671
|
-
controlInvocations = await remote.postStreamRaw(syncOptions, (line) =>
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
666
|
+
controlInvocations = await remote.postStreamRaw(syncOptions, (line) => {
|
|
667
|
+
if (typeof line == 'string') {
|
|
668
|
+
return {
|
|
669
|
+
command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
|
|
670
|
+
payload: line
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
// Directly enqueued by us
|
|
675
|
+
return line;
|
|
676
|
+
}
|
|
677
|
+
});
|
|
675
678
|
}
|
|
676
679
|
else {
|
|
677
680
|
controlInvocations = await remote.socketStreamRaw({
|
|
678
681
|
...syncOptions,
|
|
679
682
|
fetchStrategy: resolvedOptions.fetchStrategy
|
|
680
|
-
}, (
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
683
|
+
}, (payload) => {
|
|
684
|
+
if (payload instanceof Uint8Array) {
|
|
685
|
+
return {
|
|
686
|
+
command: PowerSyncControlCommand.PROCESS_BSON_LINE,
|
|
687
|
+
payload: payload
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
// Directly enqueued by us
|
|
692
|
+
return payload;
|
|
693
|
+
}
|
|
694
|
+
});
|
|
684
695
|
}
|
|
696
|
+
// The rust client will set connected: true after the first sync line because that's when it gets invoked, but
|
|
697
|
+
// we're already connected here and can report that.
|
|
698
|
+
syncImplementation.updateSyncStatus({ connected: true });
|
|
685
699
|
try {
|
|
686
700
|
while (!controlInvocations.closed) {
|
|
687
701
|
const line = await controlInvocations.read();
|
|
@@ -689,6 +703,10 @@ The next upload iteration will be delayed.`);
|
|
|
689
703
|
return;
|
|
690
704
|
}
|
|
691
705
|
await control(line.command, line.payload);
|
|
706
|
+
if (!hadSyncLine) {
|
|
707
|
+
syncImplementation.triggerCrudUpload();
|
|
708
|
+
hadSyncLine = true;
|
|
709
|
+
}
|
|
692
710
|
}
|
|
693
711
|
}
|
|
694
712
|
finally {
|
|
@@ -726,7 +744,7 @@ The next upload iteration will be delayed.`);
|
|
|
726
744
|
return {
|
|
727
745
|
priority: status.priority,
|
|
728
746
|
hasSynced: status.has_synced ?? undefined,
|
|
729
|
-
lastSyncedAt: status?.last_synced_at != null ? new Date(status.last_synced_at) : undefined
|
|
747
|
+
lastSyncedAt: status?.last_synced_at != null ? new Date(status.last_synced_at * 1000) : undefined
|
|
730
748
|
};
|
|
731
749
|
}
|
|
732
750
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powersync/common",
|
|
3
|
-
"version": "1.33.
|
|
3
|
+
"version": "1.33.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/",
|
|
6
6
|
"access": "public"
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
"exports": {
|
|
14
14
|
".": {
|
|
15
15
|
"import": "./dist/bundle.mjs",
|
|
16
|
-
"
|
|
17
|
-
"types": "./lib/index.d.ts"
|
|
16
|
+
"require": "./dist/bundle.cjs",
|
|
17
|
+
"types": "./lib/index.d.ts",
|
|
18
|
+
"default": "./dist/bundle.mjs"
|
|
18
19
|
}
|
|
19
20
|
},
|
|
20
21
|
"author": "JOURNEYAPPS",
|