@stonyx/logs 1.0.1-alpha.21 → 1.0.1-alpha.23
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/README.md +21 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +56 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -163,6 +163,27 @@ async method() {
|
|
|
163
163
|
}
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
+
#### File Write Failures
|
|
167
|
+
|
|
168
|
+
When `logToFile` is true, **the returned promise rejecting is the only failure signal.** Nothing is
|
|
169
|
+
printed and no fallback log is written when the log directory or file cannot be written: the
|
|
170
|
+
underlying `fs` error is propagated to the caller with its `code` intact (`EACCES`, `EPERM`,
|
|
171
|
+
`EROFS`, ...).
|
|
172
|
+
|
|
173
|
+
A fire-and-forget call therefore produces an **unhandled promise rejection** on a failed write.
|
|
174
|
+
Always `await` the call (or attach a `.catch()`) anywhere file logging is enabled:
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
// unhandled rejection if the log directory is not writable
|
|
178
|
+
log.error('error message', true);
|
|
179
|
+
|
|
180
|
+
// handled
|
|
181
|
+
log.error('error message', true).catch(err => process.stderr.write(`log write failed: ${err.code}\n`));
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
A write that fails because the log directory was removed at runtime is retried once against a
|
|
185
|
+
freshly created directory before the rejection surfaces.
|
|
186
|
+
|
|
166
187
|
### The Debug Method
|
|
167
188
|
|
|
168
189
|
**Log** allows for the `log.debug()` method to be overridden by a color setting. However, by default we do not define a color for debug and debug is handled differently. For console logging, all **debug** does is output the following:
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ 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>>;
|
|
16
17
|
[key: string]: unknown;
|
|
17
18
|
info: (content: string, logToFile?: boolean, overwrite?: boolean) => Promise<void>;
|
|
18
19
|
warn: (content: string, logToFile?: boolean, overwrite?: boolean) => Promise<void>;
|
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';
|
|
@@ -19,10 +19,29 @@ const defaultOptions = {
|
|
|
19
19
|
};
|
|
20
20
|
// used to sanitize defineType() options input
|
|
21
21
|
const optionKeys = Object.keys(defaultOptions);
|
|
22
|
+
/*
|
|
23
|
+
* Write failures that a recursive mkdir of the log directory can actually repair:
|
|
24
|
+
* ENOENT (the cached directory was removed at runtime) and ENOTDIR (a path component
|
|
25
|
+
* was replaced by a non-directory). These invalidate the directory cache and are
|
|
26
|
+
* retried once.
|
|
27
|
+
*
|
|
28
|
+
* Permission and mount faults (EACCES, EPERM, EROFS) are deliberately excluded: the
|
|
29
|
+
* retry's only remediation is mkdir(recursive), which is a successful no-op on an
|
|
30
|
+
* existing directory and can change neither a mode nor a mount flag. Retrying them
|
|
31
|
+
* doubled the syscalls on a permanently failing write and defeated the
|
|
32
|
+
* one-mkdir-per-directory invariant this cache exists to establish.
|
|
33
|
+
*/
|
|
34
|
+
const recoverableWriteCodes = new Set(['ENOENT', 'ENOTDIR']);
|
|
22
35
|
export default class Log {
|
|
23
36
|
options;
|
|
24
37
|
color;
|
|
25
38
|
typeOptions = {};
|
|
39
|
+
/*
|
|
40
|
+
* Instance-level cache of directory creation, keyed on the resolved directory path.
|
|
41
|
+
* resolveFilename() strips directory separators, so a filename template can never
|
|
42
|
+
* introduce a new directory and date rollover cannot invalidate an entry.
|
|
43
|
+
*/
|
|
44
|
+
directoryCache = new Map();
|
|
26
45
|
constructor(options = {}) {
|
|
27
46
|
const merged = {
|
|
28
47
|
...defaultOptions,
|
|
@@ -120,9 +139,24 @@ export default class Log {
|
|
|
120
139
|
const filenameTemplate = this.getOptionForType(type, 'filename');
|
|
121
140
|
const resolvedName = this.resolveFilename(filenameTemplate, type);
|
|
122
141
|
const targetLog = `${path}${resolvedName}`;
|
|
123
|
-
await this.validateFileAndDirectory(path, targetLog);
|
|
124
142
|
const fileAction = overwrite ? fsp.writeFile : fsp.appendFile;
|
|
125
|
-
await
|
|
143
|
+
await this.validateFileAndDirectory(path, targetLog);
|
|
144
|
+
try {
|
|
145
|
+
await fileAction(targetLog, content);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const { code } = error;
|
|
149
|
+
if (!recoverableWriteCodes.has(code))
|
|
150
|
+
throw error;
|
|
151
|
+
/*
|
|
152
|
+
* The cached directory may have been removed underneath a warm cache. Invalidate
|
|
153
|
+
* the entry and retry exactly once so a cache hit can never become a permanent
|
|
154
|
+
* silent write failure. The rejection is the caller's only failure signal.
|
|
155
|
+
*/
|
|
156
|
+
this.directoryCache.delete(path);
|
|
157
|
+
await this.validateFileAndDirectory(path, targetLog);
|
|
158
|
+
await fileAction(targetLog, content);
|
|
159
|
+
}
|
|
126
160
|
}
|
|
127
161
|
// resolves template variables in a filename string
|
|
128
162
|
resolveFilename(template, type) {
|
|
@@ -145,16 +179,27 @@ export default class Log {
|
|
|
145
179
|
// sanitize: prevent path traversal and disallow directory separators
|
|
146
180
|
return resolved.replace(/\.\./g, '').replace(/[/\\]/g, '');
|
|
147
181
|
}
|
|
148
|
-
|
|
182
|
+
/*
|
|
183
|
+
* Ensures the log directory exists, deduping concurrent and repeat calls onto a single
|
|
184
|
+
* mkdir per directory. No file bootstrap happens here: both write paths already create
|
|
185
|
+
* the file (appendFile opens 'a', writeFile opens 'w'), so a bootstrap write's only
|
|
186
|
+
* reachable effect was truncating a concurrent caller's content.
|
|
187
|
+
*
|
|
188
|
+
* targetLog is unused but retained for signature compatibility.
|
|
189
|
+
*/
|
|
149
190
|
async validateFileAndDirectory(path, targetLog) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
191
|
+
const cached = this.directoryCache.get(path);
|
|
192
|
+
if (cached)
|
|
193
|
+
return cached;
|
|
194
|
+
// cache the promise before awaiting so concurrent writers dedupe and none run early
|
|
195
|
+
const pending = fsp.mkdir(path, { recursive: true }).then(() => undefined);
|
|
196
|
+
this.directoryCache.set(path, pending);
|
|
197
|
+
// never cache a poisoned promise: drop the entry so the next write retries
|
|
198
|
+
pending.catch(() => {
|
|
199
|
+
if (this.directoryCache.get(path) === pending)
|
|
200
|
+
this.directoryCache.delete(path);
|
|
157
201
|
});
|
|
202
|
+
return pending;
|
|
158
203
|
}
|
|
159
204
|
// method to conditionally sanitize user configuration input
|
|
160
205
|
sanitizePath(path) {
|