@raisindb/function-wasm
v0.5.8
Published
Write a RaisinDB server function as a WebAssembly component in TypeScript/JavaScript
Readme
@raisindb/function-wasm
Write a RaisinDB server function as a WebAssembly component in TypeScript/JavaScript, built with jco / ComponentizeJS.
The design goal is that a QuickJS function componentizes with zero source
changes. This SDK therefore re-implements nothing: src/generated/api_wrapper.js
is a byte-identical copy of the QuickJS runtime's own wrapper (regenerated by
make gen-bindings, guarded by a freshness test), so the frozen per-method
error conventions — throw vs null vs [] vs sentinel — are literally the same
code running in both runtimes. All this SDK adds is the three globals that
wrapper reaches for, mapped onto the WIT host.
npm i -D @raisindb/function-wasm @bytecodealliance/jco @bytecodealliance/componentize-jsWrite a handler
// src/index.js
export async function handler(input) {
const people = raisin.nodes.getChildren('content', '/people');
console.log('greeting', input.name);
return { greeting: `Hello, ${input.name}!`, people: people.length };
}One module can carry many handlers, and one built artifact can back many
raisin:Function nodes:
export async function handler(input) { /* entry_file: main.wasm */ }
export async function onOrder(input) { /* entry_file: main.wasm:on-order */ }Build
npx raisin-wasm-build src/index.js --out main.wasmwhich wraps:
jco componentize <generated entry> --wit <sdk>/wit -n function --bundle --disable http fetch-event -o main.wasmYour module is never componentized directly. The WIT world exports exactly one
function — handler: func(name: string, input: string) -> result<string, string>
— so the build writes a small entry that imports your module, wraps it with
createHandler, and imports @raisindb/function-wasm/host-wit (the only module
that imports the unresolvable-in-Node specifier raisin:function/host).
name is the handler chosen by the Function node's entry_file suffix:
main.wasm -> "default" -> your handler export; main.wasm:on-order ->
"on-order" -> your onOrder (or 'on-order') export. An unknown name answers
Err listing what the component registered — the host never validates the name,
because the guest owns its handler namespace.
Expect an 8-12 MB artifact: a component embeds the whole StarlingMonkey JS
engine. The server's default cap is 32 MiB, and one artifact can back N
functions (entry_file: ../shared/main.wasm:on-order), which is how a package
with twenty handlers still ships ten megabytes.
Test — natively, no wasm
import { expect, it } from 'vitest';
import { createMockHost } from '@raisindb/function-wasm/testing';
import { createHandler } from '@raisindb/function-wasm';
import * as fn from '../src/index.js';
it('greets', async () => {
const mock = createMockHost({ context: { tenant_id: 't1' } });
mock.expect('nodes_getChildren', () => [{ path: '/people/ada' }]);
mock.install();
const out = await createHandler(fn)('default', JSON.stringify({ name: 'Ada' }));
expect(JSON.parse(out).greeting).toBe('Hello, Ada!');
expect(mock.logs[0].message).toBe('greeting Ada');
});vitest run in this package exercises the SDK the same way.
What is NOT available
The runtime is StarlingMonkey with http and fetch-event disabled, driven by
a synchronous host — not Node, and not the QuickJS runtime's full environment.
| Missing | Use instead |
|---|---|
| fetch, Request, Response, Headers | raisin.http.request(...) — the only egress the host polices |
| setTimeout / setInterval / any timer-based wait | nothing: there is no scheduler after the export returns |
| Resource.resize / toImage / getPageCount, and getBinary on a processed resource | raisin.assets.*, or keep that function in JavaScript. These need the server's per-execution temp files; calling one throws by name rather than undefined is not a function |
| require, node:* builtins, the filesystem | there are zero preopens; the component links no wasi:sockets and no wasi:http |
| dynamic import() of a module not present at build time | everything is snapshotted at componentize time |
| host imports that are asynchronous | every WIT host import is synchronous (Component Model async is not enabled) |
async/await in your handler does work: ComponentizeJS runs the event
loop to resolution around the export call. What does not work is code that
waits on something no one will ever resolve — a timer, a socket, an unresolved
promise: the component simply returns.
console.* is forwarded to the host log (ExecutionResult.logs and the SSE log
stream). Date.now() and Math.random() work (clocks and random stay
enabled).
Layout
| path | what |
|---|---|
| src/index.js | public entry — createHandler, installShim, setHost |
| src/entry.js | the name router behind the single WIT export |
| src/shim.js | __raisin_call, __raisin_internal, console, raisin.context |
| src/host.js | the host holder — what makes native tests possible |
| src/host-wit.js | the only module importing raisin:function/host |
| src/testing.js | the mock host |
| src/generated/* | generated — make gen-bindings, never hand-edited |
| wit/raisin-function.wit | generated copy of the contract |
| bin/raisin-wasm-build.js | the jco wrapper |
--bundle is not optional here: it makes the toolchain resolve the SDK through
Node's own algorithm (symlinked / file: installs included) instead of the
guest's flat virtual filesystem.
Verified against jco 1.32.1 / componentize-js 0.22.0, by building
examples/wasm-functions/wasm/demo/greet-ts (12.09 MiB, jco wit shows
import raisin:function/[email protected] + export handler and no wasi:sockets /
wasi:http):
- the CLI flags above;
- the error convention — an export returning
result<_, string>returns the ok string and throws for the err arm, where the payload ise.payloadif present, elsee.message; - the host import specifier carries the WIT package version
(
'raisin:function/[email protected]'); without it componentization fails at Wizer time withError loading module.
