@muspellheim/shared 0.6.0 → 0.7.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.
Files changed (46) hide show
  1. package/LICENSE.txt +1 -1
  2. package/README.md +11 -13
  3. package/dist/shared.d.ts +423 -0
  4. package/dist/shared.js +535 -0
  5. package/dist/shared.umd.cjs +1 -0
  6. package/package.json +27 -23
  7. package/.prettierignore +0 -3
  8. package/.prettierrc +0 -5
  9. package/deno.json +0 -15
  10. package/deno.mk +0 -68
  11. package/eslint.config.js +0 -23
  12. package/lib/assert.js +0 -15
  13. package/lib/browser/components.js +0 -165
  14. package/lib/browser/index.js +0 -3
  15. package/lib/color.js +0 -137
  16. package/lib/configurable-responses.js +0 -69
  17. package/lib/feature-toggle.js +0 -9
  18. package/lib/health.js +0 -510
  19. package/lib/index.js +0 -23
  20. package/lib/lang.js +0 -100
  21. package/lib/logging.js +0 -599
  22. package/lib/long-polling-client.js +0 -186
  23. package/lib/message-client.js +0 -68
  24. package/lib/messages.js +0 -68
  25. package/lib/metrics.js +0 -120
  26. package/lib/node/actuator-controller.js +0 -102
  27. package/lib/node/configuration-properties.js +0 -291
  28. package/lib/node/handler.js +0 -25
  29. package/lib/node/index.js +0 -9
  30. package/lib/node/logging.js +0 -60
  31. package/lib/node/long-polling.js +0 -83
  32. package/lib/node/sse-emitter.js +0 -104
  33. package/lib/node/static-files-controller.js +0 -15
  34. package/lib/output-tracker.js +0 -89
  35. package/lib/service-locator.js +0 -44
  36. package/lib/sse-client.js +0 -163
  37. package/lib/stop-watch.js +0 -54
  38. package/lib/store.js +0 -129
  39. package/lib/time.js +0 -445
  40. package/lib/util.js +0 -380
  41. package/lib/validation.js +0 -290
  42. package/lib/vector.js +0 -194
  43. package/lib/vitest/equality-testers.js +0 -19
  44. package/lib/vitest/index.js +0 -1
  45. package/lib/web-socket-client.js +0 -262
  46. package/tsconfig.json +0 -13
@@ -1,291 +0,0 @@
1
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
2
-
3
- /**
4
- * Provide the configuration of an application.
5
- *
6
- * The sources in order of priority:
7
- *
8
- * 1. JSON file
9
- * 2. Environment variables
10
- * 3. Command line arguments
11
- *
12
- * @module
13
- */
14
-
15
- import fsPromises from 'node:fs/promises';
16
- import path from 'node:path';
17
- import process from 'node:process';
18
-
19
- import { deepCopy, deepMerge } from '../util.js';
20
-
21
- // TODO How to handle optional values? Cast to which type?
22
- // TODO Use JSON schema to validate the configuration?
23
-
24
- /**
25
- * Reads the configuration from file and environment variables.
26
- *
27
- * The configuration is read from a JSON file `application.json` from the
28
- * working directory. Can be configured with `config.name`, `config.location`.
29
- *
30
- * Example:
31
- *
32
- * ```javascript
33
- * const configuration = ConfigurationProperties.create();
34
- * const config = await configuration.get();
35
- * ```
36
- *
37
- * With default values:
38
- *
39
- * ```javascript
40
- * const configuration = ConfigurationProperties.create({
41
- * defaultProperties: {
42
- * port: 8080,
43
- * database: { host: 'localhost', port: 5432 },
44
- * },
45
- * });
46
- * const config = await configuration.get();
47
- * ```
48
- *
49
- * @template T
50
- */
51
- export class ConfigurationProperties {
52
- /**
53
- * Creates an instance of the application configuration.
54
- *
55
- * @template T
56
- * @param {object} options The configuration options.
57
- * @param {T} [options.defaultProperties=null] The default configuration.
58
- * @param {string} [options.prefix=""] The prefix of the properties to get.
59
- * @return {ConfigurationProperties<T>} The new instance.
60
- */
61
- static create({ defaultProperties = null, prefix = '' } = {}) {
62
- return new ConfigurationProperties(defaultProperties, prefix, fsPromises);
63
- }
64
-
65
- /**
66
- * Creates a nullable of the application configuration.
67
- *
68
- * @template T
69
- * @param {object} options The configuration options.
70
- * @param {T} [options.defaultProperties=null] The default configuration.
71
- * @param {string} [options.prefix=""] The prefix of the properties to get.
72
- * @param {object} [options.files={}] The files and file content that are
73
- * available.
74
- * @return {ConfigurationProperties<T>} The new instance.
75
- */
76
- static createNull({
77
- defaultProperties = null,
78
- prefix = '',
79
- files = {},
80
- } = {}) {
81
- return new ConfigurationProperties(
82
- defaultProperties,
83
- prefix,
84
- new FsStub(files),
85
- );
86
- }
87
-
88
- /** @type {T} */
89
- #defaultProperties;
90
-
91
- /** @type {string} */
92
- #prefix;
93
-
94
- /** @type {fsPromises} */
95
- #fs;
96
-
97
- /** @type {T} */
98
- #cached;
99
-
100
- /**
101
- * The constructor is for internal use. Use the factory methods instead.
102
- *
103
- * @param {T} defaultProperties
104
- * @param {string} prefix
105
- * @param {fsPromises} fs
106
- * @see ConfigurationProperties.create
107
- * @see ConfigurationProperties.createNull
108
- */
109
- constructor(defaultProperties, prefix, fs) {
110
- this.#defaultProperties = defaultProperties;
111
- this.#prefix = prefix;
112
- this.#fs = fs;
113
- }
114
-
115
- /**
116
- * Loads the configuration from the file.
117
- *
118
- * @return {Promise<T>} The configuration object.
119
- */
120
- async get() {
121
- if (this.#cached !== undefined) {
122
- return this.#cached;
123
- }
124
-
125
- // TODO Interpret placeholders like ${foo.bar}
126
- // TODO Interpret placeholders with default value like ${foo.bar:default}
127
-
128
- let config = await this.#loadFile();
129
- config = deepMerge(deepCopy(this.#defaultProperties), config);
130
- // TODO apply environment variable APPLICATION_JSON as JSON string
131
- this.#applyVariables(config, getEnv);
132
- this.#applyVariables(config, getArg);
133
- this.#cached = this.#getSubset(config, this.#prefix);
134
- return this.#cached;
135
- }
136
-
137
- async #loadFile() {
138
- const { name, location } = this.#getConfig();
139
- for (const l of location) {
140
- try {
141
- const filePath = path.join(l, name);
142
- const content = await this.#fs.readFile(filePath, 'utf-8');
143
- return JSON.parse(content);
144
- } catch (err) {
145
- // @ts-ignore NodeJS error code
146
- if (err.code === 'ENOENT') {
147
- // ignore file not found
148
- continue;
149
- }
150
-
151
- throw err;
152
- }
153
- }
154
- }
155
-
156
- #getConfig() {
157
- const defaultConfig = {
158
- config: {
159
- name: 'application.json',
160
- location: ['.', 'config'],
161
- },
162
- };
163
- this.#applyVariables(defaultConfig, getEnv);
164
- this.#applyVariables(defaultConfig, getArg);
165
- const {
166
- config: { name, location },
167
- } = defaultConfig;
168
- return { name, location };
169
- }
170
-
171
- #applyVariables(config, getValue, path) {
172
- // handle object
173
- // handle array
174
- // handle string
175
- // handle number
176
- // handle boolean (true, false)
177
- // handle null (empty string set the value to null)
178
- // if value is undefined, keep the default value
179
- for (const key in config) {
180
- if (typeof config[key] === 'boolean') {
181
- const value = getValue(key, path);
182
- if (value === null) {
183
- config[key] = null;
184
- } else if (value) {
185
- config[key] = value.toLowerCase() === 'true';
186
- }
187
- } else if (typeof config[key] === 'number') {
188
- const value = getValue(key, path);
189
- if (value === null) {
190
- config[key] = null;
191
- } else if (value) {
192
- config[key] = Number(value);
193
- }
194
- } else if (typeof config[key] === 'string') {
195
- const value = getValue(key, path);
196
- if (value === null) {
197
- config[key] = null;
198
- } else if (value) {
199
- config[key] = String(value);
200
- }
201
- } else if (config[key] === null) {
202
- const value = getValue(key, path);
203
- if (value === null) {
204
- config[key] = null;
205
- } else if (value) {
206
- config[key] = value;
207
- }
208
- } else if (typeof config[key] === 'object') {
209
- const value = getValue(key, path);
210
- if (value === null) {
211
- config[key] = null;
212
- } else if (Array.isArray(config[key]) && value) {
213
- config[key] = value.split(',');
214
- } else {
215
- this.#applyVariables(config[key], getValue, key);
216
- }
217
- } else {
218
- throw new Error(`Unsupported type: ${typeof config[key]}`);
219
- }
220
- }
221
- }
222
-
223
- #getSubset(config, prefix) {
224
- if (prefix === '') {
225
- return config;
226
- }
227
-
228
- const [key, ...rest] = prefix.split('.');
229
- if (rest.length === 0) {
230
- return config[key];
231
- }
232
-
233
- return this.#getSubset(config[key], rest.join('.'));
234
- }
235
- }
236
-
237
- function getEnv(key, path = '') {
238
- if (path) {
239
- key = `${path}_${key}`;
240
- }
241
- key = key.toUpperCase();
242
- const value = process.env[key];
243
- if (value === '') {
244
- return null;
245
- }
246
- return value;
247
- }
248
-
249
- function getArg(key, path = '') {
250
- if (path) {
251
- key = `${path}.${key}`;
252
- }
253
- key = `--${key}=`;
254
- const keyValue = process.argv.find((arg) => arg.startsWith(key));
255
- if (keyValue == null) {
256
- return;
257
- }
258
-
259
- const [, value] = keyValue.split('=');
260
- if (value === '') {
261
- return null;
262
- }
263
- return value;
264
- }
265
-
266
- /**
267
- * @ignore
268
- */
269
- class FsStub {
270
- /** @type {Record<string, string>} */
271
- #files;
272
-
273
- /**
274
- * @param {Record<string, string>} files
275
- */
276
- constructor(files) {
277
- this.#files = files;
278
- }
279
-
280
- readFile(path) {
281
- const fileContent = this.#files[path];
282
- if (fileContent == null) {
283
- const err = new Error(`No such file or directory`);
284
- // @ts-ignore NodeJS error code
285
- err.code = 'ENOENT';
286
- throw err;
287
- }
288
-
289
- return JSON.stringify(fileContent);
290
- }
291
- }
@@ -1,25 +0,0 @@
1
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
2
-
3
- /**
4
- * @import express from 'express'
5
- */
6
-
7
- // TODO Remove dependency to express
8
-
9
- export function runSafe(/** @type {express.RequestHandler} */ handler) {
10
- // TODO runSafe is obsolete with with Express 5
11
- return async (request, response, next) => {
12
- try {
13
- await handler(request, response, next);
14
- } catch (error) {
15
- next(error);
16
- }
17
- };
18
- }
19
-
20
- export function reply(
21
- /** @type {express.Response} */ response,
22
- { status = 200, headers = { 'Content-Type': 'text/plain' }, body = '' } = {},
23
- ) {
24
- response.status(status).header(headers).send(body);
25
- }
package/lib/node/index.js DELETED
@@ -1,9 +0,0 @@
1
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
2
-
3
- export * from './actuator-controller.js';
4
- export * from './configuration-properties.js';
5
- export * from './handler.js';
6
- export * from './logging.js';
7
- export * from './long-polling.js';
8
- export * from './sse-emitter.js';
9
- export * from './static-files-controller.js';
@@ -1,60 +0,0 @@
1
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
2
-
3
- /**
4
- * @import { LogRecord } from '../logging.js';
5
- */
6
-
7
- import fsPromises from 'node:fs/promises';
8
-
9
- import { Handler } from '../logging.js';
10
-
11
- /**
12
- * A `Handler` that writes log messages to a file.
13
- *
14
- * @extends {Handler}
15
- */
16
- export class FileHandler extends Handler {
17
- #filename;
18
- #limit;
19
-
20
- /**
21
- * Initialize a new `FileHandler`.
22
- *
23
- * @param {string} filename The name of the file to write log messages to.
24
- * @param {number} [limit=0] The maximum size of the file in bytes before it
25
- * is rotated.
26
- */
27
- constructor(filename, limit = 0) {
28
- super();
29
- this.#filename = filename;
30
- this.#limit = limit < 0 ? 0 : limit;
31
- }
32
-
33
- /** @override */
34
- async publish(/** @type {LogRecord} */ record) {
35
- if (!this.isLoggable(record.level)) {
36
- return;
37
- }
38
-
39
- const message = this.formatter.format(record);
40
- if (this.#limit > 0) {
41
- try {
42
- const stats = await fsPromises.stat(this.#filename);
43
- const fileSize = stats.size;
44
- const newSize = fileSize + message.length;
45
- if (newSize > this.#limit) {
46
- await fsPromises.rm(this.#filename);
47
- }
48
- } catch (error) {
49
- // @ts-ignore NodeJS error code
50
- if (error.code !== 'ENOENT') {
51
- // ignore error if file does not exist
52
- console.error(error);
53
- } else {
54
- throw error;
55
- }
56
- }
57
- }
58
- await fsPromises.appendFile(this.#filename, message + '\n');
59
- }
60
- }
@@ -1,83 +0,0 @@
1
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
2
-
3
- /**
4
- * @import * as express from 'express'
5
- */
6
-
7
- import * as handler from './handler.js';
8
-
9
- // TODO Remove dependency to express
10
-
11
- export class LongPolling {
12
- #version = 0;
13
- #waiting = [];
14
- #getData;
15
-
16
- constructor(/** @type {function(): Promise<*>} */ getData) {
17
- this.#getData = getData;
18
- }
19
-
20
- async poll(
21
- /** @type {express.Request} */ request,
22
- /** @type {express.Response} */ response,
23
- ) {
24
- if (this.#isCurrentVersion(request)) {
25
- const responseData = await this.#tryLongPolling(request);
26
- handler.reply(response, responseData);
27
- } else {
28
- const responseData = await this.#getResponse();
29
- handler.reply(response, responseData);
30
- }
31
- }
32
-
33
- async send() {
34
- this.#version++;
35
- const response = await this.#getResponse();
36
- this.#waiting.forEach((resolve) => resolve(response));
37
- this.#waiting = [];
38
- }
39
-
40
- #isCurrentVersion(/** @type {express.Request} */ request) {
41
- const tag = /"(.*)"/.exec(request.get('If-None-Match'));
42
- return tag && tag[1] === String(this.#version);
43
- }
44
-
45
- #tryLongPolling(/** @type {express.Request} */ request) {
46
- const time = this.#getPollingTime(request);
47
- if (time == null) {
48
- return { status: 304 };
49
- }
50
-
51
- return this.#waitForChange(time);
52
- }
53
-
54
- #getPollingTime(/** @type {express.Request} */ request) {
55
- const wait = /\bwait=(\d+)/.exec(request.get('Prefer'));
56
- return wait != null ? Number(wait[1]) : null;
57
- }
58
-
59
- #waitForChange(/** @type {number} */ time) {
60
- return new Promise((resolve) => {
61
- this.#waiting.push(resolve);
62
- setTimeout(() => {
63
- if (this.#waiting.includes(resolve)) {
64
- this.#waiting = this.#waiting.filter((r) => r !== resolve);
65
- resolve({ status: 304 });
66
- }
67
- }, time * 1000);
68
- });
69
- }
70
-
71
- async #getResponse() {
72
- const data = await this.#getData();
73
- const body = JSON.stringify(data);
74
- return {
75
- headers: {
76
- 'Content-Type': 'application/json',
77
- ETag: `"${this.#version}"`,
78
- 'Cache-Control': 'no-store',
79
- },
80
- body,
81
- };
82
- }
83
- }
@@ -1,104 +0,0 @@
1
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
2
-
3
- /**
4
- * @import http from 'node:http'
5
- */
6
-
7
- /**
8
- * An object for sending
9
- * [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events).
10
- */
11
- export class SseEmitter {
12
- /** @type {?number} */ #timeout;
13
- /** @type {http.ServerResponse|undefined} */ #response;
14
-
15
- /**
16
- * Creates a new SSE emitter with an optional timeout.
17
- *
18
- * @param {number} [timeout] The timeout in milliseconds after which the
19
- * connection will be closed.
20
- */
21
- constructor(timeout) {
22
- this.#timeout = timeout;
23
- }
24
-
25
- /**
26
- * The timeout in milliseconds after which the connection will be closed or
27
- * undefined if no timeout is set.
28
- *
29
- * @type {number|undefined}
30
- */
31
- get timeout() {
32
- return this.#timeout;
33
- }
34
-
35
- /**
36
- * Sets and extends the response object for sending Server-Sent Events.
37
- *
38
- * @param {http.ServerResponse} outputMessage The response object to use.
39
- */
40
- extendResponse(outputMessage) {
41
- // TODO check HTTP version, is it HTTP/2 when using EventSource?
42
- outputMessage.statusCode = 200;
43
- this.#response = outputMessage
44
- .setHeader('Content-Type', 'text/event-stream')
45
- .setHeader('Cache-Control', 'no-cache')
46
- .setHeader('Keep-Alive', 'timeout=60')
47
- .setHeader('Connection', 'keep-alive');
48
-
49
- if (this.timeout != null) {
50
- const timeoutId = setTimeout(() => this.#close(), this.timeout);
51
- this.#response.addListener('close', () => clearTimeout(timeoutId));
52
- }
53
- }
54
-
55
- /**
56
- * Sends a SSE event.
57
- *
58
- * @param {object} event The event to send.
59
- * @param {string} [event.id] Add a SSE "id" line.
60
- * @param {string} [event.name] Add a SSE "event" line.
61
- * @param {number} [event.reconnectTime] Add a SSE "retry" line.
62
- * @param {string} [event.comment] Add a SSE "comment" line.
63
- * @param {string|object} [event.data] Add a SSE "data" line.
64
- */
65
- send({ id, name, reconnectTime, comment, data } = {}) {
66
- if (comment != null) {
67
- this.#response.write(`: ${comment}\n`);
68
- }
69
-
70
- if (name != null) {
71
- this.#response.write(`event: ${name}\n`);
72
- }
73
-
74
- if (data != null) {
75
- if (typeof data === 'object') {
76
- data = JSON.stringify(data);
77
- } else {
78
- data = String(data).replaceAll('\n', '\ndata: ');
79
- }
80
- this.#response.write(`data: ${data}\n`);
81
- }
82
-
83
- if (id != null) {
84
- this.#response.write(`id: ${id}\n`);
85
- }
86
-
87
- if (reconnectTime != null) {
88
- this.#response.write(`retry: ${reconnectTime}\n`);
89
- }
90
-
91
- this.#response.write('\n');
92
- }
93
-
94
- /**
95
- * Simulates a timeout.
96
- */
97
- simulateTimeout() {
98
- this.#close();
99
- }
100
-
101
- #close() {
102
- this.#response.end();
103
- }
104
- }
@@ -1,15 +0,0 @@
1
- // Copyright (c) 2024 Falko Schumann. All rights reserved. MIT license.
2
-
3
- import path from 'node:path';
4
- import express from 'express';
5
-
6
- export class StaticFilesController {
7
- /**
8
- * @param {express.Express} app
9
- * @param {string} [directory="./public"]
10
- * @param {string} [route="/"]
11
- */
12
- constructor(app, directory = './public', route = '/') {
13
- app.use(route, express.static(path.join(directory)));
14
- }
15
- }
@@ -1,89 +0,0 @@
1
- // Copyright (c) 2023-2024 Falko Schumann. All rights reserved. MIT license.
2
-
3
- /**
4
- * Tracks output events.
5
- *
6
- * This is one of the nullability patterns from James Shore's article on
7
- * [testing without mocks](https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks#output-tracking).
8
- *
9
- * Example implementation of an event store:
10
- *
11
- * ```javascript
12
- * async record(event) {
13
- * // ...
14
- * this.dispatchEvent(new CustomEvent(EVENT_RECORDED_EVENT, { detail: event }));
15
- * }
16
- *
17
- * trackEventsRecorded() {
18
- * return new OutputTracker(this, EVENT_RECORDED_EVENT);
19
- * }
20
- * ```
21
- *
22
- * Example usage:
23
- *
24
- * ```javascript
25
- * const eventsRecorded = eventStore.trackEventsRecorded();
26
- * // ...
27
- * const data = eventsRecorded.data(); // [event1, event2, ...]
28
- * ```
29
- */
30
- export class OutputTracker {
31
- /**
32
- * Creates a tracker for a specific event of an event target.
33
- *
34
- * @param {EventTarget} eventTarget The event target to track.
35
- * @param {string} event The event name to track.
36
- */
37
- static create(eventTarget, event) {
38
- return new OutputTracker(eventTarget, event);
39
- }
40
-
41
- #eventTarget;
42
- #event;
43
- #tracker;
44
- #data = [];
45
-
46
- /**
47
- * Creates a tracker for a specific event of an event target.
48
- *
49
- * @param {EventTarget} eventTarget The event target to track.
50
- * @param {string} event The event name to track.
51
- */
52
- constructor(
53
- /** @type {EventTarget} */ eventTarget,
54
- /** @type {string} */ event,
55
- ) {
56
- this.#eventTarget = eventTarget;
57
- this.#event = event;
58
- this.#tracker = (event) => this.#data.push(event.detail);
59
-
60
- this.#eventTarget.addEventListener(this.#event, this.#tracker);
61
- }
62
-
63
- /**
64
- * Returns the tracked data.
65
- *
66
- * @return {Array} The tracked data.
67
- */
68
- get data() {
69
- return this.#data;
70
- }
71
-
72
- /**
73
- * Clears the tracked data and returns the cleared data.
74
- *
75
- * @return {Array} The cleared data.
76
- */
77
- clear() {
78
- const result = [...this.#data];
79
- this.#data.length = 0;
80
- return result;
81
- }
82
-
83
- /**
84
- * Stops tracking.
85
- */
86
- stop() {
87
- this.#eventTarget.removeEventListener(this.#event, this.#tracker);
88
- }
89
- }