mlclaw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.agents/skills/mlclaw/SKILL.md +214 -0
  2. package/.agents/skills/mlclaw/agents/openai.yaml +4 -0
  3. package/.gitattributes +35 -0
  4. package/Dockerfile +45 -0
  5. package/LICENSE +21 -0
  6. package/README.md +206 -0
  7. package/assets/mlclaw.svg +143 -0
  8. package/dist/hf-state-sync.js +9532 -0
  9. package/dist/mlclaw-space-runtime.js +5010 -0
  10. package/dist/mlclaw.mjs +16502 -0
  11. package/entrypoint.sh +87 -0
  12. package/mlclaw.ps1 +108 -0
  13. package/mlclaw.sh +117 -0
  14. package/openclaw.default.json +67 -0
  15. package/package.json +66 -0
  16. package/scripts/configure-huggingface-model.mjs +86 -0
  17. package/scripts/configure-telegram.mjs +55 -0
  18. package/scripts/report-telegram-probe.mjs +18 -0
  19. package/space/README.md +42 -0
  20. package/src/hf-bucket-client/client.ts +217 -0
  21. package/src/hf-state-sync/archive.ts +137 -0
  22. package/src/hf-state-sync/cli.ts +92 -0
  23. package/src/hf-state-sync/hub.ts +81 -0
  24. package/src/hf-state-sync/manifest.ts +67 -0
  25. package/src/hf-state-sync/paths.ts +73 -0
  26. package/src/hf-state-sync/restore.ts +109 -0
  27. package/src/hf-state-sync/snapshot.ts +133 -0
  28. package/src/hf-state-sync/sqlite.ts +57 -0
  29. package/src/hf-state-sync/supervise.ts +256 -0
  30. package/src/vendor/hfjs-xet/error.ts +52 -0
  31. package/src/vendor/hfjs-xet/types/public.ts +207 -0
  32. package/src/vendor/hfjs-xet/utils/ChunkCache.ts +102 -0
  33. package/src/vendor/hfjs-xet/utils/RangeList.ts +182 -0
  34. package/src/vendor/hfjs-xet/utils/SplicedBlob.ts +249 -0
  35. package/src/vendor/hfjs-xet/utils/XetBlob.ts +732 -0
  36. package/src/vendor/hfjs-xet/utils/checkCredentials.ts +21 -0
  37. package/src/vendor/hfjs-xet/utils/combineUint8Arrays.ts +13 -0
  38. package/src/vendor/hfjs-xet/utils/createXorbs.ts +782 -0
  39. package/src/vendor/hfjs-xet/utils/shardParser.ts +152 -0
  40. package/src/vendor/hfjs-xet/utils/sum.ts +9 -0
  41. package/src/vendor/hfjs-xet/utils/uploadShards.ts +443 -0
  42. package/src/vendor/hfjs-xet/utils/xetWriteToken.ts +101 -0
  43. package/src/vendor/hfjs-xet/vendor/lz4js/index.ts +540 -0
  44. package/src/vendor/hfjs-xet/vendor/lz4js/util.ts +57 -0
  45. package/src/vendor/hfjs-xet/vendor/lz4js/xxh32.ts +99 -0
  46. package/src/vendor/hfjs-xet/vendor/type-fest/basic.ts +34 -0
  47. package/tsconfig.json +16 -0
@@ -0,0 +1,732 @@
1
+ // @ts-nocheck -- vendored upstream code, kept verbatim; our strict tsconfig does not apply.
2
+ // Vendored from huggingface/huggingface.js@f8fdf6be (packages/hub/src/utils/XetBlob.ts), MIT License.
3
+ // Delete this directory when bucket support is upstreamed to @huggingface/hub.
4
+ import { createApiError } from "../error";
5
+ import type { CredentialsParams } from "../types/public";
6
+ import { checkCredentials } from "./checkCredentials";
7
+ import { combineUint8Arrays } from "./combineUint8Arrays";
8
+ import { decompress as lz4_decompress } from "../vendor/lz4js";
9
+ import { RangeList } from "./RangeList";
10
+
11
+ const JWT_SAFETY_PERIOD = 60_000;
12
+ const JWT_CACHE_SIZE = 1_000;
13
+
14
+ export interface XetReadToken {
15
+ accessToken: string;
16
+ casUrl: string;
17
+ exp: number;
18
+ }
19
+
20
+ type XetBlobCreateOptions = {
21
+ /**
22
+ * Custom fetch function to use instead of the default one, for example to use a proxy or edit headers.
23
+ */
24
+ fetch?: typeof fetch;
25
+ // URL to get the access token from
26
+ refreshUrl: string;
27
+ size: number;
28
+ listener?: (arg: { event: "read" } | { event: "progress"; progress: { read: number; total: number } }) => void;
29
+ internalLogging?: boolean;
30
+ /**
31
+ * Pre-fetched read token to avoid the refresh URL roundtrip.
32
+ */
33
+ readToken?: XetReadToken;
34
+ } & ({ hash: string; reconstructionUrl?: string } | { hash?: string; reconstructionUrl: string }) &
35
+ Partial<CredentialsParams>;
36
+
37
+ export interface ReconstructionInfo {
38
+ /**
39
+ * List of CAS blocks
40
+ */
41
+ terms: Array<{
42
+ /** Hash of the CAS block */
43
+ hash: string;
44
+ /** Total uncompressed length of data of the chunks from range.start to range.end - 1 */
45
+ unpacked_length: number;
46
+ /** Chunks. Eg start: 10, end: 100 = chunks 10-99 */
47
+ range: { start: number; end: number };
48
+ }>;
49
+
50
+ /**
51
+ * Dictionnary of CAS block hash => list of ranges in the block + url to fetch it
52
+ */
53
+ fetch_info: Record<
54
+ string,
55
+ Array<{
56
+ url: string;
57
+ /** Chunk range */
58
+ range: { start: number; end: number };
59
+ /**
60
+ * Byte range, when making the call to the URL.
61
+ *
62
+ * We assume that we're given non-overlapping ranges for each hash
63
+ */
64
+ url_range: { start: number; end: number };
65
+ }>
66
+ >;
67
+ /**
68
+ * When doing a range request, the offset into the term's uncompressed data. Can be multiple chunks' worth of data.
69
+ */
70
+ offset_into_first_range: number;
71
+ }
72
+
73
+ export enum XetChunkCompressionScheme {
74
+ None = 0,
75
+ LZ4 = 1,
76
+ ByteGroupingLZ4 = 2,
77
+ }
78
+
79
+ const compressionSchemeLabels: Record<XetChunkCompressionScheme, string> = {
80
+ [XetChunkCompressionScheme.None]: "None",
81
+ [XetChunkCompressionScheme.LZ4]: "LZ4",
82
+ [XetChunkCompressionScheme.ByteGroupingLZ4]: "ByteGroupingLZ4",
83
+ };
84
+
85
+ interface ChunkHeader {
86
+ version: number; // u8, 1 byte
87
+ compressed_length: number; // 3 * u8, 3 bytes
88
+ compression_scheme: XetChunkCompressionScheme; // u8, 1 byte
89
+ uncompressed_length: number; // 3 * u8, 3 bytes
90
+ }
91
+
92
+ export const XET_CHUNK_HEADER_BYTES = 8;
93
+
94
+ /**
95
+ * XetBlob is a blob implementation that fetches data directly from the Xet storage
96
+ */
97
+ export class XetBlob extends Blob {
98
+ fetch: typeof fetch;
99
+ accessToken?: string;
100
+ refreshUrl: string;
101
+ reconstructionUrl?: string;
102
+ hash?: string;
103
+ start = 0;
104
+ end = 0;
105
+ internalLogging = false;
106
+ reconstructionInfo: ReconstructionInfo | undefined;
107
+ listener: XetBlobCreateOptions["listener"];
108
+
109
+ constructor(params: XetBlobCreateOptions) {
110
+ super([]);
111
+
112
+ this.fetch = params.fetch ?? fetch.bind(globalThis);
113
+ this.accessToken = checkCredentials(params);
114
+ this.refreshUrl = params.refreshUrl;
115
+ this.end = params.size;
116
+ this.reconstructionUrl = params.reconstructionUrl;
117
+ this.hash = params.hash;
118
+ this.listener = params.listener;
119
+ this.internalLogging = params.internalLogging ?? false;
120
+
121
+ if (params.readToken) {
122
+ const key = cacheKey({ refreshUrl: this.refreshUrl, initialAccessToken: this.accessToken });
123
+ jwts.set(key, {
124
+ accessToken: params.readToken.accessToken,
125
+ expiresAt: new Date(params.readToken.exp * 1000),
126
+ casUrl: params.readToken.casUrl,
127
+ });
128
+ }
129
+ }
130
+
131
+ override get size(): number {
132
+ return this.end - this.start;
133
+ }
134
+
135
+ #clone() {
136
+ const blob = new XetBlob({
137
+ fetch: this.fetch,
138
+ hash: this.hash,
139
+ refreshUrl: this.refreshUrl,
140
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
141
+ reconstructionUrl: this.reconstructionUrl!,
142
+ size: this.size,
143
+ });
144
+
145
+ blob.accessToken = this.accessToken;
146
+ blob.start = this.start;
147
+ blob.end = this.end;
148
+ blob.reconstructionInfo = this.reconstructionInfo;
149
+ blob.listener = this.listener;
150
+ blob.internalLogging = this.internalLogging;
151
+
152
+ return blob;
153
+ }
154
+
155
+ override slice(start = 0, end = this.size): XetBlob {
156
+ if (start < 0 || end < 0) {
157
+ new TypeError("Unsupported negative start/end on XetBlob.slice");
158
+ }
159
+
160
+ const slice = this.#clone();
161
+
162
+ slice.start = this.start + start;
163
+ slice.end = Math.min(this.start + end, this.end);
164
+
165
+ if (slice.start !== this.start || slice.end !== this.end) {
166
+ slice.reconstructionInfo = undefined;
167
+ }
168
+
169
+ return slice;
170
+ }
171
+
172
+ #reconstructionInfoPromise?: Promise<ReconstructionInfo>;
173
+
174
+ #loadReconstructionInfo() {
175
+ if (this.#reconstructionInfoPromise) {
176
+ return this.#reconstructionInfoPromise;
177
+ }
178
+
179
+ this.#reconstructionInfoPromise = (async () => {
180
+ const connParams = await getAccessToken(this.accessToken, this.fetch, this.refreshUrl);
181
+
182
+ // debug(
183
+ // `curl '${connParams.casUrl}/v1/reconstructions/${this.hash}' -H 'Authorization: Bearer ${connParams.accessToken}'`
184
+ // );
185
+
186
+ const resp = await this.fetch(this.reconstructionUrl ?? `${connParams.casUrl}/v1/reconstructions/${this.hash}`, {
187
+ headers: {
188
+ Authorization: `Bearer ${connParams.accessToken}`,
189
+ Range: `bytes=${this.start}-${this.end - 1}`,
190
+ },
191
+ });
192
+
193
+ if (!resp.ok) {
194
+ throw await createApiError(resp);
195
+ }
196
+
197
+ this.reconstructionInfo = (await resp.json()) as ReconstructionInfo;
198
+
199
+ return this.reconstructionInfo;
200
+ })().finally(() => (this.#reconstructionInfoPromise = undefined));
201
+
202
+ return this.#reconstructionInfoPromise;
203
+ }
204
+
205
+ async #fetch(): Promise<ReadableStream<Uint8Array>> {
206
+ if (this.size === 0) {
207
+ return new ReadableStream<Uint8Array>({
208
+ start(controller) {
209
+ controller.close();
210
+ },
211
+ });
212
+ }
213
+
214
+ if (!this.reconstructionInfo) {
215
+ await this.#loadReconstructionInfo();
216
+ }
217
+
218
+ const rangeLists = new Map<string, RangeList<Uint8Array[]>>();
219
+
220
+ if (!this.reconstructionInfo) {
221
+ throw new Error("Failed to load reconstruction info");
222
+ }
223
+
224
+ for (const term of this.reconstructionInfo.terms) {
225
+ let rangeList = rangeLists.get(term.hash);
226
+ if (!rangeList) {
227
+ rangeList = new RangeList<Uint8Array[]>();
228
+ rangeLists.set(term.hash, rangeList);
229
+ }
230
+
231
+ rangeList.add(term.range.start, term.range.end);
232
+ }
233
+ const listener = this.listener;
234
+ const log = this.internalLogging ? (...args: unknown[]) => console.log(...args) : () => {};
235
+
236
+ async function* readData(
237
+ reconstructionInfo: ReconstructionInfo,
238
+ customFetch: typeof fetch,
239
+ maxBytes: number,
240
+ reloadReconstructionInfo: () => Promise<ReconstructionInfo>,
241
+ ) {
242
+ let totalBytesRead = 0;
243
+ let readBytesToSkip = reconstructionInfo.offset_into_first_range;
244
+
245
+ for (const term of reconstructionInfo.terms) {
246
+ if (totalBytesRead >= maxBytes) {
247
+ break;
248
+ }
249
+
250
+ const rangeList = rangeLists.get(term.hash);
251
+ if (!rangeList) {
252
+ throw new Error(`Failed to find range list for term ${term.hash}`);
253
+ }
254
+
255
+ {
256
+ const termRanges = rangeList.getRanges(term.range.start, term.range.end);
257
+
258
+ if (termRanges.every((range) => range.data)) {
259
+ log("all data available for term", term.hash, readBytesToSkip);
260
+ rangeLoop: for (const range of termRanges) {
261
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
262
+ for (let chunk of range.data!) {
263
+ if (readBytesToSkip) {
264
+ const skipped = Math.min(readBytesToSkip, chunk.byteLength);
265
+ chunk = chunk.slice(skipped);
266
+ readBytesToSkip -= skipped;
267
+ if (!chunk.byteLength) {
268
+ continue;
269
+ }
270
+ }
271
+ if (chunk.byteLength > maxBytes - totalBytesRead) {
272
+ chunk = chunk.slice(0, maxBytes - totalBytesRead);
273
+ }
274
+ totalBytesRead += chunk.byteLength;
275
+ // The stream consumer can decide to transfer ownership of the chunk, so we need to return a clone
276
+ // if there's more than one range for the same term
277
+ yield range.refCount > 1 ? chunk.slice() : chunk;
278
+ listener?.({ event: "progress", progress: { read: totalBytesRead, total: maxBytes } });
279
+
280
+ if (totalBytesRead >= maxBytes) {
281
+ break rangeLoop;
282
+ }
283
+ }
284
+ }
285
+ rangeList.remove(term.range.start, term.range.end);
286
+ continue;
287
+ }
288
+ }
289
+
290
+ let fetchInfo = reconstructionInfo.fetch_info[term.hash].find(
291
+ (info) => info.range.start <= term.range.start && info.range.end >= term.range.end,
292
+ );
293
+
294
+ if (!fetchInfo) {
295
+ throw new Error(
296
+ `Failed to find fetch info for term ${term.hash} and range ${term.range.start}-${term.range.end}`,
297
+ );
298
+ }
299
+
300
+ log("term", term);
301
+ log("fetchinfo", fetchInfo);
302
+ log("readBytesToSkip", readBytesToSkip);
303
+
304
+ let resp = await customFetch(fetchInfo.url, {
305
+ headers: {
306
+ Range: `bytes=${fetchInfo.url_range.start}-${fetchInfo.url_range.end}`,
307
+ },
308
+ });
309
+
310
+ if (resp.status === 403) {
311
+ // In case it's expired
312
+ reconstructionInfo = await reloadReconstructionInfo();
313
+ fetchInfo = reconstructionInfo.fetch_info[term.hash]?.find(
314
+ (info) => info.range.start <= term.range.start && info.range.end >= term.range.end,
315
+ );
316
+ if (!fetchInfo) {
317
+ throw new Error(
318
+ `Failed to find fetch info for term ${term.hash} and range ${term.range.start}-${term.range.end} after refresh`,
319
+ );
320
+ }
321
+ resp = await customFetch(fetchInfo.url, {
322
+ headers: {
323
+ Range: `bytes=${fetchInfo.url_range.start}-${fetchInfo.url_range.end}`,
324
+ },
325
+ });
326
+ }
327
+
328
+ if (!resp.ok) {
329
+ throw await createApiError(resp);
330
+ }
331
+
332
+ log(
333
+ "expected content length",
334
+ resp.headers.get("content-length"),
335
+ "range",
336
+ fetchInfo.url_range,
337
+ resp.headers.get("content-range"),
338
+ );
339
+
340
+ const reader = resp.body?.getReader();
341
+ if (!reader) {
342
+ throw new Error("Failed to get reader from response body");
343
+ }
344
+
345
+ let done = false;
346
+ let chunkIndex = fetchInfo.range.start;
347
+ const ranges = rangeList.getRanges(fetchInfo.range.start, fetchInfo.range.end);
348
+
349
+ let leftoverBytes: Uint8Array | undefined = undefined;
350
+ let totalFetchBytes = 0;
351
+
352
+ fetchData: while (!done && totalBytesRead < maxBytes) {
353
+ const result = await reader.read();
354
+ listener?.({ event: "read" });
355
+
356
+ done = result.done;
357
+
358
+ log("read", result.value?.byteLength, "bytes", "total read", totalBytesRead, "toSkip", readBytesToSkip);
359
+
360
+ if (!result.value) {
361
+ log("no data in result, cancelled", result);
362
+ continue;
363
+ }
364
+
365
+ totalFetchBytes += result.value.byteLength;
366
+
367
+ if (leftoverBytes) {
368
+ result.value = combineUint8Arrays(leftoverBytes, result.value);
369
+ leftoverBytes = undefined;
370
+ }
371
+
372
+ while (totalBytesRead < maxBytes && result.value?.byteLength) {
373
+ if (result.value.byteLength < 8) {
374
+ // We need 8 bytes to parse the chunk header
375
+ leftoverBytes = result.value;
376
+ continue fetchData;
377
+ }
378
+
379
+ const header = new DataView(result.value.buffer, result.value.byteOffset, XET_CHUNK_HEADER_BYTES);
380
+ const chunkHeader: ChunkHeader = {
381
+ version: header.getUint8(0),
382
+ compressed_length: header.getUint8(1) | (header.getUint8(2) << 8) | (header.getUint8(3) << 16),
383
+ compression_scheme: header.getUint8(4),
384
+ uncompressed_length: header.getUint8(5) | (header.getUint8(6) << 8) | (header.getUint8(7) << 16),
385
+ };
386
+
387
+ log("chunk header", chunkHeader, "to skip", readBytesToSkip);
388
+
389
+ if (chunkHeader.version !== 0) {
390
+ throw new Error(`Unsupported chunk version ${chunkHeader.version}`);
391
+ }
392
+
393
+ if (
394
+ chunkHeader.compression_scheme !== XetChunkCompressionScheme.None &&
395
+ chunkHeader.compression_scheme !== XetChunkCompressionScheme.LZ4 &&
396
+ chunkHeader.compression_scheme !== XetChunkCompressionScheme.ByteGroupingLZ4
397
+ ) {
398
+ throw new Error(
399
+ `Unsupported compression scheme ${
400
+ compressionSchemeLabels[chunkHeader.compression_scheme] ?? chunkHeader.compression_scheme
401
+ }`,
402
+ );
403
+ }
404
+
405
+ if (result.value.byteLength < chunkHeader.compressed_length + XET_CHUNK_HEADER_BYTES) {
406
+ // We need more data to read the full chunk
407
+ leftoverBytes = result.value;
408
+ continue fetchData;
409
+ }
410
+
411
+ result.value = result.value.slice(XET_CHUNK_HEADER_BYTES);
412
+
413
+ let uncompressed =
414
+ chunkHeader.compression_scheme === XetChunkCompressionScheme.LZ4
415
+ ? lz4_decompress(result.value.slice(0, chunkHeader.compressed_length), chunkHeader.uncompressed_length)
416
+ : chunkHeader.compression_scheme === XetChunkCompressionScheme.ByteGroupingLZ4
417
+ ? bg4_regroup_bytes(
418
+ lz4_decompress(
419
+ result.value.slice(0, chunkHeader.compressed_length),
420
+ chunkHeader.uncompressed_length,
421
+ ),
422
+ )
423
+ : result.value.slice(0, chunkHeader.compressed_length);
424
+
425
+ const range = ranges.find((range) => chunkIndex >= range.start && chunkIndex < range.end);
426
+ const shouldYield = chunkIndex >= term.range.start && chunkIndex < term.range.end;
427
+ const minRefCountToStore = shouldYield ? 2 : 1;
428
+ let stored = false;
429
+
430
+ // Assuming non-overlapping fetch_info ranges for the same hash
431
+ if (range && range.refCount >= minRefCountToStore) {
432
+ range.data ??= [];
433
+ range.data.push(uncompressed);
434
+ stored = true;
435
+ }
436
+
437
+ if (shouldYield) {
438
+ if (readBytesToSkip) {
439
+ const skipped = Math.min(readBytesToSkip, uncompressed.byteLength);
440
+ uncompressed = uncompressed.slice(readBytesToSkip);
441
+ readBytesToSkip -= skipped;
442
+ }
443
+
444
+ if (uncompressed.byteLength > maxBytes - totalBytesRead) {
445
+ uncompressed = uncompressed.slice(0, maxBytes - totalBytesRead);
446
+ }
447
+
448
+ if (uncompressed.byteLength) {
449
+ log(
450
+ "yield",
451
+ uncompressed.byteLength,
452
+ "bytes",
453
+ result.value.byteLength,
454
+ "total read",
455
+ totalBytesRead,
456
+ stored,
457
+ );
458
+ totalBytesRead += uncompressed.byteLength;
459
+ yield stored ? uncompressed.slice() : uncompressed;
460
+ listener?.({ event: "progress", progress: { read: totalBytesRead, total: maxBytes } });
461
+ }
462
+ }
463
+
464
+ chunkIndex++;
465
+ result.value = result.value.slice(chunkHeader.compressed_length);
466
+ }
467
+ }
468
+
469
+ if (
470
+ done &&
471
+ totalBytesRead < maxBytes &&
472
+ totalFetchBytes < fetchInfo.url_range.end - fetchInfo.url_range.start + 1
473
+ ) {
474
+ log("done", done, "total read", totalBytesRead, maxBytes, totalFetchBytes);
475
+ log("failed to fetch all data for term", term.hash);
476
+ throw new Error(
477
+ `Failed to fetch all data for term ${term.hash}, fetched ${totalFetchBytes} bytes out of ${
478
+ fetchInfo.url_range.end - fetchInfo.url_range.start + 1
479
+ }`,
480
+ );
481
+ }
482
+
483
+ log("done", done, "total read", totalBytesRead, maxBytes, totalFetchBytes);
484
+
485
+ // Release the reader
486
+ log("cancel reader");
487
+ await reader.cancel();
488
+ }
489
+ }
490
+
491
+ const iterator = readData(
492
+ this.reconstructionInfo,
493
+ this.fetch,
494
+ this.end - this.start,
495
+ this.#loadReconstructionInfo.bind(this),
496
+ );
497
+
498
+ // todo: when Chrome/Safari support it, use ReadableStream.from(readData)
499
+ return new ReadableStream<Uint8Array>(
500
+ {
501
+ // todo: when Safari supports it, type controller as ReadableByteStreamController
502
+ async pull(controller) {
503
+ const result = await iterator.next();
504
+
505
+ if (result.value) {
506
+ controller.enqueue(result.value);
507
+ }
508
+
509
+ if (result.done) {
510
+ controller.close();
511
+ }
512
+ },
513
+ type: "bytes",
514
+ // todo: when Safari supports it, add autoAllocateChunkSize param
515
+ },
516
+ // todo : use ByteLengthQueuingStrategy when there's good support for it, currently in Node.js it fails due to size being a function
517
+ {
518
+ highWaterMark: 1_000, // 1_000 chunks for ~1MB of RAM
519
+ },
520
+ );
521
+ }
522
+
523
+ override async arrayBuffer(): Promise<ArrayBuffer> {
524
+ const result = await this.#fetch();
525
+
526
+ return new Response(result).arrayBuffer();
527
+ }
528
+
529
+ override async text(): Promise<string> {
530
+ const result = await this.#fetch();
531
+
532
+ return new Response(result).text();
533
+ }
534
+
535
+ async response(): Promise<Response> {
536
+ const result = await this.#fetch();
537
+
538
+ return new Response(result);
539
+ }
540
+
541
+ override stream(): ReturnType<Blob["stream"]> {
542
+ const stream = new TransformStream();
543
+
544
+ this.#fetch()
545
+ .then((response) => response.pipeThrough(stream))
546
+ .catch((error) => stream.writable.abort(error.message));
547
+
548
+ return stream.readable;
549
+ }
550
+ }
551
+
552
+ const jwtPromises: Map<string, Promise<{ accessToken: string; casUrl: string }>> = new Map();
553
+ /**
554
+ * Cache to store JWTs, to avoid making many auth requests when downloading multiple files from the same repo
555
+ */
556
+ const jwts: Map<
557
+ string,
558
+ {
559
+ accessToken: string;
560
+ expiresAt: Date;
561
+ casUrl: string;
562
+ }
563
+ > = new Map();
564
+
565
+ function cacheKey(params: { refreshUrl: string; initialAccessToken: string | undefined }): string {
566
+ return JSON.stringify([params.refreshUrl, params.initialAccessToken]);
567
+ }
568
+
569
+ // exported for testing purposes
570
+ export function bg4_regroup_bytes(bytes: Uint8Array): Uint8Array {
571
+ // python code
572
+
573
+ // split = len(x) // 4
574
+ // rem = len(x) % 4
575
+ // g1_pos = split + (1 if rem >= 1 else 0)
576
+ // g2_pos = g1_pos + split + (1 if rem >= 2 else 0)
577
+ // g3_pos = g2_pos + split + (1 if rem == 3 else 0)
578
+ // ret = bytearray(len(x))
579
+ // ret[0::4] = x[:g1_pos]
580
+ // ret[1::4] = x[g1_pos:g2_pos]
581
+ // ret[2::4] = x[g2_pos:g3_pos]
582
+ // ret[3::4] = x[g3_pos:]
583
+
584
+ // todo: optimize to do it in-place
585
+
586
+ const split = Math.floor(bytes.byteLength / 4);
587
+ const rem = bytes.byteLength % 4;
588
+ const g1_pos = split + (rem >= 1 ? 1 : 0);
589
+ const g2_pos = g1_pos + split + (rem >= 2 ? 1 : 0);
590
+ const g3_pos = g2_pos + split + (rem == 3 ? 1 : 0);
591
+
592
+ const ret = new Uint8Array(bytes.byteLength);
593
+ for (let i = 0, j = 0; i < bytes.byteLength; i += 4, j++) {
594
+ ret[i] = bytes[j];
595
+ }
596
+
597
+ for (let i = 1, j = g1_pos; i < bytes.byteLength; i += 4, j++) {
598
+ ret[i] = bytes[j];
599
+ }
600
+
601
+ for (let i = 2, j = g2_pos; i < bytes.byteLength; i += 4, j++) {
602
+ ret[i] = bytes[j];
603
+ }
604
+
605
+ for (let i = 3, j = g3_pos; i < bytes.byteLength; i += 4, j++) {
606
+ ret[i] = bytes[j];
607
+ }
608
+
609
+ return ret;
610
+
611
+ // alternative implementation (to benchmark which one is faster)
612
+ // for (let i = 0; i < bytes.byteLength - 3; i += 4) {
613
+ // ret[i] = bytes[i / 4];
614
+ // ret[i + 1] = bytes[g1_pos + i / 4];
615
+ // ret[i + 2] = bytes[g2_pos + i / 4];
616
+ // ret[i + 3] = bytes[g3_pos + i / 4];
617
+ // }
618
+
619
+ // if (rem === 1) {
620
+ // ret[bytes.byteLength - 1] = bytes[g1_pos - 1];
621
+ // } else if (rem === 2) {
622
+ // ret[bytes.byteLength - 2] = bytes[g1_pos - 1];
623
+ // ret[bytes.byteLength - 1] = bytes[g2_pos - 1];
624
+ // } else if (rem === 3) {
625
+ // ret[bytes.byteLength - 3] = bytes[g1_pos - 1];
626
+ // ret[bytes.byteLength - 2] = bytes[g2_pos - 1];
627
+ // ret[bytes.byteLength - 1] = bytes[g3_pos - 1];
628
+ // }
629
+ }
630
+
631
+ export function bg4_split_bytes(bytes: Uint8Array): Uint8Array {
632
+ // This function does the opposite of bg4_regroup_bytes
633
+ // It takes interleaved bytes and groups them by 4
634
+
635
+ const ret = new Uint8Array(bytes.byteLength);
636
+ const split = Math.floor(bytes.byteLength / 4);
637
+ const rem = bytes.byteLength % 4;
638
+
639
+ // Calculate group positions in the output array
640
+ const g1_pos = split + (rem >= 1 ? 1 : 0);
641
+ const g2_pos = g1_pos + split + (rem >= 2 ? 1 : 0);
642
+ const g3_pos = g2_pos + split + (rem == 3 ? 1 : 0);
643
+
644
+ // Extract every 4th byte starting from position 0, 1, 2, 3
645
+ // and place them in their respective groups
646
+ for (let i = 0, j = 0; i < bytes.byteLength; i += 4, j++) {
647
+ ret[j] = bytes[i];
648
+ }
649
+
650
+ for (let i = 1, j = g1_pos; i < bytes.byteLength; i += 4, j++) {
651
+ ret[j] = bytes[i];
652
+ }
653
+
654
+ for (let i = 2, j = g2_pos; i < bytes.byteLength; i += 4, j++) {
655
+ ret[j] = bytes[i];
656
+ }
657
+
658
+ for (let i = 3, j = g3_pos; i < bytes.byteLength; i += 4, j++) {
659
+ ret[j] = bytes[i];
660
+ }
661
+
662
+ return ret;
663
+ }
664
+
665
+ async function getAccessToken(
666
+ initialAccessToken: string | undefined,
667
+ customFetch: typeof fetch,
668
+ refreshUrl: string,
669
+ ): Promise<{ accessToken: string; casUrl: string }> {
670
+ const key = cacheKey({ refreshUrl, initialAccessToken });
671
+
672
+ const jwt = jwts.get(key);
673
+
674
+ if (jwt && jwt.expiresAt > new Date(Date.now() + JWT_SAFETY_PERIOD)) {
675
+ return { accessToken: jwt.accessToken, casUrl: jwt.casUrl };
676
+ }
677
+
678
+ // If we already have a promise for this repo, return it
679
+ const existingPromise = jwtPromises.get(key);
680
+ if (existingPromise) {
681
+ return existingPromise;
682
+ }
683
+
684
+ const promise = (async () => {
685
+ const resp = await customFetch(refreshUrl, {
686
+ headers: {
687
+ ...(initialAccessToken
688
+ ? {
689
+ Authorization: `Bearer ${initialAccessToken}`,
690
+ }
691
+ : {}),
692
+ },
693
+ });
694
+
695
+ if (!resp.ok) {
696
+ throw new Error(`Failed to get JWT token: ${resp.status} ${await resp.text()}`);
697
+ }
698
+
699
+ const json: { accessToken: string; casUrl: string; exp: number } = await resp.json();
700
+ const jwt = {
701
+ accessToken: json.accessToken,
702
+ expiresAt: new Date(json.exp * 1000),
703
+ casUrl: json.casUrl,
704
+ };
705
+
706
+ jwtPromises.delete(key);
707
+
708
+ for (const [key, value] of jwts.entries()) {
709
+ if (value.expiresAt < new Date(Date.now() + JWT_SAFETY_PERIOD)) {
710
+ jwts.delete(key);
711
+ } else {
712
+ break;
713
+ }
714
+ }
715
+ if (jwts.size >= JWT_CACHE_SIZE) {
716
+ const keyToDelete = jwts.keys().next().value;
717
+ if (keyToDelete) {
718
+ jwts.delete(keyToDelete);
719
+ }
720
+ }
721
+ jwts.set(key, jwt);
722
+
723
+ return {
724
+ accessToken: json.accessToken,
725
+ casUrl: json.casUrl,
726
+ };
727
+ })();
728
+
729
+ jwtPromises.set(key, promise);
730
+
731
+ return promise;
732
+ }