86 lines
2.2 KiB
TypeScript
86 lines
2.2 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
|
|
type Listener = () => void;
|
|
|
|
const activeByProject = new Map<string, Set<string>>();
|
|
const listeners = new Set<Listener>();
|
|
|
|
function emit(): void {
|
|
for (const listener of listeners) listener();
|
|
}
|
|
|
|
function activeSet(projectId: string): Set<string> {
|
|
let set = activeByProject.get(projectId);
|
|
if (!set) {
|
|
set = new Set();
|
|
activeByProject.set(projectId, set);
|
|
}
|
|
return set;
|
|
}
|
|
|
|
export function markProjectCliInteractionStarted(
|
|
projectId: string,
|
|
interactionId: string,
|
|
): void {
|
|
const set = activeSet(projectId);
|
|
const before = set.size;
|
|
set.add(interactionId);
|
|
if (set.size !== before) emit();
|
|
}
|
|
|
|
export function markProjectCliInteractionFinished(
|
|
projectId: string,
|
|
interactionId: string,
|
|
): void {
|
|
const set = activeByProject.get(projectId);
|
|
if (!set?.delete(interactionId)) return;
|
|
if (set.size === 0) activeByProject.delete(projectId);
|
|
emit();
|
|
}
|
|
|
|
export function markProjectCliInteractionStopped(
|
|
projectId: string,
|
|
interactionId: string,
|
|
): void {
|
|
markProjectCliInteractionFinished(projectId, interactionId);
|
|
}
|
|
|
|
export function hasProjectCliInteraction(projectId: string): boolean {
|
|
return (activeByProject.get(projectId)?.size ?? 0) > 0;
|
|
}
|
|
|
|
/**
|
|
* Drops optimistic interaction markers once the authoritative project snapshot
|
|
* confirms that no work remains. This is intentionally project-scoped: a
|
|
* terminal event received while another project is displayed must never leave
|
|
* the originating project's LED busy, nor clear activity in another project.
|
|
*/
|
|
export function reconcileProjectCliInteractionActivity(
|
|
projectId: string,
|
|
hasActiveWork: boolean,
|
|
): void {
|
|
if (hasActiveWork || !activeByProject.delete(projectId)) return;
|
|
emit();
|
|
}
|
|
|
|
export function subscribeProjectCliInteractionActivity(
|
|
listener: Listener,
|
|
): () => void {
|
|
listeners.add(listener);
|
|
return () => listeners.delete(listener);
|
|
}
|
|
|
|
export function useProjectCliInteractionActivityVersion(): number {
|
|
const [version, setVersion] = useState(0);
|
|
useEffect(
|
|
() => subscribeProjectCliInteractionActivity(() => setVersion((v) => v + 1)),
|
|
[],
|
|
);
|
|
return version;
|
|
}
|
|
|
|
export function __resetProjectCliInteractionActivityForTests(): void {
|
|
activeByProject.clear();
|
|
emit();
|
|
}
|