Plugin
An effect-based plugin/protocol system with scoped contexts and an around/before/after/error hook pipeline.
@ozaco/std/pluginA Protocol declares a contract — a context type, setup arguments, and a set of action signatures — tagged by an optional subtype symbol. A Plugin provides the concrete behavior: either standalone (definePlugin(...).build(actions)) or as an implementation of a protocol (Protocol.implement(...).build(actions)). Both are the same underlying shape, produced by the shared createHookable factory.
Plugins are activated with install, which runs setup inside the current effect scope and stores the resulting context so downstream operations can read it. Calling SomeProtocol.actions.* dispatches to whichever implementation is installed; a protocol's exec strategy decides how — the default runs the last-installed impl, while overrides can run the highest-priority one (codec) or fan out to every impl (logger). A direct SomePlugin.actions.* call always targets that plugin's own impl.
Every action flows through a hook pipeline. The Hookable methods — around, before, after, error — layer typed handlers keyed by action name onto a plugin or protocol, letting callers wrap, short-circuit, transform, or observe calls without touching the implementation. getService exposes the underlying action descriptor for advanced interop.
Definition
Declare protocols and build plugins.
defineProtocol
Declare a protocol: a tagged contract that one or more plugins implement.
function defineProtocol<
TContext,
TArgs extends unknown[] = [],
TActions extends {} = {},
TCustomActions extends {} = {},
>(options: {
subtype?: symbol
cloneable?: boolean
name: string
version: string
description?: string
handlers?: TCustomActions
defaultActions?: Partial<TActions>
exec?: Hookable.Exec
}): Protocol<TContext, TArgs, TActions, TCustomActions>The type parameters fix the shared context, the setup argument tuple, the action surface implementations must provide, and any protocol-level handlers (registry plumbing that runs once, not per-impl). Set cloneable: true to allow multiple implementations to coexist, and pass exec to override how actions dispatch across them (default: the last-installed impl wins).
import { defineProtocol } from '@ozaco/std/plugin'
const Cache = defineProtocol<
{ store: Map<string, unknown> },
[options?: { max?: number }],
{ get(key: string): Future<unknown>; set(key: string, value: unknown): Future<void> }
>({ name: 'cache', version: '0.0.1' })definePlugin
Define a standalone plugin, returning a definition you finish with `.build(actions)`.
function definePlugin<TContext, TArgs extends unknown[] = []>(options: {
subtype?: symbol
name: string
version: string
description?: string
setup(...args: TArgs): Operation<TContext>
}): Plugin.Definition<TContext, TArgs>
// Plugin.Definition:
interface Definition<TContext, TArgs> {
context: Context<TContext>
build(): Plugin<TContext, TArgs>
build<TActions>(actions: TActions): Plugin<TContext, TArgs, TActions>
}setup receives the install-time arguments and returns the plugin’s context via an effect operation. .build(actions) attaches the action implementations and yields the frozen Plugin, whose context can be read anywhere the plugin is installed.
import { definePlugin } from '@ozaco/std/plugin'
import { operation } from '@ozaco/std/effect'
const Clock = definePlugin({
name: 'clock',
version: '0.0.1',
*setup() {
return { now: Date.now }
},
}).build({
time: operation(function* () {
return Date.now()
}),
})Lifecycle
Install plugins and reach into the action machinery.
install
Run a plugin’s `setup` and store its context in the current effect scope.
function* install<TContext, TArgs extends unknown[]>(
plugin: Plugin<TContext, TArgs>,
...args: TArgs
): Operation<TContext>Calls plugin.setup(...args), sets the resulting value on the plugin’s context in the active scope, and returns it. After installation the plugin’s actions are dispatchable, and its context is visible to any operation running in that scope chain.
import { install } from '@ozaco/std/plugin'
function* main() {
const ctx = yield* install(Cache, { max: 100 })
// Cache.actions.* are now available in this scope.
}getService
Extract the raw action descriptor behind a bound plugin action handler.
const getService: (
handler: (...args: any[]) => Operation<any>,
) => Operation<Hookable.RawAction>
// Hookable.RawAction:
interface RawAction {
self?: (...args: any[]) => Operation<unknown>
context: Context<unknown>
options: { name: string; version: string; subtype?: symbol }
key: string
meta?: Record<string, any>
}Invokes handler with an internal RAW_ACTION sentinel so it returns its descriptor (the underlying self implementation, owning context, plugin options, action key, and meta) instead of executing. Fails with an unexpected tag when the handler is not part of a plugin.
import { getService } from '@ozaco/std/plugin'
const service = yield* getService(Cache.actions.get)
service.key //=> 'get'createHookable
Low-level factory powering `definePlugin`/`defineProtocol`: builds the context, action proxy, hooks and plugin builder.
function createHookable(options: {
name: string
version: string
handlers?: Record<string, any>
defaultActions?: Record<string, any>
subtype?: symbol
cloneable?: boolean
exec?: Hookable.Exec
}): {
context: Context<unknown>
actions: unknown
hooks: {
useHook(): Operation<...>
around(handlers): Operation<void>
before(handlers): Operation<void>
after(handlers): Operation<void>
error(handlers): Operation<void>
}
buildPlugin(buildOptions, buildActions?): Plugin
}Creates the scope-local Context, hook-store and chain contexts, the action Proxy that resolves each call through the before/around/after/error pipeline, and buildPlugin which wires an implementation’s setup and actions into a frozen Plugin. You rarely call this directly — prefer definePlugin/defineProtocol — but it is exported for building custom protocol machinery.
Guards
Narrow unknown values to plugins and protocols.
isPlugin
Narrow a value to a `Plugin` (tagged with the `PLUGIN` symbol).
function isPlugin(value: unknown): value is PluginisProtocol
Narrow a value to a `Protocol` (tagged with the `PROTOCOL` symbol).
function isProtocol(value: unknown): value is ProtocolTypes
Plugin
A built plugin: a hookable bundle of `setup`, `context`, `actions` and metadata.
interface Plugin<TContext = unknown, TArgs extends unknown[] = unknown[], TActions = unknown>
extends Hookable<TActions & {}> {
_t: typeof PLUGIN
_st?: symbol
name: string
version: string
description: string
setup(...args: TArgs): Operation<TContext>
context: Context<TContext>
actions: TActions
getKeys(): string[]
getMeta(key: string): Record<string, any> | undefined
}The Plugin namespace also exposes:
- •
InferContext<T>— extract the context type from a plugin. - •
Definition<TContext, TArgs>— the intermediate result ofdefinePlugin, withcontextandbuild(actions?).
Protocol
A contract that plugins implement; carries shared `context`, routed `actions`, and `implement`.
interface Protocol<
TContext = unknown,
TArgs extends unknown[] = unknown[],
TActions extends {} = {},
TSelfActions extends {} = {},
> extends Hookable<TActions> {
_t: typeof PROTOCOL
_st?: symbol
name: string
version: string
context: Context<TContext>
actions: TActions & TSelfActions
implement<TIContext extends TContext, TIArgs extends TArgs>(options: {
name: string
version: string
description?: string
setup(...args: TIArgs): Operation<TIContext>
}): Protocol.Implementation<TIContext, TIArgs, TActions>
}The Protocol namespace also exposes:
- •
InferContext<T>— extract the context type from a protocol. - •
Implementation<TContext, TArgs, TActions>— the result of.implement(...), withcontextandbuild(actions).
Hookable
The hook surface shared by plugins and protocols: `around`/`before`/`after`/`error` plus `useHook`.
interface Hookable<TActions> {
useHook(): Operation<Map<string, unknown>>
around(handlers: Hookable.Around<TActions>): Operation<void>
before(handlers: Hookable.Before<TActions>): Operation<void>
after(handlers: Hookable.After<TActions>): Operation<void>
error(handlers: Hookable.OnError<TActions>): Operation<void>
}Handler maps mirror the action tree, so before({ get: (args) => ... }) hooks the get action. The Hookable namespace also exposes the per-kind handler shapes and function types (Around/Before/After/OnError, AroundFn/BeforeFn/AfterFn/ErrorFn, AnyAction) and internal wiring types (RawAction, Exec, Call, HookStore, HookSelfEntry).
- •
around— wrap a call, deciding whether/how to invokenext. - •
before— run before the action, e.g. validation. - •
after— inspect or replace the result. - •
error— observe a thrown/failed call. - •
Exec— the dispatch strategydefineProtocolaccepts viaexec.
import { install } from '@ozaco/std/plugin'
// Layer a hook onto an installed protocol.
yield* Cache.before({
get: function* ([key]) {
yield* Logger.actions.debug('cache get', { key })
},
})