katagami 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.es.md ADDED
@@ -0,0 +1,386 @@
1
+ [English](./README.md) | [日本語](./README.ja.md) | [한국어](./README.ko.md) | [繁體中文](./README.zh-TW.md) | [简体中文](./README.zh-CN.md) | [Español](./README.es.md) | [Deutsch](./README.de.md) | [Français](./README.fr.md)
2
+
3
+ # Katagami
4
+
5
+ Contenedor DI ligero para TypeScript con inferencia de tipos completa.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/katagami)](https://www.npmjs.com/package/katagami)
8
+ [![license](https://img.shields.io/npm/l/katagami)](https://github.com/hiroiku/katagami/blob/master/LICENSE)
9
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/katagami)](https://bundlephobia.com/package/katagami)
10
+
11
+ > El nombre proviene de 型紙 _(katagami)_ — papel de estarcido de precisión utilizado en el teñido tradicional japonés para transferir patrones exactos sobre la tela. Se superponen múltiples estarcidos para componer diseños intrincados, de la misma manera que los tipos se acumulan con cada llamada en la cadena de métodos. Un estarcido solo necesita papel y un pincel, sin maquinaria elaborada — del mismo modo, Katagami no requiere decoradores ni mecanismos de metadatos y funciona con cualquier herramienta de construcción sin configuración adicional. Y al igual que los estarcidos se adaptan a diferentes telas y técnicas, Katagami se adapta a TypeScript y JavaScript, tokens de clase y tokens PropertyKey — un enfoque híbrido para una DI estricta y componible.
12
+
13
+ ## Características
14
+
15
+ | Característica | Descripción |
16
+ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
17
+ | Inferencia de tipos completa | Los tipos se acumulan mediante encadenamiento de métodos; los tokens no registrados generan errores en tiempo de compilación |
18
+ | Tres ciclos de vida | Singleton, Transient y Scoped con contenedores hijos |
19
+ | Fábricas asíncronas | Las fábricas que retornan Promise son rastreadas automáticamente por el sistema de tipos |
20
+ | Detección de dependencias circulares | Mensajes de error claros con la ruta completa del ciclo |
21
+ | Soporte Disposable | TC39 Explicit Resource Management (`Symbol.dispose` / `Symbol.asyncDispose` / `await using`) |
22
+ | Prevención de dependencias cautivas | Las fábricas Singleton/Transient no pueden acceder a tokens Scoped; detectado en tiempo de compilación |
23
+ | Resolución opcional | `tryResolve` devuelve `undefined` para tokens no registrados en lugar de lanzar |
24
+ | Estrategia de tokens híbrida | Tokens de clase para seguridad de tipos estricta, tokens PropertyKey para flexibilidad |
25
+ | Mapa de tipos con interfaz | Pasa una interfaz a `createContainer<T>()` para registro independiente del orden |
26
+ | Cero dependencias | Sin decoradores, sin reflect-metadata, sin polyfills |
27
+
28
+ ## Instalación
29
+
30
+ ```bash
31
+ npm install katagami
32
+ ```
33
+
34
+ ## Inicio rápido
35
+
36
+ ```ts
37
+ import { createContainer } from 'katagami';
38
+
39
+ class Logger {
40
+ log(msg: string) {
41
+ console.log(msg);
42
+ }
43
+ }
44
+
45
+ class UserService {
46
+ constructor(private logger: Logger) {}
47
+ greet(name: string) {
48
+ this.logger.log(`Hello, ${name}`);
49
+ }
50
+ }
51
+
52
+ const container = createContainer()
53
+ .registerSingleton(Logger, () => new Logger())
54
+ .registerSingleton(UserService, r => new UserService(r.resolve(Logger)));
55
+
56
+ const userService = container.resolve(UserService);
57
+ // ^? UserService (inferencia completa)
58
+ userService.greet('world');
59
+ ```
60
+
61
+ ## Por qué Katagami
62
+
63
+ La mayoría de los contenedores DI de TypeScript dependen de decoradores, reflect-metadata o tokens basados en cadenas de texto — cada uno con compromisos en compatibilidad de herramientas, seguridad de tipos o tamaño del paquete. Katagami adopta un enfoque diferente.
64
+
65
+ ### Sin decoradores, sin reflect-metadata
66
+
67
+ La DI basada en decoradores requiere las opciones del compilador `experimentalDecorators` y `emitDecoratorMetadata`. Las herramientas de construcción modernas como esbuild y Vite (configuración por defecto) no soportan `emitDecoratorMetadata`, y la propuesta de decoradores estándar TC39 no incluye un equivalente para la emisión automática de metadatos de tipos. Katagami no depende de nada de esto — funciona con cualquier herramienta de construcción sin configuración adicional.
68
+
69
+ ### Inferencia de tipos completa desde tokens de clase
70
+
71
+ La DI con tokens de cadena te obliga a mantener mapeos manuales de token a tipo. La coincidencia por nombre de parámetro se rompe con la minificación. Katagami usa clases directamente como tokens, así que `resolve` infiere automáticamente el tipo de retorno correcto — síncrono o `Promise` — sin anotaciones adicionales.
72
+
73
+ ### Acumulación de tipos en cadena de métodos
74
+
75
+ Los tipos se acumulan con cada llamada a `register`. Dentro de una fábrica, el resolver solo acepta tokens que ya han sido registrados en ese punto de la cadena. Resolver un token no registrado es un error de compilación, no una sorpresa en tiempo de ejecución.
76
+
77
+ ### Estrategia de tokens híbrida
78
+
79
+ Los tokens de clase te dan seguridad de tipos estricta y dependiente del orden a través del encadenamiento de métodos. Pero a veces quieres definir un conjunto de servicios por adelantado y registrarlos en cualquier orden. Pasa una interfaz a `createContainer<T>()` y usa tokens PropertyKey — el mapa de tipos se fija en el momento de la creación, así que el orden de registro no importa.
80
+
81
+ ### Cero dependencias
82
+
83
+ Sin dependencias en tiempo de ejecución, sin polyfills. No necesitas añadir reflect-metadata (~50 KB sin minificar) a tu paquete.
84
+
85
+ ## Guía
86
+
87
+ ### Singleton y Transient
88
+
89
+ Singleton crea la instancia en el primer `resolve` y la almacena en caché. Transient crea una nueva instancia cada vez.
90
+
91
+ ```ts
92
+ import { createContainer } from 'katagami';
93
+
94
+ class Database {
95
+ constructor(public id = Math.random()) {}
96
+ }
97
+
98
+ class RequestHandler {
99
+ constructor(public id = Math.random()) {}
100
+ }
101
+
102
+ const container = createContainer()
103
+ .registerSingleton(Database, () => new Database())
104
+ .registerTransient(RequestHandler, () => new RequestHandler());
105
+
106
+ // Singleton — siempre la misma instancia
107
+ container.resolve(Database) === container.resolve(Database); // true
108
+
109
+ // Transient — nueva instancia cada vez
110
+ container.resolve(RequestHandler) === container.resolve(RequestHandler); // false
111
+ ```
112
+
113
+ ### Ciclo de vida Scoped y contenedores hijos
114
+
115
+ Los registros Scoped se comportan como singletons dentro de un scope pero producen una instancia nueva en cada nuevo scope. Usa `createScope()` para crear un contenedor hijo. Los tokens Scoped no pueden resolverse desde el contenedor raíz.
116
+
117
+ ```ts
118
+ import { createContainer } from 'katagami';
119
+
120
+ class DbPool {
121
+ constructor(public name = 'main') {}
122
+ }
123
+
124
+ class RequestContext {
125
+ constructor(public id = Math.random()) {}
126
+ }
127
+
128
+ const root = createContainer()
129
+ .registerSingleton(DbPool, () => new DbPool())
130
+ .registerScoped(RequestContext, () => new RequestContext());
131
+
132
+ // Crear un scope para cada petición
133
+ const scope1 = root.createScope();
134
+ const scope2 = root.createScope();
135
+
136
+ // Scoped — mismo dentro de un scope, diferente entre scopes
137
+ scope1.resolve(RequestContext) === scope1.resolve(RequestContext); // true
138
+ scope1.resolve(RequestContext) === scope2.resolve(RequestContext); // false
139
+
140
+ // Singleton — compartido entre todos los scopes
141
+ scope1.resolve(DbPool) === scope2.resolve(DbPool); // true
142
+ ```
143
+
144
+ Los scopes también pueden anidarse. Cada scope anidado tiene su propia caché de instancias Scoped mientras comparte singletons con su padre:
145
+
146
+ ```ts
147
+ const parentScope = root.createScope();
148
+ const childScope = parentScope.createScope();
149
+
150
+ // Cada scope anidado obtiene sus propias instancias Scoped
151
+ parentScope.resolve(RequestContext) === childScope.resolve(RequestContext); // false
152
+
153
+ // Los singletons siguen siendo compartidos
154
+ parentScope.resolve(DbPool) === childScope.resolve(DbPool); // true
155
+ ```
156
+
157
+ ### Fábricas asíncronas
158
+
159
+ Las fábricas que retornan `Promise` son rastreadas automáticamente por el sistema de tipos. Cuando resuelves un token asíncrono, el tipo de retorno es `Promise<V>` en lugar de `V`:
160
+
161
+ ```ts
162
+ import { createContainer } from 'katagami';
163
+
164
+ class Database {
165
+ constructor(public connected: boolean) {}
166
+ }
167
+
168
+ class Logger {
169
+ log(msg: string) {
170
+ console.log(msg);
171
+ }
172
+ }
173
+
174
+ const container = createContainer()
175
+ .registerSingleton(Logger, () => new Logger())
176
+ .registerSingleton(Database, async () => {
177
+ await new Promise(r => setTimeout(r, 100)); // simular inicialización asíncrona
178
+ return new Database(true);
179
+ });
180
+
181
+ const logger = container.resolve(Logger);
182
+ // ^? Logger
183
+
184
+ const db = await container.resolve(Database);
185
+ // ^? Promise<Database> (con await → Database)
186
+ db.connected; // true
187
+ ```
188
+
189
+ Las fábricas asíncronas pueden depender tanto de registros síncronos como asíncronos:
190
+
191
+ ```ts
192
+ const container = createContainer()
193
+ .registerSingleton(Logger, () => new Logger())
194
+ .registerSingleton(Database, async r => {
195
+ const logger = r.resolve(Logger); // síncrono → Logger
196
+ logger.log('Conectando...');
197
+ return new Database(true);
198
+ });
199
+ ```
200
+
201
+ ### Detección de dependencias circulares
202
+
203
+ Katagami rastrea qué tokens se están resolviendo actualmente. Si se encuentra una dependencia circular, se lanza un `ContainerError` con un mensaje claro que muestra la ruta completa del ciclo:
204
+
205
+ ```ts
206
+ import { createContainer } from 'katagami';
207
+
208
+ class ServiceA {
209
+ constructor(public b: ServiceB) {}
210
+ }
211
+
212
+ class ServiceB {
213
+ constructor(public a: ServiceA) {}
214
+ }
215
+
216
+ const container = createContainer()
217
+ .registerSingleton(ServiceA, r => new ServiceA(r.resolve(ServiceB)))
218
+ .registerSingleton(ServiceB, r => new ServiceB(r.resolve(ServiceA)));
219
+
220
+ container.resolve(ServiceA);
221
+ // ContainerError: Circular dependency detected: ServiceA -> ServiceB -> ServiceA
222
+ ```
223
+
224
+ Los ciclos indirectos también son detectados:
225
+
226
+ ```
227
+ ContainerError: Circular dependency detected: ServiceX -> ServiceY -> ServiceZ -> ServiceX
228
+ ```
229
+
230
+ ### Soporte Disposable
231
+
232
+ Tanto `Container` como `Scope` implementan `AsyncDisposable`. Al ser eliminados, las instancias gestionadas se recorren en orden inverso de creación (LIFO) y sus métodos `[Symbol.asyncDispose]()` o `[Symbol.dispose]()` se llaman automáticamente.
233
+
234
+ ```ts
235
+ import { createContainer } from 'katagami';
236
+
237
+ class Connection {
238
+ async [Symbol.asyncDispose]() {
239
+ console.log('Connection closed');
240
+ }
241
+ }
242
+
243
+ // Eliminación manual
244
+ const container = createContainer().registerSingleton(Connection, () => new Connection());
245
+
246
+ container.resolve(Connection);
247
+ await container[Symbol.asyncDispose]();
248
+ // => "Connection closed"
249
+ ```
250
+
251
+ Con `await using`, los scopes se eliminan automáticamente al final del bloque:
252
+
253
+ ```ts
254
+ const root = createContainer()
255
+ .registerSingleton(DbPool, () => new DbPool())
256
+ .registerScoped(Connection, () => new Connection());
257
+
258
+ {
259
+ await using scope = root.createScope();
260
+ const conn = scope.resolve(Connection);
261
+ // ... usar conn ...
262
+ } // el scope se elimina aquí — Connection se limpia, DbPool no
263
+ ```
264
+
265
+ La eliminación del scope solo afecta a las instancias Scoped. Las instancias Singleton son propiedad del contenedor raíz y se eliminan cuando el propio contenedor es eliminado.
266
+
267
+ ### Mapa de tipos con interfaz
268
+
269
+ Cuando pasas una interfaz a `createContainer<T>()`, los tokens PropertyKey obtienen sus tipos de la interfaz en lugar de acumularlos mediante encadenamiento. Esto significa que puedes registrar y resolver tokens en cualquier orden:
270
+
271
+ ```ts
272
+ import { createContainer } from 'katagami';
273
+
274
+ class Logger {
275
+ log(msg: string) {
276
+ console.log(msg);
277
+ }
278
+ }
279
+
280
+ interface Services {
281
+ logger: Logger;
282
+ greeting: string;
283
+ }
284
+
285
+ const container = createContainer<Services>()
286
+ // 'greeting' puede referenciar 'logger' aunque se registre después
287
+ .registerSingleton('greeting', r => {
288
+ r.resolve('logger').log('Construyendo greeting...');
289
+ return 'Hello!';
290
+ })
291
+ .registerSingleton('logger', () => new Logger());
292
+
293
+ const greeting = container.resolve('greeting');
294
+ // ^? string
295
+ ```
296
+
297
+ ### Estrategia de tokens híbrida
298
+
299
+ Puedes mezclar ambos enfoques — usa tokens de clase para seguridad de tipos dependiente del orden y tokens PropertyKey para flexibilidad independiente del orden:
300
+
301
+ ```ts
302
+ const container = createContainer<Services>()
303
+ .registerSingleton(Logger, () => new Logger())
304
+ .registerSingleton('logger', () => new Logger())
305
+ .registerSingleton('greeting', r => {
306
+ r.resolve(Logger).log('Construyendo greeting...');
307
+ return 'Hello!';
308
+ });
309
+ ```
310
+
311
+ ### Prevención de dependencias cautivas
312
+
313
+ Una "dependencia cautiva" ocurre cuando un servicio de larga vida (singleton o transient) captura un servicio de corta vida (scoped), manteniéndolo vivo más allá de su scope previsto. Katagami previene esto en tiempo de compilación — las fábricas singleton y transient solo reciben un resolver limitado a tokens no Scoped:
314
+
315
+ ```ts
316
+ import { createContainer } from 'katagami';
317
+
318
+ class DbPool {}
319
+ class RequestContext {}
320
+
321
+ const container = createContainer()
322
+ .registerScoped(RequestContext, () => new RequestContext())
323
+ // @ts-expect-error — la fábrica singleton no puede resolver tokens Scoped
324
+ .registerSingleton(DbPool, r => new DbPool(r.resolve(RequestContext)));
325
+ ```
326
+
327
+ Las fábricas Scoped, por otro lado, pueden resolver tanto tokens Scoped como no Scoped:
328
+
329
+ ```ts
330
+ const container = createContainer()
331
+ .registerSingleton(DbPool, () => new DbPool())
332
+ .registerScoped(RequestContext, r => {
333
+ r.resolve(DbPool); // OK — la fábrica Scoped puede resolver tokens Singleton
334
+ return new RequestContext();
335
+ });
336
+ ```
337
+
338
+ ## API
339
+
340
+ ### `createContainer<T, ScopedT>()`
341
+
342
+ Crea un nuevo contenedor DI. Pasa una interfaz como `T` para definir el mapa de tipos para tokens PropertyKey. Pasa `ScopedT` para definir un mapa de tipos separado para tokens PropertyKey Scoped (independiente del orden, igual que `T`).
343
+
344
+ ### `container.registerSingleton(token, factory)`
345
+
346
+ Registra una fábrica como singleton. La instancia se crea en el primer `resolve` y se almacena en caché. Retorna el contenedor para encadenamiento de métodos.
347
+
348
+ ### `container.registerTransient(token, factory)`
349
+
350
+ Registra una fábrica como transient. Se crea una nueva instancia en cada `resolve`. Retorna el contenedor para encadenamiento de métodos.
351
+
352
+ ### `container.registerScoped(token, factory)`
353
+
354
+ Registra una fábrica como scoped. Dentro de un scope, la instancia se crea en el primer `resolve` y se almacena en caché para ese scope. Cada scope mantiene su propia caché. Los tokens Scoped no pueden resolverse desde el contenedor raíz. Retorna el contenedor para encadenamiento de métodos.
355
+
356
+ ### `container.resolve(token)`
357
+
358
+ Resuelve y retorna la instancia para el token dado. Lanza `ContainerError` si el token no está registrado o si se detecta una dependencia circular.
359
+
360
+ ### `container.tryResolve(token)` / `scope.tryResolve(token)`
361
+
362
+ Intenta resolver la instancia para el token dado. Devuelve `undefined` si el token no está registrado, en lugar de lanzar. Aún lanza `ContainerError` para dependencias circulares u operaciones en contenedores/scopes eliminados.
363
+
364
+ ### `container.createScope()`
365
+
366
+ Crea un nuevo `Scope` (contenedor hijo). El scope hereda todos los registros del padre. Las instancias Singleton se comparten con el padre, mientras que las instancias Scoped son locales al scope.
367
+
368
+ ### `Scope`
369
+
370
+ Un contenedor hijo con scope creado por `createScope()`. Proporciona `resolve(token)`, `tryResolve(token)`, `createScope()` (para scopes anidados) y `[Symbol.asyncDispose]()`.
371
+
372
+ ### `container[Symbol.asyncDispose]()` / `scope[Symbol.asyncDispose]()`
373
+
374
+ Elimina todas las instancias gestionadas en orden inverso de creación (LIFO). Llama a `[Symbol.asyncDispose]()` o `[Symbol.dispose]()` en cada instancia que los implemente. Idempotente — las llamadas posteriores no hacen nada. Después de la eliminación, `resolve()` y `createScope()` lanzarán `ContainerError`.
375
+
376
+ ### `ContainerError`
377
+
378
+ Clase de error lanzada para fallos del contenedor como resolver un token no registrado, dependencias circulares u operaciones en un contenedor/scope eliminado.
379
+
380
+ ### `Resolver`
381
+
382
+ Exportación de tipo que representa el resolver pasado a los callbacks de fábrica. Útil cuando necesitas tipar una función que acepta un parámetro resolver.
383
+
384
+ ## Licencia
385
+
386
+ MIT