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
+57
View File
@@ -0,0 +1,57 @@
# Build a Color Balance editor
## What you will build
A focused editor with float/color socket types and the repository's grading-wheel Color Balance definition.
## Prerequisites and checkpoint
Complete [your first node](./first-node). Confirm the empty editor renders before adding the two socket definitions.
## 1. Install dependencies
After `createFxNode`, install theme and styles, then compose `float`, compose `color`, and compose the node. Make `setState` the final bootstrap state call; attach a view, add the node through that view, attach the host, and await the view's `whenRendered()`.
```ts
import { createFxNode } from "fxnode";
const root = await createFxNode({
applicationId: "color.balance",
applicationVersion: 1,
resources: {},
});
await root.setTheme(theme);
await root.setHeaderStyles(styles);
await root.composeSocket(...floatSocket);
await root.composeSocket(...colorSocket);
await root.composeNode(...colorBalanceNode);
await root.setState({ graphId: "color-balance", catalogVersion: 1, nodes: [], links: [], metadata: {} });
const view = await root.attachView({ canvas, viewport });
host.attach(root, view);
await view.addNode({
nodeId: "color-balance",
typeId: colorBalanceNode[0],
viewPosition: { x: 300, y: 40 },
});
await view.whenRendered();
```
This is an **excerpt**: `canvas`, `host`, `viewport`, theme, styles, sockets, and node definition are application-owned setup shown in the working source.
**Checkpoint:** the Color Balance node and its three grading wheels are visible and interactive.
### Why?
Definitions refer to styles and sockets, so dependencies must exist first. The widget edits graph data; fxnode does not perform color correction or execute the graph.
## 2. Attach, verify, and clean up
Attachment starts DOM input forwarding; the view's `whenRendered()` establishes a visible-frame checkpoint. On teardown remove listeners, run `host.destroy()`, await `view.detach()`, and call `root.destroy()` (including startup failure and startup/teardown races).
## Complete example
See [`examples/color-balance/main.ts`](https://github.com/Heaust-ops/fxnode/blob/main/examples/color-balance/main.ts) and the shared [node definition](https://github.com/Heaust-ops/fxnode/blob/main/examples/shared/nodes/color-balance.ts).
## Related concepts / relevant API / next
Read [composition](../concepts/composition), then inspect [`FxNode.composeNode`](/reference/generated/fxnode/interfaces/FxNode#composenode) and continue to [live composition](./live-composition).
+72
View File
@@ -0,0 +1,72 @@
# Your first node
## What you will build
A Canvas editor containing one numeric value node, matching the repository's executable minimal example.
## Prerequisites
Install `fxnode`. Give the canvas non-zero CSS dimensions (the attributes also provide a useful fallback), then prepare a browser host that measures it and forwards input:
```html
<canvas id="graph" width="1000" height="560" style="width: 100%; height: 560px"></canvas>
```
The repository's [small host implementation](https://github.com/Heaust-ops/fxnode/blob/main/examples/shared/browser-host.ts) contains viewport, resize, and input wiring; see [browser host](../guides/browser-host) for its contract.
## Checkpoint
Your canvas has non-zero CSS dimensions and your host has produced `initialViewport`.
## 1. Prepare application-owned definitions
`theme`, `minimalStyles`, `numberSocket`, and `valueNode` below are **application-owned definitions**, not fxnode globals. The socket and node are exported as `[id, definition]` tuples so they can be passed directly to the composition methods. Define or import them before bootstrap; the executable [definition file](https://github.com/Heaust-ops/fxnode/blob/main/examples/minimal/definition.ts) is the compact reference.
## 2. Bootstrap in dependency order
Create the shared root first, then install composition dependencies in order: theme, header styles, sockets, nodes, and finally graph state. Attach the canvas view after that bootstrap.
```ts
import { createFxNode } from "fxnode";
const root = await createFxNode({
applicationId: "my.first.editor",
applicationVersion: 1,
resources: {},
});
await root.setTheme(theme);
await root.setHeaderStyles(minimalStyles);
await root.composeSocket(...numberSocket);
await root.composeNode(...valueNode);
await root.setState({ graphId: "first", catalogVersion: 1, nodes: [], links: [], metadata: {} });
const view = await root.attachView({ canvas, viewport: host.initialViewport });
host.attach(root, view);
await view.addNode({ nodeId: "value", typeId: valueNode[0], viewPosition: { x: 360, y: 190 } });
await view.whenRendered();
```
**Checkpoint:** a “Number Value” node is visible. The host is attached only after setup, and the view's `whenRendered()` confirms the committed node reached a frame.
### Why this order?
The worker validates every definition against current authority. `setState` is last so known nodes bind against the complete composition. Host attachment follows bootstrap so input cannot race setup.
## 3. Clean up
Remove application listeners, call `host.destroy()`, await `view.detach()`, then call `root.destroy()` on unmount or `pagehide`. Also destroy a late-created root if teardown wins a startup race. The complete source demonstrates that guard.
## Complete example
The complete executable source is [`examples/minimal/main.ts`](https://github.com/Heaust-ops/fxnode/blob/main/examples/minimal/main.ts), with its [`definition.ts`](https://github.com/Heaust-ops/fxnode/blob/main/examples/minimal/definition.ts).
## Related concepts
[Composition](../concepts/composition) and [worker authority](../concepts/worker-authority).
## Relevant API
[`createFxNode`](/reference/generated/fxnode/functions/createFxNode), [`FxNode`](/reference/generated/fxnode/interfaces/FxNode), and [`FxNodeView`](/reference/generated/fxnode/interfaces/FxNodeView).
## Next
Build a richer [Color Balance node](./color-balance).
+9
View File
@@ -0,0 +1,9 @@
# Tutorials
For application developers integrating fxnode for the first time. Follow these in order: each tutorial builds on the previous one's host and composition vocabulary. You will finish able to bootstrap a visible editor, install a custom widget, and safely replace a live definition with optimistic concurrency.
1. [Your first node](./first-node) — size and host a canvas, install definitions, render, and tear down.
2. [Color Balance](./color-balance) — add dependency-ordered socket types and a custom widget.
3. [Live composition](./live-composition) — migrate a visible instance using composition receipts.
Each page marks excerpts, establishes ordered checkpoints, explains why each concern exists, and links to a complete executable source.
+47
View File
@@ -0,0 +1,47 @@
# Live composition
## What you will build
An editor that replaces a version-1 node definition with version 2 and migrates its graph instance atomically.
## Prerequisites and checkpoint
Understand [composition](../concepts/composition). Start with the v1 node visible and retain the receipt's `revision`.
## 1. Acquire the v1 revision
```ts
import type { FxNode, FxNodeView } from "fxnode";
const v1Receipt = await api.composeNode("example.live.parameter", liveNodeV1);
let revision = v1Receipt.revision;
```
This is an **excerpt**: await socket dependencies first, compose v1, call `setState`, attach a view, add its instance through that view, attach the host, and render. **Checkpoint:** v1 is visible and `revision` came from its receipt—not a guessed constant.
## 2. Replace it using the v2 receipt
```ts
async function upgrade(root: FxNode, view: FxNodeView) {
const v2Receipt = await root.composeNode("example.live.parameter", liveNodeV2, {
expectedRevision: revision,
});
revision = v2Receipt.revision;
await view.whenRendered();
return v2Receipt;
}
```
Invoke this on an explicit host action. **Checkpoint:** inspect `v2Receipt.status`, `graphChanged`, `graphVersion`, and updated `revision`; the migrated v2 node is visible. Clean up the button/page listeners, host, and API on teardown.
### Why?
Composition revision and graph version are separate concurrency domains. A committed rebind can advance both; a no-op advances neither. Compare-and-swap prevents two writers from assuming the same authority.
## Complete example
See the working [`examples/live-composition/main.ts`](https://github.com/Heaust-ops/fxnode/blob/main/examples/live-composition/main.ts) and its [definitions](https://github.com/Heaust-ops/fxnode/blob/main/examples/live-composition/definitions.ts).
## Related concepts / relevant API / next
Read [graph state and events](../concepts/graph-state-and-events) and [`CompositionReceipt`](/reference/generated/fxnode/type-aliases/CompositionReceipt), then plan [lifecycle cleanup](../guides/rendering-and-lifecycle).