Skip to content

refactor(machines): make machine-transition actually pure — allocator-in-snapshot replaces spawn-counter atom (rf2-gr8q)#412

Merged
mike-thompson-day8 merged 1 commit into
mainfrom
rf2-gr8q-allocator-in-snapshot
May 12, 2026
Merged

refactor(machines): make machine-transition actually pure — allocator-in-snapshot replaces spawn-counter atom (rf2-gr8q)#412
mike-thompson-day8 merged 1 commit into
mainfrom
rf2-gr8q-allocator-in-snapshot

Conversation

@mike-thompson-day8
Copy link
Copy Markdown
Contributor

Summary

Per rf2-gr8q's Shape 1 decision (2026-05-12): the module-level (defonce spawn-counter (atom {})) in re-frame.machines.transition is gone. Spawn-id allocation for declarative :invoke / :invoke-all now lives inside the parent snapshot at the reserved :rf/spawn-counter slot — a per-machine-id integer map bumped via update-in inside apply-transition-once and returned as part of the new snapshot.

Before / after

Before. machine-transition's docstring promised "Pure function. JVM- and CLJS-runnable, deterministic." but the implementation transitively called next-spawn-id, which did (swap! spawn-counter ...) on a module-level atom. The conformance corpus only produced deterministic ids because the test fixture's :machines/reset-counters! hook zeroed the atom between fixtures — the function itself was not pure.

After. (machine-transition machine snap event) is deterministic from its arguments — calling it twice with the same triple returns identical [next-snapshot effects] pairs including the spawn-id sequencing inside emitted :rf.machine/spawn fx maps. The new re-frame.machine-transition-purity-test namespace locks the property in.

Surface changes

  • re-frame.machines.transition:

    • spawn-counter defonce + next-spawn-id removed; replaced with the pure allocate-spawned-id [snap machine-id] -> [snap' spawned-id] helper.
    • handle-invoke-spawn / handle-invoke-all-spawn thread the snapshot through allocation.
  • re-frame.machines.parallel: parallel-machine-transition and apply-initial-entry-cascade thread :rf/spawn-counter across regions (matching how :data is threaded).

  • re-frame.machines.lifecycle-fx:

    • synthesise-initial-snapshot stamps :rf/spawn-counter {} on every freshly-registered machine snapshot.
    • compute-actor-id split into pre-allocated-actor-id + allocate-actor-id-in-db (the hand-emitted-spawn fallback now bumps [:rf/spawn-counter <machine-id>] in the frame's app-db inside the same swap as the snapshot install).
  • re-frame.machines façade: spawn-counter re-export gone. reset-counters! kept (with the :machines/reset-counters! late-bind hook preserved for fixture back-compat) but now only cancels in-flight :after wall-clock timers — no more counter atom to reset.

  • spec/Spec-Schemas.md §:rf/machine-snapshot: optional :rf/spawn-counter slot documented as runtime-owned, user-read-only [:map-of :keyword :int].

  • Six conformance fixtures asserting :expect-next-snapshot after declarative spawn updated to include the bumped :rf/spawn-counter.

  • New regression test implementation/machines/test/re_frame/machine_transition_purity_test.clj asserts: identical args → identical results (including spawn-id sequencing), independent input snapshots allocate from their own counters, pre-populated counters keep their sequencing.

  • implementation/core/test/re_frame/smoke_test.clj spawn-id-is-frame-scoped rewritten to read each frame's parent machine snapshot's :rf/spawn-counter slot. rf-machine-sub's expected snapshot updated for the new slot.

Test plan

  • implementation/machines clojure -M:test: 116 tests / 316 assertions / 0 failures
  • implementation/core clojure -M:test: 226 tests / 1024 assertions / 0 failures (the conformance corpus's 110 runnable fixtures all pass)
  • npm run test:cljs: 571 tests / 1353 assertions / 0 failures
  • npm run test:browser: 559 tests / 1345 assertions / 0 failures
  • npm run test:elision, test:bundle-isolation, test:bundle-comparison, test:examples: all pass
  • implementation/http and implementation/routing clojure -M:test: all pass

…-in-snapshot replaces spawn-counter atom (rf2-gr8q)

Per rf2-gr8q's Shape 1 decision (2026-05-12): the module-level
`(defonce spawn-counter (atom {}))` in
`re-frame.machines.transition` is gone. Spawn-id allocation for
declarative `:invoke` / `:invoke-all` now lives inside the
parent snapshot at the reserved `:rf/spawn-counter` slot — a
per-machine-id integer map bumped via `update-in` inside
`apply-transition-once` and returned as part of the new
snapshot.

Before the change the function's docstring promised
"Pure function. JVM- and CLJS-runnable, deterministic." but the
implementation transitively called `next-spawn-id`, which did
`(swap! spawn-counter ...)` on a module-level atom. The
conformance corpus only produced deterministic ids because the
test fixture's `:machines/reset-counters!` hook zeroed the atom
between fixtures — the function itself was not pure.

After the change `(machine-transition machine snap event)` is
deterministic from its arguments — calling it twice with the
same triple returns identical `[next-snapshot effects]` pairs
including the spawn-id sequencing inside emitted
`:rf.machine/spawn` fx maps. The new
`re-frame.machine-transition-purity-test` namespace locks the
property in.

Concrete surface changes:

  * `re-frame.machines.transition`:
    - `(defonce spawn-counter (atom {}))` and `next-spawn-id`
      removed; replaced with the pure
      `allocate-spawned-id` helper which threads the snapshot
      through the bump.
    - `handle-invoke-spawn` and `handle-invoke-all-spawn` thread
      the snapshot through allocation; `:invoke-all`'s reduce
      pre-allocates per-child ids in declaration order so the
      counter advances deterministically per machine-id.

  * `re-frame.machines.parallel`: `parallel-machine-transition`
    and `apply-initial-entry-cascade` thread
    `:rf/spawn-counter` across regions (matching how `:data`
    is threaded) so a region's `:invoke` bumps the same
    in-snapshot counter as a flat machine would.

  * `re-frame.machines.lifecycle-fx`:
    - `synthesise-initial-snapshot` stamps `:rf/spawn-counter
      {}` on every freshly-registered machine snapshot so the
      slot is always present at runtime.
    - `compute-actor-id` split into `pre-allocated-actor-id`
      (returns `:invoke-id` or `:rf/spawned-id`) and
      `allocate-actor-id-in-db` (the fallback path for
      hand-emitted `[:rf.machine/spawn args]` fx, which now
      bumps `[:rf/spawn-counter <machine-id>]` in the frame's
      app-db inside the same swap as the snapshot install).
      Spawn-id allocation no longer hits a global atom on
      either path.

  * `re-frame.machines` (façade):
    - The `spawn-counter` re-export is gone.
    - `reset-counters!` is kept (and its
      `:machines/reset-counters!` late-bind hook is preserved
      for fixture back-compat), but now only cancels in-flight
      `:after` wall-clock timers — there is no longer a
      counter atom to reset.

  * `re-frame.test-support`: docstring + comments updated to
    reflect the new shape; the late-bind call site is
    unchanged (still resets wall-clock timers).

  * `spec/Spec-Schemas.md` §`:rf/machine-snapshot`: the
    optional `:rf/spawn-counter` slot is documented as a
    runtime-owned, user-read-only map of keyword→int.

  * Conformance fixtures asserting `:expect-next-snapshot`
    after a declarative spawn updated to include the bumped
    `:rf/spawn-counter` (six fixtures touched). Submap-checked
    `:final-app-db` assertions don't need updates — the new
    runtime keys are tolerated.

  * New regression test
    `implementation/machines/test/re_frame/machine_transition_purity_test.clj`
    asserts the rf2-gr8q invariants: identical args → identical
    results (including spawn-id sequencing), independent input
    snapshots allocate from their own counters, pre-populated
    counters keep their sequencing.

  * `implementation/core/test/re_frame/smoke_test.clj`
    `spawn-id-is-frame-scoped` rewritten: instead of reading
    the deleted `spawn-counter` atom, the test reads each
    frame's parent machine snapshot's `:rf/spawn-counter` slot.
    `rf-machine-sub`'s expected snapshot updated to include the
    new `:rf/spawn-counter {}` slot stamped by
    `synthesise-initial-snapshot`.

Tests:
  * implementation/machines `clojure -M:test`: 116 tests / 316
    assertions / 0 failures.
  * implementation/core `clojure -M:test`: 226 tests / 1024
    assertions / 0 failures (the conformance corpus's 110
    runnable fixtures all pass).
  * `npm run test:cljs`: 571 tests / 1353 assertions / 0
    failures.
  * `npm run test:browser`: 559 tests / 1345 assertions / 0
    failures.
  * `npm run test:elision`, `test:bundle-isolation`,
    `test:bundle-comparison`, `test:examples`: all pass.
  * implementation/http and implementation/routing tests: all
    pass.
@mike-thompson-day8 mike-thompson-day8 merged commit 88c1b67 into main May 12, 2026
22 checks passed
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.

1 participant