@powersync/common 1.50.0 → 1.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle.cjs +558 -481
- package/dist/bundle.cjs.map +1 -1
- package/dist/bundle.mjs +558 -480
- package/dist/bundle.mjs.map +1 -1
- package/dist/bundle.node.cjs +556 -481
- package/dist/bundle.node.cjs.map +1 -1
- package/dist/bundle.node.mjs +556 -480
- package/dist/bundle.node.mjs.map +1 -1
- package/dist/index.d.cts +73 -73
- package/lib/client/AbstractPowerSyncDatabase.js +3 -3
- package/lib/client/AbstractPowerSyncDatabase.js.map +1 -1
- package/lib/client/sync/stream/AbstractRemote.d.ts +29 -8
- package/lib/client/sync/stream/AbstractRemote.js +154 -177
- package/lib/client/sync/stream/AbstractRemote.js.map +1 -1
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +1 -0
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +69 -88
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +0 -1
- package/lib/index.js.map +1 -1
- package/lib/utils/async.d.ts +0 -9
- package/lib/utils/async.js +0 -9
- package/lib/utils/async.js.map +1 -1
- package/lib/utils/mutex.d.ts +32 -3
- package/lib/utils/mutex.js +85 -36
- package/lib/utils/mutex.js.map +1 -1
- package/lib/utils/queue.d.ts +16 -0
- package/lib/utils/queue.js +42 -0
- package/lib/utils/queue.js.map +1 -0
- package/lib/utils/stream_transform.d.ts +39 -0
- package/lib/utils/stream_transform.js +206 -0
- package/lib/utils/stream_transform.js.map +1 -0
- package/package.json +9 -7
- package/src/client/AbstractPowerSyncDatabase.ts +3 -3
- package/src/client/sync/stream/AbstractRemote.ts +182 -206
- package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +82 -83
- package/src/index.ts +1 -1
- package/src/utils/async.ts +0 -11
- package/src/utils/mutex.ts +111 -48
- package/src/utils/queue.ts +48 -0
- package/src/utils/stream_transform.ts +252 -0
- package/lib/utils/DataStream.d.ts +0 -62
- package/lib/utils/DataStream.js +0 -169
- package/lib/utils/DataStream.js.map +0 -1
- package/src/utils/DataStream.ts +0 -222
package/dist/bundle.node.cjs
CHANGED
|
@@ -785,19 +785,69 @@ class SyncingService {
|
|
|
785
785
|
}
|
|
786
786
|
|
|
787
787
|
/**
|
|
788
|
-
*
|
|
788
|
+
* A simple fixed-capacity queue implementation.
|
|
789
|
+
*
|
|
790
|
+
* Unlike a naive queue implemented by `array.push()` and `array.shift()`, this avoids moving array elements around
|
|
791
|
+
* and is `O(1)` for {@link addLast} and {@link removeFirst}.
|
|
792
|
+
*/
|
|
793
|
+
class Queue {
|
|
794
|
+
table;
|
|
795
|
+
// Index of the first element in the table.
|
|
796
|
+
head;
|
|
797
|
+
// Amount of items currently in the queue.
|
|
798
|
+
_length;
|
|
799
|
+
constructor(initialItems) {
|
|
800
|
+
this.table = [...initialItems];
|
|
801
|
+
this.head = 0;
|
|
802
|
+
this._length = this.table.length;
|
|
803
|
+
}
|
|
804
|
+
get isEmpty() {
|
|
805
|
+
return this.length == 0;
|
|
806
|
+
}
|
|
807
|
+
get length() {
|
|
808
|
+
return this._length;
|
|
809
|
+
}
|
|
810
|
+
removeFirst() {
|
|
811
|
+
if (this.isEmpty) {
|
|
812
|
+
throw new Error('Queue is empty');
|
|
813
|
+
}
|
|
814
|
+
const result = this.table[this.head];
|
|
815
|
+
this._length--;
|
|
816
|
+
this.table[this.head] = undefined;
|
|
817
|
+
this.head = (this.head + 1) % this.table.length;
|
|
818
|
+
return result;
|
|
819
|
+
}
|
|
820
|
+
addLast(element) {
|
|
821
|
+
if (this.length == this.table.length) {
|
|
822
|
+
throw new Error('Queue is full');
|
|
823
|
+
}
|
|
824
|
+
this.table[(this.head + this._length) % this.table.length] = element;
|
|
825
|
+
this._length++;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* An asynchronous semaphore implementation with associated items per lease.
|
|
789
831
|
*
|
|
790
832
|
* @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
|
|
791
833
|
*/
|
|
792
|
-
class
|
|
793
|
-
|
|
834
|
+
class Semaphore {
|
|
835
|
+
// Available items that are not currently assigned to a waiter.
|
|
836
|
+
available;
|
|
837
|
+
size;
|
|
794
838
|
// Linked list of waiters. We don't expect the wait list to become particularly large, and this allows removing
|
|
795
839
|
// aborted waiters from the middle of the list efficiently.
|
|
796
840
|
firstWaiter;
|
|
797
841
|
lastWaiter;
|
|
798
|
-
|
|
842
|
+
constructor(elements) {
|
|
843
|
+
this.available = new Queue(elements);
|
|
844
|
+
this.size = this.available.length;
|
|
845
|
+
}
|
|
846
|
+
addWaiter(requestedItems, onAcquire) {
|
|
799
847
|
const node = {
|
|
800
848
|
isActive: true,
|
|
849
|
+
acquiredItems: [],
|
|
850
|
+
remainingItems: requestedItems,
|
|
801
851
|
onAcquire,
|
|
802
852
|
prev: this.lastWaiter
|
|
803
853
|
};
|
|
@@ -823,52 +873,92 @@ class Mutex {
|
|
|
823
873
|
if (waiter == this.lastWaiter)
|
|
824
874
|
this.lastWaiter = prev;
|
|
825
875
|
}
|
|
826
|
-
|
|
876
|
+
requestPermits(amount, abort) {
|
|
877
|
+
if (amount <= 0 || amount > this.size) {
|
|
878
|
+
throw new Error(`Invalid amount of items requested (${amount}), must be between 1 and ${this.size}`);
|
|
879
|
+
}
|
|
827
880
|
return new Promise((resolve, reject) => {
|
|
828
881
|
function rejectAborted() {
|
|
829
|
-
reject(abort?.reason ?? new Error('
|
|
882
|
+
reject(abort?.reason ?? new Error('Semaphore acquire aborted'));
|
|
830
883
|
}
|
|
831
884
|
if (abort?.aborted) {
|
|
832
885
|
return rejectAborted();
|
|
833
886
|
}
|
|
834
|
-
let
|
|
887
|
+
let waiter;
|
|
835
888
|
const markCompleted = () => {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
889
|
+
const items = waiter.acquiredItems;
|
|
890
|
+
waiter.acquiredItems = []; // Avoid releasing items twice.
|
|
891
|
+
for (const element of items) {
|
|
892
|
+
// Give to next waiter, if possible.
|
|
893
|
+
const nextWaiter = this.firstWaiter;
|
|
894
|
+
if (nextWaiter) {
|
|
895
|
+
nextWaiter.acquiredItems.push(element);
|
|
896
|
+
nextWaiter.remainingItems--;
|
|
897
|
+
if (nextWaiter.remainingItems == 0) {
|
|
898
|
+
nextWaiter.onAcquire();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
// No pending waiter, return lease into pool.
|
|
903
|
+
this.available.addLast(element);
|
|
904
|
+
}
|
|
844
905
|
}
|
|
845
|
-
|
|
846
|
-
|
|
906
|
+
};
|
|
907
|
+
const onAbort = () => {
|
|
908
|
+
abort?.removeEventListener('abort', onAbort);
|
|
909
|
+
if (waiter.isActive) {
|
|
910
|
+
this.deactivateWaiter(waiter);
|
|
911
|
+
rejectAborted();
|
|
847
912
|
}
|
|
848
913
|
};
|
|
849
|
-
|
|
850
|
-
this.
|
|
851
|
-
|
|
852
|
-
|
|
914
|
+
const resolvePromise = () => {
|
|
915
|
+
this.deactivateWaiter(waiter);
|
|
916
|
+
abort?.removeEventListener('abort', onAbort);
|
|
917
|
+
const items = waiter.acquiredItems;
|
|
918
|
+
resolve({ items, release: markCompleted });
|
|
919
|
+
};
|
|
920
|
+
waiter = this.addWaiter(amount, resolvePromise);
|
|
921
|
+
// If there are items in the pool that haven't been assigned, we can pull them into this waiter. Note that this is
|
|
922
|
+
// only the case if we're the first waiter (otherwise, items would have been assigned to an earlier waiter).
|
|
923
|
+
while (!this.available.isEmpty && waiter.remainingItems > 0) {
|
|
924
|
+
waiter.acquiredItems.push(this.available.removeFirst());
|
|
925
|
+
waiter.remainingItems--;
|
|
853
926
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
const onAbort = () => {
|
|
857
|
-
abort?.removeEventListener('abort', onAbort);
|
|
858
|
-
if (node.isActive) {
|
|
859
|
-
this.deactivateWaiter(node);
|
|
860
|
-
rejectAborted();
|
|
861
|
-
}
|
|
862
|
-
};
|
|
863
|
-
node = this.addWaiter(() => {
|
|
864
|
-
abort?.removeEventListener('abort', onAbort);
|
|
865
|
-
holdsMutex = true;
|
|
866
|
-
resolve(markCompleted);
|
|
867
|
-
});
|
|
868
|
-
abort?.addEventListener('abort', onAbort);
|
|
927
|
+
if (waiter.remainingItems == 0) {
|
|
928
|
+
return resolvePromise();
|
|
869
929
|
}
|
|
930
|
+
abort?.addEventListener('abort', onAbort);
|
|
870
931
|
});
|
|
871
932
|
}
|
|
933
|
+
/**
|
|
934
|
+
* Requests a single item from the pool.
|
|
935
|
+
*
|
|
936
|
+
* The returned `release` callback must be invoked to return the item into the pool.
|
|
937
|
+
*/
|
|
938
|
+
async requestOne(abort) {
|
|
939
|
+
const { items, release } = await this.requestPermits(1, abort);
|
|
940
|
+
return { release, item: items[0] };
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Requests access to all items from the pool.
|
|
944
|
+
*
|
|
945
|
+
* The returned `release` callback must be invoked to return items into the pool.
|
|
946
|
+
*/
|
|
947
|
+
requestAll(abort) {
|
|
948
|
+
return this.requestPermits(this.size, abort);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* An asynchronous mutex implementation.
|
|
953
|
+
*
|
|
954
|
+
* @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
|
|
955
|
+
*/
|
|
956
|
+
class Mutex {
|
|
957
|
+
inner = new Semaphore([null]);
|
|
958
|
+
async acquire(abort) {
|
|
959
|
+
const { release } = await this.inner.requestOne(abort);
|
|
960
|
+
return release;
|
|
961
|
+
}
|
|
872
962
|
async runExclusive(fn, abort) {
|
|
873
963
|
const returnMutex = await this.acquire(abort);
|
|
874
964
|
try {
|
|
@@ -2167,15 +2257,6 @@ class ControlledExecutor {
|
|
|
2167
2257
|
}
|
|
2168
2258
|
}
|
|
2169
2259
|
|
|
2170
|
-
/**
|
|
2171
|
-
* A ponyfill for `Symbol.asyncIterator` that is compatible with the
|
|
2172
|
-
* [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)
|
|
2173
|
-
* we recommend for React Native.
|
|
2174
|
-
*
|
|
2175
|
-
* As long as we use this symbol (instead of `for await` and `async *`) in this package, we can be compatible with async
|
|
2176
|
-
* iterators without requiring them.
|
|
2177
|
-
*/
|
|
2178
|
-
const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
|
|
2179
2260
|
/**
|
|
2180
2261
|
* Throttle a function to be called at most once every "wait" milliseconds,
|
|
2181
2262
|
* on the trailing edge.
|
|
@@ -8141,177 +8222,10 @@ function requireDist () {
|
|
|
8141
8222
|
|
|
8142
8223
|
var distExports = requireDist();
|
|
8143
8224
|
|
|
8144
|
-
var version = "1.
|
|
8225
|
+
var version = "1.52.0";
|
|
8145
8226
|
var PACKAGE = {
|
|
8146
8227
|
version: version};
|
|
8147
8228
|
|
|
8148
|
-
const DEFAULT_PRESSURE_LIMITS = {
|
|
8149
|
-
highWater: 10,
|
|
8150
|
-
lowWater: 0
|
|
8151
|
-
};
|
|
8152
|
-
/**
|
|
8153
|
-
* A very basic implementation of a data stream with backpressure support which does not use
|
|
8154
|
-
* native JS streams or async iterators.
|
|
8155
|
-
* This is handy for environments such as React Native which need polyfills for the above.
|
|
8156
|
-
*/
|
|
8157
|
-
class DataStream extends BaseObserver {
|
|
8158
|
-
options;
|
|
8159
|
-
dataQueue;
|
|
8160
|
-
isClosed;
|
|
8161
|
-
processingPromise;
|
|
8162
|
-
notifyDataAdded;
|
|
8163
|
-
logger;
|
|
8164
|
-
mapLine;
|
|
8165
|
-
constructor(options) {
|
|
8166
|
-
super();
|
|
8167
|
-
this.options = options;
|
|
8168
|
-
this.processingPromise = null;
|
|
8169
|
-
this.isClosed = false;
|
|
8170
|
-
this.dataQueue = [];
|
|
8171
|
-
this.mapLine = options?.mapLine ?? ((line) => line);
|
|
8172
|
-
this.logger = options?.logger ?? Logger.get('DataStream');
|
|
8173
|
-
if (options?.closeOnError) {
|
|
8174
|
-
const l = this.registerListener({
|
|
8175
|
-
error: (ex) => {
|
|
8176
|
-
l?.();
|
|
8177
|
-
this.close();
|
|
8178
|
-
}
|
|
8179
|
-
});
|
|
8180
|
-
}
|
|
8181
|
-
}
|
|
8182
|
-
get highWatermark() {
|
|
8183
|
-
return this.options?.pressure?.highWaterMark ?? DEFAULT_PRESSURE_LIMITS.highWater;
|
|
8184
|
-
}
|
|
8185
|
-
get lowWatermark() {
|
|
8186
|
-
return this.options?.pressure?.lowWaterMark ?? DEFAULT_PRESSURE_LIMITS.lowWater;
|
|
8187
|
-
}
|
|
8188
|
-
get closed() {
|
|
8189
|
-
return this.isClosed;
|
|
8190
|
-
}
|
|
8191
|
-
async close() {
|
|
8192
|
-
this.isClosed = true;
|
|
8193
|
-
await this.processingPromise;
|
|
8194
|
-
this.iterateListeners((l) => l.closed?.());
|
|
8195
|
-
// Discard any data in the queue
|
|
8196
|
-
this.dataQueue = [];
|
|
8197
|
-
this.listeners.clear();
|
|
8198
|
-
}
|
|
8199
|
-
/**
|
|
8200
|
-
* Enqueues data for the consumers to read
|
|
8201
|
-
*/
|
|
8202
|
-
enqueueData(data) {
|
|
8203
|
-
if (this.isClosed) {
|
|
8204
|
-
throw new Error('Cannot enqueue data into closed stream.');
|
|
8205
|
-
}
|
|
8206
|
-
this.dataQueue.push(data);
|
|
8207
|
-
this.notifyDataAdded?.();
|
|
8208
|
-
this.processQueue();
|
|
8209
|
-
}
|
|
8210
|
-
/**
|
|
8211
|
-
* Reads data once from the data stream
|
|
8212
|
-
* @returns a Data payload or Null if the stream closed.
|
|
8213
|
-
*/
|
|
8214
|
-
async read() {
|
|
8215
|
-
if (this.closed) {
|
|
8216
|
-
return null;
|
|
8217
|
-
}
|
|
8218
|
-
// Wait for any pending processing to complete first.
|
|
8219
|
-
// This ensures we register our listener before calling processQueue(),
|
|
8220
|
-
// avoiding a race where processQueue() sees no reader and returns early.
|
|
8221
|
-
if (this.processingPromise) {
|
|
8222
|
-
await this.processingPromise;
|
|
8223
|
-
}
|
|
8224
|
-
// Re-check after await - stream may have closed while we were waiting
|
|
8225
|
-
if (this.closed) {
|
|
8226
|
-
return null;
|
|
8227
|
-
}
|
|
8228
|
-
return new Promise((resolve, reject) => {
|
|
8229
|
-
const l = this.registerListener({
|
|
8230
|
-
data: async (data) => {
|
|
8231
|
-
resolve(data);
|
|
8232
|
-
// Remove the listener
|
|
8233
|
-
l?.();
|
|
8234
|
-
},
|
|
8235
|
-
closed: () => {
|
|
8236
|
-
resolve(null);
|
|
8237
|
-
l?.();
|
|
8238
|
-
},
|
|
8239
|
-
error: (ex) => {
|
|
8240
|
-
reject(ex);
|
|
8241
|
-
l?.();
|
|
8242
|
-
}
|
|
8243
|
-
});
|
|
8244
|
-
this.processQueue();
|
|
8245
|
-
});
|
|
8246
|
-
}
|
|
8247
|
-
/**
|
|
8248
|
-
* Executes a callback for each data item in the stream
|
|
8249
|
-
*/
|
|
8250
|
-
forEach(callback) {
|
|
8251
|
-
if (this.dataQueue.length <= this.lowWatermark) {
|
|
8252
|
-
this.iterateAsyncErrored(async (l) => l.lowWater?.());
|
|
8253
|
-
}
|
|
8254
|
-
return this.registerListener({
|
|
8255
|
-
data: callback
|
|
8256
|
-
});
|
|
8257
|
-
}
|
|
8258
|
-
processQueue() {
|
|
8259
|
-
if (this.processingPromise) {
|
|
8260
|
-
return;
|
|
8261
|
-
}
|
|
8262
|
-
const promise = (this.processingPromise = this._processQueue());
|
|
8263
|
-
promise.finally(() => {
|
|
8264
|
-
this.processingPromise = null;
|
|
8265
|
-
});
|
|
8266
|
-
return promise;
|
|
8267
|
-
}
|
|
8268
|
-
hasDataReader() {
|
|
8269
|
-
return Array.from(this.listeners.values()).some((l) => !!l.data);
|
|
8270
|
-
}
|
|
8271
|
-
async _processQueue() {
|
|
8272
|
-
/**
|
|
8273
|
-
* Allow listeners to mutate the queue before processing.
|
|
8274
|
-
* This allows for operations such as dropping or compressing data
|
|
8275
|
-
* on high water or requesting more data on low water.
|
|
8276
|
-
*/
|
|
8277
|
-
if (this.dataQueue.length >= this.highWatermark) {
|
|
8278
|
-
await this.iterateAsyncErrored(async (l) => l.highWater?.());
|
|
8279
|
-
}
|
|
8280
|
-
if (this.isClosed || !this.hasDataReader()) {
|
|
8281
|
-
return;
|
|
8282
|
-
}
|
|
8283
|
-
if (this.dataQueue.length) {
|
|
8284
|
-
const data = this.dataQueue.shift();
|
|
8285
|
-
const mapped = this.mapLine(data);
|
|
8286
|
-
await this.iterateAsyncErrored(async (l) => l.data?.(mapped));
|
|
8287
|
-
}
|
|
8288
|
-
if (this.dataQueue.length <= this.lowWatermark) {
|
|
8289
|
-
const dataAdded = new Promise((resolve) => {
|
|
8290
|
-
this.notifyDataAdded = resolve;
|
|
8291
|
-
});
|
|
8292
|
-
await Promise.race([this.iterateAsyncErrored(async (l) => l.lowWater?.()), dataAdded]);
|
|
8293
|
-
this.notifyDataAdded = null;
|
|
8294
|
-
}
|
|
8295
|
-
if (this.dataQueue.length > 0) {
|
|
8296
|
-
setTimeout(() => this.processQueue());
|
|
8297
|
-
}
|
|
8298
|
-
}
|
|
8299
|
-
async iterateAsyncErrored(cb) {
|
|
8300
|
-
// Important: We need to copy the listeners, as calling a listener could result in adding another
|
|
8301
|
-
// listener, resulting in infinite loops.
|
|
8302
|
-
const listeners = Array.from(this.listeners.values());
|
|
8303
|
-
for (let i of listeners) {
|
|
8304
|
-
try {
|
|
8305
|
-
await cb(i);
|
|
8306
|
-
}
|
|
8307
|
-
catch (ex) {
|
|
8308
|
-
this.logger.error(ex);
|
|
8309
|
-
this.iterateListeners((l) => l.error?.(ex));
|
|
8310
|
-
}
|
|
8311
|
-
}
|
|
8312
|
-
}
|
|
8313
|
-
}
|
|
8314
|
-
|
|
8315
8229
|
var WebsocketDuplexConnection = {};
|
|
8316
8230
|
|
|
8317
8231
|
var hasRequiredWebsocketDuplexConnection;
|
|
@@ -8474,8 +8388,215 @@ class WebsocketClientTransport {
|
|
|
8474
8388
|
}
|
|
8475
8389
|
}
|
|
8476
8390
|
|
|
8391
|
+
const doneResult = { done: true, value: undefined };
|
|
8392
|
+
function valueResult(value) {
|
|
8393
|
+
return { done: false, value };
|
|
8394
|
+
}
|
|
8395
|
+
/**
|
|
8396
|
+
* A variant of {@link Array.map} for async iterators.
|
|
8397
|
+
*/
|
|
8398
|
+
function map(source, map) {
|
|
8399
|
+
return {
|
|
8400
|
+
next: async () => {
|
|
8401
|
+
const value = await source.next();
|
|
8402
|
+
if (value.done) {
|
|
8403
|
+
return value;
|
|
8404
|
+
}
|
|
8405
|
+
else {
|
|
8406
|
+
return { value: map(value.value) };
|
|
8407
|
+
}
|
|
8408
|
+
}
|
|
8409
|
+
};
|
|
8410
|
+
}
|
|
8411
|
+
/**
|
|
8412
|
+
* Expands a source async iterator by allowing to inject events asynchronously.
|
|
8413
|
+
*
|
|
8414
|
+
* The resulting iterator will emit all events from its source. Additionally though, events can be injected. These
|
|
8415
|
+
* events are dropped once the main iterator completes, but are otherwise forwarded.
|
|
8416
|
+
*
|
|
8417
|
+
* The iterator completes when its source completes, and it supports backpressure by only calling `next()` on the source
|
|
8418
|
+
* in response to a `next()` call from downstream if no pending injected events can be dispatched.
|
|
8419
|
+
*/
|
|
8420
|
+
function injectable(source) {
|
|
8421
|
+
let sourceIsDone = false;
|
|
8422
|
+
let waiter = undefined; // An active, waiting next() call.
|
|
8423
|
+
// A pending upstream event that couldn't be dispatched because inject() has been called before it was resolved.
|
|
8424
|
+
let pendingSourceEvent = null;
|
|
8425
|
+
let pendingInjectedEvents = [];
|
|
8426
|
+
const consumeWaiter = () => {
|
|
8427
|
+
const pending = waiter;
|
|
8428
|
+
waiter = undefined;
|
|
8429
|
+
return pending;
|
|
8430
|
+
};
|
|
8431
|
+
const fetchFromSource = () => {
|
|
8432
|
+
const resolveWaiter = (propagate) => {
|
|
8433
|
+
const active = consumeWaiter();
|
|
8434
|
+
if (active) {
|
|
8435
|
+
propagate(active);
|
|
8436
|
+
}
|
|
8437
|
+
else {
|
|
8438
|
+
pendingSourceEvent = propagate;
|
|
8439
|
+
}
|
|
8440
|
+
};
|
|
8441
|
+
const nextFromSource = source.next();
|
|
8442
|
+
nextFromSource.then((value) => {
|
|
8443
|
+
sourceIsDone = value.done == true;
|
|
8444
|
+
resolveWaiter((w) => w.resolve(value));
|
|
8445
|
+
}, (error) => {
|
|
8446
|
+
resolveWaiter((w) => w.reject(error));
|
|
8447
|
+
});
|
|
8448
|
+
};
|
|
8449
|
+
return {
|
|
8450
|
+
next: () => {
|
|
8451
|
+
return new Promise((resolve, reject) => {
|
|
8452
|
+
// First priority: Dispatch ready upstream events.
|
|
8453
|
+
if (sourceIsDone) {
|
|
8454
|
+
return resolve(doneResult);
|
|
8455
|
+
}
|
|
8456
|
+
if (pendingSourceEvent) {
|
|
8457
|
+
pendingSourceEvent({ resolve, reject });
|
|
8458
|
+
pendingSourceEvent = null;
|
|
8459
|
+
return;
|
|
8460
|
+
}
|
|
8461
|
+
// Second priority: Dispatch injected events
|
|
8462
|
+
if (pendingInjectedEvents.length) {
|
|
8463
|
+
return resolve(valueResult(pendingInjectedEvents.shift()));
|
|
8464
|
+
}
|
|
8465
|
+
// Nothing pending? Fetch from source
|
|
8466
|
+
waiter = { resolve, reject };
|
|
8467
|
+
return fetchFromSource();
|
|
8468
|
+
});
|
|
8469
|
+
},
|
|
8470
|
+
inject: (event) => {
|
|
8471
|
+
const pending = consumeWaiter();
|
|
8472
|
+
if (pending != null) {
|
|
8473
|
+
pending.resolve(valueResult(event));
|
|
8474
|
+
}
|
|
8475
|
+
else {
|
|
8476
|
+
pendingInjectedEvents.push(event);
|
|
8477
|
+
}
|
|
8478
|
+
}
|
|
8479
|
+
};
|
|
8480
|
+
}
|
|
8481
|
+
/**
|
|
8482
|
+
* Splits a byte stream at line endings, emitting each line as a string.
|
|
8483
|
+
*/
|
|
8484
|
+
function extractJsonLines(source, decoder) {
|
|
8485
|
+
let buffer = '';
|
|
8486
|
+
const pendingLines = [];
|
|
8487
|
+
let isFinalEvent = false;
|
|
8488
|
+
return {
|
|
8489
|
+
next: async () => {
|
|
8490
|
+
while (true) {
|
|
8491
|
+
if (isFinalEvent) {
|
|
8492
|
+
return doneResult;
|
|
8493
|
+
}
|
|
8494
|
+
{
|
|
8495
|
+
const first = pendingLines.shift();
|
|
8496
|
+
if (first) {
|
|
8497
|
+
return { done: false, value: first };
|
|
8498
|
+
}
|
|
8499
|
+
}
|
|
8500
|
+
const { done, value } = await source.next();
|
|
8501
|
+
if (done) {
|
|
8502
|
+
const remaining = buffer.trim();
|
|
8503
|
+
if (remaining.length != 0) {
|
|
8504
|
+
isFinalEvent = true;
|
|
8505
|
+
return { done: false, value: remaining };
|
|
8506
|
+
}
|
|
8507
|
+
return doneResult;
|
|
8508
|
+
}
|
|
8509
|
+
const data = decoder.decode(value, { stream: true });
|
|
8510
|
+
buffer += data;
|
|
8511
|
+
const lines = buffer.split('\n');
|
|
8512
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
8513
|
+
const l = lines[i].trim();
|
|
8514
|
+
if (l.length > 0) {
|
|
8515
|
+
pendingLines.push(l);
|
|
8516
|
+
}
|
|
8517
|
+
}
|
|
8518
|
+
buffer = lines[lines.length - 1];
|
|
8519
|
+
}
|
|
8520
|
+
}
|
|
8521
|
+
};
|
|
8522
|
+
}
|
|
8523
|
+
/**
|
|
8524
|
+
* Splits a concatenated stream of BSON objects by emitting individual objects.
|
|
8525
|
+
*/
|
|
8526
|
+
function extractBsonObjects(source) {
|
|
8527
|
+
// Fully read but not emitted yet.
|
|
8528
|
+
const completedObjects = [];
|
|
8529
|
+
// Whether source has returned { done: true }. We do the same once completed objects have been emitted.
|
|
8530
|
+
let isDone = false;
|
|
8531
|
+
const lengthBuffer = new DataView(new ArrayBuffer(4));
|
|
8532
|
+
let objectBody = null;
|
|
8533
|
+
// If we're parsing the length field, a number between 1 and 4 (inclusive) describing remaining bytes in the header.
|
|
8534
|
+
// If we're consuming a document, the bytes remaining.
|
|
8535
|
+
let remainingLength = 4;
|
|
8536
|
+
return {
|
|
8537
|
+
async next() {
|
|
8538
|
+
while (true) {
|
|
8539
|
+
// Before fetching new data from upstream, return completed objects.
|
|
8540
|
+
if (completedObjects.length) {
|
|
8541
|
+
return valueResult(completedObjects.shift());
|
|
8542
|
+
}
|
|
8543
|
+
if (isDone) {
|
|
8544
|
+
return doneResult;
|
|
8545
|
+
}
|
|
8546
|
+
const upstreamEvent = await source.next();
|
|
8547
|
+
if (upstreamEvent.done) {
|
|
8548
|
+
isDone = true;
|
|
8549
|
+
if (objectBody || remainingLength != 4) {
|
|
8550
|
+
throw new Error('illegal end of stream in BSON object');
|
|
8551
|
+
}
|
|
8552
|
+
return doneResult;
|
|
8553
|
+
}
|
|
8554
|
+
const chunk = upstreamEvent.value;
|
|
8555
|
+
for (let i = 0; i < chunk.length;) {
|
|
8556
|
+
const availableInData = chunk.length - i;
|
|
8557
|
+
if (objectBody) {
|
|
8558
|
+
// We're in the middle of reading a BSON document.
|
|
8559
|
+
const bytesToRead = Math.min(availableInData, remainingLength);
|
|
8560
|
+
const copySource = new Uint8Array(chunk.buffer, chunk.byteOffset + i, bytesToRead);
|
|
8561
|
+
objectBody.set(copySource, objectBody.length - remainingLength);
|
|
8562
|
+
i += bytesToRead;
|
|
8563
|
+
remainingLength -= bytesToRead;
|
|
8564
|
+
if (remainingLength == 0) {
|
|
8565
|
+
completedObjects.push(objectBody);
|
|
8566
|
+
// Prepare to read another document, starting with its length
|
|
8567
|
+
objectBody = null;
|
|
8568
|
+
remainingLength = 4;
|
|
8569
|
+
}
|
|
8570
|
+
}
|
|
8571
|
+
else {
|
|
8572
|
+
// Copy up to 4 bytes into lengthBuffer, depending on how many we still need.
|
|
8573
|
+
const bytesToRead = Math.min(availableInData, remainingLength);
|
|
8574
|
+
for (let j = 0; j < bytesToRead; j++) {
|
|
8575
|
+
lengthBuffer.setUint8(4 - remainingLength + j, chunk[i + j]);
|
|
8576
|
+
}
|
|
8577
|
+
i += bytesToRead;
|
|
8578
|
+
remainingLength -= bytesToRead;
|
|
8579
|
+
if (remainingLength == 0) {
|
|
8580
|
+
// Transition from reading length header to reading document. Subtracting 4 because the length of the
|
|
8581
|
+
// header is included in length.
|
|
8582
|
+
const length = lengthBuffer.getInt32(0, true /* little endian */);
|
|
8583
|
+
remainingLength = length - 4;
|
|
8584
|
+
if (remainingLength < 1) {
|
|
8585
|
+
throw new Error(`invalid length for bson: ${length}`);
|
|
8586
|
+
}
|
|
8587
|
+
objectBody = new Uint8Array(length);
|
|
8588
|
+
new DataView(objectBody.buffer).setInt32(0, length, true);
|
|
8589
|
+
}
|
|
8590
|
+
}
|
|
8591
|
+
}
|
|
8592
|
+
}
|
|
8593
|
+
}
|
|
8594
|
+
};
|
|
8595
|
+
}
|
|
8596
|
+
|
|
8477
8597
|
const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
|
|
8478
8598
|
const POWERSYNC_JS_VERSION = PACKAGE.version;
|
|
8599
|
+
const SYNC_QUEUE_REQUEST_HIGH_WATER = 10;
|
|
8479
8600
|
const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
|
|
8480
8601
|
// Keep alive message is sent every period
|
|
8481
8602
|
const KEEP_ALIVE_MS = 20_000;
|
|
@@ -8655,13 +8776,14 @@ class AbstractRemote {
|
|
|
8655
8776
|
return new WebSocket(url);
|
|
8656
8777
|
}
|
|
8657
8778
|
/**
|
|
8658
|
-
* Returns a data stream of sync line data.
|
|
8779
|
+
* Returns a data stream of sync line data, fetched via RSocket-over-WebSocket.
|
|
8780
|
+
*
|
|
8781
|
+
* The only mechanism to abort the returned stream is to use the abort signal in {@link SocketSyncStreamOptions}.
|
|
8659
8782
|
*
|
|
8660
|
-
* @param map Maps received payload frames to the typed event value.
|
|
8661
8783
|
* @param bson A BSON encoder and decoder. When set, the data stream will be requested with a BSON payload
|
|
8662
8784
|
* (required for compatibility with older sync services).
|
|
8663
8785
|
*/
|
|
8664
|
-
async socketStreamRaw(options,
|
|
8786
|
+
async socketStreamRaw(options, bson) {
|
|
8665
8787
|
const { path, fetchStrategy = exports.FetchStrategy.Buffered } = options;
|
|
8666
8788
|
const mimeType = bson == null ? 'application/json' : 'application/bson';
|
|
8667
8789
|
function toBuffer(js) {
|
|
@@ -8676,52 +8798,55 @@ class AbstractRemote {
|
|
|
8676
8798
|
}
|
|
8677
8799
|
const syncQueueRequestSize = fetchStrategy == exports.FetchStrategy.Buffered ? 10 : 1;
|
|
8678
8800
|
const request = await this.buildRequest(path);
|
|
8801
|
+
const url = this.options.socketUrlTransformer(request.url);
|
|
8679
8802
|
// Add the user agent in the setup payload - we can't set custom
|
|
8680
8803
|
// headers with websockets on web. The browser userAgent is however added
|
|
8681
8804
|
// automatically as a header.
|
|
8682
8805
|
const userAgent = this.getUserAgent();
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8806
|
+
// While we're connecting (a process that can't be aborted in RSocket), the WebSocket instance to close if we wanted
|
|
8807
|
+
// to abort the connection.
|
|
8808
|
+
let pendingSocket = null;
|
|
8809
|
+
let keepAliveTimeout;
|
|
8810
|
+
let rsocket = null;
|
|
8811
|
+
let queue = null;
|
|
8812
|
+
let didClose = false;
|
|
8813
|
+
const abortRequest = () => {
|
|
8814
|
+
if (didClose) {
|
|
8815
|
+
return;
|
|
8816
|
+
}
|
|
8817
|
+
didClose = true;
|
|
8818
|
+
clearTimeout(keepAliveTimeout);
|
|
8819
|
+
if (pendingSocket) {
|
|
8820
|
+
pendingSocket.close();
|
|
8821
|
+
}
|
|
8822
|
+
if (rsocket) {
|
|
8823
|
+
rsocket.close();
|
|
8824
|
+
}
|
|
8825
|
+
if (queue) {
|
|
8826
|
+
queue.stop();
|
|
8827
|
+
}
|
|
8828
|
+
};
|
|
8690
8829
|
// Handle upstream abort
|
|
8691
|
-
if (options.abortSignal
|
|
8830
|
+
if (options.abortSignal.aborted) {
|
|
8692
8831
|
throw new AbortOperation('Connection request aborted');
|
|
8693
8832
|
}
|
|
8694
8833
|
else {
|
|
8695
|
-
options.abortSignal
|
|
8696
|
-
stream.close();
|
|
8697
|
-
}, { once: true });
|
|
8834
|
+
options.abortSignal.addEventListener('abort', abortRequest);
|
|
8698
8835
|
}
|
|
8699
|
-
let keepAliveTimeout;
|
|
8700
8836
|
const resetTimeout = () => {
|
|
8701
8837
|
clearTimeout(keepAliveTimeout);
|
|
8702
8838
|
keepAliveTimeout = setTimeout(() => {
|
|
8703
8839
|
this.logger.error(`No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.`);
|
|
8704
|
-
|
|
8840
|
+
abortRequest();
|
|
8705
8841
|
}, SOCKET_TIMEOUT_MS);
|
|
8706
8842
|
};
|
|
8707
8843
|
resetTimeout();
|
|
8708
|
-
// Typescript complains about this being `never` if it's not assigned here.
|
|
8709
|
-
// This is assigned in `wsCreator`.
|
|
8710
|
-
let disposeSocketConnectionTimeout = () => { };
|
|
8711
|
-
const url = this.options.socketUrlTransformer(request.url);
|
|
8712
8844
|
const connector = new distExports.RSocketConnector({
|
|
8713
8845
|
transport: new WebsocketClientTransport({
|
|
8714
8846
|
url,
|
|
8715
8847
|
wsCreator: (url) => {
|
|
8716
|
-
const socket = this.createSocket(url);
|
|
8717
|
-
|
|
8718
|
-
closed: () => {
|
|
8719
|
-
// Allow closing the underlying WebSocket if the stream was closed before the
|
|
8720
|
-
// RSocket connect completed. This should effectively abort the request.
|
|
8721
|
-
socket.close();
|
|
8722
|
-
}
|
|
8723
|
-
});
|
|
8724
|
-
socket.addEventListener('message', (event) => {
|
|
8848
|
+
const socket = (pendingSocket = this.createSocket(url));
|
|
8849
|
+
socket.addEventListener('message', () => {
|
|
8725
8850
|
resetTimeout();
|
|
8726
8851
|
});
|
|
8727
8852
|
return socket;
|
|
@@ -8741,43 +8866,40 @@ class AbstractRemote {
|
|
|
8741
8866
|
}
|
|
8742
8867
|
}
|
|
8743
8868
|
});
|
|
8744
|
-
let rsocket;
|
|
8745
8869
|
try {
|
|
8746
8870
|
rsocket = await connector.connect();
|
|
8747
8871
|
// The connection is established, we no longer need to monitor the initial timeout
|
|
8748
|
-
|
|
8872
|
+
pendingSocket = null;
|
|
8749
8873
|
}
|
|
8750
8874
|
catch (ex) {
|
|
8751
8875
|
this.logger.error(`Failed to connect WebSocket`, ex);
|
|
8752
|
-
|
|
8753
|
-
if (!stream.closed) {
|
|
8754
|
-
await stream.close();
|
|
8755
|
-
}
|
|
8876
|
+
abortRequest();
|
|
8756
8877
|
throw ex;
|
|
8757
8878
|
}
|
|
8758
8879
|
resetTimeout();
|
|
8759
|
-
let socketIsClosed = false;
|
|
8760
|
-
const closeSocket = () => {
|
|
8761
|
-
clearTimeout(keepAliveTimeout);
|
|
8762
|
-
if (socketIsClosed) {
|
|
8763
|
-
return;
|
|
8764
|
-
}
|
|
8765
|
-
socketIsClosed = true;
|
|
8766
|
-
rsocket.close();
|
|
8767
|
-
};
|
|
8768
8880
|
// Helps to prevent double close scenarios
|
|
8769
|
-
rsocket.onClose(() => (
|
|
8770
|
-
|
|
8771
|
-
let pendingEventsCount = syncQueueRequestSize;
|
|
8772
|
-
const disposeClosedListener = stream.registerListener({
|
|
8773
|
-
closed: () => {
|
|
8774
|
-
closeSocket();
|
|
8775
|
-
disposeClosedListener();
|
|
8776
|
-
}
|
|
8777
|
-
});
|
|
8778
|
-
const socket = await new Promise((resolve, reject) => {
|
|
8881
|
+
rsocket.onClose(() => (rsocket = null));
|
|
8882
|
+
return await new Promise((resolve, reject) => {
|
|
8779
8883
|
let connectionEstablished = false;
|
|
8780
|
-
|
|
8884
|
+
let pendingEventsCount = syncQueueRequestSize;
|
|
8885
|
+
let paused = false;
|
|
8886
|
+
let res = null;
|
|
8887
|
+
function requestMore() {
|
|
8888
|
+
const delta = syncQueueRequestSize - pendingEventsCount;
|
|
8889
|
+
if (!paused && delta > 0) {
|
|
8890
|
+
res?.request(delta);
|
|
8891
|
+
pendingEventsCount = syncQueueRequestSize;
|
|
8892
|
+
}
|
|
8893
|
+
}
|
|
8894
|
+
const events = new eventIterator.EventIterator((q) => {
|
|
8895
|
+
queue = q;
|
|
8896
|
+
q.on('highWater', () => (paused = true));
|
|
8897
|
+
q.on('lowWater', () => {
|
|
8898
|
+
paused = false;
|
|
8899
|
+
requestMore();
|
|
8900
|
+
});
|
|
8901
|
+
}, { highWaterMark: SYNC_QUEUE_REQUEST_HIGH_WATER, lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER })[Symbol.asyncIterator]();
|
|
8902
|
+
res = rsocket.requestStream({
|
|
8781
8903
|
data: toBuffer(options.data),
|
|
8782
8904
|
metadata: toBuffer({
|
|
8783
8905
|
path
|
|
@@ -8802,7 +8924,7 @@ class AbstractRemote {
|
|
|
8802
8924
|
}
|
|
8803
8925
|
// RSocket will close the RSocket stream automatically
|
|
8804
8926
|
// Close the downstream stream as well - this will close the RSocket connection and WebSocket
|
|
8805
|
-
|
|
8927
|
+
abortRequest();
|
|
8806
8928
|
// Handles cases where the connection failed e.g. auth error or connection error
|
|
8807
8929
|
if (!connectionEstablished) {
|
|
8808
8930
|
reject(e);
|
|
@@ -8812,41 +8934,40 @@ class AbstractRemote {
|
|
|
8812
8934
|
// The connection is active
|
|
8813
8935
|
if (!connectionEstablished) {
|
|
8814
8936
|
connectionEstablished = true;
|
|
8815
|
-
resolve(
|
|
8937
|
+
resolve(events);
|
|
8816
8938
|
}
|
|
8817
8939
|
const { data } = payload;
|
|
8940
|
+
if (data) {
|
|
8941
|
+
queue.push(data);
|
|
8942
|
+
}
|
|
8818
8943
|
// Less events are now pending
|
|
8819
8944
|
pendingEventsCount--;
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
}
|
|
8823
|
-
stream.enqueueData(data);
|
|
8945
|
+
// Request another event (unless the downstream consumer is paused).
|
|
8946
|
+
requestMore();
|
|
8824
8947
|
},
|
|
8825
8948
|
onComplete: () => {
|
|
8826
|
-
|
|
8949
|
+
abortRequest(); // this will also emit a done event
|
|
8827
8950
|
},
|
|
8828
8951
|
onExtension: () => { }
|
|
8829
8952
|
});
|
|
8830
8953
|
});
|
|
8831
|
-
const l = stream.registerListener({
|
|
8832
|
-
lowWater: async () => {
|
|
8833
|
-
// Request to fill up the queue
|
|
8834
|
-
const required = syncQueueRequestSize - pendingEventsCount;
|
|
8835
|
-
if (required > 0) {
|
|
8836
|
-
socket.request(syncQueueRequestSize - pendingEventsCount);
|
|
8837
|
-
pendingEventsCount = syncQueueRequestSize;
|
|
8838
|
-
}
|
|
8839
|
-
},
|
|
8840
|
-
closed: () => {
|
|
8841
|
-
l();
|
|
8842
|
-
}
|
|
8843
|
-
});
|
|
8844
|
-
return stream;
|
|
8845
8954
|
}
|
|
8846
8955
|
/**
|
|
8847
|
-
*
|
|
8956
|
+
* @returns Whether the HTTP implementation on this platform can receive streamed binary responses. This is true on
|
|
8957
|
+
* all platforms except React Native (who would have guessed...), where we must not request BSON responses.
|
|
8958
|
+
*
|
|
8959
|
+
* @see https://github.com/react-native-community/fetch?tab=readme-ov-file#motivation
|
|
8960
|
+
*/
|
|
8961
|
+
get supportsStreamingBinaryResponses() {
|
|
8962
|
+
return true;
|
|
8963
|
+
}
|
|
8964
|
+
/**
|
|
8965
|
+
* Posts a `/sync/stream` request, asserts that it completes successfully and returns the streaming response as an
|
|
8966
|
+
* async iterator of byte blobs.
|
|
8967
|
+
*
|
|
8968
|
+
* To cancel the async iterator, use the abort signal from {@link SyncStreamOptions} passed to this method.
|
|
8848
8969
|
*/
|
|
8849
|
-
async
|
|
8970
|
+
async fetchStreamRaw(options) {
|
|
8850
8971
|
const { data, path, headers, abortSignal } = options;
|
|
8851
8972
|
const request = await this.buildRequest(path);
|
|
8852
8973
|
/**
|
|
@@ -8858,119 +8979,94 @@ class AbstractRemote {
|
|
|
8858
8979
|
* Aborting the active fetch request while it is being consumed seems to throw
|
|
8859
8980
|
* an unhandled exception on the window level.
|
|
8860
8981
|
*/
|
|
8861
|
-
if (abortSignal
|
|
8862
|
-
throw new AbortOperation('Abort request received before making
|
|
8982
|
+
if (abortSignal.aborted) {
|
|
8983
|
+
throw new AbortOperation('Abort request received before making fetchStreamRaw request');
|
|
8863
8984
|
}
|
|
8864
8985
|
const controller = new AbortController();
|
|
8865
|
-
let
|
|
8866
|
-
abortSignal
|
|
8867
|
-
|
|
8986
|
+
let reader = null;
|
|
8987
|
+
abortSignal.addEventListener('abort', () => {
|
|
8988
|
+
const reason = abortSignal.reason ??
|
|
8989
|
+
new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.');
|
|
8990
|
+
if (reader == null) {
|
|
8868
8991
|
// Only abort via the abort controller if the request has not resolved yet
|
|
8869
|
-
controller.abort(
|
|
8870
|
-
|
|
8992
|
+
controller.abort(reason);
|
|
8993
|
+
}
|
|
8994
|
+
else {
|
|
8995
|
+
reader.cancel(reason).catch(() => {
|
|
8996
|
+
// Cancelling the reader might rethrow an exception we would have handled by throwing in next(). So we can
|
|
8997
|
+
// ignore it here.
|
|
8998
|
+
});
|
|
8871
8999
|
}
|
|
8872
9000
|
});
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
9001
|
+
let res;
|
|
9002
|
+
let responseIsBson = false;
|
|
9003
|
+
try {
|
|
9004
|
+
const ndJson = 'application/x-ndjson';
|
|
9005
|
+
const bson = 'application/vnd.powersync.bson-stream';
|
|
9006
|
+
res = await this.fetch(request.url, {
|
|
9007
|
+
method: 'POST',
|
|
9008
|
+
headers: {
|
|
9009
|
+
...headers,
|
|
9010
|
+
...request.headers,
|
|
9011
|
+
accept: this.supportsStreamingBinaryResponses ? `${bson};q=0.9,${ndJson};q=0.8` : ndJson
|
|
9012
|
+
},
|
|
9013
|
+
body: JSON.stringify(data),
|
|
9014
|
+
signal: controller.signal,
|
|
9015
|
+
cache: 'no-store',
|
|
9016
|
+
...(this.options.fetchOptions ?? {}),
|
|
9017
|
+
...options.fetchOptions
|
|
9018
|
+
});
|
|
9019
|
+
if (!res.ok || !res.body) {
|
|
9020
|
+
const text = await res.text();
|
|
9021
|
+
this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
|
|
9022
|
+
const error = new Error(`HTTP ${res.statusText}: ${text}`);
|
|
9023
|
+
error.status = res.status;
|
|
9024
|
+
throw error;
|
|
9025
|
+
}
|
|
9026
|
+
const contentType = res.headers.get('content-type');
|
|
9027
|
+
responseIsBson = contentType == bson;
|
|
9028
|
+
}
|
|
9029
|
+
catch (ex) {
|
|
8882
9030
|
if (ex.name == 'AbortError') {
|
|
8883
9031
|
throw new AbortOperation(`Pending fetch request to ${request.url} has been aborted.`);
|
|
8884
9032
|
}
|
|
8885
9033
|
throw ex;
|
|
8886
|
-
});
|
|
8887
|
-
if (!res) {
|
|
8888
|
-
throw new Error('Fetch request was aborted');
|
|
8889
|
-
}
|
|
8890
|
-
requestResolved = true;
|
|
8891
|
-
if (!res.ok || !res.body) {
|
|
8892
|
-
const text = await res.text();
|
|
8893
|
-
this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
|
|
8894
|
-
const error = new Error(`HTTP ${res.statusText}: ${text}`);
|
|
8895
|
-
error.status = res.status;
|
|
8896
|
-
throw error;
|
|
8897
9034
|
}
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
const closeReader = async () => {
|
|
8904
|
-
try {
|
|
8905
|
-
readerReleased = true;
|
|
8906
|
-
await reader.cancel();
|
|
8907
|
-
}
|
|
8908
|
-
catch (ex) {
|
|
8909
|
-
// an error will throw if the reader hasn't been used yet
|
|
8910
|
-
}
|
|
8911
|
-
reader.releaseLock();
|
|
8912
|
-
};
|
|
8913
|
-
const stream = new DataStream({
|
|
8914
|
-
logger: this.logger,
|
|
8915
|
-
mapLine: mapLine,
|
|
8916
|
-
pressure: {
|
|
8917
|
-
highWaterMark: 20,
|
|
8918
|
-
lowWaterMark: 10
|
|
8919
|
-
}
|
|
8920
|
-
});
|
|
8921
|
-
abortSignal?.addEventListener('abort', () => {
|
|
8922
|
-
closeReader();
|
|
8923
|
-
stream.close();
|
|
8924
|
-
});
|
|
8925
|
-
const decoder = this.createTextDecoder();
|
|
8926
|
-
let buffer = '';
|
|
8927
|
-
const consumeStream = async () => {
|
|
8928
|
-
while (!stream.closed && !abortSignal?.aborted && !readerReleased) {
|
|
8929
|
-
const { done, value } = await reader.read();
|
|
8930
|
-
if (done) {
|
|
8931
|
-
const remaining = buffer.trim();
|
|
8932
|
-
if (remaining.length != 0) {
|
|
8933
|
-
stream.enqueueData(remaining);
|
|
8934
|
-
}
|
|
8935
|
-
stream.close();
|
|
8936
|
-
await closeReader();
|
|
8937
|
-
return;
|
|
9035
|
+
reader = res.body.getReader();
|
|
9036
|
+
const stream = {
|
|
9037
|
+
next: async () => {
|
|
9038
|
+
if (controller.signal.aborted) {
|
|
9039
|
+
return doneResult;
|
|
8938
9040
|
}
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
const lines = buffer.split('\n');
|
|
8942
|
-
for (var i = 0; i < lines.length - 1; i++) {
|
|
8943
|
-
var l = lines[i].trim();
|
|
8944
|
-
if (l.length > 0) {
|
|
8945
|
-
stream.enqueueData(l);
|
|
8946
|
-
}
|
|
9041
|
+
try {
|
|
9042
|
+
return await reader.read();
|
|
8947
9043
|
}
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
dispose();
|
|
8956
|
-
},
|
|
8957
|
-
closed: () => {
|
|
8958
|
-
resolve();
|
|
8959
|
-
dispose();
|
|
8960
|
-
}
|
|
8961
|
-
});
|
|
8962
|
-
});
|
|
9044
|
+
catch (ex) {
|
|
9045
|
+
if (controller.signal.aborted) {
|
|
9046
|
+
// .read() completes with an error if we cancel the reader, which we do to disconnect. So this is just
|
|
9047
|
+
// things working as intended, we can return a done event and consider the exception handled.
|
|
9048
|
+
return doneResult;
|
|
9049
|
+
}
|
|
9050
|
+
throw ex;
|
|
8963
9051
|
}
|
|
8964
9052
|
}
|
|
8965
9053
|
};
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
9054
|
+
return { isBson: responseIsBson, stream };
|
|
9055
|
+
}
|
|
9056
|
+
/**
|
|
9057
|
+
* Posts a `/sync/stream` request.
|
|
9058
|
+
*
|
|
9059
|
+
* Depending on the `Content-Type` of the response, this returns strings for sync lines or encoded BSON documents as
|
|
9060
|
+
* {@link Uint8Array}s.
|
|
9061
|
+
*/
|
|
9062
|
+
async fetchStream(options) {
|
|
9063
|
+
const { isBson, stream } = await this.fetchStreamRaw(options);
|
|
9064
|
+
if (isBson) {
|
|
9065
|
+
return extractBsonObjects(stream);
|
|
9066
|
+
}
|
|
9067
|
+
else {
|
|
9068
|
+
return extractJsonLines(stream, this.createTextDecoder());
|
|
9069
|
+
}
|
|
8974
9070
|
}
|
|
8975
9071
|
}
|
|
8976
9072
|
|
|
@@ -9478,6 +9574,19 @@ The next upload iteration will be delayed.`);
|
|
|
9478
9574
|
}
|
|
9479
9575
|
});
|
|
9480
9576
|
}
|
|
9577
|
+
async receiveSyncLines(data) {
|
|
9578
|
+
const { options, connection, bson } = data;
|
|
9579
|
+
const remote = this.options.remote;
|
|
9580
|
+
if (connection.connectionMethod == exports.SyncStreamConnectionMethod.HTTP) {
|
|
9581
|
+
return await remote.fetchStream(options);
|
|
9582
|
+
}
|
|
9583
|
+
else {
|
|
9584
|
+
return await this.options.remote.socketStreamRaw({
|
|
9585
|
+
...options,
|
|
9586
|
+
...{ fetchStrategy: connection.fetchStrategy }
|
|
9587
|
+
}, bson);
|
|
9588
|
+
}
|
|
9589
|
+
}
|
|
9481
9590
|
async legacyStreamingSyncIteration(signal, resolvedOptions) {
|
|
9482
9591
|
const rawTables = resolvedOptions.serializedSchema?.raw_tables;
|
|
9483
9592
|
if (rawTables != null && rawTables.length) {
|
|
@@ -9507,42 +9616,27 @@ The next upload iteration will be delayed.`);
|
|
|
9507
9616
|
client_id: clientId
|
|
9508
9617
|
}
|
|
9509
9618
|
};
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
stream = await this.options.remote.socketStreamRaw({
|
|
9525
|
-
...syncOptions,
|
|
9526
|
-
...{ fetchStrategy: resolvedOptions.fetchStrategy }
|
|
9527
|
-
}, (payload) => {
|
|
9528
|
-
if (payload instanceof Uint8Array) {
|
|
9529
|
-
return bson.deserialize(payload);
|
|
9530
|
-
}
|
|
9531
|
-
else {
|
|
9532
|
-
// Directly enqueued by us
|
|
9533
|
-
return payload;
|
|
9534
|
-
}
|
|
9535
|
-
}, bson);
|
|
9536
|
-
}
|
|
9619
|
+
const bson = await this.options.remote.getBSON();
|
|
9620
|
+
const source = await this.receiveSyncLines({
|
|
9621
|
+
options: syncOptions,
|
|
9622
|
+
connection: resolvedOptions,
|
|
9623
|
+
bson
|
|
9624
|
+
});
|
|
9625
|
+
const stream = injectable(map(source, (line) => {
|
|
9626
|
+
if (typeof line == 'string') {
|
|
9627
|
+
return JSON.parse(line);
|
|
9628
|
+
}
|
|
9629
|
+
else {
|
|
9630
|
+
return bson.deserialize(line);
|
|
9631
|
+
}
|
|
9632
|
+
}));
|
|
9537
9633
|
this.logger.debug('Stream established. Processing events');
|
|
9538
9634
|
this.notifyCompletedUploads = () => {
|
|
9539
|
-
|
|
9540
|
-
stream.enqueueData({ crud_upload_completed: null });
|
|
9541
|
-
}
|
|
9635
|
+
stream.inject({ crud_upload_completed: null });
|
|
9542
9636
|
};
|
|
9543
|
-
while (
|
|
9544
|
-
const line = await stream.
|
|
9545
|
-
if (
|
|
9637
|
+
while (true) {
|
|
9638
|
+
const { value: line, done } = await stream.next();
|
|
9639
|
+
if (done) {
|
|
9546
9640
|
// The stream has closed while waiting
|
|
9547
9641
|
return;
|
|
9548
9642
|
}
|
|
@@ -9721,14 +9815,17 @@ The next upload iteration will be delayed.`);
|
|
|
9721
9815
|
const syncImplementation = this;
|
|
9722
9816
|
const adapter = this.options.adapter;
|
|
9723
9817
|
const remote = this.options.remote;
|
|
9818
|
+
const controller = new AbortController();
|
|
9819
|
+
const abort = () => {
|
|
9820
|
+
return controller.abort(signal.reason);
|
|
9821
|
+
};
|
|
9822
|
+
signal.addEventListener('abort', abort);
|
|
9724
9823
|
let receivingLines = null;
|
|
9725
9824
|
let hadSyncLine = false;
|
|
9726
9825
|
let hideDisconnectOnRestart = false;
|
|
9727
9826
|
if (signal.aborted) {
|
|
9728
9827
|
throw new AbortOperation('Connection request has been aborted');
|
|
9729
9828
|
}
|
|
9730
|
-
const abortController = new AbortController();
|
|
9731
|
-
signal.addEventListener('abort', () => abortController.abort());
|
|
9732
9829
|
// Pending sync lines received from the service, as well as local events that trigger a powersync_control
|
|
9733
9830
|
// invocation (local events include refreshed tokens and completed uploads).
|
|
9734
9831
|
// This is a single data stream so that we can handle all control calls from a single place.
|
|
@@ -9736,49 +9833,36 @@ The next upload iteration will be delayed.`);
|
|
|
9736
9833
|
async function connect(instr) {
|
|
9737
9834
|
const syncOptions = {
|
|
9738
9835
|
path: '/sync/stream',
|
|
9739
|
-
abortSignal:
|
|
9836
|
+
abortSignal: controller.signal,
|
|
9740
9837
|
data: instr.request
|
|
9741
9838
|
};
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
fetchStrategy: resolvedOptions.fetchStrategy
|
|
9760
|
-
}, (payload) => {
|
|
9761
|
-
if (payload instanceof Uint8Array) {
|
|
9762
|
-
return {
|
|
9763
|
-
command: exports.PowerSyncControlCommand.PROCESS_BSON_LINE,
|
|
9764
|
-
payload: payload
|
|
9765
|
-
};
|
|
9766
|
-
}
|
|
9767
|
-
else {
|
|
9768
|
-
// Directly enqueued by us
|
|
9769
|
-
return payload;
|
|
9770
|
-
}
|
|
9771
|
-
});
|
|
9772
|
-
}
|
|
9839
|
+
controlInvocations = injectable(map(await syncImplementation.receiveSyncLines({
|
|
9840
|
+
options: syncOptions,
|
|
9841
|
+
connection: resolvedOptions
|
|
9842
|
+
}), (line) => {
|
|
9843
|
+
if (typeof line == 'string') {
|
|
9844
|
+
return {
|
|
9845
|
+
command: exports.PowerSyncControlCommand.PROCESS_TEXT_LINE,
|
|
9846
|
+
payload: line
|
|
9847
|
+
};
|
|
9848
|
+
}
|
|
9849
|
+
else {
|
|
9850
|
+
return {
|
|
9851
|
+
command: exports.PowerSyncControlCommand.PROCESS_BSON_LINE,
|
|
9852
|
+
payload: line
|
|
9853
|
+
};
|
|
9854
|
+
}
|
|
9855
|
+
}));
|
|
9773
9856
|
// The rust client will set connected: true after the first sync line because that's when it gets invoked, but
|
|
9774
9857
|
// we're already connected here and can report that.
|
|
9775
9858
|
syncImplementation.updateSyncStatus({ connected: true });
|
|
9776
9859
|
try {
|
|
9777
|
-
while (
|
|
9778
|
-
|
|
9779
|
-
if (
|
|
9780
|
-
|
|
9860
|
+
while (true) {
|
|
9861
|
+
let event = await controlInvocations.next();
|
|
9862
|
+
if (event.done) {
|
|
9863
|
+
break;
|
|
9781
9864
|
}
|
|
9865
|
+
const line = event.value;
|
|
9782
9866
|
await control(line.command, line.payload);
|
|
9783
9867
|
if (!hadSyncLine) {
|
|
9784
9868
|
syncImplementation.triggerCrudUpload();
|
|
@@ -9787,12 +9871,8 @@ The next upload iteration will be delayed.`);
|
|
|
9787
9871
|
}
|
|
9788
9872
|
}
|
|
9789
9873
|
finally {
|
|
9790
|
-
|
|
9791
|
-
|
|
9792
|
-
// refreshed. That would throw after closing (and we can't handle those events either way), so set this back
|
|
9793
|
-
// to null.
|
|
9794
|
-
controlInvocations = null;
|
|
9795
|
-
await activeInstructions.close();
|
|
9874
|
+
abort();
|
|
9875
|
+
signal.removeEventListener('abort', abort);
|
|
9796
9876
|
}
|
|
9797
9877
|
}
|
|
9798
9878
|
async function stop() {
|
|
@@ -9836,14 +9916,14 @@ The next upload iteration will be delayed.`);
|
|
|
9836
9916
|
remote.invalidateCredentials();
|
|
9837
9917
|
// Restart iteration after the credentials have been refreshed.
|
|
9838
9918
|
remote.fetchCredentials().then((_) => {
|
|
9839
|
-
controlInvocations?.
|
|
9919
|
+
controlInvocations?.inject({ command: exports.PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
|
|
9840
9920
|
}, (err) => {
|
|
9841
9921
|
syncImplementation.logger.warn('Could not prefetch credentials', err);
|
|
9842
9922
|
});
|
|
9843
9923
|
}
|
|
9844
9924
|
}
|
|
9845
9925
|
else if ('CloseSyncStream' in instruction) {
|
|
9846
|
-
|
|
9926
|
+
controller.abort();
|
|
9847
9927
|
hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
|
|
9848
9928
|
}
|
|
9849
9929
|
else if ('FlushFileSystem' in instruction) ;
|
|
@@ -9872,17 +9952,13 @@ The next upload iteration will be delayed.`);
|
|
|
9872
9952
|
}
|
|
9873
9953
|
await control(exports.PowerSyncControlCommand.START, JSON.stringify(options));
|
|
9874
9954
|
this.notifyCompletedUploads = () => {
|
|
9875
|
-
|
|
9876
|
-
controlInvocations.enqueueData({ command: exports.PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
|
|
9877
|
-
}
|
|
9955
|
+
controlInvocations?.inject({ command: exports.PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
|
|
9878
9956
|
};
|
|
9879
9957
|
this.handleActiveStreamsChange = () => {
|
|
9880
|
-
|
|
9881
|
-
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
});
|
|
9885
|
-
}
|
|
9958
|
+
controlInvocations?.inject({
|
|
9959
|
+
command: exports.PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
|
|
9960
|
+
payload: JSON.stringify(this.activeStreams)
|
|
9961
|
+
});
|
|
9886
9962
|
};
|
|
9887
9963
|
await receivingLines;
|
|
9888
9964
|
}
|
|
@@ -10899,7 +10975,7 @@ class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
10899
10975
|
* @returns A transaction of CRUD operations to upload, or null if there are none
|
|
10900
10976
|
*/
|
|
10901
10977
|
async getNextCrudTransaction() {
|
|
10902
|
-
const iterator = this.getCrudTransactions()[
|
|
10978
|
+
const iterator = this.getCrudTransactions()[Symbol.asyncIterator]();
|
|
10903
10979
|
return (await iterator.next()).value;
|
|
10904
10980
|
}
|
|
10905
10981
|
/**
|
|
@@ -10935,7 +11011,7 @@ class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
10935
11011
|
*/
|
|
10936
11012
|
getCrudTransactions() {
|
|
10937
11013
|
return {
|
|
10938
|
-
[
|
|
11014
|
+
[Symbol.asyncIterator]: () => {
|
|
10939
11015
|
let lastCrudItemId = -1;
|
|
10940
11016
|
const sql = `
|
|
10941
11017
|
WITH RECURSIVE crud_entries AS (
|
|
@@ -12104,7 +12180,6 @@ exports.DEFAULT_INDEX_OPTIONS = DEFAULT_INDEX_OPTIONS;
|
|
|
12104
12180
|
exports.DEFAULT_LOCK_TIMEOUT_MS = DEFAULT_LOCK_TIMEOUT_MS;
|
|
12105
12181
|
exports.DEFAULT_POWERSYNC_CLOSE_OPTIONS = DEFAULT_POWERSYNC_CLOSE_OPTIONS;
|
|
12106
12182
|
exports.DEFAULT_POWERSYNC_DB_OPTIONS = DEFAULT_POWERSYNC_DB_OPTIONS;
|
|
12107
|
-
exports.DEFAULT_PRESSURE_LIMITS = DEFAULT_PRESSURE_LIMITS;
|
|
12108
12183
|
exports.DEFAULT_REMOTE_LOGGER = DEFAULT_REMOTE_LOGGER;
|
|
12109
12184
|
exports.DEFAULT_REMOTE_OPTIONS = DEFAULT_REMOTE_OPTIONS;
|
|
12110
12185
|
exports.DEFAULT_RETRY_DELAY_MS = DEFAULT_RETRY_DELAY_MS;
|
|
@@ -12115,7 +12190,6 @@ exports.DEFAULT_SYNC_CLIENT_IMPLEMENTATION = DEFAULT_SYNC_CLIENT_IMPLEMENTATION;
|
|
|
12115
12190
|
exports.DEFAULT_TABLE_OPTIONS = DEFAULT_TABLE_OPTIONS;
|
|
12116
12191
|
exports.DEFAULT_WATCH_QUERY_OPTIONS = DEFAULT_WATCH_QUERY_OPTIONS;
|
|
12117
12192
|
exports.DEFAULT_WATCH_THROTTLE_MS = DEFAULT_WATCH_THROTTLE_MS;
|
|
12118
|
-
exports.DataStream = DataStream;
|
|
12119
12193
|
exports.DifferentialQueryProcessor = DifferentialQueryProcessor;
|
|
12120
12194
|
exports.EMPTY_DIFFERENTIAL = EMPTY_DIFFERENTIAL;
|
|
12121
12195
|
exports.FalsyComparator = FalsyComparator;
|
|
@@ -12133,6 +12207,7 @@ exports.OnChangeQueryProcessor = OnChangeQueryProcessor;
|
|
|
12133
12207
|
exports.OpType = OpType;
|
|
12134
12208
|
exports.OplogEntry = OplogEntry;
|
|
12135
12209
|
exports.Schema = Schema;
|
|
12210
|
+
exports.Semaphore = Semaphore;
|
|
12136
12211
|
exports.SqliteBucketStorage = SqliteBucketStorage;
|
|
12137
12212
|
exports.SyncDataBatch = SyncDataBatch;
|
|
12138
12213
|
exports.SyncDataBucket = SyncDataBucket;
|