@powersync/service-module-mongodb-storage 0.19.0 → 0.21.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 (176) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +31 -3
  3. package/dist/storage/MongoBucketStorage.js +365 -206
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +15 -4
  6. package/dist/storage/implementation/MongoBucketBatch.js +35 -9
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoCompactor.d.ts +19 -79
  9. package/dist/storage/implementation/MongoCompactor.js +27 -238
  10. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  11. package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
  12. package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
  13. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  14. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
  15. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +29 -0
  16. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
  17. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +4 -2
  18. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
  19. package/dist/storage/implementation/MongoStorageProvider.js +3 -1
  20. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  21. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +27 -11
  22. package/dist/storage/implementation/MongoSyncBucketStorage.js +78 -31
  23. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  24. package/dist/storage/implementation/MongoSyncRulesLock.js +21 -9
  25. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  26. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
  27. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +7 -28
  28. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  29. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
  30. package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
  31. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
  32. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
  33. package/dist/storage/implementation/common/PersistedBatch.d.ts +27 -0
  34. package/dist/storage/implementation/common/PersistedBatch.js +43 -2
  35. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  36. package/dist/storage/implementation/db.d.ts +4 -1
  37. package/dist/storage/implementation/db.js +14 -1
  38. package/dist/storage/implementation/db.js.map +1 -1
  39. package/dist/storage/implementation/models.d.ts +35 -0
  40. package/dist/storage/implementation/models.js.map +1 -1
  41. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +6 -0
  42. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +80 -84
  43. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  44. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +36 -5
  45. package/dist/storage/implementation/v1/MongoCompactorV1.js +229 -8
  46. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  47. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
  48. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
  49. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
  50. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
  51. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +65 -16
  52. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  53. package/dist/storage/implementation/v1/models.js +1 -0
  54. package/dist/storage/implementation/v1/models.js.map +1 -1
  55. package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
  56. package/dist/storage/implementation/v3/CompactionLease.js +131 -0
  57. package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
  58. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -1
  59. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +120 -39
  60. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  61. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +77 -25
  62. package/dist/storage/implementation/v3/MongoCompactorV3.js +585 -141
  63. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  64. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
  65. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
  66. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  67. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
  68. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
  69. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
  70. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
  71. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +91 -31
  72. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  73. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
  74. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
  75. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
  76. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +2 -0
  77. package/dist/storage/implementation/v3/PersistedBatchV3.js +52 -11
  78. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  79. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
  80. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
  81. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  82. package/dist/storage/implementation/v3/bucket-format.js +2 -0
  83. package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
  84. package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
  85. package/dist/storage/implementation/v3/compact-utils.js +216 -0
  86. package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
  87. package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
  88. package/dist/storage/implementation/v3/compaction-constants.js +6 -0
  89. package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
  90. package/dist/storage/implementation/v3/models.d.ts +89 -5
  91. package/dist/storage/implementation/v3/models.js.map +1 -1
  92. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
  93. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -4
  94. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
  95. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
  96. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -5
  97. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +10 -2
  98. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
  99. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +37 -0
  100. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +167 -0
  101. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
  102. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
  103. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +177 -23
  104. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
  105. package/dist/storage/implementation/v3/source-table-utils.d.ts +15 -7
  106. package/dist/storage/implementation/v3/source-table-utils.js +49 -57
  107. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
  108. package/dist/storage/storage-index.d.ts +2 -1
  109. package/dist/storage/storage-index.js +1 -0
  110. package/dist/storage/storage-index.js.map +1 -1
  111. package/dist/types/types.d.ts +23 -1
  112. package/dist/types/types.js +16 -7
  113. package/dist/types/types.js.map +1 -1
  114. package/dist/utils/util.js +3 -1
  115. package/dist/utils/util.js.map +1 -1
  116. package/package.json +10 -9
  117. package/src/storage/MongoBucketStorage.ts +495 -268
  118. package/src/storage/implementation/MongoBucketBatch.ts +54 -14
  119. package/src/storage/implementation/MongoCompactor.ts +35 -346
  120. package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
  121. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +34 -0
  122. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +4 -2
  123. package/src/storage/implementation/MongoStorageProvider.ts +3 -1
  124. package/src/storage/implementation/MongoSyncBucketStorage.ts +114 -40
  125. package/src/storage/implementation/MongoSyncRulesLock.ts +22 -9
  126. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +12 -44
  127. package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
  128. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
  129. package/src/storage/implementation/common/PersistedBatch.ts +54 -2
  130. package/src/storage/implementation/db.ts +21 -2
  131. package/src/storage/implementation/models.ts +38 -0
  132. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +107 -96
  133. package/src/storage/implementation/v1/MongoCompactorV1.ts +303 -19
  134. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
  135. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +87 -27
  136. package/src/storage/implementation/v1/models.ts +1 -0
  137. package/src/storage/implementation/v3/CompactionLease.ts +152 -0
  138. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +159 -42
  139. package/src/storage/implementation/v3/MongoCompactorV3.ts +744 -190
  140. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
  141. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
  142. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +111 -41
  143. package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
  144. package/src/storage/implementation/v3/PersistedBatchV3.ts +57 -12
  145. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
  146. package/src/storage/implementation/v3/bucket-format.ts +2 -0
  147. package/src/storage/implementation/v3/compact-utils.ts +320 -0
  148. package/src/storage/implementation/v3/compaction-constants.ts +5 -0
  149. package/src/storage/implementation/v3/models.ts +96 -3
  150. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +16 -8
  151. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
  152. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +15 -4
  153. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +230 -0
  154. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +257 -27
  155. package/src/storage/implementation/v3/source-table-utils.ts +82 -76
  156. package/src/storage/storage-index.ts +2 -0
  157. package/src/types/types.ts +24 -7
  158. package/src/utils/util.ts +3 -1
  159. package/test/src/__snapshots__/storage.test.ts.snap +25 -0
  160. package/test/src/__snapshots__/storage_sync.test.ts.snap +605 -16
  161. package/test/src/cleanup-stopped-sync-configs.test.ts +40 -13
  162. package/test/src/compact-utils.test.ts +226 -0
  163. package/test/src/helpers/MemoryObjectStorage.ts +14 -6
  164. package/test/src/object_storage_usage.test.ts +255 -0
  165. package/test/src/parameter_compacting_v1.test.ts +120 -0
  166. package/test/src/parameter_compacting_v3.test.ts +192 -0
  167. package/test/src/parameter_compaction_fence.test.ts +276 -0
  168. package/test/src/storage.test.ts +273 -22
  169. package/test/src/storage_compacting.test.ts +714 -244
  170. package/test/src/storage_s3_checksums.test.ts +3 -4
  171. package/test/src/storage_s3_compaction_lifecycle.test.ts +138 -12
  172. package/test/src/storage_s3_reading.test.ts +243 -1
  173. package/test/src/storage_s3_writing.test.ts +1 -0
  174. package/test/src/storage_sync.test.ts +365 -41
  175. package/test/src/util.ts +1 -1
  176. package/tsconfig.tsbuildinfo +1 -1
@@ -1,8 +1,10 @@
1
1
  import { ReplicationAssertionError } from '@powersync/lib-services-framework';
2
- import { BucketDefinitionMapping, ColumnDescriptor, storage } from '@powersync/service-core';
2
+ import { BucketDefinitionMapping, ColumnDescriptor, JsonValue, storage } from '@powersync/service-core';
3
3
  import {
4
4
  BucketDataSource,
5
5
  BucketDefinitionId,
6
+ EventDefinitionId,
7
+ HydratedEventDescriptor,
6
8
  HydratedSyncConfig,
7
9
  MatchingSources,
8
10
  ParameterIndexId,
@@ -22,12 +24,13 @@ export interface SourceTableIdentity {
22
24
  export interface SourceTableMembershipIds {
23
25
  bucketDataSourceIds: BucketDefinitionId[];
24
26
  parameterLookupSourceIds: ParameterIndexId[];
27
+ eventDefinitionIds: EventDefinitionId[];
25
28
  }
26
29
 
27
30
  export interface SourceTableDesiredResolution {
28
31
  bucketSourceById: Map<BucketDefinitionId, BucketDataSource>;
29
32
  parameterLookupSourceById: Map<ParameterIndexId, ParameterIndexLookupCreator>;
30
- triggersEvent: boolean;
33
+ eventDefinitionById: Map<EventDefinitionId, HydratedEventDescriptor>;
31
34
  }
32
35
 
33
36
  export interface SourceTableReconciliationContext {
@@ -38,6 +41,14 @@ export interface SourceTableReconciliationContext {
38
41
  syncConfig: HydratedSyncConfig;
39
42
  mapping: BucketDefinitionMapping;
40
43
  desired: SourceTableDesiredResolution;
44
+ /**
45
+ * Candidates the source connector considers safe to reuse.
46
+ */
47
+ sourceCompatibleTables: readonly storage.SourceTableCandidate[];
48
+ /**
49
+ * Source metadata for records created by this resolution.
50
+ */
51
+ newTableSourceMetadata: JsonValue;
41
52
  }
42
53
 
43
54
  export interface SourceTableReconciliationPlan {
@@ -47,7 +58,7 @@ export interface SourceTableReconciliationPlan {
47
58
  narrowingUpdates: SourceTableMembershipUpdate[];
48
59
  /**
49
60
  * Memberships for a new source-table doc covering desired ids no existing doc covers,
50
- * or null if no new doc is needed. Empty memberships indicate an event-only table.
61
+ * or null if no new doc is needed.
51
62
  */
52
63
  newTableMemberships: SourceTableMembershipIds | null;
53
64
  /** Identity-overlapping docs that conflict with the current identity and must be dropped. */
@@ -67,7 +78,8 @@ export interface NewSourceTable {
67
78
  export function sourceTableDesiredResolution(
68
79
  syncConfig: HydratedSyncConfig,
69
80
  ref: SourceTableRef,
70
- mapping: BucketDefinitionMapping
81
+ mapping: BucketDefinitionMapping,
82
+ eventById: ReadonlyMap<EventDefinitionId, HydratedEventDescriptor>
71
83
  ): SourceTableDesiredResolution {
72
84
  const matchingSources = syncConfig.getMatchingSources(ref);
73
85
  return {
@@ -77,7 +89,7 @@ export function sourceTableDesiredResolution(
77
89
  parameterLookupSourceById: new Map(
78
90
  matchingSources.parameterLookupSources.map((source) => [mapping.parameterLookupId(source), source] as const)
79
91
  ),
80
- triggersEvent: syncConfig.tableTriggersEvent(ref)
92
+ eventDefinitionById: new Map([...eventById].filter(([, event]) => event.tableTriggersEvent(ref)))
81
93
  };
82
94
  }
83
95
 
@@ -108,35 +120,39 @@ function intersectMembershipIds(
108
120
  ): SourceTableMembershipIds {
109
121
  return {
110
122
  bucketDataSourceIds: doc.bucket_data_source_ids.filter((id) => desired.bucketSourceById.has(id)),
111
- parameterLookupSourceIds: doc.parameter_lookup_source_ids.filter((id) => desired.parameterLookupSourceById.has(id))
123
+ parameterLookupSourceIds: doc.parameter_lookup_source_ids.filter((id) => desired.parameterLookupSourceById.has(id)),
124
+ eventDefinitionIds: doc.event_definition_ids.filter((id) => desired.eventDefinitionById.has(id))
112
125
  };
113
126
  }
114
127
 
115
128
  function hasMembershipIds(memberships: SourceTableMembershipIds) {
116
- return memberships.bucketDataSourceIds.length > 0 || memberships.parameterLookupSourceIds.length > 0;
129
+ return (
130
+ memberships.bucketDataSourceIds.length > 0 ||
131
+ memberships.parameterLookupSourceIds.length > 0 ||
132
+ memberships.eventDefinitionIds.length > 0
133
+ );
117
134
  }
118
135
 
119
136
  function sameMembershipIds(doc: SourceTableDocumentV3, memberships: SourceTableMembershipIds) {
120
137
  return (
121
138
  sameStringArray(doc.bucket_data_source_ids, memberships.bucketDataSourceIds) &&
122
- sameStringArray(doc.parameter_lookup_source_ids, memberships.parameterLookupSourceIds)
139
+ sameStringArray(doc.parameter_lookup_source_ids, memberships.parameterLookupSourceIds) &&
140
+ sameStringArray(doc.event_definition_ids, memberships.eventDefinitionIds)
123
141
  );
124
142
  }
125
143
 
126
144
  class SourceTableReconciliationPlanner {
127
145
  private readonly coveredBucketDataSourceIds = new Set<string>();
128
146
  private readonly coveredParameterLookupSourceIds = new Set<string>();
129
- private readonly retainedDocIds = new Set<string>();
147
+ private readonly coveredEventDefinitionIds = new Set<string>();
130
148
  private readonly tables: storage.SourceTable[] = [];
131
149
  private readonly narrowingUpdates: SourceTableMembershipUpdate[] = [];
132
- private retainedEventOnlyTable = false;
133
150
 
134
151
  constructor(private readonly context: SourceTableReconciliationContext) {}
135
152
 
136
153
  plan(candidateDocs: SourceTableDocumentV3[]): SourceTableReconciliationPlan {
137
- const { identity } = this.context;
138
154
  for (const doc of candidateDocs) {
139
- if (matchingSourceTableIdentity(doc, identity)) {
155
+ if (this.isCompatible(doc)) {
140
156
  this.retainDoc(doc);
141
157
  }
142
158
  }
@@ -145,35 +161,31 @@ class SourceTableReconciliationPlanner {
145
161
  tables: this.tables,
146
162
  narrowingUpdates: this.narrowingUpdates,
147
163
  newTableMemberships: this.newTableMemberships(),
148
- // Non-retained overlapping docs represent renames / relation-id changes /
149
- // replica-identity changes. Same-identity stale docs are retained in production.
150
- dropDocs: candidateDocs.filter(
151
- (doc) => !this.retainedDocIds.has(doc._id.toHexString()) && !matchingSourceTableIdentity(doc, identity)
152
- )
164
+ // Compatible records remain available for reuse even when not needed by this config.
165
+ dropDocs: candidateDocs.filter((doc) => !this.isCompatible(doc))
153
166
  };
154
167
  }
155
168
 
169
+ private isCompatible(doc: SourceTableDocumentV3) {
170
+ return this.compatibleTableFor(doc) != null;
171
+ }
172
+
173
+ private compatibleTableFor(doc: SourceTableDocumentV3): storage.SourceTableCandidate | undefined {
174
+ return this.context.sourceCompatibleTables.find((table) => storage.sourceTableIdEquals(table.id, doc._id));
175
+ }
176
+
156
177
  private retainDoc(doc: SourceTableDocumentV3) {
157
178
  const memberships = intersectMembershipIds(doc, this.context.desired);
158
179
  const coversDesiredMembership = hasMembershipIds(memberships);
159
- const coversEventOnlyTable =
160
- !this.desiredHasMembership() && this.context.desired.triggersEvent && !this.retainedEventOnlyTable;
161
180
 
162
181
  this.recordCoverage(doc, memberships);
163
- this.planNarrowingUpdate(doc, memberships, coversDesiredMembership, coversEventOnlyTable);
182
+ this.planNarrowingUpdate(doc, memberships, coversDesiredMembership);
164
183
 
165
- if (coversDesiredMembership || coversEventOnlyTable) {
166
- this.retainedEventOnlyTable ||= coversEventOnlyTable;
167
- this.retainedDocIds.add(doc._id.toHexString());
184
+ if (coversDesiredMembership) {
168
185
  this.tables.push(this.sourceTableFor(doc, memberships));
169
186
  }
170
187
  }
171
188
 
172
- private desiredHasMembership() {
173
- const { desired } = this.context;
174
- return desired.bucketSourceById.size > 0 || desired.parameterLookupSourceById.size > 0;
175
- }
176
-
177
189
  private recordCoverage(doc: SourceTableDocumentV3, memberships: SourceTableMembershipIds) {
178
190
  this.addCoverage(doc, 'bucket data source', this.coveredBucketDataSourceIds, memberships.bucketDataSourceIds);
179
191
  this.addCoverage(
@@ -182,12 +194,14 @@ class SourceTableReconciliationPlanner {
182
194
  this.coveredParameterLookupSourceIds,
183
195
  memberships.parameterLookupSourceIds
184
196
  );
197
+ this.addCoverage(doc, 'event definition', this.coveredEventDefinitionIds, memberships.eventDefinitionIds);
185
198
  }
186
199
 
187
200
  // Membership sets must be pairwise disjoint across the docs of one physical table:
188
- // each desired id is covered by exactly one doc, so each definition receives each
189
- // row exactly once. The algorithm maintains this (new docs only get uncovered ids,
190
- // narrowing only removes ids) - overlap means the persisted state is corrupt.
201
+ // each desired id is covered by exactly one doc, so each definition is evaluated through
202
+ // only one SourceTable for a physical row. Different docs may own different event ids.
203
+ // The algorithm maintains this (new docs only get uncovered ids, narrowing only removes ids) -
204
+ // overlap means the persisted state is corrupt.
191
205
  private addCoverage(doc: SourceTableDocumentV3, kind: string, covered: Set<string>, ids: string[]) {
192
206
  const { schema, name } = this.context.identity;
193
207
  for (const id of ids) {
@@ -203,11 +217,9 @@ class SourceTableReconciliationPlanner {
203
217
  private planNarrowingUpdate(
204
218
  doc: SourceTableDocumentV3,
205
219
  memberships: SourceTableMembershipIds,
206
- coversDesiredMembership: boolean,
207
- coversEventOnlyTable: boolean
220
+ coversDesiredMembership: boolean
208
221
  ) {
209
- const shouldNarrow =
210
- (coversDesiredMembership || coversEventOnlyTable) && !doc.snapshot_done && !sameMembershipIds(doc, memberships);
222
+ const shouldNarrow = coversDesiredMembership && !doc.snapshot_done && !sameMembershipIds(doc, memberships);
211
223
 
212
224
  if (!shouldNarrow) {
213
225
  return;
@@ -227,9 +239,12 @@ class SourceTableReconciliationPlanner {
227
239
  ),
228
240
  parameterLookupSourceIds: [...desired.parameterLookupSourceById.keys()].filter(
229
241
  (id) => !this.coveredParameterLookupSourceIds.has(id)
242
+ ),
243
+ eventDefinitionIds: [...desired.eventDefinitionById.keys()].filter(
244
+ (id) => !this.coveredEventDefinitionIds.has(id)
230
245
  )
231
246
  };
232
- if (hasMembershipIds(uncovered) || (desired.triggersEvent && this.tables.length == 0)) {
247
+ if (hasMembershipIds(uncovered)) {
233
248
  return uncovered;
234
249
  }
235
250
  return null;
@@ -237,39 +252,23 @@ class SourceTableReconciliationPlanner {
237
252
 
238
253
  private sourceTableFor(doc: SourceTableDocumentV3, memberships: SourceTableMembershipIds): storage.SourceTable {
239
254
  const { connectionTag, syncConfig, mapping, desired, storeCurrentData } = this.context;
240
- const table = sourceTableFromDocument(
255
+ const built = sourceTableFromDocument(
241
256
  doc,
242
257
  connectionTag,
243
258
  syncConfig,
244
259
  mapping,
260
+ desired.eventDefinitionById,
245
261
  matchingSourcesFor(desired, memberships),
246
262
  memberships
247
263
  );
264
+ // Use any metadata update returned by the reconciler.
265
+ const resolved = this.compatibleTableFor(doc);
266
+ const table = resolved == null ? built : built.withSourceMetadata(resolved.sourceMetadata);
248
267
  table.storeCurrentData = storeCurrentData;
249
268
  return table;
250
269
  }
251
270
  }
252
271
 
253
- function sameReplicaIdColumns(left: ReplicaIdColumn[] | undefined, right: ReplicaIdColumn[]) {
254
- return (
255
- left != null &&
256
- left.length == right.length &&
257
- left.every(
258
- (column, index) =>
259
- column.name == right[index].name && column.type == right[index].type && column.type_oid == right[index].type_oid
260
- )
261
- );
262
- }
263
-
264
- function matchingSourceTableIdentity(doc: SourceTableDocumentV3, identity: SourceTableIdentity) {
265
- return (
266
- doc.schema_name == identity.schema &&
267
- doc.table_name == identity.name &&
268
- (identity.objectId == null || doc.relation_id == identity.objectId) &&
269
- sameReplicaIdColumns(doc.replica_id_columns, identity.replicaIdColumns)
270
- );
271
- }
272
-
273
272
  export function overlappingSourceTableFilter(
274
273
  connectionId: number,
275
274
  identity: Pick<SourceTableIdentity, 'schema' | 'name' | 'objectId'>
@@ -290,7 +289,16 @@ export function createNewSourceTable(
290
289
  memberships: SourceTableMembershipIds,
291
290
  context: SourceTableReconciliationContext
292
291
  ): NewSourceTable {
293
- const { connectionId, connectionTag, identity, syncConfig, mapping, desired, storeCurrentData } = context;
292
+ const {
293
+ connectionId,
294
+ connectionTag,
295
+ identity,
296
+ syncConfig,
297
+ mapping,
298
+ desired,
299
+ storeCurrentData,
300
+ newTableSourceMetadata
301
+ } = context;
294
302
  const doc: SourceTableDocumentV3 = {
295
303
  _id: id,
296
304
  connection_id: connectionId,
@@ -301,13 +309,17 @@ export function createNewSourceTable(
301
309
  snapshot_done: false,
302
310
  snapshot_status: undefined,
303
311
  bucket_data_source_ids: memberships.bucketDataSourceIds,
304
- parameter_lookup_source_ids: memberships.parameterLookupSourceIds
312
+ parameter_lookup_source_ids: memberships.parameterLookupSourceIds,
313
+ event_definition_ids: memberships.eventDefinitionIds,
314
+ // Records created together share the same source metadata.
315
+ source_metadata: newTableSourceMetadata
305
316
  };
306
317
  const table = sourceTableFromDocument(
307
318
  doc,
308
319
  connectionTag,
309
320
  syncConfig,
310
321
  mapping,
322
+ desired.eventDefinitionById,
311
323
  matchingSourcesFor(desired, memberships),
312
324
  memberships
313
325
  );
@@ -316,17 +328,6 @@ export function createNewSourceTable(
316
328
  return { doc, table };
317
329
  }
318
330
 
319
- export function designateEventCarrier(tables: storage.SourceTable[], triggersEvent: boolean) {
320
- if (!triggersEvent) {
321
- return;
322
- }
323
-
324
- const eventCarrier = tables.find((table) => table.snapshotComplete) ?? tables[0];
325
- for (const table of tables) {
326
- table.syncEvent = table === eventCarrier;
327
- }
328
- }
329
-
330
331
  function sourceTableMembershipsFromDocument(
331
332
  doc: SourceTableDocumentV3,
332
333
  syncConfig: HydratedSyncConfig,
@@ -350,6 +351,7 @@ export function sourceTableFromDocument(
350
351
  connectionTag: string,
351
352
  syncConfig: HydratedSyncConfig,
352
353
  mapping: BucketDefinitionMapping,
354
+ eventById: ReadonlyMap<EventDefinitionId, HydratedEventDescriptor>,
353
355
  memberships?: MatchingSources,
354
356
  membershipIds?: SourceTableMembershipIds
355
357
  ): storage.SourceTable {
@@ -358,7 +360,8 @@ export function sourceTableFromDocument(
358
360
  bucketDataSourceIds: resolvedMemberships.bucketDataSources.map((source) => mapping.bucketSourceId(source)),
359
361
  parameterLookupSourceIds: resolvedMemberships.parameterLookupSources.map((source) =>
360
362
  mapping.parameterLookupId(source)
361
- )
363
+ ),
364
+ eventDefinitionIds: doc.event_definition_ids.filter((id) => eventById.has(id))
362
365
  };
363
366
  const table = new storage.SourceTable({
364
367
  id: doc._id,
@@ -368,18 +371,21 @@ export function sourceTableFromDocument(
368
371
  name: doc.table_name
369
372
  },
370
373
  objectId: doc.relation_id,
371
- replicaIdColumns: doc.replica_id_columns!.map(
372
- (c) => ({ name: c.name, typeId: c.type_oid, type: c.type }) satisfies ColumnDescriptor
373
- ),
374
+ replicaIdColumns:
375
+ doc.replica_id_columns?.map(
376
+ (column) => ({ name: column.name, typeId: column.type_oid, type: column.type }) satisfies ColumnDescriptor
377
+ ) ?? [],
374
378
  snapshotComplete: doc.snapshot_done,
375
379
  bucketDataSources: resolvedMemberships.bucketDataSources,
376
380
  parameterLookupSources: resolvedMemberships.parameterLookupSources,
377
381
  bucketDataSourceIds: new Set(resolvedMembershipIds.bucketDataSourceIds),
378
- parameterLookupSourceIds: new Set(resolvedMembershipIds.parameterLookupSourceIds)
382
+ parameterLookupSourceIds: new Set(resolvedMembershipIds.parameterLookupSourceIds),
383
+ eventDefinitionIds: new Set(resolvedMembershipIds.eventDefinitionIds),
384
+ sourceMetadata: doc.source_metadata ?? null
379
385
  });
380
386
  table.syncData = table.bucketDataSources.length > 0;
381
387
  table.syncParameters = table.parameterLookupSources.length > 0;
382
- table.syncEvent = syncConfig.tableTriggersEvent(table.ref);
388
+ table.syncEvent = table.eventDefinitionIds!.size > 0;
383
389
  table.snapshotStatus =
384
390
  doc.snapshot_status == null
385
391
  ? undefined
@@ -15,11 +15,13 @@ export { loadBucketDataDocument, serializeBucketData } from './implementation/v3
15
15
  export {
16
16
  BucketDataDocumentV3,
17
17
  BucketOperation,
18
+ ObjectStorageUsageDocument,
18
19
  RecordedLookupV3,
19
20
  ReplicationStreamDocumentV3,
20
21
  SyncConfigDefinition,
21
22
  SyncRuleConfigStateV3,
22
23
  taggedBucketParameterDocumentToTagged
23
24
  } from './implementation/v3/models.js';
25
+ export * from './implementation/v3/object-storage/ObjectStorageUsage.js';
24
26
  export * from './MongoBucketStorage.js';
25
27
  export * from './MongoReportStorage.js';
@@ -3,6 +3,20 @@ import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
3
3
  import * as service_types from '@powersync/service-types';
4
4
  import * as t from 'ts-codec';
5
5
 
6
+ /**
7
+ * AWS defaults mode, as used by the AWS SDK.
8
+ *
9
+ * This is the baseline for the object storage request timeouts, so that they do not each need to be
10
+ * configured individually.
11
+ *
12
+ * The SDK's `legacy` and `auto` modes are not selectable here: `legacy` defines no timeouts at all,
13
+ * and `auto` makes the timeouts depend on where the process happens to be running. Both are still
14
+ * handled when they come from the AWS environment, and are then treated as `standard`.
15
+ */
16
+ export const S3DefaultsMode = service_types.enumLiteral('standard', 'in-region', 'cross-region', 'mobile');
17
+
18
+ export type S3DefaultsMode = t.Encoded<typeof S3DefaultsMode>;
19
+
6
20
  const S3ObjectStorageConfig = t.object({
7
21
  type: t.literal('s3'),
8
22
  bucket: t.string,
@@ -13,21 +27,24 @@ const S3ObjectStorageConfig = t.object({
13
27
  access_key_id: t.string.optional(),
14
28
  secret_access_key: t.string.optional(),
15
29
  concurrency_limit: t.number.optional(),
30
+ // Defaults to the AWS_DEFAULTS_MODE environment variable.
31
+ defaults_mode: S3DefaultsMode.optional(),
16
32
  // Chunks whose BSON-serialized size falls below this byte threshold
17
33
  // stay inline in MongoDB instead of being offloaded to S3. Default 1024.
18
34
  inline_threshold_bytes: t.number.optional()
19
35
  });
20
36
 
21
- export const MongoStorageReadPreference = t
22
- .literal('primary')
23
- .or(t.literal('primaryPreferred'))
24
- .or(t.literal('secondary'))
25
- .or(t.literal('secondaryPreferred'))
26
- .or(t.literal('nearest'));
37
+ export const MongoStorageReadPreference = service_types.enumLiteral(
38
+ 'primary',
39
+ 'primaryPreferred',
40
+ 'secondary',
41
+ 'secondaryPreferred',
42
+ 'nearest'
43
+ );
27
44
 
28
45
  export type MongoStorageReadPreference = t.Encoded<typeof MongoStorageReadPreference>;
29
46
 
30
- export const MongoStorageConfig = lib_mongo.BaseMongoConfig.and(
47
+ export const MongoStorageConfig = service_types.configFile.BaseStorageConfig.and(lib_mongo.BaseMongoConfig).and(
31
48
  t.object({
32
49
  /**
33
50
  * Read preference for bulk checksum and bucket data reads.
package/src/utils/util.ts CHANGED
@@ -121,7 +121,9 @@ export function mapOpEntry(row: BucketDataDoc): utils.OplogEntry {
121
121
  object_type: row.table,
122
122
  object_id: row.row_id,
123
123
  checksum: Number(row.checksum),
124
- subkey: replicaIdToSubkey(row.source_table!, row.source_key!),
124
+ // Storage V3 always persists this during replication. The fallback serves V1 data
125
+ // written before subkey persistence was introduced.
126
+ subkey: row.subkey ?? replicaIdToSubkey(row.source_table!, row.source_key!),
125
127
  data: row.data
126
128
  };
127
129
  } else {
@@ -75,6 +75,31 @@ exports[`Mongo Sync Bucket Storage - Data - v3 > (insert, delete, insert), (dele
75
75
  ]
76
76
  `;
77
77
 
78
+ exports[`Mongo Sync Bucket Storage - Data - v4 > (insert, delete, insert), (delete) 1`] = `
79
+ [
80
+ {
81
+ "checksum": 2871785649,
82
+ "object_id": "test1",
83
+ "op": "PUT",
84
+ },
85
+ {
86
+ "checksum": 2872534815,
87
+ "object_id": "test1",
88
+ "op": "REMOVE",
89
+ },
90
+ {
91
+ "checksum": 2871785649,
92
+ "object_id": "test1",
93
+ "op": "PUT",
94
+ },
95
+ {
96
+ "checksum": 2872534815,
97
+ "object_id": "test1",
98
+ "op": "REMOVE",
99
+ },
100
+ ]
101
+ `;
102
+
78
103
  exports[`Mongo Sync Bucket Storage - split buckets > (insert, delete, insert), (delete) 1`] = `
79
104
  [
80
105
  {