@powersync/common 0.0.0-dev-20250423120133 → 0.0.0-dev-20250526133243

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.
@@ -196,11 +196,12 @@ The next upload iteration will be delayed.`);
196
196
  if (this.abortController) {
197
197
  await this.disconnect();
198
198
  }
199
- this.abortController = new AbortController();
199
+ const controller = new AbortController();
200
+ this.abortController = controller;
200
201
  this.streamingSyncPromise = this.streamingSync(this.abortController.signal, options);
201
202
  // Return a promise that resolves when the connection status is updated
202
203
  return new Promise((resolve) => {
203
- const l = this.registerListener({
204
+ const disposer = this.registerListener({
204
205
  statusUpdated: (update) => {
205
206
  // This is triggered as soon as a connection is read from
206
207
  if (typeof update.connected == 'undefined') {
@@ -209,12 +210,14 @@ The next upload iteration will be delayed.`);
209
210
  }
210
211
  if (update.connected == false) {
211
212
  /**
212
- * This function does not reject if initial connect attempt failed
213
+ * This function does not reject if initial connect attempt failed.
214
+ * Connected can be false if the connection attempt was aborted or if the initial connection
215
+ * attempt failed.
213
216
  */
214
217
  this.logger.warn('Initial connect attempt did not successfully connect to server');
215
218
  }
219
+ disposer();
216
220
  resolve();
217
- l();
218
221
  }
219
222
  });
220
223
  });
@@ -270,7 +273,8 @@ The next upload iteration will be delayed.`);
270
273
  connected: false,
271
274
  connecting: false,
272
275
  dataFlow: {
273
- downloading: false
276
+ downloading: false,
277
+ downloadProgress: null
274
278
  }
275
279
  });
276
280
  });
@@ -355,6 +359,9 @@ The next upload iteration will be delayed.`);
355
359
  let validatedCheckpoint = null;
356
360
  let appliedCheckpoint = null;
357
361
  const clientId = await this.options.adapter.getClientId();
362
+ if (signal.aborted) {
363
+ return;
364
+ }
358
365
  this.logger.debug('Requesting stream from server');
359
366
  const syncOptions = {
360
367
  path: '/sync/stream',
@@ -409,6 +416,7 @@ The next upload iteration will be delayed.`);
409
416
  bucketMap = newBuckets;
410
417
  await this.options.adapter.removeBuckets([...bucketsToDelete]);
411
418
  await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
419
+ await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
412
420
  }
413
421
  else if (isStreamingSyncCheckpointComplete(line)) {
414
422
  const result = await this.applyCheckpoint(targetCheckpoint, signal);
@@ -472,6 +480,7 @@ The next upload iteration will be delayed.`);
472
480
  write_checkpoint: diff.write_checkpoint
473
481
  };
474
482
  targetCheckpoint = newCheckpoint;
483
+ await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
475
484
  bucketMap = new Map();
476
485
  newBuckets.forEach((checksum, name) => bucketMap.set(name, {
477
486
  name: checksum.bucket,
@@ -486,9 +495,22 @@ The next upload iteration will be delayed.`);
486
495
  }
487
496
  else if (isStreamingSyncData(line)) {
488
497
  const { data } = line;
498
+ const previousProgress = this.syncStatus.dataFlowStatus.downloadProgress;
499
+ let updatedProgress = null;
500
+ if (previousProgress) {
501
+ updatedProgress = { ...previousProgress };
502
+ const progressForBucket = updatedProgress[data.bucket];
503
+ if (progressForBucket) {
504
+ updatedProgress[data.bucket] = {
505
+ ...progressForBucket,
506
+ sinceLast: progressForBucket.sinceLast + data.data.length
507
+ };
508
+ }
509
+ }
489
510
  this.updateSyncStatus({
490
511
  dataFlow: {
491
- downloading: true
512
+ downloading: true,
513
+ downloadProgress: updatedProgress
492
514
  }
493
515
  });
494
516
  await this.options.adapter.saveSyncData({ buckets: [SyncDataBucket.fromRow(data)] });
@@ -498,6 +520,7 @@ The next upload iteration will be delayed.`);
498
520
  if (remaining_seconds == 0) {
499
521
  // Connection would be closed automatically right after this
500
522
  this.logger.debug('Token expiring; reconnect');
523
+ this.options.remote.invalidateCredentials();
501
524
  /**
502
525
  * For a rare case where the backend connector does not update the token
503
526
  * (uses the same one), this should have some delay.
@@ -505,6 +528,12 @@ The next upload iteration will be delayed.`);
505
528
  await this.delayRetry();
506
529
  return;
507
530
  }
531
+ else if (remaining_seconds < 30) {
532
+ this.logger.debug('Token will expire soon; reconnect');
533
+ // Pre-emptively refresh the token
534
+ this.options.remote.invalidateCredentials();
535
+ return;
536
+ }
508
537
  this.triggerCrudUpload();
509
538
  }
510
539
  else {
@@ -536,6 +565,27 @@ The next upload iteration will be delayed.`);
536
565
  }
537
566
  });
538
567
  }
568
+ async updateSyncStatusForStartingCheckpoint(checkpoint) {
569
+ const localProgress = await this.options.adapter.getBucketOperationProgress();
570
+ const progress = {};
571
+ for (const bucket of checkpoint.buckets) {
572
+ const savedProgress = localProgress[bucket.bucket];
573
+ progress[bucket.bucket] = {
574
+ // The fallback priority doesn't matter here, but 3 is the one newer versions of the sync service
575
+ // will use by default.
576
+ priority: bucket.priority ?? 3,
577
+ atLast: savedProgress?.atLast ?? 0,
578
+ sinceLast: savedProgress?.sinceLast ?? 0,
579
+ targetCount: bucket.count ?? 0
580
+ };
581
+ }
582
+ this.updateSyncStatus({
583
+ dataFlow: {
584
+ downloading: true,
585
+ downloadProgress: progress
586
+ }
587
+ });
588
+ }
539
589
  async applyCheckpoint(checkpoint, abort) {
540
590
  let result = await this.options.adapter.syncLocalDatabase(checkpoint);
541
591
  const pending = this.pendingCrudUpload;
@@ -565,6 +615,7 @@ The next upload iteration will be delayed.`);
565
615
  lastSyncedAt: new Date(),
566
616
  dataFlow: {
567
617
  downloading: false,
618
+ downloadProgress: null,
568
619
  downloadError: undefined
569
620
  }
570
621
  });
@@ -79,7 +79,7 @@ export interface StreamingSyncCheckpointDiff {
79
79
  last_op_id: OpId;
80
80
  updated_buckets: BucketChecksum[];
81
81
  removed_buckets: string[];
82
- write_checkpoint: string;
82
+ write_checkpoint?: string;
83
83
  };
84
84
  }
85
85
  export interface StreamingSyncDataJSON {
@@ -0,0 +1,72 @@
1
+ /** @internal */
2
+ export type InternalProgressInformation = Record<string, {
3
+ priority: number;
4
+ atLast: number;
5
+ sinceLast: number;
6
+ targetCount: number;
7
+ }>;
8
+ /**
9
+ * @internal The priority used by the core extension to indicate that a full sync was completed.
10
+ */
11
+ export declare const FULL_SYNC_PRIORITY = 2147483647;
12
+ /**
13
+ * Information about a progressing download made by the PowerSync SDK.
14
+ *
15
+ * To obtain these values, use {@link SyncProgress}, available through
16
+ * {@link SyncStatus#downloadProgress}.
17
+ */
18
+ export interface ProgressWithOperations {
19
+ /**
20
+ * The total amount of operations to download for the current sync iteration
21
+ * to complete.
22
+ */
23
+ totalOperations: number;
24
+ /**
25
+ * The amount of operations that have already been downloaded.
26
+ */
27
+ downloadedOperations: number;
28
+ /**
29
+ * Relative progress, as {@link downloadedOperations} of {@link totalOperations}.
30
+ *
31
+ * This will be a number between `0.0` and `1.0` (inclusive).
32
+ *
33
+ * When this number reaches `1.0`, all changes have been received from the sync service.
34
+ * Actually applying these changes happens before the `downloadProgress` field is cleared from
35
+ * {@link SyncStatus}, so progress can stay at `1.0` for a short while before completing.
36
+ */
37
+ downloadedFraction: number;
38
+ }
39
+ /**
40
+ * Provides realtime progress on how PowerSync is downloading rows.
41
+ *
42
+ * The progress until the next complete sync is available through the fields on {@link ProgressWithOperations},
43
+ * which this class implements.
44
+ * Additionally, the {@link SyncProgress.untilPriority} method can be used to otbain progress towards
45
+ * a specific priority (instead of the progress for the entire download).
46
+ *
47
+ * The reported progress always reflects the status towards the end of a sync iteration (after
48
+ * which a consistent snapshot of all buckets is available locally).
49
+ *
50
+ * In rare cases (in particular, when a [compacting](https://docs.powersync.com/usage/lifecycle-maintenance/compacting-buckets)
51
+ * operation takes place between syncs), it's possible for the returned numbers to be slightly
52
+ * inaccurate. For this reason, {@link SyncProgress} should be seen as an approximation of progress.
53
+ * The information returned is good enough to build progress bars, but not exact enough to track
54
+ * individual download counts.
55
+ *
56
+ * Also note that data is downloaded in bulk, which means that individual counters are unlikely
57
+ * to be updated one-by-one.
58
+ */
59
+ export declare class SyncProgress implements ProgressWithOperations {
60
+ protected internal: InternalProgressInformation;
61
+ totalOperations: number;
62
+ downloadedOperations: number;
63
+ downloadedFraction: number;
64
+ constructor(internal: InternalProgressInformation);
65
+ /**
66
+ * Returns download progress towards all data up until the specified priority being received.
67
+ *
68
+ * The returned {@link ProgressWithOperations} tracks the target amount of operations that need
69
+ * to be downloaded in total and how many of them have already been received.
70
+ */
71
+ untilPriority(priority: number): ProgressWithOperations;
72
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @internal The priority used by the core extension to indicate that a full sync was completed.
3
+ */
4
+ export const FULL_SYNC_PRIORITY = 2147483647;
5
+ /**
6
+ * Provides realtime progress on how PowerSync is downloading rows.
7
+ *
8
+ * The progress until the next complete sync is available through the fields on {@link ProgressWithOperations},
9
+ * which this class implements.
10
+ * Additionally, the {@link SyncProgress.untilPriority} method can be used to otbain progress towards
11
+ * a specific priority (instead of the progress for the entire download).
12
+ *
13
+ * The reported progress always reflects the status towards the end of a sync iteration (after
14
+ * which a consistent snapshot of all buckets is available locally).
15
+ *
16
+ * In rare cases (in particular, when a [compacting](https://docs.powersync.com/usage/lifecycle-maintenance/compacting-buckets)
17
+ * operation takes place between syncs), it's possible for the returned numbers to be slightly
18
+ * inaccurate. For this reason, {@link SyncProgress} should be seen as an approximation of progress.
19
+ * The information returned is good enough to build progress bars, but not exact enough to track
20
+ * individual download counts.
21
+ *
22
+ * Also note that data is downloaded in bulk, which means that individual counters are unlikely
23
+ * to be updated one-by-one.
24
+ */
25
+ export class SyncProgress {
26
+ internal;
27
+ totalOperations;
28
+ downloadedOperations;
29
+ downloadedFraction;
30
+ constructor(internal) {
31
+ this.internal = internal;
32
+ const untilCompletion = this.untilPriority(FULL_SYNC_PRIORITY);
33
+ this.totalOperations = untilCompletion.totalOperations;
34
+ this.downloadedOperations = untilCompletion.downloadedOperations;
35
+ this.downloadedFraction = untilCompletion.downloadedFraction;
36
+ }
37
+ /**
38
+ * Returns download progress towards all data up until the specified priority being received.
39
+ *
40
+ * The returned {@link ProgressWithOperations} tracks the target amount of operations that need
41
+ * to be downloaded in total and how many of them have already been received.
42
+ */
43
+ untilPriority(priority) {
44
+ let total = 0;
45
+ let downloaded = 0;
46
+ for (const progress of Object.values(this.internal)) {
47
+ // Include higher-priority buckets, which are represented by lower numbers.
48
+ if (progress.priority <= priority) {
49
+ downloaded += progress.sinceLast;
50
+ total += progress.targetCount - progress.atLast;
51
+ }
52
+ }
53
+ let progress = total == 0 ? 0.0 : downloaded / total;
54
+ return {
55
+ totalOperations: total,
56
+ downloadedOperations: downloaded,
57
+ downloadedFraction: progress
58
+ };
59
+ }
60
+ }
@@ -1,3 +1,4 @@
1
+ import { InternalProgressInformation, SyncProgress } from './SyncProgress.js';
1
2
  export type SyncDataFlowStatus = Partial<{
2
3
  downloading: boolean;
3
4
  uploading: boolean;
@@ -12,6 +13,12 @@ export type SyncDataFlowStatus = Partial<{
12
13
  * Cleared on the next successful upload.
13
14
  */
14
15
  uploadError?: Error;
16
+ /**
17
+ * Internal information about how far we are downloading operations in buckets.
18
+ *
19
+ * Please use the {@link SyncStatus#downloadProgress} property to track sync progress.
20
+ */
21
+ downloadProgress: InternalProgressInformation | null;
15
22
  }>;
16
23
  export interface SyncPriorityStatus {
17
24
  priority: number;
@@ -77,6 +84,12 @@ export declare class SyncStatus {
77
84
  * Cleared on the next successful upload.
78
85
  */
79
86
  uploadError?: Error;
87
+ /**
88
+ * Internal information about how far we are downloading operations in buckets.
89
+ *
90
+ * Please use the {@link SyncStatus#downloadProgress} property to track sync progress.
91
+ */
92
+ downloadProgress: InternalProgressInformation | null;
80
93
  }>;
81
94
  /**
82
95
  * Provides sync status information for all bucket priorities, sorted by priority (highest first).
@@ -85,6 +98,13 @@ export declare class SyncStatus {
85
98
  * sorted with highest priorities (lower numbers) first.
86
99
  */
87
100
  get priorityStatusEntries(): SyncPriorityStatus[];
101
+ /**
102
+ * A realtime progress report on how many operations have been downloaded and
103
+ * how many are necessary in total to complete the next sync iteration.
104
+ *
105
+ * This field is only set when {@link SyncDataFlowStatus#downloading} is also true.
106
+ */
107
+ get downloadProgress(): SyncProgress | null;
88
108
  /**
89
109
  * Reports the sync status (a pair of {@link SyncStatus#hasSynced} and {@link SyncStatus#lastSyncedAt} fields)
90
110
  * for a specific bucket priority level.
@@ -1,3 +1,4 @@
1
+ import { SyncProgress } from './SyncProgress.js';
1
2
  export class SyncStatus {
2
3
  options;
3
4
  constructor(options) {
@@ -67,6 +68,19 @@ export class SyncStatus {
67
68
  get priorityStatusEntries() {
68
69
  return (this.options.priorityStatusEntries ?? []).slice().sort(SyncStatus.comparePriorities);
69
70
  }
71
+ /**
72
+ * A realtime progress report on how many operations have been downloaded and
73
+ * how many are necessary in total to complete the next sync iteration.
74
+ *
75
+ * This field is only set when {@link SyncDataFlowStatus#downloading} is also true.
76
+ */
77
+ get downloadProgress() {
78
+ const internalProgress = this.options.dataFlow?.downloadProgress;
79
+ if (internalProgress == null) {
80
+ return null;
81
+ }
82
+ return new SyncProgress(internalProgress);
83
+ }
70
84
  /**
71
85
  * Reports the sync status (a pair of {@link SyncStatus#hasSynced} and {@link SyncStatus#lastSyncedAt} fields)
72
86
  * for a specific bucket priority level.
@@ -18,6 +18,10 @@ export declare class Schema<S extends SchemaType = SchemaType> {
18
18
  view_name: string;
19
19
  local_only: boolean;
20
20
  insert_only: boolean;
21
+ include_old: any;
22
+ include_old_only_when_changed: boolean;
23
+ include_metadata: boolean;
24
+ ignore_empty_update: boolean;
21
25
  columns: {
22
26
  name: string;
23
27
  type: import("./Column.js").ColumnType | undefined;
@@ -1,4 +1,3 @@
1
- import { Table } from './Table.js';
2
1
  /**
3
2
  * A schema is a collection of tables. It is used to define the structure of a database.
4
3
  */
@@ -41,15 +40,7 @@ export class Schema {
41
40
  }
42
41
  convertToClassicTables(props) {
43
42
  return Object.entries(props).map(([name, table]) => {
44
- const convertedTable = new Table({
45
- name,
46
- columns: table.columns,
47
- indexes: table.indexes,
48
- localOnly: table.localOnly,
49
- insertOnly: table.insertOnly,
50
- viewName: table.viewNameOverride || name
51
- });
52
- return convertedTable;
43
+ return table.copyWithName(name);
53
44
  });
54
45
  }
55
46
  }
@@ -1,16 +1,32 @@
1
1
  import { Column, ColumnsType, ColumnType, ExtractColumnValueType } from './Column.js';
2
2
  import { Index } from './Index.js';
3
3
  import { TableV2 } from './TableV2.js';
4
- export interface TableOptions {
4
+ interface SharedTableOptions {
5
+ localOnly?: boolean;
6
+ insertOnly?: boolean;
7
+ viewName?: string;
8
+ trackPrevious?: boolean | TrackPreviousOptions;
9
+ trackMetadata?: boolean;
10
+ ignoreEmptyUpdates?: boolean;
11
+ }
12
+ /** Whether to include previous column values when PowerSync tracks local changes.
13
+ *
14
+ * Including old values may be helpful for some backend connector implementations, which is
15
+ * why it can be enabled on per-table or per-columm basis.
16
+ */
17
+ export interface TrackPreviousOptions {
18
+ /** When defined, a list of column names for which old values should be tracked. */
19
+ columns?: string[];
20
+ /** When enabled, only include values that have actually been changed by an update. */
21
+ onlyWhenChanged?: boolean;
22
+ }
23
+ export interface TableOptions extends SharedTableOptions {
5
24
  /**
6
25
  * The synced table name, matching sync rules
7
26
  */
8
27
  name: string;
9
28
  columns: Column[];
10
29
  indexes?: Index[];
11
- localOnly?: boolean;
12
- insertOnly?: boolean;
13
- viewName?: string;
14
30
  }
15
31
  export type RowType<T extends TableV2<any>> = {
16
32
  [K in keyof T['columnMap']]: ExtractColumnValueType<T['columnMap'][K]>;
@@ -18,16 +34,16 @@ export type RowType<T extends TableV2<any>> = {
18
34
  id: string;
19
35
  };
20
36
  export type IndexShorthand = Record<string, string[]>;
21
- export interface TableV2Options {
37
+ export interface TableV2Options extends SharedTableOptions {
22
38
  indexes?: IndexShorthand;
23
- localOnly?: boolean;
24
- insertOnly?: boolean;
25
- viewName?: string;
26
39
  }
27
40
  export declare const DEFAULT_TABLE_OPTIONS: {
28
41
  indexes: never[];
29
42
  insertOnly: boolean;
30
43
  localOnly: boolean;
44
+ trackPrevious: boolean;
45
+ trackMetadata: boolean;
46
+ ignoreEmptyUpdates: boolean;
31
47
  };
32
48
  export declare const InvalidSQLCharacters: RegExp;
33
49
  export declare class Table<Columns extends ColumnsType = ColumnsType> {
@@ -96,9 +112,11 @@ export declare class Table<Columns extends ColumnsType = ColumnsType> {
96
112
  *```
97
113
  */
98
114
  constructor(options: TableOptions);
115
+ copyWithName(name: string): Table;
99
116
  private isTableV1;
100
117
  private initTableV1;
101
118
  private initTableV2;
119
+ private applyDefaultOptions;
102
120
  get name(): string;
103
121
  get viewNameOverride(): string | undefined;
104
122
  get viewName(): string;
@@ -107,6 +125,9 @@ export declare class Table<Columns extends ColumnsType = ColumnsType> {
107
125
  get indexes(): Index[];
108
126
  get localOnly(): boolean;
109
127
  get insertOnly(): boolean;
128
+ get trackPrevious(): boolean | TrackPreviousOptions;
129
+ get trackMetadata(): boolean;
130
+ get ignoreEmptyUpdates(): boolean;
110
131
  get internalName(): string;
111
132
  get validName(): boolean;
112
133
  validate(): void;
@@ -115,6 +136,10 @@ export declare class Table<Columns extends ColumnsType = ColumnsType> {
115
136
  view_name: string;
116
137
  local_only: boolean;
117
138
  insert_only: boolean;
139
+ include_old: any;
140
+ include_old_only_when_changed: boolean;
141
+ include_metadata: boolean;
142
+ ignore_empty_update: boolean;
118
143
  columns: {
119
144
  name: string;
120
145
  type: ColumnType | undefined;
@@ -129,3 +154,4 @@ export declare class Table<Columns extends ColumnsType = ColumnsType> {
129
154
  }[];
130
155
  };
131
156
  }
157
+ export {};
@@ -4,7 +4,10 @@ import { IndexedColumn } from './IndexedColumn.js';
4
4
  export const DEFAULT_TABLE_OPTIONS = {
5
5
  indexes: [],
6
6
  insertOnly: false,
7
- localOnly: false
7
+ localOnly: false,
8
+ trackPrevious: false,
9
+ trackMetadata: false,
10
+ ignoreEmptyUpdates: false
8
11
  };
9
12
  export const InvalidSQLCharacters = /["'%,.#\s[\]]/;
10
13
  export class Table {
@@ -41,16 +44,21 @@ export class Table {
41
44
  this.initTableV2(optionsOrColumns, v2Options);
42
45
  }
43
46
  }
47
+ copyWithName(name) {
48
+ return new Table({
49
+ ...this.options,
50
+ name
51
+ });
52
+ }
44
53
  isTableV1(arg) {
45
54
  return 'columns' in arg && Array.isArray(arg.columns);
46
55
  }
47
56
  initTableV1(options) {
48
57
  this.options = {
49
58
  ...options,
50
- indexes: options.indexes || [],
51
- insertOnly: options.insertOnly ?? DEFAULT_TABLE_OPTIONS.insertOnly,
52
- localOnly: options.localOnly ?? DEFAULT_TABLE_OPTIONS.localOnly
59
+ indexes: options.indexes || []
53
60
  };
61
+ this.applyDefaultOptions();
54
62
  }
55
63
  initTableV2(columns, options) {
56
64
  const convertedColumns = Object.entries(columns).map(([name, columnInfo]) => new Column({ name, type: columnInfo.type }));
@@ -65,12 +73,23 @@ export class Table {
65
73
  name: '',
66
74
  columns: convertedColumns,
67
75
  indexes: convertedIndexes,
68
- insertOnly: options?.insertOnly ?? DEFAULT_TABLE_OPTIONS.insertOnly,
69
- localOnly: options?.localOnly ?? DEFAULT_TABLE_OPTIONS.localOnly,
70
- viewName: options?.viewName
76
+ viewName: options?.viewName,
77
+ insertOnly: options?.insertOnly,
78
+ localOnly: options?.localOnly,
79
+ trackPrevious: options?.trackPrevious,
80
+ trackMetadata: options?.trackMetadata,
81
+ ignoreEmptyUpdates: options?.ignoreEmptyUpdates
71
82
  };
83
+ this.applyDefaultOptions();
72
84
  this._mappedColumns = columns;
73
85
  }
86
+ applyDefaultOptions() {
87
+ this.options.insertOnly ??= DEFAULT_TABLE_OPTIONS.insertOnly;
88
+ this.options.localOnly ??= DEFAULT_TABLE_OPTIONS.localOnly;
89
+ this.options.trackPrevious ??= DEFAULT_TABLE_OPTIONS.trackPrevious;
90
+ this.options.trackMetadata ??= DEFAULT_TABLE_OPTIONS.trackMetadata;
91
+ this.options.ignoreEmptyUpdates ??= DEFAULT_TABLE_OPTIONS.ignoreEmptyUpdates;
92
+ }
74
93
  get name() {
75
94
  return this.options.name;
76
95
  }
@@ -94,10 +113,19 @@ export class Table {
94
113
  return this.options.indexes ?? [];
95
114
  }
96
115
  get localOnly() {
97
- return this.options.localOnly ?? false;
116
+ return this.options.localOnly;
98
117
  }
99
118
  get insertOnly() {
100
- return this.options.insertOnly ?? false;
119
+ return this.options.insertOnly;
120
+ }
121
+ get trackPrevious() {
122
+ return this.options.trackPrevious;
123
+ }
124
+ get trackMetadata() {
125
+ return this.options.trackMetadata;
126
+ }
127
+ get ignoreEmptyUpdates() {
128
+ return this.options.ignoreEmptyUpdates;
101
129
  }
102
130
  get internalName() {
103
131
  if (this.options.localOnly) {
@@ -124,6 +152,12 @@ export class Table {
124
152
  if (this.columns.length > MAX_AMOUNT_OF_COLUMNS) {
125
153
  throw new Error(`Table has too many columns. The maximum number of columns is ${MAX_AMOUNT_OF_COLUMNS}.`);
126
154
  }
155
+ if (this.trackMetadata && this.localOnly) {
156
+ throw new Error(`Can't include metadata for local-only tables.`);
157
+ }
158
+ if (this.trackPrevious != false && this.localOnly) {
159
+ throw new Error(`Can't include old values for local-only tables.`);
160
+ }
127
161
  const columnNames = new Set();
128
162
  columnNames.add('id');
129
163
  for (const column of this.columns) {
@@ -156,11 +190,16 @@ export class Table {
156
190
  }
157
191
  }
158
192
  toJSON() {
193
+ const trackPrevious = this.trackPrevious;
159
194
  return {
160
195
  name: this.name,
161
196
  view_name: this.viewName,
162
197
  local_only: this.localOnly,
163
198
  insert_only: this.insertOnly,
199
+ include_old: trackPrevious && (trackPrevious.columns ?? true),
200
+ include_old_only_when_changed: typeof trackPrevious == 'object' && trackPrevious.onlyWhenChanged == true,
201
+ include_metadata: this.trackMetadata,
202
+ ignore_empty_update: this.ignoreEmptyUpdates,
164
203
  columns: this.columns.map((c) => c.toJSON()),
165
204
  indexes: this.indexes.map((e) => e.toJSON(this))
166
205
  };
package/lib/index.d.ts CHANGED
@@ -18,6 +18,7 @@ export * from './client/sync/stream/AbstractRemote.js';
18
18
  export * from './client/sync/stream/AbstractStreamingSyncImplementation.js';
19
19
  export * from './client/sync/stream/streaming-sync-types.js';
20
20
  export { MAX_OP_ID } from './client/constants.js';
21
+ export { ProgressWithOperations, SyncProgress } from './db/crud/SyncProgress.js';
21
22
  export * from './db/crud/SyncStatus.js';
22
23
  export * from './db/crud/UploadQueueStatus.js';
23
24
  export * from './db/schema/Schema.js';
package/lib/index.js CHANGED
@@ -18,6 +18,7 @@ export * from './client/sync/stream/AbstractRemote.js';
18
18
  export * from './client/sync/stream/AbstractStreamingSyncImplementation.js';
19
19
  export * from './client/sync/stream/streaming-sync-types.js';
20
20
  export { MAX_OP_ID } from './client/constants.js';
21
+ export { SyncProgress } from './db/crud/SyncProgress.js';
21
22
  export * from './db/crud/SyncStatus.js';
22
23
  export * from './db/crud/UploadQueueStatus.js';
23
24
  export * from './db/schema/Schema.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/common",
3
- "version": "0.0.0-dev-20250423120133",
3
+ "version": "0.0.0-dev-20250526133243",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"