@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,196 @@
1
+ import { DataValidationError } from "../shared/errors.js";
2
+ import { z } from "zod";
3
+ //#region src/internal/validation.ts
4
+ const nonNegativeIntegerSchema = z.number().int().nonnegative();
5
+ const nonNegativeNumberSchema = z.number().nonnegative();
6
+ /**
7
+ * Schema for IPC/API string identifiers: asset key, index name, index value, file stem.
8
+ * Enforces min 1 and max 2000 characters.
9
+ */
10
+ const stringInputSchema = z.string().min(1).max(2e3);
11
+ const jsonValueSchema = z.lazy(() => z.union([
12
+ z.string(),
13
+ z.number(),
14
+ z.boolean(),
15
+ z.null(),
16
+ z.array(jsonValueSchema),
17
+ z.record(z.string(), jsonValueSchema)
18
+ ]));
19
+ const jsonObjectSchema = z.record(z.string(), jsonValueSchema);
20
+ const stringRecordSchema = z.record(z.string(), z.string());
21
+ const serializedMediaCacheErrorSchema = z.object({
22
+ name: z.string(),
23
+ code: z.string(),
24
+ message: z.string()
25
+ });
26
+ const syncRunStatsSchema = z.object({
27
+ totalAssets: nonNegativeIntegerSchema,
28
+ downloadedAssets: nonNegativeIntegerSchema,
29
+ skippedAssets: nonNegativeIntegerSchema,
30
+ bytesDownloaded: nonNegativeNumberSchema
31
+ });
32
+ const syncProgressSchema = z.object({
33
+ runId: nonNegativeIntegerSchema,
34
+ phase: z.enum([
35
+ "resolving-store",
36
+ "staging-generation",
37
+ "diffing",
38
+ "downloading",
39
+ "committing",
40
+ "pruning"
41
+ ]),
42
+ totalAssets: nonNegativeIntegerSchema,
43
+ completedAssets: nonNegativeIntegerSchema,
44
+ downloadedAssets: nonNegativeIntegerSchema,
45
+ skippedAssets: nonNegativeIntegerSchema,
46
+ bytesDownloaded: nonNegativeNumberSchema
47
+ });
48
+ const syncRunSummarySchema = z.object({
49
+ id: nonNegativeIntegerSchema,
50
+ status: z.enum([
51
+ "running",
52
+ "success",
53
+ "error"
54
+ ]),
55
+ startedAt: nonNegativeIntegerSchema,
56
+ finishedAt: nonNegativeIntegerSchema.nullable(),
57
+ errorCode: z.string().nullable(),
58
+ errorMessage: z.string().nullable(),
59
+ stats: syncRunStatsSchema
60
+ });
61
+ const mediaCacheStatusSchema = z.object({
62
+ phase: z.enum([
63
+ "idle",
64
+ "syncing",
65
+ "ready",
66
+ "error"
67
+ ]),
68
+ storageRoot: z.string().nullable().default(null),
69
+ activeGenerationId: nonNegativeIntegerSchema.nullable(),
70
+ progress: syncProgressSchema.nullable(),
71
+ lastRun: syncRunSummarySchema.nullable(),
72
+ error: serializedMediaCacheErrorSchema.nullable(),
73
+ updatedAt: nonNegativeIntegerSchema
74
+ });
75
+ const mediaKindSchema = z.enum([
76
+ "video",
77
+ "image",
78
+ "audio",
79
+ "document",
80
+ "html",
81
+ "text",
82
+ "binary"
83
+ ]);
84
+ const mediaCacheAppPathSchema = z.enum([
85
+ "home",
86
+ "appData",
87
+ "userData",
88
+ "sessionData",
89
+ "temp",
90
+ "exe",
91
+ "module",
92
+ "desktop",
93
+ "documents",
94
+ "downloads",
95
+ "music",
96
+ "pictures",
97
+ "videos",
98
+ "recent",
99
+ "logs",
100
+ "crashDumps"
101
+ ]);
102
+ const storagePathSegmentSchema = z.string().min(1).refine((segment) => !/[/\\]/.test(segment), { message: "Storage path segments must not contain path separators" }).refine((segment) => segment !== "." && segment !== "..", { message: "Storage path segments must not be \".\" or \"..\"" });
103
+ const mediaCacheStoragePathSchema = z.object({
104
+ appPath: mediaCacheAppPathSchema,
105
+ segments: z.array(storagePathSegmentSchema).optional()
106
+ });
107
+ const statusSnapshotRowSchema = z.object({ status_json: z.string() });
108
+ const syncRunRowSchema = z.object({
109
+ id: nonNegativeIntegerSchema,
110
+ started_at_ms: nonNegativeIntegerSchema,
111
+ finished_at_ms: nonNegativeIntegerSchema.nullable(),
112
+ status: z.enum([
113
+ "running",
114
+ "success",
115
+ "error"
116
+ ]),
117
+ error_code: z.string().nullable(),
118
+ error_message: z.string().nullable(),
119
+ stats_json: z.string()
120
+ });
121
+ const syncRunIdRowSchema = z.object({ id: nonNegativeIntegerSchema });
122
+ const generationIdRowSchema = z.object({ id: nonNegativeIntegerSchema });
123
+ const activeGenerationRowSchema = z.object({ generation_id: nonNegativeIntegerSchema });
124
+ /** Row shape from getGenerationAssets used during sync diffing. */
125
+ const generationAssetRowSchema = z.object({
126
+ assetKey: z.string(),
127
+ version: z.string(),
128
+ relativePath: z.string().nullable(),
129
+ mimeType: z.string(),
130
+ url: z.string()
131
+ });
132
+ /** Row shape for a fully joined active asset used for queries. */
133
+ const activeAssetRowSchema = z.object({
134
+ generationId: nonNegativeIntegerSchema,
135
+ assetKey: z.string(),
136
+ displayKey: z.string(),
137
+ version: z.string(),
138
+ mimeType: z.string(),
139
+ mediaKind: mediaKindSchema,
140
+ byteLength: nonNegativeNumberSchema.nullable(),
141
+ metadata: z.string(),
142
+ indexesJson: z.string(),
143
+ relativePath: z.string().nullable(),
144
+ url: z.string(),
145
+ fileStem: z.string(),
146
+ orderIndex: nonNegativeIntegerSchema
147
+ });
148
+ const pendingDeletionSchema = z.object({
149
+ deletionKey: z.string(),
150
+ logicalKey: z.string(),
151
+ relativePath: z.string()
152
+ });
153
+ const protocolAssetTargetRowSchema = z.object({ relative_path: z.string().nullable() });
154
+ const fileStemRowSchema = z.object({ assetKey: z.string() });
155
+ const optionalNonNegativeIntegerSchema = z.number().int().nonnegative().nullish().transform((value) => value ?? void 0);
156
+ const optionalStringSchema = z.string().nullish().transform((value) => value ?? void 0);
157
+ const paginationInputSchema = z.object({
158
+ limit: optionalNonNegativeIntegerSchema,
159
+ cursor: optionalStringSchema
160
+ });
161
+ const optionalPaginationInputSchema = paginationInputSchema.nullish().transform((value) => value ?? void 0);
162
+ const cursorPayloadSchema = z.object({ index: z.number().int().nonnegative() });
163
+ function parseWithSchema(schema, value, context) {
164
+ const result = schema.safeParse(value);
165
+ if (result.success) return result.data;
166
+ throw new DataValidationError(`Invalid ${context}: ${formatZodIssues(result.error)}`, { cause: result.error });
167
+ }
168
+ function parseJsonWithSchema(rawJson, schema, context) {
169
+ let parsed;
170
+ try {
171
+ parsed = JSON.parse(rawJson);
172
+ } catch (error) {
173
+ throw new DataValidationError(`Invalid ${context}: failed to parse JSON.`, { cause: error });
174
+ }
175
+ return parseWithSchema(schema, parsed, context);
176
+ }
177
+ function stringifyWithSchema(value, schema, context) {
178
+ let rawJson;
179
+ try {
180
+ rawJson = JSON.stringify(value);
181
+ } catch (error) {
182
+ throw new DataValidationError(`Invalid ${context}: value is not JSON serializable.`, { cause: error });
183
+ }
184
+ if (rawJson === void 0) throw new DataValidationError(`Invalid ${context}: value is not JSON serializable.`);
185
+ parseJsonWithSchema(rawJson, schema, context);
186
+ return rawJson;
187
+ }
188
+ function formatZodIssues(error) {
189
+ return error.issues.map((issue) => {
190
+ return `${issue.path.length > 0 ? issue.path.join(".") : "(root)"}: ${issue.message}`;
191
+ }).join("; ");
192
+ }
193
+ //#endregion
194
+ export { activeAssetRowSchema, activeGenerationRowSchema, cursorPayloadSchema, fileStemRowSchema, generationAssetRowSchema, generationIdRowSchema, jsonObjectSchema, jsonValueSchema, mediaCacheStatusSchema, mediaCacheStoragePathSchema, optionalPaginationInputSchema, paginationInputSchema, parseJsonWithSchema, parseWithSchema, pendingDeletionSchema, protocolAssetTargetRowSchema, serializedMediaCacheErrorSchema, statusSnapshotRowSchema, stringInputSchema, stringRecordSchema, stringifyWithSchema, syncProgressSchema, syncRunIdRowSchema, syncRunRowSchema, syncRunStatsSchema, syncRunSummarySchema };
195
+
196
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","names":[],"sources":["../../src/internal/validation.ts"],"sourcesContent":["import { z } from \"zod\";\nimport { DataValidationError } from \"../shared/errors.js\";\nimport type { ActiveAssetRow, GenerationAssetRow, PendingDeletion } from \"../main/database.js\";\nimport type {\n JsonValue,\n MediaCacheAppPath,\n MediaCacheStatus,\n MediaCacheStoragePath,\n MediaKind,\n SerializedMediaCacheError,\n SyncProgress,\n SyncRunStats,\n SyncRunSummary,\n} from \"../shared/types.js\";\n\nconst nonNegativeIntegerSchema = z.number().int().nonnegative();\nconst nonNegativeNumberSchema = z.number().nonnegative();\n\n/**\n * Schema for IPC/API string identifiers: asset key, index name, index value, file stem.\n * Enforces min 1 and max 2000 characters.\n */\nexport const stringInputSchema = z.string().min(1).max(2000);\n\nexport const jsonValueSchema: z.ZodType<JsonValue> = z.lazy(() =>\n z.union([\n z.string(),\n z.number(),\n z.boolean(),\n z.null(),\n z.array(jsonValueSchema),\n z.record(z.string(), jsonValueSchema),\n ]),\n);\n\nexport const jsonObjectSchema: z.ZodType<Record<string, JsonValue>> = z.record(\n z.string(),\n jsonValueSchema,\n);\n\nexport const stringRecordSchema = z.record(z.string(), z.string());\n\nexport const serializedMediaCacheErrorSchema: z.ZodType<SerializedMediaCacheError> = z.object({\n name: z.string(),\n code: z.string(),\n message: z.string(),\n});\n\nexport const syncRunStatsSchema: z.ZodType<SyncRunStats> = z.object({\n totalAssets: nonNegativeIntegerSchema,\n downloadedAssets: nonNegativeIntegerSchema,\n skippedAssets: nonNegativeIntegerSchema,\n bytesDownloaded: nonNegativeNumberSchema,\n});\n\nexport const syncProgressSchema: z.ZodType<SyncProgress> = z.object({\n runId: nonNegativeIntegerSchema,\n phase: z.enum([\n \"resolving-store\",\n \"staging-generation\",\n \"diffing\",\n \"downloading\",\n \"committing\",\n \"pruning\",\n ]),\n totalAssets: nonNegativeIntegerSchema,\n completedAssets: nonNegativeIntegerSchema,\n downloadedAssets: nonNegativeIntegerSchema,\n skippedAssets: nonNegativeIntegerSchema,\n bytesDownloaded: nonNegativeNumberSchema,\n});\n\nexport const syncRunSummarySchema: z.ZodType<SyncRunSummary> = z.object({\n id: nonNegativeIntegerSchema,\n status: z.enum([\"running\", \"success\", \"error\"]),\n startedAt: nonNegativeIntegerSchema,\n finishedAt: nonNegativeIntegerSchema.nullable(),\n errorCode: z.string().nullable(),\n errorMessage: z.string().nullable(),\n stats: syncRunStatsSchema,\n});\n\nexport const mediaCacheStatusSchema: z.ZodType<MediaCacheStatus> = z.object({\n phase: z.enum([\"idle\", \"syncing\", \"ready\", \"error\"]),\n storageRoot: z.string().nullable().default(null),\n activeGenerationId: nonNegativeIntegerSchema.nullable(),\n progress: syncProgressSchema.nullable(),\n lastRun: syncRunSummarySchema.nullable(),\n error: serializedMediaCacheErrorSchema.nullable(),\n updatedAt: nonNegativeIntegerSchema,\n});\n\nconst mediaKindSchema: z.ZodType<MediaKind> = z.enum([\n \"video\",\n \"image\",\n \"audio\",\n \"document\",\n \"html\",\n \"text\",\n \"binary\",\n]);\n\nconst mediaCacheAppPathSchema: z.ZodType<MediaCacheAppPath> = z.enum([\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\nconst storagePathSegmentSchema = z\n .string()\n .min(1)\n .refine((segment) => !/[/\\\\]/.test(segment), {\n message: \"Storage path segments must not contain path separators\",\n })\n .refine((segment) => segment !== \".\" && segment !== \"..\", {\n message: 'Storage path segments must not be \".\" or \"..\"',\n });\n\nexport const mediaCacheStoragePathSchema: z.ZodType<MediaCacheStoragePath> = z.object({\n appPath: mediaCacheAppPathSchema,\n segments: z.array(storagePathSegmentSchema).optional(),\n});\n\nexport const statusSnapshotRowSchema = z.object({\n status_json: z.string(),\n});\n\nexport const syncRunRowSchema = z.object({\n id: nonNegativeIntegerSchema,\n started_at_ms: nonNegativeIntegerSchema,\n finished_at_ms: nonNegativeIntegerSchema.nullable(),\n status: z.enum([\"running\", \"success\", \"error\"]),\n error_code: z.string().nullable(),\n error_message: z.string().nullable(),\n stats_json: z.string(),\n});\n\nexport const syncRunIdRowSchema = z.object({\n id: nonNegativeIntegerSchema,\n});\n\nexport const generationIdRowSchema = z.object({\n id: nonNegativeIntegerSchema,\n});\n\nexport const activeGenerationRowSchema = z.object({\n generation_id: nonNegativeIntegerSchema,\n});\n\n/** Row shape from getGenerationAssets used during sync diffing. */\nexport const generationAssetRowSchema: z.ZodType<GenerationAssetRow> = z.object({\n assetKey: z.string(),\n version: z.string(),\n relativePath: z.string().nullable(),\n mimeType: z.string(),\n url: z.string(),\n});\n\n/** Row shape for a fully joined active asset used for queries. */\nexport const activeAssetRowSchema: z.ZodType<ActiveAssetRow> = z.object({\n generationId: nonNegativeIntegerSchema,\n assetKey: z.string(),\n displayKey: z.string(),\n version: z.string(),\n mimeType: z.string(),\n mediaKind: mediaKindSchema,\n byteLength: nonNegativeNumberSchema.nullable(),\n metadata: z.string(),\n indexesJson: z.string(),\n relativePath: z.string().nullable(),\n url: z.string(),\n fileStem: z.string(),\n orderIndex: nonNegativeIntegerSchema,\n});\n\nexport const pendingDeletionSchema: z.ZodType<PendingDeletion> = z.object({\n deletionKey: z.string(),\n logicalKey: z.string(),\n relativePath: z.string(),\n});\n\nexport const protocolAssetTargetRowSchema = z.object({\n relative_path: z.string().nullable(),\n});\n\nexport const fileStemRowSchema = z.object({\n assetKey: z.string(),\n});\n\nconst optionalNonNegativeIntegerSchema = z\n .number()\n .int()\n .nonnegative()\n .nullish()\n .transform((value) => value ?? undefined);\n\nconst optionalStringSchema = z\n .string()\n .nullish()\n .transform((value) => value ?? undefined);\n\nexport const paginationInputSchema = z.object({\n limit: optionalNonNegativeIntegerSchema,\n cursor: optionalStringSchema,\n});\n\nexport const optionalPaginationInputSchema = paginationInputSchema\n .nullish()\n .transform((value) => value ?? undefined);\n\nexport const cursorPayloadSchema = z.object({\n index: z.number().int().nonnegative(),\n});\n\nexport function parseWithSchema<T>(schema: z.ZodType<T>, value: unknown, context: string): T {\n const result = schema.safeParse(value);\n if (result.success) {\n return result.data;\n }\n\n throw new DataValidationError(`Invalid ${context}: ${formatZodIssues(result.error)}`, {\n cause: result.error,\n });\n}\n\nexport function parseJsonWithSchema<T>(rawJson: string, schema: z.ZodType<T>, context: string): T {\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawJson);\n } catch (error) {\n throw new DataValidationError(`Invalid ${context}: failed to parse JSON.`, {\n cause: error,\n });\n }\n\n return parseWithSchema(schema, parsed, context);\n}\n\nexport function stringifyWithSchema<T>(\n value: unknown,\n schema: z.ZodType<T>,\n context: string,\n): string {\n let rawJson: string | undefined;\n try {\n rawJson = JSON.stringify(value);\n } catch (error) {\n throw new DataValidationError(`Invalid ${context}: value is not JSON serializable.`, {\n cause: error,\n });\n }\n if (rawJson === undefined) {\n throw new DataValidationError(`Invalid ${context}: value is not JSON serializable.`);\n }\n\n parseJsonWithSchema(rawJson, schema, context);\n return rawJson;\n}\n\nfunction formatZodIssues(error: z.ZodError): string {\n return error.issues\n .map((issue) => {\n const path = issue.path.length > 0 ? issue.path.join(\".\") : \"(root)\";\n return `${path}: ${issue.message}`;\n })\n .join(\"; \");\n}\n"],"mappings":"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"}
@@ -0,0 +1,265 @@
1
+ const require_shared_errors = require("../shared/errors.cjs");
2
+ let node_fs = require("node:fs");
3
+ let node_fs_promises = require("node:fs/promises");
4
+ let node_stream = require("node:stream");
5
+ let node_path = require("node:path");
6
+ let node_stream_promises = require("node:stream/promises");
7
+ //#region src/main/asset-download.ts
8
+ /** When `reserveFreeBytes` is omitted, preserve this much free space on the cache volume (1 GiB). */
9
+ const DEFAULT_RESERVE_FREE_BYTES = 1024 * 1024 * 1024;
10
+ /**
11
+ * Effective minimum free bytes: explicit option, or {@link DEFAULT_RESERVE_FREE_BYTES} when omitted.
12
+ * `0` means no reserved headroom.
13
+ * @internal
14
+ */
15
+ function effectiveReserveFreeBytes(explicit) {
16
+ return explicit === void 0 ? DEFAULT_RESERVE_FREE_BYTES : explicit;
17
+ }
18
+ var AssetDownloader = class {
19
+ storageRoot;
20
+ deps;
21
+ options;
22
+ constructor(storageRoot, deps, options) {
23
+ this.storageRoot = storageRoot;
24
+ this.deps = deps;
25
+ this.options = options;
26
+ }
27
+ remainingDownloadBytes(download) {
28
+ const expectedBytes = download.byteLength ?? 0;
29
+ const partialBytes = this.partialDownloadBytes(download);
30
+ return Math.max(expectedBytes - partialBytes, 0);
31
+ }
32
+ partialDownloadPath(download) {
33
+ return (0, node_path.join)(this.storageRoot, "temp", sanitizeSegment(download.assetKey), sanitizeSegment(download.version), `${sanitizeSegment(download.fileName)}.part`);
34
+ }
35
+ cleanupObsoletePartialDownloads(downloads) {
36
+ const tempRoot = (0, node_path.join)(this.storageRoot, "temp");
37
+ if (!(0, node_fs.existsSync)(tempRoot)) return;
38
+ const resumablePaths = new Set(downloads.map((download) => this.partialDownloadPath(download)));
39
+ for (const filePath of listFilesRecursively(tempRoot)) {
40
+ if (!filePath.endsWith(".part") || resumablePaths.has(filePath)) continue;
41
+ (0, node_fs.rmSync)(filePath, { force: true });
42
+ pruneEmptyParents(filePath, this.storageRoot);
43
+ }
44
+ }
45
+ async downloadAsset(download, onChunk) {
46
+ const destinationRelativePath = (0, node_path.join)("blobs", sanitizeSegment(download.assetKey), sanitizeSegment(download.version), sanitizeSegment(download.fileName));
47
+ const destinationPath = (0, node_path.join)(this.storageRoot, destinationRelativePath);
48
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(destinationPath), { recursive: true });
49
+ const tempPath = this.partialDownloadPath(download);
50
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(tempPath), { recursive: true });
51
+ let lastError = null;
52
+ for (let attempt = 0; attempt < TOTAL_DOWNLOAD_ATTEMPTS; attempt += 1) try {
53
+ return await this.downloadAssetAttempt(download, destinationPath, destinationRelativePath, tempPath, onChunk);
54
+ } catch (error) {
55
+ lastError = error;
56
+ if (require_shared_errors.isNoSpaceError(error)) {
57
+ await (0, node_fs_promises.unlink)(tempPath).catch(() => void 0);
58
+ this.options.emitLog("error", "asset_download_storage_failed", {
59
+ asset_key: download.assetKey,
60
+ url: download.request.url
61
+ });
62
+ throw new require_shared_errors.StorageLimitError(`Disk is full while downloading ${download.assetKey}.`, { cause: error });
63
+ }
64
+ if (!isRetryableDownloadError(error)) {
65
+ await (0, node_fs_promises.unlink)(tempPath).catch(() => void 0);
66
+ throw error;
67
+ }
68
+ if (attempt === TOTAL_DOWNLOAD_ATTEMPTS - 1) {
69
+ this.options.emitLog("warn", "asset_download_retry_exhausted", {
70
+ asset_key: download.assetKey,
71
+ attempt: attempt + 1,
72
+ partial_path: tempPath
73
+ });
74
+ break;
75
+ }
76
+ const delayMs = calculateRetryDelay(attempt);
77
+ this.options.emitLog("warn", "asset_download_retry_scheduled", {
78
+ asset_key: download.assetKey,
79
+ attempt: attempt + 1,
80
+ retry_delay_ms: delayMs
81
+ });
82
+ await this.deps.sleep(delayMs);
83
+ }
84
+ throw lastError;
85
+ }
86
+ async ensureFileSpaceCommit() {
87
+ const stats = await this.deps.statfs(this.storageRoot);
88
+ const availableBytes = Number(stats.bavail) * Number(stats.bsize);
89
+ const reserve = effectiveReserveFreeBytes(this.options.reserveFreeBytes);
90
+ if (availableBytes < reserve) throw new require_shared_errors.StorageLimitError(`Committing download would violate reserveFreeBytes ${reserve}.`);
91
+ }
92
+ partialDownloadBytes(download) {
93
+ const tempPath = this.partialDownloadPath(download);
94
+ return (0, node_fs.existsSync)(tempPath) ? (0, node_fs.statSync)(tempPath).size : 0;
95
+ }
96
+ async downloadAssetAttempt(download, destinationPath, destinationRelativePath, tempPath, onChunk) {
97
+ let restartedWithoutRange = false;
98
+ for (;;) {
99
+ const resumeSize = (0, node_fs.existsSync)(tempPath) ? (0, node_fs.statSync)(tempPath).size : 0;
100
+ const headers = new Headers(download.request.headers);
101
+ if (resumeSize > 0) headers.set("range", `bytes=${resumeSize}-`);
102
+ const response = await this.deps.fetchImpl(download.request.url, {
103
+ method: download.request.method ?? "GET",
104
+ headers
105
+ });
106
+ if (resumeSize > 0 && response.status === 416) {
107
+ if (restartedWithoutRange) throw createDownloadError(`Server rejected range request for ${download.assetKey}.`, false, response.status);
108
+ restartedWithoutRange = true;
109
+ await (0, node_fs_promises.unlink)(tempPath).catch(() => void 0);
110
+ this.options.emitLog("debug", "asset_download_range_restart", {
111
+ asset_key: download.assetKey,
112
+ resumed_bytes: resumeSize,
113
+ response_status: response.status,
114
+ content_range: response.headers.get("content-range")
115
+ });
116
+ continue;
117
+ }
118
+ if (!response.ok || !response.body) {
119
+ this.options.emitLog("warn", "asset_download_rejected", {
120
+ asset_key: download.assetKey,
121
+ status: response.status,
122
+ status_text: response.statusText,
123
+ url: download.request.url
124
+ });
125
+ throw createDownloadError(`Download failed for ${download.assetKey}: ${response.status} ${response.statusText}`, isRetryableStatus(response.status), response.status);
126
+ }
127
+ if (resumeSize > 0 && (response.status !== 206 || parseContentRangeStart(response.headers.get("content-range")) !== resumeSize)) {
128
+ if (restartedWithoutRange) throw createDownloadError(`Server did not honor range request for ${download.assetKey}.`, false, response.status);
129
+ restartedWithoutRange = true;
130
+ await (0, node_fs_promises.unlink)(tempPath).catch(() => void 0);
131
+ this.options.emitLog("debug", "asset_download_range_restart", {
132
+ asset_key: download.assetKey,
133
+ resumed_bytes: resumeSize,
134
+ response_status: response.status,
135
+ content_range: response.headers.get("content-range")
136
+ });
137
+ continue;
138
+ }
139
+ const nodeStream = node_stream.Readable.fromWeb(response.body);
140
+ const writeStream = (0, node_fs.createWriteStream)(tempPath, { flags: resumeSize > 0 ? "a" : "w" });
141
+ nodeStream.on("data", (chunk) => {
142
+ onChunk(chunk.byteLength);
143
+ });
144
+ try {
145
+ await (0, node_stream_promises.pipeline)(nodeStream, writeStream);
146
+ } catch (error) {
147
+ throw wrapRetryableDownloadError(error);
148
+ }
149
+ await this.ensureFileSpaceCommit();
150
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(destinationPath), { recursive: true });
151
+ (0, node_fs.rmSync)(destinationPath, { force: true });
152
+ (0, node_fs.renameSync)(tempPath, destinationPath);
153
+ return {
154
+ relativePath: destinationRelativePath,
155
+ fallbackMimeType: normalizeResponseMimeType(response.headers.get("content-type"))
156
+ };
157
+ }
158
+ }
159
+ };
160
+ function sanitizeSegment(segment) {
161
+ return encodeURIComponent(segment);
162
+ }
163
+ function pruneEmptyParents(pathToFile, storageRoot) {
164
+ let current = (0, node_path.dirname)(pathToFile);
165
+ while (current.startsWith(storageRoot) && current !== storageRoot) {
166
+ if ((0, node_fs.existsSync)(current) && (0, node_fs.readdirSync)(current).length === 0) {
167
+ (0, node_fs.rmSync)(current, {
168
+ recursive: true,
169
+ force: true
170
+ });
171
+ current = (0, node_path.dirname)(current);
172
+ continue;
173
+ }
174
+ break;
175
+ }
176
+ }
177
+ function listFilesRecursively(directory) {
178
+ if (!(0, node_fs.existsSync)(directory)) return [];
179
+ if ((0, node_fs.statSync)(directory).isFile()) return [directory];
180
+ return (0, node_fs.readdirSync)(directory).flatMap((entry) => listFilesRecursively((0, node_path.join)(directory, entry)));
181
+ }
182
+ const TOTAL_DOWNLOAD_ATTEMPTS = 4;
183
+ const RETRYABLE_STATUS_CODES = new Set([
184
+ 408,
185
+ 429,
186
+ 500,
187
+ 502,
188
+ 503,
189
+ 504
190
+ ]);
191
+ const RETRYABLE_ERROR_CODES = new Set([
192
+ "ECONNRESET",
193
+ "ECONNREFUSED",
194
+ "EHOSTUNREACH",
195
+ "ENETUNREACH",
196
+ "ENOTFOUND",
197
+ "ETIMEDOUT",
198
+ "UND_ERR_CONNECT_TIMEOUT",
199
+ "UND_ERR_HEADERS_TIMEOUT",
200
+ "UND_ERR_SOCKET"
201
+ ]);
202
+ function calculateRetryDelay(attempt) {
203
+ const baseDelay = 1e3 * Math.pow(2, attempt);
204
+ return baseDelay + Math.floor(baseDelay * .25 * Math.random());
205
+ }
206
+ function createDownloadError(message, retryable, status, cause) {
207
+ const error = new require_shared_errors.SyncFailureError(message, cause ? { cause } : void 0);
208
+ error.retryable = retryable;
209
+ error.status = status;
210
+ return error;
211
+ }
212
+ function wrapRetryableDownloadError(error) {
213
+ if (error instanceof require_shared_errors.SyncFailureError) return error;
214
+ if (error instanceof Error) {
215
+ const wrapped = new require_shared_errors.SyncFailureError(error.message, { cause: error });
216
+ wrapped.retryable = hasRetryableDownloadSignal(error);
217
+ return wrapped;
218
+ }
219
+ const wrapped = new require_shared_errors.SyncFailureError(String(error));
220
+ wrapped.retryable = false;
221
+ return wrapped;
222
+ }
223
+ function isRetryableDownloadError(error) {
224
+ if (!(error instanceof Error)) return false;
225
+ const candidate = error;
226
+ if (candidate.retryable !== void 0) return candidate.retryable;
227
+ return hasRetryableDownloadSignal(error);
228
+ }
229
+ function isRetryableStatus(status) {
230
+ return RETRYABLE_STATUS_CODES.has(status);
231
+ }
232
+ function isRetryableErrorCode(code) {
233
+ return code !== void 0 && RETRYABLE_ERROR_CODES.has(code);
234
+ }
235
+ function isRetryableMessage(message) {
236
+ const value = message.toLowerCase();
237
+ return value.includes("aborted") || value.includes("connection") || value.includes("network") || value.includes("reset") || value.includes("socket") || value.includes("terminated") || value.includes("timeout");
238
+ }
239
+ function hasRetryableDownloadSignal(error) {
240
+ return collectErrorChain(error).some((entry) => isRetryableErrorCode(entry.code) || isRetryableMessage(entry.message));
241
+ }
242
+ function collectErrorChain(error) {
243
+ const chain = [];
244
+ let current = error;
245
+ while (current instanceof Error && !chain.includes(current)) {
246
+ chain.push(current);
247
+ current = current.cause;
248
+ }
249
+ return chain;
250
+ }
251
+ function normalizeResponseMimeType(contentType) {
252
+ const value = contentType?.trim();
253
+ return value && value.length > 0 ? value : null;
254
+ }
255
+ function parseContentRangeStart(contentRange) {
256
+ if (!contentRange) return null;
257
+ const match = contentRange.match(/^bytes (\d+)-\d+\/(?:\d+|\*)$/i);
258
+ return match?.[1] ? Number.parseInt(match[1], 10) : null;
259
+ }
260
+ //#endregion
261
+ exports.AssetDownloader = AssetDownloader;
262
+ exports.DEFAULT_RESERVE_FREE_BYTES = DEFAULT_RESERVE_FREE_BYTES;
263
+ exports.effectiveReserveFreeBytes = effectiveReserveFreeBytes;
264
+
265
+ //# sourceMappingURL=asset-download.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-download.cjs","names":["isNoSpaceError","StorageLimitError","Readable","SyncFailureError"],"sources":["../../src/main/asset-download.ts"],"sourcesContent":["import {\n createWriteStream,\n existsSync,\n mkdirSync,\n readdirSync,\n renameSync,\n rmSync,\n statSync,\n} from \"node:fs\";\nimport { unlink } from \"node:fs/promises\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport type { ReadableStream as NodeReadableStream } from \"node:stream/web\";\nimport { dirname, join } from \"node:path\";\nimport { isNoSpaceError, StorageLimitError, SyncFailureError } from \"../shared/errors.js\";\nimport type { JsonValue, MediaCacheLogLevel } from \"../shared/types.js\";\n\n/** When `reserveFreeBytes` is omitted, preserve this much free space on the cache volume (1 GiB). */\nexport const DEFAULT_RESERVE_FREE_BYTES = 1024 * 1024 * 1024;\n\n/**\n * Effective minimum free bytes: explicit option, or {@link DEFAULT_RESERVE_FREE_BYTES} when omitted.\n * `0` means no reserved headroom.\n * @internal\n */\nexport function effectiveReserveFreeBytes(explicit: number | undefined): number {\n return explicit === undefined ? DEFAULT_RESERVE_FREE_BYTES : explicit;\n}\n\nexport interface QueuedAssetDownloadTarget {\n assetKey: string;\n fileName: string;\n version: string;\n byteLength?: number;\n}\n\nexport interface AssetDownloadTarget extends QueuedAssetDownloadTarget {\n request: {\n url: string;\n method?: string;\n headers?: HeadersInit;\n };\n}\n\nexport interface AssetDownloadResult {\n relativePath: string;\n fallbackMimeType: string | null;\n}\n\nexport interface AssetDownloaderDependencies {\n fetchImpl: typeof globalThis.fetch;\n sleep: (delayMs: number) => Promise<void>;\n statfs: (path: string) => Promise<StatFsResult>;\n}\n\nexport type AssetDownloadLogHandler = (\n level: MediaCacheLogLevel,\n event: string,\n fields?: Record<string, JsonValue | undefined>,\n) => void;\n\ntype StatFsResult = Awaited<ReturnType<typeof import(\"node:fs/promises\").statfs>>;\n\nexport class AssetDownloader {\n constructor(\n private readonly storageRoot: string,\n private readonly deps: AssetDownloaderDependencies,\n private readonly options: {\n reserveFreeBytes?: number;\n emitLog: AssetDownloadLogHandler;\n },\n ) {}\n\n remainingDownloadBytes(download: QueuedAssetDownloadTarget): number {\n const expectedBytes = download.byteLength ?? 0;\n const partialBytes = this.partialDownloadBytes(download);\n return Math.max(expectedBytes - partialBytes, 0);\n }\n\n partialDownloadPath(download: QueuedAssetDownloadTarget): string {\n return join(\n this.storageRoot,\n \"temp\",\n sanitizeSegment(download.assetKey),\n sanitizeSegment(download.version),\n `${sanitizeSegment(download.fileName)}.part`,\n );\n }\n\n cleanupObsoletePartialDownloads(downloads: QueuedAssetDownloadTarget[]): void {\n const tempRoot = join(this.storageRoot, \"temp\");\n if (!existsSync(tempRoot)) {\n return;\n }\n\n const resumablePaths = new Set(downloads.map((download) => this.partialDownloadPath(download)));\n for (const filePath of listFilesRecursively(tempRoot)) {\n if (!filePath.endsWith(\".part\") || resumablePaths.has(filePath)) {\n continue;\n }\n\n rmSync(filePath, { force: true });\n pruneEmptyParents(filePath, this.storageRoot);\n }\n }\n\n async downloadAsset(\n download: AssetDownloadTarget,\n onChunk: (chunkBytes: number) => void,\n ): Promise<AssetDownloadResult> {\n const destinationRelativePath = join(\n \"blobs\",\n sanitizeSegment(download.assetKey),\n sanitizeSegment(download.version),\n sanitizeSegment(download.fileName),\n );\n const destinationPath = join(this.storageRoot, destinationRelativePath);\n\n mkdirSync(dirname(destinationPath), { recursive: true });\n const tempPath = this.partialDownloadPath(download);\n mkdirSync(dirname(tempPath), { recursive: true });\n\n let lastError: unknown = null;\n\n for (let attempt = 0; attempt < TOTAL_DOWNLOAD_ATTEMPTS; attempt += 1) {\n try {\n return await this.downloadAssetAttempt(\n download,\n destinationPath,\n destinationRelativePath,\n tempPath,\n onChunk,\n );\n } catch (error) {\n lastError = error;\n\n if (isNoSpaceError(error)) {\n await unlink(tempPath).catch(() => undefined);\n this.options.emitLog(\"error\", \"asset_download_storage_failed\", {\n asset_key: download.assetKey,\n url: download.request.url,\n });\n throw new StorageLimitError(`Disk is full while downloading ${download.assetKey}.`, {\n cause: error,\n });\n }\n\n const retryable = isRetryableDownloadError(error);\n if (!retryable) {\n await unlink(tempPath).catch(() => undefined);\n throw error;\n }\n\n if (attempt === TOTAL_DOWNLOAD_ATTEMPTS - 1) {\n this.options.emitLog(\"warn\", \"asset_download_retry_exhausted\", {\n asset_key: download.assetKey,\n attempt: attempt + 1,\n partial_path: tempPath,\n });\n break;\n }\n\n const delayMs = calculateRetryDelay(attempt);\n this.options.emitLog(\"warn\", \"asset_download_retry_scheduled\", {\n asset_key: download.assetKey,\n attempt: attempt + 1,\n retry_delay_ms: delayMs,\n });\n await this.deps.sleep(delayMs);\n }\n }\n\n throw lastError;\n }\n\n async ensureFileSpaceCommit(): Promise<void> {\n const stats = await this.deps.statfs(this.storageRoot);\n const availableBytes = Number(stats.bavail) * Number(stats.bsize);\n const reserve = effectiveReserveFreeBytes(this.options.reserveFreeBytes);\n if (availableBytes < reserve) {\n throw new StorageLimitError(`Committing download would violate reserveFreeBytes ${reserve}.`);\n }\n }\n\n private partialDownloadBytes(download: QueuedAssetDownloadTarget): number {\n const tempPath = this.partialDownloadPath(download);\n return existsSync(tempPath) ? statSync(tempPath).size : 0;\n }\n\n private async downloadAssetAttempt(\n download: AssetDownloadTarget,\n destinationPath: string,\n destinationRelativePath: string,\n tempPath: string,\n onChunk: (chunkBytes: number) => void,\n ): Promise<AssetDownloadResult> {\n let restartedWithoutRange = false;\n\n for (;;) {\n const resumeSize = existsSync(tempPath) ? statSync(tempPath).size : 0;\n const headers = new Headers(download.request.headers);\n if (resumeSize > 0) {\n headers.set(\"range\", `bytes=${resumeSize}-`);\n }\n\n const response = await this.deps.fetchImpl(download.request.url, {\n method: download.request.method ?? \"GET\",\n headers,\n });\n\n if (resumeSize > 0 && response.status === 416) {\n if (restartedWithoutRange) {\n throw createDownloadError(\n `Server rejected range request for ${download.assetKey}.`,\n false,\n response.status,\n );\n }\n\n restartedWithoutRange = true;\n await unlink(tempPath).catch(() => undefined);\n this.options.emitLog(\"debug\", \"asset_download_range_restart\", {\n asset_key: download.assetKey,\n resumed_bytes: resumeSize,\n response_status: response.status,\n content_range: response.headers.get(\"content-range\"),\n });\n continue;\n }\n\n if (!response.ok || !response.body) {\n this.options.emitLog(\"warn\", \"asset_download_rejected\", {\n asset_key: download.assetKey,\n status: response.status,\n status_text: response.statusText,\n url: download.request.url,\n });\n throw createDownloadError(\n `Download failed for ${download.assetKey}: ${response.status} ${response.statusText}`,\n isRetryableStatus(response.status),\n response.status,\n );\n }\n\n if (\n resumeSize > 0 &&\n (response.status !== 206 ||\n parseContentRangeStart(response.headers.get(\"content-range\")) !== resumeSize)\n ) {\n if (restartedWithoutRange) {\n throw createDownloadError(\n `Server did not honor range request for ${download.assetKey}.`,\n false,\n response.status,\n );\n }\n\n restartedWithoutRange = true;\n await unlink(tempPath).catch(() => undefined);\n this.options.emitLog(\"debug\", \"asset_download_range_restart\", {\n asset_key: download.assetKey,\n resumed_bytes: resumeSize,\n response_status: response.status,\n content_range: response.headers.get(\"content-range\"),\n });\n continue;\n }\n\n const nodeStream = Readable.fromWeb(\n response.body as unknown as NodeReadableStream<Uint8Array>,\n );\n const writeStream = createWriteStream(tempPath, { flags: resumeSize > 0 ? \"a\" : \"w\" });\n\n nodeStream.on(\"data\", (chunk) => {\n onChunk((chunk as Buffer).byteLength);\n });\n\n try {\n await pipeline(nodeStream, writeStream);\n } catch (error) {\n throw wrapRetryableDownloadError(error);\n }\n\n await this.ensureFileSpaceCommit();\n mkdirSync(dirname(destinationPath), { recursive: true });\n rmSync(destinationPath, { force: true });\n renameSync(tempPath, destinationPath);\n return {\n relativePath: destinationRelativePath,\n fallbackMimeType: normalizeResponseMimeType(response.headers.get(\"content-type\")),\n };\n }\n }\n}\n\nfunction sanitizeSegment(segment: string): string {\n return encodeURIComponent(segment);\n}\n\nfunction pruneEmptyParents(pathToFile: string, storageRoot: string): void {\n let current = dirname(pathToFile);\n while (current.startsWith(storageRoot) && current !== storageRoot) {\n if (existsSync(current) && readdirSync(current).length === 0) {\n rmSync(current, { recursive: true, force: true });\n current = dirname(current);\n continue;\n }\n break;\n }\n}\n\nfunction listFilesRecursively(directory: string): string[] {\n if (!existsSync(directory)) {\n return [];\n }\n\n const stats = statSync(directory);\n if (stats.isFile()) {\n return [directory];\n }\n\n return readdirSync(directory).flatMap((entry) => listFilesRecursively(join(directory, entry)));\n}\n\nconst TOTAL_DOWNLOAD_ATTEMPTS = 4;\nconst RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);\nconst RETRYABLE_ERROR_CODES = new Set([\n \"ECONNRESET\",\n \"ECONNREFUSED\",\n \"EHOSTUNREACH\",\n \"ENETUNREACH\",\n \"ENOTFOUND\",\n \"ETIMEDOUT\",\n \"UND_ERR_CONNECT_TIMEOUT\",\n \"UND_ERR_HEADERS_TIMEOUT\",\n \"UND_ERR_SOCKET\",\n]);\n\ntype RetryableDownloadError = Error & {\n retryable?: boolean;\n status?: number;\n};\n\nfunction calculateRetryDelay(attempt: number): number {\n const baseDelay = 1000 * Math.pow(2, attempt);\n const jitter = Math.floor(baseDelay * 0.25 * Math.random());\n return baseDelay + jitter;\n}\n\nfunction createDownloadError(\n message: string,\n retryable: boolean,\n status?: number,\n cause?: unknown,\n): RetryableDownloadError {\n const error = new SyncFailureError(\n message,\n cause ? { cause } : undefined,\n ) as RetryableDownloadError;\n error.retryable = retryable;\n error.status = status;\n return error;\n}\n\nfunction wrapRetryableDownloadError(error: unknown): RetryableDownloadError {\n if (error instanceof SyncFailureError) {\n return error as RetryableDownloadError;\n }\n\n if (error instanceof Error) {\n const wrapped = new SyncFailureError(error.message, { cause: error }) as RetryableDownloadError;\n wrapped.retryable = hasRetryableDownloadSignal(error);\n return wrapped;\n }\n\n const wrapped = new SyncFailureError(String(error)) as RetryableDownloadError;\n wrapped.retryable = false;\n return wrapped;\n}\n\nfunction isRetryableDownloadError(error: unknown): boolean {\n if (!(error instanceof Error)) {\n return false;\n }\n\n const candidate = error as RetryableDownloadError;\n if (candidate.retryable !== undefined) {\n return candidate.retryable;\n }\n\n return hasRetryableDownloadSignal(error);\n}\n\nfunction isRetryableStatus(status: number): boolean {\n return RETRYABLE_STATUS_CODES.has(status);\n}\n\nfunction isRetryableErrorCode(code: string | undefined): boolean {\n return code !== undefined && RETRYABLE_ERROR_CODES.has(code);\n}\n\nfunction isRetryableMessage(message: string): boolean {\n const value = message.toLowerCase();\n return (\n value.includes(\"aborted\") ||\n value.includes(\"connection\") ||\n value.includes(\"network\") ||\n value.includes(\"reset\") ||\n value.includes(\"socket\") ||\n value.includes(\"terminated\") ||\n value.includes(\"timeout\")\n );\n}\n\nfunction hasRetryableDownloadSignal(error: Error): boolean {\n return collectErrorChain(error).some(\n (entry) =>\n isRetryableErrorCode((entry as NodeJS.ErrnoException).code) ||\n isRetryableMessage(entry.message),\n );\n}\n\nfunction collectErrorChain(error: Error): Error[] {\n const chain: Error[] = [];\n let current: unknown = error;\n\n while (current instanceof Error && !chain.includes(current)) {\n chain.push(current);\n current = current.cause;\n }\n\n return chain;\n}\n\nfunction normalizeResponseMimeType(contentType: string | null): string | null {\n const value = contentType?.trim();\n return value && value.length > 0 ? value : null;\n}\n\nfunction parseContentRangeStart(contentRange: string | null): number | null {\n if (!contentRange) {\n return null;\n }\n\n const match = contentRange.match(/^bytes (\\d+)-\\d+\\/(?:\\d+|\\*)$/i);\n return match?.[1] ? Number.parseInt(match[1], 10) : null;\n}\n"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ //#region src/main/asset-download.d.ts
2
+ /** When `reserveFreeBytes` is omitted, preserve this much free space on the cache volume (1 GiB). */
3
+ declare const DEFAULT_RESERVE_FREE_BYTES: number;
4
+ /**
5
+ * Effective minimum free bytes: explicit option, or {@link DEFAULT_RESERVE_FREE_BYTES} when omitted.
6
+ * `0` means no reserved headroom.
7
+ * @internal
8
+ */
9
+ declare function effectiveReserveFreeBytes(explicit: number | undefined): number;
10
+ //#endregion
11
+ export { DEFAULT_RESERVE_FREE_BYTES, effectiveReserveFreeBytes };
12
+ //# sourceMappingURL=asset-download.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-download.d.cts","names":[],"sources":["../../src/main/asset-download.ts"],"mappings":";AAkBA;AAAA,cAAa,0BAAA;;;;AAOb;;iBAAgB,yBAAA,CAA0B,QAAA"}
@@ -0,0 +1,12 @@
1
+ //#region src/main/asset-download.d.ts
2
+ /** When `reserveFreeBytes` is omitted, preserve this much free space on the cache volume (1 GiB). */
3
+ declare const DEFAULT_RESERVE_FREE_BYTES: number;
4
+ /**
5
+ * Effective minimum free bytes: explicit option, or {@link DEFAULT_RESERVE_FREE_BYTES} when omitted.
6
+ * `0` means no reserved headroom.
7
+ * @internal
8
+ */
9
+ declare function effectiveReserveFreeBytes(explicit: number | undefined): number;
10
+ //#endregion
11
+ export { DEFAULT_RESERVE_FREE_BYTES, effectiveReserveFreeBytes };
12
+ //# sourceMappingURL=asset-download.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-download.d.ts","names":[],"sources":["../../src/main/asset-download.ts"],"mappings":";AAkBA;AAAA,cAAa,0BAAA;;;;AAOb;;iBAAgB,yBAAA,CAA0B,QAAA"}