@humanspeak/svelte-headless-table 6.0.1 → 6.0.2

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.
@@ -8,6 +8,8 @@ export interface ExpandedRowsConfig<Item> {
8
8
  export interface ExpandedRowsState<Item> {
9
9
  expandedIds: RecordSetStore<string>;
10
10
  getRowState: (row: BodyRow<Item>) => ExpandedRowsRowState;
11
+ /** Cleans up internal subscriptions and clears the row state cache. Call when destroying the table. */
12
+ invalidate: () => void;
11
13
  }
12
14
  export interface ExpandedRowsRowState {
13
15
  isExpanded: Writable<boolean>;
@@ -1,6 +1,8 @@
1
+ import { MemoryCache } from '@humanspeak/memory-cache';
1
2
  import { keyed } from '@humanspeak/svelte-keyed';
2
3
  import { derived, readable } from 'svelte/store';
3
4
  import { recordSetStore } from '../utils/store.js';
5
+ import { DEFAULT_ROW_STATE_CACHE_CONFIG } from './cacheConfig.js';
4
6
  const withExpandedRows = (row, expandedIds) => {
5
7
  if (row.subRows === undefined) {
6
8
  return [row];
@@ -13,7 +15,14 @@ const withExpandedRows = (row, expandedIds) => {
13
15
  };
14
16
  export const addExpandedRows = ({ initialExpandedIds = {} } = {}) => () => {
15
17
  const expandedIds = recordSetStore(initialExpandedIds);
18
+ // LRU cache for memoized row state with automatic eviction.
19
+ // Prevents unbounded memory growth when row identities change.
20
+ const rowStateCache = new MemoryCache(DEFAULT_ROW_STATE_CACHE_CONFIG);
16
21
  const getRowState = (row) => {
22
+ const cached = rowStateCache.get(row.id);
23
+ if (cached !== undefined) {
24
+ return cached;
25
+ }
17
26
  const isExpanded = keyed(expandedIds, row.id);
18
27
  const canExpand = readable((row.subRows?.length ?? 0) > 0);
19
28
  const subRowExpandedIds = derived(expandedIds, ($expandedIds) => {
@@ -30,13 +39,26 @@ export const addExpandedRows = ({ initialExpandedIds = {} } = {}) => () => {
30
39
  const expandableSubRows = row.subRows.filter((subRow) => subRow.subRows !== undefined);
31
40
  return $subRowExpandedIds.length === expandableSubRows.length;
32
41
  });
33
- return {
42
+ const state = {
34
43
  isExpanded,
35
44
  canExpand,
36
45
  isAllSubRowsExpanded
37
46
  };
47
+ rowStateCache.set(row.id, state);
48
+ return state;
38
49
  };
39
- const pluginState = { expandedIds, getRowState };
50
+ // Clear cache when expandedIds store is cleared (data reset scenario)
51
+ const unsubscribeExpandedIds = expandedIds.subscribe(($expandedIds) => {
52
+ if (Object.keys($expandedIds).length === 0) {
53
+ rowStateCache.clear();
54
+ }
55
+ });
56
+ // Cleanup function to prevent subscription leaks when table is destroyed
57
+ const invalidate = () => {
58
+ unsubscribeExpandedIds();
59
+ rowStateCache.clear();
60
+ };
61
+ const pluginState = { expandedIds, getRowState, invalidate };
40
62
  const deriveRows = (rows) => {
41
63
  return derived([rows, expandedIds], ([$rows, $expandedIds]) => {
42
64
  return $rows.flatMap((row) => {
@@ -13,6 +13,8 @@ export interface SelectedRowsState<Item> {
13
13
  allPageRowsSelected: Writable<boolean>;
14
14
  somePageRowsSelected: Readable<boolean>;
15
15
  getRowState: (row: BodyRow<Item>) => SelectedRowsRowState;
16
+ /** Cleans up internal subscriptions and clears the row state cache. Call when destroying the table. */
17
+ invalidate: () => void;
16
18
  }
17
19
  export interface SelectedRowsRowState {
18
20
  isSelected: Writable<boolean>;
@@ -1,6 +1,8 @@
1
+ import { MemoryCache } from '@humanspeak/memory-cache';
1
2
  import { derived, get } from 'svelte/store';
2
3
  import { nonNull } from '../utils/filter.js';
3
4
  import { recordSetStore } from '../utils/store.js';
5
+ import { DEFAULT_ROW_STATE_CACHE_CONFIG } from './cacheConfig.js';
4
6
  const isAllSubRowsSelectedForRow = (row, $selectedDataIds, linkDataSubRows) => {
5
7
  if (row.isData()) {
6
8
  if (!linkDataSubRows || row.subRows === undefined) {
@@ -69,7 +71,14 @@ const getRowIsSelectedStore = (row, selectedDataIds, linkDataSubRows) => {
69
71
  };
70
72
  export const addSelectedRows = ({ initialSelectedDataIds = {}, linkDataSubRows = true } = {}) => ({ tableState }) => {
71
73
  const selectedDataIds = recordSetStore(initialSelectedDataIds);
74
+ // LRU cache for memoized row state with automatic eviction.
75
+ // Prevents unbounded memory growth when row identities change.
76
+ const rowStateCache = new MemoryCache(DEFAULT_ROW_STATE_CACHE_CONFIG);
72
77
  const getRowState = (row) => {
78
+ const cached = rowStateCache.get(row.id);
79
+ if (cached !== undefined) {
80
+ return cached;
81
+ }
73
82
  const isSelected = getRowIsSelectedStore(row, selectedDataIds, linkDataSubRows);
74
83
  const isSomeSubRowsSelected = derived([isSelected, selectedDataIds], ([$isSelected, $selectedDataIds]) => {
75
84
  if ($isSelected)
@@ -79,11 +88,24 @@ export const addSelectedRows = ({ initialSelectedDataIds = {}, linkDataSubRows =
79
88
  const isAllSubRowsSelected = derived(selectedDataIds, ($selectedDataIds) => {
80
89
  return isAllSubRowsSelectedForRow(row, $selectedDataIds, linkDataSubRows);
81
90
  });
82
- return {
91
+ const state = {
83
92
  isSelected,
84
93
  isSomeSubRowsSelected,
85
94
  isAllSubRowsSelected
86
95
  };
96
+ rowStateCache.set(row.id, state);
97
+ return state;
98
+ };
99
+ // Clear cache when selectedDataIds store is cleared (data reset scenario)
100
+ const unsubscribeSelectedDataIds = selectedDataIds.subscribe(($selectedDataIds) => {
101
+ if (Object.keys($selectedDataIds).length === 0) {
102
+ rowStateCache.clear();
103
+ }
104
+ });
105
+ // Cleanup function to prevent subscription leaks when table is destroyed
106
+ const invalidate = () => {
107
+ unsubscribeSelectedDataIds();
108
+ rowStateCache.clear();
87
109
  };
88
110
  // all rows
89
111
  const _allRowsSelected = derived([tableState.rows, selectedDataIds], ([$rows, $selectedDataIds]) => {
@@ -165,7 +187,8 @@ export const addSelectedRows = ({ initialSelectedDataIds = {}, linkDataSubRows =
165
187
  allRowsSelected,
166
188
  someRowsSelected,
167
189
  allPageRowsSelected,
168
- somePageRowsSelected
190
+ somePageRowsSelected,
191
+ invalidate
169
192
  };
170
193
  return {
171
194
  pluginState,
@@ -0,0 +1,7 @@
1
+ import type { CacheOptions } from '@humanspeak/memory-cache';
2
+ /**
3
+ * Default configuration for row state LRU caches used by plugins.
4
+ * Provides automatic eviction to prevent unbounded memory growth
5
+ * when row identities change.
6
+ */
7
+ export declare const DEFAULT_ROW_STATE_CACHE_CONFIG: CacheOptions;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Default configuration for row state LRU caches used by plugins.
3
+ * Provides automatic eviction to prevent unbounded memory growth
4
+ * when row identities change.
5
+ */
6
+ export const DEFAULT_ROW_STATE_CACHE_CONFIG = {
7
+ /** Maximum number of row state entries before LRU eviction */
8
+ maxSize: 1000,
9
+ /** Time-to-live in milliseconds (5 minutes) */
10
+ ttl: 5 * 60 * 1000
11
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-headless-table",
3
- "version": "6.0.1",
3
+ "version": "6.0.2",
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,54 +58,55 @@
58
58
  "!dist/**/*.spec.*"
59
59
  ],
60
60
  "dependencies": {
61
+ "@humanspeak/memory-cache": "^1.0.1",
61
62
  "@humanspeak/svelte-keyed": "^5.0.1",
62
63
  "@humanspeak/svelte-render": "^5.1.1",
63
64
  "@humanspeak/svelte-subscribe": "^5.0.0"
64
65
  },
65
66
  "devDependencies": {
66
- "@eslint/compat": "^1.4.0",
67
- "@eslint/js": "^9.37.0",
68
- "@faker-js/faker": "^10.0.0",
69
- "@playwright/test": "^1.56.0",
70
- "@sveltejs/adapter-auto": "^6.1.1",
71
- "@sveltejs/kit": "^2.46.4",
72
- "@sveltejs/package": "^2.5.4",
67
+ "@eslint/compat": "^2.0.0",
68
+ "@eslint/js": "^9.39.2",
69
+ "@faker-js/faker": "^10.1.0",
70
+ "@playwright/test": "^1.57.0",
71
+ "@sveltejs/adapter-auto": "^7.0.0",
72
+ "@sveltejs/kit": "^2.49.2",
73
+ "@sveltejs/package": "^2.5.7",
73
74
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
74
75
  "@testing-library/jest-dom": "^6.9.1",
75
- "@testing-library/svelte": "^5.2.8",
76
+ "@testing-library/svelte": "^5.3.1",
76
77
  "@types/eslint": "9.6.1",
77
- "@types/node": "^24.7.1",
78
- "@typescript-eslint/eslint-plugin": "^8.46.0",
79
- "@typescript-eslint/parser": "^8.46.0",
80
- "@vitest/coverage-v8": "^3.2.4",
78
+ "@types/node": "^25.0.3",
79
+ "@typescript-eslint/eslint-plugin": "^8.51.0",
80
+ "@typescript-eslint/parser": "^8.51.0",
81
+ "@vitest/coverage-v8": "^4.0.16",
81
82
  "concurrently": "^9.2.1",
82
- "eslint": "^9.37.0",
83
+ "eslint": "^9.39.2",
83
84
  "eslint-config-prettier": "10.1.8",
84
85
  "eslint-plugin-import": "2.32.0",
85
- "eslint-plugin-svelte": "3.12.4",
86
- "eslint-plugin-unused-imports": "4.2.0",
87
- "globals": "^16.4.0",
86
+ "eslint-plugin-svelte": "3.13.1",
87
+ "eslint-plugin-unused-imports": "4.3.0",
88
+ "globals": "^16.5.0",
88
89
  "husky": "^9.1.7",
89
- "prettier": "^3.6.2",
90
+ "prettier": "^3.7.4",
90
91
  "prettier-plugin-organize-imports": "^4.3.0",
91
92
  "prettier-plugin-sort-json": "^4.1.1",
92
- "prettier-plugin-svelte": "^3.4.0",
93
- "prettier-plugin-tailwindcss": "^0.6.14",
94
- "publint": "^0.3.14",
95
- "svelte": "^5.39.11",
96
- "svelte-check": "^4.3.3",
93
+ "prettier-plugin-svelte": "^3.4.1",
94
+ "prettier-plugin-tailwindcss": "^0.7.2",
95
+ "publint": "^0.3.16",
96
+ "svelte": "^5.46.1",
97
+ "svelte-check": "^4.3.5",
97
98
  "tslib": "^2.8.1",
98
- "type-fest": "^5.0.1",
99
+ "type-fest": "^5.3.1",
99
100
  "typescript": "^5.9.3",
100
- "typescript-eslint": "^8.46.0",
101
- "vite": "^7.1.9",
102
- "vitest": "^3.2.4"
101
+ "typescript-eslint": "^8.51.0",
102
+ "vite": "^7.3.0",
103
+ "vitest": "^4.0.16"
103
104
  },
104
105
  "peerDependencies": {
105
106
  "svelte": "^5.30.0"
106
107
  },
107
108
  "volta": {
108
- "node": "22.18.0"
109
+ "node": "22.21.1"
109
110
  },
110
111
  "scripts": {
111
112
  "build": "vite build && npm run package",