Run actions and stream values in TypeScript¶
Use a local action when application work has a stable, schema-described
interface and may later move behind a session. Inputs and outputs remain
AsyncNode streams in either location.
Define an action¶
Each port declares its wire representation, cardinality, and requirement. A unary port carries at most one value. A streaming port carries an ordered sequence.
import {
ActionPortSchema,
ActionRegistry,
ActionSchema,
cancelledError,
okStatus,
valueOrThrow,
} from '@curiositystack/a11';
const summarizeSchema = new ActionSchema({
name: 'summarize',
description: 'Summarize text as it arrives.',
inputs: {
documents: new ActionPortSchema({
name: 'documents', type: 'text/plain', required: true,
}),
},
outputs: {
summary: new ActionPortSchema({
name: 'summary', type: 'text/plain', unary: true, required: true,
}),
},
});
Implement the handler¶
Iterate over a streaming input and finalize the output with its last value. Returning a non-OK status ends the action and propagates that status to output readers.
const summarize = async (action) => {
const documents = valueOrThrow(await action.getInput('documents'));
const paragraphs = [];
for await (const document of documents) paragraphs.push(String(document));
if (action.signal.aborted) return cancelledError('Summary cancelled.');
const summary = valueOrThrow(await action.getOutput('summary'));
valueOrThrow(await summary.finalize(paragraphs.join(' ').slice(0, 240)));
return okStatus();
};
const registry = new ActionRegistry();
valueOrThrow(registry.register('summarize', summarizeSchema, summarize));
Run and read the action¶
Create the action from its registry, start the handler, and write the input.
run() starts work and returns immediately. wait() covers handler completion,
output closure, and terminal status publication.
const action = valueOrThrow(registry.makeAction('summarize'));
valueOrThrow(action.run());
const documents = valueOrThrow(await action.getInput('documents'));
valueOrThrow(await documents.put('A stream can publish partial work.'));
valueOrThrow(await documents.finalize('The final write closes the input.'));
const summary = valueOrThrow(await action.getOutput('summary', false));
console.log(valueOrThrow(await summary.next()));
valueOrThrow(await action.wait());
Pass false as the second getOutput argument when reading a received output.
Binding that node to the action stream would send received fragments back to
their producer.
Cancel nested work¶
A handler creates a child with makeNested. The child inherits the registry
and, by default, the parent's node map, session, transport, and framework
headers. Parent cancellation recursively cancels active children, including
children created with propagateIo set to false.
const child = valueOrThrow(action.makeNested('retrieve_context'));
valueOrThrow(child.run());
action.signal.addEventListener('abort', () => closeLocalResource(), {
once: true,
});
Handlers should observe action.signal around browser APIs, provider requests,
and long local loops. Call action.cancel(), then await action.wait() when
resource teardown must finish before the surrounding task continues.
Choose stream operations¶
putappends one value.finalize(value)appends the final value and closes the writer.nextreads the next value;for awaitreads values one-by-one.nextChunkandnextFragmentretain wire metadata and sequence details.abortWithStatusends the stream with a structured failure.closeends a writer that cannot mark a specific value as final.
See the AsyncNode lifecycle for the distinction between finality and closure.