@powersync/common 0.0.0-dev-20250418125956 → 0.0.0-dev-20250520135616
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.mjs +8 -3
- package/lib/client/AbstractPowerSyncDatabase.d.ts +1 -0
- package/lib/client/AbstractPowerSyncDatabase.js +7 -9
- package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +6 -0
- package/lib/client/sync/bucket/CrudEntry.d.ts +13 -1
- package/lib/client/sync/bucket/CrudEntry.js +16 -2
- package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +2 -1
- package/lib/client/sync/bucket/SqliteBucketStorage.js +18 -3
- package/lib/client/sync/stream/AbstractRemote.d.ts +29 -0
- package/lib/client/sync/stream/AbstractRemote.js +63 -10
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +1 -0
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +47 -2
- package/lib/client/sync/stream/streaming-sync-types.d.ts +1 -1
- package/lib/db/crud/SyncProgress.d.ts +72 -0
- package/lib/db/crud/SyncProgress.js +60 -0
- package/lib/db/crud/SyncStatus.d.ts +20 -0
- package/lib/db/crud/SyncStatus.js +14 -0
- package/lib/db/schema/Schema.d.ts +4 -0
- package/lib/db/schema/Schema.js +1 -10
- package/lib/db/schema/Table.d.ts +34 -8
- package/lib/db/schema/Table.js +48 -9
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/utils/Logger.d.ts +31 -0
- package/lib/utils/Logger.js +36 -0
- package/package.json +1 -1
|
@@ -183,6 +183,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
183
183
|
* Cannot be used while connected - this should only be called before {@link AbstractPowerSyncDatabase.connect}.
|
|
184
184
|
*/
|
|
185
185
|
updateSchema(schema: Schema): Promise<void>;
|
|
186
|
+
get logger(): Logger.ILogger;
|
|
186
187
|
/**
|
|
187
188
|
* Wait for initialization to complete.
|
|
188
189
|
* While initializing is automatic, this helps to catch and report initialization errors.
|
|
@@ -15,6 +15,7 @@ import { CrudBatch } from './sync/bucket/CrudBatch.js';
|
|
|
15
15
|
import { CrudEntry } from './sync/bucket/CrudEntry.js';
|
|
16
16
|
import { CrudTransaction } from './sync/bucket/CrudTransaction.js';
|
|
17
17
|
import { DEFAULT_CRUD_UPLOAD_THROTTLE_MS, DEFAULT_RETRY_DELAY_MS } from './sync/stream/AbstractStreamingSyncImplementation.js';
|
|
18
|
+
import { FULL_SYNC_PRIORITY } from '../db/crud/SyncProgress.js';
|
|
18
19
|
const POWERSYNC_TABLE_MATCH = /(^ps_data__|^ps_data_local__)/;
|
|
19
20
|
const DEFAULT_DISCONNECT_CLEAR_OPTIONS = {
|
|
20
21
|
clearLocal: true
|
|
@@ -42,10 +43,6 @@ export const DEFAULT_LOCK_TIMEOUT_MS = 120_000; // 2 mins
|
|
|
42
43
|
export const isPowerSyncDatabaseOptionsWithSettings = (test) => {
|
|
43
44
|
return typeof test == 'object' && isSQLOpenOptions(test.database);
|
|
44
45
|
};
|
|
45
|
-
/**
|
|
46
|
-
* The priority used by the core extension to indicate that a full sync was completed.
|
|
47
|
-
*/
|
|
48
|
-
const FULL_SYNC_PRIORITY = 2147483647;
|
|
49
46
|
export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
50
47
|
options;
|
|
51
48
|
/**
|
|
@@ -250,6 +247,9 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
250
247
|
await this.database.refreshSchema();
|
|
251
248
|
this.iterateListeners(async (cb) => cb.schemaChanged?.(schema));
|
|
252
249
|
}
|
|
250
|
+
get logger() {
|
|
251
|
+
return this.options.logger;
|
|
252
|
+
}
|
|
253
253
|
/**
|
|
254
254
|
* Wait for initialization to complete.
|
|
255
255
|
* While initializing is automatic, this helps to catch and report initialization errors.
|
|
@@ -260,6 +260,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
260
260
|
// Use the options passed in during connect, or fallback to the options set during database creation or fallback to the default options
|
|
261
261
|
resolvedConnectionOptions(options) {
|
|
262
262
|
return {
|
|
263
|
+
...options,
|
|
263
264
|
retryDelayMs: options?.retryDelayMs ?? this.options.retryDelayMs ?? this.options.retryDelay ?? DEFAULT_RETRY_DELAY_MS,
|
|
264
265
|
crudUploadThrottleMs: options?.crudUploadThrottleMs ?? this.options.crudUploadThrottleMs ?? DEFAULT_CRUD_UPLOAD_THROTTLE_MS
|
|
265
266
|
};
|
|
@@ -274,11 +275,8 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
274
275
|
if (this.closed) {
|
|
275
276
|
throw new Error('Cannot connect using a closed client');
|
|
276
277
|
}
|
|
277
|
-
const
|
|
278
|
-
this.syncStreamImplementation = this.generateSyncStreamImplementation(connector,
|
|
279
|
-
retryDelayMs,
|
|
280
|
-
crudUploadThrottleMs
|
|
281
|
-
});
|
|
278
|
+
const resolvedConnectOptions = this.resolvedConnectionOptions(options);
|
|
279
|
+
this.syncStreamImplementation = this.generateSyncStreamImplementation(connector, resolvedConnectOptions);
|
|
282
280
|
this.syncStatusListenerDisposer = this.syncStreamImplementation.registerListener({
|
|
283
281
|
statusChanged: (status) => {
|
|
284
282
|
this.currentStatus = new SyncStatus({
|
|
@@ -27,6 +27,11 @@ export interface SyncLocalDatabaseResult {
|
|
|
27
27
|
checkpointValid: boolean;
|
|
28
28
|
checkpointFailures?: string[];
|
|
29
29
|
}
|
|
30
|
+
export type SavedProgress = {
|
|
31
|
+
atLast: number;
|
|
32
|
+
sinceLast: number;
|
|
33
|
+
};
|
|
34
|
+
export type BucketOperationProgress = Record<string, SavedProgress>;
|
|
30
35
|
export interface BucketChecksum {
|
|
31
36
|
bucket: string;
|
|
32
37
|
priority?: number;
|
|
@@ -56,6 +61,7 @@ export interface BucketStorageAdapter extends BaseObserver<BucketStorageListener
|
|
|
56
61
|
setTargetCheckpoint(checkpoint: Checkpoint): Promise<void>;
|
|
57
62
|
startSession(): void;
|
|
58
63
|
getBucketStates(): Promise<BucketState[]>;
|
|
64
|
+
getBucketOperationProgress(): Promise<BucketOperationProgress>;
|
|
59
65
|
syncLocalDatabase(checkpoint: Checkpoint, priority?: number): Promise<{
|
|
60
66
|
checkpointValid: boolean;
|
|
61
67
|
ready: boolean;
|
|
@@ -51,6 +51,11 @@ export declare class CrudEntry {
|
|
|
51
51
|
* Data associated with the change.
|
|
52
52
|
*/
|
|
53
53
|
opData?: Record<string, any>;
|
|
54
|
+
/**
|
|
55
|
+
* For tables where the `trackPreviousValues` option has been enabled, this tracks previous values for
|
|
56
|
+
* `UPDATE` and `DELETE` statements.
|
|
57
|
+
*/
|
|
58
|
+
previousValues?: Record<string, any>;
|
|
54
59
|
/**
|
|
55
60
|
* Table that contained the change.
|
|
56
61
|
*/
|
|
@@ -59,8 +64,15 @@ export declare class CrudEntry {
|
|
|
59
64
|
* Auto-incrementing transaction id. This is the same for all operations within the same transaction.
|
|
60
65
|
*/
|
|
61
66
|
transactionId?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Client-side metadata attached with this write.
|
|
69
|
+
*
|
|
70
|
+
* This field is only available when the `trackMetadata` option was set to `true` when creating a table
|
|
71
|
+
* and the insert or update statement set the `_metadata` column.
|
|
72
|
+
*/
|
|
73
|
+
metadata?: string;
|
|
62
74
|
static fromRow(dbRow: CrudEntryJSON): CrudEntry;
|
|
63
|
-
constructor(clientId: number, op: UpdateType, table: string, id: string, transactionId?: number, opData?: Record<string, any
|
|
75
|
+
constructor(clientId: number, op: UpdateType, table: string, id: string, transactionId?: number, opData?: Record<string, any>, previousValues?: Record<string, any>, metadata?: string);
|
|
64
76
|
/**
|
|
65
77
|
* Converts the change to JSON format.
|
|
66
78
|
*/
|
|
@@ -30,6 +30,11 @@ export class CrudEntry {
|
|
|
30
30
|
* Data associated with the change.
|
|
31
31
|
*/
|
|
32
32
|
opData;
|
|
33
|
+
/**
|
|
34
|
+
* For tables where the `trackPreviousValues` option has been enabled, this tracks previous values for
|
|
35
|
+
* `UPDATE` and `DELETE` statements.
|
|
36
|
+
*/
|
|
37
|
+
previousValues;
|
|
33
38
|
/**
|
|
34
39
|
* Table that contained the change.
|
|
35
40
|
*/
|
|
@@ -38,17 +43,26 @@ export class CrudEntry {
|
|
|
38
43
|
* Auto-incrementing transaction id. This is the same for all operations within the same transaction.
|
|
39
44
|
*/
|
|
40
45
|
transactionId;
|
|
46
|
+
/**
|
|
47
|
+
* Client-side metadata attached with this write.
|
|
48
|
+
*
|
|
49
|
+
* This field is only available when the `trackMetadata` option was set to `true` when creating a table
|
|
50
|
+
* and the insert or update statement set the `_metadata` column.
|
|
51
|
+
*/
|
|
52
|
+
metadata;
|
|
41
53
|
static fromRow(dbRow) {
|
|
42
54
|
const data = JSON.parse(dbRow.data);
|
|
43
|
-
return new CrudEntry(parseInt(dbRow.id), data.op, data.type, data.id, dbRow.tx_id, data.data);
|
|
55
|
+
return new CrudEntry(parseInt(dbRow.id), data.op, data.type, data.id, dbRow.tx_id, data.data, data.old, data.metadata);
|
|
44
56
|
}
|
|
45
|
-
constructor(clientId, op, table, id, transactionId, opData) {
|
|
57
|
+
constructor(clientId, op, table, id, transactionId, opData, previousValues, metadata) {
|
|
46
58
|
this.clientId = clientId;
|
|
47
59
|
this.id = id;
|
|
48
60
|
this.op = op;
|
|
49
61
|
this.opData = opData;
|
|
50
62
|
this.table = table;
|
|
51
63
|
this.transactionId = transactionId;
|
|
64
|
+
this.previousValues = previousValues;
|
|
65
|
+
this.metadata = metadata;
|
|
52
66
|
}
|
|
53
67
|
/**
|
|
54
68
|
* Converts the change to JSON format.
|
|
@@ -2,7 +2,7 @@ import { Mutex } from 'async-mutex';
|
|
|
2
2
|
import { ILogger } from 'js-logger';
|
|
3
3
|
import { DBAdapter, Transaction } from '../../../db/DBAdapter.js';
|
|
4
4
|
import { BaseObserver } from '../../../utils/BaseObserver.js';
|
|
5
|
-
import { BucketState, BucketStorageAdapter, BucketStorageListener, Checkpoint, SyncLocalDatabaseResult } from './BucketStorageAdapter.js';
|
|
5
|
+
import { BucketOperationProgress, BucketState, BucketStorageAdapter, BucketStorageListener, Checkpoint, SyncLocalDatabaseResult } from './BucketStorageAdapter.js';
|
|
6
6
|
import { CrudBatch } from './CrudBatch.js';
|
|
7
7
|
import { CrudEntry } from './CrudEntry.js';
|
|
8
8
|
import { SyncDataBatch } from './SyncDataBatch.js';
|
|
@@ -30,6 +30,7 @@ export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListe
|
|
|
30
30
|
*/
|
|
31
31
|
startSession(): void;
|
|
32
32
|
getBucketStates(): Promise<BucketState[]>;
|
|
33
|
+
getBucketOperationProgress(): Promise<BucketOperationProgress>;
|
|
33
34
|
saveSyncData(batch: SyncDataBatch): Promise<void>;
|
|
34
35
|
removeBuckets(buckets: string[]): Promise<void>;
|
|
35
36
|
/**
|
|
@@ -66,6 +66,10 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
66
66
|
const result = await this.db.getAll("SELECT name as bucket, cast(last_op as TEXT) as op_id FROM ps_buckets WHERE pending_delete = 0 AND name != '$local'");
|
|
67
67
|
return result;
|
|
68
68
|
}
|
|
69
|
+
async getBucketOperationProgress() {
|
|
70
|
+
const rows = await this.db.getAll('SELECT name, count_at_last, count_since_last FROM ps_buckets');
|
|
71
|
+
return Object.fromEntries(rows.map((r) => [r.name, { atLast: r.count_at_last, sinceLast: r.count_since_last }]));
|
|
72
|
+
}
|
|
69
73
|
async saveSyncData(batch) {
|
|
70
74
|
await this.writeTransaction(async (tx) => {
|
|
71
75
|
let count = 0;
|
|
@@ -115,9 +119,9 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
115
119
|
}
|
|
116
120
|
return { ready: false, checkpointValid: false, checkpointFailures: r.checkpointFailures };
|
|
117
121
|
}
|
|
118
|
-
|
|
122
|
+
let buckets = checkpoint.buckets;
|
|
119
123
|
if (priority !== undefined) {
|
|
120
|
-
buckets.filter((b) => hasMatchingPriority(priority, b));
|
|
124
|
+
buckets = buckets.filter((b) => hasMatchingPriority(priority, b));
|
|
121
125
|
}
|
|
122
126
|
const bucketNames = buckets.map((b) => b.bucket);
|
|
123
127
|
await this.writeTransaction(async (tx) => {
|
|
@@ -161,7 +165,18 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
161
165
|
'sync_local',
|
|
162
166
|
arg
|
|
163
167
|
]);
|
|
164
|
-
|
|
168
|
+
if (result == 1) {
|
|
169
|
+
if (priority == null) {
|
|
170
|
+
const bucketToCount = Object.fromEntries(checkpoint.buckets.map((b) => [b.bucket, b.count]));
|
|
171
|
+
// The two parameters could be replaced with one, but: https://github.com/powersync-ja/better-sqlite3/pull/6
|
|
172
|
+
const jsonBucketCount = JSON.stringify(bucketToCount);
|
|
173
|
+
await tx.execute("UPDATE ps_buckets SET count_since_last = 0, count_at_last = ?->name WHERE name != '$local' AND ?->name IS NOT NULL", [jsonBucketCount, jsonBucketCount]);
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
165
180
|
});
|
|
166
181
|
}
|
|
167
182
|
async validateChecksums(checkpoint, priority) {
|
|
@@ -7,6 +7,7 @@ import { StreamingSyncLine, StreamingSyncRequest } from './streaming-sync-types.
|
|
|
7
7
|
export type BSONImplementation = typeof BSON;
|
|
8
8
|
export type RemoteConnector = {
|
|
9
9
|
fetchCredentials: () => Promise<PowerSyncCredentials | null>;
|
|
10
|
+
invalidateCredentials?: () => void;
|
|
10
11
|
};
|
|
11
12
|
export declare const DEFAULT_REMOTE_LOGGER: Logger.ILogger;
|
|
12
13
|
export type SyncStreamOptions = {
|
|
@@ -74,7 +75,35 @@ export declare abstract class AbstractRemote {
|
|
|
74
75
|
* which can be called to perform fetch requests
|
|
75
76
|
*/
|
|
76
77
|
get fetch(): FetchImplementation;
|
|
78
|
+
/**
|
|
79
|
+
* Get credentials currently cached, or fetch new credentials if none are
|
|
80
|
+
* available.
|
|
81
|
+
*
|
|
82
|
+
* These credentials may have expired already.
|
|
83
|
+
*/
|
|
77
84
|
getCredentials(): Promise<PowerSyncCredentials | null>;
|
|
85
|
+
/**
|
|
86
|
+
* Fetch a new set of credentials and cache it.
|
|
87
|
+
*
|
|
88
|
+
* Until this call succeeds, `getCredentials` will still return the
|
|
89
|
+
* old credentials.
|
|
90
|
+
*
|
|
91
|
+
* This may be called before the current credentials have expired.
|
|
92
|
+
*/
|
|
93
|
+
prefetchCredentials(): Promise<PowerSyncCredentials | null>;
|
|
94
|
+
/**
|
|
95
|
+
* Get credentials for PowerSync.
|
|
96
|
+
*
|
|
97
|
+
* This should always fetch a fresh set of credentials - don't use cached
|
|
98
|
+
* values.
|
|
99
|
+
*/
|
|
100
|
+
fetchCredentials(): Promise<PowerSyncCredentials | null>;
|
|
101
|
+
/***
|
|
102
|
+
* Immediately invalidate credentials.
|
|
103
|
+
*
|
|
104
|
+
* This may be called when the current credentials have expired.
|
|
105
|
+
*/
|
|
106
|
+
invalidateCredentials(): void;
|
|
78
107
|
getUserAgent(): string;
|
|
79
108
|
protected buildRequest(path: string): Promise<{
|
|
80
109
|
url: string;
|
|
@@ -8,8 +8,6 @@ import { AbortOperation } from '../../../utils/AbortOperation.js';
|
|
|
8
8
|
import { DataStream } from '../../../utils/DataStream.js';
|
|
9
9
|
const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
|
|
10
10
|
const POWERSYNC_JS_VERSION = PACKAGE.version;
|
|
11
|
-
// Refresh at least 30 sec before it expires
|
|
12
|
-
const REFRESH_CREDENTIALS_SAFETY_PERIOD_MS = 30_000;
|
|
13
11
|
const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
|
|
14
12
|
// Keep alive message is sent every period
|
|
15
13
|
const KEEP_ALIVE_MS = 20_000;
|
|
@@ -70,17 +68,52 @@ export class AbstractRemote {
|
|
|
70
68
|
? fetchImplementation.getFetch()
|
|
71
69
|
: fetchImplementation;
|
|
72
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Get credentials currently cached, or fetch new credentials if none are
|
|
73
|
+
* available.
|
|
74
|
+
*
|
|
75
|
+
* These credentials may have expired already.
|
|
76
|
+
*/
|
|
73
77
|
async getCredentials() {
|
|
74
|
-
|
|
75
|
-
if (expiresAt && expiresAt > new Date(new Date().valueOf() + REFRESH_CREDENTIALS_SAFETY_PERIOD_MS)) {
|
|
78
|
+
if (this.credentials) {
|
|
76
79
|
return this.credentials;
|
|
77
80
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
return this.prefetchCredentials();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Fetch a new set of credentials and cache it.
|
|
85
|
+
*
|
|
86
|
+
* Until this call succeeds, `getCredentials` will still return the
|
|
87
|
+
* old credentials.
|
|
88
|
+
*
|
|
89
|
+
* This may be called before the current credentials have expired.
|
|
90
|
+
*/
|
|
91
|
+
async prefetchCredentials() {
|
|
92
|
+
this.credentials = await this.fetchCredentials();
|
|
82
93
|
return this.credentials;
|
|
83
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Get credentials for PowerSync.
|
|
97
|
+
*
|
|
98
|
+
* This should always fetch a fresh set of credentials - don't use cached
|
|
99
|
+
* values.
|
|
100
|
+
*/
|
|
101
|
+
async fetchCredentials() {
|
|
102
|
+
const credentials = await this.connector.fetchCredentials();
|
|
103
|
+
if (credentials?.endpoint.match(POWERSYNC_TRAILING_SLASH_MATCH)) {
|
|
104
|
+
throw new Error(`A trailing forward slash "/" was found in the fetchCredentials endpoint: "${credentials.endpoint}". Remove the trailing forward slash "/" to fix this error.`);
|
|
105
|
+
}
|
|
106
|
+
return credentials;
|
|
107
|
+
}
|
|
108
|
+
/***
|
|
109
|
+
* Immediately invalidate credentials.
|
|
110
|
+
*
|
|
111
|
+
* This may be called when the current credentials have expired.
|
|
112
|
+
*/
|
|
113
|
+
invalidateCredentials() {
|
|
114
|
+
this.credentials = null;
|
|
115
|
+
this.connector.invalidateCredentials?.();
|
|
116
|
+
}
|
|
84
117
|
getUserAgent() {
|
|
85
118
|
return `powersync-js/${POWERSYNC_JS_VERSION}`;
|
|
86
119
|
}
|
|
@@ -114,6 +147,9 @@ export class AbstractRemote {
|
|
|
114
147
|
},
|
|
115
148
|
body: JSON.stringify(data)
|
|
116
149
|
});
|
|
150
|
+
if (res.status === 401) {
|
|
151
|
+
this.invalidateCredentials();
|
|
152
|
+
}
|
|
117
153
|
if (!res.ok) {
|
|
118
154
|
throw new Error(`Received ${res.status} - ${res.statusText} when posting to ${path}: ${await res.text()}}`);
|
|
119
155
|
}
|
|
@@ -128,6 +164,9 @@ export class AbstractRemote {
|
|
|
128
164
|
...request.headers
|
|
129
165
|
}
|
|
130
166
|
});
|
|
167
|
+
if (res.status === 401) {
|
|
168
|
+
this.invalidateCredentials();
|
|
169
|
+
}
|
|
131
170
|
if (!res.ok) {
|
|
132
171
|
throw new Error(`Received ${res.status} - ${res.statusText} when getting from ${path}: ${await res.text()}}`);
|
|
133
172
|
}
|
|
@@ -145,6 +184,9 @@ export class AbstractRemote {
|
|
|
145
184
|
this.logger.error(`Caught ex when POST streaming to ${path}`, ex);
|
|
146
185
|
throw ex;
|
|
147
186
|
});
|
|
187
|
+
if (res.status === 401) {
|
|
188
|
+
this.invalidateCredentials();
|
|
189
|
+
}
|
|
148
190
|
if (!res.ok) {
|
|
149
191
|
const text = await res.text();
|
|
150
192
|
this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
|
|
@@ -169,10 +211,18 @@ export class AbstractRemote {
|
|
|
169
211
|
// headers with websockets on web. The browser userAgent is however added
|
|
170
212
|
// automatically as a header.
|
|
171
213
|
const userAgent = this.getUserAgent();
|
|
214
|
+
let socketCreationError;
|
|
172
215
|
const connector = new RSocketConnector({
|
|
173
216
|
transport: new WebsocketClientTransport({
|
|
174
217
|
url: this.options.socketUrlTransformer(request.url),
|
|
175
|
-
wsCreator: (url) =>
|
|
218
|
+
wsCreator: (url) => {
|
|
219
|
+
const s = this.createSocket(url);
|
|
220
|
+
s.addEventListener('error', (e) => {
|
|
221
|
+
socketCreationError = new Error('Failed to create connection to websocket: ', e.target.url ?? '');
|
|
222
|
+
this.logger.warn('Socket error', e);
|
|
223
|
+
});
|
|
224
|
+
return s;
|
|
225
|
+
}
|
|
176
226
|
}),
|
|
177
227
|
setup: {
|
|
178
228
|
keepAlive: KEEP_ALIVE_MS,
|
|
@@ -197,7 +247,7 @@ export class AbstractRemote {
|
|
|
197
247
|
* On React native the connection exception can be `undefined` this causes issues
|
|
198
248
|
* with detecting the exception inside async-mutex
|
|
199
249
|
*/
|
|
200
|
-
throw new Error(`Could not connect to PowerSync instance: ${JSON.stringify(ex)}`);
|
|
250
|
+
throw new Error(`Could not connect to PowerSync instance: ${JSON.stringify(ex ?? socketCreationError)}`);
|
|
201
251
|
}
|
|
202
252
|
const stream = new DataStream({
|
|
203
253
|
logger: this.logger,
|
|
@@ -233,6 +283,9 @@ export class AbstractRemote {
|
|
|
233
283
|
}, syncQueueRequestSize, // The initial N amount
|
|
234
284
|
{
|
|
235
285
|
onError: (e) => {
|
|
286
|
+
if (e.message.includes('Authorization failed') || e.message.includes('PSYNC_S21')) {
|
|
287
|
+
this.invalidateCredentials();
|
|
288
|
+
}
|
|
236
289
|
// Don't log closed as an error
|
|
237
290
|
if (e.message !== 'Closed. ') {
|
|
238
291
|
this.logger.error(e);
|
|
@@ -139,6 +139,7 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
|
|
|
139
139
|
streamingSync(signal?: AbortSignal, options?: PowerSyncConnectionOptions): Promise<void>;
|
|
140
140
|
private collectLocalBucketState;
|
|
141
141
|
protected streamingSyncIteration(signal: AbortSignal, options?: PowerSyncConnectionOptions): Promise<void>;
|
|
142
|
+
private updateSyncStatusForStartingCheckpoint;
|
|
142
143
|
private applyCheckpoint;
|
|
143
144
|
protected updateSyncStatus(options: SyncStatusOptions): void;
|
|
144
145
|
private delayRetry;
|
|
@@ -270,7 +270,8 @@ The next upload iteration will be delayed.`);
|
|
|
270
270
|
connected: false,
|
|
271
271
|
connecting: false,
|
|
272
272
|
dataFlow: {
|
|
273
|
-
downloading: false
|
|
273
|
+
downloading: false,
|
|
274
|
+
downloadProgress: null
|
|
274
275
|
}
|
|
275
276
|
});
|
|
276
277
|
});
|
|
@@ -409,6 +410,7 @@ The next upload iteration will be delayed.`);
|
|
|
409
410
|
bucketMap = newBuckets;
|
|
410
411
|
await this.options.adapter.removeBuckets([...bucketsToDelete]);
|
|
411
412
|
await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
|
|
413
|
+
await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
|
|
412
414
|
}
|
|
413
415
|
else if (isStreamingSyncCheckpointComplete(line)) {
|
|
414
416
|
const result = await this.applyCheckpoint(targetCheckpoint, signal);
|
|
@@ -472,6 +474,7 @@ The next upload iteration will be delayed.`);
|
|
|
472
474
|
write_checkpoint: diff.write_checkpoint
|
|
473
475
|
};
|
|
474
476
|
targetCheckpoint = newCheckpoint;
|
|
477
|
+
await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
|
|
475
478
|
bucketMap = new Map();
|
|
476
479
|
newBuckets.forEach((checksum, name) => bucketMap.set(name, {
|
|
477
480
|
name: checksum.bucket,
|
|
@@ -486,9 +489,22 @@ The next upload iteration will be delayed.`);
|
|
|
486
489
|
}
|
|
487
490
|
else if (isStreamingSyncData(line)) {
|
|
488
491
|
const { data } = line;
|
|
492
|
+
const previousProgress = this.syncStatus.dataFlowStatus.downloadProgress;
|
|
493
|
+
let updatedProgress = null;
|
|
494
|
+
if (previousProgress) {
|
|
495
|
+
updatedProgress = { ...previousProgress };
|
|
496
|
+
const progressForBucket = updatedProgress[data.bucket];
|
|
497
|
+
if (progressForBucket) {
|
|
498
|
+
updatedProgress[data.bucket] = {
|
|
499
|
+
...progressForBucket,
|
|
500
|
+
sinceLast: progressForBucket.sinceLast + data.data.length
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
}
|
|
489
504
|
this.updateSyncStatus({
|
|
490
505
|
dataFlow: {
|
|
491
|
-
downloading: true
|
|
506
|
+
downloading: true,
|
|
507
|
+
downloadProgress: updatedProgress
|
|
492
508
|
}
|
|
493
509
|
});
|
|
494
510
|
await this.options.adapter.saveSyncData({ buckets: [SyncDataBucket.fromRow(data)] });
|
|
@@ -498,6 +514,7 @@ The next upload iteration will be delayed.`);
|
|
|
498
514
|
if (remaining_seconds == 0) {
|
|
499
515
|
// Connection would be closed automatically right after this
|
|
500
516
|
this.logger.debug('Token expiring; reconnect');
|
|
517
|
+
this.options.remote.invalidateCredentials();
|
|
501
518
|
/**
|
|
502
519
|
* For a rare case where the backend connector does not update the token
|
|
503
520
|
* (uses the same one), this should have some delay.
|
|
@@ -505,6 +522,12 @@ The next upload iteration will be delayed.`);
|
|
|
505
522
|
await this.delayRetry();
|
|
506
523
|
return;
|
|
507
524
|
}
|
|
525
|
+
else if (remaining_seconds < 30) {
|
|
526
|
+
this.logger.debug('Token will expire soon; reconnect');
|
|
527
|
+
// Pre-emptively refresh the token
|
|
528
|
+
this.options.remote.invalidateCredentials();
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
508
531
|
this.triggerCrudUpload();
|
|
509
532
|
}
|
|
510
533
|
else {
|
|
@@ -536,6 +559,27 @@ The next upload iteration will be delayed.`);
|
|
|
536
559
|
}
|
|
537
560
|
});
|
|
538
561
|
}
|
|
562
|
+
async updateSyncStatusForStartingCheckpoint(checkpoint) {
|
|
563
|
+
const localProgress = await this.options.adapter.getBucketOperationProgress();
|
|
564
|
+
const progress = {};
|
|
565
|
+
for (const bucket of checkpoint.buckets) {
|
|
566
|
+
const savedProgress = localProgress[bucket.bucket];
|
|
567
|
+
progress[bucket.bucket] = {
|
|
568
|
+
// The fallback priority doesn't matter here, but 3 is the one newer versions of the sync service
|
|
569
|
+
// will use by default.
|
|
570
|
+
priority: bucket.priority ?? 3,
|
|
571
|
+
atLast: savedProgress?.atLast ?? 0,
|
|
572
|
+
sinceLast: savedProgress?.sinceLast ?? 0,
|
|
573
|
+
targetCount: bucket.count ?? 0
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
this.updateSyncStatus({
|
|
577
|
+
dataFlow: {
|
|
578
|
+
downloading: true,
|
|
579
|
+
downloadProgress: progress
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
}
|
|
539
583
|
async applyCheckpoint(checkpoint, abort) {
|
|
540
584
|
let result = await this.options.adapter.syncLocalDatabase(checkpoint);
|
|
541
585
|
const pending = this.pendingCrudUpload;
|
|
@@ -565,6 +609,7 @@ The next upload iteration will be delayed.`);
|
|
|
565
609
|
lastSyncedAt: new Date(),
|
|
566
610
|
dataFlow: {
|
|
567
611
|
downloading: false,
|
|
612
|
+
downloadProgress: null,
|
|
568
613
|
downloadError: undefined
|
|
569
614
|
}
|
|
570
615
|
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
export type InternalProgressInformation = Record<string, {
|
|
3
|
+
priority: number;
|
|
4
|
+
atLast: number;
|
|
5
|
+
sinceLast: number;
|
|
6
|
+
targetCount: number;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* @internal The priority used by the core extension to indicate that a full sync was completed.
|
|
10
|
+
*/
|
|
11
|
+
export declare const FULL_SYNC_PRIORITY = 2147483647;
|
|
12
|
+
/**
|
|
13
|
+
* Information about a progressing download made by the PowerSync SDK.
|
|
14
|
+
*
|
|
15
|
+
* To obtain these values, use {@link SyncProgress}, available through
|
|
16
|
+
* {@link SyncStatus#downloadProgress}.
|
|
17
|
+
*/
|
|
18
|
+
export interface ProgressWithOperations {
|
|
19
|
+
/**
|
|
20
|
+
* The total amount of operations to download for the current sync iteration
|
|
21
|
+
* to complete.
|
|
22
|
+
*/
|
|
23
|
+
totalOperations: number;
|
|
24
|
+
/**
|
|
25
|
+
* The amount of operations that have already been downloaded.
|
|
26
|
+
*/
|
|
27
|
+
downloadedOperations: number;
|
|
28
|
+
/**
|
|
29
|
+
* Relative progress, as {@link downloadedOperations} of {@link totalOperations}.
|
|
30
|
+
*
|
|
31
|
+
* This will be a number between `0.0` and `1.0` (inclusive).
|
|
32
|
+
*
|
|
33
|
+
* When this number reaches `1.0`, all changes have been received from the sync service.
|
|
34
|
+
* Actually applying these changes happens before the `downloadProgress` field is cleared from
|
|
35
|
+
* {@link SyncStatus}, so progress can stay at `1.0` for a short while before completing.
|
|
36
|
+
*/
|
|
37
|
+
downloadedFraction: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Provides realtime progress on how PowerSync is downloading rows.
|
|
41
|
+
*
|
|
42
|
+
* The progress until the next complete sync is available through the fields on {@link ProgressWithOperations},
|
|
43
|
+
* which this class implements.
|
|
44
|
+
* Additionally, the {@link SyncProgress.untilPriority} method can be used to otbain progress towards
|
|
45
|
+
* a specific priority (instead of the progress for the entire download).
|
|
46
|
+
*
|
|
47
|
+
* The reported progress always reflects the status towards the end of a sync iteration (after
|
|
48
|
+
* which a consistent snapshot of all buckets is available locally).
|
|
49
|
+
*
|
|
50
|
+
* In rare cases (in particular, when a [compacting](https://docs.powersync.com/usage/lifecycle-maintenance/compacting-buckets)
|
|
51
|
+
* operation takes place between syncs), it's possible for the returned numbers to be slightly
|
|
52
|
+
* inaccurate. For this reason, {@link SyncProgress} should be seen as an approximation of progress.
|
|
53
|
+
* The information returned is good enough to build progress bars, but not exact enough to track
|
|
54
|
+
* individual download counts.
|
|
55
|
+
*
|
|
56
|
+
* Also note that data is downloaded in bulk, which means that individual counters are unlikely
|
|
57
|
+
* to be updated one-by-one.
|
|
58
|
+
*/
|
|
59
|
+
export declare class SyncProgress implements ProgressWithOperations {
|
|
60
|
+
protected internal: InternalProgressInformation;
|
|
61
|
+
totalOperations: number;
|
|
62
|
+
downloadedOperations: number;
|
|
63
|
+
downloadedFraction: number;
|
|
64
|
+
constructor(internal: InternalProgressInformation);
|
|
65
|
+
/**
|
|
66
|
+
* Returns download progress towards all data up until the specified priority being received.
|
|
67
|
+
*
|
|
68
|
+
* The returned {@link ProgressWithOperations} tracks the target amount of operations that need
|
|
69
|
+
* to be downloaded in total and how many of them have already been received.
|
|
70
|
+
*/
|
|
71
|
+
untilPriority(priority: number): ProgressWithOperations;
|
|
72
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal The priority used by the core extension to indicate that a full sync was completed.
|
|
3
|
+
*/
|
|
4
|
+
export const FULL_SYNC_PRIORITY = 2147483647;
|
|
5
|
+
/**
|
|
6
|
+
* Provides realtime progress on how PowerSync is downloading rows.
|
|
7
|
+
*
|
|
8
|
+
* The progress until the next complete sync is available through the fields on {@link ProgressWithOperations},
|
|
9
|
+
* which this class implements.
|
|
10
|
+
* Additionally, the {@link SyncProgress.untilPriority} method can be used to otbain progress towards
|
|
11
|
+
* a specific priority (instead of the progress for the entire download).
|
|
12
|
+
*
|
|
13
|
+
* The reported progress always reflects the status towards the end of a sync iteration (after
|
|
14
|
+
* which a consistent snapshot of all buckets is available locally).
|
|
15
|
+
*
|
|
16
|
+
* In rare cases (in particular, when a [compacting](https://docs.powersync.com/usage/lifecycle-maintenance/compacting-buckets)
|
|
17
|
+
* operation takes place between syncs), it's possible for the returned numbers to be slightly
|
|
18
|
+
* inaccurate. For this reason, {@link SyncProgress} should be seen as an approximation of progress.
|
|
19
|
+
* The information returned is good enough to build progress bars, but not exact enough to track
|
|
20
|
+
* individual download counts.
|
|
21
|
+
*
|
|
22
|
+
* Also note that data is downloaded in bulk, which means that individual counters are unlikely
|
|
23
|
+
* to be updated one-by-one.
|
|
24
|
+
*/
|
|
25
|
+
export class SyncProgress {
|
|
26
|
+
internal;
|
|
27
|
+
totalOperations;
|
|
28
|
+
downloadedOperations;
|
|
29
|
+
downloadedFraction;
|
|
30
|
+
constructor(internal) {
|
|
31
|
+
this.internal = internal;
|
|
32
|
+
const untilCompletion = this.untilPriority(FULL_SYNC_PRIORITY);
|
|
33
|
+
this.totalOperations = untilCompletion.totalOperations;
|
|
34
|
+
this.downloadedOperations = untilCompletion.downloadedOperations;
|
|
35
|
+
this.downloadedFraction = untilCompletion.downloadedFraction;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Returns download progress towards all data up until the specified priority being received.
|
|
39
|
+
*
|
|
40
|
+
* The returned {@link ProgressWithOperations} tracks the target amount of operations that need
|
|
41
|
+
* to be downloaded in total and how many of them have already been received.
|
|
42
|
+
*/
|
|
43
|
+
untilPriority(priority) {
|
|
44
|
+
let total = 0;
|
|
45
|
+
let downloaded = 0;
|
|
46
|
+
for (const progress of Object.values(this.internal)) {
|
|
47
|
+
// Include higher-priority buckets, which are represented by lower numbers.
|
|
48
|
+
if (progress.priority <= priority) {
|
|
49
|
+
downloaded += progress.sinceLast;
|
|
50
|
+
total += progress.targetCount - progress.atLast;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
let progress = total == 0 ? 0.0 : downloaded / total;
|
|
54
|
+
return {
|
|
55
|
+
totalOperations: total,
|
|
56
|
+
downloadedOperations: downloaded,
|
|
57
|
+
downloadedFraction: progress
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|