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.
This commit is contained in:
Timothy J. Aveni
2026-09-16 11:25:20 -07:00
parent 0e183516c0
commit 5bb8ee876d
12 changed files with 474 additions and 112 deletions
+6 -25
View File
@@ -2,21 +2,6 @@ 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) => {
@@ -42,11 +27,9 @@ export const decodeQxValue = (type, value, messages) => {
return value.kind.value.values.map((entry) => decodeQxValue(type.value, entry, messages));
}
if (type.kind === "message") {
if (type.descriptorId !== reactPropsDescriptor)
assertReferenceFree(protoValueToJs(value));
assertReferenceFree(protoValueToJs(value));
const decoded = requireMessage(messages, type.descriptorId).decode(value);
if (type.descriptorId !== reactPropsDescriptor)
assertReferenceFree(decoded);
assertReferenceFree(decoded);
return decoded;
}
if (type.kind === "object-ref") {
@@ -99,12 +82,10 @@ export const encodeQxValue = (type, value, messages) => {
referenceToWire(value);
return jsToProtoValue(value);
}
if (type.kind !== "message" || type.descriptorId !== reactPropsDescriptor)
assertReferenceFree(value);
assertReferenceFree(value);
if (type.kind === "message") {
const encoded = requireMessage(messages, type.descriptorId).encode(value);
if (type.descriptorId !== reactPropsDescriptor)
assertReferenceFree(protoValueToJs(encoded));
assertReferenceFree(protoValueToJs(encoded));
return encoded;
}
return jsToProtoValue(value);
@@ -197,9 +178,9 @@ export const bindQxHandler = (spec, handler, messages) => {
}
}));
return {
objectId: raw.objectId,
...(spec.receiver === "none" ? {} : { objectId: raw.objectId }),
signal: raw.signal,
...(raw.openSession
...(spec.receiver !== "none" && raw.openSession
? {
openSession: async () => {
const session = await raw.openSession();