webspresso 0.0.104 → 0.0.106
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 +21 -2
- package/.agents/ARCHITECTURE.md +23 -0
- package/.agents/CACHE.md +147 -0
- package/.agents/CLI.md +220 -8
- package/.agents/EARLY_HINTS.md +120 -0
- package/.agents/FLAGS.md +118 -0
- package/.agents/HEALTH.md +118 -0
- package/.agents/LIMITS.md +109 -0
- package/.agents/LOGGING.md +93 -0
- package/.agents/METRICS.md +127 -0
- package/.agents/MIGRATION_JOBS.md +95 -0
- package/.agents/ORM.md +155 -1
- package/.agents/PLUGINS.md +38 -3
- package/.agents/QUEUE.md +42 -0
- package/.agents/RATE_LIMIT.md +147 -0
- package/.agents/RESILIENCE.md +235 -0
- package/.agents/SCALE_OUT.md +109 -0
- package/.agents/SECRETS.md +117 -0
- package/AGENTS.md +16 -0
- package/README.md +2 -2
- package/bin/commands/add-aws.js +396 -0
- package/bin/commands/add-ci.js +523 -0
- package/bin/commands/add-cloudflare.js +240 -0
- package/bin/commands/add-cloudrun.js +381 -0
- package/bin/commands/add-docker.js +6 -6
- package/bin/commands/add-migration-job.js +396 -0
- package/bin/commands/assets-sync.js +209 -0
- package/bin/commands/client-generate.js +52 -0
- package/bin/commands/console.js +53 -0
- package/bin/commands/db-migrate.js +47 -2
- package/bin/commands/db-studio.js +84 -0
- package/bin/commands/deploy-check.js +242 -13
- package/bin/commands/dev.js +1 -1
- package/bin/commands/export.js +87 -0
- package/bin/commands/new.js +5 -5
- package/bin/commands/openapi.js +1 -1
- package/bin/commands/secrets-pull.js +208 -0
- package/bin/commands/start.js +1 -1
- package/bin/utils/db.js +7 -1
- package/bin/utils/project.js +2 -2
- package/bin/webspresso.js +22 -0
- package/core/app.js +421 -0
- package/core/auth/authenticate.js +93 -0
- package/core/auth/hash.js +5 -4
- package/core/auth/index.js +34 -4
- package/core/auth/jwt.js +5 -54
- package/core/auth/magic-link.js +4 -5
- package/core/auth/manager.js +53 -10
- package/core/auth/middleware.js +16 -68
- package/core/auth/oauth.js +8 -17
- package/core/auth/redis-session.js +1008 -0
- package/core/auth/sessions.js +156 -0
- package/core/auth/totp.js +2 -1
- package/core/cache/byte-size.js +123 -0
- package/core/cache/cdn.js +296 -0
- package/core/cache/index.js +33 -0
- package/core/cache/lru.js +477 -0
- package/core/cache/tagged-lru.js +354 -0
- package/core/commands/registry.js +6 -20
- package/core/console/commands.js +148 -0
- package/core/console/context.js +260 -0
- package/core/console/eval.js +70 -0
- package/core/console/index.js +22 -0
- package/core/console/repl.js +102 -0
- package/core/early-hints/index.js +267 -0
- package/core/errors/domain.js +49 -0
- package/core/errors/http.js +31 -0
- package/core/errors/index.js +10 -0
- package/core/errors/normalize.js +88 -2
- package/core/events/discovery.js +6 -24
- package/core/events/dispatcher.js +9 -7
- package/core/events/signal.js +7 -0
- package/core/export/discovery.js +191 -0
- package/core/export/exporter.js +277 -0
- package/core/export/index.js +24 -0
- package/core/flags/evaluator.js +252 -0
- package/core/flags/hasher.js +37 -0
- package/core/flags/index.js +21 -0
- package/core/flags/manager.js +263 -0
- package/core/flags/store.js +126 -0
- 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/http/adapter.js +74 -0
- package/core/http/express-adapter.js +46 -0
- package/core/http/hono-adapter.js +531 -0
- package/core/http/index.js +41 -0
- package/core/http/pipeline.js +485 -0
- package/core/index.js +336 -0
- package/core/limits/index.js +34 -0
- package/core/limits/middleware.js +325 -0
- package/core/limits/normalize.js +222 -0
- package/core/logger/formatters.js +264 -0
- package/core/logger/index.js +260 -0
- package/core/logger/trace.js +137 -0
- package/core/metrics/collectors.js +125 -0
- package/core/metrics/index.js +45 -0
- package/core/metrics/middleware.js +230 -0
- package/core/metrics/registry.js +366 -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/d1.js +265 -0
- package/core/orm/index.js +176 -13
- package/core/orm/json-fields.js +22 -8
- package/core/orm/migrate-cloud.js +194 -0
- package/core/orm/model.js +10 -0
- package/core/orm/pool.js +97 -0
- package/core/orm/query-builder.js +72 -9
- 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/cloudflare.js +131 -0
- package/core/queue/adapters/database.js +4 -8
- package/core/queue/adapters/pubsub.js +400 -0
- package/core/queue/adapters/sqs.js +419 -0
- package/core/queue/adapters/wait-until.js +77 -0
- package/core/queue/index.js +8 -0
- package/core/queue/manager.js +66 -41
- package/core/rate-limit/index.js +22 -0
- package/core/rate-limit/limiter.js +219 -0
- package/core/rate-limit/stores/memory.js +129 -0
- package/core/rate-limit/stores/redis.js +151 -0
- package/core/rate-limit/utils.js +112 -0
- package/core/resilience/circuit-breaker.js +422 -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/secrets/index.js +113 -0
- package/core/secrets/manager.js +308 -0
- package/core/secrets/providers/aws.js +288 -0
- package/core/secrets/providers/env.js +44 -0
- package/core/secrets/providers/gcp.js +307 -0
- package/core/secrets/providers/memory.js +76 -0
- package/core/shutdown/http-adapter.js +4 -1
- package/core/shutdown/shutdown-manager.js +129 -19
- package/core/stream/index.js +19 -0
- package/core/stream/text-stream.js +326 -0
- package/core/studio/index.js +43 -0
- package/core/studio/inspector.js +258 -0
- package/core/studio/server.js +325 -0
- package/core/studio/ui.js +681 -0
- package/core/utils/array.js +12 -0
- package/core/utils/base64url.js +8 -0
- package/core/utils/crypto.js +118 -0
- package/core/utils/ensure-leading-slash.js +6 -0
- package/core/utils/escape-regexp.js +7 -0
- package/core/utils/fetch.js +6 -5
- package/core/utils/fs.js +144 -0
- package/core/utils/hmac.js +6 -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/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 +1549 -2
- package/index.js +272 -2
- package/package.json +3 -2
- package/plugins/admin-panel/core/api-extensions.js +2 -5
- package/plugins/admin-panel/index.js +13 -6
- package/plugins/admin-panel/modules/menu.js +1 -1
- package/plugins/api-key/crypto.js +3 -3
- package/plugins/api-key/index.js +3 -2
- package/plugins/api-key/scopes.js +5 -7
- package/plugins/audit-log/api-handlers.js +6 -15
- package/plugins/audit-log/index.js +3 -1
- package/plugins/audit-log/parse.js +3 -3
- package/plugins/basic-auth/index.js +3 -2
- package/plugins/cors.js +3 -2
- package/plugins/csrf/index.js +9 -12
- package/plugins/csv/index.js +8 -5
- 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/email/index.js +11 -9
- package/plugins/health-check.js +2 -1
- package/plugins/index.js +5 -0
- package/plugins/lazy.js +5 -6
- package/plugins/mcp/discovery.js +18 -17
- package/plugins/polar/src/api.js +2 -1
- package/plugins/polar/src/webhooks.js +5 -6
- package/plugins/queue/index.js +18 -2
- package/plugins/rate-limit/index.js +3 -1
- package/plugins/redirect/index.js +16 -9
- package/plugins/rls/index.js +170 -0
- package/plugins/seo-checker/index.js +2 -2
- package/plugins/site-analytics/index.js +3 -1
- package/plugins/site-analytics/tracking.js +2 -2
- package/plugins/sitemap.js +3 -2
- package/plugins/swagger.js +12 -5
- package/plugins/upload/gcs-provider.js +148 -0
- package/plugins/upload/index.js +136 -77
- package/plugins/upload/orphan-pruner.js +4 -3
- package/plugins/upload/s3-provider.js +257 -0
- package/plugins/webhooks/crypto.js +6 -11
- package/plugins/webhooks/dispatcher.js +8 -8
- package/plugins/webhooks/index.js +33 -23
- package/plugins/webhooks/matcher.js +7 -11
- package/plugins/webhooks/sender.js +8 -11
- package/plugins/xlsx/index.js +7 -5
- package/src/api/api-loader.js +12 -95
- package/src/api/define-api.js +1 -0
- package/src/app-context.js +41 -5
- package/src/client-runtime/constants.js +10 -0
- package/src/client-runtime/mount.js +2 -3
- package/src/dev/banner.js +6 -2
- package/src/dev/client-gen.js +458 -0
- package/src/discovery/scan-routes.js +5 -2
- package/src/file-router.js +3 -3
- package/src/helpers.js +212 -24
- package/src/modules/module-discovery.js +1 -9
- package/src/pages/page-loader.js +13 -154
- package/src/plugin-manager.js +99 -18
- package/src/server.js +335 -133
- package/src/services/discovery.js +19 -40
- package/src/services/executor.js +95 -25
- package/src/services/memoize.js +3 -25
- package/src/services/registry.js +15 -1
- package/src/services/validator.js +38 -20
- package/src/template/precompile.js +8 -19
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso - Zero-Dependency String Utilities
|
|
5
|
+
* Fast, standard case conversion and string manipulation helpers.
|
|
6
|
+
* @module core/utils/string
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert a string (kebab-case, snake_case, spaces) to camelCase.
|
|
11
|
+
*
|
|
12
|
+
* @param {string|unknown} str - Input string
|
|
13
|
+
* @returns {string} camelCase formatted string
|
|
14
|
+
* @example
|
|
15
|
+
* toCamelCase('order-items') // -> 'orderItems'
|
|
16
|
+
* toCamelCase('get_by_id') // -> 'getById'
|
|
17
|
+
* toCamelCase('user_profile_data') // -> 'userProfileData'
|
|
18
|
+
*/
|
|
19
|
+
function toCamelCase(str) {
|
|
20
|
+
if (!str || typeof str !== 'string') return '';
|
|
21
|
+
const trimmed = str.replace(/^[-_\s]+|[-_\s]+$/g, '');
|
|
22
|
+
if (!trimmed) return '';
|
|
23
|
+
const res = trimmed.replace(/[-_\s]+([a-zA-Z0-9])/g, (_, letter) => letter.toUpperCase());
|
|
24
|
+
return res.charAt(0).toLowerCase() + res.slice(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Convert a string (camelCase, kebab-case, spaces) to snake_case.
|
|
29
|
+
*
|
|
30
|
+
* @param {string|unknown} str - Input string
|
|
31
|
+
* @returns {string} snake_case formatted string
|
|
32
|
+
* @example
|
|
33
|
+
* toSnakeCase('fooBarBaz') // -> 'foo_bar_baz'
|
|
34
|
+
* toSnakeCase('order-items') // -> 'order_items'
|
|
35
|
+
*/
|
|
36
|
+
function toSnakeCase(str) {
|
|
37
|
+
if (!str || typeof str !== 'string') return '';
|
|
38
|
+
return str
|
|
39
|
+
.trim()
|
|
40
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
41
|
+
.replace(/[-_\s]+/g, '_')
|
|
42
|
+
.replace(/^_+|_+$/g, '')
|
|
43
|
+
.toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Convert a string (camelCase, snake_case, spaces) to kebab-case.
|
|
48
|
+
*
|
|
49
|
+
* @param {string|unknown} str - Input string
|
|
50
|
+
* @returns {string} kebab-case formatted string
|
|
51
|
+
* @example
|
|
52
|
+
* toKebabCase('orderItems') // -> 'order-items'
|
|
53
|
+
* toKebabCase('foo_bar_baz') // -> 'foo-bar-baz'
|
|
54
|
+
*/
|
|
55
|
+
function toKebabCase(str) {
|
|
56
|
+
if (!str || typeof str !== 'string') return '';
|
|
57
|
+
return str
|
|
58
|
+
.trim()
|
|
59
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
60
|
+
.replace(/[-_\s]+/g, '-')
|
|
61
|
+
.replace(/^-+|-+$/g, '')
|
|
62
|
+
.toLowerCase();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Convert a string to PascalCase.
|
|
67
|
+
*
|
|
68
|
+
* @param {string|unknown} str - Input string
|
|
69
|
+
* @returns {string} PascalCase formatted string
|
|
70
|
+
* @example
|
|
71
|
+
* toPascalCase('order-items') // -> 'OrderItems'
|
|
72
|
+
* toPascalCase('user_id') // -> 'UserId'
|
|
73
|
+
*/
|
|
74
|
+
function toPascalCase(str) {
|
|
75
|
+
const camel = toCamelCase(str);
|
|
76
|
+
if (!camel) return '';
|
|
77
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
toCamelCase,
|
|
82
|
+
snakeToCamel: toCamelCase,
|
|
83
|
+
toSnakeCase,
|
|
84
|
+
camelToSnake: toSnakeCase,
|
|
85
|
+
toKebabCase,
|
|
86
|
+
toPascalCase,
|
|
87
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso - Zero-Dependency Time & Duration Utilities
|
|
5
|
+
* Fast, robust duration parsing and formatting helpers for cache TTL, JWT expiration, timeouts, and delays.
|
|
6
|
+
* @module core/utils/time
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parses duration / TTL input into seconds.
|
|
11
|
+
* Compatible with core/auth/jwt parseDuration contract.
|
|
12
|
+
*
|
|
13
|
+
* @param {string|number|unknown} duration - Duration string (e.g. '10s', '15m', '2h', '7d', '500') or seconds number
|
|
14
|
+
* @param {number} [defaultSec=3600] - Default seconds if input is invalid or missing
|
|
15
|
+
* @returns {number} Seconds
|
|
16
|
+
*/
|
|
17
|
+
function parseDurationSec(duration, defaultSec = 3600) {
|
|
18
|
+
if (typeof duration === 'number') {
|
|
19
|
+
return duration;
|
|
20
|
+
}
|
|
21
|
+
if (typeof duration !== 'string') {
|
|
22
|
+
return defaultSec;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const match = duration.trim().match(/^(\d+)\s*([smhdw])?$/i);
|
|
26
|
+
if (!match) {
|
|
27
|
+
const parsed = parseInt(duration, 10);
|
|
28
|
+
return isNaN(parsed) ? defaultSec : parsed;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const value = parseInt(match[1], 10);
|
|
32
|
+
const unit = (match[2] || 's').toLowerCase();
|
|
33
|
+
|
|
34
|
+
switch (unit) {
|
|
35
|
+
case 's': return value;
|
|
36
|
+
case 'm': return value * 60;
|
|
37
|
+
case 'h': return value * 3600;
|
|
38
|
+
case 'd': return value * 86400;
|
|
39
|
+
case 'w': return value * 604800;
|
|
40
|
+
default: return value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parses TTL / duration input into milliseconds.
|
|
46
|
+
* Compatible with src/services/memoize parseTtlMs contract.
|
|
47
|
+
*
|
|
48
|
+
* @param {string|number|boolean|unknown} ttl - Duration string ('5s', '1m', '2h', '1d', '100'), ms number, or boolean
|
|
49
|
+
* @param {number} [defaultMs=60000] - Default milliseconds if input is invalid or missing
|
|
50
|
+
* @returns {number} Milliseconds
|
|
51
|
+
*/
|
|
52
|
+
function parseDurationMs(ttl, defaultMs = 60000) {
|
|
53
|
+
if (typeof ttl === 'number') {
|
|
54
|
+
return ttl > 0 ? ttl : defaultMs;
|
|
55
|
+
}
|
|
56
|
+
if (ttl === true || !ttl) {
|
|
57
|
+
return defaultMs;
|
|
58
|
+
}
|
|
59
|
+
if (typeof ttl === 'string') {
|
|
60
|
+
const match = ttl.trim().match(/^(\d+)\s*([smhdw]|ms)?$/i);
|
|
61
|
+
if (match) {
|
|
62
|
+
const val = parseInt(match[1], 10);
|
|
63
|
+
const unit = (match[2] || 's').toLowerCase();
|
|
64
|
+
switch (unit) {
|
|
65
|
+
case 'ms': return val;
|
|
66
|
+
case 's': return val * 1_000;
|
|
67
|
+
case 'm': return val * 60_000;
|
|
68
|
+
case 'h': return val * 3_600_000;
|
|
69
|
+
case 'd': return val * 86_400_000;
|
|
70
|
+
case 'w': return val * 604_800_000;
|
|
71
|
+
default: return val * 1_000;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const num = parseInt(ttl, 10);
|
|
75
|
+
return !Number.isNaN(num) && num > 0 ? num : defaultMs;
|
|
76
|
+
}
|
|
77
|
+
return defaultMs;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns a Promise that resolves after the specified milliseconds.
|
|
82
|
+
*
|
|
83
|
+
* @param {number} ms - Milliseconds to sleep
|
|
84
|
+
* @returns {Promise<void>}
|
|
85
|
+
*/
|
|
86
|
+
function sleep(ms) {
|
|
87
|
+
return new Promise(resolve => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
parseDurationSec,
|
|
92
|
+
parseDurationMs,
|
|
93
|
+
parseDuration: parseDurationSec,
|
|
94
|
+
parseTtlMs: parseDurationMs,
|
|
95
|
+
sleep,
|
|
96
|
+
};
|
|
@@ -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
|
+
};
|