Compare commits

...

12 Commits

Author SHA1 Message Date
Quixos Subtree Publisher cd2b04dc8c Publish camino-package-runtime from 29c36195235aeb4b60042e7ec4df0b0d2bcfff33 2026-09-17 01:14:23 +00:00
Timothy J. Aveni fc838413c5 Release updated TODO template dependencies 2026-09-16 18:14:23 -07:00
Timothy J. Aveni 5bb8ee876d Add checked React field bindings and Web Studio factories
Replace opaque props with checked generic presentation contracts and lazy typed
interface references. Generate readonly/writable field APIs and component checks.

Preserve CRDT editing through explicit resolved getter/setter contracts, binding-
fenced delta RPCs, native watches and replica-aware field adapters. Custom setters
retain semantic writes; storage snapshots never grant write authority. Cover
concurrent edits, lost acknowledgements, readonly contracts and authorization.

Add receiver-free static factory dispatch, state-field binding shorthand, and
conformance-based creation. Migrate TODO, editable scaffolds and authoring guides.
Verify language/codegen, SDK, RPC, browser lifecycle, local scaffolds, production
browser bundling and CRDT persistence with temporary PostgreSQL.
2026-09-16 16:49:09 -07:00
Timothy J. Aveni 0e183516c0 Revert generated-file renaming; retain formatter exclusions 2026-09-15 20:13:10 -07:00
Timothy J. Aveni 2319b1f637 Mark first-party generated files with .gen; preserve third-party conventions 2026-09-15 17:57:46 -07:00
Timothy J. Aveni 0854822923 Add lightweight correctness linting and resolve formatting conflicts 2026-09-15 15:41:24 -07:00
Timothy J. Aveni 0d3c013c07 Format authored monorepo code with pinned language formatters 2026-09-15 15:41:24 -07:00
Timothy J. Aveni bee4452852 Repair workspace authoring: typed RPC inputs, automatic preservation, and template releases
Use checked input contracts and protobuf JSON for CLI roundtrips; generate Web Studio platform inputs; reject invalid constructors before allocation and filter Createable eligibility.

Preserve compatible storage without no-op migrations, report activation readiness, and validate migration coverage before maintenance. Follow authored package declarations during scaffold refresh and queue source capture.

Add a real local TODO check/activate/create/place/edit acceptance, publish updated protocol and SDK dependencies, and make verified template default selection explicit.
2026-09-14 16:04:59 -07:00
Quixos Subtree Publisher d3d050d9cb Publish camino-package-runtime from a9e94c961430415bcea302c47181d44ec7e2344c 2026-09-14 22:58:04 +00:00
Timothy J. Aveni e8a175504a Make workspace authoring transitions and typed invocation coherent 2026-09-14 15:58:04 -07:00
Timothy J. Aveni 3dd4e9a1ad Migrate TODO implementations to generated candidate bindings 2026-09-13 20:28:50 -07:00
Quixos Subtree Publisher 62ae43ee40 Publish camino-package-runtime from 22bb3d02264980d74de65c34bbbcb81764dc65c0 2026-09-14 03:28:50 +00:00
32 changed files with 2084 additions and 869 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"version": 1, "version": 1,
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos", "sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
"sourceCommit": "e25eee6ce4f13702b2454a9354bc79a29eb2e4a1", "sourceCommit": "29c36195235aeb4b60042e7ec4df0b0d2bcfff33",
"sourcePath": "quixos-instance/packages/camino-package-runtime", "sourcePath": "quixos-instance/packages/camino-package-runtime",
"exportName": "camino-package-runtime", "exportName": "camino-package-runtime",
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git" "mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-package-runtime.git"
+1 -1
View File
@@ -10,7 +10,6 @@ export type MessageBinding<T> = {
decode(value: Value): T; decode(value: Value): T;
}; };
export type QxLiveValue = ReturnType<typeof liveValue>; export type QxLiveValue = ReturnType<typeof liveValue>;
export declare const opaqueReactPropsBinding: MessageBinding<Record<string, unknown>>;
export type BindingValue<B> = B extends MessageBinding<infer T> ? T : never; export type BindingValue<B> = B extends MessageBinding<infer T> ? T : never;
export type QxHandler<C, O> = (context: C) => O | Promise<O>; export type QxHandler<C, O> = (context: C) => O | Promise<O>;
export type QxDerived<C, O> = { export type QxDerived<C, O> = {
@@ -71,6 +70,7 @@ export type QxPortSpec = {
inputType: QxValueType; inputType: QxValueType;
}; };
export type QxHandlerSpec = { export type QxHandlerSpec = {
receiver?: "none";
inputType: QxValueType; inputType: QxValueType;
outputType: QxValueType; outputType: QxValueType;
eventType?: QxValueType; eventType?: QxValueType;
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"bindings.d.ts","sourceRoot":"","sources":["../src/bindings.ts"],"names":[],"mappings":"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"} {"version":3,"file":"bindings.d.ts","sourceRoot":"","sources":["../src/bindings.ts"],"names":[],"mappings":"AACA,OAAO,EAAkC,KAAK,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChF,OAAO,EAGL,SAAS,EAGT,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,CAAC,MAAM,UAAU,EAAE,OAAO,MAAM,CAAC;AACxC,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,CAAC,UAAU,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AACrE,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAAE,MAAM,CAAC,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC;IAAC,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,CAAA;CAAE,CAAC;AACrF,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AACvD,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,SAAS,cAAc,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAC5E,MAAM,MAAM,SAAS,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAC7D,MAAM,MAAM,SAAS,CAAC,CAAC,EAAE,CAAC,IAAI;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;CAAE,CAAC;AACxE,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAAC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC;AACxH,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAAI;IAAE,MAAM,CAAC,EAAE,WAAW,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;CAAE,CAAC;AACxG,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,CAAC,OAAO,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAG,SAAS,CAAC,CAAC,EAAE,CAAC,CAA+B,CAAC;AAErG,yEAAyE;AACzE,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;CAAE,GACvD;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,CAAC;AACtD,MAAM,MAAM,eAAe,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,WAAW,CAAC;IAAC,UAAU,EAAE,WAAW,CAAA;CAAE,CAAC;AAC9F,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,WAAW,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,GAC3E;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;CAAE,GAC9E;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,WAAW,CAAA;CAAE,CAAC;AAChE,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,WAAW,CAAC;IACvB,UAAU,EAAE,WAAW,CAAC;IACxB,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACnC,CAAC;AACF,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC;AAG7D,eAAO,MAAM,aAAa,SAAU,WAAW,SAAS,KAAK,GAAG,SAAS,YAAY,UAAU,KAAG,GAyCjG,CAAC;AAOF,eAAO,MAAM,aAAa,SAAU,WAAW,SAAS,GAAG,YAAY,UAAU,KAAG,KA8BnF,CAAC;AAmBF,uFAAuF;AACvF,eAAO,MAAM,aAAa,GAAI,CAAC,EAAE,CAAC,QAC1B,aAAa,WACV,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,YAChC,UAAU,KACnB,cAAc,GAAG,cA4GnB,CAAC"}
+71 -40
View File
@@ -1,22 +1,7 @@
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { ValueSchema, ObjectValueSchema } from "./camino/api_pb.js"; import { ValueSchema, ObjectValueSchema } from "./camino/api_pb.js";
import { derived, jsToProtoValue, liveValue, protoValueToJs } from "./index.js"; import { derived, jsToProtoValue, liveValue, protoValueToJs, } from "./index.js";
import { assertReferenceFree, referenceToWire } from "./references.js"; import { assertReferenceFree, referenceToWire } from "./references.js";
const reactPropsDescriptor = "org.quixos.web-studio.ReactProps";
// Explicit temporary props exception, matching orch's RPC contract. Field shapes
// remain unchecked pending generics; ordinary messages and state do not gain it.
export const opaqueReactPropsBinding = {
encode(value) {
if (!value || typeof value !== "object" || Array.isArray(value))
throw new Error("React props must be an object");
return jsToProtoValue(value);
},
decode(value) {
if (value.kind.case !== "objectValue")
throw new Error("React props must be an object");
return protoValueToJs(value);
},
};
export const qxDerived = (get) => ({ kind: "derived", get }); export const qxDerived = (get) => ({ kind: "derived", get });
// Conversion belongs at the binding boundary. It does not add orchestrator validation. // Conversion belongs at the binding boundary. It does not add orchestrator validation.
export const decodeQxValue = (type, value, messages) => { export const decodeQxValue = (type, value, messages) => {
@@ -42,10 +27,8 @@ export const decodeQxValue = (type, value, messages) => {
return value.kind.value.values.map((entry) => decodeQxValue(type.value, entry, messages)); return value.kind.value.values.map((entry) => decodeQxValue(type.value, entry, messages));
} }
if (type.kind === "message") { if (type.kind === "message") {
if (type.descriptorId !== reactPropsDescriptor)
assertReferenceFree(protoValueToJs(value)); assertReferenceFree(protoValueToJs(value));
const decoded = requireMessage(messages, type.descriptorId).decode(value); const decoded = requireMessage(messages, type.descriptorId).decode(value);
if (type.descriptorId !== reactPropsDescriptor)
assertReferenceFree(decoded); assertReferenceFree(decoded);
return decoded; return decoded;
} }
@@ -99,11 +82,9 @@ export const encodeQxValue = (type, value, messages) => {
referenceToWire(value); referenceToWire(value);
return jsToProtoValue(value); return jsToProtoValue(value);
} }
if (type.kind !== "message" || type.descriptorId !== reactPropsDescriptor)
assertReferenceFree(value); assertReferenceFree(value);
if (type.kind === "message") { if (type.kind === "message") {
const encoded = requireMessage(messages, type.descriptorId).encode(value); const encoded = requireMessage(messages, type.descriptorId).encode(value);
if (type.descriptorId !== reactPropsDescriptor)
assertReferenceFree(protoValueToJs(encoded)); assertReferenceFree(protoValueToJs(encoded));
return encoded; return encoded;
} }
@@ -111,8 +92,9 @@ export const encodeQxValue = (type, value, messages) => {
}; };
const inputValue = (context, type) => { const inputValue = (context, type) => {
if (type.kind === "message" || type.kind === "record") if (type.kind === "message" || type.kind === "record")
return create(ValueSchema, { kind: { case: "objectValue", return create(ValueSchema, {
value: create(ObjectValueSchema, { fields: context.inputProto }) } }); kind: { case: "objectValue", value: create(ObjectValueSchema, { fields: context.inputProto }) },
});
return context.inputProto.value; return context.inputProto.value;
}; };
const inputFields = (type, value, messages) => { const inputFields = (type, value, messages) => {
@@ -133,37 +115,86 @@ export const bindQxHandler = (spec, handler, messages) => {
switch (port.kind) { switch (port.kind) {
case "state": { case "state": {
const state = raw.state(port.id); const state = raw.state(port.id);
return [name, { return [
...(port.primitives.includes("read") ? { get: async () => decodeQxValue(port.valueType, (await state.live()).$quixosValue, messages), live: () => state.live() } : {}), name,
...(port.primitives.includes("write") ? { set: async (value) => state.set(liveValue(encodeQxValue(port.valueType, value, messages))) } : {}), {
}]; ...(port.primitives.includes("read")
? {
get: async () => decodeQxValue(port.valueType, (await state.live()).$quixosValue, messages),
live: () => state.live(),
}
: {}),
...(port.primitives.includes("write")
? {
set: async (value) => state.set(liveValue(encodeQxValue(port.valueType, value, messages))),
}
: {}),
},
];
} }
case "edge": { case "edge": {
const edge = raw.edge(port.id); const edge = raw.edge(port.id);
return [name, { ...Object.fromEntries(port.primitives.map((primitive) => [primitive, edge[primitive]])), return [
name,
{
...Object.fromEntries(port.primitives.map((primitive) => [
primitive,
edge[primitive],
])),
...(port.primitives.includes("resolve") ? { collection: edge.collection } : {}), ...(port.primitives.includes("resolve") ? { collection: edge.collection } : {}),
...(port.primitives.includes("resolve") && port.primitives.includes("connect") && port.primitives.includes("disconnect") ? { replace: edge.replace } : {}) }]; ...(port.primitives.includes("resolve") &&
port.primitives.includes("connect") &&
port.primitives.includes("disconnect")
? { replace: edge.replace }
: {}),
},
];
} }
case "interface": { case "interface": {
const target = raw.interface(port.id); const target = raw.interface(port.id);
return [name, { objectId: target.objectId, return [
live: Object.fromEntries(Object.entries(port.operations).map(([name, operation]) => [name, name,
{
objectId: target.objectId,
live: Object.fromEntries(Object.entries(port.operations).map(([name, operation]) => [
name,
(input) => target.live(operation.id, inputFields(operation.inputType, input, messages)), (input) => target.live(operation.id, inputFields(operation.inputType, input, messages)),
])), ])),
...Object.fromEntries(Object.entries(port.operations).map(([name, operation]) => [name, ...Object.fromEntries(Object.entries(port.operations).map(([name, operation]) => [
async (input) => decodeQxValue(operation.outputType, (await target.live(operation.id, inputFields(operation.inputType, input, messages))).$quixosValue, messages), name,
])) }]; async (input) => decodeQxValue(operation.outputType, (await target.live(operation.id, inputFields(operation.inputType, input, messages)))
.$quixosValue, messages),
])),
},
];
} }
case "constructor": return [name, { construct: (input) => raw.constructor(port.id).construct(inputFields(port.inputType, input, messages)) }]; case "constructor":
return [
name,
{
construct: (input) => raw.constructor(port.id).construct(inputFields(port.inputType, input, messages)),
},
];
} }
})); }));
return { objectId: raw.objectId, signal: raw.signal, return {
...(raw.openSession ? { openSession: async () => { ...(spec.receiver === "none" ? {} : { objectId: raw.objectId }),
signal: raw.signal,
...(spec.receiver !== "none" && raw.openSession
? {
openSession: async () => {
const session = await raw.openSession(); const session = await raw.openSession();
return { id: session.id, close: () => session.close(), return {
run: (work) => session.run((next) => work(bindContext(next))) }; id: session.id,
} } : {}), close: () => session.close(),
input: decodeQxValue(spec.inputType, inputValue(raw, spec.inputType), messages), ports }; run: (work) => session.run((next) => work(bindContext(next))),
};
},
}
: {}),
input: decodeQxValue(spec.inputType, inputValue(raw, spec.inputType), messages),
ports,
};
}; };
const execute = async (raw) => { const execute = async (raw) => {
const context = bindContext(raw); const context = bindContext(raw);
+1 -1
View File
@@ -2,7 +2,7 @@ import http from "node:http";
import { type QxObjectRef } from "./references.js"; import { type QxObjectRef } from "./references.js";
export * from "./bindings.js"; export * from "./bindings.js";
export { relationshipMap, relationshipList, relationshipSet } from "./relationships.js"; export { relationshipMap, relationshipList, relationshipSet } from "./relationships.js";
export { createMigrationContext, migrationObjectId, serveMigration, type MigrationContext, type MigrationInput, type MigrationOutput, type MigrationEdge } from "./migration.js"; export { createMigrationContext, migrationObjectId, serveMigration, type MigrationContext, type MigrationInput, type MigrationOutput, type MigrationEdge, } from "./migration.js";
import { type Client, type ConnectRouter } from "@connectrpc/connect"; import { type Client, type ConnectRouter } from "@connectrpc/connect";
import { CaminoService, type Value } from "./camino/api_pb.js"; import { CaminoService, type Value } from "./camino/api_pb.js";
import { OrchestratorRuntime } from "./quixos/orch_pb.js"; import { OrchestratorRuntime } from "./quixos/orch_pb.js";
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
+139 -51
View File
@@ -1,12 +1,12 @@
import http from "node:http"; import http from "node:http";
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { createInvocationRegistry } from "./invocations.js"; import { createInvocationRegistry } from "./invocations.js";
import { isObjectReference, referenceFromWire, referenceToWire, assertReferenceFree } from "./references.js"; import { isObjectReference, referenceFromWire, referenceToWire, assertReferenceFree, } from "./references.js";
export * from "./bindings.js"; export * from "./bindings.js";
export { relationshipMap, relationshipList, relationshipSet } from "./relationships.js"; export { relationshipMap, relationshipList, relationshipSet } from "./relationships.js";
import { AsyncLocalStorage } from "node:async_hooks"; import { AsyncLocalStorage } from "node:async_hooks";
import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto"; import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
export { createMigrationContext, migrationObjectId, serveMigration } from "./migration.js"; export { createMigrationContext, migrationObjectId, serveMigration, } from "./migration.js";
import { create, equals } from "@bufbuild/protobuf"; import { create, equals } from "@bufbuild/protobuf";
import { Code, ConnectError, createClient } from "@connectrpc/connect"; import { Code, ConnectError, createClient } from "@connectrpc/connect";
import { connectNodeAdapter, createConnectTransport } from "@connectrpc/connect-node"; import { connectNodeAdapter, createConnectTransport } from "@connectrpc/connect-node";
@@ -27,13 +27,20 @@ const recordDependency = async (dependency) => {
const bytesToBase64 = (value) => Buffer.from(value).toString("base64"); const bytesToBase64 = (value) => Buffer.from(value).toString("base64");
const base64ToBytes = (value) => Buffer.from(value, "base64"); const base64ToBytes = (value) => Buffer.from(value, "base64");
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value); const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
const isWrappedValue = (value) => isRecord(value) && "$quixosValue" in value && const isWrappedValue = (value) => isRecord(value) &&
isRecord(value.$quixosValue) && value.$quixosValue.$typeName === "camino.Value"; "$quixosValue" in value &&
export const objectRef = (reference) => { referenceToWire(reference); return reference; }; isRecord(value.$quixosValue) &&
value.$quixosValue.$typeName === "camino.Value";
export const objectRef = (reference) => {
referenceToWire(reference);
return reference;
};
export const liveValue = (value) => ({ $quixosValue: value }); export const liveValue = (value) => ({ $quixosValue: value });
export const jsToProtoValue = (value) => { export const jsToProtoValue = (value) => {
if (isObjectReference(value)) if (isObjectReference(value))
return create(ValueSchema, { kind: { case: "refValue", value: create(RefValueSchema, { objectId: referenceToWire(value) }) } }); return create(ValueSchema, {
kind: { case: "refValue", value: create(RefValueSchema, { objectId: referenceToWire(value) }) },
});
if (isWrappedValue(value)) if (isWrappedValue(value))
return value.$quixosValue; return value.$quixosValue;
if (value === null || value === undefined) { if (value === null || value === undefined) {
@@ -56,37 +63,49 @@ export const jsToProtoValue = (value) => {
} }
if (isRecord(value) && "$quixosRef" in value) if (isRecord(value) && "$quixosRef" in value)
throw new Error("Raw ID wrappers are not object references"); throw new Error("Raw ID wrappers are not object references");
if (isRecord(value) && typeof value.$quixosCrdtType === "string" && if (isRecord(value) && typeof value.$quixosCrdtType === "string" && typeof value.$quixosCrdtPayload === "string") {
typeof value.$quixosCrdtPayload === "string") {
return create(ValueSchema, { return create(ValueSchema, {
kind: { case: "crdtValue", value: create(CrdtValueSchema, { kind: {
case: "crdtValue",
value: create(CrdtValueSchema, {
type: value.$quixosCrdtType, type: value.$quixosCrdtType,
encoding: typeof value.$quixosCrdtEncoding === "string" ? value.$quixosCrdtEncoding : "base64", encoding: typeof value.$quixosCrdtEncoding === "string" ? value.$quixosCrdtEncoding : "base64",
payload: base64ToBytes(value.$quixosCrdtPayload), payload: base64ToBytes(value.$quixosCrdtPayload),
}) }, }),
},
}); });
} }
if (!isRecord(value)) if (!isRecord(value))
throw new Error(`Unsupported runtime value ${typeof value}`); throw new Error(`Unsupported runtime value ${typeof value}`);
return create(ValueSchema, { return create(ValueSchema, {
kind: { case: "objectValue", value: create(ObjectValueSchema, { kind: {
case: "objectValue",
value: create(ObjectValueSchema, {
fields: Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, jsToProtoValue(entry)])), fields: Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, jsToProtoValue(entry)])),
}) }, }),
},
}); });
}; };
export const protoValueToJs = (value) => { export const protoValueToJs = (value) => {
switch (value?.kind.case) { switch (value?.kind.case) {
case "nullValue": case "nullValue":
case undefined: return null; case undefined:
return null;
case "boolValue": case "boolValue":
case "numberValue": case "numberValue":
case "stringValue": case "stringValue":
case "integerValue": return value.kind.value; case "integerValue":
case "bytesValue": return bytesToBase64(value.kind.value); return value.kind.value;
case "refValue": return referenceFromWire(value.kind.value.objectId); case "bytesValue":
case "listValue": return value.kind.value.values.map(protoValueToJs); return bytesToBase64(value.kind.value);
case "objectValue": return Object.fromEntries(Object.entries(value.kind.value.fields).map(([key, entry]) => [key, protoValueToJs(entry)])); case "refValue":
case "crdtValue": return { return referenceFromWire(value.kind.value.objectId);
case "listValue":
return value.kind.value.values.map(protoValueToJs);
case "objectValue":
return Object.fromEntries(Object.entries(value.kind.value.fields).map(([key, entry]) => [key, protoValueToJs(entry)]));
case "crdtValue":
return {
$quixosCrdtType: value.kind.value.type, $quixosCrdtType: value.kind.value.type,
$quixosCrdtEncoding: value.kind.value.encoding, $quixosCrdtEncoding: value.kind.value.encoding,
$quixosCrdtPayload: bytesToBase64(value.kind.value.payload), $quixosCrdtPayload: bytesToBase64(value.kind.value.payload),
@@ -96,9 +115,13 @@ export const protoValueToJs = (value) => {
export const protoFieldsToJs = (fields) => Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, protoValueToJs(value)])); export const protoFieldsToJs = (fields) => Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, protoValueToJs(value)]));
export class RuntimeAuthorityError extends Error { export class RuntimeAuthorityError extends Error {
retryable; retryable;
constructor(message) { super(message); this.name = "RuntimeAuthorityError"; this.retryable = /WORKSPACE_FENCED|STALE_EPOCH/.test(message); } constructor(message) {
super(message);
this.name = "RuntimeAuthorityError";
this.retryable = /WORKSPACE_FENCED|STALE_EPOCH/.test(message);
}
} }
const targetForEdge = (edge, projectionId) => edge.firstProjectionId === projectionId ? edge.secondObjectId : edge.firstObjectId; const targetForEdge = (edge, projectionId) => (edge.firstProjectionId === projectionId ? edge.secondObjectId : edge.firstObjectId);
export const createRuntimeContext = (camino, orch, request) => { export const createRuntimeContext = (camino, orch, request) => {
const ports = new Map(); const ports = new Map();
for (const dependency of request.dependencies) { for (const dependency of request.dependencies) {
@@ -130,25 +153,55 @@ export const createRuntimeContext = (camino, orch, request) => {
case "edge": { case "edge": {
const { edgeTypeId, projectionId } = dependency.binding.value; const { edgeTypeId, projectionId } = dependency.binding.value;
const dependencyObjectId = dependency.objectId || request.objectId; const dependencyObjectId = dependency.objectId || request.objectId;
const collectionResult = (response) => ({ revision: response.revision, const collectionResult = (response) => ({
entries: response.entries.map((entry) => ({ edgeId: entry.edgeId, target: referenceFromWire(entry.targetObjectId), revision: response.revision,
...(entry.key ? { key: entry.key.kind.case === "integerValue" ? BigInt(entry.key.kind.value) : protoValueToJs(entry.key) } : {}) })) }); entries: response.entries.map((entry) => ({
edgeId: entry.edgeId,
target: referenceFromWire(entry.targetObjectId),
...(entry.key
? {
key: entry.key.kind.case === "integerValue"
? BigInt(entry.key.kind.value)
: protoValueToJs(entry.key),
}
: {}),
})),
});
const edge = { const edge = {
edgeTypeId, edgeTypeId,
projectionId, projectionId,
async collection() { async collection() {
await recordDependency({ kind: "edge", objectId: dependencyObjectId, attachmentId: edgeTypeId, projectionId }); await recordDependency({
kind: "edge",
objectId: dependencyObjectId,
attachmentId: edgeTypeId,
projectionId,
});
return collectionResult(await camino.readCollection({ objectId: dependencyObjectId, edgeTypeId, projectionId })); return collectionResult(await camino.readCollection({ objectId: dependencyObjectId, edgeTypeId, projectionId }));
}, },
async replace(entries, expectedRevision) { async replace(entries, expectedRevision) {
return collectionResult(await camino.replaceCollection({ objectId: dependencyObjectId, edgeTypeId, projectionId, expectedRevision, return collectionResult(await camino.replaceCollection({
objectId: dependencyObjectId,
edgeTypeId,
projectionId,
expectedRevision,
entries: entries.map((entry) => { entries: entries.map((entry) => {
assertReferenceFree(entry.key); assertReferenceFree(entry.key);
return { edgeId: entry.edgeId ?? "", targetObjectId: referenceToWire(entry.target), key: entry.key === undefined ? undefined : jsToProtoValue(entry.key) }; return {
}) })); edgeId: entry.edgeId ?? "",
targetObjectId: referenceToWire(entry.target),
key: entry.key === undefined ? undefined : jsToProtoValue(entry.key),
};
}),
}));
}, },
async resolve() { async resolve() {
await recordDependency({ kind: "edge", objectId: dependencyObjectId, attachmentId: edgeTypeId, projectionId }); await recordDependency({
kind: "edge",
objectId: dependencyObjectId,
attachmentId: edgeTypeId,
projectionId,
});
const result = await camino.resolveEdge({ objectId: dependencyObjectId, edgeTypeId, projectionId }); const result = await camino.resolveEdge({ objectId: dependencyObjectId, edgeTypeId, projectionId });
return result.edges.map((entry) => referenceFromWire(targetForEdge(entry, projectionId))); return result.edges.map((entry) => referenceFromWire(targetForEdge(entry, projectionId)));
}, },
@@ -239,7 +292,9 @@ export const createRuntimeContext = (camino, orch, request) => {
return port; return port;
}; };
return { return {
objectId: referenceFromWire(request.objectId), get objectId() {
return referenceFromWire(request.objectId);
},
input: protoFieldsToJs(request.input), input: protoFieldsToJs(request.input),
inputProto: request.input, inputProto: request.input,
ports, ports,
@@ -265,8 +320,10 @@ const protoDependencies = (dependencies) => dependencies.map((entry) => create(D
export const createPackageRuntimeRoutes = (config) => { export const createPackageRuntimeRoutes = (config) => {
const invocations = createInvocationRegistry(); const invocations = createInvocationRegistry();
const headers = {}; const headers = {};
const processToken = process.env.CAMINO_RUNTIME_AUTH_TOKEN ?? (process.env.CAMINO_RUNTIME_AUTH_TOKEN_FILE const processToken = process.env.CAMINO_RUNTIME_AUTH_TOKEN ??
? readFileSync(process.env.CAMINO_RUNTIME_AUTH_TOKEN_FILE, "utf8").trim() : ""); (process.env.CAMINO_RUNTIME_AUTH_TOKEN_FILE
? readFileSync(process.env.CAMINO_RUNTIME_AUTH_TOKEN_FILE, "utf8").trim()
: "");
if (processToken) { if (processToken) {
headers["x-camino-runtime-token"] = processToken; headers["x-camino-runtime-token"] = processToken;
} }
@@ -276,12 +333,14 @@ export const createPackageRuntimeRoutes = (config) => {
const camino = createClient(CaminoService, createConnectTransport({ const camino = createClient(CaminoService, createConnectTransport({
baseUrl: config.caminoUrl ?? process.env.CAMINO_URL ?? "http://127.0.0.1:7310", baseUrl: config.caminoUrl ?? process.env.CAMINO_URL ?? "http://127.0.0.1:7310",
httpVersion: "1.1", httpVersion: "1.1",
interceptors: headers["x-camino-runtime-token"] ? [ interceptors: headers["x-camino-runtime-token"]
? [
(next) => async (request) => { (next) => async (request) => {
request.header.set("x-camino-runtime-token", headers["x-camino-runtime-token"]); request.header.set("x-camino-runtime-token", headers["x-camino-runtime-token"]);
return await next(request); return await next(request);
}, },
] : [], ]
: [],
})); }));
const orch = createClient(OrchestratorRuntime, createConnectTransport({ const orch = createClient(OrchestratorRuntime, createConnectTransport({
baseUrl: config.orchUrl ?? process.env.QUIXOS_ORCH_URL ?? "http://127.0.0.1:7311", baseUrl: config.orchUrl ?? process.env.QUIXOS_ORCH_URL ?? "http://127.0.0.1:7311",
@@ -298,16 +357,23 @@ export const createPackageRuntimeRoutes = (config) => {
}; };
const clientsFor = (request) => { const clientsFor = (request) => {
const context = request.context; const context = request.context;
if (process.env.QUIXOS_RUNTIME_INSTANCE_ID && (!context?.grant || context.instanceId !== process.env.QUIXOS_RUNTIME_INSTANCE_ID || !context.workspaceEpoch)) { if (process.env.QUIXOS_RUNTIME_INSTANCE_ID &&
(!context?.grant || context.instanceId !== process.env.QUIXOS_RUNTIME_INSTANCE_ID || !context.workspaceEpoch)) {
throw new ConnectError("Managed invocation requires an exact instance and epoch grant", Code.Unauthenticated); throw new ConnectError("Managed invocation requires an exact instance and epoch grant", Code.Unauthenticated);
} }
if (!context?.grant) if (!context?.grant)
return { camino, orch }; return { camino, orch };
const transport = (url) => createConnectTransport({ baseUrl: url, httpVersion: "1.1", interceptors: [(next) => async (call) => { const transport = (url) => createConnectTransport({
baseUrl: url,
httpVersion: "1.1",
interceptors: [
(next) => async (call) => {
call.header.set("x-quixos-invocation-grant", context.grant); call.header.set("x-quixos-invocation-grant", context.grant);
call.header.set("x-camino-runtime-token", processToken); call.header.set("x-camino-runtime-token", processToken);
return next(call); return next(call);
}] }); },
],
});
return { return {
camino: createClient(CaminoService, transport(config.caminoUrl ?? process.env.CAMINO_URL ?? "http://127.0.0.1:7310")), camino: createClient(CaminoService, transport(config.caminoUrl ?? process.env.CAMINO_URL ?? "http://127.0.0.1:7310")),
orch: createClient(OrchestratorRuntime, transport(config.orchUrl ?? process.env.QUIXOS_ORCH_URL ?? "http://127.0.0.1:7311")), orch: createClient(OrchestratorRuntime, transport(config.orchUrl ?? process.env.QUIXOS_ORCH_URL ?? "http://127.0.0.1:7311")),
@@ -315,9 +381,12 @@ export const createPackageRuntimeRoutes = (config) => {
}; };
const runtimeControl = async (operation, input) => { const runtimeControl = async (operation, input) => {
const response = await fetch(`${config.caminoUrl ?? process.env.CAMINO_URL ?? "http://127.0.0.1:7310"}/__runtime/${operation}`, { const response = await fetch(`${config.caminoUrl ?? process.env.CAMINO_URL ?? "http://127.0.0.1:7310"}/__runtime/${operation}`, {
method: "POST", headers: { "content-type": "application/json", "x-camino-runtime-token": processToken }, body: JSON.stringify(input), signal: AbortSignal.timeout(10_000), method: "POST",
headers: { "content-type": "application/json", "x-camino-runtime-token": processToken },
body: JSON.stringify(input),
signal: AbortSignal.timeout(10_000),
}); });
const value = await response.json(); const value = (await response.json());
if (!response.ok) if (!response.ok)
throw new RuntimeAuthorityError(value.error ?? "Runtime authority request failed"); throw new RuntimeAuthorityError(value.error ?? "Runtime authority request failed");
return value; return value;
@@ -327,8 +396,13 @@ export const createPackageRuntimeRoutes = (config) => {
return; return;
runtimeContext.openSession = async () => { runtimeContext.openSession = async () => {
const ownerId = request.context.ownerConformanceId; const ownerId = request.context.ownerConformanceId;
const registration = { grant: request.context.grant, objectId: request.objectId, ownerId, const registration = {
sessionId: `session:${randomBytes(16).toString("hex")}`, token: randomBytes(32).toString("base64url") }; grant: request.context.grant,
objectId: request.objectId,
ownerId,
sessionId: `session:${randomBytes(16).toString("hex")}`,
token: randomBytes(32).toString("base64url"),
};
const register = () => runtimeControl("register-session", registration); const register = () => runtimeControl("register-session", registration);
const registered = await register().catch((error) => { const registered = await register().catch((error) => {
// Retry a transport/lost-response failure with exactly the same identity. // Retry a transport/lost-response failure with exactly the same identity.
@@ -347,7 +421,10 @@ export const createPackageRuntimeRoutes = (config) => {
// by the caller without replaying a side-effecting callback. // by the caller without replaying a side-effecting callback.
const grant = await runtimeControl("acquire-session", registered); const grant = await runtimeControl("acquire-session", registered);
const execution = invocations.begin(grant.invocationId); const execution = invocations.begin(grant.invocationId);
const sessionRequest = { ...request, context: { grant: grant.grant, instanceId: grant.instanceId, workspaceEpoch: grant.epoch } }; const sessionRequest = {
...request,
context: { grant: grant.grant, instanceId: grant.instanceId, workspaceEpoch: grant.epoch },
};
const clients = clientsFor(sessionRequest); const clients = clientsFor(sessionRequest);
const context = createRuntimeContext(clients.camino, clients.orch, sessionRequest); const context = createRuntimeContext(clients.camino, clients.orch, sessionRequest);
context.signal = execution.signal; context.signal = execution.signal;
@@ -359,7 +436,10 @@ export const createPackageRuntimeRoutes = (config) => {
await runtimeControl("complete-invocation", { invocationId: grant.invocationId }).catch((error) => console.error("Session completion will be reconciled by the host", error)); await runtimeControl("complete-invocation", { invocationId: grant.invocationId }).catch((error) => console.error("Session completion will be reconciled by the host", error));
} }
}, },
async close() { await runtimeControl("close-session", registered); closed = true; }, async close() {
await runtimeControl("close-session", registered);
closed = true;
},
}; };
}; };
}; };
@@ -370,9 +450,15 @@ export const createPackageRuntimeRoutes = (config) => {
exportIds: Object.keys(config.exports), exportIds: Object.keys(config.exports),
capabilities: ["invocation-completion-v1", "instance-authentication-v1", "epoch-grants-v1"], capabilities: ["invocation-completion-v1", "instance-authentication-v1", "epoch-grants-v1"],
instanceId: process.env.QUIXOS_RUNTIME_INSTANCE_ID ?? "", instanceId: process.env.QUIXOS_RUNTIME_INSTANCE_ID ?? "",
authenticationProof: request.nonce && processToken ? createHmac("sha256", processToken) authenticationProof: request.nonce && processToken
.update(JSON.stringify([request.nonce, process.env.QUIXOS_RUNTIME_INSTANCE_ID ?? "", config.packageRevisionId])) ? createHmac("sha256", processToken)
.digest("hex") : "", .update(JSON.stringify([
request.nonce,
process.env.QUIXOS_RUNTIME_INSTANCE_ID ?? "",
config.packageRevisionId,
]))
.digest("hex")
: "",
}), }),
getInvocationStatus: (request, context) => { getInvocationStatus: (request, context) => {
authenticateInstance(context.requestHeader); authenticateInstance(context.requestHeader);
@@ -437,7 +523,12 @@ export const createPackageRuntimeRoutes = (config) => {
return await pending; return await pending;
const establish = (async () => { const establish = (async () => {
const controller = new AbortController(); const controller = new AbortController();
const stream = camino.watchObject({ objectId: dependency.objectId, includeSnapshot: true, attachmentIds: request.context?.grant ? [dependency.attachmentId] : [] }, { signal: controller.signal })[Symbol.asyncIterator](); const stream = camino
.watchObject({
objectId: dependency.objectId,
includeSnapshot: true,
attachmentIds: request.context?.grant ? [dependency.attachmentId] : [],
}, { signal: controller.signal })[Symbol.asyncIterator]();
try { try {
// Camino subscribes before producing the snapshot, so once this // Camino subscribes before producing the snapshot, so once this
// resolves the following state/edge read cannot race the stream. // resolves the following state/edge read cannot race the stream.
@@ -507,10 +598,7 @@ export const createPackageRuntimeRoutes = (config) => {
await abort; await abort;
break; break;
} }
const outcome = await Promise.race([ const outcome = await Promise.race([...[...subscriptions.values()].map((entry) => entry.next), abort]);
...[...subscriptions.values()].map((entry) => entry.next),
abort,
]);
if (outcome === "abort") if (outcome === "abort")
break; break;
const subscription = subscriptions.get(outcome.key); const subscription = subscriptions.get(outcome.key);
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"invocations.d.ts","sourceRoot":"","sources":["../src/invocations.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,eAAO,MAAM,wBAAwB;IAGjC,KAAK,KAAK,MAAM;QAOL,MAAM;QAA2B,MAAM;;IAElD,MAAM,KAAK,MAAM;QAAa,YAAY;QAAM,KAAK;;IACrD,MAAM,KAAK,MAAM;;;;CASpB,CAAC"} {"version":3,"file":"invocations.d.ts","sourceRoot":"","sources":["../src/invocations.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,eAAO,MAAM,wBAAwB;IAGjC,KAAK,KAAK,MAAM;QASZ,MAAM;QACN,MAAM;;IAKV,MAAM,KAAK,MAAM;QACN,YAAY;QAAM,KAAK;;IAElC,MAAM,KAAK,MAAM;;;;CASpB,CAAC"}
+9 -2
View File
@@ -11,9 +11,16 @@ export const createInvocationRegistry = () => {
throw new Error("Invocation registry full; explicit runtime retirement required"); throw new Error("Invocation registry full; explicit runtime retirement required");
const entry = { state: "running", controller: new AbortController() }; const entry = { state: "running", controller: new AbortController() };
entries.set(id, entry); entries.set(id, entry);
return { signal: entry.controller.signal, finish(failed = false) { entry.state = failed ? "failed" : "completed"; } }; return {
signal: entry.controller.signal,
finish(failed = false) {
entry.state = failed ? "failed" : "completed";
},
};
},
status(id) {
return { invocationId: id, state: entries.get(id)?.state ?? "unknown" };
}, },
status(id) { return { invocationId: id, state: entries.get(id)?.state ?? "unknown" }; },
cancel(id) { cancel(id) {
const entry = entries.get(id); const entry = entries.get(id);
if (entry && ["running", "cancellation-requested"].includes(entry.state)) { if (entry && ["running", "cancellation-requested"].includes(entry.state)) {
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"migration.d.ts","sourceRoot":"","sources":["../src/migration.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,GAAG;IAC3B,aAAa,EAAE,CAAC,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IACxD,KAAK,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;QAAC,MAAM,EAAE,CAAC,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC,EAAE,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAC5H,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAC;QAChD,MAAM,CAAC,EAAE;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,OAAO,CAAA;SAAC,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,aAAa,EAAE,CAAA;KAAC,EAAE,CAAC;CAC7E,CAAC;AACF,MAAM,MAAM,aAAa,GAAG;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAC,CAAC;AACjQ,MAAM,MAAM,eAAe,GAAG;IAAC,aAAa,EAAE,CAAC,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IACnE,MAAM,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAC,EAAE,CAAC;IAC3D,OAAO,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,EAAE,CAAC;IAChE,gBAAgB,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,aAAa,EAAE,CAAA;KAAC,EAAE,CAAA;CAAC,CAAC;AAC9D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAC,EAAE,CAAC;IAC9D,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9C,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC5D,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;IACjD,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,EAAE,CAAC;IACrC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC;CAC1D,CAAC;AACF,eAAO,MAAM,iBAAiB,gBAAiB,MAAM,QAAQ,MAAM,cAAc,MAAM,WACwB,CAAC;AAEhH;sEACsE;AACtE,eAAO,MAAM,sBAAsB,UAAW,cAAc;;;CAgD3D,CAAC;AAEF;;+EAE+E;AAC/E,eAAO,MAAM,cAAc,YAAmB,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,kBAehH,CAAC"} {"version":3,"file":"migration.d.ts","sourceRoot":"","sources":["../src/migration.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,GAAG;IAC3B,aAAa,EAAE,CAAC,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;QACpB,MAAM,EAAE,CAAC,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC,EAAE,CAAC;QACjD,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,MAAM,CAAC,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,OAAO,CAAA;SAAE,EAAE,CAAC;QAChD,KAAK,CAAC,EAAE,aAAa,EAAE,CAAC;KACzB,EAAE,CAAC;CACL,CAAC;AACF,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AACF,MAAM,MAAM,eAAe,GAAG;IAC5B,aAAa,EAAE,CAAC,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;IAC7D,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAClE,gBAAgB,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,aAAa,EAAE,CAAA;KAAE,EAAE,CAAC;CAC9D,CAAC;AACF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;IAChE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9C,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC5D,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;IACjD,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,EAAE,CAAC;IACrC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC;CAC1D,CAAC;AACF,eAAO,MAAM,iBAAiB,gBAAiB,MAAM,QAAQ,MAAM,cAAc,MAAM,WAGnE,CAAC;AAErB;sEACsE;AACtE,eAAO,MAAM,sBAAsB,UAAW,cAAc;;;CAgF3D,CAAC;AAEF;;+EAE+E;AAC/E,eAAO,MAAM,cAAc,YAAmB,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,kBAgBhH,CAAC"}
+22 -7
View File
@@ -1,11 +1,21 @@
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
export const migrationObjectId = (executionId, port, logicalKey) => `obj:migration:${createHash("sha256").update(JSON.stringify([executionId, port, logicalKey])).digest("hex")}`; export const migrationObjectId = (executionId, port, logicalKey) => `obj:migration:${createHash("sha256")
.update(JSON.stringify([executionId, port, logicalKey]))
.digest("hex")}`;
/** No ordinary RuntimeContext or network/database clients are supplied here. /** No ordinary RuntimeContext or network/database clients are supplied here.
* Process isolation belongs to the host, not this convenience API. */ * Process isolation belongs to the host, not this convenience API. */
export const createMigrationContext = (input) => { export const createMigrationContext = (input) => {
if (input.schemaVersion !== 1 || !input.executionId || new Set(input.ports.map((entry) => entry.name)).size !== input.ports.length) if (input.schemaVersion !== 1 ||
!input.executionId ||
new Set(input.ports.map((entry) => entry.name)).size !== input.ports.length)
throw new Error("Invalid migration input"); throw new Error("Invalid migration input");
const output = { schemaVersion: 1, executionId: input.executionId, writes: [], creates: [], edgeReplacements: [] }; const output = {
schemaVersion: 1,
executionId: input.executionId,
writes: [],
creates: [],
edgeReplacements: [],
};
const port = (name, access) => { const port = (name, access) => {
const selected = input.ports.find((entry) => entry.name === name); const selected = input.ports.find((entry) => entry.name === name);
if (!selected?.access.includes(access) || (selected.view === "old" && access !== "read")) if (!selected?.access.includes(access) || (selected.view === "old" && access !== "read"))
@@ -17,7 +27,8 @@ export const createMigrationContext = (input) => {
const selected = port(name, "read"), states = structuredClone(selected.states ?? []); const selected = port(name, "read"), states = structuredClone(selected.states ?? []);
if (selected.view === "new" && Object.hasOwn(selected, "defaultValue")) if (selected.view === "new" && Object.hasOwn(selected, "defaultValue"))
for (const helper of output.creates) { for (const helper of output.creates) {
if (input.ports.find((entry) => entry.name === helper.port)?.atomId === selected.attachedAtomId && !states.some((entry) => entry.objectId === helper.objectId)) if (input.ports.find((entry) => entry.name === helper.port)?.atomId === selected.attachedAtomId &&
!states.some((entry) => entry.objectId === helper.objectId))
states.push({ objectId: helper.objectId, value: structuredClone(selected.defaultValue) }); states.push({ objectId: helper.objectId, value: structuredClone(selected.defaultValue) });
} }
if (selected.view === "new") if (selected.view === "new")
@@ -30,9 +41,11 @@ export const createMigrationContext = (input) => {
else else
states[existing] = entry; states[existing] = entry;
} }
return states.sort((a, b) => a.objectId < b.objectId ? -1 : a.objectId > b.objectId ? 1 : 0); return states.sort((a, b) => (a.objectId < b.objectId ? -1 : a.objectId > b.objectId ? 1 : 0));
},
read(name, objectId) {
return context.enumerate(name).find((entry) => entry.objectId === objectId)?.value;
}, },
read(name, objectId) { return context.enumerate(name).find((entry) => entry.objectId === objectId)?.value; },
write(name, objectId, value) { write(name, objectId, value) {
port(name, "write"); port(name, "write");
const previous = output.writes.findIndex((entry) => entry.port === name && entry.objectId === objectId); const previous = output.writes.findIndex((entry) => entry.port === name && entry.objectId === objectId);
@@ -53,7 +66,9 @@ export const createMigrationContext = (input) => {
}, },
edges(name) { edges(name) {
const selected = port(name, "read"); const selected = port(name, "read");
const replacement = selected.view === "new" ? output.edgeReplacements.find((entry) => input.ports.find((candidate) => candidate.name === entry.port)?.binding === selected.binding) : undefined; const replacement = selected.view === "new"
? output.edgeReplacements.find((entry) => input.ports.find((candidate) => candidate.name === entry.port)?.binding === selected.binding)
: undefined;
return structuredClone(replacement?.edges ?? selected.edges ?? []); return structuredClone(replacement?.edges ?? selected.edges ?? []);
}, },
replaceEdges(name, edges) { replaceEdges(name, edges) {
+224 -2
View File
@@ -1,5 +1,5 @@
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import type { CaminoObject, Value } from "../camino/api_pb.js"; import type { CaminoObject, CrdtValue, Value } from "../camino/api_pb.js";
import type { PackageDescriptor } from "./package_pb.js"; import type { PackageDescriptor } from "./package_pb.js";
import type { CapabilityRef, PackageExportRef } from "./refs_pb.js"; import type { CapabilityRef, PackageExportRef } from "./refs_pb.js";
import type { DerivedDependency } from "./runtime_pb.js"; import type { DerivedDependency } from "./runtime_pb.js";
@@ -113,6 +113,30 @@ export type InvokeCapabilityRequest = Message<"quixos.orch.InvokeCapabilityReque
* Use `create(InvokeCapabilityRequestSchema)` to create a new message. * Use `create(InvokeCapabilityRequestSchema)` to create a new message.
*/ */
export declare const InvokeCapabilityRequestSchema: GenMessage<InvokeCapabilityRequest>; export declare const InvokeCapabilityRequestSchema: GenMessage<InvokeCapabilityRequest>;
/**
* @generated from message quixos.orch.InvokeClassCapabilityRequest
*/
export type InvokeClassCapabilityRequest = Message<"quixos.orch.InvokeClassCapabilityRequest"> & {
/**
* @generated from field: string conformance_id = 1;
*/
conformanceId: string;
/**
* @generated from field: string operation_id = 2;
*/
operationId: string;
/**
* @generated from field: map<string, camino.Value> input = 3;
*/
input: {
[key: string]: Value;
};
};
/**
* Describes the message quixos.orch.InvokeClassCapabilityRequest.
* Use `create(InvokeClassCapabilityRequestSchema)` to create a new message.
*/
export declare const InvokeClassCapabilityRequestSchema: GenMessage<InvokeClassCapabilityRequest>;
/** /**
* @generated from message quixos.orch.InvokeCapabilityResponse * @generated from message quixos.orch.InvokeCapabilityResponse
*/ */
@@ -141,12 +165,80 @@ export type InvokeCapabilityResponse = Message<"quixos.orch.InvokeCapabilityResp
* @generated from field: repeated quixos.runtime.DerivedDependency dependencies = 6; * @generated from field: repeated quixos.runtime.DerivedDependency dependencies = 6;
*/ */
dependencies: DerivedDependency[]; dependencies: DerivedDependency[];
/**
* @generated from field: quixos.orch.FieldEditing field_editing = 7;
*/
fieldEditing?: FieldEditing | undefined;
}; };
/** /**
* Describes the message quixos.orch.InvokeCapabilityResponse. * Describes the message quixos.orch.InvokeCapabilityResponse.
* Use `create(InvokeCapabilityResponseSchema)` to create a new message. * Use `create(InvokeCapabilityResponseSchema)` to create a new message.
*/ */
export declare const InvokeCapabilityResponseSchema: GenMessage<InvokeCapabilityResponse>; export declare const InvokeCapabilityResponseSchema: GenMessage<InvokeCapabilityResponse>;
/**
* Resolved from the checked native getter/setter binding, not Value.source.
*
* @generated from message quixos.orch.FieldEditing
*/
export type FieldEditing = Message<"quixos.orch.FieldEditing"> & {
/**
* @generated from field: string getter_operation_id = 1;
*/
getterOperationId: string;
/**
* @generated from field: string setter_operation_id = 2;
*/
setterOperationId: string;
/**
* @generated from field: string document_type = 3;
*/
documentType: string;
/**
* @generated from field: string binding_digest = 4;
*/
bindingDigest: string;
};
/**
* Describes the message quixos.orch.FieldEditing.
* Use `create(FieldEditingSchema)` to create a new message.
*/
export declare const FieldEditingSchema: GenMessage<FieldEditing>;
/**
* @generated from message quixos.orch.EditCapabilityFieldRequest
*/
export type EditCapabilityFieldRequest = Message<"quixos.orch.EditCapabilityFieldRequest"> & {
/**
* The public getter; setter must belong to the same value member.
*
* @generated from field: quixos.CapabilityRef capability = 1;
*/
capability?: CapabilityRef | undefined;
/**
* @generated from field: string object_id = 2;
*/
objectId: string;
/**
* @generated from field: string setter_operation_id = 3;
*/
setterOperationId: string;
/**
* @generated from field: string binding_digest = 4;
*/
bindingDigest: string;
/**
* @generated from field: camino.CrdtValue update = 5;
*/
update?: CrdtValue | undefined;
/**
* @generated from field: string client_mutation_id = 6;
*/
clientMutationId: string;
};
/**
* Describes the message quixos.orch.EditCapabilityFieldRequest.
* Use `create(EditCapabilityFieldRequestSchema)` to create a new message.
*/
export declare const EditCapabilityFieldRequestSchema: GenMessage<EditCapabilityFieldRequest>;
/** /**
* @generated from message quixos.orch.WatchCapabilityRequest * @generated from message quixos.orch.WatchCapabilityRequest
*/ */
@@ -203,6 +295,10 @@ export type WatchCapabilityEvent = Message<"quixos.orch.WatchCapabilityEvent"> &
* @generated from field: bool initial = 7; * @generated from field: bool initial = 7;
*/ */
initial: boolean; initial: boolean;
/**
* @generated from field: quixos.orch.FieldEditing field_editing = 8;
*/
fieldEditing?: FieldEditing | undefined;
}; };
/** /**
* Describes the message quixos.orch.WatchCapabilityEvent. * Describes the message quixos.orch.WatchCapabilityEvent.
@@ -212,7 +308,14 @@ export declare const WatchCapabilityEventSchema: GenMessage<WatchCapabilityEvent
/** /**
* @generated from message quixos.orch.GetWorkspaceRequest * @generated from message quixos.orch.GetWorkspaceRequest
*/ */
export type GetWorkspaceRequest = Message<"quixos.orch.GetWorkspaceRequest"> & {}; export type GetWorkspaceRequest = Message<"quixos.orch.GetWorkspaceRequest"> & {
/**
* Revision polling must not download the entire interface graph.
*
* @generated from field: bool include_interface_contracts = 1;
*/
includeInterfaceContracts: boolean;
};
/** /**
* Describes the message quixos.orch.GetWorkspaceRequest. * Describes the message quixos.orch.GetWorkspaceRequest.
* Use `create(GetWorkspaceRequestSchema)` to create a new message. * Use `create(GetWorkspaceRequestSchema)` to create a new message.
@@ -234,12 +337,115 @@ export type GetWorkspaceResponse = Message<"quixos.orch.GetWorkspaceResponse"> &
* @generated from field: string source_root_commit = 3; * @generated from field: string source_root_commit = 3;
*/ */
sourceRootCommit: string; sourceRootCommit: string;
/**
* Checked constructors whose wire input can be empty. The create panel uses
* class factory conformances instead of this constructor inventory.
*
* @generated from field: repeated string empty_input_constructible_atom_ids = 4;
*/
emptyInputConstructibleAtomIds: string[];
/**
* @generated from field: repeated quixos.orch.CapabilityInputContract capability_inputs = 5;
*/
capabilityInputs: CapabilityInputContract[];
/**
* @generated from field: repeated quixos.orch.ConstructorInputContract constructor_inputs = 6;
*/
constructorInputs: ConstructorInputContract[];
/**
* Exact closed interface contracts used by checked presentation consumers.
*
* @generated from field: string interfaces_json = 7;
*/
interfacesJson: string;
/**
* @generated from field: repeated quixos.orch.ClassCapability class_capabilities = 8;
*/
classCapabilities: ClassCapability[];
}; };
/** /**
* Describes the message quixos.orch.GetWorkspaceResponse. * Describes the message quixos.orch.GetWorkspaceResponse.
* Use `create(GetWorkspaceResponseSchema)` to create a new message. * Use `create(GetWorkspaceResponseSchema)` to create a new message.
*/ */
export declare const GetWorkspaceResponseSchema: GenMessage<GetWorkspaceResponse>; export declare const GetWorkspaceResponseSchema: GenMessage<GetWorkspaceResponse>;
/**
* @generated from message quixos.orch.ClassCapability
*/
export type ClassCapability = Message<"quixos.orch.ClassCapability"> & {
/**
* @generated from field: string conformance_id = 1;
*/
conformanceId: string;
/**
* @generated from field: string atom_id = 2;
*/
atomId: string;
/**
* @generated from field: string interface_revision_id = 3;
*/
interfaceRevisionId: string;
/**
* @generated from field: string definition_id = 4;
*/
definitionId: string;
/**
* @generated from field: string operation_id = 5;
*/
operationId: string;
/**
* @generated from field: string input_type_json = 6;
*/
inputTypeJson: string;
/**
* @generated from field: string output_type_json = 7;
*/
outputTypeJson: string;
};
/**
* Describes the message quixos.orch.ClassCapability.
* Use `create(ClassCapabilitySchema)` to create a new message.
*/
export declare const ClassCapabilitySchema: GenMessage<ClassCapability>;
/**
* @generated from message quixos.orch.CapabilityInputContract
*/
export type CapabilityInputContract = Message<"quixos.orch.CapabilityInputContract"> & {
/**
* @generated from field: string interface_revision_id = 1;
*/
interfaceRevisionId: string;
/**
* @generated from field: string operation_id = 2;
*/
operationId: string;
/**
* @generated from field: string type_json = 3;
*/
typeJson: string;
};
/**
* Describes the message quixos.orch.CapabilityInputContract.
* Use `create(CapabilityInputContractSchema)` to create a new message.
*/
export declare const CapabilityInputContractSchema: GenMessage<CapabilityInputContract>;
/**
* @generated from message quixos.orch.ConstructorInputContract
*/
export type ConstructorInputContract = Message<"quixos.orch.ConstructorInputContract"> & {
/**
* @generated from field: string atom_id = 1;
*/
atomId: string;
/**
* @generated from field: string type_json = 2;
*/
typeJson: string;
};
/**
* Describes the message quixos.orch.ConstructorInputContract.
* Use `create(ConstructorInputContractSchema)` to create a new message.
*/
export declare const ConstructorInputContractSchema: GenMessage<ConstructorInputContract>;
/** /**
* @generated from message quixos.orch.ListActivationsRequest * @generated from message quixos.orch.ListActivationsRequest
*/ */
@@ -461,6 +667,22 @@ export declare const OrchestratorRuntime: GenService<{
input: typeof InvokeCapabilityRequestSchema; input: typeof InvokeCapabilityRequestSchema;
output: typeof InvokeCapabilityResponseSchema; output: typeof InvokeCapabilityResponseSchema;
}; };
/**
* @generated from rpc quixos.orch.OrchestratorRuntime.EditCapabilityField
*/
editCapabilityField: {
methodKind: "unary";
input: typeof EditCapabilityFieldRequestSchema;
output: typeof InvokeCapabilityResponseSchema;
};
/**
* @generated from rpc quixos.orch.OrchestratorRuntime.InvokeClassCapability
*/
invokeClassCapability: {
methodKind: "unary";
input: typeof InvokeClassCapabilityRequestSchema;
output: typeof InvokeCapabilityResponseSchema;
};
/** /**
* @generated from rpc quixos.orch.OrchestratorRuntime.WatchCapability * @generated from rpc quixos.orch.OrchestratorRuntime.WatchCapability
*/ */
+1 -1
View File
File diff suppressed because one or more lines are too long
+46 -16
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"references.d.ts","sourceRoot":"","sources":["../src/references.ts"],"names":[],"mappings":"AAGA,OAAO,CAAC,MAAM,cAAc,EAAE,OAAO,MAAM,CAAC;AAC5C,MAAM,WAAW,WAAW,CAAC,QAAQ,SAAS,MAAM,GAAG,MAAM;IAC3D,QAAQ,CAAC,CAAC,cAAc,CAAC,EAAE;QAAC,QAAQ,EAAE,CAAC,IAAI,QAAQ,GAAG,IAAI;KAAC,CAAC;IAC5D,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;CAC7C;AAQD,eAAO,MAAM,iBAAiB,UAAW,OAAO,KAAG,KAAK,IAAI,WACU,CAAC;AAEvE,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,OAAQ,MAAM,KAAG,WAG9C,CAAC;AACF,eAAO,MAAM,eAAe,UAAW,OAAO,KAAG,MAGhD,CAAC;AACF,eAAO,MAAM,mBAAmB,UAAW,OAAO,yBAA6B,IAO9E,CAAC"} {"version":3,"file":"references.d.ts","sourceRoot":"","sources":["../src/references.ts"],"names":[],"mappings":"AAGA,OAAO,CAAC,MAAM,cAAc,EAAE,OAAO,MAAM,CAAC;AAC5C,MAAM,WAAW,WAAW,CAAC,QAAQ,SAAS,MAAM,GAAG,MAAM;IAC3D,QAAQ,CAAC,CAAC,cAAc,CAAC,EAAE;QAAE,QAAQ,EAAE,CAAC,IAAI,QAAQ,GAAG,IAAI;KAAE,CAAC;IAC9D,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;CAC7C;AAmBD,eAAO,MAAM,iBAAiB,UAAW,OAAO,KAAG,KAAK,IAAI,WACU,CAAC;AAEvE,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,OAAQ,MAAM,KAAG,WAG9C,CAAC;AACF,eAAO,MAAM,eAAe,UAAW,OAAO,KAAG,MAGhD,CAAC;AACF,eAAO,MAAM,mBAAmB,UAAW,OAAO,yBAA6B,IAU9E,CAAC"}
+16 -5
View File
@@ -2,11 +2,22 @@
* the raw ID. These handles do not themselves confer authority or a lease. */ * the raw ID. These handles do not themselves confer authority or a lease. */
const identities = new WeakMap(); const identities = new WeakMap();
class Reference { class Reference {
constructor(id) { identities.set(this, id); Object.freeze(this); } constructor(id) {
equals(other) { return isObjectReference(other) && identities.get(this) === identities.get(other); } identities.set(this, id);
toJSON() { throw new Error("Object references cannot be serialized into ordinary data"); } Object.freeze(this);
toString() { throw new Error("Object references cannot be coerced to strings"); } }
[Symbol.toPrimitive]() { throw new Error("Object references cannot be coerced to scalar values"); } equals(other) {
return isObjectReference(other) && identities.get(this) === identities.get(other);
}
toJSON() {
throw new Error("Object references cannot be serialized into ordinary data");
}
toString() {
throw new Error("Object references cannot be coerced to strings");
}
[Symbol.toPrimitive]() {
throw new Error("Object references cannot be coerced to scalar values");
}
} }
export const isObjectReference = (value) => typeof value === "object" && value !== null && identities.has(value); export const isObjectReference = (value) => typeof value === "object" && value !== null && identities.has(value);
/** Internal transport boundary; intentionally not exported from the SDK entry. */ /** Internal transport boundary; intentionally not exported from the SDK entry. */
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"relationships.d.ts","sourceRoot":"","sources":["../src/relationships.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAC,sBAAsB,EAAE,iBAAiB,EAAC,MAAM,YAAY,CAAC;AAC1E,KAAK,GAAG,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AACrC,KAAK,IAAI,CAAC,CAAC,SAAS,WAAW,IAAI;IAAC,UAAU,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAA;CAAC,CAAC;AAM9L,+EAA+E;AAC/E,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,GAAG,GAAG,GAAG,QAAQ,IAAI,CAAC,CAAC,CAAC;IACvF,IAAI;IACE,GAAG,MAAM,CAAC;;;;IACV,GAAG,MAAM,CAAC,UAAU,CAAC,oBAAoB,MAAM;IAO/C,MAAM,MAAM,CAAC,oBAAoB,MAAM;CAI7C,CAAC;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,WAAW,QAAQ,IAAI,CAAC,CAAC,CAAC;IACnE,IAAI;IACE,MAAM,QAAQ,MAAM,UAAU,CAAC,oBAAoB,MAAM;IAMzD,IAAI,SAAS,MAAM,SAAS,MAAM,oBAAoB,MAAM;IAQ5D,MAAM,SAAS,MAAM,oBAAoB,MAAM;CAKrD,CAAC;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,WAAW,QAAQ,IAAI,CAAC,CAAC,CAAC;IAClE,IAAI;IACE,GAAG,SAAS,CAAC,oBAAoB,MAAM;IAKvC,MAAM,SAAS,CAAC,oBAAoB,MAAM;CAIhD,CAAC"} {"version":3,"file":"relationships.d.ts","sourceRoot":"","sources":["../src/relationships.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC5E,KAAK,GAAG,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AACrC,KAAK,IAAI,CAAC,CAAC,SAAS,WAAW,IAAI;IACjC,UAAU,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,OAAO,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC;CACxG,CAAC;AAMF,+EAA+E;AAC/E,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,GAAG,GAAG,GAAG,QAAQ,IAAI,CAAC,CAAC,CAAC;IACvF,IAAI;IACE,GAAG,MAAM,CAAC;;;;IAIV,GAAG,MAAM,CAAC,UAAU,CAAC,oBAAoB,MAAM;IAO/C,MAAM,MAAM,CAAC,oBAAoB,MAAM;CAO7C,CAAC;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,WAAW,QAAQ,IAAI,CAAC,CAAC,CAAC;IACnE,IAAI;IACE,MAAM,QAAQ,MAAM,UAAU,CAAC,oBAAoB,MAAM;IAOzD,IAAI,SAAS,MAAM,SAAS,MAAM,oBAAoB,MAAM;IAS5D,MAAM,SAAS,MAAM,oBAAoB,MAAM;CAQrD,CAAC;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,WAAW,QAAQ,IAAI,CAAC,CAAC,CAAC;IAClE,IAAI;IACE,GAAG,SAAS,CAAC,oBAAoB,MAAM;IAKvC,MAAM,SAAS,CAAC,oBAAoB,MAAM;CAOhD,CAAC"}
+4 -1
View File
@@ -7,7 +7,10 @@ const checked = async (port, revision) => {
/** Helpers never retry a failed CAS or silently overwrite concurrent edits. */ /** Helpers never retry a failed CAS or silently overwrite concurrent edits. */
export const relationshipMap = (port) => ({ export const relationshipMap = (port) => ({
read: () => port.collection(), read: () => port.collection(),
async get(key) { const snapshot = await port.collection(); return { revision: snapshot.revision, value: snapshot.entries.find((entry) => entry.key === key)?.target }; }, async get(key) {
const snapshot = await port.collection();
return { revision: snapshot.revision, value: snapshot.entries.find((entry) => entry.key === key)?.target };
},
async set(key, target, expectedRevision) { async set(key, target, expectedRevision) {
const snapshot = await checked(port, expectedRevision); const snapshot = await checked(port, expectedRevision);
const entries = snapshot.entries.filter((entry) => entry.key !== key); const entries = snapshot.entries.filter((entry) => entry.key !== key);
Generated
+7 -7
View File
@@ -74,17 +74,17 @@
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1789328055, "lastModified": 1789602549,
"narHash": "sha256-tZVGOpDnT80j3roNQXpCxw8BxNZxqVutclxr49Vhg4w=", "narHash": "sha256-/riIaX4zEud3wPtGqI5pGm+SbHCrsqVTLZOacfOdYM4=",
"ref": "refs/tags/quixos-reachability/9867bf4552c09ee71ebeefe08e652fd870894130", "ref": "refs/tags/quixos-reachability/499b65b274f07d151c61e7710929a6323b6549a7",
"rev": "9867bf4552c09ee71ebeefe08e652fd870894130", "rev": "499b65b274f07d151c61e7710929a6323b6549a7",
"revCount": 45, "revCount": 74,
"type": "git", "type": "git",
"url": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git" "url": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git"
}, },
"original": { "original": {
"ref": "refs/tags/quixos-reachability/9867bf4552c09ee71ebeefe08e652fd870894130", "ref": "refs/tags/quixos-reachability/499b65b274f07d151c61e7710929a6323b6549a7",
"rev": "9867bf4552c09ee71ebeefe08e652fd870894130", "rev": "499b65b274f07d151c61e7710929a6323b6549a7",
"type": "git", "type": "git",
"url": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git" "url": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git"
} }
+34 -8
View File
@@ -4,14 +4,20 @@
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
quixos-protocol.url = "git+https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git?ref=refs/tags/quixos-reachability/9867bf4552c09ee71ebeefe08e652fd870894130&rev=9867bf4552c09ee71ebeefe08e652fd870894130"; quixos-protocol.url = "git+https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git?ref=refs/tags/quixos-reachability/499b65b274f07d151c61e7710929a6323b6549a7&rev=499b65b274f07d151c61e7710929a6323b6549a7";
quixosNixHelpers = { quixosNixHelpers = {
url = "git+https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git?ref=refs/tags/quixos-reachability/7177130c0365f2fa58ea4877366e1c5d17db4c01&rev=7177130c0365f2fa58ea4877366e1c5d17db4c01"; url = "git+https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git?ref=refs/tags/quixos-reachability/7177130c0365f2fa58ea4877366e1c5d17db4c01&rev=7177130c0365f2fa58ea4877366e1c5d17db4c01";
flake = false; flake = false;
}; };
}; };
outputs = inputs@{ nixpkgs, flake-utils, quixosNixHelpers, ... }: outputs =
inputs@{
nixpkgs,
flake-utils,
quixosNixHelpers,
...
}:
let let
quixosHelpers = import "${quixosNixHelpers}/quixos-package-helpers.nix"; quixosHelpers = import "${quixosNixHelpers}/quixos-package-helpers.nix";
packageOutputs = quixosHelpers.mkCaminoTsYarnNixifyFlake { packageOutputs = quixosHelpers.mkCaminoTsYarnNixifyFlake {
@@ -22,18 +28,37 @@
nativeBuildInputs = { pkgs, ... }: [ nativeBuildInputs = { pkgs, ... }: [
pkgs.protobuf pkgs.protobuf
]; ];
buildEnv = { inputs, pkgs, system }: { buildEnv =
QUIXOS_PROTO_PATH = "${pkgs.protobuf}/include:${inputs.quixos-protocol.packages.${system}.default}/proto"; {
inputs,
pkgs,
system,
}:
{
QUIXOS_PROTO_PATH = "${pkgs.protobuf}/include:${
inputs.quixos-protocol.packages.${system}.default
}/proto";
}; };
devShellPackages = { pkgs, ... }: [ devShellPackages = { pkgs, ... }: [
pkgs.protobuf pkgs.protobuf
]; ];
devShellHook = { inputs, pkgs, system, ... }: '' devShellHook =
export QUIXOS_PROTO_PATH="${pkgs.protobuf}/include:${inputs.quixos-protocol.packages.${system}.default}/proto''${QUIXOS_PROTO_PATH:+:$QUIXOS_PROTO_PATH}" {
inputs,
pkgs,
system,
...
}:
''
export QUIXOS_PROTO_PATH="${pkgs.protobuf}/include:${
inputs.quixos-protocol.packages.${system}.default
}/proto''${QUIXOS_PROTO_PATH:+:$QUIXOS_PROTO_PATH}"
''; '';
}; };
in in
packageOutputs // flake-utils.lib.eachDefaultSystem (system: packageOutputs
// flake-utils.lib.eachDefaultSystem (
system:
let let
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs { inherit system; };
builtPackage = packageOutputs.packages.${system}.default; builtPackage = packageOutputs.packages.${system}.default;
@@ -45,5 +70,6 @@
${builtPackage}/libexec/-quixos-camino-package-runtime/dist ${builtPackage}/libexec/-quixos-camino-package-runtime/dist
touch "$out" touch "$out"
''; '';
}); }
);
} }
+136 -59
View File
@@ -1,7 +1,14 @@
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { ValueSchema, ObjectValueSchema, type Value } from "./camino/api_pb.js"; import { ValueSchema, ObjectValueSchema, type Value } from "./camino/api_pb.js";
import { derived, jsToProtoValue, liveValue, protoValueToJs, import {
type RuntimeContext, type RuntimeHandler, type DerivedHandler } from "./index.js"; derived,
jsToProtoValue,
liveValue,
protoValueToJs,
type RuntimeContext,
type RuntimeHandler,
type DerivedHandler,
} from "./index.js";
export type { QxObjectRef } from "./references.js"; export type { QxObjectRef } from "./references.js";
import { assertReferenceFree, referenceToWire } from "./references.js"; import { assertReferenceFree, referenceToWire } from "./references.js";
@@ -9,24 +16,11 @@ declare const watchBrand: unique symbol;
export type QxWatchHandle = string & { readonly [watchBrand]: true }; export type QxWatchHandle = string & { readonly [watchBrand]: true };
export type MessageBinding<T> = { encode(value: T): Value; decode(value: Value): T }; export type MessageBinding<T> = { encode(value: T): Value; decode(value: Value): T };
export type QxLiveValue = ReturnType<typeof liveValue>; export type QxLiveValue = ReturnType<typeof liveValue>;
const reactPropsDescriptor = "org.quixos.web-studio.ReactProps";
// Explicit temporary props exception, matching orch's RPC contract. Field shapes
// remain unchecked pending generics; ordinary messages and state do not gain it.
export const opaqueReactPropsBinding: MessageBinding<Record<string, unknown>> = {
encode(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("React props must be an object");
return jsToProtoValue(value);
},
decode(value) {
if (value.kind.case !== "objectValue") throw new Error("React props must be an object");
return protoValueToJs(value) as Record<string, unknown>;
},
};
export type BindingValue<B> = B extends MessageBinding<infer T> ? T : never; export type BindingValue<B> = B extends MessageBinding<infer T> ? T : never;
export type QxHandler<C, O> = (context: C) => O | Promise<O>; export type QxHandler<C, O> = (context: C) => O | Promise<O>;
export type QxDerived<C, O> = { kind: "derived"; get: QxHandler<C, O> }; export type QxDerived<C, O> = { kind: "derived"; get: QxHandler<C, O> };
export type QxSession<C> = {id: string; run<T>(work: (context: C) => Promise<T>): Promise<T>; close(): Promise<void>}; export type QxSession<C> = { id: string; run<T>(work: (context: C) => Promise<T>): Promise<T>; close(): Promise<void> };
export type QxContextLifecycle<C> = {signal?: AbortSignal; openSession?: () => Promise<QxSession<C>>}; export type QxContextLifecycle<C> = { signal?: AbortSignal; openSession?: () => Promise<QxSession<C>> };
export const qxDerived = <C, O>(get: QxHandler<C, O>): QxDerived<C, O> => ({ kind: "derived", get }); export const qxDerived = <C, O>(get: QxHandler<C, O>): QxDerived<C, O> => ({ kind: "derived", get });
/** Versioned binding ABI. This mirrors the language-neutral value IR. */ /** Versioned binding ABI. This mirrors the language-neutral value IR. */
@@ -44,7 +38,10 @@ export type QxPortSpec =
| { kind: "interface"; id: string; operations: Record<string, QxOperationSpec> } | { kind: "interface"; id: string; operations: Record<string, QxOperationSpec> }
| { kind: "constructor"; id: string; inputType: QxValueType }; | { kind: "constructor"; id: string; inputType: QxValueType };
export type QxHandlerSpec = { export type QxHandlerSpec = {
inputType: QxValueType; outputType: QxValueType; eventType?: QxValueType; receiver?: "none";
inputType: QxValueType;
outputType: QxValueType;
eventType?: QxValueType;
ports: Record<string, QxPortSpec>; ports: Record<string, QxPortSpec>;
}; };
export type QxMessages = Record<string, MessageBinding<any>>; export type QxMessages = Record<string, MessageBinding<any>>;
@@ -57,18 +54,22 @@ export const decodeQxValue = (type: QxValueType, value: Value | undefined, messa
if (type.kind === "record") { if (type.kind === "record") {
if (value.kind.case !== "objectValue") throw new Error("Expected QX record"); if (value.kind.case !== "objectValue") throw new Error("Expected QX record");
const fields = value.kind.value.fields; const fields = value.kind.value.fields;
if (Object.keys(fields).some((name) => !Object.hasOwn(type.fields, name))) throw new Error("Unexpected QX record field"); if (Object.keys(fields).some((name) => !Object.hasOwn(type.fields, name)))
return Object.fromEntries(Object.entries(type.fields).map(([name, field]) => [name, decodeQxValue(field, fields[name], messages)])); throw new Error("Unexpected QX record field");
return Object.fromEntries(
Object.entries(type.fields).map(([name, field]) => [name, decodeQxValue(field, fields[name], messages)]),
);
} }
if (type.kind === "optional") return value.kind.case === "nullValue" ? null : decodeQxValue(type.value, value, messages); if (type.kind === "optional")
return value.kind.case === "nullValue" ? null : decodeQxValue(type.value, value, messages);
if (type.kind === "list") { if (type.kind === "list") {
if (value.kind.case !== "listValue") throw new Error("Expected QX list"); if (value.kind.case !== "listValue") throw new Error("Expected QX list");
return value.kind.value.values.map((entry) => decodeQxValue(type.value, entry, messages)); return value.kind.value.values.map((entry) => decodeQxValue(type.value, entry, messages));
} }
if (type.kind === "message") { if (type.kind === "message") {
if (type.descriptorId !== reactPropsDescriptor) assertReferenceFree(protoValueToJs(value)); assertReferenceFree(protoValueToJs(value));
const decoded = requireMessage(messages, type.descriptorId).decode(value); const decoded = requireMessage(messages, type.descriptorId).decode(value);
if (type.descriptorId !== reactPropsDescriptor) assertReferenceFree(decoded); assertReferenceFree(decoded);
return decoded; return decoded;
} }
if (type.kind === "object-ref") { if (type.kind === "object-ref") {
@@ -98,28 +99,39 @@ export const encodeQxValue = (type: QxValueType, value: any, messages: QxMessage
if (type.kind === "builtin" && type.name === "unit") return jsToProtoValue(null); if (type.kind === "builtin" && type.name === "unit") return jsToProtoValue(null);
if (type.kind === "record") { if (type.kind === "record") {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected QX record"); if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected QX record");
if (Object.keys(value).some((name) => !Object.hasOwn(type.fields, name))) throw new Error("Unexpected QX record field"); if (Object.keys(value).some((name) => !Object.hasOwn(type.fields, name)))
const fields = Object.fromEntries(Object.entries(type.fields).map(([name, field]) => { throw new Error("Unexpected QX record field");
if (!Object.hasOwn(value, name) && field.kind !== "optional") throw new Error(`Missing QX record field ${name}`); const fields = Object.fromEntries(
Object.entries(type.fields).map(([name, field]) => {
if (!Object.hasOwn(value, name) && field.kind !== "optional")
throw new Error(`Missing QX record field ${name}`);
return [name, encodeQxValue(field, value[name] ?? (field.kind === "optional" ? null : value[name]), messages)]; return [name, encodeQxValue(field, value[name] ?? (field.kind === "optional" ? null : value[name]), messages)];
})); }),
return create(ValueSchema, {kind: {case: "objectValue", value: create(ObjectValueSchema, {fields})}}); );
return create(ValueSchema, { kind: { case: "objectValue", value: create(ObjectValueSchema, { fields }) } });
} }
if (type.kind === "optional") return value === null ? jsToProtoValue(null) : encodeQxValue(type.value, value, messages); if (type.kind === "optional")
if (type.kind === "list") return jsToProtoValue(value.map((entry: unknown) => liveValue(encodeQxValue(type.value, entry, messages)))); return value === null ? jsToProtoValue(null) : encodeQxValue(type.value, value, messages);
if (type.kind === "object-ref") { referenceToWire(value); return jsToProtoValue(value); } if (type.kind === "list")
if (type.kind !== "message" || type.descriptorId !== reactPropsDescriptor) assertReferenceFree(value); return jsToProtoValue(value.map((entry: unknown) => liveValue(encodeQxValue(type.value, entry, messages))));
if (type.kind === "object-ref") {
referenceToWire(value);
return jsToProtoValue(value);
}
assertReferenceFree(value);
if (type.kind === "message") { if (type.kind === "message") {
const encoded = requireMessage(messages, type.descriptorId).encode(value); const encoded = requireMessage(messages, type.descriptorId).encode(value);
if (type.descriptorId !== reactPropsDescriptor) assertReferenceFree(protoValueToJs(encoded)); assertReferenceFree(protoValueToJs(encoded));
return encoded; return encoded;
} }
return jsToProtoValue(value); return jsToProtoValue(value);
}; };
const inputValue = (context: RuntimeContext, type: QxValueType) => { const inputValue = (context: RuntimeContext, type: QxValueType) => {
if (type.kind === "message" || type.kind === "record") return create(ValueSchema, { kind: { case: "objectValue", if (type.kind === "message" || type.kind === "record")
value: create(ObjectValueSchema, { fields: context.inputProto }) } }); return create(ValueSchema, {
kind: { case: "objectValue", value: create(ObjectValueSchema, { fields: context.inputProto }) },
});
return context.inputProto.value; return context.inputProto.value;
}; };
const inputFields = (type: QxValueType, value: unknown, messages: QxMessages): Record<string, unknown> => { const inputFields = (type: QxValueType, value: unknown, messages: QxMessages): Record<string, unknown> => {
@@ -134,45 +146,110 @@ const inputFields = (type: QxValueType, value: unknown, messages: QxMessages): R
/** The sole unchecked cast connects generated contracts to the dynamic RPC runtime. */ /** The sole unchecked cast connects generated contracts to the dynamic RPC runtime. */
export const bindQxHandler = <C, O>( export const bindQxHandler = <C, O>(
spec: QxHandlerSpec, handler: QxHandler<C, O> | QxDerived<C, O>, messages: QxMessages, spec: QxHandlerSpec,
handler: QxHandler<C, O> | QxDerived<C, O>,
messages: QxMessages,
): RuntimeHandler | DerivedHandler => { ): RuntimeHandler | DerivedHandler => {
const bindContext = (raw: RuntimeContext): C => { const bindContext = (raw: RuntimeContext): C => {
const ports = Object.fromEntries(Object.entries(spec.ports).map(([name, port]) => { const ports = Object.fromEntries(
Object.entries(spec.ports).map(([name, port]) => {
switch (port.kind) { switch (port.kind) {
case "state": { case "state": {
const state = raw.state(port.id); const state = raw.state(port.id);
return [name, { return [
...(port.primitives.includes("read") ? { get: async () => decodeQxValue(port.valueType, (await state.live()).$quixosValue, messages), live: () => state.live() } : {}), name,
...(port.primitives.includes("write") ? { set: async (value: unknown) => state.set(liveValue(encodeQxValue(port.valueType, value, messages))) } : {}), {
}]; ...(port.primitives.includes("read")
? {
get: async () => decodeQxValue(port.valueType, (await state.live()).$quixosValue, messages),
live: () => state.live(),
}
: {}),
...(port.primitives.includes("write")
? {
set: async (value: unknown) =>
state.set(liveValue(encodeQxValue(port.valueType, value, messages))),
}
: {}),
},
];
} }
case "edge": { case "edge": {
const edge = raw.edge(port.id); const edge = raw.edge(port.id);
return [name, {...Object.fromEntries(port.primitives.map((primitive) => [primitive, edge[primitive as "resolve" | "connect" | "disconnect"]])), return [
...(port.primitives.includes("resolve") ? {collection: edge.collection} : {}), name,
...(port.primitives.includes("resolve") && port.primitives.includes("connect") && port.primitives.includes("disconnect") ? {replace: edge.replace} : {})}]; {
...Object.fromEntries(
port.primitives.map((primitive) => [
primitive,
edge[primitive as "resolve" | "connect" | "disconnect"],
]),
),
...(port.primitives.includes("resolve") ? { collection: edge.collection } : {}),
...(port.primitives.includes("resolve") &&
port.primitives.includes("connect") &&
port.primitives.includes("disconnect")
? { replace: edge.replace }
: {}),
},
];
} }
case "interface": { case "interface": {
const target = raw.interface(port.id); const target = raw.interface(port.id);
return [name, {objectId: target.objectId, return [
live: Object.fromEntries(Object.entries(port.operations).map(([name, operation]) => [name, name,
{
objectId: target.objectId,
live: Object.fromEntries(
Object.entries(port.operations).map(([name, operation]) => [
name,
(input: unknown) => target.live(operation.id, inputFields(operation.inputType, input, messages)), (input: unknown) => target.live(operation.id, inputFields(operation.inputType, input, messages)),
])), ]),
...Object.fromEntries(Object.entries(port.operations).map(([name, operation]) => [name, ),
async (input: unknown) => decodeQxValue(operation.outputType, ...Object.fromEntries(
(await target.live(operation.id, inputFields(operation.inputType, input, messages))).$quixosValue, messages), Object.entries(port.operations).map(([name, operation]) => [
]))}]; name,
async (input: unknown) =>
decodeQxValue(
operation.outputType,
(await target.live(operation.id, inputFields(operation.inputType, input, messages)))
.$quixosValue,
messages,
),
]),
),
},
];
} }
case "constructor": return [name, { construct: (input: unknown) => raw.constructor(port.id).construct(inputFields(port.inputType, input, messages)) }]; case "constructor":
return [
name,
{
construct: (input: unknown) =>
raw.constructor(port.id).construct(inputFields(port.inputType, input, messages)),
},
];
} }
})); }),
return { objectId: raw.objectId, signal: raw.signal, );
...(raw.openSession ? {openSession: async () => { return {
...(spec.receiver === "none" ? {} : { objectId: raw.objectId }),
signal: raw.signal,
...(spec.receiver !== "none" && raw.openSession
? {
openSession: async () => {
const session = await raw.openSession!(); const session = await raw.openSession!();
return {id: session.id, close: () => session.close(), return {
run: <T>(work: (context: C) => Promise<T>) => session.run((next) => work(bindContext(next)))}; id: session.id,
}} : {}), close: () => session.close(),
input: decodeQxValue(spec.inputType, inputValue(raw, spec.inputType), messages), ports } as C; run: <T>(work: (context: C) => Promise<T>) => session.run((next) => work(bindContext(next))),
};
},
}
: {}),
input: decodeQxValue(spec.inputType, inputValue(raw, spec.inputType), messages),
ports,
} as C;
}; };
const execute = async (raw: RuntimeContext) => { const execute = async (raw: RuntimeContext) => {
const context = bindContext(raw); const context = bindContext(raw);
+249 -83
View File
@@ -1,12 +1,26 @@
import http from "node:http"; import http from "node:http";
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { createInvocationRegistry } from "./invocations.js"; import { createInvocationRegistry } from "./invocations.js";
import { isObjectReference, referenceFromWire, referenceToWire, assertReferenceFree, type QxObjectRef } from "./references.js"; import {
isObjectReference,
referenceFromWire,
referenceToWire,
assertReferenceFree,
type QxObjectRef,
} from "./references.js";
export * from "./bindings.js"; export * from "./bindings.js";
export {relationshipMap, relationshipList, relationshipSet} from "./relationships.js"; export { relationshipMap, relationshipList, relationshipSet } from "./relationships.js";
import { AsyncLocalStorage } from "node:async_hooks"; import { AsyncLocalStorage } from "node:async_hooks";
import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto"; import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
export {createMigrationContext, migrationObjectId, serveMigration, type MigrationContext, type MigrationInput, type MigrationOutput, type MigrationEdge} from "./migration.js"; export {
createMigrationContext,
migrationObjectId,
serveMigration,
type MigrationContext,
type MigrationInput,
type MigrationOutput,
type MigrationEdge,
} from "./migration.js";
import { create, equals } from "@bufbuild/protobuf"; import { create, equals } from "@bufbuild/protobuf";
import { Code, ConnectError, createClient, type Client, type ConnectRouter } from "@connectrpc/connect"; import { Code, ConnectError, createClient, type Client, type ConnectRouter } from "@connectrpc/connect";
import { connectNodeAdapter, createConnectTransport } from "@connectrpc/connect-node"; import { connectNodeAdapter, createConnectTransport } from "@connectrpc/connect-node";
@@ -63,14 +77,22 @@ const base64ToBytes = (value: string) => Buffer.from(value, "base64");
const isRecord = (value: unknown): value is Record<string, unknown> => const isRecord = (value: unknown): value is Record<string, unknown> =>
Boolean(value) && typeof value === "object" && !Array.isArray(value); Boolean(value) && typeof value === "object" && !Array.isArray(value);
const isWrappedValue = (value: unknown): value is { $quixosValue: Value } => const isWrappedValue = (value: unknown): value is { $quixosValue: Value } =>
isRecord(value) && "$quixosValue" in value && isRecord(value) &&
isRecord(value.$quixosValue) && value.$quixosValue.$typeName === "camino.Value"; "$quixosValue" in value &&
isRecord(value.$quixosValue) &&
value.$quixosValue.$typeName === "camino.Value";
export const objectRef = (reference: QxObjectRef) => { referenceToWire(reference); return reference; }; export const objectRef = (reference: QxObjectRef) => {
referenceToWire(reference);
return reference;
};
export const liveValue = (value: Value) => ({ $quixosValue: value }); export const liveValue = (value: Value) => ({ $quixosValue: value });
export const jsToProtoValue = (value: unknown): Value => { export const jsToProtoValue = (value: unknown): Value => {
if (isObjectReference(value)) return create(ValueSchema, {kind: {case: "refValue", value: create(RefValueSchema, {objectId: referenceToWire(value)})}}); if (isObjectReference(value))
return create(ValueSchema, {
kind: { case: "refValue", value: create(RefValueSchema, { objectId: referenceToWire(value) }) },
});
if (isWrappedValue(value)) return value.$quixosValue; if (isWrappedValue(value)) return value.$quixosValue;
if (value === null || value === undefined) { if (value === null || value === undefined) {
return create(ValueSchema, { kind: { case: "nullValue", value: create(NullValueSchema, {}) } }); return create(ValueSchema, { kind: { case: "nullValue", value: create(NullValueSchema, {}) } });
@@ -86,39 +108,51 @@ export const jsToProtoValue = (value: unknown): Value => {
}); });
} }
if (isRecord(value) && "$quixosRef" in value) throw new Error("Raw ID wrappers are not object references"); if (isRecord(value) && "$quixosRef" in value) throw new Error("Raw ID wrappers are not object references");
if (isRecord(value) && typeof value.$quixosCrdtType === "string" && if (isRecord(value) && typeof value.$quixosCrdtType === "string" && typeof value.$quixosCrdtPayload === "string") {
typeof value.$quixosCrdtPayload === "string") {
return create(ValueSchema, { return create(ValueSchema, {
kind: { case: "crdtValue", value: create(CrdtValueSchema, { kind: {
case: "crdtValue",
value: create(CrdtValueSchema, {
type: value.$quixosCrdtType, type: value.$quixosCrdtType,
encoding: typeof value.$quixosCrdtEncoding === "string" ? value.$quixosCrdtEncoding : "base64", encoding: typeof value.$quixosCrdtEncoding === "string" ? value.$quixosCrdtEncoding : "base64",
payload: base64ToBytes(value.$quixosCrdtPayload), payload: base64ToBytes(value.$quixosCrdtPayload),
}) }, }),
},
}); });
} }
if (!isRecord(value)) throw new Error(`Unsupported runtime value ${typeof value}`); if (!isRecord(value)) throw new Error(`Unsupported runtime value ${typeof value}`);
return create(ValueSchema, { return create(ValueSchema, {
kind: { case: "objectValue", value: create(ObjectValueSchema, { kind: {
case: "objectValue",
value: create(ObjectValueSchema, {
fields: Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, jsToProtoValue(entry)])), fields: Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, jsToProtoValue(entry)])),
}) }, }),
},
}); });
}; };
export const protoValueToJs = (value: Value | undefined): unknown => { export const protoValueToJs = (value: Value | undefined): unknown => {
switch (value?.kind.case) { switch (value?.kind.case) {
case "nullValue": case "nullValue":
case undefined: return null; case undefined:
return null;
case "boolValue": case "boolValue":
case "numberValue": case "numberValue":
case "stringValue": case "stringValue":
case "integerValue": return value.kind.value; case "integerValue":
case "bytesValue": return bytesToBase64(value.kind.value); return value.kind.value;
case "refValue": return referenceFromWire(value.kind.value.objectId); case "bytesValue":
case "listValue": return value.kind.value.values.map(protoValueToJs); return bytesToBase64(value.kind.value);
case "objectValue": return Object.fromEntries( case "refValue":
return referenceFromWire(value.kind.value.objectId);
case "listValue":
return value.kind.value.values.map(protoValueToJs);
case "objectValue":
return Object.fromEntries(
Object.entries(value.kind.value.fields).map(([key, entry]) => [key, protoValueToJs(entry)]), Object.entries(value.kind.value.fields).map(([key, entry]) => [key, protoValueToJs(entry)]),
); );
case "crdtValue": return { case "crdtValue":
return {
$quixosCrdtType: value.kind.value.type, $quixosCrdtType: value.kind.value.type,
$quixosCrdtEncoding: value.kind.value.encoding, $quixosCrdtEncoding: value.kind.value.encoding,
$quixosCrdtPayload: bytesToBase64(value.kind.value.payload), $quixosCrdtPayload: bytesToBase64(value.kind.value.payload),
@@ -144,8 +178,15 @@ export type EdgePort = {
collection(): Promise<RelationshipCollection>; collection(): Promise<RelationshipCollection>;
replace(entries: RelationshipEntry[], expectedRevision: bigint): Promise<RelationshipCollection>; replace(entries: RelationshipEntry[], expectedRevision: bigint): Promise<RelationshipCollection>;
}; };
export type RelationshipEntry<T extends QxObjectRef = QxObjectRef> = {edgeId?: string; target: T; key?: string | boolean | bigint}; export type RelationshipEntry<T extends QxObjectRef = QxObjectRef> = {
export type RelationshipCollection<T extends QxObjectRef = QxObjectRef> = {revision: bigint; entries: RelationshipEntry<T>[]}; edgeId?: string;
target: T;
key?: string | boolean | bigint;
};
export type RelationshipCollection<T extends QxObjectRef = QxObjectRef> = {
revision: bigint;
entries: RelationshipEntry<T>[];
};
export type InterfacePort = { export type InterfacePort = {
objectId: QxObjectRef; objectId: QxObjectRef;
interfaceRevisionId: string; interfaceRevisionId: string;
@@ -180,13 +221,17 @@ export type RuntimeSession = {
}; };
export class RuntimeAuthorityError extends Error { export class RuntimeAuthorityError extends Error {
readonly retryable: boolean; readonly retryable: boolean;
constructor(message: string) { super(message); this.name = "RuntimeAuthorityError"; this.retryable = /WORKSPACE_FENCED|STALE_EPOCH/.test(message); } constructor(message: string) {
super(message);
this.name = "RuntimeAuthorityError";
this.retryable = /WORKSPACE_FENCED|STALE_EPOCH/.test(message);
}
} }
const targetForEdge = ( const targetForEdge = (
edge: { firstObjectId: string; secondObjectId: string; firstProjectionId: string }, edge: { firstObjectId: string; secondObjectId: string; firstProjectionId: string },
projectionId: string, projectionId: string,
) => edge.firstProjectionId === projectionId ? edge.secondObjectId : edge.firstObjectId; ) => (edge.firstProjectionId === projectionId ? edge.secondObjectId : edge.firstObjectId);
export const createRuntimeContext = ( export const createRuntimeContext = (
camino: CaminoClient, camino: CaminoClient,
@@ -222,25 +267,63 @@ export const createRuntimeContext = (
case "edge": { case "edge": {
const { edgeTypeId, projectionId } = dependency.binding.value; const { edgeTypeId, projectionId } = dependency.binding.value;
const dependencyObjectId = dependency.objectId || request.objectId; const dependencyObjectId = dependency.objectId || request.objectId;
const collectionResult = (response: {revision: bigint; entries: {edgeId: string; targetObjectId: string; key?: Value}[]}): RelationshipCollection => ({revision: response.revision, const collectionResult = (response: {
entries: response.entries.map((entry) => ({edgeId: entry.edgeId, target: referenceFromWire(entry.targetObjectId), revision: bigint;
...(entry.key ? {key: entry.key.kind.case === "integerValue" ? BigInt(entry.key.kind.value) : protoValueToJs(entry.key) as string | boolean} : {})}))}); entries: { edgeId: string; targetObjectId: string; key?: Value }[];
}): RelationshipCollection => ({
revision: response.revision,
entries: response.entries.map((entry) => ({
edgeId: entry.edgeId,
target: referenceFromWire(entry.targetObjectId),
...(entry.key
? {
key:
entry.key.kind.case === "integerValue"
? BigInt(entry.key.kind.value)
: (protoValueToJs(entry.key) as string | boolean),
}
: {}),
})),
});
const edge: EdgePort = { const edge: EdgePort = {
edgeTypeId, edgeTypeId,
projectionId, projectionId,
async collection() { async collection() {
await recordDependency({kind: "edge", objectId: dependencyObjectId, attachmentId: edgeTypeId, projectionId}); await recordDependency({
return collectionResult(await camino.readCollection({objectId: dependencyObjectId, edgeTypeId, projectionId})); kind: "edge",
objectId: dependencyObjectId,
attachmentId: edgeTypeId,
projectionId,
});
return collectionResult(
await camino.readCollection({ objectId: dependencyObjectId, edgeTypeId, projectionId }),
);
}, },
async replace(entries, expectedRevision) { async replace(entries, expectedRevision) {
return collectionResult(await camino.replaceCollection({objectId: dependencyObjectId, edgeTypeId, projectionId, expectedRevision, return collectionResult(
await camino.replaceCollection({
objectId: dependencyObjectId,
edgeTypeId,
projectionId,
expectedRevision,
entries: entries.map((entry) => { entries: entries.map((entry) => {
assertReferenceFree(entry.key); assertReferenceFree(entry.key);
return {edgeId: entry.edgeId ?? "", targetObjectId: referenceToWire(entry.target), key: entry.key === undefined ? undefined : jsToProtoValue(entry.key)}; return {
})})); edgeId: entry.edgeId ?? "",
targetObjectId: referenceToWire(entry.target),
key: entry.key === undefined ? undefined : jsToProtoValue(entry.key),
};
}),
}),
);
}, },
async resolve() { async resolve() {
await recordDependency({ kind: "edge", objectId: dependencyObjectId, attachmentId: edgeTypeId, projectionId }); await recordDependency({
kind: "edge",
objectId: dependencyObjectId,
attachmentId: edgeTypeId,
projectionId,
});
const result = await camino.resolveEdge({ objectId: dependencyObjectId, edgeTypeId, projectionId }); const result = await camino.resolveEdge({ objectId: dependencyObjectId, edgeTypeId, projectionId });
return result.edges.map((entry) => referenceFromWire(targetForEdge(entry, projectionId))); return result.edges.map((entry) => referenceFromWire(targetForEdge(entry, projectionId)));
}, },
@@ -252,7 +335,8 @@ export const createRuntimeContext = (
const targetObjectId = referenceToWire(target); const targetObjectId = referenceToWire(target);
const result = await camino.resolveEdge({ objectId: dependencyObjectId, edgeTypeId, projectionId }); const result = await camino.resolveEdge({ objectId: dependencyObjectId, edgeTypeId, projectionId });
for (const entry of result.edges) { for (const entry of result.edges) {
if (targetForEdge(entry, projectionId) === targetObjectId) await camino.disconnectEdge({ edgeId: entry.id }); if (targetForEdge(entry, projectionId) === targetObjectId)
await camino.disconnectEdge({ edgeId: entry.id });
} }
}, },
}; };
@@ -325,7 +409,9 @@ export const createRuntimeContext = (
return port as T; return port as T;
}; };
return { return {
objectId: referenceFromWire(request.objectId), get objectId() {
return referenceFromWire(request.objectId);
},
input: protoFieldsToJs(request.input), input: protoFieldsToJs(request.input),
inputProto: request.input, inputProto: request.input,
ports, ports,
@@ -348,20 +434,21 @@ const evaluate = async (
observe?: (dependency: RuntimeDependency) => Promise<void>, observe?: (dependency: RuntimeDependency) => Promise<void>,
) => { ) => {
const dependencies = new Map<string, RuntimeDependency>(); const dependencies = new Map<string, RuntimeDependency>();
const result = await dependencyScope.run( const result = await dependencyScope.run({ dependencies, observe }, () =>
{ dependencies, observe }, isDerived(handler) ? handler.get(context) : handler(context),
() => isDerived(handler) ? handler.get(context) : handler(context),
); );
return { value: jsToProtoValue(result), dependencies: [...dependencies.values()] }; return { value: jsToProtoValue(result), dependencies: [...dependencies.values()] };
}; };
const protoDependencies = (dependencies: RuntimeDependency[]) => dependencies.map((entry) => const protoDependencies = (dependencies: RuntimeDependency[]) =>
dependencies.map((entry) =>
create(DerivedDependencySchema, { create(DerivedDependencySchema, {
kind: entry.kind, kind: entry.kind,
objectId: entry.objectId, objectId: entry.objectId,
attachmentId: entry.attachmentId, attachmentId: entry.attachmentId,
projectionId: entry.kind === "edge" ? entry.projectionId : "", projectionId: entry.kind === "edge" ? entry.projectionId : "",
})); }),
);
export const createPackageRuntimeRoutes = (config: { export const createPackageRuntimeRoutes = (config: {
packageRevisionId: string; packageRevisionId: string;
@@ -371,27 +458,38 @@ export const createPackageRuntimeRoutes = (config: {
}) => { }) => {
const invocations = createInvocationRegistry(); const invocations = createInvocationRegistry();
const headers: Record<string, string> = {}; const headers: Record<string, string> = {};
const processToken = process.env.CAMINO_RUNTIME_AUTH_TOKEN ?? (process.env.CAMINO_RUNTIME_AUTH_TOKEN_FILE const processToken =
? readFileSync(process.env.CAMINO_RUNTIME_AUTH_TOKEN_FILE, "utf8").trim() : ""); process.env.CAMINO_RUNTIME_AUTH_TOKEN ??
(process.env.CAMINO_RUNTIME_AUTH_TOKEN_FILE
? readFileSync(process.env.CAMINO_RUNTIME_AUTH_TOKEN_FILE, "utf8").trim()
: "");
if (processToken) { if (processToken) {
headers["x-camino-runtime-token"] = processToken; headers["x-camino-runtime-token"] = processToken;
} else if (process.env.CAMINO_RUNTIME_AUTH_REQUIRED === "1") { } else if (process.env.CAMINO_RUNTIME_AUTH_REQUIRED === "1") {
throw new Error("CAMINO_RUNTIME_AUTH_TOKEN is required"); throw new Error("CAMINO_RUNTIME_AUTH_TOKEN is required");
} }
const camino = createClient(CaminoService, createConnectTransport({ const camino = createClient(
CaminoService,
createConnectTransport({
baseUrl: config.caminoUrl ?? process.env.CAMINO_URL ?? "http://127.0.0.1:7310", baseUrl: config.caminoUrl ?? process.env.CAMINO_URL ?? "http://127.0.0.1:7310",
httpVersion: "1.1", httpVersion: "1.1",
interceptors: headers["x-camino-runtime-token"] ? [ interceptors: headers["x-camino-runtime-token"]
? [
(next) => async (request) => { (next) => async (request) => {
request.header.set("x-camino-runtime-token", headers["x-camino-runtime-token"]!); request.header.set("x-camino-runtime-token", headers["x-camino-runtime-token"]!);
return await next(request); return await next(request);
}, },
] : [], ]
})); : [],
const orch = createClient(OrchestratorRuntime, createConnectTransport({ }),
);
const orch = createClient(
OrchestratorRuntime,
createConnectTransport({
baseUrl: config.orchUrl ?? process.env.QUIXOS_ORCH_URL ?? "http://127.0.0.1:7311", baseUrl: config.orchUrl ?? process.env.QUIXOS_ORCH_URL ?? "http://127.0.0.1:7311",
httpVersion: "1.1", httpVersion: "1.1",
})); }),
);
const authenticateInstance = (header: Headers) => { const authenticateInstance = (header: Headers) => {
if (!process.env.QUIXOS_RUNTIME_INSTANCE_ID) return; // standalone development ABI if (!process.env.QUIXOS_RUNTIME_INSTANCE_ID) return; // standalone development ABI
@@ -403,35 +501,67 @@ export const createPackageRuntimeRoutes = (config: {
}; };
const clientsFor = (request: { context?: { grant: string; instanceId: string; workspaceEpoch: string } }) => { const clientsFor = (request: { context?: { grant: string; instanceId: string; workspaceEpoch: string } }) => {
const context = request.context; const context = request.context;
if (process.env.QUIXOS_RUNTIME_INSTANCE_ID && (!context?.grant || context.instanceId !== process.env.QUIXOS_RUNTIME_INSTANCE_ID || !context.workspaceEpoch)) { if (
process.env.QUIXOS_RUNTIME_INSTANCE_ID &&
(!context?.grant || context.instanceId !== process.env.QUIXOS_RUNTIME_INSTANCE_ID || !context.workspaceEpoch)
) {
throw new ConnectError("Managed invocation requires an exact instance and epoch grant", Code.Unauthenticated); throw new ConnectError("Managed invocation requires an exact instance and epoch grant", Code.Unauthenticated);
} }
if (!context?.grant) return { camino, orch }; if (!context?.grant) return { camino, orch };
const transport = (url: string) => createConnectTransport({ baseUrl: url, httpVersion: "1.1", interceptors: [(next) => async (call) => { const transport = (url: string) =>
createConnectTransport({
baseUrl: url,
httpVersion: "1.1",
interceptors: [
(next) => async (call) => {
call.header.set("x-quixos-invocation-grant", context.grant); call.header.set("x-quixos-invocation-grant", context.grant);
call.header.set("x-camino-runtime-token", processToken); call.header.set("x-camino-runtime-token", processToken);
return next(call); return next(call);
}] }); },
],
});
return { return {
camino: createClient(CaminoService, transport(config.caminoUrl ?? process.env.CAMINO_URL ?? "http://127.0.0.1:7310")), camino: createClient(
orch: createClient(OrchestratorRuntime, transport(config.orchUrl ?? process.env.QUIXOS_ORCH_URL ?? "http://127.0.0.1:7311")), CaminoService,
transport(config.caminoUrl ?? process.env.CAMINO_URL ?? "http://127.0.0.1:7310"),
),
orch: createClient(
OrchestratorRuntime,
transport(config.orchUrl ?? process.env.QUIXOS_ORCH_URL ?? "http://127.0.0.1:7311"),
),
}; };
}; };
const runtimeControl = async <T>(operation: string, input: unknown): Promise<T> => { const runtimeControl = async <T>(operation: string, input: unknown): Promise<T> => {
const response = await fetch(`${config.caminoUrl ?? process.env.CAMINO_URL ?? "http://127.0.0.1:7310"}/__runtime/${operation}`, { const response = await fetch(
method: "POST", headers: { "content-type": "application/json", "x-camino-runtime-token": processToken }, body: JSON.stringify(input), signal: AbortSignal.timeout(10_000), `${config.caminoUrl ?? process.env.CAMINO_URL ?? "http://127.0.0.1:7310"}/__runtime/${operation}`,
}); {
const value = await response.json() as T & {error?: string}; method: "POST",
headers: { "content-type": "application/json", "x-camino-runtime-token": processToken },
body: JSON.stringify(input),
signal: AbortSignal.timeout(10_000),
},
);
const value = (await response.json()) as T & { error?: string };
if (!response.ok) throw new RuntimeAuthorityError(value.error ?? "Runtime authority request failed"); if (!response.ok) throw new RuntimeAuthorityError(value.error ?? "Runtime authority request failed");
return value; return value;
}; };
const attachSessions = (runtimeContext: RuntimeContext, request: RuntimeRequest & {context?: {grant: string; instanceId: string; workspaceEpoch: string; ownerConformanceId?: string}}) => { const attachSessions = (
runtimeContext: RuntimeContext,
request: RuntimeRequest & {
context?: { grant: string; instanceId: string; workspaceEpoch: string; ownerConformanceId?: string };
},
) => {
if (!request.context?.grant || !request.context.ownerConformanceId) return; if (!request.context?.grant || !request.context.ownerConformanceId) return;
runtimeContext.openSession = async () => { runtimeContext.openSession = async () => {
const ownerId = request.context!.ownerConformanceId!; const ownerId = request.context!.ownerConformanceId!;
const registration = { grant: request.context!.grant, objectId: request.objectId, ownerId, const registration = {
sessionId: `session:${randomBytes(16).toString("hex")}`, token: randomBytes(32).toString("base64url") }; grant: request.context!.grant,
const register = () => runtimeControl<{sessionId: string; token: string}>("register-session", registration); objectId: request.objectId,
ownerId,
sessionId: `session:${randomBytes(16).toString("hex")}`,
token: randomBytes(32).toString("base64url"),
};
const register = () => runtimeControl<{ sessionId: string; token: string }>("register-session", registration);
const registered = await register().catch((error) => { const registered = await register().catch((error) => {
// Retry a transport/lost-response failure with exactly the same identity. // Retry a transport/lost-response failure with exactly the same identity.
// Admission/authority errors are definitive and must not be retried here. // Admission/authority errors are definitive and must not be retried here.
@@ -445,33 +575,59 @@ export const createPackageRuntimeRoutes = (config: {
if (closed) throw new RuntimeAuthorityError("SESSION_CLOSED"); if (closed) throw new RuntimeAuthorityError("SESSION_CLOSED");
// Acquisition happens before user code. A fence failure can be retried // Acquisition happens before user code. A fence failure can be retried
// by the caller without replaying a side-effecting callback. // by the caller without replaying a side-effecting callback.
const grant = await runtimeControl<{grant: string; epoch: string; instanceId: string; invocationId: string; bindingDigest: string}>("acquire-session", registered); const grant = await runtimeControl<{
grant: string;
epoch: string;
instanceId: string;
invocationId: string;
bindingDigest: string;
}>("acquire-session", registered);
const execution = invocations.begin(grant.invocationId); const execution = invocations.begin(grant.invocationId);
const sessionRequest = { ...request, context: { grant: grant.grant, instanceId: grant.instanceId, workspaceEpoch: grant.epoch } }; const sessionRequest = {
...request,
context: { grant: grant.grant, instanceId: grant.instanceId, workspaceEpoch: grant.epoch },
};
const clients = clientsFor(sessionRequest); const clients = clientsFor(sessionRequest);
const context = createRuntimeContext(clients.camino, clients.orch, sessionRequest); const context = createRuntimeContext(clients.camino, clients.orch, sessionRequest);
context.signal = execution.signal; context.signal = execution.signal;
try { return await work(context); } try {
finally { return await work(context);
} finally {
execution.finish(); execution.finish();
await runtimeControl("complete-invocation", { invocationId: grant.invocationId }).catch((error) => console.error("Session completion will be reconciled by the host", error)); await runtimeControl("complete-invocation", { invocationId: grant.invocationId }).catch((error) =>
console.error("Session completion will be reconciled by the host", error),
);
} }
}, },
async close() { await runtimeControl("close-session", registered); closed = true; }, async close() {
await runtimeControl("close-session", registered);
closed = true;
},
}; };
}; };
}; };
return (router: ConnectRouter) => router.service(PackageRuntime, { return (router: ConnectRouter) =>
handshake: (request) => create(HandshakeResponseSchema, { router.service(PackageRuntime, {
handshake: (request) =>
create(HandshakeResponseSchema, {
packageRevisionId: config.packageRevisionId, packageRevisionId: config.packageRevisionId,
runtimeProtocolVersion: "quixos-capabilities-v1", runtimeProtocolVersion: "quixos-capabilities-v1",
exportIds: Object.keys(config.exports), exportIds: Object.keys(config.exports),
capabilities: ["invocation-completion-v1", "instance-authentication-v1", "epoch-grants-v1"], capabilities: ["invocation-completion-v1", "instance-authentication-v1", "epoch-grants-v1"],
instanceId: process.env.QUIXOS_RUNTIME_INSTANCE_ID ?? "", instanceId: process.env.QUIXOS_RUNTIME_INSTANCE_ID ?? "",
authenticationProof: request.nonce && processToken ? createHmac("sha256", processToken) authenticationProof:
.update(JSON.stringify([request.nonce, process.env.QUIXOS_RUNTIME_INSTANCE_ID ?? "", config.packageRevisionId])) request.nonce && processToken
.digest("hex") : "", ? createHmac("sha256", processToken)
.update(
JSON.stringify([
request.nonce,
process.env.QUIXOS_RUNTIME_INSTANCE_ID ?? "",
config.packageRevisionId,
]),
)
.digest("hex")
: "",
}), }),
getInvocationStatus: (request, context) => { getInvocationStatus: (request, context) => {
authenticateInstance(context.requestHeader); authenticateInstance(context.requestHeader);
@@ -487,7 +643,9 @@ export const createPackageRuntimeRoutes = (config: {
const exportId = request.export?.exportId; const exportId = request.export?.exportId;
const handler = exportId ? config.exports[exportId] : undefined; const handler = exportId ? config.exports[exportId] : undefined;
if (!handler) throw new ConnectError(`Unknown export ${exportId ?? ""}`, Code.NotFound); if (!handler) throw new ConnectError(`Unknown export ${exportId ?? ""}`, Code.NotFound);
const execution = invocations.begin(request.invocationId || (process.env.QUIXOS_RUNTIME_INSTANCE_ID ? "" : randomUUID())); const execution = invocations.begin(
request.invocationId || (process.env.QUIXOS_RUNTIME_INSTANCE_ID ? "" : randomUUID()),
);
try { try {
const runtimeContext = createRuntimeContext(camino, orch, request); const runtimeContext = createRuntimeContext(camino, orch, request);
attachSessions(runtimeContext, request); attachSessions(runtimeContext, request);
@@ -515,7 +673,9 @@ export const createPackageRuntimeRoutes = (config: {
if (!handler || !isDerived(handler)) { if (!handler || !isDerived(handler)) {
throw new ConnectError(`Export ${exportId ?? ""} is not derived`, Code.FailedPrecondition); throw new ConnectError(`Export ${exportId ?? ""} is not derived`, Code.FailedPrecondition);
} }
const execution = invocations.begin(request.invocationId || (process.env.QUIXOS_RUNTIME_INSTANCE_ID ? "" : randomUUID())); const execution = invocations.begin(
request.invocationId || (process.env.QUIXOS_RUNTIME_INSTANCE_ID ? "" : randomUUID()),
);
const signal = AbortSignal.any([context.signal, execution.signal]); const signal = AbortSignal.any([context.signal, execution.signal]);
try { try {
const watchId = `watch:${randomUUID()}`; const watchId = `watch:${randomUUID()}`;
@@ -540,16 +700,23 @@ export const createPackageRuntimeRoutes = (config: {
if (pending) return await pending; if (pending) return await pending;
const establish = (async () => { const establish = (async () => {
const controller = new AbortController(); const controller = new AbortController();
const stream = camino.watchObject( const stream = camino
{ objectId: dependency.objectId, includeSnapshot: true, attachmentIds: request.context?.grant ? [dependency.attachmentId] : [] }, .watchObject(
{
objectId: dependency.objectId,
includeSnapshot: true,
attachmentIds: request.context?.grant ? [dependency.attachmentId] : [],
},
{ signal: controller.signal }, { signal: controller.signal },
)[Symbol.asyncIterator](); )
[Symbol.asyncIterator]();
try { try {
// Camino subscribes before producing the snapshot, so once this // Camino subscribes before producing the snapshot, so once this
// resolves the following state/edge read cannot race the stream. // resolves the following state/edge read cannot race the stream.
const snapshot = await stream.next(); const snapshot = await stream.next();
if (snapshot.done) throw new Error(`Dependency stream ${key} ended during setup`); if (snapshot.done) throw new Error(`Dependency stream ${key} ended during setup`);
const waitNext = () => stream.next().then( const waitNext = () =>
stream.next().then(
(result) => ({ key, done: Boolean(result.done) }), (result) => ({ key, done: Boolean(result.done) }),
(error: unknown) => ({ key, done: true, error }), (error: unknown) => ({ key, done: true, error }),
); );
@@ -614,10 +781,7 @@ export const createPackageRuntimeRoutes = (config: {
await abort; await abort;
break; break;
} }
const outcome = await Promise.race([ const outcome = await Promise.race([...[...subscriptions.values()].map((entry) => entry.next), abort]);
...[...subscriptions.values()].map((entry) => entry.next),
abort,
]);
if (outcome === "abort") break; if (outcome === "abort") break;
const subscription = subscriptions.get(outcome.key); const subscription = subscriptions.get(outcome.key);
if (!subscription) continue; if (!subscription) continue;
@@ -646,7 +810,9 @@ export const createPackageRuntimeRoutes = (config: {
signal.removeEventListener("abort", abortAll); signal.removeEventListener("abort", abortAll);
abortAll(); abortAll();
} }
} finally { execution.finish(); } } finally {
execution.finish();
}
}, },
}); });
}; };
+11 -3
View File
@@ -3,15 +3,23 @@ export const createInvocationRegistry = () => {
const entries = new Map<string, { state: string; controller: AbortController }>(); const entries = new Map<string, { state: string; controller: AbortController }>();
return { return {
begin(id: string) { begin(id: string) {
if (!id || entries.has(id)) throw new Error("Invocation ID is missing or already used; invocations are never replayed implicitly"); if (!id || entries.has(id))
throw new Error("Invocation ID is missing or already used; invocations are never replayed implicitly");
// Completed identities remain until process retirement. A bounded process // Completed identities remain until process retirement. A bounded process
// may reject new work; it must not evict and accidentally replay a call. // may reject new work; it must not evict and accidentally replay a call.
if (entries.size >= 100_000) throw new Error("Invocation registry full; explicit runtime retirement required"); if (entries.size >= 100_000) throw new Error("Invocation registry full; explicit runtime retirement required");
const entry = { state: "running", controller: new AbortController() }; const entry = { state: "running", controller: new AbortController() };
entries.set(id, entry); entries.set(id, entry);
return { signal: entry.controller.signal, finish(failed = false) { entry.state = failed ? "failed" : "completed"; } }; return {
signal: entry.controller.signal,
finish(failed = false) {
entry.state = failed ? "failed" : "completed";
},
};
},
status(id: string) {
return { invocationId: id, state: entries.get(id)?.state ?? "unknown" };
}, },
status(id: string) { return { invocationId: id, state: entries.get(id)?.state ?? "unknown" }; },
cancel(id: string) { cancel(id: string) {
const entry = entries.get(id); const entry = entries.get(id);
if (entry && ["running", "cancellation-requested"].includes(entry.state)) { if (entry && ["running", "cancellation-requested"].includes(entry.state)) {
+91 -32
View File
@@ -1,18 +1,42 @@
import {createHash} from "node:crypto"; import { createHash } from "node:crypto";
export type MigrationInput = { export type MigrationInput = {
schemaVersion: 1; executionId: string; exportId: string; schemaVersion: 1;
ports: {name: string; binding: string; view: "old" | "new"; access: ("read" | "write" | "create" | "edge")[]; atomId?: string; executionId: string;
attachedAtomId?: string; defaultValue?: unknown; exportId: string;
states?: {objectId: string; value: unknown}[]; edges?: MigrationEdge[]}[]; ports: {
name: string;
binding: string;
view: "old" | "new";
access: ("read" | "write" | "create" | "edge")[];
atomId?: string;
attachedAtomId?: string;
defaultValue?: unknown;
states?: { objectId: string; value: unknown }[];
edges?: MigrationEdge[];
}[];
};
export type MigrationEdge = {
id: string;
edgeTypeId: string;
firstObjectId: string;
secondObjectId: string;
firstProjectionId: string;
secondProjectionId: string;
firstOrdinal?: number;
secondOrdinal?: number;
firstKeyJson?: string;
secondKeyJson?: string;
};
export type MigrationOutput = {
schemaVersion: 1;
executionId: string;
writes: { port: string; objectId: string; value: unknown }[];
creates: { port: string; logicalKey: string; objectId: string }[];
edgeReplacements: { port: string; edges: MigrationEdge[] }[];
}; };
export type MigrationEdge = {id: string; edgeTypeId: string; firstObjectId: string; secondObjectId: string; firstProjectionId: string; secondProjectionId: string; firstOrdinal?: number; secondOrdinal?: number; firstKeyJson?: string; secondKeyJson?: string};
export type MigrationOutput = {schemaVersion: 1; executionId: string;
writes: {port: string; objectId: string; value: unknown}[];
creates: {port: string; logicalKey: string; objectId: string}[];
edgeReplacements: {port: string; edges: MigrationEdge[]}[]};
export type MigrationContext = { export type MigrationContext = {
enumerate(port: string): {objectId: string; value: unknown}[]; enumerate(port: string): { objectId: string; value: unknown }[];
read(port: string, objectId: string): unknown; read(port: string, objectId: string): unknown;
write(port: string, objectId: string, value: unknown): void; write(port: string, objectId: string, value: unknown): void;
create(port: string, logicalKey: string): string; create(port: string, logicalKey: string): string;
@@ -20,65 +44,100 @@ export type MigrationContext = {
replaceEdges(port: string, edges: MigrationEdge[]): void; replaceEdges(port: string, edges: MigrationEdge[]): void;
}; };
export const migrationObjectId = (executionId: string, port: string, logicalKey: string) => export const migrationObjectId = (executionId: string, port: string, logicalKey: string) =>
`obj:migration:${createHash("sha256").update(JSON.stringify([executionId, port, logicalKey])).digest("hex")}`; `obj:migration:${createHash("sha256")
.update(JSON.stringify([executionId, port, logicalKey]))
.digest("hex")}`;
/** No ordinary RuntimeContext or network/database clients are supplied here. /** No ordinary RuntimeContext or network/database clients are supplied here.
* Process isolation belongs to the host, not this convenience API. */ * Process isolation belongs to the host, not this convenience API. */
export const createMigrationContext = (input: MigrationInput) => { export const createMigrationContext = (input: MigrationInput) => {
if (input.schemaVersion !== 1 || !input.executionId || new Set(input.ports.map((entry) => entry.name)).size !== input.ports.length) throw new Error("Invalid migration input"); if (
const output: MigrationOutput = {schemaVersion: 1, executionId: input.executionId, writes: [], creates: [], edgeReplacements: []}; input.schemaVersion !== 1 ||
!input.executionId ||
new Set(input.ports.map((entry) => entry.name)).size !== input.ports.length
)
throw new Error("Invalid migration input");
const output: MigrationOutput = {
schemaVersion: 1,
executionId: input.executionId,
writes: [],
creates: [],
edgeReplacements: [],
};
const port = (name: string, access: string) => { const port = (name: string, access: string) => {
const selected = input.ports.find((entry) => entry.name === name); const selected = input.ports.find((entry) => entry.name === name);
if (!selected?.access.includes(access as "read") || (selected.view === "old" && access !== "read")) throw new Error(`Migration port ${name} does not grant ${access}`); if (!selected?.access.includes(access as "read") || (selected.view === "old" && access !== "read"))
throw new Error(`Migration port ${name} does not grant ${access}`);
return selected; return selected;
}; };
const context: MigrationContext = { const context: MigrationContext = {
enumerate(name) { enumerate(name) {
const selected = port(name, "read"), states = structuredClone(selected.states ?? []); const selected = port(name, "read"),
if (selected.view === "new" && Object.hasOwn(selected, "defaultValue")) for (const helper of output.creates) { states = structuredClone(selected.states ?? []);
if (input.ports.find((entry) => entry.name === helper.port)?.atomId === selected.attachedAtomId && !states.some((entry) => entry.objectId === helper.objectId)) states.push({objectId: helper.objectId, value: structuredClone(selected.defaultValue)}); if (selected.view === "new" && Object.hasOwn(selected, "defaultValue"))
for (const helper of output.creates) {
if (
input.ports.find((entry) => entry.name === helper.port)?.atomId === selected.attachedAtomId &&
!states.some((entry) => entry.objectId === helper.objectId)
)
states.push({ objectId: helper.objectId, value: structuredClone(selected.defaultValue) });
} }
if (selected.view === "new") for (const write of output.writes) { if (selected.view === "new")
for (const write of output.writes) {
if (input.ports.find((entry) => entry.name === write.port)?.binding !== selected.binding) continue; if (input.ports.find((entry) => entry.name === write.port)?.binding !== selected.binding) continue;
const existing = states.findIndex((entry) => entry.objectId === write.objectId), entry = {objectId: write.objectId, value: structuredClone(write.value)}; const existing = states.findIndex((entry) => entry.objectId === write.objectId),
if (existing < 0) states.push(entry); else states[existing] = entry; entry = { objectId: write.objectId, value: structuredClone(write.value) };
if (existing < 0) states.push(entry);
else states[existing] = entry;
} }
return states.sort((a, b) => a.objectId < b.objectId ? -1 : a.objectId > b.objectId ? 1 : 0); return states.sort((a, b) => (a.objectId < b.objectId ? -1 : a.objectId > b.objectId ? 1 : 0));
},
read(name, objectId) {
return context.enumerate(name).find((entry) => entry.objectId === objectId)?.value;
}, },
read(name, objectId) {return context.enumerate(name).find((entry) => entry.objectId === objectId)?.value;},
write(name, objectId, value) { write(name, objectId, value) {
port(name, "write"); port(name, "write");
const previous = output.writes.findIndex((entry) => entry.port === name && entry.objectId === objectId); const previous = output.writes.findIndex((entry) => entry.port === name && entry.objectId === objectId);
const entry = {port: name, objectId, value: structuredClone(value)}; const entry = { port: name, objectId, value: structuredClone(value) };
if (previous < 0) output.writes.push(entry); else output.writes[previous] = entry; if (previous < 0) output.writes.push(entry);
else output.writes[previous] = entry;
}, },
create(name, logicalKey) { create(name, logicalKey) {
port(name, "create"); port(name, "create");
if (!logicalKey || logicalKey.length > 1024) throw new Error("Migration creation requires a bounded stable logical key"); if (!logicalKey || logicalKey.length > 1024)
throw new Error("Migration creation requires a bounded stable logical key");
const objectId = migrationObjectId(input.executionId, name, logicalKey); const objectId = migrationObjectId(input.executionId, name, logicalKey);
if (!output.creates.some((entry) => entry.objectId === objectId)) output.creates.push({port: name, logicalKey, objectId}); if (!output.creates.some((entry) => entry.objectId === objectId))
output.creates.push({ port: name, logicalKey, objectId });
return objectId; return objectId;
}, },
edges(name) { edges(name) {
const selected = port(name, "read"); const selected = port(name, "read");
const replacement = selected.view === "new" ? output.edgeReplacements.find((entry) => input.ports.find((candidate) => candidate.name === entry.port)?.binding === selected.binding) : undefined; const replacement =
selected.view === "new"
? output.edgeReplacements.find(
(entry) => input.ports.find((candidate) => candidate.name === entry.port)?.binding === selected.binding,
)
: undefined;
return structuredClone(replacement?.edges ?? selected.edges ?? []); return structuredClone(replacement?.edges ?? selected.edges ?? []);
}, },
replaceEdges(name, edges) { replaceEdges(name, edges) {
port(name, "edge"); port(name, "edge");
const previous = output.edgeReplacements.findIndex((entry) => entry.port === name); const previous = output.edgeReplacements.findIndex((entry) => entry.port === name);
const entry = {port: name, edges: structuredClone(edges)}; const entry = { port: name, edges: structuredClone(edges) };
if (previous < 0) output.edgeReplacements.push(entry); else output.edgeReplacements[previous] = entry; if (previous < 0) output.edgeReplacements.push(entry);
else output.edgeReplacements[previous] = entry;
}, },
}; };
return {context, result: () => structuredClone(output)}; return { context, result: () => structuredClone(output) };
}; };
/** Entrypoint for an immutable package's dedicated bin/migrate executable. /** Entrypoint for an immutable package's dedicated bin/migrate executable.
* stdout is protocol-only; send diagnostics to stderr. The host independently * stdout is protocol-only; send diagnostics to stderr. The host independently
* validates every write, helper identity, contract, and completion receipt. */ * validates every write, helper identity, contract, and completion receipt. */
export const serveMigration = async (exports: Record<string, (context: MigrationContext) => void | Promise<void>>) => { export const serveMigration = async (exports: Record<string, (context: MigrationContext) => void | Promise<void>>) => {
const chunks: Buffer[] = []; let bytes = 0; const chunks: Buffer[] = [];
let bytes = 0;
for await (const chunk of process.stdin) { for await (const chunk of process.stdin) {
bytes += chunk.length; bytes += chunk.length;
if (bytes > 16 * 1024 * 1024) throw new Error("Migration input exceeds 16 MiB"); if (bytes > 16 * 1024 * 1024) throw new Error("Migration input exceeds 16 MiB");
+280 -17
View File
File diff suppressed because one or more lines are too long
+21 -7
View File
@@ -3,15 +3,26 @@
const identities = new WeakMap<object, string>(); const identities = new WeakMap<object, string>();
declare const referenceBrand: unique symbol; declare const referenceBrand: unique symbol;
export interface QxObjectRef<Identity extends string = string> { export interface QxObjectRef<Identity extends string = string> {
readonly [referenceBrand]: {readonly [K in Identity]: true}; readonly [referenceBrand]: { readonly [K in Identity]: true };
equals(other: QxObjectRef<string>): boolean; equals(other: QxObjectRef<string>): boolean;
} }
class Reference { class Reference {
constructor(id: string) { identities.set(this, id); Object.freeze(this); } constructor(id: string) {
equals(other: unknown) { return isObjectReference(other) && identities.get(this) === identities.get(other); } identities.set(this, id);
toJSON(): never { throw new Error("Object references cannot be serialized into ordinary data"); } Object.freeze(this);
toString(): never { throw new Error("Object references cannot be coerced to strings"); } }
[Symbol.toPrimitive](): never { throw new Error("Object references cannot be coerced to scalar values"); } equals(other: unknown) {
return isObjectReference(other) && identities.get(this) === identities.get(other);
}
toJSON(): never {
throw new Error("Object references cannot be serialized into ordinary data");
}
toString(): never {
throw new Error("Object references cannot be coerced to strings");
}
[Symbol.toPrimitive](): never {
throw new Error("Object references cannot be coerced to scalar values");
}
} }
export const isObjectReference = (value: unknown): value is QxObjectRef => export const isObjectReference = (value: unknown): value is QxObjectRef =>
typeof value === "object" && value !== null && identities.has(value); typeof value === "object" && value !== null && identities.has(value);
@@ -27,7 +38,10 @@ export const referenceToWire = (value: unknown): string => {
}; };
export const assertReferenceFree = (value: unknown, seen = new Set<object>()): void => { export const assertReferenceFree = (value: unknown, seen = new Set<object>()): void => {
if (!value || typeof value !== "object") return; if (!value || typeof value !== "object") return;
if (isObjectReference(value)) throw new Error("Managed references belong in declared RPC references or graph relationships, not ordinary state/messages"); if (isObjectReference(value))
throw new Error(
"Managed references belong in declared RPC references or graph relationships, not ordinary state/messages",
);
if (seen.has(value)) throw new Error("Cyclic ordinary data"); if (seen.has(value)) throw new Error("Cyclic ordinary data");
seen.add(value); seen.add(value);
if (!(value instanceof Uint8Array)) for (const child of Object.values(value)) assertReferenceFree(child, seen); if (!(value instanceof Uint8Array)) for (const child of Object.values(value)) assertReferenceFree(child, seen);
+29 -12
View File
@@ -1,7 +1,10 @@
import type {QxObjectRef} from "./references.js"; import type { QxObjectRef } from "./references.js";
import type {RelationshipCollection, RelationshipEntry} from "./index.js"; import type { RelationshipCollection, RelationshipEntry } from "./index.js";
type Key = string | boolean | bigint; type Key = string | boolean | bigint;
type Port<T extends QxObjectRef> = {collection(): Promise<RelationshipCollection<T>>; replace(entries: RelationshipEntry<T>[], expectedRevision: bigint): Promise<RelationshipCollection<T>>}; type Port<T extends QxObjectRef> = {
collection(): Promise<RelationshipCollection<T>>;
replace(entries: RelationshipEntry<T>[], expectedRevision: bigint): Promise<RelationshipCollection<T>>;
};
const checked = async <T extends QxObjectRef>(port: Port<T>, revision: bigint) => { const checked = async <T extends QxObjectRef>(port: Port<T>, revision: bigint) => {
const snapshot = await port.collection(); const snapshot = await port.collection();
if (snapshot.revision !== revision) throw new Error("STALE_COLLECTION_REVISION"); if (snapshot.revision !== revision) throw new Error("STALE_COLLECTION_REVISION");
@@ -10,31 +13,39 @@ const checked = async <T extends QxObjectRef>(port: Port<T>, revision: bigint) =
/** Helpers never retry a failed CAS or silently overwrite concurrent edits. */ /** Helpers never retry a failed CAS or silently overwrite concurrent edits. */
export const relationshipMap = <T extends QxObjectRef, K extends Key = Key>(port: Port<T>) => ({ export const relationshipMap = <T extends QxObjectRef, K extends Key = Key>(port: Port<T>) => ({
read: () => port.collection(), read: () => port.collection(),
async get(key: K) {const snapshot = await port.collection(); return {revision: snapshot.revision, value: snapshot.entries.find((entry) => entry.key === key)?.target};}, async get(key: K) {
const snapshot = await port.collection();
return { revision: snapshot.revision, value: snapshot.entries.find((entry) => entry.key === key)?.target };
},
async set(key: K, target: T, expectedRevision: bigint) { async set(key: K, target: T, expectedRevision: bigint) {
const snapshot = await checked(port, expectedRevision); const snapshot = await checked(port, expectedRevision);
const entries = snapshot.entries.filter((entry) => entry.key !== key); const entries = snapshot.entries.filter((entry) => entry.key !== key);
const existing = snapshot.entries.find((entry) => entry.key === key && entry.target.equals(target)); const existing = snapshot.entries.find((entry) => entry.key === key && entry.target.equals(target));
entries.push(existing ?? {key, target}); entries.push(existing ?? { key, target });
return port.replace(entries, expectedRevision); return port.replace(entries, expectedRevision);
}, },
async delete(key: K, expectedRevision: bigint) { async delete(key: K, expectedRevision: bigint) {
const snapshot = await checked(port, expectedRevision); const snapshot = await checked(port, expectedRevision);
return port.replace(snapshot.entries.filter((entry) => entry.key !== key), expectedRevision); return port.replace(
snapshot.entries.filter((entry) => entry.key !== key),
expectedRevision,
);
}, },
}); });
export const relationshipList = <T extends QxObjectRef>(port: Port<T>) => ({ export const relationshipList = <T extends QxObjectRef>(port: Port<T>) => ({
read: () => port.collection(), read: () => port.collection(),
async insert(index: number, target: T, expectedRevision: bigint) { async insert(index: number, target: T, expectedRevision: bigint) {
const snapshot = await checked(port, expectedRevision); const snapshot = await checked(port, expectedRevision);
if (!Number.isSafeInteger(index) || index < 0 || index > snapshot.entries.length) throw new Error("List index out of bounds"); if (!Number.isSafeInteger(index) || index < 0 || index > snapshot.entries.length)
snapshot.entries.splice(index, 0, {target}); throw new Error("List index out of bounds");
snapshot.entries.splice(index, 0, { target });
return port.replace(snapshot.entries, expectedRevision); return port.replace(snapshot.entries, expectedRevision);
}, },
async move(edgeId: string, index: number, expectedRevision: bigint) { async move(edgeId: string, index: number, expectedRevision: bigint) {
const snapshot = await checked(port, expectedRevision); const snapshot = await checked(port, expectedRevision);
const prior = snapshot.entries.findIndex((entry) => entry.edgeId === edgeId); const prior = snapshot.entries.findIndex((entry) => entry.edgeId === edgeId);
if (prior < 0 || !Number.isSafeInteger(index) || index < 0 || index >= snapshot.entries.length) throw new Error("Unknown list entry or invalid index"); if (prior < 0 || !Number.isSafeInteger(index) || index < 0 || index >= snapshot.entries.length)
throw new Error("Unknown list entry or invalid index");
const [entry] = snapshot.entries.splice(prior, 1); const [entry] = snapshot.entries.splice(prior, 1);
snapshot.entries.splice(index, 0, entry); snapshot.entries.splice(index, 0, entry);
return port.replace(snapshot.entries, expectedRevision); return port.replace(snapshot.entries, expectedRevision);
@@ -42,7 +53,10 @@ export const relationshipList = <T extends QxObjectRef>(port: Port<T>) => ({
async delete(edgeId: string, expectedRevision: bigint) { async delete(edgeId: string, expectedRevision: bigint) {
const snapshot = await checked(port, expectedRevision); const snapshot = await checked(port, expectedRevision);
if (!snapshot.entries.some((entry) => entry.edgeId === edgeId)) throw new Error("Unknown list entry"); if (!snapshot.entries.some((entry) => entry.edgeId === edgeId)) throw new Error("Unknown list entry");
return port.replace(snapshot.entries.filter((entry) => entry.edgeId !== edgeId), expectedRevision); return port.replace(
snapshot.entries.filter((entry) => entry.edgeId !== edgeId),
expectedRevision,
);
}, },
}); });
export const relationshipSet = <T extends QxObjectRef>(port: Port<T>) => ({ export const relationshipSet = <T extends QxObjectRef>(port: Port<T>) => ({
@@ -50,10 +64,13 @@ export const relationshipSet = <T extends QxObjectRef>(port: Port<T>) => ({
async add(target: T, expectedRevision: bigint) { async add(target: T, expectedRevision: bigint) {
const snapshot = await checked(port, expectedRevision); const snapshot = await checked(port, expectedRevision);
if (snapshot.entries.some((entry) => entry.target.equals(target))) return snapshot; if (snapshot.entries.some((entry) => entry.target.equals(target))) return snapshot;
return port.replace([...snapshot.entries, {target}], expectedRevision); return port.replace([...snapshot.entries, { target }], expectedRevision);
}, },
async delete(target: T, expectedRevision: bigint) { async delete(target: T, expectedRevision: bigint) {
const snapshot = await checked(port, expectedRevision); const snapshot = await checked(port, expectedRevision);
return port.replace(snapshot.entries.filter((entry) => !entry.target.equals(target)), expectedRevision); return port.replace(
snapshot.entries.filter((entry) => !entry.target.equals(target)),
expectedRevision,
);
}, },
}); });
+144 -51
View File
@@ -1,51 +1,99 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import test from "node:test"; import test from "node:test";
import { referenceFromWire } from "../dist/references.js"; import { referenceFromWire } from "../dist/references.js";
import { bindQxHandler, decodeQxValue, encodeQxValue, jsToProtoValue, liveValue, protoValueToJs, opaqueReactPropsBinding } from "../dist/index.js"; import {
bindQxHandler,
decodeQxValue,
encodeQxValue,
jsToProtoValue,
liveValue,
protoValueToJs,
} from "../dist/index.js";
const scalar = (name) => ({ kind: "scalar", name }); const scalar = (name) => ({ kind: "scalar", name });
const unit = { kind: "builtin", name: "unit" }; const unit = { kind: "builtin", name: "unit" };
test("opaque React props retain managed references while ordinary messages remain closed", () => { test("free function handlers never acquire a receiver or object session", async () => {
const handler = bindQxHandler(
{ receiver: "none", inputType: unit, outputType: unit, ports: {} },
async (context) => {
assert.equal("objectId" in context, false);
assert.equal("openSession" in context, false);
},
{},
);
await handler({
inputProto: {},
get objectId() {
throw new Error("no receiver exists");
},
openSession() {
assert.fail("no object session");
},
});
});
test("React props no longer have an opaque-message reference escape hatch", () => {
const descriptorId = "org.quixos.web-studio.ReactProps"; const descriptorId = "org.quixos.web-studio.ReactProps";
const reference = referenceFromWire("obj:task"); const reference = referenceFromWire("obj:task");
const type = {kind: "message", descriptorId}; const type = { kind: "message", descriptorId };
const messages = {[descriptorId]: opaqueReactPropsBinding}; const messages = { [descriptorId]: { encode: jsToProtoValue, decode: protoValueToJs } };
const decoded = decodeQxValue(type, encodeQxValue(type, {subject: reference, title: "Task"}, messages), messages); assert.throws(() => encodeQxValue(type, { subject: reference }, messages), /Managed references/);
assert.ok(decoded.subject.equals(reference)); assert.throws(() => decodeQxValue(type, jsToProtoValue({ subject: reference }), messages), /Managed references/);
assert.equal(decoded.title, "Task");
assert.throws(() => opaqueReactPropsBinding.encode(null), /object/);
assert.throws(() => opaqueReactPropsBinding.encode([]), /object/);
assert.throws(() => encodeQxValue({kind: "message", descriptorId: "Ordinary"}, {subject: reference},
{Ordinary: opaqueReactPropsBinding}), /Managed references/);
}); });
test("declared record inputs preserve opaque references without opening message codecs", async () => { test("declared record inputs preserve opaque references without opening message codecs", async () => {
const target = referenceFromWire("obj:board"); const target = referenceFromWire("obj:board");
const type = {kind: "record", fields: { const type = {
target: {kind: "object-ref", expectation: {kind: "atom", atomId: "board"}}, kind: "record",
x: scalar("double"), note: {kind: "optional", value: scalar("string")}, fields: {
}}; target: { kind: "object-ref", expectation: { kind: "atom", atomId: "board" } },
const encoded = encodeQxValue(type, {target, x: 12}, {}); x: scalar("double"),
note: { kind: "optional", value: scalar("string") },
},
};
const encoded = encodeQxValue(type, { target, x: 12 }, {});
const decoded = decodeQxValue(type, encoded, {}); const decoded = decodeQxValue(type, encoded, {});
assert.ok(decoded.target.equals(target)); assert.ok(decoded.target.equals(target));
assert.equal(decoded.x, 12); assert.equal(decoded.x, 12);
assert.equal(decoded.note, null); assert.equal(decoded.note, null);
assert.throws(() => JSON.stringify(decoded), /cannot be serialized/); assert.throws(() => JSON.stringify(decoded), /cannot be serialized/);
assert.throws(() => encodeQxValue(type, {target, x: 12, hidden: target}, {}), /Unexpected/); assert.throws(() => encodeQxValue(type, { target, x: 12, hidden: target }, {}), /Unexpected/);
assert.throws(() => encodeQxValue(type, {x: 12}, {}), /Missing/); assert.throws(() => encodeQxValue(type, { x: 12 }, {}), /Missing/);
assert.throws(() => encodeQxValue(type, {target: "obj:board", x: 12}, {}), /opaque object reference/); assert.throws(() => encodeQxValue(type, { target: "obj:board", x: 12 }, {}), /opaque object reference/);
const handler = bindQxHandler({inputType: type, outputType: unit, ports: {}}, async (context) => { const handler = bindQxHandler(
{ inputType: type, outputType: unit, ports: {} },
async (context) => {
assert.ok(context.input.target.equals(target)); assert.ok(context.input.target.equals(target));
assert.equal(context.input.x, 12); assert.equal(context.input.x, 12);
}, {}); },
await handler({objectId: target, inputProto: encoded.kind.value.fields}); {},
);
await handler({ objectId: target, inputProto: encoded.kind.value.fields });
}); });
test("typed sessions rebind ports and cancellation to each acquired invocation", async () => { test("typed sessions rebind ports and cancellation to each acquired invocation", async () => {
const first = new AbortController(), second = new AbortController(); const first = new AbortController(),
second = new AbortController();
let closed = false; let closed = false;
const context = (value, signal) => ({objectId: referenceFromWire("obj:owner"), inputProto: {}, signal, const context = (value, signal) => ({
state: () => ({live: async () => liveValue(jsToProtoValue(value))})}); objectId: referenceFromWire("obj:owner"),
const raw = {...context(1n, first.signal), openSession: async () => ({id: "session:test", inputProto: {},
run: (work) => work(context(2n, second.signal)), close: async () => {closed = true;}})}; signal,
const handler = bindQxHandler({inputType: unit, outputType: unit, ports: {counter: {kind: "state", id: "counter", valueType: scalar("int64"), primitives: ["read"]}}}, async (bound) => { state: () => ({ live: async () => liveValue(jsToProtoValue(value)) }),
});
const raw = {
...context(1n, first.signal),
openSession: async () => ({
id: "session:test",
run: (work) => work(context(2n, second.signal)),
close: async () => {
closed = true;
},
}),
};
const handler = bindQxHandler(
{
inputType: unit,
outputType: unit,
ports: { counter: { kind: "state", id: "counter", valueType: scalar("int64"), primitives: ["read"] } },
},
async (bound) => {
assert.equal(bound.signal, first.signal); assert.equal(bound.signal, first.signal);
const session = await bound.openSession(); const session = await bound.openSession();
await session.run(async (next) => { await session.run(async (next) => {
@@ -54,40 +102,64 @@ test("typed sessions rebind ports and cancellation to each acquired invocation",
assert.equal(next.openSession, undefined); assert.equal(next.openSession, undefined);
}); });
await session.close(); await session.close();
}, {}); },
{},
);
await handler(raw); await handler(raw);
assert.equal(closed, true); assert.equal(closed, true);
}); });
test("binding codecs round trip nested bytes, 64-bit integers, nulls, and references", () => { test("binding codecs round trip nested bytes, 64-bit integers, nulls, and references", () => {
const values = [[scalar("int64"), -(2n ** 63n)], [scalar("uint64"), 2n ** 64n - 1n], const values = [
[scalar("int64"), -(2n ** 63n)],
[scalar("uint64"), 2n ** 64n - 1n],
[scalar("bytes"), new Uint8Array([0, 255])], [scalar("bytes"), new Uint8Array([0, 255])],
[{ kind: "list", value: { kind: "optional", value: scalar("int64") } }, [null, 2n ** 60n]], [{ kind: "list", value: { kind: "optional", value: scalar("int64") } }, [null, 2n ** 60n]],
[{ kind: "object-ref", expectation: { kind: "atom", atomId: "thing" } }, referenceFromWire("obj:thing")]]; [{ kind: "object-ref", expectation: { kind: "atom", atomId: "thing" } }, referenceFromWire("obj:thing")],
];
for (const [type, value] of values) assert.deepEqual(decodeQxValue(type, encodeQxValue(type, value, {}), {}), value); for (const [type, value] of values) assert.deepEqual(decodeQxValue(type, encodeQxValue(type, value, {}), {}), value);
}); });
test("opaque references pass declared RPC boundaries but cannot enter ordinary data", () => { test("opaque references pass declared RPC boundaries but cannot enter ordinary data", () => {
const reference = referenceFromWire("obj:thing"); const reference = referenceFromWire("obj:thing");
const type = {kind: "object-ref", expectation: {kind: "atom", atomId: "thing"}}; const type = { kind: "object-ref", expectation: { kind: "atom", atomId: "thing" } };
const roundtrip = decodeQxValue(type, encodeQxValue(type, reference, {}), {}); const roundtrip = decodeQxValue(type, encodeQxValue(type, reference, {}), {});
assert.equal(reference.equals(roundtrip), true); assert.equal(reference.equals(roundtrip), true);
assert.equal(reference.equals(referenceFromWire("obj:other")), false); assert.equal(reference.equals(referenceFromWire("obj:other")), false);
assert.throws(() => JSON.stringify({nested: [reference]}), /cannot be serialized/); assert.throws(() => JSON.stringify({ nested: [reference] }), /cannot be serialized/);
assert.throws(() => String(reference), /cannot be coerced/); assert.throws(() => String(reference), /cannot be coerced/);
assert.throws(() => encodeQxValue(type, "obj:thing", {}), /opaque/); assert.throws(() => encodeQxValue(type, "obj:thing", {}), /opaque/);
assert.throws(() => encodeQxValue(scalar("string"), reference, {}), /Managed references/); assert.throws(() => encodeQxValue(scalar("string"), reference, {}), /Managed references/);
const message = {kind: "message", descriptorId: "Payload"}; const message = { kind: "message", descriptorId: "Payload" };
assert.throws(() => encodeQxValue(message, {nested: reference}, {Payload: {encode: jsToProtoValue}}), /Managed references/); assert.throws(
assert.throws(() => encodeQxValue(message, {}, {Payload: {encode: () => jsToProtoValue(reference)}}), /Managed references/); () => encodeQxValue(message, { nested: reference }, { Payload: { encode: jsToProtoValue } }),
assert.throws(() => decodeQxValue(message, jsToProtoValue(reference), {Payload: {decode: () => ({})}}), /Managed references/); /Managed references/,
);
assert.throws(
() => encodeQxValue(message, {}, { Payload: { encode: () => jsToProtoValue(reference) } }),
/Managed references/,
);
assert.throws(
() => decodeQxValue(message, jsToProtoValue(reference), { Payload: { decode: () => ({}) } }),
/Managed references/,
);
}); });
test("typed state and interface ports preserve declared values and exact operation IDs", async () => { test("typed state and interface ports preserve declared values and exact operation IDs", async () => {
const spec = { inputType: scalar("int64"), outputType: scalar("bytes"), ports: { const spec = {
inputType: scalar("int64"),
outputType: scalar("bytes"),
ports: {
data: { kind: "state", id: "state-id", valueType: scalar("int64"), primitives: ["read", "write"] }, data: { kind: "state", id: "state-id", valueType: scalar("int64"), primitives: ["read", "write"] },
reader: { kind: "interface", id: "interface-id", operations: { "payload.get": { id: "get-id", inputType: unit, outputType: scalar("bytes") } } }, reader: {
} }; kind: "interface",
id: "interface-id",
operations: { "payload.get": { id: "get-id", inputType: unit, outputType: scalar("bytes") } },
},
},
};
let written; let written;
const handler = bindQxHandler(spec, async ({ input, ports }) => { const handler = bindQxHandler(
spec,
async ({ input, ports }) => {
assert.equal(input, 2n ** 60n); assert.equal(input, 2n ** 60n);
assert.equal(await ports.data.get(), 9n); assert.equal(await ports.data.get(), 9n);
assert.deepEqual((await ports.data.live()).$quixosValue, jsToProtoValue(9n)); assert.deepEqual((await ports.data.live()).$quixosValue, jsToProtoValue(9n));
@@ -95,14 +167,32 @@ test("typed state and interface ports preserve declared values and exact operati
assert.deepEqual((await ports.reader.live["payload.get"]()).$quixosValue, jsToProtoValue(new Uint8Array([7]))); assert.deepEqual((await ports.reader.live["payload.get"]()).$quixosValue, jsToProtoValue(new Uint8Array([7])));
await ports.data.set(input); await ports.data.set(input);
return ports.reader["payload.get"](); return ports.reader["payload.get"]();
}, {}); },
const result = await handler({ inputProto: { value: jsToProtoValue(2n ** 60n) }, objectId: "obj", {},
state: (id) => { assert.equal(id, "state-id"); return { );
live: async () => liveValue(jsToProtoValue(9n)), set: async (value) => { written = value; }, const result = await handler({
}; }, inputProto: { value: jsToProtoValue(2n ** 60n) },
interface: (id) => { assert.equal(id, "interface-id"); return { objectId: referenceFromWire("obj:reader"), live: async (operation, input) => { objectId: "obj",
assert.equal(operation, "get-id"); assert.deepEqual(input, {}); return liveValue(jsToProtoValue(new Uint8Array([7]))); state: (id) => {
} }; }, assert.equal(id, "state-id");
return {
live: async () => liveValue(jsToProtoValue(9n)),
set: async (value) => {
written = value;
},
};
},
interface: (id) => {
assert.equal(id, "interface-id");
return {
objectId: referenceFromWire("obj:reader"),
live: async (operation, input) => {
assert.equal(operation, "get-id");
assert.deepEqual(input, {});
return liveValue(jsToProtoValue(new Uint8Array([7])));
},
};
},
}); });
assert.equal(written.$quixosValue.kind.value, String(2n ** 60n)); assert.equal(written.$quixosValue.kind.value, String(2n ** 60n));
assert.deepEqual(result.$quixosValue.kind.value, new Uint8Array([7])); assert.deepEqual(result.$quixosValue.kind.value, new Uint8Array([7]));
@@ -110,8 +200,11 @@ test("typed state and interface ports preserve declared values and exact operati
test("external message bindings and derived event types are used at the boundary", async () => { test("external message bindings and derived event types are used at the boundary", async () => {
const message = { kind: "message", descriptorId: "Payload" }; const message = { kind: "message", descriptorId: "Payload" };
const messages = { Payload: { encode: jsToProtoValue, decode: protoValueToJs } }; const messages = { Payload: { encode: jsToProtoValue, decode: protoValueToJs } };
const handler = bindQxHandler({ inputType: message, outputType: { kind: "builtin", name: "watch-handle" }, eventType: message, ports: {} }, const handler = bindQxHandler(
{ kind: "derived", get: ({ input }) => ({ value: input.title }) }, messages); { inputType: message, outputType: { kind: "builtin", name: "watch-handle" }, eventType: message, ports: {} },
{ kind: "derived", get: ({ input }) => ({ value: input.title }) },
messages,
);
assert.equal(handler.kind, "derived"); assert.equal(handler.kind, "derived");
const result = await handler.get({ objectId: "obj", inputProto: { title: jsToProtoValue("hello") } }); const result = await handler.get({ objectId: "obj", inputProto: { title: jsToProtoValue("hello") } });
assert.deepEqual(protoValueToJs(result.$quixosValue), { value: "hello" }); assert.deepEqual(protoValueToJs(result.$quixosValue), { value: "hello" });
+38 -21
View File
@@ -1,30 +1,47 @@
import test from 'node:test'; import test from "node:test";
import assert from 'node:assert/strict'; import assert from "node:assert/strict";
import fs from 'node:fs/promises'; import fs from "node:fs/promises";
import os from 'node:os'; import os from "node:os";
import path from 'node:path'; import path from "node:path";
import crypto from 'node:crypto'; import crypto from "node:crypto";
import {spawnSync} from 'node:child_process'; import { spawnSync } from "node:child_process";
test('supervised SDK reads a credential file and proves instance identity without an environment token', async t => { test("supervised SDK reads a credential file and proves instance identity without an environment token", async (t) => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'qx-sdk-credential-')); const directory = await fs.mkdtemp(path.join(os.tmpdir(), "qx-sdk-credential-"));
t.after(() => fs.rm(directory, {recursive:true, force:true})); t.after(() => fs.rm(directory, { recursive: true, force: true }));
const token = crypto.randomBytes(32).toString('hex'); const token = crypto.randomBytes(32).toString("hex");
const filename = path.join(directory, 'process-token'); const filename = path.join(directory, "process-token");
await fs.writeFile(filename, token, {mode:0o600}); await fs.writeFile(filename, token, { mode: 0o600 });
const env = {...process.env, CAMINO_RUNTIME_AUTH_TOKEN_FILE: filename, const env = {
CAMINO_RUNTIME_AUTH_REQUIRED:'1', QUIXOS_RUNTIME_INSTANCE_ID:'instance:test'}; ...process.env,
CAMINO_RUNTIME_AUTH_TOKEN_FILE: filename,
CAMINO_RUNTIME_AUTH_REQUIRED: "1",
QUIXOS_RUNTIME_INSTANCE_ID: "instance:test",
};
delete env.CAMINO_RUNTIME_AUTH_TOKEN; delete env.CAMINO_RUNTIME_AUTH_TOKEN;
const result = spawnSync(process.execPath, ['--input-type=module', '-e', ` const result = spawnSync(
import {createPackageRuntimeRoutes} from ${JSON.stringify(new URL('../dist/index.js', import.meta.url).href)}; process.execPath,
[
"--input-type=module",
"-e",
`
import {createPackageRuntimeRoutes} from ${JSON.stringify(new URL("../dist/index.js", import.meta.url).href)};
createPackageRuntimeRoutes({packageRevisionId:'package:test',exports:{}})({ createPackageRuntimeRoutes({packageRevisionId:'package:test',exports:{}})({
service(_type, implementation) { console.log(JSON.stringify(implementation.handshake({nonce:'challenge'}))); } service(_type, implementation) { console.log(JSON.stringify(implementation.handshake({nonce:'challenge'}))); }
}); });
`], {env, encoding:'utf8'}); `,
],
{ env, encoding: "utf8" },
);
assert.equal(result.status, 0, result.stderr); assert.equal(result.status, 0, result.stderr);
const handshake = JSON.parse(result.stdout); const handshake = JSON.parse(result.stdout);
assert.equal(handshake.authenticationProof, crypto.createHmac('sha256', token) assert.equal(
.update(JSON.stringify(['challenge','instance:test','package:test'])).digest('hex')); handshake.authenticationProof,
assert.ok(handshake.capabilities.includes('epoch-grants-v1')); crypto
.createHmac("sha256", token)
.update(JSON.stringify(["challenge", "instance:test", "package:test"]))
.digest("hex"),
);
assert.ok(handshake.capabilities.includes("epoch-grants-v1"));
assert.ok(!result.stdout.includes(token)); assert.ok(!result.stdout.includes(token));
}); });
+20 -8
View File
@@ -1,13 +1,25 @@
import test from "node:test"; import test from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import {createMigrationContext, migrationObjectId} from "../dist/migration.js"; import { createMigrationContext, migrationObjectId } from "../dist/migration.js";
test("migration context offers restricted ports and deterministic helper identities", () => { test("migration context offers restricted ports and deterministic helper identities", () => {
const input = {schemaVersion: 1, executionId: "operation:transition", exportId: "migrate", ports: [ const input = {
{name: "old", binding: "slot:name", view: "old", access: ["read"], states: [{objectId: "obj:one", value: "Alice"}]}, schemaVersion: 1,
{name: "new", binding: "slot:name", view: "new", access: ["write"]}, {name: "newRead", binding: "slot:name", view: "new", access: ["read"]}, executionId: "operation:transition",
{name: "helpers", binding: "atom:helper", view: "new", access: ["create"]}, exportId: "migrate",
]}; ports: [
const {context, result} = createMigrationContext(input); {
name: "old",
binding: "slot:name",
view: "old",
access: ["read"],
states: [{ objectId: "obj:one", value: "Alice" }],
},
{ name: "new", binding: "slot:name", view: "new", access: ["write"] },
{ name: "newRead", binding: "slot:name", view: "new", access: ["read"] },
{ name: "helpers", binding: "atom:helper", view: "new", access: ["create"] },
],
};
const { context, result } = createMigrationContext(input);
assert.equal(context.read("old", "obj:one"), "Alice"); assert.equal(context.read("old", "obj:one"), "Alice");
assert.throws(() => context.write("old", "obj:one", "Bob"), /does not grant/); assert.throws(() => context.write("old", "obj:one", "Bob"), /does not grant/);
assert.throws(() => context.read("new", "obj:one"), /does not grant/); assert.throws(() => context.read("new", "obj:one"), /does not grant/);
@@ -20,5 +32,5 @@ test("migration context offers restricted ports and deterministic helper identit
assert.equal(helper, migrationObjectId(input.executionId, "helpers", "one")); assert.equal(helper, migrationObjectId(input.executionId, "helpers", "one"));
assert.equal(context.create("helpers", "one"), helper); assert.equal(context.create("helpers", "one"), helper);
assert.equal(result().creates.length, 1); assert.equal(result().creates.length, 1);
assert.deepEqual(input.ports[0].states, [{objectId: "obj:one", value: "Alice"}]); assert.deepEqual(input.ports[0].states, [{ objectId: "obj:one", value: "Alice" }]);
}); });
+14 -7
View File
@@ -1,16 +1,23 @@
import test from "node:test"; import test from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import {relationshipMap, relationshipList, relationshipSet} from "../dist/index.js"; import { relationshipMap, relationshipList, relationshipSet } from "../dist/index.js";
import {referenceFromWire} from "../dist/references.js"; import { referenceFromWire } from "../dist/references.js";
test("relationship helpers preserve handles and require explicit collection revisions", async () => { test("relationship helpers preserve handles and require explicit collection revisions", async () => {
let current = {revision: 0n, entries: []}, serial = 0; let current = { revision: 0n, entries: [] },
const port = {collection: async () => ({revision: current.revision, entries: current.entries.map((entry) => ({...entry}))}), serial = 0;
const port = {
collection: async () => ({ revision: current.revision, entries: current.entries.map((entry) => ({ ...entry })) }),
replace: async (entries, revision) => { replace: async (entries, revision) => {
assert.equal(revision, current.revision); assert.equal(revision, current.revision);
current = {revision: revision + 1n, entries: entries.map((entry) => ({...entry, edgeId: entry.edgeId ?? `edge:${++serial}`}))}; current = {
revision: revision + 1n,
entries: entries.map((entry) => ({ ...entry, edgeId: entry.edgeId ?? `edge:${++serial}` })),
};
return port.collection(); return port.collection();
}}; },
const a = referenceFromWire("obj:a"), b = referenceFromWire("obj:b"); };
const a = referenceFromWire("obj:a"),
b = referenceFromWire("obj:b");
const map = relationshipMap(port); const map = relationshipMap(port);
await map.set("a1", a, 0n); await map.set("a1", a, 0n);
assert.equal((await map.get("a1")).value.equals(a), true); assert.equal((await map.get("a1")).value.equals(a), true);
+104 -55
View File
@@ -21,11 +21,7 @@ import {
ValueSchema, ValueSchema,
ValueSourceSchema, ValueSourceSchema,
} from "../dist/camino/api_pb.js"; } from "../dist/camino/api_pb.js";
import { import { EdgeDependencySchema, InjectedDependencySchema, PackageExportRefSchema } from "../dist/quixos/refs_pb.js";
EdgeDependencySchema,
InjectedDependencySchema,
PackageExportRefSchema,
} from "../dist/quixos/refs_pb.js";
import { InvokeCapabilityResponseSchema, OrchestratorRuntime } from "../dist/quixos/orch_pb.js"; import { InvokeCapabilityResponseSchema, OrchestratorRuntime } from "../dist/quixos/orch_pb.js";
import { DerivedDependencySchema, PackageRuntime } from "../dist/quixos/runtime_pb.js"; import { DerivedDependencySchema, PackageRuntime } from "../dist/quixos/runtime_pb.js";
@@ -93,7 +89,10 @@ test("runtime context exposes only explicitly injected ports", async () => {
resolveEdge: async () => ({ edges: [] }), resolveEdge: async () => ({ edges: [] }),
connectEdge: async () => ({}), connectEdge: async () => ({}),
}; };
const context = createRuntimeContext(camino, {}, { const context = createRuntimeContext(
camino,
{},
{
objectId: "obj:task", objectId: "obj:task",
input: {}, input: {},
dependencies: [ dependencies: [
@@ -112,7 +111,8 @@ test("runtime context exposes only explicitly injected ports", async () => {
}, },
}), }),
], ],
}); },
);
assert.equal(await context.state("port:title").get(), "obj:task/slot:task:title"); assert.equal(await context.state("port:title").get(), "obj:task/slot:task:title");
await context.state("port:title").set("Changed"); await context.state("port:title").set("Changed");
@@ -126,23 +126,30 @@ test("runtime context exposes only explicitly injected ports", async () => {
test("injected ports preserve an explicitly traversed object through PackageRuntime RPC", async () => { test("injected ports preserve an explicitly traversed object through PackageRuntime RPC", async () => {
const reads = []; const reads = [];
const caminoServer = await listen((router) => router.service(CaminoService, { const caminoServer = await listen((router) =>
router.service(CaminoService, {
readState: (request) => { readState: (request) => {
reads.push(request); reads.push(request);
return { value: jsToProtoValue("Project name") }; return { value: jsToProtoValue("Project name") };
}, },
})); }),
const runtimeServer = await listen(createPackageRuntimeRoutes({ );
const runtimeServer = await listen(
createPackageRuntimeRoutes({
packageRevisionId: "package:test@1", packageRevisionId: "package:test@1",
caminoUrl: caminoServer.url, caminoUrl: caminoServer.url,
exports: { exports: {
"export:test:value": (context) => context.state("port:value").get(), "export:test:value": (context) => context.state("port:value").get(),
}, },
})); }),
const client = createClient(PackageRuntime, createConnectTransport({ );
const client = createClient(
PackageRuntime,
createConnectTransport({
baseUrl: runtimeServer.url, baseUrl: runtimeServer.url,
httpVersion: "1.1", httpVersion: "1.1",
})); }),
);
try { try {
const response = await client.invoke({ const response = await client.invoke({
export: create(PackageExportRefSchema, { export: create(PackageExportRefSchema, {
@@ -150,11 +157,13 @@ test("injected ports preserve an explicitly traversed object through PackageRunt
exportId: "export:test:value", exportId: "export:test:value",
}), }),
objectId: "obj:component", objectId: "obj:component",
dependencies: [create(InjectedDependencySchema, { dependencies: [
create(InjectedDependencySchema, {
portId: "port:value", portId: "port:value",
objectId: "obj:project", objectId: "obj:project",
binding: { case: "stateSlotId", value: "slot:project:name" }, binding: { case: "stateSlotId", value: "slot:project:name" },
})], }),
],
}); });
assert.equal(response.ok, true); assert.equal(response.ok, true);
assert.equal(protoValueToJs(response.result), "Project name"); assert.equal(protoValueToJs(response.result), "Project name");
@@ -178,32 +187,40 @@ test("interface views invoke a related object and propagate transitive live depe
}), }),
}), }),
}); });
const orchServer = await listen((router) => router.service(OrchestratorRuntime, { const orchServer = await listen((router) =>
router.service(OrchestratorRuntime, {
invokeCapability: (request) => { invokeCapability: (request) => {
invocations.push(request); invocations.push(request);
return create(InvokeCapabilityResponseSchema, { return create(InvokeCapabilityResponseSchema, {
ok: true, ok: true,
result: sourceValue, result: sourceValue,
dependencies: [create(DerivedDependencySchema, { dependencies: [
create(DerivedDependencySchema, {
kind: "state", kind: "state",
objectId: "obj:project", objectId: "obj:project",
attachmentId: "slot:project:name", attachmentId: "slot:project:name",
})], }),
],
}); });
}, },
})); }),
const runtimeServer = await listen(createPackageRuntimeRoutes({ );
const runtimeServer = await listen(
createPackageRuntimeRoutes({
packageRevisionId: "package:test@1", packageRevisionId: "package:test@1",
orchUrl: orchServer.url, orchUrl: orchServer.url,
exports: { exports: {
"export:test:value": derived((context) => "export:test:value": derived((context) => context.interface("port:named").live("operation:named:name:get")),
context.interface("port:named").live("operation:named:name:get")),
}, },
})); }),
const client = createClient(PackageRuntime, createConnectTransport({ );
const client = createClient(
PackageRuntime,
createConnectTransport({
baseUrl: runtimeServer.url, baseUrl: runtimeServer.url,
httpVersion: "1.1", httpVersion: "1.1",
})); }),
);
try { try {
const response = await client.invoke({ const response = await client.invoke({
export: create(PackageExportRefSchema, { export: create(PackageExportRefSchema, {
@@ -211,11 +228,13 @@ test("interface views invoke a related object and propagate transitive live depe
exportId: "export:test:value", exportId: "export:test:value",
}), }),
objectId: "obj:component", objectId: "obj:component",
dependencies: [create(InjectedDependencySchema, { dependencies: [
create(InjectedDependencySchema, {
portId: "port:named", portId: "port:named",
objectId: "obj:project", objectId: "obj:project",
binding: { case: "interfaceRevisionId", value: "interface:named@1" }, binding: { case: "interfaceRevisionId", value: "interface:named@1" },
})], }),
],
}); });
assert.equal(response.ok, true); assert.equal(response.ok, true);
assert.equal(invocations[0]?.objectId, "obj:project"); assert.equal(invocations[0]?.objectId, "obj:project");
@@ -232,7 +251,8 @@ test("interface views invoke a related object and propagate transitive live depe
test("derived interface views reread after their transitive subscriptions become live", async () => { test("derived interface views reread after their transitive subscriptions become live", async () => {
let current = "before subscription"; let current = "before subscription";
let invocationCount = 0; let invocationCount = 0;
const caminoServer = await listen((router) => router.service(CaminoService, { const caminoServer = await listen((router) =>
router.service(CaminoService, {
watchObject: async function* (request, context) { watchObject: async function* (request, context) {
// Model a write racing the first nested capability read. Camino makes // Model a write racing the first nested capability read. Camino makes
// the subscription live before yielding this snapshot. // the subscription live before yielding this snapshot.
@@ -245,51 +265,66 @@ test("derived interface views reread after their transitive subscriptions become
workspaceRevisionId: "workspace:test@1", workspaceRevisionId: "workspace:test@1",
}), }),
}; };
await new Promise((resolve) => await new Promise((resolve) => context.signal.addEventListener("abort", resolve, { once: true }));
context.signal.addEventListener("abort", resolve, { once: true }));
}, },
})); }),
const orchServer = await listen((router) => router.service(OrchestratorRuntime, { );
const orchServer = await listen((router) =>
router.service(OrchestratorRuntime, {
invokeCapability: () => { invokeCapability: () => {
invocationCount += 1; invocationCount += 1;
return create(InvokeCapabilityResponseSchema, { return create(InvokeCapabilityResponseSchema, {
ok: true, ok: true,
result: jsToProtoValue(current), result: jsToProtoValue(current),
dependencies: [create(DerivedDependencySchema, { dependencies: [
create(DerivedDependencySchema, {
kind: "state", kind: "state",
objectId: "obj:project", objectId: "obj:project",
attachmentId: "slot:project:name", attachmentId: "slot:project:name",
})], }),
],
}); });
}, },
})); }),
const runtimeServer = await listen(createPackageRuntimeRoutes({ );
const runtimeServer = await listen(
createPackageRuntimeRoutes({
packageRevisionId: "package:test@1", packageRevisionId: "package:test@1",
caminoUrl: caminoServer.url, caminoUrl: caminoServer.url,
orchUrl: orchServer.url, orchUrl: orchServer.url,
exports: { exports: {
"export:test:value": derived((context) => "export:test:value": derived((context) => context.interface("port:named").invoke("operation:named:name:get")),
context.interface("port:named").invoke("operation:named:name:get")),
}, },
})); }),
const client = createClient(PackageRuntime, createConnectTransport({ );
const client = createClient(
PackageRuntime,
createConnectTransport({
baseUrl: runtimeServer.url, baseUrl: runtimeServer.url,
httpVersion: "1.1", httpVersion: "1.1",
})); }),
);
const controller = new AbortController(); const controller = new AbortController();
try { try {
const stream = client.watch({ const stream = client
.watch(
{
export: create(PackageExportRefSchema, { export: create(PackageExportRefSchema, {
packageRevisionId: "package:test@1", packageRevisionId: "package:test@1",
exportId: "export:test:value", exportId: "export:test:value",
}), }),
objectId: "obj:component", objectId: "obj:component",
dependencies: [create(InjectedDependencySchema, { dependencies: [
create(InjectedDependencySchema, {
portId: "port:named", portId: "port:named",
objectId: "obj:project", objectId: "obj:project",
binding: { case: "interfaceRevisionId", value: "interface:named@1" }, binding: { case: "interfaceRevisionId", value: "interface:named@1" },
})], }),
}, { signal: controller.signal })[Symbol.asyncIterator](); ],
},
{ signal: controller.signal },
)
[Symbol.asyncIterator]();
const initial = await stream.next(); const initial = await stream.next();
assert.equal(protoValueToJs(initial.value?.value), "after subscription"); assert.equal(protoValueToJs(initial.value?.value), "after subscription");
assert.equal(invocationCount, 2); assert.equal(invocationCount, 2);
@@ -305,7 +340,8 @@ test("derived watches subscribe before reading and emit only real changes", asyn
let current = "before"; let current = "before";
let subscribed = false; let subscribed = false;
const changes = []; const changes = [];
const caminoServer = await listen((router) => router.service(CaminoService, { const caminoServer = await listen((router) =>
router.service(CaminoService, {
readState: () => { readState: () => {
assert.equal(subscribed, true, "the dependency stream must be live before the state read"); assert.equal(subscribed, true, "the dependency stream must be live before the state read");
return { value: jsToProtoValue(current) }; return { value: jsToProtoValue(current) };
@@ -330,31 +366,44 @@ test("derived watches subscribe before reading and emit only real changes", asyn
yield { objectId: request.objectId }; yield { objectId: request.objectId };
} }
}, },
})); }),
const runtimeServer = await listen(createPackageRuntimeRoutes({ );
const runtimeServer = await listen(
createPackageRuntimeRoutes({
packageRevisionId: "package:test@1", packageRevisionId: "package:test@1",
caminoUrl: caminoServer.url, caminoUrl: caminoServer.url,
exports: { exports: {
"export:test:value": derived((context) => context.state("port:value").get()), "export:test:value": derived((context) => context.state("port:value").get()),
}, },
})); }),
const client = createClient(PackageRuntime, createConnectTransport({ );
const client = createClient(
PackageRuntime,
createConnectTransport({
baseUrl: runtimeServer.url, baseUrl: runtimeServer.url,
httpVersion: "1.1", httpVersion: "1.1",
})); }),
);
const controller = new AbortController(); const controller = new AbortController();
try { try {
const stream = client.watch({ const stream = client
.watch(
{
export: create(PackageExportRefSchema, { export: create(PackageExportRefSchema, {
packageRevisionId: "package:test@1", packageRevisionId: "package:test@1",
exportId: "export:test:value", exportId: "export:test:value",
}), }),
objectId: "obj:test", objectId: "obj:test",
dependencies: [create(InjectedDependencySchema, { dependencies: [
create(InjectedDependencySchema, {
portId: "port:value", portId: "port:value",
binding: { case: "stateSlotId", value: "slot:test:value" }, binding: { case: "stateSlotId", value: "slot:test:value" },
})], }),
}, { signal: controller.signal })[Symbol.asyncIterator](); ],
},
{ signal: controller.signal },
)
[Symbol.asyncIterator]();
const initial = await stream.next(); const initial = await stream.next();
assert.equal(protoValueToJs(initial.value?.value), "before"); assert.equal(protoValueToJs(initial.value?.value), "before");
assert.equal(initial.value?.initial, true); assert.equal(initial.value?.initial, true);