
Security News
Happy Birthday, Shai-Hulud
It has been one year since Shai-Hulud made its first appearance on npm.
@toa.io/bridges.node
Advanced tools
A component may be written as an ES module or a CommonJS one. A component that is a module states so in a
package.jsonbeside its manifest, or names its files.mjs.
A module may be written in JavaScript or in TypeScript. Node reads a
.tsas it is, so a component runs from what it was written as.
Operation's algorithms are defined as modules under the operations directory in the component
root. An algorithm module must export one function, which is an Algorithm Function, Class or
Factory. Module file name without extension is an operation name (endpoint).
// operations/create.js
export function transition(input, object, context) {
// ...
return { foo: 'bar' }
}
Exported function's name defines operation type property, thus must be one of:
transition, observation, or assignment. Second (state) argument name must be object,
objects, or changeset as it defines operation's scope.
Following function signature defines operation of observation type with objects scope.
// operations/set.js
export function observation(input, objects) {
// ...
}
See Operation properties.
// operations/transit.js
export class Transition {
#context
async mount(context) {
this.#context = context
}
execute(input, object) {
// ...
return { foo: 'bar' }
}
}
Exported class name must be one of: Transition, Observation, or Assignment, as it defines
operation's type. Class must implement Algorithm interface.
Second (state) argument name of the execute method must be object, objects, or changeset as
it defines operation's scope.
export class ObjectTransitionFactory {
async create() {
// ...
}
}
Exported class name must follow the pattern: {Subject}{Type}Factory, where Subject and Type
defines operation's scope and type respectively. Class must
implement Algorithm Factory interface.
Factory class name examples:
ObjectTransitionFactory,ObjectsObservationFactory,ChangesetAssignmentFactory.
Algorithm definition should store reference to the
contextobject without copying its value type variables as they may change over operation lifetime.
Modules in the rc directory of the component root run once per component lifetime, outside any
operation. A module must export at least one of the commands below, and may export several.
// rc/providers.js
export async function preflight(context) {
context.state.providers = await connect(context)
}
export async function dispose(context) {
await release(context.state.providers)
}
| Phase | When |
|---|---|
preflight | on connection, before operations are served |
settle | on connection, once the component can call its own operations (context.local) |
ready | on connection, once the component serves every operation and its receivers are consuming |
dispose | on disconnection, after the component has stopped serving |
pause | when the process is told to go quiet, while the component is whole and still serving |
resume | when it is working again, having gone quiet and not been taken down |
ready is where a process hands out its name. A caller given context.instance calls the
process's stateful operations by it, and a call made before ready —
from settle, or by a component settle reported the name to — may be refused with Addressee.
// rc/register.js
export async function ready(context) {
await context.remote.agents.registry.register({ input: context.instance })
}
settle and ready have no counterpart: nothing runs for them on disconnection.
pause and resume are not a lifecycle moment. The component is not taken down and nothing it
holds is closed: the process has been told to stop doing anything of its own accord, and this is
where a component does the same with whatever the runtime cannot see — an interval, a watcher, a
subscription to something outside.
// rc/poller.js
export function preflight(context) {
context.state.poller = setInterval(() => poll(context), 1000)
}
export function pause(context) {
clearInterval(context.state.poller)
}
export const resume = preflight
context.state is a component's own, kept between phases and calls, and a component has it by
declaring state: ~.
What pause released, something has to take again, and there are two places that happens.
Where the process goes on to be taken down and built again, the component that comes back is a new
one and preflight runs on it. Where the halt is called off — the
deployment did not go quiet, so nothing was closed — it is the same component, with the same
context.state and what it opened in preflight still open, and resume is the only thing that
runs.
So resume is not the counterpart of pause — preflight usually is — but the second case is one
nothing else covers, and a component that has a pause and no resume comes back from it without
what it paused. That is refused: a component whose run commands export pause and no resume
does not start, and says so.
A component that keeps its own time and does not release it goes on calling while the process is quiet, and that is what calls the halt off: what a halt is decided by is whether anything was called. Once a process is taken down such a call is refused, and a rejection nobody catches ends the process.
dispose is the counterpart of preflight: what a component opened there is released here. It runs
before the context it is given is disconnected, so a component can still reach its remotes while
releasing — but nothing calls it into the component any more, so it must not expect its own
operations to answer.
Anything a component leaves open holds the process: a toa compose exits when the last handle is
released, and a feature suite that boots a composition in its own process does the same. Background
work a component starts and does not await — a stream it drives, a client it keeps — belongs in
dispose.
A manifest says what an operation is — its type, its scope — and the bridge supplies what the
manifest leaves out by reading the module's source: the exported name is the type, the second
parameter's name is the scope, a class named after a type says the same through its execute,
and a …Factory class says both in its name. Nothing is imported to read it, so a component is
read where its dependencies are not installed — by toa deploy, by toa types — and a booting
process reads it the same way.
What is read is what is written. A name is read from export function, export class,
export const, export { meter as computation }, export default function transition, and
from module.exports = { … }, exports.name = … in a CommonJS module. A value that is computed
rather than written says its type by its name and nothing more:
// operations/create.js
export const transition = withRetries(async (input, object) => {
// ...
})
The parameters of what withRetries returns are not in this file, so the manifest declares the
scope:
# manifest.toa.yaml
operations:
create:
scope: object
A computation and an unmanaged have no scope to declare, and an effect defaults to none.
An operation, an event, a receiver, a guard or a run command may be a .ts. Node erases the types
and compiles nothing else, so there is no build step, no output directory and no loader — the file
beside the manifest is the file that runs.
// operations/create.ts
import type { Context, CreateInput } from '../types/index.d.ts'
export function transition(input: CreateInput, object: Entity, context: Context) {
// ...
return { foo: 'bar' }
}
The name a module exports still says what it is, and the second parameter still says the scope; both are read through the annotations. What Node refuses to erase — an enum, a namespace, a parameter property — is refused when the module is loaded to run, not when it is read.
Node rewrites no specifier. What is imported is what is on disk:
import { credentials } from './lib/credentials.js'
import { credentials } from './lib/credentials.ts'
import typeNode keeps an import that is not marked as one, and then resolves it at runtime — so
import { type Context } leaves an import behind and fails on a module that only ever had types.
import type is erased whole, which is also what keeps a component's own types off its runtime
graph: nothing under @toa.io/* is loaded by a component.
import { type Context } from '../types/index.d.ts'
import type { Context } from '../types/index.d.ts'
toa types writes those types: types/toa.d.ts from the manifest, rewritten every run, and
types/index.d.ts, written once and left alone, which is where what no manifest states belongs.
An enum, a namespace and a parameter property are not annotations — they emit code, and Node
refuses them:
operations/create.ts: TypeScript enum is not supported in strip-only mode. Types are erased,
never compiled, so a component is written in erasable syntax only — no enum, no namespace, no
parameter property.
A component that is typechecked can be told to refuse them first, which is what these three options are for — one per rule above:
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "esnext",
"types": ["node"],
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true
},
"include": ["operations", "events", "receivers", "guards", "rc", "types"]
}
A directory the bridge reads is a namespace, and the file name is the endpoint. Two files that resolve to one name are refused rather than ranked:
Component at '/app/components/orders' has more than one operations/create: create.js and create.ts
A file in operations is an operation, so shared code lives somewhere the bridge does not read —
a lib beside them, not among them.
migrations is not one of those directories: it holds what a component's storage is to make of
its structure, read by the runtime rather than by a bridge, and written as YAML or JSON. See
Migrations.
Node does not erase types under node_modules. A component an application writes is read from
where the application put it, and may be TypeScript; a component a package ships is read from
inside the installed package, and is transpiled before it is published.
FAQs
Toa Node Bridge (inproc)
The npm package @toa.io/bridges.node receives a total of 1,059 weekly downloads. As such, @toa.io/bridges.node popularity was classified as popular.
We found that @toa.io/bridges.node demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.