@powersync/service-module-mongodb-storage 0.15.4 → 0.16.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 (193) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js +1 -1
  3. package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js.map +1 -1
  4. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js +2 -2
  5. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +1 -1
  6. package/dist/storage/MongoBucketStorage.d.ts +2 -2
  7. package/dist/storage/MongoBucketStorage.js +47 -34
  8. package/dist/storage/MongoBucketStorage.js.map +1 -1
  9. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +17 -0
  10. package/dist/storage/implementation/BucketDefinitionMapping.js +58 -0
  11. package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
  12. package/dist/storage/implementation/MongoBucketBatch.d.ts +16 -14
  13. package/dist/storage/implementation/MongoBucketBatch.js +80 -115
  14. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  15. package/dist/storage/implementation/MongoBucketBatchShared.d.ts +5 -0
  16. package/dist/storage/implementation/MongoBucketBatchShared.js +8 -0
  17. package/dist/storage/implementation/MongoBucketBatchShared.js.map +1 -0
  18. package/dist/storage/implementation/MongoChecksums.d.ts +28 -17
  19. package/dist/storage/implementation/MongoChecksums.js +13 -72
  20. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  21. package/dist/storage/implementation/MongoCompactor.d.ts +98 -58
  22. package/dist/storage/implementation/MongoCompactor.js +229 -296
  23. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  24. package/dist/storage/implementation/MongoParameterCompactor.d.ts +11 -6
  25. package/dist/storage/implementation/MongoParameterCompactor.js +11 -8
  26. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  27. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +14 -0
  28. package/dist/storage/implementation/MongoPersistedSyncRules.js +64 -0
  29. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -0
  30. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +3 -0
  31. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +9 -0
  32. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  33. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +47 -29
  34. package/dist/storage/implementation/MongoSyncBucketStorage.js +94 -387
  35. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  36. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +5 -3
  37. package/dist/storage/implementation/MongoSyncRulesLock.js +12 -10
  38. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  39. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +1 -1
  40. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  41. package/dist/storage/implementation/OperationBatch.js +1 -1
  42. package/dist/storage/implementation/common/BucketDataDoc.d.ts +35 -0
  43. package/dist/storage/implementation/common/BucketDataDoc.js +2 -0
  44. package/dist/storage/implementation/common/BucketDataDoc.js.map +1 -0
  45. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +13 -0
  46. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +2 -0
  47. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +1 -0
  48. package/dist/storage/implementation/common/PersistedBatch.d.ts +108 -0
  49. package/dist/storage/implementation/common/PersistedBatch.js +237 -0
  50. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -0
  51. package/dist/storage/implementation/common/SingleBucketStore.d.ts +54 -0
  52. package/dist/storage/implementation/common/SingleBucketStore.js +3 -0
  53. package/dist/storage/implementation/common/SingleBucketStore.js.map +1 -0
  54. package/dist/storage/implementation/common/SourceRecordStore.d.ts +36 -0
  55. package/dist/storage/implementation/common/SourceRecordStore.js +2 -0
  56. package/dist/storage/implementation/common/SourceRecordStore.js.map +1 -0
  57. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +27 -0
  58. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +57 -0
  59. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -0
  60. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +7 -0
  61. package/dist/storage/implementation/createMongoSyncBucketStorage.js +9 -0
  62. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -0
  63. package/dist/storage/implementation/db.d.ts +32 -35
  64. package/dist/storage/implementation/db.js +77 -99
  65. package/dist/storage/implementation/db.js.map +1 -1
  66. package/dist/storage/implementation/models.d.ts +62 -33
  67. package/dist/storage/implementation/models.js +20 -1
  68. package/dist/storage/implementation/models.js.map +1 -1
  69. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +13 -0
  70. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +22 -0
  71. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -0
  72. package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +12 -0
  73. package/dist/storage/implementation/v1/MongoChecksumsV1.js +56 -0
  74. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -0
  75. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +23 -0
  76. package/dist/storage/implementation/v1/MongoCompactorV1.js +52 -0
  77. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -0
  78. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +9 -0
  79. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -0
  80. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -0
  81. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +41 -0
  82. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +283 -0
  83. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -0
  84. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +26 -0
  85. package/dist/storage/implementation/v1/PersistedBatchV1.js +183 -0
  86. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -0
  87. package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +18 -0
  88. package/dist/storage/implementation/v1/SingleBucketStoreV1.js +57 -0
  89. package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -0
  90. package/dist/storage/implementation/v1/SourceRecordStoreV1.d.ts +19 -0
  91. package/dist/storage/implementation/v1/SourceRecordStoreV1.js +105 -0
  92. package/dist/storage/implementation/v1/SourceRecordStoreV1.js.map +1 -0
  93. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +12 -0
  94. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +20 -0
  95. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -0
  96. package/dist/storage/implementation/v1/models.d.ts +34 -0
  97. package/dist/storage/implementation/v1/models.js +37 -0
  98. package/dist/storage/implementation/v1/models.js.map +1 -0
  99. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +13 -0
  100. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +34 -0
  101. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -0
  102. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +15 -0
  103. package/dist/storage/implementation/v3/MongoChecksumsV3.js +84 -0
  104. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -0
  105. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -0
  106. package/dist/storage/implementation/v3/MongoCompactorV3.js +68 -0
  107. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -0
  108. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +9 -0
  109. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +18 -0
  110. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  111. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +5 -0
  112. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +9 -0
  113. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +1 -0
  114. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +41 -0
  115. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +407 -0
  116. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -0
  117. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +29 -0
  118. package/dist/storage/implementation/v3/PersistedBatchV3.js +259 -0
  119. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -0
  120. package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +18 -0
  121. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +48 -0
  122. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -0
  123. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +22 -0
  124. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +164 -0
  125. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -0
  126. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +21 -0
  127. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +71 -0
  128. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -0
  129. package/dist/storage/implementation/v3/models.d.ts +43 -0
  130. package/dist/storage/implementation/v3/models.js +34 -0
  131. package/dist/storage/implementation/v3/models.js.map +1 -0
  132. package/dist/storage/storage-index.d.ts +6 -3
  133. package/dist/storage/storage-index.js +6 -3
  134. package/dist/storage/storage-index.js.map +1 -1
  135. package/dist/utils/util.d.ts +10 -3
  136. package/dist/utils/util.js +24 -3
  137. package/dist/utils/util.js.map +1 -1
  138. package/package.json +9 -9
  139. package/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts +1 -1
  140. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +6 -6
  141. package/src/storage/MongoBucketStorage.ts +92 -59
  142. package/src/storage/implementation/BucketDefinitionMapping.ts +72 -0
  143. package/src/storage/implementation/MongoBucketBatch.ts +110 -144
  144. package/src/storage/implementation/MongoBucketBatchShared.ts +11 -0
  145. package/src/storage/implementation/MongoChecksums.ts +52 -75
  146. package/src/storage/implementation/MongoCompactor.ts +374 -404
  147. package/src/storage/implementation/MongoParameterCompactor.ts +37 -24
  148. package/src/storage/implementation/MongoPersistedSyncRules.ts +76 -0
  149. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +17 -0
  150. package/src/storage/implementation/MongoSyncBucketStorage.ts +181 -455
  151. package/src/storage/implementation/MongoSyncRulesLock.ts +11 -13
  152. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +3 -1
  153. package/src/storage/implementation/OperationBatch.ts +1 -1
  154. package/src/storage/implementation/common/BucketDataDoc.ts +37 -0
  155. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +15 -0
  156. package/src/storage/implementation/common/PersistedBatch.ts +364 -0
  157. package/src/storage/implementation/common/SingleBucketStore.ts +63 -0
  158. package/src/storage/implementation/common/SourceRecordStore.ts +49 -0
  159. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +80 -0
  160. package/src/storage/implementation/createMongoSyncBucketStorage.ts +25 -0
  161. package/src/storage/implementation/db.ts +105 -129
  162. package/src/storage/implementation/models.ts +82 -36
  163. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +32 -0
  164. package/src/storage/implementation/v1/MongoChecksumsV1.ts +75 -0
  165. package/src/storage/implementation/v1/MongoCompactorV1.ts +93 -0
  166. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +26 -0
  167. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +448 -0
  168. package/src/storage/implementation/v1/PersistedBatchV1.ts +230 -0
  169. package/src/storage/implementation/v1/SingleBucketStoreV1.ts +74 -0
  170. package/src/storage/implementation/v1/SourceRecordStoreV1.ts +156 -0
  171. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +28 -0
  172. package/src/storage/implementation/v1/models.ts +84 -0
  173. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +44 -0
  174. package/src/storage/implementation/v3/MongoChecksumsV3.ts +120 -0
  175. package/src/storage/implementation/v3/MongoCompactorV3.ts +107 -0
  176. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +24 -0
  177. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +12 -0
  178. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +550 -0
  179. package/src/storage/implementation/v3/PersistedBatchV3.ts +318 -0
  180. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +68 -0
  181. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +226 -0
  182. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +112 -0
  183. package/src/storage/implementation/v3/models.ts +96 -0
  184. package/src/storage/storage-index.ts +6 -3
  185. package/src/utils/util.ts +34 -5
  186. package/test/src/storage_compacting.test.ts +57 -29
  187. package/test/src/storage_sync.test.ts +351 -5
  188. package/test/tsconfig.json +0 -1
  189. package/tsconfig.tsbuildinfo +1 -1
  190. package/dist/storage/implementation/PersistedBatch.d.ts +0 -71
  191. package/dist/storage/implementation/PersistedBatch.js +0 -354
  192. package/dist/storage/implementation/PersistedBatch.js.map +0 -1
  193. package/src/storage/implementation/PersistedBatch.ts +0 -432
@@ -1,12 +1,12 @@
1
1
  import crypto from 'crypto';
2
2
 
3
- import { ErrorCode, logger, ServiceError } from '@powersync/lib-services-framework';
3
+ import { ErrorCode, Logger, ServiceError } from '@powersync/lib-services-framework';
4
4
  import { storage } from '@powersync/service-core';
5
5
  import { VersionedPowerSyncMongo } from './db.js';
6
6
 
7
7
  /**
8
- * Manages a lock on a sync rules document, so that only one process
9
- * replicates those sync rules at a time.
8
+ * Manages a lock on a replication stream document, so that only one process
9
+ * processes that replication stream at a time.
10
10
  */
11
11
  export class MongoSyncRulesLock implements storage.ReplicationLock {
12
12
  private readonly refreshInterval: NodeJS.Timeout;
@@ -38,29 +38,27 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
38
38
  if (heldLock?.lock?.expires_at) {
39
39
  throw new ServiceError(
40
40
  ErrorCode.PSYNC_S1003,
41
- `Sync rules ${sync_rules.id} have been locked by another process for replication, expiring at ${heldLock.lock.expires_at.toISOString()}.`
41
+ `Replication stream is locked by another process, standing by. Lock expiring at ${heldLock.lock.expires_at.toISOString()}.`
42
42
  );
43
43
  } else {
44
- throw new ServiceError(
45
- ErrorCode.PSYNC_S1003,
46
- `Sync rules ${sync_rules.id} have been locked by another process for replication.`
47
- );
44
+ throw new ServiceError(ErrorCode.PSYNC_S1003, `Replication stream is locked by another process, standing by.`);
48
45
  }
49
46
  }
50
- logger.info(`Sync rules ${sync_rules.id} has been locked for replication with lock ID ${lockId}.`);
51
- return new MongoSyncRulesLock(db, sync_rules.id, lockId);
47
+ sync_rules.logger.info(`Locked replication stream for processing`);
48
+ return new MongoSyncRulesLock(db, sync_rules.id, lockId, sync_rules.logger);
52
49
  }
53
50
 
54
51
  constructor(
55
52
  private db: VersionedPowerSyncMongo,
56
53
  public sync_rules_id: number,
57
- private lock_id: string
54
+ private lock_id: string,
55
+ private logger: Logger
58
56
  ) {
59
57
  this.refreshInterval = setInterval(async () => {
60
58
  try {
61
59
  await this.refresh();
62
60
  } catch (e) {
63
- logger.error('Failed to refresh lock', e);
61
+ this.logger.error('Failed to refresh lock', e);
64
62
  clearInterval(this.refreshInterval);
65
63
  }
66
64
  }, 30_130);
@@ -79,7 +77,7 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
79
77
  );
80
78
  if (result.modifiedCount == 0) {
81
79
  // Log and ignore
82
- logger.warn(`Lock already released: ${this.sync_rules_id}/${this.lock_id}`);
80
+ this.logger.warn(`Lock already released: ${this.sync_rules_id}/${this.lock_id}`);
83
81
  }
84
82
  }
85
83
 
@@ -56,7 +56,9 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
56
56
  switch (this.writeCheckpointMode) {
57
57
  case storage.WriteCheckpointMode.CUSTOM:
58
58
  if (false == 'sync_rules_id' in filters) {
59
- throw new framework.ServiceAssertionError(`Sync rules ID is required for custom Write Checkpoint filtering`);
59
+ throw new framework.ServiceAssertionError(
60
+ `Replication stream ID is required for custom Write Checkpoint filtering`
61
+ );
60
62
  }
61
63
  return this.lastCustomWriteCheckpoint(filters);
62
64
  case storage.WriteCheckpointMode.MANAGED:
@@ -2,7 +2,7 @@ import { ToastableSqliteRow } from '@powersync/service-sync-rules';
2
2
  import * as bson from 'bson';
3
3
 
4
4
  import { storage } from '@powersync/service-core';
5
- import { mongoTableId } from '../storage-index.js';
5
+ import { mongoTableId } from '../../utils/util.js';
6
6
 
7
7
  /**
8
8
  * Maximum number of operations in a batch.
@@ -0,0 +1,37 @@
1
+ import { InternalOpId } from '@powersync/service-core';
2
+ import { BucketDefinitionId } from '../BucketDefinitionMapping.js';
3
+ import { BucketDataProperties } from '../models.js';
4
+
5
+ /**
6
+ * Full context identifying a bucket.
7
+ */
8
+ export interface BucketKey {
9
+ /**
10
+ * Also referred to as g / group_id.
11
+ */
12
+ replicationStreamId: number;
13
+ /**
14
+ * Bucket definition id, '0' for storage V1.
15
+ */
16
+ definitionId: BucketDefinitionId;
17
+ /**
18
+ * Bucket name.
19
+ */
20
+ bucket: string;
21
+ }
22
+
23
+ /**
24
+ * In-memory bucket data document.
25
+ *
26
+ * This is converted to/from BucketDataDocumentV1 / BucketDataDocumentV3 for storage.
27
+ */
28
+ export interface BucketDataDoc extends BucketDataProperties {
29
+ /**
30
+ * Identifies the bucket for this document.
31
+ */
32
+ bucketKey: BucketKey;
33
+ /**
34
+ * op_id
35
+ */
36
+ o: InternalOpId;
37
+ }
@@ -0,0 +1,15 @@
1
+ import { InternalOpId } from '@powersync/service-core';
2
+ import * as bson from 'bson';
3
+ import { BucketDefinitionMapping } from '../BucketDefinitionMapping.js';
4
+ import type { VersionedPowerSyncMongo } from '../db.js';
5
+
6
+ export interface MongoSyncBucketStorageContext<TDb extends VersionedPowerSyncMongo = VersionedPowerSyncMongo> {
7
+ db: TDb;
8
+ group_id: number;
9
+ mapping: BucketDefinitionMapping;
10
+ }
11
+
12
+ export interface MongoSyncBucketStorageCheckpoint {
13
+ checkpoint: InternalOpId;
14
+ snapshotTime: bson.Timestamp;
15
+ }
@@ -0,0 +1,364 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { BucketDataSource, EvaluatedParameters, EvaluatedRow } from '@powersync/service-sync-rules';
3
+ import * as bson from 'bson';
4
+
5
+ import { logger as defaultLogger, Logger } from '@powersync/lib-services-framework';
6
+ import { InternalOpId, storage, utils } from '@powersync/service-core';
7
+ import { JSONBig } from '@powersync/service-jsonbig';
8
+ import { mongoTableId, replicaIdToSubkey } from '../../../utils/util.js';
9
+ import { BucketDefinitionId, BucketDefinitionMapping } from '../BucketDefinitionMapping.js';
10
+ import { currentBucketKey, MAX_ROW_SIZE } from '../MongoBucketBatchShared.js';
11
+ import { MongoIdSequence } from '../MongoIdSequence.js';
12
+ import type { VersionedPowerSyncMongo } from '../db.js';
13
+ import { TaggedBucketParameterDocument } from '../models.js';
14
+ import { BucketDataDoc, BucketKey } from './BucketDataDoc.js';
15
+ import { SourceRecordBucketState, SourceRecordLookupState } from './SourceRecordStore.js';
16
+
17
+ /**
18
+ * Maximum size of operations we write in a single transaction.
19
+ *
20
+ * It's tricky to find the exact limit, but from experience, over 100MB
21
+ * can cause an error:
22
+ * > transaction is too large and will not fit in the storage engine cache
23
+ *
24
+ * Additionally, unbounded size here can balloon our memory usage in some edge
25
+ * cases.
26
+ *
27
+ * When we reach this threshold, we commit the transaction and start a new one.
28
+ */
29
+ const MAX_TRANSACTION_BATCH_SIZE = 30_000_000;
30
+
31
+ /**
32
+ * Limit number of documents to write in a single transaction.
33
+ *
34
+ * This has an effect on error message size in some cases.
35
+ */
36
+ const MAX_TRANSACTION_DOC_COUNT = 2_000;
37
+
38
+ export interface SaveBucketDataOptions {
39
+ op_seq: MongoIdSequence;
40
+ sourceKey: storage.ReplicaId;
41
+ table: storage.SourceTable;
42
+ evaluated: EvaluatedRow[];
43
+ before_buckets: SourceRecordBucketState[];
44
+ }
45
+
46
+ export interface SaveParameterDataOptions {
47
+ op_seq: MongoIdSequence;
48
+ sourceKey: storage.ReplicaId;
49
+ sourceTable: storage.SourceTable;
50
+ evaluated: EvaluatedParameters[];
51
+ existing_lookups: SourceRecordLookupState[];
52
+ }
53
+
54
+ export interface UpsertCurrentDataOptions {
55
+ sourceTableId: bson.ObjectId;
56
+ replicaId: storage.ReplicaId;
57
+ data: bson.Binary | null;
58
+ buckets: SourceRecordBucketState[];
59
+ lookups: SourceRecordLookupState[];
60
+ }
61
+
62
+ export interface PersistedBatchOptions {
63
+ logger?: Logger;
64
+ }
65
+
66
+ /**
67
+ * Keeps track of bulkwrite operations within a transaction.
68
+ *
69
+ * There may be multiple of these batches per transaction, but it may not span
70
+ * multiple transactions.
71
+ */
72
+ export abstract class PersistedBatch {
73
+ logger: Logger;
74
+ bucketData: BucketDataDoc[] = [];
75
+ bucketParameters: TaggedBucketParameterDocument[] = [];
76
+ bucketStates: Map<string, BucketStateUpdate> = new Map();
77
+
78
+ /**
79
+ * For debug logging only.
80
+ */
81
+ debugLastOpId: InternalOpId | null = null;
82
+
83
+ /**
84
+ * Very rough estimate of transaction size.
85
+ */
86
+ currentSize = 0;
87
+
88
+ constructor(
89
+ protected readonly db: VersionedPowerSyncMongo,
90
+ protected readonly group_id: number,
91
+ protected readonly mapping: BucketDefinitionMapping,
92
+ writtenSize: number,
93
+ options?: PersistedBatchOptions
94
+ ) {
95
+ this.currentSize = writtenSize;
96
+ this.logger = options?.logger ?? defaultLogger;
97
+ }
98
+
99
+ saveBucketData(options: SaveBucketDataOptions) {
100
+ const remaining_buckets = new Map<string, SaveBucketDataOptions['before_buckets'][number]>();
101
+ for (let bucket of options.before_buckets) {
102
+ const mapped: SourceRecordBucketState = {
103
+ bucket: bucket.bucket,
104
+ definitionId: this.checkDefinitionId(bucket.definitionId),
105
+ id: bucket.id,
106
+ table: bucket.table
107
+ };
108
+ remaining_buckets.set(currentBucketKey(mapped), mapped);
109
+ }
110
+
111
+ const dchecksum = BigInt(utils.hashDelete(replicaIdToSubkey(options.table.id, options.sourceKey)));
112
+
113
+ for (const evaluated of options.evaluated) {
114
+ const definitionId = this.getBucketDefinitionId(evaluated.source);
115
+ const key = currentBucketKey({
116
+ definitionId: definitionId,
117
+ bucket: evaluated.bucket,
118
+ table: evaluated.table,
119
+ id: evaluated.id
120
+ });
121
+
122
+ const recordData = JSONBig.stringify(evaluated.data);
123
+ const checksum = utils.hashData(evaluated.table, evaluated.id, recordData);
124
+ if (recordData.length > MAX_ROW_SIZE) {
125
+ this.logger.error(`Row ${key} too large: ${recordData.length} bytes. Removing.`);
126
+ continue;
127
+ }
128
+
129
+ remaining_buckets.delete(key);
130
+ const byteEstimate = recordData.length + 200;
131
+ this.currentSize += byteEstimate;
132
+
133
+ const op_id = options.op_seq.next();
134
+ this.debugLastOpId = op_id;
135
+
136
+ this.addBucketDataPut({
137
+ bucketKey: {
138
+ bucket: evaluated.bucket,
139
+ definitionId: definitionId,
140
+ replicationStreamId: this.group_id
141
+ },
142
+ op_id,
143
+ bucket: evaluated.bucket,
144
+ sourceTableId: options.table.id,
145
+ sourceKey: options.sourceKey,
146
+ table: evaluated.table,
147
+ rowId: evaluated.id,
148
+ checksum: BigInt(checksum),
149
+ data: recordData
150
+ });
151
+ this.incrementBucket(definitionId, evaluated.bucket, op_id, byteEstimate);
152
+ }
153
+
154
+ for (let bucket of remaining_buckets.values()) {
155
+ const definitionId = bucket.definitionId!;
156
+ const op_id = options.op_seq.next();
157
+ this.debugLastOpId = op_id;
158
+
159
+ this.addBucketDataRemove({
160
+ bucketKey: {
161
+ replicationStreamId: this.group_id,
162
+ definitionId,
163
+ bucket: bucket.bucket
164
+ },
165
+ op_id,
166
+ sourceTableId: options.table.id,
167
+ sourceKey: options.sourceKey,
168
+ table: bucket.table,
169
+ rowId: bucket.id,
170
+ checksum: dchecksum
171
+ });
172
+ this.currentSize += 200;
173
+ this.incrementBucket(definitionId, bucket.bucket, op_id, 200);
174
+ }
175
+ }
176
+
177
+ abstract saveParameterData(data: SaveParameterDataOptions): void;
178
+
179
+ abstract hardDeleteCurrentData(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId): void;
180
+
181
+ abstract softDeleteCurrentData(
182
+ sourceTableId: bson.ObjectId,
183
+ replicaId: storage.ReplicaId,
184
+ checkpointGreaterThan: bigint
185
+ ): void;
186
+
187
+ abstract upsertCurrentData(values: UpsertCurrentDataOptions): void;
188
+
189
+ protected abstract get currentDataCount(): number;
190
+
191
+ protected abstract flushBucketData(session: mongo.ClientSession): Promise<void>;
192
+
193
+ protected abstract flushBucketParameters(session: mongo.ClientSession): Promise<void>;
194
+
195
+ protected abstract flushCurrentData(session: mongo.ClientSession): Promise<void>;
196
+
197
+ protected abstract flushBucketStates(session: mongo.ClientSession): Promise<void>;
198
+
199
+ protected abstract resetCurrentData(): void;
200
+
201
+ protected abstract checkDefinitionId(definitionId: BucketDefinitionId | null): BucketDefinitionId;
202
+ protected abstract getBucketDefinitionId(bucketSource: BucketDataSource): BucketDefinitionId;
203
+
204
+ protected get bucketDataCount(): number {
205
+ return this.bucketData.length;
206
+ }
207
+
208
+ protected incrementBucket(definitionId: BucketDefinitionId, bucket: string, op_id: InternalOpId, bytes: number) {
209
+ const key = `${definitionId ?? ''}:${bucket}`;
210
+ let existingState = this.bucketStates.get(key);
211
+ if (existingState) {
212
+ existingState.lastOp = op_id;
213
+ existingState.incrementCount += 1;
214
+ existingState.incrementBytes += bytes;
215
+ } else {
216
+ this.bucketStates.set(key, {
217
+ definitionId,
218
+ bucket,
219
+ lastOp: op_id,
220
+ incrementCount: 1,
221
+ incrementBytes: bytes
222
+ });
223
+ }
224
+ }
225
+
226
+ protected addBucketDataPut(options: {
227
+ op_id: InternalOpId;
228
+ bucketKey: BucketKey;
229
+ bucket: string;
230
+ sourceTableId: storage.SourceTable['id'];
231
+ sourceKey: storage.ReplicaId;
232
+ table: string;
233
+ rowId: string;
234
+ checksum: bigint;
235
+ data: string;
236
+ }) {
237
+ this.bucketData.push({
238
+ bucketKey: options.bucketKey,
239
+ o: options.op_id,
240
+ op: 'PUT',
241
+ source_table: mongoTableId(options.sourceTableId),
242
+ source_key: options.sourceKey,
243
+ table: options.table,
244
+ row_id: options.rowId,
245
+ checksum: options.checksum,
246
+ data: options.data
247
+ });
248
+ }
249
+
250
+ protected addBucketDataRemove(options: {
251
+ op_id: InternalOpId;
252
+ bucketKey: BucketKey;
253
+ sourceTableId: storage.SourceTable['id'];
254
+ sourceKey: storage.ReplicaId;
255
+ table: string;
256
+ rowId: string;
257
+ checksum: bigint;
258
+ }) {
259
+ this.bucketData.push({
260
+ bucketKey: options.bucketKey,
261
+ o: options.op_id,
262
+ op: 'REMOVE',
263
+ source_table: mongoTableId(options.sourceTableId),
264
+ source_key: options.sourceKey,
265
+ table: options.table,
266
+ row_id: options.rowId,
267
+ checksum: options.checksum,
268
+ data: null
269
+ });
270
+ }
271
+
272
+ shouldFlushTransaction() {
273
+ return (
274
+ this.currentSize >= MAX_TRANSACTION_BATCH_SIZE ||
275
+ this.bucketDataCount >= MAX_TRANSACTION_DOC_COUNT ||
276
+ this.currentDataCount >= MAX_TRANSACTION_DOC_COUNT ||
277
+ this.bucketParameters.length >= MAX_TRANSACTION_DOC_COUNT
278
+ );
279
+ }
280
+
281
+ async flush(session: mongo.ClientSession, options?: storage.BucketBatchCommitOptions) {
282
+ const startAt = performance.now();
283
+ let flushedSomething = false;
284
+ if (this.bucketDataCount > 0) {
285
+ flushedSomething = true;
286
+ await this.flushBucketData(session);
287
+ }
288
+ if (this.bucketParameters.length > 0) {
289
+ flushedSomething = true;
290
+ await this.flushBucketParameters(session);
291
+ }
292
+ if (this.currentDataCount > 0) {
293
+ flushedSomething = true;
294
+ await this.flushCurrentData(session);
295
+ }
296
+
297
+ if (this.bucketStates.size > 0) {
298
+ flushedSomething = true;
299
+ await this.flushBucketStates(session);
300
+ }
301
+
302
+ if (flushedSomething) {
303
+ const duration = Math.round(performance.now() - startAt);
304
+ if (options?.oldestUncommittedChange != null) {
305
+ const replicationLag = Math.round((Date.now() - options.oldestUncommittedChange.getTime()) / 1000);
306
+
307
+ this.logger.info(
308
+ `Flushed ${this.bucketDataCount} + ${this.bucketParameters.length} + ${
309
+ this.currentDataCount
310
+ } updates, ${Math.round(this.currentSize / 1024)}kb in ${duration}ms. Last op_id: ${this.debugLastOpId}. Replication lag: ${replicationLag}s`,
311
+ {
312
+ flushed: {
313
+ duration: duration,
314
+ size: this.currentSize,
315
+ bucket_data_count: this.bucketDataCount,
316
+ parameter_data_count: this.bucketParameters.length,
317
+ current_data_count: this.currentDataCount,
318
+ replication_lag_seconds: replicationLag
319
+ }
320
+ }
321
+ );
322
+ } else {
323
+ this.logger.info(
324
+ `Flushed ${this.bucketDataCount} + ${this.bucketParameters.length} + ${
325
+ this.currentDataCount
326
+ } updates, ${Math.round(this.currentSize / 1024)}kb in ${duration}ms. Last op_id: ${this.debugLastOpId}`,
327
+ {
328
+ flushed: {
329
+ duration: duration,
330
+ size: this.currentSize,
331
+ bucket_data_count: this.bucketDataCount,
332
+ parameter_data_count: this.bucketParameters.length,
333
+ current_data_count: this.currentDataCount
334
+ }
335
+ }
336
+ );
337
+ }
338
+ }
339
+
340
+ const stats = {
341
+ bucketDataCount: this.bucketDataCount,
342
+ parameterDataCount: this.bucketParameters.length,
343
+ currentDataCount: this.currentDataCount,
344
+ flushedAny: flushedSomething
345
+ };
346
+
347
+ this.bucketData = [];
348
+ this.bucketParameters = [];
349
+ this.resetCurrentData();
350
+ this.bucketStates.clear();
351
+ this.currentSize = 0;
352
+ this.debugLastOpId = null;
353
+
354
+ return stats;
355
+ }
356
+ }
357
+
358
+ export interface BucketStateUpdate {
359
+ definitionId: BucketDefinitionId | null;
360
+ bucket: string;
361
+ lastOp: InternalOpId;
362
+ incrementCount: number;
363
+ incrementBytes: number;
364
+ }
@@ -0,0 +1,63 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { InternalOpId } from '@powersync/service-core';
3
+ import { BucketDataProperties } from '../models.js';
4
+ import { BucketDataDoc, BucketKey } from './BucketDataDoc.js';
5
+
6
+ const GENERIC_ID = Symbol('BucketDataDocumentGenericId');
7
+ export type BucketDataDocumentGenericId = {
8
+ b: string;
9
+ o: InternalOpId;
10
+ // Hack to ensure this can't be constructed directly
11
+ [GENERIC_ID]: true;
12
+ };
13
+
14
+ /**
15
+ * This document is never actually constructed - we use it as a "virtual" type.
16
+ *
17
+ * The actual implementations are BucketDataDocumentV1 or BucketDataDocumentV3.
18
+ * They don't fully satisfy this interface, but this works to share common implementations.
19
+ *
20
+ * The idea is that we can have a common implementation between V1 & V3, using this type,
21
+ * and operate on MongoDB collections.
22
+ *
23
+ * This interface serves two primary purposes:
24
+ * 1. Captures properties that exist on both V1 and V3 storage models.
25
+ * 2. Gives a common reference when querying or modifying collections.
26
+ *
27
+ * Generics would've been ideal, but they don't play well with MongoDB collections.
28
+ */
29
+ export interface BucketDataDocumentGeneric extends BucketDataProperties {
30
+ _id: BucketDataDocumentGenericId;
31
+ }
32
+
33
+ /**
34
+ * Represent read/write access for a single bucket.
35
+ *
36
+ * This does not implement the actual collection operations, but supports the required conversions
37
+ * between in-memory BucketDataDoc and the specific storage formats.
38
+ */
39
+ export interface SingleBucketStore {
40
+ readonly key: BucketKey;
41
+
42
+ readonly collection: mongo.Collection<BucketDataDocumentGeneric>;
43
+ docId(o: InternalOpId): BucketDataDocumentGenericId;
44
+ readonly minId: BucketDataDocumentGenericId;
45
+ readonly maxId: BucketDataDocumentGenericId;
46
+
47
+ /**
48
+ * Convert in-memory document -> persisted document.
49
+ */
50
+ toPersistedDocument(source: Omit<BucketDataDoc, 'bucketKey'>): BucketDataDocumentGeneric;
51
+
52
+ /**
53
+ * Convert persisted document -> in-memory document.
54
+ */
55
+ fromPersistedDocument(doc: BucketDataDocumentGeneric): BucketDataDoc;
56
+
57
+ /**
58
+ * Convert partial persisted document -> partial in-memory document.
59
+ */
60
+ fromPartialPersistedDocument<T extends keyof BucketDataProperties>(
61
+ doc: Pick<BucketDataDocumentGeneric, '_id' | T>
62
+ ): Pick<BucketDataDoc, 'bucketKey' | 'o' | T>;
63
+ }
@@ -0,0 +1,49 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { Logger } from '@powersync/lib-services-framework';
3
+ import { storage } from '@powersync/service-core';
4
+ import { EvaluatedParameters, EvaluatedRow } from '@powersync/service-sync-rules';
5
+ import * as bson from 'bson';
6
+ import { BucketDefinitionId, ParameterIndexId } from '../BucketDefinitionMapping.js';
7
+
8
+ export interface SourceRecordLookupEntry {
9
+ sourceTableId: bson.ObjectId;
10
+ replicaId: storage.ReplicaId;
11
+ }
12
+
13
+ export interface SourceRecordBucketState {
14
+ definitionId: BucketDefinitionId | null;
15
+ bucket: string;
16
+ table: string;
17
+ id: string;
18
+ }
19
+
20
+ export interface SourceRecordLookupState {
21
+ indexId: ParameterIndexId | null;
22
+ lookup: bson.Binary;
23
+ }
24
+
25
+ export interface LoadedSourceRecord {
26
+ sourceTableId: bson.ObjectId;
27
+ replicaId: storage.ReplicaId;
28
+ data: bson.Binary | null;
29
+ buckets: SourceRecordBucketState[];
30
+ lookups: SourceRecordLookupState[];
31
+ cacheKey: string;
32
+ }
33
+
34
+ export interface SourceRecordStore {
35
+ mapEvaluatedBuckets(evaluated: EvaluatedRow[]): SourceRecordBucketState[];
36
+ mapParameterLookups(paramEvaluated: EvaluatedParameters[]): SourceRecordLookupState[];
37
+ loadSizes(session: mongo.ClientSession, entries: SourceRecordLookupEntry[]): Promise<Map<string, number>>;
38
+ loadDocuments(
39
+ session: mongo.ClientSession,
40
+ entries: SourceRecordLookupEntry[],
41
+ idsOnly: boolean
42
+ ): Promise<Map<string, LoadedSourceRecord>>;
43
+ loadTruncateBatch(
44
+ session: mongo.ClientSession,
45
+ sourceTableId: bson.ObjectId,
46
+ limit: number
47
+ ): Promise<LoadedSourceRecord[]>;
48
+ postCommitCleanup(lastCheckpoint: bigint, logger: Logger): Promise<void>;
49
+ }
@@ -0,0 +1,80 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { DO_NOT_LOG } from '@powersync/lib-services-framework';
3
+ import { PowerSyncMongo } from '../db.js';
4
+ import { CommonSourceTableDocument, StorageConfig } from '../models.js';
5
+
6
+ export abstract class BaseVersionedPowerSyncMongo {
7
+ readonly client: mongo.MongoClient;
8
+ readonly db: mongo.Db;
9
+ readonly storageConfig: StorageConfig;
10
+ [DO_NOT_LOG] = true;
11
+
12
+ constructor(
13
+ protected readonly upstream: PowerSyncMongo,
14
+ storageConfig: StorageConfig
15
+ ) {
16
+ this.client = upstream.client;
17
+ this.db = upstream.db;
18
+ this.storageConfig = storageConfig;
19
+ }
20
+
21
+ get bucket_data() {
22
+ return this.upstream.bucket_data;
23
+ }
24
+
25
+ get op_id_sequence() {
26
+ return this.upstream.op_id_sequence;
27
+ }
28
+
29
+ get sync_rules() {
30
+ return this.upstream.sync_rules;
31
+ }
32
+
33
+ get custom_write_checkpoints() {
34
+ return this.upstream.custom_write_checkpoints;
35
+ }
36
+
37
+ get write_checkpoints() {
38
+ return this.upstream.write_checkpoints;
39
+ }
40
+
41
+ get instance() {
42
+ return this.upstream.instance;
43
+ }
44
+
45
+ get locks() {
46
+ return this.upstream.locks;
47
+ }
48
+
49
+ get checkpoint_events() {
50
+ return this.upstream.checkpoint_events;
51
+ }
52
+
53
+ get connection_report_events() {
54
+ return this.upstream.connection_report_events;
55
+ }
56
+
57
+ notifyCheckpoint() {
58
+ return this.upstream.notifyCheckpoint();
59
+ }
60
+
61
+ protected sourceRecordsCollectionName(replicationStreamId: number, sourceTableId: mongo.ObjectId) {
62
+ return this.upstream.sourceRecordsCollectionName(replicationStreamId, sourceTableId);
63
+ }
64
+
65
+ protected sourceTableCollectionName(replicationStreamId: number) {
66
+ return this.upstream.sourceTableCollectionName(replicationStreamId);
67
+ }
68
+
69
+ protected async listCollectionsByPrefix<T extends mongo.Document>(prefix: string): Promise<mongo.Collection<T>[]> {
70
+ const collections = await this.db.listCollections({ name: new RegExp(`^${prefix}`) }, { nameOnly: true }).toArray();
71
+
72
+ return collections
73
+ .filter((collection) => collection.name.startsWith(prefix))
74
+ .map((collection) => this.db.collection<T>(collection.name));
75
+ }
76
+
77
+ abstract commonSourceTables(replicationStreamId: number): mongo.Collection<CommonSourceTableDocument>;
78
+
79
+ abstract initializeStreamStorage(replicationStreamId: number): Promise<void>;
80
+ }