@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,1715 @@
1
+ import * as plugins from './plugins.js';
2
+ import { ExactUploadError } from './classes.exactuploaderror.js';
3
+ import type {
4
+ IExactUploadCapabilityProbeOptions,
5
+ IExactUploadResult,
6
+ IFastPutStreamExactOptions,
7
+ TExactUploadCapabilityProbeFailureReason,
8
+ TExactUploadCapabilityProbeResult,
9
+ TExactUploadFailurePhase,
10
+ TExactUploadMultipartCleanup,
11
+ } from './interfaces.js';
12
+ import {
13
+ beginExactUploadCapabilityOperation,
14
+ beginExactUploadProbeOperation,
15
+ mintExactUploadCapability,
16
+ type IExactUploadOperation,
17
+ } from './internal.exactupload.capability.js';
18
+ import { planMultipartUpload } from './internal.multipart.planner.js';
19
+ import {
20
+ consumeExactMultipartStream,
21
+ MultipartStreamConsumptionError,
22
+ } from './internal.multipart.stream.js';
23
+
24
+ interface IExactUploadBucketRef {
25
+ name: string;
26
+ smartbucketRef: {
27
+ readonly storageClient: plugins.s3.S3Client;
28
+ };
29
+ }
30
+
31
+ interface IProbeObject {
32
+ key: string;
33
+ versionId?: string;
34
+ }
35
+
36
+ interface IProbeMultipartUpload {
37
+ key: string;
38
+ uploadId: string;
39
+ }
40
+
41
+ interface IIndependentCleanupResult {
42
+ errors: unknown[];
43
+ timedOut: boolean;
44
+ settlement: Promise<void>;
45
+ }
46
+
47
+ interface IMultipartAbortResult {
48
+ cleanup: TExactUploadMultipartCleanup;
49
+ timedOut: boolean;
50
+ settlement: Promise<void>;
51
+ }
52
+
53
+ class ExactUploadProbeSemanticError extends Error {
54
+ constructor(
55
+ public readonly reason: Extract<
56
+ TExactUploadCapabilityProbeFailureReason,
57
+ 'conditional-put-not-enforced' | 'conditional-complete-not-enforced'
58
+ >,
59
+ ) {
60
+ super(reason);
61
+ this.name = 'ExactUploadProbeSemanticError';
62
+ }
63
+ }
64
+
65
+ const cleanupTimeoutMs = 30_000;
66
+ const cleanupVerificationMaximumPages = 100;
67
+ const multipartAbortMaximumAttempts = 64;
68
+ const multipartAbortRetryDelayMs = 100;
69
+ const probeMetadata = {
70
+ 'smartbucket-probe': 'exact-upload-v1',
71
+ };
72
+
73
+ const snapshotNativeObjectProperties = (
74
+ valueArg: IFastPutStreamExactOptions['nativeObjectProperties'],
75
+ ): IFastPutStreamExactOptions['nativeObjectProperties'] => {
76
+ if (valueArg === undefined) {
77
+ return undefined;
78
+ }
79
+ if (
80
+ valueArg === null
81
+ || typeof valueArg !== 'object'
82
+ || Array.isArray(valueArg)
83
+ ) {
84
+ throw new TypeError('nativeObjectProperties must be an object.');
85
+ }
86
+ const keys = Object.keys(valueArg);
87
+ if (keys.some((keyArg) => keyArg !== 'contentType')) {
88
+ throw new TypeError(
89
+ 'nativeObjectProperties contains an unsupported property.',
90
+ );
91
+ }
92
+ if (
93
+ valueArg.contentType !== undefined
94
+ && (
95
+ typeof valueArg.contentType !== 'string'
96
+ || valueArg.contentType.length === 0
97
+ || /[\0\r\n]/.test(valueArg.contentType)
98
+ )
99
+ ) {
100
+ throw new TypeError(
101
+ 'nativeObjectProperties.contentType must be a non-empty HTTP header value.',
102
+ );
103
+ }
104
+ return Object.freeze({
105
+ ...(valueArg.contentType === undefined
106
+ ? {}
107
+ : { contentType: valueArg.contentType }),
108
+ });
109
+ };
110
+
111
+ const detachedOperationSendSettlements = new WeakMap<
112
+ AbortSignal,
113
+ Set<Promise<void>>
114
+ >();
115
+
116
+ const trackDetachedOperationSend = (
117
+ signalArg: AbortSignal,
118
+ settlementArg: Promise<void>,
119
+ ): void => {
120
+ let settlements = detachedOperationSendSettlements.get(signalArg);
121
+ if (!settlements) {
122
+ settlements = new Set();
123
+ detachedOperationSendSettlements.set(signalArg, settlements);
124
+ }
125
+ settlements.add(settlementArg);
126
+ void settlementArg.then(() => {
127
+ settlements?.delete(settlementArg);
128
+ if (settlements?.size === 0) {
129
+ detachedOperationSendSettlements.delete(signalArg);
130
+ }
131
+ });
132
+ };
133
+
134
+ const getDetachedOperationSendSettlement = (
135
+ signalArg: AbortSignal,
136
+ ): Promise<void> | undefined => {
137
+ const settlements = detachedOperationSendSettlements.get(signalArg);
138
+ if (!settlements || settlements.size === 0) {
139
+ return undefined;
140
+ }
141
+ return Promise.allSettled([...settlements]).then(() => undefined);
142
+ };
143
+
144
+ const finishOperationAfterOwnedSettlements = (
145
+ operationArg: IExactUploadOperation,
146
+ additionalSettlementsArg: Promise<void>[] = [],
147
+ ): void => {
148
+ const detachedSendSettlement = getDetachedOperationSendSettlement(
149
+ operationArg.signal,
150
+ );
151
+ const ownedSettlements = detachedSendSettlement
152
+ ? [detachedSendSettlement, ...additionalSettlementsArg]
153
+ : additionalSettlementsArg;
154
+ if (ownedSettlements.length === 0) {
155
+ operationArg.finish();
156
+ return;
157
+ }
158
+ void Promise.allSettled(ownedSettlements).then(() => {
159
+ operationArg.finish();
160
+ });
161
+ };
162
+
163
+ const sendOperationCommand = async <T>(
164
+ operationArg: IExactUploadOperation,
165
+ sendArg: (signalArg: AbortSignal) => Promise<T>,
166
+ ): Promise<T> => {
167
+ operationArg.assertCurrent();
168
+ const sendOutcome = Promise.resolve()
169
+ .then(() => {
170
+ operationArg.assertCurrent();
171
+ return sendArg(operationArg.signal);
172
+ })
173
+ .then<
174
+ { status: 'fulfilled'; value: T },
175
+ { status: 'rejected'; cause: unknown }
176
+ >(
177
+ (valueArg) => ({
178
+ status: 'fulfilled',
179
+ value: valueArg,
180
+ }),
181
+ (causeArg: unknown) => ({
182
+ status: 'rejected',
183
+ cause: causeArg,
184
+ }),
185
+ );
186
+ let abortListener: (() => void) | undefined;
187
+ const abortOutcome = new Promise<{ status: 'aborted' }>((resolveArg) => {
188
+ abortListener = () => {
189
+ resolveArg({
190
+ status: 'aborted',
191
+ });
192
+ };
193
+ operationArg.signal.addEventListener('abort', abortListener, {
194
+ once: true,
195
+ });
196
+ if (operationArg.signal.aborted) {
197
+ abortListener();
198
+ }
199
+ });
200
+ const outcome = await Promise.race([sendOutcome, abortOutcome]);
201
+ if (abortListener) {
202
+ operationArg.signal.removeEventListener('abort', abortListener);
203
+ }
204
+ if (outcome.status === 'aborted') {
205
+ trackDetachedOperationSend(
206
+ operationArg.signal,
207
+ sendOutcome.then(() => undefined),
208
+ );
209
+ operationArg.assertCurrent();
210
+ throw new Error('Unreachable exact-upload abort state.');
211
+ }
212
+ if (outcome.status === 'rejected') {
213
+ throw outcome.cause;
214
+ }
215
+ operationArg.assertCurrent();
216
+ return outcome.value;
217
+ };
218
+
219
+ const isHttpStatus = (errorArg: unknown, statusCodeArg: number): boolean => {
220
+ if (typeof errorArg !== 'object' || errorArg === null) {
221
+ return false;
222
+ }
223
+ const metadata = (errorArg as {
224
+ $metadata?: {
225
+ httpStatusCode?: unknown;
226
+ };
227
+ }).$metadata;
228
+ return metadata?.httpStatusCode === statusCodeArg;
229
+ };
230
+
231
+ const isDefinitiveConditionalWriteRejection = (
232
+ errorArg: unknown,
233
+ ): boolean => {
234
+ return isHttpStatus(errorArg, 409) || isHttpStatus(errorArg, 412);
235
+ };
236
+
237
+ const requireNonEmptyString = (
238
+ valueArg: unknown,
239
+ fieldNameArg: string,
240
+ ): string => {
241
+ if (typeof valueArg !== 'string' || valueArg.length === 0) {
242
+ throw new Error(`${fieldNameArg} was not returned by the object provider.`);
243
+ }
244
+ return valueArg;
245
+ };
246
+
247
+ const getOptionalNonEmptyString = (valueArg: unknown): string | undefined => {
248
+ return typeof valueArg === 'string' && valueArg.length > 0
249
+ ? valueArg
250
+ : undefined;
251
+ };
252
+
253
+ const toWebReadableStream = (
254
+ readableStreamArg: plugins.stream.Readable | ReadableStream<Uint8Array>,
255
+ ): ReadableStream<Uint8Array> => {
256
+ if (
257
+ typeof readableStreamArg === 'object' &&
258
+ readableStreamArg !== null &&
259
+ typeof (readableStreamArg as ReadableStream<Uint8Array>).getReader ===
260
+ 'function'
261
+ ) {
262
+ return readableStreamArg as ReadableStream<Uint8Array>;
263
+ }
264
+ if (readableStreamArg instanceof plugins.stream.Readable) {
265
+ return plugins.stream.Readable.toWeb(
266
+ readableStreamArg,
267
+ ) as ReadableStream<Uint8Array>;
268
+ }
269
+ throw new TypeError(
270
+ 'readableStream must be a Node.js Readable or a web ReadableStream.',
271
+ );
272
+ };
273
+
274
+ const runIndependentCleanup = async (
275
+ cleanupTasksArg: Array<(signalArg: AbortSignal) => Promise<unknown>>,
276
+ operationArg: IExactUploadOperation,
277
+ ): Promise<IIndependentCleanupResult> => {
278
+ const cleanupErrors: unknown[] = [];
279
+ if (cleanupTasksArg.length === 0) {
280
+ return {
281
+ errors: cleanupErrors,
282
+ timedOut: false,
283
+ settlement: Promise.resolve(),
284
+ };
285
+ }
286
+ const cleanupAbortController = new AbortController();
287
+ const cleanupDeadlineToken = Symbol('exact-upload-cleanup-deadline');
288
+ let resolveCleanupDeadline!: (
289
+ valueArg: typeof cleanupDeadlineToken,
290
+ ) => void;
291
+ const cleanupDeadlineOutcome = new Promise<typeof cleanupDeadlineToken>(
292
+ (resolveArg) => {
293
+ resolveCleanupDeadline = resolveArg;
294
+ },
295
+ );
296
+ const cleanupDeadline = setTimeout(() => {
297
+ const deadlineError = new Error(
298
+ `Exact-upload cleanup exceeded its ${cleanupTimeoutMs}ms deadline.`,
299
+ );
300
+ cleanupAbortController.abort(deadlineError);
301
+ resolveCleanupDeadline(cleanupDeadlineToken);
302
+ }, cleanupTimeoutMs);
303
+ let cleanupDeadlineDisposed = false;
304
+ const disposeCleanupDeadline = (): void => {
305
+ if (cleanupDeadlineDisposed) {
306
+ return;
307
+ }
308
+ cleanupDeadlineDisposed = true;
309
+ clearTimeout(cleanupDeadline);
310
+ };
311
+ let releaseOwnedAbort: () => void;
312
+ try {
313
+ releaseOwnedAbort = operationArg.registerOwnedIndependentAbort(
314
+ cleanupAbortController,
315
+ disposeCleanupDeadline,
316
+ );
317
+ } catch (error) {
318
+ cleanupAbortController.abort(error);
319
+ disposeCleanupDeadline();
320
+ throw error;
321
+ }
322
+ const cleanupTaskSequence = Promise.resolve().then(async () => {
323
+ for (const cleanupTask of cleanupTasksArg) {
324
+ if (cleanupAbortController.signal.aborted) {
325
+ break;
326
+ }
327
+ try {
328
+ await cleanupTask(cleanupAbortController.signal);
329
+ } catch (error) {
330
+ cleanupErrors.push(error);
331
+ }
332
+ }
333
+ });
334
+ const normalizedTaskSequence = cleanupTaskSequence.then<
335
+ { status: 'settled' },
336
+ { status: 'settled' }
337
+ >(
338
+ () => ({
339
+ status: 'settled',
340
+ }),
341
+ (causeArg: unknown) => {
342
+ cleanupErrors.push(causeArg);
343
+ return {
344
+ status: 'settled',
345
+ };
346
+ },
347
+ );
348
+ const settlement = normalizedTaskSequence.then(() => undefined);
349
+ void settlement.then(() => {
350
+ releaseOwnedAbort();
351
+ });
352
+ try {
353
+ const outcome = await Promise.race([
354
+ normalizedTaskSequence,
355
+ cleanupDeadlineOutcome,
356
+ ]);
357
+ if (outcome === cleanupDeadlineToken) {
358
+ cleanupErrors.push(
359
+ cleanupAbortController.signal.reason ??
360
+ new Error('Exact-upload cleanup exceeded its deadline.'),
361
+ );
362
+ return {
363
+ errors: cleanupErrors,
364
+ timedOut: true,
365
+ settlement,
366
+ };
367
+ }
368
+ return {
369
+ errors: cleanupErrors,
370
+ timedOut: false,
371
+ settlement,
372
+ };
373
+ } finally {
374
+ disposeCleanupDeadline();
375
+ }
376
+ };
377
+
378
+ const assertCleanupActive = (
379
+ signalArg: AbortSignal,
380
+ assertCurrentArg: () => void,
381
+ ): void => {
382
+ if (signalArg.aborted) {
383
+ throw (
384
+ signalArg.reason ??
385
+ new Error('Exact-upload cleanup was aborted.')
386
+ );
387
+ }
388
+ assertCurrentArg();
389
+ };
390
+
391
+ const sendCleanupRequest = async <T>(
392
+ signalArg: AbortSignal,
393
+ assertCurrentArg: () => void,
394
+ sendArg: () => Promise<T>,
395
+ ): Promise<T> => {
396
+ assertCleanupActive(signalArg, assertCurrentArg);
397
+ const output = await sendArg();
398
+ assertCleanupActive(signalArg, assertCurrentArg);
399
+ return output;
400
+ };
401
+
402
+ const listProbeMultipartUploads = async (
403
+ clientArg: plugins.s3.S3Client,
404
+ bucketNameArg: string,
405
+ probeRootArg: string,
406
+ signalArg: AbortSignal,
407
+ assertCurrentArg: () => void,
408
+ ): Promise<IProbeMultipartUpload[]> => {
409
+ const uploads: IProbeMultipartUpload[] = [];
410
+ let keyMarker: string | undefined;
411
+ let uploadIdMarker: string | undefined;
412
+ for (
413
+ let pageNumber = 1;
414
+ pageNumber <= cleanupVerificationMaximumPages;
415
+ pageNumber++
416
+ ) {
417
+ const output = await sendCleanupRequest(
418
+ signalArg,
419
+ assertCurrentArg,
420
+ () =>
421
+ clientArg.send(
422
+ new plugins.s3.ListMultipartUploadsCommand({
423
+ Bucket: bucketNameArg,
424
+ Prefix: probeRootArg,
425
+ KeyMarker: keyMarker,
426
+ UploadIdMarker: uploadIdMarker,
427
+ }),
428
+ {
429
+ abortSignal: signalArg,
430
+ },
431
+ ),
432
+ );
433
+ for (const upload of output.Uploads ?? []) {
434
+ if (
435
+ typeof upload.Key !== 'string' ||
436
+ !upload.Key.startsWith(probeRootArg)
437
+ ) {
438
+ throw new Error(
439
+ 'Multipart cleanup reconciliation returned an invalid prefixed entry.',
440
+ );
441
+ }
442
+ if (
443
+ typeof upload.UploadId !== 'string' ||
444
+ upload.UploadId.length === 0
445
+ ) {
446
+ throw new Error(
447
+ `Probe multipart upload '${upload.Key}' has no UploadId.`,
448
+ );
449
+ }
450
+ uploads.push({
451
+ key: upload.Key,
452
+ uploadId: upload.UploadId,
453
+ });
454
+ }
455
+ if (!output.IsTruncated) {
456
+ return uploads;
457
+ }
458
+ if (
459
+ !output.NextKeyMarker ||
460
+ (
461
+ output.NextKeyMarker === keyMarker &&
462
+ output.NextUploadIdMarker === uploadIdMarker
463
+ )
464
+ ) {
465
+ throw new Error(
466
+ 'Multipart cleanup reconciliation returned a non-advancing page.',
467
+ );
468
+ }
469
+ keyMarker = output.NextKeyMarker;
470
+ uploadIdMarker = output.NextUploadIdMarker;
471
+ }
472
+ throw new Error('Multipart cleanup reconciliation exceeded its page bound.');
473
+ };
474
+
475
+ const listProbeObjectVersions = async (
476
+ clientArg: plugins.s3.S3Client,
477
+ bucketNameArg: string,
478
+ probeRootArg: string,
479
+ signalArg: AbortSignal,
480
+ assertCurrentArg: () => void,
481
+ ): Promise<Array<Required<IProbeObject>>> => {
482
+ const versions: Array<Required<IProbeObject>> = [];
483
+ let keyMarker: string | undefined;
484
+ let versionIdMarker: string | undefined;
485
+ for (
486
+ let pageNumber = 1;
487
+ pageNumber <= cleanupVerificationMaximumPages;
488
+ pageNumber++
489
+ ) {
490
+ const output = await sendCleanupRequest(
491
+ signalArg,
492
+ assertCurrentArg,
493
+ () =>
494
+ clientArg.send(
495
+ new plugins.s3.ListObjectVersionsCommand({
496
+ Bucket: bucketNameArg,
497
+ Prefix: probeRootArg,
498
+ KeyMarker: keyMarker,
499
+ VersionIdMarker: versionIdMarker,
500
+ }),
501
+ {
502
+ abortSignal: signalArg,
503
+ },
504
+ ),
505
+ );
506
+ for (const version of [
507
+ ...(output.Versions ?? []),
508
+ ...(output.DeleteMarkers ?? []),
509
+ ]) {
510
+ if (
511
+ typeof version.Key !== 'string' ||
512
+ !version.Key.startsWith(probeRootArg)
513
+ ) {
514
+ throw new Error(
515
+ 'Version cleanup reconciliation returned an invalid prefixed entry.',
516
+ );
517
+ }
518
+ if (
519
+ typeof version.VersionId !== 'string' ||
520
+ version.VersionId.length === 0
521
+ ) {
522
+ throw new Error(
523
+ `Probe object version '${version.Key}' has no VersionId.`,
524
+ );
525
+ }
526
+ versions.push({
527
+ key: version.Key,
528
+ versionId: version.VersionId,
529
+ });
530
+ }
531
+ if (!output.IsTruncated) {
532
+ return versions;
533
+ }
534
+ if (
535
+ !output.NextKeyMarker ||
536
+ (
537
+ output.NextKeyMarker === keyMarker &&
538
+ output.NextVersionIdMarker === versionIdMarker
539
+ )
540
+ ) {
541
+ throw new Error(
542
+ 'Version cleanup reconciliation returned a non-advancing page.',
543
+ );
544
+ }
545
+ keyMarker = output.NextKeyMarker;
546
+ versionIdMarker = output.NextVersionIdMarker;
547
+ }
548
+ throw new Error('Version cleanup reconciliation exceeded its page bound.');
549
+ };
550
+
551
+ const listProbeObjectKeys = async (
552
+ clientArg: plugins.s3.S3Client,
553
+ bucketNameArg: string,
554
+ probeRootArg: string,
555
+ signalArg: AbortSignal,
556
+ assertCurrentArg: () => void,
557
+ ): Promise<string[]> => {
558
+ const keys: string[] = [];
559
+ let continuationToken: string | undefined;
560
+ for (
561
+ let pageNumber = 1;
562
+ pageNumber <= cleanupVerificationMaximumPages;
563
+ pageNumber++
564
+ ) {
565
+ const output = await sendCleanupRequest(
566
+ signalArg,
567
+ assertCurrentArg,
568
+ () =>
569
+ clientArg.send(
570
+ new plugins.s3.ListObjectsV2Command({
571
+ Bucket: bucketNameArg,
572
+ Prefix: probeRootArg,
573
+ ContinuationToken: continuationToken,
574
+ }),
575
+ {
576
+ abortSignal: signalArg,
577
+ },
578
+ ),
579
+ );
580
+ for (const object of output.Contents ?? []) {
581
+ if (
582
+ typeof object.Key !== 'string' ||
583
+ !object.Key.startsWith(probeRootArg)
584
+ ) {
585
+ throw new Error(
586
+ 'Object cleanup reconciliation returned an invalid prefixed entry.',
587
+ );
588
+ }
589
+ keys.push(object.Key);
590
+ }
591
+ if (!output.IsTruncated) {
592
+ return keys;
593
+ }
594
+ if (
595
+ !output.NextContinuationToken ||
596
+ output.NextContinuationToken === continuationToken
597
+ ) {
598
+ throw new Error(
599
+ 'Object cleanup reconciliation returned a non-advancing page.',
600
+ );
601
+ }
602
+ continuationToken = output.NextContinuationToken;
603
+ }
604
+ throw new Error('Object cleanup reconciliation exceeded its page bound.');
605
+ };
606
+
607
+ const getHttpStatus = (errorArg: unknown): number | undefined => {
608
+ if (typeof errorArg !== 'object' || errorArg === null) {
609
+ return undefined;
610
+ }
611
+ const statusCode = (errorArg as {
612
+ $metadata?: {
613
+ httpStatusCode?: unknown;
614
+ };
615
+ }).$metadata?.httpStatusCode;
616
+ return typeof statusCode === 'number' ? statusCode : undefined;
617
+ };
618
+
619
+ const isRetryableCleanupError = (errorArg: unknown): boolean => {
620
+ const statusCode = getHttpStatus(errorArg);
621
+ return (
622
+ statusCode === undefined ||
623
+ statusCode === 409 ||
624
+ statusCode === 429 ||
625
+ statusCode >= 500
626
+ );
627
+ };
628
+
629
+ const waitForMultipartAbortRetry = (
630
+ signalArg: AbortSignal,
631
+ ): Promise<void> => {
632
+ return new Promise<void>((resolveArg, rejectArg) => {
633
+ const retryTimer = setTimeout(() => {
634
+ signalArg.removeEventListener('abort', abortRetryWait);
635
+ resolveArg();
636
+ }, multipartAbortRetryDelayMs);
637
+ const abortRetryWait = (): void => {
638
+ clearTimeout(retryTimer);
639
+ rejectArg(
640
+ signalArg.reason ??
641
+ new Error('Multipart abort reconciliation was aborted.'),
642
+ );
643
+ };
644
+ signalArg.addEventListener('abort', abortRetryWait, { once: true });
645
+ if (signalArg.aborted) {
646
+ abortRetryWait();
647
+ }
648
+ });
649
+ };
650
+
651
+ const reconcileMultipartUploadAbort = async (
652
+ clientArg: plugins.s3.S3Client,
653
+ bucketNameArg: string,
654
+ keyArg: string,
655
+ uploadIdArg: string,
656
+ signalArg: AbortSignal,
657
+ assertCurrentArg: () => void,
658
+ ): Promise<'aborted' | 'absent'> => {
659
+ const reconciliationErrors: unknown[] = [];
660
+ for (
661
+ let attempt = 1;
662
+ attempt <= multipartAbortMaximumAttempts;
663
+ attempt++
664
+ ) {
665
+ let abortError: unknown;
666
+ let abortRequestSucceeded = false;
667
+ try {
668
+ await sendCleanupRequest(
669
+ signalArg,
670
+ assertCurrentArg,
671
+ () =>
672
+ clientArg.send(
673
+ new plugins.s3.AbortMultipartUploadCommand({
674
+ Bucket: bucketNameArg,
675
+ Key: keyArg,
676
+ UploadId: uploadIdArg,
677
+ }),
678
+ {
679
+ abortSignal: signalArg,
680
+ },
681
+ ),
682
+ );
683
+ abortRequestSucceeded = true;
684
+ } catch (error) {
685
+ if (isHttpStatus(error, 404)) {
686
+ return 'absent';
687
+ }
688
+ abortError = error;
689
+ reconciliationErrors.push(error);
690
+ }
691
+
692
+ try {
693
+ await sendCleanupRequest(
694
+ signalArg,
695
+ assertCurrentArg,
696
+ () =>
697
+ clientArg.send(
698
+ new plugins.s3.ListPartsCommand({
699
+ Bucket: bucketNameArg,
700
+ Key: keyArg,
701
+ UploadId: uploadIdArg,
702
+ MaxParts: 1,
703
+ }),
704
+ {
705
+ abortSignal: signalArg,
706
+ },
707
+ ),
708
+ );
709
+ } catch (error) {
710
+ if (isHttpStatus(error, 404)) {
711
+ return abortRequestSucceeded ? 'aborted' : 'absent';
712
+ }
713
+ reconciliationErrors.push(error);
714
+ if (!isRetryableCleanupError(error)) {
715
+ throw new AggregateError(
716
+ reconciliationErrors,
717
+ `Multipart upload '${uploadIdArg}' could not be verified as aborted.`,
718
+ );
719
+ }
720
+ }
721
+
722
+ if (abortError && !isRetryableCleanupError(abortError)) {
723
+ throw new AggregateError(
724
+ reconciliationErrors,
725
+ `Multipart upload '${uploadIdArg}' could not be aborted.`,
726
+ );
727
+ }
728
+ if (attempt === multipartAbortMaximumAttempts) {
729
+ break;
730
+ }
731
+ if (attempt > 3) {
732
+ await waitForMultipartAbortRetry(signalArg);
733
+ }
734
+ }
735
+ throw new AggregateError(
736
+ reconciliationErrors,
737
+ `Multipart upload '${uploadIdArg}' still exists after repeated aborts.`,
738
+ );
739
+ };
740
+
741
+ const createProbeCleanupTasks = (
742
+ clientArg: plugins.s3.S3Client,
743
+ bucketNameArg: string,
744
+ probeRootArg: string,
745
+ providerUsesVersionsArg: boolean,
746
+ multipartUploadsArg: IProbeMultipartUpload[],
747
+ objectsArg: IProbeObject[],
748
+ assertCurrentArg: () => void,
749
+ ): Array<(signalArg: AbortSignal) => Promise<unknown>> => {
750
+ const cleanupTasks: Array<
751
+ (signalArg: AbortSignal) => Promise<unknown>
752
+ > = [];
753
+ for (const multipartUpload of multipartUploadsArg) {
754
+ cleanupTasks.push((signalArg) =>
755
+ reconcileMultipartUploadAbort(
756
+ clientArg,
757
+ bucketNameArg,
758
+ multipartUpload.key,
759
+ multipartUpload.uploadId,
760
+ signalArg,
761
+ assertCurrentArg,
762
+ ),
763
+ );
764
+ }
765
+ for (const object of objectsArg) {
766
+ cleanupTasks.push((signalArg) =>
767
+ sendCleanupRequest(
768
+ signalArg,
769
+ assertCurrentArg,
770
+ () =>
771
+ clientArg.send(
772
+ new plugins.s3.DeleteObjectCommand({
773
+ Bucket: bucketNameArg,
774
+ Key: object.key,
775
+ VersionId: object.versionId,
776
+ }),
777
+ {
778
+ abortSignal: signalArg,
779
+ },
780
+ ),
781
+ ),
782
+ );
783
+ }
784
+ cleanupTasks.push(async (signalArg) => {
785
+ const discoveredUploads = await listProbeMultipartUploads(
786
+ clientArg,
787
+ bucketNameArg,
788
+ probeRootArg,
789
+ signalArg,
790
+ assertCurrentArg,
791
+ );
792
+ for (const upload of discoveredUploads) {
793
+ await reconcileMultipartUploadAbort(
794
+ clientArg,
795
+ bucketNameArg,
796
+ upload.key,
797
+ upload.uploadId,
798
+ signalArg,
799
+ assertCurrentArg,
800
+ );
801
+ }
802
+ const remainingUploads = await listProbeMultipartUploads(
803
+ clientArg,
804
+ bucketNameArg,
805
+ probeRootArg,
806
+ signalArg,
807
+ assertCurrentArg,
808
+ );
809
+ if (remainingUploads.length > 0) {
810
+ throw new Error(
811
+ `Probe multipart upload '${remainingUploads[0].uploadId}' remains after cleanup.`,
812
+ );
813
+ }
814
+ });
815
+ if (providerUsesVersionsArg) {
816
+ cleanupTasks.push(async (signalArg) => {
817
+ const discoveredVersions = await listProbeObjectVersions(
818
+ clientArg,
819
+ bucketNameArg,
820
+ probeRootArg,
821
+ signalArg,
822
+ assertCurrentArg,
823
+ );
824
+ for (const version of discoveredVersions) {
825
+ await sendCleanupRequest(
826
+ signalArg,
827
+ assertCurrentArg,
828
+ () =>
829
+ clientArg.send(
830
+ new plugins.s3.DeleteObjectCommand({
831
+ Bucket: bucketNameArg,
832
+ Key: version.key,
833
+ VersionId: version.versionId,
834
+ }),
835
+ {
836
+ abortSignal: signalArg,
837
+ },
838
+ ),
839
+ );
840
+ }
841
+ const remainingVersions = await listProbeObjectVersions(
842
+ clientArg,
843
+ bucketNameArg,
844
+ probeRootArg,
845
+ signalArg,
846
+ assertCurrentArg,
847
+ );
848
+ if (remainingVersions.length > 0) {
849
+ throw new Error(
850
+ `Probe object version '${remainingVersions[0].versionId}' remains after cleanup.`,
851
+ );
852
+ }
853
+ });
854
+ } else {
855
+ cleanupTasks.push(async (signalArg) => {
856
+ const discoveredKeys = await listProbeObjectKeys(
857
+ clientArg,
858
+ bucketNameArg,
859
+ probeRootArg,
860
+ signalArg,
861
+ assertCurrentArg,
862
+ );
863
+ for (const key of discoveredKeys) {
864
+ await sendCleanupRequest(
865
+ signalArg,
866
+ assertCurrentArg,
867
+ () =>
868
+ clientArg.send(
869
+ new plugins.s3.DeleteObjectCommand({
870
+ Bucket: bucketNameArg,
871
+ Key: key,
872
+ }),
873
+ {
874
+ abortSignal: signalArg,
875
+ },
876
+ ),
877
+ );
878
+ }
879
+ const remainingKeys = await listProbeObjectKeys(
880
+ clientArg,
881
+ bucketNameArg,
882
+ probeRootArg,
883
+ signalArg,
884
+ assertCurrentArg,
885
+ );
886
+ if (remainingKeys.length > 0) {
887
+ throw new Error(
888
+ `Probe object '${remainingKeys[0]}' remains after cleanup.`,
889
+ );
890
+ }
891
+ });
892
+ }
893
+ return cleanupTasks;
894
+ };
895
+
896
+ export const probeExactUploadCapability = async (
897
+ bucketRefArg: IExactUploadBucketRef,
898
+ optionsArg: IExactUploadCapabilityProbeOptions,
899
+ ): Promise<TExactUploadCapabilityProbeResult> => {
900
+ let operation: IExactUploadOperation;
901
+ try {
902
+ operation = beginExactUploadProbeOperation(
903
+ bucketRefArg,
904
+ optionsArg.signal,
905
+ );
906
+ } catch (error) {
907
+ return {
908
+ supported: false,
909
+ reason: 'unavailable',
910
+ cause: error,
911
+ };
912
+ }
913
+
914
+ const probeRoot = `.smartbucket-probes/exact-upload-v1/${plugins.smartunique.uuid4()}`;
915
+ const conditionalPutKey = `${probeRoot}/conditional-put`;
916
+ const conditionalCompleteKey = `${probeRoot}/conditional-complete`;
917
+ const unfinishedMultipartUploads: IProbeMultipartUpload[] = [];
918
+ const probeObjects: IProbeObject[] = [];
919
+ let providerUsesVersions = false;
920
+ let failureReason: TExactUploadCapabilityProbeFailureReason | undefined;
921
+ let failureCause: unknown;
922
+
923
+ const recordProbeObject = (
924
+ keyArg: string,
925
+ outputArg: {
926
+ VersionId?: string;
927
+ },
928
+ ): void => {
929
+ const versionId = getOptionalNonEmptyString(outputArg.VersionId);
930
+ if (versionId) {
931
+ providerUsesVersions = true;
932
+ }
933
+ probeObjects.push({
934
+ key: keyArg,
935
+ versionId,
936
+ });
937
+ };
938
+
939
+ try {
940
+ const versioningOutput = await sendOperationCommand(
941
+ operation,
942
+ (signalArg) =>
943
+ operation.client.send(
944
+ new plugins.s3.GetBucketVersioningCommand({
945
+ Bucket: operation.bucketName,
946
+ }),
947
+ {
948
+ abortSignal: signalArg,
949
+ },
950
+ ),
951
+ );
952
+ providerUsesVersions =
953
+ versioningOutput.Status === 'Enabled' ||
954
+ versioningOutput.Status === 'Suspended';
955
+
956
+ const firstPutOutput = await sendOperationCommand(
957
+ operation,
958
+ (signalArg) =>
959
+ operation.client.send(
960
+ new plugins.s3.PutObjectCommand({
961
+ Bucket: operation.bucketName,
962
+ Key: conditionalPutKey,
963
+ Body: new Uint8Array(0),
964
+ ContentLength: 0,
965
+ Metadata: probeMetadata,
966
+ IfNoneMatch: '*',
967
+ }),
968
+ {
969
+ abortSignal: signalArg,
970
+ },
971
+ ),
972
+ );
973
+ recordProbeObject(conditionalPutKey, firstPutOutput);
974
+ requireNonEmptyString(firstPutOutput.ETag, 'PutObject ETag');
975
+
976
+ try {
977
+ const duplicatePutOutput = await sendOperationCommand(
978
+ operation,
979
+ (signalArg) =>
980
+ operation.client.send(
981
+ new plugins.s3.PutObjectCommand({
982
+ Bucket: operation.bucketName,
983
+ Key: conditionalPutKey,
984
+ Body: new Uint8Array(0),
985
+ ContentLength: 0,
986
+ Metadata: probeMetadata,
987
+ IfNoneMatch: '*',
988
+ }),
989
+ {
990
+ abortSignal: signalArg,
991
+ },
992
+ ),
993
+ );
994
+ recordProbeObject(conditionalPutKey, duplicatePutOutput);
995
+ throw new ExactUploadProbeSemanticError(
996
+ 'conditional-put-not-enforced',
997
+ );
998
+ } catch (error) {
999
+ if (error instanceof ExactUploadProbeSemanticError) {
1000
+ throw error;
1001
+ }
1002
+ operation.assertCurrent();
1003
+ if (!isHttpStatus(error, 412)) {
1004
+ throw error;
1005
+ }
1006
+ }
1007
+
1008
+ const createFirstOutput = await sendOperationCommand(
1009
+ operation,
1010
+ (signalArg) =>
1011
+ operation.client.send(
1012
+ new plugins.s3.CreateMultipartUploadCommand({
1013
+ Bucket: operation.bucketName,
1014
+ Key: conditionalCompleteKey,
1015
+ Metadata: probeMetadata,
1016
+ }),
1017
+ {
1018
+ abortSignal: signalArg,
1019
+ },
1020
+ ),
1021
+ );
1022
+ const firstUploadId = requireNonEmptyString(
1023
+ createFirstOutput.UploadId,
1024
+ 'CreateMultipartUpload UploadId',
1025
+ );
1026
+ unfinishedMultipartUploads.push({
1027
+ key: conditionalCompleteKey,
1028
+ uploadId: firstUploadId,
1029
+ });
1030
+ const firstPartOutput = await sendOperationCommand(
1031
+ operation,
1032
+ (signalArg) =>
1033
+ operation.client.send(
1034
+ new plugins.s3.UploadPartCommand({
1035
+ Bucket: operation.bucketName,
1036
+ Key: conditionalCompleteKey,
1037
+ UploadId: firstUploadId,
1038
+ PartNumber: 1,
1039
+ Body: new Uint8Array([1]),
1040
+ ContentLength: 1,
1041
+ }),
1042
+ {
1043
+ abortSignal: signalArg,
1044
+ },
1045
+ ),
1046
+ );
1047
+ const firstPartEtag = requireNonEmptyString(
1048
+ firstPartOutput.ETag,
1049
+ 'UploadPart ETag',
1050
+ );
1051
+ const firstCompleteOutput = await sendOperationCommand(
1052
+ operation,
1053
+ (signalArg) =>
1054
+ operation.client.send(
1055
+ new plugins.s3.CompleteMultipartUploadCommand({
1056
+ Bucket: operation.bucketName,
1057
+ Key: conditionalCompleteKey,
1058
+ UploadId: firstUploadId,
1059
+ MultipartUpload: {
1060
+ Parts: [
1061
+ {
1062
+ PartNumber: 1,
1063
+ ETag: firstPartEtag,
1064
+ },
1065
+ ],
1066
+ },
1067
+ IfNoneMatch: '*',
1068
+ MpuObjectSize: 1,
1069
+ }),
1070
+ {
1071
+ abortSignal: signalArg,
1072
+ },
1073
+ ),
1074
+ );
1075
+ unfinishedMultipartUploads.splice(
1076
+ unfinishedMultipartUploads.findIndex(
1077
+ (uploadArg) => uploadArg.uploadId === firstUploadId,
1078
+ ),
1079
+ 1,
1080
+ );
1081
+ recordProbeObject(conditionalCompleteKey, firstCompleteOutput);
1082
+ requireNonEmptyString(
1083
+ firstCompleteOutput.ETag,
1084
+ 'CompleteMultipartUpload ETag',
1085
+ );
1086
+
1087
+ const createSecondOutput = await sendOperationCommand(
1088
+ operation,
1089
+ (signalArg) =>
1090
+ operation.client.send(
1091
+ new plugins.s3.CreateMultipartUploadCommand({
1092
+ Bucket: operation.bucketName,
1093
+ Key: conditionalCompleteKey,
1094
+ Metadata: probeMetadata,
1095
+ }),
1096
+ {
1097
+ abortSignal: signalArg,
1098
+ },
1099
+ ),
1100
+ );
1101
+ const secondUploadId = requireNonEmptyString(
1102
+ createSecondOutput.UploadId,
1103
+ 'CreateMultipartUpload UploadId',
1104
+ );
1105
+ unfinishedMultipartUploads.push({
1106
+ key: conditionalCompleteKey,
1107
+ uploadId: secondUploadId,
1108
+ });
1109
+ const secondPartOutput = await sendOperationCommand(
1110
+ operation,
1111
+ (signalArg) =>
1112
+ operation.client.send(
1113
+ new plugins.s3.UploadPartCommand({
1114
+ Bucket: operation.bucketName,
1115
+ Key: conditionalCompleteKey,
1116
+ UploadId: secondUploadId,
1117
+ PartNumber: 1,
1118
+ Body: new Uint8Array([2]),
1119
+ ContentLength: 1,
1120
+ }),
1121
+ {
1122
+ abortSignal: signalArg,
1123
+ },
1124
+ ),
1125
+ );
1126
+ const secondPartEtag = requireNonEmptyString(
1127
+ secondPartOutput.ETag,
1128
+ 'UploadPart ETag',
1129
+ );
1130
+ try {
1131
+ const duplicateCompleteOutput = await sendOperationCommand(
1132
+ operation,
1133
+ (signalArg) =>
1134
+ operation.client.send(
1135
+ new plugins.s3.CompleteMultipartUploadCommand({
1136
+ Bucket: operation.bucketName,
1137
+ Key: conditionalCompleteKey,
1138
+ UploadId: secondUploadId,
1139
+ MultipartUpload: {
1140
+ Parts: [
1141
+ {
1142
+ PartNumber: 1,
1143
+ ETag: secondPartEtag,
1144
+ },
1145
+ ],
1146
+ },
1147
+ IfNoneMatch: '*',
1148
+ MpuObjectSize: 1,
1149
+ }),
1150
+ {
1151
+ abortSignal: signalArg,
1152
+ },
1153
+ ),
1154
+ );
1155
+ unfinishedMultipartUploads.splice(
1156
+ unfinishedMultipartUploads.findIndex(
1157
+ (uploadArg) => uploadArg.uploadId === secondUploadId,
1158
+ ),
1159
+ 1,
1160
+ );
1161
+ recordProbeObject(conditionalCompleteKey, duplicateCompleteOutput);
1162
+ throw new ExactUploadProbeSemanticError(
1163
+ 'conditional-complete-not-enforced',
1164
+ );
1165
+ } catch (error) {
1166
+ if (error instanceof ExactUploadProbeSemanticError) {
1167
+ throw error;
1168
+ }
1169
+ operation.assertCurrent();
1170
+ if (!isHttpStatus(error, 412)) {
1171
+ throw error;
1172
+ }
1173
+ }
1174
+ } catch (error) {
1175
+ if (error instanceof ExactUploadProbeSemanticError) {
1176
+ failureReason = error.reason;
1177
+ } else if (operation.signal.aborted) {
1178
+ failureReason = 'aborted';
1179
+ failureCause = operation.signal.reason;
1180
+ } else {
1181
+ failureReason = 'unavailable';
1182
+ failureCause = error;
1183
+ }
1184
+ }
1185
+
1186
+ const probeCleanupTasks = createProbeCleanupTasks(
1187
+ operation.client,
1188
+ operation.bucketName,
1189
+ probeRoot,
1190
+ providerUsesVersions,
1191
+ unfinishedMultipartUploads,
1192
+ probeObjects,
1193
+ () => operation.assertLifecycleCurrent(),
1194
+ );
1195
+ const primaryProviderSettlement =
1196
+ getDetachedOperationSendSettlement(operation.signal);
1197
+ if (primaryProviderSettlement) {
1198
+ const deferredCleanup = primaryProviderSettlement
1199
+ .then(async () => {
1200
+ try {
1201
+ operation.assertLifecycleCurrent();
1202
+ const cleanupResult = await runIndependentCleanup(
1203
+ probeCleanupTasks,
1204
+ operation,
1205
+ );
1206
+ await cleanupResult.settlement;
1207
+ } catch {
1208
+ // Lifecycle close makes deferred probe cleanup indeterminate.
1209
+ }
1210
+ })
1211
+ .then(
1212
+ () => undefined,
1213
+ () => undefined,
1214
+ );
1215
+ finishOperationAfterOwnedSettlements(operation, [deferredCleanup]);
1216
+ return {
1217
+ supported: false,
1218
+ reason: failureReason ?? 'aborted',
1219
+ cause: failureCause ?? operation.signal.reason,
1220
+ };
1221
+ }
1222
+ const cleanupResult = await runIndependentCleanup(
1223
+ probeCleanupTasks,
1224
+ operation,
1225
+ );
1226
+
1227
+ try {
1228
+ if (cleanupResult.errors.length > 0) {
1229
+ return {
1230
+ supported: false,
1231
+ reason: 'cleanup-failed',
1232
+ cause: new AggregateError(
1233
+ cleanupResult.errors,
1234
+ 'The exact-upload capability probe could not clean up safely.',
1235
+ ),
1236
+ };
1237
+ }
1238
+ if (failureReason) {
1239
+ return {
1240
+ supported: false,
1241
+ reason: failureReason,
1242
+ cause: failureCause,
1243
+ };
1244
+ }
1245
+ return {
1246
+ supported: true,
1247
+ capability: mintExactUploadCapability(bucketRefArg, operation),
1248
+ };
1249
+ } catch (error) {
1250
+ return {
1251
+ supported: false,
1252
+ reason: operation.signal.aborted ? 'aborted' : 'unavailable',
1253
+ cause: error,
1254
+ };
1255
+ } finally {
1256
+ if (cleanupResult.timedOut) {
1257
+ finishOperationAfterOwnedSettlements(operation, [
1258
+ cleanupResult.settlement,
1259
+ ]);
1260
+ } else {
1261
+ finishOperationAfterOwnedSettlements(operation);
1262
+ }
1263
+ }
1264
+ };
1265
+
1266
+ const abortMultipartUpload = async (
1267
+ operationArg: IExactUploadOperation,
1268
+ keyArg: string,
1269
+ uploadIdArg: string,
1270
+ ): Promise<IMultipartAbortResult> => {
1271
+ let reconciliationResult: 'aborted' | 'absent' | undefined;
1272
+ const cleanupResult = await runIndependentCleanup(
1273
+ [
1274
+ async (signalArg) => {
1275
+ reconciliationResult = await reconcileMultipartUploadAbort(
1276
+ operationArg.client,
1277
+ operationArg.bucketName,
1278
+ keyArg,
1279
+ uploadIdArg,
1280
+ signalArg,
1281
+ () => operationArg.assertLifecycleCurrent(),
1282
+ );
1283
+ },
1284
+ ],
1285
+ operationArg,
1286
+ );
1287
+ return {
1288
+ cleanup:
1289
+ cleanupResult.timedOut
1290
+ ? {
1291
+ status: 'deferred',
1292
+ }
1293
+ : cleanupResult.errors.length === 0
1294
+ ? {
1295
+ status: reconciliationResult ?? 'absent',
1296
+ }
1297
+ : {
1298
+ status: 'abort-failed',
1299
+ cause: new AggregateError(
1300
+ cleanupResult.errors,
1301
+ 'The multipart upload could not be aborted safely.',
1302
+ ),
1303
+ },
1304
+ timedOut: cleanupResult.timedOut,
1305
+ settlement: cleanupResult.settlement,
1306
+ };
1307
+ };
1308
+
1309
+ export const fastPutStreamExact = async (
1310
+ bucketRefArg: IExactUploadBucketRef,
1311
+ optionsArg: IFastPutStreamExactOptions,
1312
+ ): Promise<IExactUploadResult> => {
1313
+ if (typeof optionsArg.path !== 'string' || optionsArg.path.length === 0) {
1314
+ throw new TypeError('path must be a non-empty string.');
1315
+ }
1316
+ const nativeObjectProperties = snapshotNativeObjectProperties(
1317
+ optionsArg.nativeObjectProperties,
1318
+ );
1319
+ const plan = planMultipartUpload({
1320
+ objectSize: optionsArg.contentLength,
1321
+ requestedPartSize: optionsArg.requestedPartSize,
1322
+ requestedConcurrency: optionsArg.requestedConcurrency,
1323
+ });
1324
+
1325
+ let operation: IExactUploadOperation;
1326
+ try {
1327
+ operation = beginExactUploadCapabilityOperation(
1328
+ bucketRefArg,
1329
+ optionsArg.capability,
1330
+ optionsArg.signal,
1331
+ );
1332
+ } catch (error) {
1333
+ throw new ExactUploadError(
1334
+ {
1335
+ phase: 'capability',
1336
+ destinationPath: optionsArg.path,
1337
+ expectedContentLength: optionsArg.contentLength,
1338
+ destinationPublication: 'not-published',
1339
+ multipartCleanup: {
1340
+ status: 'not-required',
1341
+ },
1342
+ },
1343
+ error,
1344
+ );
1345
+ }
1346
+
1347
+ let readableStream: ReadableStream<Uint8Array>;
1348
+ try {
1349
+ readableStream = toWebReadableStream(optionsArg.readableStream);
1350
+ } catch (error) {
1351
+ operation.finish();
1352
+ throw new ExactUploadError(
1353
+ {
1354
+ phase: 'source',
1355
+ destinationPath: optionsArg.path,
1356
+ expectedContentLength: optionsArg.contentLength,
1357
+ destinationPublication: 'not-published',
1358
+ multipartCleanup: {
1359
+ status: 'not-required',
1360
+ },
1361
+ },
1362
+ error,
1363
+ );
1364
+ }
1365
+
1366
+ if (plan.classification === 'zero') {
1367
+ let putRequestAttempted = false;
1368
+ let zeroFinishSettlement: Promise<void> | undefined;
1369
+ try {
1370
+ let consumptionResult;
1371
+ try {
1372
+ consumptionResult = await consumeExactMultipartStream({
1373
+ readableStream,
1374
+ plan,
1375
+ signal: operation.signal,
1376
+ failureSettlementTimeoutMs:
1377
+ optionsArg.failureSettlementTimeoutMs,
1378
+ async onPart() {
1379
+ throw new Error('A zero-byte upload cannot contain multipart parts.');
1380
+ },
1381
+ });
1382
+ } catch (error) {
1383
+ if (
1384
+ error instanceof MultipartStreamConsumptionError &&
1385
+ (
1386
+ error.state.cleanup.callbacks === 'detached' ||
1387
+ error.state.cleanup.reader.status === 'cancel-detached'
1388
+ )
1389
+ ) {
1390
+ zeroFinishSettlement = error.ownedSettlement;
1391
+ }
1392
+ throw new ExactUploadError(
1393
+ {
1394
+ phase: 'source',
1395
+ destinationPath: optionsArg.path,
1396
+ expectedContentLength: optionsArg.contentLength,
1397
+ destinationPublication: 'not-published',
1398
+ multipartCleanup: {
1399
+ status: 'not-required',
1400
+ },
1401
+ },
1402
+ error,
1403
+ );
1404
+ }
1405
+
1406
+ try {
1407
+ const putOutput = await sendOperationCommand(
1408
+ operation,
1409
+ (signalArg) => {
1410
+ putRequestAttempted = true;
1411
+ return operation.client.send(
1412
+ new plugins.s3.PutObjectCommand({
1413
+ Bucket: operation.bucketName,
1414
+ Key: optionsArg.path,
1415
+ Body: new Uint8Array(0),
1416
+ ContentLength: 0,
1417
+ Metadata: optionsArg.nativeMetadata,
1418
+ ContentType: nativeObjectProperties?.contentType,
1419
+ IfNoneMatch: '*',
1420
+ }),
1421
+ {
1422
+ abortSignal: signalArg,
1423
+ },
1424
+ );
1425
+ },
1426
+ );
1427
+ return {
1428
+ destinationPath: optionsArg.path,
1429
+ contentLength: 0,
1430
+ etag: requireNonEmptyString(putOutput.ETag, 'PutObject ETag'),
1431
+ versionId: getOptionalNonEmptyString(putOutput.VersionId),
1432
+ partCount: 0,
1433
+ peakBufferedBytes: consumptionResult.peakBufferedBytes,
1434
+ maximumBufferedBytes: consumptionResult.maximumBufferedBytes,
1435
+ };
1436
+ } catch (error) {
1437
+ if (error instanceof ExactUploadError) {
1438
+ throw error;
1439
+ }
1440
+ throw new ExactUploadError(
1441
+ {
1442
+ phase: 'put-zero',
1443
+ destinationPath: optionsArg.path,
1444
+ expectedContentLength: optionsArg.contentLength,
1445
+ destinationPublication:
1446
+ putRequestAttempted &&
1447
+ !isDefinitiveConditionalWriteRejection(error)
1448
+ ? 'ambiguous'
1449
+ : 'not-published',
1450
+ multipartCleanup: {
1451
+ status: 'not-required',
1452
+ },
1453
+ },
1454
+ error,
1455
+ );
1456
+ }
1457
+ } finally {
1458
+ if (zeroFinishSettlement) {
1459
+ finishOperationAfterOwnedSettlements(operation, [
1460
+ zeroFinishSettlement,
1461
+ ]);
1462
+ } else {
1463
+ finishOperationAfterOwnedSettlements(operation);
1464
+ }
1465
+ }
1466
+ }
1467
+
1468
+ let phase: TExactUploadFailurePhase = 'create-multipart';
1469
+ let uploadId: string | undefined;
1470
+ let createRequestAttempted = false;
1471
+ let completeRequestAttempted = false;
1472
+ let operationFinishSettlement: Promise<void> | undefined;
1473
+ let detachedStreamError: MultipartStreamConsumptionError | undefined;
1474
+ try {
1475
+ const createOutput = await sendOperationCommand(
1476
+ operation,
1477
+ (signalArg) => {
1478
+ createRequestAttempted = true;
1479
+ return operation.client.send(
1480
+ new plugins.s3.CreateMultipartUploadCommand({
1481
+ Bucket: operation.bucketName,
1482
+ Key: optionsArg.path,
1483
+ Metadata: optionsArg.nativeMetadata,
1484
+ ContentType: nativeObjectProperties?.contentType,
1485
+ }),
1486
+ {
1487
+ abortSignal: signalArg,
1488
+ },
1489
+ );
1490
+ },
1491
+ );
1492
+ uploadId = requireNonEmptyString(
1493
+ createOutput.UploadId,
1494
+ 'CreateMultipartUpload UploadId',
1495
+ );
1496
+
1497
+ phase = 'source';
1498
+ const completedParts: plugins.s3.CompletedPart[] = [];
1499
+ let consumptionResult;
1500
+ try {
1501
+ consumptionResult = await consumeExactMultipartStream({
1502
+ readableStream,
1503
+ plan,
1504
+ signal: operation.signal,
1505
+ failureSettlementTimeoutMs:
1506
+ optionsArg.failureSettlementTimeoutMs,
1507
+ async onPart(partArg) {
1508
+ operation.assertCurrent();
1509
+ const uploadPartOutput = await operation.client.send(
1510
+ new plugins.s3.UploadPartCommand({
1511
+ Bucket: operation.bucketName,
1512
+ Key: optionsArg.path,
1513
+ UploadId: uploadId!,
1514
+ PartNumber: partArg.partNumber,
1515
+ Body: partArg.data,
1516
+ ContentLength: partArg.data.byteLength,
1517
+ }),
1518
+ {
1519
+ abortSignal: partArg.signal,
1520
+ },
1521
+ );
1522
+ operation.assertCurrent();
1523
+ completedParts[partArg.partNumber - 1] = {
1524
+ PartNumber: partArg.partNumber,
1525
+ ETag: requireNonEmptyString(
1526
+ uploadPartOutput.ETag,
1527
+ 'UploadPart ETag',
1528
+ ),
1529
+ };
1530
+ },
1531
+ });
1532
+ } catch (error) {
1533
+ if (
1534
+ error instanceof MultipartStreamConsumptionError &&
1535
+ error.state.kind === 'part-callback'
1536
+ ) {
1537
+ phase = 'upload-part';
1538
+ }
1539
+ if (
1540
+ error instanceof MultipartStreamConsumptionError &&
1541
+ (
1542
+ error.state.cleanup.callbacks === 'detached' ||
1543
+ error.state.cleanup.reader.status === 'cancel-detached'
1544
+ )
1545
+ ) {
1546
+ detachedStreamError = error;
1547
+ }
1548
+ throw error;
1549
+ }
1550
+
1551
+ phase = 'complete-multipart';
1552
+ const completeOutput = await sendOperationCommand(
1553
+ operation,
1554
+ (signalArg) => {
1555
+ completeRequestAttempted = true;
1556
+ return operation.client.send(
1557
+ new plugins.s3.CompleteMultipartUploadCommand({
1558
+ Bucket: operation.bucketName,
1559
+ Key: optionsArg.path,
1560
+ UploadId: uploadId,
1561
+ MultipartUpload: {
1562
+ Parts: completedParts,
1563
+ },
1564
+ IfNoneMatch: '*',
1565
+ MpuObjectSize: optionsArg.contentLength,
1566
+ }),
1567
+ {
1568
+ abortSignal: signalArg,
1569
+ },
1570
+ );
1571
+ },
1572
+ );
1573
+ return {
1574
+ destinationPath: optionsArg.path,
1575
+ contentLength: optionsArg.contentLength,
1576
+ etag: requireNonEmptyString(
1577
+ completeOutput.ETag,
1578
+ 'CompleteMultipartUpload ETag',
1579
+ ),
1580
+ versionId: getOptionalNonEmptyString(completeOutput.VersionId),
1581
+ partCount: completedParts.length,
1582
+ peakBufferedBytes: consumptionResult.peakBufferedBytes,
1583
+ maximumBufferedBytes: consumptionResult.maximumBufferedBytes,
1584
+ };
1585
+ } catch (error) {
1586
+ let multipartCleanup: TExactUploadMultipartCleanup;
1587
+ const primaryProviderSettlement =
1588
+ getDetachedOperationSendSettlement(operation.signal);
1589
+ if (
1590
+ uploadId &&
1591
+ detachedStreamError?.state.cleanup.callbacks === 'detached'
1592
+ ) {
1593
+ multipartCleanup = {
1594
+ status: 'deferred',
1595
+ };
1596
+ const callbackGatedCleanup = detachedStreamError.callbackSettlement
1597
+ .then(async () => {
1598
+ try {
1599
+ operation.assertLifecycleCurrent();
1600
+ const abortResult = await abortMultipartUpload(
1601
+ operation,
1602
+ optionsArg.path,
1603
+ uploadId!,
1604
+ );
1605
+ await abortResult.settlement;
1606
+ } catch {
1607
+ // Lifecycle close makes deferred cleanup indeterminate.
1608
+ }
1609
+ })
1610
+ .then(
1611
+ () => undefined,
1612
+ () => undefined,
1613
+ );
1614
+ operationFinishSettlement = Promise.allSettled([
1615
+ callbackGatedCleanup,
1616
+ detachedStreamError.ownedSettlement,
1617
+ ]).then(() => undefined);
1618
+ } else if (uploadId && primaryProviderSettlement) {
1619
+ multipartCleanup = {
1620
+ status: 'deferred',
1621
+ };
1622
+ const providerGatedCleanup = primaryProviderSettlement
1623
+ .then(async () => {
1624
+ try {
1625
+ operation.assertLifecycleCurrent();
1626
+ const abortResult = await abortMultipartUpload(
1627
+ operation,
1628
+ optionsArg.path,
1629
+ uploadId!,
1630
+ );
1631
+ await abortResult.settlement;
1632
+ } catch {
1633
+ // Lifecycle close makes deferred cleanup indeterminate.
1634
+ }
1635
+ })
1636
+ .then(
1637
+ () => undefined,
1638
+ () => undefined,
1639
+ );
1640
+ operationFinishSettlement = Promise.allSettled([
1641
+ primaryProviderSettlement,
1642
+ providerGatedCleanup,
1643
+ ]).then(() => undefined);
1644
+ } else if (uploadId) {
1645
+ const abortResult = await abortMultipartUpload(
1646
+ operation,
1647
+ optionsArg.path,
1648
+ uploadId,
1649
+ );
1650
+ multipartCleanup = abortResult.cleanup;
1651
+ if (abortResult.timedOut) {
1652
+ operationFinishSettlement = abortResult.settlement;
1653
+ }
1654
+ } else if (createRequestAttempted) {
1655
+ multipartCleanup = {
1656
+ ...(primaryProviderSettlement
1657
+ ? {
1658
+ status: 'deferred' as const,
1659
+ }
1660
+ : {
1661
+ status: 'abort-failed' as const,
1662
+ cause: new Error(
1663
+ 'The multipart upload could not be aborted because no UploadId was returned.',
1664
+ ),
1665
+ }),
1666
+ };
1667
+ if (primaryProviderSettlement) {
1668
+ operationFinishSettlement = primaryProviderSettlement;
1669
+ }
1670
+ } else {
1671
+ multipartCleanup = {
1672
+ status: 'not-required',
1673
+ };
1674
+ }
1675
+ if (
1676
+ detachedStreamError?.state.cleanup.reader.status ===
1677
+ 'cancel-detached' &&
1678
+ detachedStreamError.state.cleanup.callbacks !== 'detached'
1679
+ ) {
1680
+ const ownedSettlements = [
1681
+ detachedStreamError.ownedSettlement,
1682
+ ];
1683
+ if (operationFinishSettlement) {
1684
+ ownedSettlements.push(operationFinishSettlement);
1685
+ }
1686
+ operationFinishSettlement = Promise.allSettled(
1687
+ ownedSettlements,
1688
+ ).then(() => undefined);
1689
+ }
1690
+ throw new ExactUploadError(
1691
+ {
1692
+ phase,
1693
+ destinationPath: optionsArg.path,
1694
+ expectedContentLength: optionsArg.contentLength,
1695
+ destinationPublication:
1696
+ phase === 'complete-multipart' &&
1697
+ completeRequestAttempted &&
1698
+ !isDefinitiveConditionalWriteRejection(error)
1699
+ ? 'ambiguous'
1700
+ : 'not-published',
1701
+ multipartUploadId: uploadId,
1702
+ multipartCleanup,
1703
+ },
1704
+ error,
1705
+ );
1706
+ } finally {
1707
+ if (operationFinishSettlement) {
1708
+ finishOperationAfterOwnedSettlements(operation, [
1709
+ operationFinishSettlement,
1710
+ ]);
1711
+ } else {
1712
+ finishOperationAfterOwnedSettlements(operation);
1713
+ }
1714
+ }
1715
+ };