Skip to content

Commit 8298b84

Browse files
Merge pull request #41 from powersync-ja/feature/better-crud-uploads
[Improvement] CRUD Upload Triggers
2 parents b0b0c4f + ff6a918 commit 8298b84

File tree

8 files changed

+3950
-125
lines changed

8 files changed

+3950
-125
lines changed

.changeset/many-hairs-scream.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@journeyapps/powersync-sdk-common': patch
3+
---
4+
5+
Improved connector CRUD uploads to be triggered whenever an internal CRUD operation change is triggered. Improved CRUD upload debouncing to rather use a throttled approach - executing multiple continous write/CRUD operations will now trigger a connector upload at most (every) 1 second (by default).

apps/supabase-todolist

packages/powersync-sdk-common/src/client/AbstractPowerSyncDatabase.ts

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import { UploadQueueStats } from '../db/crud/UploadQueueStatus';
88
import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector';
99
import {
1010
AbstractStreamingSyncImplementation,
11+
DEFAULT_CRUD_UPLOAD_THROTTLE_MS,
1112
StreamingSyncImplementationListener
1213
} from './sync/stream/AbstractStreamingSyncImplementation';
1314
import { CrudBatch } from './sync/bucket/CrudBatch';
1415
import { CrudTransaction } from './sync/bucket/CrudTransaction';
15-
import { BucketStorageAdapter } from './sync/bucket/BucketStorageAdapter';
16+
import { BucketStorageAdapter, PSInternalTable } from './sync/bucket/BucketStorageAdapter';
1617
import { CrudEntry } from './sync/bucket/CrudEntry';
1718
import { mutexRunExclusive } from '../utils/mutex';
1819
import { BaseObserver } from '../utils/BaseObserver';
@@ -21,14 +22,29 @@ import { EventIterator } from 'event-iterator';
2122
export interface PowerSyncDatabaseOptions {
2223
schema: Schema;
2324
database: DBAdapter;
25+
/**
26+
* Delay for retrying sync streaming operations
27+
* from the PowerSync backend after an error occurs.
28+
*/
2429
retryDelay?: number;
30+
/**
31+
* Backend Connector CRUD operations are throttled
32+
* to occur at most every `crudUploadThrottleMs`
33+
* milliseconds.
34+
*/
35+
crudUploadThrottleMs?: number;
2536
logger?: ILogger;
2637
}
2738

2839
export interface SQLWatchOptions {
2940
signal?: AbortSignal;
3041
tables?: string[];
3142
throttleMs?: number;
43+
/**
44+
* Allows for watching any SQL table
45+
* by not removing PowerSync table name prefixes
46+
*/
47+
rawTableNames?: boolean;
3248
}
3349

3450
export interface WatchOnChangeEvent {
@@ -45,7 +61,8 @@ export const DEFAULT_WATCH_THROTTLE_MS = 30;
4561

4662
export const DEFAULT_POWERSYNC_DB_OPTIONS = {
4763
retryDelay: 5000,
48-
logger: Logger.get('PowerSyncDatabase')
64+
logger: Logger.get('PowerSyncDatabase'),
65+
crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
4966
};
5067

5168
/**
@@ -135,6 +152,19 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
135152
this.iterateListeners((cb) => cb.initialized?.());
136153
}
137154

155+
/**
156+
* Queues a CRUD upload when internal CRUD tables have been updated
157+
*/
158+
protected async watchCrudUploads() {
159+
for await (const event of this.onChange({
160+
tables: [PSInternalTable.CRUD],
161+
rawTableNames: true,
162+
signal: this.abortController?.signal
163+
})) {
164+
this.syncStreamImplementation?.triggerCrudUpload();
165+
}
166+
}
167+
138168
/**
139169
* Wait for initialization to complete.
140170
* While initializing is automatic, this helps to catch and report initialization errors.
@@ -163,6 +193,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
163193
// Begin network stream
164194
this.syncStreamImplementation.triggerCrudUpload();
165195
this.syncStreamImplementation.streamingSync(this.abortController.signal);
196+
this.watchCrudUploads();
166197
}
167198

168199
async disconnect() {
@@ -182,9 +213,9 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
182213

183214
// TODO DB name, verify this is necessary with extension
184215
await this.database.writeTransaction(async (tx) => {
185-
await tx.execute('DELETE FROM ps_oplog WHERE 1');
186-
await tx.execute('DELETE FROM ps_crud WHERE 1');
187-
await tx.execute('DELETE FROM ps_buckets WHERE 1');
216+
await tx.execute(`DELETE FROM ${PSInternalTable.OPLOG} WHERE 1`);
217+
await tx.execute(`DELETE FROM ${PSInternalTable.CRUD} WHERE 1`);
218+
await tx.execute(`DELETE FROM ${PSInternalTable.BUCKETS} WHERE 1`);
188219

189220
const existingTableRows = await tx.execute(
190221
"SELECT name FROM sqlite_master WHERE type='table' AND name GLOB 'ps_data_*'"
@@ -220,12 +251,14 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
220251
async getUploadQueueStats(includeSize?: boolean): Promise<UploadQueueStats> {
221252
return this.readTransaction(async (tx) => {
222253
if (includeSize) {
223-
const result = await tx.execute('SELECT SUM(cast(data as blob) + 20) as size, count(*) as count FROM ps_crud');
254+
const result = await tx.execute(
255+
`SELECT SUM(cast(data as blob) + 20) as size, count(*) as count FROM ${PSInternalTable.CRUD}`
256+
);
224257

225258
const row = result.rows.item(0);
226259
return new UploadQueueStats(row?.count ?? 0, row?.size ?? 0);
227260
} else {
228-
const result = await tx.execute('SELECT count(*) as count FROM ps_crud');
261+
const result = await tx.execute(`SELECT count(*) as count FROM ${PSInternalTable.CRUD}`);
229262
const row = result.rows.item(0);
230263
return new UploadQueueStats(row?.count ?? 0);
231264
}
@@ -250,9 +283,10 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
250283
* and a single transaction may be split over multiple batches.
251284
*/
252285
async getCrudBatch(limit: number): Promise<CrudBatch | null> {
253-
const result = await this.database.execute('SELECT id, tx_id, data FROM ps_crud ORDER BY id ASC LIMIT ?', [
254-
limit + 1
255-
]);
286+
const result = await this.database.execute(
287+
`SELECT id, tx_id, data FROM ${PSInternalTable.CRUD} ORDER BY id ASC LIMIT ?`,
288+
[limit + 1]
289+
);
256290

257291
const all: CrudEntry[] = result.rows?._array?.map((row) => CrudEntry.fromRow(row)) ?? [];
258292

@@ -268,11 +302,13 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
268302
const last = all[all.length - 1];
269303
return new CrudBatch(all, haveMore, async (writeCheckpoint?: string) => {
270304
await this.writeTransaction(async (tx) => {
271-
await tx.execute('DELETE FROM ps_crud WHERE id <= ?', [last.clientId]);
272-
if (writeCheckpoint != null && (await tx.execute('SELECT 1 FROM ps_crud LIMIT 1')) == null) {
273-
await tx.execute("UPDATE ps_buckets SET target_op = ? WHERE name='$local'", [writeCheckpoint]);
305+
await tx.execute(`DELETE FROM ${PSInternalTable.CRUD} WHERE id <= ?`, [last.clientId]);
306+
if (writeCheckpoint != null && (await tx.execute(`SELECT 1 FROM ${PSInternalTable.CRUD} LIMIT 1`)) == null) {
307+
await tx.execute(`UPDATE ${PSInternalTable.BUCKETS} SET target_op = ? WHERE name='$local'`, [
308+
writeCheckpoint
309+
]);
274310
} else {
275-
await tx.execute("UPDATE ps_buckets SET target_op = ? WHERE name='$local'", [
311+
await tx.execute(`UPDATE ${PSInternalTable.BUCKETS} SET target_op = ? WHERE name='$local'`, [
276312
this.bucketStorageAdapter.getMaxOpId()
277313
]);
278314
}
@@ -295,7 +331,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
295331
*/
296332
async getNextCrudTransaction(): Promise<CrudTransaction> {
297333
return await this.readTransaction(async (tx) => {
298-
const first = await tx.execute('SELECT id, tx_id, data FROM ps_crud ORDER BY id ASC LIMIT 1');
334+
const first = await tx.execute(`SELECT id, tx_id, data FROM ${PSInternalTable.CRUD} ORDER BY id ASC LIMIT 1`);
299335

300336
if (!first.rows.length) {
301337
return null;
@@ -306,7 +342,10 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
306342
if (!txId) {
307343
all = [CrudEntry.fromRow(first.rows.item(0))];
308344
} else {
309-
const result = await tx.execute('SELECT id, tx_id, data FROM ps_crud WHERE tx_id = ? ORDER BY id ASC', [txId]);
345+
const result = await tx.execute(
346+
`SELECT id, tx_id, data FROM ${PSInternalTable.CRUD} WHERE tx_id = ? ORDER BY id ASC`,
347+
[txId]
348+
);
310349
all = result.rows._array.map((row) => CrudEntry.fromRow(row));
311350
}
312351

@@ -316,14 +355,16 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
316355
all,
317356
async (writeCheckpoint?: string) => {
318357
await this.writeTransaction(async (tx) => {
319-
await tx.execute('DELETE FROM ps_crud WHERE id <= ?', [last.clientId]);
358+
await tx.execute(`DELETE FROM ${PSInternalTable.CRUD} WHERE id <= ?`, [last.clientId]);
320359
if (writeCheckpoint) {
321-
const check = await tx.execute('SELECT 1 FROM ps_crud LIMIT 1');
360+
const check = await tx.execute(`SELECT 1 FROM ${PSInternalTable.CRUD} LIMIT 1`);
322361
if (!check.rows?.length) {
323-
await tx.execute("UPDATE ps_buckets SET target_op = ? WHERE name='$local'", [writeCheckpoint]);
362+
await tx.execute(`UPDATE ${PSInternalTable.BUCKETS} SET target_op = ? WHERE name='$local'`, [
363+
writeCheckpoint
364+
]);
324365
}
325366
} else {
326-
await tx.execute("UPDATE ps_buckets SET target_op = ? WHERE name='$local'", [
367+
await tx.execute(`UPDATE ${PSInternalTable.BUCKETS} SET target_op = ? WHERE name='$local'`, [
327368
this.bucketStorageAdapter.getMaxOpId()
328369
]);
329370
}
@@ -339,9 +380,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
339380
*/
340381
async execute(sql: string, parameters?: any[]) {
341382
await this.waitForReady();
342-
const result = await this.database.execute(sql, parameters);
343-
_.defer(() => this.syncStreamImplementation?.triggerCrudUpload());
344-
return result;
383+
return this.database.execute(sql, parameters);
345384
}
346385

347386
/**
@@ -386,7 +425,6 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
386425
await this.waitForReady();
387426
return mutexRunExclusive(AbstractPowerSyncDatabase.transactionMutex, async () => {
388427
const res = await callback(this.database);
389-
_.defer(() => this.syncStreamImplementation?.triggerCrudUpload());
390428
return res;
391429
});
392430
}
@@ -415,7 +453,6 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
415453
async (tx) => {
416454
const res = await callback(tx);
417455
await tx.commit();
418-
_.defer(() => this.syncStreamImplementation?.triggerCrudUpload());
419456
return res;
420457
},
421458
{ timeoutMs: lockTimeout }
@@ -475,10 +512,13 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
475512
const dispose = this.database.registerListener({
476513
tablesUpdated: async (update) => {
477514
const { table } = update;
478-
if (!table.match(POWERSYNC_TABLE_MATCH)) {
515+
const { rawTableNames } = options;
516+
517+
if (!rawTableNames && !table.match(POWERSYNC_TABLE_MATCH)) {
479518
return;
480519
}
481-
const tableName = table.replace(POWERSYNC_TABLE_MATCH, '');
520+
521+
const tableName = rawTableNames ? table : table.replace(POWERSYNC_TABLE_MATCH, '');
482522
throttledTableUpdates.push(tableName);
483523

484524
flushTableUpdates();

packages/powersync-sdk-common/src/client/AbstractPowerSyncOpenFactory.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { DBAdapter } from '../db/DBAdapter';
22
import { Schema } from '../db/schema/Schema';
33
import { AbstractPowerSyncDatabase, PowerSyncDatabaseOptions } from './AbstractPowerSyncDatabase';
44

5-
export interface PowerSyncOpenFactoryOptions {
5+
export interface PowerSyncOpenFactoryOptions extends Partial<PowerSyncDatabaseOptions> {
66
schema: Schema;
77
/**
88
* Filename for the database.
@@ -26,7 +26,8 @@ export abstract class AbstractPowerSyncDatabaseOpenFactory {
2626
generateOptions(): PowerSyncDatabaseOptions {
2727
return {
2828
database: this.openDB(),
29-
schema: this.schema
29+
schema: this.schema,
30+
...this.options
3031
};
3132
}
3233

packages/powersync-sdk-common/src/client/sync/bucket/BucketStorageAdapter.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ export interface BucketChecksum {
3737
count: number;
3838
}
3939

40+
export enum PSInternalTable {
41+
DATA = 'ps_data',
42+
CRUD = 'ps_crud',
43+
BUCKETS = 'ps_buckets',
44+
OPLOG = 'ps_oplog'
45+
}
46+
4047
export interface BucketStorageAdapter {
4148
init(): Promise<void>;
4249
saveSyncData(batch: SyncDataBatch): Promise<void>;

packages/powersync-sdk-common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,27 @@ export interface AbstractStreamingSyncImplementationOptions {
3737
uploadCrud: () => Promise<void>;
3838
logger?: ILogger;
3939
retryDelayMs?: number;
40+
crudUploadThrottleMs?: number;
4041
}
4142

4243
export interface StreamingSyncImplementationListener extends BaseListener {
4344
statusChanged?: (status: SyncStatus) => void;
4445
}
4546

47+
export const DEFAULT_CRUD_UPLOAD_THROTTLE_MS = 1000;
48+
4649
export const DEFAULT_STREAMING_SYNC_OPTIONS = {
4750
retryDelayMs: 5000,
48-
logger: Logger.get('PowerSyncStream')
51+
logger: Logger.get('PowerSyncStream'),
52+
crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
4953
};
5054

51-
const CRUD_UPLOAD_DEBOUNCE_MS = 1000;
52-
5355
export abstract class AbstractStreamingSyncImplementation extends BaseObserver<StreamingSyncImplementationListener> {
5456
protected _lastSyncedAt: Date | null;
5557
protected options: AbstractStreamingSyncImplementationOptions;
5658

5759
syncStatus: SyncStatus;
60+
triggerCrudUpload: () => void;
5861

5962
constructor(options: AbstractStreamingSyncImplementationOptions) {
6063
super();
@@ -67,6 +70,17 @@ export abstract class AbstractStreamingSyncImplementation extends BaseObserver<S
6770
downloading: false
6871
}
6972
});
73+
74+
this.triggerCrudUpload = _.throttle(
75+
() => {
76+
if (!this.syncStatus.connected || this.syncStatus.dataFlowStatus.uploading) {
77+
return;
78+
}
79+
this._uploadAllCrud();
80+
},
81+
this.options.crudUploadThrottleMs,
82+
{ trailing: true }
83+
);
7084
}
7185

7286
get lastSyncedAt() {
@@ -88,17 +102,6 @@ export abstract class AbstractStreamingSyncImplementation extends BaseObserver<S
88102
return this.options.adapter.hasCompletedSync();
89103
}
90104

91-
triggerCrudUpload = _.debounce(
92-
() => {
93-
if (!this.syncStatus.connected || this.syncStatus.dataFlowStatus.uploading) {
94-
return;
95-
}
96-
this._uploadAllCrud();
97-
},
98-
CRUD_UPLOAD_DEBOUNCE_MS,
99-
{ trailing: true }
100-
);
101-
102105
protected async _uploadAllCrud(): Promise<void> {
103106
return this.obtainLock({
104107
type: LockType.CRUD,

packages/powersync-sdk-react-native/src/db/PowerSyncDatabase.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
2727
await this.waitForReady();
2828
await connector.uploadData(this);
2929
},
30-
retryDelayMs: this.options.retryDelay
30+
retryDelayMs: this.options.retryDelay,
31+
crudUploadThrottleMs: this.options.crudUploadThrottleMs
3132
});
3233
}
3334
}

0 commit comments

Comments
 (0)