Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions apps/marketing/src/constants/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface ChangelogEntry {
}

export const v3_0_0: ChangelogEntry = {
version: "3.0.0",
version: "3.0.1",
date: "2026-03-29",
title: "Framework Adapters & Column Virtualization",
titleLink: "/migrations/v3",
Expand All @@ -26,8 +26,7 @@ export const v3_0_0: ChangelogEntry = {
},
{
type: "breaking",
description:
"simple-table-core is now plain JS — no framework components exported",
description: "simple-table-core is now plain JS — no framework components exported",
},
{
type: "feature",
Expand Down
123 changes: 49 additions & 74 deletions apps/marketing/src/examples/infrastructure/useServerMetricsUpdates.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { useEffect, RefObject } from "react";
import type { TableAPI, Row } from "@simple-table/react";
import type { TableAPI, Row, CellValue } from "@simple-table/react";

/**
* Drives “live server metrics” without O(visible rows) concurrent `setTimeout` chains.
* A single interval samples a few visible rows per tick and applies one metric each
* (CPU + sparkline uses two `updateData` calls, at most once per tick).
*/
const TICK_MS = 800;
/** Max rows to touch per tick (each ≤ 2 `updateData` when CPU+history runs). */
const ROWS_PER_TICK = 4;
/** Slightly slower than frame budget so live updates + scroll rarely pile on one rAF. */
const TICK_MS = 50;
const ROWS_PER_TICK = 3;

type MetricSlot = 0 | 1 | 2 | 3 | 4 | 5 | 6;

Expand All @@ -23,107 +18,84 @@ function pickRandomSubset<T>(arr: T[], n: number): T[] {
return copy.slice(0, Math.min(n, copy.length));
}

function applyOneMetricUpdate(api: TableAPI, server: Row, actualRowIndex: number, slot: MetricSlot) {
function applyRowPatch(api: TableAPI, rowIndex: number, patch: Partial<Row>) {
for (const accessor of Object.keys(patch)) {
const newValue = patch[accessor];
if (newValue === undefined) continue;
api.updateData({
accessor,
rowIndex,
newValue: newValue as CellValue,
});
}
}

function computeMetricPatch(row: Row, slot: MetricSlot): Partial<Row> | null {
switch (slot) {
case 0: {
const currentCpu = server.cpuUsage as number;
if (typeof currentCpu !== "number") return;
const currentCpu = row.cpuUsage as number;
if (typeof currentCpu !== "number") return null;
const cpuChange = (Math.random() - 0.5) * 8;
const newCpu = Math.min(100, Math.max(0, currentCpu + cpuChange));
const newCpuRounded = Math.round(newCpu * 10) / 10;
api.updateData({
accessor: "cpuUsage",
rowIndex: actualRowIndex,
newValue: newCpuRounded,
});
const currentHistory = server.cpuHistory as number[];
const currentHistory = row.cpuHistory as number[];
if (Array.isArray(currentHistory) && currentHistory.length > 0) {
const updatedHistory = [...currentHistory.slice(1), newCpuRounded];
api.updateData({
accessor: "cpuHistory",
rowIndex: actualRowIndex,
newValue: updatedHistory,
});
return { cpuUsage: newCpuRounded, cpuHistory: updatedHistory };
}
break;
return { cpuUsage: newCpuRounded };
}
case 1: {
const currentMemory = server.memoryUsage as number;
if (typeof currentMemory !== "number") return;
const currentMemory = row.memoryUsage as number;
if (typeof currentMemory !== "number") return null;
const memoryChange = (Math.random() - 0.5) * 5;
const newMemory = Math.min(100, Math.max(0, currentMemory + memoryChange));
api.updateData({
accessor: "memoryUsage",
rowIndex: actualRowIndex,
newValue: Math.round(newMemory * 10) / 10,
});
break;
return { memoryUsage: Math.round(newMemory * 10) / 10 };
}
case 2: {
const currentNetIn = server.networkIn as number;
if (typeof currentNetIn !== "number") return;
const currentNetIn = row.networkIn as number;
if (typeof currentNetIn !== "number") return null;
const netChange = (Math.random() - 0.5) * 100;
const newNetIn = Math.max(0, currentNetIn + netChange);
api.updateData({
accessor: "networkIn",
rowIndex: actualRowIndex,
newValue: Math.round(newNetIn * 100) / 100,
});
break;
return { networkIn: Math.round(newNetIn * 100) / 100 };
}
case 3: {
const currentNetOut = server.networkOut as number;
if (typeof currentNetOut !== "number") return;
const currentNetOut = row.networkOut as number;
if (typeof currentNetOut !== "number") return null;
const netChange = (Math.random() - 0.5) * 60;
const newNetOut = Math.max(0, currentNetOut + netChange);
api.updateData({
accessor: "networkOut",
rowIndex: actualRowIndex,
newValue: Math.round(newNetOut * 100) / 100,
});
break;
return { networkOut: Math.round(newNetOut * 100) / 100 };
}
case 4: {
const currentResponseTime = server.responseTime as number;
if (typeof currentResponseTime !== "number") return;
const currentResponseTime = row.responseTime as number;
if (typeof currentResponseTime !== "number") return null;
const responseChange = (Math.random() - 0.5) * 100;
const newResponseTime = Math.max(10, currentResponseTime + responseChange);
api.updateData({
accessor: "responseTime",
rowIndex: actualRowIndex,
newValue: Math.round(newResponseTime * 10) / 10,
});
break;
return { responseTime: Math.round(newResponseTime * 10) / 10 };
}
case 5: {
const currentConnections = server.activeConnections as number;
if (typeof currentConnections !== "number") return;
const currentConnections = row.activeConnections as number;
if (typeof currentConnections !== "number") return null;
const connectionChange = Math.floor((Math.random() - 0.5) * 500);
const newConnections = Math.max(0, currentConnections + connectionChange);
api.updateData({
accessor: "activeConnections",
rowIndex: actualRowIndex,
newValue: newConnections,
});
break;
return { activeConnections: newConnections };
}
case 6: {
const currentRequests = server.requestsPerSec as number;
if (typeof currentRequests !== "number") return;
const currentRequests = row.requestsPerSec as number;
if (typeof currentRequests !== "number") return null;
const requestChange = Math.floor((Math.random() - 0.5) * 2000);
const newRequests = Math.max(0, currentRequests + requestChange);
api.updateData({
accessor: "requestsPerSec",
rowIndex: actualRowIndex,
newValue: newRequests,
});
break;
return { requestsPerSec: newRequests };
}
default:
break;
return null;
}
}

/**
* Drives “live server metrics” without O(visible rows) concurrent `setTimeout` chains.
* A single interval samples a few visible rows per tick and applies one metric patch each.
*/
export function useServerMetricsUpdates(tableRef: RefObject<TableAPI>, data: Row[]) {
useEffect(() => {
let isActive = true;
Expand Down Expand Up @@ -156,7 +128,10 @@ export function useServerMetricsUpdates(tableRef: RefObject<TableAPI>, data: Row
usedCpuSparkline = true;
}

applyOneMetricUpdate(api, data[idx]!, idx, slot);
const patch = computeMetricPatch(vr.row, slot);
if (patch) {
applyRowPatch(api, idx, patch);
}
}
};

Expand Down
2 changes: 1 addition & 1 deletion packages/angular/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@simple-table/angular",
"version": "3.0.0",
"version": "3.0.1",
"main": "dist/cjs/index.js",
"module": "dist/index.es.js",
"types": "dist/types/angular/src/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "simple-table-core",
"version": "3.0.0",
"version": "3.0.1",
"main": "dist/cjs/index.js",
"module": "dist/index.es.js",
"types": "dist/src/index.d.ts",
Expand Down
14 changes: 10 additions & 4 deletions packages/core/src/core/SimpleTableVanilla.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,6 @@ export class SimpleTableVanilla {
infiniteScrollThreshold: 200,
});

this.scrollManager.subscribe(() => {
this.render("scrollManager");
});

this.sectionScrollController = new SectionScrollController({
onMainSectionScrollLeft: (scrollLeft) => {
const refs = this.domManager.getRefs();
Expand Down Expand Up @@ -344,6 +340,16 @@ export class SimpleTableVanilla {
}

this.setupEventListeners();

// DimensionManager defers its first subscriber notification to the next frame
// (ResizeObserver + rAF). Prime row caches only (no DOM) so imperative callers
// (e.g. getVisibleRows right after mount) do not fall back to the full flattened list.
if (this.dimensionManager) {
this.renderOrchestrator.primeLastProcessedResult(
this.getRenderContext(),
this.getRenderState(),
);
}
}

private setupEventListeners(): void {
Expand Down
43 changes: 37 additions & 6 deletions packages/core/src/core/api/TableAPIImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SortColumn, { SortDirection } from "../../types/SortColumn";
import { FilterCondition, TableFilterState } from "../../types/FilterTypes";
import { CustomTheme } from "../../types/CustomTheme";
import UpdateDataProps from "../../types/UpdateCellProps";
import type CellValue from "../../types/CellValue";
import { SetHeaderRenameProps, ExportToCSVProps } from "../../types/TableAPI";
import RowState from "../../types/RowState";
import Cell from "../../types/Cell";
Expand Down Expand Up @@ -76,6 +77,40 @@ export class TableAPIImpl {
});
};

/** Coalesce many `updateData` calls in one turn (e.g. live metrics) into one DOM pass. */
const pendingUpdateDataByKey = new Map<string, CellValue>();
let updateDataFlushScheduled = false;

const flushPendingUpdateData = () => {
updateDataFlushScheduled = false;
if (pendingUpdateDataByKey.size === 0) return;

let needsFullRender = false;
pendingUpdateDataByKey.forEach((_value, key) => {
if (!context.cellRegistry?.get(key)) {
needsFullRender = true;
}
});

if (needsFullRender) {
pendingUpdateDataByKey.clear();
context.onRender();
return;
}

pendingUpdateDataByKey.forEach((value, key) => {
const entry = context.cellRegistry!.get(key)!;
entry.updateContent(value);
});
pendingUpdateDataByKey.clear();
};

const scheduleUpdateDataFlush = () => {
if (updateDataFlushScheduled) return;
updateDataFlushScheduled = true;
queueMicrotask(flushPendingUpdateData);
};

return {
updateData: (props: UpdateDataProps) => {
const { rowIndex, accessor, newValue } = props;
Expand All @@ -96,12 +131,8 @@ export class TableAPIImpl {
]
: [rowIndex];
const key = `${rowIdArray.join("-")}-${accessor}`;
const entry = context.cellRegistry?.get(key);
if (entry) {
entry.updateContent(newValue);
} else {
context.onRender();
}
pendingUpdateDataByKey.set(key, newValue);
scheduleUpdateDataFlush();
}
},

Expand Down
Loading
Loading