@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,407 @@
1
+ ---
2
+ name: getting-started
3
+ description: >
4
+ Full greenfield integration of @rockhall/electron-offline-content:
5
+ install, write resolveStore with createMediaStore, configure
6
+ createMediaCache in main, wire preload bridge with
7
+ exposeMediaCacheBridge, add MediaCacheProvider and hooks in React,
8
+ render first content offline. Covers app.requestSingleInstanceLock,
9
+ createMediaCache timing relative to app.whenReady, and
10
+ mediaCache.start() fire-and-forget pattern.
11
+ type: lifecycle
12
+ library: electron-offline-content
13
+ library_version: "0.4.0"
14
+ sources:
15
+ - "rockhallweb/electron-offline-content:README.md"
16
+ - "rockhallweb/electron-offline-content:src/main/index.ts"
17
+ - "rockhallweb/electron-offline-content:src/main/media-cache.ts"
18
+ - "rockhallweb/electron-offline-content:src/main/store.ts"
19
+ - "rockhallweb/electron-offline-content:src/preload/index.ts"
20
+ - "rockhallweb/electron-offline-content:src/react/index.tsx"
21
+ - "rockhallweb/electron-offline-content:examples/local/src/main.ts"
22
+ ---
23
+
24
+ # Getting Started
25
+
26
+ ## Setup
27
+
28
+ Install the package:
29
+
30
+ ```bash
31
+ pnpm add @rockhall/electron-offline-content
32
+ ```
33
+
34
+ Prerequisites: Node.js >= 24, Electron >= 40. React >= 18 is an optional peer dependency needed only for the `/react` export.
35
+
36
+ Three files wire the integration across all three Electron processes: main, preload, and renderer.
37
+
38
+ ### main.ts
39
+
40
+ ```typescript
41
+ import { app, BrowserWindow } from "electron";
42
+ import path from "node:path";
43
+ import { createMediaCache, createMediaStore } from "@rockhall/electron-offline-content/main";
44
+
45
+ if (!app.requestSingleInstanceLock()) {
46
+ app.exit(1);
47
+ }
48
+
49
+ const mediaCache = createMediaCache({
50
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
51
+ resolveStore: async () => {
52
+ const res = await fetch("https://cms.example.com/api/content");
53
+ const data = await res.json();
54
+
55
+ const store = createMediaStore();
56
+ const category = store.defineIndex("category");
57
+
58
+ for (const v of data.videos) {
59
+ store.add(["video", v.slug], {
60
+ version: v.updatedAt,
61
+ mimeType: "video/mp4",
62
+ url: v.videoUrl,
63
+ metadata: { title: v.title, category: "videos" },
64
+ indexes: [category("videos")],
65
+ });
66
+ }
67
+
68
+ return store;
69
+ },
70
+ });
71
+
72
+ async function bootstrap() {
73
+ await app.whenReady();
74
+ const win = new BrowserWindow({
75
+ webPreferences: { preload: path.join(__dirname, "preload.js") },
76
+ });
77
+ win.loadFile("index.html");
78
+ mediaCache.start();
79
+ }
80
+
81
+ bootstrap();
82
+ ```
83
+
84
+ Key ordering constraints:
85
+
86
+ 1. `app.requestSingleInstanceLock()` — before anything else.
87
+ 2. `createMediaCache()` — before `app.whenReady()`. The constructor registers `media:` as a privileged scheme, which must happen before the app ready event.
88
+ 3. `BrowserWindow` creation — after `app.whenReady()`.
89
+ 4. `mediaCache.start()` — after `app.whenReady()`, without `await`. Fire-and-forget; React hooks show progress while sync runs in the background.
90
+
91
+ ### preload.ts
92
+
93
+ ```typescript
94
+ import { exposeMediaCacheBridge } from "@rockhall/electron-offline-content/preload";
95
+
96
+ exposeMediaCacheBridge();
97
+ ```
98
+
99
+ This calls `contextBridge.exposeInMainWorld` to put the IPC bridge on `window.mediaCache`. All React hooks depend on this bridge.
100
+
101
+ ### App.tsx
102
+
103
+ ```tsx
104
+ import {
105
+ MediaCacheProvider,
106
+ useMediaByIndex,
107
+ useMediaCacheReady,
108
+ } from "@rockhall/electron-offline-content/react";
109
+
110
+ function Content() {
111
+ const ready = useMediaCacheReady();
112
+ const videos = useMediaByIndex("category", "videos", { limit: 20 });
113
+
114
+ if (!ready.data?.ready) return <p>Preparing offline content...</p>;
115
+ if (videos.loading) return <p>Loading...</p>;
116
+
117
+ return (
118
+ <div>
119
+ {videos.data?.items.map((asset) => (
120
+ <video key={asset.key} src={asset.url} title={asset.displayKey} controls />
121
+ ))}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ export function App() {
127
+ return (
128
+ <MediaCacheProvider>
129
+ <Content />
130
+ </MediaCacheProvider>
131
+ );
132
+ }
133
+ ```
134
+
135
+ `MediaCacheProvider` must wrap any component that uses hooks. Hook URLs (`asset.url`) resolve to `media://` in offline mode or remote URLs in dev passthrough — pass them directly to `src` attributes.
136
+
137
+ ## Core Patterns
138
+
139
+ ### Controlling start() timing
140
+
141
+ `mediaCache.start()` is fire-and-forget. It registers the protocol handler, attaches IPC listeners, and kicks off the initial sync. Do not `await` it at app launch — that blocks window creation until the entire download completes.
142
+
143
+ You control when syncing begins. It does not have to happen at launch:
144
+
145
+ ```typescript
146
+ async function bootstrap() {
147
+ await app.whenReady();
148
+ const win = new BrowserWindow({
149
+ webPreferences: { preload: path.join(__dirname, "preload.js") },
150
+ });
151
+ win.loadFile("index.html");
152
+ mediaCache.start();
153
+ }
154
+ ```
155
+
156
+ To defer syncing until user confirmation, skip `start()` in bootstrap and trigger it from the renderer via IPC:
157
+
158
+ ```typescript
159
+ import { ipcMain } from "electron";
160
+
161
+ ipcMain.handle("begin-sync", () => {
162
+ mediaCache.start();
163
+ });
164
+ ```
165
+
166
+ ### Building a media store with createMediaStore
167
+
168
+ `createMediaStore()` creates a flat asset store. Add assets with `store.add(assetKey, input)` and define secondary indexes with `store.defineIndex()` for querying. The first argument is an `AssetKeyInput` (`string` or `readonly string[]`); resolved assets expose `key` (hash) and `displayKey` (human-readable).
169
+
170
+ ```typescript
171
+ import { createMediaStore } from "@rockhall/electron-offline-content/main";
172
+
173
+ const store = createMediaStore();
174
+
175
+ const category = store.defineIndex("category");
176
+ const year = store.defineIndex("year");
177
+
178
+ store.add(["video", "welcome"], {
179
+ version: "v2",
180
+ mimeType: "video/mp4",
181
+ url: "https://cdn.example.com/welcome.v2.mp4",
182
+ metadata: { title: "Welcome Video", category: "lobby", year: 2026 },
183
+ indexes: [category("lobby"), year("2026")],
184
+ });
185
+
186
+ store.add(["image", "welcome-poster"], {
187
+ version: "v2",
188
+ mimeType: "image/jpeg",
189
+ url: "https://cdn.example.com/welcome-poster.jpg",
190
+ metadata: { title: "Welcome Poster", category: "lobby", year: 2026 },
191
+ indexes: [category("lobby"), year("2026")],
192
+ });
193
+ ```
194
+
195
+ Compared to deep nesting, this produces clearer validation errors (the asset that failed is obvious) and keeps each definition under ~8 lines.
196
+
197
+ ## Common Mistakes
198
+
199
+ ### CRITICAL: Creating cache after app.whenReady()
200
+
201
+ `createMediaCache()` must be called BEFORE `app.whenReady()`. The constructor calls `protocol.registerSchemesAsPrivileged` to register the `media:` scheme, which Electron requires before the app ready event. Constructing after ready causes silent scheme registration failure.
202
+
203
+ Wrong:
204
+
205
+ ```typescript
206
+ import { app } from "electron";
207
+ import { createMediaCache } from "@rockhall/electron-offline-content/main";
208
+
209
+ await app.whenReady();
210
+ const mediaCache = createMediaCache({
211
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
212
+ resolveStore: async () => store,
213
+ });
214
+ await mediaCache.start();
215
+ ```
216
+
217
+ Correct:
218
+
219
+ ```typescript
220
+ import { app } from "electron";
221
+ import { createMediaCache } from "@rockhall/electron-offline-content/main";
222
+
223
+ const mediaCache = createMediaCache({
224
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
225
+ resolveStore: async () => store,
226
+ });
227
+
228
+ await app.whenReady();
229
+ mediaCache.start();
230
+ ```
231
+
232
+ Source: README; media-cache.ts constructor calls `ensureMediaCacheProtocolSchemesPrivileged()`
233
+
234
+ ### CRITICAL: Missing preload bridge setup
235
+
236
+ Without `exposeMediaCacheBridge()` in the preload script, `window.mediaCache` is undefined and all React hooks throw `"MediaCache bridge is unavailable"`.
237
+
238
+ Wrong:
239
+
240
+ ```typescript
241
+ import { contextBridge } from "electron";
242
+ ```
243
+
244
+ Correct:
245
+
246
+ ```typescript
247
+ import { exposeMediaCacheBridge } from "@rockhall/electron-offline-content/preload";
248
+
249
+ exposeMediaCacheBridge();
250
+ ```
251
+
252
+ Source: react/index.tsx `useMediaBridge()` throw
253
+
254
+ ### HIGH: Missing MediaCacheProvider in React tree
255
+
256
+ All query hooks require a `MediaCacheProvider` ancestor. Without it, `useMediaBridge()` and the query hooks throw.
257
+
258
+ Wrong:
259
+
260
+ ```tsx
261
+ import { useMediaByIndex } from "@rockhall/electron-offline-content/react";
262
+
263
+ function App() {
264
+ const videos = useMediaByIndex("category", "videos", { limit: 20 });
265
+ return (
266
+ <div>
267
+ {videos.data?.items.map((asset) => (
268
+ <p key={asset.key}>{asset.metadata.title}</p>
269
+ ))}
270
+ </div>
271
+ );
272
+ }
273
+ ```
274
+
275
+ Correct:
276
+
277
+ ```tsx
278
+ import { MediaCacheProvider, useMediaByIndex } from "@rockhall/electron-offline-content/react";
279
+
280
+ function Content() {
281
+ const videos = useMediaByIndex("category", "videos", { limit: 20 });
282
+ return (
283
+ <div>
284
+ {videos.data?.items.map((asset) => (
285
+ <p key={asset.key}>{asset.metadata.title}</p>
286
+ ))}
287
+ </div>
288
+ );
289
+ }
290
+
291
+ function App() {
292
+ return (
293
+ <MediaCacheProvider>
294
+ <Content />
295
+ </MediaCacheProvider>
296
+ );
297
+ }
298
+ ```
299
+
300
+ Source: react/index.tsx `MediaCacheProvider`
301
+
302
+ ### HIGH: Forgetting app.requestSingleInstanceLock()
303
+
304
+ Without the instance lock, a second Electron process can launch and collide on the same storage root, causing `StorageOwnershipError`. The error surfaces at `start()` time, not at construction.
305
+
306
+ Wrong:
307
+
308
+ ```typescript
309
+ const mediaCache = createMediaCache({
310
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
311
+ resolveStore: async () => store,
312
+ });
313
+
314
+ async function bootstrap() {
315
+ await app.whenReady();
316
+ mediaCache.start();
317
+ }
318
+
319
+ bootstrap();
320
+ ```
321
+
322
+ Correct:
323
+
324
+ ```typescript
325
+ if (!app.requestSingleInstanceLock()) {
326
+ app.exit(1);
327
+ }
328
+
329
+ const mediaCache = createMediaCache({
330
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
331
+ resolveStore: async () => store,
332
+ });
333
+
334
+ async function bootstrap() {
335
+ await app.whenReady();
336
+ mediaCache.start();
337
+ }
338
+
339
+ bootstrap();
340
+ ```
341
+
342
+ Source: README; examples/local/src/main.ts
343
+
344
+ ### HIGH: Awaiting mediaCache.start() at app launch
345
+
346
+ `start()` begins the async download pipeline. Awaiting it before creating a window blocks the UI until the entire sync completes — the app appears blank until all assets download.
347
+
348
+ Wrong:
349
+
350
+ ```typescript
351
+ await app.whenReady();
352
+ await mediaCache.start();
353
+ createWindow();
354
+ ```
355
+
356
+ Correct:
357
+
358
+ ```typescript
359
+ await app.whenReady();
360
+ createWindow();
361
+ mediaCache.start();
362
+ ```
363
+
364
+ React hooks (`useMediaCacheReady`, `useMediaCacheStatus`) show sync progress while the download runs in the background.
365
+
366
+ Source: Maintainer interview; media-cache.ts
367
+
368
+ ### MEDIUM: Not realizing start() timing is flexible
369
+
370
+ Wrong:
371
+
372
+ ```typescript
373
+ async function bootstrap() {
374
+ await app.whenReady();
375
+ mediaCache.start();
376
+ createWindow();
377
+ }
378
+ ```
379
+
380
+ Correct:
381
+
382
+ ```typescript
383
+ async function bootstrap() {
384
+ await app.whenReady();
385
+ createWindow();
386
+
387
+ ipcMain.handle("user-confirmed-download", () => {
388
+ mediaCache.start();
389
+ });
390
+ }
391
+ ```
392
+
393
+ `start()` does not need to run at app launch. Developers control when syncing begins — after user confirmation, after other initialization, or triggered by a renderer button via IPC.
394
+
395
+ Source: Maintainer interview
396
+
397
+ ### HIGH Tension: Store validation vs sync-time failures
398
+
399
+ `resolveStore` must return a valid **`MediaStore`**. Validation is strict on required fields like `version` and HTTP(S) asset URLs; keys must be non-empty (`AssetKeyInput`) but are not otherwise content-validated. Use **`createMediaStore`** and **`store.add()`** so errors surface when you build the store, not only during sync.
400
+
401
+ See also: store-authoring/SKILL.md § Common Mistakes
402
+
403
+ ---
404
+
405
+ See also: store-authoring/SKILL.md — Writing resolveStore functions and using createMediaStore
406
+ See also: cache-configuration/SKILL.md — All createMediaCache options
407
+ See also: react-rendering/SKILL.md — Complete React hooks API