@stonyx/logs 1.0.1-alpha.14 → 1.0.1-alpha.16
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.d.ts +7 -0
- package/dist/index.js +57 -15
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,12 @@ export default class Log {
|
|
|
13
13
|
options: LogOptions;
|
|
14
14
|
color: Color;
|
|
15
15
|
typeOptions: Record<string, Partial<LogOptions>>;
|
|
16
|
+
directoryCache: Map<string, Promise<void>>;
|
|
17
|
+
writeFailures: Map<string, number>;
|
|
16
18
|
[key: string]: unknown;
|
|
19
|
+
info: (content: string, logToFile?: boolean, overwrite?: boolean) => Promise<void>;
|
|
20
|
+
warn: (content: string, logToFile?: boolean, overwrite?: boolean) => Promise<void>;
|
|
21
|
+
error: (content: string, logToFile?: boolean, overwrite?: boolean) => Promise<void>;
|
|
17
22
|
constructor(options?: Partial<LogOptions>);
|
|
18
23
|
defineType(type: string, setting: ColorSetting, options?: Partial<LogOptions> | null): void;
|
|
19
24
|
createConvenienceMethod(type: string): void;
|
|
@@ -23,6 +28,8 @@ export default class Log {
|
|
|
23
28
|
log(content: string, type: string, logToFile: boolean, overwrite: boolean): Promise<void>;
|
|
24
29
|
debug(content: unknown, logToFile?: boolean, overwrite?: boolean): Promise<void>;
|
|
25
30
|
writeToFile(type: string, content: string, overwrite: boolean): Promise<void>;
|
|
31
|
+
noticeWriteResult(targetLog: string, payload: Record<string, unknown> | null): void;
|
|
32
|
+
emitNotice(severity: string, event: string, payload: Record<string, unknown>): void;
|
|
26
33
|
resolveFilename(template: string, type: string): string;
|
|
27
34
|
validateFileAndDirectory(path: string, targetLog: string): Promise<void>;
|
|
28
35
|
sanitizePath(path: string): string;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { promises as fsp } from 'fs';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { hostname } from 'os';
|
|
4
4
|
import projectPath from 'path';
|
|
@@ -23,6 +23,10 @@ export default class Log {
|
|
|
23
23
|
options;
|
|
24
24
|
color;
|
|
25
25
|
typeOptions = {};
|
|
26
|
+
// resolved directory -> in-flight or settled mkdir; instance-scoped so it cannot outlive its directory
|
|
27
|
+
directoryCache = new Map();
|
|
28
|
+
// resolved target -> consecutive write failures, used to dedupe the operator notice
|
|
29
|
+
writeFailures = new Map();
|
|
26
30
|
constructor(options = {}) {
|
|
27
31
|
const merged = {
|
|
28
32
|
...defaultOptions,
|
|
@@ -118,11 +122,46 @@ export default class Log {
|
|
|
118
122
|
async writeToFile(type, content, overwrite) {
|
|
119
123
|
const path = this.getOptionForType(type, 'path');
|
|
120
124
|
const filenameTemplate = this.getOptionForType(type, 'filename');
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
const targetLog = `${path}${this.resolveFilename(filenameTemplate, type)}`;
|
|
126
|
+
const cached = this.directoryCache.has(path);
|
|
127
|
+
const attempt = async () => {
|
|
128
|
+
await this.validateFileAndDirectory(path, targetLog);
|
|
129
|
+
await (overwrite ? fsp.writeFile : fsp.appendFile)(targetLog, content);
|
|
130
|
+
};
|
|
131
|
+
try {
|
|
132
|
+
await attempt().catch(async (error) => {
|
|
133
|
+
// a warm cache can outlive its directory, so drop the entry and retry exactly once
|
|
134
|
+
if (!['ENOENT', 'EACCES', 'EPERM', 'EROFS'].includes(error?.code))
|
|
135
|
+
throw error;
|
|
136
|
+
this.directoryCache.delete(path);
|
|
137
|
+
return attempt();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
const { code, syscall } = error;
|
|
142
|
+
this.noticeWriteResult(targetLog, { targetLog, path, syscall, code, cached });
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
this.noticeWriteResult(targetLog, null);
|
|
146
|
+
}
|
|
147
|
+
// One structured stderr notice per failure episode: an emit on the first failure, silence while
|
|
148
|
+
// it keeps failing, one recovery emit on the next success. A null payload records a success.
|
|
149
|
+
noticeWriteResult(targetLog, payload) {
|
|
150
|
+
const failures = this.writeFailures.get(targetLog) ?? 0;
|
|
151
|
+
if (payload) {
|
|
152
|
+
this.writeFailures.set(targetLog, failures + 1);
|
|
153
|
+
if (!failures)
|
|
154
|
+
this.emitNotice('error', 'log-write-failed', { ...payload, suppressedCount: 0 });
|
|
155
|
+
}
|
|
156
|
+
else if (failures) {
|
|
157
|
+
this.writeFailures.delete(targetLog);
|
|
158
|
+
this.emitNotice('warn', 'log-write-recovered', { targetLog, suppressedCount: failures - 1 });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// the write-failure path must never re-enter this logger, so notices go straight to stderr
|
|
162
|
+
emitNotice(severity, event, payload) {
|
|
163
|
+
const ts = new Date().toISOString();
|
|
164
|
+
console.error(JSON.stringify({ ts, schemaVersion: 1, surface: 'stonyx-logs', sessionKey: null, project: null, severity, event, payload }));
|
|
126
165
|
}
|
|
127
166
|
// resolves template variables in a filename string
|
|
128
167
|
resolveFilename(template, type) {
|
|
@@ -145,16 +184,19 @@ export default class Log {
|
|
|
145
184
|
// sanitize: prevent path traversal and disallow directory separators
|
|
146
185
|
return resolved.replace(/\.\./g, '').replace(/[/\\]/g, '');
|
|
147
186
|
}
|
|
148
|
-
//
|
|
187
|
+
// Ensures the target's directory exists. Both write paths auto-create the file itself, so no
|
|
188
|
+
// bootstrap write is needed. `targetLog` is unused but retained for signature compatibility:
|
|
189
|
+
// resolveFilename strips separators, so the only cached invariant is the directory.
|
|
149
190
|
async validateFileAndDirectory(path, targetLog) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
fsp.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
191
|
+
let pending = this.directoryCache.get(path);
|
|
192
|
+
if (!pending) {
|
|
193
|
+
// cache the promise before awaiting: a flag here would let a concurrent write append first
|
|
194
|
+
pending = fsp.mkdir(path, { recursive: true }).then(() => undefined);
|
|
195
|
+
this.directoryCache.set(path, pending);
|
|
196
|
+
// never keep a poisoned entry - the next write retries from scratch
|
|
197
|
+
pending.catch(() => this.directoryCache.delete(path));
|
|
198
|
+
}
|
|
199
|
+
return pending;
|
|
158
200
|
}
|
|
159
201
|
// method to conditionally sanitize user configuration input
|
|
160
202
|
sanitizePath(path) {
|