Skip to content

Commit 0f28fb3

Browse files
feat: add throttle and delay to connect (#478)
1 parent a4895cc commit 0f28fb3

File tree

7 files changed

+290
-28
lines changed

7 files changed

+290
-28
lines changed

.changeset/slow-crews-watch.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@powersync/react-native': minor
3+
'@powersync/common': minor
4+
---
5+
6+
Add `retryDelayMs` and `crudUploadThrottleMs` to `connect` so that the values can be dynamically changed upon reconnecting.

packages/common/src/client/AbstractPowerSyncDatabase.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ import { CrudEntry, CrudEntryJSON } from './sync/bucket/CrudEntry.js';
2424
import { CrudTransaction } from './sync/bucket/CrudTransaction.js';
2525
import {
2626
DEFAULT_CRUD_UPLOAD_THROTTLE_MS,
27-
PowerSyncConnectionOptions,
27+
type AdditionalConnectionOptions,
28+
type PowerSyncConnectionOptions,
2829
StreamingSyncImplementation,
29-
StreamingSyncImplementationListener
30+
StreamingSyncImplementationListener,
31+
DEFAULT_RETRY_DELAY_MS,
32+
type RequiredAdditionalConnectionOptions
3033
} from './sync/stream/AbstractStreamingSyncImplementation.js';
3134
import { runOnSchemaChange } from './runOnSchemaChange.js';
3235

@@ -35,21 +38,13 @@ export interface DisconnectAndClearOptions {
3538
clearLocal?: boolean;
3639
}
3740

38-
export interface BasePowerSyncDatabaseOptions {
41+
export interface BasePowerSyncDatabaseOptions extends AdditionalConnectionOptions {
3942
/** Schema used for the local database. */
4043
schema: Schema;
41-
4244
/**
43-
* Delay for retrying sync streaming operations
44-
* from the PowerSync backend after an error occurs.
45+
* @deprecated Use {@link retryDelayMs} instead as this will be removed in future releases.
4546
*/
4647
retryDelay?: number;
47-
/**
48-
* Backend Connector CRUD operations are throttled
49-
* to occur at most every `crudUploadThrottleMs`
50-
* milliseconds.
51-
*/
52-
crudUploadThrottleMs?: number;
5348
logger?: ILogger;
5449
}
5550

@@ -129,7 +124,7 @@ export const DEFAULT_POWERSYNC_CLOSE_OPTIONS: PowerSyncCloseOptions = {
129124
export const DEFAULT_WATCH_THROTTLE_MS = 30;
130125

131126
export const DEFAULT_POWERSYNC_DB_OPTIONS = {
132-
retryDelay: 5000,
127+
retryDelayMs: 5000,
133128
logger: Logger.get('PowerSyncDatabase'),
134129
crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
135130
};
@@ -243,7 +238,8 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
243238
protected abstract openDBAdapter(options: PowerSyncDatabaseOptionsWithSettings): DBAdapter;
244239

245240
protected abstract generateSyncStreamImplementation(
246-
connector: PowerSyncBackendConnector
241+
connector: PowerSyncBackendConnector,
242+
options: RequiredAdditionalConnectionOptions
247243
): StreamingSyncImplementation;
248244

249245
protected abstract generateBucketStorageAdapter(): BucketStorageAdapter;
@@ -376,6 +372,15 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
376372
return this.waitForReady();
377373
}
378374

375+
// Use the options passed in during connect, or fallback to the options set during database creation or fallback to the default options
376+
resolvedConnectionOptions(options?: PowerSyncConnectionOptions): RequiredAdditionalConnectionOptions {
377+
return {
378+
retryDelayMs: options?.retryDelayMs ?? this.options.retryDelayMs ?? this.options.retryDelay ?? DEFAULT_RETRY_DELAY_MS,
379+
crudUploadThrottleMs:
380+
options?.crudUploadThrottleMs ?? this.options.crudUploadThrottleMs ?? DEFAULT_CRUD_UPLOAD_THROTTLE_MS
381+
};
382+
}
383+
379384
/**
380385
* Connects to stream of events from the PowerSync instance.
381386
*/
@@ -388,7 +393,12 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
388393
throw new Error('Cannot connect using a closed client');
389394
}
390395

391-
this.syncStreamImplementation = this.generateSyncStreamImplementation(connector);
396+
const { retryDelayMs, crudUploadThrottleMs } = this.resolvedConnectionOptions(options);
397+
398+
this.syncStreamImplementation = this.generateSyncStreamImplementation(connector, {
399+
retryDelayMs,
400+
crudUploadThrottleMs,
401+
});
392402
this.syncStatusListenerDisposer = this.syncStreamImplementation.registerListener({
393403
statusChanged: (status) => {
394404
this.currentStatus = new SyncStatus({

packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,16 @@ export interface LockOptions<T> {
3737
signal?: AbortSignal;
3838
}
3939

40-
export interface AbstractStreamingSyncImplementationOptions {
40+
export interface AbstractStreamingSyncImplementationOptions extends AdditionalConnectionOptions {
4141
adapter: BucketStorageAdapter;
4242
uploadCrud: () => Promise<void>;
43-
crudUploadThrottleMs?: number;
4443
/**
4544
* An identifier for which PowerSync DB this sync implementation is
4645
* linked to. Most commonly DB name, but not restricted to DB name.
4746
*/
4847
identifier?: string;
4948
logger?: ILogger;
5049
remote: AbstractRemote;
51-
retryDelayMs?: number;
5250
}
5351

5452
export interface StreamingSyncImplementationListener extends BaseListener {
@@ -67,7 +65,10 @@ export interface StreamingSyncImplementationListener extends BaseListener {
6765
* Configurable options to be used when connecting to the PowerSync
6866
* backend instance.
6967
*/
70-
export interface PowerSyncConnectionOptions {
68+
export interface PowerSyncConnectionOptions extends BaseConnectionOptions, AdditionalConnectionOptions {}
69+
70+
/** @internal */
71+
export interface BaseConnectionOptions {
7172
/**
7273
* The connection method to use when streaming updates from
7374
* the PowerSync backend instance.
@@ -81,6 +82,25 @@ export interface PowerSyncConnectionOptions {
8182
params?: Record<string, StreamingSyncRequestParameterType>;
8283
}
8384

85+
/** @internal */
86+
export interface AdditionalConnectionOptions {
87+
/**
88+
* Delay for retrying sync streaming operations
89+
* from the PowerSync backend after an error occurs.
90+
*/
91+
retryDelayMs?: number;
92+
/**
93+
* Backend Connector CRUD operations are throttled
94+
* to occur at most every `crudUploadThrottleMs`
95+
* milliseconds.
96+
*/
97+
crudUploadThrottleMs?: number;
98+
}
99+
100+
101+
/** @internal */
102+
export type RequiredAdditionalConnectionOptions = Required<AdditionalConnectionOptions>
103+
84104
export interface StreamingSyncImplementation extends BaseObserver<StreamingSyncImplementationListener>, Disposable {
85105
/**
86106
* Connects to the sync service
@@ -102,14 +122,17 @@ export interface StreamingSyncImplementation extends BaseObserver<StreamingSyncI
102122
}
103123

104124
export const DEFAULT_CRUD_UPLOAD_THROTTLE_MS = 1000;
125+
export const DEFAULT_RETRY_DELAY_MS = 5000;
105126

106127
export const DEFAULT_STREAMING_SYNC_OPTIONS = {
107-
retryDelayMs: 5000,
128+
retryDelayMs: DEFAULT_RETRY_DELAY_MS,
108129
logger: Logger.get('PowerSyncStream'),
109130
crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
110131
};
111132

112-
export const DEFAULT_STREAM_CONNECTION_OPTIONS: Required<PowerSyncConnectionOptions> = {
133+
export type RequiredPowerSyncConnectionOptions = Required<BaseConnectionOptions>;
134+
135+
export const DEFAULT_STREAM_CONNECTION_OPTIONS: RequiredPowerSyncConnectionOptions = {
113136
connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET,
114137
params: {}
115138
};
@@ -427,7 +450,7 @@ The next upload iteration will be delayed.`);
427450
type: LockType.SYNC,
428451
signal,
429452
callback: async () => {
430-
const resolvedOptions: Required<PowerSyncConnectionOptions> = {
453+
const resolvedOptions: RequiredPowerSyncConnectionOptions = {
431454
...DEFAULT_STREAM_CONNECTION_OPTIONS,
432455
...(options ?? {})
433456
};

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
DBAdapter,
66
PowerSyncBackendConnector,
77
PowerSyncDatabaseOptionsWithSettings,
8+
type RequiredAdditionalConnectionOptions,
89
SqliteBucketStorage
910
} from '@powersync/common';
1011
import { ReactNativeRemote } from '../sync/stream/ReactNativeRemote';
@@ -42,7 +43,8 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
4243
}
4344

4445
protected generateSyncStreamImplementation(
45-
connector: PowerSyncBackendConnector
46+
connector: PowerSyncBackendConnector,
47+
options: RequiredAdditionalConnectionOptions
4648
): AbstractStreamingSyncImplementation {
4749
const remote = new ReactNativeRemote(connector);
4850

@@ -53,8 +55,8 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
5355
await this.waitForReady();
5456
await connector.uploadData(this);
5557
},
56-
retryDelayMs: this.options.retryDelay,
57-
crudUploadThrottleMs: this.options.crudUploadThrottleMs,
58+
retryDelayMs: options.retryDelayMs,
59+
crudUploadThrottleMs: options.crudUploadThrottleMs,
5860
identifier: this.database.name
5961
});
6062
}

packages/web/src/db/PowerSyncDatabase.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
type PowerSyncBackendConnector,
44
type PowerSyncCloseOptions,
55
type PowerSyncConnectionOptions,
6+
type RequiredAdditionalConnectionOptions,
67
AbstractPowerSyncDatabase,
78
DBAdapter,
89
DEFAULT_POWERSYNC_CLOSE_OPTIONS,
@@ -13,7 +14,7 @@ import {
1314
PowerSyncDatabaseOptionsWithOpenFactory,
1415
PowerSyncDatabaseOptionsWithSettings,
1516
SqliteBucketStorage,
16-
StreamingSyncImplementation
17+
StreamingSyncImplementation,
1718
} from '@powersync/common';
1819
import { Mutex } from 'async-mutex';
1920
import { getNavigatorLocks } from '../shared/navigator';
@@ -194,11 +195,15 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
194195
return getNavigatorLocks().request(`lock-${this.database.name}`, cb);
195196
}
196197

197-
protected generateSyncStreamImplementation(connector: PowerSyncBackendConnector): StreamingSyncImplementation {
198+
protected generateSyncStreamImplementation(
199+
connector: PowerSyncBackendConnector,
200+
options: RequiredAdditionalConnectionOptions
201+
): StreamingSyncImplementation {
198202
const remote = new WebRemote(connector);
199-
200203
const syncOptions: WebStreamingSyncImplementationOptions = {
201204
...(this.options as {}),
205+
retryDelayMs: options.retryDelayMs,
206+
crudUploadThrottleMs: options.crudUploadThrottleMs,
202207
flags: this.resolvedFlags,
203208
adapter: this.bucketStorageAdapter,
204209
remote,
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { AbstractPowerSyncDatabase, DEFAULT_RETRY_DELAY_MS, DEFAULT_CRUD_UPLOAD_THROTTLE_MS, BucketStorageAdapter, DBAdapter, PowerSyncBackendConnector, PowerSyncDatabaseOptionsWithSettings, RequiredAdditionalConnectionOptions, StreamingSyncImplementation } from '@powersync/common';
3+
import { testSchema } from '../../utils/testDb';
4+
5+
class TestPowerSyncDatabase extends AbstractPowerSyncDatabase {
6+
protected openDBAdapter(options: PowerSyncDatabaseOptionsWithSettings): DBAdapter {
7+
return {} as any
8+
}
9+
protected generateSyncStreamImplementation(connector: PowerSyncBackendConnector, options: RequiredAdditionalConnectionOptions): StreamingSyncImplementation {
10+
return undefined as any;
11+
}
12+
protected generateBucketStorageAdapter(): BucketStorageAdapter {
13+
return {
14+
init: vi.fn()
15+
} as any
16+
}
17+
_initialize(): Promise<void> {
18+
return Promise.resolve();
19+
}
20+
21+
get database() {
22+
return {
23+
get: vi.fn().mockResolvedValue({ version: '0.3.0'}),
24+
execute: vi.fn(),
25+
refreshSchema: vi.fn(),
26+
} as any
27+
}
28+
// Expose protected method for testing
29+
public testResolvedConnectionOptions(options?: any) {
30+
return this.resolvedConnectionOptions(options);
31+
}
32+
}
33+
34+
describe('AbstractPowerSyncDatabase', () => {
35+
describe('resolvedConnectionOptions', () => {
36+
it('should use connect options when provided', () => {
37+
const db = new TestPowerSyncDatabase({
38+
schema: testSchema,
39+
database: { dbFilename: 'test.db' }
40+
});
41+
42+
const result = db.testResolvedConnectionOptions({
43+
retryDelayMs: 1000,
44+
crudUploadThrottleMs: 2000
45+
});
46+
47+
expect(result).toEqual({
48+
retryDelayMs: 1000,
49+
crudUploadThrottleMs: 2000
50+
});
51+
});
52+
53+
it('should fallback to constructor options when connect options not provided', () => {
54+
const db = new TestPowerSyncDatabase({
55+
schema: testSchema,
56+
database: { dbFilename: 'test.db' },
57+
retryDelayMs: 3000,
58+
crudUploadThrottleMs: 4000
59+
});
60+
61+
const result = db.testResolvedConnectionOptions();
62+
63+
expect(result).toEqual({
64+
retryDelayMs: 3000,
65+
crudUploadThrottleMs: 4000
66+
});
67+
});
68+
69+
it('should convert retryDelay to retryDelayMs', () => {
70+
const db = new TestPowerSyncDatabase({
71+
schema: testSchema,
72+
database: { dbFilename: 'test.db' },
73+
retryDelay: 5000
74+
});
75+
76+
const result = db.testResolvedConnectionOptions();
77+
78+
expect(result).toEqual({
79+
retryDelayMs: 5000,
80+
crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
81+
});
82+
});
83+
84+
it('should prioritize retryDelayMs over retryDelay in constructor options', () => {
85+
const db = new TestPowerSyncDatabase({
86+
schema: testSchema,
87+
database: { dbFilename: 'test.db' },
88+
retryDelay: 5000,
89+
retryDelayMs: 6000
90+
});
91+
92+
const result = db.testResolvedConnectionOptions();
93+
94+
expect(result).toEqual({
95+
retryDelayMs: 6000,
96+
crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
97+
});
98+
});
99+
100+
it('should prioritize connect options over constructor options', () => {
101+
const db = new TestPowerSyncDatabase({
102+
schema: testSchema,
103+
database: { dbFilename: 'test.db' },
104+
retryDelayMs: 5000,
105+
crudUploadThrottleMs: 6000
106+
});
107+
108+
const result = db.testResolvedConnectionOptions({
109+
retryDelayMs: 7000,
110+
crudUploadThrottleMs: 8000
111+
});
112+
113+
expect(result).toEqual({
114+
retryDelayMs: 7000,
115+
crudUploadThrottleMs: 8000
116+
});
117+
});
118+
119+
it('should use default values when no options provided', () => {
120+
const db = new TestPowerSyncDatabase({
121+
schema: testSchema,
122+
database: { dbFilename: 'test.db' }
123+
});
124+
125+
const result = db.testResolvedConnectionOptions();
126+
127+
expect(result).toEqual({
128+
retryDelayMs: DEFAULT_RETRY_DELAY_MS,
129+
crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
130+
});
131+
});
132+
133+
it('should handle partial connect options', () => {
134+
const db = new TestPowerSyncDatabase({
135+
schema: testSchema,
136+
database: { dbFilename: 'test.db' },
137+
retryDelayMs: 5000,
138+
crudUploadThrottleMs: 6000
139+
});
140+
141+
const result = db.testResolvedConnectionOptions({
142+
retryDelayMs: 7000
143+
});
144+
145+
expect(result).toEqual({
146+
retryDelayMs: 7000,
147+
crudUploadThrottleMs: 6000
148+
});
149+
});
150+
});
151+
});

0 commit comments

Comments
 (0)