@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,234 @@
1
+ import { MediaStore } from "../main/store.js";
2
+
3
+ //#region src/shared/types.d.ts
4
+ /** JSON-serializable values used in structured logs, metadata, and manifest extras. */
5
+ type JsonValue = string | number | boolean | null | JsonValue[] | {
6
+ [key: string]: JsonValue;
7
+ };
8
+ /** Minimum severity emitted when logging is configured; entries below this level are dropped. */
9
+ type MediaCacheLogLevel = "debug" | "info" | "warn" | "error";
10
+ /**
11
+ * How the built-in main-process console sink formats each line when `logging.onLog` is omitted.
12
+ * Callback loggers always receive structured {@link MediaCacheLogEvent} objects regardless of this setting.
13
+ */
14
+ type MediaCacheLogFormat = "english" | "json";
15
+ /** One structured log line from the cache; includes standard fields plus optional diagnostic keys. */
16
+ interface MediaCacheLogEvent {
17
+ [key: string]: JsonValue | undefined;
18
+ timestamp: string;
19
+ level: MediaCacheLogLevel;
20
+ event: string;
21
+ service: string;
22
+ component: string;
23
+ }
24
+ /** Receives log entries from the main-process cache when `logging.onLog` is set on {@link MediaCacheOptions}. */
25
+ type MediaCacheLogHandler = (entry: MediaCacheLogEvent) => void;
26
+ /** Logging options when using a custom structured sink. */
27
+ interface MediaCacheCustomLoggingOptions {
28
+ level?: MediaCacheLogLevel;
29
+ onLog: MediaCacheLogHandler;
30
+ format?: never;
31
+ }
32
+ /** Logging options when using the built-in development console sink. */
33
+ interface MediaCacheConsoleLoggingOptions {
34
+ level?: MediaCacheLogLevel;
35
+ format?: MediaCacheLogFormat;
36
+ onLog?: undefined;
37
+ }
38
+ /** Logging configuration for `MediaCacheOptions`; custom sinks and console formatting are mutually exclusive. */
39
+ type MediaCacheLoggingOptions = MediaCacheCustomLoggingOptions | MediaCacheConsoleLoggingOptions;
40
+ /** High-level media category derived from an asset's mimeType. */
41
+ type MediaKind = "video" | "image" | "audio" | "document" | "html" | "text" | "binary";
42
+ /** Accepted asset key input: a plain string or an array of string segments. */
43
+ type AssetKeyInput = string | readonly string[];
44
+ /** Tagged index entry produced by calling a {@link import("../main/store.js").MediaIndex} handle. */
45
+ declare class IndexTag {
46
+ readonly name: string;
47
+ readonly value: string | string[];
48
+ constructor(name: string, value: string | string[]);
49
+ }
50
+ /** Input for adding an asset to a {@link import("../main/store.js").MediaStore}. */
51
+ interface MediaAssetInput {
52
+ version: string;
53
+ mimeType: string;
54
+ url: string;
55
+ fileName?: string;
56
+ /**
57
+ * Optional declared size in bytes. When set, must be a **non-negative finite** number
58
+ * (`Number.isFinite` and `>= 0`). **Fractional values are allowed** (e.g. estimates);
59
+ * the store does not require integers.
60
+ */
61
+ byteLength?: number;
62
+ metadata?: Record<string, JsonValue>;
63
+ indexes?: IndexTag[];
64
+ }
65
+ /** Describes one user-defined or built-in index in the serialized store output. */
66
+ interface IndexDefinition {
67
+ name: string;
68
+ cardinality: "single" | "multi";
69
+ required: boolean;
70
+ builtin: boolean;
71
+ }
72
+ /** One asset in the serialized flat manifest (output of `MediaStore._serialize()`). */
73
+ interface FlatManifestAsset {
74
+ key: string;
75
+ displayKey: string;
76
+ version: string;
77
+ mimeType: string;
78
+ mediaKind: MediaKind;
79
+ url: string;
80
+ fileName: string;
81
+ fileStem: string;
82
+ byteLength?: number;
83
+ metadata: Record<string, JsonValue>;
84
+ indexes: Record<string, string | string[]>;
85
+ }
86
+ /** Serialized flat manifest produced by `MediaStore._serialize()` and consumed by the sync engine. */
87
+ interface FlatManifest {
88
+ snapshotId?: string;
89
+ retrievedAt?: string;
90
+ expiresAt?: string;
91
+ indexDefinitions: IndexDefinition[];
92
+ assets: FlatManifestAsset[];
93
+ }
94
+ /**
95
+ * After a failed sync: keep serving the last committed generation (`serve-last-snapshot`), or
96
+ * propagate the failure (`throw`). Ignored when `devPassthrough` is true (failures always throw).
97
+ */
98
+ type SyncFailureMode = "serve-last-snapshot" | "throw";
99
+ /**
100
+ * Allowed `electron.app.getPath(...)` keys for package-managed storage root resolution.
101
+ */
102
+ type MediaCacheAppPath = "home" | "appData" | "userData" | "sessionData" | "temp" | "exe" | "module" | "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos" | "recent" | "logs" | "crashDumps";
103
+ /** Storage root composed from `electron.app.getPath(appPath)` plus optional subpath segments. */
104
+ interface MediaCacheStoragePath {
105
+ appPath: MediaCacheAppPath;
106
+ segments?: string[];
107
+ }
108
+ /**
109
+ * Main-process configuration: where state lives, sync and storage guardrails, logging, and how the
110
+ * store is resolved.
111
+ *
112
+ * Default behavior is offline mode unless `process.env.NODE_ENV` is `"development"`: assets sync to
113
+ * disk and resolved URLs use the privileged `media:` protocol.
114
+ */
115
+ interface MediaCacheOptions {
116
+ storagePath: MediaCacheStoragePath;
117
+ devPassthrough?: boolean;
118
+ assetBaseUrl?: string | null;
119
+ maxCacheBytes?: number;
120
+ reserveFreeBytes?: number;
121
+ staleDeleteAfterMs?: number;
122
+ onSyncFailure?: SyncFailureMode;
123
+ syncHistoryLimit?: number;
124
+ logging?: MediaCacheLoggingOptions;
125
+ resolveStore: () => Promise<MediaStore> | MediaStore;
126
+ }
127
+ /** Cursor-based page for list APIs. */
128
+ interface PaginationInput {
129
+ limit?: number;
130
+ cursor?: string;
131
+ }
132
+ /** One page of results; `nextCursor` is null when there are no more items. */
133
+ interface PaginationResult<T> {
134
+ items: T[];
135
+ nextCursor: string | null;
136
+ }
137
+ /** Snapshot of sync and readiness state. */
138
+ interface MediaCacheStatus {
139
+ phase: "idle" | "syncing" | "ready" | "error";
140
+ storageRoot: string | null;
141
+ activeGenerationId: number | null;
142
+ progress: SyncProgress | null;
143
+ lastRun: SyncRunSummary | null;
144
+ error: SerializedMediaCacheError | null;
145
+ updatedAt: number;
146
+ }
147
+ type MediaCachePhase = MediaCacheStatus["phase"] | "loading";
148
+ /** Fine-grained sync pipeline step and counters while a run is active. */
149
+ interface SyncProgress {
150
+ runId: number;
151
+ phase: "resolving-store" | "staging-generation" | "diffing" | "downloading" | "committing" | "pruning";
152
+ totalAssets: number;
153
+ completedAssets: number;
154
+ downloadedAssets: number;
155
+ skippedAssets: number;
156
+ bytesDownloaded: number;
157
+ }
158
+ /** Counters persisted with a {@link SyncRunSummary} after (or during) a sync run. */
159
+ interface SyncRunStats {
160
+ totalAssets: number;
161
+ downloadedAssets: number;
162
+ skippedAssets: number;
163
+ bytesDownloaded: number;
164
+ }
165
+ /** Record of one sync run persisted for history. */
166
+ interface SyncRunSummary {
167
+ id: number;
168
+ status: "running" | "success" | "error";
169
+ startedAt: number;
170
+ finishedAt: number | null;
171
+ errorCode: string | null;
172
+ errorMessage: string | null;
173
+ stats: SyncRunStats;
174
+ }
175
+ /** One asset after resolution: flat key-value with `media:` URL or remote URL in passthrough mode. */
176
+ interface ResolvedMediaAsset {
177
+ key: string;
178
+ displayKey: string;
179
+ version: string;
180
+ mimeType: string;
181
+ kind: MediaKind;
182
+ byteLength?: number;
183
+ url: string;
184
+ metadata: Record<string, JsonValue>;
185
+ indexes: Record<string, string | string[]>;
186
+ }
187
+ /** Assets whose manifest file name stem matched a search. */
188
+ interface FileStemMatch {
189
+ asset: ResolvedMediaAsset;
190
+ }
191
+ /**
192
+ * Renderer-safe API to the cache: read/query operations plus status subscription.
193
+ */
194
+ interface MediaCacheBridge {
195
+ getStatus(): Promise<MediaCacheStatus>;
196
+ syncNow(): Promise<void>;
197
+ getAsset(key: AssetKeyInput): Promise<ResolvedMediaAsset | null>;
198
+ listByIndex(indexName: string, value: string, pagination?: PaginationInput): Promise<PaginationResult<ResolvedMediaAsset>>;
199
+ findByFileStem(stem: string, pagination?: PaginationInput): Promise<PaginationResult<FileStemMatch>>;
200
+ subscribeStatus(listener: (status: MediaCacheStatus) => void): () => void;
201
+ }
202
+ /** Stable error shape stored on {@link MediaCacheStatus} when a sync fails. */
203
+ interface SerializedMediaCacheError {
204
+ name: string;
205
+ code: string;
206
+ message: string;
207
+ }
208
+ /** Options for {@link import("../preload/index.js").exposeMediaCacheBridge}. */
209
+ interface PreloadExposeOptions {
210
+ key?: string;
211
+ }
212
+ /** Optional sync-complete refetch behavior for React query hooks. */
213
+ interface MediaQuerySyncOptions {
214
+ refetchOnSyncComplete?: boolean;
215
+ }
216
+ /** Derived readiness snapshot for `useMediaCacheReady()`. */
217
+ interface MediaCacheReadyState {
218
+ ready: boolean;
219
+ syncing: boolean;
220
+ phase: MediaCacheStatus["phase"];
221
+ activeGenerationId: number | null;
222
+ syncError: SerializedMediaCacheError | null;
223
+ }
224
+ /** Aggregated error view for the provider-driven `useMediaCacheErrors()`. */
225
+ interface MediaCacheErrors {
226
+ syncError: SerializedMediaCacheError | null;
227
+ statusError: Error | null;
228
+ queryErrors: Error[];
229
+ hasError: boolean;
230
+ primaryError: Error | null;
231
+ }
232
+ //#endregion
233
+ export { AssetKeyInput, FileStemMatch, FlatManifest, FlatManifestAsset, IndexDefinition, IndexTag, JsonValue, MediaAssetInput, MediaCacheAppPath, MediaCacheBridge, MediaCacheConsoleLoggingOptions, MediaCacheCustomLoggingOptions, MediaCacheErrors, MediaCacheLogEvent, MediaCacheLogFormat, MediaCacheLogHandler, MediaCacheLogLevel, MediaCacheLoggingOptions, MediaCacheOptions, MediaCachePhase, MediaCacheReadyState, MediaCacheStatus, MediaCacheStoragePath, MediaKind, MediaQuerySyncOptions, PaginationInput, PaginationResult, PreloadExposeOptions, ResolvedMediaAsset, SerializedMediaCacheError, SyncFailureMode, SyncProgress, SyncRunStats, SyncRunSummary };
234
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../src/shared/types.ts"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ //#region src/shared/types.ts
2
+ /** Tagged index entry produced by calling a {@link import("../main/store.js").MediaIndex} handle. */
3
+ var IndexTag = class {
4
+ name;
5
+ value;
6
+ constructor(name, value) {
7
+ this.name = name;
8
+ this.value = value;
9
+ }
10
+ };
11
+ //#endregion
12
+ export { IndexTag };
13
+
14
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../src/shared/types.ts"],"sourcesContent":["/** JSON-serializable values used in structured logs, metadata, and manifest extras. */\nexport type JsonValue =\n | string\n | number\n | boolean\n | null\n | JsonValue[]\n | { [key: string]: JsonValue };\n\n/** Minimum severity emitted when logging is configured; entries below this level are dropped. */\nexport type MediaCacheLogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\n/**\n * How the built-in main-process console sink formats each line when `logging.onLog` is omitted.\n * Callback loggers always receive structured {@link MediaCacheLogEvent} objects regardless of this setting.\n */\nexport type MediaCacheLogFormat = \"english\" | \"json\";\n\n/** One structured log line from the cache; includes standard fields plus optional diagnostic keys. */\nexport interface MediaCacheLogEvent {\n [key: string]: JsonValue | undefined;\n timestamp: string;\n level: MediaCacheLogLevel;\n event: string;\n service: string;\n component: string;\n}\n\n/** Receives log entries from the main-process cache when `logging.onLog` is set on {@link MediaCacheOptions}. */\nexport type MediaCacheLogHandler = (entry: MediaCacheLogEvent) => void;\n\n/** Logging options when using a custom structured sink. */\nexport interface MediaCacheCustomLoggingOptions {\n level?: MediaCacheLogLevel;\n onLog: MediaCacheLogHandler;\n format?: never;\n}\n\n/** Logging options when using the built-in development console sink. */\nexport interface MediaCacheConsoleLoggingOptions {\n level?: MediaCacheLogLevel;\n format?: MediaCacheLogFormat;\n onLog?: undefined;\n}\n\n/** Logging configuration for `MediaCacheOptions`; custom sinks and console formatting are mutually exclusive. */\nexport type MediaCacheLoggingOptions =\n | MediaCacheCustomLoggingOptions\n | MediaCacheConsoleLoggingOptions;\n\n/** High-level media category derived from an asset's mimeType. */\nexport type MediaKind = \"video\" | \"image\" | \"audio\" | \"document\" | \"html\" | \"text\" | \"binary\";\n\n/** Accepted asset key input: a plain string or an array of string segments. */\nexport type AssetKeyInput = string | readonly string[];\n\n/** Tagged index entry produced by calling a {@link import(\"../main/store.js\").MediaIndex} handle. */\nexport class IndexTag {\n constructor(\n readonly name: string,\n readonly value: string | string[],\n ) {}\n}\n\n/** Input for adding an asset to a {@link import(\"../main/store.js\").MediaStore}. */\nexport interface MediaAssetInput {\n version: string;\n mimeType: string;\n url: string;\n fileName?: string;\n /**\n * Optional declared size in bytes. When set, must be a **non-negative finite** number\n * (`Number.isFinite` and `>= 0`). **Fractional values are allowed** (e.g. estimates);\n * the store does not require integers.\n */\n byteLength?: number;\n metadata?: Record<string, JsonValue>;\n indexes?: IndexTag[];\n}\n\n/** Describes one user-defined or built-in index in the serialized store output. */\nexport interface IndexDefinition {\n name: string;\n cardinality: \"single\" | \"multi\";\n required: boolean;\n builtin: boolean;\n}\n\n/** One asset in the serialized flat manifest (output of `MediaStore._serialize()`). */\nexport interface FlatManifestAsset {\n key: string;\n displayKey: string;\n version: string;\n mimeType: string;\n mediaKind: MediaKind;\n url: string;\n fileName: string;\n fileStem: string;\n byteLength?: number;\n metadata: Record<string, JsonValue>;\n indexes: Record<string, string | string[]>;\n}\n\n/** Serialized flat manifest produced by `MediaStore._serialize()` and consumed by the sync engine. */\nexport interface FlatManifest {\n snapshotId?: string;\n retrievedAt?: string;\n expiresAt?: string;\n indexDefinitions: IndexDefinition[];\n assets: FlatManifestAsset[];\n}\n\n/**\n * After a failed sync: keep serving the last committed generation (`serve-last-snapshot`), or\n * propagate the failure (`throw`). Ignored when `devPassthrough` is true (failures always throw).\n */\nexport type SyncFailureMode = \"serve-last-snapshot\" | \"throw\";\n\n/**\n * Allowed `electron.app.getPath(...)` keys for package-managed storage root resolution.\n */\nexport type MediaCacheAppPath =\n | \"home\"\n | \"appData\"\n | \"userData\"\n | \"sessionData\"\n | \"temp\"\n | \"exe\"\n | \"module\"\n | \"desktop\"\n | \"documents\"\n | \"downloads\"\n | \"music\"\n | \"pictures\"\n | \"videos\"\n | \"recent\"\n | \"logs\"\n | \"crashDumps\";\n\n/** Storage root composed from `electron.app.getPath(appPath)` plus optional subpath segments. */\nexport interface MediaCacheStoragePath {\n appPath: MediaCacheAppPath;\n segments?: string[];\n}\n\n/**\n * Main-process configuration: where state lives, sync and storage guardrails, logging, and how the\n * store is resolved.\n *\n * Default behavior is offline mode unless `process.env.NODE_ENV` is `\"development\"`: assets sync to\n * disk and resolved URLs use the privileged `media:` protocol.\n */\nexport interface MediaCacheOptions {\n storagePath: MediaCacheStoragePath;\n devPassthrough?: boolean;\n assetBaseUrl?: string | null;\n maxCacheBytes?: number;\n reserveFreeBytes?: number;\n staleDeleteAfterMs?: number;\n onSyncFailure?: SyncFailureMode;\n syncHistoryLimit?: number;\n logging?: MediaCacheLoggingOptions;\n resolveStore: () =>\n | Promise<import(\"../main/store.js\").MediaStore>\n | import(\"../main/store.js\").MediaStore;\n}\n\n/** Cursor-based page for list APIs. */\nexport interface PaginationInput {\n limit?: number;\n cursor?: string;\n}\n\n/** One page of results; `nextCursor` is null when there are no more items. */\nexport interface PaginationResult<T> {\n items: T[];\n nextCursor: string | null;\n}\n\n/** Snapshot of sync and readiness state. */\nexport interface MediaCacheStatus {\n phase: \"idle\" | \"syncing\" | \"ready\" | \"error\";\n storageRoot: string | null;\n activeGenerationId: number | null;\n progress: SyncProgress | null;\n lastRun: SyncRunSummary | null;\n error: SerializedMediaCacheError | null;\n updatedAt: number;\n}\n\nexport type MediaCachePhase = MediaCacheStatus[\"phase\"] | \"loading\";\n\n/** Fine-grained sync pipeline step and counters while a run is active. */\nexport interface SyncProgress {\n runId: number;\n phase:\n | \"resolving-store\"\n | \"staging-generation\"\n | \"diffing\"\n | \"downloading\"\n | \"committing\"\n | \"pruning\";\n totalAssets: number;\n completedAssets: number;\n downloadedAssets: number;\n skippedAssets: number;\n bytesDownloaded: number;\n}\n\n/** Counters persisted with a {@link SyncRunSummary} after (or during) a sync run. */\nexport interface SyncRunStats {\n totalAssets: number;\n downloadedAssets: number;\n skippedAssets: number;\n bytesDownloaded: number;\n}\n\n/** Record of one sync run persisted for history. */\nexport interface SyncRunSummary {\n id: number;\n status: \"running\" | \"success\" | \"error\";\n startedAt: number;\n finishedAt: number | null;\n errorCode: string | null;\n errorMessage: string | null;\n stats: SyncRunStats;\n}\n\n/** One asset after resolution: flat key-value with `media:` URL or remote URL in passthrough mode. */\nexport interface ResolvedMediaAsset {\n key: string;\n displayKey: string;\n version: string;\n mimeType: string;\n kind: MediaKind;\n byteLength?: number;\n url: string;\n metadata: Record<string, JsonValue>;\n indexes: Record<string, string | string[]>;\n}\n\n/** Assets whose manifest file name stem matched a search. */\nexport interface FileStemMatch {\n asset: ResolvedMediaAsset;\n}\n\n/**\n * Renderer-safe API to the cache: read/query operations plus status subscription.\n */\nexport interface MediaCacheBridge {\n getStatus(): Promise<MediaCacheStatus>;\n syncNow(): Promise<void>;\n getAsset(key: AssetKeyInput): Promise<ResolvedMediaAsset | null>;\n listByIndex(\n indexName: string,\n value: string,\n pagination?: PaginationInput,\n ): Promise<PaginationResult<ResolvedMediaAsset>>;\n findByFileStem(\n stem: string,\n pagination?: PaginationInput,\n ): Promise<PaginationResult<FileStemMatch>>;\n subscribeStatus(listener: (status: MediaCacheStatus) => void): () => void;\n}\n\n/** Stable error shape stored on {@link MediaCacheStatus} when a sync fails. */\nexport interface SerializedMediaCacheError {\n name: string;\n code: string;\n message: string;\n}\n\n/** Options for {@link import(\"../preload/index.js\").exposeMediaCacheBridge}. */\nexport interface PreloadExposeOptions {\n key?: string;\n}\n\n/** Optional sync-complete refetch behavior for React query hooks. */\nexport interface MediaQuerySyncOptions {\n refetchOnSyncComplete?: boolean;\n}\n\n/** Derived readiness snapshot for `useMediaCacheReady()`. */\nexport interface MediaCacheReadyState {\n ready: boolean;\n syncing: boolean;\n phase: MediaCacheStatus[\"phase\"];\n activeGenerationId: number | null;\n syncError: SerializedMediaCacheError | null;\n}\n\n/** Aggregated error view for the provider-driven `useMediaCacheErrors()`. */\nexport interface MediaCacheErrors {\n syncError: SerializedMediaCacheError | null;\n statusError: Error | null;\n queryErrors: Error[];\n hasError: boolean;\n primaryError: Error | null;\n}\n"],"mappings":";;AAyDA,IAAa,WAAb,MAAsB;CAET;CACA;CAFX,YACE,MACA,OACA;EAFS,KAAA,OAAA;EACA,KAAA,QAAA"}
package/package.json ADDED
@@ -0,0 +1,120 @@
1
+ {
2
+ "name": "@rockhall/electron-offline-content",
3
+ "version": "0.4.0",
4
+ "description": "A package for Electron apps to download, stage, and serve offline content from a remote source. Supports video, images, audio, text content, and more.",
5
+ "keywords": [
6
+ "cache",
7
+ "electron",
8
+ "media",
9
+ "offline",
10
+ "react",
11
+ "sqlite"
12
+ ],
13
+ "homepage": "https://github.com/rockhallweb/electron-offline-content#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/rockhallweb/electron-offline-content/issues"
16
+ },
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/rockhallweb/electron-offline-content.git"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "skills",
25
+ "!skills/_artifacts",
26
+ "README.md",
27
+ "CHANGELOG.md"
28
+ ],
29
+ "type": "module",
30
+ "sideEffects": false,
31
+ "exports": {
32
+ "./main": {
33
+ "types": "./dist/main/index.d.ts",
34
+ "import": "./dist/main/index.js",
35
+ "require": "./dist/main/index.cjs"
36
+ },
37
+ "./preload": {
38
+ "types": "./dist/preload/index.d.ts",
39
+ "import": "./dist/preload/index.js",
40
+ "require": "./dist/preload/index.cjs"
41
+ },
42
+ "./react": {
43
+ "types": "./dist/react/index.d.ts",
44
+ "import": "./dist/react/index.js",
45
+ "require": "./dist/react/index.cjs"
46
+ },
47
+ "./renderer": {
48
+ "types": "./dist/renderer/index.d.ts",
49
+ "import": "./dist/renderer/index.js",
50
+ "require": "./dist/renderer/index.cjs"
51
+ },
52
+ "./package.json": "./package.json"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "scripts": {
58
+ "build": "tsdown",
59
+ "check": "tsc --noEmit",
60
+ "lint": "oxlint --config .oxlintrc.json --react-plugin --vitest-plugin --node-plugin --deny-warnings .",
61
+ "lint:fix": "oxlint --config .oxlintrc.json --react-plugin --vitest-plugin --node-plugin --fix --deny-warnings .",
62
+ "format": "oxfmt .",
63
+ "format:check": "oxfmt --check .",
64
+ "test": "vitest run --config vitest.node.config.ts && vitest run --config vitest.node.integration.config.ts && vitest run --config vitest.react.config.ts && vitest run --config vitest.renderer.config.ts",
65
+ "test:smoke": "vitest run --config vitest.node.smoke.config.ts",
66
+ "test:react": "vitest run --config vitest.react.config.ts",
67
+ "test:renderer": "vitest run --config vitest.renderer.config.ts",
68
+ "test:watch": "vitest",
69
+ "validate": "turbo run lint format:check check && turbo run test:smoke && turbo run test:react && turbo run test:renderer && turbo run build",
70
+ "worktree:new": "node scripts/worktree.mjs create",
71
+ "worktree:open": "node scripts/worktree.mjs open",
72
+ "worktree:list": "git worktree list",
73
+ "worktree:prune": "git worktree prune",
74
+ "examples:verify": "node scripts/run-examples.mjs validate",
75
+ "examples:validate": "pnpm build && node scripts/run-examples.mjs validate",
76
+ "pack:verify": "node scripts/pack-verify.mjs",
77
+ "ci:examples": "pnpm install --frozen-lockfile --dir examples/local && pnpm install --frozen-lockfile --dir examples/nasa && pnpm examples:verify",
78
+ "ci:validate": "pnpm validate"
79
+ },
80
+ "dependencies": {
81
+ "zod": "^4.4.3"
82
+ },
83
+ "devDependencies": {
84
+ "@tanstack/intent": "^0.0.41",
85
+ "@testing-library/react": "^16.3.0",
86
+ "@types/node": "^24.5.2",
87
+ "@types/react": "^19.1.13",
88
+ "@types/react-dom": "^19.1.9",
89
+ "@vitest/coverage-v8": "^4.1.6",
90
+ "electron": "^40.0.0",
91
+ "happy-dom": "^20.9.0",
92
+ "knip": "^6.12.2",
93
+ "oxfmt": "^0.49.0",
94
+ "oxlint": "^1.64.0",
95
+ "react": "^19.2.6",
96
+ "react-dom": "^19.2.6",
97
+ "tsdown": "^0.22.0",
98
+ "turbo": "^2.9.12",
99
+ "typescript": "^6.0.3",
100
+ "vitest": "^4.1.6"
101
+ },
102
+ "peerDependencies": {
103
+ "electron": ">=40.0.0",
104
+ "react": ">=18.0.0",
105
+ "react-dom": ">=18.0.0"
106
+ },
107
+ "peerDependenciesMeta": {
108
+ "react": {
109
+ "optional": true
110
+ },
111
+ "react-dom": {
112
+ "optional": true
113
+ }
114
+ },
115
+ "engines": {
116
+ "node": ">=24.0.0",
117
+ "pnpm": ">=11.0.0"
118
+ },
119
+ "packageManager": "pnpm@11.1.0"
120
+ }
@@ -0,0 +1,203 @@
1
+ ---
2
+ name: authenticated-downloads
3
+ description: >
4
+ Adding authentication to asset downloads in the flat store model by
5
+ embedding presigned (or otherwise signed) URLs in each asset’s top-level
6
+ url field during resolveStore. Pre-signed URL expiration vs catalog sync
7
+ duration tradeoff.
8
+ type: core
9
+ library: electron-offline-content
10
+ library_version: "0.4.0"
11
+ requires:
12
+ - store-authoring
13
+ sources:
14
+ - "rockhallweb/electron-offline-content:src/main/media-cache.ts"
15
+ - "rockhallweb/electron-offline-content:src/main/store.ts"
16
+ - "rockhallweb/electron-offline-content:src/shared/types.ts"
17
+ - "rockhallweb/electron-offline-content:README.md"
18
+ ---
19
+
20
+ > **Dependency:** This skill builds on store-authoring. Read it first for resolveStore and createMediaStore patterns.
21
+
22
+ ## Setup
23
+
24
+ A `resolveStore` function that embeds S3 pre-signed URLs at store build time:
25
+
26
+ ```typescript
27
+ import { createMediaCache, createMediaStore } from "@rockhall/electron-offline-content/main";
28
+ import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
29
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
30
+
31
+ const s3 = new S3Client({ region: "us-east-1" });
32
+
33
+ const mediaCache = createMediaCache({
34
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
35
+ resolveStore: async () => {
36
+ const store = createMediaStore();
37
+ const catalog = await fetchCatalog();
38
+
39
+ for (const item of catalog) {
40
+ const signedUrl = await getSignedUrl(
41
+ s3,
42
+ new GetObjectCommand({
43
+ Bucket: "my-content-bucket",
44
+ Key: item.objectKey,
45
+ }),
46
+ { expiresIn: 3600 },
47
+ );
48
+
49
+ store.add(["assets", item.id], {
50
+ version: item.revision,
51
+ mimeType: "video/mp4",
52
+ url: signedUrl,
53
+ metadata: item.metadata,
54
+ });
55
+ }
56
+
57
+ return store;
58
+ },
59
+ });
60
+ ```
61
+
62
+ ## Core Patterns
63
+
64
+ ### Embedding presigned URLs in resolveStore
65
+
66
+ All authentication is handled during `resolveStore()`. The download pipeline fetches each asset using only its `url` string. Put signing parameters, tokens, or credentials into that URL (for example an S3 presigned URL) before calling `store.add`.
67
+
68
+ ```typescript
69
+ import { createMediaStore } from "@rockhall/electron-offline-content/main";
70
+ import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
71
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
72
+
73
+ const s3 = new S3Client({ region: "us-east-1" });
74
+
75
+ async function resolveStore() {
76
+ const store = createMediaStore();
77
+ const catalog = await fetchCatalog();
78
+
79
+ for (const item of catalog) {
80
+ const signedUrl = await getSignedUrl(
81
+ s3,
82
+ new GetObjectCommand({ Bucket: "my-content-bucket", Key: item.objectKey }),
83
+ { expiresIn: 3600 },
84
+ );
85
+
86
+ store.add(["assets", item.id], {
87
+ version: item.revision,
88
+ mimeType: "video/mp4",
89
+ url: signedUrl,
90
+ });
91
+ }
92
+
93
+ return store;
94
+ }
95
+ ```
96
+
97
+ ### OAuth or rotating credentials
98
+
99
+ If you use short-lived tokens, exchange them for presigned download URLs (or a backend-issued URL that already includes auth) inside `resolveStore` before calling `store.add`. The library does not accept separate auth configuration per asset—only the final URL string.
100
+
101
+ ### TTL and catalog size
102
+
103
+ | Scenario | Recommendation |
104
+ | ------------------- | -------------------------------------------------------------------------------- |
105
+ | Any protected asset | Presign (or otherwise embed auth in) the `url` before `store.add`. |
106
+ | Small catalog | Presign in `resolveStore` with a TTL that comfortably exceeds full sync time. |
107
+ | Large catalog | Use a long TTL and set store `expiresAt` to match for fail-fast `STORE_EXPIRED`. |
108
+
109
+ ## Common Mistakes
110
+
111
+ ### HIGH: Pre-signed URL expiration too short for full catalog sync
112
+
113
+ When embedding pre-signed URLs in `resolveStore`, the TTL must exceed total download time for **all** assets. Assets late in the queue fail with opaque HTTP 403 when URLs expire mid-sync.
114
+
115
+ Set `expiresAt` on the store to the earliest shared expiration timestamp so the cache can fail fast with `STORE_EXPIRED` before a later asset is fetched with a stale URL.
116
+
117
+ Wrong — short TTL on a large catalog:
118
+
119
+ ```typescript
120
+ async function resolveStore() {
121
+ const store = createMediaStore();
122
+ for (const item of await fetchCatalog()) {
123
+ const signedUrl = await getSignedUrl(
124
+ s3,
125
+ new GetObjectCommand({ Bucket: "b", Key: item.key }),
126
+ { expiresIn: 300 }, // 5 minutes — too short for 500 assets
127
+ );
128
+ store.add(["assets", item.id], { version: item.rev, mimeType: "video/mp4", url: signedUrl });
129
+ }
130
+ return store;
131
+ }
132
+ ```
133
+
134
+ Correct — generous TTL with `expiresAt`:
135
+
136
+ ```typescript
137
+ async function resolveStore() {
138
+ const ttlSeconds = 3600;
139
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
140
+ const store = createMediaStore({ expiresAt });
141
+
142
+ for (const item of await fetchCatalog()) {
143
+ const signedUrl = await getSignedUrl(s3, new GetObjectCommand({ Bucket: "b", Key: item.key }), {
144
+ expiresIn: ttlSeconds,
145
+ });
146
+ store.add(["assets", item.id], {
147
+ version: item.rev,
148
+ mimeType: "video/mp4",
149
+ url: signedUrl,
150
+ });
151
+ }
152
+
153
+ return store;
154
+ }
155
+ ```
156
+
157
+ Source: Maintainer interview
158
+
159
+ ### HIGH: Auth not tested in offline mode
160
+
161
+ In dev passthrough mode, assets load from their original remote URLs directly in the renderer. Auth issues only surface when `devPassthrough` is `false` and the main process downloads assets using the URLs from `resolveStore`.
162
+
163
+ Wrong — testing only in dev passthrough:
164
+
165
+ ```typescript
166
+ const mediaCache = createMediaCache({
167
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
168
+ devPassthrough: true,
169
+ resolveStore: async () => buildAuthenticatedStore(),
170
+ });
171
+ // "It works!" — but auth URLs are never actually used for downloads
172
+ ```
173
+
174
+ Correct — test with `devPassthrough: false` to verify auth:
175
+
176
+ ```typescript
177
+ const mediaCache = createMediaCache({
178
+ storagePath: { appPath: "userData", segments: ["offline-media"] },
179
+ devPassthrough: false,
180
+ resolveStore: async () => buildAuthenticatedStore(),
181
+ });
182
+ ```
183
+
184
+ Source: README
185
+ Cross-skill: cache-configuration/SKILL.md § Common Mistakes
186
+
187
+ ### HIGH Tension: Pre-signed URL TTL vs catalog size
188
+
189
+ Pre-signed URLs embedded at store build time are simple but have a TTL ceiling: expiration must cover the entire download queue. For large catalogs, evaluate expected sync duration and set TTLs accordingly. Use store `expiresAt` for fail-fast behavior.
190
+
191
+ See also: store-authoring/SKILL.md § Common Mistakes
192
+
193
+ ### HIGH Tension: Dev passthrough simplicity vs production correctness
194
+
195
+ `devPassthrough` bypasses downloads entirely. Code that works in dev (where auth isn't needed for public URLs) may fail in production when assets require signed downloads.
196
+
197
+ See also: cache-configuration/SKILL.md § Common Mistakes
198
+
199
+ ---
200
+
201
+ See also: store-authoring/SKILL.md — Asset URL definition and resolveStore
202
+ See also: cache-configuration/SKILL.md — devPassthrough mode bypasses downloads
203
+ See also: production-checklist/SKILL.md — Verify auth works in offline mode before deploy