@powersync/common 0.0.0-dev-20240920103931 → 0.0.0-dev-20250207081035

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.
@@ -10,25 +10,18 @@ import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnecto
10
10
  import { BucketStorageAdapter } from './sync/bucket/BucketStorageAdapter.js';
11
11
  import { CrudBatch } from './sync/bucket/CrudBatch.js';
12
12
  import { CrudTransaction } from './sync/bucket/CrudTransaction.js';
13
- import { PowerSyncConnectionOptions, StreamingSyncImplementation, StreamingSyncImplementationListener } from './sync/stream/AbstractStreamingSyncImplementation.js';
13
+ import { type AdditionalConnectionOptions, type PowerSyncConnectionOptions, StreamingSyncImplementation, StreamingSyncImplementationListener, type RequiredAdditionalConnectionOptions } from './sync/stream/AbstractStreamingSyncImplementation.js';
14
14
  export interface DisconnectAndClearOptions {
15
15
  /** When set to false, data in local-only tables is preserved. */
16
16
  clearLocal?: boolean;
17
17
  }
18
- export interface BasePowerSyncDatabaseOptions {
18
+ export interface BasePowerSyncDatabaseOptions extends AdditionalConnectionOptions {
19
19
  /** Schema used for the local database. */
20
20
  schema: Schema;
21
21
  /**
22
- * Delay for retrying sync streaming operations
23
- * from the PowerSync backend after an error occurs.
22
+ * @deprecated Use {@link retryDelayMs} instead as this will be removed in future releases.
24
23
  */
25
24
  retryDelay?: number;
26
- /**
27
- * Backend Connector CRUD operations are throttled
28
- * to occur at most every `crudUploadThrottleMs`
29
- * milliseconds.
30
- */
31
- crudUploadThrottleMs?: number;
32
25
  logger?: ILogger;
33
26
  }
34
27
  export interface PowerSyncDatabaseOptions extends BasePowerSyncDatabaseOptions {
@@ -56,6 +49,8 @@ export interface SQLWatchOptions {
56
49
  /** The minimum interval between queries. */
57
50
  throttleMs?: number;
58
51
  /**
52
+ * @deprecated All tables specified in {@link tables} will be watched, including PowerSync tables with prefixes.
53
+ *
59
54
  * Allows for watching any SQL table
60
55
  * by not removing PowerSync table name prefixes
61
56
  */
@@ -74,6 +69,7 @@ export interface WatchOnChangeHandler {
74
69
  }
75
70
  export interface PowerSyncDBListener extends StreamingSyncImplementationListener {
76
71
  initialized: () => void;
72
+ schemaChanged: (schema: Schema) => void;
77
73
  }
78
74
  export interface PowerSyncCloseOptions {
79
75
  /**
@@ -86,10 +82,11 @@ export interface PowerSyncCloseOptions {
86
82
  export declare const DEFAULT_POWERSYNC_CLOSE_OPTIONS: PowerSyncCloseOptions;
87
83
  export declare const DEFAULT_WATCH_THROTTLE_MS = 30;
88
84
  export declare const DEFAULT_POWERSYNC_DB_OPTIONS: {
89
- retryDelay: number;
85
+ retryDelayMs: number;
90
86
  logger: Logger.ILogger;
91
87
  crudUploadThrottleMs: number;
92
88
  };
89
+ export declare const DEFAULT_CRUD_BATCH_LIMIT = 100;
93
90
  /**
94
91
  * Requesting nested or recursive locks can block the application in some circumstances.
95
92
  * This default lock timeout will act as a failsafe to throw an error if a lock cannot
@@ -144,11 +141,12 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
144
141
  * Whether a connection to the PowerSync service is currently open.
145
142
  */
146
143
  get connected(): boolean;
144
+ get connecting(): boolean;
147
145
  /**
148
146
  * Opens the DBAdapter given open options using a default open factory
149
147
  */
150
148
  protected abstract openDBAdapter(options: PowerSyncDatabaseOptionsWithSettings): DBAdapter;
151
- protected abstract generateSyncStreamImplementation(connector: PowerSyncBackendConnector): StreamingSyncImplementation;
149
+ protected abstract generateSyncStreamImplementation(connector: PowerSyncBackendConnector, options: RequiredAdditionalConnectionOptions): StreamingSyncImplementation;
152
150
  protected abstract generateBucketStorageAdapter(): BucketStorageAdapter;
153
151
  /**
154
152
  * @returns A promise which will resolve once initialization is completed.
@@ -181,6 +179,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
181
179
  * While initializing is automatic, this helps to catch and report initialization errors.
182
180
  */
183
181
  init(): Promise<void>;
182
+ resolvedConnectionOptions(options?: PowerSyncConnectionOptions): RequiredAdditionalConnectionOptions;
184
183
  /**
185
184
  * Connects to stream of events from the PowerSync instance.
186
185
  */
@@ -230,7 +229,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
230
229
  * data by transaction. One batch may contain data from multiple transactions,
231
230
  * and a single transaction may be split over multiple batches.
232
231
  */
233
- getCrudBatch(limit: number): Promise<CrudBatch | null>;
232
+ getCrudBatch(limit?: number): Promise<CrudBatch | null>;
234
233
  /**
235
234
  * Get the next recorded transaction to upload.
236
235
  *
@@ -13,7 +13,8 @@ import { PSInternalTable } from './sync/bucket/BucketStorageAdapter.js';
13
13
  import { CrudBatch } from './sync/bucket/CrudBatch.js';
14
14
  import { CrudEntry } from './sync/bucket/CrudEntry.js';
15
15
  import { CrudTransaction } from './sync/bucket/CrudTransaction.js';
16
- import { DEFAULT_CRUD_UPLOAD_THROTTLE_MS } from './sync/stream/AbstractStreamingSyncImplementation.js';
16
+ import { DEFAULT_CRUD_UPLOAD_THROTTLE_MS, DEFAULT_RETRY_DELAY_MS } from './sync/stream/AbstractStreamingSyncImplementation.js';
17
+ import { runOnSchemaChange } from './runOnSchemaChange.js';
17
18
  const POWERSYNC_TABLE_MATCH = /(^ps_data__|^ps_data_local__)/;
18
19
  const DEFAULT_DISCONNECT_CLEAR_OPTIONS = {
19
20
  clearLocal: true
@@ -23,10 +24,11 @@ export const DEFAULT_POWERSYNC_CLOSE_OPTIONS = {
23
24
  };
24
25
  export const DEFAULT_WATCH_THROTTLE_MS = 30;
25
26
  export const DEFAULT_POWERSYNC_DB_OPTIONS = {
26
- retryDelay: 5000,
27
+ retryDelayMs: 5000,
27
28
  logger: Logger.get('PowerSyncDatabase'),
28
29
  crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
29
30
  };
31
+ export const DEFAULT_CRUD_BATCH_LIMIT = 100;
30
32
  /**
31
33
  * Requesting nested or recursive locks can block the application in some circumstances.
32
34
  * This default lock timeout will act as a failsafe to throw an error if a lock cannot
@@ -112,6 +114,9 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
112
114
  get connected() {
113
115
  return this.currentStatus?.connected || false;
114
116
  }
117
+ get connecting() {
118
+ return this.currentStatus?.connecting || false;
119
+ }
115
120
  /**
116
121
  * @returns A promise which will resolve once initialization is completed.
117
122
  */
@@ -172,11 +177,11 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
172
177
  .map((n) => parseInt(n));
173
178
  }
174
179
  catch (e) {
175
- throw new Error(`Unsupported powersync extension version. Need ^0.2.0, got: ${this.sdkVersion}. Details: ${e.message}`);
180
+ throw new Error(`Unsupported powersync extension version. Need >=0.2.0 <1.0.0, got: ${this.sdkVersion}. Details: ${e.message}`);
176
181
  }
177
- // Validate ^0.2.0
178
- if (versionInts[0] != 0 || versionInts[1] != 2 || versionInts[2] < 0) {
179
- throw new Error(`Unsupported powersync extension version. Need ^0.2.0, got: ${this.sdkVersion}`);
182
+ // Validate >=0.2.0 <1.0.0
183
+ if (versionInts[0] != 0 || versionInts[1] < 2 || versionInts[2] < 0) {
184
+ throw new Error(`Unsupported powersync extension version. Need >=0.2.0 <1.0.0, got: ${this.sdkVersion}`);
180
185
  }
181
186
  }
182
187
  async updateHasSynced() {
@@ -210,6 +215,8 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
210
215
  }
211
216
  this._schema = schema;
212
217
  await this.database.execute('SELECT powersync_replace_schema(?)', [JSON.stringify(this.schema.toJSON())]);
218
+ await this.database.refreshSchema();
219
+ this.iterateListeners(async (cb) => cb.schemaChanged?.(schema));
213
220
  }
214
221
  /**
215
222
  * Wait for initialization to complete.
@@ -218,6 +225,13 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
218
225
  async init() {
219
226
  return this.waitForReady();
220
227
  }
228
+ // Use the options passed in during connect, or fallback to the options set during database creation or fallback to the default options
229
+ resolvedConnectionOptions(options) {
230
+ return {
231
+ retryDelayMs: options?.retryDelayMs ?? this.options.retryDelayMs ?? this.options.retryDelay ?? DEFAULT_RETRY_DELAY_MS,
232
+ crudUploadThrottleMs: options?.crudUploadThrottleMs ?? this.options.crudUploadThrottleMs ?? DEFAULT_CRUD_UPLOAD_THROTTLE_MS
233
+ };
234
+ }
221
235
  /**
222
236
  * Connects to stream of events from the PowerSync instance.
223
237
  */
@@ -228,7 +242,11 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
228
242
  if (this.closed) {
229
243
  throw new Error('Cannot connect using a closed client');
230
244
  }
231
- this.syncStreamImplementation = this.generateSyncStreamImplementation(connector);
245
+ const { retryDelayMs, crudUploadThrottleMs } = this.resolvedConnectionOptions(options);
246
+ this.syncStreamImplementation = this.generateSyncStreamImplementation(connector, {
247
+ retryDelayMs,
248
+ crudUploadThrottleMs,
249
+ });
232
250
  this.syncStatusListenerDisposer = this.syncStreamImplementation.registerListener({
233
251
  statusChanged: (status) => {
234
252
  this.currentStatus = new SyncStatus({
@@ -326,7 +344,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
326
344
  * data by transaction. One batch may contain data from multiple transactions,
327
345
  * and a single transaction may be split over multiple batches.
328
346
  */
329
- async getCrudBatch(limit) {
347
+ async getCrudBatch(limit = DEFAULT_CRUD_BATCH_LIMIT) {
330
348
  const result = await this.getAll(`SELECT id, tx_id, data FROM ${PSInternalTable.CRUD} ORDER BY id ASC LIMIT ?`, [limit + 1]);
331
349
  const all = result.map((row) => CrudEntry.fromRow(row)) ?? [];
332
350
  let haveMore = false;
@@ -386,13 +404,13 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
386
404
  if (writeCheckpoint) {
387
405
  const check = await tx.execute(`SELECT 1 FROM ${PSInternalTable.CRUD} LIMIT 1`);
388
406
  if (!check.rows?.length) {
389
- await tx.execute(`UPDATE ${PSInternalTable.BUCKETS} SET target_op = ? WHERE name='$local'`, [
407
+ await tx.execute(`UPDATE ${PSInternalTable.BUCKETS} SET target_op = CAST(? as INTEGER) WHERE name='$local'`, [
390
408
  writeCheckpoint
391
409
  ]);
392
410
  }
393
411
  }
394
412
  else {
395
- await tx.execute(`UPDATE ${PSInternalTable.BUCKETS} SET target_op = ? WHERE name='$local'`, [
413
+ await tx.execute(`UPDATE ${PSInternalTable.BUCKETS} SET target_op = CAST(? as INTEGER) WHERE name='$local'`, [
396
414
  this.bucketStorageAdapter.getMaxOpId()
397
415
  ]);
398
416
  }
@@ -502,7 +520,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
502
520
  if (!onResult) {
503
521
  throw new Error('onResult is required');
504
522
  }
505
- (async () => {
523
+ const watchQuery = async (abortSignal) => {
506
524
  try {
507
525
  const resolvedTables = await this.resolveTables(sql, parameters, options);
508
526
  // Fetch initial data
@@ -521,13 +539,16 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
521
539
  onError
522
540
  }, {
523
541
  ...(options ?? {}),
524
- tables: resolvedTables
542
+ tables: resolvedTables,
543
+ // Override the abort signal since we intercept it
544
+ signal: abortSignal
525
545
  });
526
546
  }
527
547
  catch (error) {
528
548
  onError?.(error);
529
549
  }
530
- })();
550
+ };
551
+ runOnSchemaChange(watchQuery, this, options);
531
552
  }
532
553
  /**
533
554
  * Execute a read query every time the source tables are modified.
@@ -536,17 +557,18 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
536
557
  */
537
558
  watchWithAsyncGenerator(sql, parameters, options) {
538
559
  return new EventIterator((eventOptions) => {
539
- (async () => {
540
- const resolvedTables = await this.resolveTables(sql, parameters, options);
541
- // Fetch initial data
542
- eventOptions.push(await this.executeReadOnly(sql, parameters));
543
- for await (const event of this.onChangeWithAsyncGenerator({
544
- ...(options ?? {}),
545
- tables: resolvedTables
546
- })) {
547
- eventOptions.push(await this.executeReadOnly(sql, parameters));
560
+ const handler = {
561
+ onResult: (result) => {
562
+ eventOptions.push(result);
563
+ },
564
+ onError: (error) => {
565
+ eventOptions.fail(error);
548
566
  }
549
- })();
567
+ };
568
+ this.watchWithCallback(sql, parameters, handler, options);
569
+ options?.signal?.addEventListener('abort', () => {
570
+ eventOptions.stop();
571
+ });
550
572
  });
551
573
  }
552
574
  async resolveTables(sql, parameters, options) {
@@ -588,7 +610,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
588
610
  throw new Error('onChange is required');
589
611
  }
590
612
  const resolvedOptions = options ?? {};
591
- const watchedTables = new Set(resolvedOptions.tables ?? []);
613
+ const watchedTables = new Set((resolvedOptions?.tables ?? []).flatMap((table) => [table, `ps_data__${table}`, `ps_data_local__${table}`]));
592
614
  const changedTables = new Set();
593
615
  const throttleMs = resolvedOptions.throttleMs ?? DEFAULT_WATCH_THROTTLE_MS;
594
616
  const executor = new ControlledExecutor(async (e) => {
@@ -602,8 +624,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
602
624
  const dispose = this.database.registerListener({
603
625
  tablesUpdated: async (update) => {
604
626
  try {
605
- const { rawTableNames } = resolvedOptions;
606
- this.processTableUpdates(update, rawTableNames, changedTables);
627
+ this.processTableUpdates(update, changedTables);
607
628
  flushTableUpdates();
608
629
  }
609
630
  catch (error) {
@@ -652,19 +673,11 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
652
673
  }
653
674
  changedTables.clear();
654
675
  }
655
- processTableUpdates(updateNotification, rawTableNames, changedTables) {
676
+ processTableUpdates(updateNotification, changedTables) {
656
677
  const tables = isBatchedUpdateNotification(updateNotification)
657
678
  ? updateNotification.tables
658
679
  : [updateNotification.table];
659
- const filteredTables = rawTableNames ? tables : tables.filter((t) => !!t.match(POWERSYNC_TABLE_MATCH));
660
- if (!filteredTables.length) {
661
- return;
662
- }
663
- // Remove any PowerSync table prefixes if necessary
664
- const mappedTableNames = rawTableNames
665
- ? filteredTables
666
- : filteredTables.map((t) => t.replace(POWERSYNC_TABLE_MATCH, ''));
667
- for (const table of mappedTableNames) {
680
+ for (const table of tables) {
668
681
  changedTables.add(table);
669
682
  }
670
683
  }
@@ -0,0 +1,7 @@
1
+ import { CompilableQuery } from './../types/types.js';
2
+ import { AbstractPowerSyncDatabase, SQLWatchOptions } from './AbstractPowerSyncDatabase.js';
3
+ export interface CompilableQueryWatchHandler<T> {
4
+ onResult: (results: T[]) => void;
5
+ onError?: (error: Error) => void;
6
+ }
7
+ export declare function compilableQueryWatch<T>(db: AbstractPowerSyncDatabase, query: CompilableQuery<T>, handler: CompilableQueryWatchHandler<T>, options?: SQLWatchOptions): void;
@@ -0,0 +1,37 @@
1
+ import { runOnSchemaChange } from './runOnSchemaChange.js';
2
+ export function compilableQueryWatch(db, query, handler, options) {
3
+ const { onResult, onError = (e) => { } } = handler ?? {};
4
+ if (!onResult) {
5
+ throw new Error('onResult is required');
6
+ }
7
+ const watchQuery = async (abortSignal) => {
8
+ try {
9
+ const toSql = query.compile();
10
+ const resolvedTables = await db.resolveTables(toSql.sql, toSql.parameters, options);
11
+ // Fetch initial data
12
+ const result = await query.execute();
13
+ onResult(result);
14
+ db.onChangeWithCallback({
15
+ onChange: async () => {
16
+ try {
17
+ const result = await query.execute();
18
+ onResult(result);
19
+ }
20
+ catch (error) {
21
+ onError(error);
22
+ }
23
+ },
24
+ onError
25
+ }, {
26
+ ...(options ?? {}),
27
+ tables: resolvedTables,
28
+ // Override the abort signal since we intercept it
29
+ signal: abortSignal
30
+ });
31
+ }
32
+ catch (error) {
33
+ onError(error);
34
+ }
35
+ };
36
+ runOnSchemaChange(watchQuery, db, options);
37
+ }
@@ -0,0 +1,2 @@
1
+ import { AbstractPowerSyncDatabase, SQLWatchOptions } from './AbstractPowerSyncDatabase.js';
2
+ export declare function runOnSchemaChange(callback: (signal: AbortSignal) => void, db: AbstractPowerSyncDatabase, options?: SQLWatchOptions): void;
@@ -0,0 +1,23 @@
1
+ export function runOnSchemaChange(callback, db, options) {
2
+ const triggerWatchedQuery = () => {
3
+ const abortController = new AbortController();
4
+ let disposeSchemaListener = null;
5
+ const stopWatching = () => {
6
+ abortController.abort('Abort triggered');
7
+ disposeSchemaListener?.();
8
+ disposeSchemaListener = null;
9
+ // Stop listening to upstream abort for this watch
10
+ options?.signal?.removeEventListener('abort', stopWatching);
11
+ };
12
+ options?.signal?.addEventListener('abort', stopWatching);
13
+ disposeSchemaListener = db.registerListener({
14
+ schemaChanged: async () => {
15
+ stopWatching();
16
+ // Re trigger the watched query (recursively), setTimeout ensures that we don't modify the list of listeners while iterating through them
17
+ setTimeout(() => triggerWatchedQuery(), 0);
18
+ }
19
+ });
20
+ callback(abortController.signal);
21
+ };
22
+ triggerWatchedQuery();
23
+ }
@@ -63,7 +63,7 @@ export class SqliteBucketStorage extends BaseObserver {
63
63
  */
64
64
  startSession() { }
65
65
  async getBucketStates() {
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');
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
69
  async saveSyncData(batch) {
@@ -204,9 +204,7 @@ export class SqliteBucketStorage extends BaseObserver {
204
204
  this.compactCounter = 0;
205
205
  }
206
206
  async updateLocalTarget(cb) {
207
- const rs1 = await this.db.getAll("SELECT target_op FROM ps_buckets WHERE name = '$local' AND target_op = ?", [
208
- MAX_OP_ID
209
- ]);
207
+ const rs1 = await this.db.getAll("SELECT target_op FROM ps_buckets WHERE name = '$local' AND target_op = CAST(? as INTEGER)", [MAX_OP_ID]);
210
208
  if (!rs1.length) {
211
209
  // Nothing to update
212
210
  return false;
@@ -6,14 +6,15 @@ import { WebsocketClientTransport } from 'rsocket-websocket-client';
6
6
  import { AbortOperation } from '../../../utils/AbortOperation.js';
7
7
  import { DataStream } from '../../../utils/DataStream.js';
8
8
  import { version as POWERSYNC_JS_VERSION } from '../../../../package.json';
9
+ const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
9
10
  // Refresh at least 30 sec before it expires
10
11
  const REFRESH_CREDENTIALS_SAFETY_PERIOD_MS = 30_000;
11
- const SYNC_QUEUE_REQUEST_N = 10;
12
+ const SYNC_QUEUE_REQUEST_N = 1;
12
13
  const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
13
14
  // Keep alive message is sent every period
14
- const KEEP_ALIVE_MS = 20_000;
15
+ const KEEP_ALIVE_MS = 60_000;
15
16
  // The ACK must be received in this period
16
- const KEEP_ALIVE_LIFETIME_MS = 30_000;
17
+ const KEEP_ALIVE_LIFETIME_MS = 90_000;
17
18
  export const DEFAULT_REMOTE_LOGGER = Logger.get('PowerSyncRemote');
18
19
  /**
19
20
  * Class wrapper for providing a fetch implementation.
@@ -61,6 +62,9 @@ export class AbstractRemote {
61
62
  return this.credentials;
62
63
  }
63
64
  this.credentials = await this.connector.fetchCredentials();
65
+ if (this.credentials?.endpoint.match(POWERSYNC_TRAILING_SLASH_MATCH)) {
66
+ throw new Error(`A trailing forward slash "/" was found in the fetchCredentials endpoint: "${this.credentials.endpoint}". Remove the trailing forward slash "/" to fix this error.`);
67
+ }
64
68
  return this.credentials;
65
69
  }
66
70
  getUserAgent() {
@@ -20,10 +20,9 @@ export interface LockOptions<T> {
20
20
  type: LockType;
21
21
  signal?: AbortSignal;
22
22
  }
23
- export interface AbstractStreamingSyncImplementationOptions {
23
+ export interface AbstractStreamingSyncImplementationOptions extends AdditionalConnectionOptions {
24
24
  adapter: BucketStorageAdapter;
25
25
  uploadCrud: () => Promise<void>;
26
- crudUploadThrottleMs?: number;
27
26
  /**
28
27
  * An identifier for which PowerSync DB this sync implementation is
29
28
  * linked to. Most commonly DB name, but not restricted to DB name.
@@ -31,7 +30,6 @@ export interface AbstractStreamingSyncImplementationOptions {
31
30
  identifier?: string;
32
31
  logger?: ILogger;
33
32
  remote: AbstractRemote;
34
- retryDelayMs?: number;
35
33
  }
36
34
  export interface StreamingSyncImplementationListener extends BaseListener {
37
35
  /**
@@ -48,7 +46,10 @@ export interface StreamingSyncImplementationListener extends BaseListener {
48
46
  * Configurable options to be used when connecting to the PowerSync
49
47
  * backend instance.
50
48
  */
51
- export interface PowerSyncConnectionOptions {
49
+ export interface PowerSyncConnectionOptions extends BaseConnectionOptions, AdditionalConnectionOptions {
50
+ }
51
+ /** @internal */
52
+ export interface BaseConnectionOptions {
52
53
  /**
53
54
  * The connection method to use when streaming updates from
54
55
  * the PowerSync backend instance.
@@ -60,6 +61,22 @@ export interface PowerSyncConnectionOptions {
60
61
  */
61
62
  params?: Record<string, StreamingSyncRequestParameterType>;
62
63
  }
64
+ /** @internal */
65
+ export interface AdditionalConnectionOptions {
66
+ /**
67
+ * Delay for retrying sync streaming operations
68
+ * from the PowerSync backend after an error occurs.
69
+ */
70
+ retryDelayMs?: number;
71
+ /**
72
+ * Backend Connector CRUD operations are throttled
73
+ * to occur at most every `crudUploadThrottleMs`
74
+ * milliseconds.
75
+ */
76
+ crudUploadThrottleMs?: number;
77
+ }
78
+ /** @internal */
79
+ export type RequiredAdditionalConnectionOptions = Required<AdditionalConnectionOptions>;
63
80
  export interface StreamingSyncImplementation extends BaseObserver<StreamingSyncImplementationListener>, Disposable {
64
81
  /**
65
82
  * Connects to the sync service
@@ -80,12 +97,14 @@ export interface StreamingSyncImplementation extends BaseObserver<StreamingSyncI
80
97
  waitForStatus(status: SyncStatusOptions): Promise<void>;
81
98
  }
82
99
  export declare const DEFAULT_CRUD_UPLOAD_THROTTLE_MS = 1000;
100
+ export declare const DEFAULT_RETRY_DELAY_MS = 5000;
83
101
  export declare const DEFAULT_STREAMING_SYNC_OPTIONS: {
84
102
  retryDelayMs: number;
85
103
  logger: Logger.ILogger;
86
104
  crudUploadThrottleMs: number;
87
105
  };
88
- export declare const DEFAULT_STREAM_CONNECTION_OPTIONS: Required<PowerSyncConnectionOptions>;
106
+ export type RequiredPowerSyncConnectionOptions = Required<BaseConnectionOptions>;
107
+ export declare const DEFAULT_STREAM_CONNECTION_OPTIONS: RequiredPowerSyncConnectionOptions;
89
108
  export declare abstract class AbstractStreamingSyncImplementation extends BaseObserver<StreamingSyncImplementationListener> implements StreamingSyncImplementation {
90
109
  protected _lastSyncedAt: Date | null;
91
110
  protected options: AbstractStreamingSyncImplementationOptions;
@@ -2,9 +2,9 @@ import Logger from 'js-logger';
2
2
  import { SyncStatus } from '../../../db/crud/SyncStatus.js';
3
3
  import { AbortOperation } from '../../../utils/AbortOperation.js';
4
4
  import { BaseObserver } from '../../../utils/BaseObserver.js';
5
+ import { throttleLeadingTrailing } from '../../../utils/throttle.js';
5
6
  import { SyncDataBucket } from '../bucket/SyncDataBucket.js';
6
7
  import { isStreamingKeepalive, isStreamingSyncCheckpoint, isStreamingSyncCheckpointComplete, isStreamingSyncCheckpointDiff, isStreamingSyncData } from './streaming-sync-types.js';
7
- import { throttleLeadingTrailing } from '../../../utils/throttle.js';
8
8
  export var LockType;
9
9
  (function (LockType) {
10
10
  LockType["CRUD"] = "crud";
@@ -16,8 +16,9 @@ export var SyncStreamConnectionMethod;
16
16
  SyncStreamConnectionMethod["WEB_SOCKET"] = "web-socket";
17
17
  })(SyncStreamConnectionMethod || (SyncStreamConnectionMethod = {}));
18
18
  export const DEFAULT_CRUD_UPLOAD_THROTTLE_MS = 1000;
19
+ export const DEFAULT_RETRY_DELAY_MS = 5000;
19
20
  export const DEFAULT_STREAMING_SYNC_OPTIONS = {
20
- retryDelayMs: 5000,
21
+ retryDelayMs: DEFAULT_RETRY_DELAY_MS,
21
22
  logger: Logger.get('PowerSyncStream'),
22
23
  crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
23
24
  };
@@ -38,6 +39,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
38
39
  this.options = { ...DEFAULT_STREAMING_SYNC_OPTIONS, ...options };
39
40
  this.syncStatus = new SyncStatus({
40
41
  connected: false,
42
+ connecting: false,
41
43
  lastSyncedAt: undefined,
42
44
  dataFlow: {
43
45
  uploading: false,
@@ -121,7 +123,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
121
123
  */
122
124
  const nextCrudItem = await this.options.adapter.nextCrudItem();
123
125
  if (nextCrudItem) {
124
- if (nextCrudItem.id == checkedCrudItem?.id) {
126
+ if (nextCrudItem.clientId == checkedCrudItem?.clientId) {
125
127
  // This will force a higher log level than exceptions which are caught here.
126
128
  this.logger.warn(`Potentially previously uploaded CRUD entries are still present in the upload queue.
127
129
  Make sure to handle uploads and complete CRUD transactions or batches by calling and awaiting their [.complete()] method.
@@ -207,7 +209,7 @@ The next upload iteration will be delayed.`);
207
209
  }
208
210
  this.streamingSyncPromise = undefined;
209
211
  this.abortController = null;
210
- this.updateSyncStatus({ connected: false });
212
+ this.updateSyncStatus({ connected: false, connecting: false });
211
213
  }
212
214
  /**
213
215
  * @deprecated use [connect instead]
@@ -238,6 +240,7 @@ The next upload iteration will be delayed.`);
238
240
  this.crudUpdateListener = undefined;
239
241
  this.updateSyncStatus({
240
242
  connected: false,
243
+ connecting: false,
241
244
  dataFlow: {
242
245
  downloading: false
243
246
  }
@@ -250,6 +253,7 @@ The next upload iteration will be delayed.`);
250
253
  * - Close any sync stream ReadableStreams (which will also close any established network requests)
251
254
  */
252
255
  while (true) {
256
+ this.updateSyncStatus({ connecting: true });
253
257
  try {
254
258
  if (signal?.aborted) {
255
259
  break;
@@ -280,6 +284,7 @@ The next upload iteration will be delayed.`);
280
284
  else {
281
285
  this.logger.error(ex);
282
286
  }
287
+ // On error, wait a little before retrying
283
288
  await this.delayRetry();
284
289
  }
285
290
  finally {
@@ -288,13 +293,13 @@ The next upload iteration will be delayed.`);
288
293
  nestedAbortController = new AbortController();
289
294
  }
290
295
  this.updateSyncStatus({
291
- connected: false
296
+ connected: false,
297
+ connecting: true // May be unnecessary
292
298
  });
293
- // On error, wait a little before retrying
294
299
  }
295
300
  }
296
301
  // Mark as disconnected if here
297
- this.updateSyncStatus({ connected: false });
302
+ this.updateSyncStatus({ connected: false, connecting: false });
298
303
  }
299
304
  async streamingSyncIteration(signal, options) {
300
305
  return await this.obtainLock({
@@ -489,6 +494,7 @@ The next upload iteration will be delayed.`);
489
494
  updateSyncStatus(options) {
490
495
  const updatedStatus = new SyncStatus({
491
496
  connected: options.connected ?? this.syncStatus.connected,
497
+ connecting: !options.connected && (options.connecting ?? this.syncStatus.connecting),
492
498
  lastSyncedAt: options.lastSyncedAt ?? this.syncStatus.lastSyncedAt,
493
499
  dataFlow: {
494
500
  ...this.syncStatus.dataFlowStatus,
@@ -90,6 +90,10 @@ export interface DBAdapter extends BaseObserverInterface<DBAdapterListener>, DBG
90
90
  readTransaction: <T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions) => Promise<T>;
91
91
  writeLock: <T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions) => Promise<T>;
92
92
  writeTransaction: <T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions) => Promise<T>;
93
+ /**
94
+ * This method refreshes the schema information across all connections. This is for advanced use cases, and should generally not be needed.
95
+ */
96
+ refreshSchema: () => Promise<void>;
93
97
  }
94
98
  export declare function isBatchedUpdateNotification(update: BatchedUpdateNotification | UpdateNotification): update is BatchedUpdateNotification;
95
99
  export declare function extractTableUpdates(update: BatchedUpdateNotification | UpdateNotification): string[];
@@ -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
@@ -12,7 +12,7 @@ export type BaseColumnType<T extends number | string | null> = {
12
12
  };
13
13
  export type ColumnsType = Record<string, BaseColumnType<any>>;
14
14
  export type ExtractColumnValueType<T extends BaseColumnType<any>> = T extends BaseColumnType<infer R> ? R : unknown;
15
- export declare const MAX_AMOUNT_OF_COLUMNS = 63;
15
+ export declare const MAX_AMOUNT_OF_COLUMNS = 1999;
16
16
  export declare const column: {
17
17
  text: BaseColumnType<string | null>;
18
18
  integer: BaseColumnType<number | null>;
@@ -14,9 +14,9 @@ const integer = {
14
14
  const real = {
15
15
  type: ColumnType.REAL
16
16
  };
17
- // There is maximum of 127 arguments for any function in SQLite. Currently we use json_object which uses 1 arg per key (column name)
18
- // and one per value, which limits it to 63 arguments.
19
- export const MAX_AMOUNT_OF_COLUMNS = 63;
17
+ // powersync-sqlite-core limits the number of column per table to 1999, due to internal SQLite limits.
18
+ // In earlier versions this was limited to 63.
19
+ export const MAX_AMOUNT_OF_COLUMNS = 1999;
20
20
  export const column = {
21
21
  text,
22
22
  integer,