@powersync/common 1.23.0 → 1.24.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.
@@ -141,6 +141,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
141
141
  * Whether a connection to the PowerSync service is currently open.
142
142
  */
143
143
  get connected(): boolean;
144
+ get connecting(): boolean;
144
145
  /**
145
146
  * Opens the DBAdapter given open options using a default open factory
146
147
  */
@@ -114,6 +114,9 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
114
114
  get connected() {
115
115
  return this.currentStatus?.connected || false;
116
116
  }
117
+ get connecting() {
118
+ return this.currentStatus?.connecting || false;
119
+ }
117
120
  /**
118
121
  * @returns A promise which will resolve once initialization is completed.
119
122
  */
@@ -16,6 +16,21 @@ export type SyncStreamOptions = {
16
16
  abortSignal?: AbortSignal;
17
17
  fetchOptions?: Request;
18
18
  };
19
+ export declare enum FetchStrategy {
20
+ /**
21
+ * Queues multiple sync events before processing, reducing round-trips.
22
+ * This comes at the cost of more processing overhead, which may cause ACK timeouts on older/weaker devices for big enough datasets.
23
+ */
24
+ Buffered = "buffered",
25
+ /**
26
+ * Processes each sync event immediately before requesting the next.
27
+ * This reduces processing overhead and improves real-time responsiveness.
28
+ */
29
+ Sequential = "sequential"
30
+ }
31
+ export type SocketSyncStreamOptions = SyncStreamOptions & {
32
+ fetchStrategy: FetchStrategy;
33
+ };
19
34
  export type FetchImplementation = typeof fetch;
20
35
  /**
21
36
  * Class wrapper for providing a fetch implementation.
@@ -72,7 +87,7 @@ export declare abstract class AbstractRemote {
72
87
  /**
73
88
  * Connects to the sync/stream websocket endpoint
74
89
  */
75
- socketStream(options: SyncStreamOptions): Promise<DataStream<StreamingSyncLine>>;
90
+ socketStream(options: SocketSyncStreamOptions): Promise<DataStream<StreamingSyncLine>>;
76
91
  /**
77
92
  * Connects to the sync/stream http endpoint
78
93
  */
@@ -9,13 +9,25 @@ import { version as POWERSYNC_JS_VERSION } from '../../../../package.json';
9
9
  const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
10
10
  // Refresh at least 30 sec before it expires
11
11
  const REFRESH_CREDENTIALS_SAFETY_PERIOD_MS = 30_000;
12
- const SYNC_QUEUE_REQUEST_N = 10;
13
12
  const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
14
13
  // Keep alive message is sent every period
15
14
  const KEEP_ALIVE_MS = 20_000;
16
15
  // The ACK must be received in this period
17
16
  const KEEP_ALIVE_LIFETIME_MS = 30_000;
18
17
  export const DEFAULT_REMOTE_LOGGER = Logger.get('PowerSyncRemote');
18
+ export var FetchStrategy;
19
+ (function (FetchStrategy) {
20
+ /**
21
+ * Queues multiple sync events before processing, reducing round-trips.
22
+ * This comes at the cost of more processing overhead, which may cause ACK timeouts on older/weaker devices for big enough datasets.
23
+ */
24
+ FetchStrategy["Buffered"] = "buffered";
25
+ /**
26
+ * Processes each sync event immediately before requesting the next.
27
+ * This reduces processing overhead and improves real-time responsiveness.
28
+ */
29
+ FetchStrategy["Sequential"] = "sequential";
30
+ })(FetchStrategy || (FetchStrategy = {}));
19
31
  /**
20
32
  * Class wrapper for providing a fetch implementation.
21
33
  * The class wrapper is used to distinguish the fetchImplementation
@@ -144,7 +156,8 @@ export class AbstractRemote {
144
156
  * Connects to the sync/stream websocket endpoint
145
157
  */
146
158
  async socketStream(options) {
147
- const { path } = options;
159
+ const { path, fetchStrategy = FetchStrategy.Buffered } = options;
160
+ const syncQueueRequestSize = fetchStrategy == FetchStrategy.Buffered ? 10 : 1;
148
161
  const request = await this.buildRequest(path);
149
162
  const bson = await this.getBSON();
150
163
  // Add the user agent in the setup payload - we can't set custom
@@ -197,7 +210,7 @@ export class AbstractRemote {
197
210
  // Helps to prevent double close scenarios
198
211
  rsocket.onClose(() => (socketIsClosed = true));
199
212
  // We initially request this amount and expect these to arrive eventually
200
- let pendingEventsCount = SYNC_QUEUE_REQUEST_N;
213
+ let pendingEventsCount = syncQueueRequestSize;
201
214
  const disposeClosedListener = stream.registerListener({
202
215
  closed: () => {
203
216
  closeSocket();
@@ -211,7 +224,7 @@ export class AbstractRemote {
211
224
  metadata: Buffer.from(bson.serialize({
212
225
  path
213
226
  }))
214
- }, SYNC_QUEUE_REQUEST_N, // The initial N amount
227
+ }, syncQueueRequestSize, // The initial N amount
215
228
  {
216
229
  onError: (e) => {
217
230
  // Don't log closed as an error
@@ -250,10 +263,10 @@ export class AbstractRemote {
250
263
  const l = stream.registerListener({
251
264
  lowWater: async () => {
252
265
  // Request to fill up the queue
253
- const required = SYNC_QUEUE_REQUEST_N - pendingEventsCount;
266
+ const required = syncQueueRequestSize - pendingEventsCount;
254
267
  if (required > 0) {
255
- socket.request(SYNC_QUEUE_REQUEST_N - pendingEventsCount);
256
- pendingEventsCount = SYNC_QUEUE_REQUEST_N;
268
+ socket.request(syncQueueRequestSize - pendingEventsCount);
269
+ pendingEventsCount = syncQueueRequestSize;
257
270
  }
258
271
  },
259
272
  closed: () => {
@@ -2,7 +2,7 @@ import Logger, { ILogger } from 'js-logger';
2
2
  import { SyncStatus, SyncStatusOptions } from '../../../db/crud/SyncStatus.js';
3
3
  import { BaseListener, BaseObserver, Disposable } from '../../../utils/BaseObserver.js';
4
4
  import { BucketStorageAdapter } from '../bucket/BucketStorageAdapter.js';
5
- import { AbstractRemote } from './AbstractRemote.js';
5
+ import { AbstractRemote, FetchStrategy } from './AbstractRemote.js';
6
6
  import { StreamingSyncRequestParameterType } from './streaming-sync-types.js';
7
7
  export declare enum LockType {
8
8
  CRUD = "crud",
@@ -56,6 +56,10 @@ export interface BaseConnectionOptions {
56
56
  * Defaults to a HTTP streaming connection.
57
57
  */
58
58
  connectionMethod?: SyncStreamConnectionMethod;
59
+ /**
60
+ * The fetch strategy to use when streaming updates from the PowerSync backend instance.
61
+ */
62
+ fetchStrategy?: FetchStrategy;
59
63
  /**
60
64
  * These parameters are passed to the sync rules, and will be available under the`user_parameters` object.
61
65
  */
@@ -4,6 +4,7 @@ import { AbortOperation } from '../../../utils/AbortOperation.js';
4
4
  import { BaseObserver } from '../../../utils/BaseObserver.js';
5
5
  import { throttleLeadingTrailing } from '../../../utils/throttle.js';
6
6
  import { SyncDataBucket } from '../bucket/SyncDataBucket.js';
7
+ import { FetchStrategy } from './AbstractRemote.js';
7
8
  import { isStreamingKeepalive, isStreamingSyncCheckpoint, isStreamingSyncCheckpointComplete, isStreamingSyncCheckpointDiff, isStreamingSyncData } from './streaming-sync-types.js';
8
9
  export var LockType;
9
10
  (function (LockType) {
@@ -24,6 +25,7 @@ export const DEFAULT_STREAMING_SYNC_OPTIONS = {
24
25
  };
25
26
  export const DEFAULT_STREAM_CONNECTION_OPTIONS = {
26
27
  connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET,
28
+ fetchStrategy: FetchStrategy.Buffered,
27
29
  params: {}
28
30
  };
29
31
  export class AbstractStreamingSyncImplementation extends BaseObserver {
@@ -39,6 +41,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
39
41
  this.options = { ...DEFAULT_STREAMING_SYNC_OPTIONS, ...options };
40
42
  this.syncStatus = new SyncStatus({
41
43
  connected: false,
44
+ connecting: false,
42
45
  lastSyncedAt: undefined,
43
46
  dataFlow: {
44
47
  uploading: false,
@@ -208,7 +211,7 @@ The next upload iteration will be delayed.`);
208
211
  }
209
212
  this.streamingSyncPromise = undefined;
210
213
  this.abortController = null;
211
- this.updateSyncStatus({ connected: false });
214
+ this.updateSyncStatus({ connected: false, connecting: false });
212
215
  }
213
216
  /**
214
217
  * @deprecated use [connect instead]
@@ -239,6 +242,7 @@ The next upload iteration will be delayed.`);
239
242
  this.crudUpdateListener = undefined;
240
243
  this.updateSyncStatus({
241
244
  connected: false,
245
+ connecting: false,
242
246
  dataFlow: {
243
247
  downloading: false
244
248
  }
@@ -251,6 +255,7 @@ The next upload iteration will be delayed.`);
251
255
  * - Close any sync stream ReadableStreams (which will also close any established network requests)
252
256
  */
253
257
  while (true) {
258
+ this.updateSyncStatus({ connecting: true });
254
259
  try {
255
260
  if (signal?.aborted) {
256
261
  break;
@@ -281,6 +286,7 @@ The next upload iteration will be delayed.`);
281
286
  else {
282
287
  this.logger.error(ex);
283
288
  }
289
+ // On error, wait a little before retrying
284
290
  await this.delayRetry();
285
291
  }
286
292
  finally {
@@ -289,13 +295,13 @@ The next upload iteration will be delayed.`);
289
295
  nestedAbortController = new AbortController();
290
296
  }
291
297
  this.updateSyncStatus({
292
- connected: false
298
+ connected: false,
299
+ connecting: true // May be unnecessary
293
300
  });
294
- // On error, wait a little before retrying
295
301
  }
296
302
  }
297
303
  // Mark as disconnected if here
298
- this.updateSyncStatus({ connected: false });
304
+ this.updateSyncStatus({ connected: false, connecting: false });
299
305
  }
300
306
  async streamingSyncIteration(signal, options) {
301
307
  return await this.obtainLock({
@@ -335,9 +341,16 @@ The next upload iteration will be delayed.`);
335
341
  client_id: clientId
336
342
  }
337
343
  };
338
- const stream = resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP
339
- ? await this.options.remote.postStream(syncOptions)
340
- : await this.options.remote.socketStream(syncOptions);
344
+ let stream;
345
+ if (resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP) {
346
+ stream = await this.options.remote.postStream(syncOptions);
347
+ }
348
+ else {
349
+ stream = await this.options.remote.socketStream({
350
+ ...syncOptions,
351
+ ...{ fetchStrategy: resolvedOptions.fetchStrategy }
352
+ });
353
+ }
341
354
  this.logger.debug('Stream established. Processing events');
342
355
  while (!stream.closed) {
343
356
  const line = await stream.read();
@@ -490,6 +503,7 @@ The next upload iteration will be delayed.`);
490
503
  updateSyncStatus(options) {
491
504
  const updatedStatus = new SyncStatus({
492
505
  connected: options.connected ?? this.syncStatus.connected,
506
+ connecting: !options.connected && (options.connecting ?? this.syncStatus.connecting),
493
507
  lastSyncedAt: options.lastSyncedAt ?? this.syncStatus.lastSyncedAt,
494
508
  dataFlow: {
495
509
  ...this.syncStatus.dataFlowStatus,
@@ -4,6 +4,7 @@ export type SyncDataFlowStatus = Partial<{
4
4
  }>;
5
5
  export type SyncStatusOptions = {
6
6
  connected?: boolean;
7
+ connecting?: boolean;
7
8
  dataFlow?: SyncDataFlowStatus;
8
9
  lastSyncedAt?: Date;
9
10
  hasSynced?: boolean;
@@ -15,6 +16,7 @@ export declare class SyncStatus {
15
16
  * true if currently connected.
16
17
  */
17
18
  get connected(): boolean;
19
+ get connecting(): boolean;
18
20
  /**
19
21
  * Time that a last sync has fully completed, if any.
20
22
  * Currently this is reset to null after a restart.
@@ -9,6 +9,9 @@ export class SyncStatus {
9
9
  get connected() {
10
10
  return this.options.connected ?? false;
11
11
  }
12
+ get connecting() {
13
+ return this.options.connecting ?? false;
14
+ }
12
15
  /**
13
16
  * Time that a last sync has fully completed, if any.
14
17
  * Currently this is reset to null after a restart.
@@ -44,11 +47,12 @@ export class SyncStatus {
44
47
  }
45
48
  getMessage() {
46
49
  const dataFlow = this.dataFlowStatus;
47
- return `SyncStatus<connected: ${this.connected} lastSyncedAt: ${this.lastSyncedAt} hasSynced: ${this.hasSynced}. Downloading: ${dataFlow.downloading}. Uploading: ${dataFlow.uploading}`;
50
+ return `SyncStatus<connected: ${this.connected} connecting: ${this.connecting} lastSyncedAt: ${this.lastSyncedAt} hasSynced: ${this.hasSynced}. Downloading: ${dataFlow.downloading}. Uploading: ${dataFlow.uploading}`;
48
51
  }
49
52
  toJSON() {
50
53
  return {
51
54
  connected: this.connected,
55
+ connecting: this.connecting,
52
56
  dataFlow: this.dataFlowStatus,
53
57
  lastSyncedAt: this.lastSyncedAt,
54
58
  hasSynced: this.hasSynced
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/common",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"