@zerotal/arch 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,331 @@
1
+ ---
2
+ title: Request Lifecycle
3
+ description: Understand the boot and per-request lifecycles so your logic runs in the right place.
4
+ ---
5
+
6
+ # Request Lifecycle
7
+
8
+ A Zerotal app has **two lifecycles** — one that runs once at startup and one that
9
+ runs for every request. Keeping them straight is the key to putting logic in the
10
+ right place.
11
+
12
+ - **The boot lifecycle** runs **once**, when the server starts. It wires
13
+ everything together — binds services, loads routes, scans your `app/*`
14
+ directories — and then opens the socket. One-time setup belongs here, in a
15
+ provider hook.
16
+ - **The request lifecycle** runs **for every incoming request**. It opens a
17
+ per-request scope, walks the middleware pipeline to your controller, builds a
18
+ response, and tears the scope back down. Per-request work belongs here, in
19
+ middleware, a controller, or an after-response hook.
20
+
21
+ If you only remember one thing: _boot is for the app, request is for the
22
+ visitor._
23
+
24
+ ## Mental model
25
+
26
+ Two clocks tick at different rates. The boot clock ticks once — everything it
27
+ sets up (container bindings, routes, the compiled route table) is shared by every
28
+ request that follows. The request clock ticks on each visit and gets its own
29
+ isolated scope, so concurrent requests never share state.
30
+
31
+ ```
32
+ boot clock ──tick── (server starts: bind, load routes, open socket)
33
+
34
+ request clock ───────────┼── tick (GET /a) → scope A → response → flush A
35
+ ├── tick (GET /b) → scope B → response → flush B
36
+ └── tick (POST /c) → scope C → response → flush C
37
+ ```
38
+
39
+ ## Boot lifecycle
40
+
41
+ Booting starts from the two files you own — `bootstrap/app.ts` (where you
42
+ configure the app, declaratively) and the managed `zt.ts` entry point (which
43
+ imports it and calls `start()`). From there `Application.boot()` runs a fixed
44
+ sequence, **once**, and is idempotent (a second call does nothing):
45
+
46
+ ```text
47
+ # boot sequence (overview)
48
+ bootstrap/app.ts ← you configure the app (declarative)
49
+ Application.create({ providers })
50
+ .bind((container) => …) ← optional: register container bindings
51
+ .routing({ … }) ← declare explicit route files
52
+ .fileBasedRouting({ … }) ← declare route directories
53
+ .use([ …middleware ]) ← register global middleware
54
+ └─ exports `app`
55
+
56
+
57
+ zt.ts (managed) ← imports bootstrap/app.ts, then: app.start(port)
58
+
59
+
60
+ app.boot() ── runs once, idempotent ───────────────────────────────────
61
+ 1. bind core singletons `config`, `events`
62
+ 2. load config scan `config/*.ts` (unless preloaded)
63
+ 3. discover providers scan `app/providers/*`
64
+ 4. run app.bind() callbacks your bootstrap bindings
65
+ 5. onRegister() each, sync providers bind their services
66
+ 6. onBooting() each, in order providers prepare; may use earlier ones
67
+ 7. onBooted() all, in parallel every binding is now resolvable
68
+ 8. discover middleware scan `app/middleware/*`
69
+ 9. load routes routing() + fileBasedRouting() files run
70
+ 10. convention phase scan app/{models, observers, policies, listeners,
71
+ events, jobs, services, validators}; serve public/
72
+
73
+
74
+ app.start() ── continues ────────────────────────────────────────────────
75
+ 11. onStarting() each last chance before the socket opens
76
+ 12. Bun.serve() server binds and starts listening
77
+ 13. onStarted() each health endpoint, PID file, signal handlers
78
+ ```
79
+
80
+ The three provider phases (steps 5–7) are the part you'll touch most. They're
81
+ **ordered for a reason**: `onRegister()` only _binds_ (nothing is resolved yet),
82
+ `onBooting()` runs **sequentially** so a later provider can depend on an earlier
83
+ one, and `onBooted()` runs in **parallel** once every binding exists — so it's the
84
+ safe place to resolve services that depend on other providers. See
85
+ [Provider lifecycle hooks](#provider-lifecycle-hooks) below.
86
+
87
+ > **Tip** — _Bind in `onRegister()`, resolve in `onBooted()`_ is the rule that
88
+ > avoids 90% of ordering bugs.
89
+
90
+ The convention phase (step 10) scans the `app/*` directories and registers what it
91
+ finds — models, observers, policies, listeners, jobs, services, validators.
92
+ Providers declare which directories get scanned via `this.app.registerConcern(...)`.
93
+ See [Conventions](/docs/conventions).
94
+
95
+ ## Request lifecycle
96
+
97
+ Now the per-request path. Every request gets its **own isolated scope** — created
98
+ when it arrives, flushed when it leaves — so concurrent requests never share
99
+ state. The route table was already compiled at boot, so matching is a fast lookup,
100
+ not a re-scan:
101
+
102
+ ```text
103
+ # per-request flow
104
+ Bun.serve() receives Request
105
+
106
+
107
+ compiled route table lookup
108
+ → match path + method → RouteDefinition
109
+
110
+ ├── no match → 404 NotFoundError → ExceptionHandler.render()
111
+
112
+
113
+ createRouteHandler(definition, container)
114
+
115
+
116
+ ScopedResolver created ← request-scoped DI scope
117
+ HttpContext created ← ctx.requestId, ctx.startedAt, ctx.url, …
118
+ RequestContext.run(ctx, fn) ← AsyncLocalStorage stores ctx for this async tree
119
+
120
+
121
+ Model bindings resolved ← the model's resolver / .bind() runs
122
+ ctx._models populated
123
+
124
+
125
+ Pipeline runs (middleware chain):
126
+ [global middleware] → [group middleware] → [route middleware] → controller action
127
+
128
+ ├── any middleware can short-circuit by returning a Response (or setting ctx.response)
129
+
130
+
131
+ Controller action executes
132
+ → sets ctx.response (via ctx.json(), ctx.view(), ctx.redirect(), etc.)
133
+
134
+
135
+ Pipeline unwinds (finally blocks in middleware run here — e.g. SessionMiddleware saves)
136
+
137
+
138
+ ctx.response returned to Bun.serve()
139
+
140
+
141
+ afterResponse callbacks fire ← ctx.afterResponse(() => sendEmail())
142
+
143
+
144
+ ScopedResolver.flush() ← request-scoped bindings disposed
145
+ ```
146
+
147
+ ## Middleware execution order
148
+
149
+ Middleware nests: the outermost layer runs first on the way in and last on the way
150
+ out. The list below reads top-to-bottom as the order requests enter, then unwinds
151
+ in reverse as responses leave:
152
+
153
+ ```text
154
+ # nesting order (outer → inner)
155
+ Global (app.use)
156
+ └── Provider auto-registered (SessionMiddleware, AuthMiddleware, …)
157
+ └── Switch middleware (withCors, withThrottle, withSecureHeaders)
158
+ └── Group middleware (Router.group({ middleware: [...] }))
159
+ └── Route middleware (Router.get('/...', C, 'a', [M]))
160
+ └── Controller action
161
+ ┌── (unwind)
162
+ ┌── (unwind)
163
+ ┌── (unwind — session saved here)
164
+ ┌── (unwind)
165
+ ```
166
+
167
+ Middleware wraps the next step — `await next()` is where the inner layers
168
+ run. Code before `next` runs on the way in; code after runs on the way out.
169
+ `next()` resolves to the downstream `Response`.
170
+
171
+ ```typescript
172
+ // app/middleware/TimingMiddleware.ts
173
+ import type { HttpContext } from "zerotal";
174
+ import type { Pipe, NextFn } from "zerotal";
175
+
176
+ export class TimingMiddleware implements Pipe<HttpContext> {
177
+ async handle(ctx: HttpContext, next: NextFn): Promise<Response | void> {
178
+ const start = performance.now();
179
+ const response = await next(); // ← inner layers run here
180
+ console.log(`${ctx.path()} took ${Math.round(performance.now() - start)}ms`);
181
+ return response;
182
+ }
183
+ }
184
+ ```
185
+
186
+ ## Exception handling
187
+
188
+ If any middleware or controller throws, the pipeline catches the error and calls
189
+ the exception handler — first to report it, then to turn it into a `Response`:
190
+
191
+ ```text
192
+ # on an unhandled throw
193
+ ExceptionHandler.report(err, ctx) ← log / Sentry / whatever
194
+ ExceptionHandler.render(err, ctx) → Response
195
+ ```
196
+
197
+ The response is sent to the client, and `afterResponse` callbacks still fire.
198
+
199
+ In production, raw 500 errors show a minimal "Internal Server Error" page.
200
+ In dev, unhandled exceptions show a full stack-trace page. See
201
+ [Error Handling](/docs/errors) for custom handlers.
202
+
203
+ ## AsyncLocalStorage context
204
+
205
+ `RequestContext` stores the request's `HttpContext` in an `AsyncLocalStorage` so
206
+ that any code running inside the async tree of a request — facades, services, ORM
207
+ models — can reach the current context without prop-drilling:
208
+
209
+ ```typescript
210
+ // in a service, anywhere in the async tree
211
+ import { RequestContext } from "zerotal";
212
+
213
+ const ctx = RequestContext.tryGet(); // HttpContext | undefined
214
+ const user = ctx?.user;
215
+ ```
216
+
217
+ > **Note** — Use `RequestContext.tryGet()` (returns `undefined` outside a request)
218
+ > for code that runs in both request and non-request contexts — CLI commands,
219
+ > queue workers, scheduled jobs. Use `RequestContext.get()` when you want it to
220
+ > throw if there is no active request.
221
+
222
+ Facades like `Auth` and `Config` use this internally. You rarely need to access
223
+ `RequestContext` directly.
224
+
225
+ ## Provider lifecycle hooks
226
+
227
+ A provider can hook into any step of the boot and shutdown sequence by overriding
228
+ these methods. The three most-used ones map to boot steps 5–7 above; the rest
229
+ bracket the server starting and stopping.
230
+
231
+ | Hook | When it runs | Typical use |
232
+ | -------------- | --------------------------------------- | --------------------------------------------- |
233
+ | `onRegister()` | Boot step 5 — sync, nothing resolved | `container.singleton()`, `Router.macro()` |
234
+ | `onBooting()` | Boot step 6 — sequential, in order | Prepare a service that a later provider needs |
235
+ | `onBooted()` | Boot step 7 — parallel, all bound | Resolve cross-provider deps, warm singletons |
236
+ | `onStarting()` | Just before the socket opens | Final pre-flight checks |
237
+ | `onStarted()` | After the server is listening | Start background timers / workers |
238
+ | `onStopping()` | Graceful shutdown, reverse order | Release resources, flush buffers |
239
+ | `onStopped()` | After shutdown completes, reverse order | Final cleanup |
240
+
241
+ Providers can also observe **every request** without registering middleware —
242
+ `onRequestReceived()` (before the pipeline runs), `onRequestProcessed()` (after
243
+ the pipeline sets `ctx.response`), and `onResponseSent()` (after the response is
244
+ sent). The full phase reference lives in
245
+ [The Application](/docs/application) and [Service Providers](/docs/providers).
246
+
247
+ ```typescript
248
+ // app/providers/PaymentProvider.ts
249
+ import { ServiceProvider } from "zerotal";
250
+
251
+ export class PaymentProvider extends ServiceProvider {
252
+ onRegister(): void {
253
+ this.app.container.singleton(
254
+ PaymentGateway,
255
+ () => new StripeGateway({ key: Bun.env.STRIPE_KEY! }),
256
+ );
257
+ }
258
+
259
+ async onBooted(): Promise<void> {
260
+ const gw = await this.app.container.make(PaymentGateway);
261
+ gw.setWebhookSecret(Bun.env.STRIPE_WEBHOOK_SECRET!);
262
+ }
263
+ }
264
+ ```
265
+
266
+ > **Warning** — Do not resolve services in `onRegister()`. Nothing is resolvable
267
+ > yet at that phase — only _bind_ there, and resolve in `onBooted()` once every
268
+ > provider has registered its bindings.
269
+
270
+ ## Which hook should I use?
271
+
272
+ - **Registering a binding (singleton, macro, alias)** → `onRegister()`. It runs
273
+ first and only binds; nothing is resolved.
274
+ - **Preparing a service a _later_ provider depends on** → `onBooting()`. It runs
275
+ sequentially in registration order, so earlier providers are already booting.
276
+ - **Resolving a service that spans providers, or warming a singleton** →
277
+ `onBooted()`. Every binding exists by now and these run in parallel.
278
+ - **Opening a socket / starting timers after the server is up** → `onStarted()`.
279
+ - **Releasing resources on shutdown** → `onStopping()` / `onStopped()` (reverse
280
+ order).
281
+ - **Observing each request without owning a middleware slot** →
282
+ `onRequestReceived()` / `onRequestProcessed()` / `onResponseSent()`.
283
+
284
+ ## Reference
285
+
286
+ Boot and shutdown phases in order, and the per-request hook surface.
287
+
288
+ | Phase / hook | Runs | Concurrency |
289
+ | ---------------------- | ---------------------------- | ------------------- |
290
+ | `onRegister()` | Boot — bind services | Sync, sequential |
291
+ | `onBooting()` | Boot — prepare services | Async, sequential |
292
+ | `onBooted()` | Boot — every binding ready | Async, parallel |
293
+ | `onStarting()` | Before `Bun.serve()` binds | Async, parallel |
294
+ | `onStarted()` | After server is listening | Async, parallel |
295
+ | `onStopping()` | Graceful shutdown | Async, reverse |
296
+ | `onStopped()` | After shutdown completes | Async/sync, reverse |
297
+ | `onRequestReceived()` | Per request, before pipeline | Async, parallel |
298
+ | `onRequestProcessed()` | Per request, after pipeline | Async, parallel |
299
+ | `onResponseSent()` | Per request, after response | Async, parallel |
300
+
301
+ ### What booting cost
302
+
303
+ `app.bootDurationMs` is the total. `app.providerReport` is the breakdown — one
304
+ `ProviderReport` per provider, in boot order:
305
+
306
+ ```typescript
307
+ for (const { name, durationMs, bindings } of app.providerReport) {
308
+ console.log(`${name} ${durationMs}ms, bound ${bindings.length}`);
309
+ }
310
+ ```
311
+
312
+ | Field | Is |
313
+ | ------------ | ------------------------------------------------------- |
314
+ | `name` | Provider class name. The array order is the boot order. |
315
+ | `durationMs` | Wall-clock across all three boot hooks. |
316
+ | `bindings` | Container tokens this provider bound, as names. |
317
+
318
+ Boot order is worth reading on its own: it decides who wins a contested binding.
319
+ And because `onBooted()` runs in parallel, these durations **overlap** — they do
320
+ not sum to `bootDurationMs`, and the report says so rather than serialising the
321
+ boot to produce a tidier number.
322
+
323
+ [DevTools](/docs/devtools) draws this as its Providers and Container tabs.
324
+
325
+ ## Next steps
326
+
327
+ - [Providers](/docs/providers) — register and boot services.
328
+ - [Middleware](/docs/middleware) — the request pipeline in depth.
329
+ - [Container](/docs/container) — dependency injection and scopes.
330
+ - [Conventions](/docs/conventions) — auto-registration of app directories.
331
+ - [HttpContext](/docs/context) — the per-request object and its helpers.