Build checked asynchronous service registries with Nix
This commit is contained in:
@@ -14,9 +14,10 @@ const output = process.env.out;
|
||||
if (!output) throw Error("Missing Nix output");
|
||||
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
const generatorDigest = digest(await fs.readFile(config.compiler));
|
||||
const runtime = config.portable
|
||||
? JSON.parse(await fs.readFile(path.join(config.sharedRuntime, "runtime.json"), "utf8"))
|
||||
: { files: [] };
|
||||
const runtime =
|
||||
config.portable || config.remote
|
||||
? JSON.parse(await fs.readFile(path.join(config.sharedRuntime, "runtime.json"), "utf8"))
|
||||
: { files: [] };
|
||||
const relative = (p) => compiler.artifactPath(p);
|
||||
const implementations = [];
|
||||
const generated = new Map();
|
||||
@@ -42,12 +43,15 @@ const coreURL = coreFile ? compiler.contentArtifactUrl(coreFile) : null;
|
||||
const coreServer = path.join(config.sharedRuntime, "core.mjs");
|
||||
await fs.mkdir(path.join(output, "share/quixos/generated"), { recursive: true });
|
||||
const portable = config.portable;
|
||||
if (portable && !/^[$A-Z_a-z][$\w]*$/.test(portable.registryExport))
|
||||
throw Error("Registry export must be a JavaScript identifier");
|
||||
for (const settings of [portable, config.remote].filter(Boolean))
|
||||
if (!/^[$A-Z_a-z][$\w]*$/.test(settings.registryExport))
|
||||
throw Error("Registry export must be a JavaScript identifier");
|
||||
const targets = portable ? [...new Set([...portable.targets, "server"])] : [];
|
||||
for (const target of targets) {
|
||||
const builds = targets.map((target) => ({ target, mode: target, settings: portable }));
|
||||
if (config.remote) builds.push({ target: "server", mode: "remote", settings: config.remote });
|
||||
for (const { target, mode, settings } of builds) {
|
||||
if (!["browser", "server"].includes(target)) throw Error(`Unsupported JS build target ${target}`);
|
||||
const work = path.resolve(`work-${target}`);
|
||||
const work = path.resolve(`work-${mode}`);
|
||||
await fs.cp(config.source, work, { recursive: true, dereference: false });
|
||||
const writable = async (directory) => {
|
||||
await fs.chmod(directory, 0o755);
|
||||
@@ -69,13 +73,16 @@ for (const target of targets) {
|
||||
}
|
||||
await fs.symlink(config.core, path.join(modules, "@quixos/camino-replica-core"));
|
||||
await fs.writeFile(path.join(work, "package.json"), JSON.stringify({ type: "module" }));
|
||||
const bindingPath = relative(portable.bindingOutput);
|
||||
const source = compiler.generatePortableTypeScriptBindings(plan.definition.bindingSchema, pkg.revision, { target });
|
||||
const bindingPath = relative(settings.bindingOutput);
|
||||
const source =
|
||||
mode === "remote"
|
||||
? compiler.generateRemoteTypeScriptBindings(plan.definition.bindingSchema, pkg.revision)
|
||||
: compiler.generatePortableTypeScriptBindings(plan.definition.bindingSchema, pkg.revision, { target });
|
||||
await fs.mkdir(path.dirname(path.join(work, bindingPath)), { recursive: true });
|
||||
await fs.writeFile(path.join(work, bindingPath), source);
|
||||
generated.set(target, { file: path.join(work, bindingPath), source });
|
||||
await fs.writeFile(path.join(output, "share/quixos/generated", `${target}.ts`), source);
|
||||
const entry = relative(portable.entry);
|
||||
generated.set(mode, { file: path.join(work, bindingPath), source });
|
||||
await fs.writeFile(path.join(output, "share/quixos/generated", `${mode}.ts`), source);
|
||||
const entry = relative(settings.entry);
|
||||
execFileSync(
|
||||
config.node,
|
||||
[
|
||||
@@ -96,7 +103,8 @@ for (const target of targets) {
|
||||
],
|
||||
{ cwd: work, stdio: "inherit" },
|
||||
);
|
||||
const bundlePath = `share/quixos/portable/${target}/module.mjs`;
|
||||
const bundlePath =
|
||||
mode === "remote" ? "share/quixos/remote/server/module.mjs" : `share/quixos/portable/${target}/module.mjs`;
|
||||
await fs.mkdir(path.dirname(path.join(output, bundlePath)), { recursive: true });
|
||||
const coreImport = target === "browser" ? coreURL : coreServer;
|
||||
const metadata = path.join(work, "bundle-meta.json");
|
||||
@@ -117,7 +125,7 @@ for (const target of targets) {
|
||||
);
|
||||
const meta = JSON.parse(await fs.readFile(metadata, "utf8"));
|
||||
const built = Object.values(meta.outputs).find((o) => o.entryPoint);
|
||||
if (!built?.exports.includes(portable.registryExport)) throw Error("Bundle lacks the declared registry export");
|
||||
if (!built?.exports.includes(settings.registryExport)) throw Error("Bundle lacks the declared registry export");
|
||||
// Portable modules must contain their ordinary source dependencies and share only the checked SDK.
|
||||
if (
|
||||
Object.values(meta.outputs).some((o) =>
|
||||
@@ -131,15 +139,19 @@ for (const target of targets) {
|
||||
)
|
||||
throw Error("Unexpected external import in portable module");
|
||||
const exports = pkg.exports
|
||||
.filter((e) => e.execution.kind !== "remote" && e.execution.targets.includes(target))
|
||||
.filter((e) =>
|
||||
mode === "remote"
|
||||
? e.execution.kind === "remote"
|
||||
: e.execution.kind !== "remote" && e.execution.targets.includes(target),
|
||||
)
|
||||
.map((e) => e.id);
|
||||
const body = {
|
||||
id: `${target}-module`,
|
||||
id: `${mode}-module`,
|
||||
target,
|
||||
compatibility: "quixos-js-module-v1",
|
||||
path: bundlePath,
|
||||
ports: "quixos-transaction-ports-v1",
|
||||
registryExport: portable.registryExport,
|
||||
registryExport: settings.registryExport,
|
||||
exports,
|
||||
files: [await recordFile(bundlePath, "text/javascript"), ...sharedFiles],
|
||||
};
|
||||
@@ -153,13 +165,32 @@ if (config.service.entry) {
|
||||
throw Error("Custom service entry must be JavaScript; use a separate typed/language build before packaging");
|
||||
serviceEntry = path.join(config.source, relative(config.service.entry));
|
||||
} else {
|
||||
if (!portable) throw Error("Generated service requires a portable registry");
|
||||
if (pkg.exports.some((e) => e.execution.kind === "remote" || !e.execution.targets.includes("server")))
|
||||
const modules = implementations.filter((implementation) => implementation.target === "server");
|
||||
if (pkg.exports.some((entry) => !modules.some((module) => module.exports.includes(entry.id))))
|
||||
throw Error("Generated service does not cover every package export; provide a custom service");
|
||||
if (!modules.length) throw Error("Generated service requires a checked registry");
|
||||
serviceEntry = path.resolve("service-entry.mjs");
|
||||
const imports = modules
|
||||
.map(
|
||||
(module, index) =>
|
||||
`import { ${module.registryExport} as registry${index} } from ${JSON.stringify(path.join(output, module.path))};`,
|
||||
)
|
||||
.join("\n");
|
||||
await fs.writeFile(
|
||||
serviceEntry,
|
||||
`import { ${portable.registryExport} as registry } from ${JSON.stringify(path.join(output, "share/quixos/portable/server/module.mjs"))};\nimport {servePortableRegistry} from ${JSON.stringify(config.serviceAdapter)};\nservePortableRegistry(registry);\n`,
|
||||
imports +
|
||||
`\nimport {servePortableRegistry} from ${JSON.stringify(config.serviceAdapter)};
|
||||
const registries = [${modules.map((_, index) => "registry" + index).join(",")}];
|
||||
const owners = new Map();
|
||||
for (const registry of registries) for (const id of registry.exports) {
|
||||
if (owners.has(id)) throw Error("Duplicate registry export " + id);
|
||||
owners.set(id, registry);
|
||||
}
|
||||
servePortableRegistry({exports: [...owners.keys()], invoke(request, channel, context) {
|
||||
const registry = owners.get(request.exportId);
|
||||
if (!registry) throw Error("Unknown export");
|
||||
return registry.invoke(request, channel, context);
|
||||
}});\n`,
|
||||
);
|
||||
}
|
||||
const serviceModule = "share/quixos/service/main.mjs";
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
typescript ? core.dependencies,
|
||||
packageRevisionId,
|
||||
portable ? null,
|
||||
remote ? null,
|
||||
service ? { },
|
||||
nodeModules ? null,
|
||||
}:
|
||||
@@ -40,6 +41,15 @@ let
|
||||
"server"
|
||||
];
|
||||
};
|
||||
remote =
|
||||
if remote == null then
|
||||
null
|
||||
else
|
||||
{
|
||||
inherit (remote) entry;
|
||||
registryExport = remote.registryExport or "registry";
|
||||
bindingOutput = remote.bindingOutput or "src/gen/remote.ts";
|
||||
};
|
||||
node = "${pkgs.nodejs_24}/bin/node";
|
||||
tsc = "${typescript}/node_modules/typescript/bin/tsc";
|
||||
esbuild = "${pkgs.esbuild}/bin/esbuild";
|
||||
|
||||
Reference in New Issue
Block a user