@powersync/common 1.33.2 → 1.35.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 +5 -5
- package/dist/bundle.mjs +3 -3
- package/lib/client/AbstractPowerSyncDatabase.d.ts +58 -13
- package/lib/client/AbstractPowerSyncDatabase.js +107 -50
- package/lib/client/ConnectionManager.d.ts +4 -4
- package/lib/client/CustomQuery.d.ts +22 -0
- package/lib/client/CustomQuery.js +42 -0
- package/lib/client/Query.d.ts +97 -0
- package/lib/client/Query.js +1 -0
- package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +2 -2
- package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +1 -3
- package/lib/client/sync/bucket/SqliteBucketStorage.js +15 -17
- package/lib/client/sync/stream/AbstractRemote.d.ts +1 -10
- package/lib/client/sync/stream/AbstractRemote.js +31 -35
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +13 -8
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +112 -82
- package/lib/client/sync/stream/streaming-sync-types.d.ts +4 -0
- package/lib/client/watched/GetAllQuery.d.ts +32 -0
- package/lib/client/watched/GetAllQuery.js +24 -0
- package/lib/client/watched/WatchedQuery.d.ts +98 -0
- package/lib/client/watched/WatchedQuery.js +12 -0
- package/lib/client/watched/processors/AbstractQueryProcessor.d.ts +67 -0
- package/lib/client/watched/processors/AbstractQueryProcessor.js +135 -0
- package/lib/client/watched/processors/DifferentialQueryProcessor.d.ts +121 -0
- package/lib/client/watched/processors/DifferentialQueryProcessor.js +166 -0
- package/lib/client/watched/processors/OnChangeQueryProcessor.d.ts +33 -0
- package/lib/client/watched/processors/OnChangeQueryProcessor.js +76 -0
- package/lib/client/watched/processors/comparators.d.ts +30 -0
- package/lib/client/watched/processors/comparators.js +34 -0
- package/lib/db/schema/RawTable.d.ts +61 -0
- package/lib/db/schema/RawTable.js +32 -0
- package/lib/db/schema/Schema.d.ts +14 -0
- package/lib/db/schema/Schema.js +20 -1
- package/lib/index.d.ts +8 -0
- package/lib/index.js +8 -0
- package/lib/utils/BaseObserver.d.ts +3 -4
- package/lib/utils/BaseObserver.js +3 -0
- package/lib/utils/MetaBaseObserver.d.ts +29 -0
- package/lib/utils/MetaBaseObserver.js +50 -0
- package/lib/utils/async.d.ts +0 -1
- package/lib/utils/async.js +0 -10
- package/package.json +1 -1
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Mutex } from 'async-mutex';
|
|
2
1
|
import { ILogger } from 'js-logger';
|
|
3
2
|
import { DBAdapter, Transaction } from '../../../db/DBAdapter.js';
|
|
4
3
|
import { BaseObserver } from '../../../utils/BaseObserver.js';
|
|
@@ -8,13 +7,12 @@ import { CrudEntry } from './CrudEntry.js';
|
|
|
8
7
|
import { SyncDataBatch } from './SyncDataBatch.js';
|
|
9
8
|
export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListener> implements BucketStorageAdapter {
|
|
10
9
|
private db;
|
|
11
|
-
private mutex;
|
|
12
10
|
private logger;
|
|
13
11
|
tableNames: Set<string>;
|
|
14
12
|
private _hasCompletedSync;
|
|
15
13
|
private updateListener;
|
|
16
14
|
private _clientId?;
|
|
17
|
-
constructor(db: DBAdapter,
|
|
15
|
+
constructor(db: DBAdapter, logger?: ILogger);
|
|
18
16
|
init(): Promise<void>;
|
|
19
17
|
dispose(): Promise<void>;
|
|
20
18
|
_getClientId(): Promise<string>;
|
|
@@ -6,16 +6,14 @@ import { PSInternalTable } from './BucketStorageAdapter.js';
|
|
|
6
6
|
import { CrudEntry } from './CrudEntry.js';
|
|
7
7
|
export class SqliteBucketStorage extends BaseObserver {
|
|
8
8
|
db;
|
|
9
|
-
mutex;
|
|
10
9
|
logger;
|
|
11
10
|
tableNames;
|
|
12
11
|
_hasCompletedSync;
|
|
13
12
|
updateListener;
|
|
14
13
|
_clientId;
|
|
15
|
-
constructor(db,
|
|
14
|
+
constructor(db, logger = Logger.get('SqliteBucketStorage')) {
|
|
16
15
|
super();
|
|
17
16
|
this.db = db;
|
|
18
|
-
this.mutex = mutex;
|
|
19
17
|
this.logger = logger;
|
|
20
18
|
this._hasCompletedSync = false;
|
|
21
19
|
this.tableNames = new Set();
|
|
@@ -66,11 +64,11 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
66
64
|
async saveSyncData(batch, fixedKeyFormat = false) {
|
|
67
65
|
await this.writeTransaction(async (tx) => {
|
|
68
66
|
for (const b of batch.buckets) {
|
|
69
|
-
|
|
67
|
+
await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
|
|
70
68
|
'save',
|
|
71
69
|
JSON.stringify({ buckets: [b.toJSON(fixedKeyFormat)] })
|
|
72
70
|
]);
|
|
73
|
-
this.logger.debug(
|
|
71
|
+
this.logger.debug(`Saved batch of data for bucket: ${b.bucket}, operations: ${b.data.length}`);
|
|
74
72
|
}
|
|
75
73
|
});
|
|
76
74
|
}
|
|
@@ -86,7 +84,7 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
86
84
|
await this.writeTransaction(async (tx) => {
|
|
87
85
|
await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', ['delete_bucket', bucket]);
|
|
88
86
|
});
|
|
89
|
-
this.logger.debug(
|
|
87
|
+
this.logger.debug(`Done deleting bucket ${bucket}`);
|
|
90
88
|
}
|
|
91
89
|
async hasCompletedSync() {
|
|
92
90
|
if (this._hasCompletedSync) {
|
|
@@ -108,6 +106,12 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
108
106
|
}
|
|
109
107
|
return { ready: false, checkpointValid: false, checkpointFailures: r.checkpointFailures };
|
|
110
108
|
}
|
|
109
|
+
if (priority == null) {
|
|
110
|
+
this.logger.debug(`Validated checksums checkpoint ${checkpoint.last_op_id}`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
this.logger.debug(`Validated checksums for partial checkpoint ${checkpoint.last_op_id}, priority ${priority}`);
|
|
114
|
+
}
|
|
111
115
|
let buckets = checkpoint.buckets;
|
|
112
116
|
if (priority !== undefined) {
|
|
113
117
|
buckets = buckets.filter((b) => hasMatchingPriority(priority, b));
|
|
@@ -124,7 +128,6 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
124
128
|
});
|
|
125
129
|
const valid = await this.updateObjectsFromBuckets(checkpoint, priority);
|
|
126
130
|
if (!valid) {
|
|
127
|
-
this.logger.debug('Not at a consistent checkpoint - cannot update local db');
|
|
128
131
|
return { ready: false, checkpointValid: true };
|
|
129
132
|
}
|
|
130
133
|
return {
|
|
@@ -177,7 +180,6 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
177
180
|
JSON.stringify({ ...checkpoint })
|
|
178
181
|
]);
|
|
179
182
|
const resultItem = rs.rows?.item(0);
|
|
180
|
-
this.logger.debug('validateChecksums priority, checkpoint, result item', priority, checkpoint, resultItem);
|
|
181
183
|
if (!resultItem) {
|
|
182
184
|
return {
|
|
183
185
|
checkpointValid: false,
|
|
@@ -210,30 +212,26 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
210
212
|
}
|
|
211
213
|
const seqBefore = rs[0]['seq'];
|
|
212
214
|
const opId = await cb();
|
|
213
|
-
this.logger.debug(`[updateLocalTarget] Updating target to checkpoint ${opId}`);
|
|
214
215
|
return this.writeTransaction(async (tx) => {
|
|
215
216
|
const anyData = await tx.execute('SELECT 1 FROM ps_crud LIMIT 1');
|
|
216
217
|
if (anyData.rows?.length) {
|
|
217
218
|
// if isNotEmpty
|
|
218
|
-
this.logger.debug(
|
|
219
|
+
this.logger.debug(`New data uploaded since write checkpoint ${opId} - need new write checkpoint`);
|
|
219
220
|
return false;
|
|
220
221
|
}
|
|
221
222
|
const rs = await tx.execute("SELECT seq FROM sqlite_sequence WHERE name = 'ps_crud'");
|
|
222
223
|
if (!rs.rows?.length) {
|
|
223
224
|
// assert isNotEmpty
|
|
224
|
-
throw new Error('
|
|
225
|
+
throw new Error('SQLite Sequence should not be empty');
|
|
225
226
|
}
|
|
226
227
|
const seqAfter = rs.rows?.item(0)['seq'];
|
|
227
|
-
this.logger.debug('seqAfter', JSON.stringify(rs.rows?.item(0)));
|
|
228
228
|
if (seqAfter != seqBefore) {
|
|
229
|
-
this.logger.debug(
|
|
229
|
+
this.logger.debug(`New data uploaded since write checpoint ${opId} - need new write checkpoint (sequence updated)`);
|
|
230
230
|
// New crud data may have been uploaded since we got the checkpoint. Abort.
|
|
231
231
|
return false;
|
|
232
232
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
]);
|
|
236
|
-
this.logger.debug(['[updateLocalTarget] Response from updating target_op ', JSON.stringify(response)]);
|
|
233
|
+
this.logger.debug(`Updating target write checkpoint to ${opId}`);
|
|
234
|
+
await tx.execute("UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name='$local'", [opId]);
|
|
237
235
|
return true;
|
|
238
236
|
});
|
|
239
237
|
}
|
|
@@ -3,7 +3,7 @@ import { type fetch } from 'cross-fetch';
|
|
|
3
3
|
import Logger, { ILogger } from 'js-logger';
|
|
4
4
|
import { DataStream } from '../../../utils/DataStream.js';
|
|
5
5
|
import { PowerSyncCredentials } from '../../connection/PowerSyncCredentials.js';
|
|
6
|
-
import {
|
|
6
|
+
import { StreamingSyncRequest } from './streaming-sync-types.js';
|
|
7
7
|
export type BSONImplementation = typeof BSON;
|
|
8
8
|
export type RemoteConnector = {
|
|
9
9
|
fetchCredentials: () => Promise<PowerSyncCredentials | null>;
|
|
@@ -120,11 +120,6 @@ export declare abstract class AbstractRemote {
|
|
|
120
120
|
*/
|
|
121
121
|
abstract getBSON(): Promise<BSONImplementation>;
|
|
122
122
|
protected createSocket(url: string): WebSocket;
|
|
123
|
-
/**
|
|
124
|
-
* Connects to the sync/stream websocket endpoint and delivers sync lines by decoding the BSON events
|
|
125
|
-
* sent by the server.
|
|
126
|
-
*/
|
|
127
|
-
socketStream(options: SocketSyncStreamOptions): Promise<DataStream<StreamingSyncLine>>;
|
|
128
123
|
/**
|
|
129
124
|
* Returns a data stream of sync line data.
|
|
130
125
|
*
|
|
@@ -133,10 +128,6 @@ export declare abstract class AbstractRemote {
|
|
|
133
128
|
* (required for compatibility with older sync services).
|
|
134
129
|
*/
|
|
135
130
|
socketStreamRaw<T>(options: SocketSyncStreamOptions, map: (buffer: Uint8Array) => T, bson?: typeof BSON): Promise<DataStream<T>>;
|
|
136
|
-
/**
|
|
137
|
-
* Connects to the sync/stream http endpoint, parsing lines as JSON.
|
|
138
|
-
*/
|
|
139
|
-
postStream(options: SyncStreamOptions): Promise<DataStream<StreamingSyncLine>>;
|
|
140
131
|
/**
|
|
141
132
|
* Connects to the sync/stream http endpoint, mapping and emitting each received string line.
|
|
142
133
|
*/
|
|
@@ -178,14 +178,6 @@ export class AbstractRemote {
|
|
|
178
178
|
createSocket(url) {
|
|
179
179
|
return new WebSocket(url);
|
|
180
180
|
}
|
|
181
|
-
/**
|
|
182
|
-
* Connects to the sync/stream websocket endpoint and delivers sync lines by decoding the BSON events
|
|
183
|
-
* sent by the server.
|
|
184
|
-
*/
|
|
185
|
-
async socketStream(options) {
|
|
186
|
-
const bson = await this.getBSON();
|
|
187
|
-
return await this.socketStreamRaw(options, (data) => bson.deserialize(data), bson);
|
|
188
|
-
}
|
|
189
181
|
/**
|
|
190
182
|
* Returns a data stream of sync line data.
|
|
191
183
|
*
|
|
@@ -212,6 +204,22 @@ export class AbstractRemote {
|
|
|
212
204
|
// headers with websockets on web. The browser userAgent is however added
|
|
213
205
|
// automatically as a header.
|
|
214
206
|
const userAgent = this.getUserAgent();
|
|
207
|
+
const stream = new DataStream({
|
|
208
|
+
logger: this.logger,
|
|
209
|
+
pressure: {
|
|
210
|
+
lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
|
|
211
|
+
},
|
|
212
|
+
mapLine: map
|
|
213
|
+
});
|
|
214
|
+
// Handle upstream abort
|
|
215
|
+
if (options.abortSignal?.aborted) {
|
|
216
|
+
throw new AbortOperation('Connection request aborted');
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
options.abortSignal?.addEventListener('abort', () => {
|
|
220
|
+
stream.close();
|
|
221
|
+
}, { once: true });
|
|
222
|
+
}
|
|
215
223
|
let keepAliveTimeout;
|
|
216
224
|
const resetTimeout = () => {
|
|
217
225
|
clearTimeout(keepAliveTimeout);
|
|
@@ -221,12 +229,22 @@ export class AbstractRemote {
|
|
|
221
229
|
}, SOCKET_TIMEOUT_MS);
|
|
222
230
|
};
|
|
223
231
|
resetTimeout();
|
|
232
|
+
// Typescript complains about this being `never` if it's not assigned here.
|
|
233
|
+
// This is assigned in `wsCreator`.
|
|
234
|
+
let disposeSocketConnectionTimeout = () => { };
|
|
224
235
|
const url = this.options.socketUrlTransformer(request.url);
|
|
225
236
|
const connector = new RSocketConnector({
|
|
226
237
|
transport: new WebsocketClientTransport({
|
|
227
238
|
url,
|
|
228
239
|
wsCreator: (url) => {
|
|
229
240
|
const socket = this.createSocket(url);
|
|
241
|
+
disposeSocketConnectionTimeout = stream.registerListener({
|
|
242
|
+
closed: () => {
|
|
243
|
+
// Allow closing the underlying WebSocket if the stream was closed before the
|
|
244
|
+
// RSocket connect completed. This should effectively abort the request.
|
|
245
|
+
socket.close();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
230
248
|
socket.addEventListener('message', (event) => {
|
|
231
249
|
resetTimeout();
|
|
232
250
|
});
|
|
@@ -250,20 +268,18 @@ export class AbstractRemote {
|
|
|
250
268
|
let rsocket;
|
|
251
269
|
try {
|
|
252
270
|
rsocket = await connector.connect();
|
|
271
|
+
// The connection is established, we no longer need to monitor the initial timeout
|
|
272
|
+
disposeSocketConnectionTimeout();
|
|
253
273
|
}
|
|
254
274
|
catch (ex) {
|
|
255
275
|
this.logger.error(`Failed to connect WebSocket`, ex);
|
|
256
276
|
clearTimeout(keepAliveTimeout);
|
|
277
|
+
if (!stream.closed) {
|
|
278
|
+
await stream.close();
|
|
279
|
+
}
|
|
257
280
|
throw ex;
|
|
258
281
|
}
|
|
259
282
|
resetTimeout();
|
|
260
|
-
const stream = new DataStream({
|
|
261
|
-
logger: this.logger,
|
|
262
|
-
pressure: {
|
|
263
|
-
lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
|
|
264
|
-
},
|
|
265
|
-
mapLine: map
|
|
266
|
-
});
|
|
267
283
|
let socketIsClosed = false;
|
|
268
284
|
const closeSocket = () => {
|
|
269
285
|
clearTimeout(keepAliveTimeout);
|
|
@@ -349,28 +365,8 @@ export class AbstractRemote {
|
|
|
349
365
|
l();
|
|
350
366
|
}
|
|
351
367
|
});
|
|
352
|
-
/**
|
|
353
|
-
* Handle abort operations here.
|
|
354
|
-
* Unfortunately cannot insert them into the connection.
|
|
355
|
-
*/
|
|
356
|
-
if (options.abortSignal?.aborted) {
|
|
357
|
-
stream.close();
|
|
358
|
-
}
|
|
359
|
-
else {
|
|
360
|
-
options.abortSignal?.addEventListener('abort', () => {
|
|
361
|
-
stream.close();
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
368
|
return stream;
|
|
365
369
|
}
|
|
366
|
-
/**
|
|
367
|
-
* Connects to the sync/stream http endpoint, parsing lines as JSON.
|
|
368
|
-
*/
|
|
369
|
-
async postStream(options) {
|
|
370
|
-
return await this.postStreamRaw(options, (line) => {
|
|
371
|
-
return JSON.parse(line);
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
370
|
/**
|
|
375
371
|
* Connects to the sync/stream http endpoint, mapping and emitting each received string line.
|
|
376
372
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ILogger } from 'js-logger';
|
|
2
2
|
import { SyncStatus, SyncStatusOptions } from '../../../db/crud/SyncStatus.js';
|
|
3
|
-
import { BaseListener, BaseObserver, Disposable } from '../../../utils/BaseObserver.js';
|
|
3
|
+
import { BaseListener, BaseObserver, BaseObserverInterface, Disposable } from '../../../utils/BaseObserver.js';
|
|
4
4
|
import { BucketStorageAdapter } from '../bucket/BucketStorageAdapter.js';
|
|
5
5
|
import { AbstractRemote, FetchStrategy } from './AbstractRemote.js';
|
|
6
6
|
import { StreamingSyncRequestParameterType } from './streaming-sync-types.js';
|
|
@@ -88,7 +88,8 @@ export interface StreamingSyncImplementationListener extends BaseListener {
|
|
|
88
88
|
* Configurable options to be used when connecting to the PowerSync
|
|
89
89
|
* backend instance.
|
|
90
90
|
*/
|
|
91
|
-
export
|
|
91
|
+
export type PowerSyncConnectionOptions = Omit<InternalConnectionOptions, 'serializedSchema'>;
|
|
92
|
+
export interface InternalConnectionOptions extends BaseConnectionOptions, AdditionalConnectionOptions {
|
|
92
93
|
}
|
|
93
94
|
/** @internal */
|
|
94
95
|
export interface BaseConnectionOptions {
|
|
@@ -114,6 +115,10 @@ export interface BaseConnectionOptions {
|
|
|
114
115
|
* These parameters are passed to the sync rules, and will be available under the`user_parameters` object.
|
|
115
116
|
*/
|
|
116
117
|
params?: Record<string, StreamingSyncRequestParameterType>;
|
|
118
|
+
/**
|
|
119
|
+
* The serialized schema - mainly used to forward information about raw tables to the sync client.
|
|
120
|
+
*/
|
|
121
|
+
serializedSchema?: any;
|
|
117
122
|
}
|
|
118
123
|
/** @internal */
|
|
119
124
|
export interface AdditionalConnectionOptions {
|
|
@@ -131,11 +136,11 @@ export interface AdditionalConnectionOptions {
|
|
|
131
136
|
}
|
|
132
137
|
/** @internal */
|
|
133
138
|
export type RequiredAdditionalConnectionOptions = Required<AdditionalConnectionOptions>;
|
|
134
|
-
export interface StreamingSyncImplementation extends
|
|
139
|
+
export interface StreamingSyncImplementation extends BaseObserverInterface<StreamingSyncImplementationListener>, Disposable {
|
|
135
140
|
/**
|
|
136
141
|
* Connects to the sync service
|
|
137
142
|
*/
|
|
138
|
-
connect(options?:
|
|
143
|
+
connect(options?: InternalConnectionOptions): Promise<void>;
|
|
139
144
|
/**
|
|
140
145
|
* Disconnects from the sync services.
|
|
141
146
|
* @throws if not connected or if abort is not controlled internally
|
|
@@ -155,7 +160,6 @@ export declare const DEFAULT_CRUD_UPLOAD_THROTTLE_MS = 1000;
|
|
|
155
160
|
export declare const DEFAULT_RETRY_DELAY_MS = 5000;
|
|
156
161
|
export declare const DEFAULT_STREAMING_SYNC_OPTIONS: {
|
|
157
162
|
retryDelayMs: number;
|
|
158
|
-
logger: Logger.ILogger;
|
|
159
163
|
crudUploadThrottleMs: number;
|
|
160
164
|
};
|
|
161
165
|
export type RequiredPowerSyncConnectionOptions = Required<BaseConnectionOptions>;
|
|
@@ -164,9 +168,11 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
|
|
|
164
168
|
protected _lastSyncedAt: Date | null;
|
|
165
169
|
protected options: AbstractStreamingSyncImplementationOptions;
|
|
166
170
|
protected abortController: AbortController | null;
|
|
171
|
+
protected uploadAbortController: AbortController | null;
|
|
167
172
|
protected crudUpdateListener?: () => void;
|
|
168
173
|
protected streamingSyncPromise?: Promise<void>;
|
|
169
|
-
|
|
174
|
+
protected logger: ILogger;
|
|
175
|
+
private isUploadingCrud;
|
|
170
176
|
private notifyCompletedUploads?;
|
|
171
177
|
syncStatus: SyncStatus;
|
|
172
178
|
triggerCrudUpload: () => void;
|
|
@@ -176,7 +182,6 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
|
|
|
176
182
|
waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void>;
|
|
177
183
|
get lastSyncedAt(): Date | undefined;
|
|
178
184
|
get isConnected(): boolean;
|
|
179
|
-
protected get logger(): Logger.ILogger;
|
|
180
185
|
dispose(): Promise<void>;
|
|
181
186
|
abstract obtainLock<T>(lockOptions: LockOptions<T>): Promise<T>;
|
|
182
187
|
hasCompletedSync(): Promise<boolean>;
|