refactor(core): extract session run coordinator machine

This commit is contained in:
Kit Langton 2026-06-06 22:50:29 -04:00
commit fc0cf2a710
4 changed files with 940 additions and 220 deletions

View file

@ -0,0 +1,358 @@
/** @internal Pure state machine for the process-local Session run coordinator. */
export * as SessionRunCoordinatorMachine from "./run-coordinator-machine"
/** @internal */
export type Mode = "run" | "wake"
/** @internal */
export type Demand = {
readonly explicit: boolean
readonly wakeSeq?: number
readonly unsequencedWake: boolean
}
type NonEmptyDemand = Demand &
({ readonly explicit: true } | { readonly wakeSeq: number } | { readonly unsequencedWake: true })
/** @internal */
export const Demand = {
empty: { explicit: false, unsequencedWake: false } satisfies Demand,
explicit: { explicit: true, unsequencedWake: false } satisfies NonEmptyDemand,
wake: (seq?: number): NonEmptyDemand =>
seq === undefined
? { explicit: false, unsequencedWake: true }
: { explicit: false, wakeSeq: seq, unsequencedWake: false },
combine: (left: Demand, right: Demand): Demand => ({
explicit: left.explicit || right.explicit,
wakeSeq:
left.wakeSeq === undefined
? right.wakeSeq
: right.wakeSeq === undefined
? left.wakeSeq
: Math.max(left.wakeSeq, right.wakeSeq),
unsequencedWake: left.unsequencedWake || right.unsequencedWake,
}),
afterBoundary: (demand: Demand, boundary?: number): Demand => ({
explicit: false,
wakeSeq:
boundary !== undefined && demand.wakeSeq !== undefined && demand.wakeSeq > boundary ? demand.wakeSeq : undefined,
unsequencedWake: false,
}),
nonEmpty: (demand: Demand): demand is NonEmptyDemand =>
demand.explicit || demand.wakeSeq !== undefined || demand.unsequencedWake,
mode: (demand: Demand): Mode => (demand.explicit ? "run" : "wake"),
}
type Running = {
readonly _tag: "Running"
readonly chain: number
readonly attempt: number
readonly current: NonEmptyDemand
readonly pending: Demand
readonly waiter?: number
}
type Stopping = {
readonly _tag: "Stopping"
readonly chain: number
readonly attempt: number
readonly current: NonEmptyDemand
readonly pending: Demand
readonly waiter?: number
readonly stopBoundary?: number
}
/** @internal */
export type Lane = Running | Stopping
/** @internal */
export type State<Key> = {
readonly closed: boolean
readonly nextID: number
readonly lanes: ReadonlyMap<Key, Lane>
readonly interruptSeq: ReadonlyMap<Key, number>
}
/** @internal */
export const initial = <Key>(): State<Key> => ({ closed: false, nextID: 1, lanes: new Map(), interruptSeq: new Map() })
/** @internal */
export type Outcome = "Success" | "Failure" | "Interrupted"
/** @internal */
export type Event<Key> =
| { readonly _tag: "Close" }
| { readonly _tag: "Run"; readonly key: Key }
| { readonly _tag: "Wake"; readonly key: Key; readonly seq?: number }
| { readonly _tag: "Interrupt"; readonly key: Key; readonly seq?: number }
| { readonly _tag: "Observe"; readonly key: Key }
| {
readonly _tag: "Settled"
readonly key: Key
readonly chain: number
readonly attempt: number
readonly outcome: Outcome
}
/** @internal */
export type Action<Key> =
| {
readonly _tag: "Start"
readonly key: Key
readonly chain: number
readonly attempt: number
readonly demand: NonEmptyDemand
readonly successor: boolean
}
| { readonly _tag: "Interrupt"; readonly attempt: number }
| { readonly _tag: "CompleteChain"; readonly chain: number }
| { readonly _tag: "CompleteWaiter"; readonly waiter: number }
| { readonly _tag: "Report"; readonly key: Key }
/** @internal */
export type Response =
| { readonly _tag: "None" }
| { readonly _tag: "AwaitChain"; readonly chain: number }
| { readonly _tag: "AwaitWaiter"; readonly waiter: number }
| { readonly _tag: "RetryAfter"; readonly chain: number }
| { readonly _tag: "ObserveChain"; readonly chain: number }
| { readonly _tag: "Idle" }
| { readonly _tag: "Closed" }
/** @internal */
export type Transition<Key> = {
readonly state: State<Key>
readonly actions: ReadonlyArray<Action<Key>>
readonly response: Response
}
const none: Response = { _tag: "None" }
/** @internal */
export const reduce = <Key>(state: State<Key>, event: Event<Key>): Transition<Key> => {
if (event._tag === "Close")
return { state: { ...state, closed: true, lanes: new Map(), interruptSeq: new Map() }, actions: [], response: none }
if (state.closed) return { state, actions: [], response: event._tag === "Run" ? { _tag: "Closed" } : none }
if (event._tag === "Run") return run(state, event)
if (event._tag === "Wake") return wake(state, event)
if (event._tag === "Interrupt") return interrupt(state, event)
if (event._tag === "Observe") {
const lane = state.lanes.get(event.key)
return {
state,
actions: [],
response: lane === undefined ? { _tag: "Idle" } : { _tag: "ObserveChain", chain: lane.chain },
}
}
return settled(state, event)
}
const run = <Key>(state: State<Key>, event: Extract<Event<Key>, { _tag: "Run" }>): Transition<Key> => {
const lane = state.lanes.get(event.key)
if (lane?._tag === "Stopping") return { state, actions: [], response: { _tag: "RetryAfter", chain: lane.chain } }
if (lane !== undefined && lane.current.explicit)
return { state, actions: [], response: { _tag: "AwaitChain", chain: lane.chain } }
if (lane !== undefined) {
const [allocated, waiter] = lane.waiter === undefined ? allocate(state) : [state, lane.waiter]
return {
state: setLane(allocated, event.key, { ...lane, pending: Demand.combine(lane.pending, Demand.explicit), waiter }),
actions: [],
response: { _tag: "AwaitWaiter", waiter },
}
}
const [withChain, chain] = allocate(state)
const [allocated, attempt] = allocate(withChain)
const next: Lane = {
_tag: "Running",
chain,
attempt,
current: Demand.explicit,
pending: Demand.empty,
}
return {
state: setLane(allocated, event.key, next),
actions: [
{
_tag: "Start",
key: event.key,
chain: next.chain,
attempt: next.attempt,
demand: next.current,
successor: false,
},
],
response: { _tag: "AwaitChain", chain: next.chain },
}
}
const wake = <Key>(state: State<Key>, event: Extract<Event<Key>, { _tag: "Wake" }>): Transition<Key> => {
const boundary = state.interruptSeq.get(event.key)
if (boundary !== undefined && (event.seq === undefined || event.seq <= boundary))
return { state, actions: [], response: none }
const lane = state.lanes.get(event.key)
if (lane !== undefined) {
if (
lane._tag === "Stopping" &&
(lane.stopBoundary === undefined || event.seq === undefined || event.seq <= lane.stopBoundary)
)
return { state, actions: [], response: none }
return {
state: setLane(state, event.key, { ...lane, pending: Demand.combine(lane.pending, Demand.wake(event.seq)) }),
actions: [],
response: none,
}
}
const [withChain, chain] = allocate(state)
const [allocated, attempt] = allocate(withChain)
const next: Lane = {
_tag: "Running",
chain,
attempt,
current: Demand.wake(event.seq),
pending: Demand.empty,
}
return {
state: setLane(allocated, event.key, next),
actions: [
{
_tag: "Start",
key: event.key,
chain: next.chain,
attempt: next.attempt,
demand: next.current,
successor: false,
},
],
response: none,
}
}
const interrupt = <Key>(state: State<Key>, event: Extract<Event<Key>, { _tag: "Interrupt" }>): Transition<Key> => {
const latest = state.interruptSeq.get(event.key)
const lane = state.lanes.get(event.key)
if (event.seq !== undefined && latest !== undefined && event.seq <= latest)
return {
state,
actions: lane?._tag === "Stopping" ? [{ _tag: "Interrupt", attempt: lane.attempt }] : [],
response: none,
}
const bounded = event.seq === undefined ? state : setInterruptSeq(state, event.key, event.seq)
if (lane === undefined) return { state: bounded, actions: [], response: none }
if (
!lane.current.explicit &&
event.seq !== undefined &&
lane.current.wakeSeq !== undefined &&
lane.current.wakeSeq > event.seq
)
return { state: bounded, actions: [], response: none }
const pending = Demand.combine(
Demand.afterBoundary(lane.current, event.seq),
Demand.afterBoundary(lane.pending, event.seq),
)
return {
state: setLane(bounded, event.key, {
_tag: "Stopping",
chain: lane.chain,
attempt: lane.attempt,
current: lane.current,
pending,
waiter: lane.waiter,
stopBoundary: lane._tag === "Stopping" ? maxSeq(lane.stopBoundary, event.seq) : event.seq,
}),
actions: [{ _tag: "Interrupt", attempt: lane.attempt }],
response: none,
}
}
const settled = <Key>(state: State<Key>, event: Extract<Event<Key>, { _tag: "Settled" }>): Transition<Key> => {
const lane = state.lanes.get(event.key)
if (lane?.chain !== event.chain || lane.attempt !== event.attempt) return { state, actions: [], response: none }
const completesWaiter = lane.current.explicit || (lane._tag === "Stopping" && !lane.current.explicit)
const waiterActions: ReadonlyArray<Action<Key>> =
completesWaiter && lane.waiter !== undefined ? [{ _tag: "CompleteWaiter", waiter: lane.waiter }] : []
const waiter = completesWaiter ? undefined : lane.waiter
if (event.outcome === "Success" && lane._tag === "Running" && Demand.nonEmpty(lane.pending)) {
const [allocated, attempt] = allocate(state)
const next = { ...lane, attempt, current: lane.pending, pending: Demand.empty, waiter }
return {
state: setLane(allocated, event.key, next),
actions: [
...waiterActions,
{
_tag: "Start",
key: event.key,
chain: next.chain,
attempt: next.attempt,
demand: next.current,
successor: true,
},
],
response: none,
}
}
const report: ReadonlyArray<Action<Key>> =
event.outcome !== "Success" &&
!(lane._tag === "Stopping" && event.outcome === "Interrupted") &&
!lane.current.explicit
? [{ _tag: "Report", key: event.key }]
: []
if (!Demand.nonEmpty(lane.pending))
return {
state: deleteLane(state, event.key),
actions: [...waiterActions, { _tag: "CompleteChain", chain: lane.chain }, ...report],
response: none,
}
const [withChain, chain] = allocate(state)
const [allocated, attempt] = allocate(withChain)
const next: Lane = {
_tag: "Running",
chain,
attempt,
current: lane.pending,
pending: Demand.empty,
waiter,
}
return {
state: setLane(allocated, event.key, next),
actions: [
...waiterActions,
{
_tag: "Start",
key: event.key,
chain: next.chain,
attempt: next.attempt,
demand: next.current,
successor: true,
},
{ _tag: "CompleteChain", chain: lane.chain },
...report,
],
response: none,
}
}
const maxSeq = (left?: number, right?: number) =>
left === undefined ? right : right === undefined ? left : Math.max(left, right)
const allocate = <Key>(state: State<Key>): readonly [State<Key>, number] => [
{ ...state, nextID: state.nextID + 1 },
state.nextID,
]
const setLane = <Key>(state: State<Key>, key: Key, lane: Lane): State<Key> => {
const lanes = new Map(state.lanes)
lanes.set(key, lane)
return { ...state, lanes }
}
const deleteLane = <Key>(state: State<Key>, key: Key): State<Key> => {
const lanes = new Map(state.lanes)
lanes.delete(key)
return { ...state, lanes }
}
const setInterruptSeq = <Key>(state: State<Key>, key: Key, seq: number): State<Key> => {
const interruptSeq = new Map(state.interruptSeq)
interruptSeq.set(key, seq)
return { ...state, interruptSeq }
}

View file

@ -1,267 +1,240 @@
export * as SessionRunCoordinator from "./run-coordinator"
import { Cause, Context, Deferred, Effect, Exit, Fiber, FiberSet, Layer, Scope } from "effect"
import { Cause, Context, Deferred, Effect, Exit, Fiber, FiberSet, Layer, Scope, SynchronizedRef } from "effect"
import { SessionRunner } from "./runner"
import { SessionSchema } from "./schema"
import { SessionRunCoordinatorMachine } from "./run-coordinator-machine"
export type Mode = "run" | "wake"
export type Mode = SessionRunCoordinatorMachine.Mode
/** Why one drain generation should run. Explicit runs dominate advisory wakes when demands coalesce. */
type Demand = { readonly _tag: "run" } | { readonly _tag: "wake"; readonly seq?: number }
/**
* Runs at most one drain chain per key while allowing different keys to drain concurrently.
*
* For each key:
*
* idle --run/wake--> draining --run/wake--> draining + one coalesced rerun --> idle
*
* `run` is an explicit drain request. It starts a chain or joins the current chain and
* upgrades a pending follow-up so the caller receives explicit-run semantics.
*
* `wake` reports that durable work may now be available. It starts a chain while idle or
* requests one coalesced follow-up while draining. Repeated wakes collapse together.
*
* `interrupt` stops the current ownership chain. Advisory wakes from before the interrupt
* boundary are suppressed; advisory wakes after the boundary run after cleanup.
*/
export interface Coordinator<Key, A, E> {
/** Starts or joins one explicit drain generation. */
readonly run: (key: Key) => Effect.Effect<A, E>
/** Coalesces one wake-up after durable work is recorded. */
readonly wake: (key: Key, seq?: number) => Effect.Effect<void>
/** Waits until the current ownership chain settles. */
readonly awaitIdle: (key: Key) => Effect.Effect<void, E>
/** Interrupts the active ownership chain without automatically draining pending wakes. */
readonly interrupt: (key: Key, seq?: number) => Effect.Effect<void>
}
/** One Session's process-local execution lane: one active demand and at most one coalesced follow-up. */
type Entry<A, E> = {
type Chain<A, E> = {
readonly done: Deferred.Deferred<A, E>
readonly settled: Deferred.Deferred<Exit.Exit<A, E>>
current: Demand
pending?: Demand
explicitWaiter?: Deferred.Deferred<A, E>
interruptSeq?: number
owner?: Fiber.Fiber<void, never>
stopping: boolean
}
/** Combines follow-up demand: runs dominate, while wakes retain the newest durable admission sequence. */
const coalesce = (left: Demand | undefined, right: Demand): Demand => {
if (left?._tag === "run" || right._tag === "run") return { _tag: "run" }
return { _tag: "wake", seq: maxSeq(left?.seq, right.seq) }
}
const maxSeq = (left: number | undefined, right: number | undefined) => {
if (left === undefined) return right
if (right === undefined) return left
return Math.max(left, right)
}
/** Constructs a scoped coordinator. Every in-memory transition is synchronous. */
export const make = <Key, A, E>(options: {
readonly drain: (key: Key, mode: Mode) => Effect.Effect<A, E>
readonly onFailure?: (key: Key, cause: Cause.Cause<E>) => Effect.Effect<void>
}): Effect.Effect<Coordinator<Key, A, E>, never, Scope.Scope> =>
Effect.gen(function* () {
const active = new Map<Key, Entry<A, E>>()
const interruptSeq = new Map<Key, number>()
const state = yield* SynchronizedRef.make(SessionRunCoordinatorMachine.initial<Key>())
const report = yield* FiberSet.makeRuntime<never, void, never>()
const fork = yield* FiberSet.makeRuntime<never, void, never>()
const chains = new Map<number, Chain<A, E>>()
const waiters = new Map<number, Deferred.Deferred<A, E>>()
const owners = new Map<number, Deferred.Deferred<Fiber.Fiber<void>>>()
const shutdown = Deferred.makeUnsafe<void>()
let closed = false
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
closed = true
Deferred.doneUnsafe(shutdown, Effect.void)
active.clear()
interruptSeq.clear()
}),
)
const makeEntry = (current: Demand, explicitWaiter?: Deferred.Deferred<A, E>): Entry<A, E> => ({
done: Deferred.makeUnsafe<A, E>(),
settled: Deferred.makeUnsafe<Exit.Exit<A, E>>(),
current,
explicitWaiter,
stopping: false,
})
const start = (key: Key, entry: Entry<A, E>, demand: Demand, successor = false) => {
const ready = Deferred.makeUnsafe<void>()
const drain = Effect.suspend(() => options.drain(key, demand._tag))
// Initial work retains immediate-start behavior but cannot run before ownership is published.
// Observer-started successors yield once so synchronous drains cannot recurse on the JS stack.
const owner = fork(
(successor
? Effect.yieldNow.pipe(Effect.andThen(drain))
: Deferred.await(ready).pipe(Effect.andThen(drain))
).pipe(
Effect.onExit((exit) => Effect.sync(() => settle(key, entry, demand, exit))),
Effect.exit,
Effect.asVoid,
),
)
entry.owner = owner
if (!successor) Deferred.doneUnsafe(ready, Effect.void)
const chain = (chainID: number) => {
const existing = chains.get(chainID)
if (existing !== undefined) return existing
const created = { done: Deferred.makeUnsafe<A, E>(), settled: Deferred.makeUnsafe<Exit.Exit<A, E>>() }
chains.set(chainID, created)
return created
}
const waiter = (waiterID: number) => {
const existing = waiters.get(waiterID)
if (existing !== undefined) return existing
const created = Deferred.makeUnsafe<A, E>()
waiters.set(waiterID, created)
return created
}
const owner = (attemptID: number) => {
const existing = owners.get(attemptID)
if (existing !== undefined) return existing
const created = Deferred.makeUnsafe<Fiber.Fiber<void>>()
owners.set(attemptID, created)
return created
}
const settle = (key: Key, entry: Entry<A, E>, demand: Demand, exit: Exit.Exit<A, E>) => {
if (closed) {
Deferred.doneUnsafe(entry.done, exit)
Deferred.doneUnsafe(entry.settled, Effect.succeed(exit))
return
}
if (demand._tag === "run" && entry.explicitWaiter !== undefined) {
Deferred.doneUnsafe(entry.explicitWaiter, exit)
entry.explicitWaiter = undefined
}
if (entry.stopping && demand._tag === "wake" && entry.explicitWaiter !== undefined) {
Deferred.doneUnsafe(entry.explicitWaiter, exit)
entry.explicitWaiter = undefined
}
if (active.get(key) !== entry) {
Deferred.doneUnsafe(entry.done, exit)
Deferred.doneUnsafe(entry.settled, Effect.succeed(exit))
return
}
if (exit._tag === "Success" && !entry.stopping) {
if (entry.pending !== undefined) {
const pending = entry.pending
entry.pending = undefined
entry.current = pending
start(key, entry, pending, true)
return
}
active.delete(key)
Deferred.doneUnsafe(entry.done, exit)
Deferred.doneUnsafe(entry.settled, Effect.succeed(exit))
return
}
const successor = entry.pending !== undefined ? makeEntry(entry.pending, entry.explicitWaiter) : undefined
if (successor === undefined) active.delete(key)
else active.set(key, successor)
if (successor !== undefined) start(key, successor, successor.current, true)
Deferred.doneUnsafe(entry.done, exit)
Deferred.doneUnsafe(entry.settled, Effect.succeed(exit))
if (
exit._tag === "Failure" &&
!(entry.stopping && Cause.hasInterruptsOnly(exit.cause)) &&
demand._tag === "wake" &&
options.onFailure !== undefined
) {
report(Effect.suspend(() => options.onFailure!(key, exit.cause)))
}
const requireChain = (chainID: number) => {
const existing = chains.get(chainID)
if (existing !== undefined) return existing
throw new Error(`Missing Session run chain ${chainID}`)
}
const requireWaiter = (waiterID: number) => {
const existing = waiters.get(waiterID)
if (existing !== undefined) return existing
throw new Error(`Missing Session run waiter ${waiterID}`)
}
const wake = (key: Key, seq?: number) =>
Effect.sync(() => {
if (closed) return
if (!isAfterInterrupt(key, seq)) return
const entry = active.get(key)
if (entry !== undefined) {
if (!acceptsWake(entry, seq)) return
entry.pending = coalesce(entry.pending, { _tag: "wake", seq })
return
}
type RuntimeResponse =
| { readonly _tag: "None" | "Idle" | "Closed" }
| { readonly _tag: "Await"; readonly deferred: Deferred.Deferred<A, E> }
| { readonly _tag: "Retry" | "Observe"; readonly deferred: Deferred.Deferred<Exit.Exit<A, E>> }
const next = makeEntry({ _tag: "wake", seq })
active.set(key, next)
start(key, next, next.current)
const transition = (event: SessionRunCoordinatorMachine.Event<Key>) =>
SynchronizedRef.modifyEffect(state, (current) => {
const result = SessionRunCoordinatorMachine.reduce(current, event)
return Effect.sync(() => {
result.actions.forEach((action) => {
if (action._tag !== "Start") return
chain(action.chain)
owner(action.attempt)
})
if (result.response._tag === "AwaitChain") chain(result.response.chain)
if (result.response._tag === "AwaitWaiter") waiter(result.response.waiter)
const response: RuntimeResponse =
result.response._tag === "AwaitChain"
? { _tag: "Await", deferred: requireChain(result.response.chain).done }
: result.response._tag === "AwaitWaiter"
? { _tag: "Await", deferred: requireWaiter(result.response.waiter) }
: result.response._tag === "RetryAfter"
? { _tag: "Retry", deferred: requireChain(result.response.chain).settled }
: result.response._tag === "ObserveChain"
? { _tag: "Observe", deferred: requireChain(result.response.chain).settled }
: { _tag: result.response._tag }
return [{ actions: result.actions, response }, result.state] as const
})
})
type Execution = { readonly _tag: "General" } | { readonly _tag: "Settlement"; readonly exit: Exit.Exit<A, E> }
const execute = (
actions: ReadonlyArray<SessionRunCoordinatorMachine.Action<Key>>,
execution: Execution,
): Effect.Effect<void> =>
Effect.forEach(
actions,
(action): Effect.Effect<void> => {
if (action._tag === "Start") {
requireChain(action.chain)
const ownerDeferred = owners.get(action.attempt)
if (ownerDeferred === undefined) return Effect.die(`Missing Session run attempt ${action.attempt}`)
const ready = Deferred.makeUnsafe<void>()
const drain = Effect.suspend(() =>
options.drain(action.key, SessionRunCoordinatorMachine.Demand.mode(action.demand)),
)
const fiber = fork(
(action.successor
? Effect.yieldNow.pipe(Effect.andThen(drain))
: Deferred.await(ready).pipe(Effect.andThen(drain))
).pipe(
Effect.onExit((result) => settle(action.key, action.chain, action.attempt, result)),
Effect.exit,
Effect.asVoid,
),
)
Deferred.doneUnsafe(ownerDeferred, Effect.succeed(fiber))
if (!action.successor) Deferred.doneUnsafe(ready, Effect.void)
return Effect.void
}
if (action._tag === "Interrupt") {
const ownerDeferred = owners.get(action.attempt)
return ownerDeferred === undefined
? Effect.void
: Deferred.await(ownerDeferred).pipe(Effect.flatMap(Fiber.interrupt))
}
if (execution._tag !== "Settlement") return Effect.die("Settlement action requires a settlement context")
if (action._tag === "CompleteWaiter") {
const deferred = requireWaiter(action.waiter)
waiters.delete(action.waiter)
Deferred.doneUnsafe(deferred, execution.exit)
return Effect.void
}
if (action._tag === "CompleteChain") {
const deferreds = requireChain(action.chain)
chains.delete(action.chain)
Deferred.doneUnsafe(deferreds.done, execution.exit)
Deferred.doneUnsafe(deferreds.settled, Effect.succeed(execution.exit))
return Effect.void
}
if (action._tag === "Report") {
const onFailure = options.onFailure
if (execution.exit._tag === "Success") return Effect.die("Failure report requires a failed settlement")
if (onFailure === undefined) return Effect.void
const cause = execution.exit.cause
report(Effect.suspend(() => onFailure(action.key, cause)))
}
return Effect.void
},
{ discard: true },
).pipe(Effect.asVoid)
const settle = (key: Key, chainID: number, attemptID: number, exit: Exit.Exit<A, E>) => {
return transition({
_tag: "Settled",
key,
chain: chainID,
attempt: attemptID,
outcome: exit._tag === "Success" ? "Success" : Cause.hasInterruptsOnly(exit.cause) ? "Interrupted" : "Failure",
}).pipe(
Effect.flatMap((result) => execute(result.actions, { _tag: "Settlement", exit })),
Effect.ensuring(Effect.sync(() => owners.delete(attemptID))),
)
}
const dispatch = (event: SessionRunCoordinatorMachine.Event<Key>) =>
Effect.uninterruptible(
transition(event).pipe(Effect.flatMap((result) => execute(result.actions, { _tag: "General" }))),
)
const run = (key: Key): Effect.Effect<A, E> =>
Effect.suspend(() =>
Effect.uninterruptibleMask((restore) => {
return transition({ _tag: "Run", key }).pipe(
Effect.flatMap((result) => {
return execute(result.actions, { _tag: "General" }).pipe(
Effect.andThen(
result.response._tag === "Await"
? awaitResult(result.response.deferred)
: result.response._tag === "Retry"
? Effect.raceFirst(
Deferred.await(result.response.deferred).pipe(Effect.as(true)),
Deferred.await(shutdown).pipe(Effect.as(false)),
).pipe(Effect.flatMap((settled) => (settled ? run(key) : Effect.interrupt)))
: Effect.interrupt,
),
restore,
)
}),
)
}),
)
const wake = (key: Key, seq?: number) => {
return Effect.uninterruptible(
Effect.suspend(() => {
return transition({ _tag: "Wake", key, seq }).pipe(
Effect.flatMap((result) => execute(result.actions, { _tag: "General" })),
)
}),
)
}
const interrupt = (key: Key, seq?: number) => dispatch({ _tag: "Interrupt", key, seq })
const awaitIdle = (key: Key): Effect.Effect<void, E> =>
Effect.gen(function* () {
let firstFailure: Cause.Cause<E> | undefined
while (!closed) {
const entry = active.get(key)
if (entry === undefined) break
let failure: Cause.Cause<E> | undefined
while (true) {
const observation = yield* transition({ _tag: "Observe", key })
if (observation.response._tag !== "Observe") break
const exit = yield* Effect.raceFirst(
Deferred.await(entry.settled),
Deferred.await(observation.response.deferred),
Deferred.await(shutdown).pipe(Effect.as(Exit.void)),
)
if (closed) break
if (exit._tag === "Failure" && firstFailure === undefined) firstFailure = exit.cause
if (exit._tag === "Failure" && failure === undefined) failure = exit.cause
}
if (firstFailure !== undefined) return yield* Effect.failCause(firstFailure)
if (failure !== undefined) return yield* Effect.failCause(failure)
return undefined
})
const interrupt = (key: Key, seq?: number): Effect.Effect<void> =>
Effect.suspend(() => {
const entry = active.get(key)
const latest = interruptSeq.get(key)
if (seq !== undefined && latest !== undefined && seq <= latest)
return entry?.stopping && entry.owner !== undefined ? Fiber.interrupt(entry.owner) : Effect.void
if (seq !== undefined) interruptSeq.set(key, seq)
if (entry?.owner === undefined) return Effect.void
if (
seq !== undefined &&
entry.current._tag === "wake" &&
entry.current.seq !== undefined &&
entry.current.seq > seq
)
return Effect.void
if (entry.stopping) {
entry.interruptSeq = maxSeq(entry.interruptSeq, seq)
suppressPendingAtOrBefore(entry, seq)
return Fiber.interrupt(entry.owner)
}
entry.stopping = true
entry.interruptSeq = seq
suppressPendingAtOrBefore(entry, seq)
return Fiber.interrupt(entry.owner)
})
yield* Effect.addFinalizer(() =>
transition({ _tag: "Close" }).pipe(Effect.andThen(Effect.sync(() => Deferred.doneUnsafe(shutdown, Effect.void)))),
)
return { run, wake, awaitIdle, interrupt }
return { run, wake, interrupt, awaitIdle }
function run(key: Key): Effect.Effect<A, E> {
return Effect.uninterruptibleMask((restore) => {
if (closed) return Effect.interrupt
const entry = active.get(key)
if (entry !== undefined) {
if (entry.stopping) {
return restore(Deferred.await(entry.settled).pipe(Effect.andThen(run(key))))
}
if (entry.current._tag === "wake") {
entry.pending = coalesce(entry.pending, { _tag: "run" })
entry.explicitWaiter ??= Deferred.makeUnsafe<A, E>()
return restore(awaitRun(entry.explicitWaiter))
}
return restore(awaitRun(entry.done))
}
const next = makeEntry({ _tag: "run" })
active.set(key, next)
start(key, next, next.current)
return restore(awaitRun(next.done))
})
}
function awaitRun(done: Deferred.Deferred<A, E>): Effect.Effect<A, E> {
return Effect.raceFirst(Deferred.await(done), Deferred.await(shutdown).pipe(Effect.andThen(Effect.interrupt)))
}
function acceptsWake(entry: Entry<A, E>, seq: number | undefined) {
return !entry.stopping || (entry.interruptSeq !== undefined && seq !== undefined && seq > entry.interruptSeq)
}
function isAfterInterrupt(key: Key, seq: number | undefined) {
const latest = interruptSeq.get(key)
return latest === undefined || (seq !== undefined && seq > latest)
}
function suppressPendingAtOrBefore(entry: Entry<A, E>, seq: number | undefined) {
if (
entry.pending?._tag === "wake" &&
seq !== undefined &&
entry.pending.seq !== undefined &&
entry.pending.seq > seq
)
return
entry.pending = undefined
function awaitResult(deferred: Deferred.Deferred<A, E>) {
return Effect.raceFirst(Deferred.await(deferred), Deferred.await(shutdown).pipe(Effect.andThen(Effect.interrupt)))
}
})

View file

@ -0,0 +1,213 @@
import { describe, expect, test } from "bun:test"
import { SessionRunCoordinatorMachine } from "../src/session/run-coordinator-machine"
const Machine = SessionRunCoordinatorMachine
describe("SessionRunCoordinatorMachine.Demand", () => {
test("empty is the combine identity", () => {
const demand = SessionRunCoordinatorMachine.Demand.combine(
SessionRunCoordinatorMachine.Demand.explicit,
SessionRunCoordinatorMachine.Demand.wake(3),
)
expect(SessionRunCoordinatorMachine.Demand.combine(SessionRunCoordinatorMachine.Demand.empty, demand)).toEqual(
demand,
)
expect(SessionRunCoordinatorMachine.Demand.combine(demand, SessionRunCoordinatorMachine.Demand.empty)).toEqual(
demand,
)
})
test("combine is associative, commutative, and idempotent", () => {
const left = Machine.Demand.explicit
const middle = Machine.Demand.wake()
const right = Machine.Demand.wake(3)
expect(Machine.Demand.combine(left, right)).toEqual(Machine.Demand.combine(right, left))
expect(Machine.Demand.combine(left, left)).toEqual(left)
expect(Machine.Demand.combine(Machine.Demand.combine(left, middle), right)).toEqual(
Machine.Demand.combine(left, Machine.Demand.combine(middle, right)),
)
})
test("afterBoundary removes explicit and stale wake components", () => {
const combined = Machine.Demand.combine(Machine.Demand.explicit, Machine.Demand.wake(3))
expect(Machine.Demand.afterBoundary(combined, 2)).toEqual(Machine.Demand.wake(3))
expect(Machine.Demand.nonEmpty(Machine.Demand.afterBoundary(combined, 3))).toBeFalse()
expect(Machine.Demand.nonEmpty(Machine.Demand.afterBoundary(combined))).toBeFalse()
})
test("mode follows only the explicit component", () => {
expect(Machine.Demand.mode(Machine.Demand.explicit)).toBe("run")
expect(Machine.Demand.mode(Machine.Demand.wake(1))).toBe("wake")
expect(Machine.Demand.mode(Machine.Demand.combine(Machine.Demand.explicit, Machine.Demand.wake(1)))).toBe("run")
})
})
describe("SessionRunCoordinatorMachine.reduce", () => {
test("ignores a stale attempt from the active chain", () => {
const active = combinedActive(3)
const result = Machine.reduce(active, {
_tag: "Settled",
key: "session",
chain: 1,
attempt: 2,
outcome: "Success",
})
expect(result.state).toBe(active)
expect(result.actions).toEqual([])
expect(result.response).toEqual({ _tag: "None" })
})
test("ignores duplicate and foreign settlements", () => {
const active = combinedActive(3)
const foreign = Machine.reduce(active, {
_tag: "Settled",
key: "session",
chain: 99,
attempt: 100,
outcome: "Failure",
})
const idle = Machine.reduce(Machine.initial<string>(), {
_tag: "Settled",
key: "session",
chain: 1,
attempt: 2,
outcome: "Success",
})
expect(foreign).toEqual({ state: active, actions: [], response: { _tag: "None" } })
expect(idle.actions).toEqual([])
expect(idle.state).toEqual(Machine.initial<string>())
})
test("completes the superseded chain when creating its successor", () => {
const interrupted = Machine.reduce(combinedActive(3), { _tag: "Interrupt", key: "session", seq: 2 })
const settled = Machine.reduce(interrupted.state, {
_tag: "Settled",
key: "session",
chain: 1,
attempt: 4,
outcome: "Interrupted",
})
expect(settled.actions).toContainEqual({ _tag: "CompleteChain", chain: 1 })
expect(settled.state.lanes.get("session")?.chain).not.toBe(1)
})
test("returns caller observation separately from executable actions", () => {
const result = Machine.reduce(Machine.initial<string>(), {
_tag: "Run",
key: "session",
})
expect(result.response).toEqual({ _tag: "AwaitChain", chain: 1 })
expect(result.actions).toEqual([
{
_tag: "Start",
key: "session",
chain: 1,
attempt: 2,
demand: Machine.Demand.explicit,
successor: false,
},
])
expect(result.state.nextID).toBe(3)
})
test("allocates only identities selected by each transition", () => {
const woken = Machine.reduce(Machine.initial<string>(), { _tag: "Wake", key: "session", seq: 1 })
expect(woken.state.nextID).toBe(3)
const coalesced = Machine.reduce(woken.state, { _tag: "Wake", key: "session", seq: 2 })
expect(coalesced.state.nextID).toBe(3)
const explicit = Machine.reduce(coalesced.state, { _tag: "Run", key: "session" })
expect(explicit.state.nextID).toBe(4)
const joined = Machine.reduce(explicit.state, { _tag: "Run", key: "session" })
expect(joined.state.nextID).toBe(4)
const continued = Machine.reduce(joined.state, {
_tag: "Settled",
key: "session",
chain: 1,
attempt: 2,
outcome: "Success",
})
expect(continued.state.nextID).toBe(5)
expect(continued.state.lanes.get("session")?.attempt).toBe(4)
})
test("interrupting an active combined demand preserves its newer wake as an advisory successor", () => {
const active = combinedActive(3)
const interrupted = Machine.reduce(active, { _tag: "Interrupt", key: "session", seq: 2 })
expect(interrupted.actions).toEqual([{ _tag: "Interrupt", attempt: 4 }])
expect(interrupted.state.lanes.get("session")?.pending).toEqual(Machine.Demand.wake(3))
const settled = Machine.reduce(interrupted.state, {
_tag: "Settled",
key: "session",
chain: 1,
attempt: 4,
outcome: "Interrupted",
})
expect(settled.state.lanes.get("session")?.current).toEqual(Machine.Demand.wake(3))
expect(settled.actions).toContainEqual({
_tag: "Start",
key: "session",
chain: 5,
attempt: 6,
demand: Machine.Demand.wake(3),
successor: true,
})
})
test("interrupting an active combined demand suppresses its wake at the boundary", () => {
const active = combinedActive(2)
const interrupted = Machine.reduce(active, { _tag: "Interrupt", key: "session", seq: 2 })
const pending = interrupted.state.lanes.get("session")?.pending
expect(pending).toBeDefined()
if (pending === undefined) throw new Error("Missing stopping lane")
expect(Machine.Demand.nonEmpty(pending)).toBeFalse()
const settled = Machine.reduce(interrupted.state, {
_tag: "Settled",
key: "session",
chain: 1,
attempt: 4,
outcome: "Interrupted",
})
expect(settled.state.lanes.has("session")).toBeFalse()
expect(settled.actions.some((action) => action._tag === "Start")).toBeFalse()
})
})
function combinedActive(seq: number) {
const woken = Machine.reduce(Machine.initial<string>(), {
_tag: "Wake",
key: "session",
seq: 1,
})
const explicit = Machine.reduce(woken.state, {
_tag: "Run",
key: "session",
})
const pending = Machine.reduce(explicit.state, {
_tag: "Wake",
key: "session",
seq,
})
const active = Machine.reduce(pending.state, {
_tag: "Settled",
key: "session",
chain: 1,
attempt: 2,
outcome: "Success",
})
return active.state
}

View file

@ -29,6 +29,51 @@ describe("SessionRunCoordinator", () => {
),
)
it.effect("allocates fresh ownership when one run effect is reused", () =>
Effect.scoped(
Effect.gen(function* () {
let runs = 0
const coordinator = yield* SessionRunCoordinator.make({ drain: () => Effect.sync(() => runs++) })
const run = coordinator.run("session")
yield* run
yield* run
expect(runs).toBe(2)
}),
),
)
it.effect("captures awaitIdle chains safely while settlement races", () =>
Effect.scoped(
Effect.gen(function* () {
const iterations = 500
const gates = Array.from({ length: iterations }, () => Deferred.makeUnsafe<void>())
let runs = 0
const coordinator = yield* SessionRunCoordinator.make({
drain: () =>
Effect.suspend(() => {
const gate = gates[runs++]
return gate === undefined ? Effect.die("Missing test gate") : Deferred.await(gate)
}),
})
for (let index = 0; index < iterations; index++) {
const run = yield* coordinator.run("session").pipe(Effect.forkChild)
yield* Effect.yieldNow
const idle = yield* coordinator.awaitIdle("session").pipe(Effect.forkChild({ startImmediately: true }))
const gate = gates[index]
if (gate === undefined) yield* Effect.die("Missing test gate")
yield* Deferred.succeed(gate, undefined)
yield* Effect.all([Fiber.join(run), Fiber.join(idle)])
}
expect(runs).toBe(iterations)
return undefined
}),
),
)
it.effect("starts a drain when woken while idle", () =>
Effect.scoped(
Effect.gen(function* () {
@ -122,6 +167,106 @@ describe("SessionRunCoordinator", () => {
),
)
it.effect("preserves a newer wake coalesced behind a pending explicit run", () =>
Effect.scoped(
Effect.gen(function* () {
const firstStarted = yield* Deferred.make<void>()
const secondStarted = yield* Deferred.make<void>()
const modes: SessionRunCoordinator.Mode[] = []
const coordinator = yield* SessionRunCoordinator.make<string, void, never>({
drain: (_key, mode) =>
Effect.sync(() => modes.push(mode)).pipe(
Effect.flatMap((run) =>
run === 1
? Deferred.succeed(firstStarted, undefined).pipe(Effect.andThen(Effect.never))
: Deferred.succeed(secondStarted, undefined),
),
),
})
yield* coordinator.wake("session", 1)
yield* Deferred.await(firstStarted)
const run = yield* coordinator.run("session").pipe(Effect.exit, Effect.forkChild)
yield* Effect.yieldNow
yield* coordinator.wake("session", 3)
yield* coordinator.interrupt("session", 2)
yield* Deferred.await(secondStarted)
yield* coordinator.awaitIdle("session").pipe(Effect.exit)
const runExit = yield* Fiber.join(run)
expect(Exit.isFailure(runExit) && Cause.hasInterruptsOnly(runExit.cause)).toBeTrue()
expect(modes).toEqual(["wake", "wake"])
}),
),
)
it.effect("preserves a newer wake from an interrupted active combined demand", () =>
Effect.scoped(
Effect.gen(function* () {
const firstGate = yield* Deferred.make<void>()
const secondStarted = yield* Deferred.make<void>()
const thirdStarted = yield* Deferred.make<void>()
const modes: SessionRunCoordinator.Mode[] = []
const coordinator = yield* SessionRunCoordinator.make<string, void, never>({
drain: (_key, mode) =>
Effect.sync(() => modes.push(mode)).pipe(
Effect.flatMap((run) => {
if (run === 1) return Deferred.await(firstGate)
if (run === 2) return Deferred.succeed(secondStarted, undefined).pipe(Effect.andThen(Effect.never))
return Deferred.succeed(thirdStarted, undefined)
}),
),
})
yield* coordinator.wake("session", 1)
const run = yield* coordinator.run("session").pipe(Effect.exit, Effect.forkChild)
yield* Effect.yieldNow
yield* coordinator.wake("session", 3)
yield* Deferred.succeed(firstGate, undefined)
yield* Deferred.await(secondStarted)
yield* coordinator.interrupt("session", 2)
yield* Deferred.await(thirdStarted)
yield* coordinator.awaitIdle("session").pipe(Effect.exit)
const runExit = yield* Fiber.join(run)
expect(Exit.isFailure(runExit) && Cause.hasInterruptsOnly(runExit.cause)).toBeTrue()
expect(modes).toEqual(["wake", "run", "wake"])
}),
),
)
it.effect("suppresses an older wake from an interrupted active combined demand", () =>
Effect.scoped(
Effect.gen(function* () {
const firstGate = yield* Deferred.make<void>()
const secondStarted = yield* Deferred.make<void>()
const modes: SessionRunCoordinator.Mode[] = []
const coordinator = yield* SessionRunCoordinator.make<string, void, never>({
drain: (_key, mode) =>
Effect.sync(() => modes.push(mode)).pipe(
Effect.flatMap((run) => {
if (run === 1) return Deferred.await(firstGate)
return Deferred.succeed(secondStarted, undefined).pipe(Effect.andThen(Effect.never))
}),
),
})
yield* coordinator.wake("session", 1)
const run = yield* coordinator.run("session").pipe(Effect.exit, Effect.forkChild)
yield* Effect.yieldNow
yield* coordinator.wake("session", 2)
yield* Deferred.succeed(firstGate, undefined)
yield* Deferred.await(secondStarted)
yield* coordinator.interrupt("session", 2)
yield* coordinator.awaitIdle("session").pipe(Effect.exit)
const runExit = yield* Fiber.join(run)
expect(Exit.isFailure(runExit) && Cause.hasInterruptsOnly(runExit.cause)).toBeTrue()
expect(modes).toEqual(["wake", "run"])
}),
),
)
it.effect("interrupts only the requested key", () =>
Effect.scoped(
Effect.gen(function* () {
@ -847,6 +992,37 @@ describe("SessionRunCoordinator", () => {
}),
)
it.effect("settles a post-stop run waiter when its owning scope closes", () =>
Effect.gen(function* () {
const scope = yield* Scope.make()
const started = yield* Deferred.make<void>()
const cleanupStarted = yield* Deferred.make<void>()
const cleanupGate = yield* Deferred.make<void>()
const coordinator = yield* SessionRunCoordinator.make<string, void, never>({
drain: () =>
Deferred.succeed(started, undefined).pipe(
Effect.andThen(Effect.never),
Effect.onInterrupt(() =>
Deferred.succeed(cleanupStarted, undefined).pipe(Effect.andThen(Deferred.await(cleanupGate))),
),
),
}).pipe(Scope.provide(scope))
yield* coordinator.wake("session")
yield* Deferred.await(started)
const interrupt = yield* coordinator.interrupt("session").pipe(Effect.forkChild)
yield* Deferred.await(cleanupStarted)
const run = yield* coordinator.run("session").pipe(Effect.forkChild)
const close = yield* Scope.close(scope, Exit.void).pipe(Effect.forkChild)
const runExit = yield* Fiber.await(run)
expect(Exit.isFailure(runExit) && Cause.hasInterruptsOnly(runExit.cause)).toBeTrue()
yield* Deferred.succeed(cleanupGate, undefined)
yield* Fiber.join(interrupt)
yield* Fiber.join(close)
}),
)
it.effect("does not start work after its owning scope closes", () =>
Effect.gen(function* () {
const scope = yield* Scope.make()