@humanspeak/svelte-headless-table 6.0.7 → 6.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bodyRows.js +13 -9
- package/dist/createViewModel.d.ts +22 -1
- package/dist/createViewModel.js +64 -11
- package/dist/headerRows.js +3 -1
- package/dist/plugins/addSortBy.js +21 -6
- package/package.json +6 -5
package/dist/bodyRows.js
CHANGED
|
@@ -235,19 +235,23 @@ export const getColumnedBodyRows = (rows, columnIdOrder) => {
|
|
|
235
235
|
if (rows.length === 0 || columnIdOrder.length === 0)
|
|
236
236
|
return rows;
|
|
237
237
|
rows.forEach((row, rowIdx) => {
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
|
|
238
|
+
// Build `cellForId` directly during the clone pass so we don't
|
|
239
|
+
// pay for an intermediate Map + Object.fromEntries detour
|
|
240
|
+
// (which is what the previous shape did — see plan-1A in
|
|
241
|
+
// .notes/performance-optimization-plan.md). The id-keyed object
|
|
242
|
+
// gives us O(1) lookups for `visibleCells` and is the same shape
|
|
243
|
+
// BodyRow.cellForId already expects.
|
|
244
|
+
const cellForId = {};
|
|
245
|
+
row.cells.forEach((cell) => {
|
|
241
246
|
const clonedCell = cell.clone();
|
|
242
247
|
clonedCell.row = columnedRows[rowIdx];
|
|
243
|
-
|
|
248
|
+
cellForId[clonedCell.id] = clonedCell;
|
|
244
249
|
});
|
|
245
|
-
const
|
|
246
|
-
const visibleCells = columnIdOrder.map((cid) => cellById.get(cid)).filter(nonUndefined);
|
|
250
|
+
const visibleCells = columnIdOrder.map((cid) => cellForId[cid]).filter(nonUndefined);
|
|
247
251
|
columnedRows[rowIdx].cells = visibleCells;
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
columnedRows[rowIdx].cellForId =
|
|
252
|
+
// `cellForId` includes hidden cells so row transformations can
|
|
253
|
+
// still reach them.
|
|
254
|
+
columnedRows[rowIdx].cellForId = cellForId;
|
|
251
255
|
});
|
|
252
256
|
return columnedRows;
|
|
253
257
|
};
|
|
@@ -60,10 +60,31 @@ export interface ViewModelDebug {
|
|
|
60
60
|
injectedPageRows: number;
|
|
61
61
|
headerRows: number;
|
|
62
62
|
};
|
|
63
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Per-derivation cumulative wall-clock in milliseconds, accumulated
|
|
65
|
+
* via `performance.now()` deltas inside each `derived(...)` body.
|
|
66
|
+
* Mirrors `derivationCalls` so the perf bench can attribute a
|
|
67
|
+
* scenario's render budget to a specific derivation rather than the
|
|
68
|
+
* aggregated `firstPaintMs`. Reset by `resetCounters()`.
|
|
69
|
+
*/
|
|
70
|
+
derivationTimings: {
|
|
71
|
+
tableAttrs: number;
|
|
72
|
+
tableHeadAttrs: number;
|
|
73
|
+
tableBodyAttrs: number;
|
|
74
|
+
visibleColumns: number;
|
|
75
|
+
columnedRows: number;
|
|
76
|
+
rows: number;
|
|
77
|
+
injectedRows: number;
|
|
78
|
+
pageRows: number;
|
|
79
|
+
injectedPageRows: number;
|
|
80
|
+
headerRows: number;
|
|
81
|
+
};
|
|
82
|
+
/** Reset all derivation call counters and timings to 0 */
|
|
64
83
|
resetCounters: () => void;
|
|
65
84
|
/** Get total derivation calls since last reset */
|
|
66
85
|
getTotalCalls: () => number;
|
|
86
|
+
/** Get total derivation wall-clock (ms) since last reset */
|
|
87
|
+
getTotalMs: () => number;
|
|
67
88
|
}
|
|
68
89
|
/**
|
|
69
90
|
* The view model for a table, containing all reactive stores and state.
|
package/dist/createViewModel.js
CHANGED
|
@@ -30,6 +30,23 @@ export const createViewModel = (table, columns, { rowDataId } = {}) => {
|
|
|
30
30
|
injectedPageRows: 0,
|
|
31
31
|
headerRows: 0
|
|
32
32
|
};
|
|
33
|
+
// Per-derivation cumulative ms, populated alongside derivationCalls.
|
|
34
|
+
// Each `derived(...)` body wraps its work in performance.now() pairs
|
|
35
|
+
// so the perf bench can attribute a scenario's render budget to a
|
|
36
|
+
// specific derivation. `rows` / `pageRows` stay at 0 — they're
|
|
37
|
+
// plugin-pipeline pass-throughs that don't run a body of their own.
|
|
38
|
+
const derivationTimings = {
|
|
39
|
+
tableAttrs: 0,
|
|
40
|
+
tableHeadAttrs: 0,
|
|
41
|
+
tableBodyAttrs: 0,
|
|
42
|
+
visibleColumns: 0,
|
|
43
|
+
columnedRows: 0,
|
|
44
|
+
rows: 0,
|
|
45
|
+
injectedRows: 0,
|
|
46
|
+
pageRows: 0,
|
|
47
|
+
injectedPageRows: 0,
|
|
48
|
+
headerRows: 0
|
|
49
|
+
};
|
|
33
50
|
const $flatColumns = getFlatColumns(columns);
|
|
34
51
|
const flatColumns = readable($flatColumns);
|
|
35
52
|
const originalRows = derived([data, flatColumns], ([$data, $flatColumns]) => {
|
|
@@ -103,9 +120,11 @@ export const createViewModel = (table, columns, { rowDataId } = {}) => {
|
|
|
103
120
|
tableAttrs = fn(tableAttrs);
|
|
104
121
|
});
|
|
105
122
|
const finalizedTableAttrs = derived(tableAttrs, ($tableAttrs) => {
|
|
123
|
+
const _t0 = performance.now();
|
|
106
124
|
derivationCalls.tableAttrs++;
|
|
107
125
|
const $finalizedAttrs = finalizeAttributes($tableAttrs);
|
|
108
126
|
_tableAttrs.set($finalizedAttrs);
|
|
127
|
+
derivationTimings.tableAttrs += performance.now() - _t0;
|
|
109
128
|
return $finalizedAttrs;
|
|
110
129
|
});
|
|
111
130
|
const deriveTableHeadAttrsFns = Object.values(pluginInstances)
|
|
@@ -116,9 +135,11 @@ export const createViewModel = (table, columns, { rowDataId } = {}) => {
|
|
|
116
135
|
tableHeadAttrs = fn(tableHeadAttrs);
|
|
117
136
|
});
|
|
118
137
|
const finalizedTableHeadAttrs = derived(tableHeadAttrs, ($tableHeadAttrs) => {
|
|
138
|
+
const _t0 = performance.now();
|
|
119
139
|
derivationCalls.tableHeadAttrs++;
|
|
120
140
|
const $finalizedAttrs = finalizeAttributes($tableHeadAttrs);
|
|
121
141
|
_tableHeadAttrs.set($finalizedAttrs);
|
|
142
|
+
derivationTimings.tableHeadAttrs += performance.now() - _t0;
|
|
122
143
|
return $finalizedAttrs;
|
|
123
144
|
});
|
|
124
145
|
const deriveTableBodyAttrsFns = Object.values(pluginInstances)
|
|
@@ -131,9 +152,11 @@ export const createViewModel = (table, columns, { rowDataId } = {}) => {
|
|
|
131
152
|
tableBodyAttrs = fn(tableBodyAttrs);
|
|
132
153
|
});
|
|
133
154
|
const finalizedTableBodyAttrs = derived(tableBodyAttrs, ($tableBodyAttrs) => {
|
|
155
|
+
const _t0 = performance.now();
|
|
134
156
|
derivationCalls.tableBodyAttrs++;
|
|
135
157
|
const $finalizedAttrs = finalizeAttributes($tableBodyAttrs);
|
|
136
158
|
_tableBodyAttrs.set($finalizedAttrs);
|
|
159
|
+
derivationTimings.tableBodyAttrs += performance.now() - _t0;
|
|
137
160
|
return $finalizedAttrs;
|
|
138
161
|
});
|
|
139
162
|
const deriveFlatColumnsFns = Object.values(pluginInstances)
|
|
@@ -146,13 +169,18 @@ export const createViewModel = (table, columns, { rowDataId } = {}) => {
|
|
|
146
169
|
visibleColumns = fn(visibleColumns);
|
|
147
170
|
});
|
|
148
171
|
const injectedColumns = derived(visibleColumns, ($visibleColumns) => {
|
|
172
|
+
const _t0 = performance.now();
|
|
149
173
|
derivationCalls.visibleColumns++;
|
|
150
174
|
_visibleColumns.set($visibleColumns);
|
|
175
|
+
derivationTimings.visibleColumns += performance.now() - _t0;
|
|
151
176
|
return $visibleColumns;
|
|
152
177
|
});
|
|
153
178
|
const columnedRows = derived([originalRows, injectedColumns], ([$originalRows, $injectedColumns]) => {
|
|
179
|
+
const _t0 = performance.now();
|
|
154
180
|
derivationCalls.columnedRows++;
|
|
155
|
-
|
|
181
|
+
const result = getColumnedBodyRows($originalRows, $injectedColumns.map((c) => c.id));
|
|
182
|
+
derivationTimings.columnedRows += performance.now() - _t0;
|
|
183
|
+
return result;
|
|
156
184
|
});
|
|
157
185
|
const deriveRowsFns = Object.values(pluginInstances)
|
|
158
186
|
.map((pluginInstance) => pluginInstance.deriveRows)
|
|
@@ -163,23 +191,39 @@ export const createViewModel = (table, columns, { rowDataId } = {}) => {
|
|
|
163
191
|
rows = fn(rows);
|
|
164
192
|
});
|
|
165
193
|
const pluginEntries = Object.entries(pluginInstances);
|
|
194
|
+
const trHookEntries = [];
|
|
195
|
+
const tdHookEntries = [];
|
|
196
|
+
for (const [name, instance] of pluginEntries) {
|
|
197
|
+
const trHook = instance.hooks?.['tbody.tr'];
|
|
198
|
+
if (trHook !== undefined)
|
|
199
|
+
trHookEntries.push([name, trHook]);
|
|
200
|
+
const tdHook = instance.hooks?.['tbody.tr.td'];
|
|
201
|
+
if (tdHook !== undefined)
|
|
202
|
+
tdHookEntries.push([name, tdHook]);
|
|
203
|
+
}
|
|
204
|
+
// Hoisted out of the per-row loop so we don't allocate a fresh
|
|
205
|
+
// closure on every iteration. For rows-10k that's 10,000 fewer
|
|
206
|
+
// closure allocations per derivation pass.
|
|
207
|
+
const injectCellState = (cell) => cell.injectState(tableState);
|
|
166
208
|
const injectedRows = derived(rows, ($rows) => {
|
|
209
|
+
const _t0 = performance.now();
|
|
167
210
|
derivationCalls.injectedRows++;
|
|
168
211
|
$rows.forEach((row) => {
|
|
169
212
|
row.injectState(tableState);
|
|
170
|
-
row.cells.forEach(
|
|
171
|
-
for (const [pluginName,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
213
|
+
row.cells.forEach(injectCellState);
|
|
214
|
+
for (const [pluginName, trHook] of trHookEntries) {
|
|
215
|
+
row.applyHook(pluginName, trHook(row));
|
|
216
|
+
}
|
|
217
|
+
if (tdHookEntries.length > 0) {
|
|
218
|
+
row.cells.forEach((cell) => {
|
|
219
|
+
for (const [pluginName, tdHook] of tdHookEntries) {
|
|
220
|
+
cell.applyHook(pluginName, tdHook(cell));
|
|
221
|
+
}
|
|
222
|
+
});
|
|
180
223
|
}
|
|
181
224
|
});
|
|
182
225
|
_rows.set($rows);
|
|
226
|
+
derivationTimings.injectedRows += performance.now() - _t0;
|
|
183
227
|
return $rows;
|
|
184
228
|
});
|
|
185
229
|
const derivePageRowsFns = Object.values(pluginInstances)
|
|
@@ -194,11 +238,14 @@ export const createViewModel = (table, columns, { rowDataId } = {}) => {
|
|
|
194
238
|
// Page rows are a subset of the same object references already processed
|
|
195
239
|
// by injectedRows — no need to re-inject state or re-apply hooks.
|
|
196
240
|
const injectedPageRows = derived(pageRows, ($pageRows) => {
|
|
241
|
+
const _t0 = performance.now();
|
|
197
242
|
derivationCalls.injectedPageRows++;
|
|
198
243
|
_pageRows.set($pageRows);
|
|
244
|
+
derivationTimings.injectedPageRows += performance.now() - _t0;
|
|
199
245
|
return $pageRows;
|
|
200
246
|
});
|
|
201
247
|
const headerRows = derived(injectedColumns, ($injectedColumns) => {
|
|
248
|
+
const _t0 = performance.now();
|
|
202
249
|
derivationCalls.headerRows++;
|
|
203
250
|
const $headerRows = getHeaderRows(columns, $injectedColumns.map((c) => c.id));
|
|
204
251
|
$headerRows.forEach((row) => {
|
|
@@ -216,6 +263,7 @@ export const createViewModel = (table, columns, { rowDataId } = {}) => {
|
|
|
216
263
|
}
|
|
217
264
|
});
|
|
218
265
|
_headerRows.set($headerRows);
|
|
266
|
+
derivationTimings.headerRows += performance.now() - _t0;
|
|
219
267
|
return $headerRows;
|
|
220
268
|
});
|
|
221
269
|
const _debug = {
|
|
@@ -230,13 +278,18 @@ export const createViewModel = (table, columns, { rowDataId } = {}) => {
|
|
|
230
278
|
pageRows: derivePageRowsFns.length + 1 // +1 for injected
|
|
231
279
|
},
|
|
232
280
|
derivationCalls,
|
|
281
|
+
derivationTimings,
|
|
233
282
|
resetCounters: () => {
|
|
234
283
|
Object.keys(derivationCalls).forEach((key) => {
|
|
235
284
|
derivationCalls[key] = 0;
|
|
285
|
+
derivationTimings[key] = 0;
|
|
236
286
|
});
|
|
237
287
|
},
|
|
238
288
|
getTotalCalls: () => {
|
|
239
289
|
return Object.values(derivationCalls).reduce((sum, count) => sum + count, 0);
|
|
290
|
+
},
|
|
291
|
+
getTotalMs: () => {
|
|
292
|
+
return Object.values(derivationTimings).reduce((sum, ms) => sum + ms, 0);
|
|
240
293
|
}
|
|
241
294
|
};
|
|
242
295
|
return {
|
package/dist/headerRows.js
CHANGED
|
@@ -215,7 +215,9 @@ export const getMergedRow = (cells) => {
|
|
|
215
215
|
}
|
|
216
216
|
const mergedCells = [];
|
|
217
217
|
let startIdx = 0;
|
|
218
|
-
|
|
218
|
+
// endIdx is initialized inside the merge branch (line ~293) before
|
|
219
|
+
// it's read; declared here only so the inner-while loop can see it.
|
|
220
|
+
let endIdx;
|
|
219
221
|
while (startIdx < cells.length) {
|
|
220
222
|
const cell = cells[startIdx].clone();
|
|
221
223
|
if (!cell.isGroup()) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MemoryCache } from '@humanspeak/memory-cache';
|
|
1
2
|
import { derived, writable } from 'svelte/store';
|
|
2
3
|
import { compare } from '../utils/compare.js';
|
|
3
4
|
import { isShiftClick } from '../utils/event.js';
|
|
@@ -169,6 +170,16 @@ export const addSortBy = ({ initialSortKeys = [], disableMultiSort = false, isMu
|
|
|
169
170
|
});
|
|
170
171
|
};
|
|
171
172
|
const pluginState = { sortKeys, preSortedRows };
|
|
173
|
+
// The `tbody.tr.td` hook output only depends on `cell.id` (the
|
|
174
|
+
// column ID — closed-over) and the reactive `sortKeys` store.
|
|
175
|
+
// Two body cells in the same column produce identical Readables,
|
|
176
|
+
// so we can share one Readable per column ID across every row.
|
|
177
|
+
// For rows-10k × 8 cols that collapses 80,000 Readable allocations
|
|
178
|
+
// per cold mount into 8. LRU eviction means we stay bounded even
|
|
179
|
+
// if column IDs churn over the view-model's lifetime.
|
|
180
|
+
const tdPropsCache = new MemoryCache({
|
|
181
|
+
maxSize: 256
|
|
182
|
+
});
|
|
172
183
|
return {
|
|
173
184
|
pluginState,
|
|
174
185
|
deriveRows,
|
|
@@ -204,12 +215,16 @@ export const addSortBy = ({ initialSortKeys = [], disableMultiSort = false, isMu
|
|
|
204
215
|
return { props };
|
|
205
216
|
},
|
|
206
217
|
'tbody.tr.td': (cell) => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
218
|
+
let props = tdPropsCache.get(cell.id);
|
|
219
|
+
if (props === undefined) {
|
|
220
|
+
props = derived(sortKeys, ($sortKeys) => {
|
|
221
|
+
const key = $sortKeys.find((k) => k.id === cell.id);
|
|
222
|
+
return {
|
|
223
|
+
order: key?.order
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
tdPropsCache.set(cell.id, props);
|
|
227
|
+
}
|
|
213
228
|
return { props };
|
|
214
229
|
}
|
|
215
230
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-headless-table",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.9",
|
|
4
4
|
"description": "A powerful, headless table library for Svelte that provides complete control over table UI while handling complex data operations like sorting, filtering, pagination, grouping, and row expansion. Build custom, accessible data tables with zero styling opinions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -69,15 +69,15 @@
|
|
|
69
69
|
"@faker-js/faker": "^10.4.0",
|
|
70
70
|
"@playwright/test": "^1.60.0",
|
|
71
71
|
"@sveltejs/adapter-auto": "^7.0.1",
|
|
72
|
-
"@sveltejs/kit": "^2.
|
|
72
|
+
"@sveltejs/kit": "^2.61.1",
|
|
73
73
|
"@sveltejs/package": "^2.5.7",
|
|
74
74
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
75
75
|
"@testing-library/jest-dom": "^6.9.1",
|
|
76
76
|
"@testing-library/svelte": "^5.3.1",
|
|
77
77
|
"@types/eslint": "9.6.1",
|
|
78
78
|
"@types/node": "^25.9.1",
|
|
79
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
80
|
-
"@typescript-eslint/parser": "^8.
|
|
79
|
+
"@typescript-eslint/eslint-plugin": "^8.60.0",
|
|
80
|
+
"@typescript-eslint/parser": "^8.60.0",
|
|
81
81
|
"@vitest/coverage-v8": "^4.1.7",
|
|
82
82
|
"eslint": "^10.4.0",
|
|
83
83
|
"eslint-config-prettier": "10.1.8",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"tslib": "^2.8.1",
|
|
98
98
|
"type-fest": "^5.6.0",
|
|
99
99
|
"typescript": "^6.0.3",
|
|
100
|
-
"typescript-eslint": "^8.
|
|
100
|
+
"typescript-eslint": "^8.60.0",
|
|
101
101
|
"vite": "^8.0.14",
|
|
102
102
|
"vitest": "^4.1.7"
|
|
103
103
|
},
|
|
@@ -120,6 +120,7 @@
|
|
|
120
120
|
"lint": "prettier --check . && eslint .",
|
|
121
121
|
"lint:fix": "npm run format && eslint . --fix",
|
|
122
122
|
"package": "svelte-kit sync && svelte-package && publint",
|
|
123
|
+
"perf:bench": "node scripts/perf-bench.mjs",
|
|
123
124
|
"preview": "vite preview",
|
|
124
125
|
"test": "vitest run --coverage",
|
|
125
126
|
"test:all": "npm run test && npm run test:e2e",
|