refactor: extract stream render model and segment-based rendering
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user