webspresso 0.0.107 → 0.0.108
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/AGENTS.md +4 -1
- package/.agents/CACHE.md +1 -0
- package/.agents/CONTEXT.md +152 -0
- package/.agents/ORM.md +74 -0
- package/.agents/SHUTDOWN.md +8 -1
- package/.agents/skills/webspresso-framework/SKILL.md +2 -1
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +9 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +18 -2
- package/.agents/skills/webspresso-services/SKILL.md +36 -3
- package/AGENTS.md +6 -3
- package/core/app.js +7 -5
- package/core/applySchema.js +3 -72
- package/core/cache/lru.js +10 -0
- package/core/cache/tagged-lru.js +4 -0
- package/core/compileSchema.js +3 -68
- package/core/compression/compressible.js +53 -6
- package/core/compression/negotiate.js +31 -12
- package/core/context/app-context.js +201 -0
- package/core/context/index.js +55 -0
- package/core/context/request.js +241 -0
- package/core/events/dispatcher.js +144 -15
- package/core/events/signal.js +9 -0
- package/core/health/probes.js +12 -2
- package/core/http/index.js +10 -1
- package/core/http/pipeline.js +519 -368
- package/core/index.js +24 -1
- package/core/orm/eager-loader.js +90 -55
- package/core/orm/index.js +3 -39
- package/core/orm/model.js +6 -1
- package/core/orm/schema-helpers.js +59 -0
- package/core/orm/transaction.js +109 -14
- package/core/plugins/index.js +8 -0
- package/core/plugins/plugin-manager.js +1098 -0
- package/core/queue/manager.js +55 -27
- package/core/rate-limit/stores/memory.js +23 -10
- package/core/resilience/circuit-breaker.js +34 -14
- package/core/schema-cache.js +2 -64
- package/core/services/builtins/auth.js +273 -0
- package/core/services/builtins/exchange.js +229 -0
- package/core/services/builtins/file-manager.js +178 -0
- package/core/services/builtins/mail.js +134 -0
- package/core/services/builtins/media.js +196 -0
- package/core/services/builtins/system.js +219 -0
- package/core/services/discovery.js +133 -0
- package/core/services/executor.js +629 -0
- package/core/services/index.js +110 -5
- package/core/services/loader.js +161 -0
- package/core/services/memoize.js +408 -0
- package/core/services/registry.js +731 -0
- package/core/services/validator.js +424 -0
- package/core/shutdown/shutdown-manager.js +146 -85
- package/core/ssr/stream.js +109 -16
- package/core/stream/text-stream.js +45 -4
- package/core/utils/index.js +110 -0
- package/core/utils/timeout.js +4 -0
- package/core/validation/apply.js +74 -0
- package/core/validation/cache.js +66 -0
- package/core/validation/compiler.js +77 -0
- package/core/validation/index.js +22 -1
- package/index.d.ts +77 -4
- package/index.js +64 -20
- package/package.json +1 -1
- package/src/app-context.js +4 -196
- package/src/dev/typegen.js +1 -1
- package/src/plugin-manager.js +5 -1086
- package/src/server.js +4 -1
- package/src/services/builtins/auth.js +2 -273
- package/src/services/builtins/exchange.js +2 -229
- package/src/services/builtins/file-manager.js +2 -178
- package/src/services/builtins/mail.js +2 -134
- package/src/services/builtins/media.js +2 -196
- package/src/services/builtins/system.js +2 -219
- package/src/services/discovery.js +2 -133
- package/src/services/executor.js +2 -575
- package/src/services/index.js +4 -109
- package/src/services/loader.js +1 -119
- package/src/services/memoize.js +2 -408
- package/src/services/registry.js +1 -711
- package/src/services/validator.js +2 -424
- package/core/utils/array.js +0 -12
- package/core/utils/base64url.js +0 -8
- package/core/utils/ensure-leading-slash.js +0 -6
- package/core/utils/escape-html.js +0 -7
- package/core/utils/escape-regexp.js +0 -7
- package/core/utils/get-client-ip.js +0 -6
- package/core/utils/hmac.js +0 -6
- package/core/utils/random.js +0 -9
- package/core/utils/safe-compare.js +0 -3
- package/core/utils/sha256.js +0 -6
- package/core/utils/strip-trailing-slash.js +0 -6
- package/core/utils/walk.js +0 -7
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso Ambient Request & Execution Context
|
|
5
|
+
* Zero-dependency execution context powered by Node.js AsyncLocalStorage.
|
|
6
|
+
* Provides composable hooks (useContext, useDb, useAuth, useUser, useLogger, useService,
|
|
7
|
+
* useReq, useRes, useParams, useQuery, useQueue, useCache, useBreaker, useTransaction, withContext)
|
|
8
|
+
* across loaders, services, models, and background workers without prop drilling.
|
|
9
|
+
*
|
|
10
|
+
* @module core/context/request
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
14
|
+
|
|
15
|
+
const requestContextStorage = new AsyncLocalStorage();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Runs a callback within an ambient request or execution context
|
|
19
|
+
* @param {Object} context - Execution context payload
|
|
20
|
+
* @param {Function} fn - Function to execute within context
|
|
21
|
+
* @returns {*} Return value of fn
|
|
22
|
+
*/
|
|
23
|
+
function runWithContext(context, fn) {
|
|
24
|
+
return requestContextStorage.run(context || {}, fn);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Run a callback with shallow-merged overrides on top of the current ambient context
|
|
29
|
+
* @param {Object} overrides - Object containing fields to override in current context
|
|
30
|
+
* @param {Function} fn - Function to execute
|
|
31
|
+
* @returns {*} Return value of fn
|
|
32
|
+
*/
|
|
33
|
+
function withContext(overrides, fn) {
|
|
34
|
+
const current = requestContextStorage.getStore() || {};
|
|
35
|
+
const merged = { ...current, ...(overrides || {}) };
|
|
36
|
+
return requestContextStorage.run(merged, fn);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get the current active ambient context (returns null if outside context)
|
|
41
|
+
* @returns {Object|null}
|
|
42
|
+
*/
|
|
43
|
+
function getContext() {
|
|
44
|
+
return requestContextStorage.getStore() || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Hook to access the ambient execution context
|
|
49
|
+
* @returns {Object} Active context object
|
|
50
|
+
* @throws {Error} If called outside of an active execution context
|
|
51
|
+
*/
|
|
52
|
+
function useContext() {
|
|
53
|
+
const store = requestContextStorage.getStore();
|
|
54
|
+
if (!store) {
|
|
55
|
+
throw new Error('useContext() must be called within an active request, job, or execution context.');
|
|
56
|
+
}
|
|
57
|
+
return store;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Hook to access the ambient database instance
|
|
62
|
+
* @returns {Object|null} Database / ORM instance or null
|
|
63
|
+
*/
|
|
64
|
+
function useDb() {
|
|
65
|
+
const store = requestContextStorage.getStore();
|
|
66
|
+
return store?.db || store?.req?.db || null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Hook to access the ambient authenticated user / principal
|
|
71
|
+
* @returns {Object|null} User / Auth object or null
|
|
72
|
+
*/
|
|
73
|
+
function useAuth() {
|
|
74
|
+
const store = requestContextStorage.getStore();
|
|
75
|
+
return (
|
|
76
|
+
store?.user ||
|
|
77
|
+
store?.auth?.user ||
|
|
78
|
+
store?.auth ||
|
|
79
|
+
store?.req?.user ||
|
|
80
|
+
store?.req?.auth?.user ||
|
|
81
|
+
store?.req?.auth ||
|
|
82
|
+
null
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Hook to access the ambient authenticated user
|
|
88
|
+
* Alias / dedicated helper for useAuth()
|
|
89
|
+
* @returns {Object|null} User object or null
|
|
90
|
+
*/
|
|
91
|
+
function useUser() {
|
|
92
|
+
return useAuth();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Hook to access the ambient logger
|
|
97
|
+
* @returns {Object} Logger instance
|
|
98
|
+
*/
|
|
99
|
+
function useLogger() {
|
|
100
|
+
const store = requestContextStorage.getStore();
|
|
101
|
+
return store?.logger || store?.req?.logger || console;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Hook to access the ambient HTTP request object
|
|
106
|
+
* @returns {Object|null} Express/Hono request object or null if outside HTTP request
|
|
107
|
+
*/
|
|
108
|
+
function useReq() {
|
|
109
|
+
const store = requestContextStorage.getStore();
|
|
110
|
+
return store?.req || null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Hook to access the ambient HTTP response object
|
|
115
|
+
* @returns {Object|null} Express/Hono response object or null if outside HTTP request
|
|
116
|
+
*/
|
|
117
|
+
function useRes() {
|
|
118
|
+
const store = requestContextStorage.getStore();
|
|
119
|
+
return store?.res || null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Hook to access the ambient route parameters
|
|
124
|
+
* @returns {Object} Route parameters map (e.g. { id: '123' })
|
|
125
|
+
*/
|
|
126
|
+
function useParams() {
|
|
127
|
+
const store = requestContextStorage.getStore();
|
|
128
|
+
return store?.params || store?.req?.params || {};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Hook to access the ambient URL query parameters
|
|
133
|
+
* @returns {Object} URL query parameters map
|
|
134
|
+
*/
|
|
135
|
+
function useQuery() {
|
|
136
|
+
const store = requestContextStorage.getStore();
|
|
137
|
+
return store?.query || store?.req?.query || {};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Hook to invoke a registered service using the ambient context
|
|
142
|
+
* @param {string} name - Service name (e.g. 'user.get')
|
|
143
|
+
* @param {*} [input={}] - Input payload
|
|
144
|
+
* @param {Object} [opts] - Optional execution options
|
|
145
|
+
* @returns {Promise<*>}
|
|
146
|
+
*/
|
|
147
|
+
async function useService(name, input = {}, opts) {
|
|
148
|
+
const store = requestContextStorage.getStore();
|
|
149
|
+
if (store?.service && typeof store.service === 'function') {
|
|
150
|
+
return store.service(name, input, opts);
|
|
151
|
+
}
|
|
152
|
+
if (store?.req?.service && typeof store.req.service === 'function') {
|
|
153
|
+
return store.req.service(name, input, opts);
|
|
154
|
+
}
|
|
155
|
+
throw new Error(`useService() failed: no service registry available in active context.`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Hook to access the ambient background queue
|
|
160
|
+
* @returns {Object|null}
|
|
161
|
+
*/
|
|
162
|
+
function useQueue() {
|
|
163
|
+
const store = requestContextStorage.getStore();
|
|
164
|
+
return store?.queue || store?.req?.queue || null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Hook to access the ambient cache
|
|
169
|
+
* @returns {Object|null}
|
|
170
|
+
*/
|
|
171
|
+
function useCache() {
|
|
172
|
+
const store = requestContextStorage.getStore();
|
|
173
|
+
return store?.cache || store?.req?.cache || null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Hook to access or create a circuit breaker by name
|
|
178
|
+
* @param {string} name - Circuit breaker name
|
|
179
|
+
* @param {Object} [options] - Circuit breaker options
|
|
180
|
+
* @returns {Object|null} CircuitBreaker instance or null
|
|
181
|
+
*/
|
|
182
|
+
function useBreaker(name, options) {
|
|
183
|
+
const store = requestContextStorage.getStore();
|
|
184
|
+
if (store?.breaker && typeof store.breaker === 'function') {
|
|
185
|
+
return store.breaker(name, options);
|
|
186
|
+
}
|
|
187
|
+
if (store?.req?.breaker && typeof store.req.breaker === 'function') {
|
|
188
|
+
return store.req.breaker(name, options);
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const { circuitBreakerRegistry } = require('../resilience');
|
|
192
|
+
return circuitBreakerRegistry.getBreaker(name, options);
|
|
193
|
+
} catch (_) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Hook to execute a callback within a managed transaction on the ambient database
|
|
200
|
+
* Automatically binds transactional context to ambient hooks inside the callback.
|
|
201
|
+
* @param {function(trx: any): Promise<*>} callback - Transaction callback
|
|
202
|
+
* @returns {Promise<*>} Result of callback
|
|
203
|
+
*/
|
|
204
|
+
async function useTransaction(callback) {
|
|
205
|
+
const db = useDb();
|
|
206
|
+
if (!db) {
|
|
207
|
+
throw new Error('useTransaction() failed: no database instance available in active context.');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (typeof db.transaction === 'function') {
|
|
211
|
+
return db.transaction(async (trx) => {
|
|
212
|
+
const current = requestContextStorage.getStore() || {};
|
|
213
|
+
const txDb = trx.db || trx;
|
|
214
|
+
const txCtx = { ...current, db: txDb, trx: trx.trx || trx };
|
|
215
|
+
return requestContextStorage.run(txCtx, () => callback(trx));
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
throw new Error('useTransaction() failed: db does not support transaction().');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
requestContextStorage,
|
|
224
|
+
runWithContext,
|
|
225
|
+
withContext,
|
|
226
|
+
getContext,
|
|
227
|
+
useContext,
|
|
228
|
+
useDb,
|
|
229
|
+
useAuth,
|
|
230
|
+
useUser,
|
|
231
|
+
useLogger,
|
|
232
|
+
useReq,
|
|
233
|
+
useRes,
|
|
234
|
+
useParams,
|
|
235
|
+
useQuery,
|
|
236
|
+
useService,
|
|
237
|
+
useQueue,
|
|
238
|
+
useCache,
|
|
239
|
+
useBreaker,
|
|
240
|
+
useTransaction,
|
|
241
|
+
};
|
|
@@ -324,7 +324,7 @@ class EventDispatcher extends EventEmitter {
|
|
|
324
324
|
} catch (err) {
|
|
325
325
|
// Emit error on dispatcher or log
|
|
326
326
|
if (this.listenerCount('error') > 0) {
|
|
327
|
-
|
|
327
|
+
this.emit('error', err);
|
|
328
328
|
} else if (this.logger && typeof this.logger.error === 'function') {
|
|
329
329
|
this.logger.error(`[EventDispatcher] Error in listener for event "${event}":`, err);
|
|
330
330
|
}
|
|
@@ -355,10 +355,19 @@ class EventDispatcher extends EventEmitter {
|
|
|
355
355
|
const meta = { event, sender, timestamp: Date.now() };
|
|
356
356
|
|
|
357
357
|
const promises = listeners.map(async (listener) => {
|
|
358
|
-
|
|
359
|
-
|
|
358
|
+
try {
|
|
359
|
+
if (listener.isAny) {
|
|
360
|
+
return await listener.handler(event, payload, meta);
|
|
361
|
+
}
|
|
362
|
+
return await listener.handler(payload, meta);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
if (this.listenerCount('error') > 0) {
|
|
365
|
+
this.emit('error', err);
|
|
366
|
+
} else if (this.logger && typeof this.logger.error === 'function') {
|
|
367
|
+
this.logger.error(`[EventDispatcher] Error in async listener for event "${event}":`, err);
|
|
368
|
+
}
|
|
369
|
+
throw err;
|
|
360
370
|
}
|
|
361
|
-
return listener.handler(payload, meta);
|
|
362
371
|
});
|
|
363
372
|
|
|
364
373
|
return Promise.allSettled(promises);
|
|
@@ -399,10 +408,19 @@ class EventDispatcher extends EventEmitter {
|
|
|
399
408
|
const results = [];
|
|
400
409
|
|
|
401
410
|
for (const listener of listeners) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
411
|
+
try {
|
|
412
|
+
const res = listener.isAny
|
|
413
|
+
? await listener.handler(event, payload, meta)
|
|
414
|
+
: await listener.handler(payload, meta);
|
|
415
|
+
results.push(res);
|
|
416
|
+
} catch (err) {
|
|
417
|
+
if (this.listenerCount('error') > 0) {
|
|
418
|
+
this.emit('error', err);
|
|
419
|
+
} else if (this.logger && typeof this.logger.error === 'function') {
|
|
420
|
+
this.logger.error(`[EventDispatcher] Error in serial listener for event "${event}":`, err);
|
|
421
|
+
}
|
|
422
|
+
throw err;
|
|
423
|
+
}
|
|
406
424
|
}
|
|
407
425
|
|
|
408
426
|
return results;
|
|
@@ -429,18 +447,113 @@ class EventDispatcher extends EventEmitter {
|
|
|
429
447
|
let currentData = payload;
|
|
430
448
|
|
|
431
449
|
for (const listener of listeners) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
450
|
+
try {
|
|
451
|
+
const res = listener.isAny
|
|
452
|
+
? await listener.handler(event, currentData, meta)
|
|
453
|
+
: await listener.handler(currentData, meta);
|
|
435
454
|
|
|
436
|
-
|
|
437
|
-
|
|
455
|
+
if (res !== undefined) {
|
|
456
|
+
currentData = res;
|
|
457
|
+
}
|
|
458
|
+
} catch (err) {
|
|
459
|
+
if (this.listenerCount('error') > 0) {
|
|
460
|
+
this.emit('error', err);
|
|
461
|
+
} else if (this.logger && typeof this.logger.error === 'function') {
|
|
462
|
+
this.logger.error(`[EventDispatcher] Error in waterfall listener for event "${event}":`, err);
|
|
463
|
+
}
|
|
464
|
+
throw err;
|
|
438
465
|
}
|
|
439
466
|
}
|
|
440
467
|
|
|
441
468
|
return currentData;
|
|
442
469
|
}
|
|
443
470
|
|
|
471
|
+
/**
|
|
472
|
+
* Asynchronously wait for a matching event to be emitted
|
|
473
|
+
* @param {string} event - Event name or pattern
|
|
474
|
+
* @param {Object|number|Function} [optionsOrTimeout=5000] - Timeout in ms, predicate, or options
|
|
475
|
+
* @param {number} [optionsOrTimeout.timeout=5000] - Timeout in ms (0 for indefinite)
|
|
476
|
+
* @param {Function} [optionsOrTimeout.filter] - Predicate (data, meta) => boolean
|
|
477
|
+
* @param {AbortSignal} [optionsOrTimeout.signal] - AbortSignal for cancellation
|
|
478
|
+
* @param {any} [optionsOrTimeout.sender] - Filter by sender
|
|
479
|
+
* @returns {Promise<{ data: any, meta: Object }>}
|
|
480
|
+
*/
|
|
481
|
+
waitFor(event, optionsOrTimeout = {}) {
|
|
482
|
+
let timeoutMs = 5000;
|
|
483
|
+
let filter = null;
|
|
484
|
+
let signal = null;
|
|
485
|
+
let sender = undefined;
|
|
486
|
+
|
|
487
|
+
if (typeof optionsOrTimeout === 'number') {
|
|
488
|
+
timeoutMs = optionsOrTimeout;
|
|
489
|
+
} else if (typeof optionsOrTimeout === 'function') {
|
|
490
|
+
filter = optionsOrTimeout;
|
|
491
|
+
} else if (typeof optionsOrTimeout === 'object' && optionsOrTimeout !== null) {
|
|
492
|
+
if (optionsOrTimeout.timeout !== undefined) timeoutMs = optionsOrTimeout.timeout;
|
|
493
|
+
if (typeof optionsOrTimeout.filter === 'function') filter = optionsOrTimeout.filter;
|
|
494
|
+
if (optionsOrTimeout.signal) signal = optionsOrTimeout.signal;
|
|
495
|
+
if (optionsOrTimeout.sender !== undefined) sender = optionsOrTimeout.sender;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return new Promise((resolve, reject) => {
|
|
499
|
+
if (signal && signal.aborted) {
|
|
500
|
+
return reject(new Error(`waitFor('${event}') aborted before execution`));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
let timeoutTimer = null;
|
|
504
|
+
let unsubscribe = null;
|
|
505
|
+
let abortHandler = null;
|
|
506
|
+
|
|
507
|
+
const cleanup = () => {
|
|
508
|
+
if (timeoutTimer) {
|
|
509
|
+
clearTimeout(timeoutTimer);
|
|
510
|
+
timeoutTimer = null;
|
|
511
|
+
}
|
|
512
|
+
if (typeof unsubscribe === 'function') {
|
|
513
|
+
unsubscribe();
|
|
514
|
+
unsubscribe = null;
|
|
515
|
+
}
|
|
516
|
+
if (signal && abortHandler) {
|
|
517
|
+
signal.removeEventListener('abort', abortHandler);
|
|
518
|
+
abortHandler = null;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
if (timeoutMs > 0) {
|
|
523
|
+
timeoutTimer = setTimeout(() => {
|
|
524
|
+
cleanup();
|
|
525
|
+
reject(new Error(`Timeout of ${timeoutMs}ms exceeded while waiting for event '${event}'`));
|
|
526
|
+
}, timeoutMs);
|
|
527
|
+
if (timeoutTimer && typeof timeoutTimer.unref === 'function') {
|
|
528
|
+
timeoutTimer.unref();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (signal) {
|
|
533
|
+
abortHandler = () => {
|
|
534
|
+
cleanup();
|
|
535
|
+
reject(new Error(`waitFor('${event}') was aborted`));
|
|
536
|
+
};
|
|
537
|
+
signal.addEventListener('abort', abortHandler, { once: true });
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const handler = (data, meta) => {
|
|
541
|
+
try {
|
|
542
|
+
if (filter && !filter(data, meta)) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
cleanup();
|
|
546
|
+
resolve({ data, meta });
|
|
547
|
+
} catch (err) {
|
|
548
|
+
cleanup();
|
|
549
|
+
reject(err);
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
unsubscribe = this.on(event, handler, { sender });
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
444
557
|
/**
|
|
445
558
|
* Set or update background queue manager for job dispatching
|
|
446
559
|
* @param {any} queueManager
|
|
@@ -462,11 +575,27 @@ class EventDispatcher extends EventEmitter {
|
|
|
462
575
|
listenAndQueue(event, jobName, queueOptions = {}) {
|
|
463
576
|
return this.on(event, (data) => {
|
|
464
577
|
if (!this.queue || typeof this.queue.dispatch !== 'function') {
|
|
465
|
-
|
|
578
|
+
const err = new Error(
|
|
466
579
|
`[EventDispatcher] Cannot queue job '${jobName}' for event '${event}'. No QueueManager is attached.`
|
|
467
580
|
);
|
|
581
|
+
if (this.listenerCount('error') > 0) {
|
|
582
|
+
this.emit('error', err);
|
|
583
|
+
} else if (this.logger && typeof this.logger.error === 'function') {
|
|
584
|
+
this.logger.error(err.message);
|
|
585
|
+
}
|
|
586
|
+
return Promise.reject(err);
|
|
587
|
+
}
|
|
588
|
+
const dispatchPromise = this.queue.dispatch(jobName, data, queueOptions);
|
|
589
|
+
if (dispatchPromise && typeof dispatchPromise.catch === 'function') {
|
|
590
|
+
dispatchPromise.catch((dispatchErr) => {
|
|
591
|
+
if (this.listenerCount('error') > 0) {
|
|
592
|
+
this.emit('error', dispatchErr);
|
|
593
|
+
} else if (this.logger && typeof this.logger.error === 'function') {
|
|
594
|
+
this.logger.error(`[EventDispatcher] Failed to dispatch queued job '${jobName}':`, dispatchErr);
|
|
595
|
+
}
|
|
596
|
+
});
|
|
468
597
|
}
|
|
469
|
-
return
|
|
598
|
+
return dispatchPromise;
|
|
470
599
|
});
|
|
471
600
|
}
|
|
472
601
|
|
package/core/events/signal.js
CHANGED
|
@@ -79,6 +79,15 @@ class Signal {
|
|
|
79
79
|
async dispatch(data, options = {}) {
|
|
80
80
|
return this.sendAsync(data, options);
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Asynchronously wait for this signal to be emitted
|
|
85
|
+
* @param {Object|number|Function} [optionsOrTimeout]
|
|
86
|
+
* @returns {Promise<{ data: any, meta: Object }>}
|
|
87
|
+
*/
|
|
88
|
+
waitFor(optionsOrTimeout) {
|
|
89
|
+
return this.dispatcher.waitFor(this.name, optionsOrTimeout);
|
|
90
|
+
}
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
/**
|
package/core/health/probes.js
CHANGED
|
@@ -112,9 +112,15 @@ async function probeDatabase(db, timeoutMs = 3000) {
|
|
|
112
112
|
timer = setTimeout(() => {
|
|
113
113
|
reject(new Error(`Database ping timed out after ${timeoutMs}ms`));
|
|
114
114
|
}, timeoutMs);
|
|
115
|
+
if (typeof timer?.unref === 'function') timer.unref();
|
|
115
116
|
});
|
|
116
117
|
|
|
117
|
-
|
|
118
|
+
const pingPromise = Promise.resolve(knex.raw('SELECT 1'));
|
|
119
|
+
if (typeof pingPromise.catch === 'function') {
|
|
120
|
+
pingPromise.catch(() => {});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await Promise.race([pingPromise, timeoutPromise]);
|
|
118
124
|
const latencyMs = Date.now() - startTime;
|
|
119
125
|
return { status: 'up', latencyMs };
|
|
120
126
|
} catch (err) {
|
|
@@ -170,9 +176,13 @@ async function probeCustomChecks(checks = {}, context = {}, timeoutMs = 3000) {
|
|
|
170
176
|
try {
|
|
171
177
|
const timeoutPromise = new Promise((_, reject) => {
|
|
172
178
|
timer = setTimeout(() => reject(new Error(`Health check "${name}" timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
179
|
+
if (typeof timer?.unref === 'function') timer.unref();
|
|
173
180
|
});
|
|
174
181
|
|
|
175
|
-
const
|
|
182
|
+
const checkPromise = Promise.resolve().then(() => checkFn(context));
|
|
183
|
+
checkPromise.catch(() => {});
|
|
184
|
+
|
|
185
|
+
const res = await Promise.race([checkPromise, timeoutPromise]);
|
|
176
186
|
const latencyMs = Date.now() - startTime;
|
|
177
187
|
|
|
178
188
|
if (res === false || (res && typeof res === 'object' && res.status === 'down')) {
|
package/core/http/index.js
CHANGED
|
@@ -7,7 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
const { HttpAdapter } = require('./adapter');
|
|
9
9
|
const { ExpressHttpAdapter, createExpressHttpAdapter } = require('./express-adapter');
|
|
10
|
-
const {
|
|
10
|
+
const {
|
|
11
|
+
createPipelineRunner,
|
|
12
|
+
runMiddlewares,
|
|
13
|
+
enrichRouteError,
|
|
14
|
+
validateRequestInput,
|
|
15
|
+
executeApiPipeline,
|
|
16
|
+
executePagePipeline,
|
|
17
|
+
} = require('./pipeline');
|
|
11
18
|
|
|
12
19
|
let _honoModule = null;
|
|
13
20
|
function getHonoModule() {
|
|
@@ -21,8 +28,10 @@ module.exports = {
|
|
|
21
28
|
HttpAdapter,
|
|
22
29
|
ExpressHttpAdapter,
|
|
23
30
|
createExpressHttpAdapter,
|
|
31
|
+
createPipelineRunner,
|
|
24
32
|
runMiddlewares,
|
|
25
33
|
enrichRouteError,
|
|
34
|
+
validateRequestInput,
|
|
26
35
|
executeApiPipeline,
|
|
27
36
|
executePagePipeline,
|
|
28
37
|
};
|