From 6c90652aa278403ff443141dd81d2a201dbbe862 Mon Sep 17 00:00:00 2001 From: Quixos Template Publisher Date: Sat, 1 Jan 2000 00:00:00 +0000 Subject: [PATCH] Export template source from 122c862dccc4a1825f19cc8d4eb1095e5661f1ea:quixos-instance/packages/presentation-runtime --- .quixos-template-source.json | 4 + flake.lock | 108 +++++++++++++++++++ flake.nix | 44 ++++++++ package.json | 9 ++ package.qx | 51 +++++++++ queries/root.graphql | 18 ++++ queries/subject.graphql | 3 + quixos.lock | 9 ++ src/portable.ts | 204 +++++++++++++++++++++++++++++++++++ 9 files changed, 450 insertions(+) create mode 100644 .quixos-template-source.json create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 package.json create mode 100644 package.qx create mode 100644 queries/root.graphql create mode 100644 queries/subject.graphql create mode 100644 quixos.lock create mode 100644 src/portable.ts diff --git a/.quixos-template-source.json b/.quixos-template-source.json new file mode 100644 index 0000000..fa1c939 --- /dev/null +++ b/.quixos-template-source.json @@ -0,0 +1,4 @@ +{ + "monorepoCommit": "122c862dccc4a1825f19cc8d4eb1095e5661f1ea", + "sourcePath": "quixos-instance/packages/presentation-runtime" +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..54403f0 --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..fc1df39 --- /dev/null +++ b/flake.nix @@ -0,0 +1,44 @@ +{ + 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; }; + dependencies = + if builtins.pathExists ./yarn-project.nix then + camino.mkCaminoYarnDependencies { + inherit pkgs; + src = ./.; + } + else + null; + in + (camino.mkCaminoTypeScriptPackage { + inherit pkgs context dependencies; + src = ./.; + entry = "src/portable.ts"; + }); + devShells.default = camino.mkCaminoYarnShell { inherit pkgs; }; + } + ); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..36633d4 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "@quixos/presentation-runtime", + "private": true, + "type": "module", + "packageManager": "yarn@4.18.0", + "scripts": { + "typecheck": "qx-workspace check" + } +} diff --git a/package.qx b/package.qx new file mode 100644 index 0000000..6d9d53d --- /dev/null +++ b/package.qx @@ -0,0 +1,51 @@ +symbol JavaScript id "target:org.quixos.execution.javascript@1"; +import interface PresentationStates; +import interface PresentationSubject; +import interface PresentationStateValues; +import interface PresentationState; +import interface PresentationMount; +import interface PresentationStateFactory; +external atom PresentationMountRecord id "atom:org.quixos.presentation-mount"; +package PresentationRuntime id "package:org.quixos.presentation-runtime" revision "package:org.quixos.presentation-runtime@1" { + operation copyEmpty id "presentation-runtime:copy-empty" : optional> -> interface-ref mode call execution transaction targets [JavaScript,server] receiver any requires {creation presentationMount id "presentationMount" : PresentationMountRecord input unit;}; + operation findEmpty id "presentation-runtime:find-empty" : string -> optional> mode call execution snapshot targets [JavaScript,server] receiver any requires {}; + operation attachEmpty id "presentation-runtime:attach-empty" : interface-ref -> unit mode call execution transaction targets [JavaScript,server] receiver any requires {}; + function createPresentationMount id "presentation-runtime:create" : unit -> interface-ref execution transaction targets [JavaScript,server] requires { + creation presentationMount id "presentationMount" : PresentationMountRecord input unit; + }; + operation attach id "presentation-runtime:attach" : record {presentationMount:interface-ref;parent:optional>;} -> unit mode call execution transaction targets [JavaScript,server] receiver any requires { + edge owner id "owner" : optional-one interface PresentationMount [resolve]; + edge parent id "parent" : optional-one interface PresentationState [resolve,connect]; + edge rootOf id "rootOf" : optional-one interface PresentationMount [connect]; + interface metadata id "metadata" : PresentationState using [presentationMount.resolve] from input; + interface presentationMount id "presentationMount" : PresentationMount using [root.resolve,attach.call] from input; + }; + operation claim id "presentation-runtime:claim" : interface-ref -> unit mode call execution transaction targets [JavaScript,server] receiver any requires { + edge members id "members" : many-unique interface PresentationState keyed "string" [connect]; + edge root id "root" : optional-one interface PresentationState [resolve]; + interface metadata id "metadata" : PresentationState using [occurrence.get,presentationMount.resolve,parent.resolve] from input; + }; + operation create id "presentation-runtime:create-state" : record {presentationMount:interface-ref;occurrence:string;parent:optional>;} -> ref mode call execution transaction targets [JavaScript,server] receiver any requires { + interface metadata id "state" : PresentationState using [occurrence.get,presentationMount.resolve,parent.resolve,source.resolve,attach.call] from input; + interface factory id "factory" : PresentationStateFactory using [create.call]; + }; + operation ensure id "presentation-runtime:ensure-state" : record {presentationMount:interface-ref;occurrence:string;parent:optional>;} -> unit mode call execution transaction targets [JavaScript,server] receiver any requires { + interface presentationMount id "presentationMount" : PresentationMount using [find.call] from input; + interface metadata id "state" : PresentationState using [occurrence.get,presentationMount.resolve,parent.resolve,source.resolve,attach.call] from input; + interface factory id "factory" : PresentationStateFactory using [create.call]; + }; + operation find id "presentation-runtime:find" : string -> optional> mode call execution snapshot targets [JavaScript,server] receiver any requires { + edge members id "members" : many-unique interface PresentationState keyed "string" [resolve]; + }; + operation allocate id "presentation-runtime:allocate" : record {presentationMount:interface-ref;occurrence:string;parent:optional>;} -> interface-ref mode call execution transaction targets [JavaScript,server] receiver any requires {interface source id "source" : PresentationStates using [create.call];}; + operation copy id "presentation-runtime:copy" : optional> -> interface-ref mode call execution transaction targets [JavaScript,server] receiver any requires { + edge members id "members" : many-unique interface PresentationState keyed "string" [resolve]; + edge root id "root" : optional-one interface PresentationState [resolve]; + interface metadata id "metadata" : PresentationState using [parent.resolve,occurrence.get,presentationMount.resolve,allocate.call,copyValues.call] from input; + creation presentationMount id "presentationMount" : PresentationMountRecord input unit; + }; + query RootInputs id "presentation:root-inputs" root PresentationMount document "queries/root.graphql" operation "PresentationRoot" {replica;max rows 5000;max result-bytes 8388608;} + query RootView id "presentation:root-view" root PresentationMount document "queries/root.graphql" operation "PresentationRoot" {local from RootInputs;watch;max rows 5000;max result-bytes 8388608;} + query SubjectInputs id "presentation:subject-inputs" root PresentationSubject document "queries/subject.graphql" operation "PresentationSubject" {replica;} + query SubjectView id "presentation:subject-view" root PresentationSubject document "queries/subject.graphql" operation "PresentationSubject" {local from SubjectInputs;watch;} +} diff --git a/queries/root.graphql b/queries/root.graphql new file mode 100644 index 0000000..608f900 --- /dev/null +++ b/queries/root.graphql @@ -0,0 +1,18 @@ +query PresentationRoot { + root { + members(all: 512) { + entries { + mapKey + node { + source { _camino { ref } } + parent { _camino { ref } } + } + } + } + root { + _camino { ref } + occurrence + source { _camino { ref } } + } + } +} diff --git a/queries/subject.graphql b/queries/subject.graphql new file mode 100644 index 0000000..052c6e5 --- /dev/null +++ b/queries/subject.graphql @@ -0,0 +1,3 @@ +query PresentationSubject { + root { _camino { ref } } +} diff --git a/quixos.lock b/quixos.lock new file mode 100644 index 0000000..e8aec19 --- /dev/null +++ b/quixos.lock @@ -0,0 +1,9 @@ +quixos-lock version 1 { + quixos source { repository "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos.git"; commit "122c862dccc4a1825f19cc8d4eb1095e5661f1ea"; } + interface PresentationStates source { repository "https://repos.quixos.org/quixos-studio/interface-presentation-states.git"; commit "9828a789b25e8a009f6b55cb32cf7e9ce1e3ac3c"; } + interface PresentationSubject source { repository "https://repos.quixos.org/quixos-studio/interface-presentation-subject.git"; commit "26b1dc891485fa268570718bf283f440691654a7"; } + interface PresentationStateValues source { repository "https://repos.quixos.org/quixos-studio/interface-presentation-state-values.git"; commit "50e11ed980b909f428e99a9b30ede1bb24941ab7"; } + interface PresentationState source { repository "https://repos.quixos.org/quixos-studio/interface-presentation-state.git"; commit "94db4272bcca52fbb23325bfa305d7331ea08602"; } + interface PresentationMount source { repository "https://repos.quixos.org/quixos-studio/interface-presentation-mount.git"; commit "30566340643355d8509bf116c2c3d91f09173d98"; } + interface PresentationStateFactory source { repository "https://repos.quixos.org/quixos-studio/interface-presentation-state-factory.git"; commit "87685dc5e408f93a778a83e31463734b8df7709a"; } +} diff --git a/src/portable.ts b/src/portable.ts new file mode 100644 index 0000000..6673b2f --- /dev/null +++ b/src/portable.ts @@ -0,0 +1,204 @@ +import { QxReject, type ExactReference, type MutationKey } from "@quixos/camino-replica-core"; +import { createRegistry, generic, type Contexts } from "./gen/qx.js"; +const metadata = (ref: ExactReference) => ({ + ...ref, + expectation: { kind: "interface" as const, id: "interface:org.quixos.presentation-state@1" as const }, +}); +const same = (a: ExactReference | null, b: ExactReference | null) => + a === null || b === null ? a === b : a.workspace === b.workspace && a.object === b.object; + +// These source contracts also exist in assemblies with no closed navigation +// application. Generated Contexts contains only the applications actually used. +type NavigationInput = Contexts["attach"]["input"] & { occurrence: string }; +type MetadataRequest = { receiver: ReturnType; input: null }; +type NavigationMetadata = { + "occurrence.get"(request: MetadataRequest): Promise; + "source.resolve"(request: MetadataRequest): Promise; + "parent.resolve"(request: MetadataRequest): Promise; + "attach.call"( + request: { receiver: ReturnType; input: Contexts["attach"]["input"] }, + key: MutationKey, + ): Promise; +}; +type CreateContext = { + receiver: ExactReference; + input: NavigationInput; + ports: { + metadata: NavigationMetadata; + factory: { "create.call"(input: { occurrence: string }, key: MutationKey): Promise }; + }; +}; +type EnsureContext = CreateContext & { + ports: { + presentationMount: { + "find.call"(request: { + receiver: NavigationInput["presentationMount"]; + input: string; + }): Promise | null>; + }; + }; +}; + +async function attach({ receiver, input, ports }: Contexts["attach"]) { + const owner = (await ports.owner.resolve())[0]?.target ?? null; + const parent = (await ports.parent.resolve())[0]?.target ?? null; + if (owner) { + if (!same(owner, input.presentationMount)) throw new QxReject("PRESENTATION_STATE_ALREADY_ATTACHED"); + if (!same(parent, input.parent)) throw new QxReject("OCCURRENCE_PARENT_MISMATCH"); + return null; + } + if (parent) throw new QxReject("UNATTACHED_PRESENTATION_PARENT"); + if (input.parent) { + const parentOwner = await ports.metadata["presentationMount.resolve"]({ receiver: input.parent, input: null }); + if (!same(parentOwner, input.presentationMount)) throw new QxReject("PRESENTATION_PARENT_MISMATCH"); + await ports.parent.connect(input.parent, { key: "parent" }); + } else { + if (await ports.presentationMount["root.resolve"]({ receiver: input.presentationMount, input: null })) + throw new QxReject("PRESENTATION_ROOT_EXISTS"); + await ports.rootOf.connect(input.presentationMount, { key: "root" }); + } + // The parent must already be attached. No operation reparents attached state, + // so attachment cannot introduce a cycle, including through pending births. + await ports.presentationMount["attach.call"]( + { receiver: input.presentationMount, input: metadata(receiver) }, + { key: "owner" }, + ); + return null; +} + +async function create({ + receiver, + input, + ports, +}: CreateContext): Promise { + if (!input.occurrence || input.occurrence.length > 128) throw new QxReject("INVALID_OCCURRENCE"); + const navigation = await ports.factory["create.call"]({ occurrence: input.occurrence }, { key: "navigation" }); + if ((await ports.metadata["occurrence.get"]({ receiver: metadata(navigation), input: null })) !== input.occurrence) + throw new QxReject("PRESENTATION_OCCURRENCE_MISMATCH"); + if (!same(await ports.metadata["source.resolve"]({ receiver: metadata(navigation), input: null }), receiver)) + throw new QxReject("PRESENTATION_SOURCE_MISMATCH"); + await ports.metadata["attach.call"]( + { receiver: metadata(navigation), input: { presentationMount: input.presentationMount, parent: input.parent } }, + { key: "attach" }, + ); + return navigation; +} +export const registry = createRegistry({ + copyEmpty: async ({ input, ports }) => { + if (input !== null) throw new QxReject("PRESENTATION_HAS_NO_STATE"); + return { + ...(await ports.presentationMount.create({ key: "copy" })), + expectation: { kind: "interface", id: "interface:org.quixos.presentation-mount@1" }, + }; + }, + findEmpty: async () => null, + attachEmpty: async () => { + throw new QxReject("PRESENTATION_STATE_NOT_INSTALLED"); + }, + ...generic.allocate(allocate), + copy, + ...generic.create(create), + find: async ({ input, ports }) => + (await ports.members.resolve()).find((entry) => entry.mapKey === input)?.target ?? null, + ...generic.ensure(async ({ receiver, input, ports }: EnsureContext) => { + const existing = await ports.presentationMount["find.call"]({ + receiver: input.presentationMount, + input: input.occurrence, + }); + if (existing) { + if (!same(await ports.metadata["source.resolve"]({ receiver: existing, input: null }), receiver)) + throw new QxReject("PRESENTATION_SOURCE_MISMATCH"); + if (!same(await ports.metadata["parent.resolve"]({ receiver: existing, input: null }), input.parent)) + throw new QxReject("OCCURRENCE_PARENT_MISMATCH"); + } else { + await create({ receiver, input, ports }); + } + return null; + }), + attach, + claim: async ({ receiver, input, ports }) => { + const request = { receiver: input, input: null }; + const owner = await ports.metadata["presentationMount.resolve"](request); + if (owner) throw new QxReject("PRESENTATION_STATE_ALREADY_ATTACHED"); + const parent = await ports.metadata["parent.resolve"](request); + if (parent) { + if (!same(await ports.metadata["presentationMount.resolve"]({ receiver: parent, input: null }), receiver)) + throw new QxReject("PRESENTATION_PARENT_MISMATCH"); + } else if (!same((await ports.root.resolve())[0]?.target ?? null, input)) { + throw new QxReject("PRESENTATION_ROOT_MISMATCH"); + } + const occurrence = await ports.metadata["occurrence.get"](request); + if (!occurrence || occurrence.length > 128) throw new QxReject("INVALID_OCCURRENCE"); + await ports.members.connect({ target: input, mapKey: occurrence }, { key: "member" }); + return null; + }, + createPresentationMount: async ({ ports }) => ({ + ...(await ports.presentationMount.create({ key: "presentationMount" })), + expectation: { kind: "interface", id: "interface:org.quixos.presentation-mount@1" }, + }), +}); + +async function allocate({ + input, + ports, +}: { + input: NavigationInput; + ports: { source: { "create.call"(input: NavigationInput, key: MutationKey): Promise } }; +}) { + return metadata(await ports.source["create.call"](input, { key: "allocate" })); +} +async function copy({ receiver, input, ports }: Contexts["copy"]) { + const members = (await ports.members.resolve()).map((entry) => entry.target); + if (members.length > 512) throw new QxReject("PRESENTATION_COPY_LIMIT"); + const originalRoot = (await ports.root.resolve())[0]?.target ?? null; + const requestedRoot = input ?? originalRoot; + if (!requestedRoot && members.length) throw new QxReject("PRESENTATION_ROOT_MISSING"); + if (requestedRoot && !members.some((member) => same(member, requestedRoot))) + throw new QxReject("PRESENTATION_COPY_ROOT_MISMATCH"); + const byId = new Map(members.map((member) => [member.object, member])); + const children = new Map(); + for (const member of members) { + const owner = await ports.metadata["presentationMount.resolve"]({ receiver: member, input: null }); + if (!same(owner, receiver)) throw new QxReject("PRESENTATION_OWNER_MISMATCH"); + const parent = await ports.metadata["parent.resolve"]({ receiver: member, input: null }); + if (parent) { + if (!byId.has(parent.object)) throw new QxReject("PRESENTATION_PARENT_MISMATCH"); + const siblings = children.get(parent.object) ?? []; + siblings.push(member); + children.set(parent.object, siblings); + } else if (!same(member, originalRoot)) throw new QxReject("PRESENTATION_ROOT_MISMATCH"); + } + const queue = requestedRoot ? [requestedRoot] : []; + const visited = new Set(); + for (let index = 0; index < queue.length; index++) { + const item = queue[index]!; + if (visited.has(item.object)) throw new QxReject("PRESENTATION_CYCLE"); + visited.add(item.object); + queue.push(...(children.get(item.object) ?? []).sort((a, b) => a.object.localeCompare(b.object))); + } + if (!input && queue.length !== members.length) throw new QxReject("PRESENTATION_DISCONNECTED_STATE"); + const presentationMount = { + ...(await ports.presentationMount.create({ key: "presentationMount" })), + expectation: { kind: "interface" as const, id: "interface:org.quixos.presentation-mount@1" as const }, + }; + const mapping: { source: (typeof members)[number]; target: (typeof members)[number] }[] = []; + const copied = new Map(); + for (const [index, source] of queue.entries()) { + const originalParent = await ports.metadata["parent.resolve"]({ receiver: source, input: null }); + const parent = same(source, requestedRoot) ? null : copied.get(originalParent!.object)!; + if (!same(source, requestedRoot) && !parent) throw new QxReject("PRESENTATION_COPY_PARENT_MISSING"); + const occurrence = await ports.metadata["occurrence.get"]({ receiver: source, input: null }); + const target = await ports.metadata["allocate.call"]( + { receiver: source, input: { presentationMount, occurrence, parent } }, + { key: "allocate:" + index }, + ); + copied.set(source.object, target); + mapping.push({ source, target }); + } + for (const [index, item] of mapping.entries()) + await ports.metadata["copyValues.call"]( + { receiver: item.source, input: { target: item.target, mapping } }, + { key: "values:" + index }, + ); + return presentationMount; +}