@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.
Files changed (201) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +128 -0
  4. package/package.json +72 -0
  5. package/src/application/Application.ts +1671 -0
  6. package/src/application/BootDoctor.ts +108 -0
  7. package/src/application/DevErrorPage.ts +567 -0
  8. package/src/application/ExceptionHandler.ts +183 -0
  9. package/src/application/currentApp.ts +73 -0
  10. package/src/assets/assets.ts +79 -0
  11. package/src/assets/index.ts +16 -0
  12. package/src/auth/AuthenticatedUser.ts +18 -0
  13. package/src/build/PackageLinter.ts +146 -0
  14. package/src/build/PackageScaffold.ts +127 -0
  15. package/src/build/codemod.ts +64 -0
  16. package/src/build/index.ts +12 -0
  17. package/src/command/Command.ts +254 -0
  18. package/src/command/CommandRunner.ts +593 -0
  19. package/src/command/OutputWriter.ts +61 -0
  20. package/src/command/builtin/CompileCommand.ts +46 -0
  21. package/src/command/builtin/CssBuildCommand.ts +71 -0
  22. package/src/command/builtin/KeyGenerateCommand.ts +58 -0
  23. package/src/command/builtin/LintPackagesCommand.ts +72 -0
  24. package/src/command/builtin/MakeCommandCommand.ts +85 -0
  25. package/src/command/builtin/MakeControllerCommand.ts +95 -0
  26. package/src/command/builtin/MakeEventCommand.ts +85 -0
  27. package/src/command/builtin/MakeJobCommand.ts +53 -0
  28. package/src/command/builtin/MakeListenerCommand.ts +35 -0
  29. package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
  30. package/src/command/builtin/MakeNotificationCommand.ts +48 -0
  31. package/src/command/builtin/MakeObserverCommand.ts +78 -0
  32. package/src/command/builtin/MakePackageCommand.ts +45 -0
  33. package/src/command/builtin/MakePolicyCommand.ts +66 -0
  34. package/src/command/builtin/MakeProviderCommand.ts +75 -0
  35. package/src/command/builtin/MakeRequestCommand.ts +47 -0
  36. package/src/command/builtin/MakeResourceCommand.ts +61 -0
  37. package/src/command/builtin/MakeTestCommand.ts +120 -0
  38. package/src/command/builtin/ReloadCommand.ts +52 -0
  39. package/src/command/builtin/ReplCommand.ts +174 -0
  40. package/src/command/builtin/RouteListCommand.ts +188 -0
  41. package/src/command/builtin/ServeCommand.ts +321 -0
  42. package/src/command/builtin/StartCommand.ts +3 -0
  43. package/src/command/builtin/StatusCommand.ts +71 -0
  44. package/src/command/builtin/TestCommand.ts +172 -0
  45. package/src/command/builtin/WorkerCommand.ts +27 -0
  46. package/src/command/builtin/index.ts +53 -0
  47. package/src/command/scaffold/worker.ts.txt +12 -0
  48. package/src/command/scaffold/zerotal.ts.txt +26 -0
  49. package/src/command/startZerotal.ts +55 -0
  50. package/src/config/AppConfig.ts +253 -0
  51. package/src/config/ConfigLoader.ts +117 -0
  52. package/src/config/ConfigManager.ts +169 -0
  53. package/src/config/index.ts +46 -0
  54. package/src/config/registry.ts +59 -0
  55. package/src/config/validation.ts +117 -0
  56. package/src/container/Container.ts +606 -0
  57. package/src/container/ContextualBindingBuilder.ts +57 -0
  58. package/src/container/ScopedResolver.ts +117 -0
  59. package/src/container/index.ts +32 -0
  60. package/src/container/inject.ts +55 -0
  61. package/src/container/types.ts +71 -0
  62. package/src/context/RequestContext.ts +91 -0
  63. package/src/contracts/auth.ts +24 -0
  64. package/src/contracts/index.ts +23 -0
  65. package/src/contracts/session.ts +70 -0
  66. package/src/contracts/transaction.ts +26 -0
  67. package/src/conventions/ConventionLoader.ts +128 -0
  68. package/src/conventions/builtinConcerns.ts +131 -0
  69. package/src/crypt/Crypt.ts +141 -0
  70. package/src/crypt/URLSigner.ts +96 -0
  71. package/src/datetime/Carbon.ts +1396 -0
  72. package/src/datetime/CarbonInterval.ts +421 -0
  73. package/src/datetime/clock.ts +28 -0
  74. package/src/datetime/index.ts +23 -0
  75. package/src/datetime/temporal-shim.ts +1 -0
  76. package/src/dev/BuildOutput.ts +131 -0
  77. package/src/dev/CssPlugins.ts +184 -0
  78. package/src/dev/DevBuildHook.ts +74 -0
  79. package/src/dev/DevOrchestrator.ts +213 -0
  80. package/src/dev/DevReloadMiddleware.ts +101 -0
  81. package/src/dev/DevReloadServer.ts +85 -0
  82. package/src/dev/DevWsServer.ts +45 -0
  83. package/src/dev/index.ts +19 -0
  84. package/src/dev/reloadClient.ts +39 -0
  85. package/src/env/Def.ts +232 -0
  86. package/src/env/EnvSchema.ts +105 -0
  87. package/src/env/index.ts +34 -0
  88. package/src/env/t.ts +128 -0
  89. package/src/errors/ConfigError.ts +12 -0
  90. package/src/errors/ContainerErrors.ts +143 -0
  91. package/src/errors/HttpError.ts +127 -0
  92. package/src/errors/ValidationError.ts +19 -0
  93. package/src/errors/ZerotalError.ts +25 -0
  94. package/src/errors/index.ts +46 -0
  95. package/src/events/CallQueuedListener.ts +66 -0
  96. package/src/events/Emitter.ts +280 -0
  97. package/src/events/EventFake.ts +160 -0
  98. package/src/events/FrameworkEvents.ts +252 -0
  99. package/src/facade/Facade.ts +101 -0
  100. package/src/facade/facades/App.ts +155 -0
  101. package/src/facade/facades/Artisan.ts +63 -0
  102. package/src/facade/facades/Config.ts +21 -0
  103. package/src/facade/facades/Events.ts +19 -0
  104. package/src/facade/facades/index.ts +28 -0
  105. package/src/global.d.ts +9 -0
  106. package/src/hash/Hash.ts +60 -0
  107. package/src/health/Health.ts +221 -0
  108. package/src/health/index.ts +27 -0
  109. package/src/helpers/Collection.ts +435 -0
  110. package/src/helpers/config.ts +59 -0
  111. package/src/helpers/fluent.ts +52 -0
  112. package/src/helpers/html.ts +11 -0
  113. package/src/helpers/index.ts +266 -0
  114. package/src/helpers/make.ts +35 -0
  115. package/src/helpers/markdown.ts +73 -0
  116. package/src/helpers/pageElements.ts +27 -0
  117. package/src/helpers/request.ts +62 -0
  118. package/src/helpers/response.ts +411 -0
  119. package/src/helpers/str.ts +208 -0
  120. package/src/http/Http.ts +298 -0
  121. package/src/http/HttpClient.ts +289 -0
  122. package/src/http/Resource.ts +171 -0
  123. package/src/http/UploadedFile.ts +204 -0
  124. package/src/http/Uri.ts +490 -0
  125. package/src/http/index.ts +46 -0
  126. package/src/http/negotiate.ts +213 -0
  127. package/src/http/originGuard.ts +76 -0
  128. package/src/http/sniffContentType.ts +105 -0
  129. package/src/http/url.ts +204 -0
  130. package/src/http/withHeaders.ts +24 -0
  131. package/src/index.ts +250 -0
  132. package/src/lock/LockManager.ts +228 -0
  133. package/src/lock/config.ts +49 -0
  134. package/src/lock/drivers/LockDriver.ts +32 -0
  135. package/src/lock/drivers/MemoryLockDriver.ts +52 -0
  136. package/src/lock/drivers/RedisLockDriver.ts +58 -0
  137. package/src/lock/drivers/SqliteLockDriver.ts +85 -0
  138. package/src/lock/errors.ts +20 -0
  139. package/src/lock/facades/Lock.ts +114 -0
  140. package/src/lock/index.ts +53 -0
  141. package/src/logger/Log.ts +35 -0
  142. package/src/logger/LogManager.ts +430 -0
  143. package/src/logger/LoggerMiddleware.ts +125 -0
  144. package/src/logger/channels/ConsoleChannel.ts +139 -0
  145. package/src/logger/channels/DailyChannel.ts +74 -0
  146. package/src/logger/channels/NullChannel.ts +17 -0
  147. package/src/logger/channels/SingleChannel.ts +34 -0
  148. package/src/logger/channels/StackChannel.ts +29 -0
  149. package/src/logger/config.ts +90 -0
  150. package/src/logger/format.ts +96 -0
  151. package/src/logger/frameworkLog.ts +93 -0
  152. package/src/logger/index.ts +68 -0
  153. package/src/logger/renderTable.ts +111 -0
  154. package/src/logger/types.ts +212 -0
  155. package/src/macros/config.macro.ts +50 -0
  156. package/src/metrics/HttpMetrics.ts +114 -0
  157. package/src/metrics/index.ts +18 -0
  158. package/src/middleware/BaseMiddleware.ts +72 -0
  159. package/src/middleware/CorsMiddleware.ts +152 -0
  160. package/src/middleware/RateLimiter.ts +255 -0
  161. package/src/middleware/SecureHeadersMiddleware.ts +127 -0
  162. package/src/middleware/ThrottleMiddleware.ts +252 -0
  163. package/src/middleware/WebhookMiddleware.ts +204 -0
  164. package/src/pipeline/ContextRegistry.ts +42 -0
  165. package/src/pipeline/HttpContext.ts +865 -0
  166. package/src/pipeline/Pipeline.ts +150 -0
  167. package/src/pipeline/currentPage.ts +46 -0
  168. package/src/pipeline/types.ts +80 -0
  169. package/src/provider/LockProvider.ts +64 -0
  170. package/src/provider/LogProvider.ts +137 -0
  171. package/src/provider/ServiceProvider.ts +84 -0
  172. package/src/provider/StorageProvider.ts +45 -0
  173. package/src/router/FileRouter.ts +526 -0
  174. package/src/router/Route.ts +76 -0
  175. package/src/router/RouteHandler.ts +335 -0
  176. package/src/router/Router.ts +1247 -0
  177. package/src/router/domain.ts +65 -0
  178. package/src/security/index.ts +22 -0
  179. package/src/storage/FakeDisk.ts +233 -0
  180. package/src/storage/StorageFilesMiddleware.ts +150 -0
  181. package/src/storage/StorageManager.ts +173 -0
  182. package/src/storage/config.ts +47 -0
  183. package/src/storage/drivers/LocalDriver.ts +138 -0
  184. package/src/storage/drivers/S3Driver.ts +169 -0
  185. package/src/storage/errors.ts +135 -0
  186. package/src/storage/facades/Storage.ts +3 -0
  187. package/src/storage/global.d.ts +7 -0
  188. package/src/storage/index.ts +22 -0
  189. package/src/storage/root.ts +59 -0
  190. package/src/storage/types.ts +104 -0
  191. package/src/support/appKey.ts +38 -0
  192. package/src/support/cookie.ts +72 -0
  193. package/src/support/crypto.ts +52 -0
  194. package/src/support/deepMerge.ts +117 -0
  195. package/src/support/env.ts +71 -0
  196. package/src/support/network.ts +79 -0
  197. package/src/support/port.ts +197 -0
  198. package/src/support/str.ts +122 -0
  199. package/src/view/FileRouteResolver.ts +59 -0
  200. package/src/view/index.ts +144 -0
  201. 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
+ }