@bjoernboss/mws 1.0.0 → 1.1.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/README.md CHANGED
@@ -13,8 +13,6 @@ Only depends on [`ws`](https://github.com/websockets/ws) at runtime.
13
13
 
14
14
  Requires Node.js 22 or later.
15
15
 
16
- Note: Look into the source `TypeScript` code on [`GitHub`](https://github.com/BjoernBoss/mws).
17
-
18
16
  ## Quick Start
19
17
 
20
18
  ```typescript
@@ -45,12 +43,34 @@ server.listen(new HelloModule(), {
45
43
  });
46
44
  ```
47
45
 
46
+ ## Listeners
47
+
48
+ `server.listen()` returns a `Listener` that emits `'listening'`, `'failed'`, and `'stopped'` events:
49
+
50
+ ```typescript
51
+ const listener = server.listen(handler, { port: 8080 });
52
+
53
+ listener.on('listening', (address) => console.log(`Listening on port ${address.port}`));
54
+ listener.on('failed', (err) => console.error(`Failed: ${err.message}`));
55
+ listener.on('stopped', () => console.log('Stopped'));
56
+ ```
57
+
58
+ A serverless listener does not bind to a port. Instead, connections are passed to it manually via `listener.handleRequest()` and `listener.handleUpgrade()` (can also be used for other listeners):
59
+
60
+ ```typescript
61
+ const listener = server.listen(handler, { serverless: { secure: false } });
62
+
63
+ /* forward requests from an external HTTP server */
64
+ externalServer.on('request', (req, resp) => listener.handleRequest(req, resp));
65
+ externalServer.on('upgrade', (req, sock, head) => listener.handleUpgrade(req, sock, head));
66
+ ```
67
+
48
68
  ## Writing Modules
49
69
 
50
70
  A module extends `ModuleHandler` and implements up to three lifecycle hooks:
51
71
 
52
72
  ```typescript
53
- import { ModuleHandler, ClientRequest, Server } from "@bjoernboss/mws";
73
+ import { ModuleHandler, ClientRequest, Server, Params } from "@bjoernboss/mws";
54
74
 
55
75
  export class MyModule extends ModuleHandler {
56
76
  constructor() {
@@ -63,7 +83,7 @@ export class MyModule extends ModuleHandler {
63
83
  }
64
84
 
65
85
  /* Called for every incoming request routed to this module */
66
- protected override async handleRequest(client: ClientRequest, params?: object): Promise<void> {
86
+ protected override async handleRequest(client: ClientRequest, params?: Params): Promise<void> {
67
87
  /* respond to the client */
68
88
  }
69
89
 
@@ -221,6 +241,8 @@ if (!await client.tryRespondFile('/var/www/index.html'))
221
241
  await client.respondHtml(page, { status: Status.Ok });
222
242
  ```
223
243
 
244
+ **Important:** Streams returned by `receiveData()` and `respondData()` can emit `'error'` events. Always register an `'error'` handler on these streams to prevent unhandled errors from crashing the process. Generally: correspondingly tagged functions may throw or error and must handle errors accordingly, to prevent crashing the process.
245
+
224
246
  Convenience methods: `respondOk`, `respondNotFound`, `respondBadRequest`, `respondForbidden`, `respondInternalError`, `respondConflict`, `respondSeeOther`, `respondTemporaryRedirect`, `respondPermanentRedirect`, `respondCreated`, and others.
225
247
 
226
248
  ## WebSocket
@@ -259,22 +281,25 @@ File paths can be tagged with a unique version identifier so clients can cache t
259
281
 
260
282
  ```typescript
261
283
  /* style.css becomes style.<id>.css — the id changes when the file changes */
262
- const versionedPath = server.cache.immutable('my-handler', '/static/style.css');
284
+ const versionedPath = server.cache.immutable('my-module', '/static/style.css');
263
285
  ```
264
286
 
265
287
  When a request arrives for an immutable path, the cache strips the version tag, serves the underlying file, and redirects stale version tags to the current one. Set `CacheConfig.immutableStatePath` to persist version mappings across restarts.
266
288
 
267
289
  ### Direct Cache Access
268
290
 
291
+ These methods are designed for modules to be used, and allows directly to write to the disk, and update the cache at the same time. May throw exceptions.
292
+
269
293
  ```typescript
270
294
  const buffer = await server.cache.read('/path/to/data.json');
271
295
  await server.cache.write('/path/to/output.json', JSON.stringify(data));
296
+ await server.cache.remove('/path/to/old.json');
272
297
  server.cache.flush();
273
298
  ```
274
299
 
275
300
  ## HTML Building
276
301
 
277
- The `build` namespace provides programmatic HTML construction with automatic escaping:
302
+ The `build` namespace provides programmatic HTML construction with automatic escaping. It allows parent modules to use the `patchHtmlPage` function, to build around the HTML, before serving it:
278
303
 
279
304
  ```typescript
280
305
  import { build } from "@bjoernboss/mws";
package/dist/base.d.ts CHANGED
@@ -146,7 +146,7 @@ export declare const Media: {
146
146
  readonly Svg: {
147
147
  readonly fileEnding: ["svg"];
148
148
  readonly mediaType: "image/svg+xml";
149
- readonly encoding: "";
149
+ readonly encoding: "charset=utf-8";
150
150
  readonly compressible: true;
151
151
  };
152
152
  readonly Unknown: {
@@ -167,32 +167,33 @@ export declare const Encoding: {
167
167
  readonly name: "br";
168
168
  readonly makeDecode: () => libZlib.BrotliDecompress;
169
169
  readonly makeEncode: () => libZlib.BrotliCompress;
170
- readonly encodeBuffer: (buffer: Buffer) => NonSharedBuffer;
170
+ readonly encodeBuffer: (buffer: Buffer) => Buffer;
171
171
  };
172
172
  readonly Zstd: {
173
173
  readonly name: "zstd";
174
174
  readonly makeDecode: () => libZlib.ZstdDecompress;
175
175
  readonly makeEncode: () => libZlib.ZstdCompress;
176
- readonly encodeBuffer: (buffer: Buffer) => NonSharedBuffer;
176
+ readonly encodeBuffer: (buffer: Buffer) => Buffer;
177
177
  };
178
178
  readonly Gzip: {
179
179
  readonly name: "gzip";
180
180
  readonly makeDecode: () => libZlib.Gunzip;
181
181
  readonly makeEncode: () => libZlib.Gzip;
182
- readonly encodeBuffer: (buffer: Buffer) => NonSharedBuffer;
182
+ readonly encodeBuffer: (buffer: Buffer) => Buffer;
183
183
  };
184
184
  readonly Deflate: {
185
185
  readonly name: "deflate";
186
186
  readonly makeDecode: () => libZlib.Inflate;
187
187
  readonly makeEncode: () => libZlib.Deflate;
188
- readonly encodeBuffer: (buffer: Buffer) => NonSharedBuffer;
188
+ readonly encodeBuffer: (buffer: Buffer) => Buffer;
189
189
  };
190
190
  readonly Identity: {
191
191
  readonly name: "identity";
192
192
  readonly makeDecode: () => libStream.PassThrough;
193
193
  readonly makeEncode: () => libStream.PassThrough;
194
- readonly encodeBuffer: (buffer: Buffer) => Buffer<ArrayBufferLike>;
194
+ readonly encodeBuffer: (buffer: Buffer) => Buffer;
195
195
  };
196
196
  };
197
+ /** minimum size for content is considered encodable */
197
198
  export declare const MIN_ENCODING_SIZE: number;
198
199
  //# sourceMappingURL=base.d.ts.map
package/dist/base.js CHANGED
@@ -33,7 +33,7 @@ export const Media = {
33
33
  Png: { fileEnding: ['png'], mediaType: 'image/png', encoding: '', compressible: false },
34
34
  Gif: { fileEnding: ['gif'], mediaType: 'image/gif', encoding: '', compressible: false },
35
35
  Jpg: { fileEnding: ['jpg', 'jpeg'], mediaType: 'image/jpeg', encoding: '', compressible: false },
36
- Svg: { fileEnding: ['svg'], mediaType: 'image/svg+xml', encoding: '', compressible: true },
36
+ Svg: { fileEnding: ['svg'], mediaType: 'image/svg+xml', encoding: 'charset=utf-8', compressible: true },
37
37
  Unknown: { fileEnding: [], mediaType: 'application/octet-stream', encoding: '', compressible: false }
38
38
  };
39
39
  export const Encoding = {
@@ -68,6 +68,6 @@ export const Encoding = {
68
68
  encodeBuffer: (buffer) => buffer
69
69
  }
70
70
  };
71
- /* minimum size for content is considered encodable */
71
+ /** minimum size for content is considered encodable */
72
72
  export const MIN_ENCODING_SIZE = 1_000;
73
73
  //# sourceMappingURL=base.js.map
package/dist/builder.d.ts CHANGED
@@ -1,27 +1,36 @@
1
+ /** wrapper around a string that has been verified as safe for direct html insertion;
2
+ * plain strings are treated as untrusted and will be html-escaped before wrapping */
1
3
  export type HtmlString = HtmlGuard | string;
2
4
  export declare class HtmlGuard {
3
5
  content: string;
4
6
  private constructor();
7
+ /** ensure the value is safe for html insertion; escapes plain strings, passes through HtmlGuard as-is */
5
8
  static get(str: HtmlString): HtmlGuard;
9
+ /** wrap a string for html insertion; if safe is false, it will be html-escaped first */
6
10
  static make(str: string, safe: boolean): HtmlGuard;
7
11
  }
12
+ /** create a secure string [if safe is true, content is taken as-is; otherwise it is html escaped] */
8
13
  export declare function Safe(content: string, safe?: boolean): HtmlGuard;
14
+ /** html component building interface (simple should return true for small one liners) */
9
15
  export interface HtmlComponent {
10
16
  finalize(indent: string): string;
11
17
  simple(): boolean;
12
18
  }
19
+ /** raw text content to be embedded into an html tree */
13
20
  export declare class EmbeddedContent implements HtmlComponent {
14
21
  private content;
15
22
  constructor(content: string, safe: boolean);
16
23
  simple(): boolean;
17
24
  finalize(_: string): string;
18
25
  }
26
+ /** self-closing html tag (e.g. <meta/>, <link/>, <br/>) */
19
27
  export declare class SingleTag implements HtmlComponent {
20
28
  private content;
21
29
  constructor(name: string, properties: Record<string, HtmlString>);
22
30
  simple(): boolean;
23
31
  finalize(indent: string): string;
24
32
  }
33
+ /** html tag with children (e.g. <div>...</div>, <p>text</p>) */
25
34
  export declare class DualTag implements HtmlComponent {
26
35
  private openTag;
27
36
  private closeTag;
@@ -30,6 +39,7 @@ export declare class DualTag implements HtmlComponent {
30
39
  simple(): boolean;
31
40
  finalize(indent: string): string;
32
41
  }
42
+ /** full html page; automatically adds utf-8 charset and defaults language to 'en' */
33
43
  export declare class HtmlPage {
34
44
  private _head;
35
45
  private _body;
@@ -45,12 +55,16 @@ export declare class HtmlPage {
45
55
  set body(value: HtmlComponent | HtmlComponent[]);
46
56
  finalize(): string;
47
57
  }
58
+ /** embed raw content; if safe is true, the content is trusted and
59
+ * inserted verbatim; otherwise it is html-escaped before insertion */
48
60
  export declare function Embed(content: string, safe: boolean): HtmlComponent;
49
61
  export declare function Meta(name: HtmlString, content: HtmlString): HtmlComponent;
50
62
  export declare function Title(name: HtmlString): HtmlComponent;
51
63
  export declare function LoadStyle(path: HtmlString): HtmlComponent;
52
64
  export declare function LoadScript(path: HtmlString, properties?: Record<string, HtmlString>): HtmlComponent;
65
+ /** inline css; content is inserted verbatim as <style> is a raw text element */
53
66
  export declare function AddStyle(content: string, properties?: Record<string, HtmlString>): HtmlComponent;
67
+ /** inline javascript; content is inserted verbatim as <script> is a raw text element */
54
68
  export declare function AddScript(content: string, properties?: Record<string, HtmlString>): HtmlComponent;
55
69
  export declare function Text(text: HtmlString, properties?: Record<string, HtmlString>): HtmlComponent;
56
70
  export declare function Div(properties?: Record<string, HtmlString>, children?: HtmlComponent[] | HtmlComponent): HtmlComponent;
package/dist/builder.js CHANGED
@@ -6,20 +6,20 @@ export class HtmlGuard {
6
6
  constructor(content) {
7
7
  this.content = content;
8
8
  }
9
- /* ensure the value is safe for html insertion; escapes plain strings, passes through HtmlGuard as-is */
9
+ /** ensure the value is safe for html insertion; escapes plain strings, passes through HtmlGuard as-is */
10
10
  static get(str) {
11
11
  return (str instanceof HtmlGuard ? str : new HtmlGuard(libHelper.escapeHtml(str)));
12
12
  }
13
- /* wrap a string for html insertion; if safe is false, it will be html-escaped first */
13
+ /** wrap a string for html insertion; if safe is false, it will be html-escaped first */
14
14
  static make(str, safe) {
15
15
  return new HtmlGuard(safe ? str : libHelper.escapeHtml(str));
16
16
  }
17
17
  }
18
- /* create a secure string [if safe is true, content is taken as-is; otherwise it is html escaped] */
18
+ /** create a secure string [if safe is true, content is taken as-is; otherwise it is html escaped] */
19
19
  export function Safe(content, safe = true) {
20
20
  return HtmlGuard.make(content, safe);
21
21
  }
22
- /* raw text content to be embedded into an html tree */
22
+ /** raw text content to be embedded into an html tree */
23
23
  export class EmbeddedContent {
24
24
  content;
25
25
  constructor(content, safe) {
@@ -32,7 +32,7 @@ export class EmbeddedContent {
32
32
  return this.content;
33
33
  }
34
34
  }
35
- /* self-closing html tag (e.g. <meta/>, <link/>, <br/>) */
35
+ /** self-closing html tag (e.g. <meta/>, <link/>, <br/>) */
36
36
  export class SingleTag {
37
37
  content;
38
38
  constructor(name, properties) {
@@ -46,7 +46,7 @@ export class SingleTag {
46
46
  return `${indent}${this.content}`;
47
47
  }
48
48
  }
49
- /* html tag with children (e.g. <div>...</div>, <p>text</p>) */
49
+ /** html tag with children (e.g. <div>...</div>, <p>text</p>) */
50
50
  export class DualTag {
51
51
  openTag;
52
52
  closeTag;
@@ -73,7 +73,7 @@ export class DualTag {
73
73
  return `${indent}${this.openTag}${body}\n${indent}${this.closeTag}`;
74
74
  }
75
75
  }
76
- /* full html page; automatically adds utf-8 charset and defaults language to 'en' */
76
+ /** full html page; automatically adds utf-8 charset and defaults language to 'en' */
77
77
  export class HtmlPage {
78
78
  _head;
79
79
  _body;
@@ -109,7 +109,7 @@ export class HtmlPage {
109
109
  return `<!DOCTYPE html>\n${page.finalize('')}`;
110
110
  }
111
111
  }
112
- /* embed raw content; if safe is true, the content is trusted and
112
+ /** embed raw content; if safe is true, the content is trusted and
113
113
  * inserted verbatim; otherwise it is html-escaped before insertion */
114
114
  export function Embed(content, safe) {
115
115
  return new EmbeddedContent(content, safe);
@@ -126,11 +126,11 @@ export function LoadStyle(path) {
126
126
  export function LoadScript(path, properties = {}) {
127
127
  return new DualTag('script', { ...properties, src: path }, []);
128
128
  }
129
- /* inline css; content is inserted verbatim as <style> is a raw text element */
129
+ /** inline css; content is inserted verbatim as <style> is a raw text element */
130
130
  export function AddStyle(content, properties = {}) {
131
131
  return new DualTag('style', properties, [new EmbeddedContent(content, true)]);
132
132
  }
133
- /* inline javascript; content is inserted verbatim as <script> is a raw text element */
133
+ /** inline javascript; content is inserted verbatim as <script> is a raw text element */
134
134
  export function AddScript(content, properties = {}) {
135
135
  return new DualTag('script', properties, new EmbeddedContent(content, true));
136
136
  }
package/dist/cache.d.ts CHANGED
@@ -2,56 +2,96 @@ import * as libLog from "./log.js";
2
2
  import * as libBase from "./base.js";
3
3
  import * as libStream from "stream";
4
4
  export interface Cached {
5
+ /** check if this is an immutable file entry */
5
6
  isImmutable(): boolean;
7
+ /** path of the file */
6
8
  filePath(): string;
9
+ /** size in bytes of the file */
7
10
  fileSize(): number;
11
+ /** fetch the last modified time formatted for the network */
8
12
  lastModified(): string;
13
+ /** fetch the unique-id to identify this version of the cached file (constructed,
14
+ * just like the cache identifies them as equivalent: size+last-modified) */
9
15
  uniqueId(): string;
16
+ /** [no-throw but errors] object or encoded entry must not be used anymore after reading or streaming from it */
10
17
  stream(options?: {
11
18
  start?: number;
12
19
  end?: number;
13
20
  }): libStream.Readable;
21
+ /** [throws] object or encoded entry must not be used anymore after reading or streaming from it */
14
22
  read(): Promise<Buffer>;
23
+ /** [throws] object or encoded entry must not be used anymore after reading or streaming from it */
15
24
  readSync(): Buffer;
25
+ /** create an encoded version of this cached entry (no encoding is equivalent to identity) */
16
26
  encoded(encoding?: libBase.EncodingType): EncodedCache;
17
27
  }
18
28
  export interface EncodedCache {
29
+ /** size in bytes of the encoding (null if not yet determined) */
19
30
  contentSize(): number | null;
31
+ /** [no-throw but errors] object or encoded entry must not be used anymore after reading or streaming from it */
20
32
  stream(): libStream.Readable;
33
+ /** [throws] object or encoded entry must not be used anymore after reading or streaming from it */
21
34
  read(): Promise<Buffer>;
35
+ /** [throws] object or encoded entry must not be used anymore after reading or streaming from it */
22
36
  readSync(): Buffer;
23
37
  }
38
+ /** all cache host operations which may throw errors and will not log them, and dont guarantee to contain the failing file path */
24
39
  export declare class CacheHost extends libLog.Logger {
25
40
  private _cacheManager;
26
41
  private _immutableManager;
27
42
  private _config;
28
43
  constructor(config?: CacheConfig | BurntCacheConfig);
29
44
  private resolveCache;
45
+ /** configuration used by this cache host */
30
46
  get config(): BurntCacheConfig;
47
+ /** [throws] if [checkFreshness] is true, re-validate the file stats on disk before serving from cache (defaults
48
+ * to false); resolve immutable ids automatically (Cached to interact with cache; null, if it does not exist,
49
+ * string if the immutable path has been permanently moved to the new path in source space) */
31
50
  fetchImmutable(path: string, options?: {
32
51
  checkFreshness?: boolean;
33
52
  }): Cached | string | null;
53
+ /** [throws] if [checkFreshness] is true re-validate the file stats on disk before serving from cache (defaults
54
+ * to false); no immutable ids are resolved (Cached to interact with cache; null, if it does not exist) */
34
55
  fetchDirect(path: string, options?: {
35
56
  checkFreshness?: boolean;
36
57
  }): Cached | null;
37
- immutable(handler: string, path: string, options?: {
58
+ /** generate a unique tagged path for the given query path, which will change whenever the underlying file changes;
59
+ * [checkFreshness]: if true, re-validate the file stats on disk to detect changes (defaults to false); creates
60
+ * a path to a file, which looks similar to the source, except that the name includes a unique id, which will be used
61
+ * to identity the given file state (will be removed from the final target path to be served, to identify the actual source) */
62
+ immutable(module: string, path: string, options?: {
38
63
  checkFreshness?: boolean;
39
64
  }): string;
65
+ /** flush all cached data and invalidate immutable stats so they are re-checked on next access */
40
66
  flush(): void;
67
+ /** [throws] read the data directly into a buffer (designed for modules to interact with) */
41
68
  read(path: string, options?: {
42
69
  checkFreshness?: boolean;
43
70
  }): Promise<Buffer | null>;
71
+ /** [throws] write data atomically to the disk and update the cache (designed for modules to interact with;
72
+ * writes as utf-8; writes data first to temporary file and then replaces the file atomically; for create,
73
+ * must not replace an existing file; returns false if the file already existed and could not be created) */
44
74
  write(path: string, data: Buffer | string, options?: {
45
75
  what?: string;
46
76
  temporary?: string;
47
- }): Promise<void>;
77
+ create?: boolean;
78
+ }): Promise<boolean>;
79
+ /** [throws] remove the data from the physical disk and from the cache (returns false if it did not exist) */
80
+ remove(path: string): Promise<boolean>;
48
81
  }
82
+ /** simple wrapper function to create a cache */
49
83
  export declare function createCache(config?: CacheConfig | BurntCacheConfig): CacheHost;
50
84
  export interface CacheConfig {
85
+ /** immutable state path is used to ensure the immutable state uses persistent ids across
86
+ * restarts (will be read upon loading; if not set, ids will be lost after a server restart) [Default: ''] */
51
87
  immutableStatePath?: string;
88
+ /** total cachable size [Default: 50_000_000] */
52
89
  cacheSize?: number;
90
+ /** upper limit for files considered cachable, others will just be streamed through [Default: 10_000_000] */
53
91
  fileSizeLimit?: number;
92
+ /** always validate file freshness before providing them [Default: false] */
54
93
  alwaysValidate?: boolean;
94
+ /** tag served content with immutable ids to encode freshness into the path [Default: true] */
55
95
  immutableTagging?: boolean;
56
96
  }
57
97
  export declare class BurntCacheConfig {
package/dist/cache.js CHANGED
@@ -7,7 +7,7 @@ import * as libFsPromises from "fs/promises";
7
7
  import * as libStream from "stream";
8
8
  import * as libCrypto from "crypto";
9
9
  const UNIQUE_ID_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';
10
- const UNIQUE_ID_LENGTH = 14;
10
+ const UNIQUE_ID_LENGTH = 10;
11
11
  const ID_EXTENSION_REGEX = RegExp(`^\\.[${UNIQUE_ID_CHARS}]{${UNIQUE_ID_LENGTH}}$`, 'i');
12
12
  function readStats(path) {
13
13
  try {
@@ -18,26 +18,31 @@ function readStats(path) {
18
18
  catch (err) {
19
19
  if (err.code == 'ENOENT')
20
20
  return null;
21
- throw new Error(`Filesystem error while checking [${path}]: ${err.message}`);
21
+ throw err;
22
22
  }
23
23
  return null;
24
24
  }
25
25
  async function atomicWrite(path, content, logger, options) {
26
26
  const tempPath = (options?.temporary ?? `${path}.temp`);
27
+ logger.trace(`Writing ${options?.what ?? 'data'} to [${path}]`);
28
+ if (options?.create === true) {
29
+ try {
30
+ await libFsPromises.writeFile(path, content, { flag: 'wx' });
31
+ }
32
+ catch (err) {
33
+ if (err.code == 'EEXIST')
34
+ return false;
35
+ throw new Error(`Failed to create file: ${err.message}`);
36
+ }
37
+ return true;
38
+ }
27
39
  let written = false;
28
40
  try {
29
- logger.trace(`Writing ${options?.what ?? 'data'} to [${path}]`);
30
- /* write the content to the temporary file */
31
41
  await libFsPromises.writeFile(tempPath, content);
32
42
  written = true;
33
43
  await libFsPromises.rename(tempPath, path);
34
- return true;
35
44
  }
36
45
  catch (err) {
37
- if (written)
38
- logger.error(`Failed to replace the original file [${path}]: ${err.message}`);
39
- else
40
- logger.error(`Failed to write to temporary file [${tempPath}]: ${err.message}`);
41
46
  try {
42
47
  await libFsPromises.unlink(tempPath);
43
48
  }
@@ -45,8 +50,11 @@ async function atomicWrite(path, content, logger, options) {
45
50
  if (err.code != 'ENOENT')
46
51
  logger.warning(`Failed to remove temporary file [${tempPath}]: ${err.message}`);
47
52
  }
53
+ if (written)
54
+ throw new Error(`Failed to replace the original file: ${err.message}`);
55
+ throw new Error(`Failed to write to temporary file [${tempPath}]: ${err.message}`);
48
56
  }
49
- return false;
57
+ return true;
50
58
  }
51
59
  class CacheManager {
52
60
  logger;
@@ -83,9 +91,9 @@ class CacheManager {
83
91
  if (this.allocated + data.byteLength > this.totalCapacity)
84
92
  this.reduce(this.totalCapacity - data.byteLength);
85
93
  /* add the entry to the cache */
86
- this.logger.log(`Added [${path}] to the cache (Size: ${data.byteLength})`);
87
94
  this.allocated += data.byteLength;
88
95
  this.map[path] = { data, encodings: {}, mtime, touched: ++this.nextStamp, age };
96
+ this.logger.log(`Added [${path}] to the cache (Size: ${data.byteLength} / Allocated: ${this.allocated})`);
89
97
  }
90
98
  addEncoding(path, data, age, name) {
91
99
  if (!this.cacheable(data.byteLength))
@@ -102,20 +110,20 @@ class CacheManager {
102
110
  if (!(path in this.map))
103
111
  return;
104
112
  /* add the entry to the cache */
105
- this.logger.log(`Added encoding [${name}] of [${path}] to the cache (Size: ${data.byteLength})`);
106
113
  this.allocated += data.byteLength;
107
114
  this.map[path].encodings[name] = data;
115
+ this.logger.log(`Added encoding [${name}] of [${path}] to the cache (Size: ${data.byteLength} / Allocated: ${this.allocated})`);
108
116
  }
109
117
  drop(path) {
110
118
  if (!(path in this.map))
111
119
  return;
112
- this.logger.log(`Dropped [${path}] and encodings from the cache`);
113
120
  /* remove all cached encodings and the entry itself */
114
121
  const entry = this.map[path];
115
122
  for (const key in entry.encodings)
116
123
  this.allocated -= entry.encodings[key].byteLength;
117
124
  this.allocated -= entry.data.byteLength;
118
125
  delete this.map[path];
126
+ this.logger.log(`Dropped [${path}] and encodings from the cache (Allocated: ${this.allocated})`);
119
127
  }
120
128
  cacheable(size) {
121
129
  return (size <= this.largestSize && size <= this.totalCapacity);
@@ -195,7 +203,12 @@ class ImmutableManager {
195
203
  }
196
204
  const content = JSON.stringify(output);
197
205
  /* ignore any read/write failures */
198
- await atomicWrite(this.writeBack.path, Buffer.from(content, 'utf-8'), this.logger, { what: 'immutable state' });
206
+ try {
207
+ await atomicWrite(this.writeBack.path, Buffer.from(content, 'utf-8'), this.logger, { what: 'immutable state' });
208
+ }
209
+ catch (err) {
210
+ this.logger.error(`Failed to write immutable state to [${this.writeBack.path}]: ${err.message}`);
211
+ }
199
212
  }
200
213
  this.writeBack.writing = null;
201
214
  resolver();
@@ -286,8 +299,8 @@ class ImmutableManager {
286
299
  if (performInitialWriteBack)
287
300
  this.storeState();
288
301
  }
289
- make(handler, path, checkFreshness) {
290
- const identifier = `${handler}:${path}`;
302
+ make(module, path, checkFreshness) {
303
+ const identifier = `${module}:${path}`;
291
304
  if (!this.immutableTagging)
292
305
  return path;
293
306
  while (true) {
@@ -593,6 +606,7 @@ function EncodedCache(cache, reader, entry, age, encoding) {
593
606
  }
594
607
  };
595
608
  }
609
+ /** all cache host operations which may throw errors and will not log them, and dont guarantee to contain the failing file path */
596
610
  export class CacheHost extends libLog.Logger {
597
611
  _cacheManager;
598
612
  _immutableManager;
@@ -630,51 +644,60 @@ export class CacheHost extends libLog.Logger {
630
644
  return new AlreadyCached(this._cacheManager, path, entry, isImmutable);
631
645
  return new NotCached(this._cacheManager, path, fileSize, mtime, this._cacheManager.allocAge(), isImmutable);
632
646
  }
633
- /* configuration used by this cache host */
647
+ /** configuration used by this cache host */
634
648
  get config() {
635
649
  return this._config;
636
650
  }
637
- /* [throws] if [checkFreshness] is true, re-validate the file stats on disk before serving from cache (defaults
638
- * to false); resolve immutable ids automatically (Cached to interact with cache; null, if it does not exist,
639
- * string if the immutable path has been permanently moved to the new path in source space) */
651
+ /** [throws] if [checkFreshness] is true, re-validate the file stats on disk before serving from cache (defaults
652
+ * to false); resolve immutable ids automatically (Cached to interact with cache; null, if it does not exist,
653
+ * string if the immutable path has been permanently moved to the new path in source space) */
640
654
  fetchImmutable(path, options) {
641
655
  return this.resolveCache(path, options?.checkFreshness ?? false, true);
642
656
  }
643
- /* [throws] if [checkFreshness] is true re-validate the file stats on disk before serving from cache (defaults
644
- * to false); no immutable ids are resolved (Cached to interact with cache; null, if it does not exist) */
657
+ /** [throws] if [checkFreshness] is true re-validate the file stats on disk before serving from cache (defaults
658
+ * to false); no immutable ids are resolved (Cached to interact with cache; null, if it does not exist) */
645
659
  fetchDirect(path, options) {
646
660
  return this.resolveCache(path, options?.checkFreshness ?? false, false);
647
661
  }
648
- /* generate a unique tagged path for the given query path, which will change whenever the underlying file changes;
649
- * [checkFreshness]: if true, re-validate the file stats on disk to detect changes (defaults to false); creates
650
- * a path to a file, which looks similar to the source, except that the name includes a unique id, which will be used
651
- * to identity the given file state (will be removed from the final target path to be served, to identify the actual source) */
652
- immutable(handler, path, options) {
653
- return this._immutableManager.make(handler, path, options?.checkFreshness ?? false);
662
+ /** generate a unique tagged path for the given query path, which will change whenever the underlying file changes;
663
+ * [checkFreshness]: if true, re-validate the file stats on disk to detect changes (defaults to false); creates
664
+ * a path to a file, which looks similar to the source, except that the name includes a unique id, which will be used
665
+ * to identity the given file state (will be removed from the final target path to be served, to identify the actual source) */
666
+ immutable(module, path, options) {
667
+ return this._immutableManager.make(module, path, options?.checkFreshness ?? false);
654
668
  }
655
- /* flush all cached data and invalidate immutable stats so they are re-checked on next access */
669
+ /** flush all cached data and invalidate immutable stats so they are re-checked on next access */
656
670
  flush() {
657
671
  this.info('Flushing cache and invalidating immutable entries');
658
672
  this._cacheManager.flush();
659
673
  this._immutableManager.invalidate();
660
674
  }
661
- /* [throws] read the data directly into a buffer (designed for modules to interact with) */
675
+ /** [throws] read the data directly into a buffer (designed for modules to interact with) */
662
676
  async read(path, options) {
663
- const entry = this.resolveCache(path, options?.checkFreshness ?? false, false);
664
- if (entry == null)
665
- return null;
666
- return entry.read();
677
+ try {
678
+ const entry = this.resolveCache(path, options?.checkFreshness ?? false, false);
679
+ if (entry == null)
680
+ return null;
681
+ return entry.read();
682
+ }
683
+ /* special abstraction to ensure check-before-use does not result in not-found */
684
+ catch (err) {
685
+ if (err.code == 'ENOENT')
686
+ return null;
687
+ throw err;
688
+ }
667
689
  }
668
- /* [throws] write data atomically to the disk and update the cache (designed for modules to interact
669
- * with; writes as utf-8; writes data first to temporary file and then replaces the file atomically) */
690
+ /** [throws] write data atomically to the disk and update the cache (designed for modules to interact with;
691
+ * writes as utf-8; writes data first to temporary file and then replaces the file atomically; for create,
692
+ * must not replace an existing file; returns false if the file already existed and could not be created) */
670
693
  async write(path, data, options) {
671
694
  if (typeof data == 'string')
672
695
  data = Buffer.from(data, 'utf-8');
673
- /* write the data atomically to the destination */
674
- if (!await atomicWrite(path, data, this, { what: (options?.what ?? 'via cache'), temporary: options?.temporary }))
675
- throw new Error('Failed to atomically write data');
696
+ /* write the data atomically to the destination (let errors propagate out) */
697
+ if (!await atomicWrite(path, data, this, { what: (options?.what ?? 'via cache'), temporary: options?.temporary, create: options?.create }))
698
+ return false;
676
699
  if (!this._cacheManager.cacheable(data.byteLength))
677
- return;
700
+ return true;
678
701
  const age = this._cacheManager.allocAge();
679
702
  /* fetch the new state and update the cache (let errors propagate out; only if the write-state seems consistent) */
680
703
  const stats = readStats(path);
@@ -682,9 +705,26 @@ export class CacheHost extends libLog.Logger {
682
705
  this._cacheManager.drop(path);
683
706
  else if (stats[0] == data.byteLength)
684
707
  this._cacheManager.add(path, data, stats[1], age);
708
+ return true;
709
+ }
710
+ /** [throws] remove the data from the physical disk and from the cache (returns false if it did not exist) */
711
+ async remove(path) {
712
+ let existed = true;
713
+ /* try to remove the physical file */
714
+ try {
715
+ await libFsPromises.unlink(path);
716
+ }
717
+ catch (err) {
718
+ if (err.code != 'ENOENT')
719
+ throw err;
720
+ existed = false;
721
+ }
722
+ /* remove the data from the cache */
723
+ this._cacheManager.drop(path);
724
+ return existed;
685
725
  }
686
726
  }
687
- /* simple wrapper function to create a cache */
727
+ /** simple wrapper function to create a cache */
688
728
  export function createCache(config) {
689
729
  return new CacheHost(config);
690
730
  }