@signalbox/core
v0.5.1
Published
Event-based application framework: typed bus, plugins, workflows, lifecycle and config
Readme
@signalbox/core
Event-based application framework for Node: a typed event bus, plugins, workflows, lifecycle, and config wiring.
Part of signalbox — see the full documentation.
Install
npm install @signalbox/coreUsage
An app is two lists: plugins that produce events and expose APIs, and workflows that react to them. They only ever meet on the bus.
import { createApp, createWorkflowDefiner, type PluginApis } from "@signalbox/core"
import { createPermissionExecution, entityRef } from "@signalbox/permissions"
// Event maps must be `type` aliases (see below).
type MyEvents = { "job:done": { id: string } }
const plugins = {
// each plugin's init return becomes ctx.plugins[name]
}
const defineWorkflow = createWorkflowDefiner<MyEvents, PluginApis<typeof plugins>>()
const permissionExecution = createPermissionExecution()
const permissions = {
runtime: permissionExecution.runtime,
core: permissionExecution.core,
host: permissionExecution.identities.issue({ principal: entityRef("system", "my-app") }),
}
const worker = defineWorkflow("worker", ctx => {
ctx.onStart(() => {
ctx.log("up")
ctx.app.emit("job:done", { id: "1" })
})
// react to app events off the workflow's own channel
ctx.app.flow("job:done").effect(({ id }) => {
ctx.log(`done ${id}`)
})
})
await createApp({ name: "my-app", permissions, plugins, workflows: [worker] }).run()The permission runtime is required explicitly. Plugin and workflow lifecycle callbacks run with host authority. Authenticated command handlers call app.command, which derives the canonical actor from an opaque identity grant. Permission-bound workflow runs intersect caller authority with an app-owned workflow ceiling.
Plugins run first, in declaration order; workflows run second with ctx.plugins, the app channel ctx.app, and lifecycle hooks (onStart, onStop, interval). Everything registered via onStop/interval is torn down in reverse order, so a workflow never outlives a plugin it depends on. run() blocks until SIGINT/SIGTERM, then stops cleanly.
Subscribe to a channel with on/once/off or start a Flow from it with flow(event). Plugin events arrive on ctx.plugins.<name>.events.
Event maps must be
typealiases, notinterfaces — an interface has no implicit index signature and won't satisfy theEventMapconstraint.
Also exports the Flow push-stream primitive (makeFlow, merge), the SignalboxError error type, and the isRoot platform helper.
License
MIT
