sandly 2.0.0 → 2.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.
Files changed (3) hide show
  1. package/README.md +165 -133
  2. package/dist/index.d.ts +17 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sandly
2
2
 
3
- Type-safe dependency injection for TypeScript. No decorators, no runtime reflection, just compile-time safety that catches errors before your code runs.
3
+ Sandly ("Services And Layers") is a type-safe dependency injection library for TypeScript. No decorators, no runtime reflection, just compile-time safety that catches errors before your code runs.
4
4
 
5
5
  ## Why Sandly?
6
6
 
@@ -10,12 +10,16 @@ Most TypeScript DI libraries rely on experimental decorators and runtime reflect
10
10
  import { Container, Layer } from 'sandly';
11
11
 
12
12
  class Database {
13
- query(sql: string) { return []; }
13
+ query(sql: string) {
14
+ return [];
15
+ }
14
16
  }
15
17
 
16
18
  class UserService {
17
- constructor(private db: Database) {}
18
- getUsers() { return this.db.query('SELECT * FROM users'); }
19
+ constructor(private db: Database) {}
20
+ getUsers() {
21
+ return this.db.query('SELECT * FROM users');
22
+ }
19
23
  }
20
24
 
21
25
  // Define layers
@@ -60,24 +64,24 @@ import { Container, Layer, Tag } from 'sandly';
60
64
 
61
65
  // Any class can be a dependency - no special base class needed
62
66
  class Database {
63
- async query(sql: string) {
64
- return [{ id: 1, name: 'Alice' }];
65
- }
66
- async close() {
67
- console.log('Database closed');
68
- }
67
+ async query(sql: string) {
68
+ return [{ id: 1, name: 'Alice' }];
69
+ }
70
+ async close() {
71
+ console.log('Database closed');
72
+ }
69
73
  }
70
74
 
71
75
  class UserRepository {
72
- constructor(private db: Database) {}
73
- findAll() {
74
- return this.db.query('SELECT * FROM users');
75
- }
76
+ constructor(private db: Database) {}
77
+ findAll() {
78
+ return this.db.query('SELECT * FROM users');
79
+ }
76
80
  }
77
81
 
78
82
  // Create layers
79
83
  const dbLayer = Layer.service(Database, [], {
80
- cleanup: (db) => db.close()
84
+ cleanup: (db) => db.close(),
81
85
  });
82
86
 
83
87
  const userRepoLayer = Layer.service(UserRepository, [Database]);
@@ -104,7 +108,9 @@ Tags identify dependencies. There are two types:
104
108
 
105
109
  ```typescript
106
110
  class UserService {
107
- getUsers() { return []; }
111
+ getUsers() {
112
+ return [];
113
+ }
108
114
  }
109
115
 
110
116
  // UserService is both the class and its tag
@@ -118,7 +124,9 @@ const PortTag = Tag.of('Port')<number>();
118
124
  const ConfigTag = Tag.of('Config')<{ apiUrl: string }>();
119
125
 
120
126
  const portLayer = Layer.value(PortTag, 3000);
121
- const configLayer = Layer.value(ConfigTag, { apiUrl: 'https://api.example.com' });
127
+ const configLayer = Layer.value(ConfigTag, {
128
+ apiUrl: 'https://api.example.com',
129
+ });
122
130
  ```
123
131
 
124
132
  ### Container
@@ -131,19 +139,20 @@ const container = Container.from(appLayer);
131
139
 
132
140
  // Or build manually
133
141
  const container = Container.builder()
134
- .add(Database, () => new Database())
135
- .add(UserService, async (ctx) =>
136
- new UserService(await ctx.resolve(Database))
137
- )
138
- .build();
142
+ .add(Database, () => new Database())
143
+ .add(
144
+ UserService,
145
+ async (ctx) => new UserService(await ctx.resolve(Database))
146
+ )
147
+ .build();
139
148
 
140
149
  // Resolve dependencies
141
150
  const db = await container.resolve(Database);
142
151
  const [db, users] = await container.resolveAll(Database, UserService);
143
152
 
144
153
  // Use and discard pattern - resolves, runs callback, then destroys
145
- const result = await container.use(UserService, (service) =>
146
- service.getUsers()
154
+ const result = await container.use(UserService, (service) =>
155
+ service.getUsers()
147
156
  );
148
157
 
149
158
  // Manual clean up
@@ -165,11 +174,12 @@ const configLayer = Layer.value(ConfigTag, { port: 3000 });
165
174
 
166
175
  // Layer.create for custom factory logic
167
176
  const cacheLayer = Layer.create({
168
- requires: [ConfigTag],
169
- apply: (builder) => builder.add(Cache, async (ctx) => {
170
- const config = await ctx.resolve(ConfigTag);
171
- return new Cache({ ttl: config.cacheTtl });
172
- })
177
+ requires: [ConfigTag],
178
+ apply: (builder) =>
179
+ builder.add(Cache, async (ctx) => {
180
+ const config = await ctx.resolve(ConfigTag);
181
+ return new Cache({ ttl: config.cacheTtl });
182
+ }),
173
183
  });
174
184
  ```
175
185
 
@@ -195,16 +205,17 @@ Scoped containers enable hierarchical dependency management:
195
205
  ```typescript
196
206
  // Application scope - use builder to add dependencies
197
207
  const appContainer = ScopedContainer.builder('app')
198
- .add(Database, () => new Database())
199
- .build();
208
+ .add(Database, () => new Database())
209
+ .build();
200
210
 
201
211
  // Request scope - use child() to create a child builder
202
- const requestContainer = appContainer.child('request')
203
- .add(RequestContext, () => new RequestContext())
204
- .build();
212
+ const requestContainer = appContainer
213
+ .child('request')
214
+ .add(RequestContext, () => new RequestContext())
215
+ .build();
205
216
 
206
217
  // Child can resolve both its own and parent dependencies
207
- const db = await requestContainer.resolve(Database); // From parent
218
+ const db = await requestContainer.resolve(Database); // From parent
208
219
  const ctx = await requestContainer.resolve(RequestContext); // From child
209
220
 
210
221
  // Destroy child without affecting parent
@@ -215,8 +226,9 @@ Or use layers with `childFrom`:
215
226
 
216
227
  ```typescript
217
228
  const appContainer = ScopedContainer.from('app', dbLayer);
218
- const requestContainer = appContainer.childFrom('request',
219
- Layer.value(RequestContext, new RequestContext())
229
+ const requestContainer = appContainer.childFrom(
230
+ 'request',
231
+ Layer.value(RequestContext, new RequestContext())
220
232
  );
221
233
  ```
222
234
 
@@ -227,8 +239,8 @@ The `use()` method resolves a service, runs a callback, and automatically destro
227
239
  ```typescript
228
240
  // Perfect for short-lived operations like Lambda handlers or worker jobs
229
241
  const result = await appContainer
230
- .childFrom('request', requestLayer)
231
- .use(UserService, (service) => service.processEvent(event));
242
+ .childFrom('request', requestLayer)
243
+ .use(UserService, (service) => service.processEvent(event));
232
244
  // Container is automatically destroyed after callback completes
233
245
  ```
234
246
 
@@ -242,7 +254,10 @@ This is especially useful for serverless functions or message handlers where the
242
254
 
243
255
  ```typescript
244
256
  class ApiClient {
245
- constructor(private config: Config, private logger: Logger) {}
257
+ constructor(
258
+ private config: Config,
259
+ private logger: Logger
260
+ ) {}
246
261
  }
247
262
 
248
263
  // Dependencies must match constructor parameters in order
@@ -250,28 +265,37 @@ const apiLayer = Layer.service(ApiClient, [Config, Logger]);
250
265
 
251
266
  // With cleanup function
252
267
  const dbLayer = Layer.service(Database, [], {
253
- cleanup: (db) => db.close()
268
+ cleanup: (db) => db.close(),
254
269
  });
255
270
  ```
256
271
 
257
- **Layer.value**: Constant values
272
+ **Layer.value**: Constant values or pre-instantiated instances
258
273
 
259
274
  ```typescript
275
+ // ValueTag (constants)
260
276
  const ApiKeyTag = Tag.of('apiKey')<string>();
261
277
  const configLayer = Layer.value(ApiKeyTag, process.env.API_KEY!);
278
+
279
+ // ServiceTag (pre-instantiated instances, useful for testing)
280
+ class UserService {
281
+ getUsers() { return []; }
282
+ }
283
+ const mockUserService = new UserService();
284
+ const testLayer = Layer.value(UserService, mockUserService);
262
285
  ```
263
286
 
264
287
  **Layer.create**: Custom factory logic
265
288
 
266
289
  ```typescript
267
290
  const dbLayer = Layer.create({
268
- requires: [ConfigTag],
269
- apply: (builder) => builder.add(Database, async (ctx) => {
270
- const config = await ctx.resolve(ConfigTag);
271
- const db = new Database(config.dbUrl);
272
- await db.connect();
273
- return db;
274
- })
291
+ requires: [ConfigTag],
292
+ apply: (builder) =>
293
+ builder.add(Database, async (ctx) => {
294
+ const config = await ctx.resolve(ConfigTag);
295
+ const db = new Database(config.dbUrl);
296
+ await db.connect();
297
+ return db;
298
+ }),
275
299
  });
276
300
  ```
277
301
 
@@ -286,10 +310,10 @@ const serviceLayer = Layer.service(UserService, [UserRepository, Logger]);
286
310
 
287
311
  // Compose into complete application
288
312
  const appLayer = serviceLayer
289
- .provide(repoLayer)
290
- .provide(dbLayer)
291
- .provide(configLayer)
292
- .provide(Layer.service(Logger, []));
313
+ .provide(repoLayer)
314
+ .provide(dbLayer)
315
+ .provide(configLayer)
316
+ .provide(Layer.service(Logger, []));
293
317
 
294
318
  // Create container - all dependencies satisfied
295
319
  const container = Container.from(appLayer);
@@ -324,48 +348,55 @@ const container = Container.from(incomplete); // Type error!
324
348
  import { ScopedContainer, Layer } from 'sandly';
325
349
 
326
350
  // App-level dependencies (shared across requests)
327
- const appContainer = ScopedContainer.from('app',
328
- Layer.mergeAll(dbLayer, loggerLayer)
351
+ const appContainer = ScopedContainer.from(
352
+ 'app',
353
+ Layer.mergeAll(dbLayer, loggerLayer)
329
354
  );
330
355
 
331
356
  // Express middleware
332
357
  app.use(async (req, res, next) => {
333
- // Create request scope with request-specific dependencies
334
- const requestScope = appContainer.childFrom('request',
335
- Layer.value(RequestContext, {
336
- requestId: crypto.randomUUID(),
337
- userId: req.user?.id
338
- })
339
- );
340
-
341
- res.locals.container = requestScope;
342
-
343
- res.on('finish', () => requestScope.destroy());
344
- next();
358
+ // Create request scope with request-specific dependencies
359
+ const requestScope = appContainer.childFrom(
360
+ 'request',
361
+ Layer.value(RequestContext, {
362
+ requestId: crypto.randomUUID(),
363
+ userId: req.user?.id,
364
+ })
365
+ );
366
+
367
+ res.locals.container = requestScope;
368
+
369
+ res.on('finish', () => requestScope.destroy());
370
+ next();
345
371
  });
346
372
 
347
373
  // Route handler
348
374
  app.get('/users', async (req, res) => {
349
- const userService = await res.locals.container.resolve(UserService);
350
- res.json(await userService.getUsers());
375
+ const userService = await res.locals.container.resolve(UserService);
376
+ res.json(await userService.getUsers());
351
377
  });
352
378
  ```
353
379
 
354
380
  ### Destruction Order
355
381
 
356
382
  When destroying a scoped container:
383
+
357
384
  1. Child scopes are destroyed first
358
385
  2. Then the current scope's finalizers run
359
386
  3. Parent scope is unaffected
360
387
 
361
388
  ```typescript
362
389
  const parent = ScopedContainer.builder('parent')
363
- .add(Database, { create: () => new Database(), cleanup: (db) => db.close() })
364
- .build();
390
+ .add(Database, {
391
+ create: () => new Database(),
392
+ cleanup: (db) => db.close(),
393
+ })
394
+ .build();
365
395
 
366
- const child = parent.child('child')
367
- .add(Cache, { create: () => new Cache(), cleanup: (c) => c.clear() })
368
- .build();
396
+ const child = parent
397
+ .child('child')
398
+ .add(Cache, { create: () => new Cache(), cleanup: (c) => c.clear() })
399
+ .build();
369
400
 
370
401
  await parent.destroy(); // Destroys child first (Cache.clear), then parent (Database.close)
371
402
  ```
@@ -376,25 +407,25 @@ Sandly provides specific error types for common issues:
376
407
 
377
408
  ```typescript
378
409
  import {
379
- UnknownDependencyError,
380
- CircularDependencyError,
381
- DependencyCreationError,
382
- DependencyFinalizationError
410
+ UnknownDependencyError,
411
+ CircularDependencyError,
412
+ DependencyCreationError,
413
+ DependencyFinalizationError,
383
414
  } from 'sandly';
384
415
 
385
416
  try {
386
- const service = await container.resolve(UserService);
417
+ const service = await container.resolve(UserService);
387
418
  } catch (error) {
388
- if (error instanceof CircularDependencyError) {
389
- console.log(error.message);
390
- // "Circular dependency detected for UserService: UserService -> Database -> UserService"
391
- }
392
-
393
- if (error instanceof DependencyCreationError) {
394
- // Get the original error that caused the failure
395
- const rootCause = error.getRootCause();
396
- console.log(rootCause.message);
397
- }
419
+ if (error instanceof CircularDependencyError) {
420
+ console.log(error.message);
421
+ // "Circular dependency detected for UserService: UserService -> Database -> UserService"
422
+ }
423
+
424
+ if (error instanceof DependencyCreationError) {
425
+ // Get the original error that caused the failure
426
+ const rootCause = error.getRootCause();
427
+ console.log(rootCause.message);
428
+ }
398
429
  }
399
430
  ```
400
431
 
@@ -402,68 +433,69 @@ try {
402
433
 
403
434
  ### Container
404
435
 
405
- | Method | Description |
406
- |--------|-------------|
407
- | `Container.from(layer)` | Create container from a fully resolved layer |
408
- | `Container.builder()` | Create a container builder |
409
- | `Container.empty()` | Create an empty container |
410
- | `Container.scoped(scope)` | Create an empty scoped container |
411
- | `container.resolve(tag)` | Get a dependency instance |
412
- | `container.resolveAll(...tags)` | Get multiple dependencies |
413
- | `container.use(tag, fn)` | Resolve, run callback, then destroy container |
414
- | `container.destroy()` | Run finalizers and clean up |
436
+ | Method | Description |
437
+ | ------------------------------- | --------------------------------------------- |
438
+ | `Container.from(layer)` | Create container from a fully resolved layer |
439
+ | `Container.builder()` | Create a container builder |
440
+ | `Container.empty()` | Create an empty container |
441
+ | `Container.scoped(scope)` | Create an empty scoped container |
442
+ | `container.resolve(tag)` | Get a dependency instance |
443
+ | `container.resolveAll(...tags)` | Get multiple dependencies |
444
+ | `container.use(tag, fn)` | Resolve, run callback, then destroy container |
445
+ | `container.destroy()` | Run finalizers and clean up |
415
446
 
416
447
  ### ContainerBuilder
417
448
 
418
- | Method | Description |
419
- |--------|-------------|
449
+ | Method | Description |
450
+ | ------------------------ | --------------------- |
420
451
  | `builder.add(tag, spec)` | Register a dependency |
421
- | `builder.build()` | Create the container |
452
+ | `builder.build()` | Create the container |
422
453
 
423
454
  ### Layer
424
455
 
425
- | Method | Description |
426
- |--------|-------------|
427
- | `Layer.service(class, deps, options?)` | Create layer for a class |
428
- | `Layer.value(tag, value)` | Create layer for a constant value |
429
- | `Layer.create({ requires, apply })` | Create custom layer |
430
- | `Layer.empty()` | Create empty layer |
431
- | `Layer.merge(a, b)` | Merge two layers |
432
- | `Layer.mergeAll(...layers)` | Merge multiple layers |
433
- | `layer.provide(dep)` | Satisfy dependencies |
434
- | `layer.provideMerge(dep)` | Satisfy and merge provisions |
435
- | `layer.merge(other)` | Merge with another layer |
456
+ | Method | Description |
457
+ | -------------------------------------- | --------------------------------- |
458
+ | `Layer.service(class, deps, options?)` | Create layer for a class |
459
+ | `Layer.value(tag, value)` | Create layer for a constant value |
460
+ | `Layer.create({ requires, apply })` | Create custom layer |
461
+ | `Layer.empty()` | Create empty layer |
462
+ | `Layer.merge(a, b)` | Merge two layers |
463
+ | `Layer.mergeAll(...layers)` | Merge multiple layers |
464
+ | `layer.provide(dep)` | Satisfy dependencies |
465
+ | `layer.provideMerge(dep)` | Satisfy and merge provisions |
466
+ | `layer.merge(other)` | Merge with another layer |
436
467
 
437
468
  ### ScopedContainer
438
469
 
439
- | Method | Description |
440
- |--------|-------------|
441
- | `ScopedContainer.builder(scope)` | Create a new scoped container builder |
442
- | `ScopedContainer.empty(scope)` | Create empty scoped container |
443
- | `ScopedContainer.from(scope, layer)` | Create from layer |
444
- | `container.child(scope)` | Create child scope builder |
445
- | `container.childFrom(scope, layer)` | Create child scope from layer (convenience) |
470
+ | Method | Description |
471
+ | ------------------------------------ | ------------------------------------------- |
472
+ | `ScopedContainer.builder(scope)` | Create a new scoped container builder |
473
+ | `ScopedContainer.empty(scope)` | Create empty scoped container |
474
+ | `ScopedContainer.from(scope, layer)` | Create from layer |
475
+ | `container.child(scope)` | Create child scope builder |
476
+ | `container.childFrom(scope, layer)` | Create child scope from layer (convenience) |
446
477
 
447
478
  ### Tag
448
479
 
449
- | Method | Description |
450
- |--------|-------------|
451
- | `Tag.of(id)<T>()` | Create a ValueTag |
452
- | `Tag.id(tag)` | Get tag's string identifier |
453
- | `Tag.isTag(value)` | Check if value is a tag |
480
+ | Method | Description |
481
+ | ------------------ | --------------------------- |
482
+ | `Tag.of(id)<T>()` | Create a ValueTag |
483
+ | `Tag.id(tag)` | Get tag's string identifier |
484
+ | `Tag.isTag(value)` | Check if value is a tag |
454
485
 
455
486
  ## Comparison with Alternatives
456
487
 
457
- | Feature | Sandly | NestJS | InversifyJS | TSyringe |
458
- |---------|--------|--------|-------------|----------|
459
- | Compile-time type safety | ✅ | ❌ | ⚠️ Partial | ❌ |
460
- | No experimental decorators | ✅ | ❌ | ❌ | ❌ |
461
- | Async factories | ✅ | ✅ | ❌ | ❌ |
462
- | Framework-agnostic | ✅ | ❌ | ✅ | ✅ |
463
- | Layer composition | ✅ | ❌ | ❌ | ❌ |
464
- | Zero dependencies | ✅ | ❌ | ❌ | ❌ |
488
+ | Feature | Sandly | NestJS | InversifyJS | TSyringe |
489
+ | -------------------------- | ------ | ------ | ----------- | -------- |
490
+ | Compile-time type safety | ✅ | ❌ | ⚠️ Partial | ❌ |
491
+ | No experimental decorators | ✅ | ❌ | ❌ | ❌ |
492
+ | Async factories | ✅ | ✅ | ❌ | ❌ |
493
+ | Framework-agnostic | ✅ | ❌ | ✅ | ✅ |
494
+ | Layer composition | ✅ | ❌ | ❌ | ❌ |
495
+ | Zero dependencies | ✅ | ❌ | ❌ | ❌ |
465
496
 
466
497
  **Choose Sandly when you want:**
498
+
467
499
  - Type safety without sacrificing simplicity
468
500
  - DI without experimental decorators
469
501
  - Composable, reusable dependency modules
package/dist/index.d.ts CHANGED
@@ -325,12 +325,14 @@ declare const Layer: {
325
325
  cleanup?: Finalizer<InstanceType<TClass>>;
326
326
  }): Layer<ExtractTags<TDeps>, TClass>;
327
327
  /**
328
- * Creates a layer that provides a constant value.
328
+ * Creates a layer that provides a constant value or pre-instantiated instance.
329
329
  *
330
- * @param tag - The ValueTag to register
331
- * @param value - The value to provide
330
+ * Works with both ValueTags (for constants) and ServiceTags (for pre-instantiated instances, useful in tests).
332
331
  *
333
- * @example
332
+ * @param tag - The tag (ValueTag or ServiceTag) to register
333
+ * @param value - The value or instance to provide
334
+ *
335
+ * @example ValueTag (constant)
334
336
  * ```typescript
335
337
  * const ApiKeyTag = Tag.of('apiKey')<string>();
336
338
  * const ConfigTag = Tag.of('config')<{ port: number }>();
@@ -338,8 +340,18 @@ declare const Layer: {
338
340
  * const configLayer = Layer.value(ApiKeyTag, 'secret-key')
339
341
  * .merge(Layer.value(ConfigTag, { port: 3000 }));
340
342
  * ```
343
+ *
344
+ * @example ServiceTag (pre-instantiated instance, useful for testing)
345
+ * ```typescript
346
+ * class UserService {
347
+ * getUsers() { return []; }
348
+ * }
349
+ *
350
+ * const mockUserService = new UserService();
351
+ * const testLayer = Layer.value(UserService, mockUserService);
352
+ * ```
341
353
  */
342
- value<T extends ValueTag<TagId, unknown>>(tag: T, value: TagType<T>): Layer<never, T>;
354
+ value<T extends AnyTag>(tag: T, value: TagType<T>): Layer<never, T>;
343
355
  /**
344
356
  * Creates a custom layer with full control over the factory logic.
345
357
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sandly",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "keywords": [
5
5
  "typescript",
6
6
  "sandly",