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.
- package/.agents/skills/mlclaw/SKILL.md +214 -0
- package/.agents/skills/mlclaw/agents/openai.yaml +4 -0
- package/.gitattributes +35 -0
- package/Dockerfile +45 -0
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/assets/mlclaw.svg +143 -0
- package/dist/hf-state-sync.js +9532 -0
- package/dist/mlclaw-space-runtime.js +5010 -0
- package/dist/mlclaw.mjs +16502 -0
- package/entrypoint.sh +87 -0
- package/mlclaw.ps1 +108 -0
- package/mlclaw.sh +117 -0
- package/openclaw.default.json +67 -0
- package/package.json +66 -0
- package/scripts/configure-huggingface-model.mjs +86 -0
- package/scripts/configure-telegram.mjs +55 -0
- package/scripts/report-telegram-probe.mjs +18 -0
- package/space/README.md +42 -0
- package/src/hf-bucket-client/client.ts +217 -0
- package/src/hf-state-sync/archive.ts +137 -0
- package/src/hf-state-sync/cli.ts +92 -0
- package/src/hf-state-sync/hub.ts +81 -0
- package/src/hf-state-sync/manifest.ts +67 -0
- package/src/hf-state-sync/paths.ts +73 -0
- package/src/hf-state-sync/restore.ts +109 -0
- package/src/hf-state-sync/snapshot.ts +133 -0
- package/src/hf-state-sync/sqlite.ts +57 -0
- package/src/hf-state-sync/supervise.ts +256 -0
- package/src/vendor/hfjs-xet/error.ts +52 -0
- package/src/vendor/hfjs-xet/types/public.ts +207 -0
- package/src/vendor/hfjs-xet/utils/ChunkCache.ts +102 -0
- package/src/vendor/hfjs-xet/utils/RangeList.ts +182 -0
- package/src/vendor/hfjs-xet/utils/SplicedBlob.ts +249 -0
- package/src/vendor/hfjs-xet/utils/XetBlob.ts +732 -0
- package/src/vendor/hfjs-xet/utils/checkCredentials.ts +21 -0
- package/src/vendor/hfjs-xet/utils/combineUint8Arrays.ts +13 -0
- package/src/vendor/hfjs-xet/utils/createXorbs.ts +782 -0
- package/src/vendor/hfjs-xet/utils/shardParser.ts +152 -0
- package/src/vendor/hfjs-xet/utils/sum.ts +9 -0
- package/src/vendor/hfjs-xet/utils/uploadShards.ts +443 -0
- package/src/vendor/hfjs-xet/utils/xetWriteToken.ts +101 -0
- package/src/vendor/hfjs-xet/vendor/lz4js/index.ts +540 -0
- package/src/vendor/hfjs-xet/vendor/lz4js/util.ts +57 -0
- package/src/vendor/hfjs-xet/vendor/lz4js/xxh32.ts +99 -0
- package/src/vendor/hfjs-xet/vendor/type-fest/basic.ts +34 -0
- 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
|
+
}
|