@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
package/dist/bundle.cjs
CHANGED
|
@@ -1398,6 +1398,8 @@ exports.EncodingType = void 0;
|
|
|
1398
1398
|
EncodingType["Base64"] = "base64";
|
|
1399
1399
|
})(exports.EncodingType || (exports.EncodingType = {}));
|
|
1400
1400
|
|
|
1401
|
+
const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
|
|
1402
|
+
|
|
1401
1403
|
function getDefaultExportFromCjs (x) {
|
|
1402
1404
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
1403
1405
|
}
|
|
@@ -1478,7 +1480,7 @@ function requireEventIterator () {
|
|
|
1478
1480
|
this.removeCallback();
|
|
1479
1481
|
});
|
|
1480
1482
|
}
|
|
1481
|
-
[
|
|
1483
|
+
[symbolAsyncIterator]() {
|
|
1482
1484
|
return {
|
|
1483
1485
|
next: (value) => {
|
|
1484
1486
|
const result = this.pushQueue.shift();
|
|
@@ -1525,7 +1527,7 @@ function requireEventIterator () {
|
|
|
1525
1527
|
queue.eventHandlers[event] = fn;
|
|
1526
1528
|
},
|
|
1527
1529
|
}) || (() => { });
|
|
1528
|
-
this[
|
|
1530
|
+
this[symbolAsyncIterator] = () => queue[symbolAsyncIterator]();
|
|
1529
1531
|
Object.freeze(this);
|
|
1530
1532
|
}
|
|
1531
1533
|
}
|
|
@@ -1969,8 +1971,15 @@ class BaseTransaction {
|
|
|
1969
1971
|
class TransactionImplementation extends DBGetUtilsDefaultMixin(BaseTransaction) {
|
|
1970
1972
|
static async runWith(ctx, fn) {
|
|
1971
1973
|
let tx = new TransactionImplementation(ctx);
|
|
1974
|
+
// For write transactions, use BEGIN IMMEDIATE to immediately obtain a write lock on the database (instead of doing
|
|
1975
|
+
// that on the first statement). If we have a genuine read-only connection, we also use BEGIN IMMEDIATE there: In
|
|
1976
|
+
// WAL mode, that ensures we pin the current state of the database (instead of the state at the first statement in
|
|
1977
|
+
// the transaction). But if we have a "fake" read-only connection implemented through `pragma query_only = true`, we
|
|
1978
|
+
// can't use this trick because it would attempt to lock the connection. So there, we use a regular `BEGIN`
|
|
1979
|
+
// statement.
|
|
1980
|
+
const useBeginImmediate = ctx.connectionType != 'queryOnly';
|
|
1972
1981
|
try {
|
|
1973
|
-
await ctx.execute('BEGIN IMMEDIATE');
|
|
1982
|
+
await ctx.execute(useBeginImmediate ? 'BEGIN IMMEDIATE' : 'BEGIN');
|
|
1974
1983
|
const result = await fn(tx);
|
|
1975
1984
|
await tx.commit();
|
|
1976
1985
|
return result;
|
|
@@ -2129,16 +2138,12 @@ class SyncStatus {
|
|
|
2129
2138
|
*
|
|
2130
2139
|
* This returns null when the database is currently being opened and we don't have reliable information about all
|
|
2131
2140
|
* included streams yet.
|
|
2132
|
-
*
|
|
2133
|
-
* @experimental Sync streams are currently in alpha.
|
|
2134
2141
|
*/
|
|
2135
2142
|
get syncStreams() {
|
|
2136
2143
|
return this.options.dataFlow?.internalStreamSubscriptions?.map((core) => new SyncStreamStatusView(this, core));
|
|
2137
2144
|
}
|
|
2138
2145
|
/**
|
|
2139
2146
|
* If the `stream` appears in {@link syncStreams}, returns the current status for that stream.
|
|
2140
|
-
*
|
|
2141
|
-
* @experimental Sync streams are currently in alpha.
|
|
2142
2147
|
*/
|
|
2143
2148
|
forStream(stream) {
|
|
2144
2149
|
const asJson = JSON.stringify(stream.parameters);
|
|
@@ -2407,15 +2412,6 @@ class ControlledExecutor {
|
|
|
2407
2412
|
}
|
|
2408
2413
|
}
|
|
2409
2414
|
|
|
2410
|
-
/**
|
|
2411
|
-
* A ponyfill for `Symbol.asyncIterator` that is compatible with the
|
|
2412
|
-
* [recommended polyfill](https://github.com/Azure/azure-sdk-for-js/blob/%40azure/core-asynciterator-polyfill_1.0.2/sdk/core/core-asynciterator-polyfill/src/index.ts#L4-L6)
|
|
2413
|
-
* we recommend for React Native.
|
|
2414
|
-
*
|
|
2415
|
-
* As long as we use this symbol (instead of `for await` and `async *`) in this package, we can be compatible with async
|
|
2416
|
-
* iterators without requiring them.
|
|
2417
|
-
*/
|
|
2418
|
-
const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
|
|
2419
2415
|
/**
|
|
2420
2416
|
* Throttle a function to be called at most once every "wait" milliseconds,
|
|
2421
2417
|
* on the trailing edge.
|
|
@@ -2730,7 +2726,7 @@ class SyncStreamSubscriptionHandle {
|
|
|
2730
2726
|
constructor(subscription) {
|
|
2731
2727
|
this.subscription = subscription;
|
|
2732
2728
|
subscription.refcount++;
|
|
2733
|
-
_finalizer?.register(this, subscription);
|
|
2729
|
+
_finalizer?.register(this, subscription, this);
|
|
2734
2730
|
}
|
|
2735
2731
|
get name() {
|
|
2736
2732
|
return this.subscription.name;
|
|
@@ -3319,6 +3315,10 @@ exports.PowerSyncControlCommand = void 0;
|
|
|
3319
3315
|
PowerSyncControlCommand["NOTIFY_TOKEN_REFRESHED"] = "refreshed_token";
|
|
3320
3316
|
PowerSyncControlCommand["NOTIFY_CRUD_UPLOAD_COMPLETED"] = "completed_upload";
|
|
3321
3317
|
PowerSyncControlCommand["UPDATE_SUBSCRIPTIONS"] = "update_subscriptions";
|
|
3318
|
+
/**
|
|
3319
|
+
* An `established` or `end` event for response streams.
|
|
3320
|
+
*/
|
|
3321
|
+
PowerSyncControlCommand["CONNECTION_STATE"] = "connection";
|
|
3322
3322
|
})(exports.PowerSyncControlCommand || (exports.PowerSyncControlCommand = {}));
|
|
3323
3323
|
|
|
3324
3324
|
/**
|
|
@@ -3500,103 +3500,6 @@ class AbortOperation extends Error {
|
|
|
3500
3500
|
}
|
|
3501
3501
|
}
|
|
3502
3502
|
|
|
3503
|
-
exports.OpTypeEnum = void 0;
|
|
3504
|
-
(function (OpTypeEnum) {
|
|
3505
|
-
OpTypeEnum[OpTypeEnum["CLEAR"] = 1] = "CLEAR";
|
|
3506
|
-
OpTypeEnum[OpTypeEnum["MOVE"] = 2] = "MOVE";
|
|
3507
|
-
OpTypeEnum[OpTypeEnum["PUT"] = 3] = "PUT";
|
|
3508
|
-
OpTypeEnum[OpTypeEnum["REMOVE"] = 4] = "REMOVE";
|
|
3509
|
-
})(exports.OpTypeEnum || (exports.OpTypeEnum = {}));
|
|
3510
|
-
/**
|
|
3511
|
-
* Used internally for sync buckets.
|
|
3512
|
-
*/
|
|
3513
|
-
class OpType {
|
|
3514
|
-
value;
|
|
3515
|
-
static fromJSON(jsonValue) {
|
|
3516
|
-
return new OpType(exports.OpTypeEnum[jsonValue]);
|
|
3517
|
-
}
|
|
3518
|
-
constructor(value) {
|
|
3519
|
-
this.value = value;
|
|
3520
|
-
}
|
|
3521
|
-
toJSON() {
|
|
3522
|
-
return Object.entries(exports.OpTypeEnum).find(([, value]) => value === this.value)[0];
|
|
3523
|
-
}
|
|
3524
|
-
}
|
|
3525
|
-
|
|
3526
|
-
class OplogEntry {
|
|
3527
|
-
op_id;
|
|
3528
|
-
op;
|
|
3529
|
-
checksum;
|
|
3530
|
-
subkey;
|
|
3531
|
-
object_type;
|
|
3532
|
-
object_id;
|
|
3533
|
-
data;
|
|
3534
|
-
static fromRow(row) {
|
|
3535
|
-
return new OplogEntry(row.op_id, OpType.fromJSON(row.op), row.checksum, row.subkey, row.object_type, row.object_id, row.data);
|
|
3536
|
-
}
|
|
3537
|
-
constructor(op_id, op, checksum, subkey, object_type, object_id, data) {
|
|
3538
|
-
this.op_id = op_id;
|
|
3539
|
-
this.op = op;
|
|
3540
|
-
this.checksum = checksum;
|
|
3541
|
-
this.subkey = subkey;
|
|
3542
|
-
this.object_type = object_type;
|
|
3543
|
-
this.object_id = object_id;
|
|
3544
|
-
this.data = data;
|
|
3545
|
-
}
|
|
3546
|
-
toJSON(fixedKeyEncoding = false) {
|
|
3547
|
-
return {
|
|
3548
|
-
op_id: this.op_id,
|
|
3549
|
-
op: this.op.toJSON(),
|
|
3550
|
-
object_type: this.object_type,
|
|
3551
|
-
object_id: this.object_id,
|
|
3552
|
-
checksum: this.checksum,
|
|
3553
|
-
data: this.data,
|
|
3554
|
-
// Older versions of the JS SDK used to always JSON.stringify here. That has always been wrong,
|
|
3555
|
-
// but we need to migrate gradually to not break existing databases.
|
|
3556
|
-
subkey: fixedKeyEncoding ? this.subkey : JSON.stringify(this.subkey)
|
|
3557
|
-
};
|
|
3558
|
-
}
|
|
3559
|
-
}
|
|
3560
|
-
|
|
3561
|
-
class SyncDataBucket {
|
|
3562
|
-
bucket;
|
|
3563
|
-
data;
|
|
3564
|
-
has_more;
|
|
3565
|
-
after;
|
|
3566
|
-
next_after;
|
|
3567
|
-
static fromRow(row) {
|
|
3568
|
-
return new SyncDataBucket(row.bucket, row.data.map((entry) => OplogEntry.fromRow(entry)), row.has_more ?? false, row.after, row.next_after);
|
|
3569
|
-
}
|
|
3570
|
-
constructor(bucket, data,
|
|
3571
|
-
/**
|
|
3572
|
-
* True if the response does not contain all the data for this bucket, and another request must be made.
|
|
3573
|
-
*/
|
|
3574
|
-
has_more,
|
|
3575
|
-
/**
|
|
3576
|
-
* The `after` specified in the request.
|
|
3577
|
-
*/
|
|
3578
|
-
after,
|
|
3579
|
-
/**
|
|
3580
|
-
* Use this for the next request.
|
|
3581
|
-
*/
|
|
3582
|
-
next_after) {
|
|
3583
|
-
this.bucket = bucket;
|
|
3584
|
-
this.data = data;
|
|
3585
|
-
this.has_more = has_more;
|
|
3586
|
-
this.after = after;
|
|
3587
|
-
this.next_after = next_after;
|
|
3588
|
-
}
|
|
3589
|
-
toJSON(fixedKeyEncoding = false) {
|
|
3590
|
-
return {
|
|
3591
|
-
bucket: this.bucket,
|
|
3592
|
-
has_more: this.has_more,
|
|
3593
|
-
after: this.after,
|
|
3594
|
-
next_after: this.next_after,
|
|
3595
|
-
data: this.data.map((entry) => entry.toJSON(fixedKeyEncoding))
|
|
3596
|
-
};
|
|
3597
|
-
}
|
|
3598
|
-
}
|
|
3599
|
-
|
|
3600
3503
|
var buffer = {};
|
|
3601
3504
|
|
|
3602
3505
|
var base64Js = {};
|
|
@@ -10752,177 +10655,10 @@ function requireDist () {
|
|
|
10752
10655
|
|
|
10753
10656
|
var distExports = requireDist();
|
|
10754
10657
|
|
|
10755
|
-
var version = "1.
|
|
10658
|
+
var version = "1.53.0";
|
|
10756
10659
|
var PACKAGE = {
|
|
10757
10660
|
version: version};
|
|
10758
10661
|
|
|
10759
|
-
const DEFAULT_PRESSURE_LIMITS = {
|
|
10760
|
-
highWater: 10,
|
|
10761
|
-
lowWater: 0
|
|
10762
|
-
};
|
|
10763
|
-
/**
|
|
10764
|
-
* A very basic implementation of a data stream with backpressure support which does not use
|
|
10765
|
-
* native JS streams or async iterators.
|
|
10766
|
-
* This is handy for environments such as React Native which need polyfills for the above.
|
|
10767
|
-
*/
|
|
10768
|
-
class DataStream extends BaseObserver {
|
|
10769
|
-
options;
|
|
10770
|
-
dataQueue;
|
|
10771
|
-
isClosed;
|
|
10772
|
-
processingPromise;
|
|
10773
|
-
notifyDataAdded;
|
|
10774
|
-
logger;
|
|
10775
|
-
mapLine;
|
|
10776
|
-
constructor(options) {
|
|
10777
|
-
super();
|
|
10778
|
-
this.options = options;
|
|
10779
|
-
this.processingPromise = null;
|
|
10780
|
-
this.isClosed = false;
|
|
10781
|
-
this.dataQueue = [];
|
|
10782
|
-
this.mapLine = options?.mapLine ?? ((line) => line);
|
|
10783
|
-
this.logger = options?.logger ?? Logger.get('DataStream');
|
|
10784
|
-
if (options?.closeOnError) {
|
|
10785
|
-
const l = this.registerListener({
|
|
10786
|
-
error: (ex) => {
|
|
10787
|
-
l?.();
|
|
10788
|
-
this.close();
|
|
10789
|
-
}
|
|
10790
|
-
});
|
|
10791
|
-
}
|
|
10792
|
-
}
|
|
10793
|
-
get highWatermark() {
|
|
10794
|
-
return this.options?.pressure?.highWaterMark ?? DEFAULT_PRESSURE_LIMITS.highWater;
|
|
10795
|
-
}
|
|
10796
|
-
get lowWatermark() {
|
|
10797
|
-
return this.options?.pressure?.lowWaterMark ?? DEFAULT_PRESSURE_LIMITS.lowWater;
|
|
10798
|
-
}
|
|
10799
|
-
get closed() {
|
|
10800
|
-
return this.isClosed;
|
|
10801
|
-
}
|
|
10802
|
-
async close() {
|
|
10803
|
-
this.isClosed = true;
|
|
10804
|
-
await this.processingPromise;
|
|
10805
|
-
this.iterateListeners((l) => l.closed?.());
|
|
10806
|
-
// Discard any data in the queue
|
|
10807
|
-
this.dataQueue = [];
|
|
10808
|
-
this.listeners.clear();
|
|
10809
|
-
}
|
|
10810
|
-
/**
|
|
10811
|
-
* Enqueues data for the consumers to read
|
|
10812
|
-
*/
|
|
10813
|
-
enqueueData(data) {
|
|
10814
|
-
if (this.isClosed) {
|
|
10815
|
-
throw new Error('Cannot enqueue data into closed stream.');
|
|
10816
|
-
}
|
|
10817
|
-
this.dataQueue.push(data);
|
|
10818
|
-
this.notifyDataAdded?.();
|
|
10819
|
-
this.processQueue();
|
|
10820
|
-
}
|
|
10821
|
-
/**
|
|
10822
|
-
* Reads data once from the data stream
|
|
10823
|
-
* @returns a Data payload or Null if the stream closed.
|
|
10824
|
-
*/
|
|
10825
|
-
async read() {
|
|
10826
|
-
if (this.closed) {
|
|
10827
|
-
return null;
|
|
10828
|
-
}
|
|
10829
|
-
// Wait for any pending processing to complete first.
|
|
10830
|
-
// This ensures we register our listener before calling processQueue(),
|
|
10831
|
-
// avoiding a race where processQueue() sees no reader and returns early.
|
|
10832
|
-
if (this.processingPromise) {
|
|
10833
|
-
await this.processingPromise;
|
|
10834
|
-
}
|
|
10835
|
-
// Re-check after await - stream may have closed while we were waiting
|
|
10836
|
-
if (this.closed) {
|
|
10837
|
-
return null;
|
|
10838
|
-
}
|
|
10839
|
-
return new Promise((resolve, reject) => {
|
|
10840
|
-
const l = this.registerListener({
|
|
10841
|
-
data: async (data) => {
|
|
10842
|
-
resolve(data);
|
|
10843
|
-
// Remove the listener
|
|
10844
|
-
l?.();
|
|
10845
|
-
},
|
|
10846
|
-
closed: () => {
|
|
10847
|
-
resolve(null);
|
|
10848
|
-
l?.();
|
|
10849
|
-
},
|
|
10850
|
-
error: (ex) => {
|
|
10851
|
-
reject(ex);
|
|
10852
|
-
l?.();
|
|
10853
|
-
}
|
|
10854
|
-
});
|
|
10855
|
-
this.processQueue();
|
|
10856
|
-
});
|
|
10857
|
-
}
|
|
10858
|
-
/**
|
|
10859
|
-
* Executes a callback for each data item in the stream
|
|
10860
|
-
*/
|
|
10861
|
-
forEach(callback) {
|
|
10862
|
-
if (this.dataQueue.length <= this.lowWatermark) {
|
|
10863
|
-
this.iterateAsyncErrored(async (l) => l.lowWater?.());
|
|
10864
|
-
}
|
|
10865
|
-
return this.registerListener({
|
|
10866
|
-
data: callback
|
|
10867
|
-
});
|
|
10868
|
-
}
|
|
10869
|
-
processQueue() {
|
|
10870
|
-
if (this.processingPromise) {
|
|
10871
|
-
return;
|
|
10872
|
-
}
|
|
10873
|
-
const promise = (this.processingPromise = this._processQueue());
|
|
10874
|
-
promise.finally(() => {
|
|
10875
|
-
this.processingPromise = null;
|
|
10876
|
-
});
|
|
10877
|
-
return promise;
|
|
10878
|
-
}
|
|
10879
|
-
hasDataReader() {
|
|
10880
|
-
return Array.from(this.listeners.values()).some((l) => !!l.data);
|
|
10881
|
-
}
|
|
10882
|
-
async _processQueue() {
|
|
10883
|
-
/**
|
|
10884
|
-
* Allow listeners to mutate the queue before processing.
|
|
10885
|
-
* This allows for operations such as dropping or compressing data
|
|
10886
|
-
* on high water or requesting more data on low water.
|
|
10887
|
-
*/
|
|
10888
|
-
if (this.dataQueue.length >= this.highWatermark) {
|
|
10889
|
-
await this.iterateAsyncErrored(async (l) => l.highWater?.());
|
|
10890
|
-
}
|
|
10891
|
-
if (this.isClosed || !this.hasDataReader()) {
|
|
10892
|
-
return;
|
|
10893
|
-
}
|
|
10894
|
-
if (this.dataQueue.length) {
|
|
10895
|
-
const data = this.dataQueue.shift();
|
|
10896
|
-
const mapped = this.mapLine(data);
|
|
10897
|
-
await this.iterateAsyncErrored(async (l) => l.data?.(mapped));
|
|
10898
|
-
}
|
|
10899
|
-
if (this.dataQueue.length <= this.lowWatermark) {
|
|
10900
|
-
const dataAdded = new Promise((resolve) => {
|
|
10901
|
-
this.notifyDataAdded = resolve;
|
|
10902
|
-
});
|
|
10903
|
-
await Promise.race([this.iterateAsyncErrored(async (l) => l.lowWater?.()), dataAdded]);
|
|
10904
|
-
this.notifyDataAdded = null;
|
|
10905
|
-
}
|
|
10906
|
-
if (this.dataQueue.length > 0) {
|
|
10907
|
-
setTimeout(() => this.processQueue());
|
|
10908
|
-
}
|
|
10909
|
-
}
|
|
10910
|
-
async iterateAsyncErrored(cb) {
|
|
10911
|
-
// Important: We need to copy the listeners, as calling a listener could result in adding another
|
|
10912
|
-
// listener, resulting in infinite loops.
|
|
10913
|
-
const listeners = Array.from(this.listeners.values());
|
|
10914
|
-
for (let i of listeners) {
|
|
10915
|
-
try {
|
|
10916
|
-
await cb(i);
|
|
10917
|
-
}
|
|
10918
|
-
catch (ex) {
|
|
10919
|
-
this.logger.error(ex);
|
|
10920
|
-
this.iterateListeners((l) => l.error?.(ex));
|
|
10921
|
-
}
|
|
10922
|
-
}
|
|
10923
|
-
}
|
|
10924
|
-
}
|
|
10925
|
-
|
|
10926
10662
|
var WebsocketDuplexConnection = {};
|
|
10927
10663
|
|
|
10928
10664
|
var hasRequiredWebsocketDuplexConnection;
|
|
@@ -11085,8 +10821,199 @@ class WebsocketClientTransport {
|
|
|
11085
10821
|
}
|
|
11086
10822
|
}
|
|
11087
10823
|
|
|
10824
|
+
const doneResult = { done: true, value: undefined };
|
|
10825
|
+
function valueResult(value) {
|
|
10826
|
+
return { done: false, value };
|
|
10827
|
+
}
|
|
10828
|
+
/**
|
|
10829
|
+
* Expands a source async iterator by allowing to inject events asynchronously.
|
|
10830
|
+
*
|
|
10831
|
+
* The resulting iterator will emit all events from its source. Additionally though, events can be injected. These
|
|
10832
|
+
* events are dropped once the main iterator completes, but are otherwise forwarded.
|
|
10833
|
+
*
|
|
10834
|
+
* The iterator completes when its source completes, and it supports backpressure by only calling `next()` on the source
|
|
10835
|
+
* in response to a `next()` call from downstream if no pending injected events can be dispatched.
|
|
10836
|
+
*/
|
|
10837
|
+
function injectable(source) {
|
|
10838
|
+
let sourceIsDone = false;
|
|
10839
|
+
let waiter = undefined; // An active, waiting next() call.
|
|
10840
|
+
// A pending upstream event that couldn't be dispatched because inject() has been called before it was resolved.
|
|
10841
|
+
let pendingSourceEvent = null;
|
|
10842
|
+
let pendingInjectedEvents = [];
|
|
10843
|
+
const consumeWaiter = () => {
|
|
10844
|
+
const pending = waiter;
|
|
10845
|
+
waiter = undefined;
|
|
10846
|
+
return pending;
|
|
10847
|
+
};
|
|
10848
|
+
const fetchFromSource = () => {
|
|
10849
|
+
const resolveWaiter = (propagate) => {
|
|
10850
|
+
const active = consumeWaiter();
|
|
10851
|
+
if (active) {
|
|
10852
|
+
propagate(active);
|
|
10853
|
+
}
|
|
10854
|
+
else {
|
|
10855
|
+
pendingSourceEvent = propagate;
|
|
10856
|
+
}
|
|
10857
|
+
};
|
|
10858
|
+
const nextFromSource = source.next();
|
|
10859
|
+
nextFromSource.then((value) => {
|
|
10860
|
+
sourceIsDone = value.done == true;
|
|
10861
|
+
resolveWaiter((w) => w.resolve(value));
|
|
10862
|
+
}, (error) => {
|
|
10863
|
+
resolveWaiter((w) => w.reject(error));
|
|
10864
|
+
});
|
|
10865
|
+
};
|
|
10866
|
+
return {
|
|
10867
|
+
next: () => {
|
|
10868
|
+
return new Promise((resolve, reject) => {
|
|
10869
|
+
// First priority: Dispatch ready upstream events.
|
|
10870
|
+
if (sourceIsDone) {
|
|
10871
|
+
return resolve(doneResult);
|
|
10872
|
+
}
|
|
10873
|
+
if (pendingSourceEvent) {
|
|
10874
|
+
pendingSourceEvent({ resolve, reject });
|
|
10875
|
+
pendingSourceEvent = null;
|
|
10876
|
+
return;
|
|
10877
|
+
}
|
|
10878
|
+
// Second priority: Dispatch injected events
|
|
10879
|
+
if (pendingInjectedEvents.length) {
|
|
10880
|
+
return resolve(valueResult(pendingInjectedEvents.shift()));
|
|
10881
|
+
}
|
|
10882
|
+
// Nothing pending? Fetch from source
|
|
10883
|
+
waiter = { resolve, reject };
|
|
10884
|
+
return fetchFromSource();
|
|
10885
|
+
});
|
|
10886
|
+
},
|
|
10887
|
+
inject: (event) => {
|
|
10888
|
+
const pending = consumeWaiter();
|
|
10889
|
+
if (pending != null) {
|
|
10890
|
+
pending.resolve(valueResult(event));
|
|
10891
|
+
}
|
|
10892
|
+
else {
|
|
10893
|
+
pendingInjectedEvents.push(event);
|
|
10894
|
+
}
|
|
10895
|
+
}
|
|
10896
|
+
};
|
|
10897
|
+
}
|
|
10898
|
+
/**
|
|
10899
|
+
* Splits a byte stream at line endings, emitting each line as a string.
|
|
10900
|
+
*/
|
|
10901
|
+
function extractJsonLines(source, decoder) {
|
|
10902
|
+
let buffer = '';
|
|
10903
|
+
const pendingLines = [];
|
|
10904
|
+
let isFinalEvent = false;
|
|
10905
|
+
return {
|
|
10906
|
+
next: async () => {
|
|
10907
|
+
while (true) {
|
|
10908
|
+
if (isFinalEvent) {
|
|
10909
|
+
return doneResult;
|
|
10910
|
+
}
|
|
10911
|
+
{
|
|
10912
|
+
const first = pendingLines.shift();
|
|
10913
|
+
if (first) {
|
|
10914
|
+
return { done: false, value: first };
|
|
10915
|
+
}
|
|
10916
|
+
}
|
|
10917
|
+
const { done, value } = await source.next();
|
|
10918
|
+
if (done) {
|
|
10919
|
+
const remaining = buffer.trim();
|
|
10920
|
+
if (remaining.length != 0) {
|
|
10921
|
+
isFinalEvent = true;
|
|
10922
|
+
return { done: false, value: remaining };
|
|
10923
|
+
}
|
|
10924
|
+
return doneResult;
|
|
10925
|
+
}
|
|
10926
|
+
const data = decoder.decode(value, { stream: true });
|
|
10927
|
+
buffer += data;
|
|
10928
|
+
const lines = buffer.split('\n');
|
|
10929
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
10930
|
+
const l = lines[i].trim();
|
|
10931
|
+
if (l.length > 0) {
|
|
10932
|
+
pendingLines.push(l);
|
|
10933
|
+
}
|
|
10934
|
+
}
|
|
10935
|
+
buffer = lines[lines.length - 1];
|
|
10936
|
+
}
|
|
10937
|
+
}
|
|
10938
|
+
};
|
|
10939
|
+
}
|
|
10940
|
+
/**
|
|
10941
|
+
* Splits a concatenated stream of BSON objects by emitting individual objects.
|
|
10942
|
+
*/
|
|
10943
|
+
function extractBsonObjects(source) {
|
|
10944
|
+
// Fully read but not emitted yet.
|
|
10945
|
+
const completedObjects = [];
|
|
10946
|
+
// Whether source has returned { done: true }. We do the same once completed objects have been emitted.
|
|
10947
|
+
let isDone = false;
|
|
10948
|
+
const lengthBuffer = new DataView(new ArrayBuffer(4));
|
|
10949
|
+
let objectBody = null;
|
|
10950
|
+
// If we're parsing the length field, a number between 1 and 4 (inclusive) describing remaining bytes in the header.
|
|
10951
|
+
// If we're consuming a document, the bytes remaining.
|
|
10952
|
+
let remainingLength = 4;
|
|
10953
|
+
return {
|
|
10954
|
+
async next() {
|
|
10955
|
+
while (true) {
|
|
10956
|
+
// Before fetching new data from upstream, return completed objects.
|
|
10957
|
+
if (completedObjects.length) {
|
|
10958
|
+
return valueResult(completedObjects.shift());
|
|
10959
|
+
}
|
|
10960
|
+
if (isDone) {
|
|
10961
|
+
return doneResult;
|
|
10962
|
+
}
|
|
10963
|
+
const upstreamEvent = await source.next();
|
|
10964
|
+
if (upstreamEvent.done) {
|
|
10965
|
+
isDone = true;
|
|
10966
|
+
if (objectBody || remainingLength != 4) {
|
|
10967
|
+
throw new Error('illegal end of stream in BSON object');
|
|
10968
|
+
}
|
|
10969
|
+
return doneResult;
|
|
10970
|
+
}
|
|
10971
|
+
const chunk = upstreamEvent.value;
|
|
10972
|
+
for (let i = 0; i < chunk.length;) {
|
|
10973
|
+
const availableInData = chunk.length - i;
|
|
10974
|
+
if (objectBody) {
|
|
10975
|
+
// We're in the middle of reading a BSON document.
|
|
10976
|
+
const bytesToRead = Math.min(availableInData, remainingLength);
|
|
10977
|
+
const copySource = new Uint8Array(chunk.buffer, chunk.byteOffset + i, bytesToRead);
|
|
10978
|
+
objectBody.set(copySource, objectBody.length - remainingLength);
|
|
10979
|
+
i += bytesToRead;
|
|
10980
|
+
remainingLength -= bytesToRead;
|
|
10981
|
+
if (remainingLength == 0) {
|
|
10982
|
+
completedObjects.push(objectBody);
|
|
10983
|
+
// Prepare to read another document, starting with its length
|
|
10984
|
+
objectBody = null;
|
|
10985
|
+
remainingLength = 4;
|
|
10986
|
+
}
|
|
10987
|
+
}
|
|
10988
|
+
else {
|
|
10989
|
+
// Copy up to 4 bytes into lengthBuffer, depending on how many we still need.
|
|
10990
|
+
const bytesToRead = Math.min(availableInData, remainingLength);
|
|
10991
|
+
for (let j = 0; j < bytesToRead; j++) {
|
|
10992
|
+
lengthBuffer.setUint8(4 - remainingLength + j, chunk[i + j]);
|
|
10993
|
+
}
|
|
10994
|
+
i += bytesToRead;
|
|
10995
|
+
remainingLength -= bytesToRead;
|
|
10996
|
+
if (remainingLength == 0) {
|
|
10997
|
+
// Transition from reading length header to reading document. Subtracting 4 because the length of the
|
|
10998
|
+
// header is included in length.
|
|
10999
|
+
const length = lengthBuffer.getInt32(0, true /* little endian */);
|
|
11000
|
+
remainingLength = length - 4;
|
|
11001
|
+
if (remainingLength < 1) {
|
|
11002
|
+
throw new Error(`invalid length for bson: ${length}`);
|
|
11003
|
+
}
|
|
11004
|
+
objectBody = new Uint8Array(length);
|
|
11005
|
+
new DataView(objectBody.buffer).setInt32(0, length, true);
|
|
11006
|
+
}
|
|
11007
|
+
}
|
|
11008
|
+
}
|
|
11009
|
+
}
|
|
11010
|
+
}
|
|
11011
|
+
};
|
|
11012
|
+
}
|
|
11013
|
+
|
|
11088
11014
|
const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
|
|
11089
11015
|
const POWERSYNC_JS_VERSION = PACKAGE.version;
|
|
11016
|
+
const SYNC_QUEUE_REQUEST_HIGH_WATER = 10;
|
|
11090
11017
|
const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
|
|
11091
11018
|
// Keep alive message is sent every period
|
|
11092
11019
|
const KEEP_ALIVE_MS = 20_000;
|
|
@@ -11266,73 +11193,67 @@ class AbstractRemote {
|
|
|
11266
11193
|
return new WebSocket(url);
|
|
11267
11194
|
}
|
|
11268
11195
|
/**
|
|
11269
|
-
* Returns a data stream of sync line data.
|
|
11196
|
+
* Returns a data stream of sync line data, fetched via RSocket-over-WebSocket.
|
|
11270
11197
|
*
|
|
11271
|
-
*
|
|
11272
|
-
* @param bson A BSON encoder and decoder. When set, the data stream will be requested with a BSON payload
|
|
11273
|
-
* (required for compatibility with older sync services).
|
|
11198
|
+
* The only mechanism to abort the returned stream is to use the abort signal in {@link SocketSyncStreamOptions}.
|
|
11274
11199
|
*/
|
|
11275
|
-
async socketStreamRaw(options
|
|
11200
|
+
async socketStreamRaw(options) {
|
|
11276
11201
|
const { path, fetchStrategy = exports.FetchStrategy.Buffered } = options;
|
|
11277
|
-
const mimeType =
|
|
11202
|
+
const mimeType = 'application/json';
|
|
11278
11203
|
function toBuffer(js) {
|
|
11279
|
-
|
|
11280
|
-
if (bson != null) {
|
|
11281
|
-
contents = bson.serialize(js);
|
|
11282
|
-
}
|
|
11283
|
-
else {
|
|
11284
|
-
contents = JSON.stringify(js);
|
|
11285
|
-
}
|
|
11286
|
-
return bufferExports.Buffer.from(contents);
|
|
11204
|
+
return bufferExports.Buffer.from(JSON.stringify(js));
|
|
11287
11205
|
}
|
|
11288
11206
|
const syncQueueRequestSize = fetchStrategy == exports.FetchStrategy.Buffered ? 10 : 1;
|
|
11289
11207
|
const request = await this.buildRequest(path);
|
|
11208
|
+
const url = this.options.socketUrlTransformer(request.url);
|
|
11290
11209
|
// Add the user agent in the setup payload - we can't set custom
|
|
11291
11210
|
// headers with websockets on web. The browser userAgent is however added
|
|
11292
11211
|
// automatically as a header.
|
|
11293
11212
|
const userAgent = this.getUserAgent();
|
|
11294
|
-
|
|
11295
|
-
|
|
11296
|
-
|
|
11297
|
-
|
|
11298
|
-
|
|
11299
|
-
|
|
11300
|
-
|
|
11213
|
+
// While we're connecting (a process that can't be aborted in RSocket), the WebSocket instance to close if we wanted
|
|
11214
|
+
// to abort the connection.
|
|
11215
|
+
let pendingSocket = null;
|
|
11216
|
+
let keepAliveTimeout;
|
|
11217
|
+
let rsocket = null;
|
|
11218
|
+
let queue = null;
|
|
11219
|
+
let didClose = false;
|
|
11220
|
+
const abortRequest = () => {
|
|
11221
|
+
if (didClose) {
|
|
11222
|
+
return;
|
|
11223
|
+
}
|
|
11224
|
+
didClose = true;
|
|
11225
|
+
clearTimeout(keepAliveTimeout);
|
|
11226
|
+
if (pendingSocket) {
|
|
11227
|
+
pendingSocket.close();
|
|
11228
|
+
}
|
|
11229
|
+
if (rsocket) {
|
|
11230
|
+
rsocket.close();
|
|
11231
|
+
}
|
|
11232
|
+
if (queue) {
|
|
11233
|
+
queue.stop();
|
|
11234
|
+
}
|
|
11235
|
+
};
|
|
11301
11236
|
// Handle upstream abort
|
|
11302
|
-
if (options.abortSignal
|
|
11237
|
+
if (options.abortSignal.aborted) {
|
|
11303
11238
|
throw new AbortOperation('Connection request aborted');
|
|
11304
11239
|
}
|
|
11305
11240
|
else {
|
|
11306
|
-
options.abortSignal
|
|
11307
|
-
stream.close();
|
|
11308
|
-
}, { once: true });
|
|
11241
|
+
options.abortSignal.addEventListener('abort', abortRequest);
|
|
11309
11242
|
}
|
|
11310
|
-
let keepAliveTimeout;
|
|
11311
11243
|
const resetTimeout = () => {
|
|
11312
11244
|
clearTimeout(keepAliveTimeout);
|
|
11313
11245
|
keepAliveTimeout = setTimeout(() => {
|
|
11314
11246
|
this.logger.error(`No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.`);
|
|
11315
|
-
|
|
11247
|
+
abortRequest();
|
|
11316
11248
|
}, SOCKET_TIMEOUT_MS);
|
|
11317
11249
|
};
|
|
11318
11250
|
resetTimeout();
|
|
11319
|
-
// Typescript complains about this being `never` if it's not assigned here.
|
|
11320
|
-
// This is assigned in `wsCreator`.
|
|
11321
|
-
let disposeSocketConnectionTimeout = () => { };
|
|
11322
|
-
const url = this.options.socketUrlTransformer(request.url);
|
|
11323
11251
|
const connector = new distExports.RSocketConnector({
|
|
11324
11252
|
transport: new WebsocketClientTransport({
|
|
11325
11253
|
url,
|
|
11326
11254
|
wsCreator: (url) => {
|
|
11327
|
-
const socket = this.createSocket(url);
|
|
11328
|
-
|
|
11329
|
-
closed: () => {
|
|
11330
|
-
// Allow closing the underlying WebSocket if the stream was closed before the
|
|
11331
|
-
// RSocket connect completed. This should effectively abort the request.
|
|
11332
|
-
socket.close();
|
|
11333
|
-
}
|
|
11334
|
-
});
|
|
11335
|
-
socket.addEventListener('message', (event) => {
|
|
11255
|
+
const socket = (pendingSocket = this.createSocket(url));
|
|
11256
|
+
socket.addEventListener('message', () => {
|
|
11336
11257
|
resetTimeout();
|
|
11337
11258
|
});
|
|
11338
11259
|
return socket;
|
|
@@ -11352,43 +11273,40 @@ class AbstractRemote {
|
|
|
11352
11273
|
}
|
|
11353
11274
|
}
|
|
11354
11275
|
});
|
|
11355
|
-
let rsocket;
|
|
11356
11276
|
try {
|
|
11357
11277
|
rsocket = await connector.connect();
|
|
11358
11278
|
// The connection is established, we no longer need to monitor the initial timeout
|
|
11359
|
-
|
|
11279
|
+
pendingSocket = null;
|
|
11360
11280
|
}
|
|
11361
11281
|
catch (ex) {
|
|
11362
11282
|
this.logger.error(`Failed to connect WebSocket`, ex);
|
|
11363
|
-
|
|
11364
|
-
if (!stream.closed) {
|
|
11365
|
-
await stream.close();
|
|
11366
|
-
}
|
|
11283
|
+
abortRequest();
|
|
11367
11284
|
throw ex;
|
|
11368
11285
|
}
|
|
11369
11286
|
resetTimeout();
|
|
11370
|
-
let socketIsClosed = false;
|
|
11371
|
-
const closeSocket = () => {
|
|
11372
|
-
clearTimeout(keepAliveTimeout);
|
|
11373
|
-
if (socketIsClosed) {
|
|
11374
|
-
return;
|
|
11375
|
-
}
|
|
11376
|
-
socketIsClosed = true;
|
|
11377
|
-
rsocket.close();
|
|
11378
|
-
};
|
|
11379
11287
|
// Helps to prevent double close scenarios
|
|
11380
|
-
rsocket.onClose(() => (
|
|
11381
|
-
|
|
11382
|
-
let pendingEventsCount = syncQueueRequestSize;
|
|
11383
|
-
const disposeClosedListener = stream.registerListener({
|
|
11384
|
-
closed: () => {
|
|
11385
|
-
closeSocket();
|
|
11386
|
-
disposeClosedListener();
|
|
11387
|
-
}
|
|
11388
|
-
});
|
|
11389
|
-
const socket = await new Promise((resolve, reject) => {
|
|
11288
|
+
rsocket.onClose(() => (rsocket = null));
|
|
11289
|
+
return await new Promise((resolve, reject) => {
|
|
11390
11290
|
let connectionEstablished = false;
|
|
11391
|
-
|
|
11291
|
+
let pendingEventsCount = syncQueueRequestSize;
|
|
11292
|
+
let paused = false;
|
|
11293
|
+
let res = null;
|
|
11294
|
+
function requestMore() {
|
|
11295
|
+
const delta = syncQueueRequestSize - pendingEventsCount;
|
|
11296
|
+
if (!paused && delta > 0) {
|
|
11297
|
+
res?.request(delta);
|
|
11298
|
+
pendingEventsCount = syncQueueRequestSize;
|
|
11299
|
+
}
|
|
11300
|
+
}
|
|
11301
|
+
const events = new domExports.EventIterator((q) => {
|
|
11302
|
+
queue = q;
|
|
11303
|
+
q.on('highWater', () => (paused = true));
|
|
11304
|
+
q.on('lowWater', () => {
|
|
11305
|
+
paused = false;
|
|
11306
|
+
requestMore();
|
|
11307
|
+
});
|
|
11308
|
+
}, { highWaterMark: SYNC_QUEUE_REQUEST_HIGH_WATER, lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER })[symbolAsyncIterator]();
|
|
11309
|
+
res = rsocket.requestStream({
|
|
11392
11310
|
data: toBuffer(options.data),
|
|
11393
11311
|
metadata: toBuffer({
|
|
11394
11312
|
path
|
|
@@ -11413,7 +11331,7 @@ class AbstractRemote {
|
|
|
11413
11331
|
}
|
|
11414
11332
|
// RSocket will close the RSocket stream automatically
|
|
11415
11333
|
// Close the downstream stream as well - this will close the RSocket connection and WebSocket
|
|
11416
|
-
|
|
11334
|
+
abortRequest();
|
|
11417
11335
|
// Handles cases where the connection failed e.g. auth error or connection error
|
|
11418
11336
|
if (!connectionEstablished) {
|
|
11419
11337
|
reject(e);
|
|
@@ -11423,41 +11341,40 @@ class AbstractRemote {
|
|
|
11423
11341
|
// The connection is active
|
|
11424
11342
|
if (!connectionEstablished) {
|
|
11425
11343
|
connectionEstablished = true;
|
|
11426
|
-
resolve(
|
|
11344
|
+
resolve(events);
|
|
11427
11345
|
}
|
|
11428
11346
|
const { data } = payload;
|
|
11347
|
+
if (data) {
|
|
11348
|
+
queue.push(data);
|
|
11349
|
+
}
|
|
11429
11350
|
// Less events are now pending
|
|
11430
11351
|
pendingEventsCount--;
|
|
11431
|
-
|
|
11432
|
-
|
|
11433
|
-
}
|
|
11434
|
-
stream.enqueueData(data);
|
|
11352
|
+
// Request another event (unless the downstream consumer is paused).
|
|
11353
|
+
requestMore();
|
|
11435
11354
|
},
|
|
11436
11355
|
onComplete: () => {
|
|
11437
|
-
|
|
11356
|
+
abortRequest(); // this will also emit a done event
|
|
11438
11357
|
},
|
|
11439
11358
|
onExtension: () => { }
|
|
11440
11359
|
});
|
|
11441
11360
|
});
|
|
11442
|
-
const l = stream.registerListener({
|
|
11443
|
-
lowWater: async () => {
|
|
11444
|
-
// Request to fill up the queue
|
|
11445
|
-
const required = syncQueueRequestSize - pendingEventsCount;
|
|
11446
|
-
if (required > 0) {
|
|
11447
|
-
socket.request(syncQueueRequestSize - pendingEventsCount);
|
|
11448
|
-
pendingEventsCount = syncQueueRequestSize;
|
|
11449
|
-
}
|
|
11450
|
-
},
|
|
11451
|
-
closed: () => {
|
|
11452
|
-
l();
|
|
11453
|
-
}
|
|
11454
|
-
});
|
|
11455
|
-
return stream;
|
|
11456
11361
|
}
|
|
11457
11362
|
/**
|
|
11458
|
-
*
|
|
11363
|
+
* @returns Whether the HTTP implementation on this platform can receive streamed binary responses. This is true on
|
|
11364
|
+
* all platforms except React Native (who would have guessed...), where we must not request BSON responses.
|
|
11365
|
+
*
|
|
11366
|
+
* @see https://github.com/react-native-community/fetch?tab=readme-ov-file#motivation
|
|
11367
|
+
*/
|
|
11368
|
+
get supportsStreamingBinaryResponses() {
|
|
11369
|
+
return true;
|
|
11370
|
+
}
|
|
11371
|
+
/**
|
|
11372
|
+
* Posts a `/sync/stream` request, asserts that it completes successfully and returns the streaming response as an
|
|
11373
|
+
* async iterator of byte blobs.
|
|
11374
|
+
*
|
|
11375
|
+
* To cancel the async iterator, use the abort signal from {@link SyncStreamOptions} passed to this method.
|
|
11459
11376
|
*/
|
|
11460
|
-
async
|
|
11377
|
+
async fetchStreamRaw(options) {
|
|
11461
11378
|
const { data, path, headers, abortSignal } = options;
|
|
11462
11379
|
const request = await this.buildRequest(path);
|
|
11463
11380
|
/**
|
|
@@ -11469,119 +11386,94 @@ class AbstractRemote {
|
|
|
11469
11386
|
* Aborting the active fetch request while it is being consumed seems to throw
|
|
11470
11387
|
* an unhandled exception on the window level.
|
|
11471
11388
|
*/
|
|
11472
|
-
if (abortSignal
|
|
11473
|
-
throw new AbortOperation('Abort request received before making
|
|
11389
|
+
if (abortSignal.aborted) {
|
|
11390
|
+
throw new AbortOperation('Abort request received before making fetchStreamRaw request');
|
|
11474
11391
|
}
|
|
11475
11392
|
const controller = new AbortController();
|
|
11476
|
-
let
|
|
11477
|
-
abortSignal
|
|
11478
|
-
|
|
11393
|
+
let reader = null;
|
|
11394
|
+
abortSignal.addEventListener('abort', () => {
|
|
11395
|
+
const reason = abortSignal.reason ??
|
|
11396
|
+
new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.');
|
|
11397
|
+
if (reader == null) {
|
|
11479
11398
|
// Only abort via the abort controller if the request has not resolved yet
|
|
11480
|
-
controller.abort(
|
|
11481
|
-
|
|
11399
|
+
controller.abort(reason);
|
|
11400
|
+
}
|
|
11401
|
+
else {
|
|
11402
|
+
reader.cancel(reason).catch(() => {
|
|
11403
|
+
// Cancelling the reader might rethrow an exception we would have handled by throwing in next(). So we can
|
|
11404
|
+
// ignore it here.
|
|
11405
|
+
});
|
|
11482
11406
|
}
|
|
11483
11407
|
});
|
|
11484
|
-
|
|
11485
|
-
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
|
|
11492
|
-
|
|
11408
|
+
let res;
|
|
11409
|
+
let responseIsBson = false;
|
|
11410
|
+
try {
|
|
11411
|
+
const ndJson = 'application/x-ndjson';
|
|
11412
|
+
const bson = 'application/vnd.powersync.bson-stream';
|
|
11413
|
+
res = await this.fetch(request.url, {
|
|
11414
|
+
method: 'POST',
|
|
11415
|
+
headers: {
|
|
11416
|
+
...headers,
|
|
11417
|
+
...request.headers,
|
|
11418
|
+
accept: this.supportsStreamingBinaryResponses ? `${bson};q=0.9,${ndJson};q=0.8` : ndJson
|
|
11419
|
+
},
|
|
11420
|
+
body: JSON.stringify(data),
|
|
11421
|
+
signal: controller.signal,
|
|
11422
|
+
cache: 'no-store',
|
|
11423
|
+
...(this.options.fetchOptions ?? {}),
|
|
11424
|
+
...options.fetchOptions
|
|
11425
|
+
});
|
|
11426
|
+
if (!res.ok || !res.body) {
|
|
11427
|
+
const text = await res.text();
|
|
11428
|
+
this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
|
|
11429
|
+
const error = new Error(`HTTP ${res.statusText}: ${text}`);
|
|
11430
|
+
error.status = res.status;
|
|
11431
|
+
throw error;
|
|
11432
|
+
}
|
|
11433
|
+
const contentType = res.headers.get('content-type');
|
|
11434
|
+
responseIsBson = contentType == bson;
|
|
11435
|
+
}
|
|
11436
|
+
catch (ex) {
|
|
11493
11437
|
if (ex.name == 'AbortError') {
|
|
11494
11438
|
throw new AbortOperation(`Pending fetch request to ${request.url} has been aborted.`);
|
|
11495
11439
|
}
|
|
11496
11440
|
throw ex;
|
|
11497
|
-
});
|
|
11498
|
-
if (!res) {
|
|
11499
|
-
throw new Error('Fetch request was aborted');
|
|
11500
|
-
}
|
|
11501
|
-
requestResolved = true;
|
|
11502
|
-
if (!res.ok || !res.body) {
|
|
11503
|
-
const text = await res.text();
|
|
11504
|
-
this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
|
|
11505
|
-
const error = new Error(`HTTP ${res.statusText}: ${text}`);
|
|
11506
|
-
error.status = res.status;
|
|
11507
|
-
throw error;
|
|
11508
11441
|
}
|
|
11509
|
-
|
|
11510
|
-
|
|
11511
|
-
|
|
11512
|
-
|
|
11513
|
-
|
|
11514
|
-
const closeReader = async () => {
|
|
11515
|
-
try {
|
|
11516
|
-
readerReleased = true;
|
|
11517
|
-
await reader.cancel();
|
|
11518
|
-
}
|
|
11519
|
-
catch (ex) {
|
|
11520
|
-
// an error will throw if the reader hasn't been used yet
|
|
11521
|
-
}
|
|
11522
|
-
reader.releaseLock();
|
|
11523
|
-
};
|
|
11524
|
-
const stream = new DataStream({
|
|
11525
|
-
logger: this.logger,
|
|
11526
|
-
mapLine: mapLine,
|
|
11527
|
-
pressure: {
|
|
11528
|
-
highWaterMark: 20,
|
|
11529
|
-
lowWaterMark: 10
|
|
11530
|
-
}
|
|
11531
|
-
});
|
|
11532
|
-
abortSignal?.addEventListener('abort', () => {
|
|
11533
|
-
closeReader();
|
|
11534
|
-
stream.close();
|
|
11535
|
-
});
|
|
11536
|
-
const decoder = this.createTextDecoder();
|
|
11537
|
-
let buffer = '';
|
|
11538
|
-
const consumeStream = async () => {
|
|
11539
|
-
while (!stream.closed && !abortSignal?.aborted && !readerReleased) {
|
|
11540
|
-
const { done, value } = await reader.read();
|
|
11541
|
-
if (done) {
|
|
11542
|
-
const remaining = buffer.trim();
|
|
11543
|
-
if (remaining.length != 0) {
|
|
11544
|
-
stream.enqueueData(remaining);
|
|
11545
|
-
}
|
|
11546
|
-
stream.close();
|
|
11547
|
-
await closeReader();
|
|
11548
|
-
return;
|
|
11442
|
+
reader = res.body.getReader();
|
|
11443
|
+
const stream = {
|
|
11444
|
+
next: async () => {
|
|
11445
|
+
if (controller.signal.aborted) {
|
|
11446
|
+
return doneResult;
|
|
11549
11447
|
}
|
|
11550
|
-
|
|
11551
|
-
|
|
11552
|
-
const lines = buffer.split('\n');
|
|
11553
|
-
for (var i = 0; i < lines.length - 1; i++) {
|
|
11554
|
-
var l = lines[i].trim();
|
|
11555
|
-
if (l.length > 0) {
|
|
11556
|
-
stream.enqueueData(l);
|
|
11557
|
-
}
|
|
11448
|
+
try {
|
|
11449
|
+
return await reader.read();
|
|
11558
11450
|
}
|
|
11559
|
-
|
|
11560
|
-
|
|
11561
|
-
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
|
|
11565
|
-
|
|
11566
|
-
dispose();
|
|
11567
|
-
},
|
|
11568
|
-
closed: () => {
|
|
11569
|
-
resolve();
|
|
11570
|
-
dispose();
|
|
11571
|
-
}
|
|
11572
|
-
});
|
|
11573
|
-
});
|
|
11451
|
+
catch (ex) {
|
|
11452
|
+
if (controller.signal.aborted) {
|
|
11453
|
+
// .read() completes with an error if we cancel the reader, which we do to disconnect. So this is just
|
|
11454
|
+
// things working as intended, we can return a done event and consider the exception handled.
|
|
11455
|
+
return doneResult;
|
|
11456
|
+
}
|
|
11457
|
+
throw ex;
|
|
11574
11458
|
}
|
|
11575
11459
|
}
|
|
11576
11460
|
};
|
|
11577
|
-
|
|
11578
|
-
|
|
11579
|
-
|
|
11580
|
-
|
|
11581
|
-
|
|
11582
|
-
|
|
11583
|
-
|
|
11584
|
-
|
|
11461
|
+
return { isBson: responseIsBson, stream };
|
|
11462
|
+
}
|
|
11463
|
+
/**
|
|
11464
|
+
* Posts a `/sync/stream` request.
|
|
11465
|
+
*
|
|
11466
|
+
* Depending on the `Content-Type` of the response, this returns strings for sync lines or encoded BSON documents as
|
|
11467
|
+
* {@link Uint8Array}s.
|
|
11468
|
+
*/
|
|
11469
|
+
async fetchStream(options) {
|
|
11470
|
+
const { isBson, stream } = await this.fetchStreamRaw(options);
|
|
11471
|
+
if (isBson) {
|
|
11472
|
+
return extractBsonObjects(stream);
|
|
11473
|
+
}
|
|
11474
|
+
else {
|
|
11475
|
+
return extractJsonLines(stream, this.createTextDecoder());
|
|
11476
|
+
}
|
|
11585
11477
|
}
|
|
11586
11478
|
}
|
|
11587
11479
|
|
|
@@ -11610,31 +11502,8 @@ function coreStatusToJs(status) {
|
|
|
11610
11502
|
priorityStatusEntries: status.priority_status.map(priorityToJs)
|
|
11611
11503
|
};
|
|
11612
11504
|
}
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
return line.data != null;
|
|
11616
|
-
}
|
|
11617
|
-
function isStreamingKeepalive(line) {
|
|
11618
|
-
return line.token_expires_in != null;
|
|
11619
|
-
}
|
|
11620
|
-
function isStreamingSyncCheckpoint(line) {
|
|
11621
|
-
return line.checkpoint != null;
|
|
11622
|
-
}
|
|
11623
|
-
function isStreamingSyncCheckpointComplete(line) {
|
|
11624
|
-
return line.checkpoint_complete != null;
|
|
11625
|
-
}
|
|
11626
|
-
function isStreamingSyncCheckpointPartiallyComplete(line) {
|
|
11627
|
-
return line.partial_checkpoint_complete != null;
|
|
11628
|
-
}
|
|
11629
|
-
function isStreamingSyncCheckpointDiff(line) {
|
|
11630
|
-
return line.checkpoint_diff != null;
|
|
11631
|
-
}
|
|
11632
|
-
function isContinueCheckpointRequest(request) {
|
|
11633
|
-
return (Array.isArray(request.buckets) &&
|
|
11634
|
-
typeof request.checkpoint_token == 'string');
|
|
11635
|
-
}
|
|
11636
|
-
function isSyncNewCheckpointRequest(request) {
|
|
11637
|
-
return typeof request.request_checkpoint == 'object';
|
|
11505
|
+
function isInterruptingInstruction(instruction) {
|
|
11506
|
+
return 'EstablishSyncStream' in instruction || 'CloseSyncStream' in instruction;
|
|
11638
11507
|
}
|
|
11639
11508
|
|
|
11640
11509
|
exports.LockType = void 0;
|
|
@@ -11649,35 +11518,21 @@ exports.SyncStreamConnectionMethod = void 0;
|
|
|
11649
11518
|
})(exports.SyncStreamConnectionMethod || (exports.SyncStreamConnectionMethod = {}));
|
|
11650
11519
|
exports.SyncClientImplementation = void 0;
|
|
11651
11520
|
(function (SyncClientImplementation) {
|
|
11652
|
-
/**
|
|
11653
|
-
* Decodes and handles sync lines received from the sync service in JavaScript.
|
|
11654
|
-
*
|
|
11655
|
-
* This is the default option.
|
|
11656
|
-
*
|
|
11657
|
-
* @deprecated We recommend the {@link RUST} client implementation for all apps. If you have issues with
|
|
11658
|
-
* the Rust client, please file an issue or reach out to us. The JavaScript client will be removed in a future
|
|
11659
|
-
* version of the PowerSync SDK.
|
|
11660
|
-
*/
|
|
11661
|
-
SyncClientImplementation["JAVASCRIPT"] = "js";
|
|
11662
11521
|
/**
|
|
11663
11522
|
* This implementation offloads the sync line decoding and handling into the PowerSync
|
|
11664
11523
|
* core extension.
|
|
11665
11524
|
*
|
|
11666
|
-
* This
|
|
11667
|
-
* recommended client implementation for all apps.
|
|
11525
|
+
* This is the only option, as an older JavaScript client implementation has been removed from the SDK.
|
|
11668
11526
|
*
|
|
11669
11527
|
* ## Compatibility warning
|
|
11670
11528
|
*
|
|
11671
11529
|
* The Rust sync client stores sync data in a format that is slightly different than the one used
|
|
11672
|
-
* by the old
|
|
11673
|
-
*
|
|
11674
|
-
* Further, the {@link JAVASCRIPT} client in recent versions of the PowerSync JS SDK (starting from
|
|
11675
|
-
* the version introducing {@link RUST} as an option) also supports the new format, so you can switch
|
|
11676
|
-
* back to {@link JAVASCRIPT} later.
|
|
11530
|
+
* by the old JavaScript client. When adopting the {@link RUST} client on existing databases, the PowerSync SDK will
|
|
11531
|
+
* migrate the format automatically.
|
|
11677
11532
|
*
|
|
11678
|
-
*
|
|
11679
|
-
*
|
|
11680
|
-
*
|
|
11533
|
+
* SDK versions supporting both the JavaScript and the Rust client support both formats with the JavaScript client
|
|
11534
|
+
* implementaiton. However, downgrading to an SDK version that only supports the JavaScript client would not be
|
|
11535
|
+
* possible anymore. Problematic SDK versions have been released before 2025-06-09.
|
|
11681
11536
|
*/
|
|
11682
11537
|
SyncClientImplementation["RUST"] = "rust";
|
|
11683
11538
|
})(exports.SyncClientImplementation || (exports.SyncClientImplementation = {}));
|
|
@@ -11700,13 +11555,7 @@ const DEFAULT_STREAM_CONNECTION_OPTIONS = {
|
|
|
11700
11555
|
serializedSchema: undefined,
|
|
11701
11556
|
includeDefaultStreams: true
|
|
11702
11557
|
};
|
|
11703
|
-
// The priority we assume when we receive checkpoint lines where no priority is set.
|
|
11704
|
-
// This is the default priority used by the sync service, but can be set to an arbitrary
|
|
11705
|
-
// value since sync services without priorities also won't send partial sync completion
|
|
11706
|
-
// messages.
|
|
11707
|
-
const FALLBACK_PRIORITY = 3;
|
|
11708
11558
|
class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
11709
|
-
_lastSyncedAt;
|
|
11710
11559
|
options;
|
|
11711
11560
|
abortController;
|
|
11712
11561
|
// In rare cases, mostly for tests, uploads can be triggered without being properly connected.
|
|
@@ -11716,6 +11565,7 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
11716
11565
|
streamingSyncPromise;
|
|
11717
11566
|
logger;
|
|
11718
11567
|
activeStreams;
|
|
11568
|
+
connectionMayHaveChanged = false;
|
|
11719
11569
|
isUploadingCrud = false;
|
|
11720
11570
|
notifyCompletedUploads;
|
|
11721
11571
|
handleActiveStreamsChange;
|
|
@@ -11795,9 +11645,6 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
11795
11645
|
this.crudUpdateListener = undefined;
|
|
11796
11646
|
this.uploadAbortController?.abort();
|
|
11797
11647
|
}
|
|
11798
|
-
async hasCompletedSync() {
|
|
11799
|
-
return this.options.adapter.hasCompletedSync();
|
|
11800
|
-
}
|
|
11801
11648
|
async getWriteCheckpoint() {
|
|
11802
11649
|
const clientId = await this.options.adapter.getClientId();
|
|
11803
11650
|
let path = `/write-checkpoint2.json?client_id=${clientId}`;
|
|
@@ -11879,7 +11726,7 @@ The next upload iteration will be delayed.`);
|
|
|
11879
11726
|
});
|
|
11880
11727
|
}
|
|
11881
11728
|
}
|
|
11882
|
-
this.uploadAbortController =
|
|
11729
|
+
this.uploadAbortController = undefined;
|
|
11883
11730
|
}
|
|
11884
11731
|
});
|
|
11885
11732
|
}
|
|
@@ -11995,6 +11842,11 @@ The next upload iteration will be delayed.`);
|
|
|
11995
11842
|
shouldDelayRetry = false;
|
|
11996
11843
|
// A disconnect was requested, we should not delay since there is no explicit retry
|
|
11997
11844
|
}
|
|
11845
|
+
else if (this.connectionMayHaveChanged && ex.message?.indexOf('No iteration is active') >= 0) {
|
|
11846
|
+
this.connectionMayHaveChanged = false;
|
|
11847
|
+
this.logger.info('Sync error after changed connection, retrying immediately');
|
|
11848
|
+
shouldDelayRetry = false;
|
|
11849
|
+
}
|
|
11998
11850
|
else {
|
|
11999
11851
|
this.logger.error(ex);
|
|
12000
11852
|
}
|
|
@@ -12025,17 +11877,14 @@ The next upload iteration will be delayed.`);
|
|
|
12025
11877
|
// Mark as disconnected if here
|
|
12026
11878
|
this.updateSyncStatus({ connected: false, connecting: false });
|
|
12027
11879
|
}
|
|
12028
|
-
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
|
|
12034
|
-
|
|
12035
|
-
|
|
12036
|
-
localDescriptions.set(entry.bucket, null);
|
|
12037
|
-
}
|
|
12038
|
-
return [req, localDescriptions];
|
|
11880
|
+
markConnectionMayHaveChanged() {
|
|
11881
|
+
// By setting this field, we'll immediately retry if the next sync event causes an error triggered by us not having
|
|
11882
|
+
// an active sync iteration on the connection in use.
|
|
11883
|
+
this.connectionMayHaveChanged = true;
|
|
11884
|
+
// This triggers a `powersync_control` invocation if a sync iteration is currently active. This is a cheap call to
|
|
11885
|
+
// make when no subscriptions have actually changed, we're mainly interested in this immediately throwing if no
|
|
11886
|
+
// iteration is active. That allows us to reconnect ASAP, instead of having to wait for the next sync line.
|
|
11887
|
+
this.handleActiveStreamsChange?.();
|
|
12039
11888
|
}
|
|
12040
11889
|
/**
|
|
12041
11890
|
* Older versions of the JS SDK used to encode subkeys as JSON in {@link OplogEntry.toJSON}.
|
|
@@ -12076,344 +11925,98 @@ The next upload iteration will be delayed.`);
|
|
|
12076
11925
|
if (invalidMetadata.length > 0) {
|
|
12077
11926
|
throw new Error(`Invalid appMetadata provided. Only string values are allowed. Invalid values: ${invalidMetadata.map(([key, value]) => `${key}: ${value}`).join(', ')}`);
|
|
12078
11927
|
}
|
|
12079
|
-
|
|
12080
|
-
this.
|
|
12081
|
-
if (clientImplementation == exports.SyncClientImplementation.JAVASCRIPT) {
|
|
12082
|
-
await this.legacyStreamingSyncIteration(signal, resolvedOptions);
|
|
12083
|
-
return null;
|
|
12084
|
-
}
|
|
12085
|
-
else {
|
|
12086
|
-
await this.requireKeyFormat(true);
|
|
12087
|
-
return await this.rustSyncIteration(signal, resolvedOptions);
|
|
12088
|
-
}
|
|
11928
|
+
await this.requireKeyFormat(true);
|
|
11929
|
+
return await this.rustSyncIteration(signal, resolvedOptions);
|
|
12089
11930
|
}
|
|
12090
11931
|
});
|
|
12091
11932
|
}
|
|
12092
|
-
|
|
12093
|
-
const
|
|
12094
|
-
|
|
12095
|
-
|
|
12096
|
-
|
|
12097
|
-
|
|
12098
|
-
this.logger.error('Sync streams require `clientImplementation: SyncClientImplementation.RUST` when connecting.');
|
|
12099
|
-
}
|
|
12100
|
-
this.logger.debug('Streaming sync iteration started');
|
|
12101
|
-
this.options.adapter.startSession();
|
|
12102
|
-
let [req, bucketMap] = await this.collectLocalBucketState();
|
|
12103
|
-
let targetCheckpoint = null;
|
|
12104
|
-
// A checkpoint that has been validated but not applied (e.g. due to pending local writes)
|
|
12105
|
-
let pendingValidatedCheckpoint = null;
|
|
12106
|
-
const clientId = await this.options.adapter.getClientId();
|
|
12107
|
-
const usingFixedKeyFormat = await this.requireKeyFormat(false);
|
|
12108
|
-
this.logger.debug('Requesting stream from server');
|
|
12109
|
-
const syncOptions = {
|
|
12110
|
-
path: '/sync/stream',
|
|
12111
|
-
abortSignal: signal,
|
|
12112
|
-
data: {
|
|
12113
|
-
buckets: req,
|
|
12114
|
-
include_checksum: true,
|
|
12115
|
-
raw_data: true,
|
|
12116
|
-
parameters: resolvedOptions.params,
|
|
12117
|
-
app_metadata: resolvedOptions.appMetadata,
|
|
12118
|
-
client_id: clientId
|
|
11933
|
+
receiveSyncLines(data) {
|
|
11934
|
+
const { options, connection } = data;
|
|
11935
|
+
const remote = this.options.remote;
|
|
11936
|
+
const openInner = async () => {
|
|
11937
|
+
if (connection.connectionMethod == exports.SyncStreamConnectionMethod.HTTP) {
|
|
11938
|
+
return await remote.fetchStream(options);
|
|
12119
11939
|
}
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
return JSON.parse(line);
|
|
12126
|
-
}
|
|
12127
|
-
else {
|
|
12128
|
-
// Directly enqueued by us
|
|
12129
|
-
return line;
|
|
12130
|
-
}
|
|
12131
|
-
});
|
|
12132
|
-
}
|
|
12133
|
-
else {
|
|
12134
|
-
const bson = await this.options.remote.getBSON();
|
|
12135
|
-
stream = await this.options.remote.socketStreamRaw({
|
|
12136
|
-
...syncOptions,
|
|
12137
|
-
...{ fetchStrategy: resolvedOptions.fetchStrategy }
|
|
12138
|
-
}, (payload) => {
|
|
12139
|
-
if (payload instanceof Uint8Array) {
|
|
12140
|
-
return bson.deserialize(payload);
|
|
12141
|
-
}
|
|
12142
|
-
else {
|
|
12143
|
-
// Directly enqueued by us
|
|
12144
|
-
return payload;
|
|
12145
|
-
}
|
|
12146
|
-
}, bson);
|
|
12147
|
-
}
|
|
12148
|
-
this.logger.debug('Stream established. Processing events');
|
|
12149
|
-
this.notifyCompletedUploads = () => {
|
|
12150
|
-
if (!stream.closed) {
|
|
12151
|
-
stream.enqueueData({ crud_upload_completed: null });
|
|
11940
|
+
else {
|
|
11941
|
+
return await this.options.remote.socketStreamRaw({
|
|
11942
|
+
...options,
|
|
11943
|
+
...{ fetchStrategy: connection.fetchStrategy }
|
|
11944
|
+
});
|
|
12152
11945
|
}
|
|
12153
11946
|
};
|
|
12154
|
-
|
|
12155
|
-
|
|
12156
|
-
|
|
12157
|
-
|
|
12158
|
-
|
|
12159
|
-
|
|
12160
|
-
if ('crud_upload_completed' in line) {
|
|
12161
|
-
if (pendingValidatedCheckpoint != null) {
|
|
12162
|
-
const { applied, endIteration } = await this.applyCheckpoint(pendingValidatedCheckpoint);
|
|
12163
|
-
if (applied) {
|
|
12164
|
-
pendingValidatedCheckpoint = null;
|
|
12165
|
-
}
|
|
12166
|
-
else if (endIteration) {
|
|
12167
|
-
break;
|
|
12168
|
-
}
|
|
11947
|
+
let inner;
|
|
11948
|
+
let done = false;
|
|
11949
|
+
return {
|
|
11950
|
+
async next() {
|
|
11951
|
+
if (done) {
|
|
11952
|
+
return doneResult;
|
|
12169
11953
|
}
|
|
12170
|
-
|
|
12171
|
-
|
|
12172
|
-
|
|
12173
|
-
|
|
12174
|
-
|
|
12175
|
-
|
|
12176
|
-
this.updateSyncStatus({
|
|
12177
|
-
connected: true
|
|
12178
|
-
});
|
|
12179
|
-
}
|
|
12180
|
-
if (isStreamingSyncCheckpoint(line)) {
|
|
12181
|
-
targetCheckpoint = line.checkpoint;
|
|
12182
|
-
// New checkpoint - existing validated checkpoint is no longer valid
|
|
12183
|
-
pendingValidatedCheckpoint = null;
|
|
12184
|
-
const bucketsToDelete = new Set(bucketMap.keys());
|
|
12185
|
-
const newBuckets = new Map();
|
|
12186
|
-
for (const checksum of line.checkpoint.buckets) {
|
|
12187
|
-
newBuckets.set(checksum.bucket, {
|
|
12188
|
-
name: checksum.bucket,
|
|
12189
|
-
priority: checksum.priority ?? FALLBACK_PRIORITY
|
|
11954
|
+
else if (inner == null) {
|
|
11955
|
+
inner = await openInner();
|
|
11956
|
+
// We're connected here, so we can tell the core extension about it.
|
|
11957
|
+
return valueResult({
|
|
11958
|
+
command: exports.PowerSyncControlCommand.CONNECTION_STATE,
|
|
11959
|
+
payload: 'established'
|
|
12190
11960
|
});
|
|
12191
|
-
bucketsToDelete.delete(checksum.bucket);
|
|
12192
|
-
}
|
|
12193
|
-
if (bucketsToDelete.size > 0) {
|
|
12194
|
-
this.logger.debug('Removing buckets', [...bucketsToDelete]);
|
|
12195
|
-
}
|
|
12196
|
-
bucketMap = newBuckets;
|
|
12197
|
-
await this.options.adapter.removeBuckets([...bucketsToDelete]);
|
|
12198
|
-
await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
|
|
12199
|
-
await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
|
|
12200
|
-
}
|
|
12201
|
-
else if (isStreamingSyncCheckpointComplete(line)) {
|
|
12202
|
-
const result = await this.applyCheckpoint(targetCheckpoint);
|
|
12203
|
-
if (result.endIteration) {
|
|
12204
|
-
return;
|
|
12205
|
-
}
|
|
12206
|
-
else if (!result.applied) {
|
|
12207
|
-
// "Could not apply checkpoint due to local data". We need to retry after
|
|
12208
|
-
// finishing uploads.
|
|
12209
|
-
pendingValidatedCheckpoint = targetCheckpoint;
|
|
12210
|
-
}
|
|
12211
|
-
else {
|
|
12212
|
-
// Nothing to retry later. This would likely already be null from the last
|
|
12213
|
-
// checksum or checksum_diff operation, but we make sure.
|
|
12214
|
-
pendingValidatedCheckpoint = null;
|
|
12215
11961
|
}
|
|
12216
|
-
}
|
|
12217
|
-
else if (isStreamingSyncCheckpointPartiallyComplete(line)) {
|
|
12218
|
-
const priority = line.partial_checkpoint_complete.priority;
|
|
12219
|
-
this.logger.debug('Partial checkpoint complete', priority);
|
|
12220
|
-
const result = await this.options.adapter.syncLocalDatabase(targetCheckpoint, priority);
|
|
12221
|
-
if (!result.checkpointValid) {
|
|
12222
|
-
// This means checksums failed. Start again with a new checkpoint.
|
|
12223
|
-
// TODO: better back-off
|
|
12224
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
12225
|
-
return;
|
|
12226
|
-
}
|
|
12227
|
-
else if (!result.ready) ;
|
|
12228
11962
|
else {
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
priorityStates.push({
|
|
12234
|
-
priority,
|
|
12235
|
-
lastSyncedAt: new Date(),
|
|
12236
|
-
hasSynced: true
|
|
12237
|
-
});
|
|
12238
|
-
this.updateSyncStatus({
|
|
12239
|
-
connected: true,
|
|
12240
|
-
priorityStatusEntries: priorityStates
|
|
12241
|
-
});
|
|
12242
|
-
}
|
|
12243
|
-
}
|
|
12244
|
-
else if (isStreamingSyncCheckpointDiff(line)) {
|
|
12245
|
-
// TODO: It may be faster to just keep track of the diff, instead of the entire checkpoint
|
|
12246
|
-
if (targetCheckpoint == null) {
|
|
12247
|
-
throw new Error('Checkpoint diff without previous checkpoint');
|
|
12248
|
-
}
|
|
12249
|
-
// New checkpoint - existing validated checkpoint is no longer valid
|
|
12250
|
-
pendingValidatedCheckpoint = null;
|
|
12251
|
-
const diff = line.checkpoint_diff;
|
|
12252
|
-
const newBuckets = new Map();
|
|
12253
|
-
for (const checksum of targetCheckpoint.buckets) {
|
|
12254
|
-
newBuckets.set(checksum.bucket, checksum);
|
|
12255
|
-
}
|
|
12256
|
-
for (const checksum of diff.updated_buckets) {
|
|
12257
|
-
newBuckets.set(checksum.bucket, checksum);
|
|
12258
|
-
}
|
|
12259
|
-
for (const bucket of diff.removed_buckets) {
|
|
12260
|
-
newBuckets.delete(bucket);
|
|
12261
|
-
}
|
|
12262
|
-
const newCheckpoint = {
|
|
12263
|
-
last_op_id: diff.last_op_id,
|
|
12264
|
-
buckets: [...newBuckets.values()],
|
|
12265
|
-
write_checkpoint: diff.write_checkpoint
|
|
12266
|
-
};
|
|
12267
|
-
targetCheckpoint = newCheckpoint;
|
|
12268
|
-
await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
|
|
12269
|
-
bucketMap = new Map();
|
|
12270
|
-
newBuckets.forEach((checksum, name) => bucketMap.set(name, {
|
|
12271
|
-
name: checksum.bucket,
|
|
12272
|
-
priority: checksum.priority ?? FALLBACK_PRIORITY
|
|
12273
|
-
}));
|
|
12274
|
-
const bucketsToDelete = diff.removed_buckets;
|
|
12275
|
-
if (bucketsToDelete.length > 0) {
|
|
12276
|
-
this.logger.debug('Remove buckets', bucketsToDelete);
|
|
12277
|
-
}
|
|
12278
|
-
await this.options.adapter.removeBuckets(bucketsToDelete);
|
|
12279
|
-
await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
|
|
12280
|
-
}
|
|
12281
|
-
else if (isStreamingSyncData(line)) {
|
|
12282
|
-
const { data } = line;
|
|
12283
|
-
const previousProgress = this.syncStatus.dataFlowStatus.downloadProgress;
|
|
12284
|
-
let updatedProgress = null;
|
|
12285
|
-
if (previousProgress) {
|
|
12286
|
-
updatedProgress = { ...previousProgress };
|
|
12287
|
-
const progressForBucket = updatedProgress[data.bucket];
|
|
12288
|
-
if (progressForBucket) {
|
|
12289
|
-
updatedProgress[data.bucket] = {
|
|
12290
|
-
...progressForBucket,
|
|
12291
|
-
since_last: progressForBucket.since_last + data.data.length
|
|
12292
|
-
};
|
|
11963
|
+
const event = await inner.next();
|
|
11964
|
+
if (event.done) {
|
|
11965
|
+
done = true;
|
|
11966
|
+
return valueResult({ command: exports.PowerSyncControlCommand.CONNECTION_STATE, payload: 'end' });
|
|
12293
11967
|
}
|
|
12294
|
-
|
|
12295
|
-
|
|
12296
|
-
|
|
12297
|
-
|
|
12298
|
-
|
|
11968
|
+
else {
|
|
11969
|
+
return valueResult({
|
|
11970
|
+
command: typeof event.value == 'string'
|
|
11971
|
+
? exports.PowerSyncControlCommand.PROCESS_TEXT_LINE
|
|
11972
|
+
: exports.PowerSyncControlCommand.PROCESS_BSON_LINE,
|
|
11973
|
+
payload: event.value
|
|
11974
|
+
});
|
|
12299
11975
|
}
|
|
12300
|
-
});
|
|
12301
|
-
await this.options.adapter.saveSyncData({ buckets: [SyncDataBucket.fromRow(data)] }, usingFixedKeyFormat);
|
|
12302
|
-
}
|
|
12303
|
-
else if (isStreamingKeepalive(line)) {
|
|
12304
|
-
const remaining_seconds = line.token_expires_in;
|
|
12305
|
-
if (remaining_seconds == 0) {
|
|
12306
|
-
// Connection would be closed automatically right after this
|
|
12307
|
-
this.logger.debug('Token expiring; reconnect');
|
|
12308
|
-
/**
|
|
12309
|
-
* For a rare case where the backend connector does not update the token
|
|
12310
|
-
* (uses the same one), this should have some delay.
|
|
12311
|
-
*/
|
|
12312
|
-
await this.delayRetry();
|
|
12313
|
-
return;
|
|
12314
|
-
}
|
|
12315
|
-
else if (remaining_seconds < 30) {
|
|
12316
|
-
this.logger.debug('Token will expire soon; reconnect');
|
|
12317
|
-
// Pre-emptively refresh the token
|
|
12318
|
-
this.options.remote.invalidateCredentials();
|
|
12319
|
-
return;
|
|
12320
11976
|
}
|
|
12321
|
-
this.triggerCrudUpload();
|
|
12322
11977
|
}
|
|
12323
|
-
|
|
12324
|
-
this.logger.debug('Received unknown sync line', line);
|
|
12325
|
-
}
|
|
12326
|
-
}
|
|
12327
|
-
this.logger.debug('Stream input empty');
|
|
12328
|
-
// Connection closed. Likely due to auth issue.
|
|
12329
|
-
return;
|
|
11978
|
+
};
|
|
12330
11979
|
}
|
|
12331
11980
|
async rustSyncIteration(signal, resolvedOptions) {
|
|
12332
11981
|
const syncImplementation = this;
|
|
12333
11982
|
const adapter = this.options.adapter;
|
|
12334
11983
|
const remote = this.options.remote;
|
|
12335
|
-
let receivingLines = null;
|
|
12336
|
-
let hadSyncLine = false;
|
|
12337
11984
|
let hideDisconnectOnRestart = false;
|
|
11985
|
+
let notifyTokenRefreshed;
|
|
12338
11986
|
if (signal.aborted) {
|
|
12339
11987
|
throw new AbortOperation('Connection request has been aborted');
|
|
12340
11988
|
}
|
|
12341
|
-
|
|
12342
|
-
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
|
|
12347
|
-
async function connect(instr) {
|
|
12348
|
-
const syncOptions = {
|
|
12349
|
-
path: '/sync/stream',
|
|
12350
|
-
abortSignal: abortController.signal,
|
|
12351
|
-
data: instr.request
|
|
11989
|
+
function startCommand() {
|
|
11990
|
+
const options = {
|
|
11991
|
+
parameters: resolvedOptions.params,
|
|
11992
|
+
app_metadata: resolvedOptions.appMetadata,
|
|
11993
|
+
active_streams: syncImplementation.activeStreams,
|
|
11994
|
+
include_defaults: resolvedOptions.includeDefaultStreams
|
|
12352
11995
|
};
|
|
12353
|
-
if (resolvedOptions.
|
|
12354
|
-
|
|
12355
|
-
if (typeof line == 'string') {
|
|
12356
|
-
return {
|
|
12357
|
-
command: exports.PowerSyncControlCommand.PROCESS_TEXT_LINE,
|
|
12358
|
-
payload: line
|
|
12359
|
-
};
|
|
12360
|
-
}
|
|
12361
|
-
else {
|
|
12362
|
-
// Directly enqueued by us
|
|
12363
|
-
return line;
|
|
12364
|
-
}
|
|
12365
|
-
});
|
|
12366
|
-
}
|
|
12367
|
-
else {
|
|
12368
|
-
controlInvocations = await remote.socketStreamRaw({
|
|
12369
|
-
...syncOptions,
|
|
12370
|
-
fetchStrategy: resolvedOptions.fetchStrategy
|
|
12371
|
-
}, (payload) => {
|
|
12372
|
-
if (payload instanceof Uint8Array) {
|
|
12373
|
-
return {
|
|
12374
|
-
command: exports.PowerSyncControlCommand.PROCESS_BSON_LINE,
|
|
12375
|
-
payload: payload
|
|
12376
|
-
};
|
|
12377
|
-
}
|
|
12378
|
-
else {
|
|
12379
|
-
// Directly enqueued by us
|
|
12380
|
-
return payload;
|
|
12381
|
-
}
|
|
12382
|
-
});
|
|
12383
|
-
}
|
|
12384
|
-
// The rust client will set connected: true after the first sync line because that's when it gets invoked, but
|
|
12385
|
-
// we're already connected here and can report that.
|
|
12386
|
-
syncImplementation.updateSyncStatus({ connected: true });
|
|
12387
|
-
try {
|
|
12388
|
-
while (!controlInvocations.closed) {
|
|
12389
|
-
const line = await controlInvocations.read();
|
|
12390
|
-
if (line == null) {
|
|
12391
|
-
return;
|
|
12392
|
-
}
|
|
12393
|
-
await control(line.command, line.payload);
|
|
12394
|
-
if (!hadSyncLine) {
|
|
12395
|
-
syncImplementation.triggerCrudUpload();
|
|
12396
|
-
hadSyncLine = true;
|
|
12397
|
-
}
|
|
12398
|
-
}
|
|
12399
|
-
}
|
|
12400
|
-
finally {
|
|
12401
|
-
const activeInstructions = controlInvocations;
|
|
12402
|
-
// We concurrently add events to the active data stream when e.g. a CRUD upload is completed or a token is
|
|
12403
|
-
// refreshed. That would throw after closing (and we can't handle those events either way), so set this back
|
|
12404
|
-
// to null.
|
|
12405
|
-
controlInvocations = null;
|
|
12406
|
-
await activeInstructions.close();
|
|
11996
|
+
if (resolvedOptions.serializedSchema) {
|
|
11997
|
+
options.schema = resolvedOptions.serializedSchema;
|
|
12407
11998
|
}
|
|
11999
|
+
return invokePowerSyncControl(exports.PowerSyncControlCommand.START, JSON.stringify(options));
|
|
12408
12000
|
}
|
|
12409
12001
|
async function stop() {
|
|
12410
|
-
await
|
|
12002
|
+
const instructions = await invokePowerSyncControl(exports.PowerSyncControlCommand.STOP);
|
|
12003
|
+
for (const instruction of instructions) {
|
|
12004
|
+
// We don't need to handle interrupting instructions since we're unconditionally ending the sync iteration at
|
|
12005
|
+
// this point.
|
|
12006
|
+
if (isInterruptingInstruction(instruction))
|
|
12007
|
+
continue;
|
|
12008
|
+
await handleInstruction(instruction);
|
|
12009
|
+
}
|
|
12411
12010
|
}
|
|
12412
|
-
async function
|
|
12011
|
+
async function invokePowerSyncControl(op, payload) {
|
|
12413
12012
|
const rawResponse = await adapter.control(op, payload ?? null);
|
|
12414
12013
|
const logger = syncImplementation.logger;
|
|
12415
12014
|
logger.trace('powersync_control', op, payload == null || typeof payload == 'string' ? payload : '<bytes>', rawResponse);
|
|
12416
|
-
|
|
12015
|
+
if (op != exports.PowerSyncControlCommand.STOP) {
|
|
12016
|
+
// Evidently we have a working connection here, otherwise powersync_control would have failed.
|
|
12017
|
+
syncImplementation.connectionMayHaveChanged = false;
|
|
12018
|
+
}
|
|
12019
|
+
return JSON.parse(rawResponse);
|
|
12417
12020
|
}
|
|
12418
12021
|
async function handleInstruction(instruction) {
|
|
12419
12022
|
if ('LogLine' in instruction) {
|
|
@@ -12432,13 +12035,6 @@ The next upload iteration will be delayed.`);
|
|
|
12432
12035
|
else if ('UpdateSyncStatus' in instruction) {
|
|
12433
12036
|
syncImplementation.updateSyncStatus(coreStatusToJs(instruction.UpdateSyncStatus.status));
|
|
12434
12037
|
}
|
|
12435
|
-
else if ('EstablishSyncStream' in instruction) {
|
|
12436
|
-
if (receivingLines != null) {
|
|
12437
|
-
// Already connected, this shouldn't happen during a single iteration.
|
|
12438
|
-
throw 'Unexpected request to establish sync stream, already connected';
|
|
12439
|
-
}
|
|
12440
|
-
receivingLines = connect(instruction.EstablishSyncStream);
|
|
12441
|
-
}
|
|
12442
12038
|
else if ('FetchCredentials' in instruction) {
|
|
12443
12039
|
if (instruction.FetchCredentials.did_expire) {
|
|
12444
12040
|
remote.invalidateCredentials();
|
|
@@ -12447,16 +12043,12 @@ The next upload iteration will be delayed.`);
|
|
|
12447
12043
|
remote.invalidateCredentials();
|
|
12448
12044
|
// Restart iteration after the credentials have been refreshed.
|
|
12449
12045
|
remote.fetchCredentials().then((_) => {
|
|
12450
|
-
|
|
12046
|
+
notifyTokenRefreshed?.();
|
|
12451
12047
|
}, (err) => {
|
|
12452
12048
|
syncImplementation.logger.warn('Could not prefetch credentials', err);
|
|
12453
12049
|
});
|
|
12454
12050
|
}
|
|
12455
12051
|
}
|
|
12456
|
-
else if ('CloseSyncStream' in instruction) {
|
|
12457
|
-
abortController.abort();
|
|
12458
|
-
hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
|
|
12459
|
-
}
|
|
12460
12052
|
else if ('FlushFileSystem' in instruction) ;
|
|
12461
12053
|
else if ('DidCompleteSync' in instruction) {
|
|
12462
12054
|
syncImplementation.updateSyncStatus({
|
|
@@ -12466,105 +12058,83 @@ The next upload iteration will be delayed.`);
|
|
|
12466
12058
|
});
|
|
12467
12059
|
}
|
|
12468
12060
|
}
|
|
12469
|
-
async function handleInstructions(instructions) {
|
|
12470
|
-
for (const instr of instructions) {
|
|
12471
|
-
await handleInstruction(instr);
|
|
12472
|
-
}
|
|
12473
|
-
}
|
|
12474
12061
|
try {
|
|
12475
|
-
const
|
|
12476
|
-
|
|
12477
|
-
|
|
12478
|
-
|
|
12479
|
-
|
|
12480
|
-
|
|
12481
|
-
|
|
12482
|
-
|
|
12062
|
+
const defaultResult = { immediateRestart: false };
|
|
12063
|
+
// Pending sync lines received from the service, as well as local events that trigger a powersync_control
|
|
12064
|
+
// invocation (local events include refreshed tokens and completed uploads).
|
|
12065
|
+
// This is a single data stream so that we can handle all control calls from a single place.
|
|
12066
|
+
let controlInvocations = null;
|
|
12067
|
+
for (const startInstruction of await startCommand()) {
|
|
12068
|
+
if ('EstablishSyncStream' in startInstruction) {
|
|
12069
|
+
const syncOptions = {
|
|
12070
|
+
path: '/sync/stream',
|
|
12071
|
+
abortSignal: signal,
|
|
12072
|
+
data: startInstruction.EstablishSyncStream.request
|
|
12073
|
+
};
|
|
12074
|
+
controlInvocations = injectable(syncImplementation.receiveSyncLines({
|
|
12075
|
+
options: syncOptions,
|
|
12076
|
+
connection: resolvedOptions
|
|
12077
|
+
}));
|
|
12078
|
+
}
|
|
12079
|
+
else if ('CloseSyncStream' in startInstruction) {
|
|
12080
|
+
return defaultResult;
|
|
12081
|
+
}
|
|
12082
|
+
else {
|
|
12083
|
+
await handleInstruction(startInstruction);
|
|
12084
|
+
}
|
|
12483
12085
|
}
|
|
12484
|
-
|
|
12086
|
+
if (controlInvocations == null)
|
|
12087
|
+
return defaultResult;
|
|
12485
12088
|
this.notifyCompletedUploads = () => {
|
|
12486
|
-
|
|
12487
|
-
controlInvocations.enqueueData({ command: exports.PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
|
|
12488
|
-
}
|
|
12089
|
+
controlInvocations.inject({ command: exports.PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
|
|
12489
12090
|
};
|
|
12490
12091
|
this.handleActiveStreamsChange = () => {
|
|
12491
|
-
|
|
12492
|
-
|
|
12493
|
-
|
|
12494
|
-
|
|
12495
|
-
});
|
|
12496
|
-
}
|
|
12092
|
+
controlInvocations.inject({
|
|
12093
|
+
command: exports.PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
|
|
12094
|
+
payload: JSON.stringify(this.activeStreams)
|
|
12095
|
+
});
|
|
12497
12096
|
};
|
|
12498
|
-
|
|
12097
|
+
notifyTokenRefreshed = () => {
|
|
12098
|
+
controlInvocations.inject({
|
|
12099
|
+
command: exports.PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED
|
|
12100
|
+
});
|
|
12101
|
+
};
|
|
12102
|
+
let hadSyncLine = false;
|
|
12103
|
+
loop: while (true) {
|
|
12104
|
+
const { done, value } = await controlInvocations.next();
|
|
12105
|
+
if (done)
|
|
12106
|
+
break;
|
|
12107
|
+
if (!hadSyncLine) {
|
|
12108
|
+
// Trigger a local CRUD upload when the first sync line has been received, this allows uploading local changes
|
|
12109
|
+
// that have been made while offline or disconnected.
|
|
12110
|
+
if (value.command == exports.PowerSyncControlCommand.PROCESS_TEXT_LINE ||
|
|
12111
|
+
value.command == exports.PowerSyncControlCommand.PROCESS_BSON_LINE) {
|
|
12112
|
+
hadSyncLine = true;
|
|
12113
|
+
this.triggerCrudUpload?.();
|
|
12114
|
+
}
|
|
12115
|
+
}
|
|
12116
|
+
const instructions = await invokePowerSyncControl(value.command, value.payload);
|
|
12117
|
+
for (const instruction of instructions) {
|
|
12118
|
+
if ('EstablishSyncStream' in instruction) {
|
|
12119
|
+
throw new Error('Received EstablishSyncStream while already connected.');
|
|
12120
|
+
}
|
|
12121
|
+
else if ('CloseSyncStream' in instruction) {
|
|
12122
|
+
hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
|
|
12123
|
+
break loop;
|
|
12124
|
+
}
|
|
12125
|
+
else {
|
|
12126
|
+
await handleInstruction(instruction);
|
|
12127
|
+
}
|
|
12128
|
+
}
|
|
12129
|
+
}
|
|
12499
12130
|
}
|
|
12500
12131
|
finally {
|
|
12501
12132
|
this.notifyCompletedUploads = this.handleActiveStreamsChange = undefined;
|
|
12133
|
+
notifyTokenRefreshed = undefined;
|
|
12502
12134
|
await stop();
|
|
12503
12135
|
}
|
|
12504
12136
|
return { immediateRestart: hideDisconnectOnRestart };
|
|
12505
12137
|
}
|
|
12506
|
-
async updateSyncStatusForStartingCheckpoint(checkpoint) {
|
|
12507
|
-
const localProgress = await this.options.adapter.getBucketOperationProgress();
|
|
12508
|
-
const progress = {};
|
|
12509
|
-
let invalidated = false;
|
|
12510
|
-
for (const bucket of checkpoint.buckets) {
|
|
12511
|
-
const savedProgress = localProgress[bucket.bucket];
|
|
12512
|
-
const atLast = savedProgress?.atLast ?? 0;
|
|
12513
|
-
const sinceLast = savedProgress?.sinceLast ?? 0;
|
|
12514
|
-
progress[bucket.bucket] = {
|
|
12515
|
-
// The fallback priority doesn't matter here, but 3 is the one newer versions of the sync service
|
|
12516
|
-
// will use by default.
|
|
12517
|
-
priority: bucket.priority ?? 3,
|
|
12518
|
-
at_last: atLast,
|
|
12519
|
-
since_last: sinceLast,
|
|
12520
|
-
target_count: bucket.count ?? 0
|
|
12521
|
-
};
|
|
12522
|
-
if (bucket.count != null && bucket.count < atLast + sinceLast) {
|
|
12523
|
-
// Either due to a defrag / sync rule deploy or a compaction operation, the size
|
|
12524
|
-
// of the bucket shrank so much that the local ops exceed the ops in the updated
|
|
12525
|
-
// bucket. We can't prossibly report progress in this case (it would overshoot 100%).
|
|
12526
|
-
invalidated = true;
|
|
12527
|
-
}
|
|
12528
|
-
}
|
|
12529
|
-
if (invalidated) {
|
|
12530
|
-
for (const bucket in progress) {
|
|
12531
|
-
const bucketProgress = progress[bucket];
|
|
12532
|
-
bucketProgress.at_last = 0;
|
|
12533
|
-
bucketProgress.since_last = 0;
|
|
12534
|
-
}
|
|
12535
|
-
}
|
|
12536
|
-
this.updateSyncStatus({
|
|
12537
|
-
dataFlow: {
|
|
12538
|
-
downloading: true,
|
|
12539
|
-
downloadProgress: progress
|
|
12540
|
-
}
|
|
12541
|
-
});
|
|
12542
|
-
}
|
|
12543
|
-
async applyCheckpoint(checkpoint) {
|
|
12544
|
-
let result = await this.options.adapter.syncLocalDatabase(checkpoint);
|
|
12545
|
-
if (!result.checkpointValid) {
|
|
12546
|
-
this.logger.debug(`Checksum mismatch in checkpoint ${checkpoint.last_op_id}, will reconnect`);
|
|
12547
|
-
// This means checksums failed. Start again with a new checkpoint.
|
|
12548
|
-
// TODO: better back-off
|
|
12549
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
12550
|
-
return { applied: false, endIteration: true };
|
|
12551
|
-
}
|
|
12552
|
-
else if (!result.ready) {
|
|
12553
|
-
this.logger.debug(`Could not apply checkpoint ${checkpoint.last_op_id} due to local data. We will retry applying the checkpoint after that upload is completed.`);
|
|
12554
|
-
return { applied: false, endIteration: false };
|
|
12555
|
-
}
|
|
12556
|
-
this.logger.debug(`Applied checkpoint ${checkpoint.last_op_id}`, checkpoint);
|
|
12557
|
-
this.updateSyncStatus({
|
|
12558
|
-
connected: true,
|
|
12559
|
-
lastSyncedAt: new Date(),
|
|
12560
|
-
dataFlow: {
|
|
12561
|
-
downloading: false,
|
|
12562
|
-
downloadProgress: null,
|
|
12563
|
-
downloadError: undefined
|
|
12564
|
-
}
|
|
12565
|
-
});
|
|
12566
|
-
return { applied: true, endIteration: false };
|
|
12567
|
-
}
|
|
12568
12138
|
updateSyncStatus(options) {
|
|
12569
12139
|
const updatedStatus = new SyncStatus({
|
|
12570
12140
|
connected: options.connected ?? this.syncStatus.connected,
|
|
@@ -14103,14 +13673,12 @@ class SqliteBucketStorage extends BaseObserver {
|
|
|
14103
13673
|
db;
|
|
14104
13674
|
logger;
|
|
14105
13675
|
tableNames;
|
|
14106
|
-
_hasCompletedSync;
|
|
14107
13676
|
updateListener;
|
|
14108
13677
|
_clientId;
|
|
14109
13678
|
constructor(db, logger = Logger.get('SqliteBucketStorage')) {
|
|
14110
13679
|
super();
|
|
14111
13680
|
this.db = db;
|
|
14112
13681
|
this.logger = logger;
|
|
14113
|
-
this._hasCompletedSync = false;
|
|
14114
13682
|
this.tableNames = new Set();
|
|
14115
13683
|
this.updateListener = db.registerListener({
|
|
14116
13684
|
tablesUpdated: (update) => {
|
|
@@ -14122,7 +13690,6 @@ class SqliteBucketStorage extends BaseObserver {
|
|
|
14122
13690
|
});
|
|
14123
13691
|
}
|
|
14124
13692
|
async init() {
|
|
14125
|
-
this._hasCompletedSync = false;
|
|
14126
13693
|
const existingTableRows = await this.db.getAll(`SELECT name FROM sqlite_master WHERE type='table' AND name GLOB 'ps_data_*'`);
|
|
14127
13694
|
for (const row of existingTableRows ?? []) {
|
|
14128
13695
|
this.tableNames.add(row.name);
|
|
@@ -14144,156 +13711,6 @@ class SqliteBucketStorage extends BaseObserver {
|
|
|
14144
13711
|
getMaxOpId() {
|
|
14145
13712
|
return MAX_OP_ID;
|
|
14146
13713
|
}
|
|
14147
|
-
/**
|
|
14148
|
-
* Reset any caches.
|
|
14149
|
-
*/
|
|
14150
|
-
startSession() { }
|
|
14151
|
-
async getBucketStates() {
|
|
14152
|
-
const result = await this.db.getAll("SELECT name as bucket, cast(last_op as TEXT) as op_id FROM ps_buckets WHERE pending_delete = 0 AND name != '$local'");
|
|
14153
|
-
return result;
|
|
14154
|
-
}
|
|
14155
|
-
async getBucketOperationProgress() {
|
|
14156
|
-
const rows = await this.db.getAll('SELECT name, count_at_last, count_since_last FROM ps_buckets');
|
|
14157
|
-
return Object.fromEntries(rows.map((r) => [r.name, { atLast: r.count_at_last, sinceLast: r.count_since_last }]));
|
|
14158
|
-
}
|
|
14159
|
-
async saveSyncData(batch, fixedKeyFormat = false) {
|
|
14160
|
-
await this.writeTransaction(async (tx) => {
|
|
14161
|
-
for (const b of batch.buckets) {
|
|
14162
|
-
await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
|
|
14163
|
-
'save',
|
|
14164
|
-
JSON.stringify({ buckets: [b.toJSON(fixedKeyFormat)] })
|
|
14165
|
-
]);
|
|
14166
|
-
this.logger.debug(`Saved batch of data for bucket: ${b.bucket}, operations: ${b.data.length}`);
|
|
14167
|
-
}
|
|
14168
|
-
});
|
|
14169
|
-
}
|
|
14170
|
-
async removeBuckets(buckets) {
|
|
14171
|
-
for (const bucket of buckets) {
|
|
14172
|
-
await this.deleteBucket(bucket);
|
|
14173
|
-
}
|
|
14174
|
-
}
|
|
14175
|
-
/**
|
|
14176
|
-
* Mark a bucket for deletion.
|
|
14177
|
-
*/
|
|
14178
|
-
async deleteBucket(bucket) {
|
|
14179
|
-
await this.writeTransaction(async (tx) => {
|
|
14180
|
-
await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', ['delete_bucket', bucket]);
|
|
14181
|
-
});
|
|
14182
|
-
this.logger.debug(`Done deleting bucket ${bucket}`);
|
|
14183
|
-
}
|
|
14184
|
-
async hasCompletedSync() {
|
|
14185
|
-
if (this._hasCompletedSync) {
|
|
14186
|
-
return true;
|
|
14187
|
-
}
|
|
14188
|
-
const r = await this.db.get(`SELECT powersync_last_synced_at() as synced_at`);
|
|
14189
|
-
const completed = r.synced_at != null;
|
|
14190
|
-
if (completed) {
|
|
14191
|
-
this._hasCompletedSync = true;
|
|
14192
|
-
}
|
|
14193
|
-
return completed;
|
|
14194
|
-
}
|
|
14195
|
-
async syncLocalDatabase(checkpoint, priority) {
|
|
14196
|
-
const r = await this.validateChecksums(checkpoint, priority);
|
|
14197
|
-
if (!r.checkpointValid) {
|
|
14198
|
-
this.logger.error('Checksums failed for', r.checkpointFailures);
|
|
14199
|
-
for (const b of r.checkpointFailures ?? []) {
|
|
14200
|
-
await this.deleteBucket(b);
|
|
14201
|
-
}
|
|
14202
|
-
return { ready: false, checkpointValid: false, checkpointFailures: r.checkpointFailures };
|
|
14203
|
-
}
|
|
14204
|
-
if (priority == null) {
|
|
14205
|
-
this.logger.debug(`Validated checksums checkpoint ${checkpoint.last_op_id}`);
|
|
14206
|
-
}
|
|
14207
|
-
else {
|
|
14208
|
-
this.logger.debug(`Validated checksums for partial checkpoint ${checkpoint.last_op_id}, priority ${priority}`);
|
|
14209
|
-
}
|
|
14210
|
-
let buckets = checkpoint.buckets;
|
|
14211
|
-
if (priority !== undefined) {
|
|
14212
|
-
buckets = buckets.filter((b) => hasMatchingPriority(priority, b));
|
|
14213
|
-
}
|
|
14214
|
-
const bucketNames = buckets.map((b) => b.bucket);
|
|
14215
|
-
await this.writeTransaction(async (tx) => {
|
|
14216
|
-
await tx.execute(`UPDATE ps_buckets SET last_op = ? WHERE name IN (SELECT json_each.value FROM json_each(?))`, [
|
|
14217
|
-
checkpoint.last_op_id,
|
|
14218
|
-
JSON.stringify(bucketNames)
|
|
14219
|
-
]);
|
|
14220
|
-
if (priority == null && checkpoint.write_checkpoint) {
|
|
14221
|
-
await tx.execute("UPDATE ps_buckets SET last_op = ? WHERE name = '$local'", [checkpoint.write_checkpoint]);
|
|
14222
|
-
}
|
|
14223
|
-
});
|
|
14224
|
-
const valid = await this.updateObjectsFromBuckets(checkpoint, priority);
|
|
14225
|
-
if (!valid) {
|
|
14226
|
-
return { ready: false, checkpointValid: true };
|
|
14227
|
-
}
|
|
14228
|
-
return {
|
|
14229
|
-
ready: true,
|
|
14230
|
-
checkpointValid: true
|
|
14231
|
-
};
|
|
14232
|
-
}
|
|
14233
|
-
/**
|
|
14234
|
-
* Atomically update the local state to the current checkpoint.
|
|
14235
|
-
*
|
|
14236
|
-
* This includes creating new tables, dropping old tables, and copying data over from the oplog.
|
|
14237
|
-
*/
|
|
14238
|
-
async updateObjectsFromBuckets(checkpoint, priority) {
|
|
14239
|
-
let arg = '';
|
|
14240
|
-
if (priority !== undefined) {
|
|
14241
|
-
const affectedBuckets = [];
|
|
14242
|
-
for (const desc of checkpoint.buckets) {
|
|
14243
|
-
if (hasMatchingPriority(priority, desc)) {
|
|
14244
|
-
affectedBuckets.push(desc.bucket);
|
|
14245
|
-
}
|
|
14246
|
-
}
|
|
14247
|
-
arg = JSON.stringify({ priority, buckets: affectedBuckets });
|
|
14248
|
-
}
|
|
14249
|
-
return this.writeTransaction(async (tx) => {
|
|
14250
|
-
const { insertId: result } = await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
|
|
14251
|
-
'sync_local',
|
|
14252
|
-
arg
|
|
14253
|
-
]);
|
|
14254
|
-
if (result == 1) {
|
|
14255
|
-
if (priority == null) {
|
|
14256
|
-
const bucketToCount = Object.fromEntries(checkpoint.buckets.map((b) => [b.bucket, b.count]));
|
|
14257
|
-
// The two parameters could be replaced with one, but: https://github.com/powersync-ja/better-sqlite3/pull/6
|
|
14258
|
-
const jsonBucketCount = JSON.stringify(bucketToCount);
|
|
14259
|
-
await tx.execute("UPDATE ps_buckets SET count_since_last = 0, count_at_last = ?->name WHERE name != '$local' AND ?->name IS NOT NULL", [jsonBucketCount, jsonBucketCount]);
|
|
14260
|
-
}
|
|
14261
|
-
return true;
|
|
14262
|
-
}
|
|
14263
|
-
else {
|
|
14264
|
-
return false;
|
|
14265
|
-
}
|
|
14266
|
-
});
|
|
14267
|
-
}
|
|
14268
|
-
async validateChecksums(checkpoint, priority) {
|
|
14269
|
-
if (priority !== undefined) {
|
|
14270
|
-
// Only validate the buckets within the priority we care about
|
|
14271
|
-
const newBuckets = checkpoint.buckets.filter((cs) => hasMatchingPriority(priority, cs));
|
|
14272
|
-
checkpoint = { ...checkpoint, buckets: newBuckets };
|
|
14273
|
-
}
|
|
14274
|
-
const rs = await this.db.execute('SELECT powersync_validate_checkpoint(?) as result', [
|
|
14275
|
-
JSON.stringify({ ...checkpoint })
|
|
14276
|
-
]);
|
|
14277
|
-
const resultItem = rs.rows?.item(0);
|
|
14278
|
-
if (!resultItem) {
|
|
14279
|
-
return {
|
|
14280
|
-
checkpointValid: false,
|
|
14281
|
-
ready: false,
|
|
14282
|
-
checkpointFailures: []
|
|
14283
|
-
};
|
|
14284
|
-
}
|
|
14285
|
-
const result = JSON.parse(resultItem['result']);
|
|
14286
|
-
if (result['valid']) {
|
|
14287
|
-
return { ready: true, checkpointValid: true };
|
|
14288
|
-
}
|
|
14289
|
-
else {
|
|
14290
|
-
return {
|
|
14291
|
-
checkpointValid: false,
|
|
14292
|
-
ready: false,
|
|
14293
|
-
checkpointFailures: result['failed_buckets']
|
|
14294
|
-
};
|
|
14295
|
-
}
|
|
14296
|
-
}
|
|
14297
13714
|
async updateLocalTarget(cb) {
|
|
14298
13715
|
const rs1 = await this.db.getAll("SELECT target_op FROM ps_buckets WHERE name = '$local' AND target_op = CAST(? as INTEGER)", [MAX_OP_ID]);
|
|
14299
13716
|
if (!rs1.length) {
|
|
@@ -14384,12 +13801,6 @@ class SqliteBucketStorage extends BaseObserver {
|
|
|
14384
13801
|
async writeTransaction(callback, options) {
|
|
14385
13802
|
return this.db.writeTransaction(callback, options);
|
|
14386
13803
|
}
|
|
14387
|
-
/**
|
|
14388
|
-
* Set a target checkpoint.
|
|
14389
|
-
*/
|
|
14390
|
-
async setTargetCheckpoint(checkpoint) {
|
|
14391
|
-
// No-op for now
|
|
14392
|
-
}
|
|
14393
13804
|
async control(op, payload) {
|
|
14394
13805
|
return await this.writeTransaction(async (tx) => {
|
|
14395
13806
|
const [[raw]] = await tx.executeRaw('SELECT powersync_control(?, ?)', [op, payload]);
|
|
@@ -14413,20 +13824,6 @@ class SqliteBucketStorage extends BaseObserver {
|
|
|
14413
13824
|
}
|
|
14414
13825
|
static _subkeyMigrationKey = 'powersync_js_migrated_subkeys';
|
|
14415
13826
|
}
|
|
14416
|
-
function hasMatchingPriority(priority, bucket) {
|
|
14417
|
-
return bucket.priority != null && bucket.priority <= priority;
|
|
14418
|
-
}
|
|
14419
|
-
|
|
14420
|
-
// TODO JSON
|
|
14421
|
-
class SyncDataBatch {
|
|
14422
|
-
buckets;
|
|
14423
|
-
static fromJSON(json) {
|
|
14424
|
-
return new SyncDataBatch(json.buckets.map((bucket) => SyncDataBucket.fromRow(bucket)));
|
|
14425
|
-
}
|
|
14426
|
-
constructor(buckets) {
|
|
14427
|
-
this.buckets = buckets;
|
|
14428
|
-
}
|
|
14429
|
-
}
|
|
14430
13827
|
|
|
14431
13828
|
/**
|
|
14432
13829
|
* Thrown when an underlying database connection is closed.
|
|
@@ -14486,10 +13883,8 @@ class Schema {
|
|
|
14486
13883
|
* developer instead of automatically by PowerSync.
|
|
14487
13884
|
* Since raw tables are not backed by JSON, running complex queries on them may be more efficient. Further, they allow
|
|
14488
13885
|
* using client-side table and column constraints.
|
|
14489
|
-
* Note that raw tables are only supported when using the new `SyncClientImplementation.rust` sync client.
|
|
14490
13886
|
*
|
|
14491
13887
|
* @param tables An object of (table name, raw table definition) entries.
|
|
14492
|
-
* @experimental Note that the raw tables API is still experimental and may change in the future.
|
|
14493
13888
|
*/
|
|
14494
13889
|
withRawTables(tables) {
|
|
14495
13890
|
for (const [name, rawTableDefinition] of Object.entries(tables)) {
|
|
@@ -14715,7 +14110,6 @@ exports.DEFAULT_INDEX_OPTIONS = DEFAULT_INDEX_OPTIONS;
|
|
|
14715
14110
|
exports.DEFAULT_LOCK_TIMEOUT_MS = DEFAULT_LOCK_TIMEOUT_MS;
|
|
14716
14111
|
exports.DEFAULT_POWERSYNC_CLOSE_OPTIONS = DEFAULT_POWERSYNC_CLOSE_OPTIONS;
|
|
14717
14112
|
exports.DEFAULT_POWERSYNC_DB_OPTIONS = DEFAULT_POWERSYNC_DB_OPTIONS;
|
|
14718
|
-
exports.DEFAULT_PRESSURE_LIMITS = DEFAULT_PRESSURE_LIMITS;
|
|
14719
14113
|
exports.DEFAULT_REMOTE_LOGGER = DEFAULT_REMOTE_LOGGER;
|
|
14720
14114
|
exports.DEFAULT_REMOTE_OPTIONS = DEFAULT_REMOTE_OPTIONS;
|
|
14721
14115
|
exports.DEFAULT_RETRY_DELAY_MS = DEFAULT_RETRY_DELAY_MS;
|
|
@@ -14726,7 +14120,6 @@ exports.DEFAULT_SYNC_CLIENT_IMPLEMENTATION = DEFAULT_SYNC_CLIENT_IMPLEMENTATION;
|
|
|
14726
14120
|
exports.DEFAULT_TABLE_OPTIONS = DEFAULT_TABLE_OPTIONS;
|
|
14727
14121
|
exports.DEFAULT_WATCH_QUERY_OPTIONS = DEFAULT_WATCH_QUERY_OPTIONS;
|
|
14728
14122
|
exports.DEFAULT_WATCH_THROTTLE_MS = DEFAULT_WATCH_THROTTLE_MS;
|
|
14729
|
-
exports.DataStream = DataStream;
|
|
14730
14123
|
exports.DifferentialQueryProcessor = DifferentialQueryProcessor;
|
|
14731
14124
|
exports.EMPTY_DIFFERENTIAL = EMPTY_DIFFERENTIAL;
|
|
14732
14125
|
exports.FalsyComparator = FalsyComparator;
|
|
@@ -14741,13 +14134,9 @@ exports.MAX_OP_ID = MAX_OP_ID;
|
|
|
14741
14134
|
exports.MEMORY_TRIGGER_CLAIM_MANAGER = MEMORY_TRIGGER_CLAIM_MANAGER;
|
|
14742
14135
|
exports.Mutex = Mutex;
|
|
14743
14136
|
exports.OnChangeQueryProcessor = OnChangeQueryProcessor;
|
|
14744
|
-
exports.OpType = OpType;
|
|
14745
|
-
exports.OplogEntry = OplogEntry;
|
|
14746
14137
|
exports.Schema = Schema;
|
|
14747
14138
|
exports.Semaphore = Semaphore;
|
|
14748
14139
|
exports.SqliteBucketStorage = SqliteBucketStorage;
|
|
14749
|
-
exports.SyncDataBatch = SyncDataBatch;
|
|
14750
|
-
exports.SyncDataBucket = SyncDataBucket;
|
|
14751
14140
|
exports.SyncProgress = SyncProgress;
|
|
14752
14141
|
exports.SyncStatus = SyncStatus;
|
|
14753
14142
|
exports.SyncingService = SyncingService;
|
|
@@ -14762,18 +14151,10 @@ exports.createBaseLogger = createBaseLogger;
|
|
|
14762
14151
|
exports.createLogger = createLogger;
|
|
14763
14152
|
exports.extractTableUpdates = extractTableUpdates;
|
|
14764
14153
|
exports.isBatchedUpdateNotification = isBatchedUpdateNotification;
|
|
14765
|
-
exports.isContinueCheckpointRequest = isContinueCheckpointRequest;
|
|
14766
14154
|
exports.isDBAdapter = isDBAdapter;
|
|
14767
14155
|
exports.isPowerSyncDatabaseOptionsWithSettings = isPowerSyncDatabaseOptionsWithSettings;
|
|
14768
14156
|
exports.isSQLOpenFactory = isSQLOpenFactory;
|
|
14769
14157
|
exports.isSQLOpenOptions = isSQLOpenOptions;
|
|
14770
|
-
exports.isStreamingKeepalive = isStreamingKeepalive;
|
|
14771
|
-
exports.isStreamingSyncCheckpoint = isStreamingSyncCheckpoint;
|
|
14772
|
-
exports.isStreamingSyncCheckpointComplete = isStreamingSyncCheckpointComplete;
|
|
14773
|
-
exports.isStreamingSyncCheckpointDiff = isStreamingSyncCheckpointDiff;
|
|
14774
|
-
exports.isStreamingSyncCheckpointPartiallyComplete = isStreamingSyncCheckpointPartiallyComplete;
|
|
14775
|
-
exports.isStreamingSyncData = isStreamingSyncData;
|
|
14776
|
-
exports.isSyncNewCheckpointRequest = isSyncNewCheckpointRequest;
|
|
14777
14158
|
exports.parseQuery = parseQuery;
|
|
14778
14159
|
exports.runOnSchemaChange = runOnSchemaChange;
|
|
14779
14160
|
exports.sanitizeSQL = sanitizeSQL;
|