1import {
2 AuthFilesystemReadScope,
3 type AuthSessionState,
4 type FilesystemBrowseEntry,
5 WS_METHODS,
6 sessionGrantsScope,
7 type SessionGrantInput,
8} from "@t3tools/contracts";
9import { Atom } from "effect/reactivity";
10
11import type {
12 EnvironmentConnectionPhase,
13 EnvironmentConnectionPresentation,
14} from "../connection/presentation.ts";
15import type { EnvironmentRegistry } from "../connection/registry.ts";
16import {
17 canNavigateUp,
18 getBrowseDirectoryPath,
19 getBrowseLeafPathSegment,
20 getBrowseParentPath,
21 hasTrailingPathSeparator,
22 isFilesystemBrowseQuery,
23} from "./projects.ts";
24import { createEnvironmentRpcQueryAtomFamily } from "./runtime.ts";
25
26export function resolveFilesystemReadAccess(input: {
27 readonly isCatalogReady: boolean;
28 readonly connection: Pick<EnvironmentConnectionPresentation, "phase" | "error"> | null;
29 readonly session: SessionGrantInput | null;
30 readonly sessionError: string | null;
31}) {
32 if (input.sessionError !== null) {
33 return { canReadFiles: false, isPending: false, error: input.sessionError };
34 }
35 if (input.session === null) {
36 // Wait for the catalog before interpreting a missing presentation as offline.
37 // Once ready, an offline environment cannot finish its session check.
38 const isPending =
39 !input.isCatalogReady ||
40 input.connection?.phase === "connected" ||
41 input.connection?.phase === "connecting" ||
42 input.connection?.phase === "reconnecting";
43 return {
44 canReadFiles: false,
45 isPending,
46 error: isPending ? null : (input.connection?.error ?? "This environment is not connected."),
47 };
48 }
49 return {
50 canReadFiles: sessionGrantsScope(input.session, AuthFilesystemReadScope),
51 isPending: false,
52 error: null,
53 };
54}
55
56export function getFilesystemBrowsePath(query: string, platform = "", enabled = true) {
57 const isBrowsing = enabled && isFilesystemBrowseQuery(query, platform);
58 const directoryPath = isBrowsing ? getBrowseDirectoryPath(query) : "";
59 const filterQuery =
60 isBrowsing && !hasTrailingPathSeparator(query) ? getBrowseLeafPathSegment(query) : "";
61 const parentPath = isBrowsing ? getBrowseParentPath(directoryPath) : null;
62
63 return {
64 isBrowsing,
65 directoryPath,
66 filterQuery,
67 parentPath,
68 canBrowseUp: isBrowsing && canNavigateUp(directoryPath),
69 };
70}
71
72export function filterFilesystemBrowseEntries(
73 entries: ReadonlyArray<FilesystemBrowseEntry>,
74 query: string,
75) {
76 const lowerQuery = query.toLowerCase();
77 const showHidden = query.startsWith(".");
78 const visibleEntries = entries.filter(
79 (entry) =>
80 entry.name.toLowerCase().startsWith(lowerQuery) &&
81 (showHidden || !entry.name.startsWith(".")),
82 );
83 const exactEntry =
84 query.length > 0 ? (visibleEntries.find((entry) => entry.name === query) ?? null) : null;
85
86 return { visibleEntries, exactEntry };
87}
88
89export function createBrowseNavigationCoordinator() {
90 let generation = 0;
91
92 return {
93 invalidate: () => {
94 generation += 1;
95 },
96 run: async (load: () => Promise<void>, commit: () => void) => {
97 const navigationGeneration = ++generation;
98 await load();
99 if (navigationGeneration !== generation) {
100 return false;
101 }
102 commit();
103 return true;
104 },
105 };
106}
107
108export function canPreloadBrowsePath(
109 connectionPhase: EnvironmentConnectionPhase | null | undefined,
110): boolean {
111 return connectionPhase === "connected";
112}
113
114export function createFilesystemEnvironmentAtoms<R, E>(
115 runtime: Atom.AtomRuntime<EnvironmentRegistry | R, E>,
116) {
117 return {
118 browse: createEnvironmentRpcQueryAtomFamily(runtime, {
119 label: "environment-data:filesystem:browse",
120 tag: WS_METHODS.filesystemBrowse,
121 }),
122 };
123}