@powersync/common 1.32.0 → 1.33.1

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.
@@ -136,7 +136,6 @@ export class ConnectionManager extends BaseObserver {
136
136
  await this.disconnectingPromise;
137
137
  this.logger.debug('Attempting to connect to PowerSync instance');
138
138
  await this.syncStreamImplementation?.connect(appliedOptions);
139
- this.syncStreamImplementation?.triggerCrudUpload();
140
139
  }
141
140
  /**
142
141
  * Close the sync connection.
@@ -51,17 +51,27 @@ export declare enum PSInternalTable {
51
51
  OPLOG = "ps_oplog",
52
52
  UNTYPED = "ps_untyped"
53
53
  }
54
+ export declare enum PowerSyncControlCommand {
55
+ PROCESS_TEXT_LINE = "line_text",
56
+ PROCESS_BSON_LINE = "line_binary",
57
+ STOP = "stop",
58
+ START = "start",
59
+ NOTIFY_TOKEN_REFRESHED = "refreshed_token",
60
+ NOTIFY_CRUD_UPLOAD_COMPLETED = "completed_upload"
61
+ }
54
62
  export interface BucketStorageListener extends BaseListener {
55
63
  crudUpdate: () => void;
56
64
  }
57
65
  export interface BucketStorageAdapter extends BaseObserver<BucketStorageListener>, Disposable {
58
66
  init(): Promise<void>;
59
- saveSyncData(batch: SyncDataBatch): Promise<void>;
67
+ saveSyncData(batch: SyncDataBatch, fixedKeyFormat?: boolean): Promise<void>;
60
68
  removeBuckets(buckets: string[]): Promise<void>;
61
69
  setTargetCheckpoint(checkpoint: Checkpoint): Promise<void>;
62
70
  startSession(): void;
63
71
  getBucketStates(): Promise<BucketState[]>;
64
72
  getBucketOperationProgress(): Promise<BucketOperationProgress>;
73
+ hasMigratedSubkeys(): Promise<boolean>;
74
+ migrateToFixedSubkeys(): Promise<void>;
65
75
  syncLocalDatabase(checkpoint: Checkpoint, priority?: number): Promise<{
66
76
  checkpointValid: boolean;
67
77
  ready: boolean;
@@ -72,17 +82,13 @@ export interface BucketStorageAdapter extends BaseObserver<BucketStorageListener
72
82
  getCrudBatch(limit?: number): Promise<CrudBatch | null>;
73
83
  hasCompletedSync(): Promise<boolean>;
74
84
  updateLocalTarget(cb: () => Promise<string>): Promise<boolean>;
75
- /**
76
- * Exposed for tests only.
77
- */
78
- autoCompact(): Promise<void>;
79
- /**
80
- * Exposed for tests only.
81
- */
82
- forceCompact(): Promise<void>;
83
85
  getMaxOpId(): string;
84
86
  /**
85
87
  * Get an unique client id.
86
88
  */
87
89
  getClientId(): Promise<string>;
90
+ /**
91
+ * Invokes the `powersync_control` function for the sync client.
92
+ */
93
+ control(op: PowerSyncControlCommand, payload: string | Uint8Array | null): Promise<string>;
88
94
  }
@@ -6,3 +6,12 @@ export var PSInternalTable;
6
6
  PSInternalTable["OPLOG"] = "ps_oplog";
7
7
  PSInternalTable["UNTYPED"] = "ps_untyped";
8
8
  })(PSInternalTable || (PSInternalTable = {}));
9
+ export var PowerSyncControlCommand;
10
+ (function (PowerSyncControlCommand) {
11
+ PowerSyncControlCommand["PROCESS_TEXT_LINE"] = "line_text";
12
+ PowerSyncControlCommand["PROCESS_BSON_LINE"] = "line_binary";
13
+ PowerSyncControlCommand["STOP"] = "stop";
14
+ PowerSyncControlCommand["START"] = "start";
15
+ PowerSyncControlCommand["NOTIFY_TOKEN_REFRESHED"] = "refreshed_token";
16
+ PowerSyncControlCommand["NOTIFY_CRUD_UPLOAD_COMPLETED"] = "completed_upload";
17
+ })(PowerSyncControlCommand || (PowerSyncControlCommand = {}));
@@ -30,6 +30,8 @@ type CrudEntryOutputJSON = {
30
30
  id: string;
31
31
  tx_id?: number;
32
32
  data?: Record<string, any>;
33
+ old?: Record<string, any>;
34
+ metadata?: string;
33
35
  };
34
36
  /**
35
37
  * A single client-side change.
@@ -74,7 +74,9 @@ export class CrudEntry {
74
74
  type: this.table,
75
75
  id: this.id,
76
76
  tx_id: this.transactionId,
77
- data: this.opData
77
+ data: this.opData,
78
+ old: this.previousValues,
79
+ metadata: this.metadata
78
80
  };
79
81
  }
80
82
  equals(entry) {
@@ -93,6 +95,15 @@ export class CrudEntry {
93
95
  * Generates an array for use in deep comparison operations
94
96
  */
95
97
  toComparisonArray() {
96
- return [this.transactionId, this.clientId, this.op, this.table, this.id, this.opData];
98
+ return [
99
+ this.transactionId,
100
+ this.clientId,
101
+ this.op,
102
+ this.table,
103
+ this.id,
104
+ this.opData,
105
+ this.previousValues,
106
+ this.metadata
107
+ ];
97
108
  }
98
109
  }
@@ -7,17 +7,17 @@ export interface OplogEntryJSON {
7
7
  object_type?: string;
8
8
  op_id: string;
9
9
  op: OpTypeJSON;
10
- subkey?: string | object;
10
+ subkey?: string;
11
11
  }
12
12
  export declare class OplogEntry {
13
13
  op_id: OpId;
14
14
  op: OpType;
15
15
  checksum: number;
16
- subkey: string;
16
+ subkey?: string | undefined;
17
17
  object_type?: string | undefined;
18
18
  object_id?: string | undefined;
19
19
  data?: string | undefined;
20
20
  static fromRow(row: OplogEntryJSON): OplogEntry;
21
- constructor(op_id: OpId, op: OpType, checksum: number, subkey: string, object_type?: string | undefined, object_id?: string | undefined, data?: string | undefined);
22
- toJSON(): OplogEntryJSON;
21
+ constructor(op_id: OpId, op: OpType, checksum: number, subkey?: string | undefined, object_type?: string | undefined, object_id?: string | undefined, data?: string | undefined);
22
+ toJSON(fixedKeyEncoding?: boolean): OplogEntryJSON;
23
23
  }
@@ -8,7 +8,7 @@ export class OplogEntry {
8
8
  object_id;
9
9
  data;
10
10
  static fromRow(row) {
11
- return new OplogEntry(row.op_id, OpType.fromJSON(row.op), row.checksum, typeof row.subkey == 'string' ? row.subkey : JSON.stringify(row.subkey), row.object_type, row.object_id, row.data);
11
+ return new OplogEntry(row.op_id, OpType.fromJSON(row.op), row.checksum, row.subkey, row.object_type, row.object_id, row.data);
12
12
  }
13
13
  constructor(op_id, op, checksum, subkey, object_type, object_id, data) {
14
14
  this.op_id = op_id;
@@ -19,7 +19,7 @@ export class OplogEntry {
19
19
  this.object_id = object_id;
20
20
  this.data = data;
21
21
  }
22
- toJSON() {
22
+ toJSON(fixedKeyEncoding = false) {
23
23
  return {
24
24
  op_id: this.op_id,
25
25
  op: this.op.toJSON(),
@@ -27,7 +27,9 @@ export class OplogEntry {
27
27
  object_id: this.object_id,
28
28
  checksum: this.checksum,
29
29
  data: this.data,
30
- subkey: JSON.stringify(this.subkey)
30
+ // Older versions of the JS SDK used to always JSON.stringify here. That has always been wrong,
31
+ // but we need to migrate gradually to not break existing databases.
32
+ subkey: fixedKeyEncoding ? this.subkey : JSON.stringify(this.subkey)
31
33
  };
32
34
  }
33
35
  }
@@ -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 { BucketOperationProgress, BucketState, BucketStorageAdapter, BucketStorageListener, Checkpoint, SyncLocalDatabaseResult } from './BucketStorageAdapter.js';
5
+ import { BucketOperationProgress, BucketState, BucketStorageAdapter, BucketStorageListener, Checkpoint, PowerSyncControlCommand, 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';
@@ -11,14 +11,9 @@ export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListe
11
11
  private mutex;
12
12
  private logger;
13
13
  tableNames: Set<string>;
14
- private pendingBucketDeletes;
15
14
  private _hasCompletedSync;
16
15
  private updateListener;
17
16
  private _clientId?;
18
- /**
19
- * Count up, and do a compact on startup.
20
- */
21
- private compactCounter;
22
17
  constructor(db: DBAdapter, mutex: Mutex, logger?: ILogger);
23
18
  init(): Promise<void>;
24
19
  dispose(): Promise<void>;
@@ -31,7 +26,7 @@ export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListe
31
26
  startSession(): void;
32
27
  getBucketStates(): Promise<BucketState[]>;
33
28
  getBucketOperationProgress(): Promise<BucketOperationProgress>;
34
- saveSyncData(batch: SyncDataBatch): Promise<void>;
29
+ saveSyncData(batch: SyncDataBatch, fixedKeyFormat?: boolean): Promise<void>;
35
30
  removeBuckets(buckets: string[]): Promise<void>;
36
31
  /**
37
32
  * Mark a bucket for deletion.
@@ -46,13 +41,6 @@ export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListe
46
41
  */
47
42
  private updateObjectsFromBuckets;
48
43
  validateChecksums(checkpoint: Checkpoint, priority: number | undefined): Promise<SyncLocalDatabaseResult>;
49
- /**
50
- * Force a compact, for tests.
51
- */
52
- forceCompact(): Promise<void>;
53
- autoCompact(): Promise<void>;
54
- private deletePendingBuckets;
55
- private clearRemoveOps;
56
44
  updateLocalTarget(cb: () => Promise<string>): Promise<boolean>;
57
45
  nextCrudItem(): Promise<CrudEntry | undefined>;
58
46
  hasCrud(): Promise<boolean>;
@@ -68,4 +56,8 @@ export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListe
68
56
  * Set a target checkpoint.
69
57
  */
70
58
  setTargetCheckpoint(checkpoint: Checkpoint): Promise<void>;
59
+ control(op: PowerSyncControlCommand, payload: string | Uint8Array | ArrayBuffer | null): Promise<string>;
60
+ hasMigratedSubkeys(): Promise<boolean>;
61
+ migrateToFixedSubkeys(): Promise<void>;
62
+ static _subkeyMigrationKey: string;
71
63
  }
@@ -4,27 +4,20 @@ import { BaseObserver } from '../../../utils/BaseObserver.js';
4
4
  import { MAX_OP_ID } from '../../constants.js';
5
5
  import { PSInternalTable } from './BucketStorageAdapter.js';
6
6
  import { CrudEntry } from './CrudEntry.js';
7
- const COMPACT_OPERATION_INTERVAL = 1_000;
8
7
  export class SqliteBucketStorage extends BaseObserver {
9
8
  db;
10
9
  mutex;
11
10
  logger;
12
11
  tableNames;
13
- pendingBucketDeletes;
14
12
  _hasCompletedSync;
15
13
  updateListener;
16
14
  _clientId;
17
- /**
18
- * Count up, and do a compact on startup.
19
- */
20
- compactCounter = COMPACT_OPERATION_INTERVAL;
21
15
  constructor(db, mutex, logger = Logger.get('SqliteBucketStorage')) {
22
16
  super();
23
17
  this.db = db;
24
18
  this.mutex = mutex;
25
19
  this.logger = logger;
26
20
  this._hasCompletedSync = false;
27
- this.pendingBucketDeletes = true;
28
21
  this.tableNames = new Set();
29
22
  this.updateListener = db.registerListener({
30
23
  tablesUpdated: (update) => {
@@ -70,18 +63,15 @@ export class SqliteBucketStorage extends BaseObserver {
70
63
  const rows = await this.db.getAll('SELECT name, count_at_last, count_since_last FROM ps_buckets');
71
64
  return Object.fromEntries(rows.map((r) => [r.name, { atLast: r.count_at_last, sinceLast: r.count_since_last }]));
72
65
  }
73
- async saveSyncData(batch) {
66
+ async saveSyncData(batch, fixedKeyFormat = false) {
74
67
  await this.writeTransaction(async (tx) => {
75
- let count = 0;
76
68
  for (const b of batch.buckets) {
77
69
  const result = await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
78
70
  'save',
79
- JSON.stringify({ buckets: [b.toJSON()] })
71
+ JSON.stringify({ buckets: [b.toJSON(fixedKeyFormat)] })
80
72
  ]);
81
73
  this.logger.debug('saveSyncData', JSON.stringify(result));
82
- count += b.data.length;
83
74
  }
84
- this.compactCounter += count;
85
75
  });
86
76
  }
87
77
  async removeBuckets(buckets) {
@@ -97,7 +87,6 @@ export class SqliteBucketStorage extends BaseObserver {
97
87
  await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', ['delete_bucket', bucket]);
98
88
  });
99
89
  this.logger.debug('done deleting bucket');
100
- this.pendingBucketDeletes = true;
101
90
  }
102
91
  async hasCompletedSync() {
103
92
  if (this._hasCompletedSync) {
@@ -138,7 +127,6 @@ export class SqliteBucketStorage extends BaseObserver {
138
127
  this.logger.debug('Not at a consistent checkpoint - cannot update local db');
139
128
  return { ready: false, checkpointValid: true };
140
129
  }
141
- await this.forceCompact();
142
130
  return {
143
131
  ready: true,
144
132
  checkpointValid: true
@@ -209,36 +197,6 @@ export class SqliteBucketStorage extends BaseObserver {
209
197
  };
210
198
  }
211
199
  }
212
- /**
213
- * Force a compact, for tests.
214
- */
215
- async forceCompact() {
216
- this.compactCounter = COMPACT_OPERATION_INTERVAL;
217
- this.pendingBucketDeletes = true;
218
- await this.autoCompact();
219
- }
220
- async autoCompact() {
221
- await this.deletePendingBuckets();
222
- await this.clearRemoveOps();
223
- }
224
- async deletePendingBuckets() {
225
- if (this.pendingBucketDeletes !== false) {
226
- await this.writeTransaction(async (tx) => {
227
- await tx.execute('INSERT INTO powersync_operations(op, data) VALUES (?, ?)', ['delete_pending_buckets', '']);
228
- });
229
- // Executed once after start-up, and again when there are pending deletes.
230
- this.pendingBucketDeletes = false;
231
- }
232
- }
233
- async clearRemoveOps() {
234
- if (this.compactCounter < COMPACT_OPERATION_INTERVAL) {
235
- return;
236
- }
237
- await this.writeTransaction(async (tx) => {
238
- await tx.execute('INSERT INTO powersync_operations(op, data) VALUES (?, ?)', ['clear_remove_ops', '']);
239
- });
240
- this.compactCounter = 0;
241
- }
242
200
  async updateLocalTarget(cb) {
243
201
  const rs1 = await this.db.getAll("SELECT target_op FROM ps_buckets WHERE name = '$local' AND target_op = CAST(? as INTEGER)", [MAX_OP_ID]);
244
202
  if (!rs1.length) {
@@ -339,6 +297,28 @@ export class SqliteBucketStorage extends BaseObserver {
339
297
  async setTargetCheckpoint(checkpoint) {
340
298
  // No-op for now
341
299
  }
300
+ async control(op, payload) {
301
+ return await this.writeTransaction(async (tx) => {
302
+ const [[raw]] = await tx.executeRaw('SELECT powersync_control(?, ?)', [op, payload]);
303
+ return raw;
304
+ });
305
+ }
306
+ async hasMigratedSubkeys() {
307
+ const { r } = await this.db.get('SELECT EXISTS(SELECT * FROM ps_kv WHERE key = ?) as r', [
308
+ SqliteBucketStorage._subkeyMigrationKey
309
+ ]);
310
+ return r != 0;
311
+ }
312
+ async migrateToFixedSubkeys() {
313
+ await this.writeTransaction(async (tx) => {
314
+ await tx.execute('UPDATE ps_oplog SET key = powersync_remove_duplicate_key_encoding(key);');
315
+ await tx.execute('INSERT OR REPLACE INTO ps_kv (key, value) VALUES (?, ?);', [
316
+ SqliteBucketStorage._subkeyMigrationKey,
317
+ '1'
318
+ ]);
319
+ });
320
+ }
321
+ static _subkeyMigrationKey = 'powersync_js_migrated_subkeys';
342
322
  }
343
323
  function hasMatchingPriority(priority, bucket) {
344
324
  return bucket.priority != null && bucket.priority <= priority;
@@ -36,5 +36,5 @@ export declare class SyncDataBucket {
36
36
  * Use this for the next request.
37
37
  */
38
38
  next_after?: OpId | undefined);
39
- toJSON(): SyncDataBucketJSON;
39
+ toJSON(fixedKeyEncoding?: boolean): SyncDataBucketJSON;
40
40
  }
@@ -27,13 +27,13 @@ export class SyncDataBucket {
27
27
  this.after = after;
28
28
  this.next_after = next_after;
29
29
  }
30
- toJSON() {
30
+ toJSON(fixedKeyEncoding = false) {
31
31
  return {
32
32
  bucket: this.bucket,
33
33
  has_more: this.has_more,
34
34
  after: this.after,
35
35
  next_after: this.next_after,
36
- data: this.data.map((entry) => entry.toJSON())
36
+ data: this.data.map((entry) => entry.toJSON(fixedKeyEncoding))
37
37
  };
38
38
  }
39
39
  }
@@ -115,18 +115,30 @@ export declare abstract class AbstractRemote {
115
115
  }>;
116
116
  post(path: string, data: any, headers?: Record<string, string>): Promise<any>;
117
117
  get(path: string, headers?: Record<string, string>): Promise<any>;
118
- postStreaming(path: string, data: any, headers?: Record<string, string>, signal?: AbortSignal): Promise<any>;
119
118
  /**
120
119
  * Provides a BSON implementation. The import nature of this varies depending on the platform
121
120
  */
122
121
  abstract getBSON(): Promise<BSONImplementation>;
123
122
  protected createSocket(url: string): WebSocket;
124
123
  /**
125
- * Connects to the sync/stream websocket endpoint
124
+ * Connects to the sync/stream websocket endpoint and delivers sync lines by decoding the BSON events
125
+ * sent by the server.
126
126
  */
127
127
  socketStream(options: SocketSyncStreamOptions): Promise<DataStream<StreamingSyncLine>>;
128
128
  /**
129
- * Connects to the sync/stream http endpoint
129
+ * Returns a data stream of sync line data.
130
+ *
131
+ * @param map Maps received payload frames to the typed event value.
132
+ * @param bson A BSON encoder and decoder. When set, the data stream will be requested with a BSON payload
133
+ * (required for compatibility with older sync services).
134
+ */
135
+ socketStreamRaw<T>(options: SocketSyncStreamOptions, map: (buffer: Uint8Array) => T, bson?: typeof BSON): Promise<DataStream<T>>;
136
+ /**
137
+ * Connects to the sync/stream http endpoint, parsing lines as JSON.
130
138
  */
131
139
  postStream(options: SyncStreamOptions): Promise<DataStream<StreamingSyncLine>>;
140
+ /**
141
+ * Connects to the sync/stream http endpoint, mapping and emitting each received string line.
142
+ */
143
+ postStreamRaw<T>(options: SyncStreamOptions, mapLine: (line: string) => T): Promise<DataStream<T>>;
132
144
  }