@powersync/common 0.0.0-dev-20240516143814 → 0.0.0-dev-20240610133845
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/lib/client/AbstractPowerSyncDatabase.d.ts +1 -1
- package/lib/client/AbstractPowerSyncDatabase.js +6 -1
- package/lib/client/sync/stream/AbstractRemote.d.ts +16 -2
- package/lib/client/sync/stream/AbstractRemote.js +25 -7
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +4 -0
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +9 -4
- package/lib/client/sync/stream/streaming-sync-types.d.ts +4 -0
- package/lib/utils/ControlledExecutor.d.ts +25 -0
- package/lib/utils/ControlledExecutor.js +50 -0
- package/package.json +1 -1
|
@@ -50,7 +50,7 @@ export interface WatchHandler {
|
|
|
50
50
|
onError?: (error: Error) => void;
|
|
51
51
|
}
|
|
52
52
|
export interface WatchOnChangeHandler {
|
|
53
|
-
onChange: (event: WatchOnChangeEvent) => void;
|
|
53
|
+
onChange: (event: WatchOnChangeEvent) => Promise<void> | void;
|
|
54
54
|
onError?: (error: Error) => void;
|
|
55
55
|
}
|
|
56
56
|
export interface PowerSyncDBListener extends StreamingSyncImplementationListener {
|
|
@@ -13,6 +13,7 @@ import { CrudBatch } from './sync/bucket/CrudBatch';
|
|
|
13
13
|
import { CrudEntry } from './sync/bucket/CrudEntry';
|
|
14
14
|
import { CrudTransaction } from './sync/bucket/CrudTransaction';
|
|
15
15
|
import { DEFAULT_CRUD_UPLOAD_THROTTLE_MS } from './sync/stream/AbstractStreamingSyncImplementation';
|
|
16
|
+
import { ControlledExecutor } from '../utils/ControlledExecutor';
|
|
16
17
|
const POWERSYNC_TABLE_MATCH = /(^ps_data__|^ps_data_local__)/;
|
|
17
18
|
const DEFAULT_DISCONNECT_CLEAR_OPTIONS = {
|
|
18
19
|
clearLocal: true
|
|
@@ -565,10 +566,13 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
565
566
|
const watchedTables = new Set(resolvedOptions.tables ?? []);
|
|
566
567
|
const changedTables = new Set();
|
|
567
568
|
const throttleMs = resolvedOptions.throttleMs ?? DEFAULT_WATCH_THROTTLE_MS;
|
|
569
|
+
const executor = new ControlledExecutor(async (e) => {
|
|
570
|
+
await onChange(e);
|
|
571
|
+
});
|
|
568
572
|
const flushTableUpdates = throttle(() => this.handleTableChanges(changedTables, watchedTables, (intersection) => {
|
|
569
573
|
if (resolvedOptions?.signal?.aborted)
|
|
570
574
|
return;
|
|
571
|
-
|
|
575
|
+
executor.schedule({ changedTables: intersection });
|
|
572
576
|
}), throttleMs, { leading: false, trailing: true });
|
|
573
577
|
const dispose = this.database.registerListener({
|
|
574
578
|
tablesUpdated: async (update) => {
|
|
@@ -583,6 +587,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
583
587
|
}
|
|
584
588
|
});
|
|
585
589
|
resolvedOptions.signal?.addEventListener('abort', () => {
|
|
590
|
+
executor.dispose();
|
|
586
591
|
dispose();
|
|
587
592
|
});
|
|
588
593
|
return () => dispose();
|
|
@@ -16,6 +16,16 @@ export type SyncStreamOptions = {
|
|
|
16
16
|
abortSignal?: AbortSignal;
|
|
17
17
|
fetchOptions?: Request;
|
|
18
18
|
};
|
|
19
|
+
export type FetchImplementation = typeof fetch;
|
|
20
|
+
/**
|
|
21
|
+
* Class wrapper for providing a fetch implementation.
|
|
22
|
+
* The class wrapper is used to distinguish the fetchImplementation
|
|
23
|
+
* option in [AbstractRemoteOptions] from the general fetch method
|
|
24
|
+
* which is typeof "function"
|
|
25
|
+
*/
|
|
26
|
+
export declare class FetchImplementationProvider {
|
|
27
|
+
getFetch(): FetchImplementation;
|
|
28
|
+
}
|
|
19
29
|
export type AbstractRemoteOptions = {
|
|
20
30
|
/**
|
|
21
31
|
* Transforms the PowerSync base URL which might contain
|
|
@@ -28,7 +38,7 @@ export type AbstractRemoteOptions = {
|
|
|
28
38
|
* Note that this usually needs to be bound to the global scope.
|
|
29
39
|
* Binding should be done before passing here.
|
|
30
40
|
*/
|
|
31
|
-
fetchImplementation:
|
|
41
|
+
fetchImplementation: FetchImplementation | FetchImplementationProvider;
|
|
32
42
|
};
|
|
33
43
|
export declare const DEFAULT_REMOTE_OPTIONS: AbstractRemoteOptions;
|
|
34
44
|
export declare abstract class AbstractRemote {
|
|
@@ -37,7 +47,11 @@ export declare abstract class AbstractRemote {
|
|
|
37
47
|
protected credentials: PowerSyncCredentials | null;
|
|
38
48
|
protected options: AbstractRemoteOptions;
|
|
39
49
|
constructor(connector: RemoteConnector, logger?: ILogger, options?: Partial<AbstractRemoteOptions>);
|
|
40
|
-
|
|
50
|
+
/**
|
|
51
|
+
* @returns a fetch implementation (function)
|
|
52
|
+
* which can be called to perform fetch requests
|
|
53
|
+
*/
|
|
54
|
+
get fetch(): FetchImplementation;
|
|
41
55
|
getCredentials(): Promise<PowerSyncCredentials | null>;
|
|
42
56
|
protected buildRequest(path: string): Promise<{
|
|
43
57
|
url: string;
|
|
@@ -15,11 +15,22 @@ const KEEP_ALIVE_MS = 20_000;
|
|
|
15
15
|
// The ACK must be received in this period
|
|
16
16
|
const KEEP_ALIVE_LIFETIME_MS = 30_000;
|
|
17
17
|
export const DEFAULT_REMOTE_LOGGER = Logger.get('PowerSyncRemote');
|
|
18
|
+
/**
|
|
19
|
+
* Class wrapper for providing a fetch implementation.
|
|
20
|
+
* The class wrapper is used to distinguish the fetchImplementation
|
|
21
|
+
* option in [AbstractRemoteOptions] from the general fetch method
|
|
22
|
+
* which is typeof "function"
|
|
23
|
+
*/
|
|
24
|
+
export class FetchImplementationProvider {
|
|
25
|
+
getFetch() {
|
|
26
|
+
return fetch.bind(globalThis);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
18
29
|
export const DEFAULT_REMOTE_OPTIONS = {
|
|
19
30
|
socketUrlTransformer: (url) => url.replace(/^https?:\/\//, function (match) {
|
|
20
31
|
return match === 'https://' ? 'wss://' : 'ws://';
|
|
21
32
|
}),
|
|
22
|
-
fetchImplementation:
|
|
33
|
+
fetchImplementation: new FetchImplementationProvider()
|
|
23
34
|
};
|
|
24
35
|
export class AbstractRemote {
|
|
25
36
|
connector;
|
|
@@ -34,8 +45,15 @@ export class AbstractRemote {
|
|
|
34
45
|
...(options ?? {})
|
|
35
46
|
};
|
|
36
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* @returns a fetch implementation (function)
|
|
50
|
+
* which can be called to perform fetch requests
|
|
51
|
+
*/
|
|
37
52
|
get fetch() {
|
|
38
|
-
|
|
53
|
+
const { fetchImplementation } = this.options;
|
|
54
|
+
return fetchImplementation instanceof FetchImplementationProvider
|
|
55
|
+
? fetchImplementation.getFetch()
|
|
56
|
+
: fetchImplementation;
|
|
39
57
|
}
|
|
40
58
|
async getCredentials() {
|
|
41
59
|
const { expiresAt } = this.credentials ?? {};
|
|
@@ -119,7 +137,7 @@ export class AbstractRemote {
|
|
|
119
137
|
async socketStream(options) {
|
|
120
138
|
const { path } = options;
|
|
121
139
|
const request = await this.buildRequest(path);
|
|
122
|
-
const
|
|
140
|
+
const bson = await this.getBSON();
|
|
123
141
|
const connector = new RSocketConnector({
|
|
124
142
|
transport: new WebsocketClientTransport({
|
|
125
143
|
url: this.options.socketUrlTransformer(request.url)
|
|
@@ -131,7 +149,7 @@ export class AbstractRemote {
|
|
|
131
149
|
metadataMimeType: 'application/bson',
|
|
132
150
|
payload: {
|
|
133
151
|
data: null,
|
|
134
|
-
metadata: Buffer.from(
|
|
152
|
+
metadata: Buffer.from(bson.serialize({
|
|
135
153
|
token: request.headers.Authorization
|
|
136
154
|
}))
|
|
137
155
|
}
|
|
@@ -167,8 +185,8 @@ export class AbstractRemote {
|
|
|
167
185
|
const socket = await new Promise((resolve, reject) => {
|
|
168
186
|
let connectionEstablished = false;
|
|
169
187
|
const res = rsocket.requestStream({
|
|
170
|
-
data: Buffer.from(
|
|
171
|
-
metadata: Buffer.from(
|
|
188
|
+
data: Buffer.from(bson.serialize(options.data)),
|
|
189
|
+
metadata: Buffer.from(bson.serialize({
|
|
172
190
|
path
|
|
173
191
|
}))
|
|
174
192
|
}, SYNC_QUEUE_REQUEST_N, // The initial N amount
|
|
@@ -199,7 +217,7 @@ export class AbstractRemote {
|
|
|
199
217
|
if (!data) {
|
|
200
218
|
return;
|
|
201
219
|
}
|
|
202
|
-
const deserializedData =
|
|
220
|
+
const deserializedData = bson.deserialize(data);
|
|
203
221
|
stream.enqueueData(deserializedData);
|
|
204
222
|
},
|
|
205
223
|
onComplete: () => {
|
|
@@ -54,6 +54,10 @@ export interface PowerSyncConnectionOptions {
|
|
|
54
54
|
* Defaults to a HTTP streaming connection.
|
|
55
55
|
*/
|
|
56
56
|
connectionMethod?: SyncStreamConnectionMethod;
|
|
57
|
+
/**
|
|
58
|
+
* Parameters to be passed to the sync rules. Parameters be available under the`user_parameters` object.
|
|
59
|
+
*/
|
|
60
|
+
params?: Record<string, any>;
|
|
57
61
|
}
|
|
58
62
|
export interface StreamingSyncImplementation extends BaseObserver<StreamingSyncImplementationListener>, Disposable {
|
|
59
63
|
/**
|
|
@@ -22,7 +22,8 @@ export const DEFAULT_STREAMING_SYNC_OPTIONS = {
|
|
|
22
22
|
crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
|
|
23
23
|
};
|
|
24
24
|
export const DEFAULT_STREAM_CONNECTION_OPTIONS = {
|
|
25
|
-
connectionMethod: SyncStreamConnectionMethod.HTTP
|
|
25
|
+
connectionMethod: SyncStreamConnectionMethod.HTTP,
|
|
26
|
+
params: {}
|
|
26
27
|
};
|
|
27
28
|
export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
28
29
|
_lastSyncedAt;
|
|
@@ -116,13 +117,16 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
116
117
|
}
|
|
117
118
|
catch (ex) {
|
|
118
119
|
this.updateSyncStatus({
|
|
119
|
-
connected: false,
|
|
120
120
|
dataFlow: {
|
|
121
121
|
uploading: false
|
|
122
122
|
}
|
|
123
123
|
});
|
|
124
124
|
await this.delayRetry();
|
|
125
|
-
|
|
125
|
+
if (!this.isConnected) {
|
|
126
|
+
// Exit the upload loop if the sync stream is no longer connected
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
this.logger.debug(`Caught exception when uploading. Upload will retry after a delay. Exception: ${ex.message}`);
|
|
126
130
|
}
|
|
127
131
|
finally {
|
|
128
132
|
this.updateSyncStatus({
|
|
@@ -312,7 +316,8 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
312
316
|
data: {
|
|
313
317
|
buckets: req,
|
|
314
318
|
include_checksum: true,
|
|
315
|
-
raw_data: true
|
|
319
|
+
raw_data: true,
|
|
320
|
+
parameters: resolvedOptions.params
|
|
316
321
|
}
|
|
317
322
|
};
|
|
318
323
|
const stream = resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP
|
|
@@ -59,6 +59,10 @@ export interface StreamingSyncRequest {
|
|
|
59
59
|
* Changes the response to stringified data in each OplogEntry
|
|
60
60
|
*/
|
|
61
61
|
raw_data: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Client parameters to be passed to the sync rules.
|
|
64
|
+
*/
|
|
65
|
+
parameters?: Record<string, any>;
|
|
62
66
|
}
|
|
63
67
|
export interface StreamingSyncCheckpoint {
|
|
64
68
|
checkpoint: Checkpoint;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ControlledExecutorOptions {
|
|
2
|
+
/**
|
|
3
|
+
* If throttling is enabled, it ensures only one task runs at a time,
|
|
4
|
+
* and only one additional task can be scheduled to run after the current task completes. The pending task will be overwritten by the latest task.
|
|
5
|
+
* Enabled by default.
|
|
6
|
+
*/
|
|
7
|
+
throttleEnabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare class ControlledExecutor<T> {
|
|
10
|
+
private task;
|
|
11
|
+
/**
|
|
12
|
+
* Represents the currently running task, which could be a Promise or undefined if no task is running.
|
|
13
|
+
*/
|
|
14
|
+
private runningTask;
|
|
15
|
+
private pendingTaskParam;
|
|
16
|
+
/**
|
|
17
|
+
* Flag to determine if throttling is enabled, which controls whether tasks are queued or run immediately.
|
|
18
|
+
*/
|
|
19
|
+
private isThrottling;
|
|
20
|
+
private closed;
|
|
21
|
+
constructor(task: (param: T) => Promise<void> | void, options?: ControlledExecutorOptions);
|
|
22
|
+
schedule(param: T): void;
|
|
23
|
+
dispose(): void;
|
|
24
|
+
private execute;
|
|
25
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export class ControlledExecutor {
|
|
2
|
+
task;
|
|
3
|
+
/**
|
|
4
|
+
* Represents the currently running task, which could be a Promise or undefined if no task is running.
|
|
5
|
+
*/
|
|
6
|
+
runningTask;
|
|
7
|
+
pendingTaskParam;
|
|
8
|
+
/**
|
|
9
|
+
* Flag to determine if throttling is enabled, which controls whether tasks are queued or run immediately.
|
|
10
|
+
*/
|
|
11
|
+
isThrottling;
|
|
12
|
+
closed;
|
|
13
|
+
constructor(task, options) {
|
|
14
|
+
this.task = task;
|
|
15
|
+
const { throttleEnabled = true } = options ?? {};
|
|
16
|
+
this.isThrottling = throttleEnabled;
|
|
17
|
+
this.closed = false;
|
|
18
|
+
}
|
|
19
|
+
schedule(param) {
|
|
20
|
+
if (this.closed) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (!this.isThrottling) {
|
|
24
|
+
this.task(param);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (this.runningTask) {
|
|
28
|
+
// set or replace the pending task param with latest one
|
|
29
|
+
this.pendingTaskParam = param;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.execute(param);
|
|
33
|
+
}
|
|
34
|
+
dispose() {
|
|
35
|
+
this.closed = true;
|
|
36
|
+
if (this.runningTask) {
|
|
37
|
+
this.runningTask = undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async execute(param) {
|
|
41
|
+
this.runningTask = this.task(param);
|
|
42
|
+
await this.runningTask;
|
|
43
|
+
this.runningTask = undefined;
|
|
44
|
+
if (this.pendingTaskParam) {
|
|
45
|
+
const pendingParam = this.pendingTaskParam;
|
|
46
|
+
this.pendingTaskParam = undefined;
|
|
47
|
+
this.execute(pendingParam);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|