Files
Endoleap/headless/README.md
T

605 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Headless Canvas: portable ESM preview
Build from the Endoleap checkout with `npm run build:headless`. The generated
`.headless-dist` directory is a private package; nothing is published to a registry.
Copy it elsewhere, or use `npm pack --offline --ignore-scripts` inside that directory.
Public exports:
| Import | Responsibility |
| --- | --- |
| `@endoleap/headless-canvas` or `/core` | Document, registry, capabilities, transactions, history, sessions, geometry and queries |
| `/runtime` | Project/Workspace/Host, storage and service port types, recovery, scoped loader and compute/display pools |
| `/service` | Foundry protocol codecs and transport adapters; credentials remain in the host |
| `/browser` | Explicit IndexedDB/Web Locks and module Worker adapters |
| `/bindings` | Portable projection, renderer descriptors, caches, connectors and hit queries |
| `/react` | React Provider and selector, element, action, mode, gesture, operation, asset and sync hooks; no React Flow runtime dependency |
| `/react-flow` | Explicit client adapter, renderer command context and framework node/edge types |
The portable entries have no React, Next, React Flow or other runtime package
dependency. They use standard platform APIs such as AbortController, Blob, URL,
TextEncoder and Web Crypto. The sample runs on Node 22+. Browser adapters require
their browser APIs; importing `/runtime` does not import the browser adapter.
Run `node consumer.mjs` inside the generated package. It uses a custom counter
component, rejects an atomic invalid transaction, commits edits, undoes/redoes,
cancels a gesture preview, deep-copies through the runtime, and reopens the engine.
The storage and protocol-2 transport are in-memory examples. Reopening demonstrates
the codec and host contract, not cross-process crash durability or a deployed server.
`npm run test:headless:package` builds, packs/unpacks into an isolated directory,
blocks framework imports and browser-only globals, runs the sample, checks Node
TypeScript consumers without the DOM library, and exercises compiled browser adapters
and module Worker URLs in isolated Chromium. The browser checks load the emitted JS
directly, without TypeScript stripping or a Next bundler.
The React adapter has an explicit entry and optional peer dependencies; portable
consumers do not load it. No registry publication, release support promise, server
deployment or full-goal completion is implied by this local package.
Experimental partial-model runtime: pass a validated prefix plus `loading` in
`EngineOptions`. Version 1 uses recovery format 7; version 2 carries snapshot-bound direct canvas
membership counts and uses format 8. Both keep the scheduler held. Version 3
uses format 9 and retains `runtime.loadingRead` separately from the moving ACK base.
Loaded non-reference data fields and node geometry can be edited and undone; creation,
deletions, ownership/reference changes, uploads, imports and AI admission wait.
`engine.loading.accept(document, versions, state)` extends the captured prefix without
replacing local edits. `engine.loading.complete(document, versions, {sourceToken})`
validates the full capture and releases scheduling only after its cut is durable.
Use `{fresh:true}` only for an explicitly fresh complete read; conflicts require the
existing reviewed recovery flow. `engine.loading.resume()` coalesces a fresh complete
read through the project's persistence adapter and persists completion before resolving.
`workspace.resumeLoading(projectId)` delegates to that owner and reports current-identity
failures. Closing the project aborts the read and saves the recoverable prefix; detaching
a view does not close its engine.
Endoleap's page now opens the same project engine for a validated prefix, allows the
restricted field/geometry edits above, coalesces incoming prefixes, and preserves its
textarea, history and viewport when the full model arrives. App navigation cancels the
view's stream and asks the retained project owner to complete through a fresh read.
Reload recovers the durable prefix when that project is opened again. This is not a
persistent HTTP cursor or a workspace-wide startup scan. Version-3 scoped ACK/chunk
coordination and later scope-based structure/import permissions are described below.
Scope-aware queries are obtained from `engine.editor.getQueries()` and share the
same publication boundary as the root. `lookupElement(id)` returns `present`,
`absent` (complete project), or `unloaded`; a missing ID in a partial model is not
proof of absence. `membersResult(canvasId)` adds a completeness flag for direct
members; `childCanvasesResult` covers published topology, while `incidentResult`
requires the complete project because another owner may contain an incoming edge.
Legacy array-returning methods enumerate loaded records only. Hit-query completeness
also includes scope coverage. Derived indexes and coverage views are not persisted.
An optional `canvasElementCounts` field in the version-1 stream header allows new
clients to certify individual scopes. Missing metadata retains legacy conservative
coverage. Counts include nodes and connections; they must cover exactly the declared
active canvases, sum to the total, and never be exceeded by received records. A scope
is ready only after its full count has entered the dependency-closed published prefix.
Version-2 loading permits `connection.path` edits and history in a certified owner
scope; endpoint, ownership, creation and deletion edits still wait. Scope completion never replaces terminal/EOF validation for the whole project.
Versions 1/2 continue to hold network scheduling. In version 3, mutable operations
wait in the scheduler's derived admission set until every participant scope is
complete and its root is durable; unrelated certified scopes can send meanwhile.
The normal frozen-body/key, writer claims and pre-dispatch durability barrier apply.
`runtime.loadingRead` retains the original source document and version vector. Incoming
prefixes and same-source completion validate against that capture; scopes with advanced
ACK versions retain their newer acknowledged records, then pending intents replay.
`Workspace.open(document, versions, {sourceToken})` completes that original stream,
even when its captured version is now older than a scoped ACK. An unrelated fresh
read waits for outstanding receipts and uses a scheduling hold while capturing and
persisting its base; edits during the read remain local and are replayed.
Recovery format 9 encodes both baselines and original frozen requests. Unknown results
still query/retry the same body/key after reopening. Partial-source completion clears
the retained read baseline and forces a full checkpoint without older partial journal
dependencies. Creation, deletion, owner/endpoint/reference changes, uploads and AI
remain gated during partial loading; broader structural scope admission and real
Foundry deployment/integration remain unfinished.
Loading version 4 adds an optional stream-header `elementOwners` manifest and uses
recovery format 10. It covers every active source ID, including unloaded records,
and must match both per-owner counts and each received record. New IDs must also
avoid the captured tombstones; new canvas IDs avoid all captured version keys.
Without this manifest the page retains version-3 field-only admission.
In version 4, complete owners can create elements and a frame/child-canvas pair,
undo/redo them, redirect loaded connections, and move/remove objects born after the
capture when the affected owners are ready. Existing-element removal/reparenting
requires the entire captured source closure; a missing incoming edge is not proof
that no edge exists. Declared references are checked against the transaction's final
model. Shared loaded asset references are permitted; new uploads, generation,
complete export remain gated in version 4. Version 5 adds import/copy admission as described below.
ACK validation reconstructs the exact protocol-2 effects of applied logical intents
on the original source; late source blocks cannot erase births, restorations or tree
changes. Both the source capture and exact request identities recover through format
10. Pure intent proofs and applied prefixes are cached by immutable source/registry/
intent identities; changing the source invalidates those proofs.
Query coverage also exposes an immutable captured ID set. A missing ID outside that
complete namespace can be reported absent before all payloads arrive; an ID inside it
remains unloaded until received. An old tombstone alone cannot classify a captured
restored ID as absent. Absence does not make an old ID available for fresh creation.
The identity manifest costs O(N) header bytes and memory and still obeys the existing
8 MiB line / 64 MiB response limits; this does not make database acquisition lazy.
Loading version 5 (recovery format 11) also retains the complete captured
`sourceCanvases` topology from the existing stream header, even for partitions
whose entry payloads are not published yet. With counts and `elementOwners`, the
page now selects this version. Existing v1–v4 recovery images retain their original
contracts; future format 12 is rejected.
`engine.imports.canStart(canvasId)` and `canDuplicate(selection, {viewCanvasId})`
return availability for UI controls, including policy and lifecycle checks. The
actual methods recheck admission. Native NDJSON import requires a complete target
owner. Deep copy requires complete selected subtrees and captured ancestor owners,
so upper-canvas incoming edges are copied from the same original endpoint into
the new partition. A declared but unpublished child is not an empty partition.
Unrelated incomplete partitions do not prevent these jobs from starting.
The existing ImportJob/source Blob/runner, history and scheduler are reused.
`source.loadingAdmission` records the source token, latest captured logical
operation sequence, and sorted required canvas IDs. On partial recovery it must
match the retained read and proved scopes. Before copy planning/resume, the source
topology is checked against that historical logical cut, independently of newer
source chunks and local edits. Source SHA, stable mappings and generation checks
continue to protect payloads and late callbacks. This is provenance metadata,
not another queue or a lock held during parsing.
Pause/reopen/resume retains source bytes and identities. Cancellation keeps already
published content and does not restart; undo remains independent of the source
parser. All content sends through the normal durability and frozen-request barriers.
Full-source completion removes the loading marker and read baseline without
replacing job/history state. In version 5, new image uploads and generation remain gated while
loading; loaded shared resource references obey the existing structural rules.
Loading version 6 (recovery format 12) admits local images in complete owners.
`engine.canStageFiles(canvasId)` supplies UI availability; `stageFiles` rechecks
actual transaction changes and policy. Existing field/geometry and import/copy
contracts continue to apply, including copying a pending image by the same
ResourceId rather than starting another upload.
`ResourceRecord.loadingAdmission` records `{version:1, sourceToken, sourceURL,
canvasIds}`. The resource's original upload OperationId precedes its first document
use. Validation checks that first use's exact owner set, complete captured/born
scopes, and the adapter's URL interpretation on reopening. New resource references
are admitted from these proved identities and the current staging transaction;
arbitrary resource IDs/URLs remain rejected. The proof and file bytes use the
existing project recovery store, not a second queue or workspace-wide AssetDepot.
Working content, history, upload intent and Blob bytes retain their existing atomic
durable cut. An upload has no canvas participants, so its own admission does not
claim a fake canvas writer; it still waits for the root/file/request barriers.
Document requests depend on stored upload results and keep normal owner/version
claims. Upload receipt validation does not advance document versions. A lost ACK
reopens the same frozen key/body and queries it before another document send.
Whole-source completion preserves pending/stored resources and history. Default
image sizing and display handles reuse the existing decoder/cache lifecycle.
Undo/redo, source failure, quota retry and late chunks do not re-upload bytes or
restore deleted incarnations. Earlier loading/recovery versions retain their old
restrictions; current recovery compatibility is described below. Generation, cross-project durable depot
sharing and broader lifecycle/acceptance work remain separate unfinished goals.
The package now exposes `/bindings` for pure projection, renderer descriptors,
geometry/projection caches, hit queries, connectors and resource-view handles.
`projectCanvas` consumes the captured Document/Registry/queries and optional gesture
geometry. It returns view DTOs; it does not own another document, history or queue.
Projection DTOs and compatibility card/edge hints no longer import React Flow types
or the legacy element classes. Legacy views re-export the same shared data types.
`/react-flow` is the explicit client/framework entry. It exports renderer factories,
the command context/hook, and `ReactFlowProjectionNode`/`ReactFlowProjectionEdge`
structural adapter types. React, React DOM and React Flow are optional peers at the
package level, required when a host opts into this entry. Core/runtime/service and
pure bindings remain usable without them. The declared floors are React/React DOM
19.3.0 and React Flow 12.12.1; tests use the locally installed compatible versions,
not a claim to have tested every allowed version.
A host retains the immutable renderer catalog and creates view-local projection
caches; installed components submit commands through its scoped Provider. Attaching
or removing UI does not replace the ProjectEngine or terminate durable jobs.
Compatibility DTOs are rendering hints, not the model schema or a migration format.
The existing application now uses the same explicit framework entry.
The packed Node consumer projects and hits its custom counter using only public
exports, with DOM globals and external runtime dependency resolution blocked. Its
NodeNext type check excludes DOM libraries. A separate peer consumer renders a
registered component, verifies its command context, and checks typed NodeProps and
EdgeProps with the declared peers. Browser tests still execute the compiled ESM
storage and Worker artifacts independently of Next.js/TypeScript stripping.
Workspace asset depot and file storage:
`createWorkspaceRuntime` owns one depot when `StoragePort.assetDepot` is provided.
`await engine.adoptResource(id, (resource, tx) => { ... }, {signal})` obtains fresh
server permission, verifies the original bytes, and runs a synchronous editor
transaction. The callback uses the existing `resource.id` / canonical `resource.src`
in a newly allocated element; it also receives server dimensions in `resource.asset`.
The project stores a `SharedAssetBinding`, not a second upload. Binding, document,
history, ledger, shared bytes and pins cross the same durable barrier. The returned
local `Result` does not imply durability or backend completion. Async or failed
callbacks leave no partial binding. The Foundry authority requires the matching
permission-checked asset metadata endpoint; cached metadata is not an authorization.
Bindings use recovery envelope 13 (including partial loading v6); readers retain
formats 1–12 and reject unknown future formats. A reopened binding can display
shared local bytes, and undo/redo reuse the asset without uploading again. The
workspace closes projects before its depot; retiring the identity fences late work.
The browser adapter uses IndexedDB version 4. After a proven stored-upload cut, a
project's `files` row holds an opaque pointer to digest-addressed `asset-bytes`.
Unconfirmed uploads and import source files keep their original Blob rows. Use
`readFile` / `scanFiles`; direct assumptions that every raw files-store row is a
Blob are invalid. Logical reads preserve the original local MIME, even if the
server detected another MIME. Existing v3 databases upgrade without dropping
checkpoints or original files and promote on a subsequent matching Root commit
(or an idempotent reindex). Older DB readers reject v4. Blob URLs and already
returned Blob objects live independently of a later persistent eviction; actual
disk reclamation remains the browser's responsibility.
Resource identities retain immutable metadata after byte eviction. Complete,
revision-matched durable manifests from every project in the workspace protect
closed editors; missing/stale/opaque coverage prevents shared deletion. Distinct
resource IDs may own the same digest; the shared byte row disappears only when
its last live identity is retired. An older Root cannot release temporary bytes
that belong to a newer adoption revision. Collection is explicit and local; this
preview does not enable background deletion on Foundry or claim the full retention,
permission-race and product acceptance matrix is finished.
React bindings:
Wrap descendants in `CanvasProvider` with an existing `engine` and, for session
hooks, an already attached `viewId`. The Provider borrows both; unmounting it does
not close the project or detach a view owned by the host. Explicitly unmount the
surface, detach its view, and close the engine at their respective lifetimes.
Import from `/react` for React only, or the re-export in `/react-flow`.
- `useCanvas(selector, isEqual?)` observes the one immutable project Root. Default
equality is `Object.is`; choose an appropriate comparator for allocated results.
Snapshots are cached per selection and unchanged selections do not notify React.
- `useElement(idOrTarget, selector?, isEqual?)` returns `present`, `absent`,
`unloaded`, or `stale`, or the supplied selector result. A Target includes
incarnation; a restored element does not silently retarget it. Custom selectors
use `Object.is` by default; provide a comparator for allocated field groups.
- `useActions(target)` exposes stable `rename`, `setData`, `resize`, `remove`, and
`moveTo` facades over normal editor commands. Optional command settings retain
label/grouping/history semantics. Call actions from events, not during render.
- `useMode(target)` returns the current view's mode and `setMode`; its session
subscription is keyed by view and target. It never writes document/history
state. Missing view context is an explicit error.
- `useGestureBindings(target)` returns effective geometry, active ownership and
`begin/update/commit/cancel`. A token protects the lease; cleanup cancels only
its own lease. Controller replacements keep independent token slots. Preview
geometry remains shared across views; active ownership and controls belong to
the owning view. Host bindings still handle pointer capture and events.
- `useOperation(id)` observes both the logical record and its frozen request;
request-only changes are included without observing unrelated Root fields.
- `useAsset(id)` resolves a releasable display handle, returns metadata plus
loading/ready/error/url/retry, and releases on ref/owner change or unmount.
Ready means the display handle resolved; it does not prove upload ACK, image
decoding or a successful subsequent fetch of a remote fallback URL.
- `useSyncStatus(canvasId?)` reports pending/blocked/unknown work plus loading,
dirty and persistence errors. Durability is project-wide even when filtering
operations to one canvas. It does not call an undurable Root synced. Pending
counts logical ledger work; running generation/import job progress is observed
separately from document synchronization.
`engine.getDurability()` now returns a stable readonly snapshot including closing,
closed and an optional error. `subscribeDurability` observes successful cuts,
failures and closure without creating another business Root. Hooks and the Provider
own no second document, ledger, network queue or storage lifecycle. Source selectors
still run on Root notifications; this is not a claim of complete per-field indexing.
A React renderer may use these hooks once its real engine exists. For the early
read-only preview before an engine opens, declare a separate `preview` renderer in
its `ReactRendererDefinition`. The assembly exposes `previewNodeTypes` and
`previewEdgeTypes`. An omitted preview gets a passive preparing placeholder; the
app never constructs a fake editor merely to satisfy the hooks. Preview functions
must render the supplied records without requiring CanvasProvider. Built-in preview
renderers are registered explicitly. Framework definitions never enter core data.
Presentation scheduling:
`createFrameQueue(clock, publish)` accumulates dirty keys in a Set and requests
one frame. `flush()` publishes now; reentrant invalidations wait for the next
frame. `dispose()` cancels pending work, including late canceled callbacks.
This queue never holds commands, undo units or requests. `createSessionFrameSource`
wraps a session source with a per-subscription queue; reads still return the
current session immediately, while preview notifications coalesce per frame.
The application uses it for CanvasView sessions. Pointerup reads its final event
and commits through the engine synchronously; persistence and Root notifications
are not frame-throttled. This does not implement paint extent indexing or selective
per-element drawing: dirty keys currently schedule a view notification.
Node paint overflow:
`createPaintIndex` owns a rebuildable view-space grid of rectangles, with bounded
cell expansion and oversized fallbacks. `projectedPaintOverflow` asks node geometry
for the `paint` role separately from logical/hit geometry, transforms its bounds
into scene space, and reports failed handlers conservatively. CanvasView queries
the overflow index against the live viewport; visible overflow or unknown bounds
suspend native culling so extended paint is retained. This currently disables
culling for the view rather than selectively retaining one node. Geometry
extensions must declare their paint extent; undeclared DOM overflow cannot be
inferred. Edge/overlay paint indexing and region-based drawing remain separate work.
`createProjectedPaintIndex` retains the grid per engine/view and caches each node's
paint result by projected node identity, immutable record identity and canvas
transform. Unchanged nodes reuse their geometry result; removed nodes release
cache entries. Viewport queries reuse the grid. `clear()` discards it. The cache
still scans projected nodes and synchronizes rectangle membership; it is not a
claim of constant-time full-frame updates or complete region-based drawing.
`createJsonArrayImportSource` is a seekable parser building block for a UTF-8 JSON
array, with byte cursors, bounded record payloads, structural depth limits and
abort/yield checks. It uses parserVersion 3 and can feed a host-managed generic
ImportRunner. It does not yet interpret the exported document envelope, and
ProjectEngine now accepts native record arrays through `imports.startJsonArray(bytes, canvasId)`; the file picker routes `.json` to it. Exported document envelopes use the separate `imports.startDocument` path described below.
A malformed structural tail ends with an explicit failure; previously emitted
records remain available. Empty source bytes reject, while `[]` is a valid empty
array. Array record errors are distinct from document schema/reference failures.
JSON array jobs preserve the original Blob and integrity digest, parserVersion 3,
byte cursor, stable IDs, pending dependencies and failures using the same ImportJob
and commit boundary as NDJSON. Such roots use recovery envelope 14; readers accept
1–16 and reject 17. Downgrading an array-bearing image to 13 rejects. This changes
neither IndexedDB schema v4 nor the server wire protocol. Native array items have
the same `{kind,key,record}` schema as NDJSON; `$target` denotes the chosen canvas.
`imports.startDocument(bytes, canvasId)` accepts the toolbar's exported document
format v1. The JSON picker distinguishes object envelopes from record arrays.
Source canvas ownership and references map to fresh IDs; its root maps to the
explicit target, and tombstones are not copied into the target. The original
file/digest and parserVersion persist. New jobs use parser 5: after the version,
project and canvas headers, records are scanned and decoded on demand rather than
indexing the full elements array first. The ordinal cursor resumes against the
same bytes. Legal prefixes survive malformed array/footer tails with an explicit
partial result. Headers following the elements array require indexed preparation
to establish ownership first. Legacy parser 4 jobs keep their indexed interpreter.
New document jobs use recovery v16; legacy document jobs keep v15 (readers accept
1–16, reject 17), without changing DB v4 or the wire protocol. Resource-bearing
records use fresh authorization when the host provides a depot and authority;
missing capabilities or denied resources produce explicit partial results.
Complete service acceptance remains pending. Source SHA verification still reads
the original file before the runner starts; this removes the second full range
scan, not the remaining integrity startup barrier.
ImportRunner hosts may provide async `beforeCommit(units, job, signal)` for read-only
preparation such as resource permission checks. It returns Availability; a denial
uses normal independent-group failure isolation. A successful result still must
pass cancellation/hold/generation/status checks before the synchronous commit.
No document, history or committed source progress advances while it waits. The document import host now uses this hook to authorize/adopt resource references as described below.
Document resource adoption now resolves fresh permission through the workspace
AssetDepot before each candidate commit, verifies original bytes/digest, and
canonicalizes `assetId` image URLs through the current authority. It retains at
most 32 MiB of prepared bytes per group (a single in-progress authority read may
add its separately limited 20 MiB); over-capacity groups fail explicitly. Authority
or depot absence remains an explicit partial-record failure. Cancellation and
generation fences run before the synchronous transaction. New bindings, adopted
byte ownership, elements, history, ledger and job progress publish together;
rejected transactions restore prior temporary bytes and leave no binding. Existing
owned resources must match immutable stored metadata; no fake upload is created.
Cached bytes never replace fresh permission resolution. Recovery remains v15.
`projectedConnectionPaintOverflow` adds conservative scene-space bounds for cubic
control hulls plus half stroke. It uses the same geometry protocol and connector
adapters as hit testing, with native handles for custom record renderers. Default
work limits are 4096 edges and 4096 total cubic segments. Missing geometry,
invalid values and exhausted budgets remain unknown and suspend native culling;
they are not treated as empty paint. CanvasView queries a separate overflow grid
against its viewport. This currently retains the whole view when needed; it is
not selective edge mounting, exact curve bounds, overlay indexing or dirty-region
rendering. Custom renderers must supply a compatible geometry protocol.
`createOverlayIndex(viewId)` owns ephemeral screen-space placements keyed by
instance ID, each with a target incarnation, placement name, measured bounds,
focus and composition state. Registration returns an update/release lease; a
late old lease cannot remove its replacement. `visible(rect, currentTarget)` also
retains focused/composing editors outside the rect and rejects stale targets.
No overlay state enters the document, history, ledger or recovery image. The
application registers frame settings and node style toolbars through an optional
DOM context, including portalled content; legacy and pre-engine renderers use no
registry. Other overlays need explicit registration. Current retention still
suspends culling for the view; it does not selectively mount each overlay target.
Pointer transport helpers (`ownsPointerCapture`, `acquirePointerCapture`,
`releasePointerCapture`) keep DOM capture separate from editor gesture ownership.
Move/up/cancel must match the active pointer ID. Lost capture also matches its
capture target and cannot cancel a target that already owns a newer platform
capture. Capture acquisition failure cancels the acquired edit lease; detached
release is idempotent. Node transforms and edge controls now share these checks
and cancel/release on blur or unmount. Native React Flow drag handling remains an
adapter path; these helpers do not replace the full tool/input controller system.
While a control owns an active capture, foreign pointer events reaching that
control are consumed without changing the lease; they do not fall through into
pane navigation/connection handlers. Outside an active control, unclaimed input
continues normally. Node drag cancellation only addresses its own pointer/token.
The view projection cache now retains node/edge array identities when all retained
items and their order are unchanged, so metadata-only roots do not hand React Flow
new collections. A changed node replaces its node array while an unchanged edge
array remains stable. Document and transform provenance still come from the fresh
projection; lifecycle/scope changes and `clear()` invalidate the retained arrays.
Projection calculation still scans the scene. This is not selective mounting or
complete dirty-region rendering.
Additional node/overlay retention now checks whether the target is already kept
by native rectangle visibility (including initial measurement/dragging). A toolbar
on an already visible node no longer disables culling for every other node. Only
an otherwise offscreen target that needs extra paint/overlay retention uses the
view fallback. Focus/IME and gesture safeguards remain. This fixes a measured
regression where selecting a frame mounted all 2003 nodes after a large copy.
Node paint signatures now compare effective geometry, lifecycle, owner, component,
transform and declared geometry `dataDependencies`, rather than immutable record
object identity. An ACK that reconstructs equal records no longer recomputes all
node paint bounds. Unrelated titles do not invalidate a protocol that declares
only another data field. Omitting dependencies conservatively compares all data;
components must declare every data field their geometry reads. The scene still
gets scanned, and connection paint currently has its separate computation path.
`createConnectionPaintIndex` retains curve hulls and grid membership across view
updates. Its signature includes the connection record/projection, numeric endpoint
anchors, transform and endpoint rectangles. Equal ACKs and unrelated node titles
do not call curve/stroke geometry again. Cached segment counts still consume the
query budget. Invalid geometry stays unknown and is retried; removed edges release
entries. Registry/project/canvas changes or `clear()` reset the cache. Hosts whose
geometry services capture changing external configuration must pass a changed
`geometryRevision` to `sync`. CanvasView now uses this index with native handles.
The edge list/signature comparison still gets scanned; this is not full dirty-region
drawing or selective offscreen mounting.
React renderer assembly now memoizes installed node/edge/preview components using
stable wrappers shared by original component identity. Explicit React.memo inputs
are preserved. `definitions[].render/preview` retain the original implementations;
`nodeTypes`/`edgeTypes` tables contain the compiled wrappers. Context and hook updates
still propagate normally. The projection cache separately shares equal data/style/
position fields across measurement-only updates without mutating input objects.
A real three-node fixture verifies that editing one node through ACK leaves an
unrelated node and nonincident edge's render function counts unchanged. Connected
edges may legitimately remeasure when a custom renderer's data changes; this does
not assume arbitrary custom Handle layouts stay fixed.
Node projections now carry readonly `__headlessTarget` display metadata with
id/incarnation/ownerCanvasId/display canvasId, written from model identity after
user data is projected. It is not a persisted element field. The application binds
legacy editing actions to that captured target through `useScopedNodeEditing`;
old incarnation/owner/placement and detached/closed-view callbacks do not write to
current records. Frame titles, node titles, text/style/branch/transform controls
use the scoped bridge. Legacy non-headless contexts retain their old interface.
Text grouping now records incarnation and drops obsolete groups. This is separate
from the public explicit-Target action API and gesture lease tokens.
`canvasKeyContext(nativeEvent)` classifies text and overlay ownership using the
composed path, including retargeted shadow inputs, native selects and registered
`data-canvas-overlay-view` placements. Pass it to `routeCanvasKey` with the native
IME state and the event's current `defaultPrevented` value. Window Escape fallback
runs in the bubble phase so target handlers can claim it first; it cancels only
this view's gesture. Style toolbars dismiss their own modes on unclaimed Escape,
while input controls retain priority. The helper needs no browser globals at
module load or when called with non-DOM ports.
Native import scanners share `ImportScheduling` (`now`, `yieldTask`,
`frameBudgetMs`, default 6 ms). Array/envelope scanners check elapsed time every
128 scan steps; NDJSON checks after each bounded chunk. A 16 KiB/step work
threshold also yields when the clock does not advance. Cancellation is checked
again after each real task yield. Byte/ordinal cursors, parser versions and source
fingerprints are unchanged. This is a soft budget: a bounded JSON decode is
indivisible. Legacy and out-of-order document headers retain indexed preparation;
canonical parser 5 headers allow progressive payload scanning.
The dedicated digest Worker uses native `crypto.subtle.digest('SHA-256', ...)`
for inputs up to 64 MiB. The browser pool admits two active Workers, each reading at most
64 MiB on this path. This bounds admitted input per Worker, not total browser
memory or native cleanup timing. Larger inputs or a missing native capability keep the 1 MiB
streaming SHA implementation. Main-thread fallback stays chunked/cooperative.
Native read/hash errors remain visible; they do not silently select another path.
Worker termination still owns cancellation. The full source check remains before
imported elements are published; no journal revisions or integrity gates are skipped.
Element hooks share one upstream editor listener per engine while subscribed.
An ID→bucket Map and listener Sets route explicit changes to their targets;
runtime-only cuts do no element lookup work. Document replacements check active
IDs, and loading coverage changes recheck missing lookups. Reads still consult the
current engine, including while unsubscribed. Buckets retain lookup values, not
per-element Root snapshots; the last listener releases its bucket and the final
bucket releases the upstream listener. No document, history or durable state is
copied into the bindings. Arbitrary `useCanvas` selectors remain Root observers.
```tsx
const title = useElement(target, view =>
view.status === 'present' && typeof view.element.data.title === 'string'
? view.element.data.title : ''
);
```
A change to another field on this target evaluates the title selector but does
not rerender its consumer. Unrelated target edits skip the selector entirely.
This is ID routing and value selection, not a static field dependency index.
Mode and gesture hooks pair keyed element lookups with small session selections,
instead of retaining a full Root/session pair per hook. All session channels in
an engine share one upstream listener. Mode buckets use view→target Maps;
gesture buckets use target IDs. Preview updates compare the active preview map,
then notify only changed targets. A WeakMap indexes each immutable target array
once for incarnation checks. Viewport/selection-only updates and connection
previews do not reevaluate these node hook selectors.
The pairing caches lookup/selection tuples; values stay fresh on direct reads,
and equal effective geometry/mode values suppress intermediate renders. The last
subscription removes its bucket and releases the upstream listener. A controller
identity change (engine/view/target/incarnation) creates a private lease slot, so
old cleanup cannot consume the replacement controller's token. This does not
change the one shared gesture owner, its history unit or durable task lifecycle.
Operation, asset metadata and sync-count channels share one upstream editor
listener per engine. Their dependency tuples contain only the tables they read
(and the loading flag for counts). Table identities use weak object IDs, so a
channel caches a primitive version signature and selected values rather than an
old Root or full table. Changed tables recheck subscribed IDs; unchanged tables
skip those reads. Read caches and last-notified values stay separate so an early
`getSnapshot()` cannot consume another observer's notification.
Sync counts build one scalar index per operations/requests/loading version:
project totals plus canvas→counts. Requests spanning multiple pending operations
count once per matching canvas. Operations with no canvas remain in project
counts. Unknown canvas queries return the shared zero count without growing the
index. This preserves the existing blocked/unknown definitions; table changes
still require a traversal, not a per-record ledger write-set update.
Asset display effects keep their previous ownership, abort/release and retry
rules. Durable-only completion/error/close is still read from the independent
durability source, so saving and synchronization remain distinct.
The host's view-owned scene cache retains one current projection and its document,
canvas, registry/catalog, selection, geometry preview, and connection target/path
inputs. Viewport, tool, modes, task receipts and other views' selections do not
rebuild that scene. Geometries are read from the captured session preview rather
than a newer live session; shared previews still appear in every view. An empty
node gesture and phase-only transitions do not require geometry projection.
Field publications, selection changes and ordinary node/edge previews patch the
changed projections through view-owned ID-to-array-position Maps. Continuous
publication changes are coalesced in a dirty Set (up to 16384 IDs); missing change
chains and canonical replacements compare document records and order first.
Equivalent replacement records retain projected objects and transforms. Entry
geometry/presentation, topology, membership/lifecycle, redirected endpoints and
unsupported scenes retain full traversal. Canvas/renderer replacement also rebuilds.
Array copying and existing paint/measurement/retention passes can still be linear;
this is not a claim that the entire view is O(changed IDs). Numeric geometry caches
remain derived Maps, with no history or durable task ownership. Engine/view
replacement discards this cache; `clear()` also drops the retained scene and
numeric caches. Query provenance is validated even on a cache hit.
Session transitions publish the final view and any owned preview cancellation in
one snapshot. Changing tools cancels only that view's active node/path gesture;
`begin`/`beginConnection` require the selection tool (`tool_not_select` otherwise).
Repeated selection of the current tool is a no-op. The React Flow adapter also
disables native connection handles in pan mode and rejects a late connection
callback against the current tool. Multitouch interruption is view scoped.
Modes remain `Map<ElementId, string>` in each view. On document publication, the
session compares the previous and current target incarnation, owner, structure,
component and data version: replacing or moving the target clears its mode,
while content edits and equivalent canonical clones preserve it. Missing targets
cannot acquire a mode. Identical mode updates and redundant clears do not notify.
This uses a borrowed current document reference, not a second mode identity store.
Each attach disposer runs once, so a late second cleanup cannot detach a newly
attached view with the same ID. Explicit canvas navigation and detach publish the
new view set together with cancellation. Session disposal fences admission before
notifying, clears all views/previews, and is idempotent. These transient actions
create no document/history/ledger entries and change no recovery format.