@ai-matrx/data 0.15.1 → 0.16.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/CHANGELOG.md +11 -0
- package/dist/files.cjs +175 -0
- package/dist/files.cjs.map +1 -1
- package/dist/files.d.cts +31 -0
- package/dist/files.d.ts +31 -0
- package/dist/files.js +175 -0
- package/dist/files.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0
|
|
4
|
+
|
|
5
|
+
- `MatrxFilesClient.healLoadError(ref, {src, attempts})` — the heal ladder
|
|
6
|
+
behind `@ai-matrx/media` 0.6's `MediaClient.healLoadError`: bearer byte
|
|
7
|
+
fetch on the primary files host, then on every other session base
|
|
8
|
+
(`FilesSession.bases()`), every attempt timed and recorded, root cause
|
|
9
|
+
named by `diagnoseHeal` (`files/heal.ts`). Never throws.
|
|
10
|
+
- `FilesHealLane` / `FilesHealAttempt` / `FilesHealDiagnosis` /
|
|
11
|
+
`FilesHealResult` — structural twins of the media types.
|
|
12
|
+
- `FilesSession.bases()` — the hosts the session lane mints on.
|
|
13
|
+
|
|
3
14
|
## 0.15.1
|
|
4
15
|
|
|
5
16
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
package/dist/files.cjs
CHANGED
|
@@ -347,12 +347,165 @@ function createFilesSession(options) {
|
|
|
347
347
|
}
|
|
348
348
|
return { secretSource, status };
|
|
349
349
|
},
|
|
350
|
+
bases() {
|
|
351
|
+
return [...new Set(options.resolveBases())];
|
|
352
|
+
},
|
|
350
353
|
reset() {
|
|
351
354
|
stateByBase.clear();
|
|
352
355
|
}
|
|
353
356
|
};
|
|
354
357
|
}
|
|
355
358
|
|
|
359
|
+
// src/files/heal.ts
|
|
360
|
+
function originOf(base) {
|
|
361
|
+
try {
|
|
362
|
+
return new URL(base).origin;
|
|
363
|
+
} catch {
|
|
364
|
+
return base;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async function runByteLane(lane, base, input) {
|
|
368
|
+
const now = input.now ?? Date.now;
|
|
369
|
+
const started = now();
|
|
370
|
+
const host = originOf(base);
|
|
371
|
+
if (!input.fileId) {
|
|
372
|
+
return {
|
|
373
|
+
attempt: {
|
|
374
|
+
lane,
|
|
375
|
+
outcome: "skipped",
|
|
376
|
+
ms: 0,
|
|
377
|
+
host,
|
|
378
|
+
detail: "no file id \u2014 the source is not one of our durable file URLs"
|
|
379
|
+
},
|
|
380
|
+
src: null
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
let response;
|
|
384
|
+
try {
|
|
385
|
+
response = await input.fetchBytes(base, input.fileId);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
return {
|
|
388
|
+
attempt: {
|
|
389
|
+
lane,
|
|
390
|
+
outcome: "network-error",
|
|
391
|
+
ms: now() - started,
|
|
392
|
+
host,
|
|
393
|
+
detail: err instanceof Error ? err.message : String(err ?? "fetch failed")
|
|
394
|
+
},
|
|
395
|
+
src: null
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
if (!response.ok) {
|
|
399
|
+
return {
|
|
400
|
+
attempt: {
|
|
401
|
+
lane,
|
|
402
|
+
outcome: "http-error",
|
|
403
|
+
status: response.status,
|
|
404
|
+
ms: now() - started,
|
|
405
|
+
host,
|
|
406
|
+
detail: `GET /files/{id}/download \u2192 HTTP ${response.status}`
|
|
407
|
+
},
|
|
408
|
+
src: null
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
const blob = await response.blob();
|
|
413
|
+
const src = input.cacheBlob(input.fileId, blob);
|
|
414
|
+
return {
|
|
415
|
+
attempt: {
|
|
416
|
+
lane,
|
|
417
|
+
outcome: "ok",
|
|
418
|
+
status: response.status,
|
|
419
|
+
ms: now() - started,
|
|
420
|
+
host,
|
|
421
|
+
detail: `${blob.size} bytes via bearer token \u2192 object URL`
|
|
422
|
+
},
|
|
423
|
+
src
|
|
424
|
+
};
|
|
425
|
+
} catch (err) {
|
|
426
|
+
return {
|
|
427
|
+
attempt: {
|
|
428
|
+
lane,
|
|
429
|
+
outcome: "network-error",
|
|
430
|
+
ms: now() - started,
|
|
431
|
+
host,
|
|
432
|
+
detail: `body read failed: ${err instanceof Error ? err.message : String(err)}`
|
|
433
|
+
},
|
|
434
|
+
src: null
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function diagnoseHeal(attempts, mintStatus) {
|
|
439
|
+
const lane1 = attempts.find((a) => a.lane === "session-refresh-retry");
|
|
440
|
+
const bearer = attempts.find((a) => a.lane === "bearer-blob");
|
|
441
|
+
const alternate = attempts.find((a) => a.lane === "bearer-blob-alternate-host");
|
|
442
|
+
const healedBy = attempts.find((a) => a.outcome === "ok");
|
|
443
|
+
if (healedBy?.lane === "bearer-blob") {
|
|
444
|
+
if (lane1 && mintStatus === void 0) return "cookie-not-sent-by-browser";
|
|
445
|
+
if (mintStatus !== void 0) return "cookie-mint-failed";
|
|
446
|
+
return "unknown";
|
|
447
|
+
}
|
|
448
|
+
if (healedBy?.lane === "bearer-blob-alternate-host") {
|
|
449
|
+
if (bearer && (bearer.outcome === "network-error" || (bearer.status ?? 0) >= 500)) {
|
|
450
|
+
return "primary-host-down-alternate-served";
|
|
451
|
+
}
|
|
452
|
+
return "unknown";
|
|
453
|
+
}
|
|
454
|
+
const byteLanes = [bearer, alternate].filter(Boolean);
|
|
455
|
+
if (byteLanes.some((a) => a.status === 401)) return "bearer-rejected";
|
|
456
|
+
if (byteLanes.some((a) => a.status === 403)) return "access-denied";
|
|
457
|
+
if (byteLanes.some((a) => a.status === 404)) return "file-missing";
|
|
458
|
+
if (byteLanes.some((a) => (a.status ?? 0) >= 500)) return "files-host-error";
|
|
459
|
+
if (byteLanes.length > 0 && byteLanes.every((a) => a.outcome === "network-error")) {
|
|
460
|
+
return "files-host-unreachable";
|
|
461
|
+
}
|
|
462
|
+
if (mintStatus !== void 0) return "cookie-mint-failed";
|
|
463
|
+
return "unknown";
|
|
464
|
+
}
|
|
465
|
+
var SUMMARY = {
|
|
466
|
+
"cookie-not-sent-by-browser": "the files-session cookie was minted (200) but the browser did not send it back on the media element request \u2014 third-party cookies are blocked in this browser (Chrome setting / Incognito, Safari, Brave)",
|
|
467
|
+
"cookie-mint-failed": "the files-session cookie mint itself failed",
|
|
468
|
+
"bearer-rejected": "the bearer token was refused (401): the sign-in on this tab is stale",
|
|
469
|
+
"access-denied": "this identity may not read this file (403)",
|
|
470
|
+
"file-missing": "the file is gone (404)",
|
|
471
|
+
"files-host-error": "the files host answered with a server error (5xx)",
|
|
472
|
+
"files-host-unreachable": "no files host answered (network/DNS/CORS)",
|
|
473
|
+
"primary-host-down-alternate-served": "the primary files host did not serve the bytes; the alternate host did",
|
|
474
|
+
unknown: "the attempt pattern matches no known cause"
|
|
475
|
+
};
|
|
476
|
+
async function healLoadErrorLadder(input) {
|
|
477
|
+
const attempts = [...input.priorAttempts];
|
|
478
|
+
let src = null;
|
|
479
|
+
let healedBy;
|
|
480
|
+
const primary = await runByteLane("bearer-blob", input.primaryBase, input);
|
|
481
|
+
attempts.push(primary.attempt);
|
|
482
|
+
if (primary.src) {
|
|
483
|
+
src = primary.src;
|
|
484
|
+
healedBy = "bearer-blob";
|
|
485
|
+
} else {
|
|
486
|
+
for (const base of input.alternateBases) {
|
|
487
|
+
const alt = await runByteLane("bearer-blob-alternate-host", base, input);
|
|
488
|
+
attempts.push(alt.attempt);
|
|
489
|
+
if (alt.src) {
|
|
490
|
+
src = alt.src;
|
|
491
|
+
healedBy = "bearer-blob-alternate-host";
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const diagnosis = diagnoseHeal(attempts, input.mintStatus);
|
|
497
|
+
const cause = SUMMARY[diagnosis];
|
|
498
|
+
const served = healedBy ? `served via ${healedBy} in ${attempts.find((a) => a.outcome === "ok")?.ms ?? 0}ms` : `every lane died (${attempts.map((a) => `${a.lane}:${a.outcome}${a.status ? ` ${a.status}` : ""}`).join(", ")})`;
|
|
499
|
+
const ifMint = input.mintStatus !== void 0 ? ` (last cookie mint status ${input.mintStatus})` : "";
|
|
500
|
+
return {
|
|
501
|
+
src,
|
|
502
|
+
healedBy,
|
|
503
|
+
attempts,
|
|
504
|
+
diagnosis,
|
|
505
|
+
summary: `${cause}${ifMint}; ${served}`
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
356
509
|
// src/files/client.ts
|
|
357
510
|
var TUS_TRANSPORT_THRESHOLD_BYTES = 80 * 1024 * 1024;
|
|
358
511
|
function resolveUploadTransport(fileSizeBytes, override) {
|
|
@@ -904,6 +1057,28 @@ function createMatrxFilesClient(config) {
|
|
|
904
1057
|
await session.ensure({ force: true });
|
|
905
1058
|
return finish("retry");
|
|
906
1059
|
},
|
|
1060
|
+
async healLoadError(ref, context) {
|
|
1061
|
+
const norm = normalizeRef(ref);
|
|
1062
|
+
const fileId = norm?.kind === "file_id" ? norm.fileId : norm?.kind === "url" ? recognizer.fileIdFromFileEndpointUrl(norm.url) : null;
|
|
1063
|
+
const primary = filesBase();
|
|
1064
|
+
return healLoadErrorLadder({
|
|
1065
|
+
fileId,
|
|
1066
|
+
priorAttempts: context.attempts,
|
|
1067
|
+
primaryBase: primary,
|
|
1068
|
+
alternateBases: session.bases().filter((b) => b !== primary),
|
|
1069
|
+
mintStatus: session.snapshot().status,
|
|
1070
|
+
fetchBytes: async (base, id) => {
|
|
1071
|
+
const headers = await authHeaders();
|
|
1072
|
+
return fetchImpl(fileUrls(base, id).download, { headers });
|
|
1073
|
+
},
|
|
1074
|
+
cacheBlob: (id, blob) => {
|
|
1075
|
+
const already = blobCache.get(id);
|
|
1076
|
+
const objectUrl = already?.url ?? URL.createObjectURL(blob);
|
|
1077
|
+
if (!already) blobCache.set(id, blob, objectUrl, { mimeType: blob.type });
|
|
1078
|
+
return objectUrl;
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
},
|
|
907
1082
|
async recoverBlobLoadError(ref, attempt) {
|
|
908
1083
|
if (attempt > 1) return "terminal";
|
|
909
1084
|
const norm = normalizeRef(ref);
|