Repository navigation
Expand file tree
/
Copy paththreads.ts
More file actions
124 lines (114 loc) · 4.98 KB
/
Copy paththreads.ts
File metadata and controls
124 lines (114 loc) · 4.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import { useAtomValue } from "@effect/atom-react";
import { enabledEnvironmentIds } from "@t3tools/client-runtime/state/connections";
import { arrayElementsEqual } from "@t3tools/client-runtime/state/entities";
import {
createEnvironmentThreadDetailAtoms,
createEnvironmentThreadShellAtoms,
createEnvironmentThreadStateAtoms,
EMPTY_ENVIRONMENT_THREAD_STATE,
type EnvironmentThreadState,
createThreadEnvironmentAtoms,
} from "@t3tools/client-runtime/state/threads";
import type { EnvironmentId, OrchestrationV2ThreadShell, ThreadId } from "@t3tools/contracts";
import * as Option from "effect/Option";
import { AsyncResult, Atom } from "effect/reactivity";
import { environmentCatalog } from "../connection/catalog";
import { connectionAtomRuntime } from "../connection/runtime";
import { environmentSnapshotAtom } from "./shell";
export const threadEnvironment = createThreadEnvironmentAtoms(
connectionAtomRuntime,
environmentSnapshotAtom,
);
const environmentThreads = createEnvironmentThreadStateAtoms(connectionAtomRuntime);
export const environmentThreadDetails = createEnvironmentThreadDetailAtoms(
environmentThreads.stateAtom,
);
export const environmentThreadShells = createEnvironmentThreadShellAtoms({
catalogValueAtom: environmentCatalog.catalogValueAtom,
snapshotAtom: threadEnvironment.snapshotAtom,
});
const EMPTY_THREAD_STATE_ATOM = Atom.make(AsyncResult.success(EMPTY_ENVIRONMENT_THREAD_STATE)).pipe(
Atom.withLabel("web-environment-thread:empty"),
);
export function useEnvironmentThread(
environmentId: EnvironmentId | null,
threadId: ThreadId | null,
): EnvironmentThreadState {
const result = useAtomValue(
environmentId !== null && threadId !== null
? environmentThreads.stateAtom(environmentId, threadId)
: EMPTY_THREAD_STATE_ATOM,
);
const state = Option.getOrElse(
AsyncResult.value(result),
() => EMPTY_ENVIRONMENT_THREAD_STATE,
) as EnvironmentThreadState;
return state;
}
const isRunning = (status: string) =>
status === "preparing" || status === "queued" || status === "starting" || status === "running";
type KeptThreads = ReadonlyMap<EnvironmentId, ReadonlySet<ThreadId>>;
function isDetailDone<E>(result: AsyncResult.AsyncResult<EnvironmentThreadState, E>): boolean {
if (!AsyncResult.isSuccess(result)) return true;
const { status, data, error } = result.value;
if (status === "deleted" || Option.isSome(error)) return true;
return (
status === "live" &&
!Option.exists(data, (thread) => thread.runs.some((run) => isRunning(run.status)))
);
}
/**
* The shell and detail streams are independent, so the shell can report a stop
* before the detail loads or catches up. A stopped thread stays mounted until
* its own detail is live and shows the stop too.
*/
export function createRunningThreadKeepAliveAtom<E>(input: {
readonly environmentIdsAtom: Atom.Atom<ReadonlyArray<EnvironmentId>>;
readonly threadsAtom: (
environmentId: EnvironmentId,
) => Atom.Atom<ReadonlyArray<Pick<OrchestrationV2ThreadShell, "id" | "status">>>;
readonly stateAtom: (
environmentId: EnvironmentId,
threadId: ThreadId,
) => Atom.Atom<AsyncResult.AsyncResult<EnvironmentThreadState, E>>;
}) {
const runningThreadIdsAtom = Atom.family((environmentId: EnvironmentId) => {
let previous: ReadonlyArray<ThreadId> = [];
return Atom.make((get) => {
const running = get(input.threadsAtom(environmentId)).flatMap((thread) =>
isRunning(thread.status) ? [thread.id] : [],
);
if (arrayElementsEqual(previous, running)) return previous;
previous = running;
return running;
}).pipe(Atom.withLabel(`web-running-thread-ids:${environmentId}`));
});
return Atom.make((get): KeptThreads => {
const previous = Option.getOrUndefined(get.self<KeptThreads>());
const kept = new Map<EnvironmentId, ReadonlySet<ThreadId>>();
for (const environmentId of get(input.environmentIdsAtom)) {
const threadIds = new Set(get(runningThreadIdsAtom(environmentId)));
for (const threadId of previous?.get(environmentId) ?? []) {
if (threadIds.has(threadId)) continue;
const stateAtom = input.stateAtom(environmentId, threadId);
// `once`, not `get`: a dependency on a stopped thread would hold its
// stream open until some other change rebuilds this atom.
if (isDetailDone(get.once(stateAtom))) continue;
threadIds.add(threadId);
get.subscribe(stateAtom, (state) => {
if (isDetailDone(state)) get.refreshSelf();
});
}
for (const threadId of threadIds) get.mount(input.stateAtom(environmentId, threadId));
kept.set(environmentId, threadIds);
}
return kept;
}).pipe(Atom.withLabel("web-running-thread-keep-alive"));
}
export const runningThreadKeepAliveAtom = createRunningThreadKeepAliveAtom({
environmentIdsAtom: Atom.map(environmentCatalog.catalogValueAtom, (catalog) => [
...enabledEnvironmentIds(catalog),
]),
threadsAtom: environmentThreadShells.environmentThreadsAtom,
stateAtom: environmentThreads.stateAtom,
});