@powersync/service-core 1.24.0 → 1.26.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/api/diagnostics.js +2 -0
  3. package/dist/api/diagnostics.js.map +1 -1
  4. package/dist/entry/commands/compact-action.js +28 -24
  5. package/dist/entry/commands/compact-action.js.map +1 -1
  6. package/dist/metrics/metrics-interfaces.d.ts +5 -1
  7. package/dist/metrics/metrics-interfaces.js.map +1 -1
  8. package/dist/metrics/open-telemetry/MetricAttributeFilteringExporter.d.ts +36 -0
  9. package/dist/metrics/open-telemetry/MetricAttributeFilteringExporter.js +70 -0
  10. package/dist/metrics/open-telemetry/MetricAttributeFilteringExporter.js.map +1 -0
  11. package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js +6 -1
  12. package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js.map +1 -1
  13. package/dist/metrics/open-telemetry/util.js +7 -3
  14. package/dist/metrics/open-telemetry/util.js.map +1 -1
  15. package/dist/replication/AbstractReplicator.js +16 -6
  16. package/dist/replication/AbstractReplicator.js.map +1 -1
  17. package/dist/routes/configure-fastify.d.ts +9 -0
  18. package/dist/routes/endpoints/admin.d.ts +18 -0
  19. package/dist/routes/endpoints/admin.js +3 -1
  20. package/dist/routes/endpoints/admin.js.map +1 -1
  21. package/dist/routes/endpoints/sync-rules.js +3 -1
  22. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  23. package/dist/storage/BucketStorageBatch.d.ts +2 -1
  24. package/dist/storage/BucketStorageBatch.js.map +1 -1
  25. package/dist/storage/BucketStorageFactory.d.ts +28 -4
  26. package/dist/storage/BucketStorageFactory.js +8 -4
  27. package/dist/storage/BucketStorageFactory.js.map +1 -1
  28. package/dist/storage/PersistedSyncConfigContent.d.ts +2 -0
  29. package/dist/storage/PersistedSyncConfigContent.js +23 -10
  30. package/dist/storage/PersistedSyncConfigContent.js.map +1 -1
  31. package/dist/storage/ReplicationEventPayload.d.ts +6 -1
  32. package/dist/storage/SourceEntity.d.ts +7 -2
  33. package/dist/storage/SourceTable.d.ts +37 -6
  34. package/dist/storage/SourceTable.js +42 -10
  35. package/dist/storage/SourceTable.js.map +1 -1
  36. package/dist/storage/SourceTableReconciler.d.ts +83 -0
  37. package/dist/storage/SourceTableReconciler.js +95 -0
  38. package/dist/storage/SourceTableReconciler.js.map +1 -0
  39. package/dist/storage/StorageVersionConfig.d.ts +4 -0
  40. package/dist/storage/StorageVersionConfig.js +10 -1
  41. package/dist/storage/StorageVersionConfig.js.map +1 -1
  42. package/dist/storage/SyncRulesBucketStorage.d.ts +45 -7
  43. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  44. package/dist/storage/WriteCheckpointAPI.d.ts +25 -3
  45. package/dist/storage/WriteCheckpointAPI.js.map +1 -1
  46. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +23 -7
  47. package/dist/storage/implementation/BucketDefinitionMapping.js +88 -14
  48. package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -1
  49. package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js +9 -1
  50. package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js.map +1 -1
  51. package/dist/storage/storage-index.d.ts +1 -0
  52. package/dist/storage/storage-index.js +1 -0
  53. package/dist/storage/storage-index.js.map +1 -1
  54. package/dist/storage/storage-metrics.js +120 -4
  55. package/dist/storage/storage-metrics.js.map +1 -1
  56. package/dist/sync/BucketChecksumState.d.ts +1 -1
  57. package/dist/sync/BucketChecksumState.js.map +1 -1
  58. package/dist/sync/sync.js +37 -9
  59. package/dist/sync/sync.js.map +1 -1
  60. package/dist/sync/util.js +10 -1
  61. package/dist/sync/util.js.map +1 -1
  62. package/dist/util/config/compound-config-collector.js +5 -0
  63. package/dist/util/config/compound-config-collector.js.map +1 -1
  64. package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js +1 -0
  65. package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js.map +1 -1
  66. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +1 -0
  67. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
  68. package/dist/util/config/sync-rules/sync-rules-provider.d.ts +2 -0
  69. package/dist/util/config/sync-rules/sync-rules-provider.js +3 -0
  70. package/dist/util/config/sync-rules/sync-rules-provider.js.map +1 -1
  71. package/dist/util/config/types.d.ts +1 -0
  72. package/dist/util/protocol-types.d.ts +4 -4
  73. package/dist/util/protocol-types.js +3 -2
  74. package/dist/util/protocol-types.js.map +1 -1
  75. package/dist/util/utils.d.ts +2 -1
  76. package/dist/util/utils.js +16 -1
  77. package/dist/util/utils.js.map +1 -1
  78. package/package.json +6 -5
  79. package/src/api/diagnostics.ts +2 -0
  80. package/src/entry/commands/compact-action.ts +32 -24
  81. package/src/metrics/metrics-interfaces.ts +6 -1
  82. package/src/metrics/open-telemetry/MetricAttributeFilteringExporter.ts +85 -0
  83. package/src/metrics/open-telemetry/OpenTelemetryMetricsFactory.ts +7 -2
  84. package/src/metrics/open-telemetry/util.ts +7 -3
  85. package/src/replication/AbstractReplicator.ts +22 -6
  86. package/src/routes/endpoints/admin.ts +3 -1
  87. package/src/routes/endpoints/sync-rules.ts +3 -1
  88. package/src/storage/BucketStorageBatch.ts +2 -1
  89. package/src/storage/BucketStorageFactory.ts +44 -8
  90. package/src/storage/PersistedSyncConfigContent.ts +26 -11
  91. package/src/storage/ReplicationEventPayload.ts +6 -1
  92. package/src/storage/SourceEntity.ts +6 -2
  93. package/src/storage/SourceTable.ts +68 -11
  94. package/src/storage/SourceTableReconciler.ts +188 -0
  95. package/src/storage/StorageVersionConfig.ts +11 -1
  96. package/src/storage/SyncRulesBucketStorage.ts +53 -7
  97. package/src/storage/WriteCheckpointAPI.ts +32 -3
  98. package/src/storage/implementation/BucketDefinitionMapping.ts +120 -17
  99. package/src/storage/implementation/IncrementalReprocessingSyncConfigLog.ts +22 -5
  100. package/src/storage/storage-index.ts +1 -0
  101. package/src/storage/storage-metrics.ts +154 -8
  102. package/src/sync/BucketChecksumState.ts +3 -1
  103. package/src/sync/sync.ts +40 -11
  104. package/src/sync/util.ts +21 -8
  105. package/src/util/config/compound-config-collector.ts +6 -0
  106. package/src/util/config/sync-rules/impl/base64-sync-rules-collector.ts +1 -0
  107. package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +1 -0
  108. package/src/util/config/sync-rules/sync-rules-provider.ts +5 -0
  109. package/src/util/config/types.ts +1 -0
  110. package/src/util/protocol-types.ts +3 -2
  111. package/src/util/utils.ts +15 -1
  112. package/test/src/AbstractReplicator.test.ts +30 -0
  113. package/test/src/MetricAttributeFilteringExporter.test.ts +96 -0
  114. package/test/src/PersistedSyncConfigContent.test.ts +152 -0
  115. package/test/src/config.test.ts +40 -0
  116. package/test/src/diagnostics.test.ts +25 -0
  117. package/test/src/routes/admin.test.ts +8 -2
  118. package/test/src/routes/stream.test.ts +123 -2
  119. package/test/src/source-table-reconciler.test.ts +244 -0
  120. package/test/src/storage/SourceTable.test.ts +4 -1
  121. package/test/src/storage-metrics.test.ts +148 -0
  122. package/test/src/sync/util.test.ts +15 -0
  123. package/test/src/util/utils.test.ts +23 -0
  124. package/tsconfig.tsbuildinfo +1 -1
@@ -2,19 +2,30 @@ import {
2
2
  BucketDataSource,
3
3
  BucketDefinitionId,
4
4
  DEFAULT_TAG,
5
+ EventDefinitionId,
5
6
  ParameterIndexId,
6
7
  ParameterIndexLookupCreator,
7
8
  SourceTableRef
8
9
  } from '@powersync/service-sync-rules';
9
10
  import { bson } from '../index.js';
10
11
  import * as util from '../util/util-index.js';
11
- import { ColumnDescriptor } from './SourceEntity.js';
12
+ import { ColumnDescriptor, JsonValue } from './SourceEntity.js';
12
13
 
13
14
  /**
14
15
  * Format of the id depends on the bucket storage module. It should be consistent within the module.
15
16
  */
16
17
  export type SourceTableId = string | bson.ObjectId;
17
18
 
19
+ /**
20
+ * Compare source-table ids without coercing between storage-specific types.
21
+ */
22
+ export function sourceTableIdEquals(left: SourceTableId, right: SourceTableId): boolean {
23
+ if (typeof left === 'string' || typeof right === 'string') {
24
+ return typeof left === 'string' && typeof right === 'string' && left === right;
25
+ }
26
+ return left.equals(right);
27
+ }
28
+
18
29
  export interface SourceTableOptions {
19
30
  id: SourceTableId;
20
31
  ref: SourceTableRef;
@@ -25,6 +36,17 @@ export interface SourceTableOptions {
25
36
  parameterLookupSources: ParameterIndexLookupCreator[];
26
37
  bucketDataSourceIds?: Set<BucketDefinitionId>;
27
38
  parameterLookupSourceIds?: Set<ParameterIndexId>;
39
+ /**
40
+ * Compiled event definitions assigned to this persisted source-table record.
41
+ *
42
+ * Undefined is the legacy/non-incremental representation where event selection is
43
+ * based on the table ref. V3 incremental storage always supplies this set.
44
+ */
45
+ eventDefinitionIds?: Set<EventDefinitionId>;
46
+ /**
47
+ * Source-specific metadata. Null when no metadata has been recorded.
48
+ */
49
+ sourceMetadata?: JsonValue;
28
50
  }
29
51
 
30
52
  export interface TableSnapshotStatus {
@@ -61,11 +83,11 @@ export class SourceTable {
61
83
  public syncParameters = true;
62
84
 
63
85
  /**
64
- * True if this table should fire events for row changes.
86
+ * True if this table should evaluate event definitions for row changes.
65
87
  *
66
- * This value is resolved externally, and cached here. When multiple SourceTables exist
67
- * for the same SourceTableRef (v3 storage), resolveTables designates exactly one of them
68
- * as the event carrier, so that a row change saved once per table fires each event once.
88
+ * This value is resolved externally, and cached here. V3 storage assigns disjoint
89
+ * event-definition ids to SourceTables for the same physical table. Multiple records
90
+ * may evaluate different events, but each event id is evaluated through at most one record.
69
91
  *
70
92
  * Defaults to true for tests.
71
93
  */
@@ -136,6 +158,14 @@ export class SourceTable {
136
158
  return this.options.parameterLookupSourceIds;
137
159
  }
138
160
 
161
+ get eventDefinitionIds() {
162
+ return this.options.eventDefinitionIds;
163
+ }
164
+
165
+ get sourceMetadata() {
166
+ return this.options.sourceMetadata ?? null;
167
+ }
168
+
139
169
  /**
140
170
  * Sanitized name of the entity in the format of "{schema}.{entity name}".
141
171
  * Suitable for safe use in Postgres queries.
@@ -152,23 +182,42 @@ export class SourceTable {
152
182
  * In-memory clone of the table status.
153
183
  */
154
184
  clone() {
185
+ return this.copyWithSourceMetadata(this.sourceMetadata);
186
+ }
187
+
188
+ /**
189
+ * Copy this table with different source metadata, preserving its resolved state.
190
+ */
191
+ withSourceMetadata(sourceMetadata: JsonValue) {
192
+ return this.copyWithSourceMetadata(sourceMetadata);
193
+ }
194
+
195
+ private copyWithSourceMetadata(sourceMetadata: JsonValue) {
155
196
  const copy = new SourceTable({
156
197
  id: this.id,
157
- ref: this.options.ref,
198
+ ref: { ...this.options.ref },
158
199
  objectId: this.objectId,
159
- replicaIdColumns: this.replicaIdColumns,
200
+ replicaIdColumns: this.replicaIdColumns.map((column) => ({ ...column })),
160
201
  snapshotComplete: this.snapshotComplete,
161
- bucketDataSources: this.bucketDataSources,
162
- parameterLookupSources: this.parameterLookupSources,
202
+ bucketDataSources: [...this.bucketDataSources],
203
+ parameterLookupSources: [...this.parameterLookupSources],
163
204
  bucketDataSourceIds: this.bucketDataSourceIds == null ? undefined : new Set(this.bucketDataSourceIds),
164
205
  parameterLookupSourceIds:
165
- this.parameterLookupSourceIds == null ? undefined : new Set(this.parameterLookupSourceIds)
206
+ this.parameterLookupSourceIds == null ? undefined : new Set(this.parameterLookupSourceIds),
207
+ eventDefinitionIds: this.eventDefinitionIds == null ? undefined : new Set(this.eventDefinitionIds),
208
+ sourceMetadata: structuredClone(sourceMetadata)
166
209
  });
167
210
  copy.syncData = this.syncData;
168
211
  copy.syncParameters = this.syncParameters;
169
212
  copy.syncEvent = this.syncEvent;
170
213
  copy.storeCurrentData = this.storeCurrentData;
171
- copy.snapshotStatus = this.snapshotStatus;
214
+ copy.snapshotStatus =
215
+ this.snapshotStatus == null
216
+ ? undefined
217
+ : {
218
+ ...this.snapshotStatus,
219
+ lastKey: this.snapshotStatus.lastKey?.slice() ?? null
220
+ };
172
221
  return copy;
173
222
  }
174
223
 
@@ -183,3 +232,11 @@ export class SourceTable {
183
232
  }
184
233
  }
185
234
  }
235
+
236
+ /**
237
+ * A cloned SourceTable exposed to reconciliation with public fields typed as read-only.
238
+ * `options` is omitted so callers cannot mutate the underlying option bag without an explicit cast.
239
+ */
240
+ export type SourceTableCandidate = Omit<Readonly<SourceTable>, 'options' | 'withSourceMetadata'> & {
241
+ withSourceMetadata(sourceMetadata: JsonValue): SourceTableCandidate;
242
+ };
@@ -0,0 +1,188 @@
1
+ import { ServiceAssertionError } from '@powersync/lib-services-framework';
2
+ import { isDeepStrictEqual } from 'node:util';
3
+ import { JsonValue, SourceEntityDescriptor } from './SourceEntity.js';
4
+ import { SourceTable, SourceTableCandidate, SourceTableId, sourceTableIdEquals } from './SourceTable.js';
5
+
6
+ /**
7
+ * A source connector's classification of overlapping persisted tables.
8
+ */
9
+ export interface SourceTableCandidateResolution {
10
+ /**
11
+ * Records storage can reuse. Copies may include updated source metadata.
12
+ */
13
+ compatibleTables: ReadonlyArray<SourceTableCandidate>;
14
+
15
+ /**
16
+ * Records that cannot be reused. Every candidate must appear in exactly one result list.
17
+ */
18
+ incompatibleTables: ReadonlyArray<SourceTableCandidate>;
19
+
20
+ /**
21
+ * Values for records storage creates during this resolution.
22
+ */
23
+ newTableValues: SourceTableCreateValues;
24
+ }
25
+
26
+ export interface SourceTableCreateValues {
27
+ /**
28
+ * Source metadata for new records. Null means no metadata.
29
+ */
30
+ sourceMetadata: JsonValue;
31
+ }
32
+
33
+ /**
34
+ * Input to a source-owned reconciliation callback. The callback may run inside a storage
35
+ * transaction, so it must not mutate storage or perform slow external work.
36
+ */
37
+ export interface SourceTableCandidateReconcilerInput {
38
+ /**
39
+ * Source entity being resolved.
40
+ */
41
+ source: SourceEntityDescriptor;
42
+
43
+ /**
44
+ * Persisted tables overlapping by name or object id.
45
+ */
46
+ candidates: ReadonlyArray<SourceTableCandidate>;
47
+ }
48
+
49
+ export type SourceTableCandidateReconciler = (
50
+ input: SourceTableCandidateReconcilerInput
51
+ ) => SourceTableCandidateResolution | Promise<SourceTableCandidateResolution>;
52
+
53
+ /**
54
+ * Compare replica-id columns in order.
55
+ */
56
+ export function sameReplicaIdColumns(
57
+ left: SourceTableCandidate['replicaIdColumns'],
58
+ right: SourceEntityDescriptor
59
+ ): boolean {
60
+ const target = right.replicaIdColumns;
61
+ return (
62
+ left.length == target.length &&
63
+ left.every(
64
+ (column, index) =>
65
+ column.name == target[index].name && column.type == target[index].type && column.typeId == target[index].typeId
66
+ )
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Compare the shared source-table identity fields.
72
+ */
73
+ export function sourceIdentityCompatible(source: SourceEntityDescriptor, candidate: SourceTableCandidate): boolean {
74
+ return (
75
+ candidate.schema == source.schema &&
76
+ candidate.name == source.name &&
77
+ (source.objectId == null || candidate.objectId == source.objectId) &&
78
+ sameReplicaIdColumns(candidate.replicaIdColumns, source)
79
+ );
80
+ }
81
+
82
+ /**
83
+ * Default identity-based reconciliation for connectors without source-specific metadata.
84
+ */
85
+ export const defaultSourceTableReconciler: SourceTableCandidateReconciler = ({ source, candidates }) => {
86
+ const compatibleTables: SourceTableCandidate[] = [];
87
+ const incompatibleTables: SourceTableCandidate[] = [];
88
+ for (const candidate of candidates) {
89
+ if (sourceIdentityCompatible(source, candidate)) {
90
+ compatibleTables.push(candidate);
91
+ } else {
92
+ incompatibleTables.push(candidate);
93
+ }
94
+ }
95
+ return { compatibleTables, incompatibleTables, newTableValues: { sourceMetadata: null } };
96
+ };
97
+
98
+ /**
99
+ * Check that every candidate was classified exactly once.
100
+ */
101
+ export function validateSourceTableCandidateResolution(
102
+ candidates: ReadonlyArray<SourceTableCandidate>,
103
+ resolution: SourceTableCandidateResolution
104
+ ): void {
105
+ const classifiedTables = [...resolution.compatibleTables, ...resolution.incompatibleTables];
106
+
107
+ for (const candidate of candidates) {
108
+ const classifications = classifiedTables.filter((table) => sourceTableIdEquals(table.id, candidate.id));
109
+ if (classifications.length !== 1) {
110
+ throw new ServiceAssertionError(
111
+ `Source table candidate ${candidate.id.toString()} must be classified exactly once, got ${classifications.length}`
112
+ );
113
+ }
114
+ }
115
+
116
+ for (const table of classifiedTables) {
117
+ if (!candidates.some((candidate) => sourceTableIdEquals(candidate.id, table.id))) {
118
+ throw new ServiceAssertionError(`Source table reconciliation returned unknown candidate ${table.id.toString()}`);
119
+ }
120
+ }
121
+ }
122
+
123
+ /**
124
+ * A source-metadata update to persist.
125
+ */
126
+ export interface SourceTableMetadataUpdate {
127
+ id: SourceTableId;
128
+ sourceMetadata: JsonValue;
129
+ }
130
+
131
+ /**
132
+ * Rebuild a resolution from storage-owned tables, applying only reconciler-owned metadata to
133
+ * compatible tables. All other mutable table state comes from storage.
134
+ *
135
+ * Reconciler candidates are typed as read-only, but TypeScript types provide no runtime protection:
136
+ * callback code can cast a cloned candidate and mutate it. Rematerializing by id ensures those
137
+ * mutations are not trusted even when the compile-time boundary is bypassed.
138
+ */
139
+ export function materializeSourceTableResolution(
140
+ tables: ReadonlyArray<SourceTable>,
141
+ resolution: SourceTableCandidateResolution
142
+ ): MaterializedSourceTableResolution {
143
+ const findTable = (candidate: SourceTableCandidate): SourceTable => {
144
+ const table = tables.find((table) => sourceTableIdEquals(table.id, candidate.id));
145
+ if (table == null) {
146
+ throw new ServiceAssertionError(`Source table candidate ${candidate.id.toString()} was not persisted`);
147
+ }
148
+ return table;
149
+ };
150
+ return {
151
+ compatibleTables: resolution.compatibleTables.map((candidate) =>
152
+ findTable(candidate).withSourceMetadata(candidate.sourceMetadata)
153
+ ),
154
+ incompatibleTables: resolution.incompatibleTables.map(findTable),
155
+ newTableValues: resolution.newTableValues
156
+ };
157
+ }
158
+
159
+ export interface MaterializedSourceTableResolution {
160
+ compatibleTables: SourceTable[];
161
+ incompatibleTables: SourceTable[];
162
+ newTableValues: SourceTableCreateValues;
163
+ }
164
+
165
+ /**
166
+ * Return source-metadata changes from compatible candidates, comparing metadata by value against
167
+ * the original storage-owned tables. The reconciler may mutate its isolated candidate clones, so
168
+ * those clones cannot be used as the persisted baseline.
169
+ */
170
+ export function diffSourceTableUpdates(
171
+ persistedTables: ReadonlyArray<SourceTable>,
172
+ resolution: SourceTableCandidateResolution
173
+ ): SourceTableMetadataUpdate[] {
174
+ const updates: SourceTableMetadataUpdate[] = [];
175
+ for (const resolvedTable of resolution.compatibleTables) {
176
+ const persistedTable = persistedTables.find((table) => sourceTableIdEquals(table.id, resolvedTable.id));
177
+ if (persistedTable == null) {
178
+ throw new ServiceAssertionError(
179
+ `Source table reconciliation returned unknown candidate ${resolvedTable.id.toString()}`
180
+ );
181
+ }
182
+ if (isDeepStrictEqual(persistedTable.sourceMetadata, resolvedTable.sourceMetadata)) {
183
+ continue;
184
+ }
185
+ updates.push({ id: resolvedTable.id, sourceMetadata: resolvedTable.sourceMetadata });
186
+ }
187
+ return updates;
188
+ }
@@ -39,6 +39,11 @@ export const STORAGE_VERSION_2 = 2;
39
39
  */
40
40
  export const STORAGE_VERSION_3 = 3;
41
41
 
42
+ /**
43
+ * Same as version 3, but marked as stable.
44
+ */
45
+ export const STORAGE_VERSION_4 = 4;
46
+
42
47
  /**
43
48
  * Oldest supported storage version.
44
49
  */
@@ -54,7 +59,7 @@ export const CURRENT_STORAGE_VERSION = STORAGE_VERSION_2;
54
59
  *
55
60
  * This includes unstable versions.
56
61
  */
57
- export const SUPPORTED_STORAGE_VERSIONS = [STORAGE_VERSION_1, STORAGE_VERSION_2, STORAGE_VERSION_3];
62
+ export const SUPPORTED_STORAGE_VERSIONS = [STORAGE_VERSION_1, STORAGE_VERSION_2, STORAGE_VERSION_3, STORAGE_VERSION_4];
58
63
 
59
64
  /**
60
65
  * Shared storage-version behavior across storage implementations.
@@ -74,5 +79,10 @@ export const STORAGE_VERSION_CONFIG: Record<number, StorageVersionConfig | undef
74
79
  version: STORAGE_VERSION_3,
75
80
  versionedBuckets: true,
76
81
  softDeleteCurrentData: true
82
+ },
83
+ [STORAGE_VERSION_4]: {
84
+ version: STORAGE_VERSION_4,
85
+ versionedBuckets: true,
86
+ softDeleteCurrentData: true
77
87
  }
78
88
  };
@@ -14,6 +14,7 @@ import { ParsedSyncConfigSet } from './ParsedSyncConfigSet.js';
14
14
  import { ParseSyncConfigOptions } from './PersistedSyncConfigContent.js';
15
15
  import { SourceEntityDescriptor } from './SourceEntity.js';
16
16
  import { SourceTable } from './SourceTable.js';
17
+ import { SourceTableCandidateReconciler } from './SourceTableReconciler.js';
17
18
  import { StorageVersionConfig } from './StorageVersionConfig.js';
18
19
  import { SyncStorageWriteCheckpointAPI } from './WriteCheckpointAPI.js';
19
20
 
@@ -96,12 +97,20 @@ export interface SyncRulesBucketStorage
96
97
  */
97
98
  reportError(e: any): Promise<void>;
98
99
 
100
+ /**
101
+ * Whether parameter compaction can run when {@link CompactOptions.incrementalOnly} is set.
102
+ *
103
+ * Storage implementations that do not support incremental parameter compaction must return
104
+ * false; the compact command will skip parameter compaction in that mode.
105
+ */
106
+ supportsIncrementalParameterCompaction(): boolean;
107
+
99
108
  compact(options?: CompactOptions): Promise<void>;
100
109
 
101
110
  /**
102
- * Lightweight "compact" process to populate the checksum cache, if any.
111
+ * Compact storage after initial replication, before the first checkpoint exists.
103
112
  */
104
- populatePersistentChecksumCache(options: PopulateChecksumCacheOptions): Promise<PopulateChecksumCacheResults>;
113
+ compactInitialReplication(options: CompactInitialReplicationOptions): Promise<CompactInitialReplicationResults>;
105
114
 
106
115
  // ## Read operations
107
116
 
@@ -212,6 +221,11 @@ export interface ResolveTablesOptions {
212
221
  * Source table or collection metadata discovered during snapshot or streaming.
213
222
  */
214
223
  source: SourceEntityDescriptor;
224
+ /**
225
+ * Classifies overlapping persisted tables. Defaults to identity-based reconciliation.
226
+ * This may run inside a storage transaction and must not mutate storage.
227
+ */
228
+ reconcileSourceTables?: SourceTableCandidateReconciler;
215
229
  /**
216
230
  * For tests only - custom id generator for stable ids.
217
231
  */
@@ -271,6 +285,14 @@ export interface CreateWriterOptions extends ParseSyncConfigOptions {
271
285
  tracer?: PerformanceTracer<'storage' | 'evaluate'>;
272
286
 
273
287
  logger?: Logger;
288
+
289
+ /**
290
+ * Aborts long-running storage work started by this writer, such as uploads to object storage.
291
+ *
292
+ * This does not replace flushing or committing at the appropriate source boundary: it only
293
+ * cancels work that is still in flight when replication stops.
294
+ */
295
+ signal?: AbortSignal;
274
296
  }
275
297
 
276
298
  export interface StorageHooks {
@@ -315,6 +337,15 @@ export interface CompactOptions {
315
337
 
316
338
  compactParameterData?: boolean;
317
339
 
340
+ /**
341
+ * Only perform compaction that can be done incrementally.
342
+ *
343
+ * This includes full bucket compaction on MongoDB V3 storage.
344
+ *
345
+ * On MongoDB v1 and Postgres storage, this makes compacting a no-op.
346
+ */
347
+ incrementalOnly?: boolean;
348
+
318
349
  /**
319
350
  * Delete client-requested write checkpoints created before this time.
320
351
  *
@@ -348,6 +379,18 @@ export interface CompactOptions {
348
379
  */
349
380
  minChangeRatio?: number;
350
381
 
382
+ /** Minimum delay before a V3 bucket is checked for chunk compaction. Default: five minutes. */
383
+ minCompactChunkIntervalMs?: number;
384
+
385
+ /** Minimum elapsed write pressure before the v3 sliding-scale full compact. Default: two hours. */
386
+ minCompactFullIntervalMs?: number;
387
+
388
+ /** Maximum age of writes not covered by a v3 full compact. */
389
+ maxCompactFullIntervalMs?: number;
390
+
391
+ /** How long a v3 worker owns a claimed bucket before another worker may recover it. */
392
+ compactLeaseDurationMs?: number;
393
+
351
394
  /**
352
395
  * Internal/testing use: Cache size for compacting parameters.
353
396
  */
@@ -358,12 +401,12 @@ export interface CompactOptions {
358
401
  logger?: Logger;
359
402
  }
360
403
 
361
- export interface PopulateChecksumCacheOptions {
404
+ export interface CompactInitialReplicationOptions {
362
405
  /**
363
- * Compute checksums up to this op id.
406
+ * Compact data up to this op id.
364
407
  *
365
408
  * Defaults to the highest persisted op id for the replication stream, which covers
366
- * the common case of populating the cache right after initial replication, before
409
+ * the common case of compacting right after initial replication, before
367
410
  * the first checkpoint exists.
368
411
  */
369
412
  maxOpId?: util.InternalOpId;
@@ -371,9 +414,9 @@ export interface PopulateChecksumCacheOptions {
371
414
  signal?: AbortSignal;
372
415
  }
373
416
 
374
- export interface PopulateChecksumCacheResults {
417
+ export interface CompactInitialReplicationResults {
375
418
  /**
376
- * Number of buckets we have calculated checksums for.
419
+ * Number of buckets processed.
377
420
  */
378
421
  buckets: number;
379
422
  }
@@ -413,6 +456,9 @@ export interface BucketDataBatchOptions {
413
456
  /** Abort any in-progress work for this batch, including object-storage downloads. */
414
457
  signal?: AbortSignal;
415
458
 
459
+ /** Traces data reads performed by the storage implementation. */
460
+ tracer?: PerformanceTracer<string>;
461
+
416
462
  /** Limit number of documents returned. Defaults to 1000. */
417
463
  limit?: number;
418
464
 
@@ -1,3 +1,5 @@
1
+ import { EventDefinitionId } from '@powersync/service-sync-rules';
2
+
1
3
  export enum WriteCheckpointMode {
2
4
  /**
3
5
  * Raw mappings of `user_id` to `write_checkpoint`s should
@@ -31,7 +33,9 @@ export interface ClientRequestedCheckpointOptions {
31
33
  checkpoint_request_id?: bigint;
32
34
  }
33
35
 
34
- export interface CustomWriteCheckpointFilters extends BaseWriteCheckpointIdentifier {
36
+ export type SyncStorageCustomWriteCheckpointFilters = BaseWriteCheckpointIdentifier;
37
+
38
+ export interface CustomWriteCheckpointFilters extends SyncStorageCustomWriteCheckpointFilters {
35
39
  /**
36
40
  * Replication stream which was active when this checkpoint was created.
37
41
  */
@@ -44,6 +48,14 @@ export interface BatchedCustomWriteCheckpointOptions extends BaseWriteCheckpoint
44
48
  * "write checkpoint" in storage APIs for backwards compatibility.
45
49
  */
46
50
  checkpoint: bigint;
51
+ /**
52
+ * Compiled event definition which created the checkpoint.
53
+ *
54
+ * Incremental replication uses this to keep checkpoint records produced by
55
+ * active and processing sync configs separate. This is required by v3
56
+ * storage and unused by legacy storage versions.
57
+ */
58
+ event_id?: EventDefinitionId;
47
59
  /**
48
60
  * Required when this custom checkpoint was created from a client checkpoint
49
61
  * request and should be eligible for checkpoint request retention cleanup.
@@ -155,12 +167,29 @@ function shouldReplaceManagedWriteCheckpoint(
155
167
  return candidateRequestId > existingRequestId;
156
168
  }
157
169
 
158
- export type SyncStorageLastWriteCheckpointFilters = BaseWriteCheckpointIdentifier | ManagedWriteCheckpointFilters;
170
+ export type SyncStorageLastWriteCheckpointFilters =
171
+ | SyncStorageCustomWriteCheckpointFilters
172
+ | ManagedWriteCheckpointFilters;
159
173
  export type LastWriteCheckpointFilters = CustomWriteCheckpointFilters | ManagedWriteCheckpointFilters;
160
174
 
175
+ /** Configures checkpoint ownership and generation for a storage instance. */
176
+ export type WriteCheckpointModeConfig =
177
+ | {
178
+ mode: WriteCheckpointMode.CUSTOM;
179
+ /**
180
+ * Name of the event whose custom checkpoints are served. Storage resolves this name through the active sync
181
+ * config's persisted mapping; integrations must not construct or persist event ids themselves. Required by
182
+ * storage implementations that scope custom checkpoints by event definition.
183
+ */
184
+ eventName?: string;
185
+ }
186
+ | {
187
+ mode: WriteCheckpointMode.MANAGED;
188
+ };
189
+
161
190
  export interface BaseWriteCheckpointAPI {
162
191
  readonly writeCheckpointMode: WriteCheckpointMode;
163
- setWriteCheckpointMode(mode: WriteCheckpointMode): void;
192
+ setWriteCheckpointMode(config: WriteCheckpointModeConfig): void;
164
193
  createManagedWriteCheckpoints(
165
194
  checkpoints: ManagedWriteCheckpointOptions[]
166
195
  ): Promise<CreateManagedWriteCheckpointsResult>;