From 3dd4e9a1adab0e11b5653a0bd90acac2f846e588 Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Sun, 13 Sep 2026 19:29:33 -0700 Subject: [PATCH] Migrate TODO implementations to generated candidate bindings --- dist/bindings.d.ts | 4 +++- dist/bindings.d.ts.map | 2 +- dist/bindings.js | 39 +++++++++++++++++++++++++++++++-------- src/bindings.ts | 32 +++++++++++++++++++++++++------- test/bindings.test.mjs | 20 ++++++++++++++++++-- 5 files changed, 78 insertions(+), 19 deletions(-) diff --git a/dist/bindings.d.ts b/dist/bindings.d.ts index 7168137..466e41b 100644 --- a/dist/bindings.d.ts +++ b/dist/bindings.d.ts @@ -1,5 +1,5 @@ import { type Value } from "./camino/api_pb.js"; -import { type RuntimeHandler, type DerivedHandler } from "./index.js"; +import { liveValue, type RuntimeHandler, type DerivedHandler } from "./index.js"; export type { QxObjectRef } from "./references.js"; declare const watchBrand: unique symbol; export type QxWatchHandle = string & { @@ -9,6 +9,8 @@ export type MessageBinding = { encode(value: T): Value; decode(value: Value): T; }; +export type QxLiveValue = ReturnType; +export declare const opaqueReactPropsBinding: MessageBinding>; export type BindingValue = B extends MessageBinding ? T : never; export type QxHandler = (context: C) => O | Promise; export type QxDerived = { diff --git a/dist/bindings.d.ts.map b/dist/bindings.d.ts.map index aaa2a9a..27a75e5 100644 --- a/dist/bindings.d.ts.map +++ b/dist/bindings.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"bindings.d.ts","sourceRoot":"","sources":["../src/bindings.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"bindings.d.ts","sourceRoot":"","sources":["../src/bindings.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/bindings.js b/dist/bindings.js index 238f138..a023e83 100644 --- a/dist/bindings.js +++ b/dist/bindings.js @@ -2,6 +2,21 @@ import { create } from "@bufbuild/protobuf"; import { ValueSchema, ObjectValueSchema } from "./camino/api_pb.js"; import { derived, jsToProtoValue, liveValue, protoValueToJs } from "./index.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 }); // Conversion belongs at the binding boundary. It does not add orchestrator validation. export const decodeQxValue = (type, value, messages) => { @@ -27,9 +42,11 @@ export const decodeQxValue = (type, value, messages) => { return value.kind.value.values.map((entry) => decodeQxValue(type.value, entry, messages)); } if (type.kind === "message") { - assertReferenceFree(protoValueToJs(value)); + if (type.descriptorId !== reactPropsDescriptor) + assertReferenceFree(protoValueToJs(value)); const decoded = requireMessage(messages, type.descriptorId).decode(value); - assertReferenceFree(decoded); + if (type.descriptorId !== reactPropsDescriptor) + assertReferenceFree(decoded); return decoded; } if (type.kind === "object-ref") { @@ -82,10 +99,12 @@ export const encodeQxValue = (type, value, messages) => { referenceToWire(value); return jsToProtoValue(value); } - assertReferenceFree(value); + if (type.kind !== "message" || type.descriptorId !== reactPropsDescriptor) + assertReferenceFree(value); if (type.kind === "message") { const encoded = requireMessage(messages, type.descriptorId).encode(value); - assertReferenceFree(protoValueToJs(encoded)); + if (type.descriptorId !== reactPropsDescriptor) + assertReferenceFree(protoValueToJs(encoded)); return encoded; } return jsToProtoValue(value); @@ -115,7 +134,7 @@ export const bindQxHandler = (spec, handler, messages) => { case "state": { const state = raw.state(port.id); return [name, { - ...(port.primitives.includes("read") ? { get: async () => decodeQxValue(port.valueType, (await state.live()).$quixosValue, 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))) } : {}), }]; } @@ -127,9 +146,13 @@ export const bindQxHandler = (spec, handler, messages) => { } case "interface": { const target = raw.interface(port.id); - return [name, Object.fromEntries(Object.entries(port.operations).map(([name, operation]) => [name, - async (input) => decodeQxValue(operation.outputType, (await target.live(operation.id, inputFields(operation.inputType, input, messages))).$quixosValue, messages), - ]))]; + return [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)), + ])), + ...Object.fromEntries(Object.entries(port.operations).map(([name, operation]) => [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)) }]; } diff --git a/src/bindings.ts b/src/bindings.ts index cb5f67b..87db362 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -8,6 +8,20 @@ import { assertReferenceFree, referenceToWire } from "./references.js"; declare const watchBrand: unique symbol; export type QxWatchHandle = string & { readonly [watchBrand]: true }; export type MessageBinding = { encode(value: T): Value; decode(value: Value): T }; +export type QxLiveValue = ReturnType; +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> = { + 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; + }, +}; export type BindingValue = B extends MessageBinding ? T : never; export type QxHandler = (context: C) => O | Promise; export type QxDerived = { kind: "derived"; get: QxHandler }; @@ -52,9 +66,9 @@ export const decodeQxValue = (type: QxValueType, value: Value | undefined, messa return value.kind.value.values.map((entry) => decodeQxValue(type.value, entry, messages)); } if (type.kind === "message") { - assertReferenceFree(protoValueToJs(value)); + if (type.descriptorId !== reactPropsDescriptor) assertReferenceFree(protoValueToJs(value)); const decoded = requireMessage(messages, type.descriptorId).decode(value); - assertReferenceFree(decoded); + if (type.descriptorId !== reactPropsDescriptor) assertReferenceFree(decoded); return decoded; } if (type.kind === "object-ref") { @@ -94,10 +108,10 @@ export const encodeQxValue = (type: QxValueType, value: any, messages: QxMessage if (type.kind === "optional") return value === null ? jsToProtoValue(null) : encodeQxValue(type.value, value, messages); if (type.kind === "list") 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" || type.descriptorId !== reactPropsDescriptor) assertReferenceFree(value); if (type.kind === "message") { const encoded = requireMessage(messages, type.descriptorId).encode(value); - assertReferenceFree(protoValueToJs(encoded)); + if (type.descriptorId !== reactPropsDescriptor) assertReferenceFree(protoValueToJs(encoded)); return encoded; } return jsToProtoValue(value); @@ -128,7 +142,7 @@ export const bindQxHandler = ( case "state": { const state = raw.state(port.id); return [name, { - ...(port.primitives.includes("read") ? { get: async () => decodeQxValue(port.valueType, (await state.live()).$quixosValue, 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))) } : {}), }]; } @@ -140,10 +154,14 @@ export const bindQxHandler = ( } case "interface": { const target = raw.interface(port.id); - return [name, Object.fromEntries(Object.entries(port.operations).map(([name, operation]) => [name, + return [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)), + ])), + ...Object.fromEntries(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)) }]; } diff --git a/test/bindings.test.mjs b/test/bindings.test.mjs index 3e0c8e7..a9f42d6 100644 --- a/test/bindings.test.mjs +++ b/test/bindings.test.mjs @@ -1,9 +1,22 @@ import assert from "node:assert/strict"; import test from "node:test"; import { referenceFromWire } from "../dist/references.js"; -import { bindQxHandler, decodeQxValue, encodeQxValue, jsToProtoValue, liveValue, protoValueToJs } from "../dist/index.js"; +import { bindQxHandler, decodeQxValue, encodeQxValue, jsToProtoValue, liveValue, protoValueToJs, opaqueReactPropsBinding } from "../dist/index.js"; const scalar = (name) => ({ kind: "scalar", name }); const unit = { kind: "builtin", name: "unit" }; +test("opaque React props retain managed references while ordinary messages remain closed", () => { + const descriptorId = "org.quixos.web-studio.ReactProps"; + const reference = referenceFromWire("obj:task"); + const type = {kind: "message", descriptorId}; + const messages = {[descriptorId]: opaqueReactPropsBinding}; + const decoded = decodeQxValue(type, encodeQxValue(type, {subject: reference, title: "Task"}, messages), messages); + assert.ok(decoded.subject.equals(reference)); + 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 () => { const target = referenceFromWire("obj:board"); const type = {kind: "record", fields: { @@ -77,6 +90,9 @@ test("typed state and interface ports preserve declared values and exact operati const handler = bindQxHandler(spec, async ({ input, ports }) => { assert.equal(input, 2n ** 60n); assert.equal(await ports.data.get(), 9n); + assert.deepEqual((await ports.data.live()).$quixosValue, jsToProtoValue(9n)); + assert.ok(ports.reader.objectId.equals(referenceFromWire("obj:reader"))); + assert.deepEqual((await ports.reader.live["payload.get"]()).$quixosValue, jsToProtoValue(new Uint8Array([7]))); await ports.data.set(input); return ports.reader["payload.get"](); }, {}); @@ -84,7 +100,7 @@ test("typed state and interface ports preserve declared values and exact operati 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 { live: async (operation, input) => { + 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]))); } }; }, });