@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.
- package/CHANGELOG.md +384 -0
- package/LICENSE +21 -0
- package/README.md +794 -0
- package/dist/internal/asset-file-name.cjs +13 -0
- package/dist/internal/asset-file-name.cjs.map +1 -0
- package/dist/internal/asset-file-name.d.cts +6 -0
- package/dist/internal/asset-file-name.d.cts.map +1 -0
- package/dist/internal/asset-file-name.d.ts +6 -0
- package/dist/internal/asset-file-name.d.ts.map +1 -0
- package/dist/internal/asset-file-name.js +12 -0
- package/dist/internal/asset-file-name.js.map +1 -0
- package/dist/internal/asset-key.cjs +30 -0
- package/dist/internal/asset-key.cjs.map +1 -0
- package/dist/internal/asset-key.d.cts +19 -0
- package/dist/internal/asset-key.d.cts.map +1 -0
- package/dist/internal/asset-key.d.ts +19 -0
- package/dist/internal/asset-key.d.ts.map +1 -0
- package/dist/internal/asset-key.js +27 -0
- package/dist/internal/asset-key.js.map +1 -0
- package/dist/internal/log-format.cjs +98 -0
- package/dist/internal/log-format.cjs.map +1 -0
- package/dist/internal/log-format.d.cts +10 -0
- package/dist/internal/log-format.d.cts.map +1 -0
- package/dist/internal/log-format.d.ts +10 -0
- package/dist/internal/log-format.d.ts.map +1 -0
- package/dist/internal/log-format.js +97 -0
- package/dist/internal/log-format.js.map +1 -0
- package/dist/internal/media-kind.cjs +46 -0
- package/dist/internal/media-kind.cjs.map +1 -0
- package/dist/internal/media-kind.d.cts +20 -0
- package/dist/internal/media-kind.d.cts.map +1 -0
- package/dist/internal/media-kind.d.ts +20 -0
- package/dist/internal/media-kind.d.ts.map +1 -0
- package/dist/internal/media-kind.js +45 -0
- package/dist/internal/media-kind.js.map +1 -0
- package/dist/internal/url-warn.cjs +14 -0
- package/dist/internal/url-warn.cjs.map +1 -0
- package/dist/internal/url-warn.d.cts +10 -0
- package/dist/internal/url-warn.d.cts.map +1 -0
- package/dist/internal/url-warn.d.ts +10 -0
- package/dist/internal/url-warn.d.ts.map +1 -0
- package/dist/internal/url-warn.js +13 -0
- package/dist/internal/url-warn.js.map +1 -0
- package/dist/internal/validation.cjs +222 -0
- package/dist/internal/validation.cjs.map +1 -0
- package/dist/internal/validation.d.cts +78 -0
- package/dist/internal/validation.d.cts.map +1 -0
- package/dist/internal/validation.d.ts +78 -0
- package/dist/internal/validation.d.ts.map +1 -0
- package/dist/internal/validation.js +196 -0
- package/dist/internal/validation.js.map +1 -0
- package/dist/main/asset-download.cjs +265 -0
- package/dist/main/asset-download.cjs.map +1 -0
- package/dist/main/asset-download.d.cts +12 -0
- package/dist/main/asset-download.d.cts.map +1 -0
- package/dist/main/asset-download.d.ts +12 -0
- package/dist/main/asset-download.d.ts.map +1 -0
- package/dist/main/asset-download.js +263 -0
- package/dist/main/asset-download.js.map +1 -0
- package/dist/main/database.cjs +473 -0
- package/dist/main/database.cjs.map +1 -0
- package/dist/main/database.d.cts +81 -0
- package/dist/main/database.d.cts.map +1 -0
- package/dist/main/database.d.ts +81 -0
- package/dist/main/database.d.ts.map +1 -0
- package/dist/main/database.js +472 -0
- package/dist/main/database.js.map +1 -0
- package/dist/main/index.cjs +22 -0
- package/dist/main/index.d.cts +7 -0
- package/dist/main/index.d.ts +7 -0
- package/dist/main/index.js +7 -0
- package/dist/main/media-cache.cjs +862 -0
- package/dist/main/media-cache.cjs.map +1 -0
- package/dist/main/media-cache.d.cts +134 -0
- package/dist/main/media-cache.d.cts.map +1 -0
- package/dist/main/media-cache.d.ts +134 -0
- package/dist/main/media-cache.d.ts.map +1 -0
- package/dist/main/media-cache.js +854 -0
- package/dist/main/media-cache.js.map +1 -0
- package/dist/main/storage-root-lock.cjs +124 -0
- package/dist/main/storage-root-lock.cjs.map +1 -0
- package/dist/main/storage-root-lock.d.cts +11 -0
- package/dist/main/storage-root-lock.d.cts.map +1 -0
- package/dist/main/storage-root-lock.d.ts +11 -0
- package/dist/main/storage-root-lock.d.ts.map +1 -0
- package/dist/main/storage-root-lock.js +120 -0
- package/dist/main/storage-root-lock.js.map +1 -0
- package/dist/main/store.cjs +197 -0
- package/dist/main/store.cjs.map +1 -0
- package/dist/main/store.d.cts +83 -0
- package/dist/main/store.d.cts.map +1 -0
- package/dist/main/store.d.ts +83 -0
- package/dist/main/store.d.ts.map +1 -0
- package/dist/main/store.js +195 -0
- package/dist/main/store.js.map +1 -0
- package/dist/preload/index.cjs +36 -0
- package/dist/preload/index.cjs.map +1 -0
- package/dist/preload/index.d.cts +14 -0
- package/dist/preload/index.d.cts.map +1 -0
- package/dist/preload/index.d.ts +14 -0
- package/dist/preload/index.d.ts.map +1 -0
- package/dist/preload/index.js +34 -0
- package/dist/preload/index.js.map +1 -0
- package/dist/react/index.cjs +199 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +50 -0
- package/dist/react/index.d.cts.map +1 -0
- package/dist/react/index.d.ts +50 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +191 -0
- package/dist/react/index.js.map +1 -0
- package/dist/renderer/helpers.cjs +36 -0
- package/dist/renderer/helpers.cjs.map +1 -0
- package/dist/renderer/helpers.d.cts +11 -0
- package/dist/renderer/helpers.d.cts.map +1 -0
- package/dist/renderer/helpers.d.ts +11 -0
- package/dist/renderer/helpers.d.ts.map +1 -0
- package/dist/renderer/helpers.js +35 -0
- package/dist/renderer/helpers.js.map +1 -0
- package/dist/renderer/index.cjs +20 -0
- package/dist/renderer/index.cjs.map +1 -0
- package/dist/renderer/index.d.cts +14 -0
- package/dist/renderer/index.d.cts.map +1 -0
- package/dist/renderer/index.d.ts +14 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/renderer/index.js +14 -0
- package/dist/renderer/index.js.map +1 -0
- package/dist/renderer/runtime.cjs +278 -0
- package/dist/renderer/runtime.cjs.map +1 -0
- package/dist/renderer/runtime.d.cts +35 -0
- package/dist/renderer/runtime.d.cts.map +1 -0
- package/dist/renderer/runtime.d.ts +35 -0
- package/dist/renderer/runtime.d.ts.map +1 -0
- package/dist/renderer/runtime.js +273 -0
- package/dist/renderer/runtime.js.map +1 -0
- package/dist/renderer/window-globals.d.cts +9 -0
- package/dist/renderer/window-globals.d.cts.map +1 -0
- package/dist/renderer/window-globals.d.ts +9 -0
- package/dist/renderer/window-globals.d.ts.map +1 -0
- package/dist/shared/errors.cjs +102 -0
- package/dist/shared/errors.cjs.map +1 -0
- package/dist/shared/errors.d.cts +45 -0
- package/dist/shared/errors.d.cts.map +1 -0
- package/dist/shared/errors.d.ts +45 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +93 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/ipc.cjs +14 -0
- package/dist/shared/ipc.cjs.map +1 -0
- package/dist/shared/ipc.d.cts +12 -0
- package/dist/shared/ipc.d.cts.map +1 -0
- package/dist/shared/ipc.d.ts +12 -0
- package/dist/shared/ipc.d.ts.map +1 -0
- package/dist/shared/ipc.js +13 -0
- package/dist/shared/ipc.js.map +1 -0
- package/dist/shared/normalize.cjs +19 -0
- package/dist/shared/normalize.cjs.map +1 -0
- package/dist/shared/normalize.d.cts +11 -0
- package/dist/shared/normalize.d.cts.map +1 -0
- package/dist/shared/normalize.d.ts +11 -0
- package/dist/shared/normalize.d.ts.map +1 -0
- package/dist/shared/normalize.js +18 -0
- package/dist/shared/normalize.js.map +1 -0
- package/dist/shared/pagination.cjs +32 -0
- package/dist/shared/pagination.cjs.map +1 -0
- package/dist/shared/pagination.d.cts +14 -0
- package/dist/shared/pagination.d.cts.map +1 -0
- package/dist/shared/pagination.d.ts +14 -0
- package/dist/shared/pagination.d.ts.map +1 -0
- package/dist/shared/pagination.js +28 -0
- package/dist/shared/pagination.js.map +1 -0
- package/dist/shared/stem.cjs +16 -0
- package/dist/shared/stem.cjs.map +1 -0
- package/dist/shared/stem.d.cts +6 -0
- package/dist/shared/stem.d.cts.map +1 -0
- package/dist/shared/stem.d.ts +6 -0
- package/dist/shared/stem.d.ts.map +1 -0
- package/dist/shared/stem.js +14 -0
- package/dist/shared/stem.js.map +1 -0
- package/dist/shared/types.cjs +15 -0
- package/dist/shared/types.cjs.map +1 -0
- package/dist/shared/types.d.cts +234 -0
- package/dist/shared/types.d.cts.map +1 -0
- package/dist/shared/types.d.ts +234 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +14 -0
- package/dist/shared/types.js.map +1 -0
- package/package.json +120 -0
- package/skills/authenticated-downloads/SKILL.md +203 -0
- package/skills/cache-configuration/SKILL.md +357 -0
- package/skills/cache-configuration/references/options.md +356 -0
- package/skills/getting-started/SKILL.md +407 -0
- package/skills/production-checklist/SKILL.md +397 -0
- package/skills/react-rendering/SKILL.md +424 -0
- package/skills/react-rendering/references/hooks.md +443 -0
- 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
|