@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.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/api-surface.md +283 -0
- package/docs/about.md +676 -0
- package/docs/admin/actions.md +183 -0
- package/docs/admin/auth.md +89 -0
- package/docs/admin/dashboard.md +155 -0
- package/docs/admin/extending-ui.md +229 -0
- package/docs/admin/extending.md +205 -0
- package/docs/admin/forms.md +155 -0
- package/docs/admin/index.md +144 -0
- package/docs/admin/operations.md +224 -0
- package/docs/admin/references.md +174 -0
- package/docs/admin/resources.md +132 -0
- package/docs/admin/structure.md +158 -0
- package/docs/admin/tables.md +185 -0
- package/docs/admin/testing.md +138 -0
- package/docs/ai.md +373 -0
- package/docs/application.md +435 -0
- package/docs/arch.md +263 -0
- package/docs/assets.md +755 -0
- package/docs/audit.md +367 -0
- package/docs/authentication.md +1000 -0
- package/docs/authorization.md +529 -0
- package/docs/broadcasting/channels.md +170 -0
- package/docs/broadcasting/client.md +108 -0
- package/docs/broadcasting/events.md +190 -0
- package/docs/broadcasting/index.md +163 -0
- package/docs/broadcasting/references.md +84 -0
- package/docs/broadcasting/testing.md +132 -0
- package/docs/cache.md +387 -0
- package/docs/carbon.md +830 -0
- package/docs/changelog.md +465 -0
- package/docs/client/auth.md +113 -0
- package/docs/client/errors.md +139 -0
- package/docs/client/files.md +118 -0
- package/docs/client/index.md +128 -0
- package/docs/client/references.md +58 -0
- package/docs/client/requests.md +131 -0
- package/docs/client/resilience.md +141 -0
- package/docs/client/testing.md +146 -0
- package/docs/commands.md +450 -0
- package/docs/components.md +2427 -0
- package/docs/config-system.md +258 -0
- package/docs/container.md +596 -0
- package/docs/context.md +579 -0
- package/docs/contributing.md +164 -0
- package/docs/controllers.md +354 -0
- package/docs/conventions.md +466 -0
- package/docs/cookies.md +101 -0
- package/docs/csrf.md +206 -0
- package/docs/database.md +476 -0
- package/docs/deployment.md +434 -0
- package/docs/devtools.md +1013 -0
- package/docs/email-verification.md +320 -0
- package/docs/encryption.md +380 -0
- package/docs/errors.md +317 -0
- package/docs/events.md +420 -0
- package/docs/flow/components.md +846 -0
- package/docs/flow/decorators.md +458 -0
- package/docs/flow/events.md +420 -0
- package/docs/flow/forms.md +551 -0
- package/docs/flow/index.md +568 -0
- package/docs/flow/layouts.md +793 -0
- package/docs/flow/lifecycle.md +336 -0
- package/docs/flow/pagination.md +402 -0
- package/docs/flow/performance.md +262 -0
- package/docs/flow/references.md +317 -0
- package/docs/flow/routing.md +550 -0
- package/docs/flow/testing.md +447 -0
- package/docs/getting-started.md +286 -0
- package/docs/health.md +269 -0
- package/docs/helpers.md +423 -0
- package/docs/i18n.md +414 -0
- package/docs/index.md +65 -0
- package/docs/inertia/build.md +95 -0
- package/docs/inertia/devtools.md +197 -0
- package/docs/inertia/index.md +267 -0
- package/docs/inertia/middleware.md +81 -0
- package/docs/inertia/props.md +507 -0
- package/docs/inertia/references.md +144 -0
- package/docs/inertia/rendering.md +189 -0
- package/docs/inertia/ssr.md +102 -0
- package/docs/inspirations.md +257 -0
- package/docs/lifecycle.md +331 -0
- package/docs/lock.md +454 -0
- package/docs/logger.md +517 -0
- package/docs/media.md +582 -0
- package/docs/middleware.md +509 -0
- package/docs/migrations.md +529 -0
- package/docs/monitor.md +377 -0
- package/docs/notifications.md +848 -0
- package/docs/orm/casts.md +365 -0
- package/docs/orm/factories.md +231 -0
- package/docs/orm/index.md +663 -0
- package/docs/orm/lifecycle.md +421 -0
- package/docs/orm/queries.md +692 -0
- package/docs/orm/relationships.md +440 -0
- package/docs/orm/serialization.md +251 -0
- package/docs/package-development.md +536 -0
- package/docs/pagination.md +340 -0
- package/docs/password-reset.md +335 -0
- package/docs/providers.md +447 -0
- package/docs/query-builder.md +465 -0
- package/docs/queue.md +549 -0
- package/docs/rate-limiting.md +253 -0
- package/docs/responses.md +336 -0
- package/docs/roles-and-2fa.md +418 -0
- package/docs/routing.md +1008 -0
- package/docs/scaffolding.md +335 -0
- package/docs/scheduler.md +533 -0
- package/docs/seeding.md +229 -0
- package/docs/session.md +451 -0
- package/docs/social.md +524 -0
- package/docs/storage.md +622 -0
- package/docs/structure.md +230 -0
- package/docs/support-policy.md +114 -0
- package/docs/telemetry.md +505 -0
- package/docs/tenancy.md +608 -0
- package/docs/testing/browser.md +153 -0
- package/docs/testing/console.md +130 -0
- package/docs/testing/database.md +238 -0
- package/docs/testing/flow-browser.md +216 -0
- package/docs/testing/http.md +441 -0
- package/docs/testing/index.md +240 -0
- package/docs/testing/mocking.md +410 -0
- package/docs/upgrade.md +192 -0
- package/docs/validator.md +549 -0
- package/docs/view.md +453 -0
- package/package.json +60 -0
- package/src/bin/mcp.ts +73 -0
- package/src/config.ts +63 -0
- package/src/errors.ts +27 -0
- package/src/index.ts +56 -0
- package/src/install/ArchInstallCommand.ts +189 -0
- package/src/install/detect.ts +98 -0
- package/src/install/guidelines.ts +290 -0
- package/src/install/markers.ts +77 -0
- package/src/install/mcpConfig.ts +81 -0
- package/src/mcp/index.ts +27 -0
- package/src/mcp/jsonrpc.ts +133 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/stdio.ts +111 -0
- package/src/mcp/types.ts +146 -0
- package/src/probe/ArchProbeCommand.ts +48 -0
- package/src/probe/sentinel.ts +13 -0
- package/src/probe/topics.ts +291 -0
- package/src/provider/ArchProvider.ts +101 -0
- package/src/tools/_probe.ts +164 -0
- package/src/tools/apiSurface.ts +241 -0
- package/src/tools/appInfo.ts +88 -0
- package/src/tools/baselines.ts +210 -0
- package/src/tools/context.ts +37 -0
- package/src/tools/doctor.ts +93 -0
- package/src/tools/index.ts +50 -0
- package/src/tools/logs.ts +265 -0
- package/src/tools/routes.ts +133 -0
- package/src/tools/schema.ts +119 -0
- package/src/tools/searchDocs.ts +345 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Service Providers
|
|
3
|
+
description: Register a feature's bindings and hook into the application boot and shutdown sequence.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Service Providers
|
|
7
|
+
|
|
8
|
+
A service provider is a **feature's setup script**. When the app boots, each
|
|
9
|
+
provider gets a turn to register its bindings into the container, wire up the
|
|
10
|
+
things that feature needs, and (optionally) hook into later stages of startup and
|
|
11
|
+
shutdown. Every framework feature — ORM, sessions, mail, queues — ships as a
|
|
12
|
+
provider, and your own app can add as many as it needs.
|
|
13
|
+
|
|
14
|
+
The mental model: the [container](/docs/container) holds the _services_, the
|
|
15
|
+
[Application](/docs/application) runs the _boot sequence_, and a provider is how a
|
|
16
|
+
feature plugs into that sequence. The two methods you'll use most are
|
|
17
|
+
`onRegister()` (bind things) and `onBooted()` (use things) — the rest of this page
|
|
18
|
+
is mostly about those two and when each runs.
|
|
19
|
+
|
|
20
|
+
> **Note** — Do you even need a provider? Often not. Registering a single binding
|
|
21
|
+
> or two doesn't justify a whole class — use the bootstrap [`app.bind()`](/docs/application#registering-services-without-a-provider)
|
|
22
|
+
> callback or the [`app/services` convention](/docs/container#the-appservices-convention)
|
|
23
|
+
> instead. Reach for a provider when there's real _bootstrapping_ involved:
|
|
24
|
+
> lifecycle hooks, config-driven wiring, middleware registration, or a feature
|
|
25
|
+
> that spans several bindings.
|
|
26
|
+
|
|
27
|
+
## Mental model
|
|
28
|
+
|
|
29
|
+
A provider extends `ServiceProvider` from `zerotal` and overrides only the
|
|
30
|
+
hooks it cares about. The framework drives every provider through the same boot
|
|
31
|
+
sequence in list order:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
register all providers onRegister() ← bind, no resolving yet
|
|
35
|
+
│
|
|
36
|
+
▼
|
|
37
|
+
boot (sequential) onBooting() ← prepare a service a later provider needs
|
|
38
|
+
│
|
|
39
|
+
▼
|
|
40
|
+
boot (parallel) onBooted() ← resolve and use, including from other providers
|
|
41
|
+
│
|
|
42
|
+
▼
|
|
43
|
+
server binding onStarting / onStarted
|
|
44
|
+
│
|
|
45
|
+
▼
|
|
46
|
+
graceful shutdown onStopping / onStopped
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// in a provider
|
|
51
|
+
import { ServiceProvider } from "zerotal";
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Anatomy of a provider
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// app/providers/AppServiceProvider.ts
|
|
58
|
+
import { ServiceProvider } from "zerotal";
|
|
59
|
+
import { PaymentGateway } from "../services/PaymentGateway.ts";
|
|
60
|
+
import { StripeGateway } from "../services/StripeGateway.ts";
|
|
61
|
+
|
|
62
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
63
|
+
/**
|
|
64
|
+
* Register bindings into the container.
|
|
65
|
+
* Called synchronously before boot. No resolved bindings are available yet.
|
|
66
|
+
*/
|
|
67
|
+
onRegister(): void {
|
|
68
|
+
this.app.container.singleton(
|
|
69
|
+
PaymentGateway,
|
|
70
|
+
() => new StripeGateway({ key: Bun.env.STRIPE_KEY! }),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Called after all providers have registered.
|
|
76
|
+
* Safe to resolve bindings from other providers here.
|
|
77
|
+
*/
|
|
78
|
+
async onBooted(): Promise<void> {
|
|
79
|
+
const gw = await this.app.container.make(PaymentGateway);
|
|
80
|
+
await gw.ping(); // verify connectivity at startup
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## How it works
|
|
86
|
+
|
|
87
|
+
A provider boots in three ordered steps. The golden rule lives here: **bind in
|
|
88
|
+
`onRegister()`, resolve in `onBooted()`.** Trying to resolve a binding too early
|
|
89
|
+
is the most common provider mistake.
|
|
90
|
+
|
|
91
|
+
| Hook | Runs | You should… |
|
|
92
|
+
| -------------- | --------------------------------- | -------------------------------------------------- |
|
|
93
|
+
| `onRegister()` | sync, before anything is resolved | _bind_ services — nothing is resolvable yet |
|
|
94
|
+
| `onBooting()` | async, sequentially in list order | prepare a service a _later_ provider needs |
|
|
95
|
+
| `onBooted()` | async, in parallel once all bound | _resolve_ services, including from other providers |
|
|
96
|
+
|
|
97
|
+
Why the order matters: during `onRegister()` no binding has been resolved yet, so
|
|
98
|
+
you can only register. By `onBooted()` every provider has registered, so resolving
|
|
99
|
+
across providers is safe. `onBooting()` sits between them for the rarer case where
|
|
100
|
+
one provider must be fully prepared before another even registers. There are also
|
|
101
|
+
`onStarting`/`onStarted` (around the server binding) and `onStopping`/`onStopped`
|
|
102
|
+
(graceful shutdown), plus per-request hooks (`onRequestReceived`,
|
|
103
|
+
`onRequestProcessed`, `onResponseSent`) — see [the full phase list](/docs/lifecycle#provider-lifecycle-hooks).
|
|
104
|
+
|
|
105
|
+
## Registering providers
|
|
106
|
+
|
|
107
|
+
List providers in `bootstrap/providers.ts`. Order matters — a provider can
|
|
108
|
+
only resolve bindings registered by providers that appear earlier in the list:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// bootstrap/providers.ts
|
|
112
|
+
import { DatabaseProvider } from "@zerotal/orm";
|
|
113
|
+
import { CacheProvider } from "@zerotal/cache";
|
|
114
|
+
import { AppServiceProvider } from "../app/providers/AppServiceProvider.ts";
|
|
115
|
+
|
|
116
|
+
const providers = [
|
|
117
|
+
DatabaseProvider, // registers DB bindings
|
|
118
|
+
CacheProvider, // may depend on DB
|
|
119
|
+
AppServiceProvider, // can use both DB and Cache
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
export default providers;
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
> **Note** — `app/providers/*` are auto-discovered. Your own providers (e.g. `AppServiceProvider`) don't
|
|
126
|
+
> need to be listed — any `ServiceProvider` under `app/providers/` is registered automatically and
|
|
127
|
+
> runs its full lifecycle, appended after the explicitly-listed providers (so framework providers
|
|
128
|
+
> boot first). List a provider explicitly only to control its order relative to others. See
|
|
129
|
+
> [Conventions](/docs/conventions#providers-appproviders).
|
|
130
|
+
|
|
131
|
+
## Declaring dependencies
|
|
132
|
+
|
|
133
|
+
Listing providers in the right order works, but it's fragile — it asks every app to
|
|
134
|
+
remember that, say, `AdminProvider` needs `FlowProvider`'s `Router.flow()` macro to
|
|
135
|
+
exist first. Instead, a provider can declare what it needs on the class itself with
|
|
136
|
+
`static dependsOn`, and the framework pulls that dependency in and boots it first:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// packages/admin/src/provider/AdminProvider.ts
|
|
140
|
+
import { ServiceProvider } from "zerotal";
|
|
141
|
+
import { FlowProvider } from "@zerotal/flow";
|
|
142
|
+
|
|
143
|
+
export class AdminProvider extends ServiceProvider {
|
|
144
|
+
static dependsOn = [FlowProvider];
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`dependsOn` holds **provider classes** — real imports, not magic strings — so it's
|
|
149
|
+
type-checked and survives renames. It does two jobs at once:
|
|
150
|
+
|
|
151
|
+
- **Pulls the dependency in.** You register only the feature you want —
|
|
152
|
+
`AdminProvider` — and `FlowProvider` comes along automatically, even when it
|
|
153
|
+
isn't listed in `bootstrap/providers.ts`. You list the _features_ your app uses,
|
|
154
|
+
not the plumbing they each need.
|
|
155
|
+
- **Orders it first.** A dependency always boots before the provider that declared
|
|
156
|
+
it, regardless of where either sits in the list.
|
|
157
|
+
|
|
158
|
+
So an app that uses the admin panel only needs the panel itself:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// bootstrap/providers.ts — FlowProvider arrives via AdminProvider.dependsOn
|
|
162
|
+
const providers = [AdminProvider];
|
|
163
|
+
|
|
164
|
+
export default providers;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Registration is idempotent
|
|
168
|
+
|
|
169
|
+
You can list a provider explicitly _and_ have it pulled in through someone's
|
|
170
|
+
`dependsOn` — it still boots exactly once. Registration is idempotent by class
|
|
171
|
+
identity, and the first registration keeps its position, so explicit and automatic
|
|
172
|
+
registration safely overlap. Listing `FlowProvider` yourself while `AdminProvider`
|
|
173
|
+
also depends on it is harmless, not a double-boot.
|
|
174
|
+
|
|
175
|
+
### The priority tiebreak
|
|
176
|
+
|
|
177
|
+
`dependsOn` orders a provider relative to the ones it names. Providers with _no_
|
|
178
|
+
dependency relationship fall back to `static priority` (lower boots earlier;
|
|
179
|
+
defaults to `0`), then to registration order. It's a coarse knob — useful for a
|
|
180
|
+
framework-core provider that should generally boot ahead of everything else,
|
|
181
|
+
without every other provider having to name it explicitly:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
export class CoreProvider extends ServiceProvider {
|
|
185
|
+
static priority = -100; // boots before ordinary (priority 0) providers
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Reach for `dependsOn` to express a real, specific dependency; reach for `priority`
|
|
190
|
+
only for broad "this should come early/late" ordering.
|
|
191
|
+
|
|
192
|
+
### Cycles and environments
|
|
193
|
+
|
|
194
|
+
The dependency graph is resolved once, at boot, with a few guarantees worth knowing:
|
|
195
|
+
|
|
196
|
+
- **Transitive.** A dependency's own `dependsOn` is pulled in too, recursively, and
|
|
197
|
+
de-duplicated.
|
|
198
|
+
- **Environment-aware.** A dependency excluded by its own `static environments`
|
|
199
|
+
isn't dragged in where it doesn't belong — a `web`-only provider won't be pulled
|
|
200
|
+
into a `console` boot just because a console-active provider lists it.
|
|
201
|
+
- **Cycle-checked.** A circular `dependsOn` throws at boot with the offending path
|
|
202
|
+
(`Circular provider dependency: A → B → A`), failing fast rather than booting in a
|
|
203
|
+
surprising order.
|
|
204
|
+
|
|
205
|
+
> **Note** — your explicit list is never reshuffled. `dependsOn`/`priority` order the
|
|
206
|
+
> providers that are _pulled in_; the providers you list by hand in
|
|
207
|
+
> `bootstrap/providers.ts` keep their authored order, with dependencies slotted in
|
|
208
|
+
> ahead of the providers that need them.
|
|
209
|
+
|
|
210
|
+
## onRegister — bind things
|
|
211
|
+
|
|
212
|
+
Register container bindings, named middleware groups, and router macros. This hook
|
|
213
|
+
is **synchronous** — no `await` — and runs before any binding is resolved, so
|
|
214
|
+
treat it as pure wiring. The factory closures you pass don't run yet; they run
|
|
215
|
+
_later_, when the binding is first resolved.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// inside a ServiceProvider
|
|
219
|
+
onRegister(): void {
|
|
220
|
+
// Singleton — one shared instance per app lifetime
|
|
221
|
+
this.app.container.singleton(AnalyticsService, () =>
|
|
222
|
+
new AnalyticsService({ apiKey: Bun.env.ANALYTICS_KEY! })
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Per-request scoped binding. The factory may be async — `make()` returns a
|
|
226
|
+
// promise, so await it inside the factory.
|
|
227
|
+
this.app.container.scoped(CartService, async (c) =>
|
|
228
|
+
new CartService(await c.make(DB))
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// A named middleware group usable in Router.group({ middleware: ['api'] })
|
|
232
|
+
Router.middlewareGroup('api', [ThrottleMiddleware, JsonMiddleware]);
|
|
233
|
+
|
|
234
|
+
// Extend the Router with a custom route method (macro)
|
|
235
|
+
Router.macro('webhook', webhookRoute);
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## onBooting — prepare in order
|
|
240
|
+
|
|
241
|
+
`onBooting()` runs **sequentially**, in provider-list order, between register and
|
|
242
|
+
booted. Reach for it only when one provider must finish preparing before the next
|
|
243
|
+
one even registers — most providers skip it entirely. It's async, so you can
|
|
244
|
+
`await`:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
// inside a ServiceProvider
|
|
248
|
+
async onBooting(): Promise<void> {
|
|
249
|
+
// e.g. open a connection pool that a provider listed after this one
|
|
250
|
+
// expects to already exist when its own onRegister() runs.
|
|
251
|
+
const db = await this.app.container.make(DB);
|
|
252
|
+
await db.connect();
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## onBooted — use things
|
|
257
|
+
|
|
258
|
+
By `onBooted()` every provider has registered, so this is the safe place to
|
|
259
|
+
_resolve_ bindings (including from other providers), start background work, and
|
|
260
|
+
register event listeners. It runs in parallel across all providers:
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
// inside a ServiceProvider
|
|
264
|
+
async onBooted(): Promise<void> {
|
|
265
|
+
// Resolve a binding from another provider
|
|
266
|
+
const db = await this.app.container.make(DB);
|
|
267
|
+
|
|
268
|
+
// Register global event listeners
|
|
269
|
+
Events.on(UserRegistered, async (event) => {
|
|
270
|
+
await sendWelcomeEmail(event.user);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Start a background polling loop (only in the 'web' runtime)
|
|
274
|
+
if (this.app.environment === 'web') {
|
|
275
|
+
startHealthMonitor();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## this.app API
|
|
281
|
+
|
|
282
|
+
`this.app` is the [Application](/docs/application) instance. The members you'll
|
|
283
|
+
use from inside a provider:
|
|
284
|
+
|
|
285
|
+
| Property / Method | Description |
|
|
286
|
+
| ----------------------------- | ------------------------------------------------------------ |
|
|
287
|
+
| `this.app.container` | The IoC [container](/docs/container) — bind and resolve here |
|
|
288
|
+
| `this.app.environment` | `'web' \| 'console' \| 'worker' \| 'test' \| 'repl'` |
|
|
289
|
+
| `this.app.useOnce(mw)` | Add a middleware to the global pipeline exactly once |
|
|
290
|
+
| `this.app.registerConcern(d)` | Contribute an auto-discovery concern (see below) |
|
|
291
|
+
|
|
292
|
+
To read configuration, resolve it from the container or use the `Config` facade
|
|
293
|
+
— `this.app.container.makeSync('config').get('app.name')`, or simply
|
|
294
|
+
`Config.get('app.name')` once booted.
|
|
295
|
+
|
|
296
|
+
### Contributing a convention
|
|
297
|
+
|
|
298
|
+
A provider can add its own auto-discovered directory by registering a concern descriptor in
|
|
299
|
+
`onRegister()`/`onBooting()`. The framework scans the directory at boot and calls `register()`
|
|
300
|
+
for each file's exports:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// inside a ServiceProvider
|
|
304
|
+
onRegister(): void {
|
|
305
|
+
this.app.registerConcern({
|
|
306
|
+
name: "validators",
|
|
307
|
+
order: 60, // models=10, observers=20, policies=30, listeners=40
|
|
308
|
+
dir: "app/validators",
|
|
309
|
+
register(mod, ctx) {
|
|
310
|
+
for (const exported of Object.values(mod)) {
|
|
311
|
+
/* register exported as needed */
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
A `run(ctx)` hook (without `dir`) defines a one-shot step instead of a directory scan. See
|
|
319
|
+
[Conventions](/docs/conventions#custom-concerns).
|
|
320
|
+
|
|
321
|
+
## Deferred providers
|
|
322
|
+
|
|
323
|
+
When a provider registers something that's rarely needed at boot — a search
|
|
324
|
+
engine, a payment SDK — you can **defer** it so it doesn't run until one of its
|
|
325
|
+
bindings is first resolved. This keeps cold-start fast.
|
|
326
|
+
|
|
327
|
+
Deferral takes two parts. First, the provider declares which tokens it provides:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// app/providers/SearchProvider.ts
|
|
331
|
+
import { ServiceProvider } from "zerotal";
|
|
332
|
+
|
|
333
|
+
export class SearchProvider extends ServiceProvider {
|
|
334
|
+
static provides = ["search.engine"] as const;
|
|
335
|
+
|
|
336
|
+
onRegister(): void {
|
|
337
|
+
this.app.container.singleton(
|
|
338
|
+
"search.engine",
|
|
339
|
+
() => new MeilisearchEngine({ host: Bun.env.MEILISEARCH_URL! }),
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Second — and this is the part that actually defers it — register it with
|
|
346
|
+
`app.defer()` instead of the normal providers array. `static provides` on its own
|
|
347
|
+
is just metadata; it's `defer()` that wires the lazy boot:
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
// bootstrap/app.ts
|
|
351
|
+
const app = Application.create({ providers });
|
|
352
|
+
app.defer([SearchProvider]); // array form reads each provider's `static provides`
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
The first `make("search.engine")` runs the provider's full
|
|
356
|
+
`onRegister → onBooting → onBooted` sequence, then resolves the binding.
|
|
357
|
+
|
|
358
|
+
> **Warning** — A deferred provider must _not_ also sit in the eager providers list
|
|
359
|
+
> or in `app/providers/*` auto-discovery, or it would boot at startup anyway,
|
|
360
|
+
> defeating the deferral. See [The Application](/docs/application#deferred-providers)
|
|
361
|
+
> for the `defer()` overloads.
|
|
362
|
+
|
|
363
|
+
## Environment-specific registration
|
|
364
|
+
|
|
365
|
+
A provider runs in every runtime by default. Branch on `this.app.environment` to
|
|
366
|
+
bind a different implementation per environment — a fake mailer under `test`, the
|
|
367
|
+
real one everywhere else:
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
// inside a ServiceProvider
|
|
371
|
+
onRegister(): void {
|
|
372
|
+
if (this.app.environment === 'test') {
|
|
373
|
+
this.app.container.singleton(Mailer, () => new FakeMailer());
|
|
374
|
+
} else {
|
|
375
|
+
this.app.container.singleton(Mailer, () => new SmtpMailer());
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
To skip a provider entirely outside certain runtimes, set `static environments`
|
|
381
|
+
on the class (e.g. `static environments = ['web']`) — the Application filters it
|
|
382
|
+
out before it's ever instantiated.
|
|
383
|
+
|
|
384
|
+
## Auto-registering middleware
|
|
385
|
+
|
|
386
|
+
Providers can push middleware into the global pipeline via `this.app.useOnce()` —
|
|
387
|
+
the framework guarantees it's added exactly once, even if the same middleware is
|
|
388
|
+
registered by several providers:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
// inside a ServiceProvider
|
|
392
|
+
onBooting(): Promise<void> {
|
|
393
|
+
this.app.useOnce(SessionMiddleware);
|
|
394
|
+
this.app.useOnce(AuthMiddleware);
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
This is how framework packages (e.g. `@zerotal/session`) inject their middleware
|
|
399
|
+
transparently, without the app developer needing to add it manually.
|
|
400
|
+
|
|
401
|
+
## Which hook should I use?
|
|
402
|
+
|
|
403
|
+
- **`onRegister`** — for _binding_ services into the container, registering
|
|
404
|
+
middleware groups, and router macros. Nothing is resolvable yet, so never call
|
|
405
|
+
`make()` here.
|
|
406
|
+
- **`onBooting`** — only when one provider must be fully prepared before a later
|
|
407
|
+
provider registers, or to auto-register middleware via `useOnce()`. Most
|
|
408
|
+
providers skip it.
|
|
409
|
+
- **`onBooted`** — for _resolving_ services (including across providers), starting
|
|
410
|
+
background work, and registering event listeners.
|
|
411
|
+
- **No provider at all** — for one or two bindings with no lifecycle needs, prefer
|
|
412
|
+
the bootstrap [`app.bind()`](/docs/application#registering-services-without-a-provider)
|
|
413
|
+
callback or the [`app/services` convention](/docs/container#the-appservices-convention).
|
|
414
|
+
|
|
415
|
+
## References
|
|
416
|
+
|
|
417
|
+
The lifecycle hooks a provider may override, in boot order:
|
|
418
|
+
|
|
419
|
+
| Hook | Signature | When it runs |
|
|
420
|
+
| -------------------- | ----------------------------------------------------- | ----------------------------------------------------------------- |
|
|
421
|
+
| `onRegister` | `onRegister(): void` | Sync, before any binding is resolved. Bind here. |
|
|
422
|
+
| `onBooting` | `onBooting(): Promise<void>` | Sequentially in list order, after all providers registered. |
|
|
423
|
+
| `onBooted` | `onBooted(): Promise<void>` | In parallel once every provider has booted. Resolve here. |
|
|
424
|
+
| `onStarting` | `onStarting(): Promise<void>` | Just before the app starts accepting work (e.g. the HTTP server). |
|
|
425
|
+
| `onStarted` | `onStarted(): Promise<void>` | Once the app has started. |
|
|
426
|
+
| `onStopping` | `onStopping(): Promise<void>` | When graceful shutdown begins. Release resources here. |
|
|
427
|
+
| `onStopped` | `onStopped(): Promise<void>` | Once shutdown is complete. |
|
|
428
|
+
| `onRequestReceived` | `onRequestReceived(ctx: HttpContext): Promise<void>` | Before the middleware pipeline runs, per request. |
|
|
429
|
+
| `onRequestProcessed` | `onRequestProcessed(ctx: HttpContext): Promise<void>` | After the pipeline completes and `ctx.response` is set. |
|
|
430
|
+
| `onResponseSent` | `onResponseSent(ctx: HttpContext): Promise<void>` | After the response has been sent to the client. |
|
|
431
|
+
| `replContext` | `replContext(): Record<string, unknown>` | Returns variables to expose in `bun zt repl`. |
|
|
432
|
+
|
|
433
|
+
Static members on the provider class:
|
|
434
|
+
|
|
435
|
+
| Member | Type | Description |
|
|
436
|
+
| --------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
437
|
+
| `static environments` | `AppEnvironment[]` | Runtimes the provider participates in. Defaults to all five. |
|
|
438
|
+
| `static provides` | `readonly (keyof ContainerBindings)[]` | Tokens this provider registers. Required for the array form of `app.defer([Provider])`. |
|
|
439
|
+
| `static dependsOn` | `ProviderClass[]` | Providers this one needs — pulled in automatically and booted first. See [Declaring dependencies](/docs/providers#declaring-dependencies). |
|
|
440
|
+
| `static priority` | `number` | Boot-order tiebreak among providers with no `dependsOn` relationship. Lower boots earlier; default `0`. |
|
|
441
|
+
|
|
442
|
+
## Next steps
|
|
443
|
+
|
|
444
|
+
- [Container](/docs/container) — the IoC container providers bind into.
|
|
445
|
+
- [Lifecycle](/docs/lifecycle) — when `onRegister` and `onBooted` run during boot.
|
|
446
|
+
- [Conventions](/docs/conventions) — auto-discovery of providers and concerns.
|
|
447
|
+
- [Application](/docs/application) — the `this.app` instance and its environment.
|