@rockhall/electron-offline-content 0.4.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 (196) hide show
  1. package/CHANGELOG.md +384 -0
  2. package/LICENSE +21 -0
  3. package/README.md +794 -0
  4. package/dist/internal/asset-file-name.cjs +13 -0
  5. package/dist/internal/asset-file-name.cjs.map +1 -0
  6. package/dist/internal/asset-file-name.d.cts +6 -0
  7. package/dist/internal/asset-file-name.d.cts.map +1 -0
  8. package/dist/internal/asset-file-name.d.ts +6 -0
  9. package/dist/internal/asset-file-name.d.ts.map +1 -0
  10. package/dist/internal/asset-file-name.js +12 -0
  11. package/dist/internal/asset-file-name.js.map +1 -0
  12. package/dist/internal/asset-key.cjs +30 -0
  13. package/dist/internal/asset-key.cjs.map +1 -0
  14. package/dist/internal/asset-key.d.cts +19 -0
  15. package/dist/internal/asset-key.d.cts.map +1 -0
  16. package/dist/internal/asset-key.d.ts +19 -0
  17. package/dist/internal/asset-key.d.ts.map +1 -0
  18. package/dist/internal/asset-key.js +27 -0
  19. package/dist/internal/asset-key.js.map +1 -0
  20. package/dist/internal/log-format.cjs +98 -0
  21. package/dist/internal/log-format.cjs.map +1 -0
  22. package/dist/internal/log-format.d.cts +10 -0
  23. package/dist/internal/log-format.d.cts.map +1 -0
  24. package/dist/internal/log-format.d.ts +10 -0
  25. package/dist/internal/log-format.d.ts.map +1 -0
  26. package/dist/internal/log-format.js +97 -0
  27. package/dist/internal/log-format.js.map +1 -0
  28. package/dist/internal/media-kind.cjs +46 -0
  29. package/dist/internal/media-kind.cjs.map +1 -0
  30. package/dist/internal/media-kind.d.cts +20 -0
  31. package/dist/internal/media-kind.d.cts.map +1 -0
  32. package/dist/internal/media-kind.d.ts +20 -0
  33. package/dist/internal/media-kind.d.ts.map +1 -0
  34. package/dist/internal/media-kind.js +45 -0
  35. package/dist/internal/media-kind.js.map +1 -0
  36. package/dist/internal/url-warn.cjs +14 -0
  37. package/dist/internal/url-warn.cjs.map +1 -0
  38. package/dist/internal/url-warn.d.cts +10 -0
  39. package/dist/internal/url-warn.d.cts.map +1 -0
  40. package/dist/internal/url-warn.d.ts +10 -0
  41. package/dist/internal/url-warn.d.ts.map +1 -0
  42. package/dist/internal/url-warn.js +13 -0
  43. package/dist/internal/url-warn.js.map +1 -0
  44. package/dist/internal/validation.cjs +222 -0
  45. package/dist/internal/validation.cjs.map +1 -0
  46. package/dist/internal/validation.d.cts +78 -0
  47. package/dist/internal/validation.d.cts.map +1 -0
  48. package/dist/internal/validation.d.ts +78 -0
  49. package/dist/internal/validation.d.ts.map +1 -0
  50. package/dist/internal/validation.js +196 -0
  51. package/dist/internal/validation.js.map +1 -0
  52. package/dist/main/asset-download.cjs +265 -0
  53. package/dist/main/asset-download.cjs.map +1 -0
  54. package/dist/main/asset-download.d.cts +12 -0
  55. package/dist/main/asset-download.d.cts.map +1 -0
  56. package/dist/main/asset-download.d.ts +12 -0
  57. package/dist/main/asset-download.d.ts.map +1 -0
  58. package/dist/main/asset-download.js +263 -0
  59. package/dist/main/asset-download.js.map +1 -0
  60. package/dist/main/database.cjs +473 -0
  61. package/dist/main/database.cjs.map +1 -0
  62. package/dist/main/database.d.cts +81 -0
  63. package/dist/main/database.d.cts.map +1 -0
  64. package/dist/main/database.d.ts +81 -0
  65. package/dist/main/database.d.ts.map +1 -0
  66. package/dist/main/database.js +472 -0
  67. package/dist/main/database.js.map +1 -0
  68. package/dist/main/index.cjs +22 -0
  69. package/dist/main/index.d.cts +7 -0
  70. package/dist/main/index.d.ts +7 -0
  71. package/dist/main/index.js +7 -0
  72. package/dist/main/media-cache.cjs +862 -0
  73. package/dist/main/media-cache.cjs.map +1 -0
  74. package/dist/main/media-cache.d.cts +134 -0
  75. package/dist/main/media-cache.d.cts.map +1 -0
  76. package/dist/main/media-cache.d.ts +134 -0
  77. package/dist/main/media-cache.d.ts.map +1 -0
  78. package/dist/main/media-cache.js +854 -0
  79. package/dist/main/media-cache.js.map +1 -0
  80. package/dist/main/storage-root-lock.cjs +124 -0
  81. package/dist/main/storage-root-lock.cjs.map +1 -0
  82. package/dist/main/storage-root-lock.d.cts +11 -0
  83. package/dist/main/storage-root-lock.d.cts.map +1 -0
  84. package/dist/main/storage-root-lock.d.ts +11 -0
  85. package/dist/main/storage-root-lock.d.ts.map +1 -0
  86. package/dist/main/storage-root-lock.js +120 -0
  87. package/dist/main/storage-root-lock.js.map +1 -0
  88. package/dist/main/store.cjs +197 -0
  89. package/dist/main/store.cjs.map +1 -0
  90. package/dist/main/store.d.cts +83 -0
  91. package/dist/main/store.d.cts.map +1 -0
  92. package/dist/main/store.d.ts +83 -0
  93. package/dist/main/store.d.ts.map +1 -0
  94. package/dist/main/store.js +195 -0
  95. package/dist/main/store.js.map +1 -0
  96. package/dist/preload/index.cjs +36 -0
  97. package/dist/preload/index.cjs.map +1 -0
  98. package/dist/preload/index.d.cts +14 -0
  99. package/dist/preload/index.d.cts.map +1 -0
  100. package/dist/preload/index.d.ts +14 -0
  101. package/dist/preload/index.d.ts.map +1 -0
  102. package/dist/preload/index.js +34 -0
  103. package/dist/preload/index.js.map +1 -0
  104. package/dist/react/index.cjs +199 -0
  105. package/dist/react/index.cjs.map +1 -0
  106. package/dist/react/index.d.cts +50 -0
  107. package/dist/react/index.d.cts.map +1 -0
  108. package/dist/react/index.d.ts +50 -0
  109. package/dist/react/index.d.ts.map +1 -0
  110. package/dist/react/index.js +191 -0
  111. package/dist/react/index.js.map +1 -0
  112. package/dist/renderer/helpers.cjs +36 -0
  113. package/dist/renderer/helpers.cjs.map +1 -0
  114. package/dist/renderer/helpers.d.cts +11 -0
  115. package/dist/renderer/helpers.d.cts.map +1 -0
  116. package/dist/renderer/helpers.d.ts +11 -0
  117. package/dist/renderer/helpers.d.ts.map +1 -0
  118. package/dist/renderer/helpers.js +35 -0
  119. package/dist/renderer/helpers.js.map +1 -0
  120. package/dist/renderer/index.cjs +20 -0
  121. package/dist/renderer/index.cjs.map +1 -0
  122. package/dist/renderer/index.d.cts +14 -0
  123. package/dist/renderer/index.d.cts.map +1 -0
  124. package/dist/renderer/index.d.ts +14 -0
  125. package/dist/renderer/index.d.ts.map +1 -0
  126. package/dist/renderer/index.js +14 -0
  127. package/dist/renderer/index.js.map +1 -0
  128. package/dist/renderer/runtime.cjs +278 -0
  129. package/dist/renderer/runtime.cjs.map +1 -0
  130. package/dist/renderer/runtime.d.cts +35 -0
  131. package/dist/renderer/runtime.d.cts.map +1 -0
  132. package/dist/renderer/runtime.d.ts +35 -0
  133. package/dist/renderer/runtime.d.ts.map +1 -0
  134. package/dist/renderer/runtime.js +273 -0
  135. package/dist/renderer/runtime.js.map +1 -0
  136. package/dist/renderer/window-globals.d.cts +9 -0
  137. package/dist/renderer/window-globals.d.cts.map +1 -0
  138. package/dist/renderer/window-globals.d.ts +9 -0
  139. package/dist/renderer/window-globals.d.ts.map +1 -0
  140. package/dist/shared/errors.cjs +102 -0
  141. package/dist/shared/errors.cjs.map +1 -0
  142. package/dist/shared/errors.d.cts +45 -0
  143. package/dist/shared/errors.d.cts.map +1 -0
  144. package/dist/shared/errors.d.ts +45 -0
  145. package/dist/shared/errors.d.ts.map +1 -0
  146. package/dist/shared/errors.js +93 -0
  147. package/dist/shared/errors.js.map +1 -0
  148. package/dist/shared/ipc.cjs +14 -0
  149. package/dist/shared/ipc.cjs.map +1 -0
  150. package/dist/shared/ipc.d.cts +12 -0
  151. package/dist/shared/ipc.d.cts.map +1 -0
  152. package/dist/shared/ipc.d.ts +12 -0
  153. package/dist/shared/ipc.d.ts.map +1 -0
  154. package/dist/shared/ipc.js +13 -0
  155. package/dist/shared/ipc.js.map +1 -0
  156. package/dist/shared/normalize.cjs +19 -0
  157. package/dist/shared/normalize.cjs.map +1 -0
  158. package/dist/shared/normalize.d.cts +11 -0
  159. package/dist/shared/normalize.d.cts.map +1 -0
  160. package/dist/shared/normalize.d.ts +11 -0
  161. package/dist/shared/normalize.d.ts.map +1 -0
  162. package/dist/shared/normalize.js +18 -0
  163. package/dist/shared/normalize.js.map +1 -0
  164. package/dist/shared/pagination.cjs +32 -0
  165. package/dist/shared/pagination.cjs.map +1 -0
  166. package/dist/shared/pagination.d.cts +14 -0
  167. package/dist/shared/pagination.d.cts.map +1 -0
  168. package/dist/shared/pagination.d.ts +14 -0
  169. package/dist/shared/pagination.d.ts.map +1 -0
  170. package/dist/shared/pagination.js +28 -0
  171. package/dist/shared/pagination.js.map +1 -0
  172. package/dist/shared/stem.cjs +16 -0
  173. package/dist/shared/stem.cjs.map +1 -0
  174. package/dist/shared/stem.d.cts +6 -0
  175. package/dist/shared/stem.d.cts.map +1 -0
  176. package/dist/shared/stem.d.ts +6 -0
  177. package/dist/shared/stem.d.ts.map +1 -0
  178. package/dist/shared/stem.js +14 -0
  179. package/dist/shared/stem.js.map +1 -0
  180. package/dist/shared/types.cjs +15 -0
  181. package/dist/shared/types.cjs.map +1 -0
  182. package/dist/shared/types.d.cts +234 -0
  183. package/dist/shared/types.d.cts.map +1 -0
  184. package/dist/shared/types.d.ts +234 -0
  185. package/dist/shared/types.d.ts.map +1 -0
  186. package/dist/shared/types.js +14 -0
  187. package/dist/shared/types.js.map +1 -0
  188. package/package.json +120 -0
  189. package/skills/authenticated-downloads/SKILL.md +203 -0
  190. package/skills/cache-configuration/SKILL.md +357 -0
  191. package/skills/cache-configuration/references/options.md +356 -0
  192. package/skills/getting-started/SKILL.md +407 -0
  193. package/skills/production-checklist/SKILL.md +397 -0
  194. package/skills/react-rendering/SKILL.md +424 -0
  195. package/skills/react-rendering/references/hooks.md +443 -0
  196. package/skills/store-authoring/SKILL.md +369 -0
@@ -0,0 +1,443 @@
1
+ # Hooks API Reference
2
+
3
+ Complete reference for the React bindings exported from `@rockhall/electron-offline-content/react`.
4
+
5
+ ## Shared Types
6
+
7
+ ### AssetKeyInput
8
+
9
+ `string | readonly string[]` — pass the same value to `store.add()` in `resolveStore` and to `useMediaAsset()` / `getAsset()`. Non-empty strings or non-empty arrays of non-empty strings are accepted; there is no further key format validation. Arrays are joined with `/` for `displayKey` on resolved assets.
10
+
11
+ ### AsyncState\<T\>
12
+
13
+ Every data-fetching hook returns this shape:
14
+
15
+ ```typescript
16
+ interface AsyncState<T> {
17
+ data: T | null;
18
+ loading: boolean;
19
+ error: Error | null;
20
+ refresh: () => Promise<void>;
21
+ }
22
+ ```
23
+
24
+ - `data` — `null` until the first successful load, then `T`.
25
+ - `loading` — `true` during initial fetch and during `refresh()`.
26
+ - `error` — Set when the underlying IPC call fails; `null` otherwise.
27
+ - `refresh()` — Re-fetches data from the bridge. Returns a promise that resolves when the fetch completes.
28
+
29
+ ### MediaCacheProvider
30
+
31
+ Context provider that supplies the `MediaCacheBridge` to all hooks.
32
+
33
+ ```typescript
34
+ function MediaCacheProvider({
35
+ bridge,
36
+ children,
37
+ }: PropsWithChildren<{ bridge?: MediaCacheBridge }>): JSX.Element;
38
+ ```
39
+
40
+ | Prop | Type | Description |
41
+ | ---------- | ------------------------------- | ---------------------------------------------------------------------------------------- |
42
+ | `bridge` | `MediaCacheBridge \| undefined` | Optional explicit bridge instance. When omitted, auto-detected from `window.mediaCache`. |
43
+ | `children` | `ReactNode` | Application tree. |
44
+
45
+ ```tsx
46
+ import { MediaCacheProvider } from "@rockhall/electron-offline-content/react";
47
+
48
+ <MediaCacheProvider>
49
+ <App />
50
+ </MediaCacheProvider>;
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Hooks
56
+
57
+ ### useMediaBridge
58
+
59
+ Low-level access to the underlying bridge methods with shared status and aggregated errors.
60
+
61
+ ```typescript
62
+ function useMediaBridge(): UseMediaBridgeResult;
63
+ ```
64
+
65
+ **Returns:** `UseMediaBridgeResult`
66
+
67
+ ```typescript
68
+ interface UseMediaBridgeResult extends MediaCacheBridge {
69
+ status: AsyncState<MediaCacheStatus>;
70
+ phase: MediaCachePhase;
71
+ errors: MediaCacheErrors;
72
+ }
73
+ ```
74
+
75
+ **Throws:** if called outside a `MediaCacheProvider`.
76
+
77
+ ```tsx
78
+ import { useMediaBridge } from "@rockhall/electron-offline-content/react";
79
+
80
+ function DebugPanel() {
81
+ const { syncNow, status, errors } = useMediaBridge();
82
+ }
83
+ ```
84
+
85
+ ---
86
+
87
+ ### useMediaCacheReady
88
+
89
+ Reports whether cached content is available for rendering.
90
+
91
+ ```typescript
92
+ function useMediaCacheReady(): AsyncState<MediaCacheReadyState>;
93
+ ```
94
+
95
+ **Returns:** `AsyncState<MediaCacheReadyState>`
96
+
97
+ ```typescript
98
+ interface MediaCacheReadyState {
99
+ ready: boolean;
100
+ syncing: boolean;
101
+ phase: "idle" | "syncing" | "ready" | "error";
102
+ activeGenerationId: number | null;
103
+ syncError: Error | null;
104
+ }
105
+ ```
106
+
107
+ | Field | Description |
108
+ | -------------------- | -------------------------------------------------------------------------------- |
109
+ | `ready` | `true` once at least one successful sync has completed and content is available. |
110
+ | `syncing` | `true` while a sync operation is in progress. |
111
+ | `phase` | Current sync lifecycle phase. |
112
+ | `activeGenerationId` | Numeric SQLite generation id, or `null` before the first successful sync. |
113
+ | `syncError` | Error from the most recent sync attempt, or `null`. |
114
+
115
+ ```tsx
116
+ import { useMediaCacheReady } from "@rockhall/electron-offline-content/react";
117
+
118
+ function Gate({ children }: { children: React.ReactNode }) {
119
+ const { data, loading } = useMediaCacheReady();
120
+
121
+ if (loading || !data?.ready) {
122
+ return <p>{data?.syncing ? "Downloading…" : "Preparing…"}</p>;
123
+ }
124
+
125
+ return <>{children}</>;
126
+ }
127
+ ```
128
+
129
+ ---
130
+
131
+ ### useMediaCacheStatus
132
+
133
+ Detailed sync status including progress counters.
134
+
135
+ ```typescript
136
+ function useMediaCacheStatus(): UseMediaCacheStatusResult;
137
+ ```
138
+
139
+ **Returns:** `UseMediaCacheStatusResult` — `AsyncState<MediaCacheStatus>` fields plus top-level `phase: MediaCachePhase` (`"loading"` until the first snapshot, then the cache phase).
140
+
141
+ ```typescript
142
+ interface MediaCacheStatus {
143
+ phase: "idle" | "syncing" | "ready" | "error";
144
+ storageRoot: string;
145
+ activeGenerationId: number | null;
146
+ progress: SyncProgress | null;
147
+ lastRun: string | null;
148
+ error: string | null;
149
+ }
150
+
151
+ interface SyncProgress {
152
+ phase:
153
+ | "resolving-store"
154
+ | "staging-generation"
155
+ | "diffing"
156
+ | "downloading"
157
+ | "committing"
158
+ | "pruning";
159
+ totalAssets: number;
160
+ completedAssets: number;
161
+ downloadedAssets: number;
162
+ skippedAssets: number;
163
+ bytesDownloaded: number;
164
+ }
165
+ ```
166
+
167
+ | Field | Description |
168
+ | -------------------- | ---------------------------------------------------------------- |
169
+ | `phase` | Current sync lifecycle phase. |
170
+ | `storageRoot` | Absolute path to the local cache directory. |
171
+ | `activeGenerationId` | Numeric id of the content generation being served. |
172
+ | `progress` | Asset-level progress during `"syncing"` phase; `null` otherwise. |
173
+ | `lastRun` | ISO timestamp of the last completed sync, or `null`. |
174
+ | `error` | Error message string from the last sync failure, or `null`. |
175
+
176
+ ```tsx
177
+ import { useMediaCacheStatus } from "@rockhall/electron-offline-content/react";
178
+
179
+ function SyncProgress() {
180
+ const { data: status, phase } = useMediaCacheStatus();
181
+
182
+ if (phase !== "syncing" || !status?.progress) return null;
183
+
184
+ const pct = Math.round((status.progress.completedAssets / status.progress.totalAssets) * 100);
185
+
186
+ return <p>Syncing: {pct}%</p>;
187
+ }
188
+ ```
189
+
190
+ ---
191
+
192
+ ### useMediaAsset
193
+
194
+ Looks up a single asset by `AssetKeyInput` — the same string or segment array passed as the first argument to `store.add()` in `resolveStore`.
195
+
196
+ ```typescript
197
+ function useMediaAsset(
198
+ key: AssetKeyInput,
199
+ options?: { refetchOnSyncComplete?: boolean },
200
+ ): AsyncState<ResolvedMediaAsset | null>;
201
+ ```
202
+
203
+ | Parameter | Type | Description |
204
+ | --------- | --------------- | --------------------------------------------------------------------------- |
205
+ | `key` | `AssetKeyInput` | `string` or `readonly string[]` — must match the key used in `store.add()`. |
206
+ | `options` | `object` | Optional. `refetchOnSyncComplete` re-fetches after sync. |
207
+
208
+ **Returns:** `AsyncState<ResolvedMediaAsset | null>`
209
+
210
+ ```tsx
211
+ import { useMediaAsset } from "@rockhall/electron-offline-content/react";
212
+
213
+ function WelcomeVideo() {
214
+ const { data: asset, loading } = useMediaAsset(["video", "welcome"]);
215
+
216
+ if (loading || !asset) return null;
217
+
218
+ return <video src={asset.url} title={asset.displayKey} controls />;
219
+ }
220
+ ```
221
+
222
+ ---
223
+
224
+ ### useMediaByIndex
225
+
226
+ Queries assets by secondary index: any name passed to `store.defineIndex()` in `resolveStore`, plus the built-in indexes `mimeType` and `mediaKind` that the store adds for every asset.
227
+
228
+ ```typescript
229
+ function useMediaByIndex(
230
+ indexName: string,
231
+ value: string,
232
+ options?: {
233
+ limit?: number;
234
+ cursor?: string;
235
+ refetchOnSyncComplete?: boolean;
236
+ },
237
+ ): AsyncState<PaginationResult<ResolvedMediaAsset>>;
238
+ ```
239
+
240
+ | Parameter | Type | Description |
241
+ | ----------- | -------- | -------------------------------------------------- |
242
+ | `indexName` | `string` | Name of the index (as defined by `defineIndex()`). |
243
+ | `value` | `string` | The index value to match. |
244
+ | `options` | `object` | Optional pagination and refetch options. |
245
+
246
+ **Options:**
247
+
248
+ | Option | Type | Default | Description |
249
+ | ----------------------- | --------- | ------- | ------------------------------ |
250
+ | `limit` | `number` | — | Maximum results per page. |
251
+ | `cursor` | `string` | — | Opaque cursor for next page. |
252
+ | `refetchOnSyncComplete` | `boolean` | `true` | Re-fetch after sync completes. |
253
+
254
+ **Returns:** `AsyncState<PaginationResult<ResolvedMediaAsset>>`
255
+
256
+ ```tsx
257
+ import { useMediaByIndex } from "@rockhall/electron-offline-content/react";
258
+
259
+ function ExhibitList() {
260
+ const { data, loading } = useMediaByIndex("category", "exhibits", {
261
+ limit: 30,
262
+ refetchOnSyncComplete: true,
263
+ });
264
+
265
+ if (loading || !data) return <p>Loading…</p>;
266
+
267
+ return (
268
+ <>
269
+ {data.items.map((asset) => (
270
+ <div key={asset.key}>
271
+ <img src={asset.url} alt={(asset.metadata.title as string) ?? asset.displayKey} />
272
+ </div>
273
+ ))}
274
+ </>
275
+ );
276
+ }
277
+ ```
278
+
279
+ ---
280
+
281
+ ### useFileStemMatch
282
+
283
+ Searches cached content by filename stem across all assets.
284
+
285
+ ```typescript
286
+ function useFileStemMatch(
287
+ stem: string,
288
+ options?: FileStemMatchQueryOptions,
289
+ ): AsyncState<PaginationResult<FileStemMatch>>;
290
+ ```
291
+
292
+ | Parameter | Type | Description |
293
+ | --------- | ---------------------------------------- | ------------------------------------------------ |
294
+ | `stem` | `string` | Filename stem to search for (without extension). |
295
+ | `options` | `FileStemMatchQueryOptions \| undefined` | Filtering and pagination options. |
296
+
297
+ **FileStemMatchQueryOptions:**
298
+
299
+ | Option | Type | Default | Description |
300
+ | ----------------------- | --------- | ------- | ------------------------------ |
301
+ | `limit` | `number` | — | Maximum matches per page. |
302
+ | `cursor` | `string` | — | Opaque cursor for next page. |
303
+ | `refetchOnSyncComplete` | `boolean` | `true` | Re-fetch after sync completes. |
304
+
305
+ **Returns:** `AsyncState<PaginationResult<FileStemMatch>>`
306
+
307
+ ```tsx
308
+ import { useFileStemMatch } from "@rockhall/electron-offline-content/react";
309
+
310
+ function Search({ query }: { query: string }) {
311
+ const { data, loading } = useFileStemMatch(query, { limit: 20 });
312
+
313
+ if (loading || !data) return <p>Searching…</p>;
314
+
315
+ return (
316
+ <ul>
317
+ {data.items.map((match) => (
318
+ <li key={match.asset.key}>{match.asset.displayKey}</li>
319
+ ))}
320
+ </ul>
321
+ );
322
+ }
323
+ ```
324
+
325
+ ---
326
+
327
+ ### useMediaCacheErrors
328
+
329
+ Aggregates sync errors and provider-wide query errors without requiring caller arguments.
330
+
331
+ ```typescript
332
+ function useMediaCacheErrors(): MediaCacheErrors;
333
+ ```
334
+
335
+ **Returns:** `MediaCacheErrors`
336
+
337
+ ```typescript
338
+ interface MediaCacheErrors {
339
+ syncError: Error | null;
340
+ statusError: Error | null;
341
+ queryErrors: Error[];
342
+ hasError: boolean;
343
+ primaryError: Error | null;
344
+ }
345
+ ```
346
+
347
+ | Field | Description |
348
+ | -------------- | ------------------------------------------------------------------------------------------ |
349
+ | `syncError` | Error from the sync process itself (from the shared provider status). |
350
+ | `statusError` | Error fetching status. |
351
+ | `queryErrors` | Array of non-null errors from mounted query hooks under the same `MediaCacheProvider`. |
352
+ | `hasError` | `true` if any of the above are set. |
353
+ | `primaryError` | First available error in priority order: `statusError` → first `queryError` → `syncError`. |
354
+
355
+ ```tsx
356
+ import { useMediaByIndex, useMediaCacheErrors } from "@rockhall/electron-offline-content/react";
357
+
358
+ function Page() {
359
+ const videos = useMediaByIndex("category", "videos");
360
+ const images = useMediaByIndex("category", "images");
361
+ const errors = useMediaCacheErrors();
362
+
363
+ if (errors.hasError) {
364
+ return <p>Error: {errors.primaryError?.message}</p>;
365
+ }
366
+
367
+ // render content
368
+ }
369
+ ```
370
+
371
+ ---
372
+
373
+ ## Return Types
374
+
375
+ ### ResolvedMediaAsset
376
+
377
+ A single resolved asset with a ready-to-render URL.
378
+
379
+ ```typescript
380
+ interface ResolvedMediaAsset {
381
+ key: string;
382
+ displayKey: string;
383
+ version: string;
384
+ kind: MediaKind;
385
+ mimeType: string;
386
+ byteLength?: number;
387
+ url: string;
388
+ indexes: Record<string, string | string[]>;
389
+ metadata: Record<string, JsonValue>;
390
+ }
391
+ ```
392
+
393
+ | Field | Description |
394
+ | ------------ | -------------------------------------------------------------------------------- |
395
+ | `key` | Stable storage identity (SHA-256–derived hash, 16 hex chars). |
396
+ | `displayKey` | Original human-readable key (`string` input, or array segments joined with `/`). |
397
+ | `version` | Content version string from the store. |
398
+ | `kind` | Media kind enum value (e.g. `"video"`, `"image"`, `"audio"`, `"document"`). |
399
+ | `mimeType` | MIME type (e.g. `"video/mp4"`). |
400
+ | `byteLength` | Size in bytes when known from the store. |
401
+ | `url` | Ready-to-render URL. `media://` in offline mode, HTTPS in devPassthrough mode. |
402
+ | `indexes` | Index names to their values; arrays for multi-cardinality indexes. |
403
+ | `metadata` | Arbitrary JSON metadata from `store.add()`. |
404
+
405
+ ### FileStemMatch
406
+
407
+ A filename stem search result.
408
+
409
+ ```typescript
410
+ interface FileStemMatch {
411
+ asset: ResolvedMediaAsset;
412
+ score: number;
413
+ }
414
+ ```
415
+
416
+ | Field | Description |
417
+ | ------- | -------------------------------------------------------- |
418
+ | `asset` | The matched `ResolvedMediaAsset`. |
419
+ | `score` | Relevance score for the match (higher is more relevant). |
420
+
421
+ ### MediaCacheReadyState
422
+
423
+ ```typescript
424
+ interface MediaCacheReadyState {
425
+ ready: boolean;
426
+ syncing: boolean;
427
+ phase: "idle" | "syncing" | "ready" | "error";
428
+ activeGenerationId: number | null;
429
+ syncError: Error | null;
430
+ }
431
+ ```
432
+
433
+ ### MediaCacheErrors
434
+
435
+ ```typescript
436
+ interface MediaCacheErrors {
437
+ syncError: Error | null;
438
+ statusError: Error | null;
439
+ queryErrors: Error[];
440
+ hasError: boolean;
441
+ primaryError: Error | null;
442
+ }
443
+ ```