mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-11-13 12:13:42 +01:00
The app held every flow in memory and redid all of its work for each new one: the sidebar mounted every domain and path node (its tree scrolled inside a container that never bounded it), every tree row looked up its atoms by scanning every atom the family had made, and the layout, detail pane and list rows re-rendered per flow. At 3,000 flows each new one cost about 90 ms of main thread, more than the traffic left time for. The app now holds a window of the newest 2,000 flows, applies new ones in batches, and loads older ones in pages from the database on scrolling to the top, through a new list_flows RPC that pages in SQL. The database keeps every flow. Also adds clear_flows: an RPC, POST /flows/clear on the control API, and a Clear button in the header that Cmd+K opens, confirming first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132 lines
4.7 KiB
TypeScript
132 lines
4.7 KiB
TypeScript
import type { Flow, ModelPayload } from "@yaakapp-internal/proxy-lib";
|
|
import { describe, expect, test } from "vite-plus/test";
|
|
import {
|
|
applyChanges,
|
|
emptyWindow,
|
|
type FlowWindow,
|
|
prependOlder,
|
|
trimWindow,
|
|
windowFromPage,
|
|
} from "./flowWindow";
|
|
|
|
/** A flow started at `second` past 10:00, with an id that sorts the same way */
|
|
function flow(second: number, extra: Partial<Flow> = {}): Flow {
|
|
const s = String(second).padStart(2, "0");
|
|
return { id: `fl_${s}`, startedAt: `2026-10-28T10:00:${s}`, ...extra } as Flow;
|
|
}
|
|
|
|
const created = (f: Flow): ModelPayload => ({
|
|
model: f,
|
|
change: { type: "upsert", created: true },
|
|
});
|
|
const updated = (f: Flow): ModelPayload => ({
|
|
model: f,
|
|
change: { type: "upsert", created: false },
|
|
});
|
|
const ids = (win: FlowWindow) => win.flows.map((f) => f.id);
|
|
|
|
describe("windowFromPage", () => {
|
|
test("holds the newest page oldest first", () => {
|
|
const win = windowFromPage([flow(3), flow(2), flow(1)], 3, 10);
|
|
expect(ids(win)).toEqual(["fl_01", "fl_02", "fl_03"]);
|
|
expect(win).toMatchObject({ hasOlder: true, total: 10 });
|
|
});
|
|
|
|
test("knows there is nothing older when the page came back short", () => {
|
|
expect(windowFromPage([flow(1)], 3, 1).hasOlder).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("applyChanges", () => {
|
|
test("appends new traffic and counts it", () => {
|
|
const win = applyChanges(windowFromPage([flow(2), flow(1)], 10, 2), [
|
|
created(flow(3)),
|
|
created(flow(4)),
|
|
]);
|
|
expect(ids(win)).toEqual(["fl_01", "fl_02", "fl_03", "fl_04"]);
|
|
expect(win.total).toBe(4);
|
|
});
|
|
|
|
test("puts a slow flow where it started", () => {
|
|
const win = applyChanges(windowFromPage([flow(5), flow(1)], 10, 2), [created(flow(3))]);
|
|
expect(ids(win)).toEqual(["fl_01", "fl_03", "fl_05"]);
|
|
});
|
|
|
|
test("orders flows that started together by id", () => {
|
|
const a = flow(2, { id: "fl_a" });
|
|
const b = flow(2, { id: "fl_b" });
|
|
const win = applyChanges(emptyWindow, [created(b), created(a)]);
|
|
expect(ids(win)).toEqual(["fl_a", "fl_b"]);
|
|
});
|
|
|
|
test("replaces a changed flow in place without counting it again", () => {
|
|
const saved = flow(2, { savedRequestId: "rq_1" });
|
|
const win = applyChanges(windowFromPage([flow(3), flow(2), flow(1)], 10, 3), [updated(saved)]);
|
|
expect(win.flows[1]).toBe(saved);
|
|
expect(win.total).toBe(3);
|
|
});
|
|
|
|
test("replaces a flow the first page already had, without counting it again", () => {
|
|
const win = applyChanges(windowFromPage([flow(2), flow(1)], 10, 2), [created(flow(2))]);
|
|
expect(ids(win)).toEqual(["fl_01", "fl_02"]);
|
|
expect(win.total).toBe(2);
|
|
});
|
|
|
|
test("leaves a flow older than the window to paging, but counts it", () => {
|
|
const win = applyChanges(windowFromPage([flow(5), flow(4)], 2, 9), [created(flow(1))]);
|
|
expect(ids(win)).toEqual(["fl_04", "fl_05"]);
|
|
expect(win.total).toBe(10);
|
|
});
|
|
|
|
test("keeps an older flow when the window already holds everything", () => {
|
|
const win = applyChanges(windowFromPage([flow(5), flow(4)], 10, 2), [created(flow(1))]);
|
|
expect(ids(win)).toEqual(["fl_01", "fl_04", "fl_05"]);
|
|
});
|
|
|
|
test("returns the same window for no changes", () => {
|
|
const win = windowFromPage([flow(1)], 10, 1);
|
|
expect(applyChanges(win, [])).toBe(win);
|
|
});
|
|
});
|
|
|
|
describe("trimWindow", () => {
|
|
const many = (n: number) =>
|
|
windowFromPage(
|
|
Array.from({ length: n }, (_, i) => flow(n - i)),
|
|
1000,
|
|
n,
|
|
);
|
|
|
|
test("leaves the window alone within the slack", () => {
|
|
const win = many(7);
|
|
expect(trimWindow(win, 4, 3)).toBe(win);
|
|
});
|
|
|
|
test("drops the oldest back to the size, an even number of them", () => {
|
|
const even = trimWindow(many(8), 4, 3);
|
|
expect(ids(even)).toEqual(["fl_05", "fl_06", "fl_07", "fl_08"]);
|
|
expect(even).toMatchObject({ hasOlder: true, total: 8 });
|
|
// Five over rounds up to six, so the list's stripes stay on the same flows.
|
|
expect(ids(trimWindow(many(9), 4, 3))).toEqual(["fl_07", "fl_08", "fl_09"]);
|
|
});
|
|
});
|
|
|
|
describe("prependOlder", () => {
|
|
test("adds an older page before the oldest flow held", () => {
|
|
const win = prependOlder(windowFromPage([flow(5), flow(4)], 2, 5), [flow(3), flow(2)], 2);
|
|
expect(ids(win)).toEqual(["fl_02", "fl_03", "fl_04", "fl_05"]);
|
|
expect(win.hasOlder).toBe(true);
|
|
});
|
|
|
|
test("knows it reached the start when a page comes back short", () => {
|
|
const win = prependOlder(windowFromPage([flow(5), flow(4)], 2, 3), [flow(1)], 2);
|
|
expect(ids(win)).toEqual(["fl_01", "fl_04", "fl_05"]);
|
|
expect(win.hasOlder).toBe(false);
|
|
});
|
|
|
|
test("skips flows the window already has", () => {
|
|
const win = prependOlder(windowFromPage([flow(5), flow(4)], 2, 5), [flow(4), flow(3)], 2);
|
|
expect(ids(win)).toEqual(["fl_03", "fl_04", "fl_05"]);
|
|
});
|
|
});
|