1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/* @ts-self-types="./build.d.ts" */
// Write a composition in code: instances of module bindings (`atlibs compose bindings` writes them beside
// atlibs.lock.json), routes by contract, parameters as placeholders. build() runs the hosts' load checks
// (checkComposition, no wasm) and points a failure at the line that made the instance or route.
// No dependencies; browser, Node and Workers.
import { checkComposition } from "./index.js";
const CALLER = /\((.*:\d+:\d+)\)$|at (.*:\d+:\d+)$/;
const site = () => {
const line = new Error().stack?.split("\n")[3] ?? "";
const m = CALLER.exec(line.trim());
return m ? (m[1] ?? m[2]) : undefined;
};
const drop = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
const list = (xs) => (xs.length ? xs : undefined);
/** A value only the host gives: { $host: name }. */
export const host = (name) => ({ $host: name });
/** A policy run before a call: an instance and one of its entries (default evaluate). */
export const policy = (inst, entry, args) => drop({ instance: inst.name, entry, args });
/** A composition; `json()` is the file hosts load. */
export function composition({ name, description, limits }) {
const parameters = [];
const instances = [];
const handlers = [];
const edges = [];
const exports = [];
const sites = new Map(); // "instance <name>" / "edge <from> <nsid>" -> file:line:col
const bindings = new Map(); // module key -> binding
const c = {
/** A parameter; returns its placeholder for an instance's input. */
param(key, { label, type = "string", required, description, secret, minimum, maximum, values } = {}) {
parameters.push(drop({ key, label, type, required, description, secret, minimum, maximum, values }));
return { $param: key };
},
/** An instance of a module binding, pinned as vendored (pin: false: unpinned, so it gets hook defaults). */
instance(name, mod, { pin = mod.pin, session, input, calls, policies, budget } = {}) {
for (const m of [mod, ...Object.values(mod.lock ?? {})]) bindings.set(m.key, m);
instances.push(drop({ name, module: mod.key, pin: pin || undefined, session, input, calls, policies, budget }));
sites.set(`instance ${name}`, site());
return { name, module: mod };
},
/** A handler the composition's caller supplies. */
handler(name, description) {
handlers.push(drop({ name, description }));
return { handler: name };
},
/** `from` calling `nsid` goes to `to` (at the entry its module implements) or to a handler; reason: the hook's. */
route(from, nsid, to, reason = from.module.hooks?.find((h) => h.nsid === nsid)?.reason, { policies } = {}) {
edges.push(drop({ from: from.name, nsid, ...(to.handler !== undefined ? { handler: to.handler } : { to: to.name }), reason, policies }));
sites.set(`edge ${from.name} ${nsid}`, site());
},
/** The entry serving contract `nsid` (default: the one the instance's module implements). */
export(nsid, inst, entry = inst.module.implements?.find((i) => i.nsid === nsid)?.entry) {
exports.push({ nsid, instance: inst.name, entry });
},
/** The manifest, after the hosts' load checks; throws "composition: ... at <file:line>". */
async build() {
const manifest = drop({
$type: "com.fedfork.exec.composition",
name,
description,
parameters: list(parameters),
instances,
handlers: list(handlers),
edges: list(edges),
exports: list(exports),
limits,
});
try {
await checkComposition(manifest, (key) => {
const b = bindings.get(key);
if (!b) throw new Error(`composition: module ${key} has no binding (atlibs add it)`);
return b;
});
} catch (error) {
const msg = error.message;
const edge = /edge (\S+) (\S+?):/.exec(msg)?.slice(1).join(" ");
const where = (edge && sites.get(`edge ${edge}`)) ?? sites.get(`instance ${/instance (\S+?):? /.exec(msg)?.[1]}`);
throw new Error(where ? `${msg}\n at ${where}` : msg, { cause: error });
}
return manifest;
},
/** build(), as the file a host loads. */
async json() {
return `${JSON.stringify(await c.build(), null, "\t")}\n`;
},
};
return c;
}