179 lines
5.0 KiB
TypeScript
179 lines
5.0 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import type { StreamItem } from "@/types/stream";
|
|
import {
|
|
findMountedWindowStart,
|
|
getWebMountedRecentStreamItems,
|
|
getWebPartialVirtualizationThreshold,
|
|
} from "./agent-stream-web-virtualization";
|
|
import {
|
|
orderHeadForStreamRenderStrategy,
|
|
orderTailForStreamRenderStrategy,
|
|
resolveStreamRenderStrategy,
|
|
} from "./stream-strategy";
|
|
|
|
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<StreamItem[], Map<string, StreamItem[]>>();
|
|
const orderedHeadCache = new WeakMap<StreamItem[], Map<string, StreamItem[]>>();
|
|
const splitHistoryCache = new WeakMap<
|
|
StreamItem[],
|
|
Map<string, Pick<AgentStreamRenderModel, "history" | "segments">>
|
|
>();
|
|
|
|
function getOrderedItems(params: {
|
|
cache: WeakMap<StreamItem[], Map<string, StreamItem[]>>;
|
|
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<AgentStreamRenderModel, "history" | "segments"> {
|
|
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<AgentStreamRenderModel, "history" | "segments">;
|
|
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<AgentStreamRenderModel, "history" | "segments">;
|
|
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,
|
|
};
|
|
}
|