Skip to content
Laminar
Language:en Author:AtomForge

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);

// … application runs …

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