@mandujs/core 0.21.0 → 0.22.1

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 (122) hide show
  1. package/package.json +101 -69
  2. package/src/auth/__tests__/login.test.ts +419 -0
  3. package/src/auth/__tests__/password.test.ts +122 -0
  4. package/src/auth/__tests__/reset.test.ts +296 -0
  5. package/src/auth/__tests__/tokens.test.ts +274 -0
  6. package/src/auth/__tests__/verification.test.ts +274 -0
  7. package/src/auth/index.ts +76 -0
  8. package/src/auth/login.ts +225 -0
  9. package/src/auth/password.ts +120 -0
  10. package/src/auth/reset.ts +243 -0
  11. package/src/auth/tokens.ts +612 -0
  12. package/src/auth/verification.ts +253 -0
  13. package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -0
  14. package/src/bundler/__tests__/cold-start.test.ts +504 -0
  15. package/src/bundler/__tests__/csp-nonce.test.ts +278 -0
  16. package/src/bundler/__tests__/dev-reliability.test.ts +619 -0
  17. package/src/bundler/__tests__/extended-watch.test.ts +710 -0
  18. package/src/bundler/__tests__/fast-refresh.test.ts +596 -0
  19. package/src/bundler/__tests__/hdr.test.ts +353 -0
  20. package/src/bundler/__tests__/hmr-client.test.ts +532 -0
  21. package/src/bundler/__tests__/manifest-schema.test.ts +266 -0
  22. package/src/bundler/__tests__/prod-smoke.test.ts +138 -0
  23. package/src/bundler/__tests__/slot-dispatch.test.ts +573 -0
  24. package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -0
  25. package/src/bundler/__tests__/vendor-cache.test.ts +455 -0
  26. package/src/bundler/build.test.ts +8 -1
  27. package/src/bundler/build.ts +310 -18
  28. package/src/bundler/css.ts +326 -323
  29. package/src/bundler/dev.ts +1611 -59
  30. package/src/bundler/fast-refresh-plugin.ts +307 -0
  31. package/src/bundler/hmr-types.ts +252 -0
  32. package/src/bundler/manifest-schema.ts +301 -0
  33. package/src/bundler/safe-build.test.ts +128 -0
  34. package/src/bundler/safe-build.ts +77 -0
  35. package/src/bundler/scenario-matrix.ts +229 -0
  36. package/src/bundler/types.ts +11 -0
  37. package/src/bundler/vendor-cache-types.ts +130 -0
  38. package/src/bundler/vendor-cache.ts +526 -0
  39. package/src/client/router.ts +214 -56
  40. package/src/db/__tests__/db.test.ts +485 -0
  41. package/src/db/index.ts +513 -0
  42. package/src/db/migrations/__tests__/runner.test.ts +661 -0
  43. package/src/db/migrations/history-table.ts +345 -0
  44. package/src/db/migrations/lock.ts +269 -0
  45. package/src/db/migrations/runner.ts +633 -0
  46. package/src/desktop/__tests__/smoke.test.ts +100 -0
  47. package/src/desktop/__tests__/window.test.ts +172 -0
  48. package/src/desktop/__tests__/worker.test.ts +266 -0
  49. package/src/desktop/index.ts +43 -0
  50. package/src/desktop/types.ts +158 -0
  51. package/src/desktop/window.ts +492 -0
  52. package/src/desktop/worker.ts +180 -0
  53. package/src/email/__tests__/email.test.ts +355 -0
  54. package/src/email/index.ts +282 -0
  55. package/src/email/resend.ts +163 -0
  56. package/src/email/smtp.ts +64 -0
  57. package/src/filling/__tests__/session-sqlite.test.ts +454 -0
  58. package/src/filling/context.ts +72 -78
  59. package/src/filling/cookie-codec.ts +299 -0
  60. package/src/filling/deps.ts +25 -1
  61. package/src/filling/filling.ts +28 -3
  62. package/src/filling/session-sqlite.ts +617 -0
  63. package/src/filling/session.ts +265 -216
  64. package/src/guard/decision-memory.test.ts +52 -22
  65. package/src/id/__tests__/id.test.ts +120 -0
  66. package/src/id/index.ts +105 -0
  67. package/src/kitchen/index.ts +2 -2
  68. package/src/kitchen/kitchen-handler.ts +86 -0
  69. package/src/kitchen/stream/activity-sse.ts +2 -1
  70. package/src/middleware/csrf.ts +328 -0
  71. package/src/middleware/index.ts +40 -0
  72. package/src/middleware/oauth/__tests__/oauth.test.ts +574 -0
  73. package/src/middleware/oauth/index.ts +505 -0
  74. package/src/middleware/oauth/providers.ts +115 -0
  75. package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -0
  76. package/src/middleware/rate-limit/index.ts +522 -0
  77. package/src/middleware/rate-limit/sqlite-store.ts +382 -0
  78. package/src/middleware/secure/__tests__/secure.test.ts +360 -0
  79. package/src/middleware/secure/csp.ts +193 -0
  80. package/src/middleware/secure/index.ts +417 -0
  81. package/src/middleware/session.ts +174 -0
  82. package/src/observability/event-bus.ts +81 -79
  83. package/src/paths.ts +37 -0
  84. package/src/perf/hmr-markers.ts +215 -0
  85. package/src/perf/index.ts +104 -0
  86. package/src/resource/__tests__/generator.test.ts +603 -2
  87. package/src/resource/ddl/__tests__/diff.test.ts +639 -0
  88. package/src/resource/ddl/__tests__/emit.test.ts +799 -0
  89. package/src/resource/ddl/__tests__/snapshot.test.ts +499 -0
  90. package/src/resource/ddl/diff.ts +392 -0
  91. package/src/resource/ddl/emit.ts +548 -0
  92. package/src/resource/ddl/persistence-types.ts +218 -0
  93. package/src/resource/ddl/snapshot.ts +447 -0
  94. package/src/resource/ddl/type-map.ts +223 -0
  95. package/src/resource/ddl/types.ts +232 -0
  96. package/src/resource/generator-repo.ts +610 -0
  97. package/src/resource/generator-schema.ts +476 -0
  98. package/src/resource/generator.ts +117 -1
  99. package/src/resource/index.ts +17 -1
  100. package/src/resource/schema.ts +30 -0
  101. package/src/router/fs-scanner.ts +3 -0
  102. package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -0
  103. package/src/runtime/__tests__/hdr-client.test.ts +223 -0
  104. package/src/runtime/__tests__/http-errors.test.ts +117 -0
  105. package/src/runtime/__tests__/not-found.test.ts +152 -0
  106. package/src/runtime/boundary.tsx +21 -1
  107. package/src/runtime/fast-refresh-runtime.ts +322 -0
  108. package/src/runtime/fast-refresh-types.ts +128 -0
  109. package/src/runtime/hmr-client.ts +409 -0
  110. package/src/runtime/http-errors.ts +113 -0
  111. package/src/runtime/index.ts +6 -0
  112. package/src/runtime/logger.ts +678 -677
  113. package/src/runtime/not-found.ts +93 -0
  114. package/src/runtime/redirect.ts +133 -0
  115. package/src/runtime/server.ts +518 -20
  116. package/src/runtime/ssr.ts +340 -10
  117. package/src/runtime/streaming-ssr.ts +222 -19
  118. package/src/scheduler/__tests__/scheduler.test.ts +514 -0
  119. package/src/scheduler/index.ts +343 -0
  120. package/src/storage/s3/__tests__/s3.test.ts +479 -0
  121. package/src/storage/s3/index.ts +412 -0
  122. package/src/testing/index.ts +58 -0
@@ -1,79 +1,81 @@
1
- /** Mandu Unified EventBus -- foundation for the observability system. */
2
-
3
- export type EventType = "http" | "mcp" | "guard" | "build" | "error" | "cache" | "ws";
4
- export type ObservabilitySeverity = "info" | "warn" | "error";
5
-
6
- export interface ObservabilityEvent {
7
- id: string;
8
- correlationId?: string;
9
- type: EventType;
10
- severity: ObservabilitySeverity;
11
- source: string;
12
- timestamp: number;
13
- message: string;
14
- data?: Record<string, unknown>;
15
- duration?: number;
16
- }
17
-
18
- export type EventHandler = (event: ObservabilityEvent) => void;
19
-
20
- class ManduEventBus {
21
- private handlers = new Map<string, Set<EventHandler>>();
22
- private recent: ObservabilityEvent[] = [];
23
- private maxRecent = 200;
24
-
25
- on(type: EventType | "*", handler: EventHandler): () => void {
26
- let set = this.handlers.get(type);
27
- if (!set) {
28
- set = new Set();
29
- this.handlers.set(type, set);
30
- }
31
- set.add(handler);
32
- return () => { set!.delete(handler); };
33
- }
34
-
35
- emit(event: Omit<ObservabilityEvent, "id" | "timestamp">): void {
36
- const full: ObservabilityEvent = {
37
- ...event,
38
- id: crypto.randomUUID(),
39
- timestamp: Date.now(),
40
- };
41
- this.recent.push(full);
42
- if (this.recent.length > this.maxRecent) {
43
- this.recent = this.recent.slice(-this.maxRecent);
44
- }
45
- this.handlers.get(full.type)?.forEach((h) => h(full));
46
- this.handlers.get("*")?.forEach((h) => h(full));
47
- }
48
-
49
- getRecent(
50
- count?: number,
51
- filter?: { type?: EventType; severity?: ObservabilitySeverity },
52
- ): ObservabilityEvent[] {
53
- let result = this.recent;
54
- if (filter?.type) result = result.filter((e) => e.type === filter.type);
55
- if (filter?.severity) result = result.filter((e) => e.severity === filter.severity);
56
- return count ? result.slice(-count) : result;
57
- }
58
-
59
- getStats(windowMs?: number): Record<EventType, { count: number; errors: number; avgDuration: number }> {
60
- const cutoff = windowMs ? Date.now() - windowMs : 0;
61
- const ALL: EventType[] = ["http", "mcp", "guard", "build", "error", "cache", "ws"];
62
- const stats = {} as Record<EventType, { count: number; errors: number; avgDuration: number }>;
63
- const dur = {} as Record<EventType, number[]>;
64
- for (const t of ALL) { stats[t] = { count: 0, errors: 0, avgDuration: 0 }; dur[t] = []; }
65
- for (const e of this.recent) {
66
- if (e.timestamp < cutoff) continue;
67
- stats[e.type].count++;
68
- if (e.severity === "error") stats[e.type].errors++;
69
- if (e.duration !== undefined) dur[e.type].push(e.duration);
70
- }
71
- for (const t of ALL) {
72
- const d = dur[t];
73
- stats[t].avgDuration = d.length ? d.reduce((a, b) => a + b, 0) / d.length : 0;
74
- }
75
- return stats;
76
- }
77
- }
78
-
79
- export const eventBus = new ManduEventBus();
1
+ /** Mandu Unified EventBus -- foundation for the observability system. */
2
+
3
+ import { newId } from "../id";
4
+
5
+ export type EventType = "http" | "mcp" | "guard" | "build" | "error" | "cache" | "ws";
6
+ export type ObservabilitySeverity = "info" | "warn" | "error";
7
+
8
+ export interface ObservabilityEvent {
9
+ id: string;
10
+ correlationId?: string;
11
+ type: EventType;
12
+ severity: ObservabilitySeverity;
13
+ source: string;
14
+ timestamp: number;
15
+ message: string;
16
+ data?: Record<string, unknown>;
17
+ duration?: number;
18
+ }
19
+
20
+ export type EventHandler = (event: ObservabilityEvent) => void;
21
+
22
+ class ManduEventBus {
23
+ private handlers = new Map<string, Set<EventHandler>>();
24
+ private recent: ObservabilityEvent[] = [];
25
+ private maxRecent = 200;
26
+
27
+ on(type: EventType | "*", handler: EventHandler): () => void {
28
+ let set = this.handlers.get(type);
29
+ if (!set) {
30
+ set = new Set();
31
+ this.handlers.set(type, set);
32
+ }
33
+ set.add(handler);
34
+ return () => { set!.delete(handler); };
35
+ }
36
+
37
+ emit(event: Omit<ObservabilityEvent, "id" | "timestamp">): void {
38
+ const full: ObservabilityEvent = {
39
+ ...event,
40
+ id: newId(),
41
+ timestamp: Date.now(),
42
+ };
43
+ this.recent.push(full);
44
+ if (this.recent.length > this.maxRecent) {
45
+ this.recent = this.recent.slice(-this.maxRecent);
46
+ }
47
+ this.handlers.get(full.type)?.forEach((h) => h(full));
48
+ this.handlers.get("*")?.forEach((h) => h(full));
49
+ }
50
+
51
+ getRecent(
52
+ count?: number,
53
+ filter?: { type?: EventType; severity?: ObservabilitySeverity },
54
+ ): ObservabilityEvent[] {
55
+ let result = this.recent;
56
+ if (filter?.type) result = result.filter((e) => e.type === filter.type);
57
+ if (filter?.severity) result = result.filter((e) => e.severity === filter.severity);
58
+ return count ? result.slice(-count) : result;
59
+ }
60
+
61
+ getStats(windowMs?: number): Record<EventType, { count: number; errors: number; avgDuration: number }> {
62
+ const cutoff = windowMs ? Date.now() - windowMs : 0;
63
+ const ALL: EventType[] = ["http", "mcp", "guard", "build", "error", "cache", "ws"];
64
+ const stats = {} as Record<EventType, { count: number; errors: number; avgDuration: number }>;
65
+ const dur = {} as Record<EventType, number[]>;
66
+ for (const t of ALL) { stats[t] = { count: 0, errors: 0, avgDuration: 0 }; dur[t] = []; }
67
+ for (const e of this.recent) {
68
+ if (e.timestamp < cutoff) continue;
69
+ stats[e.type].count++;
70
+ if (e.severity === "error") stats[e.type].errors++;
71
+ if (e.duration !== undefined) dur[e.type].push(e.duration);
72
+ }
73
+ for (const t of ALL) {
74
+ const d = dur[t];
75
+ stats[t].avgDuration = d.length ? d.reduce((a, b) => a + b, 0) / d.length : 0;
76
+ }
77
+ return stats;
78
+ }
79
+ }
80
+
81
+ export const eventBus = new ManduEventBus();
package/src/paths.ts CHANGED
@@ -20,7 +20,32 @@ export interface GeneratedPaths {
20
20
  resourceTypesDir: string;
21
21
  resourceSlotsDir: string;
22
22
  resourceClientDir: string;
23
+ /** User-authored resource schema files (spec/resources). */
23
24
  resourceSchemasDir: string;
25
+ /**
26
+ * Phase 4c — Generated per-resource repo modules
27
+ * (`.mandu/generated/server/repos/{name}.repo.ts`). DERIVED; regenerated
28
+ * on every build when the resource declares `options.persistence`.
29
+ */
30
+ resourceReposDir: string;
31
+ /**
32
+ * Phase 4c — Per-resource CREATE TABLE DDL snapshots for humans
33
+ * (`.mandu/generated/server/schema/{table}.sql`). DERIVED; not applied
34
+ * by the migration runner — applied migrations live in `migrationsDir`.
35
+ */
36
+ resourceSchemaOutDir: string;
37
+ /**
38
+ * Phase 4c — Schema state directory (`.mandu/schema`). Holds
39
+ * `applied.json` (owned by the migration runner after successful apply).
40
+ * The Phase 4c generator only READS from here — never writes.
41
+ */
42
+ schemaStateDir: string;
43
+ /**
44
+ * Phase 4c — User-visible migration files directory
45
+ * (`spec/db/migrations`). `mandu db plan` writes `NNNN_auto_*.sql`
46
+ * here, the runner reads it on `mandu db apply`.
47
+ */
48
+ migrationsDir: string;
24
49
  }
25
50
 
26
51
  /**
@@ -38,6 +63,10 @@ export function resolveGeneratedPaths(rootDir: string): GeneratedPaths {
38
63
  resourceSlotsDir: path.join(rootDir, "spec/slots"),
39
64
  resourceClientDir: path.join(rootDir, ".mandu/generated/client"),
40
65
  resourceSchemasDir: path.join(rootDir, "spec/resources"),
66
+ resourceReposDir: path.join(rootDir, ".mandu/generated/server/repos"),
67
+ resourceSchemaOutDir: path.join(rootDir, ".mandu/generated/server/schema"),
68
+ schemaStateDir: path.join(rootDir, ".mandu/schema"),
69
+ migrationsDir: path.join(rootDir, "spec/db/migrations"),
41
70
  };
42
71
  }
43
72
 
@@ -56,4 +85,12 @@ export const GENERATED_RELATIVE_PATHS = {
56
85
  slots: "spec/slots",
57
86
  client: ".mandu/generated/client",
58
87
  resourceSchemas: "spec/resources",
88
+ /** Phase 4c — repo emission target. */
89
+ resourceRepos: ".mandu/generated/server/repos",
90
+ /** Phase 4c — per-resource CREATE TABLE snapshots (documentation). */
91
+ resourceSchemaOut: ".mandu/generated/server/schema",
92
+ /** Phase 4c — applied.json lives here (runner-owned). */
93
+ schemaState: ".mandu/schema",
94
+ /** Phase 4c — user-visible migrations directory. */
95
+ migrations: "spec/db/migrations",
59
96
  } as const;
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Phase 7.0 — HMR perf marker names (B4 fix)
3
+ *
4
+ * Purpose: give Agent B (incremental bundled import), Agent A (reliability),
5
+ * and Agent F (perf validation) a single vocabulary for marker names passed
6
+ * to `mark()` / `measure()` / `withPerf()`.
7
+ *
8
+ * Why a shared module: in Phase 7.0 diagnostics we found that
9
+ * `cli/commands/dev.ts:322-363`'s `handleSSRChange` chain has NO perf
10
+ * markers — `dev:rebuild` wraps only `_doBuild`, so the true SSR rebuild
11
+ * walltime (1.5~2 s vs 200ms target) is invisible. Without consistent
12
+ * marker naming across agents, each would pick ad-hoc strings and
13
+ * Agent F's benchmark script would have to grep for a hundred variants.
14
+ *
15
+ * When adding new markers:
16
+ * 1. Add the constant here.
17
+ * 2. Use it in production code as `mark(HMR_PERF.SSR_HANDLER_RELOAD)`
18
+ * (not a string literal).
19
+ * 3. Update `docs/bun/phase-7-benchmarks.md` (Agent F owns it).
20
+ *
21
+ * References:
22
+ * docs/bun/phase-7-diagnostics/performance-reliability.md §2 B4
23
+ * docs/bun/phase-7-team-plan.md §3.2
24
+ */
25
+
26
+ /**
27
+ * All HMR-related perf markers. String literals are frozen — do not
28
+ * mutate. Marker names follow the `<area>:<step>` convention so grep
29
+ * and log aggregation work predictably.
30
+ */
31
+ export const HMR_PERF = {
32
+ // ─── File detection → build invocation ─────────────────────────────────
33
+
34
+ /** Raw fs event received (watcher). */
35
+ FILE_DETECT: "hmr:file-detect",
36
+
37
+ /** After per-file debounce, before `handleFileChange()` runs. */
38
+ DEBOUNCE_FLUSH: "hmr:debounce-flush",
39
+
40
+ /** Coalesced batch dispatched to the rebuild path. */
41
+ BATCH_DISPATCH: "hmr:batch-dispatch",
42
+
43
+ // ─── Rebuild outer frame ───────────────────────────────────────────────
44
+
45
+ /** Wall-clock from batch dispatch → WS broadcast complete. The P95
46
+ * target (≤50 ms island / ≤200 ms SSR / ≤500 ms cold) is measured on
47
+ * this marker. */
48
+ REBUILD_TOTAL: "hmr:rebuild-total",
49
+
50
+ /** `_doBuild` body (covers both `buildClientBundles` + SSR path). */
51
+ DO_BUILD: "hmr:do-build",
52
+
53
+ // ─── SSR handler reload chain (B4 newly-instrumented) ──────────────────
54
+
55
+ /** `handleSSRChange` mutex section — from enter to exit. */
56
+ SSR_HANDLER_RELOAD: "ssr:handler-reload",
57
+
58
+ /** `bundledImport` call — the single largest SSR cost today. Incremental
59
+ * path (Agent B) should target near-zero on cache hits. */
60
+ SSR_BUNDLED_IMPORT: "ssr:bundled-import",
61
+
62
+ /** `clearDefaultRegistry` + `registeredLayouts.clear` (fast). */
63
+ SSR_CLEAR_REGISTRY: "ssr:clear-registry",
64
+
65
+ /** `registerManifestHandlers(manifest, true)` — per-route re-registration. */
66
+ SSR_REGISTER_HANDLERS: "ssr:register-handlers",
67
+
68
+ /** Prerender regeneration (issue #188 fix). Measures only the prerender
69
+ * re-run, not the HTML delivery. */
70
+ PRERENDER_REGEN: "prerender:regen",
71
+
72
+ // ─── Client bundle path ────────────────────────────────────────────────
73
+
74
+ /** Per-island rebuild. Sub-marker of DO_BUILD when kind === "islands-only". */
75
+ ISLAND_REBUILD: "island:rebuild",
76
+
77
+ /** Framework bundle (runtime/router/vendor/devtools). Should be SKIPPED
78
+ * on common-dir rebuild (fire when skipFrameworkBundles === false). */
79
+ FRAMEWORK_REBUILD: "framework:rebuild",
80
+
81
+ /** Vendor shim build. Sub-marker of FRAMEWORK_REBUILD. */
82
+ VENDOR_SHIM_BUILD: "framework:vendor-shim",
83
+
84
+ // ─── HMR transport ─────────────────────────────────────────────────────
85
+
86
+ /** Time to serialize + send to all connected clients. */
87
+ HMR_BROADCAST: "hmr:broadcast",
88
+
89
+ /** Replay buffer enqueue (B8). */
90
+ HMR_REPLAY_ENQUEUE: "hmr:replay-enqueue",
91
+
92
+ /** Client reconnect — `?since=<id>` processing. */
93
+ HMR_REPLAY_FLUSH: "hmr:replay-flush",
94
+
95
+ // ─── Incremental bundled import internals (Agent B) ────────────────────
96
+
97
+ /** Import graph lookup for a root path. */
98
+ INCR_GRAPH_LOOKUP: "incr:graph-lookup",
99
+
100
+ /** Cache hit (no rebuild needed — changed file not in descendants). */
101
+ INCR_CACHE_HIT: "incr:cache-hit",
102
+
103
+ /** Cache miss (rebuild required). */
104
+ INCR_CACHE_MISS: "incr:cache-miss",
105
+
106
+ /** Graph rebuild after new build — updates descendants map. */
107
+ INCR_GRAPH_UPDATE: "incr:graph-update",
108
+
109
+ // ─── Cold boot path (Phase 7.1 B_gap — R0.3 diagnostic identified 9
110
+ // unmeasured stages accounting for 150~210 ms of the 626 ms cold
111
+ // start. Instrumenting these unlocks Tier 1 / Tier 2 optimizations.) ─
112
+
113
+ /** `validateAndReport(rootDir)` — mandu.config.ts load + schema check. */
114
+ BOOT_VALIDATE_CONFIG: "boot:validate-config",
115
+
116
+ /** `validateRuntimeLockfile` — bun.lock check + advisory warnings. */
117
+ BOOT_LOCKFILE_CHECK: "boot:lockfile-check",
118
+
119
+ /** `loadEnv({ rootDir, env: "development" })` — .env / .env.development. */
120
+ BOOT_LOAD_ENV: "boot:load-env",
121
+
122
+ /** `startSqliteStore(rootDir)` — observability store (optional). Should
123
+ * become fire-and-forget in Tier 1 so it doesn't block ready. */
124
+ BOOT_SQLITE_START: "boot:sqlite-start",
125
+
126
+ /** `checkDirectory(guardConfig, rootDir)` — Architecture Guard preflight. */
127
+ BOOT_GUARD_PREFLIGHT: "boot:guard-preflight",
128
+
129
+ /** `resolveAvailablePort(desiredPort, ...)` — port probe (may be slow on
130
+ * Windows due to TIME_WAIT; Phase 0 already added retry). */
131
+ BOOT_RESOLVE_PORT: "boot:resolve-port",
132
+
133
+ /** `createHMRServer(port, options?)` — Bun.serve + WebSocket + replay
134
+ * buffer setup. Phase 7.0.R4 added Origin allowlist + rate limit. */
135
+ BOOT_HMR_SERVER: "boot:hmr-server",
136
+
137
+ /** `startServer(manifest, ...)` — Bun.serve for the actual app. */
138
+ BOOT_START_SERVER: "boot:start-server",
139
+
140
+ /** `watchFSRoutes(...)` — chokidar watcher for spec/slots + app/ routes.
141
+ * Phase 7.1.A may not be able to remove this; it tracks new-route
142
+ * creation which `_doBuild` does not. */
143
+ BOOT_WATCH_FS_ROUTES: "boot:watch-fs-routes",
144
+
145
+ // ─── Phase 7.2 ─────────────────────────────────────────────────────────
146
+
147
+ /** Tier 2 vendor shim disk-cache hit — all 4 shim outputs loaded from
148
+ * `.mandu/vendor-cache/` instead of rebuilt. Target: >95% on warm dev. */
149
+ VENDOR_CACHE_HIT: "vendor:cache-hit",
150
+
151
+ /** Tier 2 miss — any reason (no manifest / version mismatch / tamper /
152
+ * first boot). Full `buildVendorShims` runs. */
153
+ VENDOR_CACHE_MISS: "vendor:cache-miss",
154
+
155
+ /** Tier 2 cache write — after a successful rebuild, persist to disk.
156
+ * Only fires on miss paths. */
157
+ VENDOR_CACHE_WRITE: "vendor:cache-write",
158
+
159
+ /** HDR (Hot Data Revalidation) — slot change triggered a loader
160
+ * refetch without a React tree remount. Measures WS broadcast →
161
+ * client loader settled → props applied. Target: ≤ 150 ms P95. */
162
+ HDR_REFETCH: "hdr:refetch",
163
+
164
+ // ─── Phase 7.3 ─────────────────────────────────────────────────────────
165
+
166
+ /** JIT pre-warm — `mandu dev` boot kicks off fire-and-forget imports
167
+ * of the hot SSR modules (react, react-dom, react-dom/server) so
168
+ * Bun's JITTier compiler has them ready before the user's first edit.
169
+ *
170
+ * This marker measures the prewarm Promise's total wall-clock from
171
+ * boot-start to all `import()` settling — it is NOT on the critical
172
+ * path to "ready" (see `startPrewarmIdle`), so values here are
173
+ * informational for benchmarks only. Phase 7.2 F observed first-iter
174
+ * cold +41 ms vs warm steady-state; the goal is to absorb that into
175
+ * prewarm before the user hits a file save. */
176
+ JIT_PREWARM: "boot:jit-prewarm",
177
+
178
+ /** API route handler reload (`handleAPIChange`) — `.route.ts` /
179
+ * `.route.tsx` change. Symmetric to `SSR_HANDLER_RELOAD` for page /
180
+ * layout reloads. Phase 7.2 §7.4 flagged that `handleAPIChange` was
181
+ * missing a top-level `withPerf()` wrap so Agent D's MANDU_PERF=1
182
+ * trace couldn't attribute API reload walltime.
183
+ *
184
+ * Semantically API reloads are a subset of "route handler reload"
185
+ * (`SSR_HANDLER_RELOAD`), but a distinct marker lets benchmark scripts
186
+ * separate page vs API reload populations when computing P95. */
187
+ API_HANDLER_RELOAD: "api:handler-reload",
188
+ } as const;
189
+
190
+ /**
191
+ * Union of all marker names — useful when a function accepts an arbitrary
192
+ * marker as a parameter and you want compile-time exhaustiveness.
193
+ */
194
+ export type HMRPerfMarker = (typeof HMR_PERF)[keyof typeof HMR_PERF];
195
+
196
+ /**
197
+ * Human-readable target thresholds. Source of truth for Agent F's hard
198
+ * assertion pass/fail logic. Values in milliseconds.
199
+ */
200
+ export const HMR_PERF_TARGETS = {
201
+ /** Cold dev start (`mandu dev` → "ready" log). */
202
+ COLD_START_MS: 500,
203
+
204
+ /** Island-only rebuild P95, measured on REBUILD_TOTAL. */
205
+ ISLAND_REBUILD_P95_MS: 50,
206
+
207
+ /** SSR page rebuild P95, measured on REBUILD_TOTAL when kind === "ssr-only". */
208
+ SSR_REBUILD_P95_MS: 200,
209
+
210
+ /** Common-dir rebuild P95 (fan-out across multiple islands/SSR modules). */
211
+ COMMON_DIR_REBUILD_P95_MS: 400,
212
+
213
+ /** CSS-only rebuild P95. */
214
+ CSS_REBUILD_P95_MS: 100,
215
+ } as const;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Mandu Perf Module
3
+ *
4
+ * Lightweight, opt-in measurement markers built on `Bun.nanoseconds()`.
5
+ *
6
+ * Enable at process boot with `MANDU_PERF=1`. When disabled, every function is
7
+ * a single-branch no-op (no allocation, no Map lookups, no logging) so it is
8
+ * safe to sprinkle markers across hot paths.
9
+ *
10
+ * Rationale for `console.log` (not the structured logger):
11
+ * measurement is a cross-cutting concern and must not depend on the log
12
+ * pipeline it may itself be measuring. Keeping the output path trivial also
13
+ * preserves the "zero-overhead when disabled, minimal overhead when enabled"
14
+ * contract.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { mark, measure, withPerf } from "@mandujs/core/perf";
19
+ *
20
+ * mark("build:start");
21
+ * await doWork();
22
+ * measure("full build", "build:start");
23
+ *
24
+ * const result = await withPerf("ssr:render", () => render(route));
25
+ * ```
26
+ *
27
+ * @module perf
28
+ */
29
+
30
+ // Module-load snapshot. `isPerfEnabled()` is a stable answer per process.
31
+ let enabled: boolean = process.env.MANDU_PERF === "1";
32
+
33
+ // Lazily-initialized marker table. Stays `null` when disabled so no allocation
34
+ // occurs on the disabled fast path.
35
+ let marks: Map<string, number> | null = null;
36
+
37
+ /**
38
+ * Whether perf markers are active for this process.
39
+ *
40
+ * Cached at module load from `MANDU_PERF=1`; changing the env var afterwards
41
+ * has no effect (see `_resetCacheForTesting` for test-only overrides).
42
+ */
43
+ export function isPerfEnabled(): boolean {
44
+ return enabled;
45
+ }
46
+
47
+ /**
48
+ * Record a start marker. Later passed to {@link measure} as `startName`.
49
+ *
50
+ * No-op when perf is disabled.
51
+ */
52
+ export function mark(name: string): void {
53
+ if (!enabled) return;
54
+ if (marks === null) marks = new Map<string, number>();
55
+ marks.set(name, Bun.nanoseconds());
56
+ }
57
+
58
+ /**
59
+ * Log elapsed milliseconds since `startName` was marked and return the value.
60
+ *
61
+ * Returns `0` (and does not log or throw) if `startName` was never marked, so
62
+ * callers can safely remove a `mark()` without breaking a `measure()` site.
63
+ * No-op (returns `0`) when perf is disabled.
64
+ */
65
+ export function measure(label: string, startName: string): number {
66
+ if (!enabled) return 0;
67
+ if (marks === null) return 0;
68
+ const start = marks.get(startName);
69
+ if (start === undefined) return 0;
70
+ const ms = (Bun.nanoseconds() - start) / 1_000_000;
71
+ console.log(`[perf] ${label}: ${ms.toFixed(2)}ms`);
72
+ return ms;
73
+ }
74
+
75
+ /**
76
+ * Measure the duration of `fn` and log on completion. Propagates the resolved
77
+ * value and re-throws errors (still logging the elapsed time before rethrow).
78
+ *
79
+ * When perf is disabled, `fn` is invoked directly with no wrapping overhead
80
+ * beyond a single branch and a `Promise.resolve`.
81
+ */
82
+ export async function withPerf<T>(label: string, fn: () => T | Promise<T>): Promise<T> {
83
+ if (!enabled) return await fn();
84
+ const start = Bun.nanoseconds();
85
+ try {
86
+ return await fn();
87
+ } finally {
88
+ const ms = (Bun.nanoseconds() - start) / 1_000_000;
89
+ console.log(`[perf] ${label}: ${ms.toFixed(2)}ms`);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Test-only: re-read `MANDU_PERF` from the environment and clear marker state.
95
+ *
96
+ * Not part of the public surface; intentionally prefixed with `_` and excluded
97
+ * from the module-level TSDoc examples. Stable across the test suite only.
98
+ *
99
+ * @internal
100
+ */
101
+ export function _resetCacheForTesting(): void {
102
+ enabled = process.env.MANDU_PERF === "1";
103
+ marks = null;
104
+ }