Implement shared live fields and checked query hydration
Add native register acknowledgments and idempotent mutation replay, shared optimistic field controllers, overlapping custom setters, non-suspending hooks and explicit Suspense. Carry checked @live provenance through batched queries and hydrate shared browser fields with coverage leases. Update tracker/scaffolds/guides and verify compiler, PostgreSQL, browser and immutable workspace paths.
This commit is contained in:
Vendored
+13
-4
@@ -8,7 +8,7 @@ export const queryFixtureSource = {
|
||||
repository: "https://query-fixture.example.test/source.git",
|
||||
commit: "1".repeat(40),
|
||||
};
|
||||
export async function queryWorkspaceFixture() {
|
||||
export async function queryWorkspaceFixture(options: { live?: boolean } = {}) {
|
||||
const interfaces = new Map<string, InterfaceRevision>();
|
||||
const sources: Record<string, string> = {};
|
||||
function iface(name: string, members: string, parameters = "") {
|
||||
@@ -29,7 +29,7 @@ export async function queryWorkspaceFixture() {
|
||||
`
|
||||
queryable value title id "title" : string {get id "title:get"; set id "title:set";}
|
||||
queryable value done id "done" : bool {get id "done:get";}
|
||||
queryable value rank id "rank" : int64 {get id "rank:get";}
|
||||
queryable value rank id "rank" : int64 {get id "rank:get"; set id "rank:set";}
|
||||
queryable relation assignee id "assignee" : optional-one interface PersonFacts {resolve id "assignee:resolve";}
|
||||
queryable rpc value score id "score" : int32 {get id "score:get";}
|
||||
`,
|
||||
@@ -81,7 +81,16 @@ export async function queryWorkspaceFixture() {
|
||||
"score-totals.graphql": `query ScoreTotals {root {_qx {relations {items {aggregate {count sum {score}}}}}}}`,
|
||||
};
|
||||
pkg.checkedQueries = await Promise.all(
|
||||
pkg.queries!.map((query) => compileQuery(query, allInterfaces, async (name) => documents[name]!)),
|
||||
pkg.queries!.map((query) =>
|
||||
compileQuery(query, allInterfaces, async (name) =>
|
||||
options.live
|
||||
? documents[name]!.replace(/\btitle\b/g, "title @live").replace(
|
||||
"title @live rank done",
|
||||
"title @live rank @live done @live",
|
||||
)
|
||||
: documents[name]!,
|
||||
),
|
||||
),
|
||||
);
|
||||
const implementation = (atom: string) => `conform ${atom} as TaskFacts id "${atom}-facts" {
|
||||
private state Title${atom} id "${atom}:title" on ${atom} : string policy crdt(string) default "Untitled";
|
||||
@@ -92,7 +101,7 @@ export async function queryWorkspaceFixture() {
|
||||
interface PersonFacts projection tasks id "${atom}:assignee:inverse" many;
|
||||
}
|
||||
bind title.get to state Title${atom}.read;
|
||||
bind title.set to package Queries.titleSet with {title to state Title${atom};};
|
||||
${options.live ? `bind title.set to state Title${atom}.write;` : `bind title.set to package Queries.titleSet with {title to state Title${atom};};`}
|
||||
bind done to state Done${atom}; bind rank to state Rank${atom};
|
||||
bind assignee.resolve to edge Assignee${atom}.assignee.resolve;
|
||||
bind score.get to package Queries.score query-reason "Explicit bounded score computation";
|
||||
|
||||
@@ -4,6 +4,8 @@ import { compileCapabilityResourceSource } from "../src/capability-language/inde
|
||||
import { compileQuery } from "../src/query/compile.js";
|
||||
import { QueryCompileError } from "../src/query/types.js";
|
||||
import { checkQueryTemplate } from "../src/query/templates.js";
|
||||
import { queryPresentation } from "../src/query/presentation.js";
|
||||
import { querySelectionToWire } from "../src/query/proto.js";
|
||||
import type { InterfaceRevision } from "../src/capability-model/types.js";
|
||||
|
||||
const source = { repository: "https://example.test/queries.git", commit: "a".repeat(40) };
|
||||
@@ -125,6 +127,48 @@ const document = `query Upcoming($first: Int!, $before: Int64!) {
|
||||
const compile = (query = document, row = "fragment Row on TaskFacts { title due }", clauses = "") =>
|
||||
compileQuery(fixture(clauses), [collection, facts], async (name) => (name.endsWith("row.graphql") ? row : query));
|
||||
|
||||
test("live selections preserve aliases, nullable values, fragment conditions and wire identity", async () => {
|
||||
const checked = await compile(
|
||||
`query Upcoming($show: Boolean!) {root {items(first: 3) {entries {node {plain: title ...Row @include(if: $show)}}}}}`,
|
||||
`fragment Row on TaskFacts {label: title @live due @live}`,
|
||||
);
|
||||
const fields = queryPresentation(checked, [collection, facts]);
|
||||
assert.deepEqual(
|
||||
fields.map((f) => f.path),
|
||||
[
|
||||
["root", "items", "entries", "*", "node", "label"],
|
||||
["root", "items", "entries", "*", "node", "due"],
|
||||
],
|
||||
);
|
||||
assert.equal(fields[0]!.getOperationId, "title:get");
|
||||
assert.equal(fields[0]!.setOperationId, undefined);
|
||||
assert.equal(fields[1]!.valueType.kind, "optional");
|
||||
assert.equal(fields[0]!.conditions.length, 1);
|
||||
assert.ok(JSON.stringify(checked.selection.map(querySelectionToWire)).includes(fields[0]!.selectionId));
|
||||
const defaults = await compile(
|
||||
`query Upcoming($show: Boolean! = true) {root {items(first: 3) {entries {node {...Row @include(if: $show)}}}}}`,
|
||||
"fragment Row on TaskFacts {title @live}",
|
||||
);
|
||||
assert.equal(queryPresentation(defaults, [collection, facts])[0]!.conditions[0]!.defaultValue, true);
|
||||
});
|
||||
|
||||
test("live selections reject RPC getters, synthetic selections and conflicting presentations", async () => {
|
||||
for (const [selected, code] of [
|
||||
["score @live", "QUERY_LIVE_UNSUPPORTED"],
|
||||
["_qx @live {ref}", "QUERY_LIVE_UNSUPPORTED"],
|
||||
["title @live title", "QUERY_LIVE_CONFLICT"],
|
||||
["title @live(unchecked: true)", "QUERY_VALIDATION"],
|
||||
])
|
||||
await assert.rejects(
|
||||
compile(
|
||||
`query Upcoming {root {items(first: 3) {entries {node {...Row}}}}}`,
|
||||
`fragment Row on TaskFacts {${selected}}`,
|
||||
'allow TaskFacts.score select "bounded";',
|
||||
),
|
||||
(error: unknown) => error instanceof QueryCompileError && error.code === code,
|
||||
);
|
||||
});
|
||||
|
||||
test("query dependency ports resolve exact exports without declaration ordering constraints", () => {
|
||||
const result = compileCapabilityResourceSource(
|
||||
`import interface Tasks;
|
||||
|
||||
+26
-10
@@ -7,13 +7,14 @@ import { spawnSync } from "node:child_process";
|
||||
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
|
||||
import { generateReactBindings } from "../src/bindings/react.js";
|
||||
import { reactPlatformTypes } from "../src/bindings/react-platform.js";
|
||||
import { compileQuery } from "../src/query/compile.js";
|
||||
|
||||
const source = { repository: "https://example.test/fields.git", commit: "a".repeat(40) };
|
||||
test("React bindings preserve read-only, writable and nested reference contracts", async (t) => {
|
||||
const iface = compileCapabilityResourceSource(
|
||||
`interface Fields id "fields" revision "fields@1" {
|
||||
value title id "title" : string { get id "title:get"; set id "title:set"; watch start id "watch" stop id "stop"; }
|
||||
value summary id "summary" : string { get id "summary:get"; }
|
||||
queryable value title id "title" : string { get id "title:get"; set id "title:set"; watch start id "watch" stop id "stop"; }
|
||||
queryable value summary id "summary" : string { get id "summary:get"; }
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
@@ -22,11 +23,19 @@ test("React bindings preserve read-only, writable and nested reference contracts
|
||||
const pkg = compileCapabilityResourceSource(
|
||||
`import interface Fields; package P id "p" revision "p@1" {
|
||||
function props id "props" : unit -> record {fields: interface-ref<Fields>; caption: string;};
|
||||
query Editor id "editor" root Fields document "editor.graphql" operation "Editor" {max rows 1; watch;}
|
||||
}`,
|
||||
{ source, environment: { interfaces: new Map([["Fields", iface.resource.revision]]) } },
|
||||
);
|
||||
assert.ok(pkg.ok && pkg.resource.kind === "package");
|
||||
if (!pkg.ok || pkg.resource.kind !== "package") throw new Error("package failed");
|
||||
pkg.resource.revision.checkedQueries = [
|
||||
await compileQuery(
|
||||
pkg.resource.revision.queries![0]!,
|
||||
[iface.resource.revision],
|
||||
async () => "query Editor {root {title @live summary @live plain: title}}",
|
||||
),
|
||||
];
|
||||
const schema = {
|
||||
format: "quixos-bindings",
|
||||
version: 1,
|
||||
@@ -68,7 +77,14 @@ test("React bindings preserve read-only, writable and nested reference contracts
|
||||
path.join(root, "consumer.ts"),
|
||||
`import {useLiveField, tryConform, type ReadableField, type WritableField} from "@quixos/web-studio-react-runtime";
|
||||
import {reactInterfaces} from "./react-props.gen.js";
|
||||
import type {ReactResults} from "./react-props.gen.js";
|
||||
import type {ReactResults, QueryResults} from "./react-props.gen.js";
|
||||
declare const query: QueryResults["Editor"];
|
||||
useLiveField(query.root.title).set("new");
|
||||
// @ts-expect-error live query fields retain exact setter types
|
||||
useLiveField(query.root.title).set(123);
|
||||
// @ts-expect-error readonly query fields do not acquire a setter
|
||||
useLiveField(query.root.summary).set("no");
|
||||
const plain: string = query.root.plain;
|
||||
async function lookup() {
|
||||
const view = await tryConform("object", reactInterfaces.Fields);
|
||||
if (!view) return;
|
||||
@@ -79,14 +95,14 @@ async function lookup() {
|
||||
await view.call["summary.set"]("no setter");
|
||||
}
|
||||
declare const props: ReactResults["props"];
|
||||
const [title, setTitle] = useLiveField(props.fields.fields.title);
|
||||
setTitle("new");
|
||||
const title = useLiveField(props.fields.fields.title);
|
||||
title.set("new");
|
||||
// @ts-expect-error wrong setter value
|
||||
setTitle(123);
|
||||
title.set(123);
|
||||
// @ts-expect-error read-only hook has no setter
|
||||
const [summary, setSummary] = useLiveField(props.fields.fields.summary);
|
||||
const [manual, write] = useLiveField(props.fields.fields.summary, {write: async (value: string) => {}});
|
||||
write("new");
|
||||
useLiveField(props.fields.fields.summary).set("no");
|
||||
const manual = useLiveField(props.fields.fields.summary, {id: "manual", write: async (value: string) => {}});
|
||||
manual.set("new");
|
||||
const readonly: ReadableField<string> = props.fields.fields.title;
|
||||
// @ts-expect-error read-only does not satisfy writable
|
||||
const writable: WritableField<string> = props.fields.fields.summary;
|
||||
@@ -94,7 +110,7 @@ declare const narrow: WritableField<"only">;
|
||||
// @ts-expect-error writable references are invariant
|
||||
const widened: WritableField<string> = narrow;
|
||||
// @ts-expect-error callbacks must accept the field's type
|
||||
useLiveField(props.fields.fields.summary, {write: async (value: number) => {}});
|
||||
useLiveField(props.fields.fields.summary, {id: "manual", write: async (value: number) => {}});
|
||||
`,
|
||||
);
|
||||
const result = spawnSync(
|
||||
|
||||
Reference in New Issue
Block a user