@mandujs/core 0.53.3 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +654 -654
- package/package.json +2 -2
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -451
- package/src/diagnose/checks.ts +832 -832
- package/src/diagnose/index.ts +17 -17
- package/src/diagnose/run.ts +93 -93
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
package/src/runtime/trace.ts
CHANGED
|
@@ -1,144 +1,144 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Trace 🧭
|
|
3
|
-
* Lifecycle 단계별 추적 (옵션)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ManduContext } from "../filling/context";
|
|
7
|
-
|
|
8
|
-
export type TraceEvent =
|
|
9
|
-
| "request"
|
|
10
|
-
| "parse"
|
|
11
|
-
| "transform"
|
|
12
|
-
| "beforeHandle"
|
|
13
|
-
| "handle"
|
|
14
|
-
| "afterHandle"
|
|
15
|
-
| "mapResponse"
|
|
16
|
-
| "afterResponse"
|
|
17
|
-
| "error";
|
|
18
|
-
|
|
19
|
-
export type TracePhase = "begin" | "end" | "error";
|
|
20
|
-
|
|
21
|
-
export interface TraceEntry {
|
|
22
|
-
event: TraceEvent;
|
|
23
|
-
phase: TracePhase;
|
|
24
|
-
time: number;
|
|
25
|
-
name?: string;
|
|
26
|
-
error?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface TraceCollector {
|
|
30
|
-
records: TraceEntry[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface TraceReportEntry {
|
|
34
|
-
event: TraceEvent;
|
|
35
|
-
name?: string;
|
|
36
|
-
start: number;
|
|
37
|
-
end: number;
|
|
38
|
-
duration: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface TraceReport {
|
|
42
|
-
entries: TraceReportEntry[];
|
|
43
|
-
errors: TraceEntry[];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const TRACE_KEY = "__mandu_trace";
|
|
47
|
-
|
|
48
|
-
const now = (): number => {
|
|
49
|
-
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
50
|
-
return performance.now();
|
|
51
|
-
}
|
|
52
|
-
return Date.now();
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export function enableTrace(ctx: ManduContext): TraceCollector {
|
|
56
|
-
const existing = ctx.get<TraceCollector>(TRACE_KEY);
|
|
57
|
-
if (existing) return existing;
|
|
58
|
-
const collector: TraceCollector = { records: [] };
|
|
59
|
-
ctx.set(TRACE_KEY, collector);
|
|
60
|
-
return collector;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function getTrace(ctx: ManduContext): TraceCollector | undefined {
|
|
64
|
-
return ctx.get<TraceCollector>(TRACE_KEY);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Build a normalized trace report with durations
|
|
69
|
-
*/
|
|
70
|
-
export function buildTraceReport(collector: TraceCollector): TraceReport {
|
|
71
|
-
const entries: TraceReportEntry[] = [];
|
|
72
|
-
const errors: TraceEntry[] = [];
|
|
73
|
-
const stacks = new Map<string, TraceEntry[]>();
|
|
74
|
-
|
|
75
|
-
for (const record of collector.records) {
|
|
76
|
-
if (record.phase === "error") {
|
|
77
|
-
errors.push(record);
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const key = `${record.event}:${record.name ?? ""}`;
|
|
82
|
-
if (record.phase === "begin") {
|
|
83
|
-
const stack = stacks.get(key) ?? [];
|
|
84
|
-
stack.push(record);
|
|
85
|
-
stacks.set(key, stack);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (record.phase === "end") {
|
|
90
|
-
const stack = stacks.get(key);
|
|
91
|
-
const begin = stack?.pop();
|
|
92
|
-
if (!begin) continue;
|
|
93
|
-
entries.push({
|
|
94
|
-
event: record.event,
|
|
95
|
-
name: record.name,
|
|
96
|
-
start: begin.time,
|
|
97
|
-
end: record.time,
|
|
98
|
-
duration: record.time - begin.time,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return { entries, errors };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Convert trace report to JSON string
|
|
108
|
-
*/
|
|
109
|
-
export function formatTraceReport(report: TraceReport): string {
|
|
110
|
-
return JSON.stringify(report, null, 2);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface Tracer {
|
|
114
|
-
enabled: boolean;
|
|
115
|
-
begin: (event: TraceEvent, name?: string) => () => void;
|
|
116
|
-
error: (event: TraceEvent, err: unknown, name?: string) => void;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const NOOP_TRACER: Tracer = {
|
|
120
|
-
enabled: false,
|
|
121
|
-
begin: () => () => {},
|
|
122
|
-
error: () => {},
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
export function createTracer(ctx: ManduContext, enabled?: boolean): Tracer {
|
|
126
|
-
const shouldEnable = Boolean(enabled) || ctx.has(TRACE_KEY);
|
|
127
|
-
if (!shouldEnable) return NOOP_TRACER;
|
|
128
|
-
|
|
129
|
-
const collector = enableTrace(ctx);
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
enabled: true,
|
|
133
|
-
begin: (event, name) => {
|
|
134
|
-
collector.records.push({ event, phase: "begin", time: now(), name });
|
|
135
|
-
return () => {
|
|
136
|
-
collector.records.push({ event, phase: "end", time: now(), name });
|
|
137
|
-
};
|
|
138
|
-
},
|
|
139
|
-
error: (event, err, name) => {
|
|
140
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
141
|
-
collector.records.push({ event, phase: "error", time: now(), name, error: message });
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Trace 🧭
|
|
3
|
+
* Lifecycle 단계별 추적 (옵션)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ManduContext } from "../filling/context";
|
|
7
|
+
|
|
8
|
+
export type TraceEvent =
|
|
9
|
+
| "request"
|
|
10
|
+
| "parse"
|
|
11
|
+
| "transform"
|
|
12
|
+
| "beforeHandle"
|
|
13
|
+
| "handle"
|
|
14
|
+
| "afterHandle"
|
|
15
|
+
| "mapResponse"
|
|
16
|
+
| "afterResponse"
|
|
17
|
+
| "error";
|
|
18
|
+
|
|
19
|
+
export type TracePhase = "begin" | "end" | "error";
|
|
20
|
+
|
|
21
|
+
export interface TraceEntry {
|
|
22
|
+
event: TraceEvent;
|
|
23
|
+
phase: TracePhase;
|
|
24
|
+
time: number;
|
|
25
|
+
name?: string;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TraceCollector {
|
|
30
|
+
records: TraceEntry[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TraceReportEntry {
|
|
34
|
+
event: TraceEvent;
|
|
35
|
+
name?: string;
|
|
36
|
+
start: number;
|
|
37
|
+
end: number;
|
|
38
|
+
duration: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TraceReport {
|
|
42
|
+
entries: TraceReportEntry[];
|
|
43
|
+
errors: TraceEntry[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const TRACE_KEY = "__mandu_trace";
|
|
47
|
+
|
|
48
|
+
const now = (): number => {
|
|
49
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
50
|
+
return performance.now();
|
|
51
|
+
}
|
|
52
|
+
return Date.now();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function enableTrace(ctx: ManduContext): TraceCollector {
|
|
56
|
+
const existing = ctx.get<TraceCollector>(TRACE_KEY);
|
|
57
|
+
if (existing) return existing;
|
|
58
|
+
const collector: TraceCollector = { records: [] };
|
|
59
|
+
ctx.set(TRACE_KEY, collector);
|
|
60
|
+
return collector;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getTrace(ctx: ManduContext): TraceCollector | undefined {
|
|
64
|
+
return ctx.get<TraceCollector>(TRACE_KEY);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build a normalized trace report with durations
|
|
69
|
+
*/
|
|
70
|
+
export function buildTraceReport(collector: TraceCollector): TraceReport {
|
|
71
|
+
const entries: TraceReportEntry[] = [];
|
|
72
|
+
const errors: TraceEntry[] = [];
|
|
73
|
+
const stacks = new Map<string, TraceEntry[]>();
|
|
74
|
+
|
|
75
|
+
for (const record of collector.records) {
|
|
76
|
+
if (record.phase === "error") {
|
|
77
|
+
errors.push(record);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const key = `${record.event}:${record.name ?? ""}`;
|
|
82
|
+
if (record.phase === "begin") {
|
|
83
|
+
const stack = stacks.get(key) ?? [];
|
|
84
|
+
stack.push(record);
|
|
85
|
+
stacks.set(key, stack);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (record.phase === "end") {
|
|
90
|
+
const stack = stacks.get(key);
|
|
91
|
+
const begin = stack?.pop();
|
|
92
|
+
if (!begin) continue;
|
|
93
|
+
entries.push({
|
|
94
|
+
event: record.event,
|
|
95
|
+
name: record.name,
|
|
96
|
+
start: begin.time,
|
|
97
|
+
end: record.time,
|
|
98
|
+
duration: record.time - begin.time,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { entries, errors };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Convert trace report to JSON string
|
|
108
|
+
*/
|
|
109
|
+
export function formatTraceReport(report: TraceReport): string {
|
|
110
|
+
return JSON.stringify(report, null, 2);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface Tracer {
|
|
114
|
+
enabled: boolean;
|
|
115
|
+
begin: (event: TraceEvent, name?: string) => () => void;
|
|
116
|
+
error: (event: TraceEvent, err: unknown, name?: string) => void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const NOOP_TRACER: Tracer = {
|
|
120
|
+
enabled: false,
|
|
121
|
+
begin: () => () => {},
|
|
122
|
+
error: () => {},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export function createTracer(ctx: ManduContext, enabled?: boolean): Tracer {
|
|
126
|
+
const shouldEnable = Boolean(enabled) || ctx.has(TRACE_KEY);
|
|
127
|
+
if (!shouldEnable) return NOOP_TRACER;
|
|
128
|
+
|
|
129
|
+
const collector = enableTrace(ctx);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
enabled: true,
|
|
133
|
+
begin: (event, name) => {
|
|
134
|
+
collector.records.push({ event, phase: "begin", time: now(), name });
|
|
135
|
+
return () => {
|
|
136
|
+
collector.records.push({ event, phase: "end", time: now(), name });
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
error: (event, err, name) => {
|
|
140
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
141
|
+
collector.records.push({ event, phase: "error", time: now(), name, error: message });
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -1,169 +1,169 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cron expression validation.
|
|
3
|
-
*
|
|
4
|
-
* `Bun.cron` and Cloudflare Workers Cron Triggers both accept standard 5-field
|
|
5
|
-
* POSIX-style crontab expressions plus a handful of named aliases:
|
|
6
|
-
*
|
|
7
|
-
* ┌───────────── minute (0 - 59)
|
|
8
|
-
* │ ┌─────────── hour (0 - 23)
|
|
9
|
-
* │ │ ┌───────── day of month (1 - 31)
|
|
10
|
-
* │ │ │ ┌─────── month (1 - 12) or Jan-Dec
|
|
11
|
-
* │ │ │ │ ┌───── day of week (0 - 6) or Sun-Sat (0 and 7 both = Sunday)
|
|
12
|
-
* │ │ │ │ │
|
|
13
|
-
* * * * * *
|
|
14
|
-
*
|
|
15
|
-
* Each field supports: `*`, lists (`1,2,3`), ranges (`1-5`), step values
|
|
16
|
-
* (`*\/15`, `0-30/5`), and for month/day-of-week the named aliases. We keep
|
|
17
|
-
* the validator *syntactic* — we don't try to catch logically impossible dates
|
|
18
|
-
* (`31 2 *`) because cron runtimes silently skip them anyway and a too-strict
|
|
19
|
-
* validator would reject expressions that work fine in production.
|
|
20
|
-
*
|
|
21
|
-
* Named aliases (non-standard but widely supported by vixie-cron, Bun, and
|
|
22
|
-
* Cloudflare): `@yearly`, `@annually`, `@monthly`, `@weekly`, `@daily`,
|
|
23
|
-
* `@midnight`, `@hourly`. We permit these as a single-token input.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
const NAMED_ALIASES = new Set([
|
|
27
|
-
"@yearly",
|
|
28
|
-
"@annually",
|
|
29
|
-
"@monthly",
|
|
30
|
-
"@weekly",
|
|
31
|
-
"@daily",
|
|
32
|
-
"@midnight",
|
|
33
|
-
"@hourly",
|
|
34
|
-
]);
|
|
35
|
-
|
|
36
|
-
const MONTH_NAMES = new Set([
|
|
37
|
-
"jan", "feb", "mar", "apr", "may", "jun",
|
|
38
|
-
"jul", "aug", "sep", "oct", "nov", "dec",
|
|
39
|
-
]);
|
|
40
|
-
|
|
41
|
-
const DOW_NAMES = new Set([
|
|
42
|
-
"sun", "mon", "tue", "wed", "thu", "fri", "sat",
|
|
43
|
-
]);
|
|
44
|
-
|
|
45
|
-
interface FieldSpec {
|
|
46
|
-
min: number;
|
|
47
|
-
max: number;
|
|
48
|
-
/** Names this field accepts in place of numbers (lowercased). */
|
|
49
|
-
names?: Set<string>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const FIELDS: readonly FieldSpec[] = [
|
|
53
|
-
{ min: 0, max: 59 }, // minute
|
|
54
|
-
{ min: 0, max: 23 }, // hour
|
|
55
|
-
{ min: 1, max: 31 }, // dom
|
|
56
|
-
{ min: 1, max: 12, names: MONTH_NAMES }, // month
|
|
57
|
-
{ min: 0, max: 7, names: DOW_NAMES }, // dow (0 and 7 both = Sunday)
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
function resolveToken(token: string, spec: FieldSpec): number | null {
|
|
61
|
-
const lower = token.toLowerCase();
|
|
62
|
-
if (spec.names?.has(lower)) {
|
|
63
|
-
// Return any number in range — we only care that the token is recognized.
|
|
64
|
-
return spec.min;
|
|
65
|
-
}
|
|
66
|
-
if (!/^\d+$/.test(token)) return null;
|
|
67
|
-
const n = Number(token);
|
|
68
|
-
if (!Number.isFinite(n)) return null;
|
|
69
|
-
if (n < spec.min || n > spec.max) return null;
|
|
70
|
-
return n;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function validateField(field: string, spec: FieldSpec): boolean {
|
|
74
|
-
if (field.length === 0) return false;
|
|
75
|
-
|
|
76
|
-
// Split on commas for list form.
|
|
77
|
-
const parts = field.split(",");
|
|
78
|
-
for (const part of parts) {
|
|
79
|
-
if (part.length === 0) return false;
|
|
80
|
-
|
|
81
|
-
// Step form: `X/Y` or `*/Y` or `A-B/Y`.
|
|
82
|
-
const [rangePart, stepPart] = part.split("/");
|
|
83
|
-
if (stepPart !== undefined) {
|
|
84
|
-
if (!/^\d+$/.test(stepPart)) return false;
|
|
85
|
-
const step = Number(stepPart);
|
|
86
|
-
if (step < 1) return false;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (rangePart === "*") continue;
|
|
90
|
-
|
|
91
|
-
// Range form: `A-B`.
|
|
92
|
-
if (rangePart.includes("-")) {
|
|
93
|
-
const [lo, hi] = rangePart.split("-");
|
|
94
|
-
if (lo === undefined || hi === undefined) return false;
|
|
95
|
-
const a = resolveToken(lo, spec);
|
|
96
|
-
const b = resolveToken(hi, spec);
|
|
97
|
-
if (a === null || b === null) return false;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Single token.
|
|
102
|
-
if (resolveToken(rangePart, spec) === null) return false;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Validate a cron expression at `defineCron` time. Throws with an actionable
|
|
110
|
-
* error when the schedule is malformed, so the user sees the problem during
|
|
111
|
-
* boot instead of a silent "job never fires".
|
|
112
|
-
*/
|
|
113
|
-
export function validateCronExpression(expr: string): void {
|
|
114
|
-
if (typeof expr !== "string" || expr.trim().length === 0) {
|
|
115
|
-
throw new Error(
|
|
116
|
-
`[@mandujs/core/scheduler] invalid cron expression: expected a non-empty string (got: ${JSON.stringify(expr)}).`,
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const trimmed = expr.trim();
|
|
121
|
-
|
|
122
|
-
// Named aliases are a single token.
|
|
123
|
-
if (trimmed.startsWith("@")) {
|
|
124
|
-
if (NAMED_ALIASES.has(trimmed.toLowerCase())) return;
|
|
125
|
-
throw new Error(
|
|
126
|
-
`[@mandujs/core/scheduler] invalid cron alias "${trimmed}". ` +
|
|
127
|
-
`Supported aliases: ${[...NAMED_ALIASES].join(", ")}.`,
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const fields = trimmed.split(/\s+/);
|
|
132
|
-
if (fields.length !== 5) {
|
|
133
|
-
throw new Error(
|
|
134
|
-
`[@mandujs/core/scheduler] invalid cron expression "${expr}" — ` +
|
|
135
|
-
`expected 5 space-separated fields (minute hour day-of-month month day-of-week), got ${fields.length}.`,
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
for (let i = 0; i < fields.length; i++) {
|
|
140
|
-
if (!validateField(fields[i], FIELDS[i])) {
|
|
141
|
-
const fieldNames = ["minute", "hour", "day-of-month", "month", "day-of-week"];
|
|
142
|
-
throw new Error(
|
|
143
|
-
`[@mandujs/core/scheduler] invalid cron expression "${expr}" — ` +
|
|
144
|
-
`field #${i + 1} (${fieldNames[i]}): "${fields[i]}" is out of range or malformed.`,
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Validate an IANA timezone string. Uses Intl.DateTimeFormat support detection
|
|
152
|
-
* — the same approach the Temporal proposal recommends.
|
|
153
|
-
*/
|
|
154
|
-
export function validateTimezone(tz: string): void {
|
|
155
|
-
if (typeof tz !== "string" || tz.length === 0) {
|
|
156
|
-
throw new Error(
|
|
157
|
-
`[@mandujs/core/scheduler] invalid timezone: expected a non-empty IANA string (got: ${JSON.stringify(tz)}).`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
try {
|
|
161
|
-
// `Intl.DateTimeFormat` throws `RangeError` for unknown zones.
|
|
162
|
-
Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
163
|
-
} catch {
|
|
164
|
-
throw new Error(
|
|
165
|
-
`[@mandujs/core/scheduler] unknown IANA timezone "${tz}". ` +
|
|
166
|
-
`Examples: "UTC", "America/New_York", "Asia/Seoul".`,
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Cron expression validation.
|
|
3
|
+
*
|
|
4
|
+
* `Bun.cron` and Cloudflare Workers Cron Triggers both accept standard 5-field
|
|
5
|
+
* POSIX-style crontab expressions plus a handful of named aliases:
|
|
6
|
+
*
|
|
7
|
+
* ┌───────────── minute (0 - 59)
|
|
8
|
+
* │ ┌─────────── hour (0 - 23)
|
|
9
|
+
* │ │ ┌───────── day of month (1 - 31)
|
|
10
|
+
* │ │ │ ┌─────── month (1 - 12) or Jan-Dec
|
|
11
|
+
* │ │ │ │ ┌───── day of week (0 - 6) or Sun-Sat (0 and 7 both = Sunday)
|
|
12
|
+
* │ │ │ │ │
|
|
13
|
+
* * * * * *
|
|
14
|
+
*
|
|
15
|
+
* Each field supports: `*`, lists (`1,2,3`), ranges (`1-5`), step values
|
|
16
|
+
* (`*\/15`, `0-30/5`), and for month/day-of-week the named aliases. We keep
|
|
17
|
+
* the validator *syntactic* — we don't try to catch logically impossible dates
|
|
18
|
+
* (`31 2 *`) because cron runtimes silently skip them anyway and a too-strict
|
|
19
|
+
* validator would reject expressions that work fine in production.
|
|
20
|
+
*
|
|
21
|
+
* Named aliases (non-standard but widely supported by vixie-cron, Bun, and
|
|
22
|
+
* Cloudflare): `@yearly`, `@annually`, `@monthly`, `@weekly`, `@daily`,
|
|
23
|
+
* `@midnight`, `@hourly`. We permit these as a single-token input.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const NAMED_ALIASES = new Set([
|
|
27
|
+
"@yearly",
|
|
28
|
+
"@annually",
|
|
29
|
+
"@monthly",
|
|
30
|
+
"@weekly",
|
|
31
|
+
"@daily",
|
|
32
|
+
"@midnight",
|
|
33
|
+
"@hourly",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const MONTH_NAMES = new Set([
|
|
37
|
+
"jan", "feb", "mar", "apr", "may", "jun",
|
|
38
|
+
"jul", "aug", "sep", "oct", "nov", "dec",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const DOW_NAMES = new Set([
|
|
42
|
+
"sun", "mon", "tue", "wed", "thu", "fri", "sat",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
interface FieldSpec {
|
|
46
|
+
min: number;
|
|
47
|
+
max: number;
|
|
48
|
+
/** Names this field accepts in place of numbers (lowercased). */
|
|
49
|
+
names?: Set<string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const FIELDS: readonly FieldSpec[] = [
|
|
53
|
+
{ min: 0, max: 59 }, // minute
|
|
54
|
+
{ min: 0, max: 23 }, // hour
|
|
55
|
+
{ min: 1, max: 31 }, // dom
|
|
56
|
+
{ min: 1, max: 12, names: MONTH_NAMES }, // month
|
|
57
|
+
{ min: 0, max: 7, names: DOW_NAMES }, // dow (0 and 7 both = Sunday)
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
function resolveToken(token: string, spec: FieldSpec): number | null {
|
|
61
|
+
const lower = token.toLowerCase();
|
|
62
|
+
if (spec.names?.has(lower)) {
|
|
63
|
+
// Return any number in range — we only care that the token is recognized.
|
|
64
|
+
return spec.min;
|
|
65
|
+
}
|
|
66
|
+
if (!/^\d+$/.test(token)) return null;
|
|
67
|
+
const n = Number(token);
|
|
68
|
+
if (!Number.isFinite(n)) return null;
|
|
69
|
+
if (n < spec.min || n > spec.max) return null;
|
|
70
|
+
return n;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateField(field: string, spec: FieldSpec): boolean {
|
|
74
|
+
if (field.length === 0) return false;
|
|
75
|
+
|
|
76
|
+
// Split on commas for list form.
|
|
77
|
+
const parts = field.split(",");
|
|
78
|
+
for (const part of parts) {
|
|
79
|
+
if (part.length === 0) return false;
|
|
80
|
+
|
|
81
|
+
// Step form: `X/Y` or `*/Y` or `A-B/Y`.
|
|
82
|
+
const [rangePart, stepPart] = part.split("/");
|
|
83
|
+
if (stepPart !== undefined) {
|
|
84
|
+
if (!/^\d+$/.test(stepPart)) return false;
|
|
85
|
+
const step = Number(stepPart);
|
|
86
|
+
if (step < 1) return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (rangePart === "*") continue;
|
|
90
|
+
|
|
91
|
+
// Range form: `A-B`.
|
|
92
|
+
if (rangePart.includes("-")) {
|
|
93
|
+
const [lo, hi] = rangePart.split("-");
|
|
94
|
+
if (lo === undefined || hi === undefined) return false;
|
|
95
|
+
const a = resolveToken(lo, spec);
|
|
96
|
+
const b = resolveToken(hi, spec);
|
|
97
|
+
if (a === null || b === null) return false;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Single token.
|
|
102
|
+
if (resolveToken(rangePart, spec) === null) return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Validate a cron expression at `defineCron` time. Throws with an actionable
|
|
110
|
+
* error when the schedule is malformed, so the user sees the problem during
|
|
111
|
+
* boot instead of a silent "job never fires".
|
|
112
|
+
*/
|
|
113
|
+
export function validateCronExpression(expr: string): void {
|
|
114
|
+
if (typeof expr !== "string" || expr.trim().length === 0) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`[@mandujs/core/scheduler] invalid cron expression: expected a non-empty string (got: ${JSON.stringify(expr)}).`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const trimmed = expr.trim();
|
|
121
|
+
|
|
122
|
+
// Named aliases are a single token.
|
|
123
|
+
if (trimmed.startsWith("@")) {
|
|
124
|
+
if (NAMED_ALIASES.has(trimmed.toLowerCase())) return;
|
|
125
|
+
throw new Error(
|
|
126
|
+
`[@mandujs/core/scheduler] invalid cron alias "${trimmed}". ` +
|
|
127
|
+
`Supported aliases: ${[...NAMED_ALIASES].join(", ")}.`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const fields = trimmed.split(/\s+/);
|
|
132
|
+
if (fields.length !== 5) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`[@mandujs/core/scheduler] invalid cron expression "${expr}" — ` +
|
|
135
|
+
`expected 5 space-separated fields (minute hour day-of-month month day-of-week), got ${fields.length}.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (let i = 0; i < fields.length; i++) {
|
|
140
|
+
if (!validateField(fields[i], FIELDS[i])) {
|
|
141
|
+
const fieldNames = ["minute", "hour", "day-of-month", "month", "day-of-week"];
|
|
142
|
+
throw new Error(
|
|
143
|
+
`[@mandujs/core/scheduler] invalid cron expression "${expr}" — ` +
|
|
144
|
+
`field #${i + 1} (${fieldNames[i]}): "${fields[i]}" is out of range or malformed.`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Validate an IANA timezone string. Uses Intl.DateTimeFormat support detection
|
|
152
|
+
* — the same approach the Temporal proposal recommends.
|
|
153
|
+
*/
|
|
154
|
+
export function validateTimezone(tz: string): void {
|
|
155
|
+
if (typeof tz !== "string" || tz.length === 0) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`[@mandujs/core/scheduler] invalid timezone: expected a non-empty IANA string (got: ${JSON.stringify(tz)}).`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
// `Intl.DateTimeFormat` throws `RangeError` for unknown zones.
|
|
162
|
+
Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
163
|
+
} catch {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`[@mandujs/core/scheduler] unknown IANA timezone "${tz}". ` +
|
|
166
|
+
`Examples: "UTC", "America/New_York", "Asia/Seoul".`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|