@vida-global/core 1.4.4 → 1.4.6

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/AGENTS.md ADDED
@@ -0,0 +1,13 @@
1
+ # vida-core Guide
2
+
3
+ Use this guide to understand and write code for the vida-core repo.
4
+
5
+ # Style and conventions
6
+ @agents/style.md
7
+ @agents/server.md
8
+ @agents/db.md
9
+ @agents/apis.md
10
+ @agents/testing.md
11
+
12
+
13
+ # Helpers for common use cases
@@ -30,3 +30,13 @@ const body = {baz: 1, ban: 2};
30
30
  const response = await client.post("/foo/bar", { body });
31
31
  ```
32
32
 
33
+ ## Aborting requests
34
+ All request methods accept `timeout` (ms) and/or a caller-supplied `signal` (`AbortSignal`). When both are provided, they are composed via `AbortSignal.any` — whichever fires first aborts the request. If the request is aborted (by the timeout or the external signal), an `HttpAbortError` is thrown with `reason` set to either `'timed out'` or `'aborted'`.
35
+
36
+ ```
37
+ await client.post("/foo/bar", { body, timeout: 5000 });
38
+
39
+ const ctrl = new AbortController();
40
+ await client.get("/foo/bar", { signal: ctrl.signal });
41
+ ```
42
+
@@ -1,38 +1,67 @@
1
- const { logger } = require('../logger');
2
- const { HttpError } = require('./error');
1
+ const { logger } = require('../logger');
2
+ const { HttpError, HttpAbortError } = require('./error');
3
3
 
4
4
 
5
5
  class HttpClient {
6
- async get(endpoint, { requestParams, headers }={}) {
6
+ async get(endpoint, { requestParams, headers, timeout, signal }={}) {
7
7
  return await this.#makeRequest(endpoint, "GET", arguments[1]);
8
8
  }
9
9
 
10
10
 
11
- async post(endpoint, { body, headers }={}) {
11
+ async post(endpoint, { body, headers, timeout, signal }={}) {
12
12
  return await this.#makeRequest(endpoint, "POST", arguments[1]);
13
13
  }
14
14
 
15
15
 
16
- async put(endpoint, { body, headers }={}) {
16
+ async put(endpoint, { body, headers, timeout, signal }={}) {
17
17
  return await this.#makeRequest(endpoint, "PUT", arguments[1]);
18
18
  }
19
19
 
20
20
 
21
- async delete(endpoint, { requestParams, headers }={}) {
21
+ async delete(endpoint, { requestParams, headers, timeout, signal }={}) {
22
22
  return await this.#makeRequest(endpoint, "DELETE", arguments[1]);
23
23
  }
24
24
 
25
25
 
26
- async #makeRequest(endpoint, method, { requestParams, body, headers }={}) {
26
+ async #makeRequest(endpoint, method, { requestParams, body, headers, timeout, signal }={}) {
27
27
  this.#logRequest(method, endpoint);
28
28
 
29
29
  endpoint = `${this.urlRoot}${endpoint}`;
30
30
  const url = new URL(endpoint);
31
31
  const requestData = structuredClone(requestParams || body || {});
32
- const payload = this.#prepareRequestPayload(url, requestData, headers, method);
32
+ const payload = this.#prepareRequestPayload(url, requestData, headers, method, timeout, signal);
33
33
 
34
- const res = await fetch(url.toString(), payload);
34
+ try {
35
+ const res = await fetch(url.toString(), payload);
36
+ return await this.#handleRequestResponse(res, payload);
37
+ } catch (err) {
38
+ this.#handleRequestError(err, payload, timeout, signal);
39
+ }
40
+ }
41
+
42
+
43
+ #handleRequestError(err, payload, timeout, externalSignal) {
44
+ if (this.#isAbortError(err)) {
45
+ const msg = this.#classifyAbortReason(timeout, externalSignal)
46
+ throw new HttpAbortError(msg, payload);
47
+ }
48
+ throw err;
49
+ }
50
+
51
+
52
+ #isAbortError(err) {
53
+ return err instanceof DOMException && (err.name === 'AbortError' || err.name === 'TimeoutError');
54
+ }
35
55
 
56
+
57
+ #classifyAbortReason(timeout, externalSignal) {
58
+ if (externalSignal?.aborted) return 'aborted';
59
+ if (timeout) return 'timed out';
60
+ return 'aborted';
61
+ }
62
+
63
+
64
+ async #handleRequestResponse(res, payload) {
36
65
  if (res.status && res.status < 300) {
37
66
  const json = await res.json();
38
67
  return {data: json, status: res.status};
@@ -42,7 +71,7 @@ class HttpClient {
42
71
  }
43
72
 
44
73
 
45
- #prepareRequestPayload(url, requestData, headers, method) {
74
+ #prepareRequestPayload(url, requestData, headers, method, timeout, signal) {
46
75
  const payload = {};
47
76
 
48
77
  headers ||= {};
@@ -50,10 +79,23 @@ class HttpClient {
50
79
  payload.method = method;
51
80
  this.#addRequestData(payload, requestData, method, url, payload.headers);
52
81
 
82
+ const composedSignal = this.#composeSignal(signal, timeout);
83
+ if (composedSignal) payload.signal = composedSignal;
84
+
53
85
  return payload;
54
86
  }
55
87
 
56
88
 
89
+ #composeSignal(externalSignal, timeoutMs) {
90
+ const signals = [];
91
+ if (externalSignal) signals.push(externalSignal);
92
+ if (timeoutMs) signals.push(AbortSignal.timeout(timeoutMs));
93
+ if (signals.length === 0) return undefined;
94
+ if (signals.length === 1) return signals[0];
95
+ return AbortSignal.any(signals);
96
+ }
97
+
98
+
57
99
  #addRequestData(payload, requestData, method, url, headers) {
58
100
  if (method == "POST" || method == "PUT") {
59
101
  payload.body = JSON.stringify(requestData || {});
@@ -123,7 +165,8 @@ class HttpClient {
123
165
  }
124
166
 
125
167
 
126
- module.exports = {
168
+ module.exports = {
127
169
  HttpClient,
128
- HttpError
170
+ HttpError,
171
+ HttpAbortError
129
172
  }
package/lib/http/error.js CHANGED
@@ -29,6 +29,24 @@ class HttpError extends Error {
29
29
  }
30
30
 
31
31
 
32
+ class HttpAbortError extends Error {
33
+ #requestPayload;
34
+ #reason;
35
+
36
+
37
+ constructor(reason, requestPayload) {
38
+ super(`HTTP request ${reason}`);
39
+ this.#reason = reason;
40
+ this.#requestPayload = requestPayload;
41
+ }
42
+
43
+
44
+ get reason() { return this.#reason; }
45
+ get requestPayload() { return structuredClone(this.#requestPayload); }
46
+ }
47
+
48
+
32
49
  module.exports = {
33
- HttpError
50
+ HttpError,
51
+ HttpAbortError
34
52
  }
@@ -1,2 +1,33 @@
1
- ## Logger ##
2
- A standard logger to be used across all Vida applications. Currently, a simple implementation of `pino`, supports the full `pino` API.
1
+ # Logger
2
+ A simple logger that support multiple scopes and log levels. Set the `LOG_LEVEL` environment variable to determine what log levels are output.
3
+
4
+ ## Scopes
5
+ ```
6
+ logger.addScope('foo');
7
+ logger.addScope('bar');
8
+
9
+ logger.foo.debug('my log');
10
+ // [2026-04-30 16:16:12.797 -0700][DEBUG][FOO] my log
11
+
12
+ logger.bar.verbose('my log');
13
+ // [2026-04-30 16:16:12.797 -0700][VERBOSE][BAR] my log
14
+
15
+ logger.bar.info('my log');
16
+ // [2026-04-30 16:16:12.797 -0700][INFO][BAR] my log
17
+
18
+ logger.bar.warn('my log');
19
+ // [2026-04-30 16:16:12.797 -0700][WARN][BAR] my log
20
+
21
+ logger.bar.error('my log');
22
+ // [2026-04-30 16:16:12.797 -0700][ERROR][BAR] my log
23
+ ```
24
+
25
+ ## IDs
26
+ ```
27
+ logger.addScope('http');
28
+ const requestId = 123;
29
+ const requestLogger = logger.http.createChild(requestId);
30
+
31
+ requestLogger.debug('my log');
32
+ // [2026-04-30 16:16:12.797 -0700][DEBUG][HTTP][123] my log
33
+ ``
@@ -1,25 +1,104 @@
1
- const pino = require('pino');
2
-
3
- const config = {
4
- level: process.env.LOG_LEVEL || 'info',
5
- customLevels: {
6
- test: 1000
7
- }
8
- };
9
-
10
- if (!process.env.ENV_VERCEL) {
11
- config.transport = {
12
- target: 'pino-pretty',
13
- options: {
14
- colorize: true,
15
- ignore: 'pid,hostname',
16
- translateTime: 'SYS:standard',
17
- messageFormat: '{msg}',
1
+ const winston = require('winston');
2
+ const { colorize, combine, printf, timestamp } = winston.format;
3
+
4
+
5
+ class Logger {
6
+ #id;
7
+ #logger;
8
+ #scope;
9
+
10
+
11
+ constructor(scope, id) {
12
+ this.#id = id;
13
+ this.#scope = scope || 'API';
14
+ this.#logger = winston.createLogger(this.loggerOptions);
15
+ }
16
+
17
+
18
+ get scope() { return this.#scope; }
19
+ get id() { return this.#id; }
20
+
21
+
22
+ get loggerOptions() {
23
+ const format = combine(...this.formatters);
24
+ const level = this.level;
25
+ const transports = this.transports;
26
+
27
+ return { format, level, transports };
28
+ }
29
+
30
+
31
+ get formatters() {
32
+ const colors = {
33
+ debug: 'magenta',
34
+ verbose: 'green',
35
+ info: 'cyan',
36
+ warn: 'yellow',
37
+ error: 'red',
38
+ }
39
+ const messageFormatter = winston.format(this.messageFormatter.bind(this));
40
+ return [
41
+ timestamp({format: 'YYYY-MM-DD HH:mm:ss.SSS ZZ'}),
42
+ messageFormatter(),
43
+ colorize({all: true, colors}),
44
+ printf(({ message }) => message)
45
+ ];
46
+ }
47
+
48
+
49
+ messageFormatter(info) {
50
+ const level = info.level.toUpperCase()
51
+
52
+ let prefix = `[${info.timestamp}][${level}][${this.scope.toUpperCase()}]`;
53
+ if (this.id) prefix = `${prefix}[${this.id}]`;
54
+
55
+ info.message = `${prefix} ${info.message}`;
56
+ return info;
57
+ }
58
+
59
+
60
+ get level() {
61
+ return process.env.LOG_LEVEL || 'info';
62
+ }
63
+
64
+
65
+ get transports() {
66
+ const transports = [ new winston.transports.Console() ];
67
+ return transports;
68
+ }
69
+
70
+
71
+ addScope(scope) {
72
+ const existing = this[scope];
73
+ if (existing && !existing instanceof Logger) {
74
+ throw `Property already exists at ${scope}`;
18
75
  }
19
- };
76
+
77
+ const logger = new Logger(scope, this.id);
78
+ Object.defineProperty(this, scope, { get: () => logger });
79
+ }
80
+
81
+
82
+ createChild(id) {
83
+ return new Logger(this.scope, id);
84
+ }
85
+
86
+
87
+ debug(msg) { this.#logger.debug(msg) }
88
+ error(msg) { this.#logger.error(msg) }
89
+ info(msg) { this.#logger.info(msg) }
90
+ silly(msg) { this.#logger.silly(msg) }
91
+ verbose(msg) { this.#logger.verbose(msg) }
92
+ warn(msg) { this.#logger.warn(msg) }
93
+
94
+ get _winstonLogger() {
95
+ this.warn('Directly accessing the underlying winston logger should be avoided unless necessary');
96
+ return this.#logger
97
+ }
20
98
  }
21
99
 
22
- const logger = pino(config);
100
+
101
+ const logger = new Logger();
23
102
 
24
103
 
25
- module.exports = { logger };
104
+ module.exports = { logger, Logger };
@@ -0,0 +1,39 @@
1
+ const { logger } = require('./index');
2
+ const expressWinston = require('express-winston');
3
+
4
+
5
+ const msgFormatter = (req, res) => {
6
+ let msg;
7
+ if (req.controller) {
8
+ msg = `${req.controller.constructor.name}#${req.action}`;
9
+ } else {
10
+ msg = res.statusCode >= 500 ? 'request errored' : 'request completed';
11
+ }
12
+
13
+ const headerPcs = res._header.split("\n").map(h => h.split(/:(.*)/));
14
+ const headers = Object.fromEntries(headerPcs);
15
+ const responseTime = headers['X-Response-Time'];
16
+
17
+ const requestDetails = `req: ${req.method} ${req.url.split('?')[0]} for ${req.ip}`;
18
+ const responseDetails = `res: statusCode=${res.statusCode} responseTime=${responseTime}`;
19
+
20
+ msg = `${msg}\n ${requestDetails}\n ${responseDetails}`;
21
+ if (res.error) {
22
+ msg = `${msg}\n${res.error.stack}`;
23
+ }
24
+
25
+ return msg;
26
+ };
27
+
28
+
29
+ logger.addScope('http');
30
+ const middleware = expressWinston.logger({
31
+ level: 'debug',
32
+ msg: msgFormatter,
33
+ winstonInstance: logger.http._winstonLogger,
34
+ });
35
+
36
+
37
+ module.exports = {
38
+ middleware
39
+ }
@@ -1,13 +1,13 @@
1
1
  const { ControllerImporter } = require('./controllerImporter');
2
2
  const express = require('express');
3
- const httpLogger = require('pino-http')
4
3
  const { logger } = require('../logger');
4
+ const loggingMiddleware = require('../logger/serverMiddleware');
5
5
  const mustacheExpress = require('mustache-express');
6
- const { pino } = require('pino');
7
6
  const responseTime = require('response-time');
8
7
  const IoServer = require("socket.io")
9
8
  const { Server } = require('http');
10
9
  const { SystemController } = require('./systemController');
10
+ const requestID = require( 'express-request-id');
11
11
 
12
12
 
13
13
  class VidaServer {
@@ -31,7 +31,7 @@ class VidaServer {
31
31
  await this.registerControllers();
32
32
 
33
33
  this.#httpServer.listen(this.#port, this.#host, () => {
34
- this.logger.info(`Server is running on port ${this.#port}`);
34
+ logger.info(`Server is running on port ${this.#port}`);
35
35
  if (callback) callback();
36
36
  });
37
37
  }
@@ -39,7 +39,7 @@ class VidaServer {
39
39
 
40
40
  get host() { return this.#host; }
41
41
  get port() { return this.#port; }
42
- get logger() { return logger; }
42
+ get logger() { return logger.http; }
43
43
 
44
44
 
45
45
  /***********************************************************************************************
@@ -72,11 +72,12 @@ class VidaServer {
72
72
  setupMiddleware() {
73
73
  this.use(this.jsonParsingMiddleware);
74
74
  this.use(this.octetStreamParsingMiddleware);
75
- this.use(responseTime())
75
+ this.use(responseTime({suffix: true}))
76
76
  this.use(express.static('public'))
77
77
  this.use(this.loggingMiddleware);
78
78
  this.use('/static', express.static(this.staticFilesDirectory));
79
79
  this.use(this.connectionAbortedMiddleware);
80
+ this.use(requestID());
80
81
 
81
82
  this.#expressServer.engine('html', mustacheExpress());
82
83
  }
@@ -105,15 +106,7 @@ class VidaServer {
105
106
 
106
107
 
107
108
  get loggingMiddleware() {
108
- return httpLogger({
109
- logger: this.middlewareLogger,
110
- customLogLevel: this.requestLogLevel.bind(this),
111
- customSuccessMessage: this.requestLogMessage.bind(this),
112
- customErrorMessage: this.requestLogMessage.bind(this),
113
- customErrorObject: this.requestLogDetails.bind(this),
114
- customSuccessObject: this.requestLogDetails.bind(this),
115
- wrapSerializers: false,
116
- })
109
+ return loggingMiddleware.middleware
117
110
  }
118
111
 
119
112
 
@@ -135,53 +128,6 @@ class VidaServer {
135
128
  use() { this.#expressServer.use(...arguments); }
136
129
 
137
130
 
138
- /***********************************************************************************************
139
- * LOGGING
140
- ***********************************************************************************************/
141
- requestLogDetails(req, res, err) {
142
- const details = {
143
- req: `${req.method} ${req.url}`,
144
- res: `statusCode=${res.statusCode}, responseTime=${res.get('X-Response-Time')}`,
145
- };
146
-
147
- if (res.statusCode >= 500 && res.error) {
148
- details.error = this.requestLogErrorDetails(req, res, res.error);
149
- }
150
-
151
- return details;
152
- }
153
-
154
-
155
- requestLogErrorDetails(req, res, err) {
156
- if (typeof err == 'string') return {message: err};
157
- return {
158
- type: err.constructor.name,
159
- message: err.message,
160
- stack: err.stack
161
- }
162
- }
163
-
164
-
165
- requestLogMessage(req, res) {
166
- const prefix = '[VidaServer]';
167
- if (req.controller) {
168
- return `${prefix} ${req.controller.constructor.name}#${req.action}`;
169
- }
170
-
171
- return `${prefix} ${res.status >= 500 ? 'request errored' : 'request completed'}`;
172
- }
173
-
174
-
175
- requestLogLevel(req, res) {
176
- return 'debug';
177
- }
178
-
179
-
180
- get middlewareLogger() {
181
- return logger;
182
- }
183
-
184
-
185
131
  /***********************************************************************************************
186
132
  * SETTINGS
187
133
  ***********************************************************************************************/
@@ -234,7 +180,7 @@ class VidaServer {
234
180
  const method = action.method.toLowerCase();
235
181
  const requestHandler = this.requestHandler(action.action, controllerCls)
236
182
  if (process.env.NODE_ENV != 'test') {
237
- logger.info(`ROUTE: ${method.toUpperCase().padEnd(6)} ${action.path}`);
183
+ logger.verbose(`ROUTE: ${method.toUpperCase().padEnd(6)} ${action.path}`);
238
184
  }
239
185
  this['_'+method](action.path, requestHandler);
240
186
  }