Summary: Svelte Flow starter scaffold for one-root loopable scene-graph editors with typed scene schemas and custom nodes.

💡 PATTERNS (Best Practices)

  • One-Root Topology: Enforce strict single-entry (RootNode) to guarantee deterministic execution, linear batching, and loop validation.
  • Typed Scene Schemas: Use zod or valibot for strict node data validation; sync Svelte Flow Node<T> generic with the schema to guarantee type-safe graphs.
  • Reactive State Runes (Svelte 5): Use $state() for local node UI, but centralized $derived() for global graph execution state (playhead, loop counts).
  • Custom Node Encapsulation: Decouple node logic from UI; pass strictly typed data via Svelte props (let { data }: NodeProps<SceneData>).
  • Cycle Validation (Schmofi Loop): Implement dynamic topological sorting (e.g., Kahn’s algorithm) to validate explicit loopability back to RootNode without causing infinite call stacks.

🚨 ANTI-PATTERNS (Gotchas / Warnings)

  • Multi-Root Bleed: Allowing detached sub-graphs destroys the deterministic timeline compilation required for batch video rendering.
  • Mutable Node State: Mutating node.data directly without Svelte Flow’s updateNodeData(). This silently breaks internal reactivity sync.
  • Untyped Flow Edges: Passing payload data via edges implicitly. Fix: Enforce typed connection handles (e.g., <Handle type="target" id="video-in" />).
  • Deep Reactivity Traps: Deeply nested $state inside Svelte Flow’s internal store triggers excessive re-renders. Keep scene schemas flat/normalized.
  • DOM Polling for Playhead: Using requestAnimationFrame to query DOM nodes for video status. Bind directly to Svelte/HTML5Media element state instead.

🔧 ARCHITECTURE KERNEL (Svelte + Schema)

// 📦 One-Root Typed Schema Core
import { z } from "zod"
import type { Node } from "@xyflow/svelte"
 
export const SceneNodeSchema = z.discriminatedUnion("type", [
  z.object({ type: z.literal("root"), data: z.object({ fps: z.number(), maxLoops: z.number() }) }),
  z.object({
    type: z.literal("schmofi_video"),
    data: z.object({ src: z.string(), trim: z.tuple([z.number(), z.number()]) }),
  }),
])
 
export type SceneGraphNode = z.infer<typeof SceneNodeSchema>
export type FlowNode = Node<SceneGraphNode["data"], SceneGraphNode["type"]>