pi-background-tasks 0.2.0 → 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/PUBLISHING.md +6 -6
- package/README.md +30 -12
- package/TESTING.md +21 -7
- package/TEST_PLAN.md +36 -28
- package/package.json +6 -3
- package/src/core/common.ts +461 -16
- package/src/core/registry.ts +958 -0
- package/src/core/update-check.ts +99 -0
- package/src/extension.ts +231 -909
- package/src/ui/background-tasks-manager.ts +71 -14
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Typed, offline-safe boundary for the "update available" footer notice.
|
|
5
|
+
*
|
|
6
|
+
* All network access flows through an injectable {@link FetchLike} so unit tests
|
|
7
|
+
* never touch the real npm registry. Every failure path (offline, timeout, bad
|
|
8
|
+
* status, malformed payload) resolves to `undefined` and never throws, so the
|
|
9
|
+
* footer/session can never hang or error because of an update check.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
13
|
+
export const DEFAULT_UPDATE_TIMEOUT_MS = 2000;
|
|
14
|
+
|
|
15
|
+
export type FetchResponseLike = {
|
|
16
|
+
readonly ok: boolean;
|
|
17
|
+
readonly status: number;
|
|
18
|
+
json(): Promise<unknown>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type FetchLike = (url: string, init: { signal: AbortSignal }) => Promise<FetchResponseLike>;
|
|
22
|
+
|
|
23
|
+
export type FetchLatestVersionOptions = {
|
|
24
|
+
packageName: string;
|
|
25
|
+
registryUrl?: string;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
fetchImpl?: FetchLike;
|
|
28
|
+
onError?: (error: Error) => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type PackageInfo = {
|
|
32
|
+
name?: string;
|
|
33
|
+
version?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
37
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
38
|
+
return value as Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readNonEmptyString(record: Record<string, unknown>, key: string): string | undefined {
|
|
42
|
+
const value = record[key];
|
|
43
|
+
if (typeof value !== "string") return undefined;
|
|
44
|
+
const trimmed = value.trim();
|
|
45
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Narrow an npm registry `<pkg>/latest` JSON payload to its `version` string. */
|
|
49
|
+
export function parseLatestVersionPayload(payload: unknown): string | undefined {
|
|
50
|
+
const record = asRecord(payload);
|
|
51
|
+
if (!record) return undefined;
|
|
52
|
+
return readNonEmptyString(record, "version");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Narrow a parsed `package.json` payload to the fields this extension needs. */
|
|
56
|
+
export function parsePackageInfo(payload: unknown): PackageInfo {
|
|
57
|
+
const record = asRecord(payload);
|
|
58
|
+
if (!record) return {};
|
|
59
|
+
const info: PackageInfo = {};
|
|
60
|
+
const name = readNonEmptyString(record, "name");
|
|
61
|
+
const version = readNonEmptyString(record, "version");
|
|
62
|
+
if (name !== undefined) info.name = name;
|
|
63
|
+
if (version !== undefined) info.version = version;
|
|
64
|
+
return info;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const realFetch: FetchLike = (url, init) => fetch(url, init);
|
|
68
|
+
|
|
69
|
+
/** Time-boxed, never-throwing lookup of the latest published version of a package. */
|
|
70
|
+
export async function fetchLatestVersion(options: FetchLatestVersionOptions): Promise<string | undefined> {
|
|
71
|
+
const registryUrl = (options.registryUrl ?? DEFAULT_NPM_REGISTRY_URL).replace(/\/+$/, "");
|
|
72
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_UPDATE_TIMEOUT_MS;
|
|
73
|
+
const doFetch = options.fetchImpl ?? realFetch;
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
76
|
+
try {
|
|
77
|
+
const url = `${registryUrl}/${encodeURIComponent(options.packageName)}/latest`;
|
|
78
|
+
const response = await doFetch(url, { signal: controller.signal });
|
|
79
|
+
if (!response.ok) return undefined;
|
|
80
|
+
const payload = await response.json();
|
|
81
|
+
return parseLatestVersionPayload(payload);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
84
|
+
return undefined;
|
|
85
|
+
} finally {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Read `name`/`version` from a local `package.json`, returning `{}` on every failure. */
|
|
91
|
+
export function readPackageInfo(packageJsonUrl: URL | string, onError?: (error: Error) => void): PackageInfo {
|
|
92
|
+
try {
|
|
93
|
+
const parsed: unknown = JSON.parse(readFileSync(packageJsonUrl, "utf8"));
|
|
94
|
+
return parsePackageInfo(parsed);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
}
|