@ai-matrx/media 0.2.3 → 0.3.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 +55 -0
- package/README.md +22 -0
- package/dist/core.cjs +177 -39
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +84 -4
- package/dist/core.d.ts +84 -4
- package/dist/core.js +164 -26
- package/dist/core.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -2
- package/dist/index.d.ts +77 -2
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +230 -79
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +208 -56
- package/dist/react.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# @ai-matrx/media
|
|
2
2
|
|
|
3
|
+
## 0.3.0 — 2026-08-30
|
|
4
|
+
|
|
5
|
+
**A terminal media failure can never be silent again.** Born from the
|
|
6
|
+
2026-08-30 outage: server-side cookie-secret drift killed every private image
|
|
7
|
+
on prod, the honest "unavailable" panel rendered — and nothing reached the
|
|
8
|
+
host's error system, because the package had no diagnostics port and the host
|
|
9
|
+
cannot capture what it never learns about. Per ruling C22 the capture logic
|
|
10
|
+
now lives IN the package. Additive; no default behavior changes for renders.
|
|
11
|
+
|
|
12
|
+
- **New `diagnostics` port on `MediaHostPorts`** —
|
|
13
|
+
`diagnostics?: { capture?: (info: MediaFailureInfo) => void }`.
|
|
14
|
+
`MediaFailureInfo` is strictly typed: `source: "media"`, a
|
|
15
|
+
`phase` (`"resolve" | "session" | "blob" | "element-load" | "recovery"`),
|
|
16
|
+
a durable `mediaRef` identity (file id / query-stripped URL key — NEVER a
|
|
17
|
+
signed URL), `status?` when known, a typed `retryOutcome?`, `terminal`,
|
|
18
|
+
and `message`. **Absent port → the package `console.error`s the same
|
|
19
|
+
structured payload** (the loud default) — never fully silent. A throwing
|
|
20
|
+
sink also falls back to the console.
|
|
21
|
+
- **Every terminal failure path emits exactly one capture**, latched per
|
|
22
|
+
media identity (no spam across re-renders, remounts, or the panel
|
|
23
|
+
backstop): a throwing `resolve()` (phase `resolve`), a `getBlob()`
|
|
24
|
+
rejection (phase `blob`, with the structural HTTP status when the error
|
|
25
|
+
carries one), a rejecting `recoverLoadError` (phase `session`,
|
|
26
|
+
`recovery-failed`), a terminal verdict after the ONE granted retry (phase
|
|
27
|
+
`element-load`, `retried`), a terminal verdict on the first attempt (phase
|
|
28
|
+
`recovery`, `not-attempted`), an unrecoverable element failure (phase
|
|
29
|
+
`element-load`, `not-attempted`) — plus the unavailable panel mount as a
|
|
30
|
+
latch-deduped BACKSTOP (if the panel is on screen and no path captured,
|
|
31
|
+
the mount screams).
|
|
32
|
+
- **`MediaClient.recoverLoadError` may now return a structured
|
|
33
|
+
`MediaRecoveryReport`** (`{ verdict, status?, secretSource? }`) alongside
|
|
34
|
+
the still-fully-supported bare `"retry" | "terminal"` strings — a widening,
|
|
35
|
+
zero change required in any implementation. When the client forwards the
|
|
36
|
+
`/files/session` mint's `secret_source` and it is `"ephemeral"`, the
|
|
37
|
+
package captures ONE app-wide non-terminal warning ("server cookie secret
|
|
38
|
+
is ephemeral — private renders will break across restarts/instances") the
|
|
39
|
+
moment it is seen — before the outage, not after. The field is tolerated
|
|
40
|
+
absent.
|
|
41
|
+
- New `/core` exports: `useMediaFailureCapture`, `emitMediaFailure`,
|
|
42
|
+
`failureMediaIdentity`, `resetMediaFailureLatch` (tests + auth boundaries).
|
|
43
|
+
Root types: `MediaFailureInfo`, `MediaFailurePhase`, `MediaRetryOutcome`,
|
|
44
|
+
`MediaDiagnosticsPort`, `MediaRecoveryVerdict`, `MediaRecoveryReport`.
|
|
45
|
+
- `useMediaLoadRecovery` gains an optional `failureRef` option so failures
|
|
46
|
+
are reported against the durable ref identity instead of the (stripped)
|
|
47
|
+
URL; `InlineMediaRef`/`MediaThumbnail` pass it automatically.
|
|
48
|
+
|
|
49
|
+
**Consumer action:** bind the port —
|
|
50
|
+
`ports.diagnostics = { capture: (info) => yourErrorCapture(info) }` on the
|
|
51
|
+
object passed to `MediaProvider`/`MediaHostPorts` (matrx-frontend: wire to
|
|
52
|
+
`captureError` in `lib/diagnostics/errorCaptureStore`). Without the binding
|
|
53
|
+
you get loud console.error diagnostics; with it, failures land in your error
|
|
54
|
+
system. Clients implementing `MediaClient` SHOULD start returning
|
|
55
|
+
`MediaRecoveryReport` from `recoverLoadError` with the mint's
|
|
56
|
+
`secret_source` when the server sends one.
|
|
57
|
+
|
|
3
58
|
## 0.2.3 — 2026-08-30
|
|
4
59
|
|
|
5
60
|
- `@ai-matrx/associations` is now a REAL dependency (`^0.5.2`) instead of an
|
package/README.md
CHANGED
|
@@ -124,6 +124,28 @@ automatically (it brings `@ai-matrx/design-system` with it). Nothing extra to
|
|
|
124
124
|
resolve; association affordances still degrade to absent at runtime when no
|
|
125
125
|
`AssociationsProvider` wraps the tree. The other entries never touch it.
|
|
126
126
|
|
|
127
|
+
### Diagnostics — a terminal failure is never silent
|
|
128
|
+
|
|
129
|
+
Every dead render (a refused `resolve()`, failed private-pixel bytes, a load
|
|
130
|
+
error the ONE recovery retry couldn't fix, a failed session mint) emits
|
|
131
|
+
exactly one strictly-typed `MediaFailureInfo` — `phase`, durable `mediaRef`
|
|
132
|
+
identity (never a signed URL), `status?`, `retryOutcome?`, `terminal`,
|
|
133
|
+
`message` — latched per media identity so re-renders never spam. Bind the
|
|
134
|
+
sink to your error system:
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
<MediaProvider
|
|
138
|
+
client={client}
|
|
139
|
+
ports={{ diagnostics: { capture: (info) => myErrorCapture(info) } }}
|
|
140
|
+
>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Without a bound sink the package `console.error`s the same payload — loud by
|
|
144
|
+
default, never silent. If your `MediaClient.recoverLoadError` returns a
|
|
145
|
+
`MediaRecoveryReport` carrying the session mint's
|
|
146
|
+
`secret_source: "ephemeral"`, the package immediately captures an app-wide
|
|
147
|
+
warning that private renders will break across server restarts/instances.
|
|
148
|
+
|
|
127
149
|
## Styling
|
|
128
150
|
|
|
129
151
|
`/react` uses Tailwind utility classes with semantic tokens (`bg-muted`,
|
package/dist/core.cjs
CHANGED
|
@@ -22,12 +22,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
22
22
|
var core_exports = {};
|
|
23
23
|
__export(core_exports, {
|
|
24
24
|
MediaProvider: () => MediaProvider,
|
|
25
|
+
emitMediaFailure: () => emitMediaFailure,
|
|
26
|
+
failureMediaIdentity: () => failureMediaIdentity,
|
|
25
27
|
getFileKindDetails: () => getFileKindDetails,
|
|
26
28
|
getFolderKindDetails: () => getFolderKindDetails,
|
|
29
|
+
resetMediaFailureLatch: () => resetMediaFailureLatch,
|
|
27
30
|
stableRefKey: () => stableRefKey,
|
|
28
31
|
useMediaActions: () => useMediaActions,
|
|
29
32
|
useMediaBlob: () => useMediaBlob,
|
|
30
33
|
useMediaClient: () => useMediaClient,
|
|
34
|
+
useMediaFailureCapture: () => useMediaFailureCapture,
|
|
31
35
|
useMediaHostPorts: () => useMediaHostPorts,
|
|
32
36
|
useMediaLoadRecovery: () => useMediaLoadRecovery,
|
|
33
37
|
useMediaResolution: () => useMediaResolution,
|
|
@@ -82,29 +86,107 @@ function stableRefKey(ref) {
|
|
|
82
86
|
return "<empty-ref>";
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
// src/core/
|
|
89
|
+
// src/core/diagnostics.ts
|
|
86
90
|
var import_react2 = require("react");
|
|
91
|
+
var LATCH_SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.media.failure-latch");
|
|
92
|
+
function latch() {
|
|
93
|
+
const host = globalThis;
|
|
94
|
+
host[LATCH_SLOT] ??= /* @__PURE__ */ new Set();
|
|
95
|
+
return host[LATCH_SLOT];
|
|
96
|
+
}
|
|
97
|
+
function resetMediaFailureLatch() {
|
|
98
|
+
latch().clear();
|
|
99
|
+
}
|
|
100
|
+
function failureMediaIdentity(ref, src) {
|
|
101
|
+
if (ref && typeof ref === "object" && ref.file_id) return `file:${ref.file_id}`;
|
|
102
|
+
const raw = typeof ref === "string" ? ref : (ref && typeof ref === "object" ? ref.url ?? null : null) ?? src ?? null;
|
|
103
|
+
if (!raw) return "<unknown-media>";
|
|
104
|
+
if (raw.startsWith("blob:")) return "blob:object-url";
|
|
105
|
+
try {
|
|
106
|
+
const u = new URL(raw);
|
|
107
|
+
return `url:${u.origin}${u.pathname}`;
|
|
108
|
+
} catch {
|
|
109
|
+
const clean = raw.split(/[?#]/, 1)[0] ?? raw;
|
|
110
|
+
return `ref:${clean}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function emitMediaFailure(capture, info) {
|
|
114
|
+
const key = `${info.terminal ? "terminal" : "warning"}|${info.mediaRef}`;
|
|
115
|
+
const seen = latch();
|
|
116
|
+
if (seen.has(key)) return;
|
|
117
|
+
seen.add(key);
|
|
118
|
+
if (capture) {
|
|
119
|
+
try {
|
|
120
|
+
capture(info);
|
|
121
|
+
return;
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
console.error(
|
|
126
|
+
"[@ai-matrx/media] media failure (no diagnostics sink captured it \u2014 bind MediaHostPorts.diagnostics.capture to your error system)",
|
|
127
|
+
info
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
function useMediaFailureCapture() {
|
|
131
|
+
const ports = useMediaHostPorts();
|
|
132
|
+
const capture = ports.diagnostics?.capture;
|
|
133
|
+
const captureRef = (0, import_react2.useRef)(capture);
|
|
134
|
+
captureRef.current = capture;
|
|
135
|
+
return (0, import_react2.useCallback)((info) => {
|
|
136
|
+
emitMediaFailure(captureRef.current, { source: "media", ...info });
|
|
137
|
+
}, []);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/core/use-media-resolution.ts
|
|
141
|
+
var import_react3 = require("react");
|
|
87
142
|
function useMediaResolution(ref) {
|
|
88
143
|
const client = useMediaClient();
|
|
144
|
+
const capture = useMediaFailureCapture();
|
|
89
145
|
const refKey = stableRefKey(ref);
|
|
90
|
-
|
|
91
|
-
if (!ref)
|
|
146
|
+
const { state, failureMessage } = (0, import_react3.useMemo)(() => {
|
|
147
|
+
if (!ref) {
|
|
148
|
+
return {
|
|
149
|
+
state: { resolution: null, status: "empty", reason: null },
|
|
150
|
+
failureMessage: null
|
|
151
|
+
};
|
|
152
|
+
}
|
|
92
153
|
try {
|
|
93
154
|
const resolution = client.resolve(ref);
|
|
94
|
-
if (!resolution)
|
|
95
|
-
|
|
155
|
+
if (!resolution) {
|
|
156
|
+
return {
|
|
157
|
+
state: { resolution: null, status: "empty", reason: null },
|
|
158
|
+
failureMessage: null
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
state: { resolution, status: "ready", reason: null },
|
|
163
|
+
failureMessage: null
|
|
164
|
+
};
|
|
96
165
|
} catch (err) {
|
|
97
166
|
return {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
167
|
+
state: {
|
|
168
|
+
resolution: null,
|
|
169
|
+
status: "unavailable",
|
|
170
|
+
reason: client.classifyError(err)
|
|
171
|
+
},
|
|
172
|
+
failureMessage: err instanceof Error ? err.message : String(err ?? "resolve() failed")
|
|
101
173
|
};
|
|
102
174
|
}
|
|
103
175
|
}, [client, refKey]);
|
|
176
|
+
(0, import_react3.useEffect)(() => {
|
|
177
|
+
if (state.status !== "unavailable") return;
|
|
178
|
+
capture({
|
|
179
|
+
phase: "resolve",
|
|
180
|
+
mediaRef: failureMediaIdentity(ref ?? null),
|
|
181
|
+
terminal: true,
|
|
182
|
+
message: `resolve() refused (${state.reason ?? "unknown"}): ${failureMessage ?? "no error message"}`
|
|
183
|
+
});
|
|
184
|
+
}, [refKey, state.status]);
|
|
185
|
+
return state;
|
|
104
186
|
}
|
|
105
187
|
|
|
106
188
|
// src/core/use-media-load-recovery.ts
|
|
107
|
-
var
|
|
189
|
+
var import_react4 = require("react");
|
|
108
190
|
|
|
109
191
|
// src/types.ts
|
|
110
192
|
function mintDurableSrc(url) {
|
|
@@ -112,26 +194,51 @@ function mintDurableSrc(url) {
|
|
|
112
194
|
}
|
|
113
195
|
|
|
114
196
|
// src/core/use-media-load-recovery.ts
|
|
197
|
+
function normalizeOutcome(outcome) {
|
|
198
|
+
return typeof outcome === "string" ? { verdict: outcome } : outcome;
|
|
199
|
+
}
|
|
200
|
+
var EPHEMERAL_SECRET_IDENTITY = "server:files-session-secret";
|
|
115
201
|
function useMediaLoadRecovery(src, options) {
|
|
116
202
|
const client = useMediaClient();
|
|
203
|
+
const capture = useMediaFailureCapture();
|
|
117
204
|
const recoverable = options?.recoverable !== false;
|
|
118
205
|
const onTerminal = options?.onTerminal;
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
(0,
|
|
206
|
+
const failureRef = options?.failureRef;
|
|
207
|
+
const [result, setResult] = (0, import_react4.useState)({ source: src, retryKey: 0, failed: false });
|
|
208
|
+
const attemptRef = (0, import_react4.useRef)(0);
|
|
209
|
+
(0, import_react4.useEffect)(() => {
|
|
122
210
|
attemptRef.current = 0;
|
|
123
211
|
}, [src]);
|
|
212
|
+
const identity = () => failureMediaIdentity(failureRef ?? null, src);
|
|
124
213
|
const onLoadError = (event) => {
|
|
125
214
|
if (!src || !recoverable) {
|
|
126
215
|
setResult({ source: src, retryKey: 0, failed: true });
|
|
216
|
+
if (src) {
|
|
217
|
+
capture({
|
|
218
|
+
phase: "element-load",
|
|
219
|
+
mediaRef: identity(),
|
|
220
|
+
terminal: true,
|
|
221
|
+
retryOutcome: "not-attempted",
|
|
222
|
+
message: "media element failed to load and the source is unrecoverable (foreign/object URL \u2014 a session refresh cannot fix it)"
|
|
223
|
+
});
|
|
224
|
+
}
|
|
127
225
|
onTerminal?.(event);
|
|
128
226
|
return;
|
|
129
227
|
}
|
|
130
228
|
attemptRef.current += 1;
|
|
131
229
|
const attempt = attemptRef.current;
|
|
132
230
|
client.recoverLoadError(mintDurableSrc(src), attempt).then(
|
|
133
|
-
(
|
|
134
|
-
|
|
231
|
+
(outcome) => {
|
|
232
|
+
const report = normalizeOutcome(outcome);
|
|
233
|
+
if (report.secretSource === "ephemeral") {
|
|
234
|
+
capture({
|
|
235
|
+
phase: "session",
|
|
236
|
+
mediaRef: EPHEMERAL_SECRET_IDENTITY,
|
|
237
|
+
terminal: false,
|
|
238
|
+
message: "server cookie secret is ephemeral \u2014 private renders will break across restarts/instances (set a persistent files-session secret on the server)"
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (report.verdict === "retry") {
|
|
135
242
|
setResult((current) => ({
|
|
136
243
|
source: src,
|
|
137
244
|
retryKey: current.source === src ? current.retryKey + 1 : 1,
|
|
@@ -139,11 +246,26 @@ function useMediaLoadRecovery(src, options) {
|
|
|
139
246
|
}));
|
|
140
247
|
} else {
|
|
141
248
|
setResult({ source: src, retryKey: 0, failed: true });
|
|
249
|
+
capture({
|
|
250
|
+
phase: attempt > 1 ? "element-load" : "recovery",
|
|
251
|
+
mediaRef: identity(),
|
|
252
|
+
terminal: true,
|
|
253
|
+
retryOutcome: attempt > 1 ? "retried" : "not-attempted",
|
|
254
|
+
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)",
|
|
255
|
+
...report.status !== void 0 ? { status: report.status } : {}
|
|
256
|
+
});
|
|
142
257
|
onTerminal?.(event);
|
|
143
258
|
}
|
|
144
259
|
},
|
|
145
|
-
() => {
|
|
260
|
+
(err) => {
|
|
146
261
|
setResult({ source: src, retryKey: 0, failed: true });
|
|
262
|
+
capture({
|
|
263
|
+
phase: "session",
|
|
264
|
+
mediaRef: identity(),
|
|
265
|
+
terminal: true,
|
|
266
|
+
retryOutcome: "recovery-failed",
|
|
267
|
+
message: `session recovery failed: ${err instanceof Error ? err.message : String(err ?? "unknown error")}`
|
|
268
|
+
});
|
|
147
269
|
onTerminal?.(event);
|
|
148
270
|
}
|
|
149
271
|
);
|
|
@@ -156,13 +278,21 @@ function useMediaLoadRecovery(src, options) {
|
|
|
156
278
|
}
|
|
157
279
|
|
|
158
280
|
// src/core/use-media-blob.ts
|
|
159
|
-
var
|
|
281
|
+
var import_react5 = require("react");
|
|
282
|
+
function statusOf(err) {
|
|
283
|
+
if (err && typeof err === "object") {
|
|
284
|
+
const candidate = err.status ?? err.statusCode;
|
|
285
|
+
if (typeof candidate === "number") return candidate;
|
|
286
|
+
}
|
|
287
|
+
return void 0;
|
|
288
|
+
}
|
|
160
289
|
function useMediaBlob(ref) {
|
|
161
290
|
const client = useMediaClient();
|
|
291
|
+
const capture = useMediaFailureCapture();
|
|
162
292
|
const refKey = stableRefKey(ref);
|
|
163
|
-
const [state, setState] = (0,
|
|
164
|
-
const [retryToken, setRetryToken] = (0,
|
|
165
|
-
(0,
|
|
293
|
+
const [state, setState] = (0, import_react5.useState)({ url: null, blob: null, loading: !!ref, error: null });
|
|
294
|
+
const [retryToken, setRetryToken] = (0, import_react5.useState)(0);
|
|
295
|
+
(0, import_react5.useEffect)(() => {
|
|
166
296
|
if (!ref) {
|
|
167
297
|
setState({ url: null, blob: null, loading: false, error: null });
|
|
168
298
|
return void 0;
|
|
@@ -183,6 +313,14 @@ function useMediaBlob(ref) {
|
|
|
183
313
|
if (cancelled) return;
|
|
184
314
|
const message = err instanceof Error ? err.message : String(err ?? "Failed to load file");
|
|
185
315
|
setState({ url: null, blob: null, loading: false, error: message });
|
|
316
|
+
const status = statusOf(err);
|
|
317
|
+
capture({
|
|
318
|
+
phase: "blob",
|
|
319
|
+
mediaRef: failureMediaIdentity(ref),
|
|
320
|
+
terminal: true,
|
|
321
|
+
message: `getBlob() failed: ${message}`,
|
|
322
|
+
...status !== void 0 ? { status } : {}
|
|
323
|
+
});
|
|
186
324
|
}
|
|
187
325
|
);
|
|
188
326
|
return () => {
|
|
@@ -197,17 +335,17 @@ function useMediaBlob(ref) {
|
|
|
197
335
|
}
|
|
198
336
|
|
|
199
337
|
// src/core/use-media-upload.ts
|
|
200
|
-
var
|
|
338
|
+
var import_react6 = require("react");
|
|
201
339
|
var entrySeq = 0;
|
|
202
340
|
function useMediaUpload() {
|
|
203
341
|
const client = useMediaClient();
|
|
204
|
-
const [uploading, setUploading] = (0,
|
|
205
|
-
const [progress, setProgress] = (0,
|
|
206
|
-
const [result, setResult] = (0,
|
|
207
|
-
const [error, setError] = (0,
|
|
208
|
-
const [entries, setEntries] = (0,
|
|
209
|
-
const activeCount = (0,
|
|
210
|
-
const patchEntry = (0,
|
|
342
|
+
const [uploading, setUploading] = (0, import_react6.useState)(false);
|
|
343
|
+
const [progress, setProgress] = (0, import_react6.useState)(null);
|
|
344
|
+
const [result, setResult] = (0, import_react6.useState)(null);
|
|
345
|
+
const [error, setError] = (0, import_react6.useState)(null);
|
|
346
|
+
const [entries, setEntries] = (0, import_react6.useState)([]);
|
|
347
|
+
const activeCount = (0, import_react6.useRef)(0);
|
|
348
|
+
const patchEntry = (0, import_react6.useCallback)(
|
|
211
349
|
(id, patch) => {
|
|
212
350
|
setEntries(
|
|
213
351
|
(current) => current.map((e) => e.id === id ? { ...e, ...patch } : e)
|
|
@@ -215,7 +353,7 @@ function useMediaUpload() {
|
|
|
215
353
|
},
|
|
216
354
|
[]
|
|
217
355
|
);
|
|
218
|
-
const upload = (0,
|
|
356
|
+
const upload = (0, import_react6.useCallback)(
|
|
219
357
|
async (file, opts = {}) => {
|
|
220
358
|
const id = `up-${++entrySeq}`;
|
|
221
359
|
const fileName = opts.fileName ?? (file instanceof File ? file.name : "file");
|
|
@@ -269,7 +407,7 @@ function useMediaUpload() {
|
|
|
269
407
|
},
|
|
270
408
|
[client, patchEntry]
|
|
271
409
|
);
|
|
272
|
-
const uploadMany = (0,
|
|
410
|
+
const uploadMany = (0, import_react6.useCallback)(
|
|
273
411
|
async (files, opts = {}) => {
|
|
274
412
|
if (client.uploadMany) {
|
|
275
413
|
activeCount.current += 1;
|
|
@@ -298,10 +436,10 @@ function useMediaUpload() {
|
|
|
298
436
|
},
|
|
299
437
|
[client, upload]
|
|
300
438
|
);
|
|
301
|
-
const clearEntry = (0,
|
|
439
|
+
const clearEntry = (0, import_react6.useCallback)((id) => {
|
|
302
440
|
setEntries((current) => current.filter((e) => e.id !== id));
|
|
303
441
|
}, []);
|
|
304
|
-
const reset = (0,
|
|
442
|
+
const reset = (0, import_react6.useCallback)(() => {
|
|
305
443
|
setResult(null);
|
|
306
444
|
setError(null);
|
|
307
445
|
setProgress(null);
|
|
@@ -321,12 +459,12 @@ function useMediaUpload() {
|
|
|
321
459
|
}
|
|
322
460
|
|
|
323
461
|
// src/core/use-thumbnail-source.ts
|
|
324
|
-
var
|
|
462
|
+
var import_react7 = require("react");
|
|
325
463
|
function useThumbnailSource(ref, options = {}) {
|
|
326
464
|
const { thumbnailUrl, allowSourceFallback = true } = options;
|
|
327
465
|
const client = useMediaClient();
|
|
328
466
|
const { resolution } = useMediaResolution(ref);
|
|
329
|
-
const explicitThumb = (0,
|
|
467
|
+
const explicitThumb = (0, import_react7.useMemo)(() => {
|
|
330
468
|
if (!thumbnailUrl) return null;
|
|
331
469
|
try {
|
|
332
470
|
return client.resolve(thumbnailUrl);
|
|
@@ -393,19 +531,19 @@ function useThumbnailSource(ref, options = {}) {
|
|
|
393
531
|
}
|
|
394
532
|
|
|
395
533
|
// src/core/use-media-actions.ts
|
|
396
|
-
var
|
|
534
|
+
var import_react8 = require("react");
|
|
397
535
|
function useMediaActions(ref, options = {}) {
|
|
398
536
|
const ports = useMediaHostPorts();
|
|
399
537
|
const { resolution } = useMediaResolution(ref);
|
|
400
|
-
const [busy, setBusy] = (0,
|
|
401
|
-
const [lastError, setLastError] = (0,
|
|
538
|
+
const [busy, setBusy] = (0, import_react8.useState)(null);
|
|
539
|
+
const [lastError, setLastError] = (0, import_react8.useState)(null);
|
|
402
540
|
const actions = ports.actions;
|
|
403
541
|
const { fileName, alt } = options;
|
|
404
|
-
const context = (0,
|
|
542
|
+
const context = (0, import_react8.useMemo)(() => {
|
|
405
543
|
if (!ref) return null;
|
|
406
544
|
return { ref, resolution, fileName, alt };
|
|
407
545
|
}, [ref, resolution, fileName, alt]);
|
|
408
|
-
const isAvailable = (0,
|
|
546
|
+
const isAvailable = (0, import_react8.useCallback)(
|
|
409
547
|
(kind) => {
|
|
410
548
|
if (!context || !actions) return false;
|
|
411
549
|
if (kind === "share") return Boolean(actions.share || actions.SharePopover);
|
|
@@ -413,7 +551,7 @@ function useMediaActions(ref, options = {}) {
|
|
|
413
551
|
},
|
|
414
552
|
[context, actions]
|
|
415
553
|
);
|
|
416
|
-
const run = (0,
|
|
554
|
+
const run = (0, import_react8.useCallback)(
|
|
417
555
|
async (kind) => {
|
|
418
556
|
if (!context || !actions) return;
|
|
419
557
|
const handler = actions[kind];
|