@loreai/gateway 0.14.0 → 0.14.1

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.
@@ -1,342 +0,0 @@
1
- /**
2
- * Patch Cache
3
- *
4
- * File-based cache for delta upgrade patches. Patches are downloaded
5
- * during background version checks so that `lore upgrade` can apply
6
- * them offline without any network calls.
7
- *
8
- * Cache location: <configDir>/patch-cache/
9
- * - <fromVersion>-<toVersion>.patch — raw binary patch data
10
- * - chain-<fromVersion>-<toVersion>.json — chain metadata
11
- *
12
- * Uses file-based storage to avoid bloating a DB with 50-80KB binary
13
- * blobs. Channel-agnostic — the same version-based naming works for
14
- * both nightly (GHCR) and stable (GitHub Releases) channels.
15
- *
16
- * Adapted from Sentry CLI's patch-cache.ts for Lore.
17
- */
18
-
19
- import { mkdir, readdir, unlink } from "node:fs/promises";
20
- import { join } from "node:path";
21
- import { getConfigDir } from "./binary";
22
-
23
- const PATCH_CACHE_DIR = "patch-cache";
24
-
25
- /** 7-day TTL for cached patches (milliseconds) */
26
- const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
27
-
28
- /** Maximum number of chain steps to prevent infinite loops */
29
- const MAX_CHAIN_WALK_DEPTH = 10;
30
-
31
- /** Metadata for a single patch step */
32
- export type PatchStepMeta = {
33
- fromVersion: string;
34
- toVersion: string;
35
- size: number;
36
- };
37
-
38
- /** Chain metadata stored alongside patch files */
39
- export type ChainMeta = {
40
- fromVersion: string;
41
- toVersion: string;
42
- expectedSha256: string;
43
- cachedAt: number;
44
- patches: PatchStepMeta[];
45
- };
46
-
47
- function getCacheDir(): string {
48
- return join(getConfigDir(), PATCH_CACHE_DIR);
49
- }
50
-
51
- async function ensureCacheDir(): Promise<void> {
52
- await mkdir(getCacheDir(), { recursive: true, mode: 0o700 });
53
- }
54
-
55
- /**
56
- * Sanitize version strings for safe filenames.
57
- */
58
- function sanitizeVersion(version: string): string {
59
- return version.replace(/[^a-zA-Z0-9.-]/g, "_");
60
- }
61
-
62
- /** Build the filename for a patch file given a from->to version pair. */
63
- export function patchFileName(fromVersion: string, toVersion: string): string {
64
- return `${sanitizeVersion(fromVersion)}-${sanitizeVersion(toVersion)}.patch`;
65
- }
66
-
67
- /** Build the filename for a chain metadata file. */
68
- export function chainFileName(fromVersion: string, toVersion: string): string {
69
- return `chain-${sanitizeVersion(fromVersion)}-${sanitizeVersion(toVersion)}.json`;
70
- }
71
-
72
- function isNotFound(err: unknown): boolean {
73
- return err instanceof Error && "code" in err && err.code === "ENOENT";
74
- }
75
-
76
- /**
77
- * Save a patch file and its chain metadata to the cache.
78
- */
79
- export async function savePatchesToCache(
80
- chain: {
81
- patches: { data: Uint8Array; size: number }[];
82
- expectedSha256: string;
83
- },
84
- steps: { fromVersion: string; toVersion: string }[],
85
- ): Promise<void> {
86
- await ensureCacheDir();
87
- const cacheDir = getCacheDir();
88
-
89
- // Save all patch files in parallel
90
- await Promise.all(
91
- chain.patches.flatMap((patch, i) => {
92
- const step = steps[i];
93
- if (!(step && patch)) return [];
94
- const filePath = join(
95
- cacheDir,
96
- patchFileName(step.fromVersion, step.toVersion),
97
- );
98
- return [Bun.write(filePath, patch.data)];
99
- }),
100
- );
101
-
102
- // Save chain metadata
103
- if (steps.length > 0) {
104
- const firstStep = steps.at(0);
105
- const lastStep = steps.at(-1);
106
- if (firstStep && lastStep) {
107
- const meta: ChainMeta = {
108
- fromVersion: firstStep.fromVersion,
109
- toVersion: lastStep.toVersion,
110
- expectedSha256: chain.expectedSha256,
111
- cachedAt: Date.now(),
112
- patches: steps.map((s, i) => ({
113
- fromVersion: s.fromVersion,
114
- toVersion: s.toVersion,
115
- size: chain.patches[i]?.size ?? 0,
116
- })),
117
- };
118
- const metaPath = join(
119
- cacheDir,
120
- chainFileName(firstStep.fromVersion, lastStep.toVersion),
121
- );
122
- await Bun.write(metaPath, JSON.stringify(meta));
123
- }
124
- }
125
- }
126
-
127
- /**
128
- * Load all chain metadata files from the cache directory.
129
- */
130
- async function loadAllChainMetas(cacheDir: string): Promise<ChainMeta[]> {
131
- let files: string[];
132
- try {
133
- files = await readdir(cacheDir);
134
- } catch (err) {
135
- if (isNotFound(err)) return [];
136
- throw err;
137
- }
138
-
139
- const metaFiles = files.filter(
140
- (f) => f.startsWith("chain-") && f.endsWith(".json"),
141
- );
142
-
143
- const results = await Promise.all(
144
- metaFiles.map(async (file) => {
145
- try {
146
- return (await Bun.file(join(cacheDir, file)).json()) as ChainMeta;
147
- } catch {
148
- return null;
149
- }
150
- }),
151
- );
152
-
153
- return results.filter((m): m is ChainMeta => m !== null);
154
- }
155
-
156
- /**
157
- * Build a step map from chain metadata: fromVersion -> { toVersion, size }.
158
- */
159
- function buildStepMap(
160
- chainMetas: ChainMeta[],
161
- ): Map<string, { toVersion: string; size: number }> {
162
- const stepMap = new Map<string, { toVersion: string; size: number }>();
163
- for (const meta of chainMetas) {
164
- for (const step of meta.patches) {
165
- stepMap.set(step.fromVersion, {
166
- toVersion: step.toVersion,
167
- size: step.size,
168
- });
169
- }
170
- }
171
- return stepMap;
172
- }
173
-
174
- /**
175
- * Walk from currentVersion toward targetVersion using the step map.
176
- */
177
- function walkChainSteps(
178
- stepMap: Map<string, { toVersion: string; size: number }>,
179
- currentVersion: string,
180
- targetVersion: string,
181
- ): { fromVersion: string; toVersion: string }[] | null {
182
- const steps: { fromVersion: string; toVersion: string }[] = [];
183
- let version = currentVersion;
184
- while (version !== targetVersion) {
185
- const next = stepMap.get(version);
186
- if (!next) return null;
187
- steps.push({ fromVersion: version, toVersion: next.toVersion });
188
- version = next.toVersion;
189
-
190
- if (steps.length > MAX_CHAIN_WALK_DEPTH) return null;
191
- }
192
- return steps.length > 0 ? steps : null;
193
- }
194
-
195
- /**
196
- * Try to load a complete patch chain from the cache.
197
- */
198
- export async function loadCachedChain(
199
- currentVersion: string,
200
- targetVersion: string,
201
- ): Promise<{
202
- patches: { data: Uint8Array; size: number }[];
203
- totalSize: number;
204
- expectedSha256: string;
205
- } | null> {
206
- const cacheDir = getCacheDir();
207
-
208
- const chainMetas = await loadAllChainMetas(cacheDir);
209
- if (chainMetas.length === 0) return null;
210
-
211
- const stepMap = buildStepMap(chainMetas);
212
- const steps = walkChainSteps(stepMap, currentVersion, targetVersion);
213
- if (!steps) return null;
214
-
215
- // Find the expectedSha256 from metadata
216
- let expectedSha256 = "";
217
- for (const meta of chainMetas) {
218
- if (meta.toVersion === targetVersion && meta.expectedSha256) {
219
- expectedSha256 = meta.expectedSha256;
220
- break;
221
- }
222
- }
223
- if (!expectedSha256) return null;
224
-
225
- // Load all patch files in parallel
226
- const loadResults = await Promise.all(
227
- steps.map(async (step) => {
228
- const filePath = join(
229
- cacheDir,
230
- patchFileName(step.fromVersion, step.toVersion),
231
- );
232
- try {
233
- const data = new Uint8Array(await Bun.file(filePath).arrayBuffer());
234
- return { data, size: data.byteLength };
235
- } catch (err) {
236
- if (isNotFound(err)) return null;
237
- throw err;
238
- }
239
- }),
240
- );
241
-
242
- const patches: { data: Uint8Array; size: number }[] = [];
243
- let totalSize = 0;
244
- for (const result of loadResults) {
245
- if (!result) return null;
246
- patches.push(result);
247
- totalSize += result.size;
248
- }
249
-
250
- return { patches, totalSize, expectedSha256 };
251
- }
252
-
253
- /**
254
- * Remove expired chain entries and their exclusive patch files.
255
- */
256
- async function removeExpiredEntries(
257
- cacheDir: string,
258
- files: string[],
259
- now: number,
260
- ): Promise<void> {
261
- const expiredMetas: ChainMeta[] = [];
262
- const livePatchFiles = new Set<string>();
263
-
264
- const metaResults = await Promise.all(
265
- files
266
- .filter((f) => f.startsWith("chain-") && f.endsWith(".json"))
267
- .map(async (file) => {
268
- try {
269
- const meta = (await Bun.file(
270
- join(cacheDir, file),
271
- ).json()) as ChainMeta;
272
- return { file, meta };
273
- } catch {
274
- await unlink(join(cacheDir, file)).catch(() => {});
275
- return null;
276
- }
277
- }),
278
- );
279
-
280
- for (const result of metaResults) {
281
- if (!result) continue;
282
- if (now - result.meta.cachedAt > CACHE_MAX_AGE_MS) {
283
- expiredMetas.push(result.meta);
284
- } else {
285
- for (const step of result.meta.patches) {
286
- livePatchFiles.add(patchFileName(step.fromVersion, step.toVersion));
287
- }
288
- }
289
- }
290
-
291
- const deletions: Promise<void>[] = [];
292
- for (const meta of expiredMetas) {
293
- for (const step of meta.patches) {
294
- const name = patchFileName(step.fromVersion, step.toVersion);
295
- if (!livePatchFiles.has(name)) {
296
- deletions.push(unlink(join(cacheDir, name)).catch(() => {}));
297
- }
298
- }
299
- deletions.push(
300
- unlink(
301
- join(cacheDir, chainFileName(meta.fromVersion, meta.toVersion)),
302
- ).catch(() => {}),
303
- );
304
- }
305
-
306
- await Promise.all(deletions);
307
- }
308
-
309
- /**
310
- * Remove stale cache entries older than 7 days.
311
- * Called opportunistically. Fire-and-forget.
312
- */
313
- export async function cleanupPatchCache(): Promise<void> {
314
- const cacheDir = getCacheDir();
315
- let files: string[];
316
- try {
317
- files = await readdir(cacheDir);
318
- } catch (err) {
319
- if (isNotFound(err)) return;
320
- throw err;
321
- }
322
- await removeExpiredEntries(cacheDir, files, Date.now());
323
- }
324
-
325
- /**
326
- * Remove all cached patch files and chain metadata.
327
- * Called after a successful upgrade.
328
- */
329
- export async function clearPatchCache(): Promise<void> {
330
- const cacheDir = getCacheDir();
331
- let files: string[];
332
- try {
333
- files = await readdir(cacheDir);
334
- } catch (err) {
335
- if (isNotFound(err)) return;
336
- throw err;
337
- }
338
-
339
- await Promise.all(
340
- files.map((file) => unlink(join(cacheDir, file)).catch(() => {})),
341
- );
342
- }