1export const COMPOSER_TYPING_IDLE_MS = 1500;
2
3export function createComposerTypingGuardState() {
4 return {
5 seen: new Set<string>(),
6 held: new Set<string>(),
7 focused: false,
8 lastTypedAt: -Infinity,
9 };
10}
11
12type TypingGuardEvent =
13 | { type: "requests"; requestIds: readonly string[]; now: number }
14 | { type: "type"; now: number }
15 | { type: "focus" | "blur" | "send" | "idle" };
16
17export function updateComposerTypingGuard(
18 state: ReturnType<typeof createComposerTypingGuardState>,
19 event: TypingGuardEvent,
20) {
21 switch (event.type) {
22 case "requests": {
23 const seen = new Set(state.seen);
24 const held = new Set([...state.held].filter((id) => event.requestIds.includes(id)));
25 for (const id of event.requestIds) {
26 if (
27 !seen.has(id) &&
28 state.focused &&
29 event.now - state.lastTypedAt < COMPOSER_TYPING_IDLE_MS
30 ) {
31 held.add(id);
32 }
33 seen.add(id);
34 }
35 return { ...state, seen, held };
36 }
37 case "type":
38 return { ...state, lastTypedAt: event.now };
39 case "focus":
40 return { ...state, focused: true };
41 case "blur":
42 case "send":
43 case "idle":
44 return {
45 ...state,
46 held: new Set<string>(),
47 lastTypedAt: -Infinity,
48 focused: event.type === "blur" ? false : state.focused,
49 };
50 }
51}