In an asynchronous environment, especially when orchestrating calls across
out-of-band edge tunnels or mesh networks, packets will inevitably be dropped.
Having a built-in watch/timeout primitive isn't just a safety net; it's a
structural requirement.
To keep the JSON-Lisp AST pure and easily parsable, you want to avoid
variable-length arguments when defining control flow.
Here are the two most elegant ways to express this, depending on whether you
prefer absolute minimalism (Erlang style) or self-documenting structures
(NodeJS style).
Option 1: The Fixed-Arity Envelope (Most Reduced)
By forcing the operation's arguments into a single array payload, you lock in
the position of the routing and timeout instructions. The compiler and VM will
always know exactly where to look.
Syntax:
["dispatch", target_name, args_array, on_success, timeout_ms, on_timeout]
Example:
[
"dispatch",
"fetch",
["array", "https://api.mesh/data"],
"handle_ok",
5000,
"handle_timeout"
]
Why it's elegant:
It is extremely cheap to parse. The VM's OP_SYSCALL simply pops 5 items off
the stack. The host thread receives a perfectly structured intent and knows
that if 5000ms pass, it must push a CALL handle_timeout instruction back into
the worker.
Option 2: The Options Record (Most Self-Documenting)
If you want the AST to act as a highly readable universal format, passing a
record (object) for the configuration mimics the standard JavaScript approach.
Syntax:
["dispatch", target_name, args_array, config_record]
Example:
[
"dispatch",
"fetch",
["array", "https://api.mesh/data"],
["record",
"on_success", "handle_ok",
"timeout_ms", 5000,
"on_timeout", "handle_timeout"
]
]
Why it's elegant:
It is entirely future-proof. If you ever need to add retry logic ("retries",
3) or abort signals to the AST, you just add keys to the record without having
to modify the VM's underlying OP_SYSCALL signature.
How the Host Thread Handles It
Regardless of which syntax you choose, the beauty of this CPS
(Continuation-Passing Style) approach is how simple it makes the host thread's
job. When the main thread pulls this intent from the SharedArrayBuffer, it
just wraps the native operation in a standard Promise.race():
/ Main thread pseudo-code
const fetchPromise = nativeFetch(args).then(res => ({ cb: config.on_success,
data: res }));
const timeoutPromise = new Promise(resolve =>
setTimeout(() => resolve({ cb: config.on_timeout, data: "TIMEOUT" }),
config.timeout_ms)
);
const result = await Promise.race([fetchPromise, timeoutPromise]);
sendToWorker(result.cb, result.data);
Option 1 is the most computationally efficient for the VM, while Option 2 is
the most extensible. Which of those two philosophies fits the broader vision
for your system designs?