sandly
Version:
**⚠️ This project is under heavy development and APIs may change.**
212 lines (163 loc) • 5.65 kB
Markdown
# Sandly
**⚠️ This project is under heavy development and APIs may change.**
A dependency injection framework for TypeScript that emphasizes complete type safety, modular architecture, and scope management.
_Sandly_ stands for **Services & Layers** - reflecting the framework's core concepts of organizing code into composable service layers.
## Key Features
### Complete Type Safety
- **Zero runtime errors**: All dependencies are validated at compile time
- **Automatic type inference**: Container knows exactly what services are available
- **Constructor-based DI**: Dependencies are inferred from class constructors
```typescript
import { container, Tag } from 'sandly';
class DatabaseService extends Tag.Service('DatabaseService') {
query() {
return ['data'];
}
}
const container = Container.empty().register(DatabaseService, () => new DatabaseService());
// ✅ TypeScript knows DatabaseService is available
const db = await container.resolve(DatabaseService);
// ❌ Compile error - UserService not registered
const user = await container.resolve(UserService); // Type error
```
### Modular Architecture with Layers
Organize dependencies into composable layers that promote clean architecture:
```typescript
// Infrastructure layer
const databaseLayer = layer<never, typeof DatabaseService>((container) =>
container.register(DatabaseService, () => new DatabaseService())
);
// Service layer that depends on infrastructure
const userServiceLayer = layer<typeof DatabaseService, typeof UserService>(
(container) =>
container.register(
UserService,
async (c) => new UserService(await c.resolve(DatabaseService))
)
);
// Compose layers
const appLayer = databaseLayer().provide(userServiceLayer());
const app = appLayer.register(container());
```
### Advanced Scope Management
Built-in support for request/runtime scopes with automatic cleanup:
```typescript
// Runtime-scoped dependencies (shared across requests)
const runtime = scopedContainer('runtime').register(DatabaseService, {
factory: () => new DatabaseService(),
finalizer: (db) => db.disconnect(),
});
// Lambda handler example
export const handler = async (event, context) => {
// Create request scope for this invocation
const requestContainer = runtime.child('request').register(
UserService,
async (c) => new UserService(await c.resolve(DatabaseService)) // Uses runtime DB
);
try {
const userService = await requestContainer.resolve(UserService);
return await userService.handleRequest(event);
} finally {
await requestContainer.destroy(); // Cleanup request scope
}
};
```
## Usage Examples
### Basic Container Usage
```typescript
import { container, Tag } from 'sandly';
class EmailService extends Tag.Service('EmailService') {
sendEmail(to: string, subject: string) {
return { messageId: 'msg-123' };
}
}
class UserService extends Tag.Service('UserService') {
constructor(private emailService: EmailService) {
super();
}
async createUser(email: string) {
// Create user logic
await this.emailService.sendEmail(email, 'Welcome!');
}
}
const app = container()
.register(EmailService, () => new EmailService())
.register(
UserService,
async (c) => new UserService(await c.resolve(EmailService))
);
const userService = await app.resolve(UserService);
```
### Service Pattern with Auto-Composition
```typescript
import { service, Layer } from 'sandly';
const emailService = service(EmailService, () => new EmailService());
const userService = service(
UserService,
async (container) => new UserService(await container.resolve(EmailService))
);
// Automatic dependency resolution
const app = emailService().provide(userService()).register(container());
```
### Value Tags for Configuration
```typescript
const ApiKeyTag = Tag.of('apiKey')<string>();
const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
class DatabaseService extends Tag.Service('DatabaseService') {
constructor(
private config: Inject<typeof ConfigTag>,
private apiKey: Inject<typeof ApiKeyTag>
) {
super();
}
}
const app = container()
.register(ApiKeyTag, () => process.env.API_KEY!)
.register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost' }))
.register(
DatabaseService,
async (c) =>
new DatabaseService(
await c.resolve(ConfigTag),
await c.resolve(ApiKeyTag)
)
);
```
### Complex Layer Composition
```typescript
// Infrastructure layers
const databaseLayer = layer<never, typeof DatabaseService>((container) =>
container.register(DatabaseService, () => new DatabaseService())
);
const cacheLayer = layer<never, typeof CacheService>((container) =>
container.register(CacheService, () => new CacheService())
);
// Business logic layer
const userServiceLayer = layer<
typeof DatabaseService | typeof CacheService,
typeof UserService
>((container) =>
container.register(
UserService,
async (c) =>
new UserService(
await c.resolve(DatabaseService),
await c.resolve(CacheService)
)
)
);
// Compose everything
const fullApplication = Layer.merge(databaseLayer(), cacheLayer()).provide(
userServiceLayer()
);
const app = fullApplication.register(container());
```
## Benefits
- **Type Safety**: Eliminates entire classes of runtime errors
- **Modular Design**: Layer system naturally guides you toward clean architecture
- **Performance**: Zero runtime overhead for dependency resolution
- **Flexibility**: Powerful scope management for any use case (web servers, serverless, etc.)
- **Developer Experience**: IntelliSense works perfectly, no magic strings
- **Testing**: Easy to mock dependencies and create isolated test containers
## License
MIT