~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs

0
0
main
94 lines · 4.1 KB Raw History
/* @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;
}