@ai-matrx/media 0.5.11 → 0.6.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 CHANGED
@@ -1,5 +1,36 @@
1
1
  # @ai-matrx/media
2
2
 
3
+ ## 0.6.0
4
+
5
+ **The heal ladder — the user is never denied while any lane works, and a
6
+ render that heals itself is logged as loudly as one that dies.**
7
+
8
+ - `MediaClient.healLoadError?(ref, {src, attempts})` (optional): runs AFTER the
9
+ ONE retry policy is exhausted; returns `MediaHealResult` — the lanes tried
10
+ (`MediaHealAttempt[]`: lane, outcome, status, ms, host), the lane that
11
+ healed (`healedBy`), a NAMED root cause (`MediaHealDiagnosis`, e.g.
12
+ `cookie-not-sent-by-browser`, `bearer-rejected`, `primary-host-down-alternate-served`)
13
+ and a plain-English `summary`.
14
+ - `useMediaLoadRecovery` returns `healedSrc` (a `blob:` object URL from the
15
+ bearer lane) — bind it instead of the dead src. `InlineMediaRef` and the
16
+ thumbnails do so already.
17
+ - `MediaFailureInfo` gains `healed`, `healedBy`, `attempts`, `diagnosis` and
18
+ the `"heal"` phase. A healed render captures `terminal: false, healed: true`
19
+ with the full ladder; hosts MUST persist it like a dead one (matrx-frontend
20
+ does: `code = "healed:<diagnosis>"`, `durable: true`).
21
+ - Clients without a ladder keep the pre-0.6 phases/messages exactly, with a
22
+ suffix saying no ladder ran.
23
+
24
+ Why: 2026-09-16, a private image 401'd twice in Arman's Chrome (third-party
25
+ cookies blocked) and rendered "Image unavailable" while the bearer lane would
26
+ have served it the whole time; the DB row said only "failed after retry".
27
+
28
+ ## 0.5.12
29
+
30
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
31
+ `git diff npm/media/v0.5.11..npm/media/v0.5.12 -- apps/shared/media`).
32
+ No source changes intended and no consumer action required.
33
+
3
34
  ## 0.5.11
4
35
 
5
36
  Adopts `formatCount` from `@ai-matrx/kit/format` for the text viewer's character
package/dist/core.cjs CHANGED
@@ -207,7 +207,7 @@ function useMediaLoadRecovery(src, options) {
207
207
  const recoverable = options?.recoverable !== false;
208
208
  const onTerminal = options?.onTerminal;
209
209
  const failureRef = options?.failureRef;
210
- const [result, setResult] = (0, import_react4.useState)({ source: src, retryKey: 0, failed: false });
210
+ const [result, setResult] = (0, import_react4.useState)({ source: src, retryKey: 0, failed: false, healedSrc: null });
211
211
  const attemptRef = (0, import_react4.useRef)(0);
212
212
  const recoveryInFlightRef = (0, import_react4.useRef)(false);
213
213
  (0, import_react4.useEffect)(() => {
@@ -221,7 +221,7 @@ function useMediaLoadRecovery(src, options) {
221
221
  const onLoadError = (event) => {
222
222
  if (recoveryInFlightRef.current) return;
223
223
  if (!src || !recoverable) {
224
- setResult({ source: src, retryKey: 0, failed: true });
224
+ setResult({ source: src, retryKey: 0, failed: true, healedSrc: null });
225
225
  if (src) {
226
226
  capture({
227
227
  phase: "element-load",
@@ -252,23 +252,90 @@ function useMediaLoadRecovery(src, options) {
252
252
  setResult((current) => ({
253
253
  source: src,
254
254
  retryKey: current.source === src ? current.retryKey + 1 : 1,
255
- failed: false
255
+ failed: false,
256
+ healedSrc: null
256
257
  }));
257
- } else {
258
- setResult({ source: src, retryKey: 0, failed: true });
258
+ return;
259
+ }
260
+ const lane1 = {
261
+ lane: "session-refresh-retry",
262
+ outcome: attempt > 1 ? "http-error" : "unavailable",
263
+ ms: 0,
264
+ ...report.status !== void 0 ? { status: report.status } : {},
265
+ detail: attempt > 1 ? "cookie re-minted, same durable URL retried in the element, element errored again" : "recovery declined on the first attempt (no usable session)"
266
+ };
267
+ if (!failureRef || !client.healLoadError) {
268
+ setResult({ source: src, retryKey: 0, failed: true, healedSrc: null });
259
269
  capture({
260
270
  phase: attempt > 1 ? "element-load" : "recovery",
261
271
  mediaRef: identity(),
262
272
  terminal: true,
263
273
  retryOutcome: attempt > 1 ? "retried" : "not-attempted",
264
- message: attempt > 1 ? "media element failed to load after the session-refresh retry" : "media load recovery declined on the first attempt (session refresh did not produce a usable session)",
274
+ message: (attempt > 1 ? "media element failed to load after the session-refresh retry" : "media load recovery declined on the first attempt (session refresh did not produce a usable session)") + (failureRef ? " \u2014 no heal ladder on this client (healLoadError not implemented)" : " \u2014 no durable ref supplied, so no heal lane could run"),
265
275
  ...report.status !== void 0 ? { status: report.status } : {}
266
276
  });
267
277
  onTerminal?.(event);
278
+ return;
268
279
  }
280
+ const heal = client.healLoadError(failureRef, {
281
+ src: mintDurableSrc(src),
282
+ attempts: [lane1]
283
+ });
284
+ heal.then(
285
+ (verdict) => {
286
+ if (verdict.src) {
287
+ setResult({
288
+ source: src,
289
+ retryKey: 0,
290
+ failed: false,
291
+ healedSrc: verdict.src
292
+ });
293
+ capture({
294
+ phase: "heal",
295
+ mediaRef: identity(),
296
+ terminal: false,
297
+ healed: true,
298
+ healedBy: verdict.healedBy,
299
+ attempts: verdict.attempts,
300
+ diagnosis: verdict.diagnosis,
301
+ retryOutcome: "retried",
302
+ message: `HEALED (${verdict.healedBy ?? "unknown lane"}): ${verdict.summary}`,
303
+ ...report.status !== void 0 ? { status: report.status } : {}
304
+ });
305
+ return;
306
+ }
307
+ setResult({ source: src, retryKey: 0, failed: true, healedSrc: null });
308
+ capture({
309
+ phase: "heal",
310
+ mediaRef: identity(),
311
+ terminal: true,
312
+ healed: false,
313
+ attempts: verdict.attempts,
314
+ diagnosis: verdict.diagnosis,
315
+ retryOutcome: "retried",
316
+ message: `DEAD after ${verdict.attempts.length} lane(s): ${verdict.summary}`,
317
+ ...report.status !== void 0 ? { status: report.status } : {}
318
+ });
319
+ onTerminal?.(event);
320
+ },
321
+ (err) => {
322
+ setResult({ source: src, retryKey: 0, failed: true, healedSrc: null });
323
+ capture({
324
+ phase: "heal",
325
+ mediaRef: identity(),
326
+ terminal: true,
327
+ healed: false,
328
+ attempts: [lane1],
329
+ diagnosis: "unknown",
330
+ retryOutcome: "recovery-failed",
331
+ message: `heal ladder threw: ${err instanceof Error ? err.message : String(err ?? "unknown error")}`
332
+ });
333
+ onTerminal?.(event);
334
+ }
335
+ );
269
336
  },
270
337
  (err) => {
271
- setResult({ source: src, retryKey: 0, failed: true });
338
+ setResult({ source: src, retryKey: 0, failed: true, healedSrc: null });
272
339
  capture({
273
340
  phase: "session",
274
341
  mediaRef: identity(),
@@ -283,7 +350,8 @@ function useMediaLoadRecovery(src, options) {
283
350
  return {
284
351
  retryKey: result.source === src ? result.retryKey : 0,
285
352
  onLoadError,
286
- failed: result.source === src ? result.failed : false
353
+ failed: result.source === src ? result.failed : false,
354
+ healedSrc: result.source === src ? result.healedSrc : null
287
355
  };
288
356
  }
289
357