Build checked asynchronous service registries with Nix

This commit is contained in:
Timothy J. Aveni
2026-09-20 21:06:45 -07:00
parent c4ae58831d
commit 1dadc642d1
5 changed files with 50 additions and 4 deletions
+19 -1
View File
@@ -1,5 +1,5 @@
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { generatePortableTypeScriptBindings } from "../bindings/portable.js"; import { generatePortableTypeScriptBindings, generateRemoteTypeScriptBindings } from "../bindings/portable.js";
import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js"; import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js";
import { type ArtifactReader, type PackageArtifacts } from "./artifacts.js"; import { type ArtifactReader, type PackageArtifacts } from "./artifacts.js";
import { loadArtifactWorld, type LoadedArtifactWorld } from "./files.js"; import { loadArtifactWorld, type LoadedArtifactWorld } from "./files.js";
@@ -53,6 +53,24 @@ export function expectedArtifactReceipt(
sources.set(path, bytes); sources.set(path, bytes);
return { target, path, digest: hashBytes(bytes), bytes: bytes.length }; return { target, path, digest: hashBytes(bytes), bytes: bytes.length };
}); });
// Custom language services need no TypeScript adapter. A checked JS module
// exporting remote handlers does, and must carry the exact generated source.
if (
manifest.implementations.some(
(implementation) =>
implementation.compatibility === "quixos-js-module-v1" &&
implementation.exports.some((id) =>
pkg.exports.some((entry) => entry.id === id && entry.execution.kind === "remote"),
),
)
) {
const path = "share/quixos/generated/remote.ts";
const bytes = new TextEncoder().encode(
generateRemoteTypeScriptBindings(plan.definition.bindingSchema, pkg.revision),
);
sources.set(path, bytes);
generated.push({ target: "server", path, digest: hashBytes(bytes), bytes: bytes.length });
}
return { return {
receipt: { receipt: {
schemaVersion: 1, schemaVersion: 1,
+25
View File
@@ -114,3 +114,28 @@ test("checked receipts bind regenerated code, generator identity and manifests",
/replaced or omitted/, /replaced or omitted/,
); );
}); });
test("remote JS modules witness generated adapters without imposing TypeScript on custom services", async () => {
const { asyncObjectPlan } = await import("./async-fixture.js");
const { expectedArtifactReceipt, checkArtifactReceipt } = await import("../src/execution-world/receipts.js");
const plan = await asyncObjectPlan(),
fixture = buildArtifactFixture(plan, "async:provider-runtime@2"),
generator = "a".repeat(64);
fixture.manifest.implementations = fixture.manifest.implementations.filter(
(implementation) => implementation.target === "server",
);
const { receipt, sources } = expectedArtifactReceipt(plan, fixture.manifest, generator);
assert.deepEqual(
receipt.generated.map((file) => file.path),
["share/quixos/generated/remote.ts"],
);
await checkArtifactReceipt(plan, fixture.manifest, receipt, generator, async (_revision, path) => sources.get(path)!);
await assert.rejects(
checkArtifactReceipt(plan, fixture.manifest, receipt, generator, async () => new Uint8Array()),
/replaced or omitted/,
);
fixture.manifest.implementations = fixture.manifest.implementations.filter(
(implementation) => implementation.compatibility === "quixos-service-v1",
);
assert.deepEqual(expectedArtifactReceipt(plan, fixture.manifest, generator).receipt.generated, []);
});
+2 -2
View File
@@ -5,8 +5,8 @@ import {
type PackageArtifacts, type PackageArtifacts,
type ArtifactImplementation, type ArtifactImplementation,
} from "../src/execution-world/index.js"; } from "../src/execution-world/index.js";
export const buildArtifactFixture = (plan: ExecutionPlan) => { export const buildArtifactFixture = (plan: ExecutionPlan, revision = plan.definition.packages[0]!.revision) => {
const expected = plan.definition.packages[0]!; const expected = plan.definition.packages.find((pkg) => pkg.revision === revision)!;
const bytes = new TextEncoder().encode("export const handlers = Object.freeze({});\n"); const bytes = new TextEncoder().encode("export const handlers = Object.freeze({});\n");
const implementation = ( const implementation = (
id: string, id: string,
+1
View File
@@ -2,6 +2,7 @@ external atom Document id "async:document";
import interface DocumentData; import interface DocumentData;
import interface Summarizer; import interface Summarizer;
package ControllerRuntime id "async:controller-runtime" revision "async:controller-runtime@1" { package ControllerRuntime id "async:controller-runtime" revision "async:controller-runtime@1" {
function echo id "async:controller:echo" : string -> string execution snapshot targets [server];
operation refresh id "async:controller:refresh" : record { operationId: string; } -> string operation refresh id "async:controller:refresh" : record { operationId: string; } -> string
mode call execution remote receiver atom Document requires { mode call execution remote receiver atom Document requires {
interface document id "async:controller:document" : DocumentData using [content.get, summary.set]; interface document id "async:controller:document" : DocumentData using [content.get, summary.set];
+3 -1
View File
@@ -14,7 +14,9 @@ test("remote adapters expose selected typed calls, lifetimes and explicit transa
generatePortableTypeScriptBindings(schema, "async:controller-runtime@1", { target: "browser" }), generatePortableTypeScriptBindings(schema, "async:controller-runtime@1", { target: "browser" }),
/readonly "refresh"/, /readonly "refresh"/,
); );
const entry = schema.packages.find((p) => p.revisionId === "async:controller-runtime@1")!.exports[0]!; const entry = schema.packages
.find((p) => p.revisionId === "async:controller-runtime@1")!
.exports.find((entry) => entry.id === "async:controller:refresh")!;
entry.dependencyPorts[0]!.requirement = { entry.dependencyPorts[0]!.requirement = {
kind: "state", kind: "state",
valueType: { kind: "scalar", name: "string" }, valueType: { kind: "scalar", name: "string" },