import type { ReactNode } from "react"; import type { StreamItem } from "@/types/stream"; import { findMountedWindowStart, getWebMountedRecentStreamItems, getWebPartialVirtualizationThreshold, } from "./agent-stream-web-virtualization"; import { orderHeadForStreamRenderStrategy, orderTailForStreamRenderStrategy, } from "./stream-strategy"; import { resolveStreamRenderStrategy } from "./stream-strategy-resolver"; export type StreamRenderSegments = { historyVirtualized: StreamItem[]; historyMounted: StreamItem[]; liveHead: StreamItem[]; }; export type StreamHistoryBoundary = { hasVirtualizedHistory: boolean; hasMountedHistory: boolean; hasLiveHead: boolean; historyToHeadGap: number; }; export type StreamRenderAuxiliary = { pendingPermissions: ReactNode; workingIndicator: ReactNode; }; export type AgentStreamRenderModel = { history: StreamItem[]; segments: StreamRenderSegments; boundary: StreamHistoryBoundary; auxiliary: StreamRenderAuxiliary; }; export type BuildAgentStreamRenderModelInput = { tail: StreamItem[]; head: StreamItem[]; platform: "web" | "native"; isMobileBreakpoint: boolean; }; const EMPTY_STREAM_ITEMS: StreamItem[] = []; const EMPTY_AUXILIARY: StreamRenderAuxiliary = { pendingPermissions: null, workingIndicator: null, }; const orderedTailCache = new WeakMap>(); const orderedHeadCache = new WeakMap>(); const splitHistoryCache = new WeakMap< StreamItem[], Map> >(); function getOrderedItems(params: { cache: WeakMap>; source: StreamItem[]; cacheKey: string; order: (items: StreamItem[]) => StreamItem[]; }): StreamItem[] { const { cache, source, cacheKey, order } = params; let cachedByKey = cache.get(source); if (!cachedByKey) { cachedByKey = new Map(); cache.set(source, cachedByKey); } const cached = cachedByKey.get(cacheKey); if (cached) { return cached; } const ordered = order(source); cachedByKey.set(cacheKey, ordered); return ordered; } function splitOrderedTail(params: { orderedTail: StreamItem[]; platform: "web" | "native"; isMobileBreakpoint: boolean; }): Pick { const { orderedTail, platform, isMobileBreakpoint } = params; const shouldSplitHistory = platform === "web" && !isMobileBreakpoint && orderedTail.length > getWebPartialVirtualizationThreshold(); const cacheKey = `${platform}:${isMobileBreakpoint}:${getWebMountedRecentStreamItems()}:${shouldSplitHistory}`; let cachedByKey = splitHistoryCache.get(orderedTail); if (!cachedByKey) { cachedByKey = new Map(); splitHistoryCache.set(orderedTail, cachedByKey); } const cached = cachedByKey.get(cacheKey); if (cached) { return cached; } if (!shouldSplitHistory) { const unsplit = { history: orderedTail, segments: { historyVirtualized: EMPTY_STREAM_ITEMS, historyMounted: orderedTail, liveHead: EMPTY_STREAM_ITEMS, }, } satisfies Pick; cachedByKey.set(cacheKey, unsplit); return unsplit; } const mountedWindowStart = findMountedWindowStart({ items: orderedTail, minMountedCount: getWebMountedRecentStreamItems(), }); const split = { history: orderedTail, segments: { historyVirtualized: orderedTail.slice(0, mountedWindowStart), historyMounted: orderedTail.slice(mountedWindowStart), liveHead: EMPTY_STREAM_ITEMS, }, } satisfies Pick; cachedByKey.set(cacheKey, split); return split; } export function buildAgentStreamRenderModel( input: BuildAgentStreamRenderModelInput, ): AgentStreamRenderModel { const strategy = resolveStreamRenderStrategy({ platform: input.platform === "web" ? "web" : "native", isMobileBreakpoint: input.isMobileBreakpoint, }); const orderingCacheKey = `${input.platform}:${input.isMobileBreakpoint}`; const orderedTail = getOrderedItems({ cache: orderedTailCache, source: input.tail, cacheKey: orderingCacheKey, order: (items) => orderTailForStreamRenderStrategy({ strategy, streamItems: items, }), }); const orderedHead = getOrderedItems({ cache: orderedHeadCache, source: input.head, cacheKey: orderingCacheKey, order: (items) => orderHeadForStreamRenderStrategy({ strategy, streamHead: items, }), }); const splitHistory = splitOrderedTail({ orderedTail, platform: input.platform, isMobileBreakpoint: input.isMobileBreakpoint, }); return { history: splitHistory.history, segments: { ...splitHistory.segments, liveHead: orderedHead, }, boundary: { hasVirtualizedHistory: splitHistory.segments.historyVirtualized.length > 0, hasMountedHistory: splitHistory.segments.historyMounted.length > 0, hasLiveHead: orderedHead.length > 0, historyToHeadGap: 0, }, auxiliary: EMPTY_AUXILIARY, }; }