Expose confirmed server projections through the typed replica client
This commit is contained in:
@@ -53,29 +53,68 @@ export function generatePortableClientBindings(
|
||||
}
|
||||
const queries = new Map<string, string[]>();
|
||||
const views = new Map<string, string[]>();
|
||||
const serverProjections = new Map<string, string[]>();
|
||||
const results = new Map<string, string[]>();
|
||||
for (const query of plan.definition.queries) {
|
||||
const declaration = query.query.checked.declaration;
|
||||
if (!declaration.placement) continue;
|
||||
const pkg = workspace.packageImports.find((p) => p.revisionId === query.query.packageRevisionId)!;
|
||||
const alias = JSON.stringify([pkg.displayName, declaration.displayName]);
|
||||
if (seen.has(`query:${alias}`)) throw new PortableBindingError(alias, "Ambiguous query alias");
|
||||
seen.add(`query:${alias}`);
|
||||
if (declaration.placement.kind === "local") {
|
||||
const list = views.get(pkg.displayName) ?? [];
|
||||
list.push(
|
||||
`${JSON.stringify(declaration.displayName)}: ${JSON.stringify({ id: query.id, plan: plan.digest, source: JSON.stringify([query.query.packageRevisionId, declaration.placement.sourceQueryId]) })} as const satisfies LocalQueryDescriptor`,
|
||||
);
|
||||
views.set(pkg.displayName, list);
|
||||
const addResult = () => {
|
||||
if (conditional(query.query.checked.selection)) {
|
||||
unavailable.push({ key: query.id, reason: "CONDITIONAL_RESULT_DECODER_UNSUPPORTED" });
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
const decoders = results.get(pkg.displayName) ?? [];
|
||||
decoders.push(
|
||||
`${JSON.stringify(declaration.displayName)}: (data: Json): ${tsType(query.query.checked.output)} => decodeArgument(${JSON.stringify(portableValueSchema(query.query.checked.output))}, { codec: "camino-value-v1", usage: "argument", value: data }, { workspace: workspaceId(${JSON.stringify(workspace.workspaceId)}), limits: { bytes: ${declaration.budgets.resultBytes + 128}, nodes: 100000, depth: 64 } }) as ${tsType(query.query.checked.output)}`,
|
||||
);
|
||||
results.set(pkg.displayName, decoders);
|
||||
};
|
||||
if (!declaration.placement) {
|
||||
const operations = query.operations.map(
|
||||
(key) => plan.definition.operations.find((operation) => operation.key === key)!,
|
||||
);
|
||||
const live = (selections: readonly QuerySelection[]): boolean =>
|
||||
selections.some((selection) => !!selection.liveSelectionId || live(selection.selection));
|
||||
if (
|
||||
operations.some(
|
||||
(operation) => operation.execution.kind !== "snapshot" || !operation.execution.targets.includes("server"),
|
||||
) ||
|
||||
live(query.query.checked.selection)
|
||||
) {
|
||||
unavailable.push({ key: query.id, reason: "QUERY_NOT_SNAPSHOT_PROJECTION" });
|
||||
continue;
|
||||
}
|
||||
const list = serverProjections.get(pkg.displayName) ?? [];
|
||||
list.push(
|
||||
JSON.stringify(declaration.displayName) +
|
||||
": " +
|
||||
JSON.stringify({
|
||||
kind: "server-projection",
|
||||
id: query.id,
|
||||
plan: plan.digest,
|
||||
root: declaration.root.interface,
|
||||
rootKind: declaration.root.kind,
|
||||
}) +
|
||||
" as const satisfies ServerProjection<" +
|
||||
JSON.stringify(declaration.root.interface) +
|
||||
", " +
|
||||
JSON.stringify(declaration.root.kind) +
|
||||
">",
|
||||
);
|
||||
serverProjections.set(pkg.displayName, list);
|
||||
addResult();
|
||||
continue;
|
||||
}
|
||||
if (declaration.placement.kind === "local") {
|
||||
const list = views.get(pkg.displayName) ?? [];
|
||||
list.push(
|
||||
`${JSON.stringify(declaration.displayName)}: ${JSON.stringify({ id: query.id, plan: plan.digest, source: JSON.stringify([query.query.packageRevisionId, declaration.placement.sourceQueryId]) })} as const satisfies LocalQueryDescriptor`,
|
||||
);
|
||||
views.set(pkg.displayName, list);
|
||||
addResult();
|
||||
continue;
|
||||
}
|
||||
const list = queries.get(pkg.displayName) ?? [];
|
||||
@@ -86,5 +125,5 @@ export function generatePortableClientBindings(
|
||||
}
|
||||
const groups = (entries: Map<string, string[]>) =>
|
||||
[...entries].map(([name, values]) => `${JSON.stringify(name)}: {${values.join(",\n")}}`).join(",\n");
|
||||
return `// Generated application descriptors. Regenerate; do not edit.\nimport { defineOperationDescriptor, decodeArgument, workspaceId, type Json, type PreparedChange, type ManagedReference, type ReplicaProjection, type LocalQueryDescriptor } from ${JSON.stringify(options.runtimeModule ?? "@quixos/camino-replica-core")};\nexport const planDigest = ${JSON.stringify(plan.digest)};\nexport const operations = {${groups(atoms)}} as const;\nexport const queries = {${groups(queries)}} as const;\nexport const views = {${groups(views)}} as const;\nexport const results = {${groups(results)}} as const;\nexport const unavailable = ${JSON.stringify(unavailable)} as const;\n`;
|
||||
return `// Generated application descriptors. Regenerate; do not edit.\nimport { defineOperationDescriptor, decodeArgument, workspaceId, type Json, type PreparedChange, type ManagedReference, type ReplicaProjection, type ServerProjection, type LocalQueryDescriptor } from ${JSON.stringify(options.runtimeModule ?? "@quixos/camino-replica-core")};\nexport const planDigest = ${JSON.stringify(plan.digest)};\nexport const operations = {${groups(atoms)}} as const;\nexport const queries = {${groups(queries)}} as const;\nexport const views = {${groups(views)}} as const;\nexport const serverProjections = {${groups(serverProjections)}} as const;\nexport const results = {${groups(results)}} as const;\nexport const unavailable = ${JSON.stringify(unavailable)} as const;\n`;
|
||||
}
|
||||
|
||||
@@ -82,4 +82,5 @@ revision "package:reader-runtime@1" {
|
||||
query HighlightWindowList id "reader:highlight-window-list" root workspace HighlightData document "queries/highlight-list.graphql" operation "HighlightList" { view Comment as CommentData; local from HighlightWindow; max rows 200; watch; }
|
||||
query HighlightEditorInputs id "reader:editor-inputs" root HighlightData document "queries/editor-inputs.graphql" operation "HighlightEditorInputs" { view Comment as CommentData; replica; max rows 1; }
|
||||
query HighlightEditorView id "reader:editor-view" root HighlightData document "queries/editor-view.graphql" operation "HighlightEditorView" { view Comment as CommentData; local from HighlightEditorInputs; max rows 1; watch; }
|
||||
query ConfirmedColor id "reader:confirmed-color" root HighlightData document "queries/confirmed-color.graphql" operation "ConfirmedColor" { view Comment as CommentData; max rows 1; watch; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
query ConfirmedColor {
|
||||
root { color }
|
||||
}
|
||||
@@ -8,7 +8,7 @@ test("complete reader contracts compose factories, portable selectors, CRDT edit
|
||||
assert.equal(workspace.constructors.length, 0);
|
||||
assert.equal(workspace.atoms.length, 3);
|
||||
const linked = linkQueries(workspace);
|
||||
assert.equal(linked.length, 8);
|
||||
assert.equal(linked.length, 9);
|
||||
assert.equal(linked.filter((q) => q.checked.declaration.placement?.kind === "local").length, 4);
|
||||
const screen = linked.find((q) => q.checked.declaration.displayName === "ReaderScreen")!;
|
||||
assert.ok(screen.fields.some((f) => f.memberId === "highlight:excerpt" && f.binding.kind === "package"));
|
||||
|
||||
@@ -79,3 +79,27 @@ test("a portable subset of a mixed interface excludes remote dependencies", asyn
|
||||
selected.operations!.push(operation.id);
|
||||
assert.throws(generate, /select a portable subset/);
|
||||
});
|
||||
|
||||
test("server projection descriptors are distinct from canonical inputs and have result decoders", async () => {
|
||||
const { generatePortableClientBindings } = await import("../src/bindings/portable-client.js");
|
||||
const plan = compileExecutionPlan(await loadReaderWorkspace());
|
||||
const source = generatePortableClientBindings(plan, { target: "browser" });
|
||||
const projections = source.slice(
|
||||
source.indexOf("export const serverProjections"),
|
||||
source.indexOf("export const results"),
|
||||
);
|
||||
assert.match(projections, /ConfirmedColor/);
|
||||
assert.match(projections, /satisfies ServerProjection/);
|
||||
assert.match(source.slice(source.indexOf("export const results")), /ConfirmedColor/);
|
||||
const live = compileExecutionPlan(
|
||||
await loadReaderWorkspace((name, text) =>
|
||||
name === "queries/confirmed-color.graphql" ? text.replace("color", "color @live") : text,
|
||||
),
|
||||
);
|
||||
const rejected = generatePortableClientBindings(live, { target: "browser" });
|
||||
assert.doesNotMatch(
|
||||
rejected.slice(rejected.indexOf("export const serverProjections"), rejected.indexOf("export const results")),
|
||||
/ConfirmedColor/,
|
||||
);
|
||||
assert.match(rejected, /QUERY_NOT_SNAPSHOT_PROJECTION/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user