@zerotal/core 1.0.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 +79 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/package.json +72 -0
- package/src/application/Application.ts +1671 -0
- package/src/application/BootDoctor.ts +108 -0
- package/src/application/DevErrorPage.ts +567 -0
- package/src/application/ExceptionHandler.ts +183 -0
- package/src/application/currentApp.ts +73 -0
- package/src/assets/assets.ts +79 -0
- package/src/assets/index.ts +16 -0
- package/src/auth/AuthenticatedUser.ts +18 -0
- package/src/build/PackageLinter.ts +146 -0
- package/src/build/PackageScaffold.ts +127 -0
- package/src/build/codemod.ts +64 -0
- package/src/build/index.ts +12 -0
- package/src/command/Command.ts +254 -0
- package/src/command/CommandRunner.ts +593 -0
- package/src/command/OutputWriter.ts +61 -0
- package/src/command/builtin/CompileCommand.ts +46 -0
- package/src/command/builtin/CssBuildCommand.ts +71 -0
- package/src/command/builtin/KeyGenerateCommand.ts +58 -0
- package/src/command/builtin/LintPackagesCommand.ts +72 -0
- package/src/command/builtin/MakeCommandCommand.ts +85 -0
- package/src/command/builtin/MakeControllerCommand.ts +95 -0
- package/src/command/builtin/MakeEventCommand.ts +85 -0
- package/src/command/builtin/MakeJobCommand.ts +53 -0
- package/src/command/builtin/MakeListenerCommand.ts +35 -0
- package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
- package/src/command/builtin/MakeNotificationCommand.ts +48 -0
- package/src/command/builtin/MakeObserverCommand.ts +78 -0
- package/src/command/builtin/MakePackageCommand.ts +45 -0
- package/src/command/builtin/MakePolicyCommand.ts +66 -0
- package/src/command/builtin/MakeProviderCommand.ts +75 -0
- package/src/command/builtin/MakeRequestCommand.ts +47 -0
- package/src/command/builtin/MakeResourceCommand.ts +61 -0
- package/src/command/builtin/MakeTestCommand.ts +120 -0
- package/src/command/builtin/ReloadCommand.ts +52 -0
- package/src/command/builtin/ReplCommand.ts +174 -0
- package/src/command/builtin/RouteListCommand.ts +188 -0
- package/src/command/builtin/ServeCommand.ts +321 -0
- package/src/command/builtin/StartCommand.ts +3 -0
- package/src/command/builtin/StatusCommand.ts +71 -0
- package/src/command/builtin/TestCommand.ts +172 -0
- package/src/command/builtin/WorkerCommand.ts +27 -0
- package/src/command/builtin/index.ts +53 -0
- package/src/command/scaffold/worker.ts.txt +12 -0
- package/src/command/scaffold/zerotal.ts.txt +26 -0
- package/src/command/startZerotal.ts +55 -0
- package/src/config/AppConfig.ts +253 -0
- package/src/config/ConfigLoader.ts +117 -0
- package/src/config/ConfigManager.ts +169 -0
- package/src/config/index.ts +46 -0
- package/src/config/registry.ts +59 -0
- package/src/config/validation.ts +117 -0
- package/src/container/Container.ts +606 -0
- package/src/container/ContextualBindingBuilder.ts +57 -0
- package/src/container/ScopedResolver.ts +117 -0
- package/src/container/index.ts +32 -0
- package/src/container/inject.ts +55 -0
- package/src/container/types.ts +71 -0
- package/src/context/RequestContext.ts +91 -0
- package/src/contracts/auth.ts +24 -0
- package/src/contracts/index.ts +23 -0
- package/src/contracts/session.ts +70 -0
- package/src/contracts/transaction.ts +26 -0
- package/src/conventions/ConventionLoader.ts +128 -0
- package/src/conventions/builtinConcerns.ts +131 -0
- package/src/crypt/Crypt.ts +141 -0
- package/src/crypt/URLSigner.ts +96 -0
- package/src/datetime/Carbon.ts +1396 -0
- package/src/datetime/CarbonInterval.ts +421 -0
- package/src/datetime/clock.ts +28 -0
- package/src/datetime/index.ts +23 -0
- package/src/datetime/temporal-shim.ts +1 -0
- package/src/dev/BuildOutput.ts +131 -0
- package/src/dev/CssPlugins.ts +184 -0
- package/src/dev/DevBuildHook.ts +74 -0
- package/src/dev/DevOrchestrator.ts +213 -0
- package/src/dev/DevReloadMiddleware.ts +101 -0
- package/src/dev/DevReloadServer.ts +85 -0
- package/src/dev/DevWsServer.ts +45 -0
- package/src/dev/index.ts +19 -0
- package/src/dev/reloadClient.ts +39 -0
- package/src/env/Def.ts +232 -0
- package/src/env/EnvSchema.ts +105 -0
- package/src/env/index.ts +34 -0
- package/src/env/t.ts +128 -0
- package/src/errors/ConfigError.ts +12 -0
- package/src/errors/ContainerErrors.ts +143 -0
- package/src/errors/HttpError.ts +127 -0
- package/src/errors/ValidationError.ts +19 -0
- package/src/errors/ZerotalError.ts +25 -0
- package/src/errors/index.ts +46 -0
- package/src/events/CallQueuedListener.ts +66 -0
- package/src/events/Emitter.ts +280 -0
- package/src/events/EventFake.ts +160 -0
- package/src/events/FrameworkEvents.ts +252 -0
- package/src/facade/Facade.ts +101 -0
- package/src/facade/facades/App.ts +155 -0
- package/src/facade/facades/Artisan.ts +63 -0
- package/src/facade/facades/Config.ts +21 -0
- package/src/facade/facades/Events.ts +19 -0
- package/src/facade/facades/index.ts +28 -0
- package/src/global.d.ts +9 -0
- package/src/hash/Hash.ts +60 -0
- package/src/health/Health.ts +221 -0
- package/src/health/index.ts +27 -0
- package/src/helpers/Collection.ts +435 -0
- package/src/helpers/config.ts +59 -0
- package/src/helpers/fluent.ts +52 -0
- package/src/helpers/html.ts +11 -0
- package/src/helpers/index.ts +266 -0
- package/src/helpers/make.ts +35 -0
- package/src/helpers/markdown.ts +73 -0
- package/src/helpers/pageElements.ts +27 -0
- package/src/helpers/request.ts +62 -0
- package/src/helpers/response.ts +411 -0
- package/src/helpers/str.ts +208 -0
- package/src/http/Http.ts +298 -0
- package/src/http/HttpClient.ts +289 -0
- package/src/http/Resource.ts +171 -0
- package/src/http/UploadedFile.ts +204 -0
- package/src/http/Uri.ts +490 -0
- package/src/http/index.ts +46 -0
- package/src/http/negotiate.ts +213 -0
- package/src/http/originGuard.ts +76 -0
- package/src/http/sniffContentType.ts +105 -0
- package/src/http/url.ts +204 -0
- package/src/http/withHeaders.ts +24 -0
- package/src/index.ts +250 -0
- package/src/lock/LockManager.ts +228 -0
- package/src/lock/config.ts +49 -0
- package/src/lock/drivers/LockDriver.ts +32 -0
- package/src/lock/drivers/MemoryLockDriver.ts +52 -0
- package/src/lock/drivers/RedisLockDriver.ts +58 -0
- package/src/lock/drivers/SqliteLockDriver.ts +85 -0
- package/src/lock/errors.ts +20 -0
- package/src/lock/facades/Lock.ts +114 -0
- package/src/lock/index.ts +53 -0
- package/src/logger/Log.ts +35 -0
- package/src/logger/LogManager.ts +430 -0
- package/src/logger/LoggerMiddleware.ts +125 -0
- package/src/logger/channels/ConsoleChannel.ts +139 -0
- package/src/logger/channels/DailyChannel.ts +74 -0
- package/src/logger/channels/NullChannel.ts +17 -0
- package/src/logger/channels/SingleChannel.ts +34 -0
- package/src/logger/channels/StackChannel.ts +29 -0
- package/src/logger/config.ts +90 -0
- package/src/logger/format.ts +96 -0
- package/src/logger/frameworkLog.ts +93 -0
- package/src/logger/index.ts +68 -0
- package/src/logger/renderTable.ts +111 -0
- package/src/logger/types.ts +212 -0
- package/src/macros/config.macro.ts +50 -0
- package/src/metrics/HttpMetrics.ts +114 -0
- package/src/metrics/index.ts +18 -0
- package/src/middleware/BaseMiddleware.ts +72 -0
- package/src/middleware/CorsMiddleware.ts +152 -0
- package/src/middleware/RateLimiter.ts +255 -0
- package/src/middleware/SecureHeadersMiddleware.ts +127 -0
- package/src/middleware/ThrottleMiddleware.ts +252 -0
- package/src/middleware/WebhookMiddleware.ts +204 -0
- package/src/pipeline/ContextRegistry.ts +42 -0
- package/src/pipeline/HttpContext.ts +865 -0
- package/src/pipeline/Pipeline.ts +150 -0
- package/src/pipeline/currentPage.ts +46 -0
- package/src/pipeline/types.ts +80 -0
- package/src/provider/LockProvider.ts +64 -0
- package/src/provider/LogProvider.ts +137 -0
- package/src/provider/ServiceProvider.ts +84 -0
- package/src/provider/StorageProvider.ts +45 -0
- package/src/router/FileRouter.ts +526 -0
- package/src/router/Route.ts +76 -0
- package/src/router/RouteHandler.ts +335 -0
- package/src/router/Router.ts +1247 -0
- package/src/router/domain.ts +65 -0
- package/src/security/index.ts +22 -0
- package/src/storage/FakeDisk.ts +233 -0
- package/src/storage/StorageFilesMiddleware.ts +150 -0
- package/src/storage/StorageManager.ts +173 -0
- package/src/storage/config.ts +47 -0
- package/src/storage/drivers/LocalDriver.ts +138 -0
- package/src/storage/drivers/S3Driver.ts +169 -0
- package/src/storage/errors.ts +135 -0
- package/src/storage/facades/Storage.ts +3 -0
- package/src/storage/global.d.ts +7 -0
- package/src/storage/index.ts +22 -0
- package/src/storage/root.ts +59 -0
- package/src/storage/types.ts +104 -0
- package/src/support/appKey.ts +38 -0
- package/src/support/cookie.ts +72 -0
- package/src/support/crypto.ts +52 -0
- package/src/support/deepMerge.ts +117 -0
- package/src/support/env.ts +71 -0
- package/src/support/network.ts +79 -0
- package/src/support/port.ts +197 -0
- package/src/support/str.ts +122 -0
- package/src/view/FileRouteResolver.ts +59 -0
- package/src/view/index.ts +144 -0
- package/src/view/jsx-runtime.ts +233 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The framework's IoC container: registers bindings (transient, singleton,
|
|
3
|
+
* scoped, value), resolves them with auto-wiring and cycle detection, and
|
|
4
|
+
* manages per-request scopes through AsyncLocalStorage.
|
|
5
|
+
*/
|
|
6
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
+
import type {
|
|
8
|
+
Binding,
|
|
9
|
+
BindingToken,
|
|
10
|
+
ContainerBindings,
|
|
11
|
+
Factory,
|
|
12
|
+
SingletonBinding,
|
|
13
|
+
TransientBinding,
|
|
14
|
+
ScopedBinding,
|
|
15
|
+
ValueBinding,
|
|
16
|
+
} from "./types.ts";
|
|
17
|
+
import { ScopedResolver } from "./ScopedResolver.ts";
|
|
18
|
+
import { ContextualBindingBuilder } from "./ContextualBindingBuilder.ts";
|
|
19
|
+
import { injectRegistry } from "./inject.ts";
|
|
20
|
+
import {
|
|
21
|
+
BindingNotFoundError,
|
|
22
|
+
ScopedOutsideRequestError,
|
|
23
|
+
SyncResolutionError,
|
|
24
|
+
CircularDependencyError,
|
|
25
|
+
} from "../errors/ContainerErrors.ts";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Registers and resolves bindings, with auto-wiring, scopes, and contextual overrides.
|
|
29
|
+
*
|
|
30
|
+
* The container is the framework's IoC core. Register how a token is built with
|
|
31
|
+
* {@link bind} (transient), {@link singleton}, {@link scoped}, or {@link value},
|
|
32
|
+
* then resolve it with {@link make}. Classes with declared dependencies
|
|
33
|
+
* (`@inject()`) are auto-wired without an explicit binding.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* Resolution is async-first: prefer {@link make} everywhere and reserve
|
|
37
|
+
* {@link makeSync} for Facade accessors where the singleton has already been
|
|
38
|
+
* pre-resolved. Scoped bindings only resolve inside a request scope opened by
|
|
39
|
+
* {@link runScoped}; resolving one elsewhere throws {@link ScopedOutsideRequestError}.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* class Config {}
|
|
44
|
+
* class Db {}
|
|
45
|
+
*
|
|
46
|
+
* const container = Container.createEmpty();
|
|
47
|
+
*
|
|
48
|
+
* container.bind(Db, () => new Db()); // new instance each make()
|
|
49
|
+
* container.singleton(Config, () => new Config()); // built once, cached
|
|
50
|
+
* container.value("appName", "zerotal"); // pre-built value
|
|
51
|
+
*
|
|
52
|
+
* const db = await container.make(Db);
|
|
53
|
+
* const config = await container.make(Config);
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export class Container {
|
|
57
|
+
// ── Internal state ────────────────────────────────────────────────────
|
|
58
|
+
registry = new Map<unknown, Binding<unknown>>();
|
|
59
|
+
private aliases = new Map<unknown, unknown>();
|
|
60
|
+
private contextual = new Map<unknown, Map<unknown, Binding<unknown>>>();
|
|
61
|
+
private resolvingHooks = new Map<unknown, Array<(i: unknown) => void>>();
|
|
62
|
+
private deferred = new Map<unknown, new (app: unknown) => unknown>();
|
|
63
|
+
// Pending singleton resolutions — prevents concurrent double-factory-call
|
|
64
|
+
private pending = new Map<unknown, Promise<unknown>>();
|
|
65
|
+
// Per-request scoped resolver, stored in AsyncLocalStorage so concurrent
|
|
66
|
+
// requests each see their own isolated instance with no shared mutable state.
|
|
67
|
+
private readonly _scopedStore = new AsyncLocalStorage<ScopedResolver>();
|
|
68
|
+
/** @internal Set by `Application.boot()`; used to boot deferred providers. */
|
|
69
|
+
_app: import("../application/Application.ts").Application | undefined = undefined;
|
|
70
|
+
|
|
71
|
+
// ── Registration ──────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Register a transient binding — the factory runs on every resolution.
|
|
75
|
+
*
|
|
76
|
+
* @category Binding
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* container.bind(Uuid, () => new Uuid(crypto.randomUUID()));
|
|
80
|
+
* const a = await container.make(Uuid);
|
|
81
|
+
* const b = await container.make(Uuid); // a !== b
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
bind<T>(token: BindingToken<T>, factory: Factory<T>): this {
|
|
85
|
+
this.registry.set(token, { kind: "transient", factory } satisfies TransientBinding<T>);
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Register a singleton binding — resolved once and cached for the process lifetime.
|
|
91
|
+
*
|
|
92
|
+
* @remarks
|
|
93
|
+
* The factory receives the container so it can resolve its own dependencies.
|
|
94
|
+
* Concurrent first resolutions share a single in-flight promise, so the
|
|
95
|
+
* factory runs exactly once even under parallel `make()` calls.
|
|
96
|
+
*
|
|
97
|
+
* @category Binding
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* container.singleton("config", (c) => new ConfigManager());
|
|
101
|
+
* const config = await container.make("config"); // same instance every time
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
singleton<T>(token: BindingToken<T>, factory: Factory<T>): this {
|
|
105
|
+
this.registry.set(token, {
|
|
106
|
+
kind: "singleton",
|
|
107
|
+
factory,
|
|
108
|
+
instance: undefined,
|
|
109
|
+
resolved: false,
|
|
110
|
+
} satisfies SingletonBinding<T>);
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Register a scoped binding — resolved once per request scope.
|
|
116
|
+
*
|
|
117
|
+
* @remarks
|
|
118
|
+
* Resolving a scoped token outside a request scope (see {@link runScoped})
|
|
119
|
+
* throws {@link ScopedOutsideRequestError}.
|
|
120
|
+
*
|
|
121
|
+
* @category Binding
|
|
122
|
+
*/
|
|
123
|
+
scoped<T>(token: BindingToken<T>, factory: Factory<T>): this {
|
|
124
|
+
this.registry.set(token, { kind: "scoped", factory } satisfies ScopedBinding<T>);
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Register an already-constructed value under `token`.
|
|
130
|
+
*
|
|
131
|
+
* @remarks
|
|
132
|
+
* Value bindings are the only kind (besides an already-resolved singleton)
|
|
133
|
+
* that {@link makeSync} can return.
|
|
134
|
+
*
|
|
135
|
+
* @category Binding
|
|
136
|
+
*/
|
|
137
|
+
value<T>(token: BindingToken<T>, instance: T): this {
|
|
138
|
+
this.registry.set(token, { kind: "value", instance } satisfies ValueBinding<T>);
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Make resolving `from` resolve `to` instead. Alias chains are followed transitively.
|
|
144
|
+
*
|
|
145
|
+
* @category Binding
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* container.singleton(FileLogger, () => new FileLogger());
|
|
149
|
+
* container.alias("log", FileLogger);
|
|
150
|
+
* const log = await container.make("log"); // resolves FileLogger
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
alias(from: unknown, to: unknown): this {
|
|
154
|
+
this.aliases.set(from, to);
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Register a hook fired with each freshly constructed instance of `token`.
|
|
160
|
+
*
|
|
161
|
+
* @remarks
|
|
162
|
+
* The hook runs after construction on every fresh instance — so once per
|
|
163
|
+
* process for a singleton, and on each resolution for a transient. It does not
|
|
164
|
+
* fire for an already-cached singleton or value.
|
|
165
|
+
*
|
|
166
|
+
* @category Lifecycle hooks
|
|
167
|
+
* @example
|
|
168
|
+
* ```ts
|
|
169
|
+
* container.resolving(Mailer, (mailer) => mailer.setFrom("noreply@example.com"));
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
resolving<T>(token: BindingToken<T>, hook: (instance: T) => void): this {
|
|
173
|
+
const existing = this.resolvingHooks.get(token) ?? [];
|
|
174
|
+
existing.push(hook as (instance: unknown) => void);
|
|
175
|
+
this.resolvingHooks.set(token, existing);
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Begin a contextual binding so `consumer` receives a tailored dependency.
|
|
181
|
+
*
|
|
182
|
+
* @category Binding
|
|
183
|
+
* @example
|
|
184
|
+
* ```ts
|
|
185
|
+
* // ReportService gets a FixedClock; everyone else gets the default Clock.
|
|
186
|
+
* container.singleton(Clock, () => new SystemClock());
|
|
187
|
+
* container.for(ReportService).give(Clock, () => new FixedClock());
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
for<C>(consumer: BindingToken<C>): ContextualBindingBuilder<C> {
|
|
191
|
+
return new ContextualBindingBuilder(this, consumer);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Defer a provider so it boots lazily the first time `token` is resolved.
|
|
196
|
+
*
|
|
197
|
+
* @remarks
|
|
198
|
+
* Booting the provider (its `onRegister`/`onBooting`/`onBooted` hooks) only
|
|
199
|
+
* runs when {@link Container._app} is set, i.e. inside a real application boot.
|
|
200
|
+
*
|
|
201
|
+
* @category Binding
|
|
202
|
+
*/
|
|
203
|
+
defer(token: unknown, provider: new (app: unknown) => unknown): this {
|
|
204
|
+
this.deferred.set(token, provider);
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Resolution — public surface ───────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Resolve a binding asynchronously.
|
|
212
|
+
* This is the primary resolution method. Always use this over makeSync()
|
|
213
|
+
* unless you have pre-resolved the singleton during onBooting().
|
|
214
|
+
*
|
|
215
|
+
* @param token - The class, abstract class, or registry key to resolve.
|
|
216
|
+
* @param consumer - Optional resolving consumer, used to apply a contextual
|
|
217
|
+
* override registered via {@link for}.
|
|
218
|
+
* @returns The resolved instance.
|
|
219
|
+
* @throws {BindingNotFoundError} When no binding exists and the token cannot be auto-wired.
|
|
220
|
+
* @throws {CircularDependencyError} When resolving `token` re-enters itself through its dependency chain.
|
|
221
|
+
* @throws {ScopedOutsideRequestError} When `token` is a scoped binding resolved outside a request scope.
|
|
222
|
+
* @category Resolution
|
|
223
|
+
* @example
|
|
224
|
+
* ```ts
|
|
225
|
+
* const users = await container.make(UserService);
|
|
226
|
+
* const config = await container.make("config");
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
async make<T>(token: BindingToken<T>, consumer?: unknown): Promise<T> {
|
|
230
|
+
return this._make<T>(token, consumer, []);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Resolve a binding synchronously.
|
|
235
|
+
* Only works for: value bindings, and singleton bindings already resolved.
|
|
236
|
+
* Throws SyncResolutionError for everything else.
|
|
237
|
+
* Use this exclusively in Facade accessors — after onBooting() has run.
|
|
238
|
+
*
|
|
239
|
+
* @throws {BindingNotFoundError} When no binding is registered for the token.
|
|
240
|
+
* @throws {SyncResolutionError} When the binding is a not-yet-resolved singleton, or is transient/scoped (which cannot resolve synchronously).
|
|
241
|
+
* @category Resolution
|
|
242
|
+
*/
|
|
243
|
+
makeSync<T>(token: BindingToken<T>): T {
|
|
244
|
+
const canonical = this._resolveAlias(token);
|
|
245
|
+
const binding = this.registry.get(canonical);
|
|
246
|
+
|
|
247
|
+
if (!binding) {
|
|
248
|
+
throw new BindingNotFoundError(
|
|
249
|
+
typeof canonical === "function" ? (canonical as Function).name : String(canonical),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (binding.kind === "value") return binding.instance as T;
|
|
254
|
+
|
|
255
|
+
if (binding.kind === "singleton") {
|
|
256
|
+
if (binding.resolved) return binding.instance as T;
|
|
257
|
+
throw new SyncResolutionError(
|
|
258
|
+
`Singleton '${String(canonical)}' has not been resolved yet. ` +
|
|
259
|
+
`Pre-resolve it in a ServiceProvider's onBooting() method before making sync calls.`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
throw new SyncResolutionError(
|
|
264
|
+
`'${String(canonical)}' cannot be resolved synchronously. ` +
|
|
265
|
+
`Use make() instead, or pre-resolve in a ServiceProvider.`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Construct a class by auto-wiring its declared dependencies (`@inject()`),
|
|
271
|
+
* bypassing any registered binding for that token.
|
|
272
|
+
*
|
|
273
|
+
* Used by the `app/services` convention to build a fresh instance inside a
|
|
274
|
+
* singleton/scoped factory without the factory shadowing auto-wiring.
|
|
275
|
+
*
|
|
276
|
+
* @throws {CircularDependencyError} When the class's dependency graph contains a cycle.
|
|
277
|
+
* @category Resolution
|
|
278
|
+
*/
|
|
279
|
+
async build<T>(ctor: new (...args: unknown[]) => T): Promise<T> {
|
|
280
|
+
return this._autoWire<T>(ctor, []);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Remove a binding. Returns `true` if a binding existed for the token.
|
|
285
|
+
* Follows the alias chain so `forget(alias)` removes the canonical binding.
|
|
286
|
+
*
|
|
287
|
+
* @category Resolution
|
|
288
|
+
*/
|
|
289
|
+
forget(token: BindingToken): boolean {
|
|
290
|
+
return this.registry.delete(this._resolveAlias(token));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Report whether a binding — or a deferred provider — exists for `token`,
|
|
295
|
+
* **without constructing anything**. Follows the alias chain.
|
|
296
|
+
*
|
|
297
|
+
* Used by the boot-time doctor to verify that every token a provider names in
|
|
298
|
+
* `static provides` is actually wired.
|
|
299
|
+
*
|
|
300
|
+
* @category Resolution
|
|
301
|
+
*/
|
|
302
|
+
bound(token: BindingToken): boolean {
|
|
303
|
+
const canonical = this._resolveAlias(token);
|
|
304
|
+
return this.registry.has(canonical) || this.deferred.has(canonical);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Report whether `token` resolves through a deferred provider (booted lazily
|
|
309
|
+
* on first `make()`), as opposed to an eagerly-registered binding. Follows the
|
|
310
|
+
* alias chain.
|
|
311
|
+
*
|
|
312
|
+
* @category Resolution
|
|
313
|
+
*/
|
|
314
|
+
isDeferred(token: BindingToken): boolean {
|
|
315
|
+
return this.deferred.has(this._resolveAlias(token));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Resolution — private implementation ──────────────────────────────
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Internal recursive resolver. _chain is an immutable array that travels
|
|
322
|
+
* down the call chain to detect circular dependencies.
|
|
323
|
+
* NEVER stored on the instance — safe under concurrent async calls.
|
|
324
|
+
*/
|
|
325
|
+
private async _make<T>(
|
|
326
|
+
token: BindingToken<T>,
|
|
327
|
+
consumer: unknown,
|
|
328
|
+
_chain: readonly unknown[],
|
|
329
|
+
): Promise<T> {
|
|
330
|
+
// Step 1: Resolve alias to canonical token
|
|
331
|
+
const canonical = this._resolveAlias(token);
|
|
332
|
+
|
|
333
|
+
// Step 2: Boot deferred provider if this token has one pending
|
|
334
|
+
if (this.deferred.has(canonical)) {
|
|
335
|
+
await this._bootDeferredProvider(canonical);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Step 3: Check for contextual override for this specific consumer
|
|
339
|
+
const binding =
|
|
340
|
+
consumer !== undefined
|
|
341
|
+
? (this.contextual.get(this._resolveAlias(consumer as BindingToken))?.get(canonical) ??
|
|
342
|
+
this.registry.get(canonical))
|
|
343
|
+
: this.registry.get(canonical);
|
|
344
|
+
|
|
345
|
+
if (binding) {
|
|
346
|
+
return this._resolveBinding<T>(binding as Binding<T>, canonical, _chain);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Step 4: Attempt auto-wiring via @inject() metadata
|
|
350
|
+
const ctor = canonical as new (...args: unknown[]) => T;
|
|
351
|
+
if (typeof canonical === "function" && injectRegistry.has(ctor)) {
|
|
352
|
+
return this._autoWire<T>(ctor, _chain);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
throw new BindingNotFoundError(
|
|
356
|
+
typeof canonical === "function" ? (canonical as Function).name : String(canonical),
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private async _resolveBinding<T>(
|
|
361
|
+
binding: Binding<T>,
|
|
362
|
+
token: unknown,
|
|
363
|
+
_chain: readonly unknown[],
|
|
364
|
+
): Promise<T> {
|
|
365
|
+
let instance: T;
|
|
366
|
+
|
|
367
|
+
switch (binding.kind) {
|
|
368
|
+
case "value":
|
|
369
|
+
instance = binding.instance;
|
|
370
|
+
break;
|
|
371
|
+
|
|
372
|
+
case "singleton":
|
|
373
|
+
if (binding.resolved) {
|
|
374
|
+
instance = binding.instance!;
|
|
375
|
+
} else {
|
|
376
|
+
instance = await this._resolveWithLock(binding, this._guardCycle(token, _chain));
|
|
377
|
+
}
|
|
378
|
+
break;
|
|
379
|
+
|
|
380
|
+
case "scoped": {
|
|
381
|
+
const scoped = this._scopedStore.getStore();
|
|
382
|
+
if (!scoped) {
|
|
383
|
+
throw new ScopedOutsideRequestError(
|
|
384
|
+
`Attempted to resolve a scoped binding outside of a request. ` +
|
|
385
|
+
`Scoped bindings are only available inside Bun.serve() fetch handlers.`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
const nextChain = this._guardCycle(token, _chain);
|
|
389
|
+
instance = await scoped.resolve<T>(token, () => binding.factory(this._chained(nextChain)));
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
case "transient":
|
|
394
|
+
instance = await Promise.resolve(
|
|
395
|
+
binding.factory(this._chained(this._guardCycle(token, _chain))),
|
|
396
|
+
);
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Fire resolving() hooks after construction
|
|
401
|
+
const hooks = this.resolvingHooks.get(token);
|
|
402
|
+
if (hooks) hooks.forEach((hook) => hook(instance));
|
|
403
|
+
|
|
404
|
+
return instance;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Singleton concurrency lock.
|
|
409
|
+
* If two concurrent make() calls both find resolved=false, only one runs
|
|
410
|
+
* the factory. The second waits on the same promise via this.pending.
|
|
411
|
+
*/
|
|
412
|
+
private async _resolveWithLock<T>(
|
|
413
|
+
binding: SingletonBinding<T>,
|
|
414
|
+
_chain: readonly unknown[],
|
|
415
|
+
): Promise<T> {
|
|
416
|
+
if (this.pending.has(binding)) {
|
|
417
|
+
return this.pending.get(binding) as Promise<T>;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const promise = Promise.resolve(binding.factory(this._chained(_chain)))
|
|
421
|
+
.then((instance) => {
|
|
422
|
+
binding.instance = instance;
|
|
423
|
+
binding.resolved = true;
|
|
424
|
+
return instance;
|
|
425
|
+
})
|
|
426
|
+
.finally(() => {
|
|
427
|
+
// Always clear the lock — a factory that rejects must be retryable on
|
|
428
|
+
// the next make() instead of caching the rejection forever.
|
|
429
|
+
this.pending.delete(binding);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
this.pending.set(binding, promise);
|
|
433
|
+
return promise;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Throw {@link CircularDependencyError} when `token` is already being
|
|
438
|
+
* resolved somewhere up the current chain, otherwise return the extended
|
|
439
|
+
* chain (a NEW array — `_chain` is never mutated, keeping resolution safe
|
|
440
|
+
* under concurrent async calls).
|
|
441
|
+
*/
|
|
442
|
+
private _guardCycle(token: unknown, _chain: readonly unknown[]): readonly unknown[] {
|
|
443
|
+
if (_chain.includes(token)) {
|
|
444
|
+
const errorChain = [..._chain, token].map((entry) =>
|
|
445
|
+
typeof entry === "function" ? (entry as Function).name : String(entry),
|
|
446
|
+
);
|
|
447
|
+
throw new CircularDependencyError(errorChain);
|
|
448
|
+
}
|
|
449
|
+
return [..._chain, token];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* A lightweight view of this container whose `make()` threads the current
|
|
454
|
+
* resolution chain. Passed to binding factories so that factories which
|
|
455
|
+
* `await container.make(...)` participate in cycle detection — two factories
|
|
456
|
+
* awaiting each other throw {@link CircularDependencyError} instead of
|
|
457
|
+
* deadlocking. Everything else delegates to the real container via the
|
|
458
|
+
* prototype chain; no state lives on the view itself.
|
|
459
|
+
*/
|
|
460
|
+
private _chained(chain: readonly unknown[]): Container {
|
|
461
|
+
const chained = Object.create(this) as Container;
|
|
462
|
+
chained.make = <T>(token: BindingToken<T>, consumer?: unknown): Promise<T> =>
|
|
463
|
+
this._make<T>(token, consumer, chain);
|
|
464
|
+
return chained;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Auto-wire a class by reading its @inject() metadata.
|
|
469
|
+
* _chain is immutable — each recursive call creates a new array.
|
|
470
|
+
* No instance-level stack → safe under concurrent async resolution.
|
|
471
|
+
*/
|
|
472
|
+
private async _autoWire<T>(
|
|
473
|
+
token: new (...args: unknown[]) => T,
|
|
474
|
+
_chain: readonly unknown[],
|
|
475
|
+
): Promise<T> {
|
|
476
|
+
// Cycle check + build next chain — new array, never mutates _chain
|
|
477
|
+
const nextChain = this._guardCycle(token, _chain);
|
|
478
|
+
|
|
479
|
+
const deps: BindingToken[] = (injectRegistry.get(token) as BindingToken[] | undefined) ?? [];
|
|
480
|
+
|
|
481
|
+
// Resolve all dependencies in parallel, each carrying nextChain
|
|
482
|
+
const resolvedDeps = await Promise.all(deps.map((dep) => this._make(dep, token, nextChain)));
|
|
483
|
+
|
|
484
|
+
const instance = new token(...resolvedDeps);
|
|
485
|
+
|
|
486
|
+
const hooks = this.resolvingHooks.get(token);
|
|
487
|
+
if (hooks) hooks.forEach((hook) => hook(instance));
|
|
488
|
+
|
|
489
|
+
return instance;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private async _bootDeferredProvider(token: unknown): Promise<void> {
|
|
493
|
+
const ProviderClass = this.deferred.get(token);
|
|
494
|
+
if (!ProviderClass) return;
|
|
495
|
+
|
|
496
|
+
// Remove from deferred map so this only runs once
|
|
497
|
+
this.deferred.delete(token);
|
|
498
|
+
|
|
499
|
+
const app = this._app;
|
|
500
|
+
if (!app) return; // no app context — skip (unit test scenario)
|
|
501
|
+
|
|
502
|
+
const provider = new (
|
|
503
|
+
ProviderClass as new (
|
|
504
|
+
app: unknown,
|
|
505
|
+
) => import("../provider/ServiceProvider.ts").ServiceProvider
|
|
506
|
+
)(app);
|
|
507
|
+
|
|
508
|
+
provider.onRegister();
|
|
509
|
+
await provider.onBooting();
|
|
510
|
+
await provider.onBooted();
|
|
511
|
+
|
|
512
|
+
// Track so stop() can call onStopping/onStopped
|
|
513
|
+
app._activeProviders.push(provider);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ── Scoped resolver lifecycle ─────────────────────────────────────────
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Run `callback` inside a fresh request scope.
|
|
520
|
+
*
|
|
521
|
+
* A new `ScopedResolver` is created for the duration of the call and stored
|
|
522
|
+
* in `AsyncLocalStorage` so every `container.make()` for a scoped binding
|
|
523
|
+
* resolves against **this** scope — even across `await` boundaries and even
|
|
524
|
+
* when multiple requests are in flight concurrently. The resolver is flushed
|
|
525
|
+
* (cache cleared) in a `finally` block, and the ALS entry is garbage-collected
|
|
526
|
+
* automatically when the async context exits. There is no shared mutable
|
|
527
|
+
* state on the container and therefore no possibility of cross-request leaks.
|
|
528
|
+
*
|
|
529
|
+
* @param callback Receives the resolver so the caller can pass it to
|
|
530
|
+
* `HttpContext` (and therefore to `afterResponse()` hooks).
|
|
531
|
+
* @category Scopes
|
|
532
|
+
* @example
|
|
533
|
+
* ```ts
|
|
534
|
+
* await container.runScoped(async (scoped) => {
|
|
535
|
+
* // scoped bindings resolve to per-request instances inside here
|
|
536
|
+
* const session = await container.make(RequestSession);
|
|
537
|
+
* return handle(session);
|
|
538
|
+
* });
|
|
539
|
+
* ```
|
|
540
|
+
*/
|
|
541
|
+
runScoped<T>(callback: (scoped: ScopedResolver) => Promise<T>): Promise<T> {
|
|
542
|
+
const scoped = new ScopedResolver(this);
|
|
543
|
+
return this._scopedStore.run(scoped, () => callback(scoped).finally(() => scoped.flush()));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Create a bare `ScopedResolver` without entering an ALS context.
|
|
548
|
+
* Useful in unit tests that call `scoped.resolve()` directly and do not
|
|
549
|
+
* need `container.make()` to route through the ALS store.
|
|
550
|
+
*
|
|
551
|
+
* @category Scopes
|
|
552
|
+
*/
|
|
553
|
+
createScopedResolver(): ScopedResolver {
|
|
554
|
+
return new ScopedResolver(this);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ── Contextual binding internals ──────────────────────────────────────
|
|
558
|
+
|
|
559
|
+
/** @internal Record a contextual binding for `consumer`; called by `ContextualBindingBuilder`. */
|
|
560
|
+
_setContextual(consumer: unknown, dependency: unknown, binding: Binding<unknown>): void {
|
|
561
|
+
if (!this.contextual.has(consumer)) {
|
|
562
|
+
this.contextual.set(consumer, new Map());
|
|
563
|
+
}
|
|
564
|
+
this.contextual.get(consumer)!.set(dependency, binding);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** @internal Follow the alias chain to the canonical token, guarding against cycles. */
|
|
568
|
+
_resolveAlias(token: unknown): unknown {
|
|
569
|
+
let current = token;
|
|
570
|
+
const visited = new Set<unknown>();
|
|
571
|
+
while (this.aliases.has(current)) {
|
|
572
|
+
if (visited.has(current)) break;
|
|
573
|
+
visited.add(current);
|
|
574
|
+
current = this.aliases.get(current);
|
|
575
|
+
}
|
|
576
|
+
return current;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Attempt to resolve a binding synchronously.
|
|
581
|
+
* Returns undefined instead of throwing if the key is not registered.
|
|
582
|
+
* Used by providers to check whether CommandRunner exists
|
|
583
|
+
* (it only does in console mode, not web mode).
|
|
584
|
+
*
|
|
585
|
+
* @category Resolution
|
|
586
|
+
*/
|
|
587
|
+
tryMake<K extends keyof ContainerBindings>(token: K): ContainerBindings[K] | undefined {
|
|
588
|
+
if (!this.registry.has(token as unknown)) return undefined;
|
|
589
|
+
try {
|
|
590
|
+
return this.makeSync(token) as ContainerBindings[K];
|
|
591
|
+
} catch {
|
|
592
|
+
return undefined;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ── Static factory ─────────────────────────────────────────────────────
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Create a fresh container with no bindings registered.
|
|
600
|
+
*
|
|
601
|
+
* @category Binding
|
|
602
|
+
*/
|
|
603
|
+
static createEmpty(): Container {
|
|
604
|
+
return new Container();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fluent builder for contextual bindings: registers a dependency that is
|
|
3
|
+
* resolved differently when injected into one specific consumer.
|
|
4
|
+
*/
|
|
5
|
+
import type { Container } from "./Container.ts";
|
|
6
|
+
import type { Binding, BindingToken, Factory } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Configures how a single consumer receives a given dependency.
|
|
10
|
+
*
|
|
11
|
+
* Created via `Container.for(consumer)`; the `give*` methods return the
|
|
12
|
+
* container so registration reads as one fluent statement.
|
|
13
|
+
*/
|
|
14
|
+
export class ContextualBindingBuilder<Consumer> {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly container: Container,
|
|
17
|
+
private readonly consumer: BindingToken<Consumer>,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Canonicalise a token through the alias map at registration time.
|
|
22
|
+
* This ensures the contextual map is always keyed by canonical tokens,
|
|
23
|
+
* so _make()'s alias-then-contextual lookup always matches.
|
|
24
|
+
*/
|
|
25
|
+
private canonical(token: unknown): unknown {
|
|
26
|
+
return this.container._resolveAlias(token);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Give this consumer a fresh instance of `dependency` on each resolution */
|
|
30
|
+
give<D>(dependency: BindingToken<D>, factory: Factory<D>): Container {
|
|
31
|
+
this.container._setContextual(this.canonical(this.consumer), this.canonical(dependency), {
|
|
32
|
+
kind: "transient",
|
|
33
|
+
factory,
|
|
34
|
+
} as Binding<D>);
|
|
35
|
+
return this.container;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Give this consumer a singleton instance of `dependency` */
|
|
39
|
+
giveSingleton<D>(dependency: BindingToken<D>, factory: Factory<D>): Container {
|
|
40
|
+
this.container._setContextual(this.canonical(this.consumer), this.canonical(dependency), {
|
|
41
|
+
kind: "singleton",
|
|
42
|
+
factory,
|
|
43
|
+
instance: undefined,
|
|
44
|
+
resolved: false,
|
|
45
|
+
} as Binding<D>);
|
|
46
|
+
return this.container;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Give this consumer a pre-built value for `dependency` */
|
|
50
|
+
giveValue<D>(dependency: BindingToken<D>, instance: D): Container {
|
|
51
|
+
this.container._setContextual(this.canonical(this.consumer), this.canonical(dependency), {
|
|
52
|
+
kind: "value",
|
|
53
|
+
instance,
|
|
54
|
+
} as Binding<D>);
|
|
55
|
+
return this.container;
|
|
56
|
+
}
|
|
57
|
+
}
|