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:
@@ -0,0 +1,11 @@
|
||||
# Composition
|
||||
|
||||
Composition is the application-defined graph language, distinct from graph state. It owns themes, header styles, directional socket compatibility, resources, node definitions, UI rows, defaults, bypasses, and migrations. Graph state is a document written in that language. Compatibility is checked from the destination socket's accepted source types; changing it can invalidate existing links.
|
||||
|
||||
There is no built-in registry. Browser applications install definitions through `setTheme`, `setHeaderStyles`, `setCompatibility`, `composeSocket`, and `composeNode`, or atomically with `loadComposition`. Dependencies come first; `setState` comes last. Plain structured-clone-safe data crosses the worker boundary—no callbacks or classes.
|
||||
|
||||
Updates compile, validate, rebind, and publish atomically and return a receipt (`status`, composition `revision`, graph version, and whether rebinding changed the graph). A rejected candidate changes nothing. Distinct definition IDs converge regardless of concurrent installation order once dependencies exist; updates to the same ID are ordered, and references still require their dependency to be installed first.
|
||||
|
||||
Every committed change to a node definition resets definition-bound undo/redo history, even if no current instance uses that definition. Removing a node definition preserves its instances as opaque, read-only nodes. Removing a socket type is rejected while compatibility rules, another socket type, or a node definition references it; update or remove those dependents first. A valid composition rebind may remove graph links that have become incompatible. Reintroducing compatible definitions can promote opaque instances. A migration `rename-socket` rewrites both the node's socket data and every link endpoint that refers to it in the same transaction—there is no observable half-renamed graph. A semantic no-op emits nothing and advances neither revision nor graph version.
|
||||
|
||||
Static/headless authoring can use `compileFxNodeComposition` and immutable helpers to retain literal ID types. Browser handles intentionally accept string IDs because their composition authority can change live.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Graph state and events
|
||||
|
||||
Runtime graph state contains `graphId`, `catalogVersion`, nodes, links, and metadata. It is not the persistence envelope. The worker commits commands atomically and checks optional optimistic `expectedVersion` values.
|
||||
|
||||
Committed graph changes emit mutations before snapshots, in version order. Subscribers are isolated and return an unsubscribe function. Composition changes use a separate revision domain and emit `onCompositionChanges`; when rebinding changes a graph, the composition event precedes the matching mutation and snapshot.
|
||||
|
||||
Command and composition calls resolve with receipts only after authoritative validation and publication. Use their returned versions/revisions for the next compare-and-swap rather than inferring them from event timing. Structured validation/protocol failures reject without partial mutation; a `noop` receipt means no graph publication.
|
||||
|
||||
| Domain | Meaning | Advances on |
|
||||
| ---------------------- | -------------------------------------- | ---------------------------------- |
|
||||
| Graph `version` | runtime document concurrency | graph-changing command/load/rebind |
|
||||
| Composition `revision` | live authority concurrency | committed composition update |
|
||||
| `catalogVersion` | bound composition version in documents | normalization/binding; persisted |
|
||||
|
||||
Do not compare or substitute these values. Gesture previews remain worker-local until one commit.
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# Concepts
|
||||
|
||||
For integrators deciding where application responsibilities end and fxnode authority begins. Read in this order:
|
||||
|
||||
1. [Worker authority](./worker-authority): locate truth, work, and the asynchronous host boundary.
|
||||
2. [Composition](./composition): model the application's graph language and live updates.
|
||||
3. [Graph state and events](./graph-state-and-events): reason about documents, receipts, versions, and observation.
|
||||
4. [State and persistence](./state-and-persistence): choose runtime replacement, canonical export, or replayable persistence.
|
||||
|
||||
Afterward you should be able to choose the correct API and concurrency domain, predict publication order, and design durable loading without treating fxnode as a graph evaluator. fxnode is an editor and presenter, not an evaluator.
|
||||
@@ -0,0 +1,11 @@
|
||||
# State and persistence
|
||||
|
||||
`getState()` and `setState()` exchange exact, process-local state for the currently installed composition. `setState()` is useful for bootstrap and controlled replacement, not historical imports.
|
||||
|
||||
`save()` returns the canonical current `GraphLayoutV2`—a compact graph export, not history. For replayable durable storage use `getSaveData()`: its envelope records the canonical baseline, the applied command journal since that baseline, and the effective save-time composition used to establish compatibility. The baseline and journal are composed at save time to verify that they reproduce the exported current graph.
|
||||
|
||||
`load()` accepts historical `GraphLayoutV1`, canonical `GraphLayoutV2`, or the save-data envelope. It stages decode, compatibility checks, declarative migrations, and replay before one atomic publication; structured issues identify paths/codes on failure, and rejected input leaves graph, history, and observable state unchanged. A successful graph change publishes the load mutation/snapshot as one commit. Loading an envelope installs its migrated baseline and command journal (including checkpoint placement); if the resulting graph equals current state, the load is a no-op but the validated journal/baseline is still installed for subsequent undo/redo and saves.
|
||||
|
||||
Durable `GraphLayoutV2` uses `schemaVersion: 2`; its historical `catalogVersion` field stores composition version. Unknown types and future node versions round-trip as opaque read-only records. Declarative migration edges must form a complete valid route; failures preserve the original opaque payload. Canonical ordering and bounded admission make saves deterministic and hostile inputs reject safely.
|
||||
|
||||
In short: **set/get state** for exact current runtime state; **save** for canonical `GraphLayoutV2`; **save data/load** for compatible persistence and replay. Selection, camera, hover, composition revision, and undo/redo internals are not durable graph fields.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Worker authority
|
||||
|
||||
The worker is authoritative for graph state, composition, validation, command history, hit testing, layout, gestures, and rendering. One root owns one worker and one shared graph, whether it has zero, one, or many attached views. The browser client keeps only bounded host projections. It does not keep a graph shadow.
|
||||
|
||||
Graph state, composition, persistence, events, and history belong to the root. Canvas, viewport, camera, selection, gestures, rendering, host requests, and resource authorization belong to a view. A mutation from any view changes the shared graph and schedules every attached view, while cameras and selections remain independent.
|
||||
|
||||
The worker serializes view painting and cropping through one atlas canvas and one 2D context. Each attached HTML canvas has its own presentation context; `maxViews` remains a resource bound rather than a rendering-context count.
|
||||
|
||||
The application owns the DOM: canvas sizing, listeners, focus policy, menus, dialogs, measurement, and teardown. It turns DOM events into `feedInput()` DTOs. fxnode never registers document/window/canvas listeners or creates controls.
|
||||
|
||||
Host requests cross an asynchronous trust boundary. For `resource-open`, the worker emits an immutable descriptor and one-use authorization. The application chooses UI and later calls `provideResource(authorization, data)`. The token is consumed only by a valid accepted submission: failed data validation does **not** consume it, so the application may correct the data and retry. Do not depend on the original pointer's browser activation; ask for a fresh user action when required. Authorizations become stale after relevant graph/composition changes, and transferred `ArrayBuffer`s detach.
|
||||
|
||||
This boundary makes worker ordering definitive: await composition dependencies and treat terminal startup/protocol failures as terminal.
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Examples
|
||||
|
||||
The repository has five current experiences. Images below use the examples' existing captured assets—there are no documentation copies.
|
||||
|
||||
## Minimal
|
||||
|
||||

|
||||
|
||||
_A minimal composition and one node. [Source](https://github.com/Heaust-ops/fxnode/blob/main/examples/minimal/main.ts)._
|
||||
|
||||
## Color Balance
|
||||
|
||||

|
||||
|
||||
_A focused custom-widget composition. [Source](https://github.com/Heaust-ops/fxnode/blob/main/examples/color-balance/main.ts)._
|
||||
|
||||
## Live composition
|
||||
|
||||

|
||||
|
||||
_Replacing a node definition and migrating its instance. [Source](https://github.com/Heaust-ops/fxnode/blob/main/examples/live-composition/main.ts)._
|
||||
|
||||
## Multi-view
|
||||
|
||||

|
||||
|
||||
_One worker and graph with independent cameras and selections. The application-owned toolbar targets the active view, and the canvases forward pointer events only. [Source](https://github.com/Heaust-ops/fxnode/blob/main/examples/multi-view/main.ts)._
|
||||
|
||||
## Blender-shaped gallery
|
||||
|
||||
The [larger gallery source](https://github.com/Heaust-ops/fxnode/blob/main/examples/blender/main.ts) exercises many node and interaction shapes; it is repository application code, not package authority.
|
||||
|
||||
These examples present and persist editable graphs; they do **not evaluate** them. Blender-shaped fixtures and visual regression images do not establish Blender compatibility, behavioral parity, or pixel parity.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Accessibility
|
||||
|
||||
The editor is bitmap Canvas. Its nodes, sockets, labels, controls, and relationships are not semantic accessibility-tree objects. fxnode makes **no WCAG conformance claim** and must not be the only interface when assistive access is required.
|
||||
|
||||
Applications should provide an equivalent semantic DOM workflow, status announcements, instructions, and controls. Keyboard support alone is not accessibility. An application-owned DOM add-node dialog can implement combobox/listbox semantics and focus restoration, but that does not make the Canvas editor accessible.
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# Browser host
|
||||
|
||||
`createFxNode` needs application identity/version, resource policies, and optionally a worker URL/history limit. It creates the shared root and worker without a canvas. `root.attachView()` needs a canvas, logical CSS-pixel viewport plus DPR, and optionally an initial camera. The **application owns the DOM and canvas dimensions**. Before attachment, measure the initial layout and set the backing dimensions. For a runtime resize, await `view.setViewport(next)` before updating the backing dimensions. Attach and remove your own listeners.
|
||||
|
||||
fxnode creates one module worker per root and never creates a resize observer, menu, modal, or file picker. A root may have no views or multiple views. Convert pointer/keyboard/wheel events to each view's `feedInput()` values. The worker performs authoritative hit testing and may issue view-scoped `add-node-menu` or `resource-open` host requests; your DOM decides presentation and ordering.
|
||||
|
||||
Use root methods for composition, state, persistence, subscriptions, and context-free commands. Use view methods for input, viewport changes, selection actions, resource responses, and render checkpoints. A canvas can have only one live view. On teardown, remove application listeners and observers first, detach each view, then destroy the root.
|
||||
|
||||
The repository example host defaults to `lifecycle: "explicit"`, so `host.destroy()` removes only host-owned policy
|
||||
and never detaches its view. Opt in with `lifecycle: "detach-on-disconnect"` for component-style examples. That mode
|
||||
requires an initially connected canvas and `MutationObserver`; hosts share one observer per document. A removal is
|
||||
confirmed in a microtask (so a same-task remove/reinsert or reparent survives), then host resources are synchronously
|
||||
removed before `view.detach()` is requested. Moving the canvas to another document counts as disconnection; hiding it
|
||||
or giving it zero layout size does not.
|
||||
|
||||
Resize observations are coalesced while a viewport request is in flight. The host updates canvas backing dimensions
|
||||
only after `setViewport()` acknowledges that request. A rejection preserves the prior backing store, reports through
|
||||
`onError`, and a later observation can retry.
|
||||
|
||||
Install composition before initial state. Imported/historical data belongs in `load()`, not `setState()`. See [state and persistence](../concepts/state-and-persistence).
|
||||
@@ -0,0 +1,7 @@
|
||||
# Browser support
|
||||
|
||||
The certified functional matrix is Chromium and Firefox from Playwright 1.61.1 on desktop Linux. Chromium image goldens are regression tests, not cross-engine or Blender parity tests. WebKit, Safari-branded builds, and mobile are not certified.
|
||||
|
||||
The main thread requires module `Worker`, `crypto.randomUUID`, and Canvas 2D. The worker requires `OffscreenCanvas`, a 2D context, cropped `createImageBitmap`, and `ImageBitmap.close`; there is no fallback. Named capability errors identify missing features.
|
||||
|
||||
Cross-origin isolation enables an optional `SharedArrayBuffer` pointer lane per view. Without it, normal `postMessage` transport remains functional. Limits include 16 attached views, DPR 4, 8192 logical pixels per dimension, 16,777,216 device pixels per view, 67,108,864 device pixels across all views, and history 1,000.
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# Content Security Policy
|
||||
|
||||
fxnode starts a same-origin ES module worker with no blob, classic-worker, or main-thread fallback. Permit it with an appropriate `worker-src 'self'` and `script-src`, and serve JavaScript with the correct MIME type. Pass `workerUrl` if assets move independently.
|
||||
|
||||
For optional shared-memory input use `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`; embedded cross-origin resources must satisfy CORS or CORP. Otherwise fxnode automatically uses messages. Worker construction blocked synchronously reports `worker.construct`; a worker script/network/module load failure reports `worker.load`; failure to complete startup in time reports `worker.timeout`. None silently downgrade to main-thread execution.
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# Integration guides
|
||||
|
||||
For browser/platform engineers turning an editor bootstrap into a production integration. Start with the [browser host](./browser-host), then wire [interactions](./interactions) and [rendering and lifecycle](./rendering-and-lifecycle). These establish canvas ownership, input forwarding, render checkpoints, and teardown.
|
||||
|
||||
Before release, review [browser support](./browser-support), [CSP](./csp), and [accessibility](./accessibility), in that order. The outcome is a host with explicit capability fallbacks, deployable worker policy, accessible DOM-owned controls, and no leaked listeners or workers.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Interactions
|
||||
|
||||
The host translates DOM input; the worker owns gesture state and commits. Supported editor gestures include movement, box selection, resize, link creation/replacement, Ctrl-right-click cutting, Ctrl-Alt-right-click muting, `M` node mute, `H` collapse, `G` modal move, and undo/redo.
|
||||
|
||||
Plain right-click on eligible empty canvas may request an add-node menu. The host owns its HTML, search, grouping, focus, dismissal, and calls `addNode`. Controls follow composition `ui` order. Scrubbing modifiers, text commit/cancel, and reset all become atomic commands.
|
||||
|
||||
Do not derive behavior from projected pixels or retain a parallel graph. Subscribe to committed events when application UI needs updates.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Rendering and lifecycle
|
||||
|
||||
Each view's `whenRendered()` synchronizes a frame for that view. Attach another view when a second canvas needs an independent camera or selection over the same graph; graph mutations schedule all attached views. Set the initial backing dimensions before attachment. At runtime, await that view's `setViewport(next)` first, then update the host canvas backing dimensions.
|
||||
|
||||
Keep every listener, observer, menu, and focus behavior in an application-owned cleanup object. On unmount/page teardown, remove those resources, await `view.detach()` for each view, then call `root.destroy()`. The example browser host keeps this explicit by default; its opt-in disconnect policy can perform host cleanup and request detachment when a connected canvas is removed. Destroying that host alone never detaches. View detachment is idempotent and rejects subsequent view work with `FxNodeViewDetachedError`. Root destruction is idempotent, detaches all remaining views, and makes pending and future work reject with `FxNodeDestroyedError`. Fatal startup/protocol failures also release resources and make future calls reject the stored terminal error.
|
||||
|
||||
Do not use rendering completion as graph execution completion: fxnode never executes graphs.
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
# Learn fxnode
|
||||
|
||||
Treat this as a trail map rather than a giant manual.
|
||||
|
||||
## Start
|
||||
|
||||
Build [your first node](/learn/tutorials/first-node), then tour [all examples](/learn/examples/).
|
||||
|
||||
## Build
|
||||
|
||||
Use the [browser-host guide](/learn/guides/browser-host) to connect your DOM and the [interaction guide](/learn/guides/interactions) to translate input.
|
||||
|
||||
## Understand
|
||||
|
||||
Read [worker authority](/learn/concepts/worker-authority), [composition](/learn/concepts/composition), and [state and persistence](/learn/concepts/state-and-persistence).
|
||||
|
||||
## Integrate
|
||||
|
||||
Check [browser support](/learn/guides/browser-support), [CSP](/learn/guides/csp), lifecycle, and [accessibility](/learn/guides/accessibility) before shipping.
|
||||
|
||||
## Reference
|
||||
|
||||
When you know the concept and need an exact signature, use the [API Reference](/reference/).
|
||||
@@ -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).
|
||||
@@ -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
@@ -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.
|
||||
@@ -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).
|
||||
Reference in New Issue
Block a user