@forinda/kickjs 5.8.0 → 5.9.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 (85) hide show
  1. package/README.md +137 -20
  2. package/dist/{adapter-RzfEzd-c.d.mts → adapter-BRvrS9xR.d.mts} +75 -7
  3. package/dist/adapter-BRvrS9xR.d.mts.map +1 -0
  4. package/dist/application-BChmaBl7.mjs +12 -0
  5. package/dist/application-BChmaBl7.mjs.map +1 -0
  6. package/dist/{application-CwLcDTyg.d.mts → application-BpRV6_s8.d.mts} +29 -9
  7. package/dist/application-BpRV6_s8.d.mts.map +1 -0
  8. package/dist/{bootstrap-B8y526yc.d.mts → bootstrap-DJ3Qutd_.d.mts} +2 -2
  9. package/dist/{bootstrap-B8y526yc.d.mts.map → bootstrap-DJ3Qutd_.d.mts.map} +1 -1
  10. package/dist/{bootstrap-V0q7pgA4.mjs → bootstrap-DJmYYyw7.mjs} +3 -3
  11. package/dist/{bootstrap-V0q7pgA4.mjs.map → bootstrap-DJmYYyw7.mjs.map} +1 -1
  12. package/dist/config/index.mjs +2 -2
  13. package/dist/{config-C_oycBvB.mjs → config-CTxZI8Yd.mjs} +3 -3
  14. package/dist/{config-C_oycBvB.mjs.map → config-CTxZI8Yd.mjs.map} +1 -1
  15. package/dist/{container-BvRsVif5.mjs → container-pRv37ZRS.mjs} +3 -3
  16. package/dist/{container-BvRsVif5.mjs.map → container-pRv37ZRS.mjs.map} +1 -1
  17. package/dist/{context-ByhFuf19.mjs → context-KJOHBItp.mjs} +3 -3
  18. package/dist/{context-ByhFuf19.mjs.map → context-KJOHBItp.mjs.map} +1 -1
  19. package/dist/core/adapter.d.mts +1 -1
  20. package/dist/core/adapter.mjs +1 -1
  21. package/dist/core/app-module.mjs +1 -1
  22. package/dist/core/cache.mjs +1 -1
  23. package/dist/core/container.mjs +2 -2
  24. package/dist/core/cron.mjs +2 -2
  25. package/dist/core/decorators.mjs +2 -2
  26. package/dist/core/errors.mjs +1 -1
  27. package/dist/core/interfaces.mjs +1 -1
  28. package/dist/core/logger.mjs +1 -1
  29. package/dist/core/path.mjs +1 -1
  30. package/dist/core/plugin.d.mts +1 -1
  31. package/dist/core/plugin.mjs +1 -1
  32. package/dist/core/reactivity.d.mts +1 -1
  33. package/dist/core/reactivity.mjs +2 -2
  34. package/dist/core/reactivity.mjs.map +1 -1
  35. package/dist/{core-BQHQsimv.mjs → core-XkTLgdql.mjs} +3 -3
  36. package/dist/{core-BQHQsimv.mjs.map → core-XkTLgdql.mjs.map} +1 -1
  37. package/dist/{env-D3Q6sGZP.mjs → env-BkqJDWN0.mjs} +3 -3
  38. package/dist/{env-D3Q6sGZP.mjs.map → env-BkqJDWN0.mjs.map} +1 -1
  39. package/dist/http/application.d.mts +1 -1
  40. package/dist/http/application.mjs +2 -2
  41. package/dist/http/bootstrap.d.mts +1 -1
  42. package/dist/http/bootstrap.mjs +2 -2
  43. package/dist/http/context.mjs +2 -2
  44. package/dist/http/middleware/cors.mjs +1 -1
  45. package/dist/http/middleware/csrf.mjs +1 -1
  46. package/dist/http/middleware/error-handler.mjs +2 -2
  47. package/dist/http/middleware/helmet.mjs +1 -1
  48. package/dist/http/middleware/rate-limit.mjs +1 -1
  49. package/dist/http/middleware/request-id.mjs +1 -1
  50. package/dist/http/middleware/request-logger.mjs +2 -2
  51. package/dist/http/middleware/session.mjs +1 -1
  52. package/dist/http/middleware/spa.d.mts +1 -1
  53. package/dist/http/middleware/spa.mjs +2 -2
  54. package/dist/http/middleware/upload.mjs +1 -1
  55. package/dist/http/middleware/validate.mjs +2 -2
  56. package/dist/http/middleware/views.d.mts +1 -1
  57. package/dist/http/middleware/views.mjs +2 -2
  58. package/dist/http/query/index.mjs +2 -2
  59. package/dist/http/router-builder.d.mts +1 -1
  60. package/dist/http/router-builder.mjs +2 -2
  61. package/dist/{index-C5aLtJkk.d.mts → index-qZqR_Mxr.d.mts} +3 -3
  62. package/dist/{index-C5aLtJkk.d.mts.map → index-qZqR_Mxr.d.mts.map} +1 -1
  63. package/dist/index.d.mts +10 -10
  64. package/dist/index.mjs +2 -2
  65. package/dist/{metadata-DN0sBHmY.mjs → metadata-DtljJf8g.mjs} +2 -2
  66. package/dist/{metadata-DN0sBHmY.mjs.map → metadata-DtljJf8g.mjs.map} +1 -1
  67. package/dist/{plugin-BGK3PJOJ.d.mts → plugin-CyoGnRbq.d.mts} +8 -7
  68. package/dist/plugin-CyoGnRbq.d.mts.map +1 -0
  69. package/dist/{query-BuX74AmB.mjs → query-BGd32P8z.mjs} +2 -2
  70. package/dist/{query-BuX74AmB.mjs.map → query-BGd32P8z.mjs.map} +1 -1
  71. package/dist/{reactivity-CfJceo4H.d.mts → reactivity-BRvj7bdG.d.mts} +14 -1
  72. package/dist/reactivity-BRvj7bdG.d.mts.map +1 -0
  73. package/dist/{router-builder-QNfwAjQ8.d.mts → router-builder-R6dyZ-A3.d.mts} +2 -2
  74. package/dist/{router-builder-QNfwAjQ8.d.mts.map → router-builder-R6dyZ-A3.d.mts.map} +1 -1
  75. package/dist/{spa-BlW5xxU5.d.mts → spa-DFxiK7jQ.d.mts} +2 -2
  76. package/dist/{spa-BlW5xxU5.d.mts.map → spa-DFxiK7jQ.d.mts.map} +1 -1
  77. package/dist/{views-CNCTSpcw.d.mts → views-DGbyY_oV.d.mts} +2 -2
  78. package/dist/{views-CNCTSpcw.d.mts.map → views-DGbyY_oV.d.mts.map} +1 -1
  79. package/package.json +1 -1
  80. package/dist/adapter-RzfEzd-c.d.mts.map +0 -1
  81. package/dist/application-CwLcDTyg.d.mts.map +0 -1
  82. package/dist/application-h14rczhJ.mjs +0 -12
  83. package/dist/application-h14rczhJ.mjs.map +0 -1
  84. package/dist/plugin-BGK3PJOJ.d.mts.map +0 -1
  85. package/dist/reactivity-CfJceo4H.d.mts.map +0 -1
package/README.md CHANGED
@@ -1,58 +1,175 @@
1
1
  # @forinda/kickjs
2
2
 
3
- Decorator-driven Node.js framework on Express 5 + TypeScript. Custom DI, module system, code generators, Zod-native validation, Vite HMR.
3
+ Decorator-driven Node.js framework on Express 5 + TypeScript. Custom DI container, factory-first module system, code generators, Zod-native validation, end-to-end type safety via typegen, and Vite HMR for sub-200ms dev reloads.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- # Scaffold a new project (recommended)
8
+ # Scaffold a new project (recommended) — gets you a complete layout in seconds
9
9
  npx @forinda/kickjs-cli new my-api && cd my-api && pnpm dev
10
10
 
11
11
  # Or add to an existing project
12
12
  pnpm add @forinda/kickjs express reflect-metadata zod
13
- pnpm add -D @forinda/kickjs-cli
13
+ pnpm add -D @forinda/kickjs-cli @forinda/kickjs-vite
14
14
  ```
15
15
 
16
- ## Quick Example
16
+ ## Getting Started
17
+
18
+ The CLI scaffolds a complete layout. The files below show the canonical shapes — copy them into an existing project if you're not using `kick new`.
19
+
20
+ ### 1. Service (`@Service` + DI)
17
21
 
18
22
  ```ts
19
- // src/modules/hello/hello.controller.ts
20
- import { Controller, Get, type RequestContext } from '@forinda/kickjs'
23
+ // src/modules/users/user.service.ts
24
+ import { Service } from '@forinda/kickjs'
21
25
 
22
- @Controller()
23
- export class HelloController {
24
- @Get('/')
25
- hello(ctx: RequestContext) {
26
- ctx.json({ message: 'Hello from KickJS' })
26
+ @Service()
27
+ export class UserService {
28
+ list() {
29
+ return [{ id: '1', name: 'Alice' }]
30
+ }
31
+
32
+ create(input: { name: string; email: string }) {
33
+ return { id: '2', ...input }
27
34
  }
28
35
  }
29
36
  ```
30
37
 
38
+ ### 2. Controller (`@Controller`, decorators, typed `ctx`)
39
+
31
40
  ```ts
32
- // src/modules/hello/hello.module.ts
33
- import { buildRoutes, type AppModule, type ModuleRoutes } from '@forinda/kickjs'
34
- import { HelloController } from './hello.controller'
35
-
36
- export class HelloModule implements AppModule {
37
- routes(): ModuleRoutes {
38
- // Mount prefix lives here — single source of truth for where routes mount.
39
- return { path: '/hello', router: buildRoutes(HelloController), controller: HelloController }
41
+ // src/modules/users/user.controller.ts
42
+ import { Controller, Get, Post, Autowired, type Ctx } from '@forinda/kickjs'
43
+ import { z } from 'zod'
44
+ import { UserService } from './user.service'
45
+
46
+ const createUserSchema = z.object({
47
+ name: z.string().min(1),
48
+ email: z.string().email(),
49
+ })
50
+
51
+ @Controller()
52
+ export class UserController {
53
+ @Autowired() private readonly users!: UserService
54
+
55
+ @Get('/')
56
+ list(ctx: Ctx<KickRoutes.UserController['list']>) {
57
+ ctx.json(this.users.list())
58
+ }
59
+
60
+ @Post('/', { body: createUserSchema, name: 'CreateUser' })
61
+ create(ctx: Ctx<KickRoutes.UserController['create']>) {
62
+ // ctx.body is validated + typed from the Zod schema
63
+ ctx.created(this.users.create(ctx.body))
40
64
  }
41
65
  }
42
66
  ```
43
67
 
68
+ `Ctx<KickRoutes.UserController['list']>` is generated by `kick typegen` (auto-runs on `kick dev`) — `ctx.params`, `ctx.body`, `ctx.query` all narrow to the Zod-derived shape as you save.
69
+
70
+ ### 3. Module (`defineModule()` factory)
71
+
72
+ ```ts
73
+ // src/modules/users/user.module.ts
74
+ import { defineModule } from '@forinda/kickjs'
75
+ import { UserController } from './user.controller'
76
+
77
+ export const UserModule = defineModule({
78
+ name: 'UserModule',
79
+ build: () => ({
80
+ routes() {
81
+ return { path: '/users', controller: UserController }
82
+ },
83
+ }),
84
+ })
85
+ ```
86
+
87
+ The framework derives the router from `controller` — never import `buildRoutes` directly. `path` is the **single source of truth** for the mount prefix; `@Controller()` takes no path argument in v5.
88
+
89
+ ### 4. Module registry (`defineModules()` fluent chain)
90
+
91
+ ```ts
92
+ // src/modules/index.ts
93
+ import { defineModules } from '@forinda/kickjs'
94
+ import { UserModule } from './users/user.module'
95
+
96
+ // Factories are called at the registration site — `UserModule()` produces
97
+ // the AppModule instance bootstrap registers. Each `.mount(...)` call adds
98
+ // one module; chain as many as you need.
99
+ export const modules = defineModules().mount(UserModule())
100
+ ```
101
+
102
+ ### 5. Bootstrap entry
103
+
44
104
  ```ts
45
105
  // src/index.ts
46
106
  import 'reflect-metadata'
107
+ import './config' // side-effect — registers env schema BEFORE bootstrap runs
47
108
  import { bootstrap } from '@forinda/kickjs'
48
109
  import { modules } from './modules'
49
110
 
111
+ // Always export the app. The Vite plugin reads this symbol to wire HMR;
112
+ // skipping `export` works in production but degrades dev mode to full restarts.
50
113
  export const app = await bootstrap({ modules })
51
114
  ```
52
115
 
116
+ That's it — `http://localhost:3000/api/v1/users` returns Alice; `POST /api/v1/users` with `{ name, email }` validates against the Zod schema.
117
+
118
+ ## Project Layout
119
+
120
+ `kick new` produces this convention; everything except the bootstrap entry is configurable via `kick.config.ts`:
121
+
122
+ ```text
123
+ src/
124
+ index.ts # bootstrap entry — exports `app`
125
+ config/index.ts # env schema via defineEnv() + loadEnv()
126
+ modules/
127
+ index.ts # defineModules() chain
128
+ <name>/<name>.module.ts # one module per feature
129
+ <name>/<name>.controller.ts
130
+ <name>/<name>.service.ts
131
+ middleware/ # optional — `@Middleware()` factories + global stack
132
+ adapters/ # optional — `defineAdapter()` integrations
133
+ plugins/ # optional — `definePlugin()` bundles
134
+ .agents/ # AI agent context — AGENTS.md + skills/<slug>/SKILL.md
135
+ CLAUDE.md # Claude Code root (thin pointer to .agents/)
136
+ kick.config.ts # CLI configuration (optional)
137
+ vite.config.ts # Vite + KickJS HMR
138
+ ```
139
+
140
+ ## Core Concepts
141
+
142
+ - **Custom DI container** — three scopes (singleton / transient / request), slash-delimited tokens (`createToken<T>('app/users/repository')`), constructor + property injection. No external DI dep. See [Dependency Injection](https://forinda.github.io/kick-js/guide/dependency-injection.html).
143
+ - **Factory-first** — `defineAdapter()`, `definePlugin()`, `defineModule()`, `defineHttpContextDecorator()`. No class hierarchies to inherit from. See [Adapters](https://forinda.github.io/kick-js/guide/adapters.html), [Plugins](https://forinda.github.io/kick-js/guide/plugins.html).
144
+ - **Context Contributors** — replace single-purpose middleware that only sets `ctx` values. Typed `dependsOn` (typos are TS errors, not boot-time crashes), topo-sorted at startup, runs across HTTP / WS / queue / cron. See [Context Decorators](https://forinda.github.io/kick-js/guide/context-decorators.html).
145
+ - **Env wiring** — `defineEnv()` + `loadEnv()` in `src/config/index.ts`. The `import './config'` side-effect import at the top of `src/index.ts` MUST happen before any `@Value()` injection resolves. See [Configuration](https://forinda.github.io/kick-js/guide/configuration.html).
146
+ - **Asset Manager** — typed `assets.<ns>.<key>()` accessor, configured via `assetMap` in `kick.config.ts`. Drops the `__dirname` arithmetic that production builds get wrong. See [Asset Manager](https://forinda.github.io/kick-js/guide/asset-manager.html).
147
+ - **Reactive primitives** — `ref()` / `computed()` / `reactive()` / `watch()` Vue-style; `ref` and `computed` auto-unwrap on `JSON.stringify` so they drop straight into `introspect()` snapshots.
148
+ - **DevTools** — `/_debug` browser panel with topology, container, contributors, metrics. Adapters expose state via `introspect(): IntrospectionSnapshot` (type lives in `@forinda/kickjs` — no extra import needed). See [DevTools](https://forinda.github.io/kick-js/guide/devtools.html).
149
+
150
+ ## Common Add-Ons
151
+
152
+ ```bash
153
+ kick add swagger # OpenAPI docs from decorators + Zod schemas
154
+ kick add drizzle # Drizzle ORM adapter
155
+ kick add prisma # Prisma adapter (v5 / v6 / v7)
156
+ kick add devtools # /_debug dashboard
157
+ kick add ws # WebSocket with @WsController, rooms, heartbeat
158
+ kick add queue # BullMQ / RabbitMQ / Kafka jobs
159
+ kick add testing # createTestApp + createTestModule helpers
160
+ kick add --list # full live catalog
161
+ ```
162
+
53
163
  ## Documentation
54
164
 
55
- [forinda.github.io/kick-js](https://forinda.github.io/kick-js/) — full guide, API reference, tutorials, and example apps.
165
+ **[forinda.github.io/kick-js](https://forinda.github.io/kick-js/)** — full guide, API reference, BYO recipes (auth, GraphQL, OTel, cron, mailer, multi-tenancy, notifications), example apps.
166
+
167
+ Start here:
168
+
169
+ - [Getting Started](https://forinda.github.io/kick-js/guide/getting-started.html)
170
+ - [Modules](https://forinda.github.io/kick-js/guide/modules.html) · [Controllers](https://forinda.github.io/kick-js/guide/controllers.html) · [Middleware](https://forinda.github.io/kick-js/guide/middleware.html)
171
+ - [Context Decorators](https://forinda.github.io/kick-js/guide/context-decorators.html) · [Adapters](https://forinda.github.io/kick-js/guide/adapters.html) · [Plugins](https://forinda.github.io/kick-js/guide/plugins.html)
172
+ - [BYO Recipes](https://forinda.github.io/kick-js/guide/byo-recipes.html) — auth, GraphQL, OpenTelemetry, cron, mailer, multi-tenancy, notifications
56
173
 
57
174
  ## License
58
175
 
@@ -5,6 +5,74 @@ import { s as ContributorRegistrations } from "./context-decorator-BYf8HdMl.mjs"
5
5
  import http from "node:http";
6
6
  import { ErrorRequestHandler, Express, RequestHandler } from "express";
7
7
 
8
+ //#region src/core/introspect.d.ts
9
+ /**
10
+ * Per-primitive introspection snapshot — the JSON shape an adapter or
11
+ * plugin's optional `introspect()` hook returns for DevTools.
12
+ *
13
+ * Lives in `@forinda/kickjs` (not `@forinda/kickjs-devtools-kit`) so
14
+ * adopters can type their `introspect()` return without taking on a
15
+ * dep on the kit just to satisfy the contract. The kit's own
16
+ * `IntrospectionSnapshot` type is structurally identical and continues
17
+ * to be the canonical doc location for DevTools-side consumers; the
18
+ * two stay in lockstep — if a field is added on one side it MUST be
19
+ * added on the other (covered by the type-equivalence test in the
20
+ * devtools-kit package).
21
+ *
22
+ * Cheap-by-default principle: `state` and `metrics` should be O(1) to
23
+ * compute (counters, flags, recent samples). Anything that requires a
24
+ * DB round trip or a full DI graph walk belongs behind a separate
25
+ * explicit RPC — DevTools polls topology aggressively.
26
+ */
27
+ interface IntrospectionSnapshot {
28
+ /**
29
+ * Wire-protocol version this snapshot conforms to. The kit ships a
30
+ * `PROTOCOL_VERSION` constant typed as the literal `1`; we mirror
31
+ * that literal here so structural parity is enforced both ways —
32
+ * an adopter who writes `protocolVersion: 1` satisfies both this
33
+ * type and the kit's `IntrospectionSnapshot`. Bumping is a
34
+ * coordinated kit + framework release; widen this literal in
35
+ * lockstep.
36
+ */
37
+ protocolVersion: 1;
38
+ /** Stable identity matching the `name` from `definePlugin` / `defineAdapter`. */
39
+ name: string;
40
+ /** Discriminator for what kind of primitive this is. */
41
+ kind: IntrospectionKind;
42
+ /** Optional version string from the plugin / adapter metadata. */
43
+ version?: string;
44
+ /**
45
+ * Reactive state surface — JSON-serialisable snapshot. Keep small
46
+ * (under a few KB) so polling stays cheap.
47
+ */
48
+ state?: Record<string, unknown>;
49
+ /** DI tokens this primitive registers (`provides`) or consumes (`requires`). */
50
+ tokens?: {
51
+ provides: readonly string[];
52
+ requires: readonly string[];
53
+ };
54
+ /**
55
+ * Per-instance counters — active connections, in-flight jobs, cached
56
+ * items, etc. Numbers only so the DevTools panel can chart trends.
57
+ */
58
+ metrics?: Record<string, number>;
59
+ /** Self-reported memory footprint estimate, in bytes. Optional. */
60
+ memoryBytes?: number;
61
+ }
62
+ /**
63
+ * What kind of primitive an introspection snapshot describes. Matches
64
+ * the kit's `IntrospectionKind` and the routing the topology aggregator
65
+ * does when bucketing adapters vs plugins vs middleware vs contributor
66
+ * primitives.
67
+ */
68
+ type IntrospectionKind = 'plugin' | 'adapter' | 'middleware' | 'contributor';
69
+ /**
70
+ * Convenience function signature for adopter-side helpers that return
71
+ * a complete snapshot. Used by the optional `introspect?()` hook on
72
+ * `AppAdapter` and `KickPlugin`.
73
+ */
74
+ type IntrospectFn = () => IntrospectionSnapshot | Promise<IntrospectionSnapshot>;
75
+ //#endregion
8
76
  //#region src/core/augmentation.d.ts
9
77
  /**
10
78
  * Augmentation registry — runtime no-op + the canonical
@@ -306,12 +374,12 @@ interface AppAdapter {
306
374
  * the implementation cheap (counters + flags, no DB round trips) since
307
375
  * the topology endpoint polls on a short interval.
308
376
  *
309
- * The return type is intentionally untyped at this layer to avoid
310
- * `@forinda/kickjs` taking on a runtime dep on `@forinda/kickjs-devtools-kit`.
311
- * Adapter authors should import and return `IntrospectionSnapshot`
312
- * from `@forinda/kickjs-devtools-kit` directly.
377
+ * The return type lives in `@forinda/kickjs` itself ({@link IntrospectionSnapshot})
378
+ * so adopters can author `introspect()` without importing the kit just
379
+ * to satisfy the contract. The kit's own `IntrospectionSnapshot` type
380
+ * stays structurally identical; either import works.
313
381
  */
314
- introspect?(): unknown | Promise<unknown>;
382
+ introspect?(): IntrospectionSnapshot | Promise<IntrospectionSnapshot>;
315
383
  /**
316
384
  * Optional DevTools tabs this adapter contributes (architecture.md §23).
317
385
  * Returns an array of `DevtoolsTabDescriptor` shapes — typically one
@@ -327,5 +395,5 @@ interface AppAdapter {
327
395
  /** Constructor type for AppAdapter classes */
328
396
  type AppAdapterClass = new () => AppAdapter;
329
397
  //#endregion
330
- export { MiddlewarePath as a, KickJsPluginName as c, AppAdapterClass as i, KickJsPluginRegistry as l, AdapterMiddleware as n, MiddlewarePhase as o, AppAdapter as r, AugmentationMeta as s, AdapterContext as t, defineAugmentation as u };
331
- //# sourceMappingURL=adapter-RzfEzd-c.d.mts.map
398
+ export { MiddlewarePath as a, KickJsPluginName as c, IntrospectFn as d, IntrospectionKind as f, AppAdapterClass as i, KickJsPluginRegistry as l, AdapterMiddleware as n, MiddlewarePhase as o, IntrospectionSnapshot as p, AppAdapter as r, AugmentationMeta as s, AdapterContext as t, defineAugmentation as u };
399
+ //# sourceMappingURL=adapter-BRvrS9xR.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter-BRvrS9xR.d.mts","names":[],"sources":["../src/core/introspect.ts","../src/core/augmentation.ts","../src/core/adapter.ts"],"mappings":";;;;;;;;;;;;;;;AAkBA;;;;;;;;;;;UAAiB,qBAAA;EAgBf;;;;;;;;;EANA,eAAA;EAoBW;EAlBX,IAAA;EA2B2B;EAzB3B,IAAA,EAAM,iBAAA;EAyBqB;EAvB3B,OAAA;EA8BU;;;;EAzBV,KAAA,GAAQ,MAAA;EAyB+C;EAvBvD,MAAA;IAAW,QAAA;IAA6B,QAAA;EAAA;EAuBuB;;AAAqB;;EAlBpF,OAAA,GAAU,MAAA;;EAEV,WAAA;AAAA;;;AC3BmC;AAUrC;;;KD0BY,iBAAA;ACxBkB;AAY9B;;;;AAZ8B,KD+BlB,YAAA,SAAqB,qBAAA,GAAwB,OAAA,CAAQ,qBAAA;;;;;;;;;;AA9CjE;;;;;;;;;;;;;UCGiB,oBAAA;;;;;;;;AD2BJ;KCjBD,gBAAA,SAAyB,oBAAA,gCAE3B,oBAAoB;;;;ADwBD;AAO7B;;;;;;UCnBiB,gBAAA;EDmB+C;;;;ECd9D,WAAA;EDcoF;;;;AC3CtF;;EAoCE,OAAO;AAAA;AApC4B;AAUrC;;;;AAE8B;AAY9B;;;;AAYS;AA2CT;;;;;;;;AAAwE;;;;ACvExE;;;;AAA2B;AAc3B;;;;;;;;;;;;ADtBqC,iBA+ErB,kBAAA,CAAmB,IAAA,UAAc,IAAA,GAAO,gBAAgB;;;ADlFxE;;;;;;;;;;;;;;;;;;;;;AAAA,KEWY,eAAA;AFmBC;AASb;;;;AAA6B;AAO7B;;;;;;AAhBa,KELD,cAAA,YAA0B,MAAA,GAAS,aAAA,UAAuB,MAAA;;UAGrD,iBAAA;EFkBwC;;;AAA6B;;;EEXpF,OAAA,EAAS,cAAA,GAAiB,mBAAA;EDhCX;ECkCf,KAAA,GAAQ,eAAA;;;ADlC2B;AAUrC;;EC8BE,IAAA,GAAO,cAAA;AAAA;AD5BqB;AAY9B;;;;AAYS;AA2CT;;;;;;;AAnE8B,UC4Cb,cAAA;EDuBuD;ECrBtE,GAAA,EAAK,OAAA;;EAEL,SAAA,EAAW,SAAA;EApDD;EAsDV,MAAA,GAAS,IAAA,CAAK,MAAA;;EAEd,GAAA;EAxDyB;EA0DzB,YAAA;AAAA;;;;;;;;;;;AA5C0E;AAG5E;;;;;;;;;;;;;;;;;UAwEiB,UAAA;EAzCA;;;;;EA+Cf,IAAA;EAzCoB;;;;;;;;;;;;AAIR;AA+Bd;;;;;;EA2BE,SAAA,YAAqB,gBAAA;EAuBqB;;;;EAjB1C,UAAA,KAAe,iBAAA;EAqD0B;;;;;;;;;EA1CzC,YAAA,KAAiB,wBAAA;EAjBI;;;;EAuBrB,WAAA,EAAa,GAAA,EAAK,cAAA,UAAwB,OAAA;EAA1C;;;;EAMA,YAAA,EAAc,eAAA,EAAiB,WAAA,EAAa,SAAA;EAAb;;;;;;;;;;;;EAc/B,WAAA,EAAa,GAAA,EAAK,cAAA,UAAwB,OAAA;EAsBd;;;;;;;;;;AA6BhB;AAId;;;EAvCE,UAAA,EAAY,GAAA,EAAK,cAAA,UAAwB,OAAA;EAuCO;;;;EAjChD,aAAA,KAAkB,OAAA;IAAU,IAAA;IAAc,MAAA;EAAA;;EAG1C,QAAA,KAAa,YAAA;;;;;;;;;;;;;EAcb,UAAA,KAAe,qBAAA,GAAwB,OAAA,CAAQ,qBAAA;;;;;;;;;;;EAY/C,YAAA;AAAA;;KAIU,eAAA,aAA4B,UAAU"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @forinda/kickjs v5.9.1
3
+ *
4
+ * Copyright (c) Felix Orinda
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ * @license MIT
10
+ */
11
+ import{n as tokenName,t as Container}from"./container-pRv37ZRS.mjs";import{METADATA}from"./core/interfaces.mjs";import{Logger,createLogger}from"./core/logger.mjs";import{t as getClassMeta}from"./metadata-DtljJf8g.mjs";import{g as MutableModuleRegistry,s as mountSort}from"./core-XkTLgdql.mjs";import{normalizePath}from"./core/path.mjs";import{requestId}from"./http/middleware/request-id.mjs";import{errorHandler,notFoundHandler}from"./http/middleware/error-handler.mjs";import{i as requestStore}from"./context-KJOHBItp.mjs";import{_setExternalContributorSources,buildRoutes}from"./http/router-builder.mjs";import http from"node:http";import express from"express";import crypto from"node:crypto";const REQUEST_SCOPE_MIDDLEWARE_MARKER=Symbol.for(`@kickjs/requestScopeMiddleware`);function requestScopeMiddleware(){let mw=(req,_res,next)=>{let requestIdHeader=req.headers[`x-request-id`],requestId=(Array.isArray(requestIdHeader)?requestIdHeader[0]:requestIdHeader)||crypto.randomUUID(),store={requestId,instances:new Map,values:new Map};req.requestId=requestId,requestStore.run(store,()=>next())};return mw[REQUEST_SCOPE_MIDDLEWARE_MARKER]=!0,mw}function isRequestScopeMiddleware(value){return typeof value==`function`?value[REQUEST_SCOPE_MIDDLEWARE_MARKER]===!0:!1}const log=createLogger(`Application`);var Application=class{options;app;container;httpServer=null;adapters;plugins;_moduleContributors=[];_inFlightRequests=0;_draining=!1;_shutdownInitiated=!1;_drainResolvers=[];constructor(options){this.options=options,this.app=express(),this.container=Container.getInstance(),this.plugins=mountSort(options.plugins??[],`plugin`);let allAdapters=[...this.plugins.flatMap(p=>p.adapters?.()??[]),...options.adapters??[]],anonAdapterCount=0,namedAdapters=allAdapters.map(adapter=>{if(!adapter.name){let ctorName=adapter.constructor?.name,fallback=ctorName&&ctorName!==`Object`?ctorName:`AnonymousAdapter#${anonAdapterCount++}`;return Object.assign(adapter,{name:fallback})}return adapter});this.adapters=mountSort(namedAdapters,`adapter`),Container._requestStoreProvider=()=>requestStore.getStore()??null,Logger._contextProvider=()=>{let store=requestStore.getStore();if(!store)return null;let ctx={requestId:store.requestId},traceId=store.values.get(`traceId`);traceId&&(ctx.traceId=traceId);let spanId=store.values.get(`spanId`);return spanId&&(ctx.spanId=spanId),ctx}}get isDraining(){return this._draining}get inFlightRequests(){return this._inFlightRequests}getContainer(){return this.container}handle(req,res,next){next?this.app(req,res,next):this.app(req,res)}adapterCtx(server){let env=process.env.NODE_ENV??`development`;return{app:this.app,container:this.container,server,env,isProduction:env===`production`}}async callHook(hook,ctx){if(hook)try{let result=hook(ctx);result&&typeof result.then==`function`&&await result}catch(err){log.error(err,`Adapter hook failed`)}}async setup(){log.debug(`Bootstrapping application...`),this._moduleContributors=[];let adapterMw=this.collectAdapterMiddleware();this.app.__kickApp=this;let ctx=this.adapterCtx();for(let adapter of this.adapters)await this.callHook(adapter.beforeMount?.bind(adapter),ctx);this.app.disable(`x-powered-by`),this.app.set(`trust proxy`,this.options.trustProxy??!1),this.app.use(this.requestTrackingMiddleware()),this.mountHealthEndpoints(),this.shouldAutoMountRequestScope()&&this.app.use(requestScopeMiddleware()),this.mountMiddlewareList(adapterMw.beforeGlobal);for(let plugin of this.plugins)plugin.register?.(this.container);for(let plugin of this.plugins)try{let mw=plugin.middleware?.()??[];for(let handler of mw)this.app.use(handler)}catch(err){log.error(err,`Plugin middleware failed: ${plugin.name??`unknown`}`)}if(this.options.security?.helmet!==!1)try{let{helmet:helmetFn}=await import(`./http/middleware/helmet.mjs`);this.app.use(helmetFn())}catch{}if(this.options.middleware)for(let entry of this.options.middleware)this.mountMiddlewareEntry(entry);else this.app.use(requestId()),this.app.use(express.json({limit:this.options.jsonLimit??`1mb`}));this.mountMiddlewareList(adapterMw.afterGlobal);let moduleRegistry=new MutableModuleRegistry;for(let plugin of this.plugins){for(let m of plugin.modules?.()??[])moduleRegistry.mount(m);plugin.setup?.(moduleRegistry)}for(let m of this.options.modules)moduleRegistry.mount(m);this.options.setup?.(moduleRegistry);let modules=moduleRegistry.entries.map(entry=>{let mod=typeof entry==`function`?new entry:entry;return mod.register?.(this.container),mod});this.container.bootstrap(),this.mountMiddlewareList(adapterMw.beforeRoutes);let apiPrefix=this.options.apiPrefix??`/api`,defaultVersion=this.options.defaultVersion??1,shouldLogRoutes=this.options.logRoutesTable??process.env.NODE_ENV!==`production`,mountedRoutes=[],adapterSources=[];for(let adapter of this.adapters){let adapterContribs=adapter.contributors?.()??[],adapterLabel=adapter.name??adapter.constructor.name??`adapter`;for(let registration of adapterContribs)adapterSources.push({source:`adapter`,registration,label:adapterLabel})}for(let plugin of this.plugins){let pluginContribs=plugin.contributors?.()??[],pluginLabel=plugin.name??plugin.constructor?.name??`plugin`;for(let registration of pluginContribs)adapterSources.push({source:`adapter`,registration,label:pluginLabel})}let globalSources=(this.options.contributors??[]).map(registration=>({source:`global`,registration,label:`bootstrap`}));this._moduleContributors=[];for(let mod of modules){let declaredName=mod.name,ctorName=mod.constructor?.name,moduleLabel=typeof declaredName==`string`&&declaredName.length>0?declaredName:ctorName&&ctorName!==`Object`?ctorName:`module`,moduleSources=(mod.contributors?.()??[]).map(registration=>({source:`module`,registration,label:moduleLabel}));for(let src of moduleSources)this._moduleContributors.push({registration:src.registration,label:moduleLabel});_setExternalContributorSources([...moduleSources,...adapterSources,...globalSources]);try{let result=mod.routes();if(!result)continue;let routeSets=Array.isArray(result)?result:[result];for(let route of routeSets){let mountPath=`${apiPrefix}/v${route.version??defaultVersion}${normalizePath(route.path)}`,router=route.router??buildRoutes(route.controller);if(!router)throw Error(`Module route at ${mountPath} requires either 'controller' or 'router'. Pass \`controller: SomeController\` for the common case (the framework calls buildRoutes() internally) or \`router: yourRouter\` to hand-build it.`);if(this.app.use(mountPath,router),route.controller){for(let adapter of this.adapters)try{adapter.onRouteMount?.(route.controller,mountPath)}catch(err){log.error(err,`adapter.onRouteMount() failed for ${mountPath}`)}shouldLogRoutes&&mountedRoutes.push({controller:route.controller,mountPath})}}}finally{_setExternalContributorSources([])}}if(shouldLogRoutes&&mountedRoutes.length>0){let methodOrder=[`GET`,`POST`,`PUT`,`PATCH`,`DELETE`,`HEAD`,`OPTIONS`],methodRank=m=>{let i=methodOrder.indexOf(m);return i===-1?99:i},totalRoutes=0;log.debug(`Routes:`);for(let{controller,mountPath}of mountedRoutes){let defs=getClassMeta(METADATA.ROUTES,controller,[]);if(defs.length===0)continue;let counts={};for(let def of defs){let m=def.method.toUpperCase();counts[m]=(counts[m]||0)+1}totalRoutes+=defs.length;let methods=Object.entries(counts).toSorted(([a],[b])=>methodRank(a)-methodRank(b)).map(([m,n])=>`${n} ${m}`).join(`, `),name=controller.name||`Controller`;log.debug(` ${name.padEnd(30)} ${mountPath.padEnd(25)} ${defs.length} routes (${methods})`)}log.debug(` Total: ${totalRoutes} routes`)}this.mountMiddlewareList(adapterMw.afterRoutes),this.app.use(this.options.onNotFound??notFoundHandler()),this.app.use(this.options.onError??errorHandler());for(let adapter of this.adapters)await this.callHook(adapter.beforeStart?.bind(adapter),ctx)}async registerOnly(){await this.setup()}async start(){await this.setup();let g=globalThis;if(g.__kickjs_httpServer){this.httpServer=g.__kickjs_httpServer,log.debug(`Attached to Vite dev server`);for(let adapter of this.adapters){let ctx=this.adapterCtx(this.httpServer);await this.callHook(adapter.afterStart?.bind(adapter),ctx)}for(let plugin of this.plugins)await plugin.onReady?.(this.container);return}let port=this.options.port??parseInt(process.env.PORT||`3000`,10);this.httpServer=http.createServer(this.app),this.httpServer.on(`error`,err=>{throw err.code===`EADDRINUSE`&&(log.error(`Port ${port} is already in use. Kill the existing process or use a different port:\n PORT=${port+1} kick dev\n lsof -i :${port} # find what's using it\n kill <PID> # stop it`),process.exit(1)),err}),await new Promise((resolve,reject)=>{this.httpServer.listen(port,async()=>{try{log.info(`Server running on http://localhost:${port}`);for(let adapter of this.adapters){let afterCtx=this.adapterCtx(this.httpServer);await this.callHook(adapter.afterStart?.bind(adapter),afterCtx)}for(let plugin of this.plugins)await plugin.onReady?.(this.container);resolve()}catch(err){reject(err)}})})}async rebuild(){let prevApp=this.app,prevContainer=this.container;try{Container.reset(),this.container=Container.getInstance(),this.app=express(),await this.setup()}catch(err){log.error(err,`HMR rebuild failed, keeping previous app`),this.app=prevApp,this.container=prevContainer;return}this.httpServer&&(this.httpServer.removeAllListeners(`request`),this.httpServer.on(`request`,this.app),log.debug(`HMR: Express app rebuilt and swapped`))}async shutdown(){if(this._shutdownInitiated){log.debug(`Shutdown already in progress, skipping duplicate call`);return}this._shutdownInitiated=!0,this._draining=!0,log.debug(`Shutting down — draining in-flight requests...`);let timeoutMs=this.options.shutdownTimeout??3e4,timer,forceExitPromise=timeoutMs>0?new Promise(resolve=>{timer=setTimeout(()=>resolve(`timeout`),timeoutMs),timer.unref()}):new Promise(()=>{});try{if(this.httpServer&&this.httpServer.close(()=>{}),this._inFlightRequests>0){log.debug(`Waiting for ${this._inFlightRequests} in-flight request(s) to complete...`);let drainPromise=new Promise(resolve=>{this._drainResolvers.push(()=>resolve(`drained`))});await Promise.race([drainPromise,forceExitPromise])===`timeout`?log.warn(`Shutdown timeout (${timeoutMs}ms) reached with ${this._inFlightRequests} request(s) still in-flight, forcing shutdown`):log.debug(`All in-flight requests completed`)}let results=await Promise.allSettled([...this.plugins.map(plugin=>Promise.resolve(plugin.shutdown?.())),...this.adapters.map(adapter=>Promise.resolve(adapter.shutdown?.()))]);for(let result of results)result.status===`rejected`&&log.error({err:result.reason},`Adapter shutdown failed`)}finally{timer&&clearTimeout(timer)}}getExpressApp(){return this.app}getAdapters(){return this.adapters}getPlugins(){return this.plugins}getContributors(){let out=[],ingest=(list,source,label)=>{if(list)for(let reg of list){let r=reg;if(r.key===void 0||r.key===null)continue;let key=typeof r.key==`string`?r.key:tokenName(r.key);out.push({key,source,label,dependsOn:Array.isArray(r.dependsOn)?r.dependsOn:[]})}};for(let adapter of this.adapters)ingest(adapter.contributors?.(),`adapter`,adapter.name??`adapter`);for(let plugin of this.plugins)ingest(plugin.contributors?.(),`plugin`,plugin.name??`plugin`);for(let{registration,label}of this._moduleContributors)ingest([registration],`module`,label);return ingest(this.options.contributors,`global`,`bootstrap`),out}getHttpServer(){return this.httpServer}shouldAutoMountRequestScope(){if(this.options.contextStore===`manual`)return!1;let userEntries=this.options.middleware??[];for(let entry of userEntries)if(isRequestScopeMiddleware(typeof entry==`function`?entry:entry.handler))return!1;return!0}collectAdapterMiddleware(){let result={beforeGlobal:[],afterGlobal:[],beforeRoutes:[],afterRoutes:[]};for(let adapter of this.adapters){let entries=adapter.middleware?.()??[];for(let entry of entries)result[entry.phase??`afterGlobal`].push(entry)}return result}mountMiddlewareList(entries){for(let entry of entries){if(entry.path===void 0){this.app.use(entry.handler);continue}let path=Array.isArray(entry.path)?[...entry.path]:entry.path;this.app.use(path,entry.handler)}}mountMiddlewareEntry(entry){typeof entry==`function`?this.app.use(entry):this.app.use(entry.path,entry.handler)}requestTrackingMiddleware(){return(_req,res,next)=>{this._inFlightRequests++;let onFinish=()=>{if(res.removeListener(`finish`,onFinish),res.removeListener(`close`,onFinish),this._inFlightRequests--,this._draining&&this._inFlightRequests===0){for(let resolve of this._drainResolvers)resolve();this._drainResolvers=[]}};res.on(`finish`,onFinish),res.on(`close`,onFinish),next()}}mountHealthEndpoints(){this.app.get(`/health/live`,(_req,res)=>{this._draining?res.status(503).json({status:`draining`,uptime:process.uptime()}):res.json({status:`ok`,uptime:process.uptime()})}),this.app.get(`/health/ready`,async(_req,res)=>{if(this._draining){res.status(503).json({status:`draining`,checks:[]});return}let adaptersWithHealth=this.adapters.filter(a=>a.onHealthCheck),results=(await Promise.allSettled(adaptersWithHealth.map(a=>a.onHealthCheck()))).map((c,i)=>c.status===`fulfilled`?c.value:{name:adaptersWithHealth[i].name??`unknown`,status:`down`}),healthy=results.every(r=>r.status===`up`);res.status(healthy?200:503).json({status:healthy?`ready`:`degraded`,checks:results})})}};export{requestScopeMiddleware as n,Application as t};
12
+ //# sourceMappingURL=application-BChmaBl7.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"application-BChmaBl7.mjs","names":[],"sources":["../src/http/middleware/request-scope.ts","../src/http/application.ts"],"sourcesContent":["import crypto from 'node:crypto'\nimport type { Request, Response, NextFunction } from 'express'\nimport { requestStore, type RequestStore } from '../request-store'\n\n/**\n * Marker symbol stamped on the function returned by {@link requestScopeMiddleware}.\n *\n * `Application.setup()` uses {@link isRequestScopeMiddleware} to detect whether\n * the user-supplied middleware list already contains the request-scope wrapper,\n * and skips its automatic auto-mount when one is found. Lets adopters control\n * the exact position of the ALS frame in their pipeline (e.g., after a custom\n * tracing wrapper) without ending up with two nested frames.\n */\n// `Symbol.for(...)` returns a plain `symbol` (not `unique symbol`), but the\n// runtime detection marker doesn't need the unique nominal property — it only\n// needs the interned-slot reference equality that `Symbol.for` already provides.\nconst REQUEST_SCOPE_MIDDLEWARE_MARKER = Symbol.for('@kickjs/requestScopeMiddleware')\n\n/**\n * Wraps each request in an AsyncLocalStorage context.\n * Enables REQUEST-scoped DI and automatic requestId propagation to logs.\n *\n * Should be mounted early in the middleware pipeline (before route handlers).\n * `Application` mounts this automatically unless `contextStore: 'manual'` is set\n * or the user already includes one in their middleware list.\n */\nexport function requestScopeMiddleware() {\n const mw = (req: Request, _res: Response, next: NextFunction) => {\n const requestIdHeader = req.headers['x-request-id']\n const requestId =\n (Array.isArray(requestIdHeader) ? requestIdHeader[0] : requestIdHeader) || crypto.randomUUID()\n const store: RequestStore = {\n requestId,\n instances: new Map(),\n // Explicit generic locks the \"string keys only\" decision\n // (architecture.md §20.12 #4) at construction time, so accidental\n // non-string keys can't slip in via the inferred Map<any, any>.\n values: new Map<string, unknown>(),\n }\n // Surface the requestId on req so the standalone requestId() middleware\n // (when also mounted) reuses this value instead of generating a divergent\n // second one. Without this, log lines that read from the ALS store would\n // disagree with the X-Request-Id response header for any request that\n // didn't carry an inbound x-request-id header.\n ;(req as Request & { requestId?: string }).requestId = requestId\n requestStore.run(store, () => next())\n }\n ;(mw as unknown as Record<symbol, unknown>)[REQUEST_SCOPE_MIDDLEWARE_MARKER] = true\n return mw\n}\n\n/**\n * Returns `true` when `value` is the function produced by {@link requestScopeMiddleware}.\n *\n * Detection uses a `Symbol.for(...)` marker so the check survives different\n * copies of `@forinda/kickjs` loaded under different module identities (e.g.,\n * a published package alongside a workspace-linked one during local dev).\n */\nexport function isRequestScopeMiddleware(value: unknown): boolean {\n if (typeof value !== 'function') return false\n return (value as unknown as Record<symbol, unknown>)[REQUEST_SCOPE_MIDDLEWARE_MARKER] === true\n}\n","import http from 'node:http'\nimport express, { type Express, type RequestHandler } from 'express'\nimport {\n Container,\n createLogger,\n Logger,\n normalizePath,\n tokenName,\n METADATA,\n type AppModule,\n type AppModuleClass,\n type AppModuleEntry,\n type AppAdapter,\n type AdapterContext,\n type AdapterMiddleware,\n type ContributorRegistrations,\n type KickPlugin,\n type ModuleRegistry,\n type RouteDefinition,\n type SourcedRegistration,\n mountSort,\n MutableModuleRegistry,\n} from '../core'\nimport { getClassMeta } from '../core/metadata'\nimport { requestId } from './middleware/request-id'\nimport { notFoundHandler, errorHandler } from './middleware/error-handler'\nimport { requestScopeMiddleware, isRequestScopeMiddleware } from './middleware/request-scope'\nimport { _setExternalContributorSources, buildRoutes } from './router-builder'\nimport { requestStore } from './request-store'\n\nconst log = createLogger('Application')\n\n/**\n * A middleware entry in the declarative pipeline.\n * Can be a bare handler or an object with path scoping.\n */\nexport type MiddlewareEntry = RequestHandler | { path: string; handler: RequestHandler }\n\nexport interface ApplicationOptions {\n /**\n * Feature modules to load. Accepts both class form\n * (`SomeModule extends AppModule`, instantiated via `new`) and\n * instance form (output of {@link defineModule}'s factory call,\n * e.g. `TasksModule({ scope: 'admin' })`) — the loader discriminates\n * and either `new`-s the class or uses the instance directly.\n *\n * Static path — for conditional / dynamic registration, use the\n * {@link ApplicationOptions.setup} callback instead.\n */\n modules: AppModuleEntry[]\n\n /**\n * Imperative module registration callback. Receives a\n * {@link ModuleRegistry} you call `.mount(module)` on to register\n * modules conditionally — based on env flags, runtime config, a\n * tenant list, etc. Runs after every plugin's `setup()` hook and\n * after the static `modules: [...]` array is collected.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules: [HelloModule()],\n * setup(registry) {\n * if (env.ENABLE_ADMIN) registry.mount(AdminModule())\n * },\n * })\n * ```\n */\n setup?(registry: ModuleRegistry): void\n /** Adapters that hook into the lifecycle (DB, Redis, Swagger, etc.) */\n adapters?: AppAdapter[]\n /** Server port (falls back to PORT env var, then 3000) */\n port?: number\n /** Global API prefix (default: '/api') */\n apiPrefix?: string\n /** Default API version (default: 1) — routes become /{prefix}/v{version}/{path} */\n defaultVersion?: number\n\n /**\n * Global middleware pipeline. Declared in order.\n * Replaces the hardcoded middleware stack — you control exactly what runs.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * middleware: [\n * helmet(),\n * cors(),\n * compression(),\n * morgan('dev'),\n * express.json({ limit: '1mb' }),\n * ],\n * })\n * ```\n *\n * If omitted, a sensible default is applied:\n * requestId(), express.json({ limit: '100kb' })\n */\n middleware?: MiddlewareEntry[]\n\n /** Plugins that bundle modules, adapters, middleware, and DI bindings */\n plugins?: KickPlugin[]\n\n /**\n * Global Context Contributors (#107) that apply to every route in the\n * application. Merge into the per-route pipeline at the `'global'`\n * precedence level — they lose to module, adapter, class, and method\n * contributors with the same key, providing app-wide defaults that any\n * narrower scope can override.\n *\n * @example\n * ```ts\n * const StartedAt = defineContextDecorator({\n * key: 'requestStartedAt',\n * resolve: () => Date.now(),\n * })\n *\n * bootstrap({\n * modules,\n * contributors: [StartedAt.registration],\n * })\n * ```\n */\n contributors?: ContributorRegistrations\n\n /**\n * Backing store strategy for {@link RequestContext} `set/get` and the\n * Context Contributor pipeline (#107).\n *\n * - `'auto'` (default) — Application mounts {@link requestScopeMiddleware}\n * automatically before any user middleware. If the user-supplied\n * `middleware` list already includes `requestScopeMiddleware()`,\n * detection skips the auto-mount so adopters can control its position.\n * - `'manual'` — Application never mounts the wrapper. The Context\n * Contributor pipeline still runs on every route — the runner is\n * inserted by `router-builder` regardless of ALS state. What\n * degrades without an ALS frame is the *backing store*:\n * REQUEST-scoped DI throws (no `requestStore.getStore()` to read\n * from), `Logger` loses its requestId context, and\n * `RequestContext.set/get` throws. Use `'manual'` only when you\n * genuinely intend to wrap requests in your own ALS frame (rare —\n * multi-tenant adapters used to do this; post-Phase 3 they share\n * the framework's frame instead).\n */\n contextStore?: 'auto' | 'manual'\n\n /**\n * Controls whether KickJS registers process-level handlers at bootstrap.\n *\n * - `'auto'` (default) — register `uncaughtException` /\n * `unhandledRejection` loggers AND `SIGINT` / `SIGTERM` →\n * `app.shutdown()` → `process.exit(0)`.\n * - `'errors-only'` — register the error loggers; skip the signal\n * handlers. Use this when you have your own shutdown choreographer\n * (OpenTelemetry SDK, Sentry, custom orchestrator) that calls\n * `app.shutdown()` itself and you don't want two listeners racing\n * to call `process.exit`.\n * - `'manual'` — skip everything. Adopter is responsible for both\n * error logging and signal-driven shutdown.\n *\n * Background: many observability SDKs install their own SIGTERM\n * handler to flush spans/metrics before the process exits. Two\n * separate handlers calling `process.exit(0)` race each other and\n * can truncate the in-flight flush. Setting this to `'errors-only'`\n * or `'manual'` lets the SDK own the lifecycle while still letting\n * KickJS surface uncaught exceptions / rejections via the logger.\n *\n * The dev mode auto-detect (`__kickjs_httpServer` set by the Vite\n * plugin) suppresses signal registration regardless of this option,\n * since Vite owns the lifecycle in development.\n */\n processHooks?: 'auto' | 'errors-only' | 'manual'\n\n /** Express `trust proxy` setting */\n trustProxy?: boolean | number | string | ((ip: string, hopIndex: number) => boolean)\n /** Maximum JSON body size (only used when middleware is not provided) */\n jsonLimit?: string | number\n /**\n * Log route summary on startup. Default: true in dev, false in production.\n * Set to `true` to always log, `false` to always suppress.\n */\n logRoutesTable?: boolean\n /**\n * Maximum time (ms) to wait for graceful shutdown before forcing exit.\n * Default: 30000 (30 seconds). Set to 0 to disable forced exit.\n */\n shutdownTimeout?: number\n\n /**\n * Enable cluster mode for multi-core utilization.\n *\n * When enabled, the primary process forks worker processes that share the\n * same port via Node's built-in `cluster` module (OS load balancing).\n *\n * - `true` — use all available CPU cores\n * - `{ workers: N }` — use exactly N workers\n *\n * Workers are auto-restarted on crash. SIGTERM/SIGINT on the primary is\n * forwarded to all workers.\n *\n * @example\n * ```ts\n * bootstrap({ modules, cluster: true })\n * bootstrap({ modules, cluster: { workers: 4 } })\n * ```\n */\n cluster?: boolean | { workers?: number }\n\n /**\n * Custom 404 handler for unmatched routes. Receives the raw Express\n * `(req, res, next)` args. When omitted, the built-in handler returns\n * `{ message: 'Not Found' }` with status 404.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * onNotFound: (req, res) => {\n * res.status(404).json({ error: 'Route not found', path: req.originalUrl })\n * },\n * })\n * ```\n */\n onNotFound?: (req: any, res: any, next: any) => void\n\n /**\n * Custom global error handler. Receives `(err, req, res, next)` — the\n * standard Express error-handling signature. When omitted, the built-in\n * handler formats ZodError, HttpException, and unexpected errors.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * onError: (err, req, res, next) => {\n * logger.error(err)\n * res.status(err.status ?? 500).json({ error: err.message })\n * },\n * })\n * ```\n */\n onError?: (err: any, req: any, res: any, next: any) => void\n\n /**\n * Security defaults applied automatically unless opted out.\n *\n * When not specified, secure defaults are applied:\n * - Helmet security headers are auto-injected\n * - JSON body limit defaults to 1MB\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * security: {\n * helmet: false, // disable auto-helmet\n * },\n * })\n * ```\n */\n security?: {\n /** Auto-inject helmet security headers. Default: true */\n helmet?: boolean\n }\n}\n\n/**\n * The main application class. Wires together Express, the DI container,\n * feature modules, adapters, and the middleware pipeline.\n */\nexport class Application {\n private app: Express\n private container: Container\n private httpServer: http.Server | null = null\n private readonly adapters: AppAdapter[]\n\n private readonly plugins: KickPlugin[]\n\n /**\n * Snapshot of every module-level Context Contributor registration\n * captured during `setup()`. The registrations themselves are\n * frozen by `defineContextDecorator`, so storing references is\n * safe — they outlive the module instances they came from.\n *\n * Used by {@link getContributors} so DevTools and adopter tooling\n * can render the module-level contributors alongside the adapter /\n * plugin / global ones. Populated once per `setup()` call; cleared\n * at the start of each invocation so a re-setup (test harnesses,\n * dev-server restarts) doesn't accumulate stale entries.\n */\n private _moduleContributors: Array<{\n registration: { key: string; dependsOn: readonly string[] }\n label: string\n }> = []\n\n /** Number of HTTP requests currently being processed */\n private _inFlightRequests = 0\n /** Whether the application is draining (shutting down gracefully) */\n private _draining = false\n /** Whether shutdown has already been initiated (prevents double-shutdown) */\n private _shutdownInitiated = false\n /** Resolvers waiting for in-flight requests to reach zero */\n private _drainResolvers: Array<() => void> = []\n\n constructor(private readonly options: ApplicationOptions) {\n this.app = express()\n this.container = Container.getInstance()\n\n // Sort plugins by `dependsOn` declarations BEFORE reading their adapters/etc.\n // Plugins without `dependsOn` keep their declaration order — this is a\n // pure refinement; no behaviour change for apps that don't use the field.\n this.plugins = mountSort(options.plugins ?? [], 'plugin')\n\n // Build adapter list from plugin adapters + user adapters, synthesize\n // a stable name for any anonymous adapter (so duplicate-name detection\n // and `dependsOn` resolution have something to key on), then sort by\n // adapter `dependsOn`. Plugin-shipped adapters keep their plugin's\n // relative order unless their own `dependsOn` says otherwise.\n const allAdapters = [\n ...this.plugins.flatMap((p) => p.adapters?.() ?? []),\n ...(options.adapters ?? []),\n ]\n let anonAdapterCount = 0\n const namedAdapters: Array<AppAdapter & { name: string }> = allAdapters.map((adapter) => {\n if (!adapter.name) {\n // `constructor.name === 'Object'` for plain object-literal adapters —\n // useless as an identity since two anonymous literals would collide.\n // Only borrow the constructor name when it's an actual class.\n const ctorName = adapter.constructor?.name\n const fallback =\n ctorName && ctorName !== 'Object' ? ctorName : `AnonymousAdapter#${anonAdapterCount++}`\n return Object.assign(adapter, { name: fallback })\n }\n return adapter as AppAdapter & { name: string }\n })\n this.adapters = mountSort(namedAdapters, 'adapter')\n // Wire the request store provider so Container can resolve REQUEST-scoped deps\n Container._requestStoreProvider = () => requestStore.getStore() ?? null\n // Wire logger context provider so logs auto-include requestId\n Logger._contextProvider = () => {\n const store = requestStore.getStore()\n if (!store) return null\n const ctx: Record<string, any> = { requestId: store.requestId }\n const traceId = store.values.get('traceId')\n if (traceId) ctx.traceId = traceId\n const spanId = store.values.get('spanId')\n if (spanId) ctx.spanId = spanId\n return ctx\n }\n }\n\n /** Whether the application is currently draining in-flight requests */\n get isDraining(): boolean {\n return this._draining\n }\n\n /** Number of HTTP requests currently being processed */\n get inFlightRequests(): number {\n return this._inFlightRequests\n }\n\n /** Get the DI container instance */\n getContainer(): Container {\n return this.container\n }\n\n /**\n * Express request handler — delegates to the internal Express app.\n *\n * Used by the Vite dev-server plugin to route requests through KickJS:\n * ```ts\n * const mod = await ssrLoadModule('virtual:kickjs/app')\n * mod.app.handle(req, res, next)\n * ```\n *\n * Also works as a standard Node.js request handler for production:\n * ```ts\n * http.createServer(app.handle.bind(app))\n * ```\n */\n handle(req: http.IncomingMessage, res: http.ServerResponse, next?: (err?: any) => void): void {\n if (next) {\n this.app(req as any, res as any, next)\n } else {\n this.app(req as any, res as any)\n }\n }\n\n /**\n * Full setup pipeline:\n * 1. Adapter beforeMount hooks (early routes — docs, health)\n * 2. Adapter middleware (phase: beforeGlobal)\n * 3. Global middleware (user-declared or defaults)\n * 4. Adapter middleware (phase: afterGlobal)\n * 5. Module registration + DI bootstrap\n * 6. Adapter middleware (phase: beforeRoutes)\n * 7. Module route mounting\n * 8. Adapter middleware (phase: afterRoutes)\n * 9. Error handlers (notFound + global)\n * 10. Adapter beforeStart hooks\n */\n /** Build the adapter context object (shared across all hooks) */\n private adapterCtx(server?: any): AdapterContext {\n const env = process.env.NODE_ENV ?? 'development'\n return {\n app: this.app,\n container: this.container,\n server,\n env,\n isProduction: env === 'production',\n }\n }\n\n /** Call an adapter hook, awaiting async hooks and catching errors */\n private async callHook(\n hook: ((ctx: AdapterContext) => void | Promise<void>) | undefined,\n ctx: AdapterContext,\n ): Promise<void> {\n if (!hook) return\n try {\n const result = hook(ctx)\n if (result && typeof (result as Promise<void>).then === 'function') {\n await result\n }\n } catch (err) {\n log.error(err, 'Adapter hook failed')\n }\n }\n\n async setup(): Promise<void> {\n log.debug('Bootstrapping application...')\n\n // Reset module-contributor snapshot at the very top of each\n // setup pass — before anything that could throw — so a partial\n // failure doesn't leave stale entries that getContributors()\n // would surface on the next call. See _moduleContributors field\n // declaration for the use case.\n this._moduleContributors = []\n\n // Collect adapter middleware by phase\n const adapterMw = this.collectAdapterMiddleware()\n\n // Expose the Application instance on the Express app for adapter discovery\n ;(this.app as any).__kickApp = this\n\n const ctx = this.adapterCtx()\n\n // ── 1. Adapter beforeMount hooks ──────────────────────────────────\n for (const adapter of this.adapters) {\n await this.callHook(adapter.beforeMount?.bind(adapter), ctx)\n }\n\n // ── 2. Hardened defaults ──────────────────────────────────────────\n this.app.disable('x-powered-by')\n this.app.set('trust proxy', this.options.trustProxy ?? false)\n\n // ── 2a. In-flight request tracking ──────────────────────────────\n this.app.use(this.requestTrackingMiddleware())\n\n // ── 2b. Health check endpoints (before middleware, at root) ──────\n this.mountHealthEndpoints()\n\n // ── 2c. Request scope (AsyncLocalStorage) ────────────────────────\n // Auto-mounted unless the user opted out (`contextStore: 'manual'`)\n // or already included one in their middleware list.\n if (this.shouldAutoMountRequestScope()) {\n this.app.use(requestScopeMiddleware())\n }\n\n // ── 3. Adapter middleware: beforeGlobal ───────────────────────────\n this.mountMiddlewareList(adapterMw.beforeGlobal)\n\n // ── 3b. Plugin registration ──────────────────────────────────────\n for (const plugin of this.plugins) {\n plugin.register?.(this.container)\n }\n\n // ── 3c. Plugin middleware ─────────────────────────────────────────\n for (const plugin of this.plugins) {\n try {\n const mw = plugin.middleware?.() ?? []\n for (const handler of mw) {\n this.app.use(handler)\n }\n } catch (err) {\n log.error(err, `Plugin middleware failed: ${(plugin as any).name ?? 'unknown'}`)\n }\n }\n\n // ── 4. Global middleware ─────────────────────────────────────────\n // Auto-inject helmet unless opted out\n const autoHelmet = this.options.security?.helmet !== false\n if (autoHelmet) {\n try {\n const { helmet: helmetFn } = await import('./middleware/helmet')\n this.app.use(helmetFn())\n } catch {\n // helmet middleware not available — skip silently\n }\n }\n\n if (this.options.middleware) {\n // User-declared pipeline — full control\n for (const entry of this.options.middleware) {\n this.mountMiddlewareEntry(entry)\n }\n } else {\n // Sensible defaults when no middleware declared\n this.app.use(requestId())\n this.app.use(express.json({ limit: this.options.jsonLimit ?? '1mb' }))\n }\n\n // ── 5. Adapter middleware: afterGlobal ────────────────────────────\n this.mountMiddlewareList(adapterMw.afterGlobal)\n\n // ── 6. Module registration + DI bootstrap ────────────────────────\n // Module collection — the static `modules: [...]` array AND the\n // imperative `setup(registry)` callback feed into the same\n // ordered list. Plugins go first so plugin modules / setup\n // calls run before user code (existing precedence).\n //\n // Order within the registry:\n // 1. plugin static modules (`plugin.modules?()`)\n // 2. plugin setup() calls (in plugin dependsOn-sorted order)\n // 3. user static modules (`options.modules`)\n // 4. user setup() callback\n const moduleRegistry = new MutableModuleRegistry()\n for (const plugin of this.plugins) {\n for (const m of plugin.modules?.() ?? []) moduleRegistry.mount(m)\n plugin.setup?.(moduleRegistry)\n }\n for (const m of this.options.modules) moduleRegistry.mount(m)\n this.options.setup?.(moduleRegistry)\n const allModuleEntries: AppModuleEntry[] = moduleRegistry.entries\n const modules = allModuleEntries.map((entry) => {\n // Discriminate class vs instance: classes are functions whose\n // `prototype` carries the AppModule shape; defineModule output is\n // a plain object with `routes`, `register`, `contributors`. Calling\n // `new` on a plain object throws, so we branch up-front rather\n // than try/catch.\n const mod: AppModule = typeof entry === 'function' ? new (entry as AppModuleClass)() : entry\n // `register()` is optional — modules whose classes are entirely\n // decorator-managed (@Service, @Controller, @Repository) don't need it.\n mod.register?.(this.container)\n return mod\n })\n this.container.bootstrap()\n\n // ── 7. Adapter middleware: beforeRoutes ───────────────────────────\n this.mountMiddlewareList(adapterMw.beforeRoutes)\n\n // ── 8. Mount module routes with versioning ───────────────────────\n const apiPrefix = this.options.apiPrefix ?? '/api'\n const defaultVersion = this.options.defaultVersion ?? 1\n const shouldLogRoutes = this.options.logRoutesTable ?? process.env.NODE_ENV !== 'production'\n\n // Collect route metadata during mounting (avoids calling mod.routes() twice)\n const mountedRoutes: Array<{ controller: any; mountPath: string }> = []\n\n // Context Contributors (#107) — collect adapter + plugin + global once;\n // per-module sources are computed inside the loop so module isolation is\n // preserved.\n //\n // Plugin contributors merge at the same `'adapter'` precedence as adapter\n // contributors. Plugins are conceptually \"bundles of adapters + extras\",\n // so a plugin that ships a typed contributor without standing up an\n // accompanying adapter behaves identically to one that does.\n const adapterSources: SourcedRegistration[] = []\n for (const adapter of this.adapters) {\n const adapterContribs = adapter.contributors?.() ?? []\n const adapterLabel = adapter.name ?? adapter.constructor.name ?? 'adapter'\n for (const registration of adapterContribs) {\n adapterSources.push({ source: 'adapter', registration, label: adapterLabel })\n }\n }\n for (const plugin of this.plugins) {\n const pluginContribs = plugin.contributors?.() ?? []\n const pluginLabel = plugin.name ?? plugin.constructor?.name ?? 'plugin'\n for (const registration of pluginContribs) {\n adapterSources.push({ source: 'adapter', registration, label: pluginLabel })\n }\n }\n const globalSources: SourcedRegistration[] = (this.options.contributors ?? []).map(\n (registration): SourcedRegistration => ({\n source: 'global',\n registration,\n label: 'bootstrap',\n }),\n )\n\n // Reset module-contributor snapshot at the start of each setup\n // pass so re-running `setup()` (test harnesses, dev-server\n // restarts) doesn't accumulate stale entries.\n this._moduleContributors = []\n\n for (const mod of modules) {\n // Prefer the declared `name` field (set by `defineModule({ name: 'X', ... })`)\n // over `constructor.name`. Factory-built modules are plain objects whose\n // constructor is `Object` — falling straight to `constructor.name` would\n // degrade every label in the DevTools Contributors table to \"Object\".\n const declaredName = (mod as { name?: unknown }).name\n const ctorName = mod.constructor?.name\n const moduleLabel =\n typeof declaredName === 'string' && declaredName.length > 0\n ? declaredName\n : ctorName && ctorName !== 'Object'\n ? ctorName\n : 'module'\n const moduleSources: SourcedRegistration[] = (mod.contributors?.() ?? []).map(\n (registration): SourcedRegistration => ({\n source: 'module',\n registration,\n label: moduleLabel,\n }),\n )\n // Retain registrations for `getContributors()` — these would\n // otherwise be unreachable post-bootstrap (module instances\n // are not kept on the Application).\n for (const src of moduleSources) {\n this._moduleContributors.push({\n registration: src.registration as { key: string; dependsOn: readonly string[] },\n label: moduleLabel,\n })\n }\n\n // Thread per-module + adapter + global sources to buildRoutes via the\n // module-scoped slot. Module setup is sequential, so the slot is\n // race-free. The slot MUST stay populated across both `mod.routes()`\n // AND any framework-driven `buildRoutes(controller)` calls below —\n // when a module returns `{ path, controller }` (auto-derive shape),\n // buildRoutes runs here, not inside routes(). Clearing the slot in\n // the routes()-finally would drop module/adapter/global contributors\n // on the floor and surface as `MissingContributorError` for any\n // class/method-level dependsOn referencing a module-level key.\n _setExternalContributorSources([...moduleSources, ...adapterSources, ...globalSources])\n try {\n const result = mod.routes()\n if (!result) continue // Non-HTTP modules (queues, cron) may return null\n\n const routeSets = Array.isArray(result) ? result : [result]\n\n for (const route of routeSets) {\n const version = route.version ?? defaultVersion\n const mountPath = `${apiPrefix}/v${version}${normalizePath(route.path)}`\n // Derive the router from `controller` when one wasn't passed\n // explicitly. The common-case shape is `{ path, controller }` —\n // the framework owns `buildRoutes(controller)` so adopters don't\n // import + call it themselves. Adopters who hand-build a router\n // (composing multiple controllers, mounting third-party routers)\n // pass `router` explicitly and we use it as-is.\n const router = route.router ?? buildRoutes(route.controller)\n if (!router) {\n throw new Error(\n `Module route at ${mountPath} requires either 'controller' or 'router'. ` +\n 'Pass `controller: SomeController` for the common case (the framework calls ' +\n 'buildRoutes() internally) or `router: yourRouter` to hand-build it.',\n )\n }\n this.app.use(mountPath, router)\n\n // Notify adapters (e.g. SwaggerAdapter for OpenAPI spec generation)\n if (route.controller) {\n for (const adapter of this.adapters) {\n try {\n adapter.onRouteMount?.(route.controller, mountPath)\n } catch (err) {\n log.error(err, `adapter.onRouteMount() failed for ${mountPath}`)\n }\n }\n if (shouldLogRoutes) {\n mountedRoutes.push({ controller: route.controller, mountPath })\n }\n }\n }\n } finally {\n _setExternalContributorSources([])\n }\n }\n\n // ── 8b. Log route summary ─────────────────────────────────────────\n if (shouldLogRoutes && mountedRoutes.length > 0) {\n const methodOrder = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']\n const methodRank = (m: string) => {\n const i = methodOrder.indexOf(m)\n return i === -1 ? 99 : i\n }\n\n let totalRoutes = 0\n log.debug('Routes:')\n\n for (const { controller, mountPath } of mountedRoutes) {\n const defs: RouteDefinition[] = getClassMeta<RouteDefinition[]>(\n METADATA.ROUTES,\n controller,\n [],\n )\n if (defs.length === 0) continue\n\n const counts: Record<string, number> = {}\n for (const def of defs) {\n const m = def.method.toUpperCase()\n counts[m] = (counts[m] || 0) + 1\n }\n totalRoutes += defs.length\n\n const methods = Object.entries(counts)\n .toSorted(([a], [b]) => methodRank(a) - methodRank(b))\n .map(([m, n]) => `${n} ${m}`)\n .join(', ')\n const name = controller.name || 'Controller'\n log.debug(` ${name.padEnd(30)} ${mountPath.padEnd(25)} ${defs.length} routes (${methods})`)\n }\n\n log.debug(` Total: ${totalRoutes} routes`)\n }\n\n // ── 9. Adapter middleware: afterRoutes ────────────────────────────\n this.mountMiddlewareList(adapterMw.afterRoutes)\n\n // ── 10. Error handlers ───────────────────────────────────────────\n this.app.use(this.options.onNotFound ?? notFoundHandler())\n this.app.use(this.options.onError ?? errorHandler())\n\n // ── 11. Adapter beforeStart hooks ────────────────────────────────\n for (const adapter of this.adapters) {\n await this.callHook(adapter.beforeStart?.bind(adapter), ctx)\n }\n }\n\n /** Register modules and DI without starting the HTTP server (used by kick tinker) */\n async registerOnly(): Promise<void> {\n await this.setup()\n }\n\n /**\n * Start the HTTP server.\n *\n * In **dev mode** (Vite plugin active): reuses `globalThis.__kickjs_httpServer`\n * created by Vite. Adapters (WsAdapter, Socket.IO, etc.) receive the real\n * `http.Server` through `afterStart({ server })` — zero adapter changes needed.\n *\n * In **production**: creates its own `http.Server` and binds to the port.\n */\n async start(): Promise<void> {\n await this.setup()\n\n const g = globalThis as any\n\n if (g.__kickjs_httpServer) {\n // ── DEV MODE: Vite owns the http.Server ──────────────────────\n // Don't create a new server or listen — Vite is already listening.\n // Just wire up adapters with the Vite-created server.\n this.httpServer = g.__kickjs_httpServer\n log.debug('Attached to Vite dev server')\n\n for (const adapter of this.adapters) {\n const ctx = this.adapterCtx(this.httpServer!)\n await this.callHook(adapter.afterStart?.bind(adapter), ctx)\n }\n\n for (const plugin of this.plugins) {\n await plugin.onReady?.(this.container)\n }\n\n return\n }\n\n // ── PRODUCTION: Create and own the http.Server ─────────────────\n const port = this.options.port ?? parseInt(process.env.PORT || '3000', 10)\n this.httpServer = http.createServer(this.app)\n\n this.httpServer.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n log.error(\n `Port ${port} is already in use. Kill the existing process or use a different port:\\n` +\n ` PORT=${port + 1} kick dev\\n` +\n ` lsof -i :${port} # find what's using it\\n` +\n ` kill <PID> # stop it`,\n )\n process.exit(1)\n }\n throw err\n })\n\n // Wrap listen in a Promise so afterStart/onReady errors propagate\n await new Promise<void>((resolve, reject) => {\n this.httpServer!.listen(port, async () => {\n try {\n log.info(`Server running on http://localhost:${port}`)\n\n for (const adapter of this.adapters) {\n const afterCtx = this.adapterCtx(this.httpServer!)\n await this.callHook(adapter.afterStart?.bind(adapter), afterCtx)\n }\n\n // Plugin onReady hooks\n for (const plugin of this.plugins) {\n await plugin.onReady?.(this.container)\n }\n\n resolve()\n } catch (err) {\n reject(err)\n }\n })\n })\n }\n\n /** HMR rebuild: swap Express handler without restarting the server */\n async rebuild(): Promise<void> {\n // Build the new app fully before swapping — if setup() throws,\n // the old app keeps running so the server stays responsive.\n const prevApp = this.app\n const prevContainer = this.container\n\n try {\n Container.reset()\n this.container = Container.getInstance()\n this.app = express()\n await this.setup()\n } catch (err) {\n log.error(err, 'HMR rebuild failed, keeping previous app')\n // Restore previous state so the server stays responsive\n this.app = prevApp\n this.container = prevContainer\n return\n }\n\n if (this.httpServer) {\n this.httpServer.removeAllListeners('request')\n this.httpServer.on('request', this.app)\n log.debug('HMR: Express app rebuilt and swapped')\n }\n }\n\n /**\n * Graceful shutdown with request draining.\n *\n * 1. Stops accepting new connections (server.close())\n * 2. Waits for in-flight requests to complete (up to shutdownTimeout)\n * 3. Calls adapter.shutdown() for all registered adapters\n * 4. Force-closes after timeout\n *\n * Safe to call multiple times — subsequent calls return the same promise.\n */\n async shutdown(): Promise<void> {\n // Prevent double-shutdown — return immediately if already initiated\n if (this._shutdownInitiated) {\n log.debug('Shutdown already in progress, skipping duplicate call')\n return\n }\n this._shutdownInitiated = true\n this._draining = true\n\n log.debug('Shutting down — draining in-flight requests...')\n\n const timeoutMs = this.options.shutdownTimeout ?? 30_000\n let timer: ReturnType<typeof setTimeout> | undefined\n\n // Start a force-exit timer if timeout is configured\n const forceExitPromise =\n timeoutMs > 0\n ? new Promise<'timeout'>((resolve) => {\n timer = setTimeout(() => resolve('timeout'), timeoutMs)\n timer.unref()\n })\n : new Promise<never>(() => {}) // never resolves — no forced exit\n\n try {\n // Step 1: Stop accepting new connections.\n // server.close() prevents new connections. Its callback fires only when\n // ALL existing connections are fully closed, so we do NOT await it here —\n // we track request draining separately via the tracking middleware.\n if (this.httpServer) {\n this.httpServer.close(() => {})\n }\n\n // Step 2: Wait for in-flight requests to drain (or timeout)\n if (this._inFlightRequests > 0) {\n log.debug(`Waiting for ${this._inFlightRequests} in-flight request(s) to complete...`)\n const drainPromise = new Promise<'drained'>((resolve) => {\n this._drainResolvers.push(() => resolve('drained'))\n })\n\n const result = await Promise.race([drainPromise, forceExitPromise])\n if (result === 'timeout') {\n log.warn(\n `Shutdown timeout (${timeoutMs}ms) reached with ${this._inFlightRequests} request(s) still in-flight, forcing shutdown`,\n )\n } else {\n log.debug('All in-flight requests completed')\n }\n }\n\n // Step 3: Run all plugin + adapter shutdowns concurrently\n const results = await Promise.allSettled([\n ...this.plugins.map((plugin) => Promise.resolve(plugin.shutdown?.())),\n ...this.adapters.map((adapter) => Promise.resolve(adapter.shutdown?.())),\n ])\n for (const result of results) {\n if (result.status === 'rejected') {\n log.error({ err: result.reason }, 'Adapter shutdown failed')\n }\n }\n } finally {\n if (timer) clearTimeout(timer)\n }\n }\n\n getExpressApp(): Express {\n return this.app\n }\n\n /** Get registered adapters — used by DevToolsAdapter for peer discovery */\n getAdapters(): AppAdapter[] {\n return this.adapters\n }\n\n /**\n * Get registered plugins — used by DevToolsAdapter for topology\n * introspection (architecture.md §23). Read-only; returns the\n * already-sorted plugin list (post-`dependsOn` topo-sort).\n */\n getPlugins(): readonly KickPlugin[] {\n return this.plugins\n }\n\n /**\n * Snapshot of every Context Contributor reachable from this app —\n * walks **four** registration sites: adapters, plugins, modules,\n * and the bootstrap `contributors` option. The devtools Topology\n * tab consumes this for the Contributors panel; tests + adopters\n * can call it for diagnostics.\n *\n * Per-route (method/class-decorator) registrations are NOT included\n * here — they live on the route registry rather than on any of\n * the four collection sites this method walks. A future RPC will\n * surface them separately.\n *\n * Result shape stays minimal — `{ key, source, label, dependsOn }`\n * — so we don't leak `resolve` closures or other internal fields.\n * `source` matches the five-level precedence union from\n * `ContributorSource` (minus `'method'` / `'class'` which require\n * the per-route walk noted above).\n */\n getContributors(): ReadonlyArray<{\n key: string\n source: 'module' | 'adapter' | 'plugin' | 'global'\n label: string\n dependsOn: readonly string[]\n }> {\n const out: Array<{\n key: string\n source: 'module' | 'adapter' | 'plugin' | 'global'\n label: string\n dependsOn: readonly string[]\n }> = []\n const ingest = (\n list: ReadonlyArray<unknown> | null | undefined,\n source: 'module' | 'adapter' | 'plugin' | 'global',\n label: string,\n ): void => {\n if (!list) return\n for (const reg of list) {\n const r = reg as { key?: unknown; dependsOn?: unknown }\n if (r.key === undefined || r.key === null) continue\n // `key` is typically a string but createToken<T>() yields a\n // branded token object — normalise via tokenName so both\n // surfaces flow through.\n const key = typeof r.key === 'string' ? r.key : tokenName(r.key)\n out.push({\n key,\n source,\n label,\n dependsOn: Array.isArray(r.dependsOn) ? (r.dependsOn as string[]) : [],\n })\n }\n }\n for (const adapter of this.adapters) {\n ingest(adapter.contributors?.(), 'adapter', adapter.name ?? 'adapter')\n }\n for (const plugin of this.plugins) {\n ingest(plugin.contributors?.(), 'plugin', plugin.name ?? 'plugin')\n }\n // Module-level registrations — captured during `setup()` because\n // module instances are not retained on the Application. Walk the\n // snapshot rather than calling `mod.contributors()` here.\n for (const { registration, label } of this._moduleContributors) {\n ingest([registration], 'module', label)\n }\n ingest(this.options.contributors, 'global', 'bootstrap')\n return out\n }\n\n getHttpServer(): http.Server | null {\n return this.httpServer\n }\n\n // ── Internal helpers ────────────────────────────────────────────────\n\n /**\n * Decide whether {@link Application} should auto-mount the\n * {@link requestScopeMiddleware} ALS wrapper.\n *\n * Returns `false` when:\n * - `contextStore: 'manual'` was set (caller manages ALS frames), or\n * - the user-supplied `middleware` list already includes a\n * `requestScopeMiddleware()` (detected via the symbol marker stamped\n * in `middleware/request-scope.ts`).\n *\n * Otherwise `true` — preserves the historical default of \"always wrap\n * requests in an ALS frame\" so existing apps see no behavior change.\n */\n private shouldAutoMountRequestScope(): boolean {\n if (this.options.contextStore === 'manual') return false\n\n const userEntries = this.options.middleware ?? []\n for (const entry of userEntries) {\n const handler = typeof entry === 'function' ? entry : entry.handler\n if (isRequestScopeMiddleware(handler)) return false\n }\n return true\n }\n\n private collectAdapterMiddleware() {\n const result = {\n beforeGlobal: [] as AdapterMiddleware[],\n afterGlobal: [] as AdapterMiddleware[],\n beforeRoutes: [] as AdapterMiddleware[],\n afterRoutes: [] as AdapterMiddleware[],\n }\n\n for (const adapter of this.adapters) {\n const entries = adapter.middleware?.() ?? []\n for (const entry of entries) {\n const phase = entry.phase ?? 'afterGlobal'\n result[phase].push(entry)\n }\n }\n\n return result\n }\n\n private mountMiddlewareList(entries: AdapterMiddleware[]): void {\n for (const entry of entries) {\n if (entry.path === undefined) {\n this.app.use(entry.handler)\n continue\n }\n // Copy ReadonlyArray paths to a mutable array — Express's\n // `PathParams` doesn't accept readonly arrays, even though the\n // function never mutates them. Pass-through for non-array shapes.\n const path: string | RegExp | (string | RegExp)[] = Array.isArray(entry.path)\n ? [...entry.path]\n : (entry.path as string | RegExp)\n this.app.use(path, entry.handler)\n }\n }\n\n private mountMiddlewareEntry(entry: MiddlewareEntry): void {\n if (typeof entry === 'function') {\n this.app.use(entry)\n } else {\n this.app.use(entry.path, entry.handler)\n }\n }\n\n /** Middleware that tracks in-flight requests for graceful draining */\n private requestTrackingMiddleware(): RequestHandler {\n return (_req, res, next) => {\n this._inFlightRequests++\n const onFinish = () => {\n res.removeListener('finish', onFinish)\n res.removeListener('close', onFinish)\n this._inFlightRequests--\n // If draining and no more in-flight requests, resolve all waiters\n if (this._draining && this._inFlightRequests === 0) {\n for (const resolve of this._drainResolvers) {\n resolve()\n }\n this._drainResolvers = []\n }\n }\n res.on('finish', onFinish)\n res.on('close', onFinish)\n next()\n }\n }\n\n /** Mount /health/live and /health/ready endpoints at the root (no API prefix) */\n private mountHealthEndpoints(): void {\n this.app.get('/health/live', (_req, res) => {\n if (this._draining) {\n res.status(503).json({ status: 'draining', uptime: process.uptime() })\n } else {\n res.json({ status: 'ok', uptime: process.uptime() })\n }\n })\n\n this.app.get('/health/ready', async (_req, res) => {\n if (this._draining) {\n res.status(503).json({ status: 'draining', checks: [] })\n return\n }\n const adaptersWithHealth = this.adapters.filter((a) => a.onHealthCheck)\n const checks = await Promise.allSettled(adaptersWithHealth.map((a) => a.onHealthCheck!()))\n const results = checks.map((c, i) => {\n if (c.status === 'fulfilled') return c.value\n return { name: adaptersWithHealth[i].name ?? 'unknown', status: 'down' as const }\n })\n const healthy = results.every((r) => r.status === 'up')\n res.status(healthy ? 200 : 503).json({\n status: healthy ? 'ready' : 'degraded',\n checks: results,\n })\n })\n }\n}\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
 
2
2
  import { t as Container } from "./container-BZKeHc5X.mjs";
3
3
  import { s as ContributorRegistrations } from "./context-decorator-BYf8HdMl.mjs";
4
- import { r as AppAdapter } from "./adapter-RzfEzd-c.mjs";
4
+ import { r as AppAdapter } from "./adapter-BRvrS9xR.mjs";
5
5
  import { r as AppModuleEntry } from "./app-module-BE7hAASB.mjs";
6
- import { n as ModuleRegistry, t as KickPlugin } from "./plugin-BGK3PJOJ.mjs";
6
+ import { n as ModuleRegistry, t as KickPlugin } from "./plugin-CyoGnRbq.mjs";
7
7
  import http from "node:http";
8
8
  import { Express, RequestHandler } from "express";
9
9
 
@@ -245,6 +245,19 @@ declare class Application {
245
245
  private httpServer;
246
246
  private readonly adapters;
247
247
  private readonly plugins;
248
+ /**
249
+ * Snapshot of every module-level Context Contributor registration
250
+ * captured during `setup()`. The registrations themselves are
251
+ * frozen by `defineContextDecorator`, so storing references is
252
+ * safe — they outlive the module instances they came from.
253
+ *
254
+ * Used by {@link getContributors} so DevTools and adopter tooling
255
+ * can render the module-level contributors alongside the adapter /
256
+ * plugin / global ones. Populated once per `setup()` call; cleared
257
+ * at the start of each invocation so a re-setup (test harnesses,
258
+ * dev-server restarts) doesn't accumulate stale entries.
259
+ */
260
+ private _moduleContributors;
248
261
  /** Number of HTTP requests currently being processed */
249
262
  private _inFlightRequests;
250
263
  /** Whether the application is draining (shutting down gracefully) */
@@ -329,18 +342,25 @@ declare class Application {
329
342
  getPlugins(): readonly KickPlugin[];
330
343
  /**
331
344
  * Snapshot of every Context Contributor reachable from this app —
332
- * walks the four registration sites (adapters, plugins, global, and
333
- * the bootstrap `contributors` option). Module-level contributors
334
- * are NOT included; module instances aren't retained post-bootstrap.
335
- * The devtools Topology tab consumes this for the Contributors
336
- * panel; tests + adopters can call it for diagnostics.
345
+ * walks **four** registration sites: adapters, plugins, modules,
346
+ * and the bootstrap `contributors` option. The devtools Topology
347
+ * tab consumes this for the Contributors panel; tests + adopters
348
+ * can call it for diagnostics.
349
+ *
350
+ * Per-route (method/class-decorator) registrations are NOT included
351
+ * here — they live on the route registry rather than on any of
352
+ * the four collection sites this method walks. A future RPC will
353
+ * surface them separately.
337
354
  *
338
355
  * Result shape stays minimal — `{ key, source, label, dependsOn }`
339
356
  * — so we don't leak `resolve` closures or other internal fields.
357
+ * `source` matches the five-level precedence union from
358
+ * `ContributorSource` (minus `'method'` / `'class'` which require
359
+ * the per-route walk noted above).
340
360
  */
341
361
  getContributors(): ReadonlyArray<{
342
362
  key: string;
343
- source: 'adapter' | 'plugin' | 'global';
363
+ source: 'module' | 'adapter' | 'plugin' | 'global';
344
364
  label: string;
345
365
  dependsOn: readonly string[];
346
366
  }>;
@@ -369,4 +389,4 @@ declare class Application {
369
389
  }
370
390
  //#endregion
371
391
  export { ApplicationOptions as n, MiddlewareEntry as r, Application as t };
372
- //# sourceMappingURL=application-CwLcDTyg.d.mts.map
392
+ //# sourceMappingURL=application-BpRV6_s8.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"application-BpRV6_s8.d.mts","names":[],"sources":["../src/http/application.ts"],"mappings":";;;;;;;;;;;;;;KAoCY,eAAA,GAAkB,cAAA;EAAmB,IAAA;EAAc,OAAA,EAAS,cAAc;AAAA;AAAA,UAErE,kBAAA;;;;;;;;AAFqE;AAEtF;;EAWE,OAAA,EAAS,cAAA;EAAA;;;;;;;;;;;;;;;;;EAmBT,KAAA,EAAO,QAAA,EAAU,cAAA;EA+BjB;EA7BA,QAAA,GAAW,UAAA;EAgCX;EA9BA,IAAA;EAoDA;EAlDA,SAAA;EAuEA;EArEA,cAAA;EAmGA;;;;;;;;;;;;;;;;;;;;AAwFQ;EApKR,UAAA,GAAa,eAAA;EA4KS;EAzKtB,OAAA,GAAU,UAAA;EA2M4B;;;;;;;;;;;;;;;;;;;;EArLtC,YAAA,GAAe,wBAAA;EAyJE;;;;;;;;;;;;;;;;;;;EApIjB,YAAA;EA4OoE;;;;;;;;;;;;;;;;;;;;;;;;;EAjNpE,YAAA;EAuzBA;EApzBA,UAAA,iCAA2C,EAAA,UAAY,QAAA;EAozBjC;EAlzBtB,SAAA;EAg1BQ;;;;EA30BR,cAAA;EA64B4B;AAAA;;;EAx4B5B,eAAA;;;;;;;;;;;;;;;;;;;EAoBA,OAAA;IAAsB,OAAA;EAAA;;;;;;;;;;;;;;;;EAiBtB,UAAA,IAAc,GAAA,OAAU,GAAA,OAAU,IAAA;;;;;;;;;;;;;;;;;EAkBlC,OAAA,IAAW,GAAA,OAAU,GAAA,OAAU,GAAA,OAAU,IAAA;;;;;;;;;;;;;;;;;;EAmBzC,QAAA;6DAEE,MAAA;EAAA;AAAA;;;;;cAQS,WAAA;EAAA,iBAkCkB,OAAA;EAAA,QAjCrB,GAAA;EAAA,QACA,SAAA;EAAA,QACA,UAAA;EAAA,iBACS,QAAA;EAAA,iBAEA,OAAA;;;;;;;;;;;;;UAcT,mBAAA;;UAMA,iBAAA;;UAEA,SAAA;;UAEA,kBAAA;;UAEA,eAAA;cAEqB,OAAA,EAAS,kBAAA;;MAgDlC,UAAA,CAAA;;MAKA,gBAAA,CAAA;;EAKJ,YAAA,CAAA,GAAgB,SAAA;;;;;;;;;;;;;;;EAkBhB,MAAA,CAAO,GAAA,EAAK,IAAA,CAAK,eAAA,EAAiB,GAAA,EAAK,IAAA,CAAK,cAAA,EAAgB,IAAA,IAAQ,GAAA;;;;;;;;;;;;;;;UAsB5D,UAAA;;UAYM,QAAA;EAeR,KAAA,CAAA,GAAS,OAAA;;EA6ST,YAAA,CAAA,GAAgB,OAAA;;;;;;;;;;EAahB,KAAA,CAAA,GAAS,OAAA;;EAkET,OAAA,CAAA,GAAW,OAAA;;;;;;;;;;;EAoCX,QAAA,CAAA,GAAY,OAAA;EAgElB,aAAA,CAAA,GAAiB,OAAA;;EAKjB,WAAA,CAAA,GAAe,UAAA;;;;;;EASf,UAAA,CAAA,YAAuB,UAAA;;;;;;;;;;;;;;;;;;;EAsBvB,eAAA,CAAA,GAAmB,aAAA;IACjB,GAAA;IACA,MAAA;IACA,KAAA;IACA,SAAA;EAAA;EA6CF,aAAA,CAAA,GAAiB,IAAA,CAAK,MAAA;;;;;;;;;;;;;;UAmBd,2BAAA;EAAA,QAWA,wBAAA;EAAA,QAmBA,mBAAA;EAAA,QAgBA,oBAAA;;UASA,yBAAA;;UAsBA,oBAAA;AAAA"}
@@ -1,5 +1,5 @@
1
1
 
2
- import { n as ApplicationOptions, t as Application } from "./application-CwLcDTyg.mjs";
2
+ import { n as ApplicationOptions, t as Application } from "./application-BpRV6_s8.mjs";
3
3
 
4
4
  //#region src/http/bootstrap.d.ts
5
5
  /**
@@ -25,4 +25,4 @@ import { n as ApplicationOptions, t as Application } from "./application-CwLcDTy
25
25
  declare function bootstrap(options: ApplicationOptions): Promise<Application>;
26
26
  //#endregion
27
27
  export { bootstrap as t };
28
- //# sourceMappingURL=bootstrap-B8y526yc.d.mts.map
28
+ //# sourceMappingURL=bootstrap-DJ3Qutd_.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap-B8y526yc.d.mts","names":[],"sources":["../src/http/bootstrap.ts"],"mappings":";;;;;;AAqCA;;;;;;;;;;;;;AAAiF;;;;;iBAA3D,SAAA,CAAU,OAAA,EAAS,kBAAA,GAAqB,OAAA,CAAQ,WAAA"}
1
+ {"version":3,"file":"bootstrap-DJ3Qutd_.d.mts","names":[],"sources":["../src/http/bootstrap.ts"],"mappings":";;;;;;AAqCA;;;;;;;;;;;;;AAAiF;;;;;iBAA3D,SAAA,CAAU,OAAA,EAAS,kBAAA,GAAqB,OAAA,CAAQ,WAAA"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v5.8.0
2
+ * @forinda/kickjs v5.9.1
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{t as Container}from"./container-BvRsVif5.mjs";import{createLogger}from"./core/logger.mjs";import"./core-BQHQsimv.mjs";import{reactive,ref}from"./core/reactivity.mjs";import{t as Application}from"./application-h14rczhJ.mjs";import{a as reloadEnv}from"./env-D3Q6sGZP.mjs";import cluster from"node:cluster";import os from"node:os";const log$1=createLogger(`Cluster`);function isClusterPrimary(){return cluster.isPrimary}function setupClusterPrimary(opts){let numWorkers=opts.workers??os.cpus().length,gracefulTimeout=opts.gracefulShutdownTimeout??5e3,restartDelay=opts.restartDelay??1e3,maxFailures=opts.maxFailures??5,restartWindow=opts.restartWindow??3e4,startupTimeouts=new Map,state=reactive({failureTimestamps:[],shuttingDown:!1}),activeWorkers=ref(0);log$1.info(`Primary ${process.pid} starting ${numWorkers} worker(s)`);for(let i=0;i<numWorkers;i++)cluster.fork();cluster.on(`fork`,worker=>{let timeout=setTimeout(()=>{log$1.error(`Worker ${worker.process.pid} did not respond within 10s, killing...`),worker.kill()},1e4);startupTimeouts.set(worker.id,timeout),activeWorkers.value++,log$1.debug(`Worker ${worker.process.pid} forked (id=${worker.id})`)}),cluster.on(`listening`,(worker,address)=>{let timeout=startupTimeouts.get(worker.id);timeout&&(clearTimeout(timeout),startupTimeouts.delete(worker.id)),log$1.info(`Worker ${worker.process.pid} listening on ${address.address}:${address.port}`)}),cluster.on(`online`,worker=>{log$1.debug(`Worker ${worker.process.pid} online (id=${worker.id})`)}),cluster.on(`exit`,(worker,code,signal)=>{let timeout=startupTimeouts.get(worker.id);if(timeout&&(clearTimeout(timeout),startupTimeouts.delete(worker.id)),activeWorkers.value--,worker.exitedAfterDisconnect){log$1.info(`Worker ${worker.process.pid} exited gracefully (id=${worker.id})`);return}let now=Date.now();for(;state.failureTimestamps.length>0&&now-state.failureTimestamps[0]>=restartWindow;)state.failureTimestamps.shift();if(state.failureTimestamps.push(now),state.failureTimestamps.length>maxFailures){log$1.error(`${state.failureTimestamps.length} worker failures in ${restartWindow}ms, giving up`),activeWorkers.value===0&&(log$1.error(`All workers have crashed, exiting primary`),process.exit(1));return}log$1.warn(`Worker ${worker.process.pid} died (code=${code}, signal=${signal}), restarting in ${restartDelay}ms... (failures: ${state.failureTimestamps.length}/${maxFailures})`),setTimeout(()=>{activeWorkers.value<numWorkers&&cluster.fork()},restartDelay)}),cluster.on(`disconnect`,worker=>{log$1.debug(`Worker ${worker.process.pid} disconnected (id=${worker.id})`)});for(let signal of[`SIGTERM`,`SIGINT`])process.on(signal,()=>{state.shuttingDown||(state.shuttingDown=!0,log$1.info(`Primary received ${signal}, disconnecting workers...`),cluster.disconnect(()=>{log$1.info(`All workers disconnected, exiting primary`),process.exit(0)}),setTimeout(()=>{log$1.warn(`Graceful shutdown timed out, forcing exit`),process.exit(1)},gracefulTimeout).unref())})}function tryReloadEnv(){try{reloadEnv()}catch{}}const log=createLogger(`Process`);async function bootstrap(options){let isViteDev=!!globalThis.__kickjs_httpServer;if(options.cluster&&isViteDev&&log.warn(`Cluster mode is ignored in dev — Vite owns the server. Cluster applies in production (kick start).`),options.cluster&&cluster.isPrimary&&!isViteDev)return setupClusterPrimary(typeof options.cluster==`object`?options.cluster:{}),new Application(options);let g=globalThis,processHooks=options.processHooks??`auto`;if(!g.__kickBootstrapped){if(processHooks!==`manual`&&(process.on(`uncaughtException`,err=>{log.error(err,`Uncaught exception`)}),process.on(`unhandledRejection`,reason=>{log.error(reason,`Unhandled rejection`)})),processHooks===`auto`&&!g.__kickjs_httpServer)for(let signal of[`SIGINT`,`SIGTERM`])process.on(signal,async()=>{log.info(`Received ${signal}, shutting down...`),g.__app&&await g.__app.shutdown(),process.exit(0)});g.__kickBootstrapped=!0}if(g.__app){log.debug(`HMR rebuild triggered`),tryReloadEnv(),Container.reset();let freshApp=new Application(options);return g.__app=freshApp,g.__kickjs_container=freshApp.getContainer(),await freshApp.start(),log.debug(`HMR rebuild complete`),freshApp}let app=new Application(options);return g.__app=app,g.__kickjs_container=app.getContainer(),process.env.KICK_TINKER?(await app.registerOnly(),app):(await app.start(),app)}export{isClusterPrimary as n,bootstrap as t};
12
- //# sourceMappingURL=bootstrap-V0q7pgA4.mjs.map
11
+ import{t as Container}from"./container-pRv37ZRS.mjs";import{createLogger}from"./core/logger.mjs";import"./core-XkTLgdql.mjs";import{reactive,ref}from"./core/reactivity.mjs";import{t as Application}from"./application-BChmaBl7.mjs";import{a as reloadEnv}from"./env-BkqJDWN0.mjs";import cluster from"node:cluster";import os from"node:os";const log$1=createLogger(`Cluster`);function isClusterPrimary(){return cluster.isPrimary}function setupClusterPrimary(opts){let numWorkers=opts.workers??os.cpus().length,gracefulTimeout=opts.gracefulShutdownTimeout??5e3,restartDelay=opts.restartDelay??1e3,maxFailures=opts.maxFailures??5,restartWindow=opts.restartWindow??3e4,startupTimeouts=new Map,state=reactive({failureTimestamps:[],shuttingDown:!1}),activeWorkers=ref(0);log$1.info(`Primary ${process.pid} starting ${numWorkers} worker(s)`);for(let i=0;i<numWorkers;i++)cluster.fork();cluster.on(`fork`,worker=>{let timeout=setTimeout(()=>{log$1.error(`Worker ${worker.process.pid} did not respond within 10s, killing...`),worker.kill()},1e4);startupTimeouts.set(worker.id,timeout),activeWorkers.value++,log$1.debug(`Worker ${worker.process.pid} forked (id=${worker.id})`)}),cluster.on(`listening`,(worker,address)=>{let timeout=startupTimeouts.get(worker.id);timeout&&(clearTimeout(timeout),startupTimeouts.delete(worker.id)),log$1.info(`Worker ${worker.process.pid} listening on ${address.address}:${address.port}`)}),cluster.on(`online`,worker=>{log$1.debug(`Worker ${worker.process.pid} online (id=${worker.id})`)}),cluster.on(`exit`,(worker,code,signal)=>{let timeout=startupTimeouts.get(worker.id);if(timeout&&(clearTimeout(timeout),startupTimeouts.delete(worker.id)),activeWorkers.value--,worker.exitedAfterDisconnect){log$1.info(`Worker ${worker.process.pid} exited gracefully (id=${worker.id})`);return}let now=Date.now();for(;state.failureTimestamps.length>0&&now-state.failureTimestamps[0]>=restartWindow;)state.failureTimestamps.shift();if(state.failureTimestamps.push(now),state.failureTimestamps.length>maxFailures){log$1.error(`${state.failureTimestamps.length} worker failures in ${restartWindow}ms, giving up`),activeWorkers.value===0&&(log$1.error(`All workers have crashed, exiting primary`),process.exit(1));return}log$1.warn(`Worker ${worker.process.pid} died (code=${code}, signal=${signal}), restarting in ${restartDelay}ms... (failures: ${state.failureTimestamps.length}/${maxFailures})`),setTimeout(()=>{activeWorkers.value<numWorkers&&cluster.fork()},restartDelay)}),cluster.on(`disconnect`,worker=>{log$1.debug(`Worker ${worker.process.pid} disconnected (id=${worker.id})`)});for(let signal of[`SIGTERM`,`SIGINT`])process.on(signal,()=>{state.shuttingDown||(state.shuttingDown=!0,log$1.info(`Primary received ${signal}, disconnecting workers...`),cluster.disconnect(()=>{log$1.info(`All workers disconnected, exiting primary`),process.exit(0)}),setTimeout(()=>{log$1.warn(`Graceful shutdown timed out, forcing exit`),process.exit(1)},gracefulTimeout).unref())})}function tryReloadEnv(){try{reloadEnv()}catch{}}const log=createLogger(`Process`);async function bootstrap(options){let isViteDev=!!globalThis.__kickjs_httpServer;if(options.cluster&&isViteDev&&log.warn(`Cluster mode is ignored in dev — Vite owns the server. Cluster applies in production (kick start).`),options.cluster&&cluster.isPrimary&&!isViteDev)return setupClusterPrimary(typeof options.cluster==`object`?options.cluster:{}),new Application(options);let g=globalThis,processHooks=options.processHooks??`auto`;if(!g.__kickBootstrapped){if(processHooks!==`manual`&&(process.on(`uncaughtException`,err=>{log.error(err,`Uncaught exception`)}),process.on(`unhandledRejection`,reason=>{log.error(reason,`Unhandled rejection`)})),processHooks===`auto`&&!g.__kickjs_httpServer)for(let signal of[`SIGINT`,`SIGTERM`])process.on(signal,async()=>{log.info(`Received ${signal}, shutting down...`),g.__app&&await g.__app.shutdown(),process.exit(0)});g.__kickBootstrapped=!0}if(g.__app){log.debug(`HMR rebuild triggered`),tryReloadEnv(),Container.reset();let freshApp=new Application(options);return g.__app=freshApp,g.__kickjs_container=freshApp.getContainer(),await freshApp.start(),log.debug(`HMR rebuild complete`),freshApp}let app=new Application(options);return g.__app=app,g.__kickjs_container=app.getContainer(),process.env.KICK_TINKER?(await app.registerOnly(),app):(await app.start(),app)}export{isClusterPrimary as n,bootstrap as t};
12
+ //# sourceMappingURL=bootstrap-DJmYYyw7.mjs.map