@humanspeak/svelte-headless-table 6.0.6 → 6.0.8

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 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
- // Create a shallow copy of `row.cells` to reassign each `cell`'s `row`
239
- // reference.
240
- const cells = row.cells.map((cell) => {
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
- return clonedCell;
248
+ cellForId[clonedCell.id] = clonedCell;
244
249
  });
245
- const cellById = new Map(cells.map((c) => [c.id, c]));
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
- // Include hidden cells in `cellForId` to allow row transformations on
249
- // hidden cells.
250
- columnedRows[rowIdx].cellForId = Object.fromEntries(cellById);
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
- /** Reset all derivation call counters to 0 */
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.
@@ -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
- return getColumnedBodyRows($originalRows, $injectedColumns.map((c) => c.id));
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)
@@ -164,6 +192,7 @@ export const createViewModel = (table, columns, { rowDataId } = {}) => {
164
192
  });
165
193
  const pluginEntries = Object.entries(pluginInstances);
166
194
  const injectedRows = derived(rows, ($rows) => {
195
+ const _t0 = performance.now();
167
196
  derivationCalls.injectedRows++;
168
197
  $rows.forEach((row) => {
169
198
  row.injectState(tableState);
@@ -180,6 +209,7 @@ export const createViewModel = (table, columns, { rowDataId } = {}) => {
180
209
  }
181
210
  });
182
211
  _rows.set($rows);
212
+ derivationTimings.injectedRows += performance.now() - _t0;
183
213
  return $rows;
184
214
  });
185
215
  const derivePageRowsFns = Object.values(pluginInstances)
@@ -194,11 +224,14 @@ export const createViewModel = (table, columns, { rowDataId } = {}) => {
194
224
  // Page rows are a subset of the same object references already processed
195
225
  // by injectedRows — no need to re-inject state or re-apply hooks.
196
226
  const injectedPageRows = derived(pageRows, ($pageRows) => {
227
+ const _t0 = performance.now();
197
228
  derivationCalls.injectedPageRows++;
198
229
  _pageRows.set($pageRows);
230
+ derivationTimings.injectedPageRows += performance.now() - _t0;
199
231
  return $pageRows;
200
232
  });
201
233
  const headerRows = derived(injectedColumns, ($injectedColumns) => {
234
+ const _t0 = performance.now();
202
235
  derivationCalls.headerRows++;
203
236
  const $headerRows = getHeaderRows(columns, $injectedColumns.map((c) => c.id));
204
237
  $headerRows.forEach((row) => {
@@ -216,6 +249,7 @@ export const createViewModel = (table, columns, { rowDataId } = {}) => {
216
249
  }
217
250
  });
218
251
  _headerRows.set($headerRows);
252
+ derivationTimings.headerRows += performance.now() - _t0;
219
253
  return $headerRows;
220
254
  });
221
255
  const _debug = {
@@ -230,13 +264,18 @@ export const createViewModel = (table, columns, { rowDataId } = {}) => {
230
264
  pageRows: derivePageRowsFns.length + 1 // +1 for injected
231
265
  },
232
266
  derivationCalls,
267
+ derivationTimings,
233
268
  resetCounters: () => {
234
269
  Object.keys(derivationCalls).forEach((key) => {
235
270
  derivationCalls[key] = 0;
271
+ derivationTimings[key] = 0;
236
272
  });
237
273
  },
238
274
  getTotalCalls: () => {
239
275
  return Object.values(derivationCalls).reduce((sum, count) => sum + count, 0);
276
+ },
277
+ getTotalMs: () => {
278
+ return Object.values(derivationTimings).reduce((sum, ms) => sum + ms, 0);
240
279
  }
241
280
  };
242
281
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-headless-table",
3
- "version": "6.0.6",
3
+ "version": "6.0.8",
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",
@@ -58,57 +58,58 @@
58
58
  "!dist/**/*.spec.*"
59
59
  ],
60
60
  "dependencies": {
61
- "@humanspeak/memory-cache": "^1.0.5",
61
+ "@humanspeak/memory-cache": "^1.0.6",
62
62
  "@humanspeak/svelte-keyed": "^5.0.1",
63
63
  "@humanspeak/svelte-render": "^5.1.1",
64
64
  "@humanspeak/svelte-subscribe": "^5.0.0"
65
65
  },
66
66
  "devDependencies": {
67
- "@eslint/compat": "^2.0.2",
67
+ "@eslint/compat": "^2.1.0",
68
68
  "@eslint/js": "^10.0.1",
69
- "@faker-js/faker": "^10.3.0",
70
- "@playwright/test": "^1.58.2",
69
+ "@faker-js/faker": "^10.4.0",
70
+ "@playwright/test": "^1.60.0",
71
71
  "@sveltejs/adapter-auto": "^7.0.1",
72
- "@sveltejs/kit": "^2.53.4",
72
+ "@sveltejs/kit": "^2.60.1",
73
73
  "@sveltejs/package": "^2.5.7",
74
- "@sveltejs/vite-plugin-svelte": "^6.2.4",
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
- "@types/node": "^25.3.3",
79
- "@typescript-eslint/eslint-plugin": "^8.56.1",
80
- "@typescript-eslint/parser": "^8.56.1",
81
- "@vitest/coverage-v8": "^4.0.18",
82
- "eslint": "^10.0.2",
78
+ "@types/node": "^25.9.1",
79
+ "@typescript-eslint/eslint-plugin": "^8.59.4",
80
+ "@typescript-eslint/parser": "^8.59.4",
81
+ "@vitest/coverage-v8": "^4.1.7",
82
+ "eslint": "^10.4.0",
83
83
  "eslint-config-prettier": "10.1.8",
84
84
  "eslint-plugin-import": "2.32.0",
85
- "eslint-plugin-svelte": "3.15.0",
85
+ "eslint-plugin-svelte": "3.17.1",
86
86
  "eslint-plugin-unused-imports": "4.4.1",
87
- "globals": "^17.4.0",
87
+ "globals": "^17.6.0",
88
88
  "husky": "^9.1.7",
89
- "prettier": "^3.8.1",
89
+ "prettier": "^3.8.3",
90
90
  "prettier-plugin-organize-imports": "^4.3.0",
91
91
  "prettier-plugin-sort-json": "^4.2.0",
92
- "prettier-plugin-svelte": "^3.5.1",
93
- "prettier-plugin-tailwindcss": "^0.7.2",
94
- "publint": "^0.3.18",
95
- "svelte": "^5.53.7",
96
- "svelte-check": "^4.4.4",
92
+ "prettier-plugin-svelte": "^4.0.1",
93
+ "prettier-plugin-tailwindcss": "^0.8.0",
94
+ "publint": "^0.3.21",
95
+ "svelte": "^5.55.9",
96
+ "svelte-check": "^4.4.8",
97
97
  "tslib": "^2.8.1",
98
- "type-fest": "^5.4.4",
99
- "typescript": "^5.9.3",
100
- "typescript-eslint": "^8.56.1",
101
- "vite": "^7.3.1",
102
- "vitest": "^4.0.18"
98
+ "type-fest": "^5.6.0",
99
+ "typescript": "^6.0.3",
100
+ "typescript-eslint": "^8.59.4",
101
+ "vite": "^8.0.14",
102
+ "vitest": "^4.1.7"
103
103
  },
104
104
  "peerDependencies": {
105
105
  "svelte": "^5.30.0"
106
106
  },
107
107
  "volta": {
108
- "node": "24.14.0"
108
+ "node": "24.15.0"
109
109
  },
110
110
  "scripts": {
111
111
  "build": "vite build && npm run package",
112
+ "cf-typegen": "pnpm --filter docs cf-typegen",
112
113
  "changeset": "npx @changesets/cli",
113
114
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
114
115
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
@@ -119,6 +120,7 @@
119
120
  "lint": "prettier --check . && eslint .",
120
121
  "lint:fix": "npm run format && eslint . --fix",
121
122
  "package": "svelte-kit sync && svelte-package && publint",
123
+ "perf:bench": "node scripts/perf-bench.mjs",
122
124
  "preview": "vite preview",
123
125
  "test": "vitest run --coverage",
124
126
  "test:all": "npm run test && npm run test:e2e",