@powersync/service-module-mongodb-storage 0.16.0 → 0.18.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 (206) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +35 -15
  3. package/dist/storage/MongoBucketStorage.js +309 -86
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/CheckpointState.d.ts +20 -0
  6. package/dist/storage/implementation/CheckpointState.js +31 -0
  7. package/dist/storage/implementation/CheckpointState.js.map +1 -0
  8. package/dist/storage/implementation/MongoBucketBatch.d.ts +69 -39
  9. package/dist/storage/implementation/MongoBucketBatch.js +104 -313
  10. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  11. package/dist/storage/implementation/MongoChecksums.d.ts +11 -9
  12. package/dist/storage/implementation/MongoChecksums.js +3 -111
  13. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  14. package/dist/storage/implementation/MongoCompactor.d.ts +3 -1
  15. package/dist/storage/implementation/MongoCompactor.js +18 -9
  16. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  17. package/dist/storage/implementation/MongoHydrationState.d.ts +11 -0
  18. package/dist/storage/implementation/MongoHydrationState.js +49 -0
  19. package/dist/storage/implementation/MongoHydrationState.js.map +1 -0
  20. package/dist/storage/implementation/MongoParameterCompactor.d.ts +6 -5
  21. package/dist/storage/implementation/MongoParameterCompactor.js +24 -1
  22. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  23. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +13 -0
  24. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +64 -0
  25. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -0
  26. package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +36 -0
  27. package/dist/storage/implementation/MongoPersistedReplicationStream.js +90 -0
  28. package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -0
  29. package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +25 -0
  30. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +121 -0
  31. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
  32. package/dist/storage/implementation/MongoStorageProvider.js +5 -2
  33. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  34. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +50 -21
  35. package/dist/storage/implementation/MongoSyncBucketStorage.js +62 -199
  36. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  37. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +5 -1
  38. package/dist/storage/implementation/MongoSyncRulesLock.js +9 -5
  39. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  40. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +14 -0
  41. package/dist/storage/implementation/SyncRuleStateUpdate.js +36 -0
  42. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -0
  43. package/dist/storage/implementation/common/BucketDataDoc.d.ts +1 -1
  44. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +6 -0
  45. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js +2 -0
  46. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js.map +1 -0
  47. package/dist/storage/implementation/common/PersistedBatch.d.ts +2 -3
  48. package/dist/storage/implementation/common/PersistedBatch.js +5 -0
  49. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  50. package/dist/storage/implementation/common/SourceRecordStore.d.ts +1 -2
  51. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +1 -1
  52. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +1 -1
  53. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  54. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
  55. package/dist/storage/implementation/createMongoSyncBucketStorage.js +4 -4
  56. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
  57. package/dist/storage/implementation/db.d.ts +10 -2
  58. package/dist/storage/implementation/db.js +9 -14
  59. package/dist/storage/implementation/db.js.map +1 -1
  60. package/dist/storage/implementation/models.d.ts +25 -50
  61. package/dist/storage/implementation/models.js.map +1 -1
  62. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +23 -1
  63. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +403 -2
  64. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  65. package/dist/storage/implementation/v1/MongoChecksumsV1.js +114 -3
  66. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
  67. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +1 -1
  68. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +22 -15
  69. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +100 -27
  70. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  71. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +1 -2
  72. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
  73. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +2 -1
  74. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -0
  75. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
  76. package/dist/storage/implementation/v1/models.d.ts +21 -1
  77. package/dist/storage/implementation/v1/models.js.map +1 -1
  78. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +21 -0
  79. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +459 -3
  80. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  81. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +27 -7
  82. package/dist/storage/implementation/v3/MongoChecksumsV3.js +202 -18
  83. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  84. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +20 -3
  85. package/dist/storage/implementation/v3/MongoCompactorV3.js +511 -22
  86. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  87. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +62 -0
  88. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +350 -0
  89. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
  90. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +50 -16
  91. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +292 -75
  92. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  93. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +2 -3
  94. package/dist/storage/implementation/v3/PersistedBatchV3.js +43 -20
  95. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  96. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +52 -16
  97. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -1
  98. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +3 -3
  99. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +13 -11
  100. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
  101. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +16 -14
  102. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +32 -27
  103. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  104. package/dist/storage/implementation/v3/bucket-format.d.ts +4 -0
  105. package/dist/storage/implementation/v3/bucket-format.js +58 -0
  106. package/dist/storage/implementation/v3/bucket-format.js.map +1 -0
  107. package/dist/storage/implementation/v3/chunking.d.ts +10 -0
  108. package/dist/storage/implementation/v3/chunking.js +28 -0
  109. package/dist/storage/implementation/v3/chunking.js.map +1 -0
  110. package/dist/storage/implementation/v3/models.d.ts +111 -10
  111. package/dist/storage/implementation/v3/models.js +11 -28
  112. package/dist/storage/implementation/v3/models.js.map +1 -1
  113. package/dist/storage/implementation/v3/source-table-utils.d.ts +55 -0
  114. package/dist/storage/implementation/v3/source-table-utils.js +214 -0
  115. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
  116. package/dist/storage/storage-index.d.ts +4 -4
  117. package/dist/storage/storage-index.js +4 -4
  118. package/dist/storage/storage-index.js.map +1 -1
  119. package/dist/types/types.d.ts +2 -2
  120. package/dist/utils/test-utils.d.ts +1 -2
  121. package/dist/utils/test-utils.js +5 -1
  122. package/dist/utils/test-utils.js.map +1 -1
  123. package/dist/utils/util.d.ts +1 -1
  124. package/dist/utils/util.js +3 -2
  125. package/dist/utils/util.js.map +1 -1
  126. package/package.json +9 -19
  127. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +1 -1
  128. package/src/storage/MongoBucketStorage.ts +464 -95
  129. package/src/storage/implementation/CheckpointState.ts +59 -0
  130. package/src/storage/implementation/MongoBucketBatch.ts +159 -405
  131. package/src/storage/implementation/MongoChecksums.ts +7 -138
  132. package/src/storage/implementation/MongoCompactor.ts +20 -10
  133. package/src/storage/implementation/MongoHydrationState.ts +61 -0
  134. package/src/storage/implementation/MongoParameterCompactor.ts +24 -5
  135. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +88 -0
  136. package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
  137. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +152 -0
  138. package/src/storage/implementation/MongoStorageProvider.ts +5 -2
  139. package/src/storage/implementation/MongoSyncBucketStorage.ts +89 -250
  140. package/src/storage/implementation/MongoSyncRulesLock.ts +14 -5
  141. package/src/storage/implementation/SyncRuleStateUpdate.ts +38 -0
  142. package/src/storage/implementation/common/BucketDataDoc.ts +1 -1
  143. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +7 -0
  144. package/src/storage/implementation/common/PersistedBatch.ts +8 -4
  145. package/src/storage/implementation/common/SourceRecordStore.ts +1 -2
  146. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
  147. package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
  148. package/src/storage/implementation/db.ts +16 -21
  149. package/src/storage/implementation/models.ts +28 -61
  150. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +503 -5
  151. package/src/storage/implementation/v1/MongoChecksumsV1.ts +137 -4
  152. package/src/storage/implementation/v1/MongoCompactorV1.ts +1 -1
  153. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +150 -49
  154. package/src/storage/implementation/v1/PersistedBatchV1.ts +1 -2
  155. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +11 -1
  156. package/src/storage/implementation/v1/models.ts +25 -0
  157. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +611 -4
  158. package/src/storage/implementation/v3/MongoChecksumsV3.ts +262 -26
  159. package/src/storage/implementation/v3/MongoCompactorV3.ts +647 -30
  160. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +480 -0
  161. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +397 -111
  162. package/src/storage/implementation/v3/PersistedBatchV3.ts +63 -38
  163. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +59 -19
  164. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
  165. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +48 -38
  166. package/src/storage/implementation/v3/bucket-format.ts +70 -0
  167. package/src/storage/implementation/v3/chunking.ts +38 -0
  168. package/src/storage/implementation/v3/models.ts +150 -44
  169. package/src/storage/implementation/v3/source-table-utils.ts +392 -0
  170. package/src/storage/storage-index.ts +12 -4
  171. package/src/utils/test-utils.ts +6 -3
  172. package/src/utils/util.ts +3 -2
  173. package/test/src/__snapshots__/storage.test.ts.snap +0 -75
  174. package/test/src/__snapshots__/storage_sync.test.ts.snap +48 -48
  175. package/test/src/cleanup-stopped-sync-configs.test.ts +648 -0
  176. package/test/src/storage.test.ts +11 -11
  177. package/test/src/storage_compacting.test.ts +1956 -6
  178. package/test/src/storage_sync.test.ts +1429 -42
  179. package/test/src/storeCurrentData.test.ts +221 -0
  180. package/test/src/util.ts +2 -1
  181. package/tsconfig.json +1 -4
  182. package/tsconfig.tsbuildinfo +1 -1
  183. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +0 -17
  184. package/dist/storage/implementation/BucketDefinitionMapping.js +0 -58
  185. package/dist/storage/implementation/BucketDefinitionMapping.js.map +0 -1
  186. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -14
  187. package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -64
  188. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
  189. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -15
  190. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -42
  191. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
  192. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +0 -13
  193. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +0 -2
  194. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +0 -1
  195. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +0 -9
  196. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +0 -18
  197. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +0 -1
  198. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +0 -5
  199. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +0 -9
  200. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +0 -1
  201. package/src/storage/implementation/BucketDefinitionMapping.ts +0 -72
  202. package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -76
  203. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -56
  204. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +0 -15
  205. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +0 -24
  206. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -12
@@ -1,23 +1,43 @@
1
- import { SourceTable } from '@powersync/service-core';
1
+ import { ReplicationAssertionError } from '@powersync/lib-services-framework';
2
+ import { ColumnDescriptor, InternalOpId, SourceTable, storage } from '@powersync/service-core';
3
+ import * as bson from 'bson';
4
+ import { mongoTableId } from '../../../utils/util.js';
5
+ import { calculateCheckpointState } from '../CheckpointState.js';
2
6
  import { MongoBucketBatch, MongoBucketBatchOptions } from '../MongoBucketBatch.js';
3
7
  import { PersistedBatch } from '../common/PersistedBatch.js';
4
8
  import { SourceRecordStore } from '../common/SourceRecordStore.js';
5
9
  import { PersistedBatchV1 } from './PersistedBatchV1.js';
6
10
  import { SourceRecordStoreV1 } from './SourceRecordStoreV1.js';
7
11
  import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
12
+ import { SyncRuleDocumentV1 } from './models.js';
8
13
 
9
14
  export class MongoBucketBatchV1 extends MongoBucketBatch {
10
- declare public readonly db: VersionedPowerSyncMongoV1;
15
+ declare readonly db: VersionedPowerSyncMongoV1;
11
16
 
12
17
  private readonly store: SourceRecordStore;
18
+ private needsActivation = true;
19
+ private lastWaitingLogThrottled = 0;
20
+
21
+ /**
22
+ * The last persisted op that is not yet covered by a checkpoint, if any.
23
+ *
24
+ * Seeded from the persisted keepalive_op, so that ops persisted before a restart can still be
25
+ * included in the next checkpoint.
26
+ */
27
+ private persisted_op: InternalOpId | null = null;
13
28
 
14
29
  constructor(options: MongoBucketBatchOptions) {
15
30
  super(options);
16
- this.store = new SourceRecordStoreV1(this.db, this.group_id);
31
+ this.persisted_op = options.keepaliveOp ?? null;
32
+ this.store = new SourceRecordStoreV1(this.db, this.replicationStreamId);
33
+ }
34
+
35
+ protected override recordPersistedOp(lastOp: InternalOpId): void {
36
+ this.persisted_op = lastOp;
17
37
  }
18
38
 
19
39
  protected createPersistedBatch(writtenSize: number): PersistedBatch {
20
- return new PersistedBatchV1(this.db, this.group_id, this.mapping, writtenSize, {
40
+ return new PersistedBatchV1(this.db, this.replicationStreamId, this.mapping, writtenSize, {
21
41
  logger: this.logger
22
42
  });
23
43
  }
@@ -25,8 +45,486 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
25
45
  protected get sourceRecordStore(): SourceRecordStore {
26
46
  return this.store;
27
47
  }
28
-
29
48
  protected async cleanupDroppedSourceTables(_tables: SourceTable[]) {
30
49
  // No-op for V1: source records live in a shared collection.
31
50
  }
51
+
52
+ async resolveTables(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult> {
53
+ const syncRules = options.parsedSyncConfig?.hydratedSyncConfig ?? this.sync_rules;
54
+ const { connection_id, source } = options;
55
+ const { schema, name, objectId, replicaIdColumns, connectionTag, sendsCompleteRows } = source;
56
+
57
+ const normalizedReplicaIdColumns = replicaIdColumns.map((column) => ({
58
+ name: column.name,
59
+ type: column.type,
60
+ type_oid: column.typeId
61
+ }));
62
+
63
+ let result: storage.ResolveTablesResult | null = null;
64
+ await this.db.client.withSession(async (session) => {
65
+ const col = this.db.sourceTablesV1(this.replicationStreamId);
66
+ const filter: any = {
67
+ group_id: this.replicationStreamId,
68
+ connection_id,
69
+ schema_name: schema,
70
+ table_name: name,
71
+ replica_id_columns2: normalizedReplicaIdColumns
72
+ };
73
+ if (objectId != null) {
74
+ filter.relation_id = objectId;
75
+ }
76
+
77
+ let doc = await col.findOne(filter, { session });
78
+ if (doc == null) {
79
+ doc = {
80
+ _id: options.idGenerator ? (options.idGenerator() as bson.ObjectId) : new bson.ObjectId(),
81
+ group_id: this.replicationStreamId,
82
+ connection_id,
83
+ relation_id: objectId,
84
+ schema_name: schema,
85
+ table_name: name,
86
+ replica_id_columns: null,
87
+ replica_id_columns2: normalizedReplicaIdColumns,
88
+ snapshot_done: false,
89
+ snapshot_status: undefined
90
+ };
91
+ await col.insertOne(doc, { session });
92
+ }
93
+
94
+ const sourceTable = new storage.SourceTable({
95
+ id: doc._id,
96
+ ref: source,
97
+ objectId,
98
+ replicaIdColumns,
99
+ snapshotComplete: doc.snapshot_done ?? true,
100
+ ...syncRules.getMatchingSources(source)
101
+ });
102
+ sourceTable.syncEvent = syncRules.tableTriggersEvent(source);
103
+ sourceTable.syncData = sourceTable.bucketDataSources.length > 0;
104
+ sourceTable.syncParameters = sourceTable.parameterLookupSources.length > 0;
105
+ sourceTable.storeCurrentData = sendsCompleteRows !== true;
106
+ sourceTable.snapshotStatus =
107
+ doc.snapshot_status == null
108
+ ? undefined
109
+ : {
110
+ lastKey: doc.snapshot_status.last_key?.buffer ?? null,
111
+ totalEstimatedCount: doc.snapshot_status.total_estimated_count,
112
+ replicatedCount: doc.snapshot_status.replicated_count
113
+ };
114
+
115
+ const truncateFilter = [{ schema_name: schema, table_name: name }] as any[];
116
+ if (objectId != null) {
117
+ truncateFilter.push({ relation_id: objectId });
118
+ }
119
+ const truncate = await col
120
+ .find(
121
+ {
122
+ group_id: this.replicationStreamId,
123
+ connection_id,
124
+ _id: { $ne: doc._id },
125
+ $or: truncateFilter
126
+ },
127
+ { session }
128
+ )
129
+ .toArray();
130
+ const dropTables = truncate.map((dropDoc) => {
131
+ const ref = {
132
+ connectionTag,
133
+ schema: dropDoc.schema_name,
134
+ name: dropDoc.table_name
135
+ };
136
+ const table = new storage.SourceTable({
137
+ id: dropDoc._id,
138
+ ref,
139
+ objectId: dropDoc.relation_id,
140
+ replicaIdColumns:
141
+ dropDoc.replica_id_columns2?.map(
142
+ (c) => ({ name: c.name, typeId: c.type_oid, type: c.type }) satisfies ColumnDescriptor
143
+ ) ?? [],
144
+ snapshotComplete: dropDoc.snapshot_done ?? true,
145
+ ...syncRules.getMatchingSources(ref)
146
+ });
147
+ table.syncEvent = syncRules.tableTriggersEvent(ref);
148
+ table.syncData = table.bucketDataSources.length > 0;
149
+ table.syncParameters = table.parameterLookupSources.length > 0;
150
+ return table;
151
+ });
152
+
153
+ result = { tables: [sourceTable], dropTables };
154
+ });
155
+
156
+ return result!;
157
+ }
158
+
159
+ async getSourceTableStatus(table: storage.SourceTable): Promise<storage.SourceTable | null> {
160
+ const doc = await this.db.sourceTablesV1(this.replicationStreamId).findOne(
161
+ {
162
+ group_id: this.replicationStreamId,
163
+ _id: mongoTableId(table.id)
164
+ },
165
+ { session: this.session }
166
+ );
167
+ if (doc == null) {
168
+ return null;
169
+ }
170
+
171
+ const ref = {
172
+ connectionTag: table.ref.connectionTag,
173
+ schema: doc.schema_name,
174
+ name: doc.table_name
175
+ };
176
+ const sourceTable = new storage.SourceTable({
177
+ id: doc._id,
178
+ ref,
179
+ objectId: doc.relation_id,
180
+ replicaIdColumns:
181
+ doc.replica_id_columns2?.map(
182
+ (c) => ({ name: c.name, typeId: c.type_oid, type: c.type }) satisfies ColumnDescriptor
183
+ ) ?? [],
184
+ snapshotComplete: doc.snapshot_done ?? true,
185
+ ...this.sync_rules.getMatchingSources(ref)
186
+ });
187
+ sourceTable.syncEvent = this.sync_rules.tableTriggersEvent(ref);
188
+ sourceTable.syncData = sourceTable.bucketDataSources.length > 0;
189
+ sourceTable.syncParameters = sourceTable.parameterLookupSources.length > 0;
190
+ sourceTable.snapshotStatus =
191
+ doc.snapshot_status == null
192
+ ? undefined
193
+ : {
194
+ lastKey: doc.snapshot_status.last_key?.buffer ?? null,
195
+ totalEstimatedCount: doc.snapshot_status.total_estimated_count,
196
+ replicatedCount: doc.snapshot_status.replicated_count
197
+ };
198
+ return sourceTable;
199
+ }
200
+
201
+ async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {
202
+ const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
203
+
204
+ await this.flush(options);
205
+
206
+ const now = new Date();
207
+
208
+ await this.db.write_checkpoints.updateMany(
209
+ {
210
+ processed_at_lsn: null,
211
+ 'lsns.1': { $lte: lsn }
212
+ },
213
+ {
214
+ $set: {
215
+ processed_at_lsn: lsn
216
+ }
217
+ },
218
+ {
219
+ session: this.session
220
+ }
221
+ );
222
+
223
+ const can_checkpoint = {
224
+ $and: [
225
+ { $eq: ['$snapshot_done', true] },
226
+ {
227
+ $or: [{ $eq: ['$last_checkpoint_lsn', null] }, { $lte: ['$last_checkpoint_lsn', { $literal: lsn }] }]
228
+ },
229
+ {
230
+ $or: [{ $eq: ['$no_checkpoint_before', null] }, { $lte: ['$no_checkpoint_before', { $literal: lsn }] }]
231
+ }
232
+ ]
233
+ };
234
+
235
+ const new_keepalive_op = {
236
+ $cond: [
237
+ can_checkpoint,
238
+ { $literal: null },
239
+ {
240
+ $toString: {
241
+ $max: [{ $toLong: '$keepalive_op' }, { $literal: this.persisted_op }, 0n]
242
+ }
243
+ }
244
+ ]
245
+ };
246
+
247
+ const new_last_checkpoint = {
248
+ $cond: [
249
+ can_checkpoint,
250
+ {
251
+ $max: ['$last_checkpoint', { $literal: this.persisted_op }, { $toLong: '$keepalive_op' }, 0n]
252
+ },
253
+ '$last_checkpoint'
254
+ ]
255
+ };
256
+
257
+ const preUpdateDocument = (await this.db.sync_rules.findOneAndUpdate(
258
+ { _id: this.replicationStreamId },
259
+ [
260
+ {
261
+ $set: {
262
+ _can_checkpoint: can_checkpoint,
263
+ _not_empty: createEmptyCheckpoints
264
+ ? true
265
+ : {
266
+ $or: [
267
+ { $literal: createEmptyCheckpoints },
268
+ { $ne: ['$keepalive_op', new_keepalive_op] },
269
+ { $ne: ['$last_checkpoint', new_last_checkpoint] }
270
+ ]
271
+ }
272
+ }
273
+ },
274
+ {
275
+ $set: {
276
+ last_checkpoint_lsn: {
277
+ $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: lsn }, '$last_checkpoint_lsn']
278
+ },
279
+ last_checkpoint_ts: {
280
+ $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: now }, '$last_checkpoint_ts']
281
+ },
282
+ last_keepalive_ts: { $literal: now },
283
+ last_fatal_error: { $literal: null },
284
+ last_fatal_error_ts: { $literal: null },
285
+ keepalive_op: new_keepalive_op,
286
+ last_checkpoint: new_last_checkpoint,
287
+ snapshot_lsn: {
288
+ $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: null }, '$snapshot_lsn']
289
+ }
290
+ }
291
+ },
292
+ {
293
+ $unset: ['_can_checkpoint', '_not_empty']
294
+ }
295
+ ],
296
+ {
297
+ session: this.session,
298
+ returnDocument: 'before',
299
+ projection: {
300
+ snapshot_done: 1,
301
+ last_checkpoint_lsn: 1,
302
+ no_checkpoint_before: 1,
303
+ keepalive_op: 1,
304
+ last_checkpoint: 1
305
+ }
306
+ }
307
+ )) as SyncRuleDocumentV1;
308
+
309
+ if (preUpdateDocument == null) {
310
+ throw new ReplicationAssertionError(
311
+ 'Failed to update checkpoint - no matching sync_rules document for _id: ' + this.replicationStreamId
312
+ );
313
+ }
314
+
315
+ const checkpointState = calculateCheckpointState({
316
+ lsn,
317
+ snapshotDone: preUpdateDocument.snapshot_done === true,
318
+ lastCheckpointLsn: preUpdateDocument.last_checkpoint_lsn,
319
+ noCheckpointBefore: preUpdateDocument.no_checkpoint_before,
320
+ keepaliveOp: preUpdateDocument.keepalive_op == null ? null : BigInt(preUpdateDocument.keepalive_op),
321
+ lastCheckpoint: preUpdateDocument.last_checkpoint,
322
+ persistedOp: this.persisted_op,
323
+ createEmptyCheckpoints
324
+ });
325
+ if (checkpointState.checkpointBlocked) {
326
+ if (Date.now() - this.lastWaitingLogThrottled > 5_000) {
327
+ this.logger.info(
328
+ `Waiting before creating checkpoint, currently at ${lsn} / ${checkpointState.newKeepaliveOp}. Current state: ${JSON.stringify(
329
+ {
330
+ snapshot_done: preUpdateDocument.snapshot_done,
331
+ last_checkpoint_lsn: preUpdateDocument.last_checkpoint_lsn,
332
+ no_checkpoint_before: preUpdateDocument.no_checkpoint_before
333
+ }
334
+ )}`
335
+ );
336
+ this.lastWaitingLogThrottled = Date.now();
337
+ }
338
+ } else {
339
+ if (checkpointState.checkpointCreated) {
340
+ this.logger.debug(`Created checkpoint at ${lsn} / ${checkpointState.newLastCheckpoint}`);
341
+ }
342
+ await this.autoActivate(lsn);
343
+ await this.db.notifyCheckpoint();
344
+ this.persisted_op = null;
345
+ if (checkpointState.newLastCheckpoint != null) {
346
+ await this.sourceRecordStore.postCommitCleanup(checkpointState.newLastCheckpoint, this.logger);
347
+ }
348
+ }
349
+
350
+ return {
351
+ checkpointBlocked: checkpointState.checkpointBlocked,
352
+ checkpointCreated: checkpointState.checkpointCreated
353
+ };
354
+ }
355
+
356
+ async keepalive(lsn: string): Promise<storage.CheckpointResult> {
357
+ return await this.commit(lsn, { createEmptyCheckpoints: true });
358
+ }
359
+
360
+ async setResumeLsn(lsn: string): Promise<void> {
361
+ await this.db.sync_rules.updateOne(
362
+ {
363
+ _id: this.replicationStreamId
364
+ },
365
+ {
366
+ $set: {
367
+ snapshot_lsn: lsn
368
+ }
369
+ },
370
+ { session: this.session }
371
+ );
372
+ }
373
+
374
+ async markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void> {
375
+ await this.db.sync_rules.updateOne(
376
+ {
377
+ _id: this.replicationStreamId
378
+ },
379
+ {
380
+ $set: {
381
+ snapshot_done: true,
382
+ last_keepalive_ts: new Date()
383
+ },
384
+ $max: {
385
+ no_checkpoint_before: no_checkpoint_before_lsn
386
+ }
387
+ },
388
+ { session: this.session }
389
+ );
390
+ }
391
+
392
+ async markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void> {
393
+ await this.withTransaction(async () => {
394
+ // Protect against race conditions
395
+ const blockingTables = await this.db
396
+ .sourceTablesV1(this.replicationStreamId)
397
+ .find(
398
+ {
399
+ group_id: this.replicationStreamId,
400
+ snapshot_done: false
401
+ },
402
+ {
403
+ session: this.session,
404
+ projection: { schema_name: 1, table_name: 1 }
405
+ }
406
+ )
407
+ .toArray();
408
+
409
+ if (blockingTables.length > 0) {
410
+ if (options?.throwOnConflict ?? true) {
411
+ throw new ReplicationAssertionError(
412
+ `Cannot mark snapshot done while source tables still require snapshotting. Tables: ${blockingTables.map((t) => `${t.schema_name}.${t.table_name}`).join(', ')}`
413
+ );
414
+ } else {
415
+ return;
416
+ }
417
+ }
418
+
419
+ await this.markAllSnapshotDone(no_checkpoint_before_lsn);
420
+ });
421
+ }
422
+
423
+ async markTableSnapshotRequired(_table: storage.SourceTable): Promise<void> {
424
+ await this.db.sync_rules.updateOne(
425
+ {
426
+ _id: this.replicationStreamId
427
+ },
428
+ {
429
+ $set: {
430
+ snapshot_done: false
431
+ }
432
+ },
433
+ { session: this.session }
434
+ );
435
+ }
436
+
437
+ async markTableSnapshotDone(
438
+ tables: storage.SourceTable[],
439
+ no_checkpoint_before_lsn?: string
440
+ ): Promise<storage.SourceTable[]> {
441
+ const session = this.session;
442
+ const ids = tables.map((table) => mongoTableId(table.id));
443
+
444
+ await this.withTransaction(async () => {
445
+ await this.db.sourceTablesV1(this.replicationStreamId).updateMany(
446
+ { _id: { $in: ids } },
447
+ {
448
+ $set: {
449
+ snapshot_done: true
450
+ },
451
+ $unset: {
452
+ snapshot_status: 1
453
+ }
454
+ },
455
+ { session }
456
+ );
457
+
458
+ if (no_checkpoint_before_lsn != null) {
459
+ await this.db.sync_rules.updateOne(
460
+ {
461
+ _id: this.replicationStreamId
462
+ },
463
+ {
464
+ $set: {
465
+ last_keepalive_ts: new Date()
466
+ },
467
+ $max: {
468
+ no_checkpoint_before: no_checkpoint_before_lsn
469
+ }
470
+ },
471
+ { session: this.session }
472
+ );
473
+ }
474
+ });
475
+ return tables.map((table) => {
476
+ const copy = table.clone();
477
+ copy.snapshotComplete = true;
478
+ return copy;
479
+ });
480
+ }
481
+
482
+ private async autoActivate(lsn: string): Promise<void> {
483
+ if (!this.needsActivation) {
484
+ return;
485
+ }
486
+
487
+ const session = this.session;
488
+ let activated = false;
489
+ await session.withTransaction(async () => {
490
+ const doc = (await this.db.sync_rules.findOne(
491
+ { _id: this.replicationStreamId },
492
+ { session }
493
+ )) as SyncRuleDocumentV1;
494
+ if (doc && doc.state == storage.SyncRuleState.PROCESSING && doc.snapshot_done && doc.last_checkpoint != null) {
495
+ await this.db.sync_rules.updateOne(
496
+ {
497
+ _id: this.replicationStreamId
498
+ },
499
+ {
500
+ $set: {
501
+ state: storage.SyncRuleState.ACTIVE
502
+ }
503
+ },
504
+ { session }
505
+ );
506
+
507
+ await this.db.sync_rules.updateMany(
508
+ {
509
+ _id: { $ne: this.replicationStreamId },
510
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
511
+ },
512
+ {
513
+ $set: {
514
+ state: storage.SyncRuleState.STOP
515
+ }
516
+ },
517
+ { session }
518
+ );
519
+ activated = true;
520
+ } else if (doc?.state != storage.SyncRuleState.PROCESSING) {
521
+ this.needsActivation = false;
522
+ }
523
+ });
524
+ if (activated) {
525
+ this.logger.info(`Activated new replication stream at ${lsn}`);
526
+ await this.db.notifyCheckpoint();
527
+ this.needsActivation = false;
528
+ }
529
+ }
32
530
  }
@@ -1,11 +1,20 @@
1
+ import * as lib_mongo from '@powersync/lib-service-mongodb';
1
2
  import {
3
+ addPartialChecksums,
2
4
  bson,
3
5
  BucketChecksum,
4
6
  FetchPartialBucketChecksum,
5
7
  InternalOpId,
6
- PartialChecksumMap
8
+ isPartialChecksum,
9
+ PartialChecksumMap,
10
+ PartialOrFullChecksum
7
11
  } from '@powersync/service-core';
8
- import { FetchPartialBucketChecksumByBucket, MongoChecksums } from '../MongoChecksums.js';
12
+ import {
13
+ checksumFromAggregate,
14
+ DEFAULT_OPERATION_BATCH_LIMIT,
15
+ FetchPartialBucketChecksumByBucket,
16
+ MongoChecksums
17
+ } from '../MongoChecksums.js';
9
18
  import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
10
19
 
11
20
  export class MongoChecksumsV1 extends MongoChecksums {
@@ -14,7 +23,8 @@ export class MongoChecksumsV1 extends MongoChecksums {
14
23
  async computePartialChecksumsDirectByBucket(
15
24
  batch: FetchPartialBucketChecksumByBucket[]
16
25
  ): Promise<PartialChecksumMap> {
17
- return this.computePartialChecksumsForCollection(batch, this.db.bucketDataV1, (request) => ({
26
+ const collection = this.db.bucketDataV1;
27
+ const createFilter = (request: FetchPartialBucketChecksumByBucket) => ({
18
28
  _id: {
19
29
  $gt: {
20
30
  g: this.group_id,
@@ -27,7 +37,130 @@ export class MongoChecksumsV1 extends MongoChecksums {
27
37
  o: request.end
28
38
  }
29
39
  }
30
- }));
40
+ });
41
+
42
+ const batchLimit = this.options?.operationBatchLimit ?? DEFAULT_OPERATION_BATCH_LIMIT;
43
+
44
+ // Map requests by bucket. We adjust this as we get partial results.
45
+ let requests = new Map<string, FetchPartialBucketChecksumByBucket>();
46
+ for (let request of batch) {
47
+ requests.set(request.bucket, request);
48
+ }
49
+
50
+ const partialChecksums = new Map<string, PartialOrFullChecksum>();
51
+
52
+ while (requests.size > 0) {
53
+ const filters = Array.from(requests.values(), createFilter);
54
+
55
+ // Historically, checksum may be stored as 'int' or 'double'.
56
+ // More recently, this should be a 'long'.
57
+ // $toLong ensures that we always sum it as a long, avoiding inaccuracies in the calculations.
58
+ const checksumLong = this.storageConfig.longChecksums ? '$checksum' : { $toLong: '$checksum' };
59
+
60
+ // Aggregate over a max of `batchLimit` operations at a time.
61
+ // Let's say we have 3 buckets (A, B, C), each with 10 operations, and our batch limit is 12.
62
+ // Then we'll do three batches:
63
+ // 1. Query: A[1-end], B[1-end], C[1-end]
64
+ // Returns: A[1-10], B[1-2]
65
+ // 2. Query: B[3-end], C[1-end]
66
+ // Returns: B[3-10], C[1-4]
67
+ // 3. Query: C[5-end]
68
+ // Returns: C[5-10]
69
+ const aggregate = await collection
70
+ .aggregate(
71
+ [
72
+ {
73
+ $match: {
74
+ $or: filters
75
+ }
76
+ },
77
+ // sort and limit _before_ grouping
78
+ { $sort: { _id: 1 } },
79
+ { $limit: batchLimit },
80
+ {
81
+ $group: {
82
+ _id: '$_id.b',
83
+ checksum_total: { $sum: checksumLong },
84
+ count: { $sum: 1 },
85
+ has_clear_op: {
86
+ $max: {
87
+ $cond: [{ $eq: ['$op', 'CLEAR'] }, 1, 0]
88
+ }
89
+ },
90
+ last_op: { $max: '$_id.o' }
91
+ }
92
+ },
93
+ // Sort the aggregated results (100 max, so should be fast).
94
+ // This is important to identify which buckets we have partial data for.
95
+ { $sort: { _id: 1 } }
96
+ ],
97
+ {
98
+ session: undefined,
99
+ readConcern: 'snapshot',
100
+ maxTimeMS: lib_mongo.MONGO_CHECKSUM_TIMEOUT_MS
101
+ }
102
+ )
103
+ .toArray()
104
+ .catch((e) => {
105
+ throw lib_mongo.mapQueryError(e, 'while reading checksums');
106
+ });
107
+
108
+ let batchCount = 0;
109
+ let limitReached = false;
110
+ for (let doc of aggregate) {
111
+ const bucket = doc._id;
112
+ const checksum = checksumFromAggregate(doc);
113
+
114
+ const existing = partialChecksums.get(bucket);
115
+ if (existing != null) {
116
+ partialChecksums.set(bucket, addPartialChecksums(bucket, existing, checksum));
117
+ } else {
118
+ partialChecksums.set(bucket, checksum);
119
+ }
120
+
121
+ batchCount += doc.count;
122
+ if (batchCount == batchLimit) {
123
+ // Limit reached. Request more in the next batch.
124
+ // Note that this only affects the _last_ bucket in a batch.
125
+ limitReached = true;
126
+ const req = requests.get(bucket);
127
+ requests.set(bucket, {
128
+ ...req!,
129
+ start: doc.last_op
130
+ });
131
+ } else {
132
+ // All done for this bucket
133
+ requests.delete(bucket);
134
+ }
135
+ }
136
+ if (!limitReached) {
137
+ break;
138
+ }
139
+ }
140
+
141
+ return new Map<string, PartialOrFullChecksum>(
142
+ batch.map((request) => {
143
+ const bucket = request.bucket;
144
+ let partialChecksum = partialChecksums.get(bucket);
145
+ // Could be null if we got no data
146
+ if (partialChecksum == null) {
147
+ partialChecksum = {
148
+ bucket,
149
+ partialCount: 0,
150
+ partialChecksum: 0
151
+ };
152
+ }
153
+ if (request.start == null && isPartialChecksum(partialChecksum)) {
154
+ partialChecksum = {
155
+ bucket,
156
+ count: partialChecksum.partialCount,
157
+ checksum: partialChecksum.partialChecksum
158
+ };
159
+ }
160
+
161
+ return [bucket, partialChecksum];
162
+ })
163
+ );
31
164
  }
32
165
 
33
166
  protected async fetchPreStates(
@@ -1,7 +1,7 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
2
  import { ReplicationAssertionError } from '@powersync/lib-services-framework';
3
3
  import { storage } from '@powersync/service-core';
4
- import { BucketDefinitionId } from '../BucketDefinitionMapping.js';
4
+ import { BucketDefinitionId } from '@powersync/service-sync-rules';
5
5
  import { SingleBucketStore } from '../common/SingleBucketStore.js';
6
6
  import { BucketStateDocumentBase, LEGACY_BUCKET_DATA_DEFINITION_ID } from '../models.js';
7
7
  import { DirtyBucket, MongoCompactor } from '../MongoCompactor.js';