@1771technologies/lytenyte-pro
Version:
Blazingly fast headless React data grid with 100s of features.
414 lines (413 loc) • 15.2 kB
JavaScript
import { effect, makeAtom, signal } from "@1771technologies/lytenyte-core/yinternal";
import { useRef } from "react";
import { ServerData } from "./server-data.js";
import { equal } from "@1771technologies/lytenyte-shared";
export function makeServerDataSource({ dataFetcher, dataInFilterItemFetcher, dataColumnPivotFetcher, cellUpdateHandler, cellUpdateOptimistically, blockSize = 200, }) {
let grid = null;
let flat;
let source;
void grid;
const isLoading = makeAtom(signal(false));
const loadError = makeAtom(signal(null));
const requestsForView = makeAtom(signal([]));
const cleanup = [];
const init = (g) => {
grid = g;
source = new ServerData({
defaultExpansion: g.state.rowGroupDefaultExpansion.get(),
blocksize: blockSize,
expansions: g.state.rowGroupExpansions.get(),
pivotExpansions: g.state.columnPivotRowGroupExpansions.get(),
pivotMode: g.state.columnPivotMode.get(),
onResetLoadBegin: () => {
isLoading.set(true);
loadError.set(null);
},
onInvalidate: () => g.state.rowDataStore.rowClearCache(),
onResetLoadEnd: () => isLoading.set(false),
onResetLoadError: (e) => loadError.set(e),
onFlatten: (f) => {
const store = g.state.rowDataStore;
flat = f;
store.rowTopCount.set(f.top);
store.rowCenterCount.set(f.center);
store.rowBottomCount.set(f.bottom);
store.rowClearCache();
},
});
let prevModel = {
sorts: g.state.sortModel.get(),
groups: g.state.rowGroupModel.get(),
filters: g.state.filterModel.get(),
filtersIn: g.state.filterInModel.get(),
quickSearch: g.state.quickSearch.get(),
aggregations: g.state.aggModel.get(),
pivotMode: g.state.columnPivotMode.get(),
pivotModel: g.state.columnPivotModel.get(),
};
cleanup.push(effect(() => {
const newModel = {
// @ts-expect-error this is fine - just a hidden type
sorts: g.state.sortModel.$(),
// @ts-expect-error this is fine - just a hidden type
groups: g.state.rowGroupModel.$(),
// @ts-expect-error this is fine - just a hidden type
filters: g.state.filterModel.$(),
// @ts-expect-error this is fine - just a hidden type
filtersIn: g.state.filterInModel.$(),
// @ts-expect-error this is fine - just a hidden type
quickSearch: g.state.quickSearch.$(),
// @ts-expect-error this is fine - just a hidden type
aggregations: g.state.aggModel.$(),
// @ts-expect-error this is fine - just a hidden type
pivotMode: g.state.columnPivotMode.$(),
// @ts-expect-error this is fine - just a hidden type
pivotModel: g.state.columnPivotModel.$(),
};
if (equal(newModel, prevModel))
return;
source.dataFetcher = (req, expansions, pivotExpansions) => {
return dataFetcher({
grid: g,
model: {
...newModel,
groupExpansions: expansions,
pivotGroupExpansions: pivotExpansions,
},
reqTime: Date.now(),
requests: req,
});
};
prevModel = newModel;
}));
const pivotModel = g.state.columnPivotModel.get();
let prevPivotColumnModel = pivotModel.columns;
let prevPivotColumnValues = pivotModel.values;
const updatePivotColumns = async (model, ignoreEqualCheck = false) => {
if (!ignoreEqualCheck &&
equal(prevPivotColumnModel, model.columns) &&
equal(prevPivotColumnValues, model.values))
return;
const pivotColumns = await dataColumnPivotFetcher?.({
grid: g,
model: {
...prevModel,
pivotMode: g.state.columnPivotMode.get(),
pivotModel: model,
groupExpansions: g.state.rowGroupExpansions.get(),
pivotGroupExpansions: g.state.columnPivotColumnGroupExpansions.get(),
},
reqTime: Date.now(),
});
g.state.columnPivotColumns.set(pivotColumns ?? []);
g.state.rowDataStore.rowClearCache();
prevPivotColumnModel = model.columns;
prevPivotColumnValues = model.values;
};
if (g.state.columnPivotMode.get())
updatePivotColumns(pivotModel);
// Pivot model monitoring
cleanup.push(grid.state.columnPivotMode.watch(() => {
const model = g.state.columnPivotModel.get();
updatePivotColumns(model);
}));
cleanup.push(grid.state.columnPivotModel.watch(() => {
const model = g.state.columnPivotModel.get();
updatePivotColumns(model);
}));
cleanup.push(g.state.rowGroupDefaultExpansion.watch(() => {
source.defaultExpansion = g.state.rowGroupDefaultExpansion.get();
}));
cleanup.push(g.state.viewBounds.watch(() => {
const bounds = g.state.viewBounds.get();
source.rowViewBounds = [bounds.rowCenterStart, bounds.rowCenterEnd];
const requests = source.requestsForView();
const current = requestsForView.get();
if (equal(requests, current))
return;
requestsForView.set(requests);
}));
source.dataFetcher = (req, expansions, pivotExpansions) => {
return dataFetcher({
grid: g,
model: {
...prevModel,
groupExpansions: expansions,
pivotGroupExpansions: pivotExpansions,
},
reqTime: Date.now(),
requests: req,
});
};
};
const rowById = (id) => {
return flat.rowIdToRow.get(id) ?? null;
};
const rowAllChildIds = (rowId) => {
const f = flat;
const row = f.rowIdToRow.get(rowId);
if (!row || row.kind === "leaf")
return [];
const ids = new Set();
const node = f.rowIdToTreeNode.get(row.id);
if (!node || node.kind === "leaf")
return [];
const stack = [...node.byPath.values()];
while (stack.length) {
const item = stack.pop();
if (item?.kind === "leaf") {
ids.add(item.data.id);
}
else {
stack.push(...item.byPath.values());
ids.add(item.data.id);
}
}
return [...ids];
};
const rowByIndex = (i) => {
const row = flat.rowIndexToRow.get(i);
const isLoading = flat.loading.has(i);
const isGroupLoading = flat.loadingGroup.has(i);
const errorGroup = flat.erroredGroup.get(i);
const error = flat.errored.get(i);
if (!row)
return {
id: `__loading__placeholder__${i}`,
data: null,
kind: "leaf",
loading: isLoading,
error: error,
};
if (row.kind === "leaf") {
if (error || isLoading) {
return { ...row, loading: isLoading, error: error?.error };
}
}
else if (row.kind === "branch") {
if (error || isLoading || isGroupLoading || errorGroup) {
return {
...row,
loading: isLoading,
error: error?.error,
errorGroup: errorGroup?.error,
loadingGroup: isGroupLoading,
};
}
}
return row ?? null;
};
const rowExpand = (expansions) => {
if (!grid)
return;
const mode = grid.state.columnPivotMode.get();
if (mode) {
const current = grid.state.columnPivotColumnGroupExpansions.get();
const next = { ...current, ...expansions };
source.pivotExpansions = next;
grid.state.columnPivotRowGroupExpansions.set(next);
}
else {
const current = grid.state.rowGroupExpansions.get();
const next = { ...current, ...expansions };
source.expansions = next;
grid.state.rowGroupExpansions.set(next);
}
};
const rowToIndex = (rowId) => {
return flat.rowIdToRowIndex.get(rowId) ?? null;
};
const rowSelect = (params) => {
if (!grid || params.mode === "none")
return;
if (params.mode === "single") {
if (params.deselect) {
grid.state.rowSelectedIds.set(new Set());
}
else {
grid.state.rowSelectedIds.set(new Set([params.startId]));
}
return;
}
const ids = new Set();
if (params.startId === params.endId) {
ids.add(params.startId);
if (params.selectChildren) {
rowAllChildIds(params.startId).forEach((c) => ids.add(c));
}
}
else {
const first = rowToIndex(params.startId);
const last = rowToIndex(params.endId);
if (first == null || last == null)
return;
const start = Math.min(first, last);
const end = Math.max(first, last);
for (let i = start; i <= end; i++) {
const row = rowByIndex(i);
if (!row)
continue;
if (params.selectChildren) {
rowAllChildIds(row.id).forEach((c) => ids.add(c));
}
if (row?.id)
ids.add(row.id);
}
}
if (params.deselect) {
const current = grid.state.rowSelectedIds.get();
const next = current.difference(ids);
grid.state.rowSelectedIds.set(next);
}
else {
const current = grid.state.rowSelectedIds.get();
const next = current.union(ids);
grid.state.rowSelectedIds.set(next);
}
};
const rowSelectAll = (params) => {
if (!grid)
return;
if (params.deselect) {
grid.state.rowSelectedIds.set(new Set());
return;
}
const t = flat;
grid.state.rowSelectedIds.set(new Set(t.rowIdToRow.keys()));
};
const rowSetBotData = () => {
console.error("Directly setting bottom data in the server data model is not supported.");
};
const rowSetTopData = () => {
console.error("Directly setting top data in the server data model is not supported.");
};
const rowSetCenterData = () => {
console.error("Directly setting center data in the server data model is not supported.");
};
// CRUD ops
const rowAdd = () => {
throw new Error(`Server data source does not support adding rows directly. Instead push updates via the pushResponses or pushRequests method`);
};
const rowDelete = () => {
throw new Error(`Server data source does not support deleting rows directly. Instead push updates via the pushResponses or pushRequests method`);
};
const rowUpdate = (updates) => {
const idMap = new Map([...updates.entries()]
.map(([key, data]) => {
if (typeof key === "string")
return [key, data];
const row = rowByIndex(key);
if (!row)
return null;
return [row.id, data];
})
.filter((n) => n != null));
cellUpdateHandler?.(idMap);
if (!cellUpdateOptimistically)
return;
idMap.forEach((data, id) => {
source.updateRow(id, data);
});
source.flatten();
};
const inFilterItems = (c) => {
if (!dataInFilterItemFetcher || !grid)
return [];
return dataInFilterItemFetcher({ column: c, grid, reqTime: Date.now() });
};
const rowAreAllSelected = () => {
return false;
};
const reset = () => {
source.reset();
};
const pushResponses = (responses) => {
source.handleResponses(responses);
};
const pushRequests = (requests) => {
source.handleRequests(requests);
};
const retry = () => {
source.retry();
grid?.state.rowDataStore.rowClearCache();
};
const refresh = (onSuccess, onError) => {
const requests = source.requestsForView();
pushRequests(requests, onSuccess, onError);
};
const requestForGroup = (row) => {
const index = typeof row === "number" ? row : flat.rowIdToRowIndex.get(row.id);
if (index == null)
return null;
return source.requestForGroup(index);
};
const requestForNextSlice = (req) => {
return source.requestForNextSlice(req);
};
return {
init,
rowAdd,
rowById,
rowAllChildIds,
rowByIndex,
rowDelete,
rowExpand,
rowSelect,
rowSelectAll,
rowSetBotData,
rowSetCenterData,
rowSetTopData,
rowToIndex,
rowUpdate,
inFilterItems,
rowAreAllSelected,
isLoading,
loadError,
pushResponses,
pushRequests,
reset,
retry,
refresh,
requestsForView: requestsForView,
requestForGroup,
requestForNextSlice,
get seenRequests() {
return flat.seenRequests;
},
};
}
export function useServerDataSource(p) {
const ds = useRef(null);
const fetcherRef = useRef(p.dataFetcher);
const prevExternal = useRef(p.dataFetchExternals ?? []);
const animRef = useRef(false);
if (!arrayShallow(prevExternal.current, p.dataFetchExternals ?? [])) {
prevExternal.current = p.dataFetchExternals ?? [];
fetcherRef.current = p.dataFetcher;
if (ds.current && !animRef.current) {
animRef.current = true;
queueMicrotask(() => {
ds.current.reset();
animRef.current = false;
});
}
}
if (!ds.current) {
ds.current = makeServerDataSource({
...p,
dataFetcher: (params) => {
return fetcherRef.current(params);
},
});
}
return ds.current;
}
function arrayShallow(left, right) {
if (left.length !== right.length)
return false;
for (let i = 0; i < left.length; i++) {
if (left[i] !== right[i])
return false;
}
return true;
}