Export template source from be30143a326d5d7d7b0cbabea5bc5ac0301ef5b9:quixos-instance/packages/web-studio-display-runtime

This commit is contained in:
Quixos Template Publisher
2000-01-01 00:00:00 +00:00
commit 246e0644a9
14 changed files with 311 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
{
"monorepoCommit": "be30143a326d5d7d7b0cbabea5bc5ac0301ef5b9",
"sourcePath": "quixos-instance/packages/web-studio-display-runtime"
}
+61
View File
@@ -0,0 +1,61 @@
# Browser displays
This package owns ordinary graph transactions for browser enrollment, assignment
and reported mount state. Studio supplies the host adapter; the package contains
no browser heartbeat, DOM code or private storage API.
A workspace entry conforms to `WebStudioDisplayDirectory`. Its conformance owns
an enrollment edge, and `enroll({nonce,label})` returns the same target for that
nonce. Each directory currently permits 128 targets and rejects further enrollment
explicitly. This is a bounded prototype, not an automatic device inventory GC.
`WebStudioDisplayTarget.assign` accepts an optional subject/presentation pair.
The subject conforms to `WebStudioPresentable`, whose `canPresent` validates the
selected registered presentation. This role does not require Canvas or placements;
the example workspace adapters forward their existing presentation check. Named
presentations have checked subject-only module contracts; callbacks cannot be
stored in assignments. The target's subject is a graph edge, not a string ID in
JSON. Changing the edge and desired generation is one transaction.
Assignments and observed outcomes are durable. `acknowledge` accepts a generation,
monotonic report sequence, status (`mounted`, `failed`, `empty`) and bounded error.
Old generations and reordered reports return false without modifying state.
Reported success describes a client mount; it is not a claim of trusted execution.
The browser adapter uses a per-tab nonce and Web Lock. Duplicated tabs get distinct
nonces; reloading resumes enrollment. Enrollment is a host startup/user action,
never business-object creation from a component render. Multiple displays may
mount the same subject. Losing the connection does not clear its desired state.
Studio's authenticated presence adapter validates target references against Camino
before issuing an opaque connection lease. Leases expire after 45 seconds; recent
connection observations remain for at most ten minutes, with a 256-record cap.
A target ID is not a credential. There is no persistent heartbeat history.
## Headless calls
A headless session uses the same generated descriptors and public operations as
Studio. The caller supplies normal workspace authorization when opening its
session; graph references alone grant no access.
```ts
const directory = await session.acquire(
{ enroll: interfaces.WebStudioDisplayDirectory.enroll },
session.bootstrap.entry,
);
const enrollment = await directory.enroll.call({
nonce: savedClientNonce,
label: "Preview display",
}).settled;
if (enrollment.status !== "accepted") throw Error(enrollment.status);
const target = await session.acquire(interfaces.WebStudioDisplayTarget, enrollment.value);
const result = await target.assign.call({
subject: presentableSubject,
presentation: "presentation:studio:canvas",
}).settled;
```
`tools/local-first/studio-session.test.mjs` is the executable example, including
reference rejection and stale-report tests. `package.nix` selects the server-only
TypeScript artifact. Native fields and relationships remain queryable in browser
replicas without bundling mutation implementations there.
Generated
+108
View File
@@ -0,0 +1,108 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"helpers": {
"flake": false,
"locked": {
"lastModified": 1790852599,
"narHash": "sha256-MdNtZpcsoguLPSCpAy9Naz6fkVPrftbZQbz1Aj7Zm9A=",
"ref": "refs/tags/quixos-reachability/91679e8cc82133093cb0b4f9e519cbb39a5bba4f",
"rev": "91679e8cc82133093cb0b4f9e519cbb39a5bba4f",
"revCount": 54,
"type": "git",
"url": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git"
},
"original": {
"ref": "refs/tags/quixos-reachability/91679e8cc82133093cb0b4f9e519cbb39a5bba4f",
"rev": "91679e8cc82133093cb0b4f9e519cbb39a5bba4f",
"type": "git",
"url": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1783776592,
"narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"protocol": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1791261504,
"narHash": "sha256-QSk9DWR7n5KL6cMHzmSKKXNIyaFA0s8Nu+wXjhhlENI=",
"ref": "refs/tags/quixos-reachability/5b44c8154dc038efd87cf1cbdd2c00b66bfd4f87",
"rev": "5b44c8154dc038efd87cf1cbdd2c00b66bfd4f87",
"revCount": 184,
"type": "git",
"url": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git"
},
"original": {
"ref": "refs/tags/quixos-reachability/5b44c8154dc038efd87cf1cbdd2c00b66bfd4f87",
"rev": "5b44c8154dc038efd87cf1cbdd2c00b66bfd4f87",
"type": "git",
"url": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git"
}
},
"root": {
"inputs": {
"flake-utils": [
"protocol",
"flake-utils"
],
"helpers": "helpers",
"nixpkgs": [
"protocol",
"nixpkgs"
],
"protocol": "protocol"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+37
View File
@@ -0,0 +1,37 @@
{
inputs.protocol.url = "git+https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git?ref=refs/tags/quixos-reachability/5b44c8154dc038efd87cf1cbdd2c00b66bfd4f87&rev=5b44c8154dc038efd87cf1cbdd2c00b66bfd4f87";
inputs.nixpkgs.follows = "protocol/nixpkgs";
inputs.flake-utils.follows = "protocol/flake-utils";
inputs.helpers = {
url = "git+https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git?ref=refs/tags/quixos-reachability/91679e8cc82133093cb0b4f9e519cbb39a5bba4f&rev=91679e8cc82133093cb0b4f9e519cbb39a5bba4f";
flake = false;
};
outputs =
{
nixpkgs,
flake-utils,
helpers,
...
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs { inherit system; };
camino = import (helpers + "/quixos-package-helpers.nix");
in
{
quixosPackages.checkedArtifacts =
context:
let
pkgs = camino.packageSet { inherit nixpkgs system context; };
in
camino.mkCaminoTypeScriptPackage {
inherit pkgs context;
src = ./.;
targets = [ "server" ];
entry = "src/portable.ts";
};
devShells.default = camino.mkCaminoYarnShell { inherit pkgs; };
}
);
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "@quixos/web-studio-display-runtime",
"private": true,
"type": "module"
}
+26
View File
@@ -0,0 +1,26 @@
import interface PresentationMount;
import interface WebStudioDisplayTarget;
import interface WebStudioDisplayDirectory;
import interface WebStudioPresentable;
external atom DisplayTarget id "atom:org.quixos.web-studio:display-target";
type Assignment = record {subject:interface-ref<WebStudioPresentable>;presentation:string;presentationMount:optional<interface-ref<PresentationMount>>;};
type Observed = record {generation:uint32;sequence:uint32;status:string;error:string;};
type DisplayState = record {label:string;generation:uint32;presentation:optional<string>;observed:optional<Observed>;};
package WebStudioDisplayRuntime id "package:org.quixos.web-studio:display-runtime" revision "package:org.quixos.web-studio:display-runtime@1" {
operation enroll id "displays:enroll" : record {nonce:string;label:string;} -> interface-ref<WebStudioDisplayTarget> mode call execution transaction targets [server] receiver any requires {
edge enrolled id "enrolled" : many-unique interface WebStudioDisplayTarget [resolve,connect];
interface existing id "existing" : WebStudioDisplayTarget using [nonce.get] from input;
creation target id "target" : DisplayTarget input record {nonce:string;initial:DisplayState;};
};
operation assign id "displays:assign" : optional<Assignment> -> uint32 mode call execution transaction targets [server] receiver any requires {
state data id "data" : DisplayState [read,write];
edge mount id "mount" : optional-one interface PresentationMount [resolve,connect,disconnect];
edge selected id "selected" : optional-one interface WebStudioPresentable [resolve,connect,disconnect];
interface subject id "subject" : WebStudioPresentable using [canPresent.call] from input;
};
operation acknowledge id "displays:acknowledge" : Observed -> bool mode call execution transaction targets [server] receiver any requires {state data id "data" : DisplayState [read,write];};
query Inputs id "displays:inputs" root WebStudioDisplayTarget document "queries/inputs.graphql" operation "Inputs" {replica;}
query View id "displays:view" root WebStudioDisplayTarget document "queries/view.graphql" operation "View" {local from Inputs;watch;}
query ListInputs id "displays:list-inputs" root workspace WebStudioDisplayTarget document "queries/list-inputs.graphql" operation "ListInputs" {replica;}
query List id "displays:list" root workspace WebStudioDisplayTarget document "queries/list.graphql" operation "List" {local from ListInputs;watch;}
}
+1
View File
@@ -0,0 +1 @@
query Inputs { root { _camino {ref} subject {_camino {ref}} presentationMount {_camino {ref}} status { label generation presentation observed {generation sequence status error} } } }
+1
View File
@@ -0,0 +1 @@
query ListInputs($after: Cursor) { root { objects(first:30,after:$after) { entries {node {_camino {ref} subject {_camino {ref}} presentationMount {_camino {ref}} status {label generation presentation observed {generation sequence status error}}}} pageInfo {hasNextPage endCursor} } } }
+1
View File
@@ -0,0 +1 @@
query List($after: Cursor) { root { objects(first:30,after:$after) { entries {node {_camino {ref} subject {_camino {ref}} presentationMount {_camino {ref}} status {label generation presentation observed {generation sequence status error}}}} pageInfo {hasNextPage endCursor} } } }
+1
View File
@@ -0,0 +1 @@
query View { root { _camino {ref} subject {_camino {ref}} presentationMount {_camino {ref}} status { label generation presentation observed {generation sequence status error} } } }
+1
View File
@@ -0,0 +1 @@
{ "backend": "execution" }
+7
View File
@@ -0,0 +1,7 @@
quixos-lock version 1 {
quixos source { repository "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos.git"; commit "be30143a326d5d7d7b0cbabea5bc5ac0301ef5b9"; }
interface PresentationMount source { repository "https://repos.quixos.org/quixos-studio/interface-presentation-mount.git"; commit "30566340643355d8509bf116c2c3d91f09173d98"; }
interface WebStudioDisplayTarget source { repository "https://repos.quixos.org/quixos-studio/interface-web-studio-display-target.git"; commit "1360b6175a77b8e69a5db83eaa63910d60699651"; }
interface WebStudioDisplayDirectory source { repository "https://repos.quixos.org/quixos-studio/interface-web-studio-display-directory.git"; commit "b6b70722e81f75ff2cd969118c5b8d0bba240a00"; }
interface WebStudioPresentable source { repository "https://repos.quixos.org/quixos-studio/interface-web-studio-presentable.git"; commit "f7b38219572810912fad596c7e4a33903209f71b"; }
}
+47
View File
@@ -0,0 +1,47 @@
import { QxReject } from "@quixos/camino-replica-core";
import { createRegistry } from "./gen/qx.js";
export const registry = createRegistry({
enroll: async ({ input, ports }) => {
if (!/^[a-f0-9-]{36}$/.test(input.nonce) || input.label.length > 100)
throw new QxReject("INVALID_DISPLAY_ENROLLMENT");
const enrolled = await ports.enrolled.resolve();
for (const previous of enrolled) {
if ((await ports.existing["nonce.get"]({ receiver: previous.target, input: null })) === input.nonce)
return previous.target;
}
if (enrolled.length >= 128) throw new QxReject("DISPLAY_ENROLLMENT_LIMIT");
const target = await ports.target.create(
{ nonce: input.nonce, initial: { label: input.label, generation: 0, presentation: null, observed: null } },
{ key: "display" },
);
const ref = {
...target,
expectation: { kind: "interface" as const, id: "interface:org.quixos.web-studio.display-target@1" as const },
};
await ports.enrolled.connect(ref, { key: "display" });
return ref;
},
assign: async ({ input, ports }) => {
if (input && !(await ports.subject["canPresent.call"]({ receiver: input.subject, input: input.presentation })))
throw new QxReject("INVALID_PRESENTATION");
const state = await ports.data.get();
if (state.generation === 4294967295) throw new QxReject("DISPLAY_GENERATION_EXHAUSTED");
const generation = state.generation + 1;
for (const previous of await ports.selected.resolve()) await ports.selected.disconnect(previous.entry);
if (input) await ports.selected.connect(input.subject, { key: "subject" });
for (const previous of await ports.mount.resolve()) await ports.mount.disconnect(previous.entry);
if (input?.presentationMount) await ports.mount.connect(input.presentationMount, { key: "presentationMount" });
await ports.data.set({ ...state, generation, presentation: input?.presentation ?? null, observed: null });
return generation;
},
acknowledge: async ({ input, ports }) => {
if (!["mounted", "failed", "empty"].includes(input.status) || input.error.length > 2000)
throw new QxReject("INVALID_DISPLAY_REPORT");
const state = await ports.data.get();
if (input.generation !== state.generation || (state.observed && input.sequence <= state.observed.sequence))
return false;
if ((state.presentation === null) !== (input.status === "empty")) throw new QxReject("INVALID_DISPLAY_REPORT");
await ports.data.set({ ...state, observed: input });
return true;
},
});
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"strict": true,
"target": "ES2024",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"jsx": "react-jsx",
"noEmit": true
},
"include": ["src/**/*.ts", "src/**/*.tsx"]
}