@mandujs/core 0.54.12 → 0.54.13

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.
Files changed (76) hide show
  1. package/package.json +9 -1
  2. package/scripts/postinstall-lock.ts +153 -153
  3. package/src/a11y/run-audit.ts +15 -15
  4. package/src/agent/__tests__/context.test.ts +49 -1
  5. package/src/agent/context.ts +535 -535
  6. package/src/agent/index.ts +6 -6
  7. package/src/agent/plan.ts +282 -282
  8. package/src/agent/repair.ts +171 -171
  9. package/src/agent/sync.ts +200 -200
  10. package/src/agent/types.ts +8 -0
  11. package/src/agent/verify.ts +100 -2
  12. package/src/brain/doctor/analyzer.ts +7 -7
  13. package/src/bundler/__tests__/build-runner.ts +5 -4
  14. package/src/bundler/__tests__/cold-start.test.ts +60 -60
  15. package/src/bundler/__tests__/css.test.ts +20 -20
  16. package/src/bundler/analyzer.ts +15 -15
  17. package/src/bundler/build.test.ts +66 -15
  18. package/src/bundler/build.ts +165 -103
  19. package/src/bundler/css.ts +42 -42
  20. package/src/bundler/manifest-schema.ts +21 -21
  21. package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
  22. package/src/bundler/plugins/block-generated-imports.ts +13 -13
  23. package/src/bundler/types.ts +31 -31
  24. package/src/client/island.ts +79 -79
  25. package/src/config/validate.ts +1 -1
  26. package/src/contract/schema.ts +7 -0
  27. package/src/deploy/inference/context.ts +82 -82
  28. package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
  29. package/src/devtools/client/components/panel/panel-container.tsx +1 -1
  30. package/src/error/formatter.ts +10 -1
  31. package/src/experimental/index.ts +10 -0
  32. package/src/filling/context.ts +17 -17
  33. package/src/filling/filling.ts +22 -1
  34. package/src/filling/index.ts +15 -1
  35. package/src/generator/generate.ts +30 -30
  36. package/src/generator/index.ts +3 -3
  37. package/src/generator/templates.ts +210 -210
  38. package/src/guard/check.ts +9 -9
  39. package/src/guard/config-guard.ts +13 -13
  40. package/src/guard/fs-routes-policy.ts +51 -51
  41. package/src/guard/index.ts +11 -11
  42. package/src/index.ts +0 -10
  43. package/src/internal/index.ts +25 -0
  44. package/src/kitchen/api/file-api.ts +11 -11
  45. package/src/report/index.ts +1 -1
  46. package/src/resource/__tests__/generator.test.ts +6 -6
  47. package/src/resource/__tests__/schema.test.ts +14 -14
  48. package/src/resource/ddl/__tests__/emit.test.ts +165 -165
  49. package/src/resource/ddl/emit.ts +146 -146
  50. package/src/resource/generator-schema.ts +11 -11
  51. package/src/resource/generators/slot.ts +72 -72
  52. package/src/resource/schema.ts +21 -21
  53. package/src/router/client-entry.test.ts +69 -33
  54. package/src/router/client-entry.ts +134 -74
  55. package/src/router/fs-routes.ts +24 -22
  56. package/src/router/fs-scanner.ts +21 -17
  57. package/src/router/fs-types.ts +8 -5
  58. package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
  59. package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
  60. package/src/runtime/__tests__/page-render-response.test.ts +103 -103
  61. package/src/runtime/__tests__/request-middleware.test.ts +70 -70
  62. package/src/runtime/devtools-adapter.ts +68 -68
  63. package/src/runtime/escape.ts +34 -34
  64. package/src/runtime/image-feature.ts +15 -0
  65. package/src/runtime/observability-lifecycle.ts +290 -290
  66. package/src/runtime/page-render-response.ts +106 -106
  67. package/src/runtime/rate-limit.ts +231 -0
  68. package/src/runtime/request-middleware.ts +31 -31
  69. package/src/runtime/scheduler-lifecycle.ts +64 -0
  70. package/src/runtime/server.ts +27 -295
  71. package/src/runtime/ssr.ts +59 -59
  72. package/src/runtime/static-files.ts +289 -289
  73. package/src/runtime/streaming-ssr.ts +22 -22
  74. package/src/spec/schema.ts +4 -3
  75. package/src/watcher/__tests__/watcher.test.ts +59 -59
  76. package/src/watcher/watcher.ts +61 -61
@@ -1,106 +1,106 @@
1
- import type React from "react";
2
- import type { BundleManifest } from "../bundler/types";
3
- import type { HydrationConfig } from "../spec/schema";
4
- import type { CookieManager } from "../filling/context";
5
- import { renderSSR, renderStreamingResponse, resolveAsyncElement } from "./ssr";
6
-
7
- export interface PageRenderResponseOptions {
8
- app: React.ReactElement;
9
- useStreaming: boolean;
10
- title: string;
11
- headTags: string;
12
- isDev: boolean;
13
- hmrPort?: number;
14
- routeId: string;
15
- routePattern: string;
16
- layoutChain?: string[];
17
- hydration?: HydrationConfig;
18
- bundleManifest?: BundleManifest;
19
- loaderData: unknown;
20
- cssPath?: string | false;
21
- transitions?: boolean;
22
- prefetch?: boolean;
23
- spa?: boolean;
24
- devtools?: boolean;
25
- islandPreWrapped?: boolean;
26
- cookies?: CookieManager;
27
- }
28
-
29
- export async function renderPageResponse(
30
- options: PageRenderResponseOptions
31
- ): Promise<Response> {
32
- let app = options.app;
33
-
34
- if (!options.useStreaming) {
35
- app = (await resolveAsyncElement(app)) as React.ReactElement;
36
- }
37
-
38
- const response = options.useStreaming
39
- ? await renderStreamingPageResponse(app, options)
40
- : renderNonStreamingPageResponse(app, options);
41
-
42
- return options.cookies ? options.cookies.applyToResponse(response) : response;
43
- }
44
-
45
- async function renderStreamingPageResponse(
46
- app: React.ReactElement,
47
- options: PageRenderResponseOptions
48
- ): Promise<Response> {
49
- return renderStreamingResponse(app, {
50
- title: options.title,
51
- headTags: options.headTags,
52
- isDev: options.isDev,
53
- hmrPort: options.hmrPort,
54
- routeId: options.routeId,
55
- routePattern: options.routePattern,
56
- layoutChain: options.layoutChain,
57
- hydration: options.hydration,
58
- bundleManifest: options.bundleManifest,
59
- criticalData: options.loaderData as Record<string, unknown> | undefined,
60
- enableClientRouter: true,
61
- cssPath: options.cssPath,
62
- transitions: options.transitions,
63
- prefetch: options.prefetch,
64
- spa: options.spa,
65
- devtools: options.devtools,
66
- onShellReady: () => {
67
- if (options.isDev) {
68
- console.log(`[Mandu Streaming] Shell ready: ${options.routeId}`);
69
- }
70
- },
71
- onMetrics: (metrics) => {
72
- if (options.isDev) {
73
- console.log(`[Mandu Streaming] Metrics for ${options.routeId}:`, {
74
- shellReadyTime: `${metrics.shellReadyTime}ms`,
75
- allReadyTime: `${metrics.allReadyTime}ms`,
76
- hasError: metrics.hasError,
77
- });
78
- }
79
- },
80
- });
81
- }
82
-
83
- function renderNonStreamingPageResponse(
84
- app: React.ReactElement,
85
- options: PageRenderResponseOptions
86
- ): Response {
87
- return renderSSR(app, {
88
- title: options.title,
89
- headTags: options.headTags,
90
- isDev: options.isDev,
91
- hmrPort: options.hmrPort,
92
- routeId: options.routeId,
93
- hydration: options.hydration,
94
- bundleManifest: options.bundleManifest,
95
- serverData: options.loaderData,
96
- enableClientRouter: true,
97
- routePattern: options.routePattern,
98
- cssPath: options.cssPath,
99
- islandPreWrapped: !!options.islandPreWrapped,
100
- transitions: options.transitions,
101
- prefetch: options.prefetch,
102
- spa: options.spa,
103
- devtools: options.devtools,
104
- layoutChain: options.layoutChain,
105
- });
106
- }
1
+ import type React from "react";
2
+ import type { BundleManifest } from "../bundler/types";
3
+ import type { HydrationConfig } from "../spec/schema";
4
+ import type { CookieManager } from "../filling/context";
5
+ import { renderSSR, renderStreamingResponse, resolveAsyncElement } from "./ssr";
6
+
7
+ export interface PageRenderResponseOptions {
8
+ app: React.ReactElement;
9
+ useStreaming: boolean;
10
+ title: string;
11
+ headTags: string;
12
+ isDev: boolean;
13
+ hmrPort?: number;
14
+ routeId: string;
15
+ routePattern: string;
16
+ layoutChain?: string[];
17
+ hydration?: HydrationConfig;
18
+ bundleManifest?: BundleManifest;
19
+ loaderData: unknown;
20
+ cssPath?: string | false;
21
+ transitions?: boolean;
22
+ prefetch?: boolean;
23
+ spa?: boolean;
24
+ devtools?: boolean;
25
+ islandPreWrapped?: boolean;
26
+ cookies?: CookieManager;
27
+ }
28
+
29
+ export async function renderPageResponse(
30
+ options: PageRenderResponseOptions
31
+ ): Promise<Response> {
32
+ let app = options.app;
33
+
34
+ if (!options.useStreaming) {
35
+ app = (await resolveAsyncElement(app)) as React.ReactElement;
36
+ }
37
+
38
+ const response = options.useStreaming
39
+ ? await renderStreamingPageResponse(app, options)
40
+ : renderNonStreamingPageResponse(app, options);
41
+
42
+ return options.cookies ? options.cookies.applyToResponse(response) : response;
43
+ }
44
+
45
+ async function renderStreamingPageResponse(
46
+ app: React.ReactElement,
47
+ options: PageRenderResponseOptions
48
+ ): Promise<Response> {
49
+ return renderStreamingResponse(app, {
50
+ title: options.title,
51
+ headTags: options.headTags,
52
+ isDev: options.isDev,
53
+ hmrPort: options.hmrPort,
54
+ routeId: options.routeId,
55
+ routePattern: options.routePattern,
56
+ layoutChain: options.layoutChain,
57
+ hydration: options.hydration,
58
+ bundleManifest: options.bundleManifest,
59
+ criticalData: options.loaderData as Record<string, unknown> | undefined,
60
+ enableClientRouter: true,
61
+ cssPath: options.cssPath,
62
+ transitions: options.transitions,
63
+ prefetch: options.prefetch,
64
+ spa: options.spa,
65
+ devtools: options.devtools,
66
+ onShellReady: () => {
67
+ if (options.isDev) {
68
+ console.log(`[Mandu Streaming] Shell ready: ${options.routeId}`);
69
+ }
70
+ },
71
+ onMetrics: (metrics) => {
72
+ if (options.isDev) {
73
+ console.log(`[Mandu Streaming] Metrics for ${options.routeId}:`, {
74
+ shellReadyTime: `${metrics.shellReadyTime}ms`,
75
+ allReadyTime: `${metrics.allReadyTime}ms`,
76
+ hasError: metrics.hasError,
77
+ });
78
+ }
79
+ },
80
+ });
81
+ }
82
+
83
+ function renderNonStreamingPageResponse(
84
+ app: React.ReactElement,
85
+ options: PageRenderResponseOptions
86
+ ): Response {
87
+ return renderSSR(app, {
88
+ title: options.title,
89
+ headTags: options.headTags,
90
+ isDev: options.isDev,
91
+ hmrPort: options.hmrPort,
92
+ routeId: options.routeId,
93
+ hydration: options.hydration,
94
+ bundleManifest: options.bundleManifest,
95
+ serverData: options.loaderData,
96
+ enableClientRouter: true,
97
+ routePattern: options.routePattern,
98
+ cssPath: options.cssPath,
99
+ islandPreWrapped: !!options.islandPreWrapped,
100
+ transitions: options.transitions,
101
+ prefetch: options.prefetch,
102
+ spa: options.spa,
103
+ devtools: options.devtools,
104
+ layoutChain: options.layoutChain,
105
+ });
106
+ }
@@ -0,0 +1,231 @@
1
+ export interface RateLimitOptions {
2
+ windowMs?: number;
3
+ max?: number;
4
+ message?: string;
5
+ statusCode?: number;
6
+ headers?: boolean;
7
+ /**
8
+ * Reverse proxy 헤더를 신뢰할지 여부
9
+ * - false(기본): X-Forwarded-For 등을 읽지만 spoofing 가능성을 표시
10
+ * - true: 전달된 클라이언트 IP를 완전히 신뢰
11
+ * 주의: trustProxy: false여도 클라이언트 구분을 위해 헤더를 사용하므로
12
+ * IP spoofing이 가능합니다. 신뢰할 수 있는 프록시 뒤에서만 사용하세요.
13
+ */
14
+ trustProxy?: boolean;
15
+ /**
16
+ * 메모리 보호를 위한 최대 key 수
17
+ * - 초과 시 오래된 key부터 제거
18
+ */
19
+ maxKeys?: number;
20
+ }
21
+
22
+ export interface NormalizedRateLimitOptions {
23
+ windowMs: number;
24
+ max: number;
25
+ message: string;
26
+ statusCode: number;
27
+ headers: boolean;
28
+ trustProxy: boolean;
29
+ maxKeys: number;
30
+ }
31
+
32
+ export interface RateLimitDecision {
33
+ allowed: boolean;
34
+ limit: number;
35
+ remaining: number;
36
+ resetAt: number;
37
+ }
38
+
39
+ export class MemoryRateLimiter {
40
+ private readonly store = new Map<string, { count: number; resetAt: number }>();
41
+ private lastCleanupAt = 0;
42
+
43
+ consume(req: Request, routeId: string, options: NormalizedRateLimitOptions): RateLimitDecision {
44
+ const now = Date.now();
45
+ this.maybeCleanup(now, options);
46
+
47
+ const key = `${this.getClientKey(req, options)}:${routeId}`;
48
+ const current = this.store.get(key);
49
+
50
+ if (!current || current.resetAt <= now) {
51
+ const resetAt = now + options.windowMs;
52
+ this.store.set(key, { count: 1, resetAt });
53
+ this.enforceMaxKeys(options.maxKeys);
54
+ return { allowed: true, limit: options.max, remaining: Math.max(0, options.max - 1), resetAt };
55
+ }
56
+
57
+ current.count += 1;
58
+ this.store.set(key, current);
59
+
60
+ return {
61
+ allowed: current.count <= options.max,
62
+ limit: options.max,
63
+ remaining: Math.max(0, options.max - current.count),
64
+ resetAt: current.resetAt,
65
+ };
66
+ }
67
+
68
+ private maybeCleanup(now: number, options: NormalizedRateLimitOptions): void {
69
+ if (now - this.lastCleanupAt < Math.max(1_000, options.windowMs)) {
70
+ return;
71
+ }
72
+
73
+ this.lastCleanupAt = now;
74
+ for (const [key, entry] of this.store.entries()) {
75
+ if (entry.resetAt <= now) {
76
+ this.store.delete(key);
77
+ }
78
+ }
79
+ }
80
+
81
+ private enforceMaxKeys(maxKeys: number): void {
82
+ while (this.store.size > maxKeys) {
83
+ const oldestKey = this.store.keys().next().value;
84
+ if (!oldestKey) break;
85
+ this.store.delete(oldestKey);
86
+ }
87
+ }
88
+
89
+ private getClientKey(req: Request, options: NormalizedRateLimitOptions): string {
90
+ const candidates = [
91
+ req.headers.get("x-forwarded-for")?.split(",")[0]?.trim(),
92
+ req.headers.get("x-real-ip")?.trim(),
93
+ req.headers.get("cf-connecting-ip")?.trim(),
94
+ req.headers.get("true-client-ip")?.trim(),
95
+ req.headers.get("fly-client-ip")?.trim(),
96
+ ];
97
+
98
+ for (const candidate of candidates) {
99
+ if (candidate) {
100
+ const sanitized = candidate.slice(0, 64);
101
+ return options.trustProxy ? sanitized : `unverified:${sanitized}`;
102
+ }
103
+ }
104
+
105
+ return "default";
106
+ }
107
+ }
108
+
109
+ export function normalizeRateLimitOptions(
110
+ options: boolean | RateLimitOptions | undefined
111
+ ): NormalizedRateLimitOptions | false {
112
+ if (!options) return false;
113
+ if (options === true) {
114
+ return {
115
+ windowMs: 60_000,
116
+ max: 100,
117
+ message: "Too Many Requests",
118
+ statusCode: 429,
119
+ headers: true,
120
+ trustProxy: false,
121
+ maxKeys: 10_000,
122
+ };
123
+ }
124
+
125
+ const windowMs = Number.isFinite(options.windowMs) ? Math.max(1_000, options.windowMs!) : 60_000;
126
+ const max = Number.isFinite(options.max) ? Math.max(1, Math.floor(options.max!)) : 100;
127
+ const statusCode = Number.isFinite(options.statusCode)
128
+ ? Math.min(599, Math.max(400, Math.floor(options.statusCode!)))
129
+ : 429;
130
+ const maxKeys = Number.isFinite(options.maxKeys)
131
+ ? Math.max(100, Math.floor(options.maxKeys!))
132
+ : 10_000;
133
+
134
+ return {
135
+ windowMs,
136
+ max,
137
+ message: options.message ?? "Too Many Requests",
138
+ statusCode,
139
+ headers: options.headers ?? true,
140
+ trustProxy: options.trustProxy ?? false,
141
+ maxKeys,
142
+ };
143
+ }
144
+
145
+ export function appendRateLimitHeaders(
146
+ response: Response,
147
+ decision: RateLimitDecision,
148
+ options: NormalizedRateLimitOptions
149
+ ): Response {
150
+ if (!options.headers) return response;
151
+
152
+ const headers = new Headers(response.headers);
153
+ const retryAfterSec = Math.max(1, Math.ceil((decision.resetAt - Date.now()) / 1000));
154
+
155
+ headers.set("X-RateLimit-Limit", String(decision.limit));
156
+ headers.set("X-RateLimit-Remaining", String(decision.remaining));
157
+ headers.set("X-RateLimit-Reset", String(Math.floor(decision.resetAt / 1000)));
158
+ headers.set("Retry-After", String(retryAfterSec));
159
+
160
+ return new Response(response.body, {
161
+ status: response.status,
162
+ statusText: response.statusText,
163
+ headers,
164
+ });
165
+ }
166
+
167
+ export function createRateLimitResponse(
168
+ decision: RateLimitDecision,
169
+ options: NormalizedRateLimitOptions
170
+ ): Response {
171
+ const response = Response.json(
172
+ {
173
+ error: "rate_limit_exceeded",
174
+ message: options.message,
175
+ limit: decision.limit,
176
+ remaining: decision.remaining,
177
+ retryAfter: Math.max(1, Math.ceil((decision.resetAt - Date.now()) / 1000)),
178
+ },
179
+ { status: options.statusCode }
180
+ );
181
+
182
+ return appendRateLimitHeaders(response, decision, options);
183
+ }
184
+
185
+ /**
186
+ * Create standalone rate limiter for custom API handlers.
187
+ *
188
+ * @example
189
+ * ```typescript
190
+ * import { createRateLimiter } from '@mandujs/core/runtime/server';
191
+ *
192
+ * const limiter = createRateLimiter({ max: 5, windowMs: 60000 });
193
+ *
194
+ * export async function POST(req: Request) {
195
+ * const decision = limiter.check(req, 'my-api-route');
196
+ * if (!decision.allowed) return limiter.createResponse(decision);
197
+ * return new Response('OK');
198
+ * }
199
+ * ```
200
+ */
201
+ export function createRateLimiter(options?: RateLimitOptions) {
202
+ const normalized = normalizeRateLimitOptions(options || true);
203
+ if (!normalized) {
204
+ throw new Error("Rate limiter options must be truthy");
205
+ }
206
+
207
+ const limiter = new MemoryRateLimiter();
208
+
209
+ return {
210
+ /**
211
+ * Check if request is allowed.
212
+ */
213
+ check(req: Request, routeId: string): RateLimitDecision {
214
+ return limiter.consume(req, routeId, normalized);
215
+ },
216
+
217
+ /**
218
+ * Create 429 response for denied request.
219
+ */
220
+ createResponse(decision: RateLimitDecision): Response {
221
+ return createRateLimitResponse(decision, normalized);
222
+ },
223
+
224
+ /**
225
+ * Add rate limit headers to successful response.
226
+ */
227
+ addHeaders(response: Response, decision: RateLimitDecision): Response {
228
+ return appendRateLimitHeaders(response, decision, normalized);
229
+ },
230
+ };
231
+ }
@@ -1,31 +1,31 @@
1
- import {
2
- compose as composeMiddleware,
3
- type ComposedHandler,
4
- type FinalHandler,
5
- } from "../middleware/compose";
6
- import type { Middleware } from "../middleware/define";
7
-
8
- export type { ComposedHandler, FinalHandler };
9
-
10
- export function buildRequestMiddlewareChain(
11
- middleware: Middleware[] | undefined
12
- ): ComposedHandler | undefined {
13
- return middleware && middleware.length > 0
14
- ? composeMiddleware(...middleware)
15
- : undefined;
16
- }
17
-
18
- export interface RunRequestMiddlewareOptions {
19
- req: Request;
20
- middlewareChain: ComposedHandler | undefined;
21
- finalHandler: FinalHandler;
22
- skipMiddleware?: boolean;
23
- }
24
-
25
- export async function runRequestMiddleware(
26
- options: RunRequestMiddlewareOptions
27
- ): Promise<Response | undefined> {
28
- const { req, middlewareChain, finalHandler, skipMiddleware = false } = options;
29
- if (skipMiddleware || !middlewareChain) return undefined;
30
- return middlewareChain(req, finalHandler);
31
- }
1
+ import {
2
+ compose as composeMiddleware,
3
+ type ComposedHandler,
4
+ type FinalHandler,
5
+ } from "../middleware/compose";
6
+ import type { Middleware } from "../middleware/define";
7
+
8
+ export type { ComposedHandler, FinalHandler };
9
+
10
+ export function buildRequestMiddlewareChain(
11
+ middleware: Middleware[] | undefined
12
+ ): ComposedHandler | undefined {
13
+ return middleware && middleware.length > 0
14
+ ? composeMiddleware(...middleware)
15
+ : undefined;
16
+ }
17
+
18
+ export interface RunRequestMiddlewareOptions {
19
+ req: Request;
20
+ middlewareChain: ComposedHandler | undefined;
21
+ finalHandler: FinalHandler;
22
+ skipMiddleware?: boolean;
23
+ }
24
+
25
+ export async function runRequestMiddleware(
26
+ options: RunRequestMiddlewareOptions
27
+ ): Promise<Response | undefined> {
28
+ const { req, middlewareChain, finalHandler, skipMiddleware = false } = options;
29
+ if (skipMiddleware || !middlewareChain) return undefined;
30
+ return middlewareChain(req, finalHandler);
31
+ }
@@ -0,0 +1,64 @@
1
+ import type { CronDef, CronRegistration } from "../scheduler";
2
+ import type * as SchedulerModule from "../scheduler";
3
+ import type * as SchedulerCronMiddlewareModule from "../middleware/scheduler-cron";
4
+
5
+ export interface RuntimeSchedulerOptions {
6
+ jobs?: CronDef[];
7
+ disabled?: boolean;
8
+ }
9
+
10
+ export interface RuntimeSchedulerLifecycle {
11
+ stop(): void;
12
+ }
13
+
14
+ export function startRuntimeSchedulerLifecycle(
15
+ schedulerOption: RuntimeSchedulerOptions | undefined,
16
+ ): RuntimeSchedulerLifecycle {
17
+ let schedulerRegistration: CronRegistration | null = null;
18
+ let clearActiveRegistration = (): void => {};
19
+
20
+ const jobDefs = schedulerOption?.jobs ?? [];
21
+ const schedulerDisabled = schedulerOption?.disabled === true;
22
+
23
+ if (jobDefs.length > 0 && !schedulerDisabled) {
24
+ try {
25
+ const { defineCron } = require("../scheduler") as typeof SchedulerModule;
26
+ const { setActiveSchedulerRegistration } = require("../middleware/scheduler-cron") as typeof SchedulerCronMiddlewareModule;
27
+ schedulerRegistration = defineCron(jobDefs);
28
+ schedulerRegistration.start();
29
+ setActiveSchedulerRegistration(schedulerRegistration);
30
+ clearActiveRegistration = () => setActiveSchedulerRegistration(null);
31
+
32
+ const bunJobCount = Object.keys(schedulerRegistration.status()).filter((name) => {
33
+ const def = jobDefs.find((job) => job.name === name);
34
+ const runOn = def?.runOn && def.runOn.length > 0 ? def.runOn : ["bun", "workers"];
35
+ return runOn.includes("bun");
36
+ }).length;
37
+
38
+ console.log(
39
+ `⏰ Scheduler: ${bunJobCount} cron job(s) registered on Bun runtime` +
40
+ (jobDefs.length !== bunJobCount
41
+ ? ` (${jobDefs.length - bunJobCount} workers-only — see wrangler.toml)`
42
+ : ""),
43
+ );
44
+ } catch (err) {
45
+ console.error(
46
+ "❌ [scheduler] failed to start — HTTP server continues without cron jobs:",
47
+ err instanceof Error ? err.message : err,
48
+ );
49
+ }
50
+ }
51
+
52
+ return {
53
+ stop(): void {
54
+ if (!schedulerRegistration) return;
55
+ const reg = schedulerRegistration;
56
+ schedulerRegistration = null;
57
+ void reg.stop()
58
+ .then(() => clearActiveRegistration())
59
+ .catch((err) => {
60
+ console.error("[scheduler] shutdown error:", err);
61
+ });
62
+ },
63
+ };
64
+ }