@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,357 @@
1
+ ---
2
+ name: cache-configuration
3
+ description: >
4
+ createMediaCache options and targeted modifications: storagePath with
5
+ appPath and segments, devPassthrough mode, assetBaseUrl origin override,
6
+ onSyncFailure mode selection (serve-last-snapshot vs throw),
7
+ maxCacheBytes, reserveFreeBytes, staleDeleteAfterMs, syncHistoryLimit,
8
+ nested logging config with custom sinks (pino, logtape), log levels,
9
+ console formats, and MediaCacheLogEvent structure.
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/media-cache.ts"
17
+ - "rockhallweb/electron-offline-content:src/shared/types.ts"
18
+ - "rockhallweb/electron-offline-content:src/internal/validation.ts"
19
+ - "rockhallweb/electron-offline-content:src/main/storage-root-lock.ts"
20
+ ---
21
+
22
+ # Cache Configuration
23
+
24
+ This skill builds on getting-started. Read it first for full main → preload → renderer wiring.
25
+
26
+ ## Setup
27
+
28
+ ```typescript
29
+ import { app } from "electron";
30
+ import { createMediaCache, createMediaStore } from "@rockhall/electron-offline-content/main";
31
+ import pino from "pino";
32
+
33
+ const logger = pino({ name: "media-cache" });
34
+
35
+ if (!app.requestSingleInstanceLock()) {
36
+ app.exit(1);
37
+ }
38
+
39
+ const mediaCache = createMediaCache({
40
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
41
+ onSyncFailure: "serve-last-snapshot",
42
+ maxCacheBytes: 10 * 1024 * 1024 * 1024,
43
+ reserveFreeBytes: 1 * 1024 * 1024 * 1024,
44
+ logging: {
45
+ level: "info",
46
+ onLog: (entry) => {
47
+ logger[entry.level === "debug" ? "debug" : entry.level](entry, entry.event);
48
+ },
49
+ },
50
+ resolveStore: async () => {
51
+ const res = await fetch("https://cms.example.com/api/content");
52
+ const data = await res.json();
53
+ const store = createMediaStore();
54
+ for (const item of data.items) {
55
+ store.add(item.id, {
56
+ version: item.updatedAt,
57
+ mimeType: item.mimeType,
58
+ url: item.url,
59
+ metadata: item.metadata,
60
+ });
61
+ }
62
+ return store;
63
+ },
64
+ });
65
+ ```
66
+
67
+ ## Core Patterns
68
+
69
+ ### Storage path configuration
70
+
71
+ `storagePath` maps to Electron's `app.getPath()` names. The `appPath` field accepts any `MediaCacheAppPath` value (`"userData"`, `"temp"`, `"documents"`, etc.). Use the `segments` array to add subdirectories — the package joins them with the platform path separator.
72
+
73
+ ```typescript
74
+ const mediaCache = createMediaCache({
75
+ storagePath: {
76
+ appPath: "userData",
77
+ segments: ["my-app", "offline-media"],
78
+ },
79
+ resolveStore: async () => store,
80
+ });
81
+ ```
82
+
83
+ This resolves to `<userData>/my-app/offline-media/` on disk. Each segment becomes a directory level — never include path separators inside a segment string.
84
+
85
+ ### Dev passthrough mode
86
+
87
+ `devPassthrough` skips downloads entirely and serves remote URLs directly. It defaults to `true` when `NODE_ENV === "development"` and `false` otherwise. Set it explicitly for clarity:
88
+
89
+ ```typescript
90
+ const mediaCache = createMediaCache({
91
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
92
+ devPassthrough: true,
93
+ resolveStore: async () => store,
94
+ });
95
+ ```
96
+
97
+ When `devPassthrough` is `true`:
98
+
99
+ - Downloads are skipped — assets load from their original remote URLs.
100
+ - `onSyncFailure` is overridden to `"throw"` — there is no snapshot to serve.
101
+ - Hook URLs return remote `http://` or `https://` URLs instead of `media://` URLs.
102
+ - `assetBaseUrl` becomes available for origin overrides.
103
+
104
+ ### Storage limits
105
+
106
+ Three options control disk usage:
107
+
108
+ ```typescript
109
+ const mediaCache = createMediaCache({
110
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
111
+ maxCacheBytes: 10 * 1024 * 1024 * 1024,
112
+ reserveFreeBytes: 1 * 1024 * 1024 * 1024,
113
+ staleDeleteAfterMs: 7 * 24 * 60 * 60 * 1000,
114
+ resolveStore: async () => store,
115
+ });
116
+ ```
117
+
118
+ - `maxCacheBytes` — soft cap on total cache size in bytes. The sync pipeline skips new downloads when the cache exceeds this limit.
119
+ - `reserveFreeBytes` — minimum free disk space to preserve on the cache volume (default **1 GiB** when omitted; **`0`** disables). Still recommended to set an explicit value on kiosk hardware to match SSD capacity and OS needs.
120
+ - `staleDeleteAfterMs` — how long removed assets (no longer in the store) stay on disk before deletion. Defaults to 7 days (604,800,000 ms) when unset.
121
+
122
+ ### Structured logging
123
+
124
+ Use the nested `logging` object for all log configuration. `logging.onLog` receives `MediaCacheLogEvent` objects with structured fields (`timestamp`, `level`, `event`, `service`, `component`, plus context-specific keys). Pipe them to any structured logger:
125
+
126
+ **pino:**
127
+
128
+ ```typescript
129
+ import pino from "pino";
130
+
131
+ const logger = pino({ name: "media-cache" });
132
+
133
+ const mediaCache = createMediaCache({
134
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
135
+ logging: {
136
+ level: "info",
137
+ onLog: (entry) => {
138
+ logger[entry.level === "debug" ? "debug" : entry.level](entry, entry.event);
139
+ },
140
+ },
141
+ resolveStore: async () => store,
142
+ });
143
+ ```
144
+
145
+ **logtape:**
146
+
147
+ ```typescript
148
+ import { getLogger } from "@logtape/logtape";
149
+
150
+ const logger = getLogger(["media-cache"]);
151
+
152
+ const mediaCache = createMediaCache({
153
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
154
+ logging: {
155
+ level: "info",
156
+ onLog: (entry) => {
157
+ logger[entry.level](entry.event, entry);
158
+ },
159
+ },
160
+ resolveStore: async () => store,
161
+ });
162
+ ```
163
+
164
+ When `logging.onLog` is omitted and `NODE_ENV !== "production"`, the package prints to `console`. Default `logging.level` is `"debug"` for the built-in console sink and `"info"` when a custom `logging.onLog` is provided.
165
+
166
+ Use `logging.format` only with the built-in console sink:
167
+
168
+ ```typescript
169
+ const mediaCache = createMediaCache({
170
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
171
+ logging: {
172
+ level: "debug",
173
+ format: "json",
174
+ },
175
+ resolveStore: async () => store,
176
+ });
177
+ ```
178
+
179
+ Breaking migration:
180
+
181
+ - Old flat options `onLog`, `logLevel`, and `logFormat` were removed in `0.2.0`.
182
+ - Move them under `logging`.
183
+ - Do not combine `logging.format` with `logging.onLog`; custom sinks already receive structured events.
184
+
185
+ ## Common Mistakes
186
+
187
+ ### HIGH: Setting assetBaseUrl without devPassthrough
188
+
189
+ `assetBaseUrl` is only valid in dev passthrough mode. The constructor throws if `assetBaseUrl` is set while `devPassthrough` is `false` (or defaults to `false`).
190
+
191
+ Wrong:
192
+
193
+ ```typescript
194
+ const mediaCache = createMediaCache({
195
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
196
+ assetBaseUrl: "http://localhost:3000",
197
+ resolveStore: async () => store,
198
+ });
199
+ ```
200
+
201
+ Correct:
202
+
203
+ ```typescript
204
+ const mediaCache = createMediaCache({
205
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
206
+ devPassthrough: true,
207
+ assetBaseUrl: "http://localhost:3000",
208
+ resolveStore: async () => store,
209
+ });
210
+ ```
211
+
212
+ Source: media-cache.ts constructor
213
+
214
+ ### HIGH: Using arbitrary file paths for storagePath
215
+
216
+ `storagePath` requires an object with `appPath` (an Electron `app.getPath` name) and optional `segments`. Raw string paths are not accepted.
217
+
218
+ Wrong:
219
+
220
+ ```typescript
221
+ const mediaCache = createMediaCache({
222
+ storagePath: "/tmp/my-cache" as any,
223
+ resolveStore: async () => store,
224
+ });
225
+ ```
226
+
227
+ Correct:
228
+
229
+ ```typescript
230
+ const mediaCache = createMediaCache({
231
+ storagePath: { appPath: "temp", segments: ["my-app", "cache"] },
232
+ resolveStore: async () => store,
233
+ });
234
+ ```
235
+
236
+ Source: types.ts; validation.ts
237
+
238
+ ### HIGH: Two cache instances targeting same storage root
239
+
240
+ Wrong:
241
+
242
+ ```typescript
243
+ const cacheA = createMediaCache({
244
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
245
+ resolveStore: async () => storeA,
246
+ });
247
+ const cacheB = createMediaCache({
248
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
249
+ resolveStore: async () => storeB,
250
+ });
251
+ ```
252
+
253
+ Correct:
254
+
255
+ ```typescript
256
+ const cacheA = createMediaCache({
257
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
258
+ resolveStore: async () => storeA,
259
+ });
260
+ const cacheB = createMediaCache({
261
+ storagePath: { appPath: "userData", segments: ["offline-media-b"] },
262
+ resolveStore: async () => storeB,
263
+ });
264
+ ```
265
+
266
+ `MediaCache` acquires exclusive ownership of its storage directory via a lock file. A second instance targeting the same path throws `StorageOwnershipError`. Use `app.requestSingleInstanceLock()` to prevent duplicate processes.
267
+
268
+ Source: storage-root-lock.ts
269
+
270
+ ### MEDIUM: assetBaseUrl with path or query string
271
+
272
+ `assetBaseUrl` must be an origin only — protocol, hostname, and optional port. Including a path, query string, hash, or credentials causes the constructor to throw.
273
+
274
+ Wrong:
275
+
276
+ ```typescript
277
+ const mediaCache = createMediaCache({
278
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
279
+ devPassthrough: true,
280
+ assetBaseUrl: "http://localhost:3000/api/assets?v=2",
281
+ resolveStore: async () => store,
282
+ });
283
+ ```
284
+
285
+ Correct:
286
+
287
+ ```typescript
288
+ const mediaCache = createMediaCache({
289
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
290
+ devPassthrough: true,
291
+ assetBaseUrl: "http://localhost:3000",
292
+ resolveStore: async () => store,
293
+ });
294
+ ```
295
+
296
+ Source: media-cache.ts normalizeAssetBaseUrl
297
+
298
+ ### MEDIUM: Path separators in storagePath segments
299
+
300
+ Each entry in the `segments` array becomes a single directory name. Segments must not contain `"/"` or `"\\"` — use separate array entries instead.
301
+
302
+ Wrong:
303
+
304
+ ```typescript
305
+ const mediaCache = createMediaCache({
306
+ storagePath: { appPath: "userData", segments: ["my-app/offline-media"] },
307
+ resolveStore: async () => store,
308
+ });
309
+ ```
310
+
311
+ Correct:
312
+
313
+ ```typescript
314
+ const mediaCache = createMediaCache({
315
+ storagePath: { appPath: "userData", segments: ["my-app", "offline-media"] },
316
+ resolveStore: async () => store,
317
+ });
318
+ ```
319
+
320
+ Source: validation.ts
321
+
322
+ ### CRITICAL: devPassthrough left enabled in production (cross-skill: production-checklist)
323
+
324
+ In production Electron builds, `NODE_ENV` may be unset — which defaults `devPassthrough` to `false`. But if `NODE_ENV` is explicitly set to `"development"` in a deployed build, all downloads are silently skipped and the app serves only remote URLs. Set `devPassthrough: false` explicitly for production builds:
325
+
326
+ ```typescript
327
+ const mediaCache = createMediaCache({
328
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
329
+ devPassthrough: false,
330
+ resolveStore: async () => store,
331
+ });
332
+ ```
333
+
334
+ Source: media-cache.ts; types.ts
335
+ See also: production-checklist/SKILL.md § Common Mistakes
336
+
337
+ ### HIGH Tension: Dev passthrough simplicity vs production correctness
338
+
339
+ `devPassthrough` makes development fast but changes behavior: downloads are skipped, `onSyncFailure` is overridden to `"throw"`, URLs are remote instead of `media://`. Code working in dev passthrough may break in production offline mode.
340
+
341
+ See also: production-checklist/SKILL.md § Common Mistakes
342
+
343
+ ### HIGH Tension: Sync resilience vs stale content
344
+
345
+ `"serve-last-snapshot"` is safe for kiosks (never blank screen) but may serve outdated content indefinitely if syncs keep failing. `"throw"` is honest but can leave the UI empty.
346
+
347
+ See also: production-checklist/SKILL.md § Common Mistakes
348
+
349
+ ---
350
+
351
+ See also: getting-started/SKILL.md — Initial createMediaCache setup
352
+ See also: production-checklist/SKILL.md — Go-live configuration audit
353
+ See also: authenticated-downloads/SKILL.md — Auth embedded in resolveStore
354
+
355
+ ## References
356
+
357
+ - [Complete MediaCacheOptions reference](references/options.md)
@@ -0,0 +1,356 @@
1
+ # MediaCacheOptions Reference
2
+
3
+ Complete field-by-field reference for `createMediaCache(options)`.
4
+
5
+ ```typescript
6
+ import { createMediaCache } from "@rockhall/electron-offline-content/main";
7
+ ```
8
+
9
+ ---
10
+
11
+ ## storagePath
12
+
13
+ | | |
14
+ | ------------ | ----------------------- |
15
+ | **Type** | `MediaCacheStoragePath` |
16
+ | **Required** | Yes |
17
+ | **Default** | — |
18
+
19
+ Root directory for cached assets, metadata database, and lock file. Built from an Electron `app.getPath` name plus optional subdirectory segments.
20
+
21
+ **Constraints:** `appPath` must be a valid `MediaCacheAppPath` value. `segments`, if provided, must not contain `"/"` or `"\\"` characters.
22
+
23
+ ```typescript
24
+ storagePath: { appPath: "userData", segments: ["my-app", "offline-media"] }
25
+ ```
26
+
27
+ ### MediaCacheStoragePath
28
+
29
+ ```typescript
30
+ interface MediaCacheStoragePath {
31
+ appPath: MediaCacheAppPath;
32
+ segments?: string[];
33
+ }
34
+ ```
35
+
36
+ ### MediaCacheAppPath
37
+
38
+ Union of Electron `app.getPath` names:
39
+
40
+ | Value | Electron path |
41
+ | --------------- | ------------------------------------------------------------------------- |
42
+ | `"home"` | User home directory |
43
+ | `"appData"` | Per-user application data (`%APPDATA%` / `~/Library/Application Support`) |
44
+ | `"userData"` | `appData` + app name |
45
+ | `"sessionData"` | Session-specific data |
46
+ | `"temp"` | Temporary directory |
47
+ | `"exe"` | Executable path |
48
+ | `"module"` | `libchromiumcontent` library |
49
+ | `"desktop"` | Desktop directory |
50
+ | `"documents"` | Documents directory |
51
+ | `"downloads"` | Downloads directory |
52
+ | `"music"` | Music directory |
53
+ | `"pictures"` | Pictures directory |
54
+ | `"videos"` | Videos directory |
55
+ | `"recent"` | Recent files directory |
56
+ | `"logs"` | Log files directory |
57
+ | `"crashDumps"` | Crash dumps directory |
58
+
59
+ ---
60
+
61
+ ## devPassthrough
62
+
63
+ | | |
64
+ | ------------ | ----------------------------------------------------------- |
65
+ | **Type** | `boolean` |
66
+ | **Required** | No |
67
+ | **Default** | `true` when `NODE_ENV === "development"`, `false` otherwise |
68
+
69
+ Skips all downloads and serves remote URLs directly. Intended for development only.
70
+
71
+ **Constraints:** When `true`, downloads are skipped, `onSyncFailure` is overridden to `"throw"`, and hook URLs return remote `http://` or `https://` URLs instead of `media://`.
72
+
73
+ ```typescript
74
+ devPassthrough: true;
75
+ ```
76
+
77
+ ---
78
+
79
+ ## assetBaseUrl
80
+
81
+ | | |
82
+ | ------------ | ---------------- |
83
+ | **Type** | `string \| null` |
84
+ | **Required** | No |
85
+ | **Default** | `null` |
86
+
87
+ Origin override for asset URLs in dev passthrough mode. Replaces the origin of each asset’s `url` (the top-level string on every store entry).
88
+
89
+ **Constraints:** Must be an origin only (protocol + hostname + optional port). Must not include path, query, hash, or credentials. Requires `devPassthrough: true` — constructor throws if set while `devPassthrough` is `false`.
90
+
91
+ ```typescript
92
+ assetBaseUrl: "http://localhost:3000";
93
+ ```
94
+
95
+ ---
96
+
97
+ ## maxCacheBytes
98
+
99
+ | | |
100
+ | ------------ | --------- |
101
+ | **Type** | `number` |
102
+ | **Required** | No |
103
+ | **Default** | Unlimited |
104
+
105
+ Soft cap on total cache size in bytes. The sync pipeline skips new downloads when the cache exceeds this limit.
106
+
107
+ **Constraints:** Must be a positive integer when provided.
108
+
109
+ ```typescript
110
+ maxCacheBytes: 10 * 1024 * 1024 * 1024; // 10 GB
111
+ ```
112
+
113
+ ---
114
+
115
+ ## reserveFreeBytes
116
+
117
+ | | |
118
+ | ------------ | ----------------------------------- |
119
+ | **Type** | `number` |
120
+ | **Required** | No |
121
+ | **Default** | `1073741824` (1 GiB, `1024³` bytes) |
122
+
123
+ Minimum free disk space to preserve in bytes on the volume that holds the cache. Sync refuses work when projected free space would drop below this value. **`0`** disables the reservation (legacy behavior when the option was omitted).
124
+
125
+ **Constraints:** Must be a non-negative integer when provided. Omit the option to use the default; set **`0`** explicitly to allow filling the volume up to other limits.
126
+
127
+ ```typescript
128
+ reserveFreeBytes: 1 * 1024 * 1024 * 1024; // 1 GB
129
+ ```
130
+
131
+ ---
132
+
133
+ ## staleDeleteAfterMs
134
+
135
+ | | |
136
+ | ------------ | -------------------- |
137
+ | **Type** | `number` |
138
+ | **Required** | No |
139
+ | **Default** | `604800000` (7 days) |
140
+
141
+ Milliseconds to retain assets that are no longer present in the store before deleting them from disk.
142
+
143
+ **Constraints:** Must be a non-negative integer when provided.
144
+
145
+ ```typescript
146
+ staleDeleteAfterMs: 7 * 24 * 60 * 60 * 1000; // 7 days
147
+ ```
148
+
149
+ ---
150
+
151
+ ## onSyncFailure
152
+
153
+ | | |
154
+ | ------------ | ---------------------------------- |
155
+ | **Type** | `"serve-last-snapshot" \| "throw"` |
156
+ | **Required** | No |
157
+ | **Default** | `"serve-last-snapshot"` |
158
+
159
+ Behavior when a sync fails.
160
+
161
+ - `"serve-last-snapshot"` — continue serving the most recently committed store generation. Safe for kiosks (prevents blank screens) but may serve stale content.
162
+ - `"throw"` — propagate the sync error. Honest, but can leave the UI empty if no prior generation exists.
163
+
164
+ **Constraints:** Overridden to `"throw"` when `devPassthrough` is `true`.
165
+
166
+ ```typescript
167
+ onSyncFailure: "serve-last-snapshot";
168
+ ```
169
+
170
+ ---
171
+
172
+ ## syncHistoryLimit
173
+
174
+ | | |
175
+ | ------------ | ---------------------------- |
176
+ | **Type** | `number` |
177
+ | **Required** | No |
178
+ | **Default** | Package default (see source) |
179
+
180
+ Maximum number of sync generation records retained in the metadata database. Older generations are pruned after successful commits.
181
+
182
+ **Constraints:** Must be a positive integer when provided.
183
+
184
+ ```typescript
185
+ syncHistoryLimit: 5;
186
+ ```
187
+
188
+ ---
189
+
190
+ ## logging
191
+
192
+ | | |
193
+ | ------------ | ---------------------------------- |
194
+ | **Type** | `MediaCacheLoggingOptions` |
195
+ | **Required** | No |
196
+ | **Default** | Built-in console sink when omitted |
197
+
198
+ Nested logging configuration for either the built-in console sink or a custom structured logger.
199
+
200
+ **Constraints:** `format` cannot be combined with `onLog` in the same object. Use `format` only for the built-in console sink.
201
+
202
+ ```typescript
203
+ logging: {
204
+ level: "info",
205
+ onLog: (entry) => {
206
+ logger.info(entry, entry.event);
207
+ },
208
+ };
209
+ ```
210
+
211
+ ---
212
+
213
+ ### MediaCacheLoggingOptions
214
+
215
+ ```typescript
216
+ type MediaCacheLoggingOptions =
217
+ | MediaCacheCustomLoggingOptions
218
+ | {
219
+ level?: "debug" | "info" | "warn" | "error";
220
+ format?: never;
221
+ onLog: (entry: MediaCacheLogEvent) => void;
222
+ }
223
+ | {
224
+ level?: "debug" | "info" | "warn" | "error";
225
+ format?: "english" | "json";
226
+ onLog?: undefined;
227
+ };
228
+ ```
229
+
230
+ `MediaCacheLoggingOptions` is a discriminated union. The custom-sink branch is represented by `MediaCacheCustomLoggingOptions` in `types.ts`, and TypeScript rejects `format` when `onLog` is present because that branch uses `format?: never`.
231
+
232
+ ### `logging.level`
233
+
234
+ | | |
235
+ | ------------ | ---------------------------------------------------- |
236
+ | **Type** | `"debug" \| "info" \| "warn" \| "error"` |
237
+ | **Required** | No |
238
+ | **Default** | `"debug"` (console sink) / `"info"` (custom `onLog`) |
239
+
240
+ Minimum log level emitted. Events below this level are discarded.
241
+
242
+ **Constraints:** None.
243
+
244
+ ```typescript
245
+ logging: {
246
+ level: "info";
247
+ }
248
+ ```
249
+
250
+ ### `logging.format`
251
+
252
+ | | |
253
+ | ------------ | --------------------- |
254
+ | **Type** | `"english" \| "json"` |
255
+ | **Required** | No |
256
+ | **Default** | `"english"` |
257
+
258
+ Format for the built-in console sink. `"english"` produces human-readable lines. `"json"` produces one JSON object per line.
259
+
260
+ **Constraints:** Invalid when `logging.onLog` is provided.
261
+
262
+ ```typescript
263
+ logging: {
264
+ format: "json";
265
+ }
266
+ ```
267
+
268
+ ### `logging.onLog`
269
+
270
+ | | |
271
+ | ------------ | ----------------------------------------------------------------- |
272
+ | **Type** | `(entry: MediaCacheLogEvent) => void` |
273
+ | **Required** | No |
274
+ | **Default** | Built-in console sink (disabled when `NODE_ENV === "production"`) |
275
+
276
+ Custom log handler. Receives structured `MediaCacheLogEvent` objects for every log emission. When provided, the built-in console sink is disabled.
277
+
278
+ **Constraints:** The handler is called synchronously on the main thread. Cannot be combined with `logging.format`.
279
+
280
+ ```typescript
281
+ logging: {
282
+ onLog: (entry) => {
283
+ logger.info(entry, entry.event);
284
+ },
285
+ };
286
+ ```
287
+
288
+ ### MediaCacheLogEvent
289
+
290
+ ```typescript
291
+ interface MediaCacheLogEvent {
292
+ [key: string]: JsonValue | undefined;
293
+ timestamp: string; // ISO 8601
294
+ level: MediaCacheLogLevel; // "debug" | "info" | "warn" | "error"
295
+ event: string; // e.g. "sync.asset.downloaded", "protocol.request.served"
296
+ service: string; // package identifier
297
+ component: string; // e.g. "sync", "protocol", "database"
298
+ }
299
+ ```
300
+
301
+ Additional context-specific keys vary by event (e.g. `assetKey`, `bytesDownloaded`, `durationMs`). All values are JSON-serializable.
302
+
303
+ ### Migration
304
+
305
+ Flat logging options were removed in `0.2.0`.
306
+
307
+ ```typescript
308
+ // Before
309
+ createMediaCache({
310
+ logLevel: "info",
311
+ onLog: (entry) => logger.info(entry, entry.event),
312
+ resolveStore,
313
+ });
314
+
315
+ // After
316
+ createMediaCache({
317
+ logging: {
318
+ level: "info",
319
+ onLog: (entry) => logger.info(entry, entry.event),
320
+ },
321
+ resolveStore,
322
+ });
323
+ ```
324
+
325
+ ---
326
+
327
+ ## resolveStore
328
+
329
+ | | |
330
+ | ------------ | ----------------------------------------- |
331
+ | **Type** | `() => Promise<MediaStore> \| MediaStore` |
332
+ | **Required** | Yes |
333
+ | **Default** | — |
334
+
335
+ Function called at the start of each sync cycle to produce the current asset store. May be async. The return value is normalized and validated before the download pipeline begins.
336
+
337
+ **Constraints:** Must return a valid `MediaStore` built with `createMediaStore()`. See store-authoring/SKILL.md for structure and validation rules.
338
+
339
+ ```typescript
340
+ import { createMediaStore } from "@rockhall/electron-offline-content/main";
341
+
342
+ resolveStore: async () => {
343
+ const res = await fetch("https://cms.example.com/api/content");
344
+ const data = await res.json();
345
+ const store = createMediaStore();
346
+ for (const item of data.items) {
347
+ store.add(item.id, {
348
+ version: item.updatedAt,
349
+ mimeType: item.mimeType,
350
+ url: item.url,
351
+ metadata: item.metadata,
352
+ });
353
+ }
354
+ return store;
355
+ };
356
+ ```