Build checked asynchronous service registries with Nix
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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, []);
|
||||||
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user