/** * What a handler receives, and how it fails. One context shape for every * action of every kind — the fields an action does not have are undefined — * so adding a capability later never changes a handler's signature. */ import type { ApiClient } from "../api/client"; import type { ActionContext } from "../scope/context"; import type { BuildContext, BuildContextOptions } from "./build-context"; import type { EventBlock } from "./event"; import type { Resource } from "./resource"; import type { Secrets } from "./secrets"; export interface ActionErrorOptions { /** Whether the platform may retry the action. Default false. */ retryable?: boolean; /** Anything worth carrying to the action's results. */ details?: Record; } /** * Fails the action with a named code. Any other throw is reported as * EXECUTION_ERROR; this one keeps the code the author chose, so tests, logs * and traces all read the same name. */ export class ActionError extends Error { readonly code: string; readonly retryable: boolean; readonly details?: Record; constructor(code: string, message: string, opts: ActionErrorOptions = {}) { super(message); this.name = "ActionError"; this.code = code; this.retryable = opts.retryable ?? false; this.details = opts.details; } /** A retryable failure: the platform may run the action again. */ static retry( code: string, message: string, details?: Record ): ActionError { return new ActionError(code, message, { retryable: true, details }); } } export interface HandlerSettings { /** * Provider attributes by category, resolved at the action's NRN and, like * `np service workflow build-context`, by the scope's dimensions (the * service's when there is no scope). `dimensions` overrides that choice. */ providers( categories: string[], opts?: { dimensions?: Record } ): Promise>>; /** NRN parameters, secrets included. */ parameters(): Promise>; /** What `np service workflow build-context` prints, memoized per action. */ buildContext(opts?: BuildContextOptions): Promise; } /** What a handler receives: the shared context plus what the action is. */ export interface HandlerContext< A = Record, P = Record > extends Omit, "entity" | "api"> { /** The platform API, typed from its OpenAPI specs: `ctx.api.GET("/scope/{id}", { params: { path: { id } } })`. */ api: ApiClient; /** The resource this action targets, as declared. */ resource: Resource; /** The action key the handler was bound to: `create`, `update`, `delete`, a custom slug, an event name. */ action: string; /** Input for this action: the entity's attributes merged with the action's parameters. */ params: P; /** The service the action belongs to, as the platform sent it. */ service: Record; /** The link, on link actions. */ link?: Record; /** The scope, on scope actions (from the action's parameters or the notification). */ scope?: Record; /** Provider and parameter values by category or name, resolved lazily. */ settings: HandlerSettings; /** The event block: kind, action, entity, package and trace, as data. */ event: EventBlock; /** Random secrets for credentials a handler mints. */ secrets: Secrets; /** Aborted when the worker is asked to stop; long steps should honour it. */ signal: AbortSignal; /** `execute()` only: the document the agent handed over, parsed. */ payload?: unknown; /** `execute()` only: the document as received. */ raw?: string; } /** The context of an `event()` handler: `ctx.event` carries the notification's name, source and context. */ export interface EventContext extends HandlerContext { event: EventBlock & { name: string; source: string; context: Record; }; } /** The context of the `execute()` handler: the document, parsed and raw. */ export interface ExecuteContext extends HandlerContext { payload: unknown; raw: string; } export type HandlerFn< A = Record, P = Record, R = unknown > = (ctx: HandlerContext) => Promise | R; /** One bound action of one resource (internal: what dispatch looks up). */ export interface Handler< A = Record, P = Record, R = unknown > { readonly resource: Resource; readonly kind: string; readonly action: string; readonly fn: HandlerFn; } /** The key a handler answers, for lookups and error messages. */ export const handlerKey = (h: Pick): string => `${h.resource.ref.ref}#${h.action}`;