@powersync/service-module-mongodb-storage 0.0.0-dev-20260602073133 → 0.0.0-dev-20260909133214

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 (275) hide show
  1. package/CHANGELOG.md +177 -5
  2. package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.d.ts +3 -0
  3. package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js +41 -0
  4. package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js.map +1 -0
  5. package/dist/storage/MongoBucketStorage.d.ts +71 -18
  6. package/dist/storage/MongoBucketStorage.js +516 -202
  7. package/dist/storage/MongoBucketStorage.js.map +1 -1
  8. package/dist/storage/implementation/MongoBucketBatch.d.ts +71 -29
  9. package/dist/storage/implementation/MongoBucketBatch.js +140 -66
  10. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  11. package/dist/storage/implementation/MongoChecksums.d.ts +41 -13
  12. package/dist/storage/implementation/MongoChecksums.js +36 -121
  13. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  14. package/dist/storage/implementation/MongoCompactor.d.ts +28 -90
  15. package/dist/storage/implementation/MongoCompactor.js +87 -481
  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 +82 -8
  21. package/dist/storage/implementation/MongoParameterCompactor.js +312 -76
  22. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  23. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +21 -0
  24. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +93 -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 +125 -0
  31. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
  32. package/dist/storage/implementation/MongoStorageProvider.js +41 -4
  33. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  34. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +81 -37
  35. package/dist/storage/implementation/MongoSyncBucketStorage.js +159 -86
  36. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  37. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +1 -1
  38. package/dist/storage/implementation/MongoSyncRulesLock.js +23 -11
  39. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  40. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +11 -8
  41. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +159 -33
  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 +28 -0
  47. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js +2 -0
  48. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js.map +1 -0
  49. package/dist/storage/implementation/common/PersistedBatch.d.ts +34 -2
  50. package/dist/storage/implementation/common/PersistedBatch.js +63 -10
  51. package/dist/storage/implementation/common/PersistedBatch.js.map +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 +4 -1
  58. package/dist/storage/implementation/db.js +31 -16
  59. package/dist/storage/implementation/db.js.map +1 -1
  60. package/dist/storage/implementation/models.d.ts +55 -13
  61. package/dist/storage/implementation/models.js.map +1 -1
  62. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +15 -1
  63. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +135 -107
  64. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  65. package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +4 -4
  66. package/dist/storage/implementation/v1/MongoChecksumsV1.js +119 -7
  67. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
  68. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +47 -7
  69. package/dist/storage/implementation/v1/MongoCompactorV1.js +478 -8
  70. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  71. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
  72. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
  73. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
  74. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +33 -27
  75. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +146 -73
  76. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  77. package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
  78. package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
  79. package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
  80. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +2 -1
  81. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -0
  82. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
  83. package/dist/storage/implementation/v1/models.d.ts +10 -1
  84. package/dist/storage/implementation/v1/models.js +1 -0
  85. package/dist/storage/implementation/v1/models.js.map +1 -1
  86. package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
  87. package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
  88. package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
  89. package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
  90. package/dist/storage/implementation/v3/CompactionLease.js +131 -0
  91. package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
  92. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +16 -5
  93. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +393 -262
  94. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  95. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +30 -9
  96. package/dist/storage/implementation/v3/MongoChecksumsV3.js +161 -20
  97. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  98. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +108 -18
  99. package/dist/storage/implementation/v3/MongoCompactorV3.js +1115 -47
  100. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  101. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +12 -3
  102. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +13 -8
  103. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -1
  104. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +54 -0
  105. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +319 -0
  106. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
  107. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +61 -36
  108. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +353 -161
  109. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  110. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
  111. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
  112. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
  113. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +8 -3
  114. package/dist/storage/implementation/v3/PersistedBatchV3.js +131 -29
  115. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  116. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +3 -3
  117. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +13 -11
  118. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
  119. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +26 -14
  120. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +73 -29
  121. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  122. package/dist/storage/implementation/v3/bucket-format.d.ts +13 -0
  123. package/dist/storage/implementation/v3/bucket-format.js +79 -0
  124. package/dist/storage/implementation/v3/bucket-format.js.map +1 -0
  125. package/dist/storage/implementation/v3/chunking.d.ts +10 -0
  126. package/dist/storage/implementation/v3/chunking.js +30 -0
  127. package/dist/storage/implementation/v3/chunking.js.map +1 -0
  128. package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
  129. package/dist/storage/implementation/v3/compact-utils.js +216 -0
  130. package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
  131. package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
  132. package/dist/storage/implementation/v3/compaction-constants.js +6 -0
  133. package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
  134. package/dist/storage/implementation/v3/models.d.ts +187 -23
  135. package/dist/storage/implementation/v3/models.js +11 -28
  136. package/dist/storage/implementation/v3/models.js.map +1 -1
  137. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +20 -0
  138. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +48 -0
  139. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
  140. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +33 -0
  141. package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
  142. package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
  143. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +68 -0
  144. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +154 -0
  145. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
  146. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +37 -0
  147. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +167 -0
  148. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
  149. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +67 -0
  150. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +439 -0
  151. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
  152. package/dist/storage/implementation/v3/source-table-utils.d.ts +63 -0
  153. package/dist/storage/implementation/v3/source-table-utils.js +206 -0
  154. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
  155. package/dist/storage/storage-index.d.ts +5 -4
  156. package/dist/storage/storage-index.js +5 -4
  157. package/dist/storage/storage-index.js.map +1 -1
  158. package/dist/types/types.d.ts +71 -3
  159. package/dist/types/types.js +58 -2
  160. package/dist/types/types.js.map +1 -1
  161. package/dist/utils/test-utils.d.ts +1 -2
  162. package/dist/utils/test-utils.js +7 -1
  163. package/dist/utils/test-utils.js.map +1 -1
  164. package/dist/utils/util.d.ts +9 -2
  165. package/dist/utils/util.js +131 -5
  166. package/dist/utils/util.js.map +1 -1
  167. package/package.json +13 -19
  168. package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
  169. package/src/storage/MongoBucketStorage.ts +728 -242
  170. package/src/storage/implementation/MongoBucketBatch.ts +196 -91
  171. package/src/storage/implementation/MongoChecksums.ts +93 -153
  172. package/src/storage/implementation/MongoCompactor.ts +106 -631
  173. package/src/storage/implementation/MongoHydrationState.ts +61 -0
  174. package/src/storage/implementation/MongoParameterCompactor.ts +392 -87
  175. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +122 -0
  176. package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
  177. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +156 -0
  178. package/src/storage/implementation/MongoStorageProvider.ts +48 -4
  179. package/src/storage/implementation/MongoSyncBucketStorage.ts +226 -118
  180. package/src/storage/implementation/MongoSyncRulesLock.ts +28 -12
  181. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +194 -52
  182. package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
  183. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +31 -0
  184. package/src/storage/implementation/common/PersistedBatch.ts +83 -15
  185. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
  186. package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
  187. package/src/storage/implementation/db.ts +40 -22
  188. package/src/storage/implementation/models.ts +58 -14
  189. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +174 -125
  190. package/src/storage/implementation/v1/MongoChecksumsV1.ts +154 -12
  191. package/src/storage/implementation/v1/MongoCompactorV1.ts +594 -25
  192. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
  193. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +226 -129
  194. package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
  195. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +11 -1
  196. package/src/storage/implementation/v1/models.ts +12 -1
  197. package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
  198. package/src/storage/implementation/v3/CompactionLease.ts +152 -0
  199. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +502 -324
  200. package/src/storage/implementation/v3/MongoChecksumsV3.ts +250 -32
  201. package/src/storage/implementation/v3/MongoCompactorV3.ts +1439 -70
  202. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +19 -9
  203. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +473 -0
  204. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +453 -211
  205. package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
  206. package/src/storage/implementation/v3/PersistedBatchV3.ts +169 -45
  207. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
  208. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +99 -38
  209. package/src/storage/implementation/v3/bucket-format.ts +99 -0
  210. package/src/storage/implementation/v3/chunking.ts +40 -0
  211. package/src/storage/implementation/v3/compact-utils.ts +320 -0
  212. package/src/storage/implementation/v3/compaction-constants.ts +5 -0
  213. package/src/storage/implementation/v3/models.ts +228 -58
  214. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +62 -0
  215. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +44 -0
  216. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +182 -0
  217. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +230 -0
  218. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +573 -0
  219. package/src/storage/implementation/v3/source-table-utils.ts +398 -0
  220. package/src/storage/storage-index.ts +14 -4
  221. package/src/types/types.ts +76 -2
  222. package/src/utils/test-utils.ts +8 -3
  223. package/src/utils/util.ts +188 -7
  224. package/test/src/__snapshots__/storage.test.ts.snap +4 -54
  225. package/test/src/__snapshots__/storage_sync.test.ts.snap +652 -645
  226. package/test/src/cleanup-stopped-sync-configs.test.ts +698 -0
  227. package/test/src/compact-utils.test.ts +226 -0
  228. package/test/src/helpers/MemoryObjectStorage.ts +61 -0
  229. package/test/src/helpers/s3TestFactory.ts +69 -0
  230. package/test/src/object_storage_lifecycle.test.ts +59 -0
  231. package/test/src/object_storage_usage.test.ts +255 -0
  232. package/test/src/parameter_compacting_v1.test.ts +120 -0
  233. package/test/src/parameter_compacting_v3.test.ts +192 -0
  234. package/test/src/parameter_compaction_fence.test.ts +276 -0
  235. package/test/src/setup.ts +6 -1
  236. package/test/src/storage.test.ts +478 -13
  237. package/test/src/storage_compacting.test.ts +2516 -107
  238. package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
  239. package/test/src/storage_s3_checksums.test.ts +190 -0
  240. package/test/src/storage_s3_compaction_lifecycle.test.ts +543 -0
  241. package/test/src/storage_s3_reading.test.ts +768 -0
  242. package/test/src/storage_s3_writing.test.ts +136 -0
  243. package/test/src/storage_sync.test.ts +1507 -76
  244. package/test/src/storeCurrentData.test.ts +227 -0
  245. package/test/src/util.ts +3 -2
  246. package/tsconfig.json +1 -4
  247. package/tsconfig.tsbuildinfo +1 -1
  248. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +0 -15
  249. package/dist/storage/implementation/BucketDefinitionMapping.js +0 -58
  250. package/dist/storage/implementation/BucketDefinitionMapping.js.map +0 -1
  251. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -14
  252. package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -67
  253. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
  254. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -29
  255. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -76
  256. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
  257. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +0 -13
  258. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +0 -2
  259. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +0 -1
  260. package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
  261. package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
  262. package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
  263. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +0 -4
  264. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +0 -9
  265. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +0 -1
  266. package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
  267. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -48
  268. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
  269. package/src/storage/implementation/BucketDefinitionMapping.ts +0 -75
  270. package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -82
  271. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -101
  272. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +0 -15
  273. package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
  274. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -11
  275. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -68
@@ -0,0 +1,573 @@
1
+ import {
2
+ DeleteObjectsCommand,
3
+ GetObjectCommand,
4
+ ListObjectsV2Command,
5
+ ListObjectsV2CommandOutput,
6
+ PutObjectCommand,
7
+ S3Client
8
+ } from '@aws-sdk/client-s3';
9
+ import { logger } from '@powersync/lib-services-framework';
10
+ import { acquireSemaphoreAbortable, isAbortError } from '@powersync/service-core';
11
+ import { loadConfigsForDefaultMode, type DefaultsMode, type ResolvedDefaultsMode } from '@smithy/core/client';
12
+ import { isThrottlingError, isTransientError } from '@smithy/core/retry';
13
+ import { NodeHttpHandler } from '@smithy/node-http-handler';
14
+ import { Semaphore, SemaphoreInterface } from 'async-mutex';
15
+ import {
16
+ ObjectStorageError,
17
+ type ObjectStorage,
18
+ type ObjectStorageOperationOptions,
19
+ type ObjectStoragePutMetadata
20
+ } from './ObjectStorage.js';
21
+
22
+ const DEFAULT_S3_OPERATION_CONCURRENCY = 16;
23
+ const S3_DELETE_PREFIX_BATCH_SIZE = 1000;
24
+ /**
25
+ * Slightly smaller than DEFAULT_S3_OPERATION_CONCURRENCY.
26
+ */
27
+ const S3_DELETE_PREFIX_CONCURRENCY = 12;
28
+ const MAX_S3_PREFIX_BYTES = 256;
29
+ const SAFE_S3_KEY_BYTES = 896;
30
+
31
+ /**
32
+ * Timeouts are derived from the AWS defaults mode, rather than being individually configurable.
33
+ *
34
+ * The mode - AWS_DEFAULTS_MODE or the defaults_mode storage option - describes the expected latency
35
+ * between this service and the object storage endpoint. We use the connection timeout it defines
36
+ * as the baseline for the other timeouts:
37
+ *
38
+ * | mode | connection | request | operation | queue |
39
+ * | ---------------------- | ---------- | ------- | --------- | ----- |
40
+ * | in-region | 1.1s | 5.5s | 11s | 44s |
41
+ * | standard, cross-region | 3.1s | 15.5s | 31s | 124s |
42
+ * | mobile | 30s | 150s | 300s | 1200s |
43
+ *
44
+ * The multipliers are sized for the objects we store: bucket data chunks target 1MB, and are
45
+ * bounded by the 16MB BSON document limit. Even at the upper bound, the request timeout only has to
46
+ * cover a transfer of a few MB/s, and the operation timeout leaves room for a second attempt.
47
+ *
48
+ * `legacy`, the mode used when nothing is configured, does not define any timeouts, and is treated
49
+ * as `standard` here. Running without timeouts is not an option: a stalled request holds one of the
50
+ * limited operation slots until the process restarts, and enough of those block all reads.
51
+ *
52
+ * `auto` is also treated as `standard` - see resolveConfiguredDefaultsMode() below.
53
+ */
54
+ const BASELINE_CONNECTION_TIMEOUT_MS = 3_100;
55
+ const REQUEST_TIMEOUT_FACTOR = 5;
56
+ const OPERATION_TIMEOUT_FACTOR = 2;
57
+ /**
58
+ * How long an operation may wait for a concurrency slot, as a multiple of the operation timeout.
59
+ *
60
+ * Every slot holder now releases within the operation timeout, so the queue always drains: this is
61
+ * a backpressure limit rather than a deadlock guard. Waiting this long means at least
62
+ * `factor * concurrencyLimit` operations ahead of us each took their full deadline - 64 at the
63
+ * default concurrency. Real operations complete in a fraction of the deadline, so the number of
64
+ * queued operations this actually tolerates is far higher, well above the ~2000 uploads that the
65
+ * largest possible replication flush (MAX_TRANSACTION_DOC_COUNT) can enqueue at once.
66
+ */
67
+ const QUEUE_TIMEOUT_FACTOR = 4;
68
+
69
+ export interface S3TimeoutProfile {
70
+ /**
71
+ * Per attempt: establishing the connection, including waiting for a socket from the pool.
72
+ */
73
+ connectionTimeoutMs: number;
74
+ /**
75
+ * Per attempt: from starting the request until the response headers are received. For uploads
76
+ * this includes sending the body.
77
+ */
78
+ requestTimeoutMs: number;
79
+ /**
80
+ * The complete operation, including SDK retries and streaming the response body.
81
+ *
82
+ * The SDK timeouts above stop applying once the response headers are received, so this is what
83
+ * bounds how long a single operation can occupy a concurrency slot.
84
+ */
85
+ operationTimeoutMs: number;
86
+ /**
87
+ * Waiting for a concurrency slot, before the operation itself starts.
88
+ */
89
+ queueTimeoutMs: number;
90
+ }
91
+
92
+ export function resolveTimeoutProfile(mode: ResolvedDefaultsMode): S3TimeoutProfile {
93
+ // Only connectionTimeout is currently vended by the SDK, but requestTimeout is part of the same
94
+ // contract - use it as the hint if a future version starts defining it.
95
+ const { connectionTimeout, requestTimeout } = loadConfigsForDefaultMode(mode);
96
+ const connectionTimeoutMs = connectionTimeout ?? BASELINE_CONNECTION_TIMEOUT_MS;
97
+ const requestTimeoutMs = requestTimeout ?? connectionTimeoutMs * REQUEST_TIMEOUT_FACTOR;
98
+ const operationTimeoutMs = requestTimeoutMs * OPERATION_TIMEOUT_FACTOR;
99
+ return {
100
+ connectionTimeoutMs,
101
+ requestTimeoutMs,
102
+ operationTimeoutMs,
103
+ queueTimeoutMs: operationTimeoutMs * QUEUE_TIMEOUT_FACTOR
104
+ };
105
+ }
106
+
107
+ const AWS_DEFAULTS_MODE_ENV = 'AWS_DEFAULTS_MODE';
108
+ const RESOLVED_DEFAULTS_MODES: ResolvedDefaultsMode[] = ['standard', 'in-region', 'cross-region', 'mobile', 'legacy'];
109
+
110
+ /**
111
+ * Resolve the configured AWS defaults mode: the explicit option, then the AWS_DEFAULTS_MODE
112
+ * environment variable, then `legacy`, which is also the AWS default.
113
+ *
114
+ * Unlike the SDK's own lookup, this does not read defaults_mode from the AWS shared config file.
115
+ * That lookup is asynchronous, and the mode is only used to pick timeouts, which we want available
116
+ * synchronously so that operations never have to wait on configuration.
117
+ *
118
+ * `auto` is substituted with `standard`. It asks the SDK to detect whether it is running in the
119
+ * same region as the endpoint, which means querying the EC2 instance metadata service on startup:
120
+ *
121
+ * - The detection only succeeds on EC2-family compute with instance metadata reachable. Anywhere
122
+ * else - including EC2 with IMDS disabled or behind a low hop limit - it silently falls back to
123
+ * `standard`, so the timeouts differ by deployment with no indication of why.
124
+ * - It requires a resolvable region, and fails the client outright when there is none.
125
+ * - Timeouts that depend on where the process happens to run are hard to reason about when
126
+ * diagnosing a stall.
127
+ *
128
+ * Deployments that want the tighter in-region timeouts should ask for them explicitly.
129
+ */
130
+ function resolveConfiguredDefaultsMode(configured?: DefaultsMode): ResolvedDefaultsMode {
131
+ // An empty environment variable is treated as unset, matching the SDK.
132
+ const mode = (configured || process.env[AWS_DEFAULTS_MODE_ENV] || 'legacy').toLowerCase();
133
+ if (mode === 'auto') {
134
+ logger.warn(
135
+ `Ignoring AWS defaults mode "auto" for object storage, using "standard" instead. ` +
136
+ `Configure storage.object_storage.defaults_mode to select a mode explicitly.`
137
+ );
138
+ return 'standard';
139
+ }
140
+ const resolved = RESOLVED_DEFAULTS_MODES.find((candidate) => candidate === mode);
141
+ if (resolved == null) {
142
+ throw new Error(
143
+ `Invalid AWS defaults mode ${JSON.stringify(mode)}, expected one of ${RESOLVED_DEFAULTS_MODES.join(', ')}, auto`
144
+ );
145
+ }
146
+ return resolved;
147
+ }
148
+
149
+ /**
150
+ * An S3 operation holding a concurrency slot, released when the operation is disposed.
151
+ */
152
+ interface S3Operation extends AsyncDisposable {
153
+ /**
154
+ * The caller's signal, combined with the operation deadline.
155
+ *
156
+ * Use this for the request itself, so that neither a stalled request nor a stalled response body
157
+ * can hold on to the slot indefinitely.
158
+ */
159
+ signal: AbortSignal;
160
+ /**
161
+ * The error to report for a failed operation.
162
+ */
163
+ error(cause: unknown): Error;
164
+ }
165
+
166
+ export interface S3ObjectStorageOptions {
167
+ bucket: string;
168
+ region?: string;
169
+ prefix?: string;
170
+ endpoint?: string;
171
+ forcePathStyle?: boolean;
172
+ accessKeyId?: string;
173
+ secretAccessKey?: string;
174
+ concurrencyLimit?: number;
175
+ /**
176
+ * AWS defaults mode, used as the baseline for the request timeouts.
177
+ *
178
+ * Defaults to the AWS_DEFAULTS_MODE environment variable. `auto` is treated as `standard`.
179
+ */
180
+ defaultsMode?: DefaultsMode;
181
+ }
182
+
183
+ export class S3ObjectStorage implements ObjectStorage {
184
+ /**
185
+ * Public for tests only.
186
+ */
187
+ public readonly client: S3Client;
188
+ private bucket: string;
189
+ private prefix: string;
190
+ private readonly operationSemaphore: SemaphoreInterface;
191
+ private readonly timeouts: S3TimeoutProfile;
192
+
193
+ constructor(options: S3ObjectStorageOptions) {
194
+ const concurrencyLimit = options.concurrencyLimit ?? DEFAULT_S3_OPERATION_CONCURRENCY;
195
+ if (!Number.isInteger(concurrencyLimit) || concurrencyLimit <= 0) {
196
+ throw new Error('S3 object storage concurrencyLimit must be a positive integer');
197
+ }
198
+ if ((options.accessKeyId == null) !== (options.secretAccessKey == null)) {
199
+ throw new Error('S3 object storage accessKeyId and secretAccessKey must be configured together');
200
+ }
201
+
202
+ const prefix = options.prefix ?? '';
203
+ if (Buffer.byteLength(prefix, 'utf8') > MAX_S3_PREFIX_BYTES) {
204
+ throw new Error(`S3 object storage prefix must be at most ${MAX_S3_PREFIX_BYTES} UTF-8 bytes`);
205
+ }
206
+ if (prefix.endsWith('/')) {
207
+ throw new Error('S3 object storage prefix must not end with "/"');
208
+ }
209
+
210
+ this.bucket = options.bucket;
211
+ this.prefix = prefix;
212
+ this.operationSemaphore = new Semaphore(concurrencyLimit);
213
+
214
+ // Resolve the mode once, and share it with the client so that its own defaults agree with ours.
215
+ const defaultsMode = resolveConfiguredDefaultsMode(options.defaultsMode);
216
+ this.timeouts = resolveTimeoutProfile(defaultsMode);
217
+
218
+ this.client = new S3Client({
219
+ region: options.region,
220
+ endpoint: options.endpoint,
221
+ forcePathStyle: options.forcePathStyle,
222
+ defaultsMode,
223
+ requestHandler: new NodeHttpHandler({
224
+ connectionTimeout: this.timeouts.connectionTimeoutMs,
225
+ requestTimeout: this.timeouts.requestTimeoutMs,
226
+ // Without this, exceeding requestTimeout only logs a warning.
227
+ throwOnRequestTimeout: true
228
+ }),
229
+ credentials:
230
+ options.accessKeyId && options.secretAccessKey
231
+ ? { accessKeyId: options.accessKeyId, secretAccessKey: options.secretAccessKey }
232
+ : undefined
233
+ });
234
+ }
235
+
236
+ async put(
237
+ path: string,
238
+ data: Uint8Array,
239
+ metadata: ObjectStoragePutMetadata,
240
+ options?: ObjectStorageOperationOptions
241
+ ): Promise<void> {
242
+ const fullPath = this.fullPath(path);
243
+ await using operation = await this.withOperation('upload', fullPath, options?.signal);
244
+ try {
245
+ await this.client.send(
246
+ new PutObjectCommand({
247
+ Bucket: this.bucket,
248
+ Key: fullPath,
249
+ Body: data,
250
+ ContentType: metadata.contentType,
251
+ ContentEncoding: metadata.contentEncoding ?? undefined
252
+ }),
253
+ { abortSignal: operation.signal }
254
+ );
255
+ } catch (error) {
256
+ throw operation.error(error);
257
+ }
258
+ }
259
+
260
+ async get(
261
+ path: string,
262
+ options?: ObjectStorageOperationOptions
263
+ ): Promise<{ data: Uint8Array; metadata: ObjectStoragePutMetadata }> {
264
+ const fullPath = this.fullPath(path);
265
+ await using operation = await this.withOperation('download', fullPath, options?.signal);
266
+ // Includes the operation deadline, which also covers streaming the response body below.
267
+ const signal = operation.signal;
268
+ try {
269
+ const response = await this.client.send(
270
+ new GetObjectCommand({
271
+ Bucket: this.bucket,
272
+ Key: fullPath
273
+ }),
274
+ { abortSignal: signal }
275
+ );
276
+ const contentLength = response.ContentLength;
277
+ if (contentLength == null) {
278
+ throw new Error(`S3 download response for ${fullPath} is missing ContentLength`);
279
+ }
280
+ if (!Number.isSafeInteger(contentLength) || contentLength < 0) {
281
+ throw new Error(`S3 download response for ${fullPath} has invalid ContentLength ${contentLength}`);
282
+ }
283
+
284
+ const data = new Uint8Array(contentLength);
285
+ let offset = 0;
286
+ const stream = response.Body as AsyncIterable<Uint8Array>;
287
+ for await (const chunk of stream) {
288
+ signal.throwIfAborted();
289
+ const nextOffset = offset + chunk.byteLength;
290
+ if (nextOffset > contentLength) {
291
+ throw new Error(
292
+ `S3 download ContentLength mismatch for ${fullPath}: expected ${contentLength} bytes, received at least ${nextOffset}`
293
+ );
294
+ }
295
+ data.set(chunk, offset);
296
+ offset = nextOffset;
297
+ }
298
+ if (offset !== contentLength) {
299
+ throw new Error(
300
+ `S3 download ContentLength mismatch for ${fullPath}: expected ${contentLength} bytes, received ${offset}`
301
+ );
302
+ }
303
+
304
+ return {
305
+ data,
306
+ metadata: {
307
+ contentType: response.ContentType ?? 'application/octet-stream',
308
+ contentEncoding: response.ContentEncoding ?? null
309
+ }
310
+ };
311
+ } catch (err: any) {
312
+ if (err.name === 'NoSuchKey' || err.Code === 'NoSuchKey') {
313
+ throw new ObjectStorageError(`S3 object not found: ${fullPath}`, { cause: err, retryable: false });
314
+ }
315
+ throw operation.error(err);
316
+ }
317
+ }
318
+
319
+ async *list(prefix: string, options?: ObjectStorageOperationOptions): AsyncIterable<string> {
320
+ const fullPrefix = this.fullPath(prefix);
321
+ let continuationToken: string | undefined;
322
+ const signal = options?.signal;
323
+
324
+ do {
325
+ signal?.throwIfAborted();
326
+ const response = await this.listPage(fullPrefix, continuationToken, signal);
327
+ for (const object of response.Contents ?? []) {
328
+ if (object.Key == null) {
329
+ continue;
330
+ }
331
+ yield this.prefix ? object.Key.slice(this.prefix.length + 1) : object.Key;
332
+ }
333
+ continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined;
334
+ if (response.IsTruncated && continuationToken == null) {
335
+ throw new Error(`S3 listing for ${fullPrefix} was truncated without a continuation token`);
336
+ }
337
+ } while (continuationToken != null);
338
+ }
339
+
340
+ async delete(paths: string[], options?: ObjectStorageOperationOptions): Promise<void> {
341
+ const fullPaths = paths.map((path) => ({ Key: this.fullPath(path) }));
342
+ await this.deleteFullPaths(fullPaths, options?.signal);
343
+ }
344
+
345
+ async deletePrefix(prefix: string, options?: ObjectStorageOperationOptions): Promise<{ objectCount: number }> {
346
+ const fullPrefix = this.fullPath(prefix);
347
+ const signal = options?.signal;
348
+ let continuationToken: string | undefined;
349
+ let objectCount = 0;
350
+ const deleting = new Set<Promise<void>>();
351
+ let deleteError: { error: unknown } | undefined;
352
+
353
+ const scheduleDelete = (fullPaths: { Key: string }[]) => {
354
+ let tracked: Promise<void>;
355
+ tracked = this.deleteFullPaths(fullPaths, signal)
356
+ .catch((error) => {
357
+ deleteError ??= { error };
358
+ })
359
+ .finally(() => deleting.delete(tracked));
360
+ deleting.add(tracked);
361
+ };
362
+ const throwDeleteError = () => {
363
+ if (deleteError != null) {
364
+ throw deleteError.error;
365
+ }
366
+ };
367
+
368
+ let operationError: { error: unknown } | undefined;
369
+ try {
370
+ do {
371
+ signal?.throwIfAborted();
372
+ const response = await this.listPage(fullPrefix, continuationToken, signal);
373
+ throwDeleteError();
374
+ const fullPaths: { Key: string }[] = [];
375
+ for (const object of response.Contents ?? []) {
376
+ if (object.Key != null) {
377
+ fullPaths.push({ Key: object.Key });
378
+ }
379
+ }
380
+ objectCount += fullPaths.length;
381
+ if (fullPaths.length !== 0) {
382
+ while (deleting.size >= S3_DELETE_PREFIX_CONCURRENCY) {
383
+ await Promise.race(deleting);
384
+ throwDeleteError();
385
+ }
386
+ scheduleDelete(fullPaths);
387
+ }
388
+ continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined;
389
+ if (response.IsTruncated && continuationToken == null) {
390
+ throw new Error(`S3 listing for ${fullPrefix} was truncated without a continuation token`);
391
+ }
392
+ } while (continuationToken != null);
393
+ } catch (error) {
394
+ operationError = { error };
395
+ }
396
+
397
+ await Promise.all(deleting);
398
+ if (operationError != null) {
399
+ throw operationError.error;
400
+ }
401
+ throwDeleteError();
402
+ return { objectCount };
403
+ }
404
+
405
+ private async listPage(
406
+ fullPrefix: string,
407
+ continuationToken: string | undefined,
408
+ signal?: AbortSignal
409
+ ): Promise<ListObjectsV2CommandOutput> {
410
+ // Acquired outside the try, matching the other operations: withOperation() already reports its
411
+ // own failures as ObjectStorageError.
412
+ await using operation = await this.withOperation('list', fullPrefix, signal);
413
+ try {
414
+ return await this.client.send(
415
+ new ListObjectsV2Command({
416
+ Bucket: this.bucket,
417
+ Prefix: fullPrefix,
418
+ ContinuationToken: continuationToken,
419
+ MaxKeys: S3_DELETE_PREFIX_BATCH_SIZE
420
+ }),
421
+ { abortSignal: operation.signal }
422
+ );
423
+ } catch (error) {
424
+ throw operation.error(error);
425
+ }
426
+ }
427
+
428
+ private async deleteFullPaths(fullPaths: { Key: string }[], signal?: AbortSignal): Promise<void> {
429
+ if (fullPaths.length === 0) return;
430
+ await using operation = await this.withOperation('delete', `${fullPaths.length} objects`, signal);
431
+ let response;
432
+ try {
433
+ response = await this.client.send(
434
+ new DeleteObjectsCommand({
435
+ Bucket: this.bucket,
436
+ Delete: { Objects: fullPaths, Quiet: true }
437
+ }),
438
+ { abortSignal: operation.signal }
439
+ );
440
+ } catch (error) {
441
+ throw operation.error(error);
442
+ }
443
+ if (response.Errors?.length) {
444
+ const errors = response.Errors.map((error) => `${error.Key}: ${error.Code}`).join(', ');
445
+ throw new ObjectStorageError(`Failed to delete S3 objects: ${errors}`, {
446
+ cause: response.Errors,
447
+ retryable: response.Errors.some((error) => isTransientS3Error({ name: error.Code }))
448
+ });
449
+ }
450
+ }
451
+
452
+ private fullPath(path: string): string {
453
+ const fullPath = this.prefix ? `${this.prefix}/${path}` : path;
454
+ const size = Buffer.byteLength(fullPath, 'utf8');
455
+ if (size > SAFE_S3_KEY_BYTES) {
456
+ throw new Error(`S3 object key is ${size} bytes, exceeding the safe limit of ${SAFE_S3_KEY_BYTES} bytes`);
457
+ }
458
+ return fullPath;
459
+ }
460
+
461
+ private async withOperation(operation: string, target: string, signal?: AbortSignal): Promise<S3Operation> {
462
+ signal?.throwIfAborted();
463
+ const { operationTimeoutMs, queueTimeoutMs } = this.timeouts;
464
+
465
+ // Waiting for a slot is bounded even when the caller has no signal of its own, which is the
466
+ // case for uploads and deletes.
467
+ const queueDeadline = timeoutSignal(queueTimeoutMs, () =>
468
+ s3TimeoutError(operation, target, `timed out after ${queueTimeoutMs} ms waiting for a concurrency slot`)
469
+ );
470
+ let acquired;
471
+ try {
472
+ acquired = await acquireSemaphoreAbortable(
473
+ this.operationSemaphore,
474
+ signal ? AbortSignal.any([signal, queueDeadline.signal]) : queueDeadline.signal
475
+ );
476
+ } finally {
477
+ queueDeadline.cancel();
478
+ }
479
+ if (acquired === 'aborted') {
480
+ // A caller abort takes precedence: it is a cancellation, not an S3 failure.
481
+ signal?.throwIfAborted();
482
+ throw queueDeadline.signal.reason;
483
+ }
484
+
485
+ const [, release] = acquired;
486
+ if (signal?.aborted) {
487
+ release();
488
+ signal.throwIfAborted();
489
+ }
490
+ // This deadline only starts once the operation holds a slot - queueing is covered above.
491
+ const deadline = timeoutSignal(operationTimeoutMs, () =>
492
+ s3TimeoutError(operation, target, `did not complete within ${operationTimeoutMs} ms`)
493
+ );
494
+ const operationSignal = signal ? AbortSignal.any([signal, deadline.signal]) : deadline.signal;
495
+ return {
496
+ signal: operationSignal,
497
+ error: (cause) => {
498
+ // Aborting a request that is already streaming destroys the socket, and the failure
499
+ // surfaces as a generic "aborted" error that says nothing about the cause. The abort
500
+ // reason is always the better answer: the caller's reason when the caller stopped us -
501
+ // callers abort with the error that stopped them, and upstream handling matches on that
502
+ // type - and an already-classified timeout when the deadline did.
503
+ if (operationSignal.aborted) {
504
+ return operationSignal.reason as Error;
505
+ }
506
+ return s3OperationError(operation, target, cause);
507
+ },
508
+ [Symbol.asyncDispose]: async () => {
509
+ deadline.cancel();
510
+ release();
511
+ }
512
+ };
513
+ }
514
+ }
515
+
516
+ /**
517
+ * An abort signal that fires after the given timeout, using a timer that does not hold the process
518
+ * open.
519
+ *
520
+ * The reason is what callers see when the timeout fires, so it is the error to report - not a
521
+ * marker to translate later.
522
+ */
523
+ function timeoutSignal(timeoutMs: number, createReason: () => unknown): { signal: AbortSignal; cancel: () => void } {
524
+ const controller = new AbortController();
525
+ const timer = setTimeout(() => controller.abort(createReason()), timeoutMs);
526
+ timer.unref();
527
+ return { signal: controller.signal, cancel: () => clearTimeout(timer) };
528
+ }
529
+
530
+ /**
531
+ * A timeout that the compactor may retry, matching how the AWS SDK classifies its own request
532
+ * timeouts.
533
+ */
534
+ function s3TimeoutError(operation: string, target: string, message: string): ObjectStorageError {
535
+ return new ObjectStorageError(`S3 ${operation} for ${target} ${message}`, {
536
+ cause: new DOMException(message, 'TimeoutError'),
537
+ retryable: true
538
+ });
539
+ }
540
+
541
+ function s3OperationError(operation: string, target: string, error: unknown): Error {
542
+ if (error instanceof ObjectStorageError) {
543
+ return error;
544
+ }
545
+ // Preserve abort errors so callers can distinguish cancellation from an S3
546
+ // failure and avoid retrying work after shutdown.
547
+ if (isAbortError(error)) {
548
+ return error as Error;
549
+ }
550
+ const detail = error instanceof Error ? `: ${error.message}` : '';
551
+ return new ObjectStorageError(`S3 ${operation} failed for ${target}${detail}`, {
552
+ cause: error,
553
+ retryable: isTransientS3Error(error)
554
+ });
555
+ }
556
+
557
+ function isTransientS3Error(error: unknown): boolean {
558
+ if (typeof error != 'object' || error == null) {
559
+ return false;
560
+ }
561
+ if (isAbortError(error)) {
562
+ return false;
563
+ }
564
+
565
+ const sdkError = error as Parameters<typeof isTransientError>[0];
566
+ // Use the same classification as the AWS SDK retry middleware. The SDK
567
+ // returns the final error after its own attempt/quota limits are exhausted;
568
+ // this tells the compactor whether restarting the larger bucket operation is
569
+ // still appropriate.
570
+ // One exception is clock skew errors: We don't treat that as retryable by us,
571
+ // although the SDK may internally retry them.
572
+ return isThrottlingError(sdkError) || isTransientError(sdkError);
573
+ }