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:
+7
-19
@@ -16,19 +16,6 @@ declare const watchBrand: unique symbol;
|
||||
export type QxWatchHandle = string & { readonly [watchBrand]: true };
|
||||
export type MessageBinding<T> = { encode(value: T): Value; decode(value: Value): T };
|
||||
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 QxHandler<C, O> = (context: C) => O | Promise<O>;
|
||||
export type QxDerived<C, O> = { kind: "derived"; get: QxHandler<C, O> };
|
||||
@@ -51,6 +38,7 @@ export type QxPortSpec =
|
||||
| { kind: "interface"; id: string; operations: Record<string, QxOperationSpec> }
|
||||
| { kind: "constructor"; id: string; inputType: QxValueType };
|
||||
export type QxHandlerSpec = {
|
||||
receiver?: "none";
|
||||
inputType: QxValueType;
|
||||
outputType: QxValueType;
|
||||
eventType?: QxValueType;
|
||||
@@ -79,9 +67,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") {
|
||||
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") {
|
||||
@@ -130,10 +118,10 @@ export const encodeQxValue = (type: QxValueType, value: any, messages: QxMessage
|
||||
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);
|
||||
@@ -245,9 +233,9 @@ export const bindQxHandler = <C, O>(
|
||||
}),
|
||||
);
|
||||
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!();
|
||||
|
||||
Reference in New Issue
Block a user