Skip to content
Source
Sign inSign up

olderSettledShelf.ts

139 lines5.8 KB

1import { useAtomValue } from "@effect/atom-react";
2import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/models";
3import {
4 isListedSettledThread,
5 settledShelfFrontier,
6 sortOlderSettledScopes,
7 unloadedOlderSettledCount,
8 wantedOlderSettledPages,
9 type OlderSettled,
10 type OlderSettledScope,
11} from "@t3tools/client-runtime/state/threads";
12import type { EnvironmentId, ProjectId } from "@t3tools/contracts";
13import { Atom } from "effect/reactivity";
14import { useEffect, useMemo, useState } from "react";
15​
16import { appAtomRegistry } from "./atom-registry";
17import { olderSettledThreads, threadFamilies } from "./threads";
18​
19const NO_PAGES: ReadonlyMap<EnvironmentId, number> = new Map();
20const NO_OLDER_SETTLED: Atom.Atom<OlderSettled | null> = Atom.make(null);
21​
22/**
23 * The Settled shelf for one scope when shells leave older settled threads out: which rows it may
24 * list, how many threads it holds in all, and the pages read below each environment's cut, whose
25 * rows merge into the thread list. `useOlderSettledShelfGrowth` reads the next pages.
26 */
27export function useOlderSettledShelf(input: {
28 readonly scopes: ReadonlyArray<OlderSettledScope>;
29 /** A collapsed shelf mounts no pages, so a reconnect reads none; expanding restores them. */
30 readonly expanded: boolean;
31 /** Rows the shelf shows; pages are read until it lists that many. */
32 readonly visibleCount: number;
33}) {
34 const scopes = useMemo(() => sortOlderSettledScopes(input.scopes), [input.scopes]);
35 const scopeKey = JSON.stringify(scopes);
36 const [requested, setRequested] = useState({ scopeKey, pages: NO_PAGES });
37 const pages = requested.scopeKey === scopeKey && input.expanded ? requested.pages : NO_PAGES;
38 const shelfScopes = scopes.map((scope) => ({
39 scope,
40 pages: pages.get(scope.environmentId) ?? 0,
41 }));
42 const shelfKey = JSON.stringify(shelfScopes);
43 const shelf = useAtomValue(olderSettledThreads.shelfAtom(shelfScopes));
44 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- mounts older settled thread pages into the shared thread registry while the shelf shows them
45 useEffect(() => {
46 const releases = (JSON.parse(shelfKey) as typeof shelfScopes).flatMap(({ scope, pages }) =>
47 pages === 0
48 ? []
49 : [
50 threadFamilies.mountRows(
51 appAtomRegistry,
52 scope.environmentId,
53 olderSettledThreads.rowsKey(scope, pages),
54 olderSettledThreads.pageRowsAtom(scope, pages),
55 ),
56 ],
57 );
58 return () => {
59 for (const release of releases) release();
60 };
61 }, [shelfKey]);
62​
63 const olderOf = (environmentId: EnvironmentId) => shelf.get(environmentId)?.older ?? null;
64 const pagesOf = (environmentId: EnvironmentId) => shelf.get(environmentId)?.pages;
65 const frontier = settledShelfFrontier([...shelf.values()]);
66 const loaded = shelfScopes.filter(({ pages }) => pages > 0);
67 return {
68 environments: shelf,
69 visibleCount: input.expanded ? input.visibleCount : 0,
70 pages,
71 grow: (next: ReadonlyMap<EnvironmentId, number>) => setRequested({ scopeKey, pages: next }),
72 isListed: (thread: EnvironmentThreadShell) => isListedSettledThread(thread.source, frontier),
73 /** Older settled threads in scope that are not local; add it to the local Settled rows. */
74 unloadedCount: (threads: ReadonlyArray<EnvironmentThreadShell>) =>
75 unloadedOlderSettledCount({ olderSettled: olderOf, scopes, threads }),
76 loading: loaded.some(({ scope }) => pagesOf(scope.environmentId)?.status === "pending"),
77 failed: loaded.some(({ scope }) => pagesOf(scope.environmentId)?.status === "failed"),
78 retry: () => {
79 for (const { scope, pages } of loaded) {
80 olderSettledThreads.retryPages(appAtomRegistry, scope, pages);
81 }
82 },
83 };
85​
86/** Reads the next pages while the shelf lists fewer rows than it shows. */
87export function useOlderSettledShelfGrowth(
88 shelf: ReturnType<typeof useOlderSettledShelf>,
89 listedSettledThreads: ReadonlyArray<EnvironmentThreadShell>,
90) {
91 const wanted = wantedOlderSettledPages({
92 requested: shelf.pages,
93 environments: shelf.environments,
94 listedCount: listedSettledThreads.length,
95 visibleCount: shelf.visibleCount,
96 });
97 const { grow, pages } = shelf;
98 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- requests more shelf pages when the loaded rows list fewer threads than the shelf shows
99 useEffect(() => {
100 if (wanted !== pages) grow(wanted);
101 }, [wanted, pages, grow]);
103​
104/**
105 * Threads of these projects that this client does not hold (older settled threads), for counts
106 * shown outside the shelf, such as a removal confirm.
107 */
108export function readUnloadedThreadCount(
109 members: ReadonlyArray<{ readonly environmentId: EnvironmentId; readonly id: ProjectId }>,
110 threads: ReadonlyArray<EnvironmentThreadShell>,
111): number {
112 return unloadedOlderSettledCount({
113 olderSettled: (environmentId) =>
114 appAtomRegistry.get(olderSettledThreads.olderSettledAtom(environmentId)),
115 scopes: [...new Set(members.map((member) => member.environmentId))].map((environmentId) => ({
116 environmentId,
117 projectIds: members.flatMap((member) =>
118 member.environmentId === environmentId ? [member.id] : [],
119 ),
120 })),
121 threads: threads.filter((thread) =>
122 members.some(
123 (member) => member.environmentId === thread.environmentId && member.id === thread.projectId,
124 ),
125 ),
126 });
128​
129/** How many older settled threads one project has, loaded or not, kept current for render. */
130export function useOlderSettledCount(
131 project: { readonly environmentId: EnvironmentId; readonly id: ProjectId } | null,
132): number {
133 const older = useAtomValue(
134 project === null
135 ? NO_OLDER_SETTLED
136 : olderSettledThreads.olderSettledAtom(project.environmentId),
137 );
138 return project === null ? 0 : (older?.counts.get(project.id) ?? 0);