Dependency injection
Service providers
A provider is how a capability enters the app. It registers bindings, then boots them once everything is registered. Every adapter and extension is, underneath, a provider, and your own modules attach through the same seam.
#Register, then boot
The two phases must stay separate. register() only declares bindings and must not resolve anything, because other providers may not have registered yet. Once all have, the kernel calls each boot(), where resolving is safe.
import { Provider } from '@stone-js/core'
@Provider()
export class DataProvider {
constructor ({ container, blueprint }) {
this.container = container
this.blueprint = blueprint
}
// Phase 1: declare bindings. Do not resolve here.
register () {
this.container.singleton('db', () => new Db(this.blueprint.get('stone.db')))
this.container.alias('db', ['database'])
}
// Phase 2: everything is registered; resolving is now safe.
async boot () {
await this.container.make('db').connect()
}
// Optional: skip this provider in contexts where it does not apply.
mustSkip () {
return this.blueprint.get('stone.db') === undefined
}
}import { defineServiceProvider } from '@stone-js/core'
const DataProvider = ({ container, blueprint }) => ({
register () {
container.singleton('db', () => new Db(blueprint.get('stone.db')))
container.alias('db', ['database'])
},
async boot () {
await container.make('db').connect()
},
mustSkip () {
return blueprint.get('stone.db') === undefined
}
})
export const providers = [defineServiceProvider(DataProvider, {}, true)]#The provider contract
| Member | Type | Description |
|---|---|---|
| register() | () => void | Declare bindings only. Never resolve here. |
| boot() | () => Promiseable<void> | Run once all providers have registered; resolving is safe. |
| mustSkip() | () => Promiseable<boolean> | Return true to skip this provider in a context it does not apply to. |
#Providers forbid the function form
A provider always needs the container to register into it, so it must be a class or a factory, never a plain function. This is the one place the three forms are restricted, and the restriction is just the definition of what a provider is.
#State that must outlive the event
The container is an execution context: it is created for one event and thrown away with it, and every provider registers again with the next one. That is deliberate, it is what makes an event's work isolated from the next event's, and on a function-as-a-service platform it is simply the truth: a cold start restarts everything.
The answer is not to hold state somewhere the framework owns. There is no such place, on purpose, and one would be wrong anyway: on serverless it would be per warm container, so a count would be wrong, unshared and reset unpredictably, while looking correct in development.
The principle
The framework keeps nothing between events. What must outlive one belongs to a store, because a store is the persistence boundary, and choosing it is choosing where the state is kept.
In Stone.js
A driver holds its own backing: a database, Redis, or the in-memory store, which is the reference implementation of that boundary and honest about being one process wide. Providers, managers and everything else are rebuilt per event.
// The store is chosen, and the application knows what it chose.
blueprint.set('stone.cache', {
default: 'shared',
stores: [{ name: 'shared', driver: 'redis', url: process.env.REDIS_URL }]
})Your own store plugs into the same seam, and it holds whatever it needs to hold, in the place your deployment can actually share:
blueprint.set('stone.rateLimit', {
default: 'table',
limiters: [{ name: 'table', factory: () => ({
hit: async (key, limit, windowMs) => await countInMyTable(key, limit, windowMs)
}) }]
})