From 1dadc642d12049a5230370ef6c3e99c8e4426659 Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Sun, 20 Sep 2026 20:58:01 -0700 Subject: [PATCH] Build checked asynchronous service registries with Nix --- src/execution-world/receipts.ts | 20 ++++++++++++++- test/artifact-files.test.ts | 25 +++++++++++++++++++ test/artifact-fixture.ts | 4 +-- .../async-objects/ControllerRuntime.qx | 1 + test/remote-bindings.test.ts | 4 ++- 5 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/execution-world/receipts.ts b/src/execution-world/receipts.ts index 693d4ee..4dd4b90 100644 --- a/src/execution-world/receipts.ts +++ b/src/execution-world/receipts.ts @@ -1,5 +1,5 @@ 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 ArtifactReader, type PackageArtifacts } from "./artifacts.js"; import { loadArtifactWorld, type LoadedArtifactWorld } from "./files.js"; @@ -53,6 +53,24 @@ export function expectedArtifactReceipt( sources.set(path, bytes); 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 { receipt: { schemaVersion: 1, diff --git a/test/artifact-files.test.ts b/test/artifact-files.test.ts index cf1b03b..754ca2a 100644 --- a/test/artifact-files.test.ts +++ b/test/artifact-files.test.ts @@ -114,3 +114,28 @@ test("checked receipts bind regenerated code, generator identity and manifests", /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, []); +}); diff --git a/test/artifact-fixture.ts b/test/artifact-fixture.ts index 48144e8..4b8de4c 100644 --- a/test/artifact-fixture.ts +++ b/test/artifact-fixture.ts @@ -5,8 +5,8 @@ import { type PackageArtifacts, type ArtifactImplementation, } from "../src/execution-world/index.js"; -export const buildArtifactFixture = (plan: ExecutionPlan) => { - const expected = plan.definition.packages[0]!; +export const buildArtifactFixture = (plan: ExecutionPlan, revision = plan.definition.packages[0]!.revision) => { + const expected = plan.definition.packages.find((pkg) => pkg.revision === revision)!; const bytes = new TextEncoder().encode("export const handlers = Object.freeze({});\n"); const implementation = ( id: string, diff --git a/test/fixtures/async-objects/ControllerRuntime.qx b/test/fixtures/async-objects/ControllerRuntime.qx index 5b06255..3ccd904 100644 --- a/test/fixtures/async-objects/ControllerRuntime.qx +++ b/test/fixtures/async-objects/ControllerRuntime.qx @@ -2,6 +2,7 @@ external atom Document id "async:document"; import interface DocumentData; import interface Summarizer; 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 mode call execution remote receiver atom Document requires { interface document id "async:controller:document" : DocumentData using [content.get, summary.set]; diff --git a/test/remote-bindings.test.ts b/test/remote-bindings.test.ts index 39a559c..aad99fd 100644 --- a/test/remote-bindings.test.ts +++ b/test/remote-bindings.test.ts @@ -14,7 +14,9 @@ test("remote adapters expose selected typed calls, lifetimes and explicit transa generatePortableTypeScriptBindings(schema, "async:controller-runtime@1", { target: "browser" }), /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 = { kind: "state", valueType: { kind: "scalar", name: "string" },