Export template source from 488f7e229afed8f03fc3bfa4abe707da0c613460:quixos-instance/packages/web-studio-display-runtime
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"monorepoCommit": "488f7e229afed8f03fc3bfa4abe707da0c613460",
|
||||||
|
"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 "488f7e229afed8f03fc3bfa4abe707da0c613460"; }
|
||||||
|
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