Skip to content

Move the daemon's remaining singletons into layers #38

Description

@FreshlyBrewedCode

Parent

#32

What to build

The daemon's remaining module-level mutable singletons move into layers, and the optional
test-seam fields they forced come out.

Four maps currently live at module scope: server/runs.ts's active run registry,
server/pubsub.ts's subscribers, lib/dedupe.ts's exported dedupeRegistry, and
lib/workspace.ts's refreshGates. Because they are process-wide, two daemons cannot coexist in
one process and tests share state across files.

The cost is already being paid in instalments, as optional constructor fields that exist purely
because there is no context to read from: dedupeRegistry? on both StartTrackedRunOptions and
DispatchEnv, registry? and now?: () => number on SchedulerDeps, and beforeStart?: () => Promise<void> on StartTrackedRunOptions — a hook whose only job is holding the
reserved-but-not-started window open in one test. Every feature since #13 has added another. This
ticket ends that drip.

The clock seam is worth calling out: Effect ships Clock and a TestClock, so now?: () => number should not survive as a bespoke field.

What must not change is the concurrency reasoning. startTrackedRun reserves its registry slot and
claims its dedupe key synchronously, check-then-set with no await in between, so two
near-simultaneous starts cannot both slip past (D24/D29, #15). Moving the state behind a service
must preserve that atomicity exactly — this is the one place where a careless refactor introduces a
real race.

See docs/adr/0009-imperative-workflows-effect-boundary.md §5.

Acceptance criteria

  • The active-run registry, pubsub, dedupe registry and mirror-refresh gate are services resolved from context, not module-level maps
  • Two daemons can run in one process without sharing run state, and a test proves it
  • dedupeRegistry?, registry?, now? and beforeStart? are gone as optional test-seam fields; tests provide layers instead
  • The clock comes from Effect's Clock; scheduler tests use TestClock rather than an injected now
  • Slot reservation and dedupe-key claim remain synchronous check-then-set with no await between check and set
  • Existing concurrency, dedupe and nested-run tests pass unchanged in intent
  • bun run check passes

Blocked by

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    taskA single self-contained piece of work that ships as one PR

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions