mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-11-14 04:33:43 +01:00
98 lines
3.5 KiB
TypeScript
98 lines
3.5 KiB
TypeScript
import type { Flow, FlowSortKey, ModelPayload, SortValue } from "@yaakapp-internal/proxy-lib";
|
|
import type { FlowQuery } from "./query";
|
|
|
|
/**
|
|
* The flows the list shows while it is sorted: read from the database in the sort's order, a
|
|
* page at a time from the top, as `list_flows` orders them for `sort`. Flows that change are
|
|
* placed by the same comparison, and only up to where the pages read end, so paging on never
|
|
* skips or repeats one.
|
|
*/
|
|
export interface SortedWindow {
|
|
/** Unique to this load, so a page read for an earlier sort or search is dropped */
|
|
key: string;
|
|
sort: FlowSortKey[];
|
|
query: FlowQuery | null;
|
|
/** In the sort's order */
|
|
flows: Flow[];
|
|
/**
|
|
* The last flow of the last page, as it was read, while the database has more after it: where
|
|
* the next page starts. Null once every flow is read.
|
|
*/
|
|
boundary: Flow | null;
|
|
/** The next page's `cursor`, from the last page: the boundary's sort values as read */
|
|
cursor: SortValue[] | null;
|
|
/** The first page has not arrived yet */
|
|
loading: boolean;
|
|
/** Changes that came while the first page loaded, applied once it is in */
|
|
buffered: ModelPayload[];
|
|
}
|
|
|
|
/** Where `flow` goes in `flows`, which `compare` orders */
|
|
function insertionIndex(flows: Flow[], flow: Flow, compare: (a: Flow, b: Flow) => number): number {
|
|
let lo = 0;
|
|
let hi = flows.length;
|
|
while (lo < hi) {
|
|
const mid = (lo + hi) >>> 1;
|
|
const item = flows[mid];
|
|
if (item != null && compare(item, flow) < 0) lo = mid + 1;
|
|
else hi = mid;
|
|
}
|
|
return lo;
|
|
}
|
|
|
|
/**
|
|
* Applies flow events to a sorted window's flows: a changed flow moves to where `compare` puts
|
|
* it, a deleted one leaves, and one that sorts after `boundary`, where the pages read end, is
|
|
* left for paging. With `searchKey`, only flows the backend matched against that search join or
|
|
* stay; a flow matched against another search is left as it is, to be matched again.
|
|
*/
|
|
export function applySortedChanges(
|
|
flows: Flow[],
|
|
boundary: Flow | null,
|
|
changes: ModelPayload[],
|
|
compare: (a: Flow, b: Flow) => number,
|
|
searchKey: string | null,
|
|
): Flow[] {
|
|
if (changes.length === 0) return flows;
|
|
const next = flows.slice();
|
|
for (const c of changes) {
|
|
const deleted = c.change.type === "delete";
|
|
if (!deleted && searchKey != null && c.search?.key !== searchKey) continue;
|
|
const existing = next.findIndex((f) => f.id === c.model.id);
|
|
if (existing !== -1) next.splice(existing, 1);
|
|
if (deleted || (searchKey != null && c.search?.matches !== true)) continue;
|
|
if (boundary != null && compare(c.model, boundary) > 0) continue;
|
|
next.splice(insertionIndex(next, c.model, compare), 0, c.model);
|
|
}
|
|
return next;
|
|
}
|
|
|
|
/** A page from `list_flows` as a sorted window takes it: where it ends, and where the next starts */
|
|
export function pageBoundary(
|
|
page: Flow[],
|
|
cursor: SortValue[] | null,
|
|
limit: number,
|
|
): Pick<SortedWindow, "boundary" | "cursor"> {
|
|
const more = page.length >= limit;
|
|
return { boundary: more ? (page.at(-1) ?? null) : null, cursor: more ? cursor : null };
|
|
}
|
|
|
|
/**
|
|
* Adds the next page from `list_flows`, read after the window's boundary, less any it has and
|
|
* any `keep` leaves out
|
|
*/
|
|
export function appendPage(
|
|
win: SortedWindow,
|
|
page: Flow[],
|
|
cursor: SortValue[] | null,
|
|
limit: number,
|
|
keep: (flow: Flow) => boolean = () => true,
|
|
): SortedWindow {
|
|
const held = new Set(win.flows.map((f) => f.id));
|
|
return {
|
|
...win,
|
|
flows: [...win.flows, ...page.filter((f) => !held.has(f.id) && keep(f))],
|
|
...pageBoundary(page, cursor, limit),
|
|
};
|
|
}
|