Lifecycle
Assembly is synchronous, but real components need asynchronous startup (open a pool, ping a server) and shutdown. Laminar offers two hooks and two runners.
Declaring hooks
Spread the result of onInit / onDispose into the object a factory returns. No wrapper, no extra argument.
export const prismaService = defineService((config) => {
const pool = new Pool(config.database);
return {
db: new PrismaClient({ adapter: new PrismaPg(pool) }),
...onInit(async () => { await pool.query('SELECT 1'); }),
...onDispose(() => pool.end()),
};
});
Components without hooks are skipped silently.
Running hooks
const services = createServices(config);
await init(services);
await dispose(services);
createServices never runs onInit for you.
Order
flowchart TB
subgraph "init: depth-first, parent before child"
direction LR
a1[db] --> a2[cache] --> a3[mailer]
end
subgraph "dispose: reverse order"
direction LR
b1[mailer] --> b2[cache] --> b3[db]
end
For a flat layer this is definition order for init and its reverse for dispose.
Between layers the caller is responsible for ordering:
await init(services);
await init(modules);
await dispose(modules);
await dispose(services);
How traversal works
Every factory result that is an object gets branded internally with a COMPONENT symbol. init / dispose walk only branded objects, so a Prisma model, a Date or any other arbitrary data inside a component is never traversed.
Behaviour to know
Lifecycle
Assembly is synchronous, but real components need asynchronous startup (open a pool, ping a server) and shutdown. Laminar offers two hooks and two runners.
Declaring hooks
Spread the result of
onInit/onDisposeinto the object a factory returns. No wrapper, no extra argument.export const prismaService = defineService((config) => { const pool = new Pool(config.database); return { db: new PrismaClient({ adapter: new PrismaPg(pool) }), ...onInit(async () => { await pool.query('SELECT 1'); }), ...onDispose(() => pool.end()), }; });Components without hooks are skipped silently.
Running hooks
const services = createServices(config); await init(services); // … application runs … await dispose(services);createServicesnever runsonInitfor you.Order
flowchart TB subgraph "init: depth-first, parent before child" direction LR a1[db] --> a2[cache] --> a3[mailer] end subgraph "dispose: reverse order" direction LR b1[mailer] --> b2[cache] --> b3[db] endFor a flat layer this is definition order for
initand its reverse fordispose.Between layers the caller is responsible for ordering:
await init(services); await init(modules); await dispose(modules); await dispose(services);How traversal works
Every factory result that is an object gets branded internally with a
COMPONENTsymbol.init/disposewalk only branded objects, so a Prisma model, aDateor any other arbitrary data inside a component is never traversed.Behaviour to know