Skip to content

fix: atomic runtime admission, guarded external values, single-clone history commits - #26

Merged
rendis merged 2 commits into
mainfrom
issue/wonderful-fermi-8r8eid
Oct 5, 2026
Merged

rendis merged 2 commits into
mainfrom
issue/wonderful-fermi-8r8eid

Conversation

@rendis

@rendis rendis commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Follow-up to the changes merged in #22, #23 and #24. Each fix below was first reproduced with a failing test.

Runtime (Go)

Cancellation no longer aborts an operation that has already started.

  • Before: cancellation was checked between every callback and again after each one returned. If the context expired while a successful action ran, SendEvent returned context canceled and the universe stayed in the previous reality, even though the action's side effects had already happened. A retry would repeat them.
  • Now: cancellation is honored while waiting for the machine lock and at admission. Once admitted, an operation runs to completion.
  • Callbacks still receive the context. One that returns an error, including ctx.Err(), fails like any other callback error.
  • An invoke that would start with an already-cancelled context is skipped instead of failing the transition.

MaxConcurrentInvokes now rejects a step before it runs anything.

  • Before: invokes were admitted one by one, after the transition actions had run. A rejected invoke aborted the transition while the invokes admitted before it kept running.
  • Now: before its first callback, each transition step checks that the pool has room for every invoke it can launch: transition, exit and target-entry invokes, including machine and universe constants. Entries check their entry invokes the same way.
  • If the invokes do not fit, the step is rejected with ResourceLimitError before any action runs or invoke starts.

docs/runtime.md and CHANGELOG.md describe the new contract.

Studio

A malformed value or defaultValue no longer crashes the editor.

  • The new tryBuildEditorStateFromExternalValue checks that the definition is an object with a universes map, and catches deserialization errors.
  • On the first render, the editor falls back to an empty machine. On a controlled update, it keeps the current graph. Both cases report the error with console.error.
  • Machines that are valid JSON but have semantic errors still load, so they can be fixed in the editor.

The package styles.css no longer contains @tailwind directives.

  • Per the README, the host app's own Tailwind setup generates the utilities.
  • Under Tailwind 4, the directives made the package stylesheet emit utilities outside any cascade layer (.transform, .filter).
  • I checked the change against the Vite dev server in Chromium.

Gesture checkpoints are cloned once instead of twice.

  • A snapshot from createHistorySnapshot is already a detached copy. When it is committed with commit-snapshot, the history now keeps that copy instead of cloning it again.
  • Other payloads are still cloned. Callers must not mutate a snapshot after committing it; this is documented in the JSDoc.

Double-clone benchmark (Node 22, editor-core dist, median of 15 runs)

Before is the reducer at fb3d56d and after is this branch; nothing else differs. Each gesture captures a base snapshot, applies 30 silent node moves, then commits once.

Realities Commit only, 10 gestures (before → after) Full gesture, 10 gestures (before → after)
100 4.1 ms → 0.5 ms 7.5 ms → 4.0 ms
1,000 35.0 ms → 4.5 ms 68.3 ms → 36.8 ms
5,000 186 ms → 28 ms 362 ms → 206 ms

The commit step is about 7–8× faster, and a full drag gesture about 1.8× faster. The cost that remains is the single clone taken when the gesture starts. A second run gave the same numbers.

Validation

All steps of the Quality workflow were run locally on this branch, and all passed:

  • go build ./..., go vet ./..., go test -race -cover ./... (11 packages), make test-fuzz-smoke
  • pnpm install --frozen-lockfile, lint, typecheck, test (294 tests), Stryker dry-run, build, audit

The four new Go tests fail against fb3d56d and pass here. The ten new external-value tests failed with TypeError before the fix.

claude added 2 commits October 5, 2026 15:05
…invoke limits

Cancellation was checked between every callback and after each one returned,
so a context that expired while a successful action ran aborted the transition
after its side effects. The universe stayed in the previous reality and a retry
would repeat the side effects. Cancellation is now honored only while waiting
for the machine lock and at admission; callbacks still receive the context and
can stop cooperatively by returning an error.

MaxConcurrentInvokes admitted invokes one by one after transition actions ran,
so a rejected invoke aborted the transition while earlier invokes kept running.
Each transition step now reserves capacity for its transition, exit, and target
entry invokes (including machine and universe constants) before its first
callback, and entries reserve their entry invokes the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyRBcc1W1QuigZPCFx26zm
… avoid double clone

- External `value`/`defaultValue` went straight into deserializeStatePro, so a
  null universe, null reality, or non-array `always` threw while building the
  reducer's initial state. Definitions are now checked structurally and
  deserialization errors are caught: the initial render falls back to an empty
  machine and a controlled update keeps the current graph, reporting via
  console.error. Semantically invalid machines still load so they can be fixed.
- The package styles.css kept Tailwind 3 directives. The host app already owns
  utility generation per the README; under Tailwind 4 the directives emitted
  unlayered utilities (.transform, .filter) from the package stylesheet.
- Gesture checkpoints were cloned by createHistorySnapshot and again when
  committed. The history now takes ownership of snapshots created by
  createHistorySnapshot; other payloads are still cloned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyRBcc1W1QuigZPCFx26zm
@rendis
rendis merged commit 94ba36f into main Oct 5, 2026
6 checks passed
@rendis
rendis deleted the issue/wonderful-fermi-8r8eid branch October 5, 2026 15:45
rendis pushed a commit that referenced this pull request Oct 5, 2026
#26 let commit-snapshot take ownership of any snapshot returned by the public
createHistorySnapshot, to skip a second clone. That contradicts the contract
documented in docs/studio-performance.md (supplied checkpoints are mutable and
caller-owned): a consumer that mutated a checkpoint after committing it
silently changed what undo restored.

createHistorySnapshot is copied on commit again. Ownership transfer is now an
internal, non-exported mechanism used only for the editor's own drag/gesture
checkpoint, which is never exposed, so the editor keeps the single-clone gain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyRBcc1W1QuigZPCFx26zm
rendis added a commit that referenced this pull request Oct 5, 2026
fix: atomic runtime admission, guarded external values, single-clone history commits
rendis added a commit that referenced this pull request Oct 5, 2026
#26 let commit-snapshot take ownership of any snapshot returned by the public
createHistorySnapshot, to skip a second clone. That contradicts the contract
documented in docs/studio-performance.md (supplied checkpoints are mutable and
caller-owned): a consumer that mutated a checkpoint after committing it
silently changed what undo restored.

createHistorySnapshot is copied on commit again. Ownership transfer is now an
internal, non-exported mechanism used only for the editor's own drag/gesture
checkpoint, which is never exposed, so the editor keeps the single-clone gain.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants