Skip to content

Latest commit

 

History

History
118 lines (86 loc) · 5.47 KB

File metadata and controls

118 lines (86 loc) · 5.47 KB
title Events & Hooks
description System-wide event bus for loose coupling between plugins

Events & Hooks

ObjectStack has two distinct hook systems. They look similar but use different APIs, payloads, ordering, and error semantics — pick the right one for the job:

  • Kernel lifecycle hooksctx.hook(name, handler) / ctx.trigger(name, ...args) on the PluginContext. Used for system bootstrap events and custom plugin-to-plugin events, matched by exact name.
  • Data lifecycle hooks — object-level hooks (beforeInsert, afterUpdate, …) registered via object Hook metadata or engine.registerHook(). They receive a single HookContext and run on record mutations.

Kernel Lifecycle Hooks

Lifecycle Events

Triggered by the Kernel during bootstrap and shutdown:

Event Description
kernel:ready All plugins have successfully started. System is live.
kernel:bootstrapped Fired after every kernel:ready handler has settled, before kernel:listening. The "all bootstrap + seed data is ready" anchor — use it for reconcile/backfill work that consumes data a later-starting plugin produces during kernel:ready.
kernel:listening Fired after every kernel:ready and kernel:bootstrapped handler has completed (e.g. the HTTP server is accepting connections).
kernel:shutdown Shutdown signal received. Plugins should clean up resources.

Listening to Kernel Events

ctx.hook('kernel:ready', async () => {
  ctx.logger.info('System is ready! Sending startup notification...');
});

ctx.hook(name, handler) takes exactly two arguments — there is no options/priority parameter. Kernel hooks run in registration order, and ctx.trigger() awaits each handler sequentially.

Emitting Custom Events

Plugins can trigger their own namespaced events for inter-plugin communication. Use ctx.trigger() (it is async and returns a Promise); handlers receive the positional arguments you pass to trigger():

// Trigger a custom business event (await it — trigger returns a Promise)
await ctx.trigger('order:shipped', { orderId: '123', carrier: 'fedex' });

// Another plugin listens — the handler receives the same positional args
ctx.hook('order:shipped', async ({ orderId, carrier }) => {
  await sendTrackingEmail(orderId, carrier);
});
Kernel hooks are matched by **exact name** — there is no wildcard or namespace-glob support. A handler registered under `'order:*'` will never fire for `'order:shipped'`.

Data Lifecycle Hooks

The Data Engine runs hooks around record reads and mutations. Write operations fire before* and after* events:

Event When
beforeInsert / afterInsert Around record creation.
beforeUpdate / afterUpdate Around record update — single-id and bulk (multi: true).
beforeDelete / afterDelete Around record deletion — single-id and bulk (multi: true).

Reads fire beforeFind / afterFind, for both find and findOne (one read event covers every read shape). There are no per-method (findOne/count/aggregate) or *Many events: read authorization and row filtering are handled by RLS/permission rules, field masking by field-level metadata, and bulk writes by the same before*/after* write events (with the row-scoping predicate in ctx.input.ast).

The HookContext

Every data hook is a single-argument handler (ctx: HookContext) => void | Promise<void>. The context exposes:

Field Description
ctx.object Target object name (immutable).
ctx.event Current lifecycle event, e.g. 'beforeInsert' (immutable).
ctx.input Mutable input. Shapes: insert { data }, update { id, data }, delete { id }. Modify this to change the operation.
ctx.result Operation result, available in after* events (mutable).
ctx.previous Record state before the operation (update/delete).
ctx.session Auth/tenancy info (userId, organizationId, roles, …).
ctx.api Scoped cross-object data access.

Registering Data Hooks

// Enrich a record before it is created
engine.registerHook('beforeInsert', async (ctx) => {
  if (ctx.object === 'order' && ctx.input.data.amount > 10000) {
    ctx.input.data.requires_approval = true;
  }
}, { object: 'order' });

// React after a record is created — the created record is on ctx.result
engine.registerHook('afterInsert', async (ctx) => {
  await notifyWebhook(ctx.object, ctx.result?.id);
}, { object: 'order' });

Error Handling

What happens when a data hook throws is governed by the hook's onError policy ('abort' | 'log', default 'abort'):

  • onError: 'abort' (default) — the error rolls back the transaction (when the hook is blocking), cancelling the operation.
  • onError: 'log' — the error is logged and execution continues; the operation is not cancelled.
engine.registerHook('beforeInsert', async (ctx) => {
  if (ctx.object === 'invoice' && !ctx.input.data.customer_id) {
    throw new Error('Invoice must have a customer'); // Aborts the insert (default onError)
  }
});

Ordering

Data hooks accept a priority option (default 100). They are sorted so that lower priority values run first:

engine.registerHook('beforeInsert', validate,  { priority: 50 });  // runs first
engine.registerHook('beforeInsert', enrich,    { priority: 100 }); // runs after