@teepin/sdk 0.2.0 → 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 +181 -396
  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 +8 -5
@@ -0,0 +1,1275 @@
1
+ // Copyright 2026 TEEPIN Project
2
+ // Licensed under the Apache License, Version 2.0
3
+ // Reads straight from the storage network (TEEPIN-STORAGE-DESIGN.md sections 21.3, 22.5 and 24), a port of
4
+ // teepin-python's _reads.py with the same rules.
5
+ //
6
+ // A read session (object/read) gives an object's format, where its bytes are, its data key, and signed requests for
7
+ // them. Every byte is verified before it is used:
8
+ //
9
+ // - tws1 in its own blob: windows (the server's, 64 MiB), read segment by segment as they stream, each GCM tag checked
10
+ // (the header's key commitment first, the last segment told by the session's size). A window that breaks off goes
11
+ // on from its last verified segment, under a new signature.
12
+ // - anything packed: the needle first (TPK1, the object id, which is the session's version, the stored size, and the
13
+ // stored bytes' SHA-256), then the whole object decrypted (tws1, teepin-v1) or taken as stored (plain).
14
+ // - plain in its own blob (legacy objects only): as stored.
15
+ // - source "api": an object still in the API's old stage. It isn't read (the API's content route is retired): the read
16
+ // fails with stagedError until the object has been moved into storage.
17
+ //
18
+ // A whole-object read also checks the plaintext SHA-256 against the session's, as its last bytes arrive, except for a
19
+ // legacy object stored in parts, whose checksum is a composite. Streamed bytes are hashed with sha256.ts, since a
20
+ // browser's WebCrypto can only hash a whole buffer.
21
+ //
22
+ // Data keys are cached in memory for the client's keyCacheTtl (10 minutes), by bucket, key and version, so later reads
23
+ // sign without them. Each read takes its own copy and zeroes it when done; the cache zeroes its copies on expiry,
24
+ // eviction and close.
25
+ //
26
+ // Failures: a passing one (the network, a busy storage network) is retried with backoff; an expired or refused
27
+ // signature is signed again; a 404 is signed again once (a server pack may have been compacted), then NotFoundError;
28
+ // a failed check is retried once, then DataIntegrityError.
29
+ //
30
+ // A share link's reads (LinkReads, section 26.3) are the same, with their sessions from the link's token.
31
+ //
32
+ // Cancellation is an AbortSignal (section 22.13's rules). An abort ends a read at once and rejects with the signal's
33
+ // reason: open responses are closed, a wait is cut short, and a download's parallel windows are all stopped and waited
34
+ // for, so no request is left running. Cleanup never retries or sleeps.
35
+ //
36
+ // Nothing here imports a Node module: the same code runs in Node 20+ and browsers.
37
+ import { backoff } from '../client.js';
38
+ import { APIError, ChecksumMismatchError, ConnectionError, DataIntegrityError, InvalidLinkError, KeyErasedError, NotFoundError, ObjectChangedError, TeepinError, apiError, } from '../errors.js';
39
+ import { concat, constantTimeEqual, fromBase64, isInt } from './bytes.js';
40
+ import { SIGN_MARGIN_MS } from './engine.js';
41
+ import { FORMAT_SERVER, FORMAT_TWS1, FORMATS, SOURCE_API, SOURCE_STORAGE, openServerV1 } from './formats.js';
42
+ import { Sha256 } from './sha256.js';
43
+ import * as tws1 from './tws1.js';
44
+ /** The server's read window until it shows its own: 64 MiB of stored bytes. */
45
+ export const DEFAULT_WINDOW = 64 * 2 ** 20;
46
+ /** How long a data key stays cached, in ms, unless the client says. */
47
+ export const DEFAULT_KEY_CACHE_TTL_MS = 600000;
48
+ /** The most a plain blob's read takes from its body at once. */
49
+ const READ_CHUNK = 2 ** 20;
50
+ /** Keys a batch read session names at most (the control plane's cap). */
51
+ const READ_BATCH = 1000;
52
+ const EMPTY = new Uint8Array(0);
53
+ const bucketPath = (bucket) => `/v1/storage/buckets/${encodeURIComponent(bucket)}`;
54
+ const unquote = (etag) => etag.trim().replace(/^W\//, '').replace(/^"|"$/g, '');
55
+ const str = (v) => (v === undefined || v === null ? '' : String(v));
56
+ const int = (v) => Math.trunc(Number(v || 0)) || 0;
57
+ /** A time from the API, or undefined when there is none or it doesn't parse. */
58
+ function when(v) {
59
+ if (!v)
60
+ return undefined;
61
+ const t = Date.parse(String(v));
62
+ return Number.isNaN(t) ? undefined : new Date(t);
63
+ }
64
+ async function sha256Hex(data) {
65
+ return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', data)), (b) => b.toString(16).padStart(2, '0')).join('');
66
+ }
67
+ /** The error for reading an object still in the API's old stage (a session with source "api"): it can't be read yet. */
68
+ export function stagedError(key) {
69
+ return new TeepinError(`${key}: this object is still being moved into storage and can't be read yet; try again later`);
70
+ }
71
+ /** A window that stopped short: go on from the last verified byte. A ConnectionError, as it is to a caller. */
72
+ class Broken extends ConnectionError {
73
+ }
74
+ /** A signature too close to its expiry to use. */
75
+ class Expired extends TeepinError {
76
+ constructor() {
77
+ super('the signature is about to expire');
78
+ this.name = 'Expired';
79
+ }
80
+ }
81
+ /** A controller that aborts with parent's reason, and a function that unlinks it once it is no longer needed. */
82
+ export function linked(parent) {
83
+ const ctl = new AbortController();
84
+ if (!parent)
85
+ return { ctl, unlink: () => { } };
86
+ if (parent.aborted) {
87
+ ctl.abort(parent.reason);
88
+ return { ctl, unlink: () => { } };
89
+ }
90
+ const onAbort = () => ctl.abort(parent.reason);
91
+ parent.addEventListener('abort', onAbort, { once: true });
92
+ return { ctl, unlink: () => parent.removeEventListener('abort', onAbort) };
93
+ }
94
+ // --- sessions -----------------------------------------------------------------
95
+ /** One signed request to the storage network. */
96
+ export class Signed {
97
+ url;
98
+ headers;
99
+ expires;
100
+ prefix;
101
+ first;
102
+ last;
103
+ constructor(url, headers,
104
+ /** When the signature expires (ms since the epoch); undefined when the session didn't say. */
105
+ expires,
106
+ /** Bytes before the object's own (a packed object's needle). */
107
+ prefix,
108
+ /** The object's stored bytes it covers, inclusive. */
109
+ first, last) {
110
+ this.url = url;
111
+ this.headers = headers;
112
+ this.expires = expires;
113
+ this.prefix = prefix;
114
+ this.first = first;
115
+ this.last = last;
116
+ }
117
+ static of(d) {
118
+ const headers = {};
119
+ for (const [k, v] of Object.entries(d.headers ?? {}))
120
+ headers[k] = str(v);
121
+ return new Signed(str(d.url), headers, when(d.expires_at)?.getTime(), int(d.prefix), int(d.stored_first), int(d.stored_last));
122
+ }
123
+ /** The bytes it answers: the prefix and the object's own. */
124
+ get length() {
125
+ return this.prefix + this.last - this.first + 1;
126
+ }
127
+ /** Whether it can still be used: not within a minute of its expiry. */
128
+ fresh() {
129
+ return this.expires === undefined || this.expires - Date.now() > SIGN_MARGIN_MS;
130
+ }
131
+ }
132
+ /** One read session. dek is this session's own copy of the data key. */
133
+ export class Session {
134
+ bucket;
135
+ key;
136
+ version;
137
+ size;
138
+ storedSize;
139
+ etag;
140
+ sha256;
141
+ contentType;
142
+ metadata;
143
+ format;
144
+ source;
145
+ uploadedAt;
146
+ dek;
147
+ ad;
148
+ requests;
149
+ constructor(bucket, d) {
150
+ this.bucket = bucket;
151
+ this.key = str(d.key);
152
+ this.version = str(d.version);
153
+ this.size = int(d.size);
154
+ this.storedSize = int(d.stored_size);
155
+ this.etag = unquote(str(d.etag));
156
+ this.sha256 = str(d.sha256);
157
+ this.contentType = str(d.content_type);
158
+ this.metadata = { ...(d.metadata ?? {}) };
159
+ this.format = str(d.format);
160
+ this.source = str(d.source) || SOURCE_STORAGE;
161
+ this.uploadedAt = when(d.uploaded_at);
162
+ this.dek = d.key_material ? fromBase64(String(d.key_material)) : null;
163
+ this.ad = fromBase64(str(d.associated_data));
164
+ this.requests = (d.requests ?? []).map((r) => Signed.of(r));
165
+ if (this.source === SOURCE_STORAGE && !FORMATS.includes(this.format)) {
166
+ throw new DataIntegrityError(`${this.key}: unknown storage format ${JSON.stringify(this.format)}`, 'format');
167
+ }
168
+ }
169
+ /** Still in the API's old stage: not readable yet (stagedError). */
170
+ get staged() {
171
+ return this.source === SOURCE_API;
172
+ }
173
+ get packed() {
174
+ return this.requests.length > 0 && this.requests[this.requests.length - 1].prefix > 0;
175
+ }
176
+ /** The window it signs: its last request (a later window's first one is the header). */
177
+ get window() {
178
+ return this.requests[this.requests.length - 1];
179
+ }
180
+ get needsKey() {
181
+ return !this.staged && this.storedSize > 0 && (this.format === FORMAT_TWS1 || this.format === FORMAT_SERVER);
182
+ }
183
+ /** Whether the whole object's SHA-256 can be checked (not a legacy composite of parts). */
184
+ get checkable() {
185
+ return Boolean(this.sha256) && !this.etag.includes('-');
186
+ }
187
+ /** Zeroes this session's copy of the key. */
188
+ forget() {
189
+ this.dek?.fill(0);
190
+ this.dek = null;
191
+ }
192
+ /** The object's details, as statObject would give them. */
193
+ object() {
194
+ const parts = /-(\d+)$/.exec(this.etag);
195
+ const t = this.uploadedAt ?? new Date(NaN);
196
+ return {
197
+ id: this.version,
198
+ key: this.key,
199
+ size: this.size,
200
+ contentType: this.contentType || undefined,
201
+ metadata: this.metadata,
202
+ checksumSha256: this.sha256 || undefined,
203
+ etag: this.etag,
204
+ partCount: parts ? Number(parts[1]) : 0,
205
+ status: 'available',
206
+ backend: '',
207
+ createdAt: t,
208
+ updatedAt: t,
209
+ uploadedAt: this.uploadedAt,
210
+ };
211
+ }
212
+ }
213
+ /**
214
+ * Data keys by bucket and key, each for one version, for ttl ms (0: none are kept). It hands out copies, so zeroing one
215
+ * never breaks a read in progress, and zeroes its own on expiry, replacement, drop and clear.
216
+ */
217
+ export class KeyCache {
218
+ ttl;
219
+ /** The clock, in ms (monotonic); a test may move it. */
220
+ now = () => performance.now();
221
+ entries = new Map();
222
+ constructor(ttl) {
223
+ this.ttl = ttl;
224
+ }
225
+ static id(bucket, key) {
226
+ return JSON.stringify([bucket, key]);
227
+ }
228
+ /** The cached version and a copy of its key, or undefined. */
229
+ get(bucket, key) {
230
+ const id = KeyCache.id(bucket, key);
231
+ const e = this.entries.get(id);
232
+ if (!e)
233
+ return undefined;
234
+ if (this.now() - e.at > this.ttl) {
235
+ this.entries.delete(id);
236
+ e.key.fill(0);
237
+ return undefined;
238
+ }
239
+ return { version: e.version, key: e.key.slice() };
240
+ }
241
+ /** Keeps a copy of dek for this version (the caller keeps its own). */
242
+ put(bucket, key, version, dek) {
243
+ if (this.ttl <= 0)
244
+ return; // the cache is off: the caller's copy is the only one
245
+ const id = KeyCache.id(bucket, key);
246
+ const old = this.entries.get(id);
247
+ if (old && old.version === version)
248
+ return;
249
+ this.entries.set(id, { version, key: dek.slice(), at: this.now() });
250
+ old?.key.fill(0);
251
+ }
252
+ drop(bucket, key) {
253
+ const id = KeyCache.id(bucket, key);
254
+ const e = this.entries.get(id);
255
+ this.entries.delete(id);
256
+ e?.key.fill(0);
257
+ }
258
+ clear() {
259
+ for (const e of this.entries.values())
260
+ e.key.fill(0);
261
+ this.entries.clear();
262
+ }
263
+ get size() {
264
+ return this.entries.size;
265
+ }
266
+ }
267
+ /** One read's failures since its last verified bytes, and what each means. */
268
+ export class Tries {
269
+ reads;
270
+ s;
271
+ signal;
272
+ passing = 0;
273
+ bad = 0;
274
+ notFound = false;
275
+ constructor(reads, s, signal) {
276
+ this.reads = reads;
277
+ this.s = s;
278
+ this.signal = signal;
279
+ }
280
+ progressed() {
281
+ this.passing = 0;
282
+ this.bad = 0;
283
+ }
284
+ /** Resolves when the read should try again (after any wait); throws otherwise. An abort is never tried again. */
285
+ async failed(err) {
286
+ if (this.signal?.aborted)
287
+ throw this.signal.reason;
288
+ // A share link that expired (or was never good) stays so: signing again would only be refused again.
289
+ if (err instanceof ObjectChangedError || err instanceof InvalidLinkError)
290
+ throw err;
291
+ if (err instanceof DataIntegrityError) {
292
+ this.bad++; // retried once: a flipped byte in transit, or for good
293
+ if (this.bad > 1)
294
+ throw err;
295
+ return;
296
+ }
297
+ if (err instanceof APIError && err.statusCode === 404) {
298
+ // A server pack may have been compacted, so the bytes moved: sign again, once.
299
+ this.reads.keys.drop(this.s.bucket, this.s.key);
300
+ if (this.notFound) {
301
+ throw new NotFoundError({ statusCode: 404, message: `${this.s.key}: the storage network doesn't have this object`, code: 'object_not_found' });
302
+ }
303
+ this.notFound = true;
304
+ return;
305
+ }
306
+ const resign = err instanceof Expired || (err instanceof APIError && (err.statusCode === 401 || err.statusCode === 403));
307
+ const passing = err instanceof ConnectionError || (err instanceof APIError && (err.statusCode === 429 || err.statusCode >= 500));
308
+ if (!(resign || passing))
309
+ throw err;
310
+ this.passing++;
311
+ if (this.passing > this.reads.client.maxRetries) {
312
+ if (err instanceof Expired)
313
+ throw new TeepinError(`${this.s.key}: every signature expired before it could be used; check this computer's clock`);
314
+ throw err;
315
+ }
316
+ if (passing)
317
+ await this.reads.client.sleep(backoff(this.passing - 1, err), this.signal);
318
+ }
319
+ }
320
+ /** A signed GET's body as it arrives, read a piece at a time; each read waits at most idleMs for bytes. */
321
+ export class Body {
322
+ ctl;
323
+ unlink;
324
+ idleMs;
325
+ signal;
326
+ reader;
327
+ rest = EMPTY;
328
+ ended;
329
+ closed = false;
330
+ constructor(response, ctl, unlink, idleMs, signal) {
331
+ this.ctl = ctl;
332
+ this.unlink = unlink;
333
+ this.idleMs = idleMs;
334
+ this.signal = signal;
335
+ this.reader = response.body?.getReader();
336
+ this.ended = !this.reader;
337
+ }
338
+ /** Up to max bytes (fewer is fine), empty at the end. A body that breaks off throws ConnectionError. */
339
+ async read(max) {
340
+ while (!this.rest.length) {
341
+ const c = await this.next();
342
+ if (!c)
343
+ return EMPTY;
344
+ this.rest = c;
345
+ }
346
+ const out = this.rest.length > max ? this.rest.subarray(0, max) : this.rest;
347
+ this.rest = this.rest.subarray(out.length);
348
+ return out;
349
+ }
350
+ /** n bytes, or fewer where the body ends. */
351
+ async readExact(n) {
352
+ const first = await this.read(n);
353
+ if (first.length === n || !first.length)
354
+ return first;
355
+ const parts = [first];
356
+ let got = first.length;
357
+ while (got < n) {
358
+ const c = await this.read(n - got);
359
+ if (!c.length)
360
+ break;
361
+ parts.push(c);
362
+ got += c.length;
363
+ }
364
+ return concat(parts);
365
+ }
366
+ async next() {
367
+ if (this.ended || !this.reader)
368
+ return null;
369
+ const ms = this.idleMs;
370
+ const timer = ms > 0 ? setTimeout(() => this.ctl.abort(new ConnectionError(`the storage network sent nothing for ${ms} ms`)), ms) : undefined;
371
+ try {
372
+ const { done, value } = await this.reader.read();
373
+ if (done) {
374
+ this.ended = true;
375
+ return null;
376
+ }
377
+ return value;
378
+ }
379
+ catch (err) {
380
+ if (this.signal?.aborted)
381
+ throw this.signal.reason;
382
+ const why = this.ctl.signal.reason;
383
+ if (this.ctl.signal.aborted && why instanceof ConnectionError)
384
+ throw why;
385
+ throw new ConnectionError(`the read broke off: ${err instanceof Error ? err.message : String(err)}`, err);
386
+ }
387
+ finally {
388
+ clearTimeout(timer);
389
+ }
390
+ }
391
+ /** Ends the request: a body not read to its end is cancelled and its connection dropped. Never waits. */
392
+ close() {
393
+ if (this.closed)
394
+ return;
395
+ this.closed = true;
396
+ this.unlink();
397
+ if (!this.ended) {
398
+ this.ctl.abort(new TeepinError('the read was closed'));
399
+ this.reader?.cancel().catch(() => undefined);
400
+ }
401
+ }
402
+ }
403
+ class Progress {
404
+ total;
405
+ fn;
406
+ done = 0;
407
+ constructor(total, fn) {
408
+ this.total = total;
409
+ this.fn = fn;
410
+ }
411
+ add(n) {
412
+ if (!this.fn || n === 0)
413
+ return;
414
+ this.done += n;
415
+ this.fn(this.done, this.total);
416
+ }
417
+ }
418
+ /**
419
+ * Reads through read sessions, for one client. The methods tests replace (as the Python tests patch _reads.py) are
420
+ * call, session, window, open and plaintext.
421
+ */
422
+ export class DirectReads {
423
+ client;
424
+ keys;
425
+ constructor(client) {
426
+ this.client = client;
427
+ this.keys = new KeyCache(client.keyCacheTtl);
428
+ }
429
+ /** Zeroes the cached data keys. */
430
+ close() {
431
+ this.keys.clear();
432
+ }
433
+ // --- sessions ---------------------------------------------------------------
434
+ /** One read session (object/read), retried by the client as a safe call. */
435
+ async call(bucket, key, off, length, includeKey, signal) {
436
+ const json = { key, stored_offset: off, stored_length: length, include_key: includeKey };
437
+ const { data } = await this.client.call({
438
+ method: 'POST',
439
+ path: bucketPath(bucket) + '/object/read',
440
+ json,
441
+ mode: 'safe',
442
+ signal,
443
+ });
444
+ return new Session(bucket, data ?? {});
445
+ }
446
+ /**
447
+ * A read's first session, with the data key: from the cache when it holds this version's, so the server needn't send
448
+ * it again. An object still in the old stage throws stagedError.
449
+ */
450
+ async session(bucket, key, off = 0, length = 0, signal) {
451
+ let cached = this.keys.get(bucket, key);
452
+ let s;
453
+ try {
454
+ s = await this.call(bucket, key, off, length, cached === undefined, signal);
455
+ if (s.needsKey && !s.dek) {
456
+ if (cached && cached.version === s.version) {
457
+ s.dek = cached.key;
458
+ cached = undefined;
459
+ }
460
+ else {
461
+ s = await this.call(bucket, key, off, length, true, signal);
462
+ }
463
+ }
464
+ }
465
+ finally {
466
+ cached?.key.fill(0);
467
+ }
468
+ if (s.staged) {
469
+ s.forget();
470
+ throw stagedError(key);
471
+ }
472
+ if (s.dek)
473
+ this.keys.put(bucket, key, s.version, s.dek);
474
+ return s;
475
+ }
476
+ /**
477
+ * Read sessions for many objects, signed 1,000 to a request (objects/read): a Session, or the error for an object
478
+ * that can't be read (each on its own; stagedError for one still in the old stage). Each Session holds its own key;
479
+ * the caller forgets it.
480
+ */
481
+ async sessions(bucket, keys, signal) {
482
+ const out = new Map();
483
+ const unique = [...new Set(keys)];
484
+ for (let i = 0; i < unique.length; i += READ_BATCH) {
485
+ const { data } = await this.client.call({
486
+ method: 'POST',
487
+ path: bucketPath(bucket) + '/objects/read',
488
+ json: { keys: unique.slice(i, i + READ_BATCH), include_keys: true },
489
+ mode: 'safe',
490
+ signal,
491
+ });
492
+ for (const item of data?.objects ?? []) {
493
+ const k = str(item.key);
494
+ if (item.error) {
495
+ out.set(k, sessionError(k, item.error));
496
+ continue;
497
+ }
498
+ let s;
499
+ try {
500
+ s = new Session(bucket, item);
501
+ }
502
+ catch (err) {
503
+ if (!(err instanceof DataIntegrityError))
504
+ throw err;
505
+ out.set(k, err);
506
+ continue;
507
+ }
508
+ if (s.staged) {
509
+ s.forget();
510
+ out.set(k, stagedError(k));
511
+ continue;
512
+ }
513
+ if (s.dek)
514
+ this.keys.put(bucket, k, s.version, s.dek);
515
+ out.set(k, s);
516
+ }
517
+ }
518
+ for (const k of unique) {
519
+ if (!out.has(k))
520
+ out.set(k, new NotFoundError({ statusCode: 404, message: `${k}: missing from the read batch's answer`, code: 'object_not_found' }));
521
+ }
522
+ return out;
523
+ }
524
+ /** A later window of the same version, signed without the key. */
525
+ async window(s0, off, length, signal) {
526
+ const s = await this.call(s0.bucket, s0.key, off, length, false, signal);
527
+ if (s.version !== s0.version) {
528
+ s.forget();
529
+ throw new ObjectChangedError();
530
+ }
531
+ s.forget(); // never used: the read has its own copy (a server that sent one anyway gets it zeroed)
532
+ return s;
533
+ }
534
+ /** A signed GET, its headers in (the client's timeout), its body to read; Expired for a signature about to expire. */
535
+ async open(r, signal) {
536
+ if (!r.fresh())
537
+ throw new Expired();
538
+ signal?.throwIfAborted();
539
+ const { ctl, unlink } = linked(signal);
540
+ const ms = this.client.timeout;
541
+ const timer = ms > 0 ? setTimeout(() => ctl.abort(new ConnectionError(`no answer from the storage network in ${ms} ms`)), ms) : undefined;
542
+ try {
543
+ const response = await this.client.sendSigned({ method: 'GET', url: r.url, headers: r.headers, signal: ctl.signal });
544
+ return new Body(response, ctl, unlink, ms, signal);
545
+ }
546
+ catch (err) {
547
+ unlink();
548
+ if (signal?.aborted)
549
+ throw signal.reason;
550
+ throw err;
551
+ }
552
+ finally {
553
+ clearTimeout(timer);
554
+ }
555
+ }
556
+ // --- verified bytes ---------------------------------------------------------
557
+ /**
558
+ * The object's bytes [a, b), each verified before it is yielded; with check (a whole read), the object's SHA-256
559
+ * too, throwing ChecksumMismatchError after the last bytes.
560
+ */
561
+ async *plaintext(s, dek, a, b, check, signal) {
562
+ const checked = check && s.checkable;
563
+ if (!s.requests.length) {
564
+ if (s.size)
565
+ throw new DataIntegrityError(`${s.key}: the read session signed nothing`, 'session');
566
+ return;
567
+ }
568
+ if (s.packed || s.format === FORMAT_SERVER) {
569
+ const data = await this.whole(s, dek, signal);
570
+ if (checked) {
571
+ const got = await sha256Hex(data);
572
+ if (got !== s.sha256)
573
+ throw new ChecksumMismatchError(`${s.key}: received ${got}, stored ${s.sha256}`);
574
+ }
575
+ if (b > a)
576
+ yield data.subarray(a, b);
577
+ return;
578
+ }
579
+ let chunks;
580
+ if (s.format === FORMAT_TWS1) {
581
+ if (!dek)
582
+ throw new DataIntegrityError(`${s.key}: the read session carried no key`, 'key');
583
+ chunks = this.tws1Blob(s, dek, a, b, signal);
584
+ }
585
+ else {
586
+ chunks = this.rawBlob(s, a, b, signal);
587
+ }
588
+ const h = checked ? new Sha256() : null;
589
+ for await (const c of chunks) {
590
+ h?.update(c);
591
+ yield c;
592
+ }
593
+ if (h) {
594
+ const got = h.hexDigest();
595
+ if (got !== s.sha256)
596
+ throw new ChecksumMismatchError(`${s.key}: received ${got}, stored ${s.sha256}`);
597
+ }
598
+ }
599
+ /** A packed object, its needle checked and decrypted whole; or a teepin-v1 object in its own blob, read whole and opened. */
600
+ async whole(s, dek, signal) {
601
+ const stored = s.packed ? await this.packed(s, signal) : await collect(this.rawBlob(s, 0, s.storedSize, signal));
602
+ let data;
603
+ if (s.format === FORMAT_TWS1 || s.format === FORMAT_SERVER) {
604
+ if (!dek)
605
+ throw new DataIntegrityError(`${s.key}: the read session carried no key`, 'key');
606
+ data = s.format === FORMAT_TWS1 ? await tws1.decrypt(dek, stored) : await openServerV1(dek, stored, s.ad);
607
+ }
608
+ else {
609
+ data = stored;
610
+ }
611
+ if (data.length !== s.size)
612
+ throw new DataIntegrityError(`${s.key}: ${data.length} bytes, not the ${s.size} stored`, 'size');
613
+ return data;
614
+ }
615
+ async packed(s0, signal) {
616
+ const tries = new Tries(this, s0, signal);
617
+ let cur = s0;
618
+ for (;;) {
619
+ try {
620
+ const r = cur.window;
621
+ const body = await this.open(r, signal);
622
+ let data;
623
+ try {
624
+ data = await body.readExact(r.length);
625
+ }
626
+ finally {
627
+ body.close();
628
+ }
629
+ if (data.length !== r.length)
630
+ throw new Broken(`${data.length} of ${r.length} bytes`);
631
+ const stored = data.subarray(r.prefix);
632
+ await tws1.checkNeedle(data.subarray(0, r.prefix), tws1.objectIdBytes(s0.version), stored);
633
+ return stored;
634
+ }
635
+ catch (err) {
636
+ await tries.failed(err);
637
+ cur = await this.window(s0, 0, 0, signal);
638
+ }
639
+ }
640
+ }
641
+ async *tws1Blob(s0, dek, a, b, signal) {
642
+ const size = s0.size;
643
+ const n = tws1.segments(size);
644
+ const r = tws1.locate(size, a, b - a);
645
+ let seg = r.firstSegment;
646
+ const last = b <= a ? seg : Math.floor((b - 1) / tws1.SEGMENT_SIZE);
647
+ const [lastOff, lastLen] = tws1.segmentAt(size, last);
648
+ const end = lastOff + lastLen; // the stored end, exclusive
649
+ let skip = r.skip;
650
+ let left = r.length;
651
+ let keys = null;
652
+ let cap = DEFAULT_WINDOW;
653
+ const tries = new Tries(this, s0, signal);
654
+ let start = seg === 0 ? 0 : tws1.segmentAt(size, seg)[0];
655
+ let cur = s0.requests.length && s0.window.first === start ? s0 : null;
656
+ while (seg <= last) {
657
+ start = seg === 0 ? 0 : tws1.segmentAt(size, seg)[0];
658
+ let asked = 0;
659
+ try {
660
+ if (!cur) {
661
+ asked = aligned(start, end, cap);
662
+ cur = await this.window(s0, start, asked, signal);
663
+ }
664
+ const win = cur.window;
665
+ if (win.first !== start)
666
+ throw new DataIntegrityError(`${s0.key}: a window signed from ${win.first}, not ${start}`, 'window');
667
+ if (asked && win.last - win.first + 1 < asked)
668
+ cap = win.last - win.first + 1; // the server's window
669
+ if (!keys && start > 0)
670
+ keys = await this.header(cur, dek, signal);
671
+ const body = await this.open(win, signal);
672
+ try {
673
+ if (start === 0) {
674
+ const head = await body.readExact(tws1.HEADER_SIZE);
675
+ if (head.length < tws1.HEADER_SIZE)
676
+ throw new Broken('the header broke off');
677
+ if (!keys)
678
+ keys = await tws1.openHeader(dek, head);
679
+ else if (!constantTimeEqual(head, keys.header))
680
+ throw new DataIntegrityError(`${s0.key}: the header changed`, 'header');
681
+ }
682
+ while (seg <= last) {
683
+ const [soff, slen] = tws1.segmentAt(size, seg);
684
+ if (soff + slen - 1 > win.last)
685
+ break; // the window ends inside this segment: the next one starts with it
686
+ const data = await body.readExact(slen);
687
+ if (data.length < slen)
688
+ throw new Broken(`segment ${seg} broke off`);
689
+ if (!keys)
690
+ throw new DataIntegrityError(`${s0.key}: no header to open segment ${seg} with`, 'header');
691
+ const plain = await keys.open(seg, seg === n - 1, data);
692
+ seg++;
693
+ tries.progressed();
694
+ const out = skip || left < plain.length ? plain.subarray(skip, skip + left) : plain;
695
+ skip = 0;
696
+ left -= out.length;
697
+ if (out.length)
698
+ yield out;
699
+ }
700
+ }
701
+ finally {
702
+ body.close();
703
+ }
704
+ }
705
+ catch (err) {
706
+ await tries.failed(err);
707
+ }
708
+ cur = null;
709
+ }
710
+ }
711
+ /** The object's keys from the header a later window's session signs first; its commitment is checked. */
712
+ async header(cur, dek, signal) {
713
+ const hr = cur.requests.length > 1 ? cur.requests[0] : null;
714
+ if (!hr || hr.first !== 0)
715
+ throw new DataIntegrityError(`${cur.key}: the read session didn't sign the header`, 'header');
716
+ const body = await this.open(hr, signal);
717
+ let head;
718
+ try {
719
+ head = await body.readExact(tws1.HEADER_SIZE);
720
+ }
721
+ finally {
722
+ body.close();
723
+ }
724
+ if (head.length < tws1.HEADER_SIZE)
725
+ throw new Broken('the header broke off');
726
+ return tws1.openHeader(dek, head);
727
+ }
728
+ /** Stored bytes [a, b) of an object in its own blob, as stored. */
729
+ async *rawBlob(s0, a, b, signal) {
730
+ let pos = a;
731
+ let cap = DEFAULT_WINDOW;
732
+ const tries = new Tries(this, s0, signal);
733
+ let cur = s0.requests.length && s0.window.first === a ? s0 : null;
734
+ while (pos < b) {
735
+ let asked = 0;
736
+ try {
737
+ if (!cur) {
738
+ asked = Math.min(b - pos, cap);
739
+ cur = await this.window(s0, pos, asked, signal);
740
+ }
741
+ const win = cur.window;
742
+ if (win.first !== pos)
743
+ throw new DataIntegrityError(`${s0.key}: a window signed from ${win.first}, not ${pos}`, 'window');
744
+ if (asked && win.last - win.first + 1 < asked)
745
+ cap = win.last - win.first + 1;
746
+ const body = await this.open(win, signal);
747
+ try {
748
+ let want = Math.min(b, win.last + 1) - pos;
749
+ while (want > 0) {
750
+ const chunk = await body.read(Math.min(READ_CHUNK, want));
751
+ if (!chunk.length)
752
+ throw new Broken(`${want} bytes short`);
753
+ pos += chunk.length;
754
+ want -= chunk.length;
755
+ tries.progressed();
756
+ yield chunk;
757
+ }
758
+ }
759
+ finally {
760
+ body.close();
761
+ }
762
+ }
763
+ catch (err) {
764
+ await tries.failed(err);
765
+ }
766
+ cur = null;
767
+ }
768
+ }
769
+ // --- whole operations ---------------------------------------------------------
770
+ /** How a download splits: one range per server window (partSize may split finer), or one range for an object read whole. */
771
+ ranges(s, partSize) {
772
+ const size = s.size;
773
+ if (s.packed || !s.requests.length || s.format === FORMAT_SERVER || s.window.last + 1 >= s.storedSize)
774
+ return [[0, size]];
775
+ let stride = s.window.last - s.window.first + 1;
776
+ if (s.format === FORMAT_TWS1) {
777
+ const seg = tws1.SEGMENT_SIZE;
778
+ stride = Math.max(1, Math.floor((stride - tws1.HEADER_SIZE) / tws1.STORED_SEGMENT_SIZE)) * seg;
779
+ if (partSize > 0)
780
+ stride = Math.min(stride, Math.max(seg, Math.floor(partSize / seg) * seg));
781
+ }
782
+ else if (partSize > 0) {
783
+ stride = Math.min(stride, partSize);
784
+ }
785
+ const out = [];
786
+ for (let a = 0; a < size; a += stride)
787
+ out.push([a, Math.min(a + stride, size)]);
788
+ return out;
789
+ }
790
+ /**
791
+ * Writes the whole object to the target open makes (once per attempt, given the object's details): windows in
792
+ * parallel, each pinned to the first session's version. A SHA-256 mismatch is tried again once.
793
+ */
794
+ async download(bucket, key, open, p) {
795
+ let prog;
796
+ for (let attempt = 0;; attempt++) {
797
+ const s = await this.session(bucket, key, 0, 0, p.signal);
798
+ const dek = s.dek ? s.dek.slice() : null;
799
+ s.forget();
800
+ const progress = (prog ??= new Progress(s.size, p.onProgress));
801
+ try {
802
+ const target = open(s.object());
803
+ const ranges = this.ranges(s, p.partSize);
804
+ if (ranges.length === 1 || p.concurrency === 1) {
805
+ await this.writeRange(s, dek, 0, s.size, p.verify, target, progress, p.signal);
806
+ return s.object();
807
+ }
808
+ await pool(p.concurrency, ranges, p.signal, (r, signal) => this.writeRange(s, dek, r[0], r[1], false, target, progress, signal));
809
+ if (p.verify && s.checkable && target.sha256) {
810
+ const got = await target.sha256(s.size);
811
+ if (got !== s.sha256)
812
+ throw new ChecksumMismatchError(`${key}: received ${got}, stored ${s.sha256}`);
813
+ }
814
+ return s.object();
815
+ }
816
+ catch (err) {
817
+ if (!(err instanceof ChecksumMismatchError) || attempt > 0)
818
+ throw err;
819
+ }
820
+ finally {
821
+ dek?.fill(0);
822
+ }
823
+ }
824
+ }
825
+ async writeRange(s, dek, a, b, check, target, prog, signal) {
826
+ let pos = a;
827
+ for await (const c of this.plaintext(s, dek, a, b, check, signal)) {
828
+ await target.write(pos, c);
829
+ pos += c.length;
830
+ prog.add(c.length);
831
+ }
832
+ }
833
+ /**
834
+ * The first part of getObject: the session, checked, and the range [a, b) to read (start and end inclusive, end
835
+ * undefined for to the end); the bytes come from body.
836
+ */
837
+ async get(bucket, key, start, end, ifMatch, signal) {
838
+ const ranged = start > 0 || end !== undefined;
839
+ // A range signs a little at first (its window comes later); a whole read signs a window.
840
+ const s = await this.session(bucket, key, 0, ranged ? tws1.HEADER_SIZE + tws1.STORED_SEGMENT_SIZE : 0, signal);
841
+ if (ifMatch && s.etag !== unquote(ifMatch)) {
842
+ s.forget();
843
+ throw new ObjectChangedError();
844
+ }
845
+ const b = end === undefined ? s.size : Math.min(end + 1, s.size);
846
+ if (start < 0 || (ranged && start >= s.size) || start > b) {
847
+ s.forget();
848
+ throw apiError({ statusCode: 416, message: `${key}: bytes ${start}- are past the object's ${s.size} bytes`, code: 'invalid_range' });
849
+ }
850
+ const dek = s.dek ? s.dek.slice() : null;
851
+ s.forget();
852
+ return { session: s, dek, a: start, b, ranged };
853
+ }
854
+ /** The verified bytes [a, b); the key copy is zeroed at the end, or when the reader stops early. */
855
+ async *body(s, dek, a, b, check, signal) {
856
+ try {
857
+ yield* this.plaintext(s, dek, a, b, check, signal);
858
+ }
859
+ finally {
860
+ dek?.fill(0);
861
+ }
862
+ }
863
+ /**
864
+ * The first part of openObject: the session, the key copy, the block (one window, or the whole object) and the
865
+ * alignment of a jump.
866
+ */
867
+ async openFile(bucket, key, signal) {
868
+ const s = await this.session(bucket, key, 0, 0, signal);
869
+ const dek = s.dek ? s.dek.slice() : null;
870
+ s.forget();
871
+ return { session: s, dek, ...this.layout(s) };
872
+ }
873
+ /** How an ObjectFile reads s: its block (one window, or the whole object) and the alignment of a jump. */
874
+ layout(s) {
875
+ const block = this.ranges(s, 0)[0][1] || 1;
876
+ const align = s.packed || s.format === FORMAT_SERVER ? Math.max(1, s.size) : tws1.SEGMENT_SIZE;
877
+ return { block, align };
878
+ }
879
+ /** The verified bytes [a, b), joined; the whole object's SHA-256 is checked when that is what's asked for. */
880
+ fetch(s, dek, a, b, signal) {
881
+ return collect(this.plaintext(s, dek, a, b, a === 0 && b === s.size, signal));
882
+ }
883
+ }
884
+ // --- share links ------------------------------------------------------------------
885
+ /** The route that opens a share link: a read session for the token, with no sign-in (section 26.2). */
886
+ export const LINK_READ_PATH = '/v1/storage/links/read';
887
+ /**
888
+ * Reads through a share link (section 26.3): the same engine and checks as DirectReads, with the link's read sessions
889
+ * (POST /v1/storage/links/read) in place of the authenticated ones. Every session carries the data key, so none is
890
+ * cached (the client's keyCacheTtl is 0), and every session after the first must be of the version the link first
891
+ * opened: ObjectChangedError otherwise. The bucket and key the engine passes around are unused: the token names the
892
+ * object.
893
+ *
894
+ * The first session takes the link's token. Each answer brings a continuation, and every later session takes the
895
+ * newest one instead: it reads only the version first opened (the server answers 412 for a replaced object, which is
896
+ * ObjectChangedError here), lasts 30 minutes from its issue, and never past the link's expiry plus 24 hours, so a
897
+ * download that began while the link was valid can finish after it expires. A continuation left idle too long is
898
+ * refused; the link's token is then tried once more, which opens only while the link itself is valid.
899
+ */
900
+ export class LinkReads extends DirectReads {
901
+ token;
902
+ /** The version the link first opened (set by openLink); '' until then. */
903
+ version = '';
904
+ /** From the link's answers: how the link is meant to be offered, and the name to save the object as. */
905
+ disposition = '';
906
+ fileName = '';
907
+ /** The newest continuation, and when it stops working (ms); '' until the first answer. */
908
+ continuation = '';
909
+ continuationEnds = 0;
910
+ constructor(client, token) {
911
+ super(client);
912
+ this.token = token;
913
+ }
914
+ /** One read session for the link, retried by the client as a safe call; the version pinned once it is known. */
915
+ async call(bucket, _key, off, length, _includeKey, signal) {
916
+ const cont = this.continuation;
917
+ let d;
918
+ try {
919
+ d = await this.openWith(cont ? { continuation: cont } : { token: this.token }, off, length, signal);
920
+ }
921
+ catch (err) {
922
+ if (!cont)
923
+ throw err;
924
+ if (err instanceof APIError && err.statusCode === 412 && err.code === 'precondition_failed')
925
+ throw new ObjectChangedError();
926
+ if (!(err instanceof InvalidLinkError))
927
+ throw err;
928
+ d = await this.openWith({ token: this.token }, off, length, signal);
929
+ }
930
+ this.disposition = str(d.disposition) || this.disposition;
931
+ this.fileName = str(d.file_name) || this.fileName;
932
+ this.keep(d.continuation);
933
+ const s = new Session(bucket, d);
934
+ if (this.version && s.version !== this.version) {
935
+ s.forget();
936
+ throw new ObjectChangedError();
937
+ }
938
+ return s;
939
+ }
940
+ /** POST /v1/storage/links/read with the link's token or a continuation. */
941
+ async openWith(cred, off, length, signal) {
942
+ const { data } = await this.client.call({
943
+ method: 'POST',
944
+ path: LINK_READ_PATH,
945
+ json: { ...cred, stored_offset: off, stored_length: length },
946
+ mode: 'safe',
947
+ signal,
948
+ });
949
+ return data ?? {};
950
+ }
951
+ /** Keeps an answer's continuation when it lasts at least as long as the one held (windows may answer out of order). */
952
+ keep(c) {
953
+ if (!c || typeof c !== 'object')
954
+ return;
955
+ const token = str(c.token);
956
+ const ends = Date.parse(str(c.expires_at));
957
+ if (!token || Number.isNaN(ends) || ends < this.continuationEnds)
958
+ return;
959
+ this.continuation = token;
960
+ this.continuationEnds = ends;
961
+ }
962
+ /** A read's first session, always with its key (nothing is cached); an object still in the old stage throws stagedError. */
963
+ async session(bucket, key, off = 0, length = 0, signal) {
964
+ const s = await this.call(bucket, key, off, length, true, signal);
965
+ if (s.staged) {
966
+ s.forget();
967
+ throw stagedError(this.fileName || s.key || 'the linked object');
968
+ }
969
+ return s;
970
+ }
971
+ }
972
+ /** Everything chunks yields, joined. */
973
+ export async function collect(chunks) {
974
+ const parts = [];
975
+ for await (const c of chunks)
976
+ parts.push(c);
977
+ return parts.length === 1 ? parts[0] : concat(parts);
978
+ }
979
+ /**
980
+ * Runs fn for every item, at most n at once. The first failure stops the others (their signal aborts), and every
981
+ * worker is waited for before it is thrown, so nothing is left running; an abort of signal throws its reason.
982
+ */
983
+ async function pool(n, items, signal, fn) {
984
+ const { ctl, unlink } = linked(signal);
985
+ let next = 0;
986
+ let failed = false;
987
+ let failure;
988
+ const worker = async () => {
989
+ while (!failed && next < items.length && !ctl.signal.aborted) {
990
+ const item = items[next++];
991
+ try {
992
+ await fn(item, ctl.signal);
993
+ }
994
+ catch (err) {
995
+ if (!failed) {
996
+ failed = true;
997
+ failure = err;
998
+ }
999
+ ctl.abort(err);
1000
+ }
1001
+ }
1002
+ };
1003
+ try {
1004
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(n, items.length)) }, worker));
1005
+ }
1006
+ finally {
1007
+ unlink();
1008
+ }
1009
+ if (signal?.aborted)
1010
+ throw signal.reason;
1011
+ if (failed)
1012
+ throw failure;
1013
+ }
1014
+ /**
1015
+ * A ReadableStream of a read's verified chunks, made only as they are asked for. An abort of signal, or a cancel, ends
1016
+ * it at once: the stream errors with the reason, the read's requests stop, and release runs (it zeroes the read's key
1017
+ * copy), as it does when the stream ends.
1018
+ */
1019
+ export function readable(chunks, signal, release) {
1020
+ const { ctl, unlink } = linked(signal);
1021
+ let it;
1022
+ let finished = false;
1023
+ const end = () => {
1024
+ if (finished)
1025
+ return;
1026
+ finished = true;
1027
+ unlink();
1028
+ release?.();
1029
+ void it?.return(undefined).catch(() => undefined);
1030
+ };
1031
+ return new ReadableStream({
1032
+ start(c) {
1033
+ const onAbort = () => {
1034
+ try {
1035
+ c.error(ctl.signal.reason);
1036
+ }
1037
+ catch {
1038
+ // already closed
1039
+ }
1040
+ end();
1041
+ };
1042
+ if (ctl.signal.aborted)
1043
+ onAbort();
1044
+ else
1045
+ ctl.signal.addEventListener('abort', onAbort, { once: true });
1046
+ },
1047
+ async pull(c) {
1048
+ it ??= chunks(ctl.signal);
1049
+ try {
1050
+ const r = await it.next();
1051
+ if (r.done) {
1052
+ end();
1053
+ c.close();
1054
+ }
1055
+ else {
1056
+ c.enqueue(r.value);
1057
+ }
1058
+ }
1059
+ catch (err) {
1060
+ end();
1061
+ c.error(ctl.signal.aborted ? ctl.signal.reason : err);
1062
+ }
1063
+ },
1064
+ cancel(reason) {
1065
+ ctl.abort(reason ?? new TeepinError('the read was cancelled'));
1066
+ end();
1067
+ },
1068
+ }, { highWaterMark: 0 });
1069
+ }
1070
+ /** A stream's bytes, joined; its error, as it is. */
1071
+ export async function drain(body) {
1072
+ const reader = body.getReader();
1073
+ const parts = [];
1074
+ try {
1075
+ for (;;) {
1076
+ const { done, value } = await reader.read();
1077
+ if (done)
1078
+ break;
1079
+ parts.push(value);
1080
+ }
1081
+ }
1082
+ finally {
1083
+ reader.releaseLock();
1084
+ }
1085
+ return concat(parts);
1086
+ }
1087
+ const closedError = () => new TeepinError('the object file is closed');
1088
+ /**
1089
+ * An object open for reading at any offset (TEEPIN-STORAGE-DESIGN.md section 22.5): read(offset, length), size, info
1090
+ * and close().
1091
+ *
1092
+ * Reading straight on (each read starting where the last ended) fetches a block at a time (one window, 64 MiB on the
1093
+ * storage network) and the next prefetch blocks ahead, each signed as it is fetched; a read somewhere else fetches
1094
+ * only what it needs, in whole segments. Up to prefetch + 2 blocks are held. Every byte is verified before it is read,
1095
+ * and every block is read from the version first opened: ObjectChangedError if the object is replaced. Close it when
1096
+ * done: that stops its fetches and zeroes its copy of the data key.
1097
+ */
1098
+ export class ObjectFile {
1099
+ /** The object's size in bytes. */
1100
+ size;
1101
+ /** The object's details, as headObject gives them. */
1102
+ info;
1103
+ fetch;
1104
+ block;
1105
+ align;
1106
+ prefetch;
1107
+ onClose;
1108
+ signal;
1109
+ ctl;
1110
+ unlink;
1111
+ blocks = new Map(); // oldest first
1112
+ end = 0; // where the last read ended: reading on from here is reading straight on
1113
+ isClosed = false;
1114
+ constructor(fetch, size, block, align, info, prefetch = 2, onClose, signal) {
1115
+ this.fetch = fetch;
1116
+ this.size = size;
1117
+ this.block = Math.max(1, block);
1118
+ this.align = Math.max(1, align);
1119
+ this.prefetch = Math.max(0, Math.floor(prefetch) || 0);
1120
+ this.info = info;
1121
+ this.onClose = onClose;
1122
+ this.signal = signal;
1123
+ const { ctl, unlink } = linked(signal);
1124
+ this.ctl = ctl;
1125
+ this.unlink = unlink;
1126
+ }
1127
+ get closed() {
1128
+ return this.isClosed;
1129
+ }
1130
+ /** Up to length bytes from offset: fewer only at the object's end, none past it. */
1131
+ async read(offset, length) {
1132
+ this.checkOpen();
1133
+ if (!isInt(offset) || !isInt(length) || offset < 0 || length < 0) {
1134
+ throw new TeepinError(`read(${offset}, ${length}): the offset and length must be whole numbers, 0 or more`);
1135
+ }
1136
+ const n = Math.min(length, Math.max(0, this.size - offset));
1137
+ if (n === 0)
1138
+ return new Uint8Array(0);
1139
+ try {
1140
+ const straight = offset === this.end;
1141
+ // A jump fetches only what it reads, unless the object is read whole anyway (packed): then its one block is
1142
+ // cached like any other.
1143
+ const whole = this.align >= this.block;
1144
+ let data;
1145
+ if (!straight && !whole && n < this.block / 2 && !this.blocks.has(Math.floor(offset / this.block))) {
1146
+ data = await this.some(offset, n);
1147
+ }
1148
+ else {
1149
+ const out = [];
1150
+ let pos = offset;
1151
+ let left = n;
1152
+ while (left > 0) {
1153
+ const i = Math.floor(pos / this.block);
1154
+ const blk = await this.get(i);
1155
+ const off = pos - i * this.block;
1156
+ const piece = blk.subarray(off, off + left);
1157
+ if (!piece.length)
1158
+ throw new DataIntegrityError('the object is shorter than its size', 'truncated');
1159
+ out.push(piece);
1160
+ pos += piece.length;
1161
+ left -= piece.length;
1162
+ }
1163
+ this.ahead(Math.floor(pos / this.block));
1164
+ data = out.length === 1 ? out[0].slice() : concat(out);
1165
+ }
1166
+ this.checkOpen();
1167
+ this.end = offset + data.length;
1168
+ return data;
1169
+ }
1170
+ catch (err) {
1171
+ if (this.signal?.aborted)
1172
+ throw this.signal.reason;
1173
+ if (this.isClosed)
1174
+ throw closedError();
1175
+ throw err;
1176
+ }
1177
+ }
1178
+ /** Stops every fetch and zeroes this file's copy of the data key. Reads after it fail. */
1179
+ close() {
1180
+ if (this.isClosed)
1181
+ return;
1182
+ this.isClosed = true;
1183
+ this.ctl.abort(closedError());
1184
+ this.blocks.clear();
1185
+ this.unlink();
1186
+ this.onClose?.();
1187
+ }
1188
+ checkOpen() {
1189
+ if (this.signal?.aborted)
1190
+ throw this.signal.reason;
1191
+ if (this.isClosed)
1192
+ throw closedError();
1193
+ }
1194
+ /** Block i's fetch, started if it isn't, and made the newest; the oldest past the bound are dropped. */
1195
+ submit(i) {
1196
+ let e = this.blocks.get(i);
1197
+ if (e) {
1198
+ this.blocks.delete(i);
1199
+ }
1200
+ else {
1201
+ const a = i * this.block;
1202
+ const b = Math.min(a + this.block, this.size);
1203
+ const { ctl, unlink } = linked(this.ctl.signal);
1204
+ const promise = (async () => {
1205
+ try {
1206
+ return await this.fetch(a, b, ctl.signal);
1207
+ }
1208
+ finally {
1209
+ unlink();
1210
+ }
1211
+ })();
1212
+ const entry = { promise, ctl, readers: 0 };
1213
+ // A failed block is fetched again by the next read that needs it (and a prefetch nobody waits for fails quietly).
1214
+ promise.catch(() => {
1215
+ if (this.blocks.get(i) === entry)
1216
+ this.blocks.delete(i);
1217
+ });
1218
+ e = entry;
1219
+ }
1220
+ this.blocks.set(i, e);
1221
+ while (this.blocks.size > this.prefetch + 2) {
1222
+ const [oldest, old] = this.blocks.entries().next().value;
1223
+ this.blocks.delete(oldest);
1224
+ if (old.readers === 0)
1225
+ old.ctl.abort(new TeepinError('dropped from the read-ahead'));
1226
+ }
1227
+ return e;
1228
+ }
1229
+ async get(i) {
1230
+ const e = this.submit(i);
1231
+ e.readers++;
1232
+ try {
1233
+ return await e.promise;
1234
+ }
1235
+ finally {
1236
+ e.readers--;
1237
+ }
1238
+ }
1239
+ ahead(i) {
1240
+ const last = Math.floor((this.size - 1) / this.block);
1241
+ for (let j = i; j <= Math.min(i + this.prefetch, last); j++)
1242
+ if (j * this.block < this.size)
1243
+ this.submit(j);
1244
+ }
1245
+ /** n bytes from pos, fetched alone, whole segments only. */
1246
+ async some(pos, n) {
1247
+ const a = Math.floor(pos / this.align) * this.align;
1248
+ const b = Math.min(this.size, Math.ceil((pos + n) / this.align) * this.align);
1249
+ const data = await this.fetch(a, b, this.ctl.signal);
1250
+ return data.slice(pos - a, pos - a + n);
1251
+ }
1252
+ }
1253
+ /** The error a batch read gives one object, as the single route would throw it. */
1254
+ function sessionError(key, err) {
1255
+ const code = str(err.code);
1256
+ const message = `${key}: ${str(err.error) || code.replaceAll('_', ' ')}`;
1257
+ if (code === 'key_erased')
1258
+ return new KeyErasedError({ statusCode: 410, message, code });
1259
+ if (code === 'object_not_found')
1260
+ return new NotFoundError({ statusCode: 404, message, code });
1261
+ return new APIError({ statusCode: 500, message, code });
1262
+ }
1263
+ /** Stored bytes to ask for from start: whole segments within cap (the header too from 0), at most to end. */
1264
+ function aligned(start, end, cap) {
1265
+ let want;
1266
+ if (start === 0) {
1267
+ const n = Math.max(1, Math.floor((cap - tws1.HEADER_SIZE) / tws1.STORED_SEGMENT_SIZE));
1268
+ want = tws1.HEADER_SIZE + n * tws1.STORED_SEGMENT_SIZE;
1269
+ }
1270
+ else {
1271
+ want = Math.max(1, Math.floor(cap / tws1.STORED_SEGMENT_SIZE)) * tws1.STORED_SEGMENT_SIZE;
1272
+ }
1273
+ return Math.min(end - start, want);
1274
+ }
1275
+ //# sourceMappingURL=reads.js.map