@stonyx/logs 1.0.1-alpha.21 → 1.0.1-alpha.22

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 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 { mkdirSync, promises as fsp } from 'fs';
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,21 @@ const defaultOptions = {
19
19
  };
20
20
  // used to sanitize defineType() options input
21
21
  const optionKeys = Object.keys(defaultOptions);
22
+ /*
23
+ * Write failures where the cached log directory may have been removed or made
24
+ * unavailable at runtime. These invalidate the directory cache and are retried once.
25
+ */
26
+ const recoverableWriteCodes = new Set(['ENOENT', 'EACCES', 'EPERM', 'EROFS']);
22
27
  export default class Log {
23
28
  options;
24
29
  color;
25
30
  typeOptions = {};
31
+ /*
32
+ * Instance-level cache of directory creation, keyed on the resolved directory path.
33
+ * resolveFilename() strips directory separators, so a filename template can never
34
+ * introduce a new directory and date rollover cannot invalidate an entry.
35
+ */
36
+ directoryCache = new Map();
26
37
  constructor(options = {}) {
27
38
  const merged = {
28
39
  ...defaultOptions,
@@ -120,9 +131,24 @@ export default class Log {
120
131
  const filenameTemplate = this.getOptionForType(type, 'filename');
121
132
  const resolvedName = this.resolveFilename(filenameTemplate, type);
122
133
  const targetLog = `${path}${resolvedName}`;
123
- await this.validateFileAndDirectory(path, targetLog);
124
134
  const fileAction = overwrite ? fsp.writeFile : fsp.appendFile;
125
- await fileAction(targetLog, content);
135
+ await this.validateFileAndDirectory(path, targetLog);
136
+ try {
137
+ await fileAction(targetLog, content);
138
+ }
139
+ catch (error) {
140
+ const { code } = error;
141
+ if (!code || !recoverableWriteCodes.has(code))
142
+ throw error;
143
+ /*
144
+ * The cached directory may have been removed underneath a warm cache. Invalidate
145
+ * the entry and retry exactly once so a cache hit can never become a permanent
146
+ * silent write failure. The rejection is the caller's only failure signal.
147
+ */
148
+ this.directoryCache.delete(path);
149
+ await this.validateFileAndDirectory(path, targetLog);
150
+ await fileAction(targetLog, content);
151
+ }
126
152
  }
127
153
  // resolves template variables in a filename string
128
154
  resolveFilename(template, type) {
@@ -145,16 +171,27 @@ export default class Log {
145
171
  // sanitize: prevent path traversal and disallow directory separators
146
172
  return resolved.replace(/\.\./g, '').replace(/[/\\]/g, '');
147
173
  }
148
- // attempts to create file and/or directory if they don't already exist
174
+ /*
175
+ * Ensures the log directory exists, deduping concurrent and repeat calls onto a single
176
+ * mkdir per directory. No file bootstrap happens here: both write paths already create
177
+ * the file (appendFile opens 'a', writeFile opens 'w'), so a bootstrap write's only
178
+ * reachable effect was truncating a concurrent caller's content.
179
+ *
180
+ * targetLog is unused but retained for signature compatibility.
181
+ */
149
182
  async validateFileAndDirectory(path, targetLog) {
150
- const errorMethod = this.error;
151
- mkdirSync(path, { recursive: true });
152
- await fsp.access(targetLog).catch(() => {
153
- fsp.writeFile(targetLog, '').catch(() => {
154
- errorMethod(`Failed to create log file: ${targetLog}.`
155
- + '\n Verify that the application runner has write permissions');
156
- });
183
+ const cached = this.directoryCache.get(path);
184
+ if (cached)
185
+ return cached;
186
+ // cache the promise before awaiting so concurrent writers dedupe and none run early
187
+ const pending = fsp.mkdir(path, { recursive: true }).then(() => undefined);
188
+ this.directoryCache.set(path, pending);
189
+ // never cache a poisoned promise: drop the entry so the next write retries
190
+ pending.catch(() => {
191
+ if (this.directoryCache.get(path) === pending)
192
+ this.directoryCache.delete(path);
157
193
  });
194
+ return pending;
158
195
  }
159
196
  // method to conditionally sanitize user configuration input
160
197
  sanitizePath(path) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonyx/logs",
3
- "version": "1.0.1-alpha.21",
3
+ "version": "1.0.1-alpha.22",
4
4
  "description": "Simplified logging for node applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",