webspresso 0.0.103 → 0.0.105
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 +6 -0
- package/.agents/ARCHITECTURE.md +23 -0
- package/.agents/CACHE.md +147 -0
- package/.agents/CLI.md +1 -1
- package/.agents/EARLY_HINTS.md +120 -0
- package/.agents/HEALTH.md +118 -0
- package/.agents/LIMITS.md +109 -0
- package/.agents/ORM.md +155 -1
- package/.agents/PLUGINS.md +28 -0
- package/.agents/RESILIENCE.md +235 -0
- package/AGENTS.md +6 -0
- package/README.md +2 -2
- package/bin/commands/add-docker.js +6 -6
- package/bin/commands/dev.js +4 -3
- package/bin/commands/new.js +5 -5
- package/bin/commands/openapi.js +1 -1
- package/bin/commands/start.js +1 -1
- package/bin/utils/orm-map-html.js +2 -8
- package/bin/utils/project.js +2 -2
- package/core/auth/hash.js +5 -4
- package/core/auth/jwt.js +6 -57
- package/core/auth/magic-link.js +4 -5
- package/core/auth/manager.js +3 -5
- package/core/auth/oauth.js +45 -30
- package/core/auth/totp.js +5 -16
- package/core/auth/user-agent.js +1 -21
- package/core/cache/byte-size.js +123 -0
- package/core/cache/index.js +21 -0
- package/core/cache/lru.js +477 -0
- package/core/cache/tagged-lru.js +346 -0
- package/core/commands/registry.js +6 -20
- package/core/content/renderer.js +1 -13
- package/core/early-hints/index.js +267 -0
- package/core/errors/domain.js +48 -0
- package/core/errors/http.js +31 -0
- package/core/errors/index.js +10 -0
- package/core/errors/middleware.js +2 -8
- package/core/errors/normalize.js +88 -2
- package/core/events/discovery.js +6 -24
- package/core/events/dispatcher.js +2 -7
- package/core/forms/form-builder.js +1 -9
- package/core/health/index.js +41 -0
- package/core/health/middleware.js +213 -0
- package/core/health/normalize.js +140 -0
- package/core/health/probes.js +203 -0
- package/core/limits/index.js +34 -0
- package/core/limits/middleware.js +317 -0
- package/core/limits/normalize.js +222 -0
- package/core/orm/batch.js +70 -0
- package/core/orm/cache/fingerprint.js +2 -2
- package/core/orm/cache/memory-provider.js +27 -110
- package/core/orm/index.js +76 -8
- package/core/orm/json-fields.js +22 -8
- package/core/orm/model.js +10 -0
- package/core/orm/query-builder.js +63 -9
- package/core/orm/query-filters.js +393 -0
- package/core/orm/repository.js +318 -19
- package/core/orm/schema-helpers.js +8 -10
- package/core/orm/scopes.js +321 -1
- package/core/orm/transaction.js +10 -2
- package/core/orm/utils.js +4 -86
- package/core/queue/adapters/database.js +4 -8
- package/core/queue/manager.js +55 -41
- package/core/resilience/circuit-breaker.js +391 -0
- package/core/resilience/errors.js +63 -0
- package/core/resilience/index.js +53 -0
- package/core/resilience/registry.js +97 -0
- package/core/resilience/retry.js +203 -0
- package/core/schema-cache.js +9 -2
- package/core/shutdown/http-adapter.js +4 -1
- package/core/shutdown/shutdown-manager.js +132 -31
- package/core/utils/array.js +12 -0
- package/core/utils/base64url.js +8 -0
- package/core/utils/crypto.js +191 -0
- package/core/utils/ensure-leading-slash.js +6 -0
- package/core/utils/escape-html.js +7 -0
- package/core/utils/escape-regexp.js +7 -0
- package/core/utils/fetch.js +121 -0
- package/core/utils/fs.js +144 -0
- package/core/utils/get-client-ip.js +6 -0
- package/core/utils/hmac.js +6 -0
- package/core/utils/html.js +72 -0
- package/core/utils/ip.js +86 -0
- package/core/utils/json.js +98 -0
- package/core/utils/object.js +106 -0
- package/core/utils/random.js +9 -0
- package/core/utils/regex.js +77 -0
- package/core/utils/safe-compare.js +3 -0
- package/core/utils/sha256.js +6 -0
- package/core/utils/string.js +87 -0
- package/core/utils/strip-trailing-slash.js +6 -0
- package/core/utils/time.js +96 -0
- package/core/utils/timeout.js +348 -0
- package/core/utils/url.js +106 -0
- package/core/utils/walk.js +7 -0
- package/index.d.ts +627 -2
- package/index.js +158 -0
- package/package.json +1 -2
- package/plugins/admin-panel/auth.js +15 -7
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +82 -10
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +4 -2
- package/plugins/admin-panel/client/parts/08-record-list.js +3 -2
- package/plugins/admin-panel/client/parts/10-export-registry.js +6 -0
- package/plugins/admin-panel/core/api-extensions.js +3 -80
- package/plugins/admin-panel/index.js +1 -1
- package/plugins/admin-panel/lib/login-rate-limiter.js +15 -13
- package/plugins/admin-panel/modules/menu.js +1 -1
- package/plugins/api-key/crypto.js +3 -26
- package/plugins/api-key/scopes.js +5 -7
- package/plugins/audit-log/api-handlers.js +8 -7
- package/plugins/audit-log/middleware.js +11 -2
- package/plugins/audit-log/parse.js +3 -3
- package/plugins/basic-auth/index.js +2 -26
- package/plugins/cors.js +2 -2
- package/plugins/csrf/index.js +7 -23
- package/plugins/dashboard/app.js +2 -2
- package/plugins/dashboard/index.js +5 -5
- package/plugins/data-exchange/record-selection.js +2 -5
- package/plugins/email/api-handlers.js +5 -18
- package/plugins/email/auth-bridge.js +3 -2
- package/plugins/file-manager/admin-component.js +3 -1
- package/plugins/file-manager/core.js +34 -4
- package/plugins/health-check.js +2 -1
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +5 -6
- package/plugins/mcp/auth.js +1 -13
- package/plugins/mcp/discovery.js +18 -17
- package/plugins/mcp/index.js +23 -1
- package/plugins/mcp/transports/sse.js +10 -1
- package/plugins/orm-cache-admin/admin-component.js +3 -2
- package/plugins/polar/src/api.js +11 -3
- package/plugins/polar/src/webhooks.js +7 -13
- package/plugins/rate-limit/index.js +3 -1
- package/plugins/recaptcha/helpers.js +1 -7
- package/plugins/recaptcha/verify.js +37 -23
- package/plugins/redirect/index.js +57 -7
- package/plugins/rest-resources/index.js +1 -207
- package/plugins/rls/index.js +170 -0
- package/plugins/schema-explorer/admin-component.js +4 -2
- package/plugins/schema-explorer.js +40 -29
- package/plugins/seo-checker/index.js +5 -2
- package/plugins/site-analytics/tracking.js +21 -18
- package/plugins/sitemap.js +21 -17
- package/plugins/swagger.js +13 -17
- package/plugins/upload/index.js +4 -3
- package/plugins/upload/orphan-pruner.js +4 -3
- package/plugins/webhooks/crypto.js +6 -28
- package/plugins/webhooks/dispatcher.js +14 -9
- package/plugins/webhooks/index.js +6 -0
- package/plugins/webhooks/matcher.js +28 -17
- package/plugins/webhooks/sender.js +40 -13
- package/src/app-context.js +41 -5
- package/src/dev/banner.js +6 -2
- package/src/dev-reload/index.js +47 -6
- package/src/file-router.js +1 -3
- package/src/helpers.js +180 -33
- package/src/modules/module-discovery.js +1 -9
- package/src/pages/page-loader.js +53 -1
- package/src/plugin-manager.js +90 -17
- package/src/server.js +137 -26
- package/src/services/discovery.js +19 -40
- package/src/services/executor.js +86 -25
- package/src/services/memoize.js +3 -25
- package/src/services/registry.js +6 -0
- package/src/template/precompile.js +8 -19
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso - Zero-Dependency Centralized Timeout Engine & Middleware
|
|
5
|
+
*
|
|
6
|
+
* Provides:
|
|
7
|
+
* - Centralized global timeouts configuration hierarchy (`normalizeTimeouts`, `DEFAULT_TIMEOUTS`)
|
|
8
|
+
* - Universal async wrapper with timer unref and AbortSignal support (`withTimeout`)
|
|
9
|
+
* - Native, zero-dependency drop-in replacement for connect-timeout (`createTimeoutMiddleware`)
|
|
10
|
+
* - Semantic `TimeoutError`
|
|
11
|
+
*
|
|
12
|
+
* @module core/utils/timeout
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { parseDurationMs } = require('./time');
|
|
16
|
+
const { WebspressoError } = require('../errors/base');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default framework timeout thresholds (in milliseconds)
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_TIMEOUTS = Object.freeze({
|
|
22
|
+
request: 30_000, // HTTP request processing timeout (30s)
|
|
23
|
+
service: 15_000, // Services layer execution timeout (15s)
|
|
24
|
+
queue: 30_000, // Background job queue worker timeout (30s)
|
|
25
|
+
fetch: 10_000, // Outgoing external fetchWithTimeout (10s)
|
|
26
|
+
webhook: 10_000, // Outgoing webhook dispatch timeout (10s)
|
|
27
|
+
shutdown: 10_000, // Graceful shutdown server draining timeout (10s)
|
|
28
|
+
loader: 10_000, // SSR page data loader load() execution timeout (10s)
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Normalizes application timeout settings from createApp options into a single,
|
|
33
|
+
* cohesive configuration object with sensible defaults and backward compatibility.
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} [options={}] - Options passed to createApp() or framework components
|
|
36
|
+
* @returns {Readonly<{
|
|
37
|
+
* request: number | false,
|
|
38
|
+
* service: number | false,
|
|
39
|
+
* queue: number | false,
|
|
40
|
+
* fetch: number | false,
|
|
41
|
+
* webhook: number | false,
|
|
42
|
+
* shutdown: number | false,
|
|
43
|
+
* loader: number | false,
|
|
44
|
+
* }>}
|
|
45
|
+
*/
|
|
46
|
+
function normalizeTimeouts(options = {}) {
|
|
47
|
+
const rootTimeouts = (options && typeof options.timeouts === 'object' && options.timeouts !== null)
|
|
48
|
+
? options.timeouts
|
|
49
|
+
: {};
|
|
50
|
+
|
|
51
|
+
function parseVal(val, defaultVal) {
|
|
52
|
+
if (val === false || val === 'false' || val === 0 || val === '0') {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (val === null || val === undefined) {
|
|
56
|
+
return defaultVal;
|
|
57
|
+
}
|
|
58
|
+
return parseDurationMs(val, defaultVal);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Request timeout: timeouts.request -> options.timeout -> options.server.timeout -> default (30s)
|
|
62
|
+
const requestRaw = rootTimeouts.request !== undefined
|
|
63
|
+
? rootTimeouts.request
|
|
64
|
+
: (options.timeout !== undefined
|
|
65
|
+
? options.timeout
|
|
66
|
+
: (options.server?.timeout !== undefined ? options.server.timeout : DEFAULT_TIMEOUTS.request));
|
|
67
|
+
|
|
68
|
+
// Service timeout: timeouts.service -> options.services.timeout -> options.serviceTimeout -> default (15s)
|
|
69
|
+
const serviceRaw = rootTimeouts.service !== undefined
|
|
70
|
+
? rootTimeouts.service
|
|
71
|
+
: (options.services?.timeout !== undefined
|
|
72
|
+
? options.services.timeout
|
|
73
|
+
: (options.serviceTimeout !== undefined ? options.serviceTimeout : DEFAULT_TIMEOUTS.service));
|
|
74
|
+
|
|
75
|
+
// Queue timeout: timeouts.queue -> options.queue.timeout -> options.queueTimeout -> default (30s)
|
|
76
|
+
const queueRaw = rootTimeouts.queue !== undefined
|
|
77
|
+
? rootTimeouts.queue
|
|
78
|
+
: (options.queue?.timeout !== undefined
|
|
79
|
+
? options.queue.timeout
|
|
80
|
+
: (options.queueTimeout !== undefined ? options.queueTimeout : DEFAULT_TIMEOUTS.queue));
|
|
81
|
+
|
|
82
|
+
// Fetch timeout: timeouts.fetch -> options.fetch.timeout -> options.fetchTimeout -> default (10s)
|
|
83
|
+
const fetchRaw = rootTimeouts.fetch !== undefined
|
|
84
|
+
? rootTimeouts.fetch
|
|
85
|
+
: (options.fetch?.timeout !== undefined
|
|
86
|
+
? options.fetch.timeout
|
|
87
|
+
: (options.fetchTimeout !== undefined ? options.fetchTimeout : DEFAULT_TIMEOUTS.fetch));
|
|
88
|
+
|
|
89
|
+
// Webhook timeout: timeouts.webhook -> options.webhooks.timeout -> options.webhookTimeout -> default (10s)
|
|
90
|
+
const webhookRaw = rootTimeouts.webhook !== undefined
|
|
91
|
+
? rootTimeouts.webhook
|
|
92
|
+
: (options.webhooks?.timeout !== undefined
|
|
93
|
+
? options.webhooks.timeout
|
|
94
|
+
: (options.webhookTimeout !== undefined ? options.webhookTimeout : DEFAULT_TIMEOUTS.webhook));
|
|
95
|
+
|
|
96
|
+
// Shutdown timeout: timeouts.shutdown -> options.shutdown.timeout -> options.server.shutdown.timeout -> default (10s)
|
|
97
|
+
const shutdownRaw = rootTimeouts.shutdown !== undefined
|
|
98
|
+
? rootTimeouts.shutdown
|
|
99
|
+
: (options.shutdown?.timeout !== undefined
|
|
100
|
+
? options.shutdown.timeout
|
|
101
|
+
: (options.server?.shutdown?.timeout !== undefined ? options.server.shutdown.timeout : DEFAULT_TIMEOUTS.shutdown));
|
|
102
|
+
|
|
103
|
+
// Page loader timeout: timeouts.loader -> options.loader.timeout -> options.loaderTimeout -> default (10s)
|
|
104
|
+
const loaderRaw = rootTimeouts.loader !== undefined
|
|
105
|
+
? rootTimeouts.loader
|
|
106
|
+
: (options.loader?.timeout !== undefined
|
|
107
|
+
? options.loader.timeout
|
|
108
|
+
: (options.loaderTimeout !== undefined ? options.loaderTimeout : DEFAULT_TIMEOUTS.loader));
|
|
109
|
+
|
|
110
|
+
return Object.freeze({
|
|
111
|
+
request: parseVal(requestRaw, DEFAULT_TIMEOUTS.request),
|
|
112
|
+
service: parseVal(serviceRaw, DEFAULT_TIMEOUTS.service),
|
|
113
|
+
queue: parseVal(queueRaw, DEFAULT_TIMEOUTS.queue),
|
|
114
|
+
fetch: parseVal(fetchRaw, DEFAULT_TIMEOUTS.fetch),
|
|
115
|
+
webhook: parseVal(webhookRaw, DEFAULT_TIMEOUTS.webhook),
|
|
116
|
+
shutdown: parseVal(shutdownRaw, DEFAULT_TIMEOUTS.shutdown),
|
|
117
|
+
loader: parseVal(loaderRaw, DEFAULT_TIMEOUTS.loader),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Standard framework Timeout Error
|
|
123
|
+
*/
|
|
124
|
+
class TimeoutError extends WebspressoError {
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} [message='Operation timed out']
|
|
127
|
+
* @param {Object} [options]
|
|
128
|
+
* @param {number} [options.timeout]
|
|
129
|
+
* @param {string} [options.code='ETIMEDOUT']
|
|
130
|
+
* @param {number} [options.status=504]
|
|
131
|
+
* @param {number} [options.statusCode=504]
|
|
132
|
+
* @param {string} [options.scope='timeout']
|
|
133
|
+
*/
|
|
134
|
+
constructor(message = 'Operation timed out', options = {}) {
|
|
135
|
+
super(message, { code: options.code || 'ETIMEDOUT', ...options });
|
|
136
|
+
this.name = 'TimeoutError';
|
|
137
|
+
this.status = typeof options.status === 'number' ? options.status : (options.statusCode || 504);
|
|
138
|
+
this.statusCode = this.status;
|
|
139
|
+
this.timeout = options.timeout !== undefined ? options.timeout : null;
|
|
140
|
+
this.scope = options.scope || 'timeout';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Universal async execution wrapper with timeout, timer unreferencing,
|
|
146
|
+
* and AbortController / AbortSignal cancellation support.
|
|
147
|
+
*
|
|
148
|
+
* @template T
|
|
149
|
+
* @param {Promise<T> | ((signal?: AbortSignal) => Promise<T> | T)} promiseOrFn
|
|
150
|
+
* @param {number | string | boolean} timeout - Timeout duration (ms number, '5s', or false)
|
|
151
|
+
* @param {Object} [options={}]
|
|
152
|
+
* @param {AbortController} [options.controller] - Optional AbortController to abort on timeout
|
|
153
|
+
* @param {AbortSignal} [options.signal] - Optional external AbortSignal to monitor
|
|
154
|
+
* @param {string} [options.errorMessage] - Custom error message
|
|
155
|
+
* @param {string} [options.errorCode='ETIMEDOUT'] - Custom error code
|
|
156
|
+
* @param {number} [options.statusCode=504] - HTTP status code
|
|
157
|
+
* @param {string} [options.scope='timeout'] - Scope identifier ('service', 'queue', 'fetch', etc.)
|
|
158
|
+
* @param {boolean} [options.unref=true] - Whether to unref the timer
|
|
159
|
+
* @param {((timeoutMs: number) => Error)} [options.createError] - Custom error factory
|
|
160
|
+
* @returns {Promise<T>}
|
|
161
|
+
*/
|
|
162
|
+
async function withTimeout(promiseOrFn, timeout, options = {}) {
|
|
163
|
+
const timeoutMs = (timeout === false || timeout === 0 || timeout === null || timeout === undefined)
|
|
164
|
+
? 0
|
|
165
|
+
: parseDurationMs(timeout, 0);
|
|
166
|
+
|
|
167
|
+
// If timeout is disabled or <= 0, run directly
|
|
168
|
+
if (timeoutMs <= 0) {
|
|
169
|
+
if (typeof promiseOrFn === 'function') {
|
|
170
|
+
return await promiseOrFn(options.controller?.signal || options.signal);
|
|
171
|
+
}
|
|
172
|
+
return await promiseOrFn;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// If external signal is already aborted, reject immediately
|
|
176
|
+
if (options.signal && options.signal.aborted) {
|
|
177
|
+
const reason = options.signal.reason || new Error('This operation was aborted');
|
|
178
|
+
throw reason;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let timer = null;
|
|
182
|
+
let callerSignalListener = null;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
186
|
+
timer = setTimeout(() => {
|
|
187
|
+
timer = null;
|
|
188
|
+
let err;
|
|
189
|
+
if (typeof options.createError === 'function') {
|
|
190
|
+
err = options.createError(timeoutMs);
|
|
191
|
+
} else {
|
|
192
|
+
const msg = options.errorMessage || `Operation timed out after ${timeoutMs}ms`;
|
|
193
|
+
err = new TimeoutError(msg, {
|
|
194
|
+
timeout: timeoutMs,
|
|
195
|
+
code: options.errorCode || 'ETIMEDOUT',
|
|
196
|
+
statusCode: options.statusCode || 504,
|
|
197
|
+
scope: options.scope || 'timeout',
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (options.controller && typeof options.controller.abort === 'function') {
|
|
202
|
+
try {
|
|
203
|
+
options.controller.abort(err);
|
|
204
|
+
} catch (_) {}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
reject(err);
|
|
208
|
+
}, timeoutMs);
|
|
209
|
+
|
|
210
|
+
if (options.unref !== false && timer && typeof timer.unref === 'function') {
|
|
211
|
+
timer.unref();
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const executionSignal = options.controller ? options.controller.signal : options.signal;
|
|
216
|
+
const executionPromise = typeof promiseOrFn === 'function'
|
|
217
|
+
? promiseOrFn(executionSignal)
|
|
218
|
+
: promiseOrFn;
|
|
219
|
+
|
|
220
|
+
if (options.signal) {
|
|
221
|
+
const abortPromise = new Promise((_, reject) => {
|
|
222
|
+
callerSignalListener = () => {
|
|
223
|
+
const reason = options.signal.reason || new Error('This operation was aborted');
|
|
224
|
+
reject(reason);
|
|
225
|
+
};
|
|
226
|
+
options.signal.addEventListener('abort', callerSignalListener, { once: true });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return await Promise.race([executionPromise, timeoutPromise, abortPromise]);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return await Promise.race([executionPromise, timeoutPromise]);
|
|
233
|
+
} finally {
|
|
234
|
+
if (timer) {
|
|
235
|
+
clearTimeout(timer);
|
|
236
|
+
timer = null;
|
|
237
|
+
}
|
|
238
|
+
if (options.signal && callerSignalListener) {
|
|
239
|
+
options.signal.removeEventListener('abort', callerSignalListener);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Creates an Express request timeout middleware.
|
|
246
|
+
* Zero-dependency replacement for connect-timeout with native AbortController,
|
|
247
|
+
* timer unreferencing, and automatic request cleanup.
|
|
248
|
+
*
|
|
249
|
+
* @param {string | number | boolean} timeoutConfig - Timeout duration (e.g. '30s', 30000, or false)
|
|
250
|
+
* @param {Object} [options={}]
|
|
251
|
+
* @param {boolean} [options.respond=true] - Whether to pass 503 error to next() when timed out
|
|
252
|
+
* @returns {Function} Express middleware (req, res, next)
|
|
253
|
+
*/
|
|
254
|
+
function createTimeoutMiddleware(timeoutConfig, options = {}) {
|
|
255
|
+
if (timeoutConfig === false || timeoutConfig === 'false' || timeoutConfig === 0) {
|
|
256
|
+
return function disabledTimeoutMiddleware(req, res, next) {
|
|
257
|
+
req.timedout = false;
|
|
258
|
+
req.clearTimeout = () => {};
|
|
259
|
+
next();
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const timeoutMs = parseDurationMs(timeoutConfig, DEFAULT_TIMEOUTS.request);
|
|
264
|
+
if (timeoutMs <= 0) {
|
|
265
|
+
return function zeroTimeoutMiddleware(req, res, next) {
|
|
266
|
+
req.timedout = false;
|
|
267
|
+
req.clearTimeout = () => {};
|
|
268
|
+
next();
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return function requestTimeoutMiddleware(req, res, next) {
|
|
273
|
+
req.timedout = false;
|
|
274
|
+
|
|
275
|
+
// Attach native AbortController and AbortSignal to req
|
|
276
|
+
let controller = null;
|
|
277
|
+
if (typeof AbortController !== 'undefined') {
|
|
278
|
+
controller = new AbortController();
|
|
279
|
+
req.abortController = controller;
|
|
280
|
+
req.signal = controller.signal;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let timer = setTimeout(() => {
|
|
284
|
+
timer = null;
|
|
285
|
+
req.timedout = true;
|
|
286
|
+
|
|
287
|
+
const err = new Error(`Response timeout after ${timeoutMs}ms`);
|
|
288
|
+
err.timeout = timeoutMs;
|
|
289
|
+
err.status = 503;
|
|
290
|
+
err.statusCode = 503;
|
|
291
|
+
err.code = 'ETIMEDOUT';
|
|
292
|
+
|
|
293
|
+
if (controller) {
|
|
294
|
+
try {
|
|
295
|
+
controller.abort(err);
|
|
296
|
+
} catch (_) {}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
req.emit('timeout', timeoutMs);
|
|
301
|
+
} catch (_) {}
|
|
302
|
+
|
|
303
|
+
if (!res.headersSent && !res.writableEnded) {
|
|
304
|
+
if (options.respond !== false) {
|
|
305
|
+
next(err);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}, timeoutMs);
|
|
309
|
+
|
|
310
|
+
if (timer && typeof timer.unref === 'function') {
|
|
311
|
+
timer.unref();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
req.clearTimeout = function clearTimeoutHandler() {
|
|
315
|
+
if (timer) {
|
|
316
|
+
clearTimeout(timer);
|
|
317
|
+
timer = null;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const cleanup = () => {
|
|
322
|
+
if (timer) {
|
|
323
|
+
clearTimeout(timer);
|
|
324
|
+
timer = null;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
res.once('finish', cleanup);
|
|
329
|
+
res.once('close', () => {
|
|
330
|
+
cleanup();
|
|
331
|
+
if (!res.writableEnded && !req.timedout && controller) {
|
|
332
|
+
try {
|
|
333
|
+
controller.abort(new Error('Client connection closed'));
|
|
334
|
+
} catch (_) {}
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
next();
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
module.exports = {
|
|
343
|
+
DEFAULT_TIMEOUTS,
|
|
344
|
+
normalizeTimeouts,
|
|
345
|
+
TimeoutError,
|
|
346
|
+
withTimeout,
|
|
347
|
+
createTimeoutMiddleware,
|
|
348
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso - Zero-Dependency URL & Path Normalization Utilities
|
|
5
|
+
* High-performance, linear-time URL and path formatting helpers.
|
|
6
|
+
* @module core/utils/url
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { trimUrlPathSlashes } = require('../url-path-normalize');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Safely removes trailing slash(es) from a URL or path.
|
|
13
|
+
*
|
|
14
|
+
* @param {string|unknown} input - URL, pathname, or path fragment
|
|
15
|
+
* @param {Object} [options]
|
|
16
|
+
* @param {boolean} [options.preserveRoot=false] - If true and input resolves to '/', keeps single '/'
|
|
17
|
+
* @returns {string} Truncated string without trailing slash(es)
|
|
18
|
+
*/
|
|
19
|
+
function stripTrailingSlash(input, options = {}) {
|
|
20
|
+
if (input == null) return '';
|
|
21
|
+
let s = String(input);
|
|
22
|
+
if (s.length === 0) return '';
|
|
23
|
+
|
|
24
|
+
const preserveRoot = options.preserveRoot === true;
|
|
25
|
+
|
|
26
|
+
let end = s.length;
|
|
27
|
+
while (end > 0 && s.charCodeAt(end - 1) === 47 /* / */) {
|
|
28
|
+
end--;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (end === 0) {
|
|
32
|
+
return preserveRoot ? '/' : '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return s.slice(0, end);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Ensures a path starts with a leading slash ('/').
|
|
40
|
+
*
|
|
41
|
+
* @param {string|unknown} input - Path fragment
|
|
42
|
+
* @returns {string} Path with leading slash
|
|
43
|
+
*/
|
|
44
|
+
function ensureLeadingSlash(input) {
|
|
45
|
+
if (input == null) return '/';
|
|
46
|
+
const s = String(input).trim();
|
|
47
|
+
if (s.length === 0) return '/';
|
|
48
|
+
return s.charCodeAt(0) === 47 /* / */ ? s : `/${s}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Ensures a URL or path ends with a trailing slash ('/').
|
|
53
|
+
*
|
|
54
|
+
* @param {string|unknown} input - Path or URL
|
|
55
|
+
* @returns {string} String with trailing slash
|
|
56
|
+
*/
|
|
57
|
+
function ensureTrailingSlash(input) {
|
|
58
|
+
if (input == null) return '/';
|
|
59
|
+
const s = String(input);
|
|
60
|
+
if (s.length === 0) return '/';
|
|
61
|
+
return s.charCodeAt(s.length - 1) === 47 /* / */ ? s : `${s}/`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Safely removes leading slash(es) from a path fragment.
|
|
66
|
+
*
|
|
67
|
+
* @param {string|unknown} input - Path fragment
|
|
68
|
+
* @returns {string} Path without leading slash(es)
|
|
69
|
+
*/
|
|
70
|
+
function stripLeadingSlash(input) {
|
|
71
|
+
if (input == null) return '';
|
|
72
|
+
let s = String(input);
|
|
73
|
+
let start = 0;
|
|
74
|
+
while (start < s.length && s.charCodeAt(start) === 47 /* / */) {
|
|
75
|
+
start++;
|
|
76
|
+
}
|
|
77
|
+
return s.slice(start);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Joins a base URL with one or more path fragments without double slashes.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} base - Base URL or path prefix
|
|
84
|
+
* @param {...string} paths - Path fragments to append
|
|
85
|
+
* @returns {string} Combined normalized URL or path
|
|
86
|
+
*/
|
|
87
|
+
function joinUrl(base, ...paths) {
|
|
88
|
+
let result = stripTrailingSlash(base || '');
|
|
89
|
+
for (const part of paths) {
|
|
90
|
+
if (part == null) continue;
|
|
91
|
+
const clean = stripLeadingSlash(stripTrailingSlash(part));
|
|
92
|
+
if (clean.length > 0) {
|
|
93
|
+
result = result ? `${result}/${clean}` : clean;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return result || '/';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
stripTrailingSlash,
|
|
101
|
+
ensureLeadingSlash,
|
|
102
|
+
ensureTrailingSlash,
|
|
103
|
+
stripLeadingSlash,
|
|
104
|
+
joinUrl,
|
|
105
|
+
trimUrlPathSlashes,
|
|
106
|
+
};
|