@powersync/service-module-mongodb-storage 0.0.0-dev-20260803144812 → 0.0.0-dev-20260929095716

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 (213) hide show
  1. package/CHANGELOG.md +129 -7
  2. package/dist/storage/MongoBucketStorage.d.ts +42 -5
  3. package/dist/storage/MongoBucketStorage.js +404 -215
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +39 -14
  6. package/dist/storage/implementation/MongoBucketBatch.js +169 -98
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoCompactor.d.ts +21 -79
  9. package/dist/storage/implementation/MongoCompactor.js +27 -238
  10. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  11. package/dist/storage/implementation/MongoIdSequence.d.ts +10 -1
  12. package/dist/storage/implementation/MongoIdSequence.js +14 -1
  13. package/dist/storage/implementation/MongoIdSequence.js.map +1 -1
  14. package/dist/storage/implementation/MongoOpIdAllocator.d.ts +27 -0
  15. package/dist/storage/implementation/MongoOpIdAllocator.js +97 -0
  16. package/dist/storage/implementation/MongoOpIdAllocator.js.map +1 -0
  17. package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
  18. package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
  19. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  20. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
  21. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +35 -2
  22. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
  23. package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +2 -1
  24. package/dist/storage/implementation/MongoPersistedReplicationStream.js +7 -4
  25. package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -1
  26. package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +3 -3
  27. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +10 -8
  28. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
  29. package/dist/storage/implementation/MongoStorageProvider.js +8 -4
  30. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  31. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +32 -11
  32. package/dist/storage/implementation/MongoSyncBucketStorage.js +95 -31
  33. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  34. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +26 -2
  35. package/dist/storage/implementation/MongoSyncRulesLock.js +59 -10
  36. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  37. package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
  38. package/dist/storage/implementation/MongoWriteBatch.js +149 -0
  39. package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
  40. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
  41. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +9 -30
  42. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  43. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
  44. package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
  45. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
  46. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
  47. package/dist/storage/implementation/common/PersistedBatch.d.ts +32 -4
  48. package/dist/storage/implementation/common/PersistedBatch.js +57 -14
  49. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  50. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
  51. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
  52. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  53. package/dist/storage/implementation/db.d.ts +9 -1
  54. package/dist/storage/implementation/db.js +28 -1
  55. package/dist/storage/implementation/db.js.map +1 -1
  56. package/dist/storage/implementation/models.d.ts +35 -0
  57. package/dist/storage/implementation/models.js.map +1 -1
  58. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +11 -0
  59. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +227 -221
  60. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  61. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +38 -6
  62. package/dist/storage/implementation/v1/MongoCompactorV1.js +247 -20
  63. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  64. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
  65. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
  66. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
  67. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
  68. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +66 -17
  69. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  70. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
  71. package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
  72. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
  73. package/dist/storage/implementation/v1/models.js +1 -0
  74. package/dist/storage/implementation/v1/models.js.map +1 -1
  75. package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
  76. package/dist/storage/implementation/v3/CompactionLease.js +131 -0
  77. package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
  78. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +14 -2
  79. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +261 -194
  80. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  81. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +83 -25
  82. package/dist/storage/implementation/v3/MongoCompactorV3.js +702 -159
  83. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  84. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
  85. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
  86. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  87. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
  88. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
  89. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
  90. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
  91. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +92 -32
  92. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  93. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
  94. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
  95. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
  96. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +14 -8
  97. package/dist/storage/implementation/v3/PersistedBatchV3.js +77 -45
  98. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  99. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
  100. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
  101. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  102. package/dist/storage/implementation/v3/bucket-format.js +2 -0
  103. package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
  104. package/dist/storage/implementation/v3/compact-utils.d.ts +108 -0
  105. package/dist/storage/implementation/v3/compact-utils.js +223 -0
  106. package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
  107. package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
  108. package/dist/storage/implementation/v3/compaction-constants.js +6 -0
  109. package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
  110. package/dist/storage/implementation/v3/models.d.ts +90 -6
  111. package/dist/storage/implementation/v3/models.js.map +1 -1
  112. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
  113. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +16 -5
  114. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
  115. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
  116. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +6 -8
  117. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +17 -11
  118. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
  119. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +38 -0
  120. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +164 -0
  121. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
  122. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
  123. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +197 -25
  124. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
  125. package/dist/storage/implementation/v3/source-table-utils.d.ts +15 -7
  126. package/dist/storage/implementation/v3/source-table-utils.js +49 -57
  127. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
  128. package/dist/storage/storage-index.d.ts +2 -1
  129. package/dist/storage/storage-index.js +1 -0
  130. package/dist/storage/storage-index.js.map +1 -1
  131. package/dist/types/types.d.ts +28 -1
  132. package/dist/types/types.js +28 -7
  133. package/dist/types/types.js.map +1 -1
  134. package/dist/utils/test-utils.d.ts +4 -2
  135. package/dist/utils/test-utils.js +7 -4
  136. package/dist/utils/test-utils.js.map +1 -1
  137. package/dist/utils/util.js +3 -1
  138. package/dist/utils/util.js.map +1 -1
  139. package/package.json +10 -9
  140. package/src/storage/MongoBucketStorage.ts +545 -277
  141. package/src/storage/implementation/MongoBucketBatch.ts +258 -124
  142. package/src/storage/implementation/MongoCompactor.ts +37 -346
  143. package/src/storage/implementation/MongoIdSequence.ts +21 -1
  144. package/src/storage/implementation/MongoOpIdAllocator.ts +105 -0
  145. package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
  146. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +46 -1
  147. package/src/storage/implementation/MongoPersistedReplicationStream.ts +12 -4
  148. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +48 -29
  149. package/src/storage/implementation/MongoStorageProvider.ts +16 -4
  150. package/src/storage/implementation/MongoSyncBucketStorage.ts +133 -40
  151. package/src/storage/implementation/MongoSyncRulesLock.ts +78 -12
  152. package/src/storage/implementation/MongoWriteBatch.ts +172 -0
  153. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +16 -46
  154. package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
  155. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
  156. package/src/storage/implementation/common/PersistedBatch.ts +76 -20
  157. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
  158. package/src/storage/implementation/db.ts +36 -2
  159. package/src/storage/implementation/models.ts +39 -0
  160. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +272 -254
  161. package/src/storage/implementation/v1/MongoCompactorV1.ts +327 -40
  162. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
  163. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +88 -28
  164. package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
  165. package/src/storage/implementation/v1/models.ts +1 -0
  166. package/src/storage/implementation/v3/CompactionLease.ts +152 -0
  167. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +339 -247
  168. package/src/storage/implementation/v3/MongoCompactorV3.ts +955 -225
  169. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
  170. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
  171. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +112 -42
  172. package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
  173. package/src/storage/implementation/v3/PersistedBatchV3.ts +94 -51
  174. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
  175. package/src/storage/implementation/v3/bucket-format.ts +2 -0
  176. package/src/storage/implementation/v3/compact-utils.ts +329 -0
  177. package/src/storage/implementation/v3/compaction-constants.ts +5 -0
  178. package/src/storage/implementation/v3/models.ts +97 -4
  179. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +24 -9
  180. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
  181. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +26 -14
  182. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +229 -0
  183. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +279 -29
  184. package/src/storage/implementation/v3/source-table-utils.ts +82 -76
  185. package/src/storage/storage-index.ts +2 -0
  186. package/src/types/types.ts +42 -7
  187. package/src/utils/test-utils.ts +9 -5
  188. package/src/utils/util.ts +3 -1
  189. package/test/src/__snapshots__/storage.test.ts.snap +25 -0
  190. package/test/src/__snapshots__/storage_sync.test.ts.snap +794 -205
  191. package/test/src/chunk_compaction_config.test.ts +38 -0
  192. package/test/src/cleanup-stopped-sync-configs.test.ts +59 -26
  193. package/test/src/compact-utils.test.ts +251 -0
  194. package/test/src/concurrent_writers.test.ts +599 -0
  195. package/test/src/connection_config.test.ts +159 -0
  196. package/test/src/helpers/MemoryObjectStorage.ts +14 -6
  197. package/test/src/helpers/concurrentWriter.mjs +29 -0
  198. package/test/src/helpers/s3TestFactory.ts +12 -12
  199. package/test/src/object_storage_usage.test.ts +268 -0
  200. package/test/src/parameter_compacting_v1.test.ts +120 -0
  201. package/test/src/parameter_compacting_v3.test.ts +192 -0
  202. package/test/src/parameter_compaction_fence.test.ts +276 -0
  203. package/test/src/storage.test.ts +280 -24
  204. package/test/src/storage_compacting.test.ts +1097 -253
  205. package/test/src/storage_object_storage_inline_threshold.test.ts +3 -3
  206. package/test/src/storage_s3_checksums.test.ts +6 -7
  207. package/test/src/storage_s3_compaction_lifecycle.test.ts +143 -17
  208. package/test/src/storage_s3_reading.test.ts +294 -6
  209. package/test/src/storage_s3_writing.test.ts +4 -3
  210. package/test/src/storage_sync.test.ts +412 -80
  211. package/test/src/storeCurrentData.test.ts +5 -5
  212. package/test/src/util.ts +1 -1
  213. package/tsconfig.tsbuildinfo +1 -1
@@ -1,11 +1,11 @@
1
- import { mongo } from '@powersync/lib-service-mongodb';
2
1
  import { Logger } from '@powersync/lib-services-framework';
3
2
  import * as bson from 'bson';
4
3
  import { createHash, randomUUID } from 'node:crypto';
4
+ import { MongoWriteBatch } from '../../MongoWriteBatch.js';
5
5
  import { ObjectStorageDeletionMarker } from '../models.js';
6
6
  import { VersionedPowerSyncMongoV3 } from '../VersionedPowerSyncMongoV3.js';
7
7
  import { BucketDataObjectStorage } from './BucketDataObjectStorage.js';
8
- import { ObjectStorage } from './ObjectStorage.js';
8
+ import { ObjectStorage, ObjectStorageOperationOptions } from './ObjectStorage.js';
9
9
 
10
10
  export const OBJECT_STORAGE_UPLOAD_LEASE_MS = 60 * 60 * 1000;
11
11
  export const OBJECT_STORAGE_PUBLICATION_SAFETY_MARGIN_MS = 2 * 60 * 1000;
@@ -93,7 +93,7 @@ export class ObjectStorageLifecycle {
93
93
  *
94
94
  * Call this in the same transaction as the one creating the references.
95
95
  */
96
- async publishUploads(uploads: PreparedObjectStorageUpload[], session: mongo.ClientSession): Promise<void> {
96
+ publishUploads(uploads: PreparedObjectStorageUpload[], writes: MongoWriteBatch): void {
97
97
  for (const upload of uploads) {
98
98
  if (!this.canPublish(upload)) {
99
99
  throw new Error(`Object storage publication lease expired for ${upload.path}`);
@@ -103,12 +103,13 @@ export class ObjectStorageLifecycle {
103
103
  return;
104
104
  }
105
105
 
106
- const result = await this.db
107
- .pendingObjectStorageDeletes(this.replicationStreamId)
108
- .deleteMany({ _id: { $in: uploads.map((upload) => upload.markerId) } }, { session });
109
- if (result.deletedCount !== uploads.length) {
110
- throw new Error(`Missing object storage publication markers`);
111
- }
106
+ writes.deleteMany(
107
+ this.db.pendingObjectStorageDeletes(this.replicationStreamId),
108
+ { _id: { $in: uploads.map((upload) => upload.markerId) } },
109
+ (deletedCount) => {
110
+ if (deletedCount !== uploads.length) throw new Error('Missing object storage publication markers');
111
+ }
112
+ );
112
113
  }
113
114
 
114
115
  /**
@@ -122,14 +123,14 @@ export class ObjectStorageLifecycle {
122
123
  *
123
124
  * Call this in the same transaction as the one removing the references to these files.
124
125
  */
125
- async retire(paths: Iterable<string>, session: mongo.ClientSession, now = new Date()): Promise<void> {
126
+ retire(paths: Iterable<string>, writes: MongoWriteBatch, now = new Date()): void {
126
127
  const markers: ObjectStorageDeletionMarker[] = Array.from(paths, (path) => ({
127
128
  _id: new bson.ObjectId(),
128
129
  path,
129
130
  delete_after: new Date(now.getTime() + OBJECT_STORAGE_REFERENCE_GRACE_MS)
130
131
  }));
131
132
  if (markers.length) {
132
- await this.db.pendingObjectStorageDeletes(this.replicationStreamId).insertMany(markers, { session });
133
+ writes.insertMany(this.db.pendingObjectStorageDeletes(this.replicationStreamId), markers);
133
134
  }
134
135
  }
135
136
 
@@ -137,11 +138,12 @@ export class ObjectStorageLifecycle {
137
138
  * Discover and delete objects by their storage prefix
138
139
  * without touching the database.
139
140
  */
140
- async deletePrefix(prefix: string, options?: { signal?: AbortSignal }): Promise<{ objectCount: number }> {
141
+ async deletePrefix(prefix: string, options?: ObjectStorageOperationOptions): Promise<{ objectCount: number }> {
141
142
  return this.objectStorage.deletePrefix(prefix, { signal: options?.signal });
142
143
  }
143
144
 
144
- async cleanup(logger: Logger): Promise<void> {
145
+ async cleanup(logger: Logger, options?: ObjectStorageOperationOptions): Promise<void> {
146
+ const signal = options?.signal;
145
147
  const markers = this.db.pendingObjectStorageDeletes(this.replicationStreamId);
146
148
  const cursor = markers.find({ delete_after: { $lte: new Date() } });
147
149
  let batch: ObjectStorageDeletionMarker[] = [];
@@ -153,14 +155,24 @@ export class ObjectStorageLifecycle {
153
155
  const deleting = batch;
154
156
  batch = [];
155
157
  try {
156
- await this.bucketData.delete(deleting.map((marker) => marker.path));
158
+ await this.bucketData.delete(
159
+ deleting.map((marker) => marker.path),
160
+ { signal }
161
+ );
157
162
  await markers.deleteMany({ _id: { $in: deleting.map((marker) => marker._id) } });
158
163
  } catch (error) {
164
+ if (signal?.aborted) {
165
+ // Shutting down: stop instead of working through the remaining markers. They stay
166
+ // pending, and are cleaned up during the next compaction. Checked on the signal rather
167
+ // than the error, since a caller may abort with any reason.
168
+ throw error;
169
+ }
159
170
  logger.warn('Failed to clean up object storage deletion markers; will retry during the next compaction', error);
160
171
  }
161
172
  };
162
173
 
163
174
  for await (const marker of cursor) {
175
+ signal?.throwIfAborted();
164
176
  batch.push(marker);
165
177
  if (batch.length === 500) {
166
178
  await deleteBatch();
@@ -0,0 +1,229 @@
1
+ import * as lib_mongo from '@powersync/lib-service-mongodb';
2
+ import { mongo } from '@powersync/lib-service-mongodb';
3
+ import { ReplicationAssertionError } from '@powersync/lib-services-framework';
4
+ import { BucketDefinitionId } from '@powersync/service-sync-rules';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { MongoWriteBatch } from '../../MongoWriteBatch.js';
7
+ import { BucketDataDocumentV3, ObjectStorageUsageDocument } from '../models.js';
8
+ import { VersionedPowerSyncMongoV3 } from '../VersionedPowerSyncMongoV3.js';
9
+
10
+ export const OBJECT_STORAGE_USAGE_BASE_WRITER_ID = '__base__';
11
+ export const DEFAULT_OBJECT_STORAGE_USAGE_STALE_WRITER_MS = 30 * 60 * 1000;
12
+ export const DEFAULT_OBJECT_STORAGE_USAGE_FOLD_LIMIT = 100;
13
+
14
+ export interface ReplicationStreamObjectStorageDefinitionUsageResult {
15
+ replication_stream_id: number;
16
+ definition_id: BucketDefinitionId;
17
+ active_bytes: bigint;
18
+ }
19
+
20
+ export function createObjectStorageUsageWriterId(): string {
21
+ return randomUUID();
22
+ }
23
+
24
+ /**
25
+ * Tracks active S3 references. Values are signed writer-local deltas, so reference changes and
26
+ * accounting updates can be committed together without making replication and compaction share a
27
+ * hot counter document.
28
+ */
29
+ export class ObjectStorageUsage {
30
+ constructor(
31
+ private readonly db: VersionedPowerSyncMongoV3,
32
+ private readonly replicationStreamId: number,
33
+ readonly writerId?: string
34
+ ) {
35
+ if (writerId === OBJECT_STORAGE_USAGE_BASE_WRITER_ID) {
36
+ throw new ReplicationAssertionError('The reserved object-storage usage writer id cannot be used by a writer');
37
+ }
38
+ }
39
+
40
+ static bytes(document: Pick<BucketDataDocumentV3, 'storage_ref'>): bigint {
41
+ return BigInt(document.storage_ref?.file_size ?? 0);
42
+ }
43
+
44
+ static async readAllDefinitionUsage(
45
+ db: VersionedPowerSyncMongoV3
46
+ ): Promise<ReplicationStreamObjectStorageDefinitionUsageResult[]> {
47
+ return db.client.withSession({ snapshot: true }, async (session) => {
48
+ // Deliberately read the whole usage collection. The number of replication streams is
49
+ // expected to stay low, so a collection scan is cheaper than issuing one _id range query
50
+ // per stream (and the usage collection is bounded by streams and writers, not buckets).
51
+ const entries = await db.objectStorageUsage
52
+ .aggregate<{
53
+ _id: { replication_stream_id: number; definition_id: BucketDefinitionId };
54
+ active_bytes: bigint;
55
+ }>(
56
+ [
57
+ { $project: { replication_stream_id: '$_id.g', definitions: { $objectToArray: '$definitions' } } },
58
+ { $unwind: '$definitions' },
59
+ {
60
+ $group: {
61
+ _id: {
62
+ replication_stream_id: '$replication_stream_id',
63
+ definition_id: '$definitions.k'
64
+ },
65
+ active_bytes: { $sum: '$definitions.v' }
66
+ }
67
+ }
68
+ ],
69
+ { session, readConcern: 'snapshot' }
70
+ )
71
+ .toArray()
72
+ .catch((error) => {
73
+ if (lib_mongo.isMongoNamespaceNotFoundError(error)) {
74
+ return [];
75
+ }
76
+ throw error;
77
+ });
78
+
79
+ return entries.map((entry) => {
80
+ const activeBytes = BigInt(entry.active_bytes ?? 0);
81
+ if (activeBytes < 0n) {
82
+ return {
83
+ replication_stream_id: entry._id.replication_stream_id,
84
+ definition_id: entry._id.definition_id,
85
+ active_bytes: 0n
86
+ };
87
+ }
88
+ return {
89
+ replication_stream_id: entry._id.replication_stream_id,
90
+ definition_id: entry._id.definition_id,
91
+ active_bytes: activeBytes
92
+ };
93
+ });
94
+ });
95
+ }
96
+
97
+ applyDelta(definitionId: BucketDefinitionId, delta: bigint, writes: MongoWriteBatch): void {
98
+ this.applyDeltas(new Map([[definitionId, delta]]), writes);
99
+ }
100
+
101
+ applyDeltas(deltas: ReadonlyMap<BucketDefinitionId, bigint>, writes: MongoWriteBatch): void {
102
+ if (this.writerId == null) {
103
+ throw new ReplicationAssertionError('A writer id is required to apply object-storage usage');
104
+ }
105
+
106
+ const increments: Record<string, bigint> = {};
107
+ for (const [definitionId, delta] of deltas) {
108
+ if (delta === 0n) {
109
+ continue;
110
+ }
111
+ increments[this.definitionPath(definitionId)] = delta;
112
+ }
113
+ if (Object.keys(increments).length === 0) {
114
+ return;
115
+ }
116
+
117
+ writes.updateOne(
118
+ this.db.objectStorageUsage,
119
+ { _id: this.documentId() },
120
+ { $inc: increments, $currentDate: { updated_at: true } },
121
+ { upsert: true }
122
+ );
123
+ }
124
+
125
+ async removeDefinition(definitionId: BucketDefinitionId, session: mongo.ClientSession): Promise<void> {
126
+ await this.db.objectStorageUsage.updateMany(
127
+ { '_id.g': this.replicationStreamId },
128
+ { $unset: { [this.definitionPath(definitionId)]: 1 } },
129
+ { session }
130
+ );
131
+ }
132
+
133
+ async removeStream(session: mongo.ClientSession): Promise<void> {
134
+ await this.db.objectStorageUsage.deleteMany({ '_id.g': this.replicationStreamId }, { session });
135
+ }
136
+
137
+ async foldStaleWriterDeltas(
138
+ options: {
139
+ staleWriterMs?: number;
140
+ limit?: number;
141
+ } = {}
142
+ ): Promise<void> {
143
+ const staleWriterMs = options.staleWriterMs ?? DEFAULT_OBJECT_STORAGE_USAGE_STALE_WRITER_MS;
144
+ const limit = options.limit ?? DEFAULT_OBJECT_STORAGE_USAGE_FOLD_LIMIT;
145
+ if (limit <= 0) {
146
+ return;
147
+ }
148
+
149
+ await this.db.client.withSession((session) =>
150
+ session.withTransaction(
151
+ async () => {
152
+ const staleDocuments = await this.db.objectStorageUsage
153
+ .find(
154
+ {
155
+ '_id.g': this.replicationStreamId,
156
+ '_id.w': { $ne: OBJECT_STORAGE_USAGE_BASE_WRITER_ID },
157
+ $expr: {
158
+ $lt: [
159
+ '$updated_at',
160
+ { $dateSubtract: { startDate: '$$NOW', unit: 'millisecond', amount: staleWriterMs } }
161
+ ]
162
+ }
163
+ },
164
+ { session, sort: { updated_at: 1 }, limit }
165
+ )
166
+ .toArray();
167
+ if (staleDocuments.length === 0) {
168
+ return;
169
+ }
170
+
171
+ const deltas = new Map<BucketDefinitionId, bigint>();
172
+ for (const document of staleDocuments) {
173
+ for (const [definitionId, delta] of Object.entries(document.definitions ?? {})) {
174
+ this.validateDefinitionId(definitionId);
175
+ deltas.set(definitionId, (deltas.get(definitionId) ?? 0n) + BigInt(delta));
176
+ }
177
+ }
178
+
179
+ const increments: Record<string, bigint> = {};
180
+ for (const [definitionId, delta] of deltas) {
181
+ if (delta !== 0n) {
182
+ increments[this.definitionPath(definitionId)] = delta;
183
+ }
184
+ }
185
+ if (Object.keys(increments).length > 0) {
186
+ await this.db.objectStorageUsage.updateOne(
187
+ { _id: this.documentId(OBJECT_STORAGE_USAGE_BASE_WRITER_ID) },
188
+ {
189
+ $inc: increments,
190
+ $currentDate: { updated_at: true }
191
+ },
192
+ { upsert: true, session }
193
+ );
194
+ }
195
+
196
+ await this.db.objectStorageUsage.deleteMany(
197
+ {
198
+ '_id.g': this.replicationStreamId,
199
+ _id: { $in: staleDocuments.map((document) => document._id) }
200
+ },
201
+ { session }
202
+ );
203
+ },
204
+ { readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' } }
205
+ )
206
+ );
207
+ }
208
+
209
+ private definitionPath(definitionId: BucketDefinitionId): string {
210
+ this.validateDefinitionId(definitionId);
211
+ return `definitions.${definitionId}`;
212
+ }
213
+
214
+ private documentId(writerId = this.writerId): ObjectStorageUsageDocument['_id'] {
215
+ if (writerId == null) {
216
+ throw new ReplicationAssertionError('A writer id is required to identify object-storage usage');
217
+ }
218
+ return {
219
+ g: this.replicationStreamId,
220
+ w: writerId
221
+ };
222
+ }
223
+
224
+ private validateDefinitionId(definitionId: BucketDefinitionId): void {
225
+ if (definitionId.length === 0 || definitionId.includes('.') || definitionId.includes('$')) {
226
+ throw new ReplicationAssertionError(`Invalid bucket definition id for object-storage usage: ${definitionId}`);
227
+ }
228
+ }
229
+ }