katagami 2.3.0 → 3.0.1
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/README.md +82 -592
- package/dist/chunk-J2NYR3SH.js +6 -0
- package/dist/container/index.d.cts +108 -0
- package/dist/container/index.d.ts +5 -82
- package/dist/disposable/index.cjs +16 -25
- package/dist/disposable/index.d.cts +69 -0
- package/dist/disposable/index.d.ts +16 -8
- package/dist/disposable/index.js +1 -1
- package/dist/error/index.d.cts +11 -0
- package/dist/index.cjs +181 -86
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +177 -45
- package/dist/internal.d.cts +29 -0
- package/dist/internal.d.ts +3 -1
- package/dist/lazy/index.cjs +16 -25
- package/dist/lazy/index.d.cts +33 -0
- package/dist/lazy/index.d.ts +7 -9
- package/dist/lazy/index.js +1 -1
- package/dist/resolver/index.d.cts +93 -0
- package/dist/scope/index.d.cts +120 -0
- package/dist/scope/index.d.ts +4 -4
- package/docs/README.de.md +65 -0
- package/docs/README.es.md +65 -0
- package/docs/README.fr.md +65 -0
- package/docs/README.ja.md +66 -0
- package/docs/README.ko.md +65 -0
- package/docs/README.zh-CN.md +65 -0
- package/docs/README.zh-TW.md +65 -0
- package/docs/ai-coding-agents.md +78 -0
- package/docs/articles/ai-coding-agents.ja.md +83 -0
- package/docs/articles/ai-coding-agents.md +70 -0
- package/docs/articles/request-scope.md +48 -0
- package/docs/articles/without-decorators.md +54 -0
- package/docs/choosing-di.md +30 -0
- package/docs/growth/baseline-2026-09-11.json +68 -0
- package/docs/growth/github-metadata.json +13 -0
- package/docs/growth/rollout.md +77 -0
- package/docs/guide.md +186 -0
- package/docs/type-safety.md +126 -0
- package/examples/request-scope/README.md +37 -0
- package/examples/request-scope/app.ts +31 -0
- package/examples/request-scope/demo.ts +10 -0
- package/examples/request-scope/tsconfig.json +11 -0
- package/llms.txt +16 -0
- package/package.json +56 -28
- package/dist/index-g50fxds1.js +0 -34
- package/dist/index-jx8b52m0.js +0 -4
- package/dist/scope/index.cjs +0 -212
- package/dist/scope/index.js +0 -153
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"collectedAt": "2026-09-11T10:53:46.834Z",
|
|
3
|
+
"package": "katagami",
|
|
4
|
+
"npm": {
|
|
5
|
+
"week": {
|
|
6
|
+
"downloads": 2,
|
|
7
|
+
"start": "2026-09-03",
|
|
8
|
+
"end": "2026-09-09",
|
|
9
|
+
"package": "katagami"
|
|
10
|
+
},
|
|
11
|
+
"month": {
|
|
12
|
+
"downloads": 109,
|
|
13
|
+
"start": "2026-08-11",
|
|
14
|
+
"end": "2026-09-09",
|
|
15
|
+
"package": "katagami"
|
|
16
|
+
},
|
|
17
|
+
"nonOverlappingWeeks": [
|
|
18
|
+
{
|
|
19
|
+
"start": "2026-07-16",
|
|
20
|
+
"end": "2026-07-22",
|
|
21
|
+
"downloads": 2
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"start": "2026-07-23",
|
|
25
|
+
"end": "2026-07-29",
|
|
26
|
+
"downloads": 15
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"start": "2026-07-30",
|
|
30
|
+
"end": "2026-08-05",
|
|
31
|
+
"downloads": 8
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"start": "2026-08-06",
|
|
35
|
+
"end": "2026-08-12",
|
|
36
|
+
"downloads": 57
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"start": "2026-08-13",
|
|
40
|
+
"end": "2026-08-19",
|
|
41
|
+
"downloads": 48
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"start": "2026-08-20",
|
|
45
|
+
"end": "2026-08-26",
|
|
46
|
+
"downloads": 15
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"start": "2026-08-27",
|
|
50
|
+
"end": "2026-09-02",
|
|
51
|
+
"downloads": 14
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"start": "2026-09-03",
|
|
55
|
+
"end": "2026-09-09",
|
|
56
|
+
"downloads": 2
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
"github": {
|
|
61
|
+
"stars": 4,
|
|
62
|
+
"forks": 0,
|
|
63
|
+
"topics": [],
|
|
64
|
+
"homepage": null,
|
|
65
|
+
"pushedAt": "2026-02-09T11:30:14Z"
|
|
66
|
+
},
|
|
67
|
+
"errors": []
|
|
68
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Type-safe dependency injection for TypeScript, with inferred types and scope checks for AI-assisted development. No decorators or reflect-metadata.",
|
|
3
|
+
"homepage": "https://github.com/hiroiku/katagami#readme",
|
|
4
|
+
"topics": [
|
|
5
|
+
"typescript",
|
|
6
|
+
"dependency-injection",
|
|
7
|
+
"di-container",
|
|
8
|
+
"type-safety",
|
|
9
|
+
"type-inference",
|
|
10
|
+
"ai-assisted-development",
|
|
11
|
+
"coding-agents"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Katagami discovery and adoption rollout
|
|
2
|
+
|
|
3
|
+
## Positioning and audience
|
|
4
|
+
|
|
5
|
+
Type-safe dependency injection for TypeScript, with compiler feedback for AI-assisted development.
|
|
6
|
+
The first audience is developers building a new TypeScript application with coding agents who need
|
|
7
|
+
explicit dependency wiring, fake injection or request state. Avoid forcing a container into tiny apps.
|
|
8
|
+
|
|
9
|
+
## Release assets
|
|
10
|
+
|
|
11
|
+
- English and seven localized README entry points.
|
|
12
|
+
- npm description/keywords and [GitHub metadata](./github-metadata.json).
|
|
13
|
+
- [AI guide](../ai-coding-agents.md), [type guarantees](../type-safety.md), [API guide](../guide.md).
|
|
14
|
+
- [Runnable starter](../../examples/request-scope/README.md) with checked examples.
|
|
15
|
+
- [AI coding article](../articles/ai-coding-agents.md), [request scopes](../articles/request-scope.md),
|
|
16
|
+
[decorator-free DI](../articles/without-decorators.md) and a [Japanese Zenn draft](../articles/ai-coding-agents.ja.md).
|
|
17
|
+
- [Evaluation protocol](../../benchmarks/agent-wiring/README.md) and results summarizer; agent benefits remain unmeasured.
|
|
18
|
+
|
|
19
|
+
## First four weeks
|
|
20
|
+
|
|
21
|
+
| Period | Action | Evidence to retain |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| Week 1 | Ship checks, README, package metadata and starter; update GitHub Topics | Released version, CI run and public metrics snapshot |
|
|
24
|
+
| Week 2 | Publish the worked-example article on the owner's chosen blog/Zenn account; adapt relative links to the published location | URL, date, channel and campaign label |
|
|
25
|
+
| Weeks 3–4 | Observe 3–5 willing developers using the starter with their usual agent | Task attempted, first-run outcome, diagnostic confusion, whether they keep using it |
|
|
26
|
+
| Weekly | Capture npm trends and, locally, owner-only repository traffic | Non-overlapping date ranges and unavailable data recorded as null |
|
|
27
|
+
| After four weeks | Compare the funnel and fix the largest observed obstacle | Evidence for discovery, first successful use, continued use; no attribution from downloads alone |
|
|
28
|
+
|
|
29
|
+
Publishing destinations and interview participants are selected by the owner. The article files
|
|
30
|
+
are ready for adaptation, but an external publication or a participant interview must be recorded
|
|
31
|
+
only after it actually happens. Do not post or message people without authorization for that destination.
|
|
32
|
+
|
|
33
|
+
## Collect metrics
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
bun run metrics --output metrics.local/public.json
|
|
37
|
+
bun run metrics --traffic --output metrics.local/owner-traffic.json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The second command requires authenticated owner access through `gh` and contains private traffic
|
|
41
|
+
data. `metrics.local/` is ignored by Git. Do not commit those snapshots. Public release baselines can
|
|
42
|
+
be recorded separately under this directory. Re-run weekly: GitHub traffic is a short rolling window.
|
|
43
|
+
|
|
44
|
+
Track npm totals across four weeks using the non-overlapping weekly buckets, then compare with
|
|
45
|
+
the preceding four weeks. Downloads are not unique users or installations and can include automation.
|
|
46
|
+
Traffic, clones and external adoption reports provide different evidence; none alone is a conversion rate.
|
|
47
|
+
Where the chosen publication platform supports it, use a per-channel campaign link to a page with
|
|
48
|
+
analytics. GitHub alone does not provide complete visit-to-install attribution.
|
|
49
|
+
|
|
50
|
+
## Interview script
|
|
51
|
+
|
|
52
|
+
Ask a consenting participant to add a service and a fake to the starter using their normal agent.
|
|
53
|
+
Observe rather than coach the first attempt. Record:
|
|
54
|
+
|
|
55
|
+
1. What made the package seem relevant, and what almost stopped installation?
|
|
56
|
+
2. Did the first run work? Which command or prerequisite was unclear?
|
|
57
|
+
3. Could the agent fix a missing registration and an incorrect scope without suppressions?
|
|
58
|
+
4. Were error messages actionable? Which documentation did the participant or agent actually read?
|
|
59
|
+
5. Would the participant use it in a real project? Follow up on continued use with their consent.
|
|
60
|
+
|
|
61
|
+
Record observations without names or private code unless the participant agrees. A suggested
|
|
62
|
+
invitation, for an owner-approved channel:
|
|
63
|
+
|
|
64
|
+
> TypeScriptのDIコンテナKatagamiの導入体験を改善しています。普段使っているAIエージェントで
|
|
65
|
+
> 小さなサンプルにサービスとテスト用の依存を追加する、20分ほどの試用に協力いただける方を探しています。
|
|
66
|
+
> うまく動かない点や分かりにくい点を知りたいです。公開コードだけで参加できます。
|
|
67
|
+
|
|
68
|
+
## Search and claim hygiene
|
|
69
|
+
|
|
70
|
+
Keep the description specific to dependency injection. Link guides from the README and use actual
|
|
71
|
+
examples for queries about AI coding agents, request scopes and DI without decorators. `llms.txt`
|
|
72
|
+
is a navigation aid, not a promised ranking boost.
|
|
73
|
+
|
|
74
|
+
Sources: [npm discovery metadata](https://docs.npmjs.com/files/package.json/),
|
|
75
|
+
[npm README publication](https://docs.npmjs.com/about-package-readme-files/),
|
|
76
|
+
[GitHub Topics](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/classifying-your-repository-with-topics),
|
|
77
|
+
[Google AI search guidance](https://developers.google.com/search/docs/appearance/ai-features).
|
package/docs/guide.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Katagami usage guide and API
|
|
2
|
+
|
|
3
|
+
## Compatibility
|
|
4
|
+
|
|
5
|
+
Katagami publishes ESM, CommonJS and TypeScript declarations, with no runtime dependencies.
|
|
6
|
+
The CI consumer checks run on Node.js 22 and 24; runtime tests and examples also run with Bun.
|
|
7
|
+
Type examples use TypeScript 5.9, `strict: true`, and the `ES2022` and `ESNext.Disposable` libraries.
|
|
8
|
+
Browser APIs in examples, such as `crypto.randomUUID`, additionally need the `DOM` library.
|
|
9
|
+
|
|
10
|
+
Core DI does not require decorator compiler flags, metadata emission or polyfills.
|
|
11
|
+
The optional disposable entry point uses `Symbol.dispose` / `Symbol.asyncDispose`; the runtime
|
|
12
|
+
must supply those symbols. `await using` requires a compiler/runtime that supports or transforms it.
|
|
13
|
+
Katagami does not provide those polyfills. `lazy` uses JavaScript `Proxy`.
|
|
14
|
+
|
|
15
|
+
## Lifetimes
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createContainer, createScope } from 'katagami';
|
|
19
|
+
|
|
20
|
+
const container = createContainer()
|
|
21
|
+
.registerSingleton('shared', () => ({ value: 0 }))
|
|
22
|
+
.registerTransient('fresh', () => ({ value: 0 }))
|
|
23
|
+
.registerScoped('request', () => ({ value: 0 }));
|
|
24
|
+
|
|
25
|
+
const first = createScope(container);
|
|
26
|
+
const second = createScope(container);
|
|
27
|
+
first.resolve('shared') === second.resolve('shared'); // true
|
|
28
|
+
first.resolve('request') === first.resolve('request'); // true
|
|
29
|
+
first.resolve('request') === second.resolve('request'); // false
|
|
30
|
+
first.resolve('fresh') === first.resolve('fresh'); // false
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Factories run when first resolved; transient factories run on each resolution.
|
|
34
|
+
Use `createScope(existingScope)` for another scope with its own scoped cache and shared singletons.
|
|
35
|
+
Singleton and transient factories cannot directly resolve scoped registrations through their typed resolver.
|
|
36
|
+
|
|
37
|
+
## Class tokens and async factories
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { createContainer, createScope } from 'katagami';
|
|
41
|
+
|
|
42
|
+
class Database {
|
|
43
|
+
query() { return ['Ada']; }
|
|
44
|
+
}
|
|
45
|
+
class UserService {
|
|
46
|
+
constructor(private db: Database) {}
|
|
47
|
+
list() { return this.db.query(); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const container = createContainer()
|
|
51
|
+
.registerSingleton(Database, async () => new Database())
|
|
52
|
+
.registerScoped(UserService, async r => new UserService(await r.resolve(Database)));
|
|
53
|
+
|
|
54
|
+
const service = await createScope(container).resolve(UserService);
|
|
55
|
+
service.list();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Class return types are inferred from constructors. Classes follow TypeScript's structural typing;
|
|
59
|
+
see [token identity](./type-safety.md#class-tokens-use-structural-typing).
|
|
60
|
+
Async factories produce Promise-typed resolutions. Await them explicitly.
|
|
61
|
+
|
|
62
|
+
## Composition and test substitution
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { createContainer, createScope } from 'katagami';
|
|
66
|
+
|
|
67
|
+
const infrastructure = createContainer()
|
|
68
|
+
.registerSingleton('users', () => ({ find: (id: string) => `user-${id}` }));
|
|
69
|
+
const fakeInfrastructure = createContainer()
|
|
70
|
+
.registerSingleton('users', () => ({ find: (id: string) => `fake-${id}` }));
|
|
71
|
+
|
|
72
|
+
const testContainer = createContainer()
|
|
73
|
+
.use(infrastructure)
|
|
74
|
+
.use(fakeInfrastructure)
|
|
75
|
+
.registerScoped('handler', r => (id: string) => r.resolve('users').find(id));
|
|
76
|
+
|
|
77
|
+
createScope(testContainer).resolve('handler')('1'); // fake-1
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`use(source)` copies registration entries and replaces entries for matching tokens. It does not
|
|
81
|
+
share the source's singleton cache. Containers are mutable: `register*` and `use` update the container
|
|
82
|
+
and return it with accumulated types. Build the composition before creating or resolving scopes.
|
|
83
|
+
Preserve each token's value type and lifetime when substituting registrations.
|
|
84
|
+
|
|
85
|
+
## Optional and multiple resolution
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { createContainer, createScope } from 'katagami';
|
|
89
|
+
|
|
90
|
+
const scope = createScope(createContainer()
|
|
91
|
+
.registerSingleton('plugin', () => 'first')
|
|
92
|
+
.registerSingleton('plugin', () => 'second'));
|
|
93
|
+
|
|
94
|
+
scope.resolve('plugin'); // 'second'
|
|
95
|
+
scope.resolveAll('plugin'); // ['first', 'second']
|
|
96
|
+
scope.tryResolve('optional'); // undefined
|
|
97
|
+
scope.tryResolveAll('optional'); // undefined
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Repeated `register*` calls accumulate factories for a token. `resolve` selects the last;
|
|
101
|
+
`resolveAll` returns an array in registration order. For async factories, the array contains promises;
|
|
102
|
+
use `Promise.all`. Keep one consistent value type and lifetime for a token.
|
|
103
|
+
`tryResolve` and `tryResolveAll` accept missing tokens intentionally. They still report circular
|
|
104
|
+
dependencies and operations on disposed scopes.
|
|
105
|
+
|
|
106
|
+
## Resource cleanup
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { createContainer, createScope } from 'katagami';
|
|
110
|
+
import { disposable } from 'katagami/disposable';
|
|
111
|
+
|
|
112
|
+
const container = createContainer().registerScoped('connection', () => ({
|
|
113
|
+
query: () => 'ok',
|
|
114
|
+
[Symbol.dispose]() { console.log('connection closed'); },
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
{
|
|
118
|
+
await using scope = disposable(createScope(container));
|
|
119
|
+
scope.resolve('connection').query();
|
|
120
|
+
} // the scoped connection is disposed here
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`disposable(containerOrScope)` returns a view with `[Symbol.asyncDispose]()` and hides registration
|
|
124
|
+
methods. Disposable scopes retain resolution methods. `createScope` accepts either disposable view
|
|
125
|
+
and retains its registered token types.
|
|
126
|
+
|
|
127
|
+
Scope disposal cleans its cached scoped instances; container disposal cleans its singleton cache.
|
|
128
|
+
Instances are processed in reverse cache insertion order, asynchronous results are awaited, and
|
|
129
|
+
cleanup errors are combined in an `AggregateError`. Disposal is idempotent. Transient instances
|
|
130
|
+
are not cached or automatically owned: arrange their cleanup explicitly.
|
|
131
|
+
|
|
132
|
+
## Lazy resolution
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { createContainer, createScope } from 'katagami';
|
|
136
|
+
import { lazy } from 'katagami/lazy';
|
|
137
|
+
|
|
138
|
+
class Report { render() { return 'report'; } }
|
|
139
|
+
const scope = createScope(createContainer().registerSingleton(Report, () => new Report()));
|
|
140
|
+
const report = lazy(scope, Report);
|
|
141
|
+
report.render(); // resolves on first access
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`lazy` accepts synchronous class tokens on a scope or disposable scope. It does not accept async
|
|
145
|
+
or PropertyKey tokens. Core imports do not include the lazy or disposable implementation; these
|
|
146
|
+
are separate entry points with `sideEffects: false` for bundlers.
|
|
147
|
+
|
|
148
|
+
## Predeclared service maps
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { createContainer, createScope } from 'katagami';
|
|
152
|
+
|
|
153
|
+
interface Services {
|
|
154
|
+
greeting: string;
|
|
155
|
+
name: string;
|
|
156
|
+
}
|
|
157
|
+
const container = createContainer<Services>()
|
|
158
|
+
.registerSingleton('greeting', r => `Hello, ${r.resolve('name')}`)
|
|
159
|
+
.registerSingleton('name', () => 'Ada');
|
|
160
|
+
|
|
161
|
+
createScope(container).resolve('greeting');
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The first generic defines non-scoped PropertyKey tokens; the second defines scoped PropertyKey tokens.
|
|
165
|
+
Maps permit forward references. They do not verify that each declared key has a registration.
|
|
166
|
+
Default accumulated registration provides the registration-order checks described in the
|
|
167
|
+
[type-safety guide](./type-safety.md).
|
|
168
|
+
|
|
169
|
+
## API reference
|
|
170
|
+
|
|
171
|
+
| API | Behavior |
|
|
172
|
+
| --- | --- |
|
|
173
|
+
| `createContainer<T, ScopedT>()` | Create a registration container; omit type maps to infer them by chaining |
|
|
174
|
+
| `registerSingleton(token, factory)` | Register a shared, cached factory |
|
|
175
|
+
| `registerTransient(token, factory)` | Register a factory called on every resolution |
|
|
176
|
+
| `registerScoped(token, factory)` | Register a factory cached per scope |
|
|
177
|
+
| `use(source)` | Copy another container's registrations, replacing matching token entries |
|
|
178
|
+
| `createScope(source)` | Create a scope from a container, scope or disposable view |
|
|
179
|
+
| `scope.resolve(token)` | Resolve the last registration; fail if missing |
|
|
180
|
+
| `scope.resolveAll(token)` | Resolve every registration for a token |
|
|
181
|
+
| `scope.tryResolve(token)` | Resolve the last registration or return `undefined` |
|
|
182
|
+
| `scope.tryResolveAll(token)` | Resolve all registrations or return `undefined` |
|
|
183
|
+
| `disposable(source)` from `katagami/disposable` | Add async cleanup and return a restricted view |
|
|
184
|
+
| `lazy(scope, classToken)` from `katagami/lazy` | Defer synchronous resolution until first access |
|
|
185
|
+
| `ContainerError` | Runtime error for missing registrations, cycles and invalid scope operations |
|
|
186
|
+
| `Resolver` (type export) | Factory resolver type; retain inferred generics when extracting factories |
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Type-safe dependency injection: what Katagami checks
|
|
2
|
+
|
|
3
|
+
Katagami's default `createContainer()` accumulates types from registrations. With literal keys
|
|
4
|
+
or unique symbols preserved, a resolver can only accept tokens accumulated into its visible
|
|
5
|
+
registration set. No separate service interface is required. Run TypeScript with `strict: true`.
|
|
6
|
+
|
|
7
|
+
## Accumulated registrations
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createContainer, createScope } from 'katagami';
|
|
11
|
+
|
|
12
|
+
const DATABASE = Symbol('database');
|
|
13
|
+
const OTHER_DATABASE = Symbol('database');
|
|
14
|
+
const container = createContainer()
|
|
15
|
+
.registerSingleton(DATABASE, () => ({ query: () => 'ok' }))
|
|
16
|
+
.registerSingleton('greeting', () => 'hello');
|
|
17
|
+
|
|
18
|
+
const scope = createScope(container);
|
|
19
|
+
const greeting: string = scope.resolve('greeting');
|
|
20
|
+
scope.resolve(DATABASE).query();
|
|
21
|
+
// @ts-expect-error — this literal key has not been accumulated
|
|
22
|
+
scope.resolve('missing');
|
|
23
|
+
// @ts-expect-error — the same description does not make two symbols identical
|
|
24
|
+
scope.resolve(OTHER_DATABASE);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Inside a factory, the visible set is the set accumulated **before that registration**.
|
|
28
|
+
Singleton and transient factories receive the non-scoped set. Scoped factories receive both sets.
|
|
29
|
+
Scopes can resolve both. `use()` includes the source module's registration types.
|
|
30
|
+
`disposable()` and nested scopes retain the registration state, including asynchronous results.
|
|
31
|
+
|
|
32
|
+
| Pattern | Compile-time behavior |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| Unregistered literal key / distinct unique symbol in accumulated mode | Rejected |
|
|
35
|
+
| A structurally incompatible, unregistered class | Rejected |
|
|
36
|
+
| Scoped literal token in a singleton or transient factory | Rejected |
|
|
37
|
+
| Accessing a service method on an unresolved `Promise<Service>` | Rejected |
|
|
38
|
+
| A key declared in `createContainer<Services>()`, but never registered | Accepted; runtime resolution can fail |
|
|
39
|
+
| An unregistered class structurally compatible with a registered class | Can be accepted; runtime resolution can fail |
|
|
40
|
+
| `tryResolve` of an unregistered token | Intentionally accepted; absence is a normal result |
|
|
41
|
+
|
|
42
|
+
The checked examples and regression tests establish these specific behaviors. They do not prove
|
|
43
|
+
arbitrary program correctness or an improvement in AI coding performance.
|
|
44
|
+
|
|
45
|
+
## Class tokens use structural typing
|
|
46
|
+
|
|
47
|
+
Accumulating a class type does not give TypeScript a nominal identity for each constructor.
|
|
48
|
+
The following compiles in accumulated mode, but throws `ContainerError` if executed:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { createContainer, createScope } from 'katagami';
|
|
52
|
+
|
|
53
|
+
class Registered { value = 1; }
|
|
54
|
+
class NotRegistered { value = 1; }
|
|
55
|
+
|
|
56
|
+
const scope = createScope(createContainer()
|
|
57
|
+
.registerSingleton(Registered, () => new Registered()));
|
|
58
|
+
|
|
59
|
+
scope.resolve(NotRegistered); // Same structure to TypeScript; different runtime Map key
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This includes empty classes and can include subclasses. TypeScript checks compatibility;
|
|
63
|
+
the runtime registry checks token identity. When that distinction matters, use unique symbols
|
|
64
|
+
or literal keys, or distinguish class types with separate private member declarations:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { createContainer, createScope } from 'katagami';
|
|
68
|
+
|
|
69
|
+
class Registered {
|
|
70
|
+
declare private brand: undefined;
|
|
71
|
+
value = 1;
|
|
72
|
+
}
|
|
73
|
+
class NotRegistered {
|
|
74
|
+
declare private brand: undefined;
|
|
75
|
+
value = 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const scope = createScope(createContainer()
|
|
79
|
+
.registerSingleton(Registered, () => new Registered()));
|
|
80
|
+
// @ts-expect-error — these separately declared private members make the types incompatible
|
|
81
|
+
scope.resolve(NotRegistered);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
See [TypeScript's type compatibility documentation](https://www.typescriptlang.org/docs/handbook/type-compatibility).
|
|
85
|
+
|
|
86
|
+
## Predeclared interface maps
|
|
87
|
+
|
|
88
|
+
`createContainer<Services>()` starts with the declared keys already visible. This enables
|
|
89
|
+
order-independent registration, but that type map is a declaration of intent, not a proof
|
|
90
|
+
that every key has a runtime factory:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { createContainer, createScope } from 'katagami';
|
|
94
|
+
|
|
95
|
+
interface Services { greeting: string; }
|
|
96
|
+
const scope = createScope(createContainer<Services>());
|
|
97
|
+
scope.resolve('greeting'); // Compiles, but throws if executed: no factory was registered
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Prefer default accumulated registration when you want registration-order checks.
|
|
101
|
+
Use a predeclared map when order independence is useful and verify the composition in runtime tests.
|
|
102
|
+
|
|
103
|
+
## Preserve the information the checker needs
|
|
104
|
+
|
|
105
|
+
- Keep literal tokens narrow. A token widened to `string`, or a broad index signature, loses the finite key set.
|
|
106
|
+
- Keep the return value of each registration chain. Do not replace its inferred type with a broader annotation.
|
|
107
|
+
- Keep a token's value type and lifetime consistent across registrations and `use()` replacements.
|
|
108
|
+
Katagami is mutable at runtime; arbitrary mutation through aliases is not proven safe by the accumulated type.
|
|
109
|
+
- Use the factory's supplied resolver. Closures over another scope can bypass its lifetime restrictions.
|
|
110
|
+
- Fix the dependency or lifetime; `any`, assertions and suppressions can bypass TypeScript checks.
|
|
111
|
+
- `tryResolve` is for optional dependencies, not a workaround for required registration errors.
|
|
112
|
+
|
|
113
|
+
## Runtime checks and limits
|
|
114
|
+
|
|
115
|
+
Runtime checks report missing registrations, disposed scopes and circular resolution paths.
|
|
116
|
+
The captive-dependency guard detects scoped resolution in an active singleton call chain,
|
|
117
|
+
including indirect synchronous calls. It does not prove all lifetime relationships: in particular,
|
|
118
|
+
do not rely on it for work resumed after an `await` or for dependencies captured from another scope.
|
|
119
|
+
Use the typed factory API and runtime tests together.
|
|
120
|
+
|
|
121
|
+
## Verification
|
|
122
|
+
|
|
123
|
+
`bun run typecheck` runs the original type tests and the guarantee regressions in
|
|
124
|
+
`src/container/guarantees/typetest.ts`. Negative checks use `@ts-expect-error`, so a previously
|
|
125
|
+
rejected expression becoming accepted makes the check fail. These files are not runtime examples.
|
|
126
|
+
`bun run docs:check` type-checks every TypeScript fence in the maintained Markdown documentation.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# TypeScript request scopes without decorators
|
|
2
|
+
|
|
3
|
+
A small starter for an HTTP handler, CLI, or worker: inject a user repository,
|
|
4
|
+
create isolated state for each request, and dispose that state after success or failure.
|
|
5
|
+
It uses Katagami's accumulated literal tokens; no token-to-type interface is needed.
|
|
6
|
+
|
|
7
|
+
Copy `app.ts`, `demo.ts`, and `tsconfig.json` into a new directory, then run:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm init -y
|
|
11
|
+
npm pkg set type=module
|
|
12
|
+
npm install katagami
|
|
13
|
+
npm install --save-dev typescript
|
|
14
|
+
npx tsc --noEmit
|
|
15
|
+
npx tsc
|
|
16
|
+
node build/demo.js
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The demo prints a greeting for Ada and a guest greeting with different request IDs.
|
|
20
|
+
The example is checked on Node.js 22 and 24 in CI and runs on Bun as well.
|
|
21
|
+
It relies on `crypto.randomUUID`, `Symbol.dispose`, and `Symbol.asyncDispose` in the host.
|
|
22
|
+
TypeScript compiles `await using`; Katagami does not install polyfills.
|
|
23
|
+
|
|
24
|
+
From this repository, run `bun run build`, then `bun examples/request-scope/demo.ts`.
|
|
25
|
+
`bun run test:examples` checks concurrency, fake injection and cleanup on rejection.
|
|
26
|
+
`bun run test:package` also compiles and runs this starter against the packed npm artifact in Node.js.
|
|
27
|
+
|
|
28
|
+
For an HTTP application, call the returned `greet` function inside the request handler.
|
|
29
|
+
Create the application once, outside the handler. Each call creates and disposes its own scope.
|
|
30
|
+
Pass authentication context explicitly into the application when adding authentication;
|
|
31
|
+
this example does not provide an authentication or authorization system.
|
|
32
|
+
|
|
33
|
+
Try changing `registerScoped('greeting', ...)` to `registerSingleton('greeting', ...)`.
|
|
34
|
+
`npx tsc --noEmit` will reject `r.resolve('request')`. Restore the scoped registration:
|
|
35
|
+
the greeting captures request state, so it must share that request's lifetime.
|
|
36
|
+
|
|
37
|
+
See the [AI coding guide](../../docs/ai-coding-agents.md) and [type guarantees](../../docs/type-safety.md).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createContainer, createScope } from 'katagami';
|
|
2
|
+
import { disposable } from 'katagami/disposable';
|
|
3
|
+
|
|
4
|
+
export interface UserRepository {
|
|
5
|
+
findName(id: string): Promise<string | undefined>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Inject infrastructure once; create a separate scope for each request. */
|
|
9
|
+
export function createApp(repository: UserRepository, onCleanup: (id: string) => void = () => {}) {
|
|
10
|
+
const container = createContainer()
|
|
11
|
+
.registerSingleton('users', () => repository)
|
|
12
|
+
.registerScoped('request', () => ({
|
|
13
|
+
id: crypto.randomUUID(),
|
|
14
|
+
[Symbol.dispose]() {
|
|
15
|
+
onCleanup(this.id);
|
|
16
|
+
},
|
|
17
|
+
}))
|
|
18
|
+
.registerScoped('greeting', r => {
|
|
19
|
+
const users = r.resolve('users');
|
|
20
|
+
const request = r.resolve('request');
|
|
21
|
+
return async (userId: string) => ({
|
|
22
|
+
requestId: request.id,
|
|
23
|
+
message: `Hello, ${(await users.findName(userId)) ?? 'guest'}!`,
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return async (userId: string) => {
|
|
28
|
+
await using scope = disposable(createScope(container));
|
|
29
|
+
return await scope.resolve('greeting')(userId);
|
|
30
|
+
};
|
|
31
|
+
}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Katagami
|
|
2
|
+
|
|
3
|
+
> Type-safe dependency injection for TypeScript. Accumulated registration types, scope-aware factories and async tracking for AI-assisted development. No decorators or runtime dependencies.
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
- [Quick start](https://github.com/hiroiku/katagami/blob/master/README.md): installation and a checked example.
|
|
8
|
+
- [AI coding agents](https://github.com/hiroiku/katagami/blob/master/docs/ai-coding-agents.md): workflow, diagnostics and project instructions.
|
|
9
|
+
- [Type safety](https://github.com/hiroiku/katagami/blob/master/docs/type-safety.md): accumulated tokens, structural classes, declared maps and limits.
|
|
10
|
+
- [API guide](https://github.com/hiroiku/katagami/blob/master/docs/guide.md): lifetimes, factories, modules, disposal and lazy resolution.
|
|
11
|
+
- [Request-scope starter](https://github.com/hiroiku/katagami/tree/master/examples/request-scope): a runnable example with fake injection and cleanup.
|
|
12
|
+
- [Choosing DI](https://github.com/hiroiku/katagami/blob/master/docs/choosing-di.md): use cases and trade-offs.
|
|
13
|
+
|
|
14
|
+
## Optional
|
|
15
|
+
|
|
16
|
+
- [Evaluation protocol](https://github.com/hiroiku/katagami/tree/master/benchmarks/agent-wiring): test agent outcomes before claiming performance benefits.
|