Export template source from be30143a326d5d7d7b0cbabea5bc5ac0301ef5b9:quixos-instance/packages/web-studio-display-runtime
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"monorepoCommit": "be30143a326d5d7d7b0cbabea5bc5ac0301ef5b9",
|
||||
"sourcePath": "quixos-instance/packages/web-studio-display-runtime"
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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; };
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "@quixos/web-studio-display-runtime",
|
||||
"private": true,
|
||||
"type": "module"
|
||||
}
|
||||
+26
@@ -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;}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
query Inputs { root { _camino {ref} subject {_camino {ref}} presentationMount {_camino {ref}} status { label generation presentation observed {generation sequence status error} } } }
|
||||
@@ -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} } } }
|
||||
@@ -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} } } }
|
||||
@@ -0,0 +1 @@
|
||||
query View { root { _camino {ref} subject {_camino {ref}} presentationMount {_camino {ref}} status { label generation presentation observed {generation sequence status error} } } }
|
||||
@@ -0,0 +1 @@
|
||||
{ "backend": "execution" }
|
||||
@@ -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"; }
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "ES2024",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"jsx": "react-jsx",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||
}
|
||||
Reference in New Issue
Block a user