@magicvr/schema-ui-protocol 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Runtime page-schema loader for the schema-driven render path (R1 · GOAL-002).
3
+ *
4
+ * Resolves a manifest `PageEntry`'s schemaUrl (with route-parameter expansion),
5
+ * fetches the page document, forces structural validation (D-VAL) against the
6
+ * pinned page/node schemas, and returns the parsed document or throws a unified
7
+ * `PageSchemaError`. The fetcher is injectable so tests exercise network,
8
+ * parse, and validation failure paths without a server. This module does NOT
9
+ * switch the app's default render branch (that is GOAL-003).
10
+ */
11
+ import { resolveSchemaUrl } from "@magicvr/schema-ui-protocol/app-manifest";
12
+ import { validatePageDocument } from "@magicvr/schema-ui-protocol/conformance/runtime-schema-validate";
13
+ import { withTimeout } from "@magicvr/schema-ui-lib/lib/fetch-timeout";
14
+ /** Unified, observable error for the schema loading + validation pipeline. */
15
+ export class PageSchemaError extends Error {
16
+ code;
17
+ url;
18
+ issues;
19
+ constructor(code, url, message, issues) {
20
+ super(message);
21
+ this.name = "PageSchemaError";
22
+ this.code = code;
23
+ this.url = url;
24
+ this.issues = issues;
25
+ }
26
+ }
27
+ function defaultBaseURL() {
28
+ return typeof globalThis.location !== "undefined" ? globalThis.location.origin : "";
29
+ }
30
+ /**
31
+ * Load and validate a page document for the given manifest page and route
32
+ * params. Resolves the schemaUrl (expanding `{param}` placeholders), fetches
33
+ * it, enforces structural validation, and verifies the document's `meta.pageId`
34
+ * matches the manifest page. Returns the parsed page document on success.
35
+ */
36
+ export async function loadPageDocument(page, params, options = {}) {
37
+ const baseURL = options.baseURL ?? defaultBaseURL();
38
+ // W10 F-002: the default transport is timeout-bounded; an injected fetcher
39
+ // (tests) is honored as-is so failure paths stay deterministic.
40
+ const fetcher = options.fetcher ?? withTimeout();
41
+ const url = resolveSchemaUrl(baseURL, page.schemaUrl, params);
42
+ // W19 perf (2026-08): per-visit schema rediscovery was pure waste — the
43
+ // document is static per (schemaUrl, params) until the app reloads. The
44
+ // shell-owned cache skips one fetch + one D-VAL pass per navigation.
45
+ if (options.cache !== undefined) {
46
+ const cached = options.cache.get(url);
47
+ if (cached !== undefined) {
48
+ return cached;
49
+ }
50
+ }
51
+ if (typeof fetcher !== "function") {
52
+ throw new PageSchemaError("PAGE_LOAD_FAILED", url, "Fetch is unavailable.");
53
+ }
54
+ let response;
55
+ try {
56
+ response = await fetcher(url);
57
+ }
58
+ catch (error) {
59
+ throw new PageSchemaError("PAGE_LOAD_FAILED", url, error instanceof Error ? error.message : "Network request failed.");
60
+ }
61
+ if (!response.ok) {
62
+ throw new PageSchemaError(response.status === 404 ? "PAGE_NOT_FOUND" : "PAGE_LOAD_FAILED", url, `Page document request failed with HTTP ${response.status}.`);
63
+ }
64
+ let document;
65
+ try {
66
+ document = await response.json();
67
+ }
68
+ catch {
69
+ throw new PageSchemaError("PAGE_PARSE_FAILED", url, "Response body is not valid JSON.");
70
+ }
71
+ const validation = validatePageDocument(document);
72
+ if (!validation.ok) {
73
+ throw new PageSchemaError("PAGE_SCHEMA_INVALID", url, "Page document failed structural validation.", validation.errors);
74
+ }
75
+ const meta = document?.meta;
76
+ if (typeof meta?.pageId === "string" && meta.pageId !== page.pageId) {
77
+ throw new PageSchemaError("PAGE_ID_MISMATCH", url, `Page document meta.pageId (${meta.pageId}) does not match manifest pageId (${page.pageId}).`);
78
+ }
79
+ // Cache only validated documents; a bounded map keeps param-paged schema
80
+ // URLs (e.g. /schema/orders/{id}) from growing without limit.
81
+ if (options.cache !== undefined) {
82
+ if (options.cache.size >= 64) {
83
+ options.cache.clear();
84
+ }
85
+ options.cache.set(url, document);
86
+ }
87
+ return document;
88
+ }