@lossless.org/client 0.1.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 (255) hide show
  1. package/.smartconfig.json +37 -0
  2. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  3. package/dist_ts/00_commitinfo_data.js +9 -0
  4. package/dist_ts/classes.losslessorgclient.d.ts +32 -0
  5. package/dist_ts/classes.losslessorgclient.js +103 -0
  6. package/dist_ts/core/classes.error.d.ts +16 -0
  7. package/dist_ts/core/classes.error.js +29 -0
  8. package/dist_ts/core/classes.operation.d.ts +17 -0
  9. package/dist_ts/core/classes.operation.js +70 -0
  10. package/dist_ts/core/interfaces.d.ts +18 -0
  11. package/dist_ts/core/interfaces.js +2 -0
  12. package/dist_ts/index.d.ts +4 -0
  13. package/dist_ts/index.js +3 -0
  14. package/dist_ts/nosqldb/classes.atomicdelete.d.ts +26 -0
  15. package/dist_ts/nosqldb/classes.atomicdelete.js +44 -0
  16. package/dist_ts/nosqldb/classes.atomicfindoneandupdate.d.ts +27 -0
  17. package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +86 -0
  18. package/dist_ts/nosqldb/classes.atomicupdate.d.ts +26 -0
  19. package/dist_ts/nosqldb/classes.atomicupdate.js +55 -0
  20. package/dist_ts/nosqldb/classes.bootstraprecord.d.ts +14 -0
  21. package/dist_ts/nosqldb/classes.bootstraprecord.js +80 -0
  22. package/dist_ts/nosqldb/classes.bsonassertion.d.ts +2 -0
  23. package/dist_ts/nosqldb/classes.bsonassertion.js +29 -0
  24. package/dist_ts/nosqldb/classes.bsonobjectid.d.ts +2 -0
  25. package/dist_ts/nosqldb/classes.bsonobjectid.js +50 -0
  26. package/dist_ts/nosqldb/classes.bsonsnapshot.d.ts +3 -0
  27. package/dist_ts/nosqldb/classes.bsonsnapshot.js +126 -0
  28. package/dist_ts/nosqldb/classes.cached-document.d.ts +27 -0
  29. package/dist_ts/nosqldb/classes.cached-document.js +112 -0
  30. package/dist_ts/nosqldb/classes.collection.d.ts +302 -0
  31. package/dist_ts/nosqldb/classes.collection.js +1568 -0
  32. package/dist_ts/nosqldb/classes.collectionfactory.d.ts +30 -0
  33. package/dist_ts/nosqldb/classes.collectionfactory.js +65 -0
  34. package/dist_ts/nosqldb/classes.collectionlifecycle.d.ts +4 -0
  35. package/dist_ts/nosqldb/classes.collectionlifecycle.js +15 -0
  36. package/dist_ts/nosqldb/classes.collectionpreparation.d.ts +34 -0
  37. package/dist_ts/nosqldb/classes.collectionpreparation.js +134 -0
  38. package/dist_ts/nosqldb/classes.collectiontopology.d.ts +47 -0
  39. package/dist_ts/nosqldb/classes.collectiontopology.js +388 -0
  40. package/dist_ts/nosqldb/classes.connection.d.ts +18 -0
  41. package/dist_ts/nosqldb/classes.connection.js +23 -0
  42. package/dist_ts/nosqldb/classes.convenience.d.ts +1 -0
  43. package/dist_ts/nosqldb/classes.convenience.js +5 -0
  44. package/dist_ts/nosqldb/classes.cursor.d.ts +15 -0
  45. package/dist_ts/nosqldb/classes.cursor.js +82 -0
  46. package/dist_ts/nosqldb/classes.cursoroperation.d.ts +7 -0
  47. package/dist_ts/nosqldb/classes.cursoroperation.js +36 -0
  48. package/dist_ts/nosqldb/classes.db.d.ts +98 -0
  49. package/dist_ts/nosqldb/classes.db.js +434 -0
  50. package/dist_ts/nosqldb/classes.distributedcoordinator.d.ts +52 -0
  51. package/dist_ts/nosqldb/classes.distributedcoordinator.js +314 -0
  52. package/dist_ts/nosqldb/classes.doc.d.ts +722 -0
  53. package/dist_ts/nosqldb/classes.doc.js +2976 -0
  54. package/dist_ts/nosqldb/classes.easystore.d.ts +42 -0
  55. package/dist_ts/nosqldb/classes.easystore.js +199 -0
  56. package/dist_ts/nosqldb/classes.exactpersistence.d.ts +141 -0
  57. package/dist_ts/nosqldb/classes.exactpersistence.js +1110 -0
  58. package/dist_ts/nosqldb/classes.lucene.adapter.d.ts +173 -0
  59. package/dist_ts/nosqldb/classes.lucene.adapter.js +623 -0
  60. package/dist_ts/nosqldb/classes.mongoadministration.d.ts +73 -0
  61. package/dist_ts/nosqldb/classes.mongoadministration.js +497 -0
  62. package/dist_ts/nosqldb/classes.namespaceinspection.d.ts +42 -0
  63. package/dist_ts/nosqldb/classes.namespaceinspection.js +138 -0
  64. package/dist_ts/nosqldb/classes.operationbudget.d.ts +22 -0
  65. package/dist_ts/nosqldb/classes.operationbudget.js +65 -0
  66. package/dist_ts/nosqldb/classes.ordinarypersistence.d.ts +18 -0
  67. package/dist_ts/nosqldb/classes.ordinarypersistence.js +71 -0
  68. package/dist_ts/nosqldb/classes.persistence.d.ts +14 -0
  69. package/dist_ts/nosqldb/classes.persistence.js +34 -0
  70. package/dist_ts/nosqldb/classes.session.d.ts +28 -0
  71. package/dist_ts/nosqldb/classes.session.js +246 -0
  72. package/dist_ts/nosqldb/classes.storedinspection.d.ts +44 -0
  73. package/dist_ts/nosqldb/classes.storedinspection.js +100 -0
  74. package/dist_ts/nosqldb/classes.testdatabase.d.ts +56 -0
  75. package/dist_ts/nosqldb/classes.testdatabase.js +250 -0
  76. package/dist_ts/nosqldb/classes.watcher.d.ts +37 -0
  77. package/dist_ts/nosqldb/classes.watcher.js +98 -0
  78. package/dist_ts/nosqldb/index.d.ts +27 -0
  79. package/dist_ts/nosqldb/index.js +18 -0
  80. package/dist_ts/nosqldb/logging.d.ts +2 -0
  81. package/dist_ts/nosqldb/logging.js +3 -0
  82. package/dist_ts/nosqldb/plugins.d.ts +17 -0
  83. package/dist_ts/nosqldb/plugins.js +20 -0
  84. package/dist_ts/nosqldb/shim.d.ts +1 -0
  85. package/dist_ts/nosqldb/shim.js +8 -0
  86. package/dist_ts/nosqldb/testsupport.d.ts +3 -0
  87. package/dist_ts/nosqldb/testsupport.js +3 -0
  88. package/dist_ts/objectstorage/classes.bucket.d.ts +227 -0
  89. package/dist_ts/objectstorage/classes.bucket.js +939 -0
  90. package/dist_ts/objectstorage/classes.connection.d.ts +21 -0
  91. package/dist_ts/objectstorage/classes.connection.js +27 -0
  92. package/dist_ts/objectstorage/classes.directory.d.ts +124 -0
  93. package/dist_ts/objectstorage/classes.directory.js +335 -0
  94. package/dist_ts/objectstorage/classes.exactpathpurgeerror.d.ts +5 -0
  95. package/dist_ts/objectstorage/classes.exactpathpurgeerror.js +18 -0
  96. package/dist_ts/objectstorage/classes.exactuploaderror.d.ts +5 -0
  97. package/dist_ts/objectstorage/classes.exactuploaderror.js +19 -0
  98. package/dist_ts/objectstorage/classes.file.d.ts +91 -0
  99. package/dist_ts/objectstorage/classes.file.js +255 -0
  100. package/dist_ts/objectstorage/classes.listcursor.d.ts +41 -0
  101. package/dist_ts/objectstorage/classes.listcursor.js +67 -0
  102. package/dist_ts/objectstorage/classes.metadata.d.ts +57 -0
  103. package/dist_ts/objectstorage/classes.metadata.js +113 -0
  104. package/dist_ts/objectstorage/classes.smartbucket.d.ts +57 -0
  105. package/dist_ts/objectstorage/classes.smartbucket.js +357 -0
  106. package/dist_ts/objectstorage/classes.trash.d.ts +11 -0
  107. package/dist_ts/objectstorage/classes.trash.js +22 -0
  108. package/dist_ts/objectstorage/classes.watcher.d.ts +79 -0
  109. package/dist_ts/objectstorage/classes.watcher.js +249 -0
  110. package/dist_ts/objectstorage/helpers.d.ts +25 -0
  111. package/dist_ts/objectstorage/helpers.js +215 -0
  112. package/dist_ts/objectstorage/index.d.ts +13 -0
  113. package/dist_ts/objectstorage/index.js +13 -0
  114. package/dist_ts/objectstorage/interfaces.d.ts +276 -0
  115. package/dist_ts/objectstorage/interfaces.js +2 -0
  116. package/dist_ts/objectstorage/internal.exactpathpurge.operations.d.ts +11 -0
  117. package/dist_ts/objectstorage/internal.exactpathpurge.operations.js +1069 -0
  118. package/dist_ts/objectstorage/internal.exactupload.capability.d.ts +40 -0
  119. package/dist_ts/objectstorage/internal.exactupload.capability.js +382 -0
  120. package/dist_ts/objectstorage/internal.exactupload.operations.d.ts +11 -0
  121. package/dist_ts/objectstorage/internal.exactupload.operations.js +1104 -0
  122. package/dist_ts/objectstorage/internal.multipart.planner.d.ts +8 -0
  123. package/dist_ts/objectstorage/internal.multipart.planner.js +63 -0
  124. package/dist_ts/objectstorage/internal.multipart.stream.d.ts +17 -0
  125. package/dist_ts/objectstorage/internal.multipart.stream.js +440 -0
  126. package/dist_ts/objectstorage/internal.multipart.types.d.ts +82 -0
  127. package/dist_ts/objectstorage/internal.multipart.types.js +2 -0
  128. package/dist_ts/objectstorage/plugins.d.ts +17 -0
  129. package/dist_ts/objectstorage/plugins.js +23 -0
  130. package/dist_ts/plugins.d.ts +4 -0
  131. package/dist_ts/plugins.js +5 -0
  132. package/dist_ts/sqldb/classes.clickhouseconnection.d.ts +39 -0
  133. package/dist_ts/sqldb/classes.clickhouseconnection.js +225 -0
  134. package/dist_ts/sqldb/classes.sqlconnection.d.ts +50 -0
  135. package/dist_ts/sqldb/classes.sqlconnection.js +376 -0
  136. package/dist_ts/sqldb/classes.sqltable.d.ts +29 -0
  137. package/dist_ts/sqldb/classes.sqltable.js +70 -0
  138. package/dist_ts/sqldb/clickhouse/classes.metricwriter.d.ts +27 -0
  139. package/dist_ts/sqldb/clickhouse/classes.metricwriter.js +89 -0
  140. package/dist_ts/sqldb/clickhouse/index.d.ts +9 -0
  141. package/dist_ts/sqldb/clickhouse/index.js +13 -0
  142. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.d.ts +88 -0
  143. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.js +304 -0
  144. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.d.ts +28 -0
  145. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.js +46 -0
  146. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.d.ts +43 -0
  147. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.js +155 -0
  148. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.resultset.d.ts +13 -0
  149. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.resultset.js +36 -0
  150. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.d.ts +49 -0
  151. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.js +106 -0
  152. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.d.ts +51 -0
  153. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.js +96 -0
  154. package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.d.ts +5 -0
  155. package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.js +6 -0
  156. package/dist_ts/sqldb/clickhouse/smartclickhouse.types.d.ts +53 -0
  157. package/dist_ts/sqldb/clickhouse/smartclickhouse.types.js +41 -0
  158. package/dist_ts/sqldb/index.d.ts +6 -0
  159. package/dist_ts/sqldb/index.js +6 -0
  160. package/dist_ts/sqldb/interfaces.d.ts +53 -0
  161. package/dist_ts/sqldb/interfaces.js +19 -0
  162. package/dist_ts/sqldb/internal.sqlpool.d.ts +51 -0
  163. package/dist_ts/sqldb/internal.sqlpool.js +214 -0
  164. package/dist_ts/sqldb/plugins.clickhouse.d.ts +4 -0
  165. package/dist_ts/sqldb/plugins.clickhouse.js +5 -0
  166. package/dist_ts/sqldb/plugins.mariadb.d.ts +4 -0
  167. package/dist_ts/sqldb/plugins.mariadb.js +5 -0
  168. package/docs/source/smartbucket/license.md +19 -0
  169. package/docs/source/smartclickhouse/license.md +19 -0
  170. package/license.md +21 -0
  171. package/package.json +101 -0
  172. package/readme.md +157 -0
  173. package/ts/00_commitinfo_data.ts +8 -0
  174. package/ts/classes.losslessorgclient.ts +104 -0
  175. package/ts/core/classes.error.ts +31 -0
  176. package/ts/core/classes.operation.ts +60 -0
  177. package/ts/core/interfaces.ts +24 -0
  178. package/ts/index.ts +4 -0
  179. package/ts/nosqldb/classes.atomicdelete.ts +81 -0
  180. package/ts/nosqldb/classes.atomicfindoneandupdate.ts +152 -0
  181. package/ts/nosqldb/classes.atomicupdate.ts +99 -0
  182. package/ts/nosqldb/classes.bootstraprecord.ts +107 -0
  183. package/ts/nosqldb/classes.bsonassertion.ts +25 -0
  184. package/ts/nosqldb/classes.bsonobjectid.ts +45 -0
  185. package/ts/nosqldb/classes.bsonsnapshot.ts +105 -0
  186. package/ts/nosqldb/classes.cached-document.ts +67 -0
  187. package/ts/nosqldb/classes.collection.ts +2399 -0
  188. package/ts/nosqldb/classes.collectionfactory.ts +87 -0
  189. package/ts/nosqldb/classes.collectionlifecycle.ts +24 -0
  190. package/ts/nosqldb/classes.collectionpreparation.ts +164 -0
  191. package/ts/nosqldb/classes.collectiontopology.ts +645 -0
  192. package/ts/nosqldb/classes.connection.ts +31 -0
  193. package/ts/nosqldb/classes.convenience.ts +5 -0
  194. package/ts/nosqldb/classes.cursor.ts +88 -0
  195. package/ts/nosqldb/classes.cursoroperation.ts +38 -0
  196. package/ts/nosqldb/classes.db.ts +637 -0
  197. package/ts/nosqldb/classes.distributedcoordinator.ts +305 -0
  198. package/ts/nosqldb/classes.doc.ts +4928 -0
  199. package/ts/nosqldb/classes.easystore.ts +154 -0
  200. package/ts/nosqldb/classes.exactpersistence.ts +1974 -0
  201. package/ts/nosqldb/classes.lucene.adapter.ts +780 -0
  202. package/ts/nosqldb/classes.mongoadministration.ts +867 -0
  203. package/ts/nosqldb/classes.namespaceinspection.ts +199 -0
  204. package/ts/nosqldb/classes.operationbudget.ts +75 -0
  205. package/ts/nosqldb/classes.ordinarypersistence.ts +84 -0
  206. package/ts/nosqldb/classes.persistence.ts +62 -0
  207. package/ts/nosqldb/classes.session.ts +389 -0
  208. package/ts/nosqldb/classes.storedinspection.ts +151 -0
  209. package/ts/nosqldb/classes.testdatabase.ts +272 -0
  210. package/ts/nosqldb/classes.watcher.ts +96 -0
  211. package/ts/nosqldb/index.ts +51 -0
  212. package/ts/nosqldb/logging.ts +3 -0
  213. package/ts/nosqldb/plugins.ts +37 -0
  214. package/ts/nosqldb/shim.ts +6 -0
  215. package/ts/nosqldb/testsupport.ts +13 -0
  216. package/ts/objectstorage/classes.bucket.ts +1114 -0
  217. package/ts/objectstorage/classes.connection.ts +36 -0
  218. package/ts/objectstorage/classes.directory.ts +425 -0
  219. package/ts/objectstorage/classes.exactpathpurgeerror.ts +24 -0
  220. package/ts/objectstorage/classes.exactuploaderror.ts +25 -0
  221. package/ts/objectstorage/classes.file.ts +303 -0
  222. package/ts/objectstorage/classes.listcursor.ts +89 -0
  223. package/ts/objectstorage/classes.metadata.ts +134 -0
  224. package/ts/objectstorage/classes.smartbucket.ts +445 -0
  225. package/ts/objectstorage/classes.trash.ts +30 -0
  226. package/ts/objectstorage/classes.watcher.ts +289 -0
  227. package/ts/objectstorage/helpers.ts +254 -0
  228. package/ts/objectstorage/index.ts +13 -0
  229. package/ts/objectstorage/interfaces.ts +389 -0
  230. package/ts/objectstorage/internal.exactpathpurge.operations.ts +1936 -0
  231. package/ts/objectstorage/internal.exactupload.capability.ts +580 -0
  232. package/ts/objectstorage/internal.exactupload.operations.ts +1715 -0
  233. package/ts/objectstorage/internal.multipart.planner.ts +90 -0
  234. package/ts/objectstorage/internal.multipart.stream.ts +606 -0
  235. package/ts/objectstorage/internal.multipart.types.ts +106 -0
  236. package/ts/objectstorage/plugins.ts +35 -0
  237. package/ts/plugins.ts +4 -0
  238. package/ts/sqldb/classes.clickhouseconnection.ts +189 -0
  239. package/ts/sqldb/classes.sqlconnection.ts +350 -0
  240. package/ts/sqldb/classes.sqltable.ts +70 -0
  241. package/ts/sqldb/clickhouse/classes.metricwriter.ts +72 -0
  242. package/ts/sqldb/clickhouse/index.ts +16 -0
  243. package/ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.ts +372 -0
  244. package/ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.ts +52 -0
  245. package/ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.ts +120 -0
  246. package/ts/sqldb/clickhouse/smartclickhouse.classes.resultset.ts +44 -0
  247. package/ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.ts +123 -0
  248. package/ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.ts +114 -0
  249. package/ts/sqldb/clickhouse/smartclickhouse.plugins.ts +5 -0
  250. package/ts/sqldb/clickhouse/smartclickhouse.types.ts +134 -0
  251. package/ts/sqldb/index.ts +6 -0
  252. package/ts/sqldb/interfaces.ts +61 -0
  253. package/ts/sqldb/internal.sqlpool.ts +179 -0
  254. package/ts/sqldb/plugins.clickhouse.ts +4 -0
  255. package/ts/sqldb/plugins.mariadb.ts +4 -0
@@ -0,0 +1,1936 @@
1
+ import * as plugins from './plugins.js';
2
+ import { ExactPathPurgeError } from './classes.exactpathpurgeerror.js';
3
+ import type {
4
+ IExactPathPurgeCapabilityProbeOptions,
5
+ IExactPathPurgeCounts,
6
+ IExactPathPurgeEvidence,
7
+ IExactPathPurgeOptions,
8
+ IExactPathPurgeResult,
9
+ TExactPathPurgeCapabilityProbeFailureReason,
10
+ TExactPathPurgeCapabilityProbeResult,
11
+ TExactPathPurgeFailurePhase,
12
+ TExactPathPurgeFailureReason,
13
+ TExactPathPurgeVersioningMode,
14
+ } from './interfaces.js';
15
+ import {
16
+ beginExactPathPurgeCapabilityOperation,
17
+ beginExactPathPurgeProbeOperation,
18
+ mintExactPathPurgeCapability,
19
+ type IExactPathPurgeOperation,
20
+ type IExactUploadOperation,
21
+ } from './internal.exactupload.capability.js';
22
+
23
+ interface IExactPathPurgeBucketRef {
24
+ name: string;
25
+ smartbucketRef: {
26
+ readonly storageClient: plugins.s3.S3Client;
27
+ };
28
+ }
29
+
30
+ interface IVersionEntry {
31
+ type: 'version' | 'delete-marker';
32
+ versionId: string;
33
+ etag?: string;
34
+ size?: number;
35
+ lastModifiedMs: number;
36
+ isLatest: boolean;
37
+ }
38
+
39
+ interface IUnversionedEntry {
40
+ type: 'unversioned';
41
+ etag: string;
42
+ size: number;
43
+ lastModifiedMs: number;
44
+ }
45
+
46
+ type TPurgeEntry = IVersionEntry | IUnversionedEntry;
47
+
48
+ interface IExactPathSnapshot {
49
+ mode: TExactPathPurgeVersioningMode;
50
+ entries: TPurgeEntry[];
51
+ counts: IExactPathPurgeCounts;
52
+ pagesScanned: number;
53
+ neighborEntriesObserved: number;
54
+ identity: string;
55
+ fingerprint: string;
56
+ }
57
+
58
+ interface IDeadlineSignal {
59
+ signal: AbortSignal;
60
+ didTimeout(): boolean;
61
+ dispose(): void;
62
+ }
63
+
64
+ interface IIndependentTaskResult<T> {
65
+ succeeded: boolean;
66
+ timedOut: boolean;
67
+ settlement: Promise<void>;
68
+ value?: T;
69
+ cause?: unknown;
70
+ }
71
+
72
+ interface IVerificationResult {
73
+ remainingSnapshot: IExactPathSnapshot;
74
+ verificationScansCompleted: number;
75
+ pagesScanned: number;
76
+ neighborEntriesObserved: number;
77
+ verifiedAt?: string;
78
+ }
79
+
80
+ class ExactPathInventoryFailure extends Error {
81
+ constructor(
82
+ public readonly reason: Extract<
83
+ TExactPathPurgeFailureReason,
84
+ | 'malformed-provider-response'
85
+ | 'inventory-limit-exceeded'
86
+ | 'concurrent-recreation'
87
+ | 'versioning-mode-changed'
88
+ >,
89
+ messageArg: string,
90
+ optionsArg?: {
91
+ cause?: unknown;
92
+ },
93
+ ) {
94
+ super(messageArg, optionsArg);
95
+ this.name = 'ExactPathInventoryFailure';
96
+ }
97
+ }
98
+
99
+ class ExactPathPurgeProbeSemanticError extends Error {
100
+ constructor(
101
+ public readonly reason: Extract<
102
+ TExactPathPurgeCapabilityProbeFailureReason,
103
+ 'conditional-delete-not-enforced' | 'versioning-mode-changed'
104
+ >,
105
+ messageArg: string,
106
+ optionsArg?: {
107
+ cause?: unknown;
108
+ },
109
+ ) {
110
+ super(messageArg, optionsArg);
111
+ this.name = 'ExactPathPurgeProbeSemanticError';
112
+ }
113
+ }
114
+
115
+ class ExactPathPurgeDeadlineError extends Error {
116
+ constructor(messageArg: string) {
117
+ super(messageArg);
118
+ this.name = 'ExactPathPurgeDeadlineError';
119
+ }
120
+ }
121
+
122
+ const inventoryMaximumEntries = 10_000;
123
+ const inventoryMaximumPages = 100;
124
+ const inventoryPageSize = 1_000;
125
+ const purgeDefaultTimeoutMs = 30_000;
126
+ const purgeMaximumTimeoutMs = 300_000;
127
+ const probeTimeoutMs = 30_000;
128
+ const reconciliationTimeoutMs = 20_000;
129
+ const emptyCounts = (): IExactPathPurgeCounts => ({
130
+ unversionedObjects: 0,
131
+ versions: 0,
132
+ nullVersions: 0,
133
+ deleteMarkers: 0,
134
+ });
135
+
136
+ const createEmptyEvidence = (): IExactPathPurgeEvidence => ({
137
+ initialCounts: emptyCounts(),
138
+ remainingCounts: emptyCounts(),
139
+ inventoryScansCompleted: 0,
140
+ verificationScansCompleted: 0,
141
+ pagesScanned: 0,
142
+ neighborEntriesObserved: 0,
143
+ deleteRequestsAttempted: 0,
144
+ deleteRequestsAcknowledged: 0,
145
+ });
146
+
147
+ const copyCounts = (
148
+ countsArg: IExactPathPurgeCounts,
149
+ ): IExactPathPurgeCounts => ({
150
+ unversionedObjects: countsArg.unversionedObjects,
151
+ versions: countsArg.versions,
152
+ nullVersions: countsArg.nullVersions,
153
+ deleteMarkers: countsArg.deleteMarkers,
154
+ });
155
+
156
+ const copyEvidence = (
157
+ evidenceArg: IExactPathPurgeEvidence,
158
+ ): IExactPathPurgeEvidence => ({
159
+ initialCounts: copyCounts(evidenceArg.initialCounts),
160
+ remainingCounts: copyCounts(evidenceArg.remainingCounts),
161
+ initialFingerprint: evidenceArg.initialFingerprint,
162
+ remainingFingerprint: evidenceArg.remainingFingerprint,
163
+ inventoryScansCompleted: evidenceArg.inventoryScansCompleted,
164
+ verificationScansCompleted: evidenceArg.verificationScansCompleted,
165
+ pagesScanned: evidenceArg.pagesScanned,
166
+ neighborEntriesObserved: evidenceArg.neighborEntriesObserved,
167
+ deleteRequestsAttempted: evidenceArg.deleteRequestsAttempted,
168
+ deleteRequestsAcknowledged: evidenceArg.deleteRequestsAcknowledged,
169
+ verifiedAt: evidenceArg.verifiedAt,
170
+ });
171
+
172
+ const getHttpStatus = (errorArg: unknown): number | undefined => {
173
+ if (typeof errorArg !== 'object' || errorArg === null) {
174
+ return undefined;
175
+ }
176
+ const statusCode = (errorArg as {
177
+ $metadata?: {
178
+ httpStatusCode?: unknown;
179
+ };
180
+ }).$metadata?.httpStatusCode;
181
+ return typeof statusCode === 'number' ? statusCode : undefined;
182
+ };
183
+
184
+ const isHttpStatus = (errorArg: unknown, statusCodeArg: number): boolean => {
185
+ return getHttpStatus(errorArg) === statusCodeArg;
186
+ };
187
+
188
+ const requireNonEmptyString = (
189
+ valueArg: unknown,
190
+ fieldNameArg: string,
191
+ ): string => {
192
+ if (typeof valueArg !== 'string' || valueArg.length === 0) {
193
+ throw new ExactPathInventoryFailure(
194
+ 'malformed-provider-response',
195
+ `${fieldNameArg} must be a non-empty string.`,
196
+ );
197
+ }
198
+ return valueArg;
199
+ };
200
+
201
+ const requireSafeNonNegativeInteger = (
202
+ valueArg: unknown,
203
+ fieldNameArg: string,
204
+ ): number => {
205
+ if (
206
+ typeof valueArg !== 'number' ||
207
+ !Number.isSafeInteger(valueArg) ||
208
+ valueArg < 0
209
+ ) {
210
+ throw new ExactPathInventoryFailure(
211
+ 'malformed-provider-response',
212
+ `${fieldNameArg} must be a non-negative safe integer.`,
213
+ );
214
+ }
215
+ return valueArg;
216
+ };
217
+
218
+ const requireDateMilliseconds = (
219
+ valueArg: unknown,
220
+ fieldNameArg: string,
221
+ ): number => {
222
+ if (!(valueArg instanceof Date) || Number.isNaN(valueArg.getTime())) {
223
+ throw new ExactPathInventoryFailure(
224
+ 'malformed-provider-response',
225
+ `${fieldNameArg} must be a valid Date.`,
226
+ );
227
+ }
228
+ return valueArg.getTime();
229
+ };
230
+
231
+ const requireBoolean = (
232
+ valueArg: unknown,
233
+ fieldNameArg: string,
234
+ ): boolean => {
235
+ if (typeof valueArg !== 'boolean') {
236
+ throw new ExactPathInventoryFailure(
237
+ 'malformed-provider-response',
238
+ `${fieldNameArg} must be a boolean.`,
239
+ );
240
+ }
241
+ return valueArg;
242
+ };
243
+
244
+ const createDeadlineSignal = (
245
+ callerSignalArg: AbortSignal | undefined,
246
+ timeoutMsArg: number,
247
+ timeoutMessageArg: string,
248
+ ): IDeadlineSignal => {
249
+ const abortController = new AbortController();
250
+ let timedOut = false;
251
+ const abortForCaller = (): void => {
252
+ abortController.abort(
253
+ callerSignalArg?.reason ??
254
+ new Error('The exact-path purge operation was aborted.'),
255
+ );
256
+ };
257
+ callerSignalArg?.addEventListener('abort', abortForCaller, {
258
+ once: true,
259
+ });
260
+ if (callerSignalArg?.aborted) {
261
+ abortForCaller();
262
+ }
263
+ const deadlineTimer = setTimeout(() => {
264
+ timedOut = true;
265
+ abortController.abort(
266
+ new ExactPathPurgeDeadlineError(timeoutMessageArg),
267
+ );
268
+ }, timeoutMsArg);
269
+ let disposed = false;
270
+ return {
271
+ signal: abortController.signal,
272
+ didTimeout(): boolean {
273
+ return timedOut;
274
+ },
275
+ dispose(): void {
276
+ if (disposed) {
277
+ return;
278
+ }
279
+ disposed = true;
280
+ clearTimeout(deadlineTimer);
281
+ callerSignalArg?.removeEventListener('abort', abortForCaller);
282
+ },
283
+ };
284
+ };
285
+
286
+ const detachedProviderSendSettlements = new WeakMap<
287
+ AbortSignal,
288
+ Set<Promise<void>>
289
+ >();
290
+
291
+ const trackDetachedProviderSend = (
292
+ signalArg: AbortSignal,
293
+ settlementArg: Promise<void>,
294
+ ): void => {
295
+ let settlements = detachedProviderSendSettlements.get(signalArg);
296
+ if (!settlements) {
297
+ settlements = new Set();
298
+ detachedProviderSendSettlements.set(signalArg, settlements);
299
+ }
300
+ settlements.add(settlementArg);
301
+ void settlementArg.then(() => {
302
+ settlements?.delete(settlementArg);
303
+ if (settlements?.size === 0) {
304
+ detachedProviderSendSettlements.delete(signalArg);
305
+ }
306
+ });
307
+ };
308
+
309
+ const getDetachedProviderSendSettlement = (
310
+ signalArg: AbortSignal,
311
+ ): Promise<void> | undefined => {
312
+ const settlements = detachedProviderSendSettlements.get(signalArg);
313
+ if (!settlements || settlements.size === 0) {
314
+ return undefined;
315
+ }
316
+ return Promise.allSettled([...settlements]).then(() => undefined);
317
+ };
318
+
319
+ const runIndependentTask = async <T>(
320
+ taskArg: (signalArg: AbortSignal) => Promise<T>,
321
+ timeoutMsArg: number,
322
+ operationArg: IExactUploadOperation,
323
+ ): Promise<IIndependentTaskResult<T>> => {
324
+ const abortController = new AbortController();
325
+ const deadlineToken = Symbol('exact-path-purge-independent-deadline');
326
+ let resolveDeadline!: (valueArg: typeof deadlineToken) => void;
327
+ const deadlineOutcome = new Promise<typeof deadlineToken>((resolveArg) => {
328
+ resolveDeadline = resolveArg;
329
+ });
330
+ const deadlineTimer = setTimeout(() => {
331
+ const deadlineError = new ExactPathPurgeDeadlineError(
332
+ `Exact-path purge reconciliation exceeded ${timeoutMsArg}ms.`,
333
+ );
334
+ abortController.abort(deadlineError);
335
+ resolveDeadline(deadlineToken);
336
+ }, timeoutMsArg);
337
+ let deadlineDisposed = false;
338
+ const disposeDeadline = (): void => {
339
+ if (deadlineDisposed) {
340
+ return;
341
+ }
342
+ deadlineDisposed = true;
343
+ clearTimeout(deadlineTimer);
344
+ };
345
+ let releaseOwnedAbort: () => void;
346
+ try {
347
+ releaseOwnedAbort = operationArg.registerOwnedIndependentAbort(
348
+ abortController,
349
+ disposeDeadline,
350
+ );
351
+ } catch (error) {
352
+ abortController.abort(error);
353
+ disposeDeadline();
354
+ throw error;
355
+ }
356
+ const taskOutcome = Promise.resolve()
357
+ .then(() => taskArg(abortController.signal))
358
+ .then<
359
+ { status: 'fulfilled'; value: T },
360
+ { status: 'rejected'; cause: unknown }
361
+ >(
362
+ (valueArg) => ({
363
+ status: 'fulfilled',
364
+ value: valueArg,
365
+ }),
366
+ (causeArg: unknown) => ({
367
+ status: 'rejected',
368
+ cause: causeArg,
369
+ }),
370
+ );
371
+ const settlement = taskOutcome.then(async () => {
372
+ await getDetachedProviderSendSettlement(abortController.signal);
373
+ });
374
+ void settlement.then(() => {
375
+ releaseOwnedAbort();
376
+ });
377
+ try {
378
+ const outcome = await Promise.race([taskOutcome, deadlineOutcome]);
379
+ if (outcome === deadlineToken) {
380
+ return {
381
+ succeeded: false,
382
+ timedOut: true,
383
+ settlement,
384
+ cause:
385
+ abortController.signal.reason ??
386
+ new ExactPathPurgeDeadlineError(
387
+ 'Exact-path purge reconciliation exceeded its deadline.',
388
+ ),
389
+ };
390
+ }
391
+ if (outcome.status === 'rejected') {
392
+ return {
393
+ succeeded: false,
394
+ timedOut: false,
395
+ settlement,
396
+ cause: outcome.cause,
397
+ };
398
+ }
399
+ return {
400
+ succeeded: true,
401
+ timedOut: false,
402
+ settlement,
403
+ value: outcome.value,
404
+ };
405
+ } finally {
406
+ disposeDeadline();
407
+ }
408
+ };
409
+
410
+ const assertSignalActive = (signalArg: AbortSignal): void => {
411
+ if (signalArg.aborted) {
412
+ throw (
413
+ signalArg.reason ??
414
+ new Error('The exact-path purge operation was aborted.')
415
+ );
416
+ }
417
+ };
418
+
419
+ const sendWithOptionalFence = async <T>(
420
+ sendArg: () => Promise<T>,
421
+ assertCurrentArg?: () => void,
422
+ signalArg?: AbortSignal,
423
+ ): Promise<T> => {
424
+ if (signalArg) {
425
+ assertSignalActive(signalArg);
426
+ }
427
+ assertCurrentArg?.();
428
+ const sendOutcome = Promise.resolve()
429
+ .then(() => {
430
+ if (signalArg) {
431
+ assertSignalActive(signalArg);
432
+ }
433
+ assertCurrentArg?.();
434
+ return sendArg();
435
+ })
436
+ .then<
437
+ { status: 'fulfilled'; value: T },
438
+ { status: 'rejected'; cause: unknown }
439
+ >(
440
+ (valueArg) => ({
441
+ status: 'fulfilled',
442
+ value: valueArg,
443
+ }),
444
+ (causeArg: unknown) => ({
445
+ status: 'rejected',
446
+ cause: causeArg,
447
+ }),
448
+ );
449
+ let outcome:
450
+ | Awaited<typeof sendOutcome>
451
+ | {
452
+ status: 'aborted';
453
+ };
454
+ if (signalArg) {
455
+ let abortListener: (() => void) | undefined;
456
+ const abortOutcome = new Promise<{ status: 'aborted' }>((resolveArg) => {
457
+ abortListener = () => {
458
+ resolveArg({
459
+ status: 'aborted',
460
+ });
461
+ };
462
+ signalArg.addEventListener('abort', abortListener, {
463
+ once: true,
464
+ });
465
+ if (signalArg.aborted) {
466
+ abortListener();
467
+ }
468
+ });
469
+ outcome = await Promise.race([sendOutcome, abortOutcome]);
470
+ if (abortListener) {
471
+ signalArg.removeEventListener('abort', abortListener);
472
+ }
473
+ if (outcome.status === 'aborted') {
474
+ trackDetachedProviderSend(
475
+ signalArg,
476
+ sendOutcome.then(() => undefined),
477
+ );
478
+ assertSignalActive(signalArg);
479
+ throw new Error('Unreachable exact-path purge abort state.');
480
+ }
481
+ } else {
482
+ outcome = await sendOutcome;
483
+ }
484
+ if (outcome.status === 'rejected') {
485
+ throw outcome.cause;
486
+ }
487
+ const output = outcome.value;
488
+ if (signalArg) {
489
+ assertSignalActive(signalArg);
490
+ }
491
+ assertCurrentArg?.();
492
+ return output;
493
+ };
494
+
495
+ const finishOperationAfterOwnedSettlements = (
496
+ operationArg: IExactUploadOperation,
497
+ additionalSettlementsArg: Promise<void>[] = [],
498
+ ): void => {
499
+ const detachedProviderSettlement =
500
+ getDetachedProviderSendSettlement(operationArg.signal);
501
+ const ownedSettlements = detachedProviderSettlement
502
+ ? [detachedProviderSettlement, ...additionalSettlementsArg]
503
+ : additionalSettlementsArg;
504
+ if (ownedSettlements.length === 0) {
505
+ operationArg.finish();
506
+ return;
507
+ }
508
+ void Promise.allSettled(ownedSettlements).then(() => {
509
+ operationArg.finish();
510
+ });
511
+ };
512
+
513
+ const readVersioningMode = async (
514
+ clientArg: plugins.s3.S3Client,
515
+ bucketNameArg: string,
516
+ signalArg: AbortSignal,
517
+ assertCurrentArg?: () => void,
518
+ ): Promise<TExactPathPurgeVersioningMode> => {
519
+ const output = await sendWithOptionalFence(
520
+ () =>
521
+ clientArg.send(
522
+ new plugins.s3.GetBucketVersioningCommand({
523
+ Bucket: bucketNameArg,
524
+ }),
525
+ {
526
+ abortSignal: signalArg,
527
+ },
528
+ ),
529
+ assertCurrentArg,
530
+ signalArg,
531
+ );
532
+ if (output.Status === undefined) {
533
+ return 'unversioned';
534
+ }
535
+ if (output.Status === 'Enabled') {
536
+ return 'enabled';
537
+ }
538
+ if (output.Status === 'Suspended') {
539
+ return 'suspended';
540
+ }
541
+ throw new ExactPathInventoryFailure(
542
+ 'malformed-provider-response',
543
+ `Unsupported bucket versioning status '${String(output.Status)}'.`,
544
+ );
545
+ };
546
+
547
+ const assertExpectedMode = (
548
+ actualModeArg: TExactPathPurgeVersioningMode,
549
+ expectedModeArg: TExactPathPurgeVersioningMode,
550
+ ): void => {
551
+ if (actualModeArg !== expectedModeArg) {
552
+ throw new ExactPathInventoryFailure(
553
+ 'versioning-mode-changed',
554
+ `Bucket versioning changed from '${expectedModeArg}' to '${actualModeArg}'.`,
555
+ );
556
+ }
557
+ };
558
+
559
+ const stableEntryToken = (entryArg: TPurgeEntry): string => {
560
+ if (entryArg.type === 'unversioned') {
561
+ return 'unversioned';
562
+ }
563
+ return `${entryArg.type}:${entryArg.versionId}`;
564
+ };
565
+
566
+ const canonicalEntry = (entryArg: TPurgeEntry): string => {
567
+ if (entryArg.type === 'unversioned') {
568
+ return JSON.stringify([
569
+ entryArg.type,
570
+ entryArg.etag,
571
+ entryArg.size,
572
+ entryArg.lastModifiedMs,
573
+ ]);
574
+ }
575
+ return JSON.stringify([
576
+ entryArg.type,
577
+ entryArg.versionId,
578
+ entryArg.etag ?? null,
579
+ entryArg.size ?? null,
580
+ entryArg.lastModifiedMs,
581
+ entryArg.isLatest,
582
+ ]);
583
+ };
584
+
585
+ const stableEntryIdentity = (entryArg: TPurgeEntry): string => {
586
+ if (entryArg.type === 'unversioned') {
587
+ return canonicalEntry(entryArg);
588
+ }
589
+ return JSON.stringify([
590
+ entryArg.type,
591
+ entryArg.versionId,
592
+ entryArg.etag ?? null,
593
+ entryArg.size ?? null,
594
+ entryArg.lastModifiedMs,
595
+ ]);
596
+ };
597
+
598
+ const createSnapshot = (
599
+ modeArg: TExactPathPurgeVersioningMode,
600
+ entriesArg: TPurgeEntry[],
601
+ pagesScannedArg: number,
602
+ neighborEntriesObservedArg: number,
603
+ ): IExactPathSnapshot => {
604
+ const sortedEntries = [...entriesArg].sort((leftArg, rightArg) => {
605
+ return stableEntryToken(leftArg).localeCompare(stableEntryToken(rightArg));
606
+ });
607
+ const canonicalEntries = sortedEntries.map(canonicalEntry);
608
+ const identity = JSON.stringify([modeArg, canonicalEntries]);
609
+ const counts = emptyCounts();
610
+ for (const entry of sortedEntries) {
611
+ if (entry.type === 'unversioned') {
612
+ counts.unversionedObjects++;
613
+ } else if (entry.type === 'delete-marker') {
614
+ counts.deleteMarkers++;
615
+ } else {
616
+ counts.versions++;
617
+ if (entry.versionId === 'null') {
618
+ counts.nullVersions++;
619
+ }
620
+ }
621
+ }
622
+ return {
623
+ mode: modeArg,
624
+ entries: sortedEntries,
625
+ counts,
626
+ pagesScanned: pagesScannedArg,
627
+ neighborEntriesObserved: neighborEntriesObservedArg,
628
+ identity,
629
+ fingerprint: plugins.crypto
630
+ .createHash('sha256')
631
+ .update(identity)
632
+ .digest('hex'),
633
+ };
634
+ };
635
+
636
+ const validateListTruncation = (
637
+ isTruncatedArg: unknown,
638
+ fieldNameArg: string,
639
+ ): boolean => {
640
+ if (
641
+ isTruncatedArg !== undefined &&
642
+ typeof isTruncatedArg !== 'boolean'
643
+ ) {
644
+ throw new ExactPathInventoryFailure(
645
+ 'malformed-provider-response',
646
+ `${fieldNameArg} must be a boolean when present.`,
647
+ );
648
+ }
649
+ return isTruncatedArg === true;
650
+ };
651
+
652
+ const inventoryVersionedExactPath = async (
653
+ clientArg: plugins.s3.S3Client,
654
+ bucketNameArg: string,
655
+ pathArg: string,
656
+ modeArg: Extract<TExactPathPurgeVersioningMode, 'enabled' | 'suspended'>,
657
+ signalArg: AbortSignal,
658
+ assertCurrentArg?: () => void,
659
+ ): Promise<IExactPathSnapshot> => {
660
+ const entries: IVersionEntry[] = [];
661
+ const exactEntryTokens = new Set<string>();
662
+ let keyMarker: string | undefined;
663
+ let versionIdMarker: string | undefined;
664
+ let neighborEntriesObserved = 0;
665
+ let pagesScanned = 0;
666
+
667
+ for (
668
+ let pageNumber = 1;
669
+ pageNumber <= inventoryMaximumPages;
670
+ pageNumber++
671
+ ) {
672
+ pagesScanned++;
673
+ const output = await sendWithOptionalFence(
674
+ () =>
675
+ clientArg.send(
676
+ new plugins.s3.ListObjectVersionsCommand({
677
+ Bucket: bucketNameArg,
678
+ Prefix: pathArg,
679
+ KeyMarker: keyMarker,
680
+ VersionIdMarker: versionIdMarker,
681
+ MaxKeys: inventoryPageSize,
682
+ }),
683
+ {
684
+ abortSignal: signalArg,
685
+ },
686
+ ),
687
+ assertCurrentArg,
688
+ signalArg,
689
+ );
690
+ if (
691
+ output.Versions !== undefined &&
692
+ !Array.isArray(output.Versions)
693
+ ) {
694
+ throw new ExactPathInventoryFailure(
695
+ 'malformed-provider-response',
696
+ 'ListObjectVersions Versions must be an array.',
697
+ );
698
+ }
699
+ if (
700
+ output.DeleteMarkers !== undefined &&
701
+ !Array.isArray(output.DeleteMarkers)
702
+ ) {
703
+ throw new ExactPathInventoryFailure(
704
+ 'malformed-provider-response',
705
+ 'ListObjectVersions DeleteMarkers must be an array.',
706
+ );
707
+ }
708
+
709
+ for (const version of output.Versions ?? []) {
710
+ const key = requireNonEmptyString(version.Key, 'ObjectVersion Key');
711
+ if (!key.startsWith(pathArg)) {
712
+ throw new ExactPathInventoryFailure(
713
+ 'malformed-provider-response',
714
+ `ListObjectVersions returned key '${key}' outside the requested prefix.`,
715
+ );
716
+ }
717
+ const versionId = requireNonEmptyString(
718
+ version.VersionId,
719
+ `ObjectVersion '${key}' VersionId`,
720
+ );
721
+ if (key !== pathArg) {
722
+ neighborEntriesObserved++;
723
+ continue;
724
+ }
725
+ if (exactEntryTokens.has(versionId)) {
726
+ throw new ExactPathInventoryFailure(
727
+ 'malformed-provider-response',
728
+ `Object version '${versionId}' was returned more than once.`,
729
+ );
730
+ }
731
+ exactEntryTokens.add(versionId);
732
+ entries.push({
733
+ type: 'version',
734
+ versionId,
735
+ etag: requireNonEmptyString(
736
+ version.ETag,
737
+ `ObjectVersion '${key}' ETag`,
738
+ ),
739
+ size: requireSafeNonNegativeInteger(
740
+ version.Size,
741
+ `ObjectVersion '${key}' Size`,
742
+ ),
743
+ lastModifiedMs: requireDateMilliseconds(
744
+ version.LastModified,
745
+ `ObjectVersion '${key}' LastModified`,
746
+ ),
747
+ isLatest: requireBoolean(
748
+ version.IsLatest,
749
+ `ObjectVersion '${key}' IsLatest`,
750
+ ),
751
+ });
752
+ }
753
+
754
+ for (const marker of output.DeleteMarkers ?? []) {
755
+ const key = requireNonEmptyString(marker.Key, 'DeleteMarker Key');
756
+ if (!key.startsWith(pathArg)) {
757
+ throw new ExactPathInventoryFailure(
758
+ 'malformed-provider-response',
759
+ `ListObjectVersions returned marker key '${key}' outside the requested prefix.`,
760
+ );
761
+ }
762
+ const versionId = requireNonEmptyString(
763
+ marker.VersionId,
764
+ `DeleteMarker '${key}' VersionId`,
765
+ );
766
+ if (key !== pathArg) {
767
+ neighborEntriesObserved++;
768
+ continue;
769
+ }
770
+ if (exactEntryTokens.has(versionId)) {
771
+ throw new ExactPathInventoryFailure(
772
+ 'malformed-provider-response',
773
+ `Delete marker '${versionId}' was returned more than once.`,
774
+ );
775
+ }
776
+ exactEntryTokens.add(versionId);
777
+ entries.push({
778
+ type: 'delete-marker',
779
+ versionId,
780
+ lastModifiedMs: requireDateMilliseconds(
781
+ marker.LastModified,
782
+ `DeleteMarker '${key}' LastModified`,
783
+ ),
784
+ isLatest: requireBoolean(
785
+ marker.IsLatest,
786
+ `DeleteMarker '${key}' IsLatest`,
787
+ ),
788
+ });
789
+ }
790
+
791
+ if (entries.length > inventoryMaximumEntries) {
792
+ throw new ExactPathInventoryFailure(
793
+ 'inventory-limit-exceeded',
794
+ `Exact path contains more than ${inventoryMaximumEntries} versions and markers.`,
795
+ );
796
+ }
797
+
798
+ const isTruncated = validateListTruncation(
799
+ output.IsTruncated,
800
+ 'ListObjectVersions IsTruncated',
801
+ );
802
+ if (!isTruncated) {
803
+ const latestCount = entries.filter((entryArg) => entryArg.isLatest).length;
804
+ if (entries.length > 0 && latestCount !== 1) {
805
+ throw new ExactPathInventoryFailure(
806
+ 'malformed-provider-response',
807
+ `Exact version inventory must contain exactly one latest entry, received ${latestCount}.`,
808
+ );
809
+ }
810
+ const nullVersionCount = entries.filter(
811
+ (entryArg) =>
812
+ entryArg.type === 'version' && entryArg.versionId === 'null',
813
+ ).length;
814
+ if (nullVersionCount > 1) {
815
+ throw new ExactPathInventoryFailure(
816
+ 'malformed-provider-response',
817
+ 'Exact version inventory contains more than one null data version.',
818
+ );
819
+ }
820
+ return createSnapshot(
821
+ modeArg,
822
+ entries,
823
+ pagesScanned,
824
+ neighborEntriesObserved,
825
+ );
826
+ }
827
+ const nextKeyMarker = requireNonEmptyString(
828
+ output.NextKeyMarker,
829
+ 'ListObjectVersions NextKeyMarker',
830
+ );
831
+ const nextVersionIdMarker =
832
+ output.NextVersionIdMarker === undefined
833
+ ? undefined
834
+ : requireNonEmptyString(
835
+ output.NextVersionIdMarker,
836
+ 'ListObjectVersions NextVersionIdMarker',
837
+ );
838
+ if (
839
+ nextKeyMarker === keyMarker &&
840
+ nextVersionIdMarker === versionIdMarker
841
+ ) {
842
+ throw new ExactPathInventoryFailure(
843
+ 'malformed-provider-response',
844
+ 'ListObjectVersions pagination did not advance.',
845
+ );
846
+ }
847
+ keyMarker = nextKeyMarker;
848
+ versionIdMarker = nextVersionIdMarker;
849
+ }
850
+
851
+ throw new ExactPathInventoryFailure(
852
+ 'inventory-limit-exceeded',
853
+ `Exact version inventory exceeded ${inventoryMaximumPages} pages.`,
854
+ );
855
+ };
856
+
857
+ const inventoryUnversionedExactPath = async (
858
+ clientArg: plugins.s3.S3Client,
859
+ bucketNameArg: string,
860
+ pathArg: string,
861
+ signalArg: AbortSignal,
862
+ assertCurrentArg?: () => void,
863
+ ): Promise<IExactPathSnapshot> => {
864
+ let exactEntry: IUnversionedEntry | undefined;
865
+ let continuationToken: string | undefined;
866
+ let neighborEntriesObserved = 0;
867
+ let pagesScanned = 0;
868
+
869
+ for (
870
+ let pageNumber = 1;
871
+ pageNumber <= inventoryMaximumPages;
872
+ pageNumber++
873
+ ) {
874
+ pagesScanned++;
875
+ const output = await sendWithOptionalFence(
876
+ () =>
877
+ clientArg.send(
878
+ new plugins.s3.ListObjectsV2Command({
879
+ Bucket: bucketNameArg,
880
+ Prefix: pathArg,
881
+ ContinuationToken: continuationToken,
882
+ MaxKeys: inventoryPageSize,
883
+ }),
884
+ {
885
+ abortSignal: signalArg,
886
+ },
887
+ ),
888
+ assertCurrentArg,
889
+ signalArg,
890
+ );
891
+ if (output.Contents !== undefined && !Array.isArray(output.Contents)) {
892
+ throw new ExactPathInventoryFailure(
893
+ 'malformed-provider-response',
894
+ 'ListObjectsV2 Contents must be an array.',
895
+ );
896
+ }
897
+ for (const object of output.Contents ?? []) {
898
+ const key = requireNonEmptyString(object.Key, 'Object Key');
899
+ if (!key.startsWith(pathArg)) {
900
+ throw new ExactPathInventoryFailure(
901
+ 'malformed-provider-response',
902
+ `ListObjectsV2 returned key '${key}' outside the requested prefix.`,
903
+ );
904
+ }
905
+ if (key !== pathArg) {
906
+ neighborEntriesObserved++;
907
+ continue;
908
+ }
909
+ if (exactEntry) {
910
+ throw new ExactPathInventoryFailure(
911
+ 'malformed-provider-response',
912
+ 'ListObjectsV2 returned the exact unversioned key more than once.',
913
+ );
914
+ }
915
+ exactEntry = {
916
+ type: 'unversioned',
917
+ etag: requireNonEmptyString(object.ETag, `Object '${key}' ETag`),
918
+ size: requireSafeNonNegativeInteger(
919
+ object.Size,
920
+ `Object '${key}' Size`,
921
+ ),
922
+ lastModifiedMs: requireDateMilliseconds(
923
+ object.LastModified,
924
+ `Object '${key}' LastModified`,
925
+ ),
926
+ };
927
+ }
928
+ const isTruncated = validateListTruncation(
929
+ output.IsTruncated,
930
+ 'ListObjectsV2 IsTruncated',
931
+ );
932
+ if (!isTruncated) {
933
+ return createSnapshot(
934
+ 'unversioned',
935
+ exactEntry ? [exactEntry] : [],
936
+ pagesScanned,
937
+ neighborEntriesObserved,
938
+ );
939
+ }
940
+ const nextContinuationToken = requireNonEmptyString(
941
+ output.NextContinuationToken,
942
+ 'ListObjectsV2 NextContinuationToken',
943
+ );
944
+ if (nextContinuationToken === continuationToken) {
945
+ throw new ExactPathInventoryFailure(
946
+ 'malformed-provider-response',
947
+ 'ListObjectsV2 pagination did not advance.',
948
+ );
949
+ }
950
+ continuationToken = nextContinuationToken;
951
+ }
952
+
953
+ throw new ExactPathInventoryFailure(
954
+ 'inventory-limit-exceeded',
955
+ `Exact object inventory exceeded ${inventoryMaximumPages} pages.`,
956
+ );
957
+ };
958
+
959
+ const inventoryExactPath = (
960
+ clientArg: plugins.s3.S3Client,
961
+ bucketNameArg: string,
962
+ pathArg: string,
963
+ modeArg: TExactPathPurgeVersioningMode,
964
+ signalArg: AbortSignal,
965
+ assertCurrentArg?: () => void,
966
+ ): Promise<IExactPathSnapshot> => {
967
+ if (modeArg === 'unversioned') {
968
+ return inventoryUnversionedExactPath(
969
+ clientArg,
970
+ bucketNameArg,
971
+ pathArg,
972
+ signalArg,
973
+ assertCurrentArg,
974
+ );
975
+ }
976
+ return inventoryVersionedExactPath(
977
+ clientArg,
978
+ bucketNameArg,
979
+ pathArg,
980
+ modeArg,
981
+ signalArg,
982
+ assertCurrentArg,
983
+ );
984
+ };
985
+
986
+ const assertSnapshotUnchanged = (
987
+ initialSnapshotArg: IExactPathSnapshot,
988
+ currentSnapshotArg: IExactPathSnapshot,
989
+ ): void => {
990
+ if (
991
+ initialSnapshotArg.mode !== currentSnapshotArg.mode ||
992
+ initialSnapshotArg.identity !== currentSnapshotArg.identity
993
+ ) {
994
+ throw new ExactPathInventoryFailure(
995
+ 'concurrent-recreation',
996
+ 'The exact path changed between its two pre-delete inventories.',
997
+ );
998
+ }
999
+ };
1000
+
1001
+ const deleteEntry = async (
1002
+ clientArg: plugins.s3.S3Client,
1003
+ bucketNameArg: string,
1004
+ pathArg: string,
1005
+ entryArg: TPurgeEntry,
1006
+ signalArg: AbortSignal,
1007
+ assertCurrentArg?: () => void,
1008
+ ): Promise<void> => {
1009
+ await sendWithOptionalFence(
1010
+ () =>
1011
+ clientArg.send(
1012
+ new plugins.s3.DeleteObjectCommand({
1013
+ Bucket: bucketNameArg,
1014
+ Key: pathArg,
1015
+ VersionId:
1016
+ entryArg.type === 'unversioned'
1017
+ ? undefined
1018
+ : entryArg.versionId,
1019
+ IfMatch:
1020
+ entryArg.type === 'delete-marker'
1021
+ ? undefined
1022
+ : entryArg.etag,
1023
+ }),
1024
+ {
1025
+ abortSignal: signalArg,
1026
+ },
1027
+ ),
1028
+ assertCurrentArg,
1029
+ signalArg,
1030
+ );
1031
+ };
1032
+
1033
+ const snapshotIsEmpty = (snapshotArg: IExactPathSnapshot): boolean => {
1034
+ return snapshotArg.entries.length === 0;
1035
+ };
1036
+
1037
+ const classifyRemainingSnapshot = (
1038
+ initialSnapshotArg: IExactPathSnapshot,
1039
+ remainingSnapshotArg: IExactPathSnapshot,
1040
+ ): 'empty' | 'captured-remains' | 'concurrent-recreation' => {
1041
+ if (snapshotIsEmpty(remainingSnapshotArg)) {
1042
+ return 'empty';
1043
+ }
1044
+ const initialEntries = new Map(
1045
+ initialSnapshotArg.entries.map((entryArg) => [
1046
+ stableEntryToken(entryArg),
1047
+ stableEntryIdentity(entryArg),
1048
+ ]),
1049
+ );
1050
+ for (const remainingEntry of remainingSnapshotArg.entries) {
1051
+ const initialIdentity = initialEntries.get(
1052
+ stableEntryToken(remainingEntry),
1053
+ );
1054
+ if (
1055
+ initialIdentity === undefined ||
1056
+ initialIdentity !== stableEntryIdentity(remainingEntry)
1057
+ ) {
1058
+ return 'concurrent-recreation';
1059
+ }
1060
+ }
1061
+ return 'captured-remains';
1062
+ };
1063
+
1064
+ const verifyExactPathEmpty = async (
1065
+ clientArg: plugins.s3.S3Client,
1066
+ bucketNameArg: string,
1067
+ pathArg: string,
1068
+ expectedModeArg: TExactPathPurgeVersioningMode,
1069
+ signalArg: AbortSignal,
1070
+ assertCurrentArg?: () => void,
1071
+ ): Promise<IVerificationResult> => {
1072
+ let pagesScanned = 0;
1073
+ let neighborEntriesObserved = 0;
1074
+ let verificationScansCompleted = 0;
1075
+ let remainingSnapshot = createSnapshot(
1076
+ expectedModeArg,
1077
+ [],
1078
+ 0,
1079
+ 0,
1080
+ );
1081
+ for (let scanNumber = 1; scanNumber <= 2; scanNumber++) {
1082
+ const actualMode = await readVersioningMode(
1083
+ clientArg,
1084
+ bucketNameArg,
1085
+ signalArg,
1086
+ assertCurrentArg,
1087
+ );
1088
+ assertExpectedMode(actualMode, expectedModeArg);
1089
+ remainingSnapshot = await inventoryExactPath(
1090
+ clientArg,
1091
+ bucketNameArg,
1092
+ pathArg,
1093
+ expectedModeArg,
1094
+ signalArg,
1095
+ assertCurrentArg,
1096
+ );
1097
+ verificationScansCompleted++;
1098
+ pagesScanned += remainingSnapshot.pagesScanned;
1099
+ neighborEntriesObserved +=
1100
+ remainingSnapshot.neighborEntriesObserved;
1101
+ if (!snapshotIsEmpty(remainingSnapshot)) {
1102
+ return {
1103
+ remainingSnapshot,
1104
+ verificationScansCompleted,
1105
+ pagesScanned,
1106
+ neighborEntriesObserved,
1107
+ };
1108
+ }
1109
+ }
1110
+ return {
1111
+ remainingSnapshot,
1112
+ verificationScansCompleted,
1113
+ pagesScanned,
1114
+ neighborEntriesObserved,
1115
+ verifiedAt: new Date().toISOString(),
1116
+ };
1117
+ };
1118
+
1119
+ const createPurgeError = (
1120
+ phaseArg: TExactPathPurgeFailurePhase,
1121
+ reasonArg: TExactPathPurgeFailureReason,
1122
+ pathArg: string,
1123
+ evidenceArg: IExactPathPurgeEvidence,
1124
+ causeArg: unknown,
1125
+ versioningModeArg?: TExactPathPurgeVersioningMode,
1126
+ ): ExactPathPurgeError => {
1127
+ return new ExactPathPurgeError(
1128
+ {
1129
+ phase: phaseArg,
1130
+ reason: reasonArg,
1131
+ destinationPath: pathArg,
1132
+ versioningMode: versioningModeArg,
1133
+ evidence: copyEvidence(evidenceArg),
1134
+ },
1135
+ causeArg,
1136
+ );
1137
+ };
1138
+
1139
+ const failureReasonForInventoryError = (
1140
+ errorArg: unknown,
1141
+ ): TExactPathPurgeFailureReason => {
1142
+ if (errorArg instanceof ExactPathInventoryFailure) {
1143
+ return errorArg.reason;
1144
+ }
1145
+ return 'unavailable';
1146
+ };
1147
+
1148
+ const cleanupProbePath = async (
1149
+ clientArg: plugins.s3.S3Client,
1150
+ bucketNameArg: string,
1151
+ pathArg: string,
1152
+ expectedModeArg: TExactPathPurgeVersioningMode,
1153
+ signalArg: AbortSignal,
1154
+ assertCurrentArg: () => void,
1155
+ ): Promise<void> => {
1156
+ const actualMode = await readVersioningMode(
1157
+ clientArg,
1158
+ bucketNameArg,
1159
+ signalArg,
1160
+ assertCurrentArg,
1161
+ );
1162
+ assertExpectedMode(actualMode, expectedModeArg);
1163
+ const snapshot = await inventoryExactPath(
1164
+ clientArg,
1165
+ bucketNameArg,
1166
+ pathArg,
1167
+ expectedModeArg,
1168
+ signalArg,
1169
+ assertCurrentArg,
1170
+ );
1171
+ for (const entry of snapshot.entries) {
1172
+ await sendWithOptionalFence(
1173
+ () =>
1174
+ clientArg.send(
1175
+ new plugins.s3.DeleteObjectCommand({
1176
+ Bucket: bucketNameArg,
1177
+ Key: pathArg,
1178
+ VersionId:
1179
+ entry.type === 'unversioned' ? undefined : entry.versionId,
1180
+ }),
1181
+ {
1182
+ abortSignal: signalArg,
1183
+ },
1184
+ ),
1185
+ assertCurrentArg,
1186
+ signalArg,
1187
+ );
1188
+ }
1189
+ const verification = await verifyExactPathEmpty(
1190
+ clientArg,
1191
+ bucketNameArg,
1192
+ pathArg,
1193
+ expectedModeArg,
1194
+ signalArg,
1195
+ assertCurrentArg,
1196
+ );
1197
+ if (!snapshotIsEmpty(verification.remainingSnapshot)) {
1198
+ throw new Error('The exact-path purge probe key remains after cleanup.');
1199
+ }
1200
+ };
1201
+
1202
+ const headProbeEntry = async (
1203
+ operationArg: IExactUploadOperation,
1204
+ pathArg: string,
1205
+ entryArg: TPurgeEntry,
1206
+ ): Promise<string> => {
1207
+ const output = await sendWithOptionalFence(
1208
+ () =>
1209
+ operationArg.client.send(
1210
+ new plugins.s3.HeadObjectCommand({
1211
+ Bucket: operationArg.bucketName,
1212
+ Key: pathArg,
1213
+ VersionId:
1214
+ entryArg.type === 'unversioned'
1215
+ ? undefined
1216
+ : entryArg.versionId,
1217
+ }),
1218
+ {
1219
+ abortSignal: operationArg.signal,
1220
+ },
1221
+ ),
1222
+ () => operationArg.assertCurrent(),
1223
+ operationArg.signal,
1224
+ );
1225
+ return requireNonEmptyString(output.ETag, 'HeadObject ETag');
1226
+ };
1227
+
1228
+ const assertProbeEntryAbsentByHead = async (
1229
+ operationArg: IExactUploadOperation,
1230
+ pathArg: string,
1231
+ entryArg: TPurgeEntry,
1232
+ ): Promise<void> => {
1233
+ try {
1234
+ await sendWithOptionalFence(
1235
+ () =>
1236
+ operationArg.client.send(
1237
+ new plugins.s3.HeadObjectCommand({
1238
+ Bucket: operationArg.bucketName,
1239
+ Key: pathArg,
1240
+ VersionId:
1241
+ entryArg.type === 'unversioned'
1242
+ ? undefined
1243
+ : entryArg.versionId,
1244
+ }),
1245
+ {
1246
+ abortSignal: operationArg.signal,
1247
+ },
1248
+ ),
1249
+ () => operationArg.assertCurrent(),
1250
+ operationArg.signal,
1251
+ );
1252
+ } catch (error) {
1253
+ operationArg.assertCurrent();
1254
+ if (isHttpStatus(error, 404)) {
1255
+ return;
1256
+ }
1257
+ throw error;
1258
+ }
1259
+ throw new ExactPathPurgeProbeSemanticError(
1260
+ 'conditional-delete-not-enforced',
1261
+ 'The correctly deleted probe object is still readable by exact identity.',
1262
+ );
1263
+ };
1264
+
1265
+ const getSingleProbeDataEntry = (
1266
+ snapshotArg: IExactPathSnapshot,
1267
+ ): IVersionEntry | IUnversionedEntry => {
1268
+ if (
1269
+ snapshotArg.entries.length !== 1 ||
1270
+ snapshotArg.entries[0].type === 'delete-marker'
1271
+ ) {
1272
+ throw new ExactPathPurgeProbeSemanticError(
1273
+ 'conditional-delete-not-enforced',
1274
+ 'The purge probe object did not produce exactly one data entry.',
1275
+ );
1276
+ }
1277
+ const entry = snapshotArg.entries[0];
1278
+ if (
1279
+ snapshotArg.mode === 'enabled' &&
1280
+ (
1281
+ entry.type === 'unversioned' ||
1282
+ entry.versionId === 'null'
1283
+ )
1284
+ ) {
1285
+ throw new ExactPathPurgeProbeSemanticError(
1286
+ 'conditional-delete-not-enforced',
1287
+ 'Enabled versioning did not return a concrete object VersionId.',
1288
+ );
1289
+ }
1290
+ if (
1291
+ snapshotArg.mode === 'suspended' &&
1292
+ (
1293
+ entry.type === 'unversioned' ||
1294
+ entry.versionId !== 'null'
1295
+ )
1296
+ ) {
1297
+ throw new ExactPathPurgeProbeSemanticError(
1298
+ 'conditional-delete-not-enforced',
1299
+ 'Suspended versioning did not return the current null data version.',
1300
+ );
1301
+ }
1302
+ if (
1303
+ snapshotArg.mode === 'unversioned' &&
1304
+ entry.type !== 'unversioned'
1305
+ ) {
1306
+ throw new ExactPathPurgeProbeSemanticError(
1307
+ 'conditional-delete-not-enforced',
1308
+ 'Unversioned storage returned a versioned probe entry.',
1309
+ );
1310
+ }
1311
+ return entry;
1312
+ };
1313
+
1314
+ export const probeExactPathPurgeCapability = async (
1315
+ bucketRefArg: IExactPathPurgeBucketRef,
1316
+ optionsArg: IExactPathPurgeCapabilityProbeOptions,
1317
+ ): Promise<TExactPathPurgeCapabilityProbeResult> => {
1318
+ const timeoutMs = optionsArg.timeoutMs ?? probeTimeoutMs;
1319
+ if (
1320
+ !Number.isSafeInteger(timeoutMs) ||
1321
+ timeoutMs <= 0 ||
1322
+ timeoutMs > probeTimeoutMs
1323
+ ) {
1324
+ throw new TypeError(
1325
+ `Exact-path purge probe timeoutMs must be a positive safe integer no greater than ${probeTimeoutMs}.`,
1326
+ );
1327
+ }
1328
+ const deadlineSignal = createDeadlineSignal(
1329
+ optionsArg.signal,
1330
+ timeoutMs,
1331
+ `Exact-path purge capability probe exceeded ${timeoutMs}ms.`,
1332
+ );
1333
+ let operation: IExactUploadOperation;
1334
+ try {
1335
+ operation = beginExactPathPurgeProbeOperation(
1336
+ bucketRefArg,
1337
+ deadlineSignal.signal,
1338
+ );
1339
+ } catch (error) {
1340
+ deadlineSignal.dispose();
1341
+ return {
1342
+ supported: false,
1343
+ reason: deadlineSignal.didTimeout() ? 'timeout' : 'unavailable',
1344
+ cause: error,
1345
+ };
1346
+ }
1347
+
1348
+ const probePath = `.smartbucket-probes/exact-path-purge-v1/${plugins.smartunique.uuid4()}`;
1349
+ let versioningMode: TExactPathPurgeVersioningMode | undefined;
1350
+ let failureReason: TExactPathPurgeCapabilityProbeFailureReason | undefined;
1351
+ let failureCause: unknown;
1352
+
1353
+ try {
1354
+ versioningMode = await readVersioningMode(
1355
+ operation.client,
1356
+ operation.bucketName,
1357
+ operation.signal,
1358
+ () => operation.assertCurrent(),
1359
+ );
1360
+ const putProbeObject = async (): Promise<IExactPathSnapshot> => {
1361
+ await sendWithOptionalFence(
1362
+ () =>
1363
+ operation.client.send(
1364
+ new plugins.s3.PutObjectCommand({
1365
+ Bucket: operation.bucketName,
1366
+ Key: probePath,
1367
+ Body: new Uint8Array([1]),
1368
+ ContentLength: 1,
1369
+ Metadata: {
1370
+ 'smartbucket-probe': 'exact-path-purge-v1',
1371
+ },
1372
+ }),
1373
+ {
1374
+ abortSignal: operation.signal,
1375
+ },
1376
+ ),
1377
+ () => operation.assertCurrent(),
1378
+ operation.signal,
1379
+ );
1380
+ const actualMode = await readVersioningMode(
1381
+ operation.client,
1382
+ operation.bucketName,
1383
+ operation.signal,
1384
+ () => operation.assertCurrent(),
1385
+ );
1386
+ if (actualMode !== versioningMode) {
1387
+ throw new ExactPathPurgeProbeSemanticError(
1388
+ 'versioning-mode-changed',
1389
+ 'Bucket versioning changed during the purge probe.',
1390
+ );
1391
+ }
1392
+ return inventoryExactPath(
1393
+ operation.client,
1394
+ operation.bucketName,
1395
+ probePath,
1396
+ versioningMode!,
1397
+ operation.signal,
1398
+ () => operation.assertCurrent(),
1399
+ );
1400
+ };
1401
+
1402
+ const initialSnapshot = await putProbeObject();
1403
+ const initialEntry = getSingleProbeDataEntry(initialSnapshot);
1404
+ const impossibleEtag = `"smartbucket-never-match-${plugins.smartunique.uuid4()}"`;
1405
+ try {
1406
+ await sendWithOptionalFence(
1407
+ () =>
1408
+ operation.client.send(
1409
+ new plugins.s3.DeleteObjectCommand({
1410
+ Bucket: operation.bucketName,
1411
+ Key: probePath,
1412
+ VersionId:
1413
+ initialEntry.type === 'unversioned'
1414
+ ? undefined
1415
+ : initialEntry.versionId,
1416
+ IfMatch: impossibleEtag,
1417
+ }),
1418
+ {
1419
+ abortSignal: operation.signal,
1420
+ },
1421
+ ),
1422
+ () => operation.assertCurrent(),
1423
+ operation.signal,
1424
+ );
1425
+ throw new ExactPathPurgeProbeSemanticError(
1426
+ 'conditional-delete-not-enforced',
1427
+ 'The provider accepted a mismatched conditional delete.',
1428
+ );
1429
+ } catch (error) {
1430
+ if (error instanceof ExactPathPurgeProbeSemanticError) {
1431
+ throw error;
1432
+ }
1433
+ operation.assertCurrent();
1434
+ if (!isHttpStatus(error, 412)) {
1435
+ throw error;
1436
+ }
1437
+ }
1438
+ const retainedEtag = await headProbeEntry(
1439
+ operation,
1440
+ probePath,
1441
+ initialEntry,
1442
+ );
1443
+ if (retainedEtag !== initialEntry.etag) {
1444
+ throw new ExactPathPurgeProbeSemanticError(
1445
+ 'conditional-delete-not-enforced',
1446
+ 'The mismatched conditional delete changed the probe object.',
1447
+ );
1448
+ }
1449
+
1450
+ let correctDeleteCause: unknown;
1451
+ try {
1452
+ await deleteEntry(
1453
+ operation.client,
1454
+ operation.bucketName,
1455
+ probePath,
1456
+ initialEntry,
1457
+ operation.signal,
1458
+ () => operation.assertCurrent(),
1459
+ );
1460
+ } catch (error) {
1461
+ correctDeleteCause = error;
1462
+ }
1463
+ await assertProbeEntryAbsentByHead(
1464
+ operation,
1465
+ probePath,
1466
+ initialEntry,
1467
+ );
1468
+ const afterCorrectDelete = await inventoryExactPath(
1469
+ operation.client,
1470
+ operation.bucketName,
1471
+ probePath,
1472
+ versioningMode,
1473
+ operation.signal,
1474
+ () => operation.assertCurrent(),
1475
+ );
1476
+ if (!snapshotIsEmpty(afterCorrectDelete)) {
1477
+ throw new ExactPathPurgeProbeSemanticError(
1478
+ 'conditional-delete-not-enforced',
1479
+ 'The correctly matched conditional delete did not remove the probe object.',
1480
+ {
1481
+ cause: correctDeleteCause,
1482
+ },
1483
+ );
1484
+ }
1485
+
1486
+ if (versioningMode !== 'unversioned') {
1487
+ const markerSourceSnapshot = await putProbeObject();
1488
+ getSingleProbeDataEntry(markerSourceSnapshot);
1489
+ await sendWithOptionalFence(
1490
+ () =>
1491
+ operation.client.send(
1492
+ new plugins.s3.DeleteObjectCommand({
1493
+ Bucket: operation.bucketName,
1494
+ Key: probePath,
1495
+ }),
1496
+ {
1497
+ abortSignal: operation.signal,
1498
+ },
1499
+ ),
1500
+ () => operation.assertCurrent(),
1501
+ operation.signal,
1502
+ );
1503
+ const markerSnapshot = await inventoryExactPath(
1504
+ operation.client,
1505
+ operation.bucketName,
1506
+ probePath,
1507
+ versioningMode,
1508
+ operation.signal,
1509
+ () => operation.assertCurrent(),
1510
+ );
1511
+ const marker = markerSnapshot.entries.find(
1512
+ (entryArg): entryArg is IVersionEntry =>
1513
+ entryArg.type === 'delete-marker' && entryArg.isLatest,
1514
+ );
1515
+ if (!marker) {
1516
+ throw new ExactPathPurgeProbeSemanticError(
1517
+ 'conditional-delete-not-enforced',
1518
+ 'The provider did not expose the expected delete marker.',
1519
+ );
1520
+ }
1521
+ await deleteEntry(
1522
+ operation.client,
1523
+ operation.bucketName,
1524
+ probePath,
1525
+ marker,
1526
+ operation.signal,
1527
+ () => operation.assertCurrent(),
1528
+ );
1529
+ const dataAfterMarker = await inventoryExactPath(
1530
+ operation.client,
1531
+ operation.bucketName,
1532
+ probePath,
1533
+ versioningMode,
1534
+ operation.signal,
1535
+ () => operation.assertCurrent(),
1536
+ );
1537
+ for (const entry of dataAfterMarker.entries) {
1538
+ await deleteEntry(
1539
+ operation.client,
1540
+ operation.bucketName,
1541
+ probePath,
1542
+ entry,
1543
+ operation.signal,
1544
+ () => operation.assertCurrent(),
1545
+ );
1546
+ }
1547
+ const afterMarkerCleanup = await inventoryExactPath(
1548
+ operation.client,
1549
+ operation.bucketName,
1550
+ probePath,
1551
+ versioningMode,
1552
+ operation.signal,
1553
+ () => operation.assertCurrent(),
1554
+ );
1555
+ if (!snapshotIsEmpty(afterMarkerCleanup)) {
1556
+ throw new ExactPathPurgeProbeSemanticError(
1557
+ 'conditional-delete-not-enforced',
1558
+ 'Exact delete-marker removal did not empty the probe path.',
1559
+ );
1560
+ }
1561
+ }
1562
+ } catch (error) {
1563
+ if (error instanceof ExactPathPurgeProbeSemanticError) {
1564
+ failureReason = error.reason;
1565
+ } else if (deadlineSignal.didTimeout()) {
1566
+ failureReason = 'timeout';
1567
+ failureCause = error;
1568
+ } else if (operation.signal.aborted) {
1569
+ failureReason = 'aborted';
1570
+ failureCause = operation.signal.reason;
1571
+ } else {
1572
+ failureReason = 'unavailable';
1573
+ failureCause = error;
1574
+ }
1575
+ }
1576
+
1577
+ deadlineSignal.dispose();
1578
+ const primaryProviderSettlement =
1579
+ getDetachedProviderSendSettlement(operation.signal);
1580
+ if (primaryProviderSettlement) {
1581
+ const deferredCleanup = primaryProviderSettlement
1582
+ .then(async () => {
1583
+ try {
1584
+ operation.assertLifecycleCurrent();
1585
+ if (versioningMode === undefined) {
1586
+ return;
1587
+ }
1588
+ const cleanupResult = await runIndependentTask(
1589
+ (signalArg) =>
1590
+ cleanupProbePath(
1591
+ operation.client,
1592
+ operation.bucketName,
1593
+ probePath,
1594
+ versioningMode!,
1595
+ signalArg,
1596
+ () => operation.assertLifecycleCurrent(),
1597
+ ),
1598
+ reconciliationTimeoutMs,
1599
+ operation,
1600
+ );
1601
+ await cleanupResult.settlement;
1602
+ } catch {
1603
+ // Forced lifecycle detach makes deferred cleanup indeterminate.
1604
+ }
1605
+ })
1606
+ .then(
1607
+ () => undefined,
1608
+ () => undefined,
1609
+ );
1610
+ finishOperationAfterOwnedSettlements(operation, [deferredCleanup]);
1611
+ return {
1612
+ supported: false,
1613
+ reason:
1614
+ failureReason ??
1615
+ (deadlineSignal.didTimeout()
1616
+ ? 'timeout'
1617
+ : operation.signal.aborted
1618
+ ? 'aborted'
1619
+ : 'unavailable'),
1620
+ cause: failureCause ?? operation.signal.reason,
1621
+ };
1622
+ }
1623
+ const cleanupResult =
1624
+ versioningMode === undefined
1625
+ ? {
1626
+ succeeded: true,
1627
+ timedOut: false,
1628
+ settlement: Promise.resolve(),
1629
+ }
1630
+ : await runIndependentTask(
1631
+ (signalArg) =>
1632
+ cleanupProbePath(
1633
+ operation.client,
1634
+ operation.bucketName,
1635
+ probePath,
1636
+ versioningMode!,
1637
+ signalArg,
1638
+ () => operation.assertLifecycleCurrent(),
1639
+ ),
1640
+ reconciliationTimeoutMs,
1641
+ operation,
1642
+ );
1643
+ try {
1644
+ if (!cleanupResult.succeeded) {
1645
+ return {
1646
+ supported: false,
1647
+ reason: 'cleanup-failed',
1648
+ cause: cleanupResult.cause,
1649
+ };
1650
+ }
1651
+ if (failureReason) {
1652
+ return {
1653
+ supported: false,
1654
+ reason: failureReason,
1655
+ cause: failureCause,
1656
+ };
1657
+ }
1658
+ return {
1659
+ supported: true,
1660
+ capability: mintExactPathPurgeCapability(
1661
+ bucketRefArg,
1662
+ operation,
1663
+ versioningMode!,
1664
+ ),
1665
+ versioningMode: versioningMode!,
1666
+ };
1667
+ } catch (error) {
1668
+ return {
1669
+ supported: false,
1670
+ reason: deadlineSignal.didTimeout()
1671
+ ? 'timeout'
1672
+ : operation.signal.aborted
1673
+ ? 'aborted'
1674
+ : 'unavailable',
1675
+ cause: error,
1676
+ };
1677
+ } finally {
1678
+ if (cleanupResult.timedOut) {
1679
+ finishOperationAfterOwnedSettlements(operation, [
1680
+ cleanupResult.settlement,
1681
+ ]);
1682
+ } else {
1683
+ finishOperationAfterOwnedSettlements(operation);
1684
+ }
1685
+ }
1686
+ };
1687
+
1688
+ export const purgeExactPath = async (
1689
+ bucketRefArg: IExactPathPurgeBucketRef,
1690
+ optionsArg: IExactPathPurgeOptions,
1691
+ ): Promise<IExactPathPurgeResult> => {
1692
+ if (typeof optionsArg.path !== 'string' || optionsArg.path.length === 0) {
1693
+ throw new TypeError('path must be a non-empty string.');
1694
+ }
1695
+ const timeoutMs = optionsArg.timeoutMs ?? purgeDefaultTimeoutMs;
1696
+ if (
1697
+ !Number.isSafeInteger(timeoutMs) ||
1698
+ timeoutMs <= 0 ||
1699
+ timeoutMs > purgeMaximumTimeoutMs
1700
+ ) {
1701
+ throw new TypeError(
1702
+ `Exact-path purge timeoutMs must be a positive safe integer no greater than ${purgeMaximumTimeoutMs}.`,
1703
+ );
1704
+ }
1705
+ const evidence = createEmptyEvidence();
1706
+ const deadlineSignal = createDeadlineSignal(
1707
+ optionsArg.signal,
1708
+ timeoutMs,
1709
+ `Exact-path purge exceeded ${timeoutMs}ms.`,
1710
+ );
1711
+ let operation: IExactPathPurgeOperation;
1712
+ try {
1713
+ operation = beginExactPathPurgeCapabilityOperation(
1714
+ bucketRefArg,
1715
+ optionsArg.capability,
1716
+ deadlineSignal.signal,
1717
+ );
1718
+ } catch (error) {
1719
+ deadlineSignal.dispose();
1720
+ throw createPurgeError(
1721
+ 'capability',
1722
+ deadlineSignal.didTimeout() ? 'timeout' : 'unavailable',
1723
+ optionsArg.path,
1724
+ evidence,
1725
+ error,
1726
+ );
1727
+ }
1728
+
1729
+ let initialSnapshot: IExactPathSnapshot;
1730
+ try {
1731
+ const initialMode = await readVersioningMode(
1732
+ operation.client,
1733
+ operation.bucketName,
1734
+ operation.signal,
1735
+ () => operation.assertCurrent(),
1736
+ );
1737
+ assertExpectedMode(initialMode, operation.versioningMode);
1738
+ initialSnapshot = await inventoryExactPath(
1739
+ operation.client,
1740
+ operation.bucketName,
1741
+ optionsArg.path,
1742
+ operation.versioningMode,
1743
+ operation.signal,
1744
+ () => operation.assertCurrent(),
1745
+ );
1746
+ evidence.inventoryScansCompleted++;
1747
+ evidence.pagesScanned += initialSnapshot.pagesScanned;
1748
+ evidence.neighborEntriesObserved +=
1749
+ initialSnapshot.neighborEntriesObserved;
1750
+ evidence.initialCounts = copyCounts(initialSnapshot.counts);
1751
+ evidence.initialFingerprint = initialSnapshot.fingerprint;
1752
+
1753
+ const consistencyMode = await readVersioningMode(
1754
+ operation.client,
1755
+ operation.bucketName,
1756
+ operation.signal,
1757
+ () => operation.assertCurrent(),
1758
+ );
1759
+ assertExpectedMode(consistencyMode, operation.versioningMode);
1760
+ const consistencySnapshot = await inventoryExactPath(
1761
+ operation.client,
1762
+ operation.bucketName,
1763
+ optionsArg.path,
1764
+ operation.versioningMode,
1765
+ operation.signal,
1766
+ () => operation.assertCurrent(),
1767
+ );
1768
+ evidence.inventoryScansCompleted++;
1769
+ evidence.pagesScanned += consistencySnapshot.pagesScanned;
1770
+ evidence.neighborEntriesObserved +=
1771
+ consistencySnapshot.neighborEntriesObserved;
1772
+ assertSnapshotUnchanged(initialSnapshot, consistencySnapshot);
1773
+ if (snapshotIsEmpty(initialSnapshot)) {
1774
+ evidence.remainingCounts = copyCounts(consistencySnapshot.counts);
1775
+ evidence.remainingFingerprint = consistencySnapshot.fingerprint;
1776
+ evidence.verificationScansCompleted = 2;
1777
+ evidence.verifiedAt = new Date().toISOString();
1778
+ deadlineSignal.dispose();
1779
+ operation.finish();
1780
+ return {
1781
+ destinationPath: optionsArg.path,
1782
+ status: 'already-absent',
1783
+ versioningMode: operation.versioningMode,
1784
+ purgedCounts: copyCounts(initialSnapshot.counts),
1785
+ evidence: copyEvidence(evidence),
1786
+ };
1787
+ }
1788
+ } catch (error) {
1789
+ const phase: TExactPathPurgeFailurePhase =
1790
+ error instanceof ExactPathInventoryFailure &&
1791
+ error.message.includes('changed between')
1792
+ ? 'consistency'
1793
+ : 'inventory';
1794
+ const reason = deadlineSignal.didTimeout()
1795
+ ? 'timeout'
1796
+ : operation.signal.aborted
1797
+ ? 'aborted'
1798
+ : failureReasonForInventoryError(error);
1799
+ deadlineSignal.dispose();
1800
+ finishOperationAfterOwnedSettlements(operation);
1801
+ throw createPurgeError(
1802
+ phase,
1803
+ reason,
1804
+ optionsArg.path,
1805
+ evidence,
1806
+ error,
1807
+ operation.versioningMode,
1808
+ );
1809
+ }
1810
+
1811
+ let deleteCause: unknown;
1812
+ for (const entry of initialSnapshot.entries) {
1813
+ evidence.deleteRequestsAttempted++;
1814
+ try {
1815
+ await deleteEntry(
1816
+ operation.client,
1817
+ operation.bucketName,
1818
+ optionsArg.path,
1819
+ entry,
1820
+ operation.signal,
1821
+ () => operation.assertCurrent(),
1822
+ );
1823
+ evidence.deleteRequestsAcknowledged++;
1824
+ } catch (error) {
1825
+ deleteCause = error;
1826
+ break;
1827
+ }
1828
+ }
1829
+ deadlineSignal.dispose();
1830
+
1831
+ const primaryProviderSettlement =
1832
+ getDetachedProviderSendSettlement(operation.signal);
1833
+ if (primaryProviderSettlement) {
1834
+ finishOperationAfterOwnedSettlements(operation);
1835
+ throw createPurgeError(
1836
+ 'delete',
1837
+ deadlineSignal.didTimeout()
1838
+ ? 'timeout'
1839
+ : operation.signal.aborted
1840
+ ? 'aborted'
1841
+ : 'delete-unverified',
1842
+ optionsArg.path,
1843
+ evidence,
1844
+ deleteCause ?? operation.signal.reason,
1845
+ operation.versioningMode,
1846
+ );
1847
+ }
1848
+
1849
+ const verificationResult = await runIndependentTask(
1850
+ (signalArg) =>
1851
+ verifyExactPathEmpty(
1852
+ operation.client,
1853
+ operation.bucketName,
1854
+ optionsArg.path,
1855
+ operation.versioningMode,
1856
+ signalArg,
1857
+ () => operation.assertLifecycleCurrent(),
1858
+ ),
1859
+ reconciliationTimeoutMs,
1860
+ operation,
1861
+ );
1862
+
1863
+ try {
1864
+ if (!verificationResult.succeeded || !verificationResult.value) {
1865
+ throw createPurgeError(
1866
+ 'verification',
1867
+ 'verification-ambiguous',
1868
+ optionsArg.path,
1869
+ evidence,
1870
+ verificationResult.cause,
1871
+ operation.versioningMode,
1872
+ );
1873
+ }
1874
+ const verification = verificationResult.value;
1875
+ evidence.verificationScansCompleted =
1876
+ verification.verificationScansCompleted;
1877
+ evidence.pagesScanned += verification.pagesScanned;
1878
+ evidence.neighborEntriesObserved +=
1879
+ verification.neighborEntriesObserved;
1880
+ evidence.remainingCounts = copyCounts(
1881
+ verification.remainingSnapshot.counts,
1882
+ );
1883
+ evidence.remainingFingerprint =
1884
+ verification.remainingSnapshot.fingerprint;
1885
+ evidence.verifiedAt = verification.verifiedAt;
1886
+
1887
+ const remainingClassification = classifyRemainingSnapshot(
1888
+ initialSnapshot,
1889
+ verification.remainingSnapshot,
1890
+ );
1891
+ if (
1892
+ remainingClassification === 'concurrent-recreation' ||
1893
+ isHttpStatus(deleteCause, 409) ||
1894
+ isHttpStatus(deleteCause, 412)
1895
+ ) {
1896
+ throw createPurgeError(
1897
+ 'verification',
1898
+ 'concurrent-recreation',
1899
+ optionsArg.path,
1900
+ evidence,
1901
+ deleteCause,
1902
+ operation.versioningMode,
1903
+ );
1904
+ }
1905
+ if (remainingClassification === 'captured-remains') {
1906
+ const reason: TExactPathPurgeFailureReason = deadlineSignal.didTimeout()
1907
+ ? 'timeout'
1908
+ : operation.signal.aborted
1909
+ ? 'aborted'
1910
+ : 'delete-unverified';
1911
+ throw createPurgeError(
1912
+ 'delete',
1913
+ reason,
1914
+ optionsArg.path,
1915
+ evidence,
1916
+ deleteCause,
1917
+ operation.versioningMode,
1918
+ );
1919
+ }
1920
+ return {
1921
+ destinationPath: optionsArg.path,
1922
+ status: 'purged',
1923
+ versioningMode: operation.versioningMode,
1924
+ purgedCounts: copyCounts(initialSnapshot.counts),
1925
+ evidence: copyEvidence(evidence),
1926
+ };
1927
+ } finally {
1928
+ if (verificationResult.timedOut) {
1929
+ finishOperationAfterOwnedSettlements(operation, [
1930
+ verificationResult.settlement,
1931
+ ]);
1932
+ } else {
1933
+ finishOperationAfterOwnedSettlements(operation);
1934
+ }
1935
+ }
1936
+ };