| 1 | import type { |
| 2 | EnvironmentId, |
| 3 | OrchestrationProjectShell, |
| 4 | OrchestrationV2ShellSnapshot, |
| 5 | ProjectId, |
| 6 | ScopedProjectRef, |
| 7 | ServerConfig, |
| 8 | } from "@t3tools/contracts"; |
| 9 | import { Atom } from "effect/reactivity"; |
| 10 | |
| 11 | import type { EnvironmentProject } from "./models.ts"; |
| 12 | import { scopeProject } from "./models.ts"; |
| 13 | import { isScratchProject } from "./projects.ts"; |
| 14 | import { type EnvironmentCatalogState, enabledEnvironmentIds } from "./connections.ts"; |
| 15 | import { arrayElementsEqual, parseProjectKey, projectKey, projectRefsEqual } from "./entities.ts"; |
| 16 | |
| 17 | const EMPTY_PROJECTS: ReadonlyArray<OrchestrationProjectShell> = Object.freeze([]); |
| 18 | const EMPTY_PROJECT_INDEX: ReadonlyMap<ProjectId, OrchestrationProjectShell> = new Map(); |
| 19 | |
| 20 | export function createEnvironmentProjectAtoms(input: { |
| 21 | readonly catalogValueAtom: Atom.Atom<EnvironmentCatalogState>; |
| 22 | readonly snapshotAtom: ( |
| 23 | environmentId: EnvironmentId, |
| 24 | ) => Atom.Atom<OrchestrationV2ShellSnapshot | null>; |
| 25 | readonly serverConfigValueAtom?: ( |
| 26 | environmentId: EnvironmentId, |
| 27 | ) => Atom.Atom<Pick<ServerConfig, "scratchWorkspaceRoot"> | null>; |
| 28 | }) { |
| 29 | const environmentProjectsAtom = Atom.family((environmentId: EnvironmentId) => |
| 30 | Atom.make( |
| 31 | (get): ReadonlyArray<OrchestrationProjectShell> => |
| 32 | get(input.snapshotAtom(environmentId))?.projects ?? EMPTY_PROJECTS, |
| 33 | ).pipe(Atom.withLabel(`environment-projects:${environmentId}`)), |
| 34 | ); |
| 35 | |
| 36 | const environmentProjectIndexAtom = Atom.family((environmentId: EnvironmentId) => |
| 37 | Atom.make((get): ReadonlyMap<ProjectId, OrchestrationProjectShell> => { |
| 38 | const projects = get(environmentProjectsAtom(environmentId)); |
| 39 | if (projects.length === 0) { |
| 40 | return EMPTY_PROJECT_INDEX; |
| 41 | } |
| 42 | return new Map(projects.map((project) => [project.id, project] as const)); |
| 43 | }).pipe(Atom.withLabel(`environment-project-index:${environmentId}`)), |
| 44 | ); |
| 45 | |
| 46 | const environmentProjectRefsAtom = Atom.family((environmentId: EnvironmentId) => { |
| 47 | let previous: ReadonlyArray<ScopedProjectRef> = []; |
| 48 | return Atom.make((get) => { |
| 49 | const next = get(environmentProjectsAtom(environmentId)).map((project) => ({ |
| 50 | environmentId, |
| 51 | projectId: project.id, |
| 52 | })); |
| 53 | if (projectRefsEqual(previous, next)) { |
| 54 | return previous; |
| 55 | } |
| 56 | previous = next; |
| 57 | return next; |
| 58 | }).pipe(Atom.withLabel(`environment-project-refs:${environmentId}`)); |
| 59 | }); |
| 60 | |
| 61 | const projectAtomFamily = Atom.family((key: string) => { |
| 62 | const ref = parseProjectKey(key); |
| 63 | let previousSource: OrchestrationProjectShell | null = null; |
| 64 | let previousIsScratch = false; |
| 65 | let previousValue: EnvironmentProject | null = null; |
| 66 | return Atom.make((get) => { |
| 67 | const source = get(environmentProjectIndexAtom(ref.environmentId)).get(ref.projectId) ?? null; |
| 68 | const scratch = |
| 69 | source !== null && |
| 70 | isScratchProject( |
| 71 | source, |
| 72 | input.serverConfigValueAtom |
| 73 | ? get(input.serverConfigValueAtom(ref.environmentId))?.scratchWorkspaceRoot |
| 74 | : null, |
| 75 | ); |
| 76 | if (source === previousSource && scratch === previousIsScratch) { |
| 77 | return previousValue; |
| 78 | } |
| 79 | previousSource = source; |
| 80 | previousIsScratch = scratch; |
| 81 | previousValue = |
| 82 | source === null |
| 83 | ? null |
| 84 | : { |
| 85 | ...scopeProject(ref.environmentId, source), |
| 86 | ...(scratch ? { isScratch: true as const } : {}), |
| 87 | }; |
| 88 | return previousValue; |
| 89 | }).pipe(Atom.withLabel(`environment-project:${key}`)); |
| 90 | }); |
| 91 | |
| 92 | let previousProjectRefs: ReadonlyArray<ScopedProjectRef> = []; |
| 93 | const projectRefsAtom = Atom.make((get) => { |
| 94 | const refs: ScopedProjectRef[] = []; |
| 95 | for (const environmentId of enabledEnvironmentIds(get(input.catalogValueAtom))) { |
| 96 | refs.push(...get(environmentProjectRefsAtom(environmentId))); |
| 97 | } |
| 98 | if (projectRefsEqual(previousProjectRefs, refs)) { |
| 99 | return previousProjectRefs; |
| 100 | } |
| 101 | previousProjectRefs = refs; |
| 102 | return refs; |
| 103 | }).pipe(Atom.withLabel("environment-project-refs")); |
| 104 | |
| 105 | let previousProjects: ReadonlyArray<EnvironmentProject> = []; |
| 106 | const projectsAtom = Atom.make((get) => { |
| 107 | const next = get(projectRefsAtom).flatMap((ref) => { |
| 108 | const project = get(projectAtomFamily(projectKey(ref))); |
| 109 | return project === null ? [] : [project]; |
| 110 | }); |
| 111 | if (arrayElementsEqual(previousProjects, next)) { |
| 112 | return previousProjects; |
| 113 | } |
| 114 | previousProjects = next; |
| 115 | return previousProjects; |
| 116 | }).pipe(Atom.withLabel("environment-project-list")); |
| 117 | |
| 118 | const projectConfigsReadyAtom = Atom.make((get) => { |
| 119 | const catalog = get(input.catalogValueAtom); |
| 120 | if (!catalog.isReady) return false; |
| 121 | for (const environmentId of enabledEnvironmentIds(catalog)) { |
| 122 | if (input.serverConfigValueAtom && get(input.serverConfigValueAtom(environmentId)) === null) { |
| 123 | return false; |
| 124 | } |
| 125 | } |
| 126 | return true; |
| 127 | }).pipe(Atom.withLabel("environment-project-configs-ready")); |
| 128 | |
| 129 | return { |
| 130 | environmentProjectsAtom, |
| 131 | environmentProjectIndexAtom, |
| 132 | environmentProjectRefsAtom, |
| 133 | projectRefsAtom, |
| 134 | projectsAtom, |
| 135 | projectConfigsReadyAtom, |
| 136 | projectAtom: (ref: ScopedProjectRef) => projectAtomFamily(projectKey(ref)), |
| 137 | }; |
| 138 | } |