@rockhall/electron-offline-content 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +384 -0
- package/LICENSE +21 -0
- package/README.md +794 -0
- package/dist/internal/asset-file-name.cjs +13 -0
- package/dist/internal/asset-file-name.cjs.map +1 -0
- package/dist/internal/asset-file-name.d.cts +6 -0
- package/dist/internal/asset-file-name.d.cts.map +1 -0
- package/dist/internal/asset-file-name.d.ts +6 -0
- package/dist/internal/asset-file-name.d.ts.map +1 -0
- package/dist/internal/asset-file-name.js +12 -0
- package/dist/internal/asset-file-name.js.map +1 -0
- package/dist/internal/asset-key.cjs +30 -0
- package/dist/internal/asset-key.cjs.map +1 -0
- package/dist/internal/asset-key.d.cts +19 -0
- package/dist/internal/asset-key.d.cts.map +1 -0
- package/dist/internal/asset-key.d.ts +19 -0
- package/dist/internal/asset-key.d.ts.map +1 -0
- package/dist/internal/asset-key.js +27 -0
- package/dist/internal/asset-key.js.map +1 -0
- package/dist/internal/log-format.cjs +98 -0
- package/dist/internal/log-format.cjs.map +1 -0
- package/dist/internal/log-format.d.cts +10 -0
- package/dist/internal/log-format.d.cts.map +1 -0
- package/dist/internal/log-format.d.ts +10 -0
- package/dist/internal/log-format.d.ts.map +1 -0
- package/dist/internal/log-format.js +97 -0
- package/dist/internal/log-format.js.map +1 -0
- package/dist/internal/media-kind.cjs +46 -0
- package/dist/internal/media-kind.cjs.map +1 -0
- package/dist/internal/media-kind.d.cts +20 -0
- package/dist/internal/media-kind.d.cts.map +1 -0
- package/dist/internal/media-kind.d.ts +20 -0
- package/dist/internal/media-kind.d.ts.map +1 -0
- package/dist/internal/media-kind.js +45 -0
- package/dist/internal/media-kind.js.map +1 -0
- package/dist/internal/url-warn.cjs +14 -0
- package/dist/internal/url-warn.cjs.map +1 -0
- package/dist/internal/url-warn.d.cts +10 -0
- package/dist/internal/url-warn.d.cts.map +1 -0
- package/dist/internal/url-warn.d.ts +10 -0
- package/dist/internal/url-warn.d.ts.map +1 -0
- package/dist/internal/url-warn.js +13 -0
- package/dist/internal/url-warn.js.map +1 -0
- package/dist/internal/validation.cjs +222 -0
- package/dist/internal/validation.cjs.map +1 -0
- package/dist/internal/validation.d.cts +78 -0
- package/dist/internal/validation.d.cts.map +1 -0
- package/dist/internal/validation.d.ts +78 -0
- package/dist/internal/validation.d.ts.map +1 -0
- package/dist/internal/validation.js +196 -0
- package/dist/internal/validation.js.map +1 -0
- package/dist/main/asset-download.cjs +265 -0
- package/dist/main/asset-download.cjs.map +1 -0
- package/dist/main/asset-download.d.cts +12 -0
- package/dist/main/asset-download.d.cts.map +1 -0
- package/dist/main/asset-download.d.ts +12 -0
- package/dist/main/asset-download.d.ts.map +1 -0
- package/dist/main/asset-download.js +263 -0
- package/dist/main/asset-download.js.map +1 -0
- package/dist/main/database.cjs +473 -0
- package/dist/main/database.cjs.map +1 -0
- package/dist/main/database.d.cts +81 -0
- package/dist/main/database.d.cts.map +1 -0
- package/dist/main/database.d.ts +81 -0
- package/dist/main/database.d.ts.map +1 -0
- package/dist/main/database.js +472 -0
- package/dist/main/database.js.map +1 -0
- package/dist/main/index.cjs +22 -0
- package/dist/main/index.d.cts +7 -0
- package/dist/main/index.d.ts +7 -0
- package/dist/main/index.js +7 -0
- package/dist/main/media-cache.cjs +862 -0
- package/dist/main/media-cache.cjs.map +1 -0
- package/dist/main/media-cache.d.cts +134 -0
- package/dist/main/media-cache.d.cts.map +1 -0
- package/dist/main/media-cache.d.ts +134 -0
- package/dist/main/media-cache.d.ts.map +1 -0
- package/dist/main/media-cache.js +854 -0
- package/dist/main/media-cache.js.map +1 -0
- package/dist/main/storage-root-lock.cjs +124 -0
- package/dist/main/storage-root-lock.cjs.map +1 -0
- package/dist/main/storage-root-lock.d.cts +11 -0
- package/dist/main/storage-root-lock.d.cts.map +1 -0
- package/dist/main/storage-root-lock.d.ts +11 -0
- package/dist/main/storage-root-lock.d.ts.map +1 -0
- package/dist/main/storage-root-lock.js +120 -0
- package/dist/main/storage-root-lock.js.map +1 -0
- package/dist/main/store.cjs +197 -0
- package/dist/main/store.cjs.map +1 -0
- package/dist/main/store.d.cts +83 -0
- package/dist/main/store.d.cts.map +1 -0
- package/dist/main/store.d.ts +83 -0
- package/dist/main/store.d.ts.map +1 -0
- package/dist/main/store.js +195 -0
- package/dist/main/store.js.map +1 -0
- package/dist/preload/index.cjs +36 -0
- package/dist/preload/index.cjs.map +1 -0
- package/dist/preload/index.d.cts +14 -0
- package/dist/preload/index.d.cts.map +1 -0
- package/dist/preload/index.d.ts +14 -0
- package/dist/preload/index.d.ts.map +1 -0
- package/dist/preload/index.js +34 -0
- package/dist/preload/index.js.map +1 -0
- package/dist/react/index.cjs +199 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +50 -0
- package/dist/react/index.d.cts.map +1 -0
- package/dist/react/index.d.ts +50 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +191 -0
- package/dist/react/index.js.map +1 -0
- package/dist/renderer/helpers.cjs +36 -0
- package/dist/renderer/helpers.cjs.map +1 -0
- package/dist/renderer/helpers.d.cts +11 -0
- package/dist/renderer/helpers.d.cts.map +1 -0
- package/dist/renderer/helpers.d.ts +11 -0
- package/dist/renderer/helpers.d.ts.map +1 -0
- package/dist/renderer/helpers.js +35 -0
- package/dist/renderer/helpers.js.map +1 -0
- package/dist/renderer/index.cjs +20 -0
- package/dist/renderer/index.cjs.map +1 -0
- package/dist/renderer/index.d.cts +14 -0
- package/dist/renderer/index.d.cts.map +1 -0
- package/dist/renderer/index.d.ts +14 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/renderer/index.js +14 -0
- package/dist/renderer/index.js.map +1 -0
- package/dist/renderer/runtime.cjs +278 -0
- package/dist/renderer/runtime.cjs.map +1 -0
- package/dist/renderer/runtime.d.cts +35 -0
- package/dist/renderer/runtime.d.cts.map +1 -0
- package/dist/renderer/runtime.d.ts +35 -0
- package/dist/renderer/runtime.d.ts.map +1 -0
- package/dist/renderer/runtime.js +273 -0
- package/dist/renderer/runtime.js.map +1 -0
- package/dist/renderer/window-globals.d.cts +9 -0
- package/dist/renderer/window-globals.d.cts.map +1 -0
- package/dist/renderer/window-globals.d.ts +9 -0
- package/dist/renderer/window-globals.d.ts.map +1 -0
- package/dist/shared/errors.cjs +102 -0
- package/dist/shared/errors.cjs.map +1 -0
- package/dist/shared/errors.d.cts +45 -0
- package/dist/shared/errors.d.cts.map +1 -0
- package/dist/shared/errors.d.ts +45 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +93 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/ipc.cjs +14 -0
- package/dist/shared/ipc.cjs.map +1 -0
- package/dist/shared/ipc.d.cts +12 -0
- package/dist/shared/ipc.d.cts.map +1 -0
- package/dist/shared/ipc.d.ts +12 -0
- package/dist/shared/ipc.d.ts.map +1 -0
- package/dist/shared/ipc.js +13 -0
- package/dist/shared/ipc.js.map +1 -0
- package/dist/shared/normalize.cjs +19 -0
- package/dist/shared/normalize.cjs.map +1 -0
- package/dist/shared/normalize.d.cts +11 -0
- package/dist/shared/normalize.d.cts.map +1 -0
- package/dist/shared/normalize.d.ts +11 -0
- package/dist/shared/normalize.d.ts.map +1 -0
- package/dist/shared/normalize.js +18 -0
- package/dist/shared/normalize.js.map +1 -0
- package/dist/shared/pagination.cjs +32 -0
- package/dist/shared/pagination.cjs.map +1 -0
- package/dist/shared/pagination.d.cts +14 -0
- package/dist/shared/pagination.d.cts.map +1 -0
- package/dist/shared/pagination.d.ts +14 -0
- package/dist/shared/pagination.d.ts.map +1 -0
- package/dist/shared/pagination.js +28 -0
- package/dist/shared/pagination.js.map +1 -0
- package/dist/shared/stem.cjs +16 -0
- package/dist/shared/stem.cjs.map +1 -0
- package/dist/shared/stem.d.cts +6 -0
- package/dist/shared/stem.d.cts.map +1 -0
- package/dist/shared/stem.d.ts +6 -0
- package/dist/shared/stem.d.ts.map +1 -0
- package/dist/shared/stem.js +14 -0
- package/dist/shared/stem.js.map +1 -0
- package/dist/shared/types.cjs +15 -0
- package/dist/shared/types.cjs.map +1 -0
- package/dist/shared/types.d.cts +234 -0
- package/dist/shared/types.d.cts.map +1 -0
- package/dist/shared/types.d.ts +234 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +14 -0
- package/dist/shared/types.js.map +1 -0
- package/package.json +120 -0
- package/skills/authenticated-downloads/SKILL.md +203 -0
- package/skills/cache-configuration/SKILL.md +357 -0
- package/skills/cache-configuration/references/options.md +356 -0
- package/skills/getting-started/SKILL.md +407 -0
- package/skills/production-checklist/SKILL.md +397 -0
- package/skills/react-rendering/SKILL.md +424 -0
- package/skills/react-rendering/references/hooks.md +443 -0
- package/skills/store-authoring/SKILL.md +369 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { StorageLimitError, SyncFailureError, isNoSpaceError } from "../shared/errors.js";
|
|
2
|
+
import { createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { unlink } from "node:fs/promises";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { pipeline } from "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 join(this.storageRoot, "temp", sanitizeSegment(download.assetKey), sanitizeSegment(download.version), `${sanitizeSegment(download.fileName)}.part`);
|
|
34
|
+
}
|
|
35
|
+
cleanupObsoletePartialDownloads(downloads) {
|
|
36
|
+
const tempRoot = join(this.storageRoot, "temp");
|
|
37
|
+
if (!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
|
+
rmSync(filePath, { force: true });
|
|
42
|
+
pruneEmptyParents(filePath, this.storageRoot);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async downloadAsset(download, onChunk) {
|
|
46
|
+
const destinationRelativePath = join("blobs", sanitizeSegment(download.assetKey), sanitizeSegment(download.version), sanitizeSegment(download.fileName));
|
|
47
|
+
const destinationPath = join(this.storageRoot, destinationRelativePath);
|
|
48
|
+
mkdirSync(dirname(destinationPath), { recursive: true });
|
|
49
|
+
const tempPath = this.partialDownloadPath(download);
|
|
50
|
+
mkdirSync(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 (isNoSpaceError(error)) {
|
|
57
|
+
await 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 StorageLimitError(`Disk is full while downloading ${download.assetKey}.`, { cause: error });
|
|
63
|
+
}
|
|
64
|
+
if (!isRetryableDownloadError(error)) {
|
|
65
|
+
await 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 StorageLimitError(`Committing download would violate reserveFreeBytes ${reserve}.`);
|
|
91
|
+
}
|
|
92
|
+
partialDownloadBytes(download) {
|
|
93
|
+
const tempPath = this.partialDownloadPath(download);
|
|
94
|
+
return existsSync(tempPath) ? statSync(tempPath).size : 0;
|
|
95
|
+
}
|
|
96
|
+
async downloadAssetAttempt(download, destinationPath, destinationRelativePath, tempPath, onChunk) {
|
|
97
|
+
let restartedWithoutRange = false;
|
|
98
|
+
for (;;) {
|
|
99
|
+
const resumeSize = existsSync(tempPath) ? 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 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 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 = Readable.fromWeb(response.body);
|
|
140
|
+
const writeStream = createWriteStream(tempPath, { flags: resumeSize > 0 ? "a" : "w" });
|
|
141
|
+
nodeStream.on("data", (chunk) => {
|
|
142
|
+
onChunk(chunk.byteLength);
|
|
143
|
+
});
|
|
144
|
+
try {
|
|
145
|
+
await pipeline(nodeStream, writeStream);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
throw wrapRetryableDownloadError(error);
|
|
148
|
+
}
|
|
149
|
+
await this.ensureFileSpaceCommit();
|
|
150
|
+
mkdirSync(dirname(destinationPath), { recursive: true });
|
|
151
|
+
rmSync(destinationPath, { force: true });
|
|
152
|
+
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 = dirname(pathToFile);
|
|
165
|
+
while (current.startsWith(storageRoot) && current !== storageRoot) {
|
|
166
|
+
if (existsSync(current) && readdirSync(current).length === 0) {
|
|
167
|
+
rmSync(current, {
|
|
168
|
+
recursive: true,
|
|
169
|
+
force: true
|
|
170
|
+
});
|
|
171
|
+
current = dirname(current);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function listFilesRecursively(directory) {
|
|
178
|
+
if (!existsSync(directory)) return [];
|
|
179
|
+
if (statSync(directory).isFile()) return [directory];
|
|
180
|
+
return readdirSync(directory).flatMap((entry) => listFilesRecursively(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 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 SyncFailureError) return error;
|
|
214
|
+
if (error instanceof Error) {
|
|
215
|
+
const wrapped = new SyncFailureError(error.message, { cause: error });
|
|
216
|
+
wrapped.retryable = hasRetryableDownloadSignal(error);
|
|
217
|
+
return wrapped;
|
|
218
|
+
}
|
|
219
|
+
const wrapped = new 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
|
+
export { AssetDownloader, DEFAULT_RESERVE_FREE_BYTES, effectiveReserveFreeBytes };
|
|
262
|
+
|
|
263
|
+
//# sourceMappingURL=asset-download.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-download.js","names":[],"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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"}
|