@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
|
@@ -4,7 +4,6 @@ import { InternalProgressInformation } from '../../../db/crud/SyncProgress.js';
|
|
|
4
4
|
import { SyncStatus, SyncStatusOptions } from '../../../db/crud/SyncStatus.js';
|
|
5
5
|
import { AbortOperation } from '../../../utils/AbortOperation.js';
|
|
6
6
|
import { BaseListener, BaseObserver, BaseObserverInterface, Disposable } from '../../../utils/BaseObserver.js';
|
|
7
|
-
import { DataStream } from '../../../utils/DataStream.js';
|
|
8
7
|
import { throttleLeadingTrailing } from '../../../utils/async.js';
|
|
9
8
|
import {
|
|
10
9
|
BucketChecksum,
|
|
@@ -19,7 +18,6 @@ import { AbstractRemote, FetchStrategy, SyncStreamOptions } from './AbstractRemo
|
|
|
19
18
|
import { EstablishSyncStream, Instruction, coreStatusToJs } from './core-instruction.js';
|
|
20
19
|
import {
|
|
21
20
|
BucketRequest,
|
|
22
|
-
CrudUploadNotification,
|
|
23
21
|
StreamingSyncLine,
|
|
24
22
|
StreamingSyncLineOrCrudUploadComplete,
|
|
25
23
|
StreamingSyncRequestParameterType,
|
|
@@ -30,6 +28,15 @@ import {
|
|
|
30
28
|
isStreamingSyncCheckpointPartiallyComplete,
|
|
31
29
|
isStreamingSyncData
|
|
32
30
|
} from './streaming-sync-types.js';
|
|
31
|
+
import {
|
|
32
|
+
extractBsonObjects,
|
|
33
|
+
extractJsonLines,
|
|
34
|
+
injectable,
|
|
35
|
+
InjectableIterator,
|
|
36
|
+
map,
|
|
37
|
+
SimpleAsyncIterator
|
|
38
|
+
} from '../../../utils/stream_transform.js';
|
|
39
|
+
import type { BSON } from 'bson';
|
|
33
40
|
|
|
34
41
|
export enum LockType {
|
|
35
42
|
CRUD = 'crud',
|
|
@@ -682,6 +689,27 @@ The next upload iteration will be delayed.`);
|
|
|
682
689
|
});
|
|
683
690
|
}
|
|
684
691
|
|
|
692
|
+
private async receiveSyncLines(data: {
|
|
693
|
+
options: SyncStreamOptions;
|
|
694
|
+
connection: RequiredPowerSyncConnectionOptions;
|
|
695
|
+
bson?: typeof BSON;
|
|
696
|
+
}): Promise<SimpleAsyncIterator<Uint8Array | string>> {
|
|
697
|
+
const { options, connection, bson } = data;
|
|
698
|
+
const remote = this.options.remote;
|
|
699
|
+
|
|
700
|
+
if (connection.connectionMethod == SyncStreamConnectionMethod.HTTP) {
|
|
701
|
+
return await remote.fetchStream(options);
|
|
702
|
+
} else {
|
|
703
|
+
return await this.options.remote.socketStreamRaw(
|
|
704
|
+
{
|
|
705
|
+
...options,
|
|
706
|
+
...{ fetchStrategy: connection.fetchStrategy }
|
|
707
|
+
},
|
|
708
|
+
bson
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
685
713
|
private async legacyStreamingSyncIteration(signal: AbortSignal, resolvedOptions: RequiredPowerSyncConnectionOptions) {
|
|
686
714
|
const rawTables = resolvedOptions.serializedSchema?.raw_tables;
|
|
687
715
|
if (rawTables != null && rawTables.length) {
|
|
@@ -717,46 +745,31 @@ The next upload iteration will be delayed.`);
|
|
|
717
745
|
}
|
|
718
746
|
};
|
|
719
747
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
748
|
+
const bson = await this.options.remote.getBSON();
|
|
749
|
+
const source = await this.receiveSyncLines({
|
|
750
|
+
options: syncOptions,
|
|
751
|
+
connection: resolvedOptions,
|
|
752
|
+
bson
|
|
753
|
+
});
|
|
754
|
+
const stream: InjectableIterator<StreamingSyncLineOrCrudUploadComplete> = injectable(
|
|
755
|
+
map(source, (line) => {
|
|
723
756
|
if (typeof line == 'string') {
|
|
724
757
|
return JSON.parse(line) as StreamingSyncLine;
|
|
725
758
|
} else {
|
|
726
|
-
|
|
727
|
-
return line;
|
|
759
|
+
return bson.deserialize(line) as StreamingSyncLine;
|
|
728
760
|
}
|
|
729
|
-
})
|
|
730
|
-
|
|
731
|
-
const bson = await this.options.remote.getBSON();
|
|
732
|
-
stream = await this.options.remote.socketStreamRaw(
|
|
733
|
-
{
|
|
734
|
-
...syncOptions,
|
|
735
|
-
...{ fetchStrategy: resolvedOptions.fetchStrategy }
|
|
736
|
-
},
|
|
737
|
-
(payload: Uint8Array | CrudUploadNotification) => {
|
|
738
|
-
if (payload instanceof Uint8Array) {
|
|
739
|
-
return bson.deserialize(payload) as StreamingSyncLine;
|
|
740
|
-
} else {
|
|
741
|
-
// Directly enqueued by us
|
|
742
|
-
return payload;
|
|
743
|
-
}
|
|
744
|
-
},
|
|
745
|
-
bson
|
|
746
|
-
);
|
|
747
|
-
}
|
|
761
|
+
})
|
|
762
|
+
);
|
|
748
763
|
|
|
749
764
|
this.logger.debug('Stream established. Processing events');
|
|
750
765
|
|
|
751
766
|
this.notifyCompletedUploads = () => {
|
|
752
|
-
|
|
753
|
-
stream.enqueueData({ crud_upload_completed: null });
|
|
754
|
-
}
|
|
767
|
+
stream.inject({ crud_upload_completed: null });
|
|
755
768
|
};
|
|
756
769
|
|
|
757
|
-
while (
|
|
758
|
-
const line = await stream.
|
|
759
|
-
if (
|
|
770
|
+
while (true) {
|
|
771
|
+
const { value: line, done } = await stream.next();
|
|
772
|
+
if (done) {
|
|
760
773
|
// The stream has closed while waiting
|
|
761
774
|
return;
|
|
762
775
|
}
|
|
@@ -942,6 +955,11 @@ The next upload iteration will be delayed.`);
|
|
|
942
955
|
const syncImplementation = this;
|
|
943
956
|
const adapter = this.options.adapter;
|
|
944
957
|
const remote = this.options.remote;
|
|
958
|
+
const controller = new AbortController();
|
|
959
|
+
const abort = () => {
|
|
960
|
+
return controller.abort(signal.reason);
|
|
961
|
+
};
|
|
962
|
+
signal.addEventListener('abort', abort);
|
|
945
963
|
let receivingLines: Promise<void> | null = null;
|
|
946
964
|
let hadSyncLine = false;
|
|
947
965
|
let hideDisconnectOnRestart = false;
|
|
@@ -949,64 +967,53 @@ The next upload iteration will be delayed.`);
|
|
|
949
967
|
if (signal.aborted) {
|
|
950
968
|
throw new AbortOperation('Connection request has been aborted');
|
|
951
969
|
}
|
|
952
|
-
const abortController = new AbortController();
|
|
953
|
-
signal.addEventListener('abort', () => abortController.abort());
|
|
954
970
|
|
|
955
971
|
// Pending sync lines received from the service, as well as local events that trigger a powersync_control
|
|
956
972
|
// invocation (local events include refreshed tokens and completed uploads).
|
|
957
973
|
// This is a single data stream so that we can handle all control calls from a single place.
|
|
958
|
-
let controlInvocations:
|
|
974
|
+
let controlInvocations: InjectableIterator<EnqueuedCommand> | null = null;
|
|
959
975
|
|
|
960
976
|
async function connect(instr: EstablishSyncStream) {
|
|
961
977
|
const syncOptions: SyncStreamOptions = {
|
|
962
978
|
path: '/sync/stream',
|
|
963
|
-
abortSignal:
|
|
979
|
+
abortSignal: controller.signal,
|
|
964
980
|
data: instr.request
|
|
965
981
|
};
|
|
966
982
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
// Directly enqueued by us
|
|
976
|
-
return line;
|
|
977
|
-
}
|
|
978
|
-
});
|
|
979
|
-
} else {
|
|
980
|
-
controlInvocations = await remote.socketStreamRaw(
|
|
981
|
-
{
|
|
982
|
-
...syncOptions,
|
|
983
|
-
fetchStrategy: resolvedOptions.fetchStrategy
|
|
984
|
-
},
|
|
985
|
-
(payload: Uint8Array | EnqueuedCommand) => {
|
|
986
|
-
if (payload instanceof Uint8Array) {
|
|
983
|
+
controlInvocations = injectable(
|
|
984
|
+
map(
|
|
985
|
+
await syncImplementation.receiveSyncLines({
|
|
986
|
+
options: syncOptions,
|
|
987
|
+
connection: resolvedOptions
|
|
988
|
+
}),
|
|
989
|
+
(line) => {
|
|
990
|
+
if (typeof line == 'string') {
|
|
987
991
|
return {
|
|
988
|
-
command: PowerSyncControlCommand.
|
|
989
|
-
payload:
|
|
992
|
+
command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
|
|
993
|
+
payload: line
|
|
990
994
|
};
|
|
991
995
|
} else {
|
|
992
|
-
|
|
993
|
-
|
|
996
|
+
return {
|
|
997
|
+
command: PowerSyncControlCommand.PROCESS_BSON_LINE,
|
|
998
|
+
payload: line
|
|
999
|
+
};
|
|
994
1000
|
}
|
|
995
1001
|
}
|
|
996
|
-
)
|
|
997
|
-
|
|
1002
|
+
)
|
|
1003
|
+
);
|
|
998
1004
|
|
|
999
1005
|
// The rust client will set connected: true after the first sync line because that's when it gets invoked, but
|
|
1000
1006
|
// we're already connected here and can report that.
|
|
1001
1007
|
syncImplementation.updateSyncStatus({ connected: true });
|
|
1002
1008
|
|
|
1003
1009
|
try {
|
|
1004
|
-
while (
|
|
1005
|
-
|
|
1006
|
-
if (
|
|
1007
|
-
|
|
1010
|
+
while (true) {
|
|
1011
|
+
let event = await controlInvocations.next();
|
|
1012
|
+
if (event.done) {
|
|
1013
|
+
break;
|
|
1008
1014
|
}
|
|
1009
1015
|
|
|
1016
|
+
const line = event.value;
|
|
1010
1017
|
await control(line.command, line.payload);
|
|
1011
1018
|
|
|
1012
1019
|
if (!hadSyncLine) {
|
|
@@ -1015,12 +1022,8 @@ The next upload iteration will be delayed.`);
|
|
|
1015
1022
|
}
|
|
1016
1023
|
}
|
|
1017
1024
|
} finally {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
// refreshed. That would throw after closing (and we can't handle those events either way), so set this back
|
|
1021
|
-
// to null.
|
|
1022
|
-
controlInvocations = null;
|
|
1023
|
-
await activeInstructions.close();
|
|
1025
|
+
abort();
|
|
1026
|
+
signal.removeEventListener('abort', abort);
|
|
1024
1027
|
}
|
|
1025
1028
|
}
|
|
1026
1029
|
|
|
@@ -1072,7 +1075,7 @@ The next upload iteration will be delayed.`);
|
|
|
1072
1075
|
// Restart iteration after the credentials have been refreshed.
|
|
1073
1076
|
remote.fetchCredentials().then(
|
|
1074
1077
|
(_) => {
|
|
1075
|
-
controlInvocations?.
|
|
1078
|
+
controlInvocations?.inject({ command: PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
|
|
1076
1079
|
},
|
|
1077
1080
|
(err) => {
|
|
1078
1081
|
syncImplementation.logger.warn('Could not prefetch credentials', err);
|
|
@@ -1080,7 +1083,7 @@ The next upload iteration will be delayed.`);
|
|
|
1080
1083
|
);
|
|
1081
1084
|
}
|
|
1082
1085
|
} else if ('CloseSyncStream' in instruction) {
|
|
1083
|
-
|
|
1086
|
+
controller.abort();
|
|
1084
1087
|
hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
|
|
1085
1088
|
} else if ('FlushFileSystem' in instruction) {
|
|
1086
1089
|
// Not necessary on JS platforms.
|
|
@@ -1113,17 +1116,13 @@ The next upload iteration will be delayed.`);
|
|
|
1113
1116
|
await control(PowerSyncControlCommand.START, JSON.stringify(options));
|
|
1114
1117
|
|
|
1115
1118
|
this.notifyCompletedUploads = () => {
|
|
1116
|
-
|
|
1117
|
-
controlInvocations.enqueueData({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
|
|
1118
|
-
}
|
|
1119
|
+
controlInvocations?.inject({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
|
|
1119
1120
|
};
|
|
1120
1121
|
this.handleActiveStreamsChange = () => {
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
});
|
|
1126
|
-
}
|
|
1122
|
+
controlInvocations?.inject({
|
|
1123
|
+
command: PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
|
|
1124
|
+
payload: JSON.stringify(this.activeStreams)
|
|
1125
|
+
});
|
|
1127
1126
|
};
|
|
1128
1127
|
await receivingLines;
|
|
1129
1128
|
} finally {
|
package/src/index.ts
CHANGED
|
@@ -59,9 +59,9 @@ export * from './client/watched/WatchedQuery.js';
|
|
|
59
59
|
export * from './utils/AbortOperation.js';
|
|
60
60
|
export * from './utils/BaseObserver.js';
|
|
61
61
|
export * from './utils/ControlledExecutor.js';
|
|
62
|
-
export * from './utils/DataStream.js';
|
|
63
62
|
export * from './utils/Logger.js';
|
|
64
63
|
export * from './utils/mutex.js';
|
|
65
64
|
export * from './utils/parseQuery.js';
|
|
65
|
+
export type { SimpleAsyncIterator } from './utils/stream_transform.js';
|
|
66
66
|
|
|
67
67
|
export * from './types/types.js';
|
package/src/utils/async.ts
CHANGED
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A ponyfill for `Symbol.asyncIterator` that is compatible with the
|
|
3
|
-
* [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)
|
|
4
|
-
* we recommend for React Native.
|
|
5
|
-
*
|
|
6
|
-
* As long as we use this symbol (instead of `for await` and `async *`) in this package, we can be compatible with async
|
|
7
|
-
* iterators without requiring them.
|
|
8
|
-
*/
|
|
9
|
-
export const symbolAsyncIterator: typeof Symbol.asyncIterator =
|
|
10
|
-
Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
|
|
11
|
-
|
|
12
1
|
/**
|
|
13
2
|
* Throttle a function to be called at most once every "wait" milliseconds,
|
|
14
3
|
* on the trailing edge.
|
package/src/utils/mutex.ts
CHANGED
|
@@ -1,21 +1,32 @@
|
|
|
1
|
+
import { Queue } from './queue.js';
|
|
2
|
+
|
|
1
3
|
export type UnlockFn = () => void;
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
|
-
* An asynchronous
|
|
6
|
+
* An asynchronous semaphore implementation with associated items per lease.
|
|
5
7
|
*
|
|
6
8
|
* @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
|
|
7
9
|
*/
|
|
8
|
-
export class
|
|
9
|
-
|
|
10
|
+
export class Semaphore<T> {
|
|
11
|
+
// Available items that are not currently assigned to a waiter.
|
|
12
|
+
private readonly available: Queue<T>;
|
|
10
13
|
|
|
14
|
+
readonly size: number;
|
|
11
15
|
// Linked list of waiters. We don't expect the wait list to become particularly large, and this allows removing
|
|
12
16
|
// aborted waiters from the middle of the list efficiently.
|
|
13
|
-
private firstWaiter?:
|
|
14
|
-
private lastWaiter?:
|
|
17
|
+
private firstWaiter?: SemaphoreWaitNode<T>;
|
|
18
|
+
private lastWaiter?: SemaphoreWaitNode<T>;
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
constructor(elements: Iterable<T>) {
|
|
21
|
+
this.available = new Queue(elements);
|
|
22
|
+
this.size = this.available.length;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private addWaiter(requestedItems: number, onAcquire: () => void): SemaphoreWaitNode<T> {
|
|
26
|
+
const node: SemaphoreWaitNode<T> = {
|
|
18
27
|
isActive: true,
|
|
28
|
+
acquiredItems: [],
|
|
29
|
+
remainingItems: requestedItems,
|
|
19
30
|
onAcquire,
|
|
20
31
|
prev: this.lastWaiter
|
|
21
32
|
};
|
|
@@ -30,7 +41,7 @@ export class Mutex {
|
|
|
30
41
|
return node;
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
private deactivateWaiter(waiter:
|
|
44
|
+
private deactivateWaiter(waiter: SemaphoreWaitNode<T>) {
|
|
34
45
|
const { prev, next } = waiter;
|
|
35
46
|
waiter.isActive = false;
|
|
36
47
|
|
|
@@ -40,77 +51,129 @@ export class Mutex {
|
|
|
40
51
|
if (waiter == this.lastWaiter) this.lastWaiter = prev;
|
|
41
52
|
}
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
private requestPermits(amount: number, abort?: AbortSignal): Promise<{ items: T[]; release: UnlockFn }> {
|
|
55
|
+
if (amount <= 0 || amount > this.size) {
|
|
56
|
+
throw new Error(`Invalid amount of items requested (${amount}), must be between 1 and ${this.size}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
44
59
|
return new Promise((resolve, reject) => {
|
|
45
60
|
function rejectAborted() {
|
|
46
|
-
reject(abort?.reason ?? new Error('
|
|
61
|
+
reject(abort?.reason ?? new Error('Semaphore acquire aborted'));
|
|
47
62
|
}
|
|
48
63
|
if (abort?.aborted) {
|
|
49
64
|
return rejectAborted();
|
|
50
65
|
}
|
|
51
66
|
|
|
52
|
-
let
|
|
67
|
+
let waiter: SemaphoreWaitNode<T>;
|
|
53
68
|
|
|
54
69
|
const markCompleted = () => {
|
|
55
|
-
|
|
56
|
-
|
|
70
|
+
const items = waiter.acquiredItems;
|
|
71
|
+
waiter.acquiredItems = []; // Avoid releasing items twice.
|
|
72
|
+
|
|
73
|
+
for (const element of items) {
|
|
74
|
+
// Give to next waiter, if possible.
|
|
75
|
+
const nextWaiter = this.firstWaiter;
|
|
76
|
+
if (nextWaiter) {
|
|
77
|
+
nextWaiter.acquiredItems.push(element);
|
|
78
|
+
nextWaiter.remainingItems--;
|
|
79
|
+
if (nextWaiter.remainingItems == 0) {
|
|
80
|
+
nextWaiter.onAcquire();
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// No pending waiter, return lease into pool.
|
|
84
|
+
this.available.addLast(element);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const onAbort = () => {
|
|
90
|
+
abort?.removeEventListener('abort', onAbort);
|
|
57
91
|
|
|
58
|
-
|
|
59
|
-
if (waiter) {
|
|
92
|
+
if (waiter.isActive) {
|
|
60
93
|
this.deactivateWaiter(waiter);
|
|
61
|
-
|
|
62
|
-
waiter.onAcquire();
|
|
63
|
-
} else {
|
|
64
|
-
this.inCriticalSection = false;
|
|
94
|
+
rejectAborted();
|
|
65
95
|
}
|
|
66
96
|
};
|
|
67
97
|
|
|
68
|
-
|
|
69
|
-
this.
|
|
70
|
-
|
|
71
|
-
return resolve(markCompleted);
|
|
72
|
-
} else {
|
|
73
|
-
let node: MutexWaitNode;
|
|
98
|
+
const resolvePromise = () => {
|
|
99
|
+
this.deactivateWaiter(waiter);
|
|
100
|
+
abort?.removeEventListener('abort', onAbort);
|
|
74
101
|
|
|
75
|
-
const
|
|
76
|
-
|
|
102
|
+
const items = waiter.acquiredItems;
|
|
103
|
+
resolve({ items, release: markCompleted });
|
|
104
|
+
};
|
|
77
105
|
|
|
78
|
-
|
|
79
|
-
this.deactivateWaiter(node);
|
|
80
|
-
rejectAborted();
|
|
81
|
-
}
|
|
82
|
-
};
|
|
106
|
+
waiter = this.addWaiter(amount, resolvePromise);
|
|
83
107
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
108
|
+
// If there are items in the pool that haven't been assigned, we can pull them into this waiter. Note that this is
|
|
109
|
+
// only the case if we're the first waiter (otherwise, items would have been assigned to an earlier waiter).
|
|
110
|
+
while (!this.available.isEmpty && waiter.remainingItems > 0) {
|
|
111
|
+
waiter.acquiredItems.push(this.available.removeFirst());
|
|
112
|
+
waiter.remainingItems--;
|
|
113
|
+
}
|
|
89
114
|
|
|
90
|
-
|
|
115
|
+
if (waiter.remainingItems == 0) {
|
|
116
|
+
return resolvePromise();
|
|
91
117
|
}
|
|
118
|
+
|
|
119
|
+
abort?.addEventListener('abort', onAbort);
|
|
92
120
|
});
|
|
93
121
|
}
|
|
94
122
|
|
|
95
|
-
|
|
96
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Requests a single item from the pool.
|
|
125
|
+
*
|
|
126
|
+
* The returned `release` callback must be invoked to return the item into the pool.
|
|
127
|
+
*/
|
|
128
|
+
async requestOne(abort?: AbortSignal): Promise<{ item: T; release: UnlockFn }> {
|
|
129
|
+
const { items, release } = await this.requestPermits(1, abort);
|
|
130
|
+
return { release, item: items[0] };
|
|
131
|
+
}
|
|
97
132
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Requests access to all items from the pool.
|
|
135
|
+
*
|
|
136
|
+
* The returned `release` callback must be invoked to return items into the pool.
|
|
137
|
+
*/
|
|
138
|
+
requestAll(abort?: AbortSignal): Promise<{ items: T[]; release: UnlockFn }> {
|
|
139
|
+
return this.requestPermits(this.size, abort);
|
|
103
140
|
}
|
|
104
141
|
}
|
|
105
142
|
|
|
106
|
-
interface
|
|
143
|
+
interface SemaphoreWaitNode<T> {
|
|
107
144
|
/**
|
|
108
145
|
* Whether the waiter is currently active (not aborted and not fullfilled).
|
|
109
146
|
*/
|
|
110
147
|
isActive: boolean;
|
|
148
|
+
acquiredItems: T[];
|
|
149
|
+
remainingItems: number;
|
|
111
150
|
onAcquire: () => void;
|
|
112
|
-
prev?:
|
|
113
|
-
next?:
|
|
151
|
+
prev?: SemaphoreWaitNode<T>;
|
|
152
|
+
next?: SemaphoreWaitNode<T>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* An asynchronous mutex implementation.
|
|
157
|
+
*
|
|
158
|
+
* @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
|
|
159
|
+
*/
|
|
160
|
+
export class Mutex {
|
|
161
|
+
private inner = new Semaphore([null]);
|
|
162
|
+
|
|
163
|
+
async acquire(abort?: AbortSignal): Promise<UnlockFn> {
|
|
164
|
+
const { release } = await this.inner.requestOne(abort);
|
|
165
|
+
return release;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async runExclusive<T>(fn: () => PromiseLike<T> | T, abort?: AbortSignal): Promise<T> {
|
|
169
|
+
const returnMutex = await this.acquire(abort);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
return await fn();
|
|
173
|
+
} finally {
|
|
174
|
+
returnMutex();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
114
177
|
}
|
|
115
178
|
|
|
116
179
|
/**
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A simple fixed-capacity queue implementation.
|
|
3
|
+
*
|
|
4
|
+
* Unlike a naive queue implemented by `array.push()` and `array.shift()`, this avoids moving array elements around
|
|
5
|
+
* and is `O(1)` for {@link addLast} and {@link removeFirst}.
|
|
6
|
+
*/
|
|
7
|
+
export class Queue<T> {
|
|
8
|
+
private table: (T | undefined)[];
|
|
9
|
+
// Index of the first element in the table.
|
|
10
|
+
private head: number;
|
|
11
|
+
// Amount of items currently in the queue.
|
|
12
|
+
private _length: number;
|
|
13
|
+
|
|
14
|
+
constructor(initialItems: Iterable<T>) {
|
|
15
|
+
this.table = [...initialItems];
|
|
16
|
+
this.head = 0;
|
|
17
|
+
this._length = this.table.length;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get isEmpty(): boolean {
|
|
21
|
+
return this.length == 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get length(): number {
|
|
25
|
+
return this._length;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
removeFirst(): T {
|
|
29
|
+
if (this.isEmpty) {
|
|
30
|
+
throw new Error('Queue is empty');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const result = this.table[this.head] as T;
|
|
34
|
+
this._length--;
|
|
35
|
+
this.table[this.head] = undefined;
|
|
36
|
+
this.head = (this.head + 1) % this.table.length;
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
addLast(element: T) {
|
|
41
|
+
if (this.length == this.table.length) {
|
|
42
|
+
throw new Error('Queue is full');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.table[(this.head + this._length) % this.table.length] = element;
|
|
46
|
+
this._length++;
|
|
47
|
+
}
|
|
48
|
+
}
|