@inferdi/koa 4.0.8

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 InferDI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # @inferdi/koa
2
+
3
+ <div align="center">
4
+ <img src="https://raw.githubusercontent.com/inferdi/inferdi/main/assets/logo.png" alt="InferDI" width="150" height="150" />
5
+
6
+ [![JSR](https://jsr.io/badges/@inferdi/koa)](https://jsr.io/@inferdi/koa)
7
+ [![npm version](https://img.shields.io/npm/v/@inferdi/koa)](https://www.npmjs.com/package/@inferdi/koa)
8
+ ![License](https://img.shields.io/npm/l/@inferdi/koa.svg)
9
+
10
+ Koa request-scope middleware for [InferDI](https://github.com/inferdi/inferdi).
11
+ </div>
12
+
13
+ > **Part of the [InferDI](https://github.com/inferdi/inferdi) project** — a
14
+ > zero-dependency, decorator-free, strongly typed DI container for TypeScript.
15
+ > Core package: [`@inferdi/inferdi`](https://www.npmjs.com/package/@inferdi/inferdi)
16
+ > ([JSR](https://jsr.io/@inferdi/inferdi)).
17
+
18
+ This middleware wires InferDI into Koa v3 without decorators, reflection,
19
+ controller scanning, router patching, `app.context` mutation, or handler
20
+ parameter injection. Your application still builds an explicit InferDI graph and
21
+ resolves services with `.get(key)` — the adapter only manages the per-request
22
+ scope.
23
+
24
+ ## Table of Contents
25
+
26
+ - [Install](#install)
27
+ - [Request Scope](#request-scope)
28
+ - [Options](#options)
29
+ - [Streaming](#streaming)
30
+ - [API](#api)
31
+ - [Related](#related)
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pnpm add @inferdi/inferdi @inferdi/koa koa
37
+ pnpm add -D @types/koa
38
+ # or
39
+ deno add jsr:@inferdi/inferdi jsr:@inferdi/koa npm:koa
40
+ ```
41
+
42
+ ```ts
43
+ import Koa from 'koa'
44
+ import { inferdiKoa } from '@inferdi/koa'
45
+ ```
46
+
47
+ Koa publishes JavaScript and keeps TypeScript declarations in `@types/koa`.
48
+ `@inferdi/koa` lists that package as an optional peer so TypeScript consumers
49
+ can keep Koa's own context types in their application dependency graph.
50
+
51
+ ## Request Scope
52
+
53
+ The middleware creates one InferDI scope per request, exposes it as
54
+ `ctx.state.di`, and disposes it after the underlying Node response finishes or
55
+ the connection closes.
56
+
57
+ ```ts
58
+ import Koa from 'koa'
59
+ import { inferdiKoa, type InferdiScopeOf } from '@inferdi/koa'
60
+ import { buildRootContainer } from './container.js'
61
+
62
+ const root = buildRootContainer()
63
+
64
+ declare module 'koa' {
65
+ interface DefaultState {
66
+ di: InferdiScopeOf<typeof root>
67
+ }
68
+ }
69
+
70
+ const app = new Koa()
71
+
72
+ app.use(inferdiKoa({
73
+ container: root,
74
+ setupScope: (scope, ctx) => {
75
+ const request = scope.get('request')
76
+ request.requestId = crypto.randomUUID()
77
+ request.userId = ctx.get('x-user-id') || undefined
78
+ request.ip = ctx.ip
79
+ },
80
+ }))
81
+
82
+ app.use(async (ctx) => {
83
+ const id = ctx.path.split('/').pop() ?? ''
84
+ ctx.body = await ctx.state.di.get('users').profile(id)
85
+ })
86
+ ```
87
+
88
+ For a custom state key, pass `key` and publish that key in your Koa state type:
89
+
90
+ ```ts
91
+ import type { DefaultState, ParameterizedContext } from 'koa'
92
+ import {
93
+ inferdiKoa,
94
+ type InferdiKoaState,
95
+ type InferdiScopeOf,
96
+ } from '@inferdi/koa'
97
+
98
+ type AppState =
99
+ & DefaultState
100
+ & InferdiKoaState<InferdiScopeOf<typeof root>, 'container'>
101
+ type AppContext = ParameterizedContext<AppState>
102
+
103
+ app.use(inferdiKoa({ container: root, key: 'container' }))
104
+
105
+ app.use(async (ctx: AppContext) => {
106
+ ctx.body = await ctx.state.container.get('users').profile('42')
107
+ })
108
+ ```
109
+
110
+ The package does not globally augment Koa state with `any`, `unknown`, or a base
111
+ container. You own the concrete state type through declaration merging or local
112
+ Koa generics.
113
+
114
+ ## Options
115
+
116
+ ```ts
117
+ app.use(inferdiKoa({
118
+ container: root,
119
+ createScope: (root, ctx) => root.createScope(),
120
+ setupScope: (scope, ctx) => {},
121
+ disposeScope: (scope, ctx) => scope.dispose(),
122
+ autoDispose: true,
123
+ onDisposeError: (error, ctx) => {
124
+ ctx.app.emit('error', error, ctx)
125
+ },
126
+ }))
127
+ ```
128
+
129
+ | Option | Default | Description |
130
+ | --- | --- | --- |
131
+ | `container` | — | **Required.** The root container. Must structurally provide `createScope()`. The root is never disposed by this middleware. |
132
+ | `key` | `'di'` | Koa `ctx.state` key used to expose the request scope. |
133
+ | `createScope` | `root.createScope()` | Overrides how a request scope is created. May be async. |
134
+ | `setupScope` | — | Hydrates the scope before it is exposed to downstream middleware. May be async. |
135
+ | `disposeScope` | `scope.dispose()` | Overrides request-scope disposal. May be async. |
136
+ | `autoDispose` | `true` | Set to `false`, or return `false`, when application code owns disposal. |
137
+ | `onDisposeError` | — | Optional sink for cleanup failures. Returning normally marks the cleanup error as handled. |
138
+
139
+ If `setupScope` fails after a scope has been created, the middleware disposes
140
+ that half-built scope before rethrowing. If setup cleanup also fails, the
141
+ failure is surfaced as an `AggregateError`.
142
+
143
+ Response-completion cleanup failures happen after Koa's `await next()` promise
144
+ chain. By default they are emitted through `ctx.app.emit('error', error, ctx)`
145
+ and swallowed so an already-completed response is not corrupted. If
146
+ `onDisposeError` throws or rejects, the adapter emits an `AggregateError`
147
+ containing both the original cleanup error and the handler error.
148
+
149
+ ## Streaming
150
+
151
+ Normal Koa stream bodies do not need a special skip. When `ctx.body` is a Node
152
+ stream, Koa pipes it through `ctx.res`, and this middleware waits for the
153
+ response `finish` or `close` event before disposing the scope.
154
+
155
+ Use `skipInferdiDispose(ctx)` only when application code intentionally keeps the
156
+ scope beyond the HTTP response boundary:
157
+
158
+ ```ts
159
+ import { skipInferdiDispose } from '@inferdi/koa'
160
+
161
+ app.use(async (ctx) => {
162
+ if (ctx.path !== '/background') return
163
+
164
+ skipInferdiDispose(ctx)
165
+ const scope = ctx.state.di
166
+
167
+ queue.add(async () => {
168
+ try {
169
+ await scope.get('jobs').run()
170
+ } finally {
171
+ await scope.dispose()
172
+ }
173
+ })
174
+
175
+ ctx.body = { status: 'queued' }
176
+ })
177
+ ```
178
+
179
+ If an application sets `ctx.respond = false` and writes to `ctx.res` manually,
180
+ the adapter still relies on `finish` or `close`. If the response is never ended
181
+ or closed, no middleware can know when to dispose the request scope.
182
+
183
+ ## API
184
+
185
+ ```ts
186
+ export type MaybePromise<T> = T | Promise<T>
187
+
188
+ export interface InferdiScope {
189
+ dispose(): MaybePromise<void>
190
+ }
191
+
192
+ export interface InferdiRoot<Scope extends InferdiScope = InferdiScope> {
193
+ createScope(): Scope
194
+ }
195
+
196
+ export type InferdiScopeOf<Root extends InferdiRoot> =
197
+ ReturnType<Root['createScope']>
198
+
199
+ export type InferdiKoaState<
200
+ Scope extends InferdiScope,
201
+ Key extends string = 'di',
202
+ > = { [P in Key]: Scope }
203
+
204
+ export type InferdiKoaContext<StateT, ContextT, Scope, Key> =
205
+ ParameterizedContext<StateT & InferdiKoaState<Scope, Key>, ContextT>
206
+
207
+ export interface InferdiKoaOptions<Root, StateT, ContextT, Key, Scope> {
208
+ /* ... */
209
+ }
210
+
211
+ export function inferdiKoa(options: InferdiKoaOptions): Middleware
212
+ export function skipInferdiDispose(context: Context): void
213
+ ```
214
+
215
+ ## Related
216
+
217
+ | Package | JSR | npm | Description |
218
+ | --- | --- | --- | --- |
219
+ | [`@inferdi/inferdi`](https://github.com/inferdi/inferdi/tree/main/packages/inferdi) | [JSR](https://jsr.io/@inferdi/inferdi) | [npm](https://www.npmjs.com/package/@inferdi/inferdi) | Core DI container — zero-dependency, decorator-free, strongly typed |
220
+ | [`@inferdi/fastify`](https://github.com/inferdi/inferdi/tree/main/packages/fastify) | [JSR](https://jsr.io/@inferdi/fastify) | [npm](https://www.npmjs.com/package/@inferdi/fastify) | Fastify v5 request-scope adapter |
221
+ | [`@inferdi/hono`](https://github.com/inferdi/inferdi/tree/main/packages/hono) | [JSR](https://jsr.io/@inferdi/hono) | [npm](https://www.npmjs.com/package/@inferdi/hono) | Hono request-scope middleware |
222
+ | [`@inferdi/koa`](https://github.com/inferdi/inferdi/tree/main/packages/koa) | [JSR](https://jsr.io/@inferdi/koa) | [npm](https://www.npmjs.com/package/@inferdi/koa) | Koa v3 request-scope middleware |
223
+ | [`@inferdi/elysia`](https://github.com/inferdi/inferdi/tree/main/packages/elysia) | [JSR](https://jsr.io/@inferdi/elysia) | [npm](https://www.npmjs.com/package/@inferdi/elysia) | Elysia request-scope plugin |
224
+
225
+ The project repository lives at [inferdi/inferdi](https://github.com/inferdi/inferdi). This adapter targets [Koa](https://koajs.com) v3.
package/dist/index.cjs ADDED
@@ -0,0 +1,167 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ var skippedContexts = /* @__PURE__ */ new WeakSet();
5
+ function isPromiseLike(value) {
6
+ return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
7
+ }
8
+ function cleanupError(errors) {
9
+ if (errors.length === 1) return errors[0];
10
+ return new AggregateError(errors, "InferDI Koa request cleanup failed");
11
+ }
12
+ function throwCollected(errors) {
13
+ if (errors.length === 1) {
14
+ throw errors[0];
15
+ }
16
+ throw new AggregateError(errors, "InferDI Koa request lifecycle failed");
17
+ }
18
+ function emitAppError(error, context) {
19
+ try {
20
+ context.app.emit("error", error, context);
21
+ } catch {
22
+ }
23
+ }
24
+ function emitUnhandledCleanupErrors(errors, context) {
25
+ if (errors.length === 0) return;
26
+ emitAppError(cleanupError(errors), context);
27
+ }
28
+ function handleCleanupError(error, context, onDisposeError, errors) {
29
+ if (onDisposeError === void 0) {
30
+ errors.push(error);
31
+ return;
32
+ }
33
+ try {
34
+ const handling = onDisposeError(error, context);
35
+ if (isPromiseLike(handling)) {
36
+ return handling.then(void 0, (handlerError) => {
37
+ errors.push(error);
38
+ errors.push(handlerError);
39
+ });
40
+ }
41
+ } catch (handlerError) {
42
+ errors.push(error);
43
+ errors.push(handlerError);
44
+ }
45
+ }
46
+ function disposeWithErrors(scope, context, disposeScope, onDisposeError, errors) {
47
+ try {
48
+ const disposing = disposeScope(scope, context);
49
+ if (isPromiseLike(disposing)) {
50
+ return disposing.then(
51
+ void 0,
52
+ (error) => handleCleanupError(
53
+ error,
54
+ context,
55
+ onDisposeError,
56
+ errors
57
+ )
58
+ );
59
+ }
60
+ } catch (error) {
61
+ return handleCleanupError(error, context, onDisposeError, errors);
62
+ }
63
+ }
64
+ async function disposeAfterSetupFailure(scope, context, setupError, disposeScope, onDisposeError) {
65
+ const errors = [setupError];
66
+ const disposing = disposeWithErrors(
67
+ scope,
68
+ context,
69
+ disposeScope,
70
+ onDisposeError,
71
+ errors
72
+ );
73
+ await disposing;
74
+ throwCollected(errors);
75
+ }
76
+ function skipInferdiDispose(context) {
77
+ skippedContexts.add(context);
78
+ }
79
+ function inferdiKoa(options) {
80
+ const root = options.container;
81
+ const key = options.key ?? "di";
82
+ const createScope = options.createScope ?? ((root2) => root2.createScope());
83
+ const setupScope = options.setupScope;
84
+ const disposeScope = options.disposeScope ?? ((scope) => scope.dispose());
85
+ const autoDispose = options.autoDispose;
86
+ const onDisposeError = options.onDisposeError;
87
+ return async function inferdiKoaMiddleware(context, next) {
88
+ const setupContext = context;
89
+ const typedContext = context;
90
+ const scopeResult = createScope(root, setupContext);
91
+ const scope = isPromiseLike(scopeResult) ? await scopeResult : scopeResult;
92
+ typedContext.state[key] = scope;
93
+ if (setupScope !== void 0) {
94
+ try {
95
+ const setupResult = setupScope(scope, setupContext);
96
+ if (isPromiseLike(setupResult)) {
97
+ await setupResult;
98
+ }
99
+ } catch (error) {
100
+ skippedContexts.delete(context);
101
+ await disposeAfterSetupFailure(
102
+ scope,
103
+ typedContext,
104
+ error,
105
+ disposeScope,
106
+ onDisposeError
107
+ );
108
+ }
109
+ }
110
+ let disposed = false;
111
+ const response = context.res;
112
+ const runCleanup = async () => {
113
+ if (disposed) return;
114
+ disposed = true;
115
+ const skipped = skippedContexts.delete(context);
116
+ if (skipped) return;
117
+ const errors = [];
118
+ let shouldDispose = autoDispose !== false;
119
+ if (typeof autoDispose === "function") {
120
+ try {
121
+ const autoDisposeResult = autoDispose(typedContext);
122
+ shouldDispose = isPromiseLike(autoDisposeResult) ? await autoDisposeResult !== false : autoDisposeResult !== false;
123
+ } catch (error) {
124
+ shouldDispose = true;
125
+ const handling = handleCleanupError(
126
+ error,
127
+ typedContext,
128
+ onDisposeError,
129
+ errors
130
+ );
131
+ if (isPromiseLike(handling)) {
132
+ await handling;
133
+ }
134
+ }
135
+ }
136
+ if (shouldDispose) {
137
+ const disposing = disposeWithErrors(
138
+ scope,
139
+ typedContext,
140
+ disposeScope,
141
+ onDisposeError,
142
+ errors
143
+ );
144
+ if (isPromiseLike(disposing)) {
145
+ await disposing;
146
+ }
147
+ }
148
+ emitUnhandledCleanupErrors(errors, context);
149
+ };
150
+ const onComplete = () => {
151
+ response.removeListener("finish", onComplete);
152
+ response.removeListener("close", onComplete);
153
+ void runCleanup().then(void 0, (error) => {
154
+ emitAppError(error, context);
155
+ });
156
+ };
157
+ response.on("finish", onComplete);
158
+ response.on("close", onComplete);
159
+ if (response.destroyed) onComplete();
160
+ await next();
161
+ };
162
+ }
163
+
164
+ exports.inferdiKoa = inferdiKoa;
165
+ exports.skipInferdiDispose = skipInferdiDispose;
166
+ //# sourceMappingURL=index.cjs.map
167
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":["root"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Koa v3 request-scope middleware for InferDI.\n *\n * Creates one InferDI scope per Koa request, exposes it through `ctx.state`,\n * and disposes it from the underlying Node response completion events. The\n * package is lifecycle glue only: no decorators, reflection, route scanning,\n * controller layer, or handler parameter injection.\n *\n * @example\n * ```ts\n * import Koa from 'koa'\n * import { inferdiKoa, type InferdiScopeOf } from '@inferdi/koa'\n *\n * const root = buildRootContainer()\n *\n * declare module 'koa' {\n * interface DefaultState {\n * di: InferdiScopeOf<typeof root>\n * }\n * }\n *\n * const app = new Koa()\n * app.use(inferdiKoa({ container: root }))\n *\n * app.use(async (ctx) => {\n * ctx.body = await ctx.state.di.get('users').profile('42')\n * })\n * ```\n *\n * @module\n */\n\nimport type {\n Context,\n DefaultContext,\n DefaultState,\n Middleware,\n ParameterizedContext,\n} from 'koa'\n\n/**\n * A value of type `T` or a promise resolving to it. Used by scope hooks so they\n * accept both synchronous and asynchronous implementations.\n *\n * @template T - The resolved value type.\n */\nexport type MaybePromise<T> = T | Promise<T>\n\n/**\n * Minimal structural contract for a disposable InferDI request scope. Any\n * `Container` instance satisfies it.\n */\nexport interface InferdiScope {\n /**\n * Releases everything the scope owns. InferDI's `Container.dispose()` is\n * idempotent; custom scope implementations should preserve that behavior.\n */\n dispose(): MaybePromise<void>\n}\n\n/**\n * Structural contract for the root container handed to the middleware.\n *\n * @template Scope - The per-request scope type produced by `createScope()`.\n */\nexport interface InferdiRoot<Scope extends InferdiScope = InferdiScope> {\n /** Opens a fresh request scope. Called once per request. */\n createScope(): Scope\n}\n\n/**\n * Extracts the request-scope type created by a root container.\n *\n * @template Root - A structural root container with `createScope()`.\n */\nexport type InferdiScopeOf<Root extends InferdiRoot> =\n ReturnType<Root['createScope']>\n\n/**\n * Koa state fragment that exposes a concrete scope under a state key.\n *\n * @template Scope - The scope exposed on `ctx.state`.\n * @template Key - The Koa state key. Defaults to `'di'`.\n */\nexport type InferdiKoaState<\n Scope extends InferdiScope,\n Key extends string = 'di',\n> = {\n [P in Key]: Scope\n}\n\n/**\n * Koa context with a concrete InferDI request scope present on `ctx.state`.\n *\n * @template StateT - Existing Koa state type.\n * @template ContextT - Existing Koa context extension type.\n * @template Scope - The scope exposed on `ctx.state`.\n * @template Key - The Koa state key. Defaults to `'di'`.\n */\nexport type InferdiKoaContext<\n StateT,\n ContextT,\n Scope extends InferdiScope,\n Key extends string = 'di',\n> = ParameterizedContext<\n StateT & InferdiKoaState<Scope, Key>,\n ContextT\n>\n\n/**\n * Options for {@link inferdiKoa}.\n *\n * @template Root - The root container type.\n * @template StateT - Existing Koa state available to setup hooks.\n * @template ContextT - Existing Koa context extensions available to hooks.\n * @template Key - The `ctx.state` key.\n * @template Scope - The per-request scope type.\n */\nexport interface InferdiKoaOptions<\n Root extends InferdiRoot,\n StateT = DefaultState,\n ContextT = DefaultContext,\n Key extends string = 'di',\n Scope extends InferdiScope = InferdiScopeOf<Root>,\n> {\n /** The root container. It is never disposed by this middleware. */\n readonly container: Root\n /** Koa `ctx.state` key. Defaults to `'di'`. */\n readonly key?: Key\n /**\n * Overrides how a request scope is created. Defaults to\n * `root.createScope()`. May be async.\n */\n readonly createScope?: (\n root: Root,\n context: ParameterizedContext<StateT, ContextT>,\n ) => MaybePromise<Scope>\n /**\n * Optional hook to hydrate the freshly created scope before downstream\n * middleware can read it from `ctx.state`.\n */\n readonly setupScope?: (\n scope: Scope,\n context: ParameterizedContext<StateT, ContextT>,\n ) => MaybePromise<void>\n /**\n * Overrides request-scope disposal. Defaults to `scope.dispose()`.\n */\n readonly disposeScope?: (\n scope: Scope,\n context: InferdiKoaContext<StateT, ContextT, Scope, Key>,\n ) => MaybePromise<void>\n /**\n * Controls whether the middleware disposes the request scope after response\n * completion. Returning `false` transfers disposal responsibility to\n * application code.\n */\n readonly autoDispose?:\n | boolean\n | ((\n context: InferdiKoaContext<StateT, ContextT, Scope, Key>,\n ) => MaybePromise<boolean>)\n /**\n * Optional sink for cleanup failures. Returning normally marks the cleanup\n * error as handled; omitted failures are emitted through `ctx.app`.\n */\n readonly onDisposeError?: (\n error: unknown,\n context: InferdiKoaContext<StateT, ContextT, Scope, Key>,\n ) => MaybePromise<void>\n}\n\ntype TypedContext<\n StateT,\n ContextT,\n Scope extends InferdiScope,\n Key extends string,\n> = InferdiKoaContext<StateT, ContextT, Scope, Key>\n\nconst skippedContexts = new WeakSet<Context>()\n\nfunction isPromiseLike<T>(value: T | PromiseLike<T>): value is PromiseLike<T> {\n return (\n value !== null &&\n (typeof value === 'object' || typeof value === 'function') &&\n typeof (value as { then?: unknown }).then === 'function'\n )\n}\n\nfunction cleanupError(errors: unknown[]): unknown {\n if (errors.length === 1) return errors[0]\n\n return new AggregateError(errors, 'InferDI Koa request cleanup failed')\n}\n\nfunction throwCollected(errors: unknown[]): never {\n if (errors.length === 1) {\n throw errors[0]\n }\n\n throw new AggregateError(errors, 'InferDI Koa request lifecycle failed')\n}\n\nfunction emitAppError(error: unknown, context: Context): void {\n try {\n context.app.emit('error', error, context)\n } catch {\n // Response cleanup must not fail because application error reporting failed.\n }\n}\n\nfunction emitUnhandledCleanupErrors(\n errors: unknown[],\n context: Context,\n): void {\n if (errors.length === 0) return\n\n emitAppError(cleanupError(errors), context)\n}\n\nfunction handleCleanupError<\n StateT,\n ContextT,\n Scope extends InferdiScope,\n Key extends string,\n>(\n error: unknown,\n context: TypedContext<StateT, ContextT, Scope, Key>,\n onDisposeError:\n | ((\n error: unknown,\n context: TypedContext<StateT, ContextT, Scope, Key>,\n ) => MaybePromise<void>)\n | undefined,\n errors: unknown[],\n): void | PromiseLike<void> {\n if (onDisposeError === undefined) {\n errors.push(error)\n return\n }\n\n try {\n const handling = onDisposeError(error, context)\n if (isPromiseLike(handling)) {\n return handling.then(undefined, (handlerError) => {\n errors.push(error)\n errors.push(handlerError)\n })\n }\n } catch (handlerError) {\n errors.push(error)\n errors.push(handlerError)\n }\n}\n\nfunction disposeWithErrors<\n StateT,\n ContextT,\n Scope extends InferdiScope,\n Key extends string,\n>(\n scope: Scope,\n context: TypedContext<StateT, ContextT, Scope, Key>,\n disposeScope: (\n scope: Scope,\n context: TypedContext<StateT, ContextT, Scope, Key>,\n ) => MaybePromise<void>,\n onDisposeError:\n | ((\n error: unknown,\n context: TypedContext<StateT, ContextT, Scope, Key>,\n ) => MaybePromise<void>)\n | undefined,\n errors: unknown[],\n): void | PromiseLike<void> {\n try {\n const disposing = disposeScope(scope, context)\n if (isPromiseLike(disposing)) {\n return disposing.then(\n undefined,\n (error) => handleCleanupError(\n error,\n context,\n onDisposeError,\n errors,\n ),\n )\n }\n } catch (error) {\n return handleCleanupError(error, context, onDisposeError, errors)\n }\n}\n\nasync function disposeAfterSetupFailure<\n StateT,\n ContextT,\n Scope extends InferdiScope,\n Key extends string,\n>(\n scope: Scope,\n context: TypedContext<StateT, ContextT, Scope, Key>,\n setupError: unknown,\n disposeScope: (\n scope: Scope,\n context: TypedContext<StateT, ContextT, Scope, Key>,\n ) => MaybePromise<void>,\n onDisposeError:\n | ((\n error: unknown,\n context: TypedContext<StateT, ContextT, Scope, Key>,\n ) => MaybePromise<void>)\n | undefined,\n): Promise<never> {\n const errors = [setupError]\n const disposing = disposeWithErrors(\n scope,\n context,\n disposeScope,\n onDisposeError,\n errors,\n )\n\n await disposing\n throwCollected(errors)\n}\n\n/**\n * Marks the current Koa request scope as manually owned by application code.\n *\n * Koa stream responses normally do not need this: the middleware waits for the\n * underlying Node response `finish` / `close` events. Use this only when a\n * route intentionally keeps the scope beyond the HTTP response boundary, such\n * as background work that will dispose the scope later.\n *\n * @param context - The current Koa context.\n */\nexport function skipInferdiDispose(context: Context): void {\n skippedContexts.add(context)\n}\n\n/**\n * Creates Koa middleware that opens one InferDI request scope per request and\n * exposes it as `ctx.state.di`.\n *\n * @template Root - The root container type.\n * @template StateT - Existing Koa state visible in setup hooks.\n * @template ContextT - Existing Koa context extensions visible in hooks.\n * @template Scope - The request scope type.\n */\nexport function inferdiKoa<\n Root extends InferdiRoot,\n StateT = DefaultState,\n ContextT = DefaultContext,\n Scope extends InferdiScope = InferdiScopeOf<Root>,\n>(\n options: Omit<\n InferdiKoaOptions<Root, StateT, ContextT, 'di', Scope>,\n 'key'\n > & {\n readonly key?: 'di'\n },\n): Middleware<StateT & InferdiKoaState<Scope, 'di'>, ContextT>\n\n/**\n * Creates Koa middleware that opens one InferDI request scope per request and\n * exposes it under a custom `ctx.state` key.\n *\n * @template Root - The root container type.\n * @template StateT - Existing Koa state visible in setup hooks.\n * @template ContextT - Existing Koa context extensions visible in hooks.\n * @template Key - The Koa state key.\n * @template Scope - The request scope type.\n */\nexport function inferdiKoa<\n Root extends InferdiRoot,\n StateT = DefaultState,\n ContextT = DefaultContext,\n Key extends string = string,\n Scope extends InferdiScope = InferdiScopeOf<Root>,\n>(\n options: InferdiKoaOptions<Root, StateT, ContextT, Key, Scope> & {\n readonly key: Key\n },\n): Middleware<StateT & InferdiKoaState<Scope, Key>, ContextT>\n\nexport function inferdiKoa<\n Root extends InferdiRoot,\n StateT = DefaultState,\n ContextT = DefaultContext,\n Key extends string = string,\n Scope extends InferdiScope = InferdiScopeOf<Root>,\n>(\n options: InferdiKoaOptions<Root, StateT, ContextT, Key, Scope>,\n): Middleware<StateT & InferdiKoaState<Scope, Key>, ContextT> {\n const root = options.container\n const key = (options.key ?? 'di') as Key\n const createScope =\n options.createScope ??\n ((root: Root) => root.createScope() as Scope)\n const setupScope = options.setupScope\n const disposeScope =\n options.disposeScope ??\n ((scope: Scope) => scope.dispose())\n const autoDispose = options.autoDispose\n const onDisposeError = options.onDisposeError\n\n return async function inferdiKoaMiddleware(context, next) {\n const setupContext = context as ParameterizedContext<StateT, ContextT>\n const typedContext = context as TypedContext<\n StateT,\n ContextT,\n Scope,\n Key\n >\n const scopeResult = createScope(root, setupContext)\n const scope = isPromiseLike(scopeResult)\n ? await scopeResult\n : scopeResult\n\n // Expose the scope before `setupScope` so the setup-failure disposal hooks\n // (`disposeScope` / `onDisposeError`) see the same `ctx.state[key]` their\n // typed context promises. Nothing downstream runs until `next()`, so a\n // half-built scope is never observable outside cleanup.\n ;(typedContext.state as InferdiKoaState<Scope, Key>)[key] = scope\n\n if (setupScope !== undefined) {\n try {\n const setupResult = setupScope(scope, setupContext)\n if (isPromiseLike(setupResult)) {\n await setupResult\n }\n } catch (error) {\n skippedContexts.delete(context)\n await disposeAfterSetupFailure(\n scope,\n typedContext,\n error,\n disposeScope,\n onDisposeError,\n )\n }\n }\n\n let disposed = false\n const response = context.res\n\n const runCleanup = async () => {\n if (disposed) return\n\n disposed = true\n const skipped = skippedContexts.delete(context)\n if (skipped) return\n\n const errors: unknown[] = []\n let shouldDispose = autoDispose !== false\n\n if (typeof autoDispose === 'function') {\n try {\n const autoDisposeResult = autoDispose(typedContext)\n shouldDispose = isPromiseLike(autoDisposeResult)\n ? await autoDisposeResult !== false\n : autoDisposeResult !== false\n } catch (error) {\n shouldDispose = true\n const handling = handleCleanupError(\n error,\n typedContext,\n onDisposeError,\n errors,\n )\n if (isPromiseLike(handling)) {\n await handling\n }\n }\n }\n\n if (shouldDispose) {\n const disposing = disposeWithErrors(\n scope,\n typedContext,\n disposeScope,\n onDisposeError,\n errors,\n )\n if (isPromiseLike(disposing)) {\n await disposing\n }\n }\n\n emitUnhandledCleanupErrors(errors, context)\n }\n\n // One shared completion handler, attached with `on` rather than `once`: it\n // removes both listeners on the first event, so each request avoids the two\n // per-listener `once` wrapper allocations, while `runCleanup`'s `disposed`\n // flag keeps it idempotent. `finish` = response written; `close` =\n // connection closed before completion.\n const onComplete = () => {\n response.removeListener('finish', onComplete)\n response.removeListener('close', onComplete)\n // `runCleanup` handles lifecycle errors itself; this is a final bug guard.\n /* v8 ignore next 3 */\n void runCleanup().then(undefined, (error) => {\n emitAppError(error, context)\n })\n }\n\n response.on('finish', onComplete)\n response.on('close', onComplete)\n\n // An async `createScope` / `setupScope` can yield the event loop long enough\n // for the client to disconnect. If `close` already fired during that await,\n // the listeners above missed it; `destroyed` is true iff that already\n // happened, so dispose now instead of leaking the scope.\n if (response.destroyed) onComplete()\n\n await next()\n }\n}\n"]}
@@ -0,0 +1,163 @@
1
+ import { ParameterizedContext, DefaultState, DefaultContext, Middleware, Context } from 'koa';
2
+
3
+ /**
4
+ * Koa v3 request-scope middleware for InferDI.
5
+ *
6
+ * Creates one InferDI scope per Koa request, exposes it through `ctx.state`,
7
+ * and disposes it from the underlying Node response completion events. The
8
+ * package is lifecycle glue only: no decorators, reflection, route scanning,
9
+ * controller layer, or handler parameter injection.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import Koa from 'koa'
14
+ * import { inferdiKoa, type InferdiScopeOf } from '@inferdi/koa'
15
+ *
16
+ * const root = buildRootContainer()
17
+ *
18
+ * declare module 'koa' {
19
+ * interface DefaultState {
20
+ * di: InferdiScopeOf<typeof root>
21
+ * }
22
+ * }
23
+ *
24
+ * const app = new Koa()
25
+ * app.use(inferdiKoa({ container: root }))
26
+ *
27
+ * app.use(async (ctx) => {
28
+ * ctx.body = await ctx.state.di.get('users').profile('42')
29
+ * })
30
+ * ```
31
+ *
32
+ * @module
33
+ */
34
+
35
+ /**
36
+ * A value of type `T` or a promise resolving to it. Used by scope hooks so they
37
+ * accept both synchronous and asynchronous implementations.
38
+ *
39
+ * @template T - The resolved value type.
40
+ */
41
+ type MaybePromise<T> = T | Promise<T>;
42
+ /**
43
+ * Minimal structural contract for a disposable InferDI request scope. Any
44
+ * `Container` instance satisfies it.
45
+ */
46
+ interface InferdiScope {
47
+ /**
48
+ * Releases everything the scope owns. InferDI's `Container.dispose()` is
49
+ * idempotent; custom scope implementations should preserve that behavior.
50
+ */
51
+ dispose(): MaybePromise<void>;
52
+ }
53
+ /**
54
+ * Structural contract for the root container handed to the middleware.
55
+ *
56
+ * @template Scope - The per-request scope type produced by `createScope()`.
57
+ */
58
+ interface InferdiRoot<Scope extends InferdiScope = InferdiScope> {
59
+ /** Opens a fresh request scope. Called once per request. */
60
+ createScope(): Scope;
61
+ }
62
+ /**
63
+ * Extracts the request-scope type created by a root container.
64
+ *
65
+ * @template Root - A structural root container with `createScope()`.
66
+ */
67
+ type InferdiScopeOf<Root extends InferdiRoot> = ReturnType<Root['createScope']>;
68
+ /**
69
+ * Koa state fragment that exposes a concrete scope under a state key.
70
+ *
71
+ * @template Scope - The scope exposed on `ctx.state`.
72
+ * @template Key - The Koa state key. Defaults to `'di'`.
73
+ */
74
+ type InferdiKoaState<Scope extends InferdiScope, Key extends string = 'di'> = {
75
+ [P in Key]: Scope;
76
+ };
77
+ /**
78
+ * Koa context with a concrete InferDI request scope present on `ctx.state`.
79
+ *
80
+ * @template StateT - Existing Koa state type.
81
+ * @template ContextT - Existing Koa context extension type.
82
+ * @template Scope - The scope exposed on `ctx.state`.
83
+ * @template Key - The Koa state key. Defaults to `'di'`.
84
+ */
85
+ type InferdiKoaContext<StateT, ContextT, Scope extends InferdiScope, Key extends string = 'di'> = ParameterizedContext<StateT & InferdiKoaState<Scope, Key>, ContextT>;
86
+ /**
87
+ * Options for {@link inferdiKoa}.
88
+ *
89
+ * @template Root - The root container type.
90
+ * @template StateT - Existing Koa state available to setup hooks.
91
+ * @template ContextT - Existing Koa context extensions available to hooks.
92
+ * @template Key - The `ctx.state` key.
93
+ * @template Scope - The per-request scope type.
94
+ */
95
+ interface InferdiKoaOptions<Root extends InferdiRoot, StateT = DefaultState, ContextT = DefaultContext, Key extends string = 'di', Scope extends InferdiScope = InferdiScopeOf<Root>> {
96
+ /** The root container. It is never disposed by this middleware. */
97
+ readonly container: Root;
98
+ /** Koa `ctx.state` key. Defaults to `'di'`. */
99
+ readonly key?: Key;
100
+ /**
101
+ * Overrides how a request scope is created. Defaults to
102
+ * `root.createScope()`. May be async.
103
+ */
104
+ readonly createScope?: (root: Root, context: ParameterizedContext<StateT, ContextT>) => MaybePromise<Scope>;
105
+ /**
106
+ * Optional hook to hydrate the freshly created scope before downstream
107
+ * middleware can read it from `ctx.state`.
108
+ */
109
+ readonly setupScope?: (scope: Scope, context: ParameterizedContext<StateT, ContextT>) => MaybePromise<void>;
110
+ /**
111
+ * Overrides request-scope disposal. Defaults to `scope.dispose()`.
112
+ */
113
+ readonly disposeScope?: (scope: Scope, context: InferdiKoaContext<StateT, ContextT, Scope, Key>) => MaybePromise<void>;
114
+ /**
115
+ * Controls whether the middleware disposes the request scope after response
116
+ * completion. Returning `false` transfers disposal responsibility to
117
+ * application code.
118
+ */
119
+ readonly autoDispose?: boolean | ((context: InferdiKoaContext<StateT, ContextT, Scope, Key>) => MaybePromise<boolean>);
120
+ /**
121
+ * Optional sink for cleanup failures. Returning normally marks the cleanup
122
+ * error as handled; omitted failures are emitted through `ctx.app`.
123
+ */
124
+ readonly onDisposeError?: (error: unknown, context: InferdiKoaContext<StateT, ContextT, Scope, Key>) => MaybePromise<void>;
125
+ }
126
+ /**
127
+ * Marks the current Koa request scope as manually owned by application code.
128
+ *
129
+ * Koa stream responses normally do not need this: the middleware waits for the
130
+ * underlying Node response `finish` / `close` events. Use this only when a
131
+ * route intentionally keeps the scope beyond the HTTP response boundary, such
132
+ * as background work that will dispose the scope later.
133
+ *
134
+ * @param context - The current Koa context.
135
+ */
136
+ declare function skipInferdiDispose(context: Context): void;
137
+ /**
138
+ * Creates Koa middleware that opens one InferDI request scope per request and
139
+ * exposes it as `ctx.state.di`.
140
+ *
141
+ * @template Root - The root container type.
142
+ * @template StateT - Existing Koa state visible in setup hooks.
143
+ * @template ContextT - Existing Koa context extensions visible in hooks.
144
+ * @template Scope - The request scope type.
145
+ */
146
+ declare function inferdiKoa<Root extends InferdiRoot, StateT = DefaultState, ContextT = DefaultContext, Scope extends InferdiScope = InferdiScopeOf<Root>>(options: Omit<InferdiKoaOptions<Root, StateT, ContextT, 'di', Scope>, 'key'> & {
147
+ readonly key?: 'di';
148
+ }): Middleware<StateT & InferdiKoaState<Scope, 'di'>, ContextT>;
149
+ /**
150
+ * Creates Koa middleware that opens one InferDI request scope per request and
151
+ * exposes it under a custom `ctx.state` key.
152
+ *
153
+ * @template Root - The root container type.
154
+ * @template StateT - Existing Koa state visible in setup hooks.
155
+ * @template ContextT - Existing Koa context extensions visible in hooks.
156
+ * @template Key - The Koa state key.
157
+ * @template Scope - The request scope type.
158
+ */
159
+ declare function inferdiKoa<Root extends InferdiRoot, StateT = DefaultState, ContextT = DefaultContext, Key extends string = string, Scope extends InferdiScope = InferdiScopeOf<Root>>(options: InferdiKoaOptions<Root, StateT, ContextT, Key, Scope> & {
160
+ readonly key: Key;
161
+ }): Middleware<StateT & InferdiKoaState<Scope, Key>, ContextT>;
162
+
163
+ export { type InferdiKoaContext, type InferdiKoaOptions, type InferdiKoaState, type InferdiRoot, type InferdiScope, type InferdiScopeOf, type MaybePromise, inferdiKoa, skipInferdiDispose };
@@ -0,0 +1,163 @@
1
+ import { ParameterizedContext, DefaultState, DefaultContext, Middleware, Context } from 'koa';
2
+
3
+ /**
4
+ * Koa v3 request-scope middleware for InferDI.
5
+ *
6
+ * Creates one InferDI scope per Koa request, exposes it through `ctx.state`,
7
+ * and disposes it from the underlying Node response completion events. The
8
+ * package is lifecycle glue only: no decorators, reflection, route scanning,
9
+ * controller layer, or handler parameter injection.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import Koa from 'koa'
14
+ * import { inferdiKoa, type InferdiScopeOf } from '@inferdi/koa'
15
+ *
16
+ * const root = buildRootContainer()
17
+ *
18
+ * declare module 'koa' {
19
+ * interface DefaultState {
20
+ * di: InferdiScopeOf<typeof root>
21
+ * }
22
+ * }
23
+ *
24
+ * const app = new Koa()
25
+ * app.use(inferdiKoa({ container: root }))
26
+ *
27
+ * app.use(async (ctx) => {
28
+ * ctx.body = await ctx.state.di.get('users').profile('42')
29
+ * })
30
+ * ```
31
+ *
32
+ * @module
33
+ */
34
+
35
+ /**
36
+ * A value of type `T` or a promise resolving to it. Used by scope hooks so they
37
+ * accept both synchronous and asynchronous implementations.
38
+ *
39
+ * @template T - The resolved value type.
40
+ */
41
+ type MaybePromise<T> = T | Promise<T>;
42
+ /**
43
+ * Minimal structural contract for a disposable InferDI request scope. Any
44
+ * `Container` instance satisfies it.
45
+ */
46
+ interface InferdiScope {
47
+ /**
48
+ * Releases everything the scope owns. InferDI's `Container.dispose()` is
49
+ * idempotent; custom scope implementations should preserve that behavior.
50
+ */
51
+ dispose(): MaybePromise<void>;
52
+ }
53
+ /**
54
+ * Structural contract for the root container handed to the middleware.
55
+ *
56
+ * @template Scope - The per-request scope type produced by `createScope()`.
57
+ */
58
+ interface InferdiRoot<Scope extends InferdiScope = InferdiScope> {
59
+ /** Opens a fresh request scope. Called once per request. */
60
+ createScope(): Scope;
61
+ }
62
+ /**
63
+ * Extracts the request-scope type created by a root container.
64
+ *
65
+ * @template Root - A structural root container with `createScope()`.
66
+ */
67
+ type InferdiScopeOf<Root extends InferdiRoot> = ReturnType<Root['createScope']>;
68
+ /**
69
+ * Koa state fragment that exposes a concrete scope under a state key.
70
+ *
71
+ * @template Scope - The scope exposed on `ctx.state`.
72
+ * @template Key - The Koa state key. Defaults to `'di'`.
73
+ */
74
+ type InferdiKoaState<Scope extends InferdiScope, Key extends string = 'di'> = {
75
+ [P in Key]: Scope;
76
+ };
77
+ /**
78
+ * Koa context with a concrete InferDI request scope present on `ctx.state`.
79
+ *
80
+ * @template StateT - Existing Koa state type.
81
+ * @template ContextT - Existing Koa context extension type.
82
+ * @template Scope - The scope exposed on `ctx.state`.
83
+ * @template Key - The Koa state key. Defaults to `'di'`.
84
+ */
85
+ type InferdiKoaContext<StateT, ContextT, Scope extends InferdiScope, Key extends string = 'di'> = ParameterizedContext<StateT & InferdiKoaState<Scope, Key>, ContextT>;
86
+ /**
87
+ * Options for {@link inferdiKoa}.
88
+ *
89
+ * @template Root - The root container type.
90
+ * @template StateT - Existing Koa state available to setup hooks.
91
+ * @template ContextT - Existing Koa context extensions available to hooks.
92
+ * @template Key - The `ctx.state` key.
93
+ * @template Scope - The per-request scope type.
94
+ */
95
+ interface InferdiKoaOptions<Root extends InferdiRoot, StateT = DefaultState, ContextT = DefaultContext, Key extends string = 'di', Scope extends InferdiScope = InferdiScopeOf<Root>> {
96
+ /** The root container. It is never disposed by this middleware. */
97
+ readonly container: Root;
98
+ /** Koa `ctx.state` key. Defaults to `'di'`. */
99
+ readonly key?: Key;
100
+ /**
101
+ * Overrides how a request scope is created. Defaults to
102
+ * `root.createScope()`. May be async.
103
+ */
104
+ readonly createScope?: (root: Root, context: ParameterizedContext<StateT, ContextT>) => MaybePromise<Scope>;
105
+ /**
106
+ * Optional hook to hydrate the freshly created scope before downstream
107
+ * middleware can read it from `ctx.state`.
108
+ */
109
+ readonly setupScope?: (scope: Scope, context: ParameterizedContext<StateT, ContextT>) => MaybePromise<void>;
110
+ /**
111
+ * Overrides request-scope disposal. Defaults to `scope.dispose()`.
112
+ */
113
+ readonly disposeScope?: (scope: Scope, context: InferdiKoaContext<StateT, ContextT, Scope, Key>) => MaybePromise<void>;
114
+ /**
115
+ * Controls whether the middleware disposes the request scope after response
116
+ * completion. Returning `false` transfers disposal responsibility to
117
+ * application code.
118
+ */
119
+ readonly autoDispose?: boolean | ((context: InferdiKoaContext<StateT, ContextT, Scope, Key>) => MaybePromise<boolean>);
120
+ /**
121
+ * Optional sink for cleanup failures. Returning normally marks the cleanup
122
+ * error as handled; omitted failures are emitted through `ctx.app`.
123
+ */
124
+ readonly onDisposeError?: (error: unknown, context: InferdiKoaContext<StateT, ContextT, Scope, Key>) => MaybePromise<void>;
125
+ }
126
+ /**
127
+ * Marks the current Koa request scope as manually owned by application code.
128
+ *
129
+ * Koa stream responses normally do not need this: the middleware waits for the
130
+ * underlying Node response `finish` / `close` events. Use this only when a
131
+ * route intentionally keeps the scope beyond the HTTP response boundary, such
132
+ * as background work that will dispose the scope later.
133
+ *
134
+ * @param context - The current Koa context.
135
+ */
136
+ declare function skipInferdiDispose(context: Context): void;
137
+ /**
138
+ * Creates Koa middleware that opens one InferDI request scope per request and
139
+ * exposes it as `ctx.state.di`.
140
+ *
141
+ * @template Root - The root container type.
142
+ * @template StateT - Existing Koa state visible in setup hooks.
143
+ * @template ContextT - Existing Koa context extensions visible in hooks.
144
+ * @template Scope - The request scope type.
145
+ */
146
+ declare function inferdiKoa<Root extends InferdiRoot, StateT = DefaultState, ContextT = DefaultContext, Scope extends InferdiScope = InferdiScopeOf<Root>>(options: Omit<InferdiKoaOptions<Root, StateT, ContextT, 'di', Scope>, 'key'> & {
147
+ readonly key?: 'di';
148
+ }): Middleware<StateT & InferdiKoaState<Scope, 'di'>, ContextT>;
149
+ /**
150
+ * Creates Koa middleware that opens one InferDI request scope per request and
151
+ * exposes it under a custom `ctx.state` key.
152
+ *
153
+ * @template Root - The root container type.
154
+ * @template StateT - Existing Koa state visible in setup hooks.
155
+ * @template ContextT - Existing Koa context extensions visible in hooks.
156
+ * @template Key - The Koa state key.
157
+ * @template Scope - The request scope type.
158
+ */
159
+ declare function inferdiKoa<Root extends InferdiRoot, StateT = DefaultState, ContextT = DefaultContext, Key extends string = string, Scope extends InferdiScope = InferdiScopeOf<Root>>(options: InferdiKoaOptions<Root, StateT, ContextT, Key, Scope> & {
160
+ readonly key: Key;
161
+ }): Middleware<StateT & InferdiKoaState<Scope, Key>, ContextT>;
162
+
163
+ export { type InferdiKoaContext, type InferdiKoaOptions, type InferdiKoaState, type InferdiRoot, type InferdiScope, type InferdiScopeOf, type MaybePromise, inferdiKoa, skipInferdiDispose };
package/dist/index.js ADDED
@@ -0,0 +1,164 @@
1
+ // src/index.ts
2
+ var skippedContexts = /* @__PURE__ */ new WeakSet();
3
+ function isPromiseLike(value) {
4
+ return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
5
+ }
6
+ function cleanupError(errors) {
7
+ if (errors.length === 1) return errors[0];
8
+ return new AggregateError(errors, "InferDI Koa request cleanup failed");
9
+ }
10
+ function throwCollected(errors) {
11
+ if (errors.length === 1) {
12
+ throw errors[0];
13
+ }
14
+ throw new AggregateError(errors, "InferDI Koa request lifecycle failed");
15
+ }
16
+ function emitAppError(error, context) {
17
+ try {
18
+ context.app.emit("error", error, context);
19
+ } catch {
20
+ }
21
+ }
22
+ function emitUnhandledCleanupErrors(errors, context) {
23
+ if (errors.length === 0) return;
24
+ emitAppError(cleanupError(errors), context);
25
+ }
26
+ function handleCleanupError(error, context, onDisposeError, errors) {
27
+ if (onDisposeError === void 0) {
28
+ errors.push(error);
29
+ return;
30
+ }
31
+ try {
32
+ const handling = onDisposeError(error, context);
33
+ if (isPromiseLike(handling)) {
34
+ return handling.then(void 0, (handlerError) => {
35
+ errors.push(error);
36
+ errors.push(handlerError);
37
+ });
38
+ }
39
+ } catch (handlerError) {
40
+ errors.push(error);
41
+ errors.push(handlerError);
42
+ }
43
+ }
44
+ function disposeWithErrors(scope, context, disposeScope, onDisposeError, errors) {
45
+ try {
46
+ const disposing = disposeScope(scope, context);
47
+ if (isPromiseLike(disposing)) {
48
+ return disposing.then(
49
+ void 0,
50
+ (error) => handleCleanupError(
51
+ error,
52
+ context,
53
+ onDisposeError,
54
+ errors
55
+ )
56
+ );
57
+ }
58
+ } catch (error) {
59
+ return handleCleanupError(error, context, onDisposeError, errors);
60
+ }
61
+ }
62
+ async function disposeAfterSetupFailure(scope, context, setupError, disposeScope, onDisposeError) {
63
+ const errors = [setupError];
64
+ const disposing = disposeWithErrors(
65
+ scope,
66
+ context,
67
+ disposeScope,
68
+ onDisposeError,
69
+ errors
70
+ );
71
+ await disposing;
72
+ throwCollected(errors);
73
+ }
74
+ function skipInferdiDispose(context) {
75
+ skippedContexts.add(context);
76
+ }
77
+ function inferdiKoa(options) {
78
+ const root = options.container;
79
+ const key = options.key ?? "di";
80
+ const createScope = options.createScope ?? ((root2) => root2.createScope());
81
+ const setupScope = options.setupScope;
82
+ const disposeScope = options.disposeScope ?? ((scope) => scope.dispose());
83
+ const autoDispose = options.autoDispose;
84
+ const onDisposeError = options.onDisposeError;
85
+ return async function inferdiKoaMiddleware(context, next) {
86
+ const setupContext = context;
87
+ const typedContext = context;
88
+ const scopeResult = createScope(root, setupContext);
89
+ const scope = isPromiseLike(scopeResult) ? await scopeResult : scopeResult;
90
+ typedContext.state[key] = scope;
91
+ if (setupScope !== void 0) {
92
+ try {
93
+ const setupResult = setupScope(scope, setupContext);
94
+ if (isPromiseLike(setupResult)) {
95
+ await setupResult;
96
+ }
97
+ } catch (error) {
98
+ skippedContexts.delete(context);
99
+ await disposeAfterSetupFailure(
100
+ scope,
101
+ typedContext,
102
+ error,
103
+ disposeScope,
104
+ onDisposeError
105
+ );
106
+ }
107
+ }
108
+ let disposed = false;
109
+ const response = context.res;
110
+ const runCleanup = async () => {
111
+ if (disposed) return;
112
+ disposed = true;
113
+ const skipped = skippedContexts.delete(context);
114
+ if (skipped) return;
115
+ const errors = [];
116
+ let shouldDispose = autoDispose !== false;
117
+ if (typeof autoDispose === "function") {
118
+ try {
119
+ const autoDisposeResult = autoDispose(typedContext);
120
+ shouldDispose = isPromiseLike(autoDisposeResult) ? await autoDisposeResult !== false : autoDisposeResult !== false;
121
+ } catch (error) {
122
+ shouldDispose = true;
123
+ const handling = handleCleanupError(
124
+ error,
125
+ typedContext,
126
+ onDisposeError,
127
+ errors
128
+ );
129
+ if (isPromiseLike(handling)) {
130
+ await handling;
131
+ }
132
+ }
133
+ }
134
+ if (shouldDispose) {
135
+ const disposing = disposeWithErrors(
136
+ scope,
137
+ typedContext,
138
+ disposeScope,
139
+ onDisposeError,
140
+ errors
141
+ );
142
+ if (isPromiseLike(disposing)) {
143
+ await disposing;
144
+ }
145
+ }
146
+ emitUnhandledCleanupErrors(errors, context);
147
+ };
148
+ const onComplete = () => {
149
+ response.removeListener("finish", onComplete);
150
+ response.removeListener("close", onComplete);
151
+ void runCleanup().then(void 0, (error) => {
152
+ emitAppError(error, context);
153
+ });
154
+ };
155
+ response.on("finish", onComplete);
156
+ response.on("close", onComplete);
157
+ if (response.destroyed) onComplete();
158
+ await next();
159
+ };
160
+ }
161
+
162
+ export { inferdiKoa, skipInferdiDispose };
163
+ //# sourceMappingURL=index.js.map
164
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":["root"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Koa v3 request-scope middleware for InferDI.\n *\n * Creates one InferDI scope per Koa request, exposes it through `ctx.state`,\n * and disposes it from the underlying Node response completion events. The\n * package is lifecycle glue only: no decorators, reflection, route scanning,\n * controller layer, or handler parameter injection.\n *\n * @example\n * ```ts\n * import Koa from 'koa'\n * import { inferdiKoa, type InferdiScopeOf } from '@inferdi/koa'\n *\n * const root = buildRootContainer()\n *\n * declare module 'koa' {\n * interface DefaultState {\n * di: InferdiScopeOf<typeof root>\n * }\n * }\n *\n * const app = new Koa()\n * app.use(inferdiKoa({ container: root }))\n *\n * app.use(async (ctx) => {\n * ctx.body = await ctx.state.di.get('users').profile('42')\n * })\n * ```\n *\n * @module\n */\n\nimport type {\n Context,\n DefaultContext,\n DefaultState,\n Middleware,\n ParameterizedContext,\n} from 'koa'\n\n/**\n * A value of type `T` or a promise resolving to it. Used by scope hooks so they\n * accept both synchronous and asynchronous implementations.\n *\n * @template T - The resolved value type.\n */\nexport type MaybePromise<T> = T | Promise<T>\n\n/**\n * Minimal structural contract for a disposable InferDI request scope. Any\n * `Container` instance satisfies it.\n */\nexport interface InferdiScope {\n /**\n * Releases everything the scope owns. InferDI's `Container.dispose()` is\n * idempotent; custom scope implementations should preserve that behavior.\n */\n dispose(): MaybePromise<void>\n}\n\n/**\n * Structural contract for the root container handed to the middleware.\n *\n * @template Scope - The per-request scope type produced by `createScope()`.\n */\nexport interface InferdiRoot<Scope extends InferdiScope = InferdiScope> {\n /** Opens a fresh request scope. Called once per request. */\n createScope(): Scope\n}\n\n/**\n * Extracts the request-scope type created by a root container.\n *\n * @template Root - A structural root container with `createScope()`.\n */\nexport type InferdiScopeOf<Root extends InferdiRoot> =\n ReturnType<Root['createScope']>\n\n/**\n * Koa state fragment that exposes a concrete scope under a state key.\n *\n * @template Scope - The scope exposed on `ctx.state`.\n * @template Key - The Koa state key. Defaults to `'di'`.\n */\nexport type InferdiKoaState<\n Scope extends InferdiScope,\n Key extends string = 'di',\n> = {\n [P in Key]: Scope\n}\n\n/**\n * Koa context with a concrete InferDI request scope present on `ctx.state`.\n *\n * @template StateT - Existing Koa state type.\n * @template ContextT - Existing Koa context extension type.\n * @template Scope - The scope exposed on `ctx.state`.\n * @template Key - The Koa state key. Defaults to `'di'`.\n */\nexport type InferdiKoaContext<\n StateT,\n ContextT,\n Scope extends InferdiScope,\n Key extends string = 'di',\n> = ParameterizedContext<\n StateT & InferdiKoaState<Scope, Key>,\n ContextT\n>\n\n/**\n * Options for {@link inferdiKoa}.\n *\n * @template Root - The root container type.\n * @template StateT - Existing Koa state available to setup hooks.\n * @template ContextT - Existing Koa context extensions available to hooks.\n * @template Key - The `ctx.state` key.\n * @template Scope - The per-request scope type.\n */\nexport interface InferdiKoaOptions<\n Root extends InferdiRoot,\n StateT = DefaultState,\n ContextT = DefaultContext,\n Key extends string = 'di',\n Scope extends InferdiScope = InferdiScopeOf<Root>,\n> {\n /** The root container. It is never disposed by this middleware. */\n readonly container: Root\n /** Koa `ctx.state` key. Defaults to `'di'`. */\n readonly key?: Key\n /**\n * Overrides how a request scope is created. Defaults to\n * `root.createScope()`. May be async.\n */\n readonly createScope?: (\n root: Root,\n context: ParameterizedContext<StateT, ContextT>,\n ) => MaybePromise<Scope>\n /**\n * Optional hook to hydrate the freshly created scope before downstream\n * middleware can read it from `ctx.state`.\n */\n readonly setupScope?: (\n scope: Scope,\n context: ParameterizedContext<StateT, ContextT>,\n ) => MaybePromise<void>\n /**\n * Overrides request-scope disposal. Defaults to `scope.dispose()`.\n */\n readonly disposeScope?: (\n scope: Scope,\n context: InferdiKoaContext<StateT, ContextT, Scope, Key>,\n ) => MaybePromise<void>\n /**\n * Controls whether the middleware disposes the request scope after response\n * completion. Returning `false` transfers disposal responsibility to\n * application code.\n */\n readonly autoDispose?:\n | boolean\n | ((\n context: InferdiKoaContext<StateT, ContextT, Scope, Key>,\n ) => MaybePromise<boolean>)\n /**\n * Optional sink for cleanup failures. Returning normally marks the cleanup\n * error as handled; omitted failures are emitted through `ctx.app`.\n */\n readonly onDisposeError?: (\n error: unknown,\n context: InferdiKoaContext<StateT, ContextT, Scope, Key>,\n ) => MaybePromise<void>\n}\n\ntype TypedContext<\n StateT,\n ContextT,\n Scope extends InferdiScope,\n Key extends string,\n> = InferdiKoaContext<StateT, ContextT, Scope, Key>\n\nconst skippedContexts = new WeakSet<Context>()\n\nfunction isPromiseLike<T>(value: T | PromiseLike<T>): value is PromiseLike<T> {\n return (\n value !== null &&\n (typeof value === 'object' || typeof value === 'function') &&\n typeof (value as { then?: unknown }).then === 'function'\n )\n}\n\nfunction cleanupError(errors: unknown[]): unknown {\n if (errors.length === 1) return errors[0]\n\n return new AggregateError(errors, 'InferDI Koa request cleanup failed')\n}\n\nfunction throwCollected(errors: unknown[]): never {\n if (errors.length === 1) {\n throw errors[0]\n }\n\n throw new AggregateError(errors, 'InferDI Koa request lifecycle failed')\n}\n\nfunction emitAppError(error: unknown, context: Context): void {\n try {\n context.app.emit('error', error, context)\n } catch {\n // Response cleanup must not fail because application error reporting failed.\n }\n}\n\nfunction emitUnhandledCleanupErrors(\n errors: unknown[],\n context: Context,\n): void {\n if (errors.length === 0) return\n\n emitAppError(cleanupError(errors), context)\n}\n\nfunction handleCleanupError<\n StateT,\n ContextT,\n Scope extends InferdiScope,\n Key extends string,\n>(\n error: unknown,\n context: TypedContext<StateT, ContextT, Scope, Key>,\n onDisposeError:\n | ((\n error: unknown,\n context: TypedContext<StateT, ContextT, Scope, Key>,\n ) => MaybePromise<void>)\n | undefined,\n errors: unknown[],\n): void | PromiseLike<void> {\n if (onDisposeError === undefined) {\n errors.push(error)\n return\n }\n\n try {\n const handling = onDisposeError(error, context)\n if (isPromiseLike(handling)) {\n return handling.then(undefined, (handlerError) => {\n errors.push(error)\n errors.push(handlerError)\n })\n }\n } catch (handlerError) {\n errors.push(error)\n errors.push(handlerError)\n }\n}\n\nfunction disposeWithErrors<\n StateT,\n ContextT,\n Scope extends InferdiScope,\n Key extends string,\n>(\n scope: Scope,\n context: TypedContext<StateT, ContextT, Scope, Key>,\n disposeScope: (\n scope: Scope,\n context: TypedContext<StateT, ContextT, Scope, Key>,\n ) => MaybePromise<void>,\n onDisposeError:\n | ((\n error: unknown,\n context: TypedContext<StateT, ContextT, Scope, Key>,\n ) => MaybePromise<void>)\n | undefined,\n errors: unknown[],\n): void | PromiseLike<void> {\n try {\n const disposing = disposeScope(scope, context)\n if (isPromiseLike(disposing)) {\n return disposing.then(\n undefined,\n (error) => handleCleanupError(\n error,\n context,\n onDisposeError,\n errors,\n ),\n )\n }\n } catch (error) {\n return handleCleanupError(error, context, onDisposeError, errors)\n }\n}\n\nasync function disposeAfterSetupFailure<\n StateT,\n ContextT,\n Scope extends InferdiScope,\n Key extends string,\n>(\n scope: Scope,\n context: TypedContext<StateT, ContextT, Scope, Key>,\n setupError: unknown,\n disposeScope: (\n scope: Scope,\n context: TypedContext<StateT, ContextT, Scope, Key>,\n ) => MaybePromise<void>,\n onDisposeError:\n | ((\n error: unknown,\n context: TypedContext<StateT, ContextT, Scope, Key>,\n ) => MaybePromise<void>)\n | undefined,\n): Promise<never> {\n const errors = [setupError]\n const disposing = disposeWithErrors(\n scope,\n context,\n disposeScope,\n onDisposeError,\n errors,\n )\n\n await disposing\n throwCollected(errors)\n}\n\n/**\n * Marks the current Koa request scope as manually owned by application code.\n *\n * Koa stream responses normally do not need this: the middleware waits for the\n * underlying Node response `finish` / `close` events. Use this only when a\n * route intentionally keeps the scope beyond the HTTP response boundary, such\n * as background work that will dispose the scope later.\n *\n * @param context - The current Koa context.\n */\nexport function skipInferdiDispose(context: Context): void {\n skippedContexts.add(context)\n}\n\n/**\n * Creates Koa middleware that opens one InferDI request scope per request and\n * exposes it as `ctx.state.di`.\n *\n * @template Root - The root container type.\n * @template StateT - Existing Koa state visible in setup hooks.\n * @template ContextT - Existing Koa context extensions visible in hooks.\n * @template Scope - The request scope type.\n */\nexport function inferdiKoa<\n Root extends InferdiRoot,\n StateT = DefaultState,\n ContextT = DefaultContext,\n Scope extends InferdiScope = InferdiScopeOf<Root>,\n>(\n options: Omit<\n InferdiKoaOptions<Root, StateT, ContextT, 'di', Scope>,\n 'key'\n > & {\n readonly key?: 'di'\n },\n): Middleware<StateT & InferdiKoaState<Scope, 'di'>, ContextT>\n\n/**\n * Creates Koa middleware that opens one InferDI request scope per request and\n * exposes it under a custom `ctx.state` key.\n *\n * @template Root - The root container type.\n * @template StateT - Existing Koa state visible in setup hooks.\n * @template ContextT - Existing Koa context extensions visible in hooks.\n * @template Key - The Koa state key.\n * @template Scope - The request scope type.\n */\nexport function inferdiKoa<\n Root extends InferdiRoot,\n StateT = DefaultState,\n ContextT = DefaultContext,\n Key extends string = string,\n Scope extends InferdiScope = InferdiScopeOf<Root>,\n>(\n options: InferdiKoaOptions<Root, StateT, ContextT, Key, Scope> & {\n readonly key: Key\n },\n): Middleware<StateT & InferdiKoaState<Scope, Key>, ContextT>\n\nexport function inferdiKoa<\n Root extends InferdiRoot,\n StateT = DefaultState,\n ContextT = DefaultContext,\n Key extends string = string,\n Scope extends InferdiScope = InferdiScopeOf<Root>,\n>(\n options: InferdiKoaOptions<Root, StateT, ContextT, Key, Scope>,\n): Middleware<StateT & InferdiKoaState<Scope, Key>, ContextT> {\n const root = options.container\n const key = (options.key ?? 'di') as Key\n const createScope =\n options.createScope ??\n ((root: Root) => root.createScope() as Scope)\n const setupScope = options.setupScope\n const disposeScope =\n options.disposeScope ??\n ((scope: Scope) => scope.dispose())\n const autoDispose = options.autoDispose\n const onDisposeError = options.onDisposeError\n\n return async function inferdiKoaMiddleware(context, next) {\n const setupContext = context as ParameterizedContext<StateT, ContextT>\n const typedContext = context as TypedContext<\n StateT,\n ContextT,\n Scope,\n Key\n >\n const scopeResult = createScope(root, setupContext)\n const scope = isPromiseLike(scopeResult)\n ? await scopeResult\n : scopeResult\n\n // Expose the scope before `setupScope` so the setup-failure disposal hooks\n // (`disposeScope` / `onDisposeError`) see the same `ctx.state[key]` their\n // typed context promises. Nothing downstream runs until `next()`, so a\n // half-built scope is never observable outside cleanup.\n ;(typedContext.state as InferdiKoaState<Scope, Key>)[key] = scope\n\n if (setupScope !== undefined) {\n try {\n const setupResult = setupScope(scope, setupContext)\n if (isPromiseLike(setupResult)) {\n await setupResult\n }\n } catch (error) {\n skippedContexts.delete(context)\n await disposeAfterSetupFailure(\n scope,\n typedContext,\n error,\n disposeScope,\n onDisposeError,\n )\n }\n }\n\n let disposed = false\n const response = context.res\n\n const runCleanup = async () => {\n if (disposed) return\n\n disposed = true\n const skipped = skippedContexts.delete(context)\n if (skipped) return\n\n const errors: unknown[] = []\n let shouldDispose = autoDispose !== false\n\n if (typeof autoDispose === 'function') {\n try {\n const autoDisposeResult = autoDispose(typedContext)\n shouldDispose = isPromiseLike(autoDisposeResult)\n ? await autoDisposeResult !== false\n : autoDisposeResult !== false\n } catch (error) {\n shouldDispose = true\n const handling = handleCleanupError(\n error,\n typedContext,\n onDisposeError,\n errors,\n )\n if (isPromiseLike(handling)) {\n await handling\n }\n }\n }\n\n if (shouldDispose) {\n const disposing = disposeWithErrors(\n scope,\n typedContext,\n disposeScope,\n onDisposeError,\n errors,\n )\n if (isPromiseLike(disposing)) {\n await disposing\n }\n }\n\n emitUnhandledCleanupErrors(errors, context)\n }\n\n // One shared completion handler, attached with `on` rather than `once`: it\n // removes both listeners on the first event, so each request avoids the two\n // per-listener `once` wrapper allocations, while `runCleanup`'s `disposed`\n // flag keeps it idempotent. `finish` = response written; `close` =\n // connection closed before completion.\n const onComplete = () => {\n response.removeListener('finish', onComplete)\n response.removeListener('close', onComplete)\n // `runCleanup` handles lifecycle errors itself; this is a final bug guard.\n /* v8 ignore next 3 */\n void runCleanup().then(undefined, (error) => {\n emitAppError(error, context)\n })\n }\n\n response.on('finish', onComplete)\n response.on('close', onComplete)\n\n // An async `createScope` / `setupScope` can yield the event loop long enough\n // for the client to disconnect. If `close` already fired during that await,\n // the listeners above missed it; `destroyed` is true iff that already\n // happened, so dispose now instead of leaking the scope.\n if (response.destroyed) onComplete()\n\n await next()\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@inferdi/koa",
3
+ "version": "4.0.8",
4
+ "description": "Koa request-scope middleware for InferDI.",
5
+ "keywords": [
6
+ "typescript",
7
+ "dependency-injection",
8
+ "di",
9
+ "ioc",
10
+ "inferdi",
11
+ "koa",
12
+ "middleware",
13
+ "request-scope"
14
+ ],
15
+ "author": {
16
+ "name": "Viacheslav Kabanov",
17
+ "email": "vkabanov@inferdi.com",
18
+ "url": "https://github.com/maxrendel"
19
+ },
20
+ "license": "MIT",
21
+ "homepage": "https://github.com/inferdi/inferdi/tree/main/packages/koa#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/inferdi/inferdi/issues"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/inferdi/inferdi.git",
28
+ "directory": "packages/koa"
29
+ },
30
+ "type": "module",
31
+ "main": "./dist/index.cjs",
32
+ "module": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "import": {
37
+ "types": "./dist/index.d.ts",
38
+ "default": "./dist/index.js"
39
+ },
40
+ "require": {
41
+ "types": "./dist/index.d.cts",
42
+ "default": "./dist/index.cjs"
43
+ }
44
+ },
45
+ "./package.json": "./package.json"
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "README.md",
50
+ "LICENSE"
51
+ ],
52
+ "sideEffects": false,
53
+ "engines": {
54
+ "node": ">=18.0.0"
55
+ },
56
+ "peerDependencies": {
57
+ "@inferdi/inferdi": "^4.0.0",
58
+ "@types/koa": "^3.0.0",
59
+ "koa": "^3.0.0"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "@types/koa": {
63
+ "optional": true
64
+ }
65
+ },
66
+ "devDependencies": {
67
+ "@types/koa": "^3.0.0",
68
+ "@vitest/coverage-v8": "^4.1.5",
69
+ "koa": "^3.0.0",
70
+ "tsup": "^8.3.0",
71
+ "typescript": "^5.6.0",
72
+ "vitest": "^4.1.5",
73
+ "@inferdi/inferdi": "4.0.8"
74
+ },
75
+ "publishConfig": {
76
+ "access": "public"
77
+ },
78
+ "scripts": {
79
+ "build": "tsup",
80
+ "clean": "rm -rf dist",
81
+ "typecheck": "tsc --noEmit",
82
+ "test": "vitest run",
83
+ "test:watch": "vitest",
84
+ "test:coverage": "vitest run --coverage",
85
+ "test:types": "vitest --typecheck.enabled --run"
86
+ }
87
+ }