@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
@@ -0,0 +1,38 @@
1
+ import { BucketDataDoc } from '../common/BucketDataDoc.js';
2
+
3
+ export const DEFAULT_MAX_DOC_SIZE_BYTES = 1024 * 1024; // 1MB
4
+
5
+ /**
6
+ * Split an array of bucket data operations into chunks where each chunk's
7
+ * total data size stays close to `maxDocSizeBytes`.
8
+ *
9
+ * The threshold is a target, not a hard ceiling — a single operation whose
10
+ * `data` field exceeds the limit is placed in its own chunk and is NOT split.
11
+ */
12
+ export function chunkBucketData(
13
+ operations: BucketDataDoc[],
14
+ maxDocSizeBytes = DEFAULT_MAX_DOC_SIZE_BYTES
15
+ ): BucketDataDoc[][] {
16
+ const chunks: BucketDataDoc[][] = [];
17
+ let currentChunk: BucketDataDoc[] = [];
18
+ let currentSize = 0;
19
+
20
+ for (const op of operations) {
21
+ const opSize = op.data?.length ?? 0;
22
+
23
+ if (currentSize + opSize > maxDocSizeBytes && currentChunk.length > 0) {
24
+ chunks.push(currentChunk);
25
+ currentChunk = [];
26
+ currentSize = 0;
27
+ }
28
+
29
+ currentChunk.push(op);
30
+ currentSize += opSize;
31
+ }
32
+
33
+ if (currentChunk.length > 0) {
34
+ chunks.push(currentChunk);
35
+ }
36
+
37
+ return chunks;
38
+ }
@@ -1,19 +1,113 @@
1
- import { InternalOpId } from '@powersync/service-core';
1
+ import {
2
+ InternalOpId,
3
+ PersistedDefinitionMapping,
4
+ SerializedSyncPlan,
5
+ SyncRuleState,
6
+ deserializeParameterLookup as deserializeParameterLookupCore
7
+ } from '@powersync/service-core';
8
+ import {
9
+ BucketDefinitionId,
10
+ ParameterIndexId,
11
+ ScopedParameterLookup,
12
+ SqliteJsonValue
13
+ } from '@powersync/service-sync-rules';
2
14
  import * as bson from 'bson';
3
- import { BucketDefinitionId, ParameterIndexId } from '../BucketDefinitionMapping.js';
4
- import { BucketDataDoc, BucketKey } from '../common/BucketDataDoc.js';
5
15
  import {
6
- BucketDataDocumentBase,
7
16
  BucketDataKey,
8
17
  BucketParameterDocumentBase,
9
18
  BucketStateDocumentBase,
10
19
  CurrentBucket,
20
+ OpType,
11
21
  ReplicaId,
12
- SourceTableDocument,
22
+ SourceTableDocumentSnapshotStatus,
13
23
  SourceTableKey,
24
+ SyncRuleCheckpointFields,
25
+ SyncRuleDocumentBase,
14
26
  TaggedBucketParameterDocument
15
27
  } from '../models.js';
16
28
 
29
+ /**
30
+ * Embedded in sync_rules.sync_configs.
31
+ */
32
+ export interface SyncRuleConfigStateV3 extends SyncRuleCheckpointFields {
33
+ _id: bson.ObjectId;
34
+
35
+ /**
36
+ * If false, we cannot create any checkpoints.
37
+ */
38
+ snapshot_done: boolean;
39
+
40
+ state: SyncRuleState;
41
+ }
42
+
43
+ /**
44
+ * Represents the state of a replication stream, in the sync_rules collection.
45
+ *
46
+ * Differences from V1:
47
+ * 1. The static config is moved into a separate sync_configs collection.
48
+ * 2. The same replication stream may be shared by multiple sync config instances.
49
+ */
50
+ export interface ReplicationStreamDocumentV3 extends SyncRuleDocumentBase {
51
+ storage_version: number;
52
+
53
+ /**
54
+ * These contain the checkpoint/state per sync config.
55
+ *
56
+ * In common cases we'd have one active config or one active + one processing config,
57
+ * but the model allows multiple configs in any state.
58
+ */
59
+ sync_configs: SyncRuleConfigStateV3[];
60
+
61
+ /**
62
+ * The monotonic head of the stream's op sequence: the highest op id persisted to bucket data,
63
+ * whether or not yet covered by a checkpoint.
64
+ *
65
+ * This is shared across all sync configs of the stream (they share the global op sequence).
66
+ * It is never cleared, only `$max`-advanced. A newly-appended config that replicates nothing
67
+ * adopts this value as its checkpoint rather than starting at 0.
68
+ *
69
+ * Stored as a mongo Long, nullable.
70
+ */
71
+ last_persisted_op?: bigint | null;
72
+
73
+ /**
74
+ * The stream's replication position: all source changes up to this LSN have been processed,
75
+ * and the resulting ops persisted. Replication resumes from here.
76
+ *
77
+ * Like {@link last_persisted_op}, this is shared across all sync configs of the stream -
78
+ * per-config last_checkpoint_lsn values are consistency markers, not replication positions.
79
+ *
80
+ * Set via setResumeLsn() (snapshot start, and per-batch progress during streaming), and
81
+ * advanced on every commit/keepalive - including checkpoint-blocked ones, since commit
82
+ * flushes first and blocking only delays consistency markers, not data persistence.
83
+ */
84
+ resume_lsn?: string | null;
85
+ }
86
+
87
+ /**
88
+ * Static sync config definition.
89
+ *
90
+ * This should be treated as immutable - we don't update this after initial creation.
91
+ */
92
+ export interface SyncConfigDefinition {
93
+ _id: bson.ObjectId;
94
+ created_at: Date;
95
+ storage_version: number;
96
+ /**
97
+ * The related SyncRuleDocumentV3.
98
+ *
99
+ * Note that a specific sync config definition never moves between replication streams. Instead, we can create a new copy for the new replication stream.
100
+ *
101
+ * When terminating a specific sync config definition, we remove the reference from replication stream -> sync config, but this reference here remains as a historical record.
102
+ */
103
+ replication_stream_id: number;
104
+
105
+ content: string;
106
+ serialized_plan?: SerializedSyncPlan | null;
107
+
108
+ rule_mapping: PersistedDefinitionMapping;
109
+ }
110
+
17
111
  export interface CurrentBucketV3 extends CurrentBucket {
18
112
  def: BucketDefinitionId;
19
113
  }
@@ -40,50 +134,26 @@ export interface BucketParameterDocumentV3 extends BucketParameterDocumentBase<S
40
134
 
41
135
  export type BucketDataKeyV3 = BucketDataKey;
42
136
 
43
- export interface BucketDataDocumentV3 extends BucketDataDocumentBase {
44
- _id: BucketDataKeyV3;
45
- }
46
-
47
- export function serializeBucketDataV3(document: BucketDataDoc): BucketDataDocumentV3 {
48
- const { bucketKey, o } = document;
49
- return {
50
- _id: {
51
- b: bucketKey.bucket,
52
- o: o
53
- },
54
- // List fields directly, so that we don't accidentally persist any unknown fields
55
- op: document.op,
56
- source_table: document.source_table,
57
- source_key: document.source_key,
58
- table: document.table,
59
- row_id: document.row_id,
60
- checksum: document.checksum,
61
- data: document.data,
62
- target_op: document.target_op
63
- };
64
- }
65
-
66
- export function loadBucketDataDocumentV3(
67
- context: Pick<BucketKey, 'replicationStreamId' | 'definitionId'>,
68
- doc: BucketDataDocumentV3
69
- ): BucketDataDoc {
70
- const { _id, ...rest } = doc;
71
- return {
72
- bucketKey: {
73
- ...context,
74
- bucket: _id.b
75
- },
76
- o: _id.o,
77
- ...rest
78
- };
79
- }
80
-
81
137
  export function taggedBucketParameterDocumentToV3(document: TaggedBucketParameterDocument): BucketParameterDocumentV3 {
82
138
  const { index: _index, ...rest } = document;
83
139
  return rest as BucketParameterDocumentV3;
84
140
  }
85
141
 
86
- export interface SourceTableDocumentV3 extends SourceTableDocument {
142
+ export interface ReplicaIdColumn {
143
+ name: string;
144
+ type_oid?: number;
145
+ type?: string;
146
+ }
147
+
148
+ export interface SourceTableDocumentV3 {
149
+ _id: bson.ObjectId;
150
+ connection_id: number;
151
+ relation_id: number | string | undefined;
152
+ schema_name: string;
153
+ table_name: string;
154
+ replica_id_columns: ReplicaIdColumn[];
155
+ snapshot_done: boolean;
156
+ snapshot_status: SourceTableDocumentSnapshotStatus | undefined;
87
157
  bucket_data_source_ids: BucketDefinitionId[];
88
158
  parameter_lookup_source_ids: ParameterIndexId[];
89
159
  latest_pending_delete?: InternalOpId | undefined;
@@ -94,3 +164,39 @@ export interface BucketStateDocumentV3 extends BucketStateDocumentBase {
94
164
  d: BucketDefinitionId;
95
165
  };
96
166
  }
167
+
168
+ export interface BucketOperation {
169
+ o: bigint;
170
+ op: OpType;
171
+ source_table?: bson.ObjectId;
172
+ source_key?: ReplicaId;
173
+ table?: string;
174
+ row_id?: string;
175
+ checksum: bigint;
176
+ data: string | null;
177
+ }
178
+
179
+ export interface BucketDataDocumentV3 {
180
+ _id: BucketDataKey;
181
+ min_op: bigint;
182
+ checksum: bigint;
183
+ count: number;
184
+ size: number;
185
+ target_op?: bigint | null;
186
+ ops: BucketOperation[];
187
+ }
188
+
189
+ export function serializeParameterLookup(lookup: ScopedParameterLookup): bson.Binary {
190
+ return new bson.Binary(bson.serialize({ l: lookup.values.slice(2) }));
191
+ }
192
+
193
+ export function deserializeParameterLookup(lookup: bson.Binary, indexId: ParameterIndexId): SqliteJsonValue[] {
194
+ return [indexId, '', ...deserializeParameterLookupCore(lookup)];
195
+ }
196
+
197
+ export function taggedBucketParameterDocumentToTagged(
198
+ document: TaggedBucketParameterDocument
199
+ ): BucketParameterDocumentV3 {
200
+ const { index: _index, ...rest } = document;
201
+ return rest as BucketParameterDocumentV3;
202
+ }
@@ -0,0 +1,392 @@
1
+ import { ReplicationAssertionError } from '@powersync/lib-services-framework';
2
+ import { BucketDefinitionMapping, ColumnDescriptor, storage } from '@powersync/service-core';
3
+ import {
4
+ BucketDataSource,
5
+ BucketDefinitionId,
6
+ HydratedSyncConfig,
7
+ MatchingSources,
8
+ ParameterIndexId,
9
+ ParameterIndexLookupCreator,
10
+ SourceTableRef
11
+ } from '@powersync/service-sync-rules';
12
+ import * as bson from 'bson';
13
+ import { ReplicaIdColumn, SourceTableDocumentV3 } from './models.js';
14
+
15
+ export interface SourceTableIdentity {
16
+ schema: string;
17
+ name: string;
18
+ objectId: number | string | undefined;
19
+ replicaIdColumns: ReplicaIdColumn[];
20
+ }
21
+
22
+ export interface SourceTableMembershipIds {
23
+ bucketDataSourceIds: BucketDefinitionId[];
24
+ parameterLookupSourceIds: ParameterIndexId[];
25
+ }
26
+
27
+ export interface SourceTableDesiredResolution {
28
+ bucketSourceById: Map<BucketDefinitionId, BucketDataSource>;
29
+ parameterLookupSourceById: Map<ParameterIndexId, ParameterIndexLookupCreator>;
30
+ triggersEvent: boolean;
31
+ }
32
+
33
+ export interface SourceTableReconciliationContext {
34
+ connectionId: number;
35
+ connectionTag: string;
36
+ identity: SourceTableIdentity;
37
+ storeCurrentData: boolean;
38
+ syncConfig: HydratedSyncConfig;
39
+ mapping: BucketDefinitionMapping;
40
+ desired: SourceTableDesiredResolution;
41
+ }
42
+
43
+ export interface SourceTableReconciliationPlan {
44
+ /** Retained source tables, with memberships narrowed to the desired set. */
45
+ tables: storage.SourceTable[];
46
+ /** Membership narrowing to persist for retained snapshot-incomplete docs. */
47
+ narrowingUpdates: SourceTableMembershipUpdate[];
48
+ /**
49
+ * 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.
51
+ */
52
+ newTableMemberships: SourceTableMembershipIds | null;
53
+ /** Identity-overlapping docs that conflict with the current identity and must be dropped. */
54
+ dropDocs: SourceTableDocumentV3[];
55
+ }
56
+
57
+ export interface SourceTableMembershipUpdate {
58
+ id: bson.ObjectId;
59
+ memberships: SourceTableMembershipIds;
60
+ }
61
+
62
+ export interface NewSourceTable {
63
+ doc: SourceTableDocumentV3;
64
+ table: storage.SourceTable;
65
+ }
66
+
67
+ export function sourceTableDesiredResolution(
68
+ syncConfig: HydratedSyncConfig,
69
+ ref: SourceTableRef,
70
+ mapping: BucketDefinitionMapping
71
+ ): SourceTableDesiredResolution {
72
+ const matchingSources = syncConfig.getMatchingSources(ref);
73
+ return {
74
+ bucketSourceById: new Map(
75
+ matchingSources.bucketDataSources.map((source) => [mapping.bucketSourceId(source), source] as const)
76
+ ),
77
+ parameterLookupSourceById: new Map(
78
+ matchingSources.parameterLookupSources.map((source) => [mapping.parameterLookupId(source), source] as const)
79
+ ),
80
+ triggersEvent: syncConfig.tableTriggersEvent(ref)
81
+ };
82
+ }
83
+
84
+ export function planSourceTableReconciliation(
85
+ candidateDocs: SourceTableDocumentV3[],
86
+ context: SourceTableReconciliationContext
87
+ ): SourceTableReconciliationPlan {
88
+ return new SourceTableReconciliationPlanner(context).plan(candidateDocs);
89
+ }
90
+
91
+ function sameStringArray(left: string[], right: string[]) {
92
+ return left.length == right.length && left.every((value, index) => value == right[index]);
93
+ }
94
+
95
+ function matchingSourcesFor(
96
+ desired: SourceTableDesiredResolution,
97
+ memberships: SourceTableMembershipIds
98
+ ): MatchingSources {
99
+ return {
100
+ bucketDataSources: memberships.bucketDataSourceIds.map((id) => desired.bucketSourceById.get(id)!),
101
+ parameterLookupSources: memberships.parameterLookupSourceIds.map((id) => desired.parameterLookupSourceById.get(id)!)
102
+ };
103
+ }
104
+
105
+ function intersectMembershipIds(
106
+ doc: SourceTableDocumentV3,
107
+ desired: SourceTableDesiredResolution
108
+ ): SourceTableMembershipIds {
109
+ return {
110
+ 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))
112
+ };
113
+ }
114
+
115
+ function hasMembershipIds(memberships: SourceTableMembershipIds) {
116
+ return memberships.bucketDataSourceIds.length > 0 || memberships.parameterLookupSourceIds.length > 0;
117
+ }
118
+
119
+ function sameMembershipIds(doc: SourceTableDocumentV3, memberships: SourceTableMembershipIds) {
120
+ return (
121
+ sameStringArray(doc.bucket_data_source_ids, memberships.bucketDataSourceIds) &&
122
+ sameStringArray(doc.parameter_lookup_source_ids, memberships.parameterLookupSourceIds)
123
+ );
124
+ }
125
+
126
+ class SourceTableReconciliationPlanner {
127
+ private readonly coveredBucketDataSourceIds = new Set<string>();
128
+ private readonly coveredParameterLookupSourceIds = new Set<string>();
129
+ private readonly retainedDocIds = new Set<string>();
130
+ private readonly tables: storage.SourceTable[] = [];
131
+ private readonly narrowingUpdates: SourceTableMembershipUpdate[] = [];
132
+ private retainedEventOnlyTable = false;
133
+
134
+ constructor(private readonly context: SourceTableReconciliationContext) {}
135
+
136
+ plan(candidateDocs: SourceTableDocumentV3[]): SourceTableReconciliationPlan {
137
+ const { identity } = this.context;
138
+ for (const doc of candidateDocs) {
139
+ if (matchingSourceTableIdentity(doc, identity)) {
140
+ this.retainDoc(doc);
141
+ }
142
+ }
143
+
144
+ return {
145
+ tables: this.tables,
146
+ narrowingUpdates: this.narrowingUpdates,
147
+ 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
+ )
153
+ };
154
+ }
155
+
156
+ private retainDoc(doc: SourceTableDocumentV3) {
157
+ const memberships = intersectMembershipIds(doc, this.context.desired);
158
+ const coversDesiredMembership = hasMembershipIds(memberships);
159
+ const coversEventOnlyTable =
160
+ !this.desiredHasMembership() && this.context.desired.triggersEvent && !this.retainedEventOnlyTable;
161
+
162
+ this.recordCoverage(doc, memberships);
163
+ this.planNarrowingUpdate(doc, memberships, coversDesiredMembership, coversEventOnlyTable);
164
+
165
+ if (coversDesiredMembership || coversEventOnlyTable) {
166
+ this.retainedEventOnlyTable ||= coversEventOnlyTable;
167
+ this.retainedDocIds.add(doc._id.toHexString());
168
+ this.tables.push(this.sourceTableFor(doc, memberships));
169
+ }
170
+ }
171
+
172
+ private desiredHasMembership() {
173
+ const { desired } = this.context;
174
+ return desired.bucketSourceById.size > 0 || desired.parameterLookupSourceById.size > 0;
175
+ }
176
+
177
+ private recordCoverage(doc: SourceTableDocumentV3, memberships: SourceTableMembershipIds) {
178
+ this.addCoverage(doc, 'bucket data source', this.coveredBucketDataSourceIds, memberships.bucketDataSourceIds);
179
+ this.addCoverage(
180
+ doc,
181
+ 'parameter lookup source',
182
+ this.coveredParameterLookupSourceIds,
183
+ memberships.parameterLookupSourceIds
184
+ );
185
+ }
186
+
187
+ // 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.
191
+ private addCoverage(doc: SourceTableDocumentV3, kind: string, covered: Set<string>, ids: string[]) {
192
+ const { schema, name } = this.context.identity;
193
+ for (const id of ids) {
194
+ if (covered.has(id)) {
195
+ throw new ReplicationAssertionError(
196
+ `Source table ${doc._id} duplicates coverage of ${kind} ${id} for ${schema}.${name}`
197
+ );
198
+ }
199
+ covered.add(id);
200
+ }
201
+ }
202
+
203
+ private planNarrowingUpdate(
204
+ doc: SourceTableDocumentV3,
205
+ memberships: SourceTableMembershipIds,
206
+ coversDesiredMembership: boolean,
207
+ coversEventOnlyTable: boolean
208
+ ) {
209
+ const shouldNarrow =
210
+ (coversDesiredMembership || coversEventOnlyTable) && !doc.snapshot_done && !sameMembershipIds(doc, memberships);
211
+
212
+ if (!shouldNarrow) {
213
+ return;
214
+ }
215
+
216
+ this.narrowingUpdates.push({
217
+ id: doc._id,
218
+ memberships
219
+ });
220
+ }
221
+
222
+ private newTableMemberships(): SourceTableMembershipIds | null {
223
+ const { desired } = this.context;
224
+ const uncovered: SourceTableMembershipIds = {
225
+ bucketDataSourceIds: [...desired.bucketSourceById.keys()].filter(
226
+ (id) => !this.coveredBucketDataSourceIds.has(id)
227
+ ),
228
+ parameterLookupSourceIds: [...desired.parameterLookupSourceById.keys()].filter(
229
+ (id) => !this.coveredParameterLookupSourceIds.has(id)
230
+ )
231
+ };
232
+ if (hasMembershipIds(uncovered) || (desired.triggersEvent && this.tables.length == 0)) {
233
+ return uncovered;
234
+ }
235
+ return null;
236
+ }
237
+
238
+ private sourceTableFor(doc: SourceTableDocumentV3, memberships: SourceTableMembershipIds): storage.SourceTable {
239
+ const { connectionTag, syncConfig, mapping, desired, storeCurrentData } = this.context;
240
+ const table = sourceTableFromDocument(
241
+ doc,
242
+ connectionTag,
243
+ syncConfig,
244
+ mapping,
245
+ matchingSourcesFor(desired, memberships),
246
+ memberships
247
+ );
248
+ table.storeCurrentData = storeCurrentData;
249
+ return table;
250
+ }
251
+ }
252
+
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
+ export function overlappingSourceTableFilter(
274
+ connectionId: number,
275
+ identity: Pick<SourceTableIdentity, 'schema' | 'name' | 'objectId'>
276
+ ): Record<string, unknown> {
277
+ const clauses = [{ schema_name: identity.schema, table_name: identity.name }] as Record<string, unknown>[];
278
+ if (identity.objectId != null) {
279
+ clauses.push({ relation_id: identity.objectId });
280
+ }
281
+
282
+ return {
283
+ connection_id: connectionId,
284
+ $or: clauses
285
+ };
286
+ }
287
+
288
+ export function createNewSourceTable(
289
+ id: bson.ObjectId,
290
+ memberships: SourceTableMembershipIds,
291
+ context: SourceTableReconciliationContext
292
+ ): NewSourceTable {
293
+ const { connectionId, connectionTag, identity, syncConfig, mapping, desired, storeCurrentData } = context;
294
+ const doc: SourceTableDocumentV3 = {
295
+ _id: id,
296
+ connection_id: connectionId,
297
+ relation_id: identity.objectId,
298
+ schema_name: identity.schema,
299
+ table_name: identity.name,
300
+ replica_id_columns: identity.replicaIdColumns,
301
+ snapshot_done: false,
302
+ snapshot_status: undefined,
303
+ bucket_data_source_ids: memberships.bucketDataSourceIds,
304
+ parameter_lookup_source_ids: memberships.parameterLookupSourceIds
305
+ };
306
+ const table = sourceTableFromDocument(
307
+ doc,
308
+ connectionTag,
309
+ syncConfig,
310
+ mapping,
311
+ matchingSourcesFor(desired, memberships),
312
+ memberships
313
+ );
314
+ table.storeCurrentData = storeCurrentData;
315
+
316
+ return { doc, table };
317
+ }
318
+
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
+ function sourceTableMembershipsFromDocument(
331
+ doc: SourceTableDocumentV3,
332
+ syncConfig: HydratedSyncConfig,
333
+ mapping: BucketDefinitionMapping
334
+ ): MatchingSources {
335
+ const bucketDataSourceIds = new Set(doc.bucket_data_source_ids);
336
+ const parameterLookupSourceIds = new Set(doc.parameter_lookup_source_ids);
337
+
338
+ return {
339
+ bucketDataSources: syncConfig.bucketDataSources.filter((source) =>
340
+ bucketDataSourceIds.has(mapping.bucketSourceId(source))
341
+ ),
342
+ parameterLookupSources: syncConfig.bucketParameterLookupSources.filter((source) =>
343
+ parameterLookupSourceIds.has(mapping.parameterLookupId(source))
344
+ )
345
+ };
346
+ }
347
+
348
+ export function sourceTableFromDocument(
349
+ doc: SourceTableDocumentV3,
350
+ connectionTag: string,
351
+ syncConfig: HydratedSyncConfig,
352
+ mapping: BucketDefinitionMapping,
353
+ memberships?: MatchingSources,
354
+ membershipIds?: SourceTableMembershipIds
355
+ ): storage.SourceTable {
356
+ const resolvedMemberships = memberships ?? sourceTableMembershipsFromDocument(doc, syncConfig, mapping);
357
+ const resolvedMembershipIds = membershipIds ?? {
358
+ bucketDataSourceIds: resolvedMemberships.bucketDataSources.map((source) => mapping.bucketSourceId(source)),
359
+ parameterLookupSourceIds: resolvedMemberships.parameterLookupSources.map((source) =>
360
+ mapping.parameterLookupId(source)
361
+ )
362
+ };
363
+ const table = new storage.SourceTable({
364
+ id: doc._id,
365
+ ref: {
366
+ connectionTag,
367
+ schema: doc.schema_name,
368
+ name: doc.table_name
369
+ },
370
+ 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
+ snapshotComplete: doc.snapshot_done,
375
+ bucketDataSources: resolvedMemberships.bucketDataSources,
376
+ parameterLookupSources: resolvedMemberships.parameterLookupSources,
377
+ bucketDataSourceIds: new Set(resolvedMembershipIds.bucketDataSourceIds),
378
+ parameterLookupSourceIds: new Set(resolvedMembershipIds.parameterLookupSourceIds)
379
+ });
380
+ table.syncData = table.bucketDataSources.length > 0;
381
+ table.syncParameters = table.parameterLookupSources.length > 0;
382
+ table.syncEvent = syncConfig.tableTriggersEvent(table.ref);
383
+ table.snapshotStatus =
384
+ doc.snapshot_status == null
385
+ ? undefined
386
+ : {
387
+ lastKey: doc.snapshot_status.last_key?.buffer ?? null,
388
+ totalEstimatedCount: doc.snapshot_status.total_estimated_count,
389
+ replicatedCount: doc.snapshot_status.replicated_count
390
+ };
391
+ return table;
392
+ }
@@ -1,17 +1,25 @@
1
1
  export * as test_utils from '../utils/test-utils.js';
2
2
  export * from '../utils/util.js';
3
- export * from './implementation/BucketDefinitionMapping.js';
4
3
  export * from './implementation/common/PersistedBatch.js';
5
4
  export * from './implementation/createMongoSyncBucketStorage.js';
6
5
  export * from './implementation/db.js';
7
6
  export * from './implementation/models.js';
8
7
  export * from './implementation/MongoIdSequence.js';
9
- export * from './implementation/MongoPersistedSyncRules.js';
10
- export * from './implementation/MongoPersistedSyncRulesContent.js';
8
+ export * from './implementation/MongoParsedSyncConfigSet.js';
9
+ export * from './implementation/MongoPersistedReplicationStream.js';
11
10
  export * from './implementation/MongoStorageProvider.js';
12
11
  export * from './implementation/MongoSyncRulesLock.js';
13
12
  export * from './implementation/OperationBatch.js';
14
13
  export * from './implementation/v1/models.js';
15
- export * from './implementation/v3/models.js';
14
+ export { loadBucketDataDocument, serializeBucketData } from './implementation/v3/bucket-format.js';
15
+ export {
16
+ BucketDataDocumentV3,
17
+ BucketOperation,
18
+ RecordedLookupV3,
19
+ ReplicationStreamDocumentV3,
20
+ SyncConfigDefinition,
21
+ SyncRuleConfigStateV3,
22
+ taggedBucketParameterDocumentToTagged
23
+ } from './implementation/v3/models.js';
16
24
  export * from './MongoBucketStorage.js';
17
25
  export * from './MongoReportStorage.js';