poe-code 3.0.298 → 3.0.299
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/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/packages/cached-resource/dist/api-fetch.d.ts +1 -0
- package/packages/cached-resource/dist/api-fetch.js +12 -3
- package/packages/cached-resource/dist/cache-orchestrator.js +14 -1
- package/packages/cached-resource/dist/disk-cache.js +31 -6
package/package.json
CHANGED
|
@@ -3,4 +3,5 @@ interface ApiFetchDeps {
|
|
|
3
3
|
fetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
4
4
|
}
|
|
5
5
|
export declare function fetchFromApi<T>(config: Pick<CacheConfig, "apiEndpoint" | "fetchTimeout">, deps?: Partial<ApiFetchDeps>): Promise<T>;
|
|
6
|
+
export declare function validateFetchConfig(config: Pick<CacheConfig, "apiEndpoint" | "fetchTimeout">): void;
|
|
6
7
|
export {};
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
export async function fetchFromApi(config, deps) {
|
|
2
|
-
|
|
3
|
-
throw new Error("fetchTimeout must be a finite non-negative number");
|
|
4
|
-
}
|
|
2
|
+
validateFetchConfig(config);
|
|
5
3
|
const fetchFn = deps?.fetch ?? globalThis.fetch;
|
|
6
4
|
const controller = new AbortController();
|
|
7
5
|
const timeoutId = setTimeout(() => controller.abort(), config.fetchTimeout);
|
|
@@ -24,3 +22,14 @@ export async function fetchFromApi(config, deps) {
|
|
|
24
22
|
clearTimeout(timeoutId);
|
|
25
23
|
}
|
|
26
24
|
}
|
|
25
|
+
export function validateFetchConfig(config) {
|
|
26
|
+
if (!Number.isFinite(config.fetchTimeout) || config.fetchTimeout < 0) {
|
|
27
|
+
throw new Error("fetchTimeout must be a finite non-negative number");
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
new URL(config.apiEndpoint);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw new Error("apiEndpoint must be a valid URL", { cause: error });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadFromDisk, persist } from "./disk-cache.js";
|
|
2
|
-
import { fetchFromApi } from "./api-fetch.js";
|
|
2
|
+
import { fetchFromApi, validateFetchConfig } from "./api-fetch.js";
|
|
3
3
|
export async function resolveData(bundledData, config, deps, options) {
|
|
4
4
|
if (!Number.isFinite(config.freshTtl) || config.freshTtl < 0) {
|
|
5
5
|
throw new Error("freshTtl must be a finite non-negative number");
|
|
@@ -8,6 +8,15 @@ export async function resolveData(bundledData, config, deps, options) {
|
|
|
8
8
|
if (!forceRefresh) {
|
|
9
9
|
const memoryCached = deps.memoryCache.get(config.cacheName);
|
|
10
10
|
if (memoryCached) {
|
|
11
|
+
const isStale = Date.now() - memoryCached.timestamp > config.freshTtl;
|
|
12
|
+
if (isStale && deps.revalidator && !offline && !preferOffline) {
|
|
13
|
+
deps.revalidator.trigger(config.cacheName, async () => {
|
|
14
|
+
const data = await fetchFromApi(config, { fetch: deps.fetch });
|
|
15
|
+
const cached = { data, timestamp: Date.now() };
|
|
16
|
+
deps.memoryCache.set(config.cacheName, cached);
|
|
17
|
+
await persist(data, config, { fs: deps.fs });
|
|
18
|
+
});
|
|
19
|
+
}
|
|
11
20
|
return memoryCached;
|
|
12
21
|
}
|
|
13
22
|
const diskCached = await loadFromDisk(config, { fs: deps.fs });
|
|
@@ -28,6 +37,10 @@ export async function resolveData(bundledData, config, deps, options) {
|
|
|
28
37
|
if (offline) {
|
|
29
38
|
return { data: bundledData, timestamp: 0 };
|
|
30
39
|
}
|
|
40
|
+
if (preferOffline && !forceRefresh) {
|
|
41
|
+
return { data: bundledData, timestamp: 0 };
|
|
42
|
+
}
|
|
43
|
+
validateFetchConfig(config);
|
|
31
44
|
try {
|
|
32
45
|
const data = await fetchFromApi(config, { fetch: deps.fetch });
|
|
33
46
|
const cached = { data, timestamp: Date.now() };
|
|
@@ -2,6 +2,9 @@ import { isAbsolute, join, relative, resolve } from "node:path";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
const TEMP_WRITE_MAX_ATTEMPTS = 3;
|
|
5
|
+
const CACHE_PATH_ERROR_MESSAGE = "Cache path must remain inside its configured directory.";
|
|
6
|
+
class DiskCacheConfigError extends Error {
|
|
7
|
+
}
|
|
5
8
|
export async function loadFromDisk(config, deps) {
|
|
6
9
|
if (!Number.isFinite(config.staleTtl) || config.staleTtl < 0) {
|
|
7
10
|
throw new Error("staleTtl must be a finite non-negative number");
|
|
@@ -18,7 +21,10 @@ export async function loadFromDisk(config, deps) {
|
|
|
18
21
|
}
|
|
19
22
|
return cached;
|
|
20
23
|
}
|
|
21
|
-
catch {
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error instanceof DiskCacheConfigError) {
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
22
28
|
return null;
|
|
23
29
|
}
|
|
24
30
|
}
|
|
@@ -32,7 +38,10 @@ export async function persist(data, config, deps) {
|
|
|
32
38
|
};
|
|
33
39
|
await writeCacheFile(filePath, JSON.stringify(cached), deps.fs);
|
|
34
40
|
}
|
|
35
|
-
catch {
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof DiskCacheConfigError) {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
36
45
|
// Disk cache writes are best-effort; callers can still use memory or bundled data.
|
|
37
46
|
return;
|
|
38
47
|
}
|
|
@@ -54,8 +63,15 @@ export async function removeFromDisk(config, deps) {
|
|
|
54
63
|
export function resolveCacheDir(appName, deps) {
|
|
55
64
|
const xdgCacheHome = getOwnEnvValue(deps?.env ?? process.env, "XDG_CACHE_HOME");
|
|
56
65
|
const home = deps?.homedir ? deps.homedir() : os.homedir();
|
|
57
|
-
const cacheRoot = xdgCacheHome
|
|
58
|
-
|
|
66
|
+
const cacheRoot = xdgCacheHome && xdgCacheHome.trim().length > 0
|
|
67
|
+
? xdgCacheHome
|
|
68
|
+
: join(home, ".cache");
|
|
69
|
+
if (!isAbsolute(cacheRoot)) {
|
|
70
|
+
throw new Error("XDG_CACHE_HOME must be an absolute path");
|
|
71
|
+
}
|
|
72
|
+
const normalizedAppName = appName.trim();
|
|
73
|
+
assertSafeAppName(normalizedAppName);
|
|
74
|
+
const cacheDir = join(cacheRoot, normalizedAppName);
|
|
59
75
|
assertContainedPath(cacheRoot, cacheDir);
|
|
60
76
|
return cacheDir;
|
|
61
77
|
}
|
|
@@ -68,7 +84,7 @@ async function resolveCachePath(config, fs) {
|
|
|
68
84
|
const canonicalExistingPath = await fs.realpath(existingPath);
|
|
69
85
|
const canonicalCacheDir = await fs.realpath(config.cacheDir);
|
|
70
86
|
if (canonicalCacheDir !== resolve(config.cacheDir)) {
|
|
71
|
-
throw new
|
|
87
|
+
throw new DiskCacheConfigError(CACHE_PATH_ERROR_MESSAGE);
|
|
72
88
|
}
|
|
73
89
|
assertContainedPath(canonicalCacheDir, canonicalExistingPath);
|
|
74
90
|
return cachePath;
|
|
@@ -116,7 +132,16 @@ async function writeCacheFileOnce(temporaryPath, filePath, content, fs) {
|
|
|
116
132
|
function assertContainedPath(basePath, targetPath) {
|
|
117
133
|
const relativePath = relative(resolve(basePath), resolve(targetPath));
|
|
118
134
|
if (relativePath === ".." || relativePath.startsWith(`..${pathSeparator()}`) || isAbsolute(relativePath)) {
|
|
119
|
-
throw new
|
|
135
|
+
throw new DiskCacheConfigError(CACHE_PATH_ERROR_MESSAGE);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function assertSafeAppName(appName) {
|
|
139
|
+
if (appName.length === 0 ||
|
|
140
|
+
appName === "." ||
|
|
141
|
+
appName === ".." ||
|
|
142
|
+
appName.includes("/") ||
|
|
143
|
+
appName.includes("\\")) {
|
|
144
|
+
throw new Error("appName must be a single non-empty directory name");
|
|
120
145
|
}
|
|
121
146
|
}
|
|
122
147
|
function pathSeparator() {
|