@teepin/sdk 0.2.1 → 0.3.0-rc.1

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 (134) hide show
  1. package/README.md +41 -36
  2. package/dist/cjs/client.js +42 -4
  3. package/dist/cjs/client.js.map +1 -1
  4. package/dist/cjs/direct/bytes.js +50 -0
  5. package/dist/cjs/direct/bytes.js.map +1 -0
  6. package/dist/cjs/direct/common.js +167 -0
  7. package/dist/cjs/direct/common.js.map +1 -0
  8. package/dist/cjs/direct/downloads-plan.js +127 -0
  9. package/dist/cjs/direct/downloads-plan.js.map +1 -0
  10. package/dist/cjs/direct/engine.js +405 -0
  11. package/dist/cjs/direct/engine.js.map +1 -0
  12. package/dist/cjs/direct/formats.js +44 -0
  13. package/dist/cjs/direct/formats.js.map +1 -0
  14. package/dist/cjs/direct/hpke.js +275 -0
  15. package/dist/cjs/direct/hpke.js.map +1 -0
  16. package/dist/cjs/direct/journal.js +80 -0
  17. package/dist/cjs/direct/journal.js.map +1 -0
  18. package/dist/cjs/direct/large.js +850 -0
  19. package/dist/cjs/direct/large.js.map +1 -0
  20. package/dist/cjs/direct/reads.js +1314 -0
  21. package/dist/cjs/direct/reads.js.map +1 -0
  22. package/dist/cjs/direct/sha256.js +122 -0
  23. package/dist/cjs/direct/sha256.js.map +1 -0
  24. package/dist/cjs/direct/tws1.js +480 -0
  25. package/dist/cjs/direct/tws1.js.map +1 -0
  26. package/dist/cjs/direct/uploads.js +1706 -0
  27. package/dist/cjs/direct/uploads.js.map +1 -0
  28. package/dist/cjs/errors.js +70 -13
  29. package/dist/cjs/errors.js.map +1 -1
  30. package/dist/cjs/index.js +5 -1
  31. package/dist/cjs/index.js.map +1 -1
  32. package/dist/cjs/links.js +195 -0
  33. package/dist/cjs/links.js.map +1 -0
  34. package/dist/cjs/node/downloads.js +835 -0
  35. package/dist/cjs/node/downloads.js.map +1 -0
  36. package/dist/cjs/node/journal.js +841 -0
  37. package/dist/cjs/node/journal.js.map +1 -0
  38. package/dist/cjs/node/uploads.js +219 -0
  39. package/dist/cjs/node/uploads.js.map +1 -0
  40. package/dist/cjs/node.js +26 -39
  41. package/dist/cjs/node.js.map +1 -1
  42. package/dist/cjs/storage.js +214 -184
  43. package/dist/cjs/storage.js.map +1 -1
  44. package/dist/cjs/transfer.js +18 -351
  45. package/dist/cjs/transfer.js.map +1 -1
  46. package/dist/esm/client.js +42 -4
  47. package/dist/esm/client.js.map +1 -1
  48. package/dist/esm/direct/bytes.js +42 -0
  49. package/dist/esm/direct/bytes.js.map +1 -0
  50. package/dist/esm/direct/common.js +155 -0
  51. package/dist/esm/direct/common.js.map +1 -0
  52. package/dist/esm/direct/downloads-plan.js +117 -0
  53. package/dist/esm/direct/downloads-plan.js.map +1 -0
  54. package/dist/esm/direct/engine.js +385 -0
  55. package/dist/esm/direct/engine.js.map +1 -0
  56. package/dist/esm/direct/formats.js +40 -0
  57. package/dist/esm/direct/formats.js.map +1 -0
  58. package/dist/esm/direct/hpke.js +262 -0
  59. package/dist/esm/direct/hpke.js.map +1 -0
  60. package/dist/esm/direct/journal.js +75 -0
  61. package/dist/esm/direct/journal.js.map +1 -0
  62. package/dist/esm/direct/large.js +816 -0
  63. package/dist/esm/direct/large.js.map +1 -0
  64. package/dist/esm/direct/reads.js +1275 -0
  65. package/dist/esm/direct/reads.js.map +1 -0
  66. package/dist/esm/direct/sha256.js +118 -0
  67. package/dist/esm/direct/sha256.js.map +1 -0
  68. package/dist/esm/direct/tws1.js +460 -0
  69. package/dist/esm/direct/tws1.js.map +1 -0
  70. package/dist/esm/direct/uploads.js +1660 -0
  71. package/dist/esm/direct/uploads.js.map +1 -0
  72. package/dist/esm/errors.js +64 -11
  73. package/dist/esm/errors.js.map +1 -1
  74. package/dist/esm/index.js +1 -0
  75. package/dist/esm/index.js.map +1 -1
  76. package/dist/esm/links.js +189 -0
  77. package/dist/esm/links.js.map +1 -0
  78. package/dist/esm/node/downloads.js +829 -0
  79. package/dist/esm/node/downloads.js.map +1 -0
  80. package/dist/esm/node/journal.js +824 -0
  81. package/dist/esm/node/journal.js.map +1 -0
  82. package/dist/esm/node/uploads.js +187 -0
  83. package/dist/esm/node/uploads.js.map +1 -0
  84. package/dist/esm/node.js +17 -14
  85. package/dist/esm/node.js.map +1 -1
  86. package/dist/esm/storage.js +214 -184
  87. package/dist/esm/storage.js.map +1 -1
  88. package/dist/esm/transfer.js +19 -349
  89. package/dist/esm/transfer.js.map +1 -1
  90. package/dist/types/client.d.ts +45 -4
  91. package/dist/types/client.d.ts.map +1 -1
  92. package/dist/types/direct/bytes.d.ts +11 -0
  93. package/dist/types/direct/bytes.d.ts.map +1 -0
  94. package/dist/types/direct/common.d.ts +47 -0
  95. package/dist/types/direct/common.d.ts.map +1 -0
  96. package/dist/types/direct/downloads-plan.d.ts +75 -0
  97. package/dist/types/direct/downloads-plan.d.ts.map +1 -0
  98. package/dist/types/direct/engine.d.ts +191 -0
  99. package/dist/types/direct/engine.d.ts.map +1 -0
  100. package/dist/types/direct/formats.d.ts +9 -0
  101. package/dist/types/direct/formats.d.ts.map +1 -0
  102. package/dist/types/direct/hpke.d.ts +55 -0
  103. package/dist/types/direct/hpke.d.ts.map +1 -0
  104. package/dist/types/direct/journal.d.ts +55 -0
  105. package/dist/types/direct/journal.d.ts.map +1 -0
  106. package/dist/types/direct/large.d.ts +170 -0
  107. package/dist/types/direct/large.d.ts.map +1 -0
  108. package/dist/types/direct/reads.d.ts +306 -0
  109. package/dist/types/direct/reads.d.ts.map +1 -0
  110. package/dist/types/direct/sha256.d.ts +16 -0
  111. package/dist/types/direct/sha256.d.ts.map +1 -0
  112. package/dist/types/direct/tws1.d.ts +157 -0
  113. package/dist/types/direct/tws1.d.ts.map +1 -0
  114. package/dist/types/direct/uploads.d.ts +461 -0
  115. package/dist/types/direct/uploads.d.ts.map +1 -0
  116. package/dist/types/errors.d.ts +47 -8
  117. package/dist/types/errors.d.ts.map +1 -1
  118. package/dist/types/index.d.ts +1 -0
  119. package/dist/types/index.d.ts.map +1 -1
  120. package/dist/types/links.d.ts +86 -0
  121. package/dist/types/links.d.ts.map +1 -0
  122. package/dist/types/node/downloads.d.ts +73 -0
  123. package/dist/types/node/downloads.d.ts.map +1 -0
  124. package/dist/types/node/journal.d.ts +162 -0
  125. package/dist/types/node/journal.d.ts.map +1 -0
  126. package/dist/types/node/uploads.d.ts +86 -0
  127. package/dist/types/node/uploads.d.ts.map +1 -0
  128. package/dist/types/node.d.ts +4 -2
  129. package/dist/types/node.d.ts.map +1 -1
  130. package/dist/types/storage.d.ts +107 -64
  131. package/dist/types/storage.d.ts.map +1 -1
  132. package/dist/types/transfer.d.ts +14 -23
  133. package/dist/types/transfer.d.ts.map +1 -1
  134. package/package.json +2 -2
@@ -0,0 +1,1706 @@
1
+ "use strict";
2
+ // Copyright 2026 TEEPIN Project
3
+ // Licensed under the Apache License, Version 2.0
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || function (mod) {
21
+ if (mod && mod.__esModule) return mod;
22
+ var result = {};
23
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
24
+ __setModuleDefault(result, mod);
25
+ return result;
26
+ };
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.normalizeIfMatch = exports.DirectUploads = exports.RunCloser = exports.RunStore = exports.PackStore = exports.Pack = exports.Item = exports.MemoryPins = exports.webDigests = exports.emptyPutFinal = exports.stopsRun = exports.describe = exports.CLEANUP_MS = exports.STATE_WARN_BYTES = exports.STATE_WARN_ENV = exports.RUN_EXPIRY_MS = exports.uuid7 = exports.toBase64 = exports.Slots = exports.Progress = exports.objectFrom = exports.linked = exports.joinCapped = exports.Deferred = exports.abortable = void 0;
29
+ const errors_js_1 = require("../errors.js");
30
+ const md5_js_1 = require("../md5.js");
31
+ const bytes_js_1 = require("./bytes.js");
32
+ const engine_js_1 = require("./engine.js");
33
+ const common_js_1 = require("./common.js");
34
+ const hpke = __importStar(require("./hpke.js"));
35
+ const journal_js_1 = require("./journal.js");
36
+ const large_js_1 = require("./large.js");
37
+ const sha256_js_1 = require("./sha256.js");
38
+ const tws1 = __importStar(require("./tws1.js"));
39
+ var common_js_2 = require("./common.js");
40
+ Object.defineProperty(exports, "abortable", { enumerable: true, get: function () { return common_js_2.abortable; } });
41
+ Object.defineProperty(exports, "Deferred", { enumerable: true, get: function () { return common_js_2.Deferred; } });
42
+ Object.defineProperty(exports, "joinCapped", { enumerable: true, get: function () { return common_js_2.joinCapped; } });
43
+ Object.defineProperty(exports, "linked", { enumerable: true, get: function () { return common_js_2.linked; } });
44
+ Object.defineProperty(exports, "objectFrom", { enumerable: true, get: function () { return common_js_2.objectFrom; } });
45
+ Object.defineProperty(exports, "Progress", { enumerable: true, get: function () { return common_js_2.Progress; } });
46
+ Object.defineProperty(exports, "Slots", { enumerable: true, get: function () { return common_js_2.Slots; } });
47
+ Object.defineProperty(exports, "toBase64", { enumerable: true, get: function () { return common_js_2.toBase64; } });
48
+ Object.defineProperty(exports, "uuid7", { enumerable: true, get: function () { return common_js_2.uuid7; } });
49
+ /** An unfinished run is shown as expired after this long untouched (7 days). */
50
+ exports.RUN_EXPIRY_MS = 7 * 86400000;
51
+ /** The state directory's size that earns a warning (section 17.12), unless TEEPIN_STATE_WARN_BYTES says. */
52
+ exports.STATE_WARN_ENV = 'TEEPIN_STATE_WARN_BYTES';
53
+ exports.STATE_WARN_BYTES = 2 ** 30;
54
+ /** How long a stopping step waits for its tasks, and a cleanup call may take, in ms (section 22.13's cap). */
55
+ exports.CLEANUP_MS = 5000;
56
+ /** The longest a timer can be set for (setTimeout's limit); a longer one would fire at once. */
57
+ const MAX_TIMER_MS = 2 ** 31 - 1;
58
+ const str = (v) => (v === undefined || v === null ? '' : String(v));
59
+ const int = (v) => Math.trunc(Number(v || 0)) || 0;
60
+ const bucketPath = (bucket) => `/v1/storage/buckets/${encodeURIComponent(bucket)}`;
61
+ const unquote = (etag) => etag.trim().replace(/^W\//, '').replace(/^"|"$/g, '');
62
+ const hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
63
+ /** What an error says, for a journal: its code (or name) and message. */
64
+ function describe(err) {
65
+ if (err === undefined || err === null)
66
+ return '';
67
+ const code = (err instanceof errors_js_1.APIError && err.code) || (err instanceof Error ? err.name : 'Error');
68
+ return `${code}: ${err instanceof Error ? err.message : String(err)}`;
69
+ }
70
+ exports.describe = describe;
71
+ /** Errors that stop a whole run, with its files kept for resumeUploads. */
72
+ function stopsRun(err) {
73
+ return err instanceof errors_js_1.UnauthorizedError || err instanceof errors_js_1.ForbiddenError || err instanceof errors_js_1.PaymentRequiredError;
74
+ }
75
+ exports.stopsRun = stopsRun;
76
+ /**
77
+ * Whether an empty file's failed put is final. A passing failure (the network, a busy server) or one that stops the run
78
+ * (a revoked key, no credit) leaves it queued in the journal for resumeUploads, as a pack's files are left.
79
+ */
80
+ function emptyPutFinal(err) {
81
+ return !((0, engine_js_1.isPassing)(err) || stopsRun(err));
82
+ }
83
+ exports.emptyPutFinal = emptyPutFinal;
84
+ /** An object as a journal keeps it (JSON), and back. */
85
+ function objectJSON(o) {
86
+ return { ...o, createdAt: o.createdAt.toISOString(), updatedAt: o.updatedAt.toISOString(), uploadedAt: o.uploadedAt?.toISOString() };
87
+ }
88
+ function reviveObject(v) {
89
+ if (!v || typeof v !== 'object')
90
+ return undefined;
91
+ const d = v;
92
+ const date = (x) => new Date(str(x) || NaN);
93
+ return {
94
+ id: str(d.id),
95
+ key: str(d.key),
96
+ size: Number(d.size ?? 0),
97
+ contentType: d.contentType || undefined,
98
+ metadata: d.metadata,
99
+ checksumSha256: d.checksumSha256 || undefined,
100
+ etag: str(d.etag),
101
+ partCount: Number(d.partCount ?? 0),
102
+ status: str(d.status),
103
+ backend: str(d.backend),
104
+ createdAt: date(d.createdAt),
105
+ updatedAt: date(d.updatedAt),
106
+ uploadedAt: d.uploadedAt ? date(d.uploadedAt) : undefined,
107
+ };
108
+ }
109
+ /** Runs fn for every item, at most n at once; the first failure stops the rest. An abort throws its reason. */
110
+ async function pool(n, items, signal, fn) {
111
+ let next = 0;
112
+ let failure;
113
+ const worker = async () => {
114
+ while (!failure && next < items.length && !signal?.aborted) {
115
+ const item = items[next++];
116
+ try {
117
+ await fn(item);
118
+ }
119
+ catch (err) {
120
+ failure ??= { err };
121
+ }
122
+ }
123
+ };
124
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(n, items.length)) }, worker));
125
+ if (signal?.aborted)
126
+ throw signal.reason;
127
+ if (failure)
128
+ throw failure.err;
129
+ }
130
+ exports.webDigests = {
131
+ md5: md5_js_1.md5Hex,
132
+ sha256: async (data) => hex(new Uint8Array(await crypto.subtle.digest('SHA-256', data))),
133
+ incremental: () => {
134
+ const h = new sha256_js_1.Sha256();
135
+ return { update: (d) => void h.update(d), hex: () => h.hexDigest() };
136
+ },
137
+ };
138
+ /** Pins kept for the life of this client (browsers; the node entry keeps them in the state directory). */
139
+ class MemoryPins {
140
+ pins = new Map();
141
+ async check(accountId, version, publicKey) {
142
+ const id = JSON.stringify([accountId, version]);
143
+ const encoded = (0, common_js_1.toBase64)(publicKey);
144
+ const have = this.pins.get(id);
145
+ if (have !== undefined)
146
+ return have === encoded;
147
+ this.pins.set(id, encoded);
148
+ return true;
149
+ }
150
+ }
151
+ exports.MemoryPins = MemoryPins;
152
+ // --- items and packs ----------------------------------------------------------------------------------------------
153
+ /** One object on its way into a pack. */
154
+ class Item {
155
+ key;
156
+ size;
157
+ sha256;
158
+ read;
159
+ ifNoneMatch;
160
+ path;
161
+ fileId;
162
+ objectId = '';
163
+ result;
164
+ error;
165
+ /** false when the error leaves the item journaled for resumeUploads. */
166
+ final = true;
167
+ /** putObject's: set when a pack takes the item, resolved when its result is in, and to wake its waiting caller. */
168
+ picked = false;
169
+ done;
170
+ wake;
171
+ /** The pack and object id its file row last journaled. */
172
+ journaled = '';
173
+ contentType;
174
+ metadata;
175
+ ifMatch;
176
+ constructor(key, size, sha256, read, contentType = '', metadata, ifNoneMatch = false, ifMatch = '', path, fileId) {
177
+ this.key = key;
178
+ this.size = size;
179
+ this.sha256 = sha256;
180
+ this.read = read;
181
+ this.ifNoneMatch = ifNoneMatch;
182
+ this.path = path;
183
+ this.fileId = fileId;
184
+ this.contentType = contentType || '';
185
+ this.metadata = { ...(metadata ?? {}) };
186
+ this.ifMatch = ifMatch || '';
187
+ }
188
+ reserveJSON() {
189
+ const o = { key: this.key, size: this.size, sha256: this.sha256 };
190
+ if (this.contentType)
191
+ o.content_type = this.contentType;
192
+ if (Object.keys(this.metadata).length)
193
+ o.metadata = this.metadata;
194
+ if (this.ifNoneMatch)
195
+ o.if_none_match = true;
196
+ if (this.ifMatch)
197
+ o.if_match = this.ifMatch;
198
+ return o;
199
+ }
200
+ }
201
+ exports.Item = Item;
202
+ /** One pack's state, kept in its store (a run's journal, or memory). */
203
+ class Pack {
204
+ bucket;
205
+ items;
206
+ store;
207
+ id;
208
+ state = 'new';
209
+ idem = (0, common_js_1.uuid7)();
210
+ reservation = {};
211
+ signBody = {};
212
+ signed = {};
213
+ size = 0;
214
+ md5 = '';
215
+ sha256 = '';
216
+ fileName = '';
217
+ data;
218
+ attempts = 0;
219
+ rebuilds = 0;
220
+ verifies = 0;
221
+ resigns = 0;
222
+ waited = 0;
223
+ throttled = false;
224
+ lastError = '';
225
+ constructor(bucket, items, store, id) {
226
+ this.bucket = bucket;
227
+ this.items = items;
228
+ this.store = store;
229
+ this.id = id ?? (0, common_js_1.uuid7)();
230
+ }
231
+ get serverId() {
232
+ return str(this.reservation.pack);
233
+ }
234
+ }
235
+ exports.Pack = Pack;
236
+ /** The pack can't go on: its items fail with err (final: false keeps them journaled for resumeUploads). */
237
+ class Stop extends Error {
238
+ err;
239
+ final;
240
+ constructor(err, final = true) {
241
+ super(err instanceof Error ? err.message : String(err));
242
+ this.err = err;
243
+ this.final = final;
244
+ }
245
+ }
246
+ /** A pack's state and bytes in memory: putObject's packs, and the packs of a run journaled in memory. */
247
+ class PackStore {
248
+ async savePack(p) {
249
+ void p;
250
+ }
251
+ async putBytes(p, data) {
252
+ p.data = data;
253
+ }
254
+ async getBytes(p) {
255
+ return p.data;
256
+ }
257
+ async dropBytes(p) {
258
+ p.data = undefined;
259
+ }
260
+ async itemsDone(done) {
261
+ for (const [it, obj, err, final] of done) {
262
+ it.result = obj;
263
+ it.error = err;
264
+ it.final = final;
265
+ }
266
+ }
267
+ }
268
+ exports.PackStore = PackStore;
269
+ /** A run's packs in its journal, and their bytes in its directory (or in memory, for a run journaled in memory). */
270
+ class RunStore extends PackStore {
271
+ journal;
272
+ place;
273
+ constructor(journal, place) {
274
+ super();
275
+ this.journal = journal;
276
+ this.place = place;
277
+ }
278
+ async savePack(p) {
279
+ const entries = [
280
+ {
281
+ table: 'packs',
282
+ id: p.id,
283
+ row: {
284
+ _id: p.id,
285
+ idempotency_key: p.idem,
286
+ state: p.state,
287
+ reservation: p.reservation,
288
+ file_name: p.fileName,
289
+ size: p.size,
290
+ md5: p.md5,
291
+ sha256: p.sha256,
292
+ sign_body: p.signBody,
293
+ signed: p.signed,
294
+ attempts: p.attempts,
295
+ last_error: p.lastError,
296
+ updated: Date.now(),
297
+ },
298
+ },
299
+ ];
300
+ for (const it of p.items) {
301
+ const mark = `${p.id}|${it.objectId}`;
302
+ if (it.fileId === undefined || it.result || it.error !== undefined || it.journaled === mark)
303
+ continue;
304
+ const row = this.journal.get('files', it.fileId);
305
+ if (!row)
306
+ continue;
307
+ entries.push({ table: 'files', id: it.fileId, row: { ...row, pack: p.id, object_id: it.objectId || null, state: 'packed' } });
308
+ }
309
+ entries.push(...this.touch());
310
+ await this.journal.write(entries);
311
+ for (const it of p.items)
312
+ if (it.fileId !== undefined && !it.result && it.error === undefined)
313
+ it.journaled = `${p.id}|${it.objectId}`;
314
+ }
315
+ /** The run row with its updated time now. */
316
+ touch() {
317
+ const run = this.journal.get('run', 'run');
318
+ return run ? [{ table: 'run', id: 'run', row: { ...run, updated: Date.now() } }] : [];
319
+ }
320
+ async putBytes(p, data) {
321
+ if (!this.place) {
322
+ p.data = data;
323
+ return;
324
+ }
325
+ p.fileName = `${p.id}.pack`;
326
+ await this.place.writePack(p.fileName, data);
327
+ }
328
+ async getBytes(p) {
329
+ if (!this.place)
330
+ return p.data;
331
+ if (!p.fileName)
332
+ return undefined;
333
+ return this.place.readPack(p.fileName);
334
+ }
335
+ async dropBytes(p) {
336
+ p.data = undefined;
337
+ if (this.place && p.fileName) {
338
+ await this.place.removePack(p.fileName).catch(() => undefined);
339
+ p.fileName = '';
340
+ }
341
+ }
342
+ async itemsDone(done) {
343
+ await super.itemsDone(done);
344
+ const entries = [];
345
+ for (const [it, obj, err, final] of done) {
346
+ if (it.fileId === undefined)
347
+ continue;
348
+ const row = this.journal.get('files', it.fileId);
349
+ if (!row)
350
+ continue;
351
+ if (obj)
352
+ entries.push({ table: 'files', id: it.fileId, row: { ...row, state: 'saved', object_id: obj.id, result: objectJSON(obj), error: '' } });
353
+ else if (final)
354
+ entries.push({ table: 'files', id: it.fileId, row: { ...row, state: 'failed', error: describe(err) } });
355
+ else
356
+ entries.push({ table: 'files', id: it.fileId, row: { ...row, error: describe(err) } });
357
+ }
358
+ if (entries.length)
359
+ await this.journal.write(entries);
360
+ }
361
+ }
362
+ exports.RunStore = RunStore;
363
+ /**
364
+ * Who releases an interrupted run: whichever ends last, the run's own step (once it has stopped) or its last pack task.
365
+ * A run that isn't interrupted is finished by its own step as usual, and this never fires.
366
+ */
367
+ class RunCloser {
368
+ live = 0;
369
+ stopped = false;
370
+ fired = false;
371
+ started() {
372
+ this.live++;
373
+ }
374
+ /** A task ended (taskDone) or the run's step stopped: true for the one call that should release the run. */
375
+ ended(taskDone) {
376
+ if (taskDone)
377
+ this.live--;
378
+ else
379
+ this.stopped = true;
380
+ if (this.stopped && this.live === 0 && !this.fired) {
381
+ this.fired = true;
382
+ return true;
383
+ }
384
+ return false;
385
+ }
386
+ }
387
+ exports.RunCloser = RunCloser;
388
+ /**
389
+ * Direct uploads for one storage service: the account key, putObject's shared packs, upload runs, resume and cleanup.
390
+ * The methods tests replace (as the Python tests patch _direct.py) are newDataKey, encryptObject, wrapKey, putSigned,
391
+ * expiresSoon, dispatch, send and hashParts.
392
+ */
393
+ class DirectUploads {
394
+ client;
395
+ limit;
396
+ budget = new engine_js_1.RetryBudget();
397
+ parts;
398
+ /** The sizes it works with (tests shrink them). */
399
+ sizes = {
400
+ ...engine_js_1.PART_RULES,
401
+ packObjectLimit: engine_js_1.PACK_OBJECT_LIMIT,
402
+ packTarget: engine_js_1.PACK_TARGET_BYTES,
403
+ packMinTarget: engine_js_1.PACK_MIN_TARGET,
404
+ packMaxObjects: engine_js_1.PACK_MAX_OBJECTS,
405
+ streamConcurrency: engine_js_1.STREAM_CONCURRENCY,
406
+ };
407
+ /**
408
+ * The measured upload speed to the storage network: shrinks packs and new uploads' parts on a slow link (section
409
+ * 22.7). Tests set it with speed.set.
410
+ */
411
+ speed = new engine_js_1.SpeedMeter();
412
+ /**
413
+ * A storage PUT's own timer: at least minMs, and its bytes at bytesPerSecond (30 s and 64 KiB/s); on a link measured
414
+ * slower, twice its time at that speed (section 22.7). Tests shorten it.
415
+ */
416
+ putTimeout = { minMs: 30000, bytesPerSecond: 65536 };
417
+ /** How long a stopping step waits for its tasks, and a cleanup call may take (ms). */
418
+ cleanupMs = exports.CLEANUP_MS;
419
+ digests = exports.webDigests;
420
+ pins = new MemoryPins();
421
+ /** Runs on disk: the node entry's (undefined in browsers). */
422
+ host;
423
+ /** Where warnings go: a run removed by cleanUploads, a large state directory. */
424
+ warn = (m) => console.warn(m);
425
+ accountId;
426
+ queue = [];
427
+ /** Wake-ups of putObject calls waiting for a slot: resolved when one comes free. */
428
+ slotWaiters = [];
429
+ memory = new PackStore();
430
+ constructor(client) {
431
+ this.client = client;
432
+ this.limit = new engine_js_1.AdaptiveLimit(engine_js_1.START_CONCURRENCY, client.uploadConcurrency);
433
+ this.parts = new large_js_1.Parts(this);
434
+ }
435
+ // --- what tests replace -----------------------------------------------------
436
+ newDataKey() {
437
+ return tws1.newDataKey();
438
+ }
439
+ encryptObject(dek, plain) {
440
+ return tws1.encrypt(dek, plain);
441
+ }
442
+ wrapKey(publicKey, account, objectId, dek) {
443
+ return hpke.wrapKey(publicKey, account, objectId, dek);
444
+ }
445
+ /** Whether a signature is too close to its expiry to use. */
446
+ expiresSoon(signed) {
447
+ const exp = Date.parse(str(signed.expires_at));
448
+ return Number.isNaN(exp) || exp - Date.now() < engine_js_1.SIGN_MARGIN_MS;
449
+ }
450
+ /**
451
+ * Sends one signed PUT to the storage network, with exactly the signed headers and never the API key; returns its
452
+ * ETag, unquoted ("" when the answer has none, as a browser sees it: the storage network doesn't expose it).
453
+ */
454
+ async putSigned(signed, data, signal) {
455
+ const bps = this.speed.bytesPerSecond;
456
+ const t = this.putTimeout;
457
+ const ms = Math.max(t.minMs, (data.length / t.bytesPerSecond) * 1000, bps ? ((2 * data.length) / bps) * 1000 : 0);
458
+ const { ctl, unlink } = (0, common_js_1.linked)(signal);
459
+ const timer = setTimeout(() => ctl.abort(new PutTimedOut(ms)), ms);
460
+ try {
461
+ const headers = {};
462
+ for (const [k, v] of Object.entries(signed.headers ?? {}))
463
+ headers[k] = str(v);
464
+ const res = await this.client.sendSigned({ method: str(signed.method) || 'PUT', url: str(signed.url), headers, body: data, signal: ctl.signal });
465
+ await res.arrayBuffer().catch(() => undefined);
466
+ return unquote(res.headers.get('etag') ?? '');
467
+ }
468
+ catch (err) {
469
+ if (signal?.aborted)
470
+ throw signal.reason;
471
+ throw err;
472
+ }
473
+ finally {
474
+ clearTimeout(timer);
475
+ unlink();
476
+ }
477
+ }
478
+ /**
479
+ * putSigned, measured (section 22.7): the PUT every pack and part goes through. One that succeeds is a sample; one its
480
+ * own timer ended with no answer is an upper bound (a slow link, measured from a cold start too). Any other failure (an
481
+ * error status, a refused connection, the caller's abort) is never measured.
482
+ */
483
+ async putMeasured(signed, data, signal) {
484
+ const start = performance.now();
485
+ let etag;
486
+ try {
487
+ etag = await this.putSigned(signed, data, signal);
488
+ }
489
+ catch (err) {
490
+ if (err instanceof PutTimedOut && !signal?.aborted)
491
+ this.speed.observeTimeout(data.length, performance.now() - start);
492
+ throw err;
493
+ }
494
+ this.speed.observe(data.length, performance.now() - start);
495
+ return etag;
496
+ }
497
+ /**
498
+ * The pack/parts choice for every way in (putObject, upload, uploadFiles, copyObject): an object over this goes in
499
+ * parts. At the speed measured so far (section 22.7).
500
+ */
501
+ packObjectLimit() {
502
+ return (0, engine_js_1.packObjectLimit)(this.speed.requestBytes(), this.sizes.packObjectLimit, this.sizes.minBytes);
503
+ }
504
+ /** The next pack's target size, at the speed measured so far. */
505
+ packTarget() {
506
+ return (0, engine_js_1.packTarget)(this.speed.requestBytes(), this.sizes.packTarget, this.sizes.packMinTarget);
507
+ }
508
+ // --- shared plumbing ----------------------------------------------------------
509
+ /** Where runs keep their state, when it's on disk. */
510
+ get runHost() {
511
+ if (!this.host)
512
+ throw new errors_js_1.TeepinError('upload runs on disk need @teepin/sdk/node');
513
+ return this.host;
514
+ }
515
+ /**
516
+ * A control-plane call (each is idempotent, so retried by the client as safe), with rounds of waiting while the
517
+ * control plane is busy or failing, up to the upload's wait (p's; with no p, none). Returns the status and the answer.
518
+ */
519
+ async control(method, path, body, p, signal, timeout) {
520
+ for (let round = 0;; round++) {
521
+ try {
522
+ const { response } = await this.client.send({ method, path, json: body ?? undefined, mode: 'safe', signal, timeout });
523
+ const text = response.status === 204 ? '' : await response.text();
524
+ let data = {};
525
+ try {
526
+ data = text ? JSON.parse(text) : {};
527
+ }
528
+ catch {
529
+ throw new errors_js_1.TeepinError(`${method} ${path}: the answer isn't JSON`);
530
+ }
531
+ return { status: response.status, data: data };
532
+ }
533
+ catch (err) {
534
+ if (signal?.aborted)
535
+ throw signal.reason;
536
+ if (err instanceof errors_js_1.APIError) {
537
+ // 501: direct storage is off. A 404 with no code of ours: a deployment from before the routes existed (a missing
538
+ // bucket or pack always names itself).
539
+ if (err.code === 'direct_upload_unsupported' || err.statusCode === 501 || (err.statusCode === 404 && (err.code === '' || err.code === 'not_found'))) {
540
+ throw unsupported(err);
541
+ }
542
+ }
543
+ if (!(0, engine_js_1.isPassing)(err) || !p)
544
+ throw err;
545
+ await this.wait(p, round, err, signal);
546
+ }
547
+ }
548
+ }
549
+ /** One round of waiting for a service to come back; StorageUnavailableError once p has waited as long as it may. */
550
+ async wait(p, round, err, signal) {
551
+ const delay = (0, engine_js_1.waitDelayMs)(round, err instanceof errors_js_1.APIError ? err.retryAfterMs : undefined);
552
+ if ((0, engine_js_1.isThrottle)(err))
553
+ p.throttled = true;
554
+ if (p.waited + delay > this.client.uploadWait) {
555
+ throw new errors_js_1.StorageUnavailableError(`storage didn't come back within ${Math.round(this.client.uploadWait / 1000)} s: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
556
+ }
557
+ p.waited += delay;
558
+ await this.client.sleep(delay, signal);
559
+ }
560
+ /** The account key from an answer, checked against the suite, this client's account and the pin. */
561
+ async checkKey(raw) {
562
+ const ak = (raw ?? {});
563
+ if (ak.suite !== hpke.SUITE)
564
+ throw new errors_js_1.DataIntegrityError(`unsupported key suite ${JSON.stringify(ak.suite)}`, 'account_key');
565
+ const account = str(ak.account_id);
566
+ if (!account)
567
+ throw new errors_js_1.DataIntegrityError("the API didn't name the account its key belongs to", 'account_key');
568
+ const publicKey = (0, bytes_js_1.fromBase64)(str(ak.public_key));
569
+ const version = int(ak.version);
570
+ if (this.accountId === undefined)
571
+ this.accountId = account;
572
+ else if (this.accountId !== account) {
573
+ throw new errors_js_1.DataIntegrityError(`the API named account ${account}, not ${this.accountId}, in one client`, 'account_key');
574
+ }
575
+ if (!(await this.pins.check(account, version, publicKey))) {
576
+ throw new errors_js_1.DataIntegrityError(`account ${account}'s key version ${version} changed; refusing to encrypt to it`, 'account_key');
577
+ }
578
+ return { account: tws1.objectIdBytes(account), publicKey, version };
579
+ }
580
+ /** Items' results: journaled together, then their callers woken. */
581
+ async done(store, done) {
582
+ if (!done.length)
583
+ return;
584
+ await store.itemsDone(done);
585
+ for (const [it] of done)
586
+ it.done?.resolve();
587
+ }
588
+ // --- a pack's life --------------------------------------------------------------
589
+ /**
590
+ * Takes a pack to committed, delivering every item's result. Throws only an error that stops a whole run (auth,
591
+ * permission, credit), after recording it on the pack's items, or the abort's reason, recording nothing.
592
+ */
593
+ async drive(p, signal) {
594
+ try {
595
+ while (p.state !== 'committed' && p.state !== 'failed') {
596
+ signal?.throwIfAborted();
597
+ switch (p.state) {
598
+ case 'new':
599
+ await this.reserve(p, signal);
600
+ break;
601
+ case 'reserved':
602
+ await this.build(p);
603
+ break;
604
+ case 'built':
605
+ await this.sign(p, signal);
606
+ break;
607
+ case 'signed':
608
+ await this.send(p, signal);
609
+ break;
610
+ case 'conflict':
611
+ case 'bad':
612
+ await this.renew(p);
613
+ break;
614
+ default:
615
+ await this.commit(p, signal);
616
+ }
617
+ }
618
+ }
619
+ catch (err) {
620
+ // Interrupted: the store holds the state for resume, and the abort's reason wins over whatever was met.
621
+ if (signal?.aborted)
622
+ throw signal.reason;
623
+ if (err instanceof Stop)
624
+ return this.fail(p, err.err, err.final);
625
+ if (stopsRun(err)) {
626
+ await this.fail(p, err, false);
627
+ throw err;
628
+ }
629
+ if (err instanceof errors_js_1.StorageUnavailableError)
630
+ return this.fail(p, err, false);
631
+ if (err instanceof errors_js_1.TeepinError)
632
+ return this.fail(p, err, true);
633
+ throw err;
634
+ }
635
+ }
636
+ async fail(p, err, final) {
637
+ p.lastError = describe(err);
638
+ if (final) {
639
+ p.state = 'failed';
640
+ await p.store.dropBytes(p);
641
+ }
642
+ await p.store.savePack(p);
643
+ await this.done(p.store, p.items.filter((it) => !it.result && it.error === undefined).map((it) => [it, undefined, err, final]));
644
+ }
645
+ async reserve(p, signal) {
646
+ await p.store.savePack(p); // the idempotency key, before the call
647
+ const body = { idempotency_key: p.idem, objects: p.items.map((it) => it.reserveJSON()) };
648
+ let res;
649
+ try {
650
+ res = (await this.control(`POST`, `${bucketPath(p.bucket)}/packs`, body, p, signal)).data;
651
+ }
652
+ catch (err) {
653
+ if (err instanceof errors_js_1.APIError && err.code === 'reservation_abandoned') {
654
+ p.state = 'conflict';
655
+ await p.store.savePack(p);
656
+ return;
657
+ }
658
+ if (err instanceof errors_js_1.APIError && err.code === 'pack_committed') {
659
+ // Committed earlier, its answer lost: commit again for the stored result.
660
+ p.reservation = { pack: str(err.details.pack) };
661
+ p.state = 'committing';
662
+ await p.store.savePack(p);
663
+ return;
664
+ }
665
+ throw err;
666
+ }
667
+ await this.checkKey(res.account_key);
668
+ const ids = new Map((res.objects ?? []).map((o) => [str(o.key), str(o.object_id)]));
669
+ for (const it of p.items)
670
+ it.objectId = ids.get(it.key) ?? '';
671
+ p.reservation = res;
672
+ p.state = 'reserved';
673
+ await p.store.savePack(p);
674
+ }
675
+ async build(p) {
676
+ const key = await this.checkKey(p.reservation.account_key);
677
+ const built = await this.buildPack(p, key);
678
+ if ('changed' in built) {
679
+ await this.done(p.store, built.changed.map(([it, err]) => [it, undefined, err, true]));
680
+ // The reservation names files that changed: the rest go under a new one.
681
+ const gone = new Set(built.changed.map(([it]) => it));
682
+ p.items = p.items.filter((it) => !gone.has(it));
683
+ p.state = p.items.length ? 'conflict' : 'failed';
684
+ p.rebuilds--; // not the pack's fault
685
+ await p.store.savePack(p);
686
+ return;
687
+ }
688
+ await p.store.putBytes(p, built.data);
689
+ p.size = built.data.length;
690
+ p.md5 = this.digests.md5(built.data);
691
+ p.sha256 = await this.digests.sha256(built.data);
692
+ p.signBody = { size: p.size, md5: p.md5, sha256: p.sha256, objects: built.objects };
693
+ p.state = 'built';
694
+ await p.store.savePack(p);
695
+ }
696
+ /**
697
+ * Every item read, checked against its queued hash, encrypted with a fresh key and appended to the pack, then
698
+ * decrypt-checked ("saved" means readable, section 19.3) and its key wrapped; or the items that changed since they
699
+ * were queued.
700
+ */
701
+ async buildPack(p, key) {
702
+ const pack = new tws1.Pack();
703
+ const built = [];
704
+ const changed = [];
705
+ try {
706
+ for (const it of p.items) {
707
+ let plain;
708
+ try {
709
+ plain = await it.read();
710
+ }
711
+ catch (err) {
712
+ changed.push([it, new errors_js_1.FileChangedError(it.path ?? it.key, err instanceof Error ? err.message : String(err))]);
713
+ continue;
714
+ }
715
+ if (plain.length !== it.size || (await this.digests.sha256(plain)) !== it.sha256) {
716
+ changed.push([it, new errors_js_1.FileChangedError(it.path ?? it.key)]);
717
+ continue;
718
+ }
719
+ const dek = this.newDataKey();
720
+ const stored = await this.encryptObject(dek, plain);
721
+ const off = await pack.appendNeedle(tws1.objectIdBytes(it.objectId), stored);
722
+ built.push({ it, dek, off, n: stored.length });
723
+ }
724
+ if (changed.length)
725
+ return { changed };
726
+ const data = pack.bytes();
727
+ for (const { it, dek, off, n } of built) {
728
+ let ok;
729
+ try {
730
+ const stored = await tws1.readNeedle(data, off, tws1.objectIdBytes(it.objectId), n);
731
+ ok = (await this.digests.sha256(await tws1.decrypt(dek, stored))) === it.sha256;
732
+ }
733
+ catch (err) {
734
+ if (!(err instanceof errors_js_1.DataIntegrityError))
735
+ throw err;
736
+ ok = false;
737
+ }
738
+ if (!ok)
739
+ throw new Stop(new errors_js_1.DataIntegrityError(`${it.key} didn't decrypt back to its source`, 'build'));
740
+ }
741
+ const objects = [];
742
+ for (const { it, dek, off, n } of built) {
743
+ const wrapped = await this.wrapKey(key.publicKey, key.account, tws1.objectIdBytes(it.objectId), dek);
744
+ objects.push({ object_id: it.objectId, offset: off, stored_size: n, wrapped_key: (0, common_js_1.toBase64)(wrapped), key_version: key.version });
745
+ }
746
+ return { data, objects };
747
+ }
748
+ finally {
749
+ for (const { dek } of built)
750
+ dek.fill(0);
751
+ }
752
+ }
753
+ async sign(p, signal) {
754
+ let signed;
755
+ try {
756
+ signed = (await this.control(`POST`, `${bucketPath(p.bucket)}/packs/${p.serverId}/sign`, p.signBody, p, signal)).data;
757
+ }
758
+ catch (err) {
759
+ if (err instanceof errors_js_1.APIError && err.code === 'reservation_abandoned') {
760
+ p.state = 'conflict';
761
+ await p.store.savePack(p);
762
+ return;
763
+ }
764
+ if (err instanceof errors_js_1.APIError && ['manifest_mismatch', 'bad_wrapped_key', 'invalid_request', 'pack_not_found'].includes(err.code))
765
+ throw new Stop(err);
766
+ throw err;
767
+ }
768
+ p.signed = signed;
769
+ p.state = 'signed';
770
+ await p.store.savePack(p);
771
+ }
772
+ /** A pack's one PUT. Replaced in tests. */
773
+ async send(p, signal) {
774
+ if (this.expiresSoon(p.signed)) {
775
+ p.state = 'built'; // the same manifest signs again
776
+ await p.store.savePack(p);
777
+ return;
778
+ }
779
+ const data = await p.store.getBytes(p);
780
+ if (!data || (await this.digests.sha256(data)) !== p.sha256) {
781
+ p.state = 'bad'; // the pack file is lost or damaged: a new reservation, fresh keys
782
+ await p.store.savePack(p);
783
+ return;
784
+ }
785
+ p.state = 'uploading';
786
+ await p.store.savePack(p);
787
+ let err;
788
+ this.budget.onRequest();
789
+ try {
790
+ await this.putMeasured(p.signed, data, signal);
791
+ }
792
+ catch (e) {
793
+ if (signal?.aborted)
794
+ throw signal.reason;
795
+ if (!(e instanceof errors_js_1.TeepinError))
796
+ throw e;
797
+ err = e;
798
+ }
799
+ switch ((0, engine_js_1.storageOutcome)(err)) {
800
+ case 'sent':
801
+ p.state = 'sent';
802
+ break;
803
+ case 'unknown':
804
+ p.state = 'unknown';
805
+ break;
806
+ case 'resign':
807
+ if (++p.resigns > 3)
808
+ throw new Stop(err);
809
+ p.state = 'built';
810
+ break;
811
+ case 'bad':
812
+ p.state = 'bad';
813
+ break;
814
+ case 'unsent':
815
+ p.state = 'signed';
816
+ if (!this.budget.tryRetry())
817
+ await this.wait(p, p.attempts, err, signal);
818
+ p.attempts++;
819
+ break;
820
+ case 'busy':
821
+ p.state = 'signed';
822
+ await this.wait(p, p.attempts, err, signal);
823
+ p.attempts++;
824
+ break;
825
+ default:
826
+ throw new Stop(err);
827
+ }
828
+ await p.store.savePack(p);
829
+ }
830
+ async commit(p, signal) {
831
+ const unknown = p.state === 'unknown' || p.state === 'uploading';
832
+ if (p.state === 'sent') {
833
+ p.state = 'committing';
834
+ await p.store.savePack(p);
835
+ }
836
+ let status;
837
+ let res;
838
+ try {
839
+ ({ status, data: res } = await this.control(`POST`, `${bucketPath(p.bucket)}/packs/${p.serverId}/commit`, undefined, p, signal, 70000));
840
+ }
841
+ catch (err) {
842
+ if (!(err instanceof errors_js_1.APIError))
843
+ throw err;
844
+ if (err.code === 'reservation_abandoned')
845
+ p.state = 'conflict';
846
+ else if (err.code === 'pack_mismatch')
847
+ p.state = 'bad';
848
+ else if (err.code === 'pack_not_signed')
849
+ p.state = 'built';
850
+ else if (err.code === 'bucket_not_found')
851
+ throw new Stop(err);
852
+ else
853
+ throw err;
854
+ await p.store.savePack(p);
855
+ return;
856
+ }
857
+ if (status === 202) {
858
+ p.verifies++;
859
+ if (unknown || p.verifies > engine_js_1.COMMIT_VERIFY_TRIES) {
860
+ p.state = 'signed'; // not there: send the same bytes again
861
+ p.verifies = 0;
862
+ }
863
+ else {
864
+ await this.client.sleep(1000, signal);
865
+ }
866
+ await p.store.savePack(p);
867
+ return;
868
+ }
869
+ await this.deliver(p, res);
870
+ p.state = 'committed';
871
+ await p.store.dropBytes(p);
872
+ await p.store.savePack(p);
873
+ }
874
+ /** A new reservation and fresh keys for the pack's items (section 17.3). */
875
+ async renew(p) {
876
+ p.rebuilds++;
877
+ if (p.rebuilds > engine_js_1.MAX_REBUILDS)
878
+ throw new Stop(new errors_js_1.TeepinError(`the pack was rebuilt ${engine_js_1.MAX_REBUILDS} times: ${p.lastError || p.state}`));
879
+ p.lastError = p.state;
880
+ await p.store.dropBytes(p);
881
+ p.idem = (0, common_js_1.uuid7)();
882
+ p.reservation = {};
883
+ p.signBody = {};
884
+ p.signed = {};
885
+ p.verifies = 0;
886
+ p.resigns = 0;
887
+ for (const it of p.items)
888
+ it.objectId = '';
889
+ p.state = 'new';
890
+ await p.store.savePack(p);
891
+ }
892
+ async deliver(p, res) {
893
+ const when = new Date(str(res.committed_at) || NaN);
894
+ const backend = str(res.backend);
895
+ const objects = res.objects ?? [];
896
+ const byId = new Map(objects.map((o) => [str(o.object_id), o]));
897
+ const byKey = new Map(objects.map((o) => [str(o.key), o]));
898
+ const done = [];
899
+ for (const it of p.items) {
900
+ if (it.result || it.error !== undefined)
901
+ continue;
902
+ const r = byId.get(it.objectId) ?? byKey.get(it.key);
903
+ if (!r)
904
+ done.push([it, undefined, new errors_js_1.TeepinError(`the commit didn't report ${it.key}`), true]);
905
+ else if (r.result === 'saved') {
906
+ const obj = {
907
+ id: str(r.object_id) || it.objectId,
908
+ key: it.key,
909
+ size: it.size,
910
+ contentType: it.contentType || undefined,
911
+ metadata: it.metadata,
912
+ checksumSha256: it.sha256,
913
+ etag: it.sha256,
914
+ partCount: 0,
915
+ status: 'available',
916
+ backend,
917
+ createdAt: when,
918
+ updatedAt: when,
919
+ uploadedAt: when,
920
+ };
921
+ done.push([it, obj, undefined, true]);
922
+ }
923
+ else {
924
+ const code = str(r.error) || 'upload_failed';
925
+ done.push([it, undefined, (0, errors_js_1.apiError)({ statusCode: code === 'precondition_failed' ? 412 : 409, message: `${it.key}: ${code.replaceAll('_', ' ')}`, code }), true]);
926
+ }
927
+ }
928
+ await this.done(p.store, done);
929
+ }
930
+ // --- putObject: shared packs ----------------------------------------------------
931
+ /**
932
+ * Stores one object of up to 16 MiB in a pack shared with the other calls waiting now, and returns it once its pack
933
+ * commits. A waiting call dispatches the queue itself whenever a slot comes free (release wakes it), so a slot freed
934
+ * anywhere serves the queue. The wait for a slot is bounded by uploadWait: an item no pack took by then leaves the
935
+ * queue (so it certainly wasn't saved) with StorageUnavailableError; once a pack has it, the pack's own bounded waits
936
+ * decide (section 22.4). An abort while it waits takes it out of the queue; once a pack has it, the pack goes on (it
937
+ * is shared) and the call stops waiting for it.
938
+ */
939
+ async put(bucket, key, data, opts = {}, signal) {
940
+ signal?.throwIfAborted();
941
+ const sha = await this.digests.sha256(data);
942
+ const it = new Item(key, data.length, sha, async () => data, opts.contentType, opts.metadata, Boolean(opts.ifNoneMatch), normalizeIfMatch(opts.ifMatch));
943
+ it.done = new common_js_1.Deferred();
944
+ this.queue.push([bucket, it]);
945
+ this.limit.setPriority(true);
946
+ const wait = this.client.uploadWait;
947
+ const deadline = Date.now() + wait;
948
+ const leave = () => {
949
+ const queued = this.queue.some(([, i]) => i === it);
950
+ if (queued) {
951
+ this.queue = this.queue.filter(([, i]) => i !== it);
952
+ this.limit.setPriority(this.queue.length > 0);
953
+ }
954
+ return queued;
955
+ };
956
+ try {
957
+ while (!it.picked) {
958
+ // Registered before dispatching, so a slot freed meanwhile wakes this call.
959
+ const woken = new common_js_1.Deferred();
960
+ it.wake = woken;
961
+ this.slotWaiters.push(woken);
962
+ this.dispatch();
963
+ if (it.picked)
964
+ break;
965
+ const left = deadline - Date.now();
966
+ if (left <= 0)
967
+ break;
968
+ let timer;
969
+ try {
970
+ // A timer past 2^31 - 1 ms fires at once in Node and browsers, so a very long uploadWait waits in slices.
971
+ const slice = Math.min(left, MAX_TIMER_MS);
972
+ await (0, common_js_1.abortable)(Promise.race([woken.promise, new Promise((r) => (timer = setTimeout(r, slice)))]), signal);
973
+ }
974
+ finally {
975
+ clearTimeout(timer);
976
+ }
977
+ }
978
+ }
979
+ catch (err) {
980
+ leave();
981
+ throw err;
982
+ }
983
+ if (!it.picked && leave())
984
+ throw new errors_js_1.StorageUnavailableError(`no upload slot came free within ${Math.round(wait / 1000)} s; ${key} wasn't saved`);
985
+ await (0, common_js_1.abortable)(it.done.promise, signal);
986
+ if (it.error !== undefined)
987
+ throw it.error;
988
+ if (!it.result)
989
+ throw new errors_js_1.TeepinError(`${key}: no result`);
990
+ return it.result;
991
+ }
992
+ /** Forms a pack from what's waiting whenever a send slot is free. Replaced in tests. */
993
+ dispatch() {
994
+ while (this.queue.length && this.limit.tryAcquire()) {
995
+ const bucket = this.queue[0][0];
996
+ const waiting = this.queue.filter(([b]) => b === bucket).map(([, it]) => it);
997
+ const [batch] = (0, engine_js_1.nextPack)(waiting, (i) => i.size, (i) => i.key, this.packTarget(), this.sizes.packMaxObjects);
998
+ const chosen = new Set(batch);
999
+ this.queue = this.queue.filter(([, it]) => !chosen.has(it));
1000
+ this.limit.setPriority(this.queue.length > 0);
1001
+ for (const it of batch) {
1002
+ it.picked = true;
1003
+ it.wake?.resolve(); // its caller may be waiting for a slot: it has one now
1004
+ }
1005
+ void this.runShared(new Pack(bucket, batch, this.memory));
1006
+ }
1007
+ }
1008
+ async runShared(p) {
1009
+ let outcome = 'failed';
1010
+ try {
1011
+ await this.drive(p);
1012
+ outcome = p.throttled ? 'throttled' : p.state === 'committed' ? 'ok' : 'failed';
1013
+ }
1014
+ catch (err) {
1015
+ for (const it of p.items)
1016
+ if (!it.result && it.error === undefined)
1017
+ it.error = err; // every caller gets its answer
1018
+ }
1019
+ finally {
1020
+ for (const it of p.items)
1021
+ it.done?.resolve();
1022
+ this.release(outcome);
1023
+ }
1024
+ }
1025
+ /**
1026
+ * Every slot comes back through here, from putObject's packs and a run's alike, and putObject's waiting calls are
1027
+ * woken to dispatch: while they wait, a run's next pack waits too (AdaptiveLimit.priority).
1028
+ */
1029
+ release(outcome) {
1030
+ this.limit.release(outcome);
1031
+ const waiters = this.slotWaiters;
1032
+ this.slotWaiters = [];
1033
+ for (const w of waiters)
1034
+ w.resolve();
1035
+ }
1036
+ // --- empty objects, large objects and streams ------------------------------------
1037
+ /**
1038
+ * An empty object: the metadata-only route (section 23.5, A2), which carries no bytes (a pack holds none, and an
1039
+ * upload needs none). Retried by the client as a safe call; no rounds of waiting.
1040
+ */
1041
+ async putEmpty(bucket, key, opts = {}, signal) {
1042
+ const body = { key };
1043
+ if (opts.contentType)
1044
+ body.content_type = opts.contentType;
1045
+ if (opts.metadata && Object.keys(opts.metadata).length)
1046
+ body.metadata = opts.metadata;
1047
+ if (opts.ifNoneMatch)
1048
+ body.if_none_match = true;
1049
+ const ifMatch = normalizeIfMatch(opts.ifMatch);
1050
+ if (ifMatch)
1051
+ body.if_match = ifMatch;
1052
+ try {
1053
+ const { data } = await this.client.call({ method: 'POST', path: `${bucketPath(bucket)}/object/empty`, json: body, mode: 'safe', signal });
1054
+ return (0, common_js_1.objectFrom)(data);
1055
+ }
1056
+ catch (err) {
1057
+ if (signal?.aborted)
1058
+ throw signal.reason;
1059
+ if (err instanceof errors_js_1.APIError &&
1060
+ (err.code === 'direct_upload_unsupported' || err.statusCode === 501 || (err.statusCode === 404 && (err.code === '' || err.code === 'not_found')))) {
1061
+ throw unsupported(err);
1062
+ }
1063
+ if (err instanceof errors_js_1.PreconditionFailedError && opts.ifNoneMatch && (err.attempts ?? 1) > 1) {
1064
+ throw new errors_js_1.PreconditionFailedError({
1065
+ statusCode: 412,
1066
+ code: err.code,
1067
+ message: `${err.serverMessage}; if this upload was retried, an earlier attempt may have stored it`,
1068
+ requestId: err.requestId,
1069
+ });
1070
+ }
1071
+ throw err;
1072
+ }
1073
+ }
1074
+ /** An object over the pack limit from bytes, a Blob or any seekable source, in parts. Kept in memory: nothing could resume it after a crash. */
1075
+ async putLarge(bucket, key, source, opts = {}, signal) {
1076
+ const psize = this.partSize(source.size, opts.partSize ?? 0, key);
1077
+ let hashed;
1078
+ try {
1079
+ hashed = await this.hashParts(source, psize);
1080
+ }
1081
+ catch (err) {
1082
+ if (signal?.aborted)
1083
+ throw signal.reason;
1084
+ throw new errors_js_1.FileChangedError(key, err instanceof Error ? err.message : String(err));
1085
+ }
1086
+ const u = new large_js_1.Large(bucket, key, source.size, hashed.whole, (o, n) => source.readAt(o, n), {
1087
+ contentType: opts.contentType,
1088
+ metadata: opts.metadata,
1089
+ ifNoneMatch: opts.ifNoneMatch,
1090
+ ifMatch: normalizeIfMatch(opts.ifMatch),
1091
+ partSize: psize,
1092
+ partHashes: hashed.parts,
1093
+ });
1094
+ return this.parts.run(u, opts.onProgress ? new common_js_1.Progress(source.size, opts.onProgress) : undefined, signal);
1095
+ }
1096
+ /**
1097
+ * A stream over the pack limit whose size isn't known (section 22.1, E2 b), in parts held in memory (concurrency of
1098
+ * them, the sizes' streamConcurrency unless given). Nothing could resume it after a crash.
1099
+ */
1100
+ async putStream(bucket, key, source, opts = {}, signal) {
1101
+ const s = new large_js_1.StreamUpload(bucket, key, this.streamPartSize(opts.expectedSize ?? 0, opts.partSize ?? 0, key), {
1102
+ contentType: opts.contentType,
1103
+ metadata: opts.metadata,
1104
+ ifNoneMatch: opts.ifNoneMatch,
1105
+ ifMatch: normalizeIfMatch(opts.ifMatch),
1106
+ });
1107
+ const link = this.speed.link;
1108
+ // Its parts were shrunk for a slow link: running out of them is the connection's speed (section 22.7).
1109
+ if (link && s.partSize < (0, engine_js_1.streamPartSize)(opts.expectedSize ?? 0, opts.partSize ?? 0, this.sizes))
1110
+ s.slow = link;
1111
+ const n = opts.concurrency && opts.concurrency > 0 ? opts.concurrency : this.sizes.streamConcurrency;
1112
+ return this.parts.runStream(s, source, n, opts.onProgress ? new common_js_1.Progress(-1, opts.onProgress) : undefined, signal);
1113
+ }
1114
+ /**
1115
+ * A new large upload's part size for size bytes, by this engine's rules, at the speed measured so far: TooLargeError
1116
+ * with reason connection_speed when it can't be sent at it (section 22.7). name is for the error.
1117
+ */
1118
+ partSize(size, want = 0, name = '') {
1119
+ return (0, engine_js_1.directPartSize)(size, want, this.sizes, this.speed.link, name);
1120
+ }
1121
+ /** A new stream's part size (section 22.1, E2 b), at the speed measured so far, as partSize. */
1122
+ streamPartSize(expectedSize = 0, want = 0, name = '') {
1123
+ return (0, engine_js_1.streamPartSize)(expectedSize, want, this.sizes, this.speed.link, name);
1124
+ }
1125
+ /**
1126
+ * In one pass: the whole source's SHA-256 (declared at create) and each part's (checked as the part is sent, D1).
1127
+ * Replaced in tests.
1128
+ */
1129
+ async hashParts(source, partSize) {
1130
+ if (source.hashParts)
1131
+ return source.hashParts(partSize);
1132
+ const whole = this.digests.incremental();
1133
+ const parts = new Map();
1134
+ for (const p of tws1.parts(source.size, partSize)) {
1135
+ const data = await source.readAt(p.plainFirst, p.plainSize);
1136
+ if (data.length !== p.plainSize)
1137
+ throw new errors_js_1.TeepinError('the source changed size while it was read');
1138
+ whole.update(data);
1139
+ parts.set(p.number, await this.digests.sha256(data));
1140
+ }
1141
+ return { whole: whole.hex(), parts };
1142
+ }
1143
+ // --- upload runs ------------------------------------------------------------------
1144
+ /** A run's first step: journaled (on disk unless journal is false), and every file hashed. */
1145
+ async beginFiles(bucket, entries, opts, signal) {
1146
+ const host = this.runHost;
1147
+ const place = opts.journal === false ? undefined : await host.create();
1148
+ const j = place?.journal ?? new journal_js_1.MemoryJournal();
1149
+ const now = Date.now();
1150
+ const store = new RunStore(j, place);
1151
+ try {
1152
+ await j.put('run', 'run', {
1153
+ id: place?.id ?? (0, common_js_1.uuid7)(),
1154
+ kind: 'upload',
1155
+ bucket,
1156
+ created: now,
1157
+ updated: now,
1158
+ status: 'open',
1159
+ options: { if_none_match: Boolean(opts.ifNoneMatch) },
1160
+ });
1161
+ const hashed = new Array(entries.length);
1162
+ await pool(this.limit.ceiling, [...entries.keys()], signal, async (i) => {
1163
+ hashed[i] = await host.hashFile(entries[i][1]);
1164
+ });
1165
+ return await this.journalFiles(bucket, store, entries, hashed, opts);
1166
+ }
1167
+ catch (err) {
1168
+ await this.abandon(store, place); // nothing is under way
1169
+ throw signal?.aborted ? signal.reason : err;
1170
+ }
1171
+ }
1172
+ async journalFiles(bucket, store, entries, hashed, opts) {
1173
+ const j = store.journal;
1174
+ const host = this.runHost;
1175
+ const results = new Map();
1176
+ const items = [];
1177
+ const rows = [];
1178
+ entries.forEach(([key, path], i) => {
1179
+ const h = hashed[i];
1180
+ const id = j.nextId('files');
1181
+ if (h instanceof Error) {
1182
+ rows.push({ table: 'files', id, row: { _id: id, path, key, size: -1, mtime: '', sha256: '', state: 'failed', error: describe(h) } });
1183
+ results.set(id, { key, path, status: 'failed', error: h });
1184
+ return;
1185
+ }
1186
+ const ct = opts.contentType || host.contentType(path) || '';
1187
+ rows.push({
1188
+ table: 'files',
1189
+ id,
1190
+ row: {
1191
+ _id: id,
1192
+ path,
1193
+ key,
1194
+ size: h.size,
1195
+ mtime: h.mtime,
1196
+ sha256: h.sha256,
1197
+ content_type: ct,
1198
+ metadata: { ...(opts.metadata ?? {}) },
1199
+ if_none_match: Boolean(opts.ifNoneMatch),
1200
+ if_match: '',
1201
+ state: 'queued',
1202
+ },
1203
+ });
1204
+ items.push(this.fileItem(key, path, h.size, h.sha256, ct, opts.metadata, Boolean(opts.ifNoneMatch), id));
1205
+ });
1206
+ await j.write([...rows, ...store.touch()]);
1207
+ return { bucket, store, place: store.place, items, packs: [], results };
1208
+ }
1209
+ fileItem(key, path, size, sha, contentType, metadata, ifNoneMatch, fileId) {
1210
+ const host = this.runHost;
1211
+ return new Item(key, size, sha, () => host.readWhole(path, size), contentType, metadata, ifNoneMatch, '', path, fileId);
1212
+ }
1213
+ /**
1214
+ * A run's middle and last steps: its empty files put (the A2 route), then its queued items packed, its packs already
1215
+ * under way and its large files sent, every file's result recorded and the run finished. partSize is for files sent
1216
+ * in parts (0: the default).
1217
+ */
1218
+ async runState(state, onProgress, partSize = 0, signal) {
1219
+ try {
1220
+ const empties = state.items.filter((it) => it.size === 0);
1221
+ for (const it of empties) {
1222
+ try {
1223
+ const obj = await this.putEmpty(state.bucket, it.key, { contentType: it.contentType || undefined, metadata: it.metadata, ifNoneMatch: it.ifNoneMatch }, signal);
1224
+ await this.done(state.store, [[it, obj, undefined, true]]);
1225
+ }
1226
+ catch (err) {
1227
+ if (signal?.aborted || !(err instanceof errors_js_1.TeepinError))
1228
+ throw err;
1229
+ await this.done(state.store, [[it, undefined, err, emptyPutFinal(err)]]);
1230
+ }
1231
+ }
1232
+ }
1233
+ catch (err) {
1234
+ await this.abandon(state.store, state.place); // interrupted before any pack started
1235
+ throw signal?.aborted ? signal.reason : err;
1236
+ }
1237
+ // The empty files stay in state.items, so send reports them with the rest (it packs only sizes above 0).
1238
+ return this.sendRun(state, onProgress, partSize, signal);
1239
+ }
1240
+ async sendRun(state, onProgress, partSize, signal) {
1241
+ const { bucket, store, place, items } = state;
1242
+ const lim = this.packObjectLimit();
1243
+ // A file whose upload in parts a crash left open goes on in parts, whatever the speed now (section 22.7).
1244
+ const inParts = new Set(store.journal
1245
+ .rows('uploads')
1246
+ .filter((r) => r.state !== 'completed' && r.state !== 'failed')
1247
+ .map((r) => str(r.file_id)));
1248
+ const large = items.filter((it) => it.size > lim || (it.size > 0 && it.fileId !== undefined && inParts.has(it.fileId)));
1249
+ const chosen = new Set(large);
1250
+ const small = items.filter((it) => it.size > 0 && !chosen.has(it));
1251
+ // The small files are packed as slots come free, each pack at the speed measured by then (section 22.7).
1252
+ const packs = [...state.packs];
1253
+ let total = 0;
1254
+ for (const p of packs)
1255
+ for (const it of p.items)
1256
+ total += it.size;
1257
+ for (const it of [...small, ...large])
1258
+ total += it.size;
1259
+ const prog = new common_js_1.Progress(total, onProgress);
1260
+ const stop = [];
1261
+ const closer = new RunCloser();
1262
+ // The run's tasks stop when the caller aborts, or when this step fails.
1263
+ const { ctl, unlink } = (0, common_js_1.linked)(signal);
1264
+ let failure;
1265
+ const failed = (err) => {
1266
+ if (failure)
1267
+ return;
1268
+ failure = { err };
1269
+ ctl.abort(err);
1270
+ };
1271
+ const pack = async (p) => {
1272
+ let outcome = 'failed';
1273
+ try {
1274
+ if (stop.length) {
1275
+ await this.fail(p, stop[0], false);
1276
+ return;
1277
+ }
1278
+ await this.drive(p, ctl.signal);
1279
+ outcome = p.throttled ? 'throttled' : p.state === 'committed' ? 'ok' : 'failed';
1280
+ for (const it of p.items)
1281
+ if (it.result)
1282
+ prog.add(it.size);
1283
+ }
1284
+ catch (err) {
1285
+ if (ctl.signal.aborted || !stopsRun(err))
1286
+ throw err;
1287
+ stop.push(err);
1288
+ }
1289
+ finally {
1290
+ this.release(outcome);
1291
+ }
1292
+ };
1293
+ const go = async (p) => {
1294
+ try {
1295
+ await pack(p);
1296
+ }
1297
+ finally {
1298
+ if (closer.ended(true))
1299
+ await this.abandon(store, place); // the last task of an interrupted run releases it
1300
+ }
1301
+ };
1302
+ const tasks = [];
1303
+ try {
1304
+ const start = (p) => {
1305
+ closer.started();
1306
+ tasks.push(go(p).catch(failed));
1307
+ };
1308
+ for (const p of [...packs]) {
1309
+ await this.limit.acquire(ctl.signal);
1310
+ if (failure) {
1311
+ this.release('failed');
1312
+ throw failure.err;
1313
+ }
1314
+ start(p);
1315
+ }
1316
+ for (let rest = small; rest.length;) {
1317
+ await this.limit.acquire(ctl.signal);
1318
+ if (failure) {
1319
+ this.release('failed');
1320
+ throw failure.err;
1321
+ }
1322
+ let chunk;
1323
+ [chunk, rest] = (0, engine_js_1.nextPack)(rest, (i) => i.size, (i) => i.key, this.packTarget(), this.sizes.packMaxObjects);
1324
+ const p = new Pack(bucket, chunk, store);
1325
+ packs.push(p);
1326
+ start(p);
1327
+ }
1328
+ for (const it of large) {
1329
+ if (stop.length) {
1330
+ await this.done(store, [[it, undefined, stop[0], false]]);
1331
+ continue;
1332
+ }
1333
+ try {
1334
+ const u = await this.largeItem(bucket, it, store, partSize);
1335
+ const obj = await this.parts.run(u, prog, ctl.signal);
1336
+ await this.done(store, [[it, obj, undefined, true]]);
1337
+ }
1338
+ catch (err) {
1339
+ if (ctl.signal.aborted)
1340
+ throw ctl.signal.reason;
1341
+ if (stopsRun(err)) {
1342
+ await this.done(store, [[it, undefined, err, false]]);
1343
+ stop.push(err);
1344
+ }
1345
+ else if (err instanceof errors_js_1.StorageUnavailableError)
1346
+ await this.done(store, [[it, undefined, err, false]]);
1347
+ else if (err instanceof errors_js_1.TeepinError)
1348
+ await this.done(store, [[it, undefined, err, true]]);
1349
+ else
1350
+ throw err;
1351
+ }
1352
+ }
1353
+ // A task that fails stops the run at once (its controller aborts), as joining it first would.
1354
+ await (0, common_js_1.abortable)(Promise.all(tasks), ctl.signal);
1355
+ if (failure)
1356
+ throw failure.err;
1357
+ }
1358
+ catch (err) {
1359
+ // Interrupted, or failed: every pack still under way is stopped and waited for (up to the cap); then the run's
1360
+ // journal is closed and its lock released, so resumeUploads in this same process can finish it. Never while a pack
1361
+ // might still commit (it could be saved twice): whichever ends last, this step or the last pack task, releases it.
1362
+ ctl.abort(err);
1363
+ await (0, common_js_1.joinCapped)(tasks, this.cleanupMs);
1364
+ if (closer.ended(false))
1365
+ await this.abandon(store, place);
1366
+ unlink();
1367
+ throw signal?.aborted ? signal.reason : failure ? failure.err : err;
1368
+ }
1369
+ unlink();
1370
+ const seen = new Set(items);
1371
+ const every = [...items, ...packs.flatMap((p) => p.items).filter((it) => !seen.has(it))];
1372
+ const results = state.results;
1373
+ for (const it of every) {
1374
+ if (it.fileId === undefined || results.has(it.fileId))
1375
+ continue;
1376
+ // pending: a passing error (storage down, a revoked key) left it journaled for resumeUploads; failed is final.
1377
+ const status = it.result ? 'saved' : it.final ? 'failed' : 'pending';
1378
+ results.set(it.fileId, { key: it.key, path: it.path, status, object: it.result, error: it.error });
1379
+ }
1380
+ await this.finish(store, place);
1381
+ if (stop.length)
1382
+ throw stop[0];
1383
+ return [...results.entries()].sort(([a], [b]) => Number(a) - Number(b)).map(([, r]) => r);
1384
+ }
1385
+ /** A large file's upload: the one its journal left unfinished, or a new one. */
1386
+ async largeItem(bucket, it, store, partSize) {
1387
+ const host = this.runHost;
1388
+ const path = it.path ?? '';
1389
+ const readAt = (off, n) => host.readAt(path, off, n);
1390
+ const open = store.journal
1391
+ .rows('uploads')
1392
+ .filter((r) => str(r.file_id) === it.fileId && r.state !== 'completed' && r.state !== 'failed')
1393
+ .sort((a, b) => Number(b.updated) - Number(a.updated));
1394
+ const common = { contentType: it.contentType, metadata: it.metadata, ifNoneMatch: it.ifNoneMatch, ifMatch: it.ifMatch, path, store, fileId: it.fileId };
1395
+ if (open.length) {
1396
+ const u = await large_js_1.Large.fromRow(open[0], readAt, common);
1397
+ if (u.state === 'new' && u.plain.size === 0) {
1398
+ // Never created, so no part hashes were journaled: take them now, against the queued hash.
1399
+ const h = await this.hashFileParts(it, readAt, u.partSize);
1400
+ if (h.whole !== it.sha256)
1401
+ throw new errors_js_1.FileChangedError(path);
1402
+ u.plain = h.parts;
1403
+ }
1404
+ return u;
1405
+ }
1406
+ const psize = this.partSize(it.size, partSize, it.key);
1407
+ const h = await this.hashFileParts(it, readAt, psize);
1408
+ if (h.whole !== it.sha256)
1409
+ throw new errors_js_1.FileChangedError(path);
1410
+ return new large_js_1.Large(bucket, it.key, it.size, it.sha256, readAt, { ...common, partSize: psize, partHashes: h.parts });
1411
+ }
1412
+ /** A large file's whole and part hashes; a file that can't be read the same way is a changed file. */
1413
+ async hashFileParts(it, readAt, partSize) {
1414
+ const host = this.runHost;
1415
+ const path = it.path ?? '';
1416
+ const source = { size: it.size, readAt };
1417
+ if (host.hashParts)
1418
+ source.hashParts = (ps) => host.hashParts(path, it.size, ps);
1419
+ try {
1420
+ return await this.hashParts(source, partSize);
1421
+ }
1422
+ catch (err) {
1423
+ if (err instanceof errors_js_1.FileChangedError)
1424
+ throw err;
1425
+ throw new errors_js_1.FileChangedError(path || it.key, err instanceof Error ? err.message : String(err));
1426
+ }
1427
+ }
1428
+ /** An interrupted run, left for resumeUploads: its journal closed and its lock released (its directory stays). Safe to call twice. */
1429
+ async abandon(store, place) {
1430
+ if (place)
1431
+ await place.release();
1432
+ else
1433
+ await store.journal.close().catch(() => undefined);
1434
+ }
1435
+ async finish(store, place) {
1436
+ const j = store.journal;
1437
+ const counts = countStates(j.rows('files'));
1438
+ const pending = (counts.get('queued') ?? 0) + (counts.get('packed') ?? 0);
1439
+ const run = j.get('run', 'run');
1440
+ if (pending) {
1441
+ if (run)
1442
+ await j.put('run', 'run', { ...run, updated: Date.now(), status: 'open' });
1443
+ await this.abandon(store, place);
1444
+ return;
1445
+ }
1446
+ await j.close();
1447
+ if (!place)
1448
+ return;
1449
+ const host = this.runHost;
1450
+ await host.history(`run ${str(run?.id) || place.id} bucket=${str(run?.bucket)} saved=${counts.get('saved') ?? 0} failed=${counts.get('failed') ?? 0}`);
1451
+ await place.remove();
1452
+ const size = await host.size().catch(() => 0);
1453
+ const warnAt = (0, engine_js_1.envNumber)(exports.STATE_WARN_ENV, exports.STATE_WARN_BYTES);
1454
+ if (size > warnAt)
1455
+ this.warn(`teepin: the upload state directory holds ${size} bytes; cleanUploads() removes old runs`);
1456
+ }
1457
+ // --- resume, listing and cleanup ------------------------------------------------------
1458
+ /** Continues every interrupted run no other process (or run in this one) is working on, and returns their files' results. */
1459
+ async resume(onProgress, signal) {
1460
+ const host = this.runHost;
1461
+ const out = [];
1462
+ for (const id of await host.ids()) {
1463
+ signal?.throwIfAborted();
1464
+ let place;
1465
+ try {
1466
+ place = await host.take(id);
1467
+ }
1468
+ catch (err) {
1469
+ // A damaged journal: never skipped silently, and never in the way of the other runs.
1470
+ this.warn(`teepin: upload run ${id} can't be resumed: ${err instanceof Error ? err.message : String(err)}`);
1471
+ continue;
1472
+ }
1473
+ if (!place)
1474
+ continue;
1475
+ let state;
1476
+ try {
1477
+ state = await this.beginResume(place, signal);
1478
+ }
1479
+ catch (err) {
1480
+ await place.release();
1481
+ throw signal?.aborted ? signal.reason : err;
1482
+ }
1483
+ out.push(...(await this.runState(state, onProgress, 0, signal))); // it releases the run itself, as it ends
1484
+ }
1485
+ return out;
1486
+ }
1487
+ /** A run's first step on resume: its journal read back, its queued files checked again (a changed one fails, and its large upload is cancelled). */
1488
+ async beginResume(place, signal) {
1489
+ const store = new RunStore(place.journal, place);
1490
+ const { state, changed } = await this.readRun(place, store);
1491
+ for (const [fid, it, path] of changed) {
1492
+ signal?.throwIfAborted();
1493
+ await this.endUploads(store, fid); // its large upload, if any: cancelled, its key deleted
1494
+ const err = new errors_js_1.FileChangedError(path);
1495
+ await this.done(store, [[it, undefined, err, true]]);
1496
+ state.results.set(fid, { key: it.key, path, status: 'failed', error: err });
1497
+ }
1498
+ return state;
1499
+ }
1500
+ async readRun(place, store) {
1501
+ const host = this.runHost;
1502
+ const j = store.journal;
1503
+ const bucket = str(j.get('run', 'run')?.bucket);
1504
+ const results = new Map();
1505
+ const byPack = new Map();
1506
+ const queued = [];
1507
+ const changed = [];
1508
+ for (const f of j.rows('files')) {
1509
+ const id = str(f._id);
1510
+ const key = str(f.key);
1511
+ const path = str(f.path);
1512
+ if (f.state === 'saved') {
1513
+ results.set(id, { key, path, status: 'saved', object: reviveObject(f.result) });
1514
+ continue;
1515
+ }
1516
+ if (f.state === 'failed') {
1517
+ results.set(id, { key, path, status: 'failed', error: new errors_js_1.TeepinError(str(f.error)) });
1518
+ continue;
1519
+ }
1520
+ const it = this.fileItem(key, path, int(f.size), str(f.sha256), str(f.content_type), f.metadata ?? {}, Boolean(f.if_none_match), id);
1521
+ it.objectId = str(f.object_id);
1522
+ if (f.state === 'packed' && f.pack) {
1523
+ it.journaled = `${str(f.pack)}|${it.objectId}`;
1524
+ const list = byPack.get(str(f.pack)) ?? [];
1525
+ list.push(it);
1526
+ byPack.set(str(f.pack), list);
1527
+ }
1528
+ else {
1529
+ // Queued: checked again as it's queued again (section 17.5); a change fails it.
1530
+ const h = await host.hashFile(path);
1531
+ if (h instanceof Error || h.size !== int(f.size) || h.sha256 !== str(f.sha256)) {
1532
+ changed.push([id, it, path]);
1533
+ continue;
1534
+ }
1535
+ queued.push(it);
1536
+ }
1537
+ }
1538
+ const packs = [];
1539
+ for (const r of j.rows('packs')) {
1540
+ const pid = str(r._id);
1541
+ if (r.state === 'committed' || r.state === 'failed')
1542
+ continue;
1543
+ const its = byPack.get(pid);
1544
+ byPack.delete(pid);
1545
+ if (!its?.length)
1546
+ continue;
1547
+ const p = new Pack(bucket, its, store, pid);
1548
+ p.state = (str(r.state) || 'new');
1549
+ p.idem = str(r.idempotency_key);
1550
+ p.reservation = r.reservation ?? {};
1551
+ p.signBody = r.sign_body ?? {};
1552
+ p.signed = r.signed ?? {};
1553
+ p.fileName = str(r.file_name);
1554
+ p.size = int(r.size);
1555
+ p.md5 = str(r.md5);
1556
+ p.sha256 = str(r.sha256);
1557
+ p.attempts = int(r.attempts);
1558
+ if (p.state === 'uploading')
1559
+ p.state = 'unknown'; // the PUT may have landed: commit first
1560
+ packs.push(p);
1561
+ }
1562
+ for (const its of byPack.values())
1563
+ queued.push(...its); // packed into a pack that's gone: queue them again
1564
+ await this.sweepOrphans(place, j, packs);
1565
+ return { state: { bucket, store, place, items: queued, packs, results }, changed };
1566
+ }
1567
+ /** Pack files the journal doesn't name, and data keys of uploads that have ended, are deleted (section 17.12, E3). */
1568
+ async sweepOrphans(place, j, packs) {
1569
+ const keep = new Set(packs.map((p) => p.fileName).filter(Boolean));
1570
+ for (const name of await place.packNames().catch(() => []))
1571
+ if (!keep.has(name))
1572
+ await place.removePack(name).catch(() => undefined);
1573
+ const open = new Set(j
1574
+ .rows('uploads')
1575
+ .filter((r) => OPEN_UPLOAD.has(str(r.state)))
1576
+ .map((r) => str(r._id)));
1577
+ for (const name of await place.keyNames().catch(() => []))
1578
+ if (!open.has(name))
1579
+ await place.removeKey(name).catch(() => undefined);
1580
+ }
1581
+ /**
1582
+ * Cancels a file's unfinished large uploads at the storage network and deletes their journaled data keys (E3): a file
1583
+ * that won't be sent must not leave parts behind to be kept, and billed, until they expire.
1584
+ */
1585
+ async endUploads(store, fileId) {
1586
+ for (const r of store.journal.rows('uploads')) {
1587
+ if (str(r.file_id) !== fileId || !OPEN_UPLOAD.has(str(r.state)))
1588
+ continue;
1589
+ if (r.upload_id)
1590
+ await this.abortOne(str(r.bucket), str(r.upload_id));
1591
+ await store.journal.put('uploads', str(r._id), { ...r, state: 'failed', updated: Date.now() });
1592
+ await store.place?.removeKey(str(r._id)).catch(() => undefined);
1593
+ }
1594
+ }
1595
+ /** Cancels an upload: one call (the client's own retries, none while stopping), best effort, since the upload sweeper ends what it misses. */
1596
+ async abortOne(bucket, uploadId, stopping = false) {
1597
+ try {
1598
+ await this.client.send({
1599
+ method: 'DELETE',
1600
+ path: `${bucketPath(bucket)}/uploads/${encodeURIComponent(uploadId)}`,
1601
+ mode: 'safe',
1602
+ maxRetries: stopping ? 0 : undefined,
1603
+ timeout: this.cleanupMs,
1604
+ });
1605
+ }
1606
+ catch {
1607
+ // the upload sweeper ends it
1608
+ }
1609
+ }
1610
+ /** The runs kept in the state directory. */
1611
+ async listRuns() {
1612
+ const host = this.runHost;
1613
+ const out = [];
1614
+ const now = Date.now();
1615
+ for (const id of await host.ids()) {
1616
+ const seen = await host.peek(id).catch(() => undefined); // a damaged journal is left out, not fatal
1617
+ if (!seen)
1618
+ continue;
1619
+ const run = seen.journal.get('run', 'run');
1620
+ if (!run)
1621
+ continue;
1622
+ const counts = countStates(seen.journal.rows('files'));
1623
+ let files = 0;
1624
+ for (const n of counts.values())
1625
+ files += n;
1626
+ const updated = new Date(Number(run.updated));
1627
+ out.push({
1628
+ id: str(run.id) || id,
1629
+ bucket: str(run.bucket),
1630
+ created: new Date(Number(run.created)),
1631
+ updated,
1632
+ files,
1633
+ saved: counts.get('saved') ?? 0,
1634
+ failed: counts.get('failed') ?? 0,
1635
+ pending: (counts.get('queued') ?? 0) + (counts.get('packed') ?? 0),
1636
+ expired: now - updated.getTime() > exports.RUN_EXPIRY_MS,
1637
+ inUse: seen.inUse,
1638
+ });
1639
+ }
1640
+ return out;
1641
+ }
1642
+ /**
1643
+ * Removes runs untouched for longer than olderThan ms that no process is working on, with a warning for each: never
1644
+ * silently. Their open large uploads are cancelled, and their data keys go with their journals (E3).
1645
+ */
1646
+ async clean(olderThan, signal) {
1647
+ const host = this.runHost;
1648
+ const removed = [];
1649
+ const cutoff = Date.now() - olderThan;
1650
+ for (const r of await this.listRuns()) {
1651
+ signal?.throwIfAborted();
1652
+ if (r.inUse || r.updated.getTime() > cutoff)
1653
+ continue;
1654
+ const place = await host.take(r.id).catch(() => undefined);
1655
+ if (!place)
1656
+ continue;
1657
+ try {
1658
+ for (const u of place.journal.rows('uploads')) {
1659
+ if (u.upload_id && OPEN_UPLOAD.has(str(u.state)))
1660
+ await this.abortOne(str(u.bucket), str(u.upload_id));
1661
+ }
1662
+ this.warn(`teepin: removing upload run ${r.id} (bucket ${r.bucket}): ${r.pending} files not uploaded, untouched since ${r.updated.toISOString()}`);
1663
+ await place.remove();
1664
+ removed.push(r);
1665
+ }
1666
+ catch (err) {
1667
+ await place.release();
1668
+ throw err;
1669
+ }
1670
+ }
1671
+ await host.trimHistory?.();
1672
+ return removed;
1673
+ }
1674
+ }
1675
+ exports.DirectUploads = DirectUploads;
1676
+ // --- helpers ------------------------------------------------------------------------------------------------------------
1677
+ /** A storage PUT its own timer ended with no answer (putSigned): a ConnectionError, and evidence of a slow link. */
1678
+ class PutTimedOut extends errors_js_1.ConnectionError {
1679
+ constructor(ms) {
1680
+ super(`no answer from the storage network in ${Math.round(ms)} ms`);
1681
+ }
1682
+ }
1683
+ /** An If-Match value as the control plane takes it: unquoted, or "*". */
1684
+ function normalizeIfMatch(v) {
1685
+ if (!v)
1686
+ return '';
1687
+ return v === '*' ? '*' : unquote(v);
1688
+ }
1689
+ exports.normalizeIfMatch = normalizeIfMatch;
1690
+ function countStates(rows) {
1691
+ const out = new Map();
1692
+ for (const r of rows)
1693
+ out.set(str(r.state), (out.get(str(r.state)) ?? 0) + 1);
1694
+ return out;
1695
+ }
1696
+ /** The states of a large upload that may still have parts at the storage network. */
1697
+ const OPEN_UPLOAD = new Set(['new', 'sending', 'completing']);
1698
+ function unsupported(err) {
1699
+ return (0, errors_js_1.apiError)({
1700
+ statusCode: err.statusCode,
1701
+ message: "this deployment doesn't accept direct uploads, and this SDK uploads no other way",
1702
+ code: err.code || 'direct_upload_unsupported',
1703
+ requestId: err.requestId,
1704
+ });
1705
+ }
1706
+ //# sourceMappingURL=uploads.js.map