@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.
- package/dist/plugins/addExpandedRows.d.ts +2 -0
- package/dist/plugins/addExpandedRows.js +24 -2
- package/dist/plugins/addSelectedRows.d.ts +2 -0
- package/dist/plugins/addSelectedRows.js +25 -2
- package/dist/plugins/cacheConfig.d.ts +7 -0
- package/dist/plugins/cacheConfig.js +11 -0
- package/package.json +29 -28
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": "^
|
|
67
|
-
"@eslint/js": "^9.
|
|
68
|
-
"@faker-js/faker": "^10.
|
|
69
|
-
"@playwright/test": "^1.
|
|
70
|
-
"@sveltejs/adapter-auto": "^
|
|
71
|
-
"@sveltejs/kit": "^2.
|
|
72
|
-
"@sveltejs/package": "^2.5.
|
|
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.
|
|
76
|
+
"@testing-library/svelte": "^5.3.1",
|
|
76
77
|
"@types/eslint": "9.6.1",
|
|
77
|
-
"@types/node": "^
|
|
78
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
79
|
-
"@typescript-eslint/parser": "^8.
|
|
80
|
-
"@vitest/coverage-v8": "^
|
|
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.
|
|
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.
|
|
86
|
-
"eslint-plugin-unused-imports": "4.
|
|
87
|
-
"globals": "^16.
|
|
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.
|
|
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.
|
|
93
|
-
"prettier-plugin-tailwindcss": "^0.
|
|
94
|
-
"publint": "^0.3.
|
|
95
|
-
"svelte": "^5.
|
|
96
|
-
"svelte-check": "^4.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.
|
|
99
|
+
"type-fest": "^5.3.1",
|
|
99
100
|
"typescript": "^5.9.3",
|
|
100
|
-
"typescript-eslint": "^8.
|
|
101
|
-
"vite": "^7.
|
|
102
|
-
"vitest": "^
|
|
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.
|
|
109
|
+
"node": "22.21.1"
|
|
109
110
|
},
|
|
110
111
|
"scripts": {
|
|
111
112
|
"build": "vite build && npm run package",
|