@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,596 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Service Container
|
|
3
|
+
description: Register how each object is built once, then resolve it by name with dependencies wired automatically.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Service Container
|
|
7
|
+
|
|
8
|
+
The service container is the part of Zerotal that **builds your objects for you**:
|
|
9
|
+
you describe how to construct something once, then ask for it by name and it wires
|
|
10
|
+
up the dependencies.
|
|
11
|
+
|
|
12
|
+
Most classes need other things to do their job: a repository needs a database
|
|
13
|
+
connection, a mailer needs an API key, a service needs a logger. Wiring all of
|
|
14
|
+
that by hand — `new PostRepository(new CacheManager(config), new Logger())` —
|
|
15
|
+
gets tedious and couples every caller to the exact construction details. The
|
|
16
|
+
container takes that job over: you tell it _how_ to build something once, and from
|
|
17
|
+
then on you just ask for it by name. It figures out the dependencies, builds them
|
|
18
|
+
in the right order, and hands you a finished object.
|
|
19
|
+
|
|
20
|
+
This is **dependency injection**: your classes declare _what_ they need, and the
|
|
21
|
+
container supplies it — instead of each class reaching out and constructing its
|
|
22
|
+
own dependencies. The payoff is testability (swap a real mailer for a fake one in
|
|
23
|
+
one line) and decoupling (a class depends on a `Mailer` contract, not on
|
|
24
|
+
`SendGridMailer`).
|
|
25
|
+
|
|
26
|
+
> **Note** — Do I need to learn all of this? Not up front. In everyday app code
|
|
27
|
+
> you'll mostly _resolve_ services with [`App.make()`](#app-level-dependency-injection)
|
|
28
|
+
> or let [`@inject()`](#auto-wiring-with-inject) wire them for you, and _register_
|
|
29
|
+
> the occasional one with the [`app/services` convention](#app-level-dependency-injection).
|
|
30
|
+
> The lifetimes, contextual bindings, aliases, and hooks below are there when you
|
|
31
|
+
> need finer control — reach for them as the need arises, not before.
|
|
32
|
+
|
|
33
|
+
## Accessing the container
|
|
34
|
+
|
|
35
|
+
Inside a `ServiceProvider`, the container lives at `this.app.container`:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// in a ServiceProvider method
|
|
39
|
+
this.app.container.singleton(CacheManager, () => new CacheManager());
|
|
40
|
+
const cache = await this.app.container.make(CacheManager);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Anywhere else, reach it through the application singleton:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// in application code outside a provider
|
|
47
|
+
import { Application } from "zerotal";
|
|
48
|
+
|
|
49
|
+
const container = currentApp().container;
|
|
50
|
+
const cache = await container.make(CacheManager);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> **Tip** — In application code, prefer [`@inject()`](#auto-wiring-with-inject) so the
|
|
54
|
+
> container wires dependencies for you, or the [`App` facade](#app-level-dependency-injection)
|
|
55
|
+
> (`App.make(...)`) for terse access — reaching for `currentApp()` is a
|
|
56
|
+
> last resort.
|
|
57
|
+
|
|
58
|
+
> **Note** — Reading the examples: a code block that opens with
|
|
59
|
+
> `this.app.container.…` is inside a `ServiceProvider`. A block that uses a bare
|
|
60
|
+
> `container.…` assumes you already obtained it one of the two ways above. Blocks
|
|
61
|
+
> that belong in a specific file (`bootstrap/app.ts`, `app/services/…`) say so in a
|
|
62
|
+
> comment on the first line.
|
|
63
|
+
|
|
64
|
+
## Binding lifetimes
|
|
65
|
+
|
|
66
|
+
A _lifetime_ answers one question: **when you ask for this thing twice, do you get
|
|
67
|
+
the same instance or a new one?** That's the only real decision when registering a
|
|
68
|
+
binding, and it comes down to whether the object holds state and who that state
|
|
69
|
+
belongs to.
|
|
70
|
+
|
|
71
|
+
**Which should I use?**
|
|
72
|
+
|
|
73
|
+
- Reach for **singleton** by default for services — a database manager, a cache,
|
|
74
|
+
an HTTP client. They're expensive to build, safe to share, and you want one of
|
|
75
|
+
them. This is the most common choice.
|
|
76
|
+
- Use **scoped** when the object carries data that belongs to _one request_ and
|
|
77
|
+
must never bleed into another — the current user, a per-request "unit of work",
|
|
78
|
+
a request-id. Each request gets its own; concurrent requests stay isolated.
|
|
79
|
+
- Use **transient** for cheap, stateless, or deliberately short-lived objects
|
|
80
|
+
where sharing would be surprising — a fresh report builder per call.
|
|
81
|
+
- Use **value** when you already _have_ the finished object and just want the
|
|
82
|
+
container to hand it back — a config blob, a pre-configured SDK client.
|
|
83
|
+
|
|
84
|
+
When in doubt, start with singleton; move a binding to scoped only once you find
|
|
85
|
+
it holding per-request state.
|
|
86
|
+
|
|
87
|
+
| Lifetime | Method | Instances created | Use for |
|
|
88
|
+
| --------- | ------------- | ------------------------------- | -------------------------------------- |
|
|
89
|
+
| Singleton | `singleton()` | Once per application | Shared, stateful services (cache, db) |
|
|
90
|
+
| Scoped | `scoped()` | Once per HTTP request | Per-request state (current user, cart) |
|
|
91
|
+
| Transient | `bind()` | Every `make()` call | Stateless or short-lived objects |
|
|
92
|
+
| Value | `value()` | Never — you supply the instance | Config objects, pre-built clients |
|
|
93
|
+
|
|
94
|
+
### Singleton
|
|
95
|
+
|
|
96
|
+
Created once, then cached for the lifetime of the application. Every caller gets
|
|
97
|
+
the same instance. The factory receives the container so it can resolve its own
|
|
98
|
+
dependencies:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// in a ServiceProvider's onRegister()
|
|
102
|
+
this.app.container.singleton(CacheManager, async (c) => {
|
|
103
|
+
const cfg = await c.make("config");
|
|
104
|
+
return new CacheManager(cfg.get("cache"));
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Singleton resolution is **concurrency-safe**: if two requests resolve the same
|
|
109
|
+
unresolved singleton at the same time, the factory runs exactly once and the
|
|
110
|
+
second caller awaits the same in-flight promise.
|
|
111
|
+
|
|
112
|
+
### Scoped
|
|
113
|
+
|
|
114
|
+
Created once per HTTP request and isolated between concurrent requests via
|
|
115
|
+
`AsyncLocalStorage`. Resolving a scoped binding outside of a request context
|
|
116
|
+
throws `ScopedOutsideRequestError`:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// in a ServiceProvider's onRegister()
|
|
120
|
+
this.app.container.scoped(UserSession, (c) => new UserSession());
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Transient
|
|
124
|
+
|
|
125
|
+
A fresh instance on every `make()`:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// in a ServiceProvider's onRegister()
|
|
129
|
+
this.app.container.bind(ReportGenerator, () => new ReportGenerator());
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Value
|
|
133
|
+
|
|
134
|
+
Bind a pre-built instance directly — no factory, no lazy construction:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// in a ServiceProvider's onRegister()
|
|
138
|
+
this.app.container.value("config", configObject);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Registering bindings
|
|
142
|
+
|
|
143
|
+
Bindings are registered in a `ServiceProvider`'s `onRegister()`:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// app/providers/AppServiceProvider.ts
|
|
147
|
+
import { ServiceProvider } from "zerotal";
|
|
148
|
+
import { CacheManager } from "../services/CacheManager.ts";
|
|
149
|
+
import { PaymentGateway } from "../services/PaymentGateway.ts";
|
|
150
|
+
|
|
151
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
152
|
+
onRegister(): void {
|
|
153
|
+
// Singleton — shared across all requests
|
|
154
|
+
this.app.container.singleton(CacheManager, async (c) => {
|
|
155
|
+
const cfg = await c.make("config");
|
|
156
|
+
return new CacheManager(cfg.get("cache"));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Scoped — isolated per request
|
|
160
|
+
this.app.container.scoped(PaymentGateway, async (c) => {
|
|
161
|
+
const cfg = await c.make("config");
|
|
162
|
+
return new PaymentGateway(cfg.get("stripe.secret"));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
> **Warning** — `onRegister()` is synchronous and runs before any binding is resolved.
|
|
169
|
+
> Register here; **resolve** in `onBooted()`, once every provider has registered. See
|
|
170
|
+
> [Service Providers](/docs/providers).
|
|
171
|
+
|
|
172
|
+
## App-level dependency injection
|
|
173
|
+
|
|
174
|
+
Everything above is the container's full API, mostly used by framework packages
|
|
175
|
+
inside providers. For your own application services you rarely need that
|
|
176
|
+
machinery — a full `ServiceProvider` is overkill for one class. These are the
|
|
177
|
+
lighter, everyday paths, all backed by the same container.
|
|
178
|
+
|
|
179
|
+
**Which one should I use?** Match the path to what you're doing:
|
|
180
|
+
|
|
181
|
+
- **Just need to _use_ a service?** Resolve it — [`App.make(MyService)`](#the-app-facade)
|
|
182
|
+
or `@inject(MyService)` on the class that depends on it. No registration needed
|
|
183
|
+
for a plain class; the container auto-wires it.
|
|
184
|
+
- **Want a service to be a shared singleton?** Drop it in `app/services/` and add
|
|
185
|
+
[`static lifetime = "singleton"`](#the-appservices-convention). Zero wiring.
|
|
186
|
+
- **Binding a contract to an implementation** (e.g. `Mailer` → `SendGridMailer`),
|
|
187
|
+
or doing setup that doesn't belong next to a single class? Use the
|
|
188
|
+
[bootstrap `bind()` callback](#bootstrap-bind-callback).
|
|
189
|
+
- **Genuinely complex bootstrapping** — config-driven wiring, lifecycle hooks,
|
|
190
|
+
middleware registration? _Then_ write a [`ServiceProvider`](/docs/providers).
|
|
191
|
+
|
|
192
|
+
As a rule of thumb: start with the convention, graduate to `bind()` when a class
|
|
193
|
+
can't register itself, and only reach for a provider when you need lifecycle hooks.
|
|
194
|
+
|
|
195
|
+
### The App facade
|
|
196
|
+
|
|
197
|
+
`App` is the canonical surface for application code — resolve from anywhere
|
|
198
|
+
(pages, controllers, jobs) without importing `Application`:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// in a controller, page, or job
|
|
202
|
+
import { App } from "zerotal";
|
|
203
|
+
import { UsersService } from "@app/services/users-service.ts";
|
|
204
|
+
|
|
205
|
+
const users = await App.make(UsersService); // auto-wired
|
|
206
|
+
const events = await App.make("events"); // named binding
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
It also exposes registration (`bind`, `singleton`, `scoped`, `value`, `alias`,
|
|
210
|
+
`forget`), introspection (`bound`, `container`, `instance()`), and environment
|
|
211
|
+
helpers (`environment()`, `isProduction()`, `isLocal()`). The `make()` and
|
|
212
|
+
`app()` global helpers are shorthands:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// in application code
|
|
216
|
+
import { make, app } from "zerotal";
|
|
217
|
+
|
|
218
|
+
const users = await make(UsersService); // = App.make(UsersService)
|
|
219
|
+
const kernel = app(); // the Application instance
|
|
220
|
+
const same = await app(UsersService); // = make(UsersService)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Bootstrap `bind()` callback
|
|
224
|
+
|
|
225
|
+
Register bindings in `bootstrap/app.ts` without a provider. The callback runs at
|
|
226
|
+
boot, before any provider's `onRegister()`, so providers can still override:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// bootstrap/app.ts — the callback receives the live container
|
|
230
|
+
Application.create({ providers })
|
|
231
|
+
.bind((container) => {
|
|
232
|
+
container.singleton(Clock, () => new SystemClock());
|
|
233
|
+
container.for(ReportService).give(Clock, () => new FixedClock()); // contextual
|
|
234
|
+
})
|
|
235
|
+
.fileBasedRouting({ web: basePath("app/flow/pages") });
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
This is the place for **interface → implementation** bindings, since a class
|
|
239
|
+
can't register itself against a different token:
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// bootstrap/app.ts
|
|
243
|
+
Application.create({ providers }).bind((container) =>
|
|
244
|
+
container.singleton(Mailer, () => new SendGridMailer(env("SENDGRID_KEY"))),
|
|
245
|
+
);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### The `app/services` convention
|
|
249
|
+
|
|
250
|
+
Any class under `app/services/` is auto-discovered at boot. Declare its lifetime
|
|
251
|
+
next to the class with a `static lifetime` flag and it's registered for you:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// app/services/users-service.ts
|
|
255
|
+
@inject(Auth)
|
|
256
|
+
export class UsersService {
|
|
257
|
+
static lifetime = "singleton" as const; // "singleton" | "scoped" | "transient"
|
|
258
|
+
|
|
259
|
+
constructor(private auth: AuthManager) {}
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Now `App.make(UsersService)` returns the **same** singleton on every call.
|
|
264
|
+
`transient` (or no flag) registers nothing — the container still auto-wires the
|
|
265
|
+
class on demand, just with a fresh instance each time.
|
|
266
|
+
|
|
267
|
+
### Registration is boot-time only
|
|
268
|
+
|
|
269
|
+
The container is process-global and shared across every concurrent request, so
|
|
270
|
+
the `App` registration methods are **locked once `boot()` completes** — calling
|
|
271
|
+
`App.singleton()` (etc.) from inside a request throws `ContainerLockedError`.
|
|
272
|
+
Register during boot; for genuinely per-request state, register a `scoped`
|
|
273
|
+
binding at boot and the container hands each request its own instance.
|
|
274
|
+
|
|
275
|
+
> **Danger** — The container is shared across every concurrent request. Mutating
|
|
276
|
+
> it at request time would leak state between requests — which is exactly why the
|
|
277
|
+
> `App` facade locks registration after boot. Use a `scoped` binding for anything
|
|
278
|
+
> that must be per-request.
|
|
279
|
+
|
|
280
|
+
> **Note** — the lock guards the `App` facade, not the raw container. Framework
|
|
281
|
+
> internals (deferred providers that register lazily) and tests use
|
|
282
|
+
> `container.*` directly and are intentionally unaffected.
|
|
283
|
+
|
|
284
|
+
## Resolving bindings
|
|
285
|
+
|
|
286
|
+
### Async resolution — make
|
|
287
|
+
|
|
288
|
+
`make()` is the primary way to resolve a binding. It's async because factories
|
|
289
|
+
may be async and deferred providers may need to boot first:
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
// in application code
|
|
293
|
+
const cache = await container.make(CacheManager);
|
|
294
|
+
const cfg = await container.make("config");
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Sync resolution — makeSync
|
|
298
|
+
|
|
299
|
+
`makeSync()` resolves **without awaiting** but only works for two cases:
|
|
300
|
+
|
|
301
|
+
- **value** bindings, and
|
|
302
|
+
- **singleton** bindings that have already been resolved.
|
|
303
|
+
|
|
304
|
+
Anything else throws `SyncResolutionError`. This is what [facades](#facades) use
|
|
305
|
+
internally — which is why providers pre-resolve (warm) their singleton in
|
|
306
|
+
`onBooted()` before any facade call happens:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
// in application code
|
|
310
|
+
const cache = container.makeSync(CacheManager); // throws if not yet resolved
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Safe lookup — tryMake
|
|
314
|
+
|
|
315
|
+
`tryMake()` resolves a string-keyed binding synchronously, returning `undefined`
|
|
316
|
+
instead of throwing when the token isn't registered. Useful for optional
|
|
317
|
+
services that exist only in certain runtime modes:
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// in a ServiceProvider — CommandRunner is only bound in console mode
|
|
321
|
+
const runner = container.tryMake("commandRunner");
|
|
322
|
+
if (runner) runner.register(MyCommand);
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Auto-wiring with @inject
|
|
326
|
+
|
|
327
|
+
_Auto-wiring_ means: list what a class needs, and the container builds those
|
|
328
|
+
dependencies and passes them to the constructor for you — no factory function, no
|
|
329
|
+
manual `new`. It's the most common way app code consumes the container, because
|
|
330
|
+
the class stays honest about its dependencies (they're right there in the
|
|
331
|
+
constructor) while you never have to assemble them by hand.
|
|
332
|
+
|
|
333
|
+
Pass a class's dependency tokens straight to `@inject(...)`. The container
|
|
334
|
+
resolves each token — recursively, and **in parallel** — before constructing the
|
|
335
|
+
class:
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
// app/repositories/PostRepository.ts
|
|
339
|
+
import { inject } from "zerotal";
|
|
340
|
+
import { CacheManager } from "../services/CacheManager.ts";
|
|
341
|
+
import { Logger } from "../services/Logger.ts";
|
|
342
|
+
|
|
343
|
+
@inject(CacheManager, Logger)
|
|
344
|
+
export class PostRepository {
|
|
345
|
+
constructor(
|
|
346
|
+
private cache: CacheManager,
|
|
347
|
+
private logger: Logger,
|
|
348
|
+
) {}
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
No registration needed — the container auto-wires on first `make()`:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// in application code
|
|
356
|
+
const repo = await container.make(PostRepository);
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
The token order must match the constructor parameter order. Any listed token may
|
|
360
|
+
itself be auto-wired or bound by a provider. Tokens may be classes, abstract
|
|
361
|
+
classes, or string keys from `ContainerBindings`.
|
|
362
|
+
|
|
363
|
+
## Binding tokens
|
|
364
|
+
|
|
365
|
+
A token is the key the container resolves against — a class constructor, an
|
|
366
|
+
abstract class, or a string key declared in `ContainerBindings`:
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
// in application code
|
|
370
|
+
// Class token (most common)
|
|
371
|
+
container.singleton(CacheManager, factory);
|
|
372
|
+
const cache = await container.make(CacheManager);
|
|
373
|
+
|
|
374
|
+
// String token (typed via ContainerBindings)
|
|
375
|
+
container.value("config", cfg);
|
|
376
|
+
const cfg = await container.make("config");
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Packages extend the `ContainerBindings` interface via declaration merging so
|
|
380
|
+
string tokens stay fully type-safe:
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
// in a package's types.ts
|
|
384
|
+
declare module "zerotal" {
|
|
385
|
+
interface ContainerBindings {
|
|
386
|
+
db: SQL;
|
|
387
|
+
search: SearchClient;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Resolution order
|
|
393
|
+
|
|
394
|
+
When you call `make(token, consumer?)`, the container walks these steps:
|
|
395
|
+
|
|
396
|
+
1. **Alias** — follow the alias map to the canonical token.
|
|
397
|
+
2. **Deferred** — if a deferred provider is registered for this token, boot it
|
|
398
|
+
now (once), then continue.
|
|
399
|
+
3. **Contextual** — if a `consumer` was supplied and has a contextual override
|
|
400
|
+
for this token, use it.
|
|
401
|
+
4. **Registry** — otherwise use the registered binding.
|
|
402
|
+
5. **Auto-wire** — if nothing is registered but the token is a class with
|
|
403
|
+
`@inject()`, construct it by resolving its dependencies.
|
|
404
|
+
6. Otherwise throw `BindingNotFoundError`.
|
|
405
|
+
|
|
406
|
+
After construction, any [`resolving()`](#resolving-hooks) hooks for the token fire.
|
|
407
|
+
|
|
408
|
+
## Contextual bindings
|
|
409
|
+
|
|
410
|
+
Hand a different implementation of the same dependency to different consumers:
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
// in a ServiceProvider or bootstrap bind() callback
|
|
414
|
+
// PostController gets the Redis cache; ReportController gets the file cache
|
|
415
|
+
container.for(PostController).give(CacheDriver, () => new RedisCache());
|
|
416
|
+
container.for(ReportController).give(CacheDriver, () => new FileCache());
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
`for(consumer).give(dependency, factory)` registers a transient contextual
|
|
420
|
+
binding. The variants control lifetime:
|
|
421
|
+
|
|
422
|
+
| Method | Lifetime of the contextual instance |
|
|
423
|
+
| ----------------------------- | ------------------------------------- |
|
|
424
|
+
| `give(dep, factory)` | Transient — new instance per resolve |
|
|
425
|
+
| `giveSingleton(dep, factory)` | Singleton within the contextual scope |
|
|
426
|
+
| `giveValue(dep, instance)` | A pre-built value |
|
|
427
|
+
|
|
428
|
+
The `consumer` argument flows automatically when a class is auto-wired — the
|
|
429
|
+
container passes the class being constructed as the consumer when resolving each
|
|
430
|
+
of its `@inject` tokens.
|
|
431
|
+
|
|
432
|
+
## Aliases
|
|
433
|
+
|
|
434
|
+
Bind one token as an alias for another. Resolving the alias returns the target's
|
|
435
|
+
instance — handy for binding an interface/contract token to a concrete class:
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
// in a ServiceProvider or bootstrap bind() callback
|
|
439
|
+
container.alias(CacheContract, CacheManager);
|
|
440
|
+
const cache = await container.make(CacheContract); // → the CacheManager singleton
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Alias chains are followed to the canonical token, and the resolver guards against
|
|
444
|
+
alias cycles.
|
|
445
|
+
|
|
446
|
+
## resolving hooks
|
|
447
|
+
|
|
448
|
+
Run a callback every time a token resolves — for post-construction setup without
|
|
449
|
+
subclassing or wrapping the factory:
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
// in a ServiceProvider or bootstrap bind() callback
|
|
453
|
+
container.resolving(Logger, (logger) => {
|
|
454
|
+
logger.setChannel("app");
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Hooks fire for every binding kind (and for auto-wired classes) right after the
|
|
459
|
+
instance is constructed.
|
|
460
|
+
|
|
461
|
+
## Deferred providers
|
|
462
|
+
|
|
463
|
+
Register a provider so it boots only when one of its tokens is first resolved —
|
|
464
|
+
keeping cold-start fast for services not used on every request:
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
// in a ServiceProvider or bootstrap bind() callback
|
|
468
|
+
container.defer(SearchClient, SearchServiceProvider);
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
The first `make(SearchClient)` runs the provider's full
|
|
472
|
+
`onRegister → onBooting → onBooted` sequence, then resolves the binding. The
|
|
473
|
+
provider is also tracked so its `onStopping`/`onStopped` hooks run at shutdown.
|
|
474
|
+
See [The Application](/docs/application#deferred-providers) for the app-level
|
|
475
|
+
`defer()` sugar.
|
|
476
|
+
|
|
477
|
+
## Scoped resolution lifecycle
|
|
478
|
+
|
|
479
|
+
Each HTTP request runs inside `container.runScoped()`, which creates a fresh
|
|
480
|
+
`ScopedResolver`, stores it in `AsyncLocalStorage`, and flushes it when the
|
|
481
|
+
request finishes:
|
|
482
|
+
|
|
483
|
+
- Every `make()` of a scoped binding during the request resolves against **that**
|
|
484
|
+
request's resolver — even across `await` boundaries and concurrent requests.
|
|
485
|
+
- There is no shared mutable state on the container, so scoped instances can
|
|
486
|
+
never leak between requests.
|
|
487
|
+
- After the response is sent the resolver is flushed; resolving a scoped binding
|
|
488
|
+
afterward throws `ScopedAfterFlushError`.
|
|
489
|
+
|
|
490
|
+
You rarely call `runScoped()` yourself — the HTTP pipeline does it per request.
|
|
491
|
+
|
|
492
|
+
## Errors
|
|
493
|
+
|
|
494
|
+
| Error | Thrown when |
|
|
495
|
+
| --------------------------- | -------------------------------------------------------------------- |
|
|
496
|
+
| `BindingNotFoundError` | A token has no binding and can't be auto-wired. |
|
|
497
|
+
| `ScopedOutsideRequestError` | A scoped binding is resolved outside any request scope. |
|
|
498
|
+
| `SyncResolutionError` | `makeSync()` is used on a non-value / unresolved-singleton binding. |
|
|
499
|
+
| `CircularDependencyError` | Auto-wiring detects a dependency cycle (clear chain in the message). |
|
|
500
|
+
| `ScopedAfterFlushError` | A scoped binding is resolved after its request scope was flushed. |
|
|
501
|
+
| `ContainerLockedError` | An `App` registration method is called after `boot()` completes. |
|
|
502
|
+
|
|
503
|
+
All extend `ZerotalError`.
|
|
504
|
+
|
|
505
|
+
## References
|
|
506
|
+
|
|
507
|
+
Members of the `Container` class:
|
|
508
|
+
|
|
509
|
+
| Method | Signature | Description |
|
|
510
|
+
| --------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
511
|
+
| `singleton(token, factory)` | `singleton<T>(token: BindingToken<T>, factory: Factory<T>): this` | Bind a once-per-app instance. |
|
|
512
|
+
| `scoped(token, factory)` | `scoped<T>(token: BindingToken<T>, factory: Factory<T>): this` | Bind a once-per-request instance. |
|
|
513
|
+
| `bind(token, factory)` | `bind<T>(token: BindingToken<T>, factory: Factory<T>): this` | Bind a transient (new each time). |
|
|
514
|
+
| `value(token, instance)` | `value<T>(token: BindingToken<T>, instance: T): this` | Bind a pre-built value. |
|
|
515
|
+
| `make(token, consumer?)` | `make<T>(token: BindingToken<T>, consumer?: unknown): Promise<T>` | Resolve asynchronously (preferred). |
|
|
516
|
+
| `makeSync(token)` | `makeSync<T>(token: BindingToken<T>): T` | Resolve synchronously (value / resolved-singleton only). |
|
|
517
|
+
| `build(ctor)` | `build<T>(ctor: new (...args: unknown[]) => T): Promise<T>` | Auto-wire a class, ignoring any registered binding. |
|
|
518
|
+
| `tryMake(token)` | `tryMake<K>(token: K): ContainerBindings[K] \| undefined` | Sync resolve or `undefined` if unregistered. |
|
|
519
|
+
| `forget(token)` | `forget(token: BindingToken): boolean` | Remove a binding; `true` if one existed. |
|
|
520
|
+
| `alias(from, to)` | `alias(from: unknown, to: unknown): this` | Resolve `from` as `to`. |
|
|
521
|
+
| `for(consumer)` | `for<C>(consumer: BindingToken<C>): ContextualBindingBuilder<C>` | Begin a contextual binding (`give` / `giveSingleton` / `giveValue`). |
|
|
522
|
+
| `resolving(token, hook)` | `resolving<T>(token: BindingToken<T>, hook: (i: T) => void): this` | Run a hook after each resolution. |
|
|
523
|
+
| `defer(token, Provider)` | `defer(token: unknown, provider: new (app) => unknown): this` | Boot a provider lazily on first resolve. |
|
|
524
|
+
| `runScoped(cb)` | `runScoped<T>(cb: (scoped: ScopedResolver) => Promise<T>): Promise<T>` | Run `cb` inside a fresh request scope. |
|
|
525
|
+
|
|
526
|
+
## Facades
|
|
527
|
+
|
|
528
|
+
A facade is a thin static class that resolves a container binding for you, so
|
|
529
|
+
consumers write `Cache.get(key)` instead of `await container.make("cache")`.
|
|
530
|
+
Facades are sugar over the container — same instance, less ceremony.
|
|
531
|
+
|
|
532
|
+
They are only usable **after the application has booted** (all providers have run
|
|
533
|
+
`onRegister`/`onBooting`/`onBooted`), because they resolve their binding
|
|
534
|
+
synchronously via `makeSync`. That's why providers pre-resolve their singleton in
|
|
535
|
+
`onBooted()` — it warms the binding so the facade works everywhere afterwards.
|
|
536
|
+
|
|
537
|
+
> **Warning** — Using a facade at module scope (top-level code that runs on
|
|
538
|
+
> import) throws `FacadeAccessedBeforeBootError`, because the container isn't ready
|
|
539
|
+
> yet. Move facade calls inside a function, controller method, or provider hook.
|
|
540
|
+
|
|
541
|
+
### Built-in facades
|
|
542
|
+
|
|
543
|
+
Core ships a few, importable from `zerotal`:
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
// in application code (after boot)
|
|
547
|
+
import { Config, Events, Artisan } from "zerotal";
|
|
548
|
+
|
|
549
|
+
// Config — read loaded configuration
|
|
550
|
+
Config.get("app.name");
|
|
551
|
+
|
|
552
|
+
// Events — the class-based event bus (events and listeners are plain classes)
|
|
553
|
+
Events.on(UserRegistered, SendWelcomeEmail);
|
|
554
|
+
await Events.emit(new UserRegistered(user.id, user.email));
|
|
555
|
+
|
|
556
|
+
// Artisan — invoke a CLI command programmatically
|
|
557
|
+
await Artisan.call("migrate");
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
Packages ship their own — for example `Cache` from `@zerotal/cache`, `Auth` from
|
|
561
|
+
`@zerotal/auth`:
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
// in application code (after boot)
|
|
565
|
+
import { Cache } from "@zerotal/cache";
|
|
566
|
+
import { Auth } from "@zerotal/auth";
|
|
567
|
+
|
|
568
|
+
await Cache.put("key", value, 300);
|
|
569
|
+
Auth.check(); // → boolean
|
|
570
|
+
Auth.user(); // → AuthenticatedUser (throws if guest)
|
|
571
|
+
Auth.userOrNull(); // → AuthenticatedUser | undefined
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### Defining a facade
|
|
575
|
+
|
|
576
|
+
Build one with `createFacade<T>(token)`, passing the same token the provider binds:
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
// src/facades/Cache.ts
|
|
580
|
+
import { createFacade } from "zerotal";
|
|
581
|
+
import type { CacheManager } from "../CacheManager.ts";
|
|
582
|
+
|
|
583
|
+
export const Cache = createFacade<CacheManager>("cache");
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
Every static call proxies to the resolved instance, so `Cache.get(...)` is exactly
|
|
587
|
+
`(await container.make("cache")).get(...)` — just synchronous and terminless. See
|
|
588
|
+
[Package Development](/docs/package-development#facades) for where facades
|
|
589
|
+
fit in a package, and prefer [`@inject()`](#auto-wiring-with-inject) over facades
|
|
590
|
+
inside your own application services where testability matters.
|
|
591
|
+
|
|
592
|
+
## Next steps
|
|
593
|
+
|
|
594
|
+
- [Service Providers](/docs/providers) — where bindings are registered and booted.
|
|
595
|
+
- [The Application](/docs/application) — the container's owner and lifecycle engine.
|
|
596
|
+
- [Request Lifecycle](/docs/lifecycle) — where the per-request scope is opened and flushed.
|