import type { EnvironmentId, OrchestrationShellSnapshot, OrchestrationThreadShell, ProjectId, ScopedProjectRef, ScopedThreadRef, ThreadId, } from "@t3tools/contracts"; import { Atom } from "effect/unstable/reactivity"; import type { EnvironmentThreadShell } from "./models.ts"; import { scopeThreadShell } from "./models.ts"; import type { EnvironmentCatalogState } from "./connections.ts"; import { arrayElementsEqual, parseProjectRefCollectionKey, parseThreadKey, projectRefCollectionKey, threadKey, threadRefsEqual, } from "./entities.ts"; const EMPTY_THREADS: ReadonlyArray = Object.freeze([]); const EMPTY_SCOPED_THREAD_REFS: ReadonlyArray = Object.freeze([]); const EMPTY_THREAD_INDEX: ReadonlyMap = new Map(); const EMPTY_THREAD_REFS_BY_PROJECT: ReadonlyMap< ProjectId, ReadonlyArray > = new Map(); export function createEnvironmentThreadShellAtoms(input: { readonly catalogValueAtom: Atom.Atom; readonly snapshotAtom: ( environmentId: EnvironmentId, ) => Atom.Atom; }) { const environmentThreadsAtom = Atom.family((environmentId: EnvironmentId) => Atom.make( (get): ReadonlyArray => get(input.snapshotAtom(environmentId))?.threads ?? EMPTY_THREADS, ).pipe(Atom.withLabel(`environment-threads:${environmentId}`)), ); const environmentThreadIndexAtom = Atom.family((environmentId: EnvironmentId) => Atom.make((get): ReadonlyMap => { const threads = get(environmentThreadsAtom(environmentId)); if (threads.length === 0) { return EMPTY_THREAD_INDEX; } return new Map(threads.map((thread) => [thread.id, thread] as const)); }).pipe(Atom.withLabel(`environment-thread-index:${environmentId}`)), ); const environmentThreadRefsAtom = Atom.family((environmentId: EnvironmentId) => { let previous: ReadonlyArray = []; return Atom.make((get) => { const next = get(environmentThreadsAtom(environmentId)).map((thread) => ({ environmentId, threadId: thread.id, })); if (threadRefsEqual(previous, next)) { return previous; } previous = next; return next; }).pipe(Atom.withLabel(`environment-thread-refs:${environmentId}`)); }); const environmentThreadRefsByProjectAtom = Atom.family((environmentId: EnvironmentId) => { let previous: ReadonlyMap< ProjectId, ReadonlyArray > = EMPTY_THREAD_REFS_BY_PROJECT; return Atom.make((get) => { const grouped = new Map(); for (const thread of get(environmentThreadsAtom(environmentId))) { const refs = grouped.get(thread.projectId); const ref = { environmentId, threadId: thread.id }; if (refs === undefined) { grouped.set(thread.projectId, [ref]); } else { refs.push(ref); } } if (grouped.size === 0) { previous = EMPTY_THREAD_REFS_BY_PROJECT; return previous; } const next = new Map>(); for (const [projectId, refs] of grouped) { const previousRefs = previous.get(projectId); next.set( projectId, previousRefs !== undefined && threadRefsEqual(previousRefs, refs) ? previousRefs : refs, ); } previous = next; return previous; }).pipe(Atom.withLabel(`environment-thread-refs-by-project:${environmentId}`)); }); const threadShellAtomFamily = Atom.family((key: string) => { const ref = parseThreadKey(key); let previousSource: OrchestrationThreadShell | null = null; let previousValue: EnvironmentThreadShell | null = null; return Atom.make((get) => { const source = get(environmentThreadIndexAtom(ref.environmentId)).get(ref.threadId) ?? null; if (source === previousSource) { return previousValue; } previousSource = source; previousValue = source === null ? null : scopeThreadShell(ref.environmentId, source); return previousValue; }).pipe(Atom.withLabel(`environment-thread-shell:${key}`)); }); const threadShellsForProjectRefsAtomFamily = Atom.family((key: string) => { const projectRefs = parseProjectRefCollectionKey(key); let previous: ReadonlyArray = []; return Atom.make((get) => { const next: EnvironmentThreadShell[] = []; const seen = new Set(); for (const projectRef of projectRefs) { const refs = get(environmentThreadRefsByProjectAtom(projectRef.environmentId)).get( projectRef.projectId, ) ?? EMPTY_SCOPED_THREAD_REFS; for (const ref of refs) { const key = threadKey(ref); if (seen.has(key)) { continue; } seen.add(key); const thread = get(threadShellAtomFamily(key)); if (thread !== null) { next.push(thread); } } } if (arrayElementsEqual(previous, next)) { return previous; } previous = next; return previous; }).pipe(Atom.withLabel(`environment-thread-shells-for-projects:${key}`)); }); let previousThreadRefs: ReadonlyArray = []; const threadRefsAtom = Atom.make((get) => { const refs: ScopedThreadRef[] = []; for (const environmentId of get(input.catalogValueAtom).entries.keys()) { refs.push(...get(environmentThreadRefsAtom(environmentId))); } if (threadRefsEqual(previousThreadRefs, refs)) { return previousThreadRefs; } previousThreadRefs = refs; return refs; }).pipe(Atom.withLabel("environment-thread-refs")); let previousThreadShells: ReadonlyArray = []; const threadShellsAtom = Atom.make((get) => { const next = get(threadRefsAtom).flatMap((ref) => { const thread = get(threadShellAtomFamily(threadKey(ref))); return thread === null ? [] : [thread]; }); if (arrayElementsEqual(previousThreadShells, next)) { return previousThreadShells; } previousThreadShells = next; return previousThreadShells; }).pipe(Atom.withLabel("environment-thread-shell-list")); return { environmentThreadsAtom, environmentThreadIndexAtom, environmentThreadRefsAtom, environmentThreadRefsByProjectAtom, threadRefsAtom, threadShellsAtom, threadShellsForProjectRefsAtom: (refs: ReadonlyArray) => threadShellsForProjectRefsAtomFamily(projectRefCollectionKey(refs)), threadShellAtom: (ref: ScopedThreadRef) => threadShellAtomFamily(threadKey(ref)), }; }