import { useAtomRefresh, useAtomValue } from "@effect/atom-react"; import type { EnvironmentId, ProjectListEntriesResult, ProjectReadFileResult, } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Option from "effect/Option"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; import { useCallback } from "react"; import { appAtomRegistry } from "~/rpc/atomRegistry"; import { projectEnvironment } from "~/state/projects"; import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime"; const EMPTY_PROJECT_FILE_PATH = ""; const EMPTY_PROJECT_FILE_QUERY_ATOM = Atom.make( AsyncResult.initial(false), ).pipe(Atom.withLabel("project-file-query:empty")); function optimisticFileAtom(environmentId: EnvironmentId, cwd: string, relativePath: string) { return projectEnvironment.optimisticFile({ environmentId, cwd, relativePath }); } interface ProjectQueryState { readonly data: A | null; readonly error: string | null; readonly isPending: boolean; readonly refresh: () => void; } export function getProjectEntriesQueryAtom(environmentId: EnvironmentId, cwd: string) { return projectEnvironment.listEntries({ environmentId, input: { cwd } }); } export function getProjectFileQueryAtom( environmentId: EnvironmentId, cwd: string, relativePath: string | null, ) { return projectEnvironment.readFile({ environmentId, input: { cwd, relativePath: relativePath ?? EMPTY_PROJECT_FILE_PATH }, }); } export function setProjectFileQueryData( environmentId: EnvironmentId, cwd: string, relativePath: string, contents: string, ): void { appAtomRegistry.set(optimisticFileAtom(environmentId, cwd, relativePath), { confirmedAgainst: undefined, data: { relativePath, contents, byteLength: new TextEncoder().encode(contents).byteLength, truncated: false, }, }); } export function getOptimisticProjectFileQueryData( environmentId: EnvironmentId, cwd: string, relativePath: string, ): ProjectReadFileResult | null { return appAtomRegistry.get(optimisticFileAtom(environmentId, cwd, relativePath))?.data ?? null; } export function confirmProjectFileQueryData( environmentId: EnvironmentId, cwd: string, relativePath: string, contents: string, ): boolean { const atom = optimisticFileAtom(environmentId, cwd, relativePath); const optimisticFile = appAtomRegistry.get(atom); if (optimisticFile?.data.contents !== contents) return false; const queryAtom = getProjectFileQueryAtom(environmentId, cwd, relativePath); const confirmed = { ...optimisticFile, confirmedAgainst: appAtomRegistry.get(queryAtom), }; appAtomRegistry.set(atom, confirmed); appAtomRegistry.refresh(queryAtom); void executeAtomQuery(appAtomRegistry, queryAtom, { reportDefect: false, reportFailure: false, }).then((result) => { if (result._tag === "Success" && appAtomRegistry.get(atom) === confirmed) { appAtomRegistry.set(atom, null); } }); return true; } export function resolveProjectFileQueryData( environmentId: EnvironmentId, cwd: string, relativePath: string | null, data: ProjectReadFileResult | null, ): ProjectReadFileResult | null { if (relativePath === null) return data; return appAtomRegistry.get(optimisticFileAtom(environmentId, cwd, relativePath))?.data ?? data; } export function clearProjectFileQueryData( environmentId: EnvironmentId, cwd: string, relativePath: string, ): void { appAtomRegistry.set(optimisticFileAtom(environmentId, cwd, relativePath), null); } function errorMessage(result: AsyncResult.AsyncResult): string | null { if (result._tag !== "Failure") return null; const cause = Cause.squash(result.cause); return cause instanceof Error ? cause.message : "Workspace query failed."; } export function useProjectEntriesQuery( environmentId: EnvironmentId, cwd: string, ): ProjectQueryState { const atom = getProjectEntriesQueryAtom(environmentId, cwd); const result = useAtomValue(atom); const refreshAtom = useAtomRefresh(atom); const refresh = useCallback(() => refreshAtom(), [refreshAtom]); return { data: Option.getOrNull(AsyncResult.value(result)), error: errorMessage(result), isPending: result.waiting, refresh, }; } export function useProjectFileQuery( environmentId: EnvironmentId, cwd: string, relativePath: string | null, enabled = true, ): ProjectQueryState { const atom = enabled ? getProjectFileQueryAtom(environmentId, cwd, relativePath) : EMPTY_PROJECT_FILE_QUERY_ATOM; const result = useAtomValue(atom); const refreshAtom = useAtomRefresh(atom); const refresh = useCallback(() => refreshAtom(), [refreshAtom]); const data = Option.getOrNull(AsyncResult.value(result)); const optimisticResult = useAtomValue( optimisticFileAtom(environmentId, cwd, relativePath ?? EMPTY_PROJECT_FILE_PATH), ); const optimisticFile = relativePath === null ? null : optimisticResult; return { data: optimisticFile?.data ?? data, error: errorMessage(result), isPending: result.waiting, refresh, }; }