Compose modules
A composition runs modules as one program: instances of modules (yours, or anyone's by <handle>/<name>),
routes from one instance's hook to a module that implements it, policies that may veto a call, and
parameters. You write it in TypeScript; the composition.json it writes is what hosts load. Commit both.
In the module repo from Write a module (or a new one):
atlibs new my-module > /dev/null && cd my-module && cargo wasm 2>&1 | tail -1
npm init -y > /dev/null && npm pkg set type=module && npm i @publicdomainrelay/atlibs@0.5.0 > /dev/null
atlibs add bobvmbuilder.bsky.social/compose 2>&1 | tail -1
wrote ./atlibs.modules.js
atlibs add vendored compose and wrote atlibs.modules.js: compose (pinned) and myModule
(modules/my-module). Save as compose.ts:
cat > compose.ts <<'EOF'
import { composition, policy } from "@publicdomainrelay/atlibs/compose/build.js";
import { compose, myModule } from "./atlibs.modules.js";
const c = composition({ name: "my-composition", description: "me describes an account; ui asks lookup to double n, if the even policy allows." });
const did = c.param("did", { label: "DID", values: { default: "did:plc:ocnuqjlzgohypj3zoznermms" } });
c.instance("me", myModule, { input: { did } });
const ui = c.instance("ui", compose);
const lookup = c.instance("lookup", compose);
const even = c.instance("even", compose);
c.route(ui, "com.example.compose.lookup", lookup, "ui needs n doubled", { policies: [policy(even, "even")] });
process.stdout.write(await c.json());
EOF
node compose.ts > my-composition.json && atlibs compose check my-composition.json | jq -c '[.instances[].name]'
["me","ui","lookup","even"]
A route to a module that does not implement the hook, or an input outside the module's config, is a
type error in your editor, and node compose.ts names the line.
Run it
atlibs run <composition> <instance> <entry> [input]:
atlibs run my-composition.json me describe | jq -c .result
atlibs run my-composition.json me describe --params '{"inputs":{"did":"did:plc:z72i7hdynmk6r22z27h6tvur"}}' | jq -c .result
atlibs run my-composition.json ui caller '{"nsid":"com.example.compose.lookup","input":{"n":4}}' | jq -c .result.q.output.doubled
atlibs run my-composition.json ui caller '{"nsid":"com.example.compose.lookup","input":{"n":3}}' | jq -c .result.q
{"collections":…,"handle":"bobvmbuilder.bsky.social"}
{"collections":…,"handle":"bsky.app"}
8
{"error":"policy even denied: n 3 is not even","ok":false}
Publish, add, run
After publishing my-module (the record names each module by at:// URI: a key becomes yours,
<handle>/<name>[@pin] its publisher's, with the full pin):
git add compose.ts my-composition.json && git commit -m my-composition
atlibs publish composition my-composition.json --as you.bsky.social
cd "$(mktemp -d)"
atlibs add you.bsky.social/my-composition
atlibs run you.bsky.social/my-composition me describe
The rkey is the file name (--rkey to choose). Remove it: atlibs publish delete com.fedfork.exec.composition/my-composition.
In a page
atlibs stage --target spa writes the composition and every module it names:
atlibs stage --target spa --out site my-composition.json > /dev/null 2>&1
find site -type f | LC_ALL=C sort
site/composition.json
site/modules/compose.json
site/modules/compose.wasm
site/modules/my-module.json
site/modules/my-module.wasm
site/stage.json
Save as site/index.html:
<!DOCTYPE html>
<meta charset="utf-8">
<script type="importmap">
{ "imports": { "atlibs/": "https://cdn.jsdelivr.net/npm/@publicdomainrelay/atlibs@0.5.0/src/" } }
</script>
<output></output>
<script type="module">
import { bindParameters, loadComposition, runCompose, staged } from 'atlibs/compose/index.js'
import { createPluginContext } from 'atlibs/plugin-context/index.js'
import { hostCall } from 'atlibs/run-module/host-call.js'
const ctx = createPluginContext({ id: 'demo' })
const manifest = bindParameters(await (await fetch('composition.json')).json())
const composition = await loadComposition(ctx, manifest, { load: staged(location.href) })
const input = { nsid: 'com.example.compose.lookup', input: { n: 4 } }
const ran = await runCompose(ctx, composition, 'ui', 'caller', input, { callFor: () => hostCall({ ctx }) })
document.querySelector('output').textContent = JSON.stringify(ran.result.q.output.doubled)
</script>
8
Serve site/ (python3 -m http.server --directory site).
An instance whose input has a $param: bindParameters(manifest, { inputs }) before runCompose.
More: handlers, callbacks, hooks, exports, bindings: Compositions.
Previous: Publish