@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.
- package/CHANGELOG.md +59 -0
- package/dist/api/diagnostics.js +2 -0
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/entry/commands/compact-action.js +28 -24
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/metrics/metrics-interfaces.d.ts +5 -1
- package/dist/metrics/metrics-interfaces.js.map +1 -1
- package/dist/metrics/open-telemetry/MetricAttributeFilteringExporter.d.ts +36 -0
- package/dist/metrics/open-telemetry/MetricAttributeFilteringExporter.js +70 -0
- package/dist/metrics/open-telemetry/MetricAttributeFilteringExporter.js.map +1 -0
- package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js +6 -1
- package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js.map +1 -1
- package/dist/metrics/open-telemetry/util.js +7 -3
- package/dist/metrics/open-telemetry/util.js.map +1 -1
- package/dist/replication/AbstractReplicator.js +16 -6
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +9 -0
- package/dist/routes/endpoints/admin.d.ts +18 -0
- package/dist/routes/endpoints/admin.js +3 -1
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js +3 -1
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/storage/BucketStorageBatch.d.ts +2 -1
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/BucketStorageFactory.d.ts +28 -4
- package/dist/storage/BucketStorageFactory.js +8 -4
- package/dist/storage/BucketStorageFactory.js.map +1 -1
- package/dist/storage/PersistedSyncConfigContent.d.ts +2 -0
- package/dist/storage/PersistedSyncConfigContent.js +23 -10
- package/dist/storage/PersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/ReplicationEventPayload.d.ts +6 -1
- package/dist/storage/SourceEntity.d.ts +7 -2
- package/dist/storage/SourceTable.d.ts +37 -6
- package/dist/storage/SourceTable.js +42 -10
- package/dist/storage/SourceTable.js.map +1 -1
- package/dist/storage/SourceTableReconciler.d.ts +83 -0
- package/dist/storage/SourceTableReconciler.js +95 -0
- package/dist/storage/SourceTableReconciler.js.map +1 -0
- package/dist/storage/StorageVersionConfig.d.ts +4 -0
- package/dist/storage/StorageVersionConfig.js +10 -1
- package/dist/storage/StorageVersionConfig.js.map +1 -1
- package/dist/storage/SyncRulesBucketStorage.d.ts +45 -7
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/storage/WriteCheckpointAPI.d.ts +25 -3
- package/dist/storage/WriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +23 -7
- package/dist/storage/implementation/BucketDefinitionMapping.js +88 -14
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -1
- package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js +9 -1
- package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js.map +1 -1
- package/dist/storage/storage-index.d.ts +1 -0
- package/dist/storage/storage-index.js +1 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/storage/storage-metrics.js +120 -4
- package/dist/storage/storage-metrics.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +1 -1
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/sync.js +37 -9
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/util.js +10 -1
- package/dist/sync/util.js.map +1 -1
- package/dist/util/config/compound-config-collector.js +5 -0
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js +1 -0
- package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +1 -0
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
- package/dist/util/config/sync-rules/sync-rules-provider.d.ts +2 -0
- package/dist/util/config/sync-rules/sync-rules-provider.js +3 -0
- package/dist/util/config/sync-rules/sync-rules-provider.js.map +1 -1
- package/dist/util/config/types.d.ts +1 -0
- package/dist/util/protocol-types.d.ts +4 -4
- package/dist/util/protocol-types.js +3 -2
- package/dist/util/protocol-types.js.map +1 -1
- package/dist/util/utils.d.ts +2 -1
- package/dist/util/utils.js +16 -1
- package/dist/util/utils.js.map +1 -1
- package/package.json +6 -5
- package/src/api/diagnostics.ts +2 -0
- package/src/entry/commands/compact-action.ts +32 -24
- package/src/metrics/metrics-interfaces.ts +6 -1
- package/src/metrics/open-telemetry/MetricAttributeFilteringExporter.ts +85 -0
- package/src/metrics/open-telemetry/OpenTelemetryMetricsFactory.ts +7 -2
- package/src/metrics/open-telemetry/util.ts +7 -3
- package/src/replication/AbstractReplicator.ts +22 -6
- package/src/routes/endpoints/admin.ts +3 -1
- package/src/routes/endpoints/sync-rules.ts +3 -1
- package/src/storage/BucketStorageBatch.ts +2 -1
- package/src/storage/BucketStorageFactory.ts +44 -8
- package/src/storage/PersistedSyncConfigContent.ts +26 -11
- package/src/storage/ReplicationEventPayload.ts +6 -1
- package/src/storage/SourceEntity.ts +6 -2
- package/src/storage/SourceTable.ts +68 -11
- package/src/storage/SourceTableReconciler.ts +188 -0
- package/src/storage/StorageVersionConfig.ts +11 -1
- package/src/storage/SyncRulesBucketStorage.ts +53 -7
- package/src/storage/WriteCheckpointAPI.ts +32 -3
- package/src/storage/implementation/BucketDefinitionMapping.ts +120 -17
- package/src/storage/implementation/IncrementalReprocessingSyncConfigLog.ts +22 -5
- package/src/storage/storage-index.ts +1 -0
- package/src/storage/storage-metrics.ts +154 -8
- package/src/sync/BucketChecksumState.ts +3 -1
- package/src/sync/sync.ts +40 -11
- package/src/sync/util.ts +21 -8
- package/src/util/config/compound-config-collector.ts +6 -0
- package/src/util/config/sync-rules/impl/base64-sync-rules-collector.ts +1 -0
- package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +1 -0
- package/src/util/config/sync-rules/sync-rules-provider.ts +5 -0
- package/src/util/config/types.ts +1 -0
- package/src/util/protocol-types.ts +3 -2
- package/src/util/utils.ts +15 -1
- package/test/src/AbstractReplicator.test.ts +30 -0
- package/test/src/MetricAttributeFilteringExporter.test.ts +96 -0
- package/test/src/PersistedSyncConfigContent.test.ts +152 -0
- package/test/src/config.test.ts +40 -0
- package/test/src/diagnostics.test.ts +25 -0
- package/test/src/routes/admin.test.ts +8 -2
- package/test/src/routes/stream.test.ts +123 -2
- package/test/src/source-table-reconciler.test.ts +244 -0
- package/test/src/storage/SourceTable.test.ts +4 -1
- package/test/src/storage-metrics.test.ts +148 -0
- package/test/src/sync/util.test.ts +15 -0
- package/test/src/util/utils.test.ts +23 -0
- 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
|
|
86
|
+
* True if this table should evaluate event definitions for row changes.
|
|
65
87
|
*
|
|
66
|
-
* This value is resolved externally, and cached here.
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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 =
|
|
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
|
-
*
|
|
111
|
+
* Compact storage after initial replication, before the first checkpoint exists.
|
|
103
112
|
*/
|
|
104
|
-
|
|
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
|
|
404
|
+
export interface CompactInitialReplicationOptions {
|
|
362
405
|
/**
|
|
363
|
-
*
|
|
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
|
|
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
|
|
417
|
+
export interface CompactInitialReplicationResults {
|
|
375
418
|
/**
|
|
376
|
-
* Number of buckets
|
|
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
|
|
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 =
|
|
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(
|
|
192
|
+
setWriteCheckpointMode(config: WriteCheckpointModeConfig): void;
|
|
164
193
|
createManagedWriteCheckpoints(
|
|
165
194
|
checkpoints: ManagedWriteCheckpointOptions[]
|
|
166
195
|
): Promise<CreateManagedWriteCheckpointsResult>;
|