Components and dependency injection
Every part of a Cell application is a component: a class the DI container creates, wires and keeps. Controllers, services, middleware and RPC services are all components. Cell's container is its own (@celljs/core), with no third-party IoC library.
Declaring and injecting
import { Autowired, Component, Value } from '@celljs/core';
@Component()
export class TodoRepository {
protected readonly todos: string[] = [];
add(title: string) { this.todos.push(title); }
all() { return [...this.todos]; }
}
@Component()
export class TodoService {
@Autowired() // by the declared type
protected readonly repository: TodoRepository;
@Value('app.todos.max') // configuration, see cell.yml
protected readonly max: number;
add(title: string) {
if (this.repository.all().length >= this.max) {
throw new Error('Too many todos');
}
this.repository.add(title);
}
}- Components are singletons by default: one instance per application.
@Component({ scope: Scope.Transient })makes a new one for every injection. - Inject into properties (above) or constructor parameters (
constructor(@Autowired(TodoRepository) protected readonly repository: TodoRepository)). Constructor parameters without a decorator are injected by their declared class. - The file must be imported by a DI module, or the class is never registered.
@Value('key')reads the mergedcell.yml;@Value()without a key uses the property name.
Interfaces: a Symbol with the same name
TypeScript interfaces do not exist at runtime, so the identifier of an interface is a Symbol exported under the same name. This is the convention throughout Cell:
// src/common/mailer.ts
export const Mailer = Symbol('Mailer');
export interface Mailer {
send(to: string, text: string): Promise<void>;
}// src/node/mailer-impl.ts
import { Component } from '@celljs/core';
import { Mailer } from '../common/mailer';
@Component(Mailer)
export class MailerImpl implements Mailer {
async send(to: string, text: string): Promise<void> { /* ... */ }
}
// elsewhere
@Autowired(Mailer)
protected readonly mailer: Mailer;@Component(Mailer) registers the class under the symbol and under the class itself; both give the same instance. @Component({ id: [A, B] }) registers more identifiers.
Several implementations
| Need | Write |
|---|---|
| All implementations | @Autowired(Handler) protected readonly handlers: Handler[] (an array type) |
| None is fine | add @Optional(): undefined, or [] for an array |
| Pick one by name | @Component({ id: Store, name: 'redis' }) and @Autowired(Store) @Named('redis') |
| Pick one by tag | @Component({ id: Store, tag: { tag: 'region', value: 'eu' } }) and @Autowired(Store) @Tagged('region', 'eu') |
| A default among several | @Component({ id: Store, default: true }): plain @Autowired(Store) gets it |
| Replace a framework component | @Component({ id: ErrorHandler, rebind: true }): removes the earlier bindings of that id |
| Many, sorted, created lazily | @AutowiredProvider(Step) protected readonly steps: Provider<Step>, then this.steps.sortSync() (highest priority first) |
Extension points of the framework (Middleware, ErrorHandler, ApplicationLifecycle, HealthIndicator, ...) are such multi-implementation symbols: you add an implementation by declaring @Component(Middleware) on your class, and rebind: true replaces the built-in one.
Lifecycle
import { Application, ApplicationLifecycle, Component, PostConstruct } from '@celljs/core';
@Component(ApplicationLifecycle)
export class Warmup implements ApplicationLifecycle<Application> {
initialize(): void { /* synchronous, before any onStart */ }
async onStart(app: Application): Promise<void> { /* awaited one after another */ }
onStop(app: Application): void { /* on shutdown and before every reload in cell dev */ }
}
@Component()
export class Cache {
@PostConstruct()
protected init(): void { /* after the properties are injected, once per instance */ }
}An error thrown in initialize, onStart or onStop is logged and does not stop the other lifecycles. For work that must not run at module load (opening connections, timers), use onStart, not the constructor: cell check creates the components without starting the application.
Logging
import { Autowired, Component, Logger } from '@celljs/core';
@Component()
export class TodoService {
@Autowired(Logger)
protected readonly logger: Logger;
add(title: string) {
this.logger.info(`added ${title}`);
}
}The level comes from logger.level (verbose, debug, info (default), warn, error). @celljs/logger replaces the console logger with winston.
Aspects (AOP)
An aspect runs code around the methods of components created with proxy: true (controllers and RPC services are proxied by default):
import { Aspect, MethodBeforeAdvice } from '@celljs/core';
@Aspect({ id: MethodBeforeAdvice, pointcut: 'Audited' })
export class AuditAdvice implements MethodBeforeAdvice {
async before(method: string | number | symbol, args: any[], target: any): Promise<void> {
console.log('call', String(method), args);
}
}
@Component({ proxy: true, sysTags: ['Audited'] })
export class AccountService { /* ... */ }AfterReturningAdvice and AfterThrowsAdvice run after a method returns or throws. Without pointcut an advice applies to every proxied component. A proxied method always returns a Promise, also when it is synchronous. aop.enabled: false turns proxies off.
Outside the container
There is no global container. A component that picks components at run time injects the container (@Autowired(Container)); code that is not a component (a script, a test) uses the container it created: ApplicationFactory.createContainer(props, ...modules) (@celljs/core/lib/common/application/application-factory) returns it, and createTestApp() has app.get(). A { detached: true } property is read from the container that created the instance, so it only works on components.
When wiring fails
The container reports wiring problems as structured errors, and npx cell check finds them without starting the application:
| Code | Meaning | Usual fix |
|---|---|---|
CELL_DI_NO_BINDING | Nothing is registered for an identifier | Import the file from a DI module; add @Component(Id); install the package |
CELL_DI_AMBIGUOUS | Several bindings match a single injection | Inject an array, or use @Named / @Tagged / default: true, or rebind: true |
CELL_DI_CIRCULAR | Components need each other while being created | Get one side lazily (@AutowiredProvider, or @Autowired({ detached: true }) on a property), or move the shared part into another component |
CELL_DI_MISSING_INJECT | A constructor parameter has no identifier | Add @Autowired(Id) |
The message names the identifier, who needs it and the resolution chain. The core reference covers every decorator in detail.