import type { IMemorySpaceAddress } from "../storage/interface.ts"; import type { Action } from "./types.ts"; export type NodeKind = "computation" | "effect"; export type NodeStatus = "never-ran" | "clean" | "invalid"; export interface SchedulerNode { readonly action: Action; // Mutable for the one sanctioned transition: computation → effect // promotion on re-registration ("once an effect, stays an effect"). kind: NodeKind; parentAction?: Action; children?: Set; status: NodeStatus; invalidCauses: IMemorySpaceAddress[]; liveRefs: number; provisionalDemand: boolean; provisionalDemandPass?: number; passRuns: number; retries: number; } export class NodeRegistry { private records = new WeakMap(); private childActionsByParent = new WeakMap>(); private all = new Set(); private activeEffects = new Set(); private activeComputations = new Set(); readonly effects: ReadonlySet = this.activeEffects; readonly computations: ReadonlySet = this.activeComputations; register( action: Action, kind: NodeKind, parentAction?: Action, ): SchedulerNode { const existing = this.records.get(action); if (existing) { if (existing.kind !== kind) { // v1 parity: a computation re-subscribed with `isEffect: true` is // promoted ("once an effect, stays an effect"). Demotion has no // sanctioned caller and stays an error. if (existing.kind === "computation" && kind === "effect") { existing.kind = "effect"; } else { throw new Error( `Scheduler action re-registered as ${kind}; was ${existing.kind}`, ); } } this.activate(existing); return existing; } const record: SchedulerNode = { action, kind, status: "never-ran", invalidCauses: [], liveRefs: 0, provisionalDemand: false, passRuns: 0, retries: 0, }; this.records.set(action, record); const children = this.childActionsByParent.get(action); if (children) { record.children = children; } this.activate(record); if (parentAction !== undefined) { this.captureParentAction(record, parentAction); } return record; } remove(action: Action): SchedulerNode | undefined { const record = this.records.get(action); if (!record) return undefined; this.all.delete(record); this.activeEffects.delete(action); this.activeComputations.delete(action); return record; } get(action: Action): SchedulerNode | undefined { return this.records.get(action); } linkParent( childAction: Action, parentAction: Action | null | undefined, options: { allowExisting?: boolean } = {}, ): SchedulerNode | undefined { const { allowExisting = true } = options; if (!parentAction || parentAction === childAction) return undefined; const child = this.records.get(childAction); if (!child) return undefined; if (!allowExisting && child.parentAction) { return this.parentOf(childAction); } if (child.parentAction && child.parentAction !== parentAction) { this.childActionsByParent.get(child.parentAction)?.delete(child.action); } this.captureParentAction(child, parentAction); return this.parentOf(childAction); } parentOf(action: Action): SchedulerNode | undefined { const parentAction = this.records.get(action)?.parentAction; return parentAction ? this.records.get(parentAction) : undefined; } /** * The captured parent ACTION, independent of whether the parent's record * is (still/already) registered — exact parity with the v1 parent WeakMap. * Demand and trace checks key off action objects, so they must see the * parent through registration churn windows where parentOf() is undefined. */ parentActionOf(action: Action): Action | undefined { return this.records.get(action)?.parentAction; } childrenOf(action: Action): ReadonlySet | undefined { const childActions = this.childActionsByParent.get(action); if (!childActions) return undefined; const children = new Set(); for (const childAction of childActions) { const child = this.records.get(childAction); if (child) children.add(child); } return children; } isEffect(action: Action): boolean { return this.activeEffects.has(action); } isComputation(action: Action): boolean { return this.activeComputations.has(action); } isKnownEffect(action: Action): boolean { return this.records.get(action)?.kind === "effect"; } isKnownComputation(action: Action): boolean { return this.records.get(action)?.kind === "computation"; } *nodes(kind?: NodeKind): IterableIterator { for (const record of this.all) { if (kind === undefined || record.kind === kind) { yield record; } } } size(kind: NodeKind): number { return kind === "effect" ? this.activeEffects.size : this.activeComputations.size; } isAncestor( sourceAction: Action, candidateAncestor: Action, ): boolean { let parentAction = this.records.get(sourceAction)?.parentAction; while (parentAction) { if (parentAction === candidateAncestor) { return true; } parentAction = this.records.get(parentAction)?.parentAction; } return false; } private captureParentAction( child: SchedulerNode, parentAction: Action, ): void { child.parentAction = parentAction; let children = this.childActionsByParent.get(parentAction); if (!children) { children = new Set(); this.childActionsByParent.set(parentAction, children); } children.add(child.action); const parent = this.records.get(parentAction); if (parent) { parent.children = children; } } private activate(record: SchedulerNode): void { this.all.add(record); if (record.kind === "effect") { this.activeEffects.add(record.action); this.activeComputations.delete(record.action); } else { this.activeComputations.add(record.action); this.activeEffects.delete(record.action); } } }