Skip to content

Revive Halo as a scoped React action runtime - #17

Draft
robertdp wants to merge 16 commits into
masterfrom
revive-v4-tasks
Draft

Revive Halo as a scoped React action runtime#17
robertdp wants to merge 16 commits into
masterfrom
revive-v4-tasks

Conversation

@robertdp

@robertdp robertdp commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Summary

Revive Halo v4 as a modern, component-scoped action runtime for PureScript React.

This replaces the old Free/FreeAp evaluator with a direct ownership runtime while preserving the application's own monad. It adds safe component processes, typed managed tasks, subscriptions, synchronous cleanup, StrictMode reactivation, and comprehensive example-led documentation.

The v4 API is unreleased, so this PR favors one coherent public model over compatibility aliases or migration shims.

Public model

Halo augments an application monad rather than replacing it:

type UI a = Halo.HaloM Props State Action AppM a

newtype AppM a = AppM (ReaderT Env Aff a)

runAppM :: Env -> AppM ~> Aff
runAppM env (AppM program) = runReaderT program env

The interpreter is explicit at either React entry point:

Halo.component "Profile" (runAppM env) spec
halo <- Halo.useHalo (runAppM env) hookSpec

Both expose current component state, an immutable task view, and synchronous action dispatch. component owns the complete React component; useHalo composes with other hooks.

Actions are handled through named lifecycle callbacks:

handlers = Halo.defaultHandlers
  { onAction = case _ of
      Rename name -> modify_ _ { name = name }
      Load -> do
        result <- lift loadProfile
        modify_ _ { profile = Just result }
  }

Application capabilities such as MonadEffect, MonadAff, MonadAsk, MonadTell, and MonadThrow continue to route through m. Halo owns MonadState for component state and provides a direct abstract Parallel counterpart without Free/FreeAp.

Ownership and concurrency

Every handler invocation is an independent root in one React activation. New roots capture the latest handlers, error callback, state setter, and application interpreter. Existing roots retain their interpreter snapshot.

fork creates an independently cancellable component process:

fiber <- Halo.fork synchronize
Halo.kill fiber

A fork may outlive its launching handler, but not its activation. It inherits the launching root's interpreter. kill removes and fences it synchronously, then waits for Aff cancellation and finalizers.

The runtime rejects stale state commits, capability registration, dispatch, and newly lifted application effects after kill or deactivation—even when code catches the initial Aff cancellation.

Typed managed tasks

React.Halo.Task stores typed lifecycle outcomes in component state without introducing a task monad or retaining computations and inputs.

For ordinary record state, one type-level label supplies both field location and identity:

type State =
  { search :: Task.State SearchError Results
  }

searchSlot :: Task.Slot "search" State SearchError Results
searchSlot = Task.slot (Proxy :: Proxy "search")

initialState =
  { search: Task.idle searchSlot }

Task.slotAt proxy lens supports nested or custom lawful focuses.

Task bodies remain ordinary HaloM values returning Either error result:

Task.supersede searchSlot do
  lift (Search.run query)

The policies are:

  • once — start only from Idle; typed outcomes remain terminal until reset;
  • startIfInactive — preserve active work, but restart from idle or terminal state;
  • supersede — make the newest invocation authoritative immediately;
  • debounce — trailing-edge latest-wins with a private cancellable timer; and
  • reset — publish Idle, cancel active work, and await finalizers.

Rendering observes tasks through the coherent runtime view:

case Task.toStatus tasks searchSlot of
  Task.Idle -> renderPrompt
  Task.Active -> renderSpinner
  Task.Failed error -> renderError error
  Task.Succeeded result -> renderResult result

Exact authority includes runtime identity, activation generation, and ForkId. Ordinary component-state writes reconcile registered slots before publication. Copied, stale, cross-slot, or cross-runtime active values cannot commit, cross-cancel, or project as authoritative.

Supersession fences old work before starting the replacement and does not wait for old finalizers. Reset fences and publishes immediately, then waits. Unexpected current task failures return the slot to Idle before normal ForkError routing.

Lifecycle, subscriptions, and cleanup

React development StrictMode setup-cleanup-setup creates a fresh usable activation. Deactivation fences the complete old activation before foreign cleanup or cancellation requests. If task state was normalized during cleanup, reactivation publishes the coherent normalized state before new onActivate work.

Halo replaces the Halogen subscription dependency with a small emitter API:

names = Halo.makeEmitter \emit -> source.listen emit
actions = NameChanged <$> names
void $ Halo.subscribe actions

Emitter has a Functor instance for mapping source values into actions without changing registration or cleanup behavior. Emitters deliberately do not impose queue, backpressure, or multi-source synchronization semantics.

Subscriptions and generic cleanup are activation-scoped:

cleanupId <- Halo.registerCleanup removeListener
Halo.releaseCleanup cleanupId

Deactivation attempts every remaining emitter and generic cleanup even if one throws. Cleanup failures route as DeactivationError without blocking other cleanup or cancellation requests. Stale subscription callbacks and cleanup IDs cannot affect a later activation.

React cleanup remains synchronous. Asynchronous resource release belongs in Aff finalizers owned by handlers, tasks, or forks; there is no asynchronous onDeactivate callback.

Tooling and package cleanup

  • Pin PureScript 0.15.16 and Spago 1.0.4.
  • Use registry package set 80.8.0.
  • Keep package.json limited to the two development-tool dependencies.
  • Add no npm runtime entry point or runtime dependencies.
  • Replace Dhall/Bower-era configuration with current Spago metadata and lockfiles.
  • Remove the Free evaluator, old control modules, Halogen subscription dependency, keyed scheduler experiments, and obsolete package files.
  • Modernize CI around the pinned toolchain and strict package checks.

Documentation

  • Keep the README as a concise full overview, beginning with component and useHalo.
  • Organize the guide as an entry-point-first, example-led series:
    • getting started;
    • actions, effects, and state;
    • managed work; and
    • lifecycle and resources.
  • Compile-check the README and guide examples.
  • Keep exact API contracts in public source comments used by generated documentation.
  • Document runtime ownership invariants separately for maintainers.
  • Add contributor guidance and a concise repository agent control plane.
  • Omit migration documentation because no released v4 compatibility path exists.

Validation

  • npm run format:check
  • npx spago build --strict --pedantic-packages --offline — zero project warnings or errors and no dependency findings
  • npm test -- --offline — 41/41 passing
  • npx spago docs --offline
  • fresh consumer build from the documented dependencies with --strict --pedantic-packages --offline
  • all 40 local documentation links resolve
  • generated public API inspected against the intended exports
  • git diff --check

Independent bounded reviews covered the scoped runtime, AppM interpreter capture, forks, cancellation fencing, parallelism, task ownership, slot collisions, stale snapshots, StrictMode normalization, cleanup, subscriptions, errors, and documentation. The final branded task model was reviewed finding-free after its ownership regressions were added.

Known gaps

  • The deterministic runtime suite models React setup-cleanup-setup, but the repository does not yet contain a real React DOM/StrictMode mounting fixture.
  • npm audit reports five vulnerabilities in the PureScript installer's development-only transitive dependencies. The forced remediation downgrades PureScript; Halo has no npm runtime dependencies.
  • v4 is not published yet, so the README uses a local Spago package override until release.

@robertdp robertdp changed the title Revive Halo with explicit component-scoped tasks Revive Halo with first-class component tasks Sep 1, 2026
@robertdp robertdp changed the title Revive Halo with first-class component tasks Revive Halo with scoped application effects Sep 1, 2026
@robertdp robertdp changed the title Revive Halo with scoped application effects Revive Halo as a component-scoped action runtime Sep 1, 2026
@robertdp robertdp changed the title Revive Halo as a component-scoped action runtime Revive Halo with component-scoped actions and tasks Sep 1, 2026
@robertdp robertdp changed the title Revive Halo with component-scoped actions and tasks Revive Halo as a scoped React action runtime Sep 1, 2026
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