@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,369 @@
1
+ ---
2
+ name: store-authoring
3
+ description: >
4
+ Writing resolveStore functions against any remote source (CMS, S3,
5
+ REST API). Using createMediaStore, store.defineIndex, and store.add
6
+ for flat asset stores. User-defined secondary indexes for querying.
7
+ Per-asset versioning, asset key conventions, validation rules,
8
+ version-driven cache busting. Embedding presigned URLs in asset
9
+ url fields during resolveStore.
10
+ type: core
11
+ library: electron-offline-content
12
+ library_version: "0.4.0"
13
+ requires:
14
+ - getting-started
15
+ sources:
16
+ - "rockhallweb/electron-offline-content:src/main/store.ts"
17
+ - "rockhallweb/electron-offline-content:src/shared/normalize.ts"
18
+ - "rockhallweb/electron-offline-content:src/shared/types.ts"
19
+ - "rockhallweb/electron-offline-content:src/internal/validation.ts"
20
+ - "rockhallweb/electron-offline-content:src/internal/asset-file-name.ts"
21
+ ---
22
+
23
+ > **Dependency:** This skill builds on getting-started. Read it first for full main → preload → renderer wiring.
24
+
25
+ ## Setup
26
+
27
+ A complete `resolveStore` function that fetches from an API and builds a flat asset store with secondary indexes:
28
+
29
+ ```typescript
30
+ import { createMediaStore } from "@rockhall/electron-offline-content/main";
31
+
32
+ interface ApiCourse {
33
+ id: string;
34
+ title: string;
35
+ revision: string;
36
+ level: "beginner" | "advanced";
37
+ videoUrl: string;
38
+ posterUrl: string;
39
+ subtitleUrl: string | null;
40
+ }
41
+
42
+ async function resolveStore() {
43
+ const res = await fetch("https://cms.example.com/api/courses");
44
+ const courses: ApiCourse[] = await res.json();
45
+
46
+ const store = createMediaStore();
47
+ const level = store.defineIndex("level");
48
+ const type = store.defineIndex("type");
49
+
50
+ for (const course of courses) {
51
+ store.add(["course", course.id, "video"], {
52
+ version: course.revision,
53
+ mimeType: "video/mp4",
54
+ url: course.videoUrl,
55
+ metadata: { title: course.title, level: course.level, type: "video" },
56
+ indexes: [level(course.level), type("video")],
57
+ });
58
+
59
+ store.add(["course", course.id, "poster"], {
60
+ version: course.revision,
61
+ mimeType: "image/jpeg",
62
+ url: course.posterUrl,
63
+ metadata: { title: `${course.title} poster`, level: course.level, type: "poster" },
64
+ indexes: [level(course.level), type("poster")],
65
+ });
66
+
67
+ if (course.subtitleUrl) {
68
+ store.add(["course", course.id, "subs-en"], {
69
+ version: course.revision,
70
+ mimeType: "text/vtt",
71
+ fileName: "en.vtt",
72
+ url: course.subtitleUrl,
73
+ metadata: { title: `${course.title} subtitles`, level: course.level, type: "subtitle" },
74
+ indexes: [level(course.level), type("subtitle")],
75
+ });
76
+ }
77
+ }
78
+
79
+ return store;
80
+ }
81
+ ```
82
+
83
+ Resolved assets expose `asset.key` (storage hash) and `asset.displayKey` (human-readable path, e.g. `course/intro-101/video`).
84
+
85
+ ## Core Patterns
86
+
87
+ ### User-defined secondary indexes
88
+
89
+ Indexes replace the old namespace hierarchy. Define them with `store.defineIndex()` before adding assets, then pass `IndexTag` values from each handle into `store.add()` for querying with `useMediaByIndex` in the renderer or `listByIndex` in the main process.
90
+
91
+ ```typescript
92
+ import { createMediaStore } from "@rockhall/electron-offline-content/main";
93
+
94
+ const store = createMediaStore();
95
+
96
+ const category = store.defineIndex("category");
97
+ const year = store.defineIndex("year");
98
+ const floor = store.defineIndex("floor");
99
+
100
+ for (const exhibit of exhibits) {
101
+ store.add(["exhibit", exhibit.slug], {
102
+ version: exhibit.revision,
103
+ mimeType: exhibit.mimeType,
104
+ url: exhibit.mediaUrl,
105
+ metadata: {
106
+ title: exhibit.title,
107
+ category: exhibit.category,
108
+ year: exhibit.year,
109
+ floor: exhibit.floor,
110
+ },
111
+ indexes: [category(exhibit.category), year(String(exhibit.year)), floor(exhibit.floor)],
112
+ });
113
+ }
114
+ ```
115
+
116
+ ```tsx
117
+ // In renderer — query by index
118
+ const floor2 = useMediaByIndex("floor", "2", { limit: 100 });
119
+ const videos2026 = useMediaByIndex("year", "2026");
120
+ ```
121
+
122
+ ### Asset key conventions
123
+
124
+ The first argument to `store.add()` is an `AssetKeyInput`: a non-empty string or a non-empty array of non-empty string segments. Arrays avoid ambiguity at segment boundaries and produce a readable `displayKey` (segments joined with `/`) while `key` on resolved assets remains the storage hash.
125
+
126
+ ```typescript
127
+ store.add(["video", "welcome"], { ... });
128
+ store.add(["video", "welcome", "poster"], { ... });
129
+ store.add(["inductee", "2026", "beyonce", "ceremony"], { ... });
130
+ store.add(["inductee", "2026", "beyonce", "poster"], { ... });
131
+ ```
132
+
133
+ Use the same `AssetKeyInput` in `useMediaAsset(key)` for single-asset lookups. Protocol URLs still use the encoded storage identity derived from the hash.
134
+
135
+ ### Per-asset versioning
136
+
137
+ Each asset has its own `version` string. When the version changes, the asset is re-downloaded. Assets with unchanged versions keep their cached blobs.
138
+
139
+ ```typescript
140
+ store.add(["video", "welcome"], {
141
+ version: apiEntry.updatedAt, // timestamp-based: "2026-03-15T08:30:00Z"
142
+ mimeType: "video/mp4",
143
+ url: apiEntry.videoUrl,
144
+ });
145
+
146
+ store.add(["image", "logo"], {
147
+ version: apiEntry.contentMd5, // content-hash: "a1b2c3d4e5f6"
148
+ mimeType: "image/png",
149
+ url: apiEntry.logoUrl,
150
+ });
151
+ ```
152
+
153
+ ### Embedding auth in resolveStore
154
+
155
+ Since `resolveAssetRequest` has been removed, embed presigned (or otherwise auth-bearing) URLs in each asset’s `url` field during `resolveStore()`:
156
+
157
+ ```typescript
158
+ import { createMediaStore } from "@rockhall/electron-offline-content/main";
159
+ import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
160
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
161
+
162
+ const s3 = new S3Client({ region: "us-east-1" });
163
+
164
+ async function resolveStore() {
165
+ const store = createMediaStore();
166
+ const catalog = await fetchCatalog();
167
+
168
+ for (const item of catalog) {
169
+ const signedUrl = await getSignedUrl(
170
+ s3,
171
+ new GetObjectCommand({ Bucket: "my-content-bucket", Key: item.objectKey }),
172
+ { expiresIn: 3600 },
173
+ );
174
+
175
+ store.add(["s3", item.id], {
176
+ version: item.revision,
177
+ mimeType: "video/mp4",
178
+ url: signedUrl,
179
+ });
180
+ }
181
+
182
+ return store;
183
+ }
184
+ ```
185
+
186
+ ### Multi-asset content
187
+
188
+ Related assets share a key prefix and use the same indexes. Use `useMediaAsset(key)` for individual lookups or `useMediaByIndex` to query by a shared index value:
189
+
190
+ ```typescript
191
+ const store = createMediaStore();
192
+ const inductee = store.defineIndex("inductee");
193
+ const role = store.defineIndex("role");
194
+
195
+ store.add(["inductee", "2026", "beyonce", "ceremony"], {
196
+ version: "3",
197
+ mimeType: "video/mp4",
198
+ url: "https://cdn.example.com/beyonce-ceremony.mp4",
199
+ metadata: { inducteeId: "beyonce-2026", role: "primary", title: "Beyoncé Induction Ceremony" },
200
+ indexes: [inductee("beyonce-2026"), role("primary")],
201
+ });
202
+
203
+ store.add(["inductee", "2026", "beyonce", "poster"], {
204
+ version: "3",
205
+ mimeType: "image/jpeg",
206
+ url: "https://cdn.example.com/beyonce-poster.jpg",
207
+ metadata: { inducteeId: "beyonce-2026", role: "poster", title: "Beyoncé Poster" },
208
+ indexes: [inductee("beyonce-2026"), role("poster")],
209
+ });
210
+
211
+ store.add(["inductee", "2026", "beyonce", "subs-en"], {
212
+ version: "3",
213
+ mimeType: "text/vtt",
214
+ fileName: "en.vtt",
215
+ url: "https://cdn.example.com/beyonce-en.vtt",
216
+ metadata: { inducteeId: "beyonce-2026", role: "subtitle", title: "English subtitles" },
217
+ indexes: [inductee("beyonce-2026"), role("subtitle")],
218
+ });
219
+ ```
220
+
221
+ ```tsx
222
+ // In renderer — look up related assets by index
223
+ const beyonceAssets = useMediaByIndex("inductee", "beyonce-2026");
224
+ // Or look up individual assets by the same AssetKeyInput used in resolveStore
225
+ const ceremony = useMediaAsset(["inductee", "2026", "beyonce", "ceremony"]);
226
+ const poster = useMediaAsset(["inductee", "2026", "beyonce", "poster"]);
227
+ // asset.displayKey shows the joined path; asset.key is the stable hash
228
+ ```
229
+
230
+ ## Common Mistakes
231
+
232
+ ### HIGH: Duplicate asset keys in store.add calls
233
+
234
+ `store.add()` throws `StoreValidationError` when a key has already been added to the store. Each asset key must be unique.
235
+
236
+ ```typescript
237
+ // WRONG — duplicate key (same AssetKeyInput resolves to the same storage hash)
238
+ store.add(["video", "welcome"], { version: "1", mimeType: "video/mp4", url });
239
+ store.add(["video", "welcome"], { version: "2", mimeType: "video/mp4", url });
240
+ ```
241
+
242
+ ```typescript
243
+ // CORRECT — unique keys
244
+ store.add(["video", "welcome"], { version: "1", mimeType: "video/mp4", url });
245
+ store.add(["video", "welcome-v2"], { version: "2", mimeType: "video/mp4", url });
246
+ ```
247
+
248
+ Source: store.ts; validation.ts
249
+
250
+ ### HIGH: Omitting required asset version field
251
+
252
+ `version` is required (min length 1) and drives cache busting. Fails validation at `store.add()` time.
253
+
254
+ ```typescript
255
+ // WRONG — missing version
256
+ store.add(["video", "welcome"], {
257
+ mimeType: "video/mp4",
258
+ url,
259
+ });
260
+ ```
261
+
262
+ ```typescript
263
+ // CORRECT — version present
264
+ store.add(["video", "welcome"], {
265
+ version: "2026-03-15",
266
+ mimeType: "video/mp4",
267
+ url,
268
+ });
269
+ ```
270
+
271
+ Source: validation.ts
272
+
273
+ ### HIGH: Asset URL without filename in path
274
+
275
+ When `fileName` is omitted, it is derived from the URL basename. URLs ending in `/` or with no parseable filename fail derivation.
276
+
277
+ ```typescript
278
+ // WRONG — URL has no filename to derive
279
+ store.add(["video", "ceremony"], {
280
+ version: "1",
281
+ mimeType: "video/mp4",
282
+ url: "https://cdn.example.com/api/stream/",
283
+ });
284
+ ```
285
+
286
+ ```typescript
287
+ // CORRECT — explicit fileName when URL lacks one
288
+ store.add(["video", "ceremony"], {
289
+ version: "1",
290
+ mimeType: "video/mp4",
291
+ fileName: "ceremony.mp4",
292
+ url: "https://cdn.example.com/api/stream/",
293
+ });
294
+ ```
295
+
296
+ Source: internal/asset-file-name.ts
297
+
298
+ ### HIGH: Forgetting to defineIndex before querying
299
+
300
+ Indexes must be defined with `store.defineIndex()` before assets are added. If you query an undefined index with `useMediaByIndex` or `listByIndex`, the query returns no results.
301
+
302
+ ```typescript
303
+ // WRONG — index not defined
304
+ const store = createMediaStore();
305
+ store.add(["video", "welcome"], {
306
+ version: "1",
307
+ mimeType: "video/mp4",
308
+ url,
309
+ metadata: { category: "lobby" },
310
+ });
311
+ // useMediaByIndex("category", "lobby") returns nothing
312
+ ```
313
+
314
+ ```typescript
315
+ // CORRECT — define index before adding assets
316
+ const store = createMediaStore();
317
+ const category = store.defineIndex("category");
318
+ store.add(["video", "welcome"], {
319
+ version: "1",
320
+ mimeType: "video/mp4",
321
+ url,
322
+ metadata: { category: "lobby" },
323
+ indexes: [category("lobby")],
324
+ });
325
+ ```
326
+
327
+ Source: store.ts; README
328
+
329
+ ### MEDIUM: Using non-HTTP asset URLs
330
+
331
+ Validation enforces `http://` or `https://` schemes. `file://`, `data:`, `blob:` are rejected.
332
+
333
+ ### MEDIUM: Index function returning inconsistent types
334
+
335
+ Index functions must return a `string`. Returning `undefined`, `null`, or a non-string value for some assets causes those assets to be missing from index queries.
336
+
337
+ ```typescript
338
+ // WRONG — returns number, not string
339
+ store.defineIndex("year", (asset) => asset.metadata.year);
340
+
341
+ // CORRECT — always return a string
342
+ store.defineIndex("year", (asset) => String(asset.metadata.year));
343
+ ```
344
+
345
+ Source: store.ts
346
+
347
+ ### MEDIUM: Too many fine-grained indexes
348
+
349
+ Each index adds overhead during store building and sync. Define indexes that match your actual query patterns. Prefer a single index with meaningful values over many single-purpose indexes.
350
+
351
+ Source: Maintainer interview
352
+
353
+ ### HIGH Tension: Strict validation vs sync-time failures
354
+
355
+ Use **`createMediaStore`** and **`store.add()`** so errors surface when you build the store, not only during sync. Validation catches missing `version`, duplicate keys, and invalid URLs at build time.
356
+
357
+ See also: getting-started/SKILL.md § Common Mistakes
358
+
359
+ ### HIGH Tension: Pre-signed URL TTL vs catalog size
360
+
361
+ Pre-signed URLs embedded in `resolveStore` need a TTL that covers the full download queue. For large catalogs (100+ assets), sign URLs with a generous TTL and set `expiresAt` on the store so the cache can fail fast with `STORE_EXPIRED` before a stale URL is fetched.
362
+
363
+ See also: authenticated-downloads/SKILL.md § Common Mistakes
364
+
365
+ ## Cross-References
366
+
367
+ See also: getting-started/SKILL.md — Full main → preload → renderer wiring
368
+ See also: react-rendering/SKILL.md — Index definitions determine how hooks query content
369
+ See also: authenticated-downloads/SKILL.md — Auth strategies for asset sources