@oh-my-pi/pi-utils 17.2.9 → 17.2.10
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 +21 -0
- package/dist/types/acp/connection.d.ts +118 -0
- package/dist/types/acp/protocol.d.ts +526 -0
- package/dist/types/acp/schema.d.ts +41 -0
- package/dist/types/acp/stream.d.ts +8 -0
- package/dist/types/acp/transport.d.ts +81 -0
- package/dist/types/acp.d.ts +6 -0
- package/dist/types/browsers.d.ts +68 -0
- package/dist/types/chalk.d.ts +125 -0
- package/dist/types/dates.d.ts +7 -0
- package/dist/types/docx/converter.d.ts +46 -0
- package/dist/types/docx/xml.d.ts +26 -0
- package/dist/types/docx/zip.d.ts +6 -0
- package/dist/types/docx.d.ts +11 -0
- package/dist/types/dom/core.d.ts +431 -0
- package/dist/types/dom/parser.d.ts +7 -0
- package/dist/types/dom/selector.d.ts +5 -0
- package/dist/types/dom.d.ts +5 -0
- package/dist/types/headers.d.ts +34 -0
- package/dist/types/logger/rotating-file.d.ts +18 -0
- package/dist/types/lru.d.ts +46 -0
- package/dist/types/marked/core.d.ts +445 -0
- package/dist/types/marked.d.ts +2 -0
- package/dist/types/postmortem.d.ts +14 -0
- package/dist/types/procmgr.d.ts +16 -0
- package/dist/types/prompt.d.ts +2 -2
- package/dist/types/readability/readability.d.ts +9 -0
- package/dist/types/readability/readerable.d.ts +10 -0
- package/dist/types/readability/types.d.ts +70 -0
- package/dist/types/readability.d.ts +4 -0
- package/dist/types/template.d.ts +62 -0
- package/dist/types/turndown/gfm.d.ts +11 -0
- package/dist/types/turndown/html.d.ts +5 -0
- package/dist/types/turndown/service.d.ts +21 -0
- package/dist/types/turndown/types.d.ts +70 -0
- package/dist/types/turndown.d.ts +4 -0
- package/dist/types/vterm/buffer.d.ts +99 -0
- package/dist/types/vterm/terminal.d.ts +44 -0
- package/dist/types/vterm.d.ts +8 -0
- package/dist/types/xml.d.ts +31 -0
- package/package.json +2 -5
- package/src/acp/connection.ts +344 -0
- package/src/acp/protocol.ts +466 -0
- package/src/acp/schema.ts +160 -0
- package/src/acp/stream.ts +82 -0
- package/src/acp/transport.ts +213 -0
- package/src/acp.ts +6 -0
- package/src/browsers.ts +501 -0
- package/src/chalk.ts +312 -0
- package/src/dates.ts +194 -0
- package/src/docx/converter.ts +681 -0
- package/src/docx/xml.ts +166 -0
- package/src/docx/zip.ts +87 -0
- package/src/docx.ts +20 -0
- package/src/dom/core.ts +1254 -0
- package/src/dom/parser.ts +370 -0
- package/src/dom/selector.ts +290 -0
- package/src/dom.ts +33 -0
- package/src/headers.ts +167 -0
- package/src/logger/rotating-file.ts +149 -0
- package/src/logger.ts +12 -28
- package/src/lru.ts +185 -0
- package/src/marked/core.ts +1576 -0
- package/src/marked.ts +2 -0
- package/src/postmortem.ts +44 -1
- package/src/procmgr.ts +21 -4
- package/src/prompt.ts +5 -22
- package/src/readability/readability.ts +533 -0
- package/src/readability/readerable.ts +51 -0
- package/src/readability/types.ts +72 -0
- package/src/readability.ts +11 -0
- package/src/template.ts +586 -0
- package/src/turndown/gfm.ts +106 -0
- package/src/turndown/html.ts +257 -0
- package/src/turndown/service.ts +334 -0
- package/src/turndown/types.ts +81 -0
- package/src/turndown.ts +5 -0
- package/src/vterm/buffer.ts +218 -0
- package/src/vterm/terminal.ts +773 -0
- package/src/vterm.ts +8 -0
- package/src/xml.ts +313 -0
- package/src/winston-daily-rotate-file.d.ts +0 -6
package/src/headers.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/** Behavior-compatible reimplementation of header-generator's used surface. */
|
|
2
|
+
|
|
3
|
+
/** A browser family supported by the curated header profiles. */
|
|
4
|
+
export type BrowserName = "chrome" | "firefox" | "safari";
|
|
5
|
+
|
|
6
|
+
/** A desktop operating system supported by the curated header profiles. */
|
|
7
|
+
export type OperatingSystem = "windows" | "macos" | "linux";
|
|
8
|
+
|
|
9
|
+
/** Constructor and per-call constraints for header generation. */
|
|
10
|
+
export interface HeaderGeneratorOptions {
|
|
11
|
+
/** Browser families eligible for a draw. */
|
|
12
|
+
browsers: BrowserName[];
|
|
13
|
+
/** Browser selection query; the supported `last 3 versions` query uses the curated versions. */
|
|
14
|
+
browserListQuery: string;
|
|
15
|
+
/** Desktop operating systems eligible for a draw. */
|
|
16
|
+
operatingSystems: OperatingSystem[];
|
|
17
|
+
/** Device classes eligible for a draw. */
|
|
18
|
+
devices: "desktop"[];
|
|
19
|
+
/** Ordered locales for the Accept-Language value. */
|
|
20
|
+
locales: string[];
|
|
21
|
+
/** HTTP protocol generation mode. */
|
|
22
|
+
httpVersion: "1" | "2";
|
|
23
|
+
/** Whether impossible constraints throw instead of relaxing to a coherent profile. */
|
|
24
|
+
strict: boolean;
|
|
25
|
+
/** Random source returning a value in the range from zero (inclusive) to one (exclusive). */
|
|
26
|
+
rng: () => number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A generated HTTP request header map. */
|
|
30
|
+
export type Headers = Record<string, string>;
|
|
31
|
+
|
|
32
|
+
type ResolvedOptions = Omit<HeaderGeneratorOptions, "rng">;
|
|
33
|
+
|
|
34
|
+
type BrowserProfile = {
|
|
35
|
+
browser: BrowserName;
|
|
36
|
+
operatingSystem: OperatingSystem;
|
|
37
|
+
version: number;
|
|
38
|
+
userAgent: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const DEFAULT_OPTIONS: ResolvedOptions = {
|
|
42
|
+
browsers: ["chrome", "firefox", "safari"],
|
|
43
|
+
browserListQuery: "",
|
|
44
|
+
operatingSystems: ["windows", "macos", "linux"],
|
|
45
|
+
devices: ["desktop"],
|
|
46
|
+
locales: ["en-US", "en"],
|
|
47
|
+
httpVersion: "2",
|
|
48
|
+
strict: false,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const VERSIONS: Readonly<Record<BrowserName, readonly number[]>> = {
|
|
52
|
+
chrome: [149, 150, 151],
|
|
53
|
+
firefox: [147, 148, 149],
|
|
54
|
+
safari: [26, 26.1, 26.2],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function pick<T>(values: readonly T[], rng: () => number): T {
|
|
58
|
+
const random = rng();
|
|
59
|
+
const index = Math.min(values.length - 1, Math.max(0, Math.floor(random * values.length)));
|
|
60
|
+
const value = values[index];
|
|
61
|
+
if (value === undefined) throw new Error("Cannot choose from an empty header profile list");
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatLocales(locales: readonly string[]): string {
|
|
66
|
+
return locales
|
|
67
|
+
.slice(0, 10)
|
|
68
|
+
.map((locale, index) => {
|
|
69
|
+
if (index === 0) return locale;
|
|
70
|
+
const quality = Math.max(0.1, 1 - index / 10).toFixed(1);
|
|
71
|
+
return `${locale};q=${quality}`;
|
|
72
|
+
})
|
|
73
|
+
.join(",");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function makeUserAgent(browser: BrowserName, operatingSystem: OperatingSystem, version: number): string {
|
|
77
|
+
if (browser === "safari") {
|
|
78
|
+
return `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/${version.toFixed(1)} Safari/605.1.15`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const platform =
|
|
82
|
+
operatingSystem === "windows"
|
|
83
|
+
? "Windows NT 10.0; Win64; x64"
|
|
84
|
+
: operatingSystem === "macos"
|
|
85
|
+
? "Macintosh; Intel Mac OS X 10_15_7"
|
|
86
|
+
: "X11; Linux x86_64";
|
|
87
|
+
if (browser === "firefox") {
|
|
88
|
+
return `Mozilla/5.0 (${platform}; rv:${version}.0) Gecko/20100101 Firefox/${version}.0`;
|
|
89
|
+
}
|
|
90
|
+
return `Mozilla/5.0 (${platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version}.0.0.0 Safari/537.36`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function makeProfile(options: ResolvedOptions, rng: () => number): BrowserProfile {
|
|
94
|
+
const candidates: Array<{ browser: BrowserName; operatingSystem: OperatingSystem }> = [];
|
|
95
|
+
for (const browser of options.browsers) {
|
|
96
|
+
for (const operatingSystem of options.operatingSystems) {
|
|
97
|
+
if (browser === "safari" && operatingSystem !== "macos") continue;
|
|
98
|
+
candidates.push({ browser, operatingSystem });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (candidates.length === 0) {
|
|
103
|
+
if (options.strict) throw new Error("No coherent browser profile matches the requested options");
|
|
104
|
+
candidates.push({ browser: "chrome", operatingSystem: "windows" });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const candidate = pick(candidates, rng);
|
|
108
|
+
const version = pick(VERSIONS[candidate.browser], rng);
|
|
109
|
+
return {
|
|
110
|
+
...candidate,
|
|
111
|
+
version,
|
|
112
|
+
userAgent: makeUserAgent(candidate.browser, candidate.operatingSystem, version),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Generates coherent modern desktop browser navigation headers. */
|
|
117
|
+
export class HeaderGenerator {
|
|
118
|
+
#options: ResolvedOptions;
|
|
119
|
+
#rng: () => number;
|
|
120
|
+
|
|
121
|
+
/** Creates a generator with reusable constraints and an optionally injectable RNG. */
|
|
122
|
+
constructor(options: Partial<HeaderGeneratorOptions> = {}) {
|
|
123
|
+
this.#rng = options.rng ?? Math.random;
|
|
124
|
+
this.#options = { ...DEFAULT_OPTIONS, ...options };
|
|
125
|
+
if (this.#options.devices.some(device => device !== "desktop") && this.#options.strict) {
|
|
126
|
+
throw new Error("Only desktop browser profiles are available");
|
|
127
|
+
}
|
|
128
|
+
if (this.#options.locales.length === 0 && this.#options.strict) {
|
|
129
|
+
throw new Error("At least one locale is required");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Generates one header set, applying per-call constraints and request overrides. */
|
|
134
|
+
getHeaders(options: Partial<HeaderGeneratorOptions> = {}, overrides: Headers = {}): Headers {
|
|
135
|
+
const resolved = { ...this.#options, ...options };
|
|
136
|
+
const rng = options.rng ?? this.#rng;
|
|
137
|
+
const profile = makeProfile(resolved, rng);
|
|
138
|
+
const headers: Headers = {
|
|
139
|
+
accept:
|
|
140
|
+
profile.browser === "chrome"
|
|
141
|
+
? "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
|
|
142
|
+
: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
143
|
+
"user-agent": profile.userAgent,
|
|
144
|
+
"accept-encoding": profile.browser === "safari" ? "gzip, deflate, br" : "gzip, deflate, br, zstd",
|
|
145
|
+
"accept-language": formatLocales(resolved.locales.length > 0 ? resolved.locales : DEFAULT_OPTIONS.locales),
|
|
146
|
+
"upgrade-insecure-requests": "1",
|
|
147
|
+
"sec-fetch-dest": "document",
|
|
148
|
+
"sec-fetch-mode": "navigate",
|
|
149
|
+
"sec-fetch-site": "none",
|
|
150
|
+
"sec-fetch-user": "?1",
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (profile.browser === "chrome") {
|
|
154
|
+
headers["sec-ch-ua"] =
|
|
155
|
+
`"Google Chrome";v="${profile.version}", "Chromium";v="${profile.version}", "Not_A Brand";v="24"`;
|
|
156
|
+
headers["sec-ch-ua-mobile"] = "?0";
|
|
157
|
+
headers["sec-ch-ua-platform"] =
|
|
158
|
+
profile.operatingSystem === "windows"
|
|
159
|
+
? '"Windows"'
|
|
160
|
+
: profile.operatingSystem === "macos"
|
|
161
|
+
? '"macOS"'
|
|
162
|
+
: '"Linux"';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { ...headers, ...overrides };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** Behavior-compatible reimplementation of winston-daily-rotate-file's used surface. */
|
|
2
|
+
import * as crypto from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
|
|
7
|
+
interface AuditEntry {
|
|
8
|
+
readonly date: number;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly hash: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface AuditState {
|
|
14
|
+
readonly keep: { readonly days: false; readonly amount: number };
|
|
15
|
+
readonly auditLog: string;
|
|
16
|
+
readonly files: AuditEntry[];
|
|
17
|
+
readonly hashType: "sha256";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Configuration for a process-local rotating file sink. */
|
|
21
|
+
export interface RotatingFileOptions {
|
|
22
|
+
readonly directory: string;
|
|
23
|
+
readonly filenamePrefix: string;
|
|
24
|
+
readonly filenameSuffix: string;
|
|
25
|
+
readonly auditFile: string;
|
|
26
|
+
readonly maxBytes: number;
|
|
27
|
+
readonly maxFiles: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isAuditEntry(value: unknown): value is AuditEntry {
|
|
31
|
+
if (value === null || typeof value !== "object") return false;
|
|
32
|
+
const entry = value as Record<string, unknown>;
|
|
33
|
+
return typeof entry.date === "number" && typeof entry.name === "string" && typeof entry.hash === "string";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Synchronous append sink with local-day and size rotation plus bounded retention. */
|
|
37
|
+
export class RotatingFileSink {
|
|
38
|
+
readonly #directory: string;
|
|
39
|
+
readonly #filenamePrefix: string;
|
|
40
|
+
readonly #filenameSuffix: string;
|
|
41
|
+
readonly #auditFile: string;
|
|
42
|
+
readonly #maxBytes: number;
|
|
43
|
+
readonly #maxFiles: number;
|
|
44
|
+
#files: AuditEntry[];
|
|
45
|
+
#activeDay: string | undefined;
|
|
46
|
+
#activeIndex = 0;
|
|
47
|
+
#activePath: string | undefined;
|
|
48
|
+
#activeBytes = 0;
|
|
49
|
+
#closed = false;
|
|
50
|
+
|
|
51
|
+
constructor(options: RotatingFileOptions) {
|
|
52
|
+
this.#directory = options.directory;
|
|
53
|
+
this.#filenamePrefix = options.filenamePrefix;
|
|
54
|
+
this.#filenameSuffix = options.filenameSuffix;
|
|
55
|
+
this.#auditFile = options.auditFile;
|
|
56
|
+
this.#maxBytes = options.maxBytes;
|
|
57
|
+
this.#maxFiles = options.maxFiles;
|
|
58
|
+
this.#files = this.#readAudit();
|
|
59
|
+
const now = new Date();
|
|
60
|
+
this.#selectFile(this.#localDay(now));
|
|
61
|
+
const activePath = this.#activePath;
|
|
62
|
+
if (activePath) {
|
|
63
|
+
this.#registerFile(activePath, now.getTime());
|
|
64
|
+
fs.closeSync(fs.openSync(activePath, "a"));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Append one already-formatted log record. */
|
|
69
|
+
write(line: string): void {
|
|
70
|
+
if (this.#closed) return;
|
|
71
|
+
const now = new Date();
|
|
72
|
+
this.#selectFile(this.#localDay(now));
|
|
73
|
+
const activePath = this.#activePath;
|
|
74
|
+
if (!activePath) return;
|
|
75
|
+
this.#registerFile(activePath, now.getTime());
|
|
76
|
+
const record = `${line}${os.EOL}`;
|
|
77
|
+
fs.appendFileSync(activePath, record, "utf8");
|
|
78
|
+
this.#activeBytes += Buffer.byteLength(record);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Stop accepting records. Synchronous writes require no drain phase. */
|
|
82
|
+
close(): void {
|
|
83
|
+
this.#closed = true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#localDay(date: Date): string {
|
|
87
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#selectFile(day: string): void {
|
|
91
|
+
if (day !== this.#activeDay) {
|
|
92
|
+
this.#activeDay = day;
|
|
93
|
+
this.#activeIndex = 0;
|
|
94
|
+
this.#setActivePath(day, 0);
|
|
95
|
+
}
|
|
96
|
+
while (this.#activeBytes > this.#maxBytes) {
|
|
97
|
+
this.#activeIndex++;
|
|
98
|
+
this.#setActivePath(day, this.#activeIndex);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#setActivePath(day: string, index: number): void {
|
|
103
|
+
const suffix = index === 0 ? "" : `.${index}`;
|
|
104
|
+
this.#activePath = path.join(
|
|
105
|
+
this.#directory,
|
|
106
|
+
`${this.#filenamePrefix}.${day}.${this.#filenameSuffix}.log${suffix}`,
|
|
107
|
+
);
|
|
108
|
+
try {
|
|
109
|
+
this.#activeBytes = fs.statSync(this.#activePath).size;
|
|
110
|
+
} catch {
|
|
111
|
+
this.#activeBytes = 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#registerFile(filePath: string, date: number): void {
|
|
116
|
+
if (this.#files.some(file => file.name === filePath)) return;
|
|
117
|
+
const hash = crypto.createHash("sha256").update(`${filePath}LOG_FILE${date}`).digest("hex");
|
|
118
|
+
this.#files.push({ date, name: filePath, hash });
|
|
119
|
+
while (this.#files.length > this.#maxFiles) {
|
|
120
|
+
const removed = this.#files.shift();
|
|
121
|
+
if (!removed) break;
|
|
122
|
+
try {
|
|
123
|
+
fs.rmSync(removed.name, { force: true });
|
|
124
|
+
} catch {
|
|
125
|
+
// Retention is best-effort; the current record must still be written.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
this.#writeAudit();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#readAudit(): AuditEntry[] {
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(fs.readFileSync(this.#auditFile, "utf8")) as { files?: unknown };
|
|
134
|
+
return Array.isArray(parsed.files) ? parsed.files.filter(isAuditEntry) : [];
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#writeAudit(): void {
|
|
141
|
+
const state: AuditState = {
|
|
142
|
+
keep: { days: false, amount: this.#maxFiles },
|
|
143
|
+
auditLog: this.#auditFile,
|
|
144
|
+
files: this.#files,
|
|
145
|
+
hashType: "sha256",
|
|
146
|
+
};
|
|
147
|
+
fs.writeFileSync(this.#auditFile, JSON.stringify(state, undefined, 4), "utf8");
|
|
148
|
+
}
|
|
149
|
+
}
|
package/src/logger.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/// <reference path="./winston-daily-rotate-file.d.ts" />
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Centralized logger for omp.
|
|
5
3
|
*
|
|
@@ -16,11 +14,8 @@ import * as fs from "node:fs";
|
|
|
16
14
|
import * as os from "node:os";
|
|
17
15
|
import * as path from "node:path";
|
|
18
16
|
import { isPromise } from "node:util/types";
|
|
19
|
-
import type DailyRotateFile from "winston-daily-rotate-file";
|
|
20
|
-
// Import the implementation directly because the package index imports and mutates
|
|
21
|
-
// Winston. The exact workspace catalog pin protects this internal entrypoint.
|
|
22
|
-
import DailyRotateFileImplementation from "winston-daily-rotate-file/daily-rotate-file.js";
|
|
23
17
|
import { getLogsDir } from "./dirs";
|
|
18
|
+
import { RotatingFileSink } from "./logger/rotating-file";
|
|
24
19
|
import { drainModuleLoadEvents } from "./timing-buffer";
|
|
25
20
|
/** Severity names accepted by the centralized logger. */
|
|
26
21
|
export type LogLevel = "error" | "warn" | "info" | "debug";
|
|
@@ -200,24 +195,18 @@ function formatLogInfo(info: NormalizedLogInfo): string {
|
|
|
200
195
|
return JSON.stringify(entry, jsonReplacer) as string;
|
|
201
196
|
}
|
|
202
197
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
close(): void;
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
/** Build a rotating file transport with process-local rotation and shared retention. */
|
|
209
|
-
function makeFileTransport(dir?: string): FileTransport {
|
|
198
|
+
/** Build a rotating file sink with process-local rotation and shared retention. */
|
|
199
|
+
function makeFileTransport(dir?: string): RotatingFileSink {
|
|
210
200
|
const logsDir = ensureDir(dir ?? getLogsDir());
|
|
211
201
|
pruneStaleProcessLogs(logsDir);
|
|
212
|
-
return new
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
202
|
+
return new RotatingFileSink({
|
|
203
|
+
directory: logsDir,
|
|
204
|
+
filenamePrefix: "omp",
|
|
205
|
+
filenameSuffix: String(process.pid),
|
|
206
|
+
maxBytes: 10 * 1024 * 1024,
|
|
217
207
|
maxFiles: 5,
|
|
218
|
-
zippedArchive: false,
|
|
219
208
|
auditFile: path.join(logsDir, `.omp.${process.pid}-audit.json`),
|
|
220
|
-
})
|
|
209
|
+
});
|
|
221
210
|
}
|
|
222
211
|
|
|
223
212
|
/**
|
|
@@ -227,16 +216,13 @@ function makeFileTransport(dir?: string): FileTransport {
|
|
|
227
216
|
let transportOpts: { console?: boolean; file?: boolean | string } = { file: true };
|
|
228
217
|
|
|
229
218
|
interface LocalTransports {
|
|
230
|
-
readonly file:
|
|
219
|
+
readonly file: RotatingFileSink | undefined;
|
|
231
220
|
readonly console: boolean;
|
|
232
221
|
}
|
|
233
222
|
|
|
234
223
|
/** Local transports, constructed lazily on first log emission. */
|
|
235
224
|
let activeTransports: LocalTransports | undefined;
|
|
236
225
|
|
|
237
|
-
const TRANSPORT_MESSAGE = Symbol.for("message");
|
|
238
|
-
const onTransportLogged = (): void => {};
|
|
239
|
-
|
|
240
226
|
function buildTransports(opts: { console?: boolean; file?: boolean | string }): LocalTransports {
|
|
241
227
|
return {
|
|
242
228
|
file: opts.file ? makeFileTransport(typeof opts.file === "string" ? opts.file : undefined) : undefined,
|
|
@@ -254,11 +240,9 @@ function emitLocally(level: LogLevel, message: string, context: Record<string, u
|
|
|
254
240
|
const info = normalizeLogInfo(level, message, context);
|
|
255
241
|
if (!transports.file && !transports.console) return;
|
|
256
242
|
|
|
257
|
-
// Winston applied the shared format before dispatch, then the same format a
|
|
258
|
-
// second time inside Console. Keep those evaluation and timestamp semantics.
|
|
259
243
|
const line = formatLogInfo(info);
|
|
260
|
-
if (transports.file) transports.file.
|
|
261
|
-
if (transports.console)
|
|
244
|
+
if (transports.file) transports.file.write(line);
|
|
245
|
+
if (transports.console) fs.writeSync(1, `${formatLogInfo(info)}${os.EOL}`);
|
|
262
246
|
}
|
|
263
247
|
|
|
264
248
|
/**
|
package/src/lru.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/** Behavior-compatible reimplementation of lru-cache's used surface. */
|
|
2
|
+
|
|
3
|
+
/** Why an entry left the cache. */
|
|
4
|
+
export type DisposeReason = "evict" | "set" | "delete" | "expire";
|
|
5
|
+
|
|
6
|
+
/** Options supported by {@link LRUCache}. */
|
|
7
|
+
export interface LRUCacheOptions<K, V> {
|
|
8
|
+
/** Maximum number of retained entries. */
|
|
9
|
+
max?: number;
|
|
10
|
+
/** Maximum aggregate calculated size. */
|
|
11
|
+
maxSize?: number;
|
|
12
|
+
/** Maximum calculated size of one entry. */
|
|
13
|
+
maxEntrySize?: number;
|
|
14
|
+
/** Calculates an entry's size. */
|
|
15
|
+
sizeCalculation?: (value: V, key: K) => number;
|
|
16
|
+
/** Entry lifetime in milliseconds; zero disables expiry. */
|
|
17
|
+
ttl?: number;
|
|
18
|
+
/** Refreshes an entry's lifetime when it is read. */
|
|
19
|
+
updateAgeOnGet?: boolean;
|
|
20
|
+
/** Called synchronously before an entry is removed. */
|
|
21
|
+
dispose?: (value: V, key: K, reason: DisposeReason) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Entry<V> {
|
|
25
|
+
value: V;
|
|
26
|
+
size: number;
|
|
27
|
+
start: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function positiveInteger(value: number | undefined, name: string): number {
|
|
31
|
+
if (value === undefined) return 0;
|
|
32
|
+
if (!Number.isInteger(value) || value < 0) throw new TypeError(`${name} must be a non-negative integer`);
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A bounded least-recently-used cache with optional size and lifetime limits. */
|
|
37
|
+
export class LRUCache<K, V> {
|
|
38
|
+
readonly #entries = new Map<K, Entry<V>>();
|
|
39
|
+
readonly #max: number;
|
|
40
|
+
readonly #maxSize: number;
|
|
41
|
+
readonly #maxEntrySize: number;
|
|
42
|
+
readonly #sizeCalculation: ((value: V, key: K) => number) | undefined;
|
|
43
|
+
readonly #ttl: number;
|
|
44
|
+
readonly #updateAgeOnGet: boolean;
|
|
45
|
+
readonly #dispose: ((value: V, key: K, reason: DisposeReason) => void) | undefined;
|
|
46
|
+
#calculatedSize = 0;
|
|
47
|
+
|
|
48
|
+
/** Creates an empty cache. */
|
|
49
|
+
constructor(options: LRUCacheOptions<K, V>) {
|
|
50
|
+
this.#max = positiveInteger(options.max, "max");
|
|
51
|
+
this.#maxSize = positiveInteger(options.maxSize, "maxSize");
|
|
52
|
+
const explicitMaxEntrySize = positiveInteger(options.maxEntrySize, "maxEntrySize");
|
|
53
|
+
this.#maxEntrySize = explicitMaxEntrySize || this.#maxSize;
|
|
54
|
+
this.#ttl = positiveInteger(options.ttl, "ttl");
|
|
55
|
+
if (this.#max === 0 && this.#maxSize === 0 && this.#ttl === 0) {
|
|
56
|
+
throw new TypeError("At least one of max, maxSize, or ttl is required");
|
|
57
|
+
}
|
|
58
|
+
if ((this.#maxSize !== 0 || this.#maxEntrySize !== 0) && options.sizeCalculation === undefined) {
|
|
59
|
+
throw new TypeError("sizeCalculation is required when a size limit is set");
|
|
60
|
+
}
|
|
61
|
+
this.#sizeCalculation = options.sizeCalculation;
|
|
62
|
+
this.#updateAgeOnGet = options.updateAgeOnGet === true;
|
|
63
|
+
this.#dispose = options.dispose;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Number of entries, including stale entries not yet removed by `get`. */
|
|
67
|
+
get size(): number {
|
|
68
|
+
return this.#entries.size;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Aggregate calculated size of retained entries. */
|
|
72
|
+
get calculatedSize(): number {
|
|
73
|
+
return this.#calculatedSize;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Stores a value and makes it most recently used. */
|
|
77
|
+
set(key: K, value: V | undefined): this {
|
|
78
|
+
if (value === undefined) {
|
|
79
|
+
this.delete(key);
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const size = this.#entrySize(value, key);
|
|
84
|
+
const previous = this.#entries.get(key);
|
|
85
|
+
if (this.#maxEntrySize !== 0 && size > this.#maxEntrySize) {
|
|
86
|
+
if (previous !== undefined) this.#remove(key, previous, "set");
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (previous !== undefined) {
|
|
91
|
+
if (previous.value !== value) this.#dispose?.(previous.value, key, "set");
|
|
92
|
+
this.#calculatedSize -= previous.size;
|
|
93
|
+
this.#entries.delete(key);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
while (
|
|
97
|
+
(this.#max !== 0 && this.#entries.size >= this.#max) ||
|
|
98
|
+
(this.#maxSize !== 0 && this.#calculatedSize + size > this.#maxSize)
|
|
99
|
+
) {
|
|
100
|
+
const oldest = this.#entries.entries().next().value as [K, Entry<V>] | undefined;
|
|
101
|
+
if (oldest === undefined) break;
|
|
102
|
+
this.#remove(oldest[0], oldest[1], "evict");
|
|
103
|
+
}
|
|
104
|
+
this.#entries.set(key, { value, size, start: this.#ttl === 0 ? 0 : performance.now() });
|
|
105
|
+
this.#calculatedSize += size;
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Returns a value and makes a fresh entry most recently used. */
|
|
110
|
+
get(key: K): V | undefined {
|
|
111
|
+
const entry = this.#entries.get(key);
|
|
112
|
+
if (entry === undefined) return undefined;
|
|
113
|
+
if (this.#isStale(entry)) {
|
|
114
|
+
this.#remove(key, entry, "expire");
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
if (this.#updateAgeOnGet && this.#ttl !== 0) entry.start = performance.now();
|
|
118
|
+
this.#entries.delete(key);
|
|
119
|
+
this.#entries.set(key, entry);
|
|
120
|
+
return entry.value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Reports whether a fresh value is present without changing recency. */
|
|
124
|
+
has(key: K): boolean {
|
|
125
|
+
const entry = this.#entries.get(key);
|
|
126
|
+
return entry !== undefined && !this.#isStale(entry);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Returns a fresh value without changing recency or removing stale data. */
|
|
130
|
+
peek(key: K): V | undefined {
|
|
131
|
+
const entry = this.#entries.get(key);
|
|
132
|
+
return entry === undefined || this.#isStale(entry) ? undefined : entry.value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Removes a value, returning whether one was present. */
|
|
136
|
+
delete(key: K): boolean {
|
|
137
|
+
const entry = this.#entries.get(key);
|
|
138
|
+
if (entry === undefined) return false;
|
|
139
|
+
this.#remove(key, entry, "delete");
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Removes every value from least to most recently used. */
|
|
144
|
+
clear(): void {
|
|
145
|
+
for (const [key, entry] of this.#entries) this.#dispose?.(entry.value, key, "delete");
|
|
146
|
+
this.#entries.clear();
|
|
147
|
+
this.#calculatedSize = 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Iterates fresh keys from most to least recently used. */
|
|
151
|
+
*keys(): Generator<K, void, unknown> {
|
|
152
|
+
const entries = [...this.#entries.entries()];
|
|
153
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
154
|
+
const [key, entry] = entries[index]!;
|
|
155
|
+
if (!this.#isStale(entry)) yield key;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Iterates fresh values from most to least recently used. */
|
|
160
|
+
*values(): Generator<V, void, unknown> {
|
|
161
|
+
const entries = [...this.#entries.values()];
|
|
162
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
163
|
+
const entry = entries[index]!;
|
|
164
|
+
if (!this.#isStale(entry)) yield entry.value;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#entrySize(value: V, key: K): number {
|
|
169
|
+
if (this.#sizeCalculation === undefined) return 0;
|
|
170
|
+
const size = this.#sizeCalculation(value, key);
|
|
171
|
+
if (!Number.isInteger(size) || size <= 0)
|
|
172
|
+
throw new TypeError("sizeCalculation return invalid (expect positive integer)");
|
|
173
|
+
return size;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#isStale(entry: Entry<V>): boolean {
|
|
177
|
+
return this.#ttl !== 0 && performance.now() - entry.start > this.#ttl;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#remove(key: K, entry: Entry<V>, reason: DisposeReason): void {
|
|
181
|
+
this.#dispose?.(entry.value, key, reason);
|
|
182
|
+
this.#entries.delete(key);
|
|
183
|
+
this.#calculatedSize -= entry.size;
|
|
184
|
+
}
|
|
185
|
+
}
|