1import { hubConnection } from "../connection/hub";
2import { useAtomValue } from "@effect/atom-react";
3import { createEnvironmentSessionAtoms } from "@t3tools/client-runtime/state/session";
4import {
5 AuthOrchestrationOperateScope,
6 type AuthEnvironmentScope,
7 type AuthSessionState,
8 type EnvironmentId,
9 sessionGrantsScope,
10} from "@t3tools/contracts";
11import * as Option from "effect/Option";
12import { AsyncResult, Atom } from "effect/reactivity";
13import { useMemo } from "react";
14
15import { connectionAtomRuntime } from "../connection/runtime";
16import { appAtomRegistry } from "./atom-registry";
17
18export const environmentSession = createEnvironmentSessionAtoms(connectionAtomRuntime);
19
20const EMPTY_SESSION_STATE_ATOM = Atom.make(AsyncResult.initial<AuthSessionState>());
21
22function sessionHasScope(
23 result: AsyncResult.AsyncResult<AuthSessionState, unknown>,
24 scope: AuthEnvironmentScope,
25): boolean {
26 const session = Option.getOrNull(AsyncResult.value(result));
27 return result._tag !== "Failure" && session !== null && sessionGrantsScope(session, scope);
28}
29
30/** Uses the selected environment's grant, including cached scopes during a refresh. */
31export function useEnvironmentScope(
32 environmentId: EnvironmentId | null,
33 scope: AuthEnvironmentScope,
34): boolean {
35 const result = useAtomValue(
36 environmentId === null
37 ? EMPTY_SESSION_STATE_ATOM
38 : environmentSession.sessionStateAtom(environmentId),
39 );
40 return sessionHasScope(result, scope);
41}
42
43/** Keeps background workers subscribed to each target's grant. */
44export function useEnvironmentsWithScope(
45 environments: ReadonlyArray<{ readonly environmentId: EnvironmentId }>,
46 scope: AuthEnvironmentScope,
47): ReadonlySet<EnvironmentId> {
48 const permittedEnvironments = useMemo(
49 () =>
50 Atom.make((get) => {
51 const permitted = new Set<EnvironmentId>();
52 for (const { environmentId } of environments) {
53 if (sessionHasScope(get(environmentSession.sessionStateAtom(environmentId)), scope)) {
54 permitted.add(environmentId);
55 }
56 }
57 return permitted;
58 }),
59 [environments, scope],
60 );
61 return useAtomValue(permittedEnvironments);
62}
63
64export function readEnvironmentScope(
65 environmentId: EnvironmentId,
66 scope: AuthEnvironmentScope,
67): boolean {
68 const result = appAtomRegistry.get(environmentSession.sessionStateAtom(environmentId));
69 return sessionHasScope(result, scope);
70}
71
72const EMPTY_PREPARED_CONNECTION_ATOM = Atom.make(Option.none()).pipe(
73 Atom.withLabel("mobile-prepared-connection:empty"),
74);
75
76export function usePreparedConnection(environmentId: EnvironmentId | null) {
77 return useAtomValue(
78 environmentId === null
79 ? EMPTY_PREPARED_CONNECTION_ATOM
80 : environmentSession.preparedConnectionValueAtom(environmentId),
81 );
82}
83
84export function useThreadOrganizationPermission(environmentId: EnvironmentId | null): boolean {
85 const source = useEnvironmentScope(environmentId, AuthOrchestrationOperateScope);
86 const central = useAtomValue(hubConnection.organizationPermissionAtom(environmentId));
87 return source || central;
88}
89
90export function readThreadOrganizationPermission(environmentId: EnvironmentId): boolean {
91 return (
92 readEnvironmentScope(environmentId, AuthOrchestrationOperateScope) ||
93 appAtomRegistry.get(hubConnection.organizationPermissionAtom(environmentId))
94 );
95}