Handler
Handlers are functions that are called by adapters and orchestrators to implement certain functionality. Handlers can be based on user-defined APIs or be related to internal logic, for example the adapter loop.
Internal handlers
The internal handlers have predefined meaning and names and are often related to some common integration tasks. They have the following purpose:
send: prepare the data for sending, adapting it for the underlying protocol. The input data is assumed to be protocol/API independent as much as possible.receive: transform the received data to be protocol/API independent and remove any data not needed by the rest of the system.encode: JavaScript objects passed to these handlers are converted to Buffer, which is then passed to the network.decode: data frames coming from the network as Buffers are passed to this handler and it converts them to JavaScript objectsexec: this handler is called by default if no handler is defined for the $meta.methodready: this handler is called when the adapter is ready to process calls, which for some adapters means that connection has been established or a TCP port was opened for listening.idleSend: this handler is called when there has been an idle period (longer than the configured) of no outgoing messages. It usually sends some kind of echo/keep alive message available in the protocol.idleReceive: this handler is called when there has been an idle period (longer than the configured) of no incoming messages. It usually disconnects the adapter, as the expectation is that the other side is sending some keep-alive messages.drainSend: this handler is called when the send queue is emptied or has been empty for a pre-configured period. It can be used to trigger processing of some pending operations that happen during the idle time of the adapter.
Internal API handlers
The internal API handlers usually implement some business functionality.
They use namespaces to prefix the names of the API methods. The framework
works best when the naming convention for the methods uses a
semantic triple
in the format subjectObjectPredicate, where:
subjectis the namespace and often is same as the name of the realm if the realm defines only one namespace.objectis often some entity within the realmpredicateis the action being executed on the entity
Here are some examples:
If we have a realm named user that has the goal to implement role-based
access control, we can imagine it has the following namespaces:
identity: for implementing the authenticationpermission: for implementing the authorizationuser: for managing the users and roles. It could have methods for objects nameduserandrole, for example:userUserAdd- for creating usersuserRoleEdit- for editing roles
All handlers are converted to async functions
Library functions
The library functions implement some reusable functionality that is repeated across some of the handlers within the same realm. Any handler, that has a name that does not match the internal handlers or the API namespaces is considered to be a library function and is not exposed anywhere else, except to the sibling handlers.
Folder structure
The handlers and library functions are grouped together and given a name. This happens by defining them in a subfolder within the realm folder. This folder is usually in another one, which is used for defining a layer. The most common approach is to create a separate file for each handler and use the handler name as file name. This serves multiple reasons:
- allow fast finding of handlers within code editors. For example,
in VSCode ctrl+p and then typing the first letters of the semantic
triple will bring the desired handler (i.e.
ctrl+p uuais likely to finduserUserAdd.ts) - easier code review by avoiding files with thousands of rows and a lot of nesting
- better isolation between the handlers
The group name is in the format realmname.foldername.
This name is then used in the imports property in the adapters and orchestrators.
Let's imagine a realm named example which implements a namespace math with
several methods for calculating the sum and the average of an array of integer numbers.
To do so, it defines a library function sum and handlers mathNumberSum and
mathNumberAverage. It attaches the handlers to an orchestrator mathDispatch.
The following structure is used:
📁 example ├──📁 orchestrator | ├──📁 math | | ├── error.ts | | ├── sum.ts | | ├── mathNumberSum.ts | | └── mathNumberAverage.ts | └── mathDispatch.ts └── server.ts
Defining handlers and library functions
To enable interoperability between the handlers, library functions, orchestrators, adapters and the framework, a specific pattern is used to define them.
To define a library function, use the library function from the framework and
pass a function that returns the desired library function with the appropriate name:
// example/orchestrator/math/sum.ts
import {library} from '@feasibleone/blong';
export default library(api =>
function sum(...params: number[]) {
// implementation
}
);
To define a handler, use the handler function from the framework and
pass a function that returns the desired handler with the appropriate name:
// example/orchestrator/math/mathNumberSum.ts
import {handler} from '@feasibleone/blong';
export default handler(api =>
function mathNumberSum(...params: number[]) {
// implementation
}
);
Interoperability
Handlers and functions can call each other by referring through
the api parameter. It also allows to access other functions of
the framework.
The api parameter has the properties, which are often
used through destructuring. Check the following example,
that explain their usage:
-
example/orchestrator/math/error.ts- defines the errors.import {library} from '@feasibleone/blong';export default library(({lib: {error // framework function for defining typed errors}}) => {error({numberInteger: 'Numbers must be integer'})}); -
example/orchestrator/math/sum.ts- defines the reusable library functionsum.import { library } from '@feasibleone/blong';export default library(({errors // access the defined errors}) =>function sum(params: number[]) {if (!params.every(Number.isInteger)) throw errors.numberInteger();return params.reduce((prev, cur) => prev + cur, 0);}); -
example/orchestrator/math/mathNumberSum.ts- defines the handler for calculating the sum.import {handler} from '@feasibleone/blong';export default handler(({lib: {sum // user defined library function}}) =>function mathNumberSum(params) {return sum(params);}); -
example/orchestrator/math/mathNumberAverage.ts- defines the handler for calculating the average.import {handler} from '@feasibleone/blong';export default handler(({config: {precision // access configuration},handler: {mathNumberSum // local or remote handler}}) => async function mathNumberAverage(numbers: number[], $meta){if (!numbers?.length) return;return ((await mathNumberSum(numbers, $meta)) / numbers.length).toPrecision(precision)}) -
example/orchestrator/mathDispatch.ts- defines a dispatch orchestrator.import {orchestrator} from '@feasibleone/blong';export default orchestrator(() => ({extends: 'orchestrator.dispatch',})); -
example/server.ts- defines theexamplerealm and the default configuration for the orchestrator.import {realm} from '@feasibleone/blong';export default realm(() => ({config: {default: {mathDispatch: {namespace: 'number',imports: 'example.number',},},},children: ['./orchestrator'],...rest}));
Folder-Level Configuration (config.ts)
A config.ts file can be placed in any handler folder to define configuration for all handlers in
that folder. The file supports activation-based config (default, dev, prod, etc.), keeping
environment-specific values co-located with the handlers that use them.
example/
└── orchestrator/
└── math/
├── config.ts ← default config for math handlers
├── ~.schema.ts
└── mathNumberAverage.ts
// example/orchestrator/math/config.ts
export default {
default: {
precision: 4,
},
dev: {
precision: 8,
},
};
Handlers in the folder receive this config automatically via their config parameter.
To override values from outside the folder (e.g. for deployment-specific secrets or URLs that
cannot live in source code), use the namespace property in the realm's server.ts:
// example/server.ts — override math handler config
import {realm} from '@feasibleone/blong';
export default realm(() => ({
url: import.meta.url,
config: {
prod: {
namespace: {
math: {
precision: 8, // override with deployment-specific value
},
},
},
},
}));
Priority: Realm namespace override > config.ts active environment activation > config.ts default
Handler-Test Continuum
Handlers and tests share deep structural similarities — both orchestrate sequences of calls, validate results, and produce outputs. The framework embraces this by providing mechanisms that work identically in both contexts.
Checkpoints
The checkpoint function records progress through multi-step
operations. It is available via $meta.checkpoint and should always be called
with optional chaining to ensure zero overhead in production:
export default handler(({handler: {validate, persist}}) =>
async function orderProcess(params, $meta) {
const validated = await validate(params, $meta);
$meta.checkpoint?.('validated', {orderId: validated.id});
const saved = await persist(validated, $meta);
$meta.checkpoint?.('persisted', {orderId: saved.id, version: saved.version});
return saved;
}
);
In test mode, checkpoints are recorded in $meta.checkpoints and can be
asserted on. In debug mode, they emit structured log entries. In production,
the ?. operator ensures they are no-ops. See the
checkpoint concept for details.
Optional Assertions
Handlers destructure assert from lib, following the same pattern as
checkpoint: undefined in production, active in test/debug mode. Both
use optional chaining for zero-cost in production:
export default handler(({lib: {assert}, handler: {accountGet, accountUpdate}}) =>
async function accountDebit({accountId, amount}, $meta) {
const account = await accountGet({accountId}, $meta);
assert?.ok(account.balance >= amount, 'Sufficient funds');
$meta.checkpoint?.('balance-checked', {balance: account.balance});
const result = await accountUpdate(
{accountId, balance: account.balance - amount},
$meta,
);
assert?.equal(result.balance, account.balance - amount, 'Balance updated correctly');
$meta.checkpoint?.('debit-applied', {newBalance: result.balance});
return result;
}
);
In production (checkpointMode: 'production'), both assert and checkpoint
are undefined — all assert?. and checkpoint?. calls are no-ops. In
test/debug mode, assert is node:assert and failures are reported normally.
Graduating Tests to Handlers
A test handler that proves a workflow works can be promoted to a production
handler by moving it from the test layer to the orchestrator layer,
changing assertions from mandatory to optional, and adding checkpoints.
See the unified handler-test rationale
for the full design.