1import { useAtomValue } from "@effect/atom-react";
2import type { EnvironmentShellStatus } from "@t3tools/client-runtime/state/shell";
3import type { EnvironmentId, MessageId } from "@t3tools/contracts";
4import { Atom } from "effect/reactivity";
5
6import { appAtomRegistry } from "./atom-registry";
7import { environmentShell } from "./shell";
8import { threadOutboxManager } from "./thread-outbox";
9
10const threadOutboxShellStatusesAtom = Atom.make(
11 (get): ReadonlyMap<EnvironmentId, EnvironmentShellStatus> => {
12 const statuses = new Map<EnvironmentId, EnvironmentShellStatus>();
13 for (const queue of Object.values(get(threadOutboxManager.queuedMessagesByThreadKeyAtom))) {
14 const environmentId = queue[0]?.environmentId;
15 if (environmentId !== undefined && !statuses.has(environmentId)) {
16 statuses.set(environmentId, get(environmentShell.stateValueAtom(environmentId)).status);
17 }
18 }
19 return statuses;
20 },
21).pipe(Atom.withLabel("mobile:thread-outbox:shell-statuses"));
22
23/**
24 * Queued pending tasks the outbox drain must not deliver right now: the one
25 * open in the new-task editor, plus any whose latest edits could not be saved
26 * back yet (delivering those would send stale content). Editing sessions hold
27 * their message id here and release it once the queued payload is current.
28 */
29export const editingQueuedMessageIdsAtom = Atom.make<Readonly<Record<MessageId, true>>>({}).pipe(
30 Atom.keepAlive,
31 Atom.withLabel("mobile:thread-outbox:editing-message-ids"),
32);
33
34export const dispatchingQueuedMessageIdAtom = Atom.make<MessageId | null>(null).pipe(
35 Atom.keepAlive,
36 Atom.withLabel("mobile:thread-outbox:dispatching-message-id"),
37);
38
39export function holdEditingQueuedMessage(messageId: MessageId): void {
40 const current = appAtomRegistry.get(editingQueuedMessageIdsAtom);
41 if (current[messageId]) {
42 return;
43 }
44 appAtomRegistry.set(editingQueuedMessageIdsAtom, { ...current, [messageId]: true });
45}
46
47export function releaseEditingQueuedMessage(messageId: MessageId): void {
48 const current = appAtomRegistry.get(editingQueuedMessageIdsAtom);
49 if (!current[messageId]) {
50 return;
51 }
52 const next = { ...current };
53 delete next[messageId];
54 appAtomRegistry.set(editingQueuedMessageIdsAtom, next);
55}
56
57export function useThreadOutboxMessages() {
58 return useAtomValue(threadOutboxManager.queuedMessagesByThreadKeyAtom);
59}
60
61/**
62 * Thread keys (`environmentId:threadId`) of existing threads with a message
63 * waiting in the outbox. Creations are excluded: they have no thread row yet
64 * and surface as pending tasks instead. Derived once so list builders and
65 * reorder planners agree on which settled threads are pulled back to active.
66 */
67export const queuedThreadKeysAtom = Atom.make((get): ReadonlySet<string> => {
68 const keys = new Set<string>();
69 for (const [threadKey, queue] of Object.entries(
70 get(threadOutboxManager.queuedMessagesByThreadKeyAtom),
71 )) {
72 if (queue.some((message) => message.creation === undefined)) {
73 keys.add(threadKey);
74 }
75 }
76 return keys;
77}).pipe(Atom.withLabel("mobile:thread-outbox:queued-thread-keys"));
78
79export function useQueuedThreadKeys(): ReadonlySet<string> {
80 return useAtomValue(queuedThreadKeysAtom);
81}
82
83export function useThreadOutboxShellStatuses() {
84 return useAtomValue(threadOutboxShellStatusesAtom);
85}