1190 lines
79 KiB
Markdown
1190 lines
79 KiB
Markdown
# 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 5 (files retain their version-1 pointer format). 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). That parser change retained DB v4 and the wire protocol; the
|
||
subsequent checkpoint representation upgrade uses DB v5 separately. 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.
|
||
|
||
Gesture admission also checks the target owner's visible ancestry relative to the
|
||
owning view's canvas. A collapsed or opaque ancestor is `target_hidden`; an owner
|
||
outside that view's subtree is `target_outside_view`. Node and path gestures use
|
||
the same rule. The temporary owner-to-chain Map shares work for multi-selection;
|
||
it lives only during admission or a document publication, with no project scan.
|
||
|
||
An active gesture cancels with `coordinate_context_changed` when an entry in that
|
||
relative chain changes geometry, normalized content/unit scale, identity, or
|
||
parent relation, or becomes hidden. Entry titles and unrelated style fields do
|
||
not affect the chain. Ancestors above an independently opened child view are
|
||
excluded. Target component/version changes cancel with `target_changed`; removal
|
||
of the view canvas publishes fallback navigation and `canvas_changed` together.
|
||
Late updates/commit cannot use the canceled preview. Failed admission does not
|
||
replace the accepted baseline, and no chain, preview or gesture enters recovery.
|
||
|
||
Internal parser byte ports and read evidence (`import-byte-reader.ts`,
|
||
`import-read-proof.ts`) support progressive native imports before
|
||
whole-file hashing completes. NDJSON, JSON array, streaming document and its indexed
|
||
fallback all accept the optional `readBytes` port; the default Blob path keeps
|
||
its original slice request bounds and verifies the returned byte count.
|
||
|
||
The proof reader binds a file ID and byte length to SHA-256 hashes of aligned
|
||
16 KiB blocks, recording evidence only after an entire range is delivered.
|
||
A restored manifest can verify its recorded blocks before parsing resumes.
|
||
Unrecorded blocks are explicitly outside this proof: the manifest is neither a
|
||
whole-file digest nor authority to dispatch mutations. New native jobs use source
|
||
integrity version 2 and recovery format 17. Parser cursor, IDs, pending locators,
|
||
read evidence and accepted document changes publish through the same StateRoot.
|
||
The complete-source digest runs concurrently; content can display and be edited
|
||
before that digest finishes. Legacy source version 1 keeps its SHA-first path,
|
||
including array recovery 14 and streaming document recovery 16. Copy sources
|
||
retain their existing interpreters. IndexedDB schema remains 4; wire remains 2.
|
||
|
||
The scheduler admits no newly compiled project mutation while any accepted source
|
||
is unsealed or its seal has not reached storage. `durableImportSeals` is a derived
|
||
file-ID → SHA Map rebuilt from each successfully stored cut, not a permission
|
||
boolean or a second job store. Existing immutable requests keep their original
|
||
verified bodies. The gate is conservatively project-wide, including unrelated
|
||
new edits; dependency-scoped dispatch is not claimed.
|
||
|
||
Pause, partial success and cancel after an accepted prefix retain verification
|
||
and source pins. Cancel before any accepted content abandons verification without
|
||
blocking unrelated sends. Read/digest failure retains local content and the gate;
|
||
`retry()` rebuilds the reader and verifies again without restarting a cancelled
|
||
parser. Reopening verifies saved block evidence before continuing the same cursor
|
||
and IDs. Known complete hashes are checked before resumed parsing. A late SHA
|
||
seal changes source metadata without changing parser generation; every parser
|
||
checkpoint merges that current engine-owned metadata instead of reverting it.
|
||
Public runtime updates cannot forge or remove existing proof/hash evidence.
|
||
|
||
Readers and verification tasks belong to ProjectEngine, with per-job Maps and
|
||
AbortControllers. Recovery holds abort and later restart unfinished verification;
|
||
close fences late results and releases readers. Job source metadata is durable;
|
||
read caches, pending Promises and controllers are transient. No extra document,
|
||
business queue or history stack is introduced.
|
||
|
||
The reader owns a hash Map (at most 4096 blocks for the 64 MiB source limit), a
|
||
bounded LRU byte Map (four blocks by default), and up to four shared in-flight
|
||
block Promises/four range reads. Each range is capped at 4 MiB. Aborted waiters
|
||
release their own read but do not free occupied native reads early or cancel
|
||
another waiter. Close fences late results and clears byte buffers; checkpoints
|
||
contain no byte buffers. These bounds do not assert native Blob memory release.
|
||
|
||
Byte I/O/capacity/integrity failures throw instead of becoming a terminal malformed
|
||
record. In particular a document reader preserves its ordinal for retry; the
|
||
engine pauses the import, retains its editable prefix, and resumes after the I/O
|
||
recovers. Ordinary malformed JSON/UTF-8 still uses the existing record diagnostics.
|
||
|
||
|
||
Ordinary node insertion now keeps existing node/edge projections and transforms.
|
||
A continuous change hint records element IDs rather than treating every element
|
||
creation as a canvas topology change. A new visible known node uses the shared
|
||
single-node builder; ordered member IDs restore the original depth-first canvas
|
||
order. Hidden additions update the document without rebuilding visible objects.
|
||
The layout index is rebuilt when visible node membership changes, so subsequent
|
||
field edits address the new indices. Canvas changes, removal, reparenting,
|
||
connection insertion/endpoints and unsupported placeholders retain full traversal.
|
||
|
||
This reduces projection construction, not the whole path to O(k). Reordering
|
||
node references and rebuilding layout indices still takes linear work; missing
|
||
membership services require a record scan. The 1000-node insertion test resolves
|
||
only the new renderer, keeps all 1000 prior node objects and forbids full document
|
||
record enumeration when a membership index is available. Actual toolbar insertion
|
||
and ACK are checked separately in the page integration harness.
|
||
|
||
A local Chrome157/Next-development 1000-child copy probe completed with a first
|
||
presented frame around 348 ms and frame-gap P95 around 33 ms. These observations
|
||
are not an SLA or proof of overall speedup. The 10000-child baseline probe at
|
||
0ce3abb remained unresponsive with a CPU-active renderer and approximately
|
||
4.4 GiB sampled physical footprint, and was interrupted. Its completion, first
|
||
frame and root cause remain unverified; 10000-node support is not claimed.
|
||
|
||
|
||
Node measurement requests now share a microtask batch per React Flow store.
|
||
`createNodeInternalsBatch` owns a Map from mount tokens to node IDs and a dirty
|
||
Set of tokens. A flush deduplicates live IDs and invokes the public bulk update
|
||
once. Releasing a mount removes its pending request without touching a replacement
|
||
mount for the same ID. Reentrant invalidation schedules another flush; separate
|
||
stores never share work. The React adapter keys its WeakMap by `getState`, because
|
||
`useStoreApi()` creates a separate wrapper per hook. Acquisition/release happens
|
||
in effects, and the invalidate callback stays stable across renders. The SDK
|
||
still schedules its own animation frame; the adapter adds no extra frame timer.
|
||
|
||
Inactive node/frame toolbars no longer mount `NodeToolbar`. AnimatePresence owns
|
||
the conditional toolbar so selection, portal editing and exit animation remain
|
||
available. In React Flow 12.12.1 each mounted toolbar subscribes to a selector
|
||
that scans the whole node array; an empty child was not an inactive subscription.
|
||
Likewise each individual forced measurement updates all absolute positions, so
|
||
per-node mount calls multiplied work. Both paths are bounded by actual active
|
||
UI needs now. These are renderer resources; no model, history or durable queue
|
||
state changes.
|
||
|
||
A 10000-node page reached its initial scene after these changes, but its copy
|
||
probe still exceeded the 240-second completion window. During the copy it accepted
|
||
read-only diagnostics and displayed incremental content; commit/persistence costs
|
||
remain unresolved at that scale. Do not interpret the initial-scene improvement
|
||
or the batching checks as a passed 10000-node copy budget.
|
||
|
||
|
||
Browser storage writes `journalVersion: 2` deltas whose SHA-256 covers the ordered
|
||
JSON object `{journalVersion, baseRevision, baseChecksum, header, patches}`. The
|
||
prior checksum may belong to a full checkpoint, a v1 delta's reconstructed image,
|
||
or another v2 delta. Checkpoints and full-record fallbacks still hash the entire
|
||
image. Readers retain v1's full-image checksum rule and select the interpreter
|
||
explicitly; unknown journal versions fail. DB schema remains 4 and RecoveryImage
|
||
remains 1–17. This changes the internal journal interpreter, not wire protocol 2.
|
||
|
||
The recovery codec records exact UTF-8 byte lengths while producing immutable JSON,
|
||
including omission/null normalization and escaped Unicode. Only factory-owned
|
||
images expose the cached size, so choosing a smaller delta no longer first
|
||
serializes the complete owned image. Mutable callers retain defensive copying
|
||
and full size calculation. Noncanonical image header order/fields use a full
|
||
record to preserve exact serialization.
|
||
|
||
Each leader retains its last verified image/checksum and checkpoint revision in
|
||
WeakMaps. Both advance only after transaction completion. Every next cut still
|
||
checks stored metadata, prior checksum, continuous revision and local revision;
|
||
all file/depot writes remain atomic with the cut. Same-cut retries reread and
|
||
validate the chain, compare complete serialized images (including property order),
|
||
and keep the existing restrictions on new files. No Root cuts are skipped.
|
||
Recovery hashes/applies outside the read transaction, validates every reconstructed
|
||
image, and retains original frozen request bytes. A v1-only reader must reject a
|
||
v2 row rather than ignore its checksum semantics.
|
||
|
||
The 10000-child copy probe still exceeded its 240-second completion window after
|
||
this change. The exact hashed-input browser test proves removal of whole-image
|
||
hashing for ordinary adjacent delta writes; this is not a passed end-to-end copy
|
||
budget or a claim that encoding, rendering and dispatch costs are all resolved.
|
||
|
||
|
||
Import identity capture now reuses a frozen, own-data string dictionary when no
|
||
new ID is reserved. One active runner retains its last ID-table reference and a
|
||
reverse Set; only the first actual allocation copies both. Mutable dictionaries
|
||
and frozen getters are captured defensively, and a late reserve callback cannot
|
||
mutate an already published result. The index is cleared when the pump ends,
|
||
including pause/cancel/close/restart; the durable `stableIds` remains on ImportJob.
|
||
Allocation keeps the caller port method receiver. No task or recovery version
|
||
changes, and first-item publication/durability barriers remain in place.
|
||
|
||
Recovery also reuses frozen plain document views across metadata-only Roots.
|
||
`isFactoryDocument` identifies an envelope with fixed table membership produced
|
||
by core constructors; it is not a content-validation or deep-immutability test.
|
||
A WeakMap owns only a derived plain view. Its array/tuple containers are frozen
|
||
without freezing caller-owned records. The codec still independently checks deep
|
||
immutability, so mutable values inherited through `applyChanges` are reevaluated.
|
||
Unowned envelopes reread mutable map membership. A test prohibits document-record
|
||
enumeration during a metadata-only encode after the initial view is built.
|
||
|
||
The 1000-child copy probe completes with these paths, but the 10000-child copy
|
||
still exceeds 240 seconds. Removing ID-table copies and metadata-only document
|
||
scans is directly verified; a general throughput improvement at 10000 nodes is
|
||
not established. Per-record commit, ACK and remaining table traversal costs remain
|
||
part of the larger implementation goal.
|
||
|
||
Import commit sizing uses runner-local feedback rather than dividing total
|
||
transaction time by unit count and treating it as a purely marginal cost.
|
||
The first ready record remains immediate. Successful batches within the soft
|
||
frame budget grow by a bounded step; an over-budget batch reduces the limit.
|
||
A short tail or dependency group cannot increase it. `maxBatch`, byte/change
|
||
limits, atomic reference cycles, parser slices and durability backpressure keep
|
||
their existing roles. Rejected attempts do not train the success-cost estimate.
|
||
The limit is a scheduling hint, not another persistent queue or an assurance
|
||
that an indivisible transaction or subsequent renderer work fits one frame.
|
||
|
||
Node paint entries retain a detached dependency signature until an input changes.
|
||
Unchanged rows compare scalar geometry/transform/preview values and declared data
|
||
fields directly, without allocating replacement signature objects or dependency
|
||
arrays. Unspecified data dependencies still compare the complete data snapshot.
|
||
Nested mutable data is copied on capture, and renderer fallback changes the
|
||
selected protocol, so neither same object identity nor equal geometry alone
|
||
permits reuse. `stats().captured` counts actual dependency captures. Projection
|
||
retention also keeps the existing retained row when the incoming node is the same
|
||
object in the same lifetime. Both caches remain derived view-owned state; the
|
||
full membership scan and paint-grid reconciliation remain present.
|
||
|
||
Editor create commands now stage transaction-local append views instead of
|
||
copying all document tables for every element/canvas. Each view has fixed
|
||
membership, so a policy or caller retaining an earlier `getDocument()` prefix
|
||
cannot observe later additions. Ordered policy checks, ID/tombstone admission,
|
||
record cloning and final document validation remain unchanged. At most 64 new
|
||
entries are copied in a staging window before folding to a new read-only base.
|
||
The transaction materializes an owned document once before publication; a
|
||
non-create command can materialize earlier through the normal change path.
|
||
These views are unpublished preparation, not a second document store or durable
|
||
queue. Rollback drops them without publishing history, ledger or runtime state.
|
||
|
||
Resource retention now shares one synchronous history/ledger analysis between
|
||
working and acknowledged document scans. The temporary scanner holds interpreted
|
||
transition ranges, the earliest pending sequence, the resource-field union and
|
||
schema candidates only for IDs used by historical/pending data-field changes.
|
||
Create-only histories need no per-element schema table. Both documents still
|
||
produce their own pins and completeness result; all supported transitions,
|
||
including resolved evidence, are validated. Cross-schema fallback pins and owner
|
||
order are preserved. The scanner is internal to one retention pass and is not
|
||
stored across Roots, projects or registry changes.
|
||
|
||
Query view construction no longer leaves a callback capturing the previous query
|
||
in the retained method context. Latest methods retain their own document/tables,
|
||
not the complete chain of discarded generations. Explicitly held historical
|
||
queries remain valid. A child-process GC check covers raw queries and the actual
|
||
editor with query coverage installed.
|
||
|
||
Persistence uses one writer and the existing pending Root Map. `writePrefix`
|
||
captures a finite Root boundary, validates every receipt and publishes at most one
|
||
durable-generation update for that prefix. The receipt-derived maximum survives
|
||
a failed metadata publication as a writer publication obligation. The writer
|
||
promise is installed before work, so synchronous listeners cannot start another
|
||
writer. No existing Root is skipped or rewritten.
|
||
|
||
Transport's internal `persistThrough` waits for its fixed request/dispatch Root;
|
||
it does not chase later local edits. Public `persist` still flushes all pending
|
||
Roots and the derived publication obligation, including metadata echoes. Failed
|
||
writes reject their barriers and retain the exact suffix for retry. Network
|
||
capacity grants recheck leader/identity before calling the adapter. A fenced
|
||
scheduler pump reports failed state publication without an unhandled rejection.
|
||
`settle` repeats until the Root and current task queues stabilize, preserving
|
||
future retry/held-job semantics instead of relying on two fixed passes.
|
||
|
||
The actual-page test uses bounded CDP requests and rejects disconnected/crashed
|
||
or dialog-blocked targets without accepting dialogs. A failed protocol call cannot
|
||
hang a second metrics read. Browser cleanup escalates only the fixture's own
|
||
process after its exit grace period. Optional heap/receipt sampling and a pinned
|
||
baseline override affect only fixture copies and are diagnostic, not product APIs
|
||
or production performance claims.
|
||
|
||
Recovery serialization memoizes entry arrays only for core-owned read-only Map
|
||
views. The core tracks membership ownership in a WeakSet; the codec's WeakMap
|
||
holds frozen pair/array containers per view. Value objects are not frozen by this
|
||
step. The existing recursive codec still checks nested mutability and depth, so
|
||
mutable values and external Maps are re-read. This is an encoding cache, not a
|
||
second state table; recovery JSON order, formats and journal checks remain the
|
||
same. Replacing a table gets a new entry view and leaves prior images unchanged.
|
||
|
||
Static element validation proofs apply only to records detached and deeply frozen
|
||
by the core's element-data factory. Document construction, element replacement
|
||
and staged creations share this private provenance. Re-enveloping an already
|
||
owned record preserves its immutable value identity instead of cloning it again;
|
||
external and lookalike inputs are still detached. A separate WeakSet records a
|
||
successful intrinsic shape check. Registry support, tombstones, owner membership
|
||
and connection topology remain live checks. Custom schemas are always called with their original receiver.
|
||
Only framework-owned data-only validators can reuse a successful result for the
|
||
same detached record and current validator function; their private proof keeps
|
||
one validator identity per record.
|
||
External or shallow-frozen records never gain a proof. Field updates without
|
||
factory provenance use the complete path. Document equality rejects different
|
||
table sizes before allocating entry arrays; equal-sized tables keep the existing
|
||
ordered comparison and no-op rules.
|
||
|
||
Synchronous document validation loops directly over the same per-record checks
|
||
used by the cooperative iterator. The iterator retains its original yield points;
|
||
the atomic publication path does not pay a generator handoff per unchanged record.
|
||
|
||
Resolved Foundry document receipts have an explicit retention policy. The SDK
|
||
keeps raw receipts by default; CanvasRuntimeHost opts into `summary`. Only a
|
||
successfully reconciled protocol-2 document change with known model/record fields
|
||
can become the adapter's `foundry.reconciled-document` version-1 summary. It keeps
|
||
the request ID, covered logical operation IDs, project ID and actual returned
|
||
canvas versions. Frozen request bytes, ledger outcome, ACK base and history remain
|
||
in their existing owners. This summary does not retain the complete raw response
|
||
for audit, and cannot be used as a fresh server proof or replay input.
|
||
|
||
The engine prepares the summary for the same Root publication that resolves the
|
||
ledger and installs the ACK/document/history result. If that publication rejects,
|
||
the original raw receipt remains available with the unresolved request. A failed
|
||
or invalid summary codec reports the error and retains raw data without failing a
|
||
valid ACK. Unknown fields/components/versions, unresolved requests and run/asset
|
||
receipts stay raw. Existing stored raw receipts are not migrated by this policy.
|
||
The recovery envelope remains unchanged: receipts are opaque JSON, and the
|
||
adapter-local summary has its own version and explicit replay rejection.
|
||
|
||
The application's initial loading fit targets the current canvas's direct node
|
||
members, including partition entries. Expanded descendant cards remain rendered
|
||
and pannable but cannot move their parent entry outside the initial viewport.
|
||
The manual fit control keeps its existing whole-scene behavior.
|
||
|
||
Pending-operation reconciliation replays ordered change batches in one private
|
||
core draft. Each change still sees the results of earlier changes: field presence,
|
||
incarnation, before-value conditions and current owner scope are checked in that
|
||
order. Already-satisfied changes are skipped, but field scope is checked first.
|
||
Only the final document envelope escapes; a late conflict discards the draft and
|
||
leaves the input ACK and ledger unchanged. The runtime retains sequence sorting,
|
||
operation/history-effect admission and final registry/topology validation. This
|
||
removes a full table copy for every pending change without adding intermediate
|
||
Roots, history units or network writes. `applyChanges` uses the same conditional
|
||
change application path and retains its existing strict/default behavior.
|
||
|
||
CanvasView coalesces Root presentation notifications with `createFrameSource`.
|
||
Local user updates/deletes, undo and redo flush pending presentation work
|
||
synchronously so controlled inputs retain their native value/caret. Pure creation
|
||
and background invalidations may share a frame. Each subscription owns one
|
||
disposable frame queue; all editor commits still
|
||
advance scene-change hints synchronously. Snapshot reads always delegate to the
|
||
current authoritative Root, so commands, history, persistence and input identity
|
||
checks do not wait for a frame. The adapter holds no delayed document snapshot.
|
||
Unsubscription cancels pending work, including callbacks delivered late by a
|
||
clock; eager subscription failures also dispose their queue. This batches source
|
||
notifications, not every React render: another store/prop update may read the
|
||
current Root before the pending frame. Session and element subscriptions retain
|
||
their existing routing and immediate read semantics.
|
||
|
||
The actual-page performance harness defaults to development webpack. Add
|
||
`--production` together with `--performance` to build its isolated application
|
||
with `next build --webpack` and serve that completed build with `next start`.
|
||
The report identifies the mode and records build time separately from browser
|
||
copy timings; build failure aborts before measurement. Example:
|
||
|
||
```sh
|
||
HEADLESS_PERFORMANCE_COUNT=10000 HEADLESS_PERFORMANCE_REPORT=/private/tmp/headless-production-10000.json node tests/headless/renderer.integration.mjs --performance --production
|
||
```
|
||
|
||
Both modes retain the same full-copy, upper-canvas edge, synchronization and
|
||
far-card viewport assertions. Production here means an optimized frontend served
|
||
against the fixture HTTP service; it does not claim production Foundry validation.
|
||
|
||
Copy snapshot encoding/index scanning and legacy prepared-source encoding share
|
||
the import time-slice scheduler through an internal copy-budget adapter. They no
|
||
longer sleep every 32 records. Record bytes contribute weighted work so larger
|
||
records trigger budget checks sooner; the existing coarse-clock work ceiling is
|
||
retained. Topology/full validation iterators use the same slice budget. This is
|
||
a soft scheduling budget: JSON operations, document construction and a single
|
||
validation step remain indivisible. Copy cancellation still reports
|
||
`copy_aborted`; host clock/yield errors remain observable. Snapshot formats,
|
||
byte locators, size limits, source digests, lazy payload reads and admission/seal
|
||
ordering are unchanged. No partially encoded snapshot is admitted or returned.
|
||
|
||
ProjectEngine allows the existing import feedback controller to probe batches up
|
||
to 128 ordinary units; the low-level runner's default remains 32 unless the host overrides
|
||
it. The ceiling is not a fixed batch size or a higher time budget. Initial batches
|
||
start at one; measured commits still grow/back off against the same 6ms soft
|
||
budget. Parser time checks, the pending/decoded byte limits, SCC isolation,
|
||
resource authorization limits, durable send barriers and source ordering remain
|
||
in force. Hosts that set a lower runner ceiling retain that ordinary-batch bound. An
|
||
indivisible SCC can exceed the soft unit limit under its existing hard byte and
|
||
change-count limits.
|
||
|
||
Full browser recovery cuts now use a versioned envelope:
|
||
`{checkpointVersion:1, header:{formatVersion,rootRevision,localRevision}, bytes:Blob, checksum}`.
|
||
The Blob contains exact UTF-8 `JSON.stringify(image)` bytes, not another document
|
||
model. It can represent a checkpoint or a full replacement journal cut. Delta
|
||
journals retain versions 1/2; legacy object cuts remain readable. DB v5 fences DB
|
||
v4 clients before these rows are written. Recovery envelope/wire versions do not
|
||
change. Rolling application code back requires retaining the v5 storage reader;
|
||
an old v4 client cannot open the upgraded database.
|
||
|
||
For codec-owned sealed images, serialization splits large containers/strings and
|
||
uses existing codec byte facts to stringify small owned subtrees natively. It
|
||
preserves key order, escaping and surrogate pairs, yielding on the shared soft
|
||
time/work budget before starting an IDB write transaction. External images retain
|
||
the defensive capture/native JSON path. Each yield rechecks the storage owner.
|
||
The complete Blob and checksum are prepared first; head, cut, files and depot
|
||
changes still commit in one transaction. No Root is skipped. Reopening validates
|
||
headers/sequence, hashes exact Blob bytes, decodes strict UTF-8, checks the decoded
|
||
header and then applies later deltas. Future checkpoint versions reject reads
|
||
and writes; malformed/tampered data is never treated as an empty document. Legacy
|
||
recovery, JSON parse, recovery-image capture, same-cut retry comparison and
|
||
individual native operations still have synchronous work; this is not a strict
|
||
6ms bound for all storage paths or a disk-space reduction guarantee.
|
||
|
||
Before Blob serialization, ProjectEngine now awaits cooperative recovery-image
|
||
encoding inside its existing ordered Root writer. Private `dataArray` / `dataRecord`
|
||
constructors copy and freeze DTO container membership; they do not claim their
|
||
children are immutable. Preparation captures each unknown subtree synchronously,
|
||
including getters, custom `toJSON`, Dates and external shallow-frozen records.
|
||
Previously validated immutable codec facts can be reused. Only then does a
|
||
budgeted traversal assemble the owned containers, preserving key order, omitted
|
||
values, sparse-array normalization, depth checks and exact UTF-8 byte facts.
|
||
The default uses `scheduler.yield()` when available (with its receiver), otherwise
|
||
a timer, sharing the 6ms soft budget / 16384-work ceiling. One shared budget spans
|
||
nested containers. Public synchronous encoding and recovery/storage formats stay
|
||
unchanged; async images receive the same codec ownership seal without a second
|
||
full encoding pass.
|
||
|
||
A yield does not publish or skip a Root. Later edits may publish while encoding
|
||
is suspended; the old image still contains its captured state, and the next cut
|
||
is saved after it. Request dispatch retains the durable-prefix barrier. Encoding
|
||
or scheduling failures retain the pending Root for retry and produce no partial
|
||
storage commit. Initial DTO membership copying, synchronous preparation, unknown
|
||
subtrees and individual scalar operations remain synchronous. First-time JSON
|
||
normalization can defer only when its detached provenance is established. This is partial time slicing, not a strict whole-Root latency
|
||
bound. Grouped history entries now keep shape ownership for newly built changes and
|
||
operation-ID arrays, including undo/redo lifecycle remaps. Import runners keep it
|
||
for their own job envelopes, completed-key arrays, binding dictionaries and
|
||
pending locator tables; newly
|
||
allocated identity tables use the same private constructor. The engine's source
|
||
proof refresh preserves this envelope ownership. Arbitrary host checkpoint
|
||
replacements keep defensive capture. These constructors still freeze nested data
|
||
before freezing their container, retaining the prior deep-freeze behavior; their
|
||
brand alone never permits mutable child values to escape capture.
|
||
|
||
Primitive values captured in these tables cannot change while suspended. Their
|
||
normalization and UTF-8 sizing now run in the cooperative traversal rather than
|
||
the preparation scan. Unknown object/proxy reflection stays entirely within the
|
||
original normalizer; preparation adds no proxy traps or callback invocations.
|
||
Membership copying and the preparation scan itself remain synchronous, as do
|
||
unknown subtrees and individual native operations.
|
||
|
||
A blocked schema upgrade retires its pending open. If the old connection closes
|
||
later, that abandoned request aborts in `onupgradeneeded` rather than silently
|
||
upgrading after rejection. A fresh adapter can retry once the blocking client has
|
||
closed; closing an adapter before its upgrade event also aborts the pending open.
|
||
|
||
Stream-copy planning separates cooperative task yields from durable preparation
|
||
checkpoints. ProjectEngine requests checkpoints on phase changes, the first
|
||
nonempty ID allocation, approximately 50ms of elapsed work or 8192 planning
|
||
steps at slice boundaries. It also persists the final ready mapping before
|
||
entering execution. The planner exposes only a tiny immutable progress read to
|
||
make this decision; full checkpoint cloning happens only when saving/returning.
|
||
Persistence wait time is excluded from the next interval. Each small planning
|
||
slice still yields and checks cancellation. Already-published Roots retain their
|
||
normal ordered durability; unsaved work is private preparation with no created
|
||
elements or requests. The low-level helper defaults to its prior per-slice
|
||
checkpoint policy unless a cadence is requested. Checkpoint/source formats and
|
||
resume IDs stay unchanged; a crash/pause can redo only unsaved private planning.
|
||
|
||
The app renderer retains explicit view owners without turning off native culling.
|
||
Adapter-local `__endoleapRetain` flags cover node modes/gestures, focused native
|
||
inputs, registered Portal overlays, known node paint overflow and curved-edge
|
||
paint. Retained connections also retain their endpoint owners. The complete
|
||
nodes/edges stay in React Flow's lookup; geometry, dragging, model data and hidden
|
||
state are not fabricated. A view-local weak cache preserves tagged row identity,
|
||
and ending an owner returns the original row. Detached focus is retired even
|
||
when removing a renderer emits no blur. Flags never enter a Root or file.
|
||
Application panels/toolbars explicitly mark `data-canvas-ui-view` with their
|
||
current view ID, including portalled dialogs. Their inputs have a lifetime
|
||
independent of node culling; focusing them does not pin native nodes. Registered
|
||
node portals still declare their explicit target. Only unscoped third-party
|
||
inputs/portals keep the conservative whole-graph compatibility fallback; missing
|
||
ownership is not guessed. Exhausting curve-inspection work retains the edge
|
||
layer and its endpoints, while unrelated nodes remain culled. This can still
|
||
retain many endpoints/edges; it is not a constant-cost or complete geometry proof.
|
||
|
||
React Flow 12.12.1 does not expose a retained-ID selector. The app-only
|
||
`scripts/loaders/xyflow-retained.cjs` extends its two ESM visibility selectors and
|
||
fixes the empty selection-listener guard at compile time, under both webpack
|
||
and Turbopack in `next.config.ts`. It requires
|
||
the pinned version and exact source anchors, rejecting mismatches before silently
|
||
losing editor retention. Dependency upgrades require reviewing this extension.
|
||
Shared node_modules are not edited. This is an app renderer build requirement;
|
||
the portable headless SDK/optional bindings do not include the Next loader.
|
||
|
||
The pinned dependency treats an empty `onSelectionChangeHandlers` array as
|
||
truthy. The app guard checks its length, so an unused SelectionListenerInner no
|
||
longer scans both native lookups on every store update. A callback prop or a real
|
||
`useOnSelectionChange` subscriber still mounts the original listener. This does
|
||
not add a second selection cache. The isolated page fixture checks notification,
|
||
unsubscribe/re-subscribe and scan counts; counters and the fixture observer are
|
||
excluded from performance builds and the production app. The VM regression also
|
||
checks callback-prop delivery against both installed ESM entry files.
|
||
|
||
The actual-page performance probe double-clicks a visible text card with native
|
||
Chrome input, checks mounted-node growth, inserts text and checks DOM identity,
|
||
focus and content before measuring deep copy. It then clears selection with a
|
||
native empty-pane click. Reports include `inputCheck.beforeEditing` and
|
||
`inputCheck.duringEditing`, `inputCheck.offscreenEditing` and
|
||
`inputCheck.offscreenPortal`; this preflight is outside the copy timing window.
|
||
The screen-pan probes preserve native IME and an unsaved Portal title, bound
|
||
mounted owners, check release after blur, and remount saved content. Independent
|
||
AI-panel focus/pan is recorded as `panelFocusedNodes`/`offscreenPanel`; blur saves
|
||
settings without starting a generation task. The actual-page suite also exceeds
|
||
the 4096-edge inspection limit and checks unrelated offscreen cards stay culled. Add
|
||
`--turbopack` to the page/performance runner to use the app's default compiler.
|
||
Turbopack fixtures live temporarily under the physical dependency workspace so
|
||
its filesystem fence can resolve Next; the exact unique directory is removed in
|
||
finally. Webpack fixtures continue to use the OS temporary directory.
|
||
|
||
Set `HEADLESS_PERFORMANCE_EDIT_DURING_COPY=1` with `--performance` and at least
|
||
1000 nodes to add a mixed-workload probe. After the first copied frame, it finds
|
||
an exposed native hit point on the original card, double-clicks it, composes
|
||
Chinese text and inserts three text chunks while copying. It checks focus/DOM
|
||
retention, bounded mounted nodes and that input actually overlapped publication.
|
||
After all ACKs it checks the original's new text and the copy's captured old text
|
||
separately. No synthetic click passes through an overlapping copied card.
|
||
`duringCopyInput.samples` records browser input-event time to the second rAF,
|
||
latest value and retained focus. These frame opportunities exclude time before
|
||
the browser dispatches the input event; they are not end-to-end key latency or
|
||
proof that every intermediate composition string reached the display. Mixed
|
||
workload timings must be kept separate from the no-input copy baseline.
|
||
|
||
Resource retention reuses flattened history-transition changes across durability
|
||
cuts only after successful validation and a descriptor-based proof of immutable
|
||
plain data. Proofs are weakly held. Every own property must be a data property;
|
||
shallow freezing, accessors, custom prototypes, sparse arrays and hidden cycles
|
||
do not qualify. Cached ranges are frozen. Mutable evidence remains live and is
|
||
validated on every read. This cache contains neither resource IDs nor schema
|
||
results: each scan still applies the current registry and keeps working/ACK
|
||
owners separate. Explicit `validateHistoryTransition(..., checkChanges)` calls
|
||
always execute the caller's validator. No persistence/wire format changes.
|
||
|
||
CPU/allocation diagnostics found repeated history-transition validation under
|
||
resource retention. The production mixed-input probe and a same-machine old
|
||
history-path control both pass, but observed total runtimes are similar; this
|
||
change demonstrates reduced repeated interpretation, not an end-to-end speedup.
|
||
|
||
The 10k production copy/input probe keeps one native editor focused from the
|
||
first presented copy through the 50% and 90% publication thresholds, read from
|
||
the actual import status UI. Samples record phase, committed count, focus/DOM
|
||
retention and input-to-two-rAF delay; middle/tail samples also record CDP dispatch
|
||
round-trip time separately. That round trip includes transport and driver work,
|
||
and is not a precise browser queue-time measurement. The final server document
|
||
must retain the original's later edits while the copy retains its earlier source
|
||
snapshot. Smaller scenarios retain the early burst because they can finish
|
||
before a later publication threshold is observed.
|
||
|
||
Pending import records remain bounded seekable locators, not decoded document
|
||
payloads. Runner-created locators, dependency arrays, parser appends and filtered
|
||
pending tables retain private shape ownership, so recovery encoding can slice
|
||
these waiting records even when endpoints have not arrived. Dependency admission,
|
||
capacity checks and durable send barriers are unchanged. The asynchronous encoder
|
||
also rechecks its immutable fact cache when assembling a prepared container:
|
||
when the same source occurs twice in one image, the second occurrence can reuse
|
||
the first completed encoding. Preparation still captures unsafe occurrences
|
||
independently; getters and mutable descendants never qualify for this reuse.
|
||
|
||
First-time element and planning-checkpoint recovery encoding uses the core's
|
||
detached-JSON ownership proof instead of synchronously normalizing every field
|
||
before the first yield.
|
||
Only `detachJsonData` results qualify: JSON-cloned, recursively frozen records
|
||
with immutable JSON descendants. A shallow freeze, a document factory envelope,
|
||
or an arbitrary host object does not qualify. Preparation requires the standard
|
||
Object/Array prototype chain without inherited `toJSON`; otherwise it captures
|
||
through the original normalizer. Every unknown normalization invalidates this
|
||
prototype check because callbacks may change the environment. The cooperative
|
||
walk reads the already detached own data and validates depth while building the
|
||
same immutable JSON and byte facts. A callback later in capture cannot change an
|
||
earlier record's meaning. Cold and cached subtrees both retain the depth limit.
|
||
This moves the first full field/row walk into the shared budget; container-member
|
||
capture and individual native/scalar operations still have synchronous work.
|
||
|
||
Step 4's acceptance evidence, commands and checkpoint encoding boundaries
|
||
are recorded in [STEP4_ACCEPTANCE.md](STEP4_ACCEPTANCE.md). Passing
|
||
functional fixtures is not a claim that all performance limits are resolved.
|
||
|
||
The shared core `json-data.ts` factory gives element data and duplicate planner
|
||
checkpoints the same unforgeable clone provenance; element validity and schema
|
||
proofs remain separate. It replaces their existing `freeze(clone(...))` operation
|
||
without a second clone or a wire-format change. Engine-created job/source/
|
||
preparation envelopes keep shape ownership through admission, checkpoint refresh,
|
||
ready mapping, source verification and pause/resume/cancel. Fresh allocation
|
||
arrays, ready ID tables and root lists use the same shape constructors. Unknown
|
||
children still require synchronous capture; copying an envelope never grants a
|
||
deep-data fact to them. Legacy/restored data retains defensive capture until a
|
||
new planner snapshot supplies fresh provenance.
|
||
|
||
A final ready plan has already crossed its required preparation durability
|
||
barrier. Its encoded facts may therefore already exist, and the ready source
|
||
can reuse them without an artificial yield. Tests distinguish first-time
|
||
preparation slicing from ready-source byte/mapping preservation and reopening.
|
||
|
||
### AI wire negotiation
|
||
|
||
The AI service adapter explicitly requests `responseFormat: 'canvas-v2'` for
|
||
admission, cancellation, output application, receipt queries, model catalogs
|
||
and run polling. `createFoundryTransport` maps it to
|
||
`Accept: application/vnd.foundry.canvas+json;version=2`. A custom `ServiceTransport`
|
||
must preserve that negotiation as well as the frozen body, idempotency key,
|
||
conditional version and account scope. The Next application proxy also declares
|
||
v2, but an independent SDK consumer cannot rely on that proxy being present.
|
||
|
||
`runs.test.mjs` exercises all three writes and their receipt queries through the
|
||
shared HTTP transport. `foundry-ai.integration.mjs` additionally requires a real
|
||
successful 202 before testing lost admission recovery; a rejected admission is
|
||
not evidence for response-loss recovery. Its remote worker fixture replaces
|
||
only generation output, not authenticated HTTP, PostgreSQL or service receipts.
|