feat: add render graph driven renderer architecture

Amp-Thread-ID: https://ampcode.com/threads/T-019f9d91-77c1-7206-a60f-ed6554ce92ab

Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-07-27 02:53:44 +00:00
co-authored by heaust
parent 8a8369706b
commit d4e8634f67
290 changed files with 48804 additions and 1995 deletions
+188
View File
@@ -0,0 +1,188 @@
import type { JsonValue } from "./types.js";
export interface StructuredDataLimits {
readonly maxValues: number;
readonly maxStringCodeUnits: number;
readonly maxDepth: number;
readonly maxIssues: number;
}
export interface StructuredDataIssue {
readonly code: string;
readonly path: string;
readonly message: string;
}
export interface StructuredDataMetrics {
readonly values: number;
readonly stringCodeUnits: number;
readonly depth: number;
}
export type StructuredDataAdmissionResult =
| { readonly ok: true; readonly value: unknown; readonly metrics: StructuredDataMetrics }
| { readonly ok: false; readonly issues: readonly StructuredDataIssue[] };
/** Inspects hostile input without invoking accessors and returns a detached structured clone. */
export function admitStructuredData(input: unknown, limits: StructuredDataLimits): StructuredDataAdmissionResult {
const issues: StructuredDataIssue[] = [],
seen = new WeakSet<object>();
let values = 0,
strings = 0,
maxDepth = 0,
terminal = false;
const add = (code: string, path: string, message: string) => {
if (issues.length < limits.maxIssues) issues.push({ code, path, message });
};
const walk = (v: unknown, path: string, depth: number): void => {
try {
maxDepth = Math.max(maxDepth, depth);
if (terminal) return;
if (++values > limits.maxValues) {
add("limit.values", path, "Document exceeds inspected value limit");
terminal = true;
return;
}
if (depth > limits.maxDepth) {
add("limit.depth", path, "Document exceeds JSON depth limit");
terminal = true;
return;
}
if (typeof v === "string") {
strings += v.length;
if (strings > limits.maxStringCodeUnits) {
add("limit.strings", path, "Document exceeds string limit");
terminal = true;
}
return;
}
if (v === null || typeof v === "boolean") return;
if (typeof v === "number") {
if (!Number.isFinite(v)) add("value.finite", path, "Numbers must be finite");
return;
}
if (typeof v !== "object") {
add("data.type", path, "Unsupported value");
return;
}
if (seen.has(v)) {
add("data.identity", path, "Shared or cyclic identity is unsupported");
return;
}
seen.add(v);
if (Array.isArray(v)) {
if (Object.getPrototypeOf(v) !== Array.prototype) add("data.array", path, "Array must be ordinary");
const ds = Object.getOwnPropertyDescriptors(v),
ld = Object.getOwnPropertyDescriptor(v, "length"),
length = ld && "value" in ld ? ld.value : undefined;
if (!Number.isSafeInteger(length)) {
add("data.array", path, "Invalid array length");
return;
}
if (Number(length) > limits.maxValues - values) {
add("limit.values", path, "Document exceeds inspected value limit");
terminal = true;
return;
}
for (let i = 0; i < Number(length); i++) {
if (terminal) break;
const d = ds[String(i)];
if (!d) {
add("data.array", `${path}/${i}`, "Sparse arrays are unsupported");
continue;
}
if (!d.enumerable || !("value" in d)) {
add("data.inspect", `${path}/${i}`, "Element must be an enumerable data property");
continue;
}
walk(d.value, `${path}/${i}`, depth + 1);
}
for (const k of Reflect.ownKeys(ds)) {
if (terminal) break;
if (k !== "length" && !(typeof k === "string" && /^(0|[1-9]\d*)$/.test(k) && Number(k) < Number(length)))
add(typeof k === "symbol" ? "data.symbol" : "data.array", path, "Extra array properties are unsupported");
}
return;
}
const proto = Object.getPrototypeOf(v);
if (proto !== Object.prototype && proto !== null) {
add("data.type", path, "Object must be an ordinary record");
return;
}
for (const k of Reflect.ownKeys(v)) {
if (typeof k !== "string") {
add("data.symbol", path, "Symbol properties are unsupported");
continue;
}
strings += k.length;
if (strings > limits.maxStringCodeUnits) {
add("limit.strings", path, "Document exceeds string limit");
terminal = true;
return;
}
const d = Object.getOwnPropertyDescriptor(v, k);
if (!d || !d.enumerable || !("value" in d)) {
add("data.inspect", `${path}/${k}`, "Property must be enumerable data");
continue;
}
walk(d.value, `${path}/${k}`, depth + 1);
}
} catch {
add("data.inspect", path, "Value could not be inspected");
terminal = true;
}
};
walk(input, "", 0);
if (issues.length) return { ok: false, issues };
try {
return {
ok: true,
value: structuredClone(input),
metrics: Object.freeze({ values, stringCodeUnits: strings, depth: maxDepth }),
};
} catch {
return { ok: false, issues: [{ code: "data.clone", path: "/", message: "Document could not be cloned" }] };
}
}
export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function isJson(value: unknown, depth = 0): value is JsonValue {
if (depth > 50) return false;
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
if (typeof value === "number") return Number.isFinite(value);
if (Array.isArray(value)) return value.every((item) => isJson(item, depth + 1));
return isRecord(value) && Object.values(value).every((item) => isJson(item, depth + 1));
}
export function deepFreeze<T>(value: T): T {
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
for (const child of Object.values(value)) deepFreeze(child);
Object.freeze(value);
}
return value;
}
export function cloneJson<T>(value: T): T {
return deepFreeze(structuredClone(value));
}
export function nullRecord<T>(entries: Iterable<readonly [string, T]> = []): Readonly<Record<string, T>> {
const result = Object.create(null) as Record<string, T>;
for (const [key, value] of entries) result[key] = value;
return Object.freeze(result);
}
export function canonicalize(value: unknown): unknown {
if (Array.isArray(value)) return value.map(canonicalize);
if (isRecord(value)) {
const result = Object.create(null) as Record<string, unknown>;
for (const key of Object.keys(value).sort()) result[key] = canonicalize(value[key]);
return result;
}
return value;
}
/** Canonical structural equality for admitted JSON-shaped values. */
export function canonicalJsonEqual(left: unknown, right: unknown): boolean {
return JSON.stringify(canonicalize(left)) === JSON.stringify(canonicalize(right));
}
+113
View File
@@ -0,0 +1,113 @@
import type { FxNodeCompositionData, NodeTypeId } from "../composition/types.js";
declare const brand: unique symbol;
export type NodeId = string & { readonly [brand]: "NodeId" };
export type LinkId = string & { readonly [brand]: "LinkId" };
export type SocketId = string & { readonly [brand]: "SocketId" };
export type CommandId = string & { readonly [brand]: "CommandId" };
export type GraphId = string & { readonly [brand]: "GraphId" };
export type JsonValue = null | boolean | number | string | readonly JsonValue[] | { readonly [key: string]: JsonValue };
export interface Vec2 {
readonly x: number;
readonly y: number;
}
export type SocketDataType = string;
export type ParameterValue =
| { readonly kind: "number"; readonly value: number }
| { readonly kind: "boolean"; readonly value: boolean }
| { readonly kind: "string"; readonly value: string }
| { readonly kind: "vector"; readonly value: readonly [number, number, number] }
| { readonly kind: "color"; readonly value: readonly [number, number, number, number] }
| { readonly kind: "json"; readonly value: JsonValue };
export interface Socket {
readonly id: SocketId;
readonly key: string;
readonly label: string;
readonly direction: "input" | "output";
readonly dataType: SocketDataType;
readonly accepts: readonly SocketDataType[];
readonly maxIncomingLinks: number;
readonly defaultValue?: ParameterValue | null | undefined;
readonly visible: boolean;
readonly metadata?: Readonly<Record<string, JsonValue>>;
}
export interface NodeBase {
readonly id: NodeId;
readonly typeId: string;
readonly typeVersion: number;
/** Upper-left origin. Child positions are local to their parent frame. +Y is up. */
readonly position: Vec2;
/** Positive logical dimensions. */
readonly size: Vec2;
readonly label: string;
readonly parameters: Readonly<Record<string, ParameterValue | JsonValue>>;
readonly sockets: readonly Socket[];
readonly muted: boolean;
readonly collapsed: boolean;
readonly parentId?: NodeId | undefined;
readonly extensions: Readonly<Record<string, JsonValue>>;
}
export interface KnownNode<C extends FxNodeCompositionData = FxNodeCompositionData> extends NodeBase {
readonly known: true;
readonly typeId: NodeTypeId<C>;
readonly parameters: Readonly<Record<string, ParameterValue>>;
}
export interface UnknownNode extends NodeBase {
readonly known: false;
}
export type GraphNode<C extends FxNodeCompositionData = FxNodeCompositionData> = KnownNode<C> | UnknownNode;
export interface GraphLink {
readonly id: LinkId;
readonly fromNodeId: NodeId;
readonly fromSocketId: SocketId;
readonly toNodeId: NodeId;
readonly toSocketId: SocketId;
readonly muted: boolean;
readonly extensions: Readonly<Record<string, JsonValue>>;
}
export interface GraphDocument<C extends FxNodeCompositionData = FxNodeCompositionData> {
readonly schemaVersion: 2;
readonly graphId: GraphId;
readonly catalogVersion: number;
readonly nodes: Readonly<Record<string, GraphNode<C>>>;
readonly links: Readonly<Record<string, GraphLink>>;
readonly metadata: Readonly<Record<string, JsonValue>>;
}
export interface GraphLayoutNodeV1 extends Omit<NodeBase, "known"> {
readonly parameters: Readonly<Record<string, ParameterValue | JsonValue>>;
}
export interface GraphLinkV1 extends Omit<GraphLink, "muted"> {}
export interface GraphLayoutV1 {
readonly schemaVersion: 1;
readonly graphId: GraphId;
readonly catalogVersion: number;
readonly nodes: readonly GraphLayoutNodeV1[];
readonly links: readonly GraphLinkV1[];
readonly metadata: Readonly<Record<string, JsonValue>>;
}
export interface GraphLayoutV2 {
readonly schemaVersion: 2;
readonly graphId: GraphId;
readonly catalogVersion: number;
readonly nodes: readonly GraphLayoutNodeV1[];
readonly links: readonly GraphLink[];
readonly metadata: Readonly<Record<string, JsonValue>>;
}
export interface GraphState<C extends FxNodeCompositionData = FxNodeCompositionData> {
readonly graphId: GraphId;
readonly catalogVersion: number;
readonly nodes: readonly GraphNode<C>[];
readonly links: readonly GraphLink[];
readonly metadata: Readonly<Record<string, JsonValue>>;
}
export interface GraphSnapshot<C extends FxNodeCompositionData = FxNodeCompositionData> extends GraphState<C> {
readonly version: number;
}
export const nodeId = (value: string): NodeId => value as NodeId;
export const linkId = (value: string): LinkId => value as LinkId;
export const socketId = (value: string): SocketId => value as SocketId;
export const commandId = (value: string): CommandId => value as CommandId;
export const graphId = (value: string): GraphId => value as GraphId;