@reforgium/statum 3.1.3 → 3.1.5
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/CHANGELOG.md +23 -1
- package/README.md +3 -3
- package/fesm2022/reforgium-statum.mjs +44 -32
- package/package.json +1 -1
- package/types/reforgium-statum.d.ts +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
|
-
## [3.1.
|
|
1
|
+
## [3.1.5]: 2026-04-14
|
|
2
|
+
|
|
3
|
+
### Fix:
|
|
4
|
+
- `PagedQueryStore`: page cache is now invalidated when `routeParams` change even if `reset: false` is used, so long-lived singleton-like stores do not replay cached pages from the previous logical dataset/session after a screen remount or route-context change
|
|
5
|
+
- `PagedQueryStore.updateConfig(...)` / `copy(...)`: cached page window is now cleared before transport reconfiguration so reused source instances do not leak stale buffered pages into `@reforgium/data-grid` source mode
|
|
6
|
+
|
|
7
|
+
### Test:
|
|
8
|
+
- added regression coverage for cache invalidation on route-param session changes without store destruction
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## [3.1.4]: 2026-04-10
|
|
13
|
+
|
|
14
|
+
### Fix:
|
|
15
|
+
- `PagedQueryStore`: `totalElements` now preserves `undefined` when `parseResponse` omits the field, so unknown-total datasets stay open-ended instead of being forced back to `0` after reset/refetch.
|
|
16
|
+
- `PagedQueryStore`: `refetchWith(...)` now treats real filters/query/sort changes as dataset replacement, clearing page cache/items and bumping `version` so `@reforgium/data-grid` infinity buffers do not keep stale rows after an empty successful response.
|
|
17
|
+
|
|
18
|
+
### Test:
|
|
19
|
+
- added regression coverage for omitted `totalElements`, reset/refetch clearing, and empty-result unknown-total flows
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## [3.1.3]: 2026-04-06
|
|
2
24
|
|
|
3
25
|
### Fix:
|
|
4
26
|
- `PagedQueryStore`: all public state getters (`filters`, `query`, `page`, `pageSize`, `totalElements`, `sort`) and direct `#routeParams` reads now wrap the underlying signal in `untracked()`; previously calling `fetch()`, `refetchWith()`, `updatePage()`, `setRouteParams()`, etc. from inside an Angular `effect()` or `computed()` would inadvertently subscribe the reactive context to internal state signals, causing cascading re-runs; reactive subscriptions remain available exclusively through the `*State` readonly signals (`filtersState`, `sortState`, `pageState`, …)
|
package/README.md
CHANGED
|
@@ -345,7 +345,7 @@ Lightweight store for server-side pagination with filtering, dynamic query param
|
|
|
345
345
|
| version | `WritableSignal<number>` | Increments when the dataset is reset |
|
|
346
346
|
| page | `number` | Current page (0-based) |
|
|
347
347
|
| pageSize | `number` | Page size |
|
|
348
|
-
| totalElements | `number`
|
|
348
|
+
| totalElements | `number \| undefined` | Total items on server, if known |
|
|
349
349
|
| filters | `Partial<F>` | Active filters |
|
|
350
350
|
| query | `Record<string, unknown>` | Active query params |
|
|
351
351
|
| sort | `ReadonlyArray<{ sort: string; order: 'asc' \| 'desc' }>` | Active sort state |
|
|
@@ -396,7 +396,7 @@ Concurrency can be configured per store:
|
|
|
396
396
|
| Method | Cache read | Cache reset | Notes |
|
|
397
397
|
|----------------|------------|-------------|--------------------------------------------------|
|
|
398
398
|
| fetch | no | yes | Always starts clean from page `0` |
|
|
399
|
-
| refetchWith | no |
|
|
399
|
+
| refetchWith | no | conditional | Reloads current state; when filters/query/sort actually change, resets to page `0` and replaces the dataset |
|
|
400
400
|
| updatePage | yes | no | Can bypass with `ignoreCache: true` |
|
|
401
401
|
| updatePageSize | no | yes | Prevents mixed caches for different page sizes |
|
|
402
402
|
| updateByOffset | yes | no | Internally maps to `page + size` |
|
|
@@ -422,7 +422,7 @@ store.updateSort({ sort: 'name', order: 'asc' });
|
|
|
422
422
|
|
|
423
423
|
`cached()` remains a bounded hot-cache view. It is useful for cache-aware revisit/export/search helpers, but it is not the right datasource for infinity scrolling once cache eviction matters. For `data-grid` infinity mode, prefer passing the whole store as a `GridPagedDataSource` (`[source]="store"`) and let the grid keep its own page buffer.
|
|
424
424
|
|
|
425
|
-
`sort` and `routeParams` should be changed only through `setSort(...)` and `setRouteParams(...)`. Direct state mutation setters for `page`, `pageSize`, `filters`, `query`, and `totalElements` are available for low-level integration scenarios (such as external data-grid source contracts) but prefer the explicit store methods for typical use.
|
|
425
|
+
`sort` and `routeParams` should be changed only through `setSort(...)` and `setRouteParams(...)`. Direct state mutation setters for `page`, `pageSize`, `filters`, `query`, and `totalElements` are available for low-level integration scenarios (such as external data-grid source contracts) but prefer the explicit store methods for typical use. `totalElements` may be `undefined` when the backend does not report a total.
|
|
426
426
|
|
|
427
427
|
### PagedQueryStore + DataGrid source mode
|
|
428
428
|
|
|
@@ -277,10 +277,10 @@ class KeyedScheduler {
|
|
|
277
277
|
class ResourceStore {
|
|
278
278
|
http = inject(HttpClient);
|
|
279
279
|
serializer;
|
|
280
|
-
#value = signal(null, ...(ngDevMode ? [{ debugName: "#value" }] : []));
|
|
281
|
-
#status = signal('idle', ...(ngDevMode ? [{ debugName: "#status" }] : []));
|
|
282
|
-
#error = signal(null, ...(ngDevMode ? [{ debugName: "#error" }] : []));
|
|
283
|
-
#activeRequests = signal(0, ...(ngDevMode ? [{ debugName: "#activeRequests" }] : []));
|
|
280
|
+
#value = signal(null, ...(ngDevMode ? [{ debugName: "#value" }] : /* istanbul ignore next */ []));
|
|
281
|
+
#status = signal('idle', ...(ngDevMode ? [{ debugName: "#status" }] : /* istanbul ignore next */ []));
|
|
282
|
+
#error = signal(null, ...(ngDevMode ? [{ debugName: "#error" }] : /* istanbul ignore next */ []));
|
|
283
|
+
#activeRequests = signal(0, ...(ngDevMode ? [{ debugName: "#activeRequests" }] : /* istanbul ignore next */ []));
|
|
284
284
|
/**
|
|
285
285
|
* Current resource value.
|
|
286
286
|
* Returns `null` if no data yet or the request failed.
|
|
@@ -298,7 +298,7 @@ class ResourceStore {
|
|
|
298
298
|
* Convenience loading flag: `true` when `loading` or `stale`.
|
|
299
299
|
* Useful for spinners and disabling buttons.
|
|
300
300
|
*/
|
|
301
|
-
loading = computed(() => this.#activeRequests() > 0 || this.#status() === 'stale', ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
301
|
+
loading = computed(() => this.#activeRequests() > 0 || this.#status() === 'stale', ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
302
302
|
routes;
|
|
303
303
|
opts;
|
|
304
304
|
maxEntries;
|
|
@@ -839,22 +839,22 @@ class PagedQueryStore {
|
|
|
839
839
|
#transport;
|
|
840
840
|
#cache;
|
|
841
841
|
/** Current page data (reactive). */
|
|
842
|
-
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
842
|
+
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
843
843
|
/** Merged cache of pages (flat list) handy for search/export. */
|
|
844
|
-
cached = signal([], ...(ngDevMode ? [{ debugName: "cached" }] : []));
|
|
844
|
+
cached = signal([], ...(ngDevMode ? [{ debugName: "cached" }] : /* istanbul ignore next */ []));
|
|
845
845
|
/** Loading flag of the current operation. */
|
|
846
|
-
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
846
|
+
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
847
847
|
/** Last request error (if any). */
|
|
848
|
-
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
848
|
+
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
849
849
|
/** Increments when the current dataset is reset/replaced. Useful for external consumers with local buffers. */
|
|
850
|
-
version = signal(0, ...(ngDevMode ? [{ debugName: "version" }] : []));
|
|
851
|
-
#page = signal(0, ...(ngDevMode ? [{ debugName: "#page" }] : []));
|
|
852
|
-
#pageSize = signal(20, ...(ngDevMode ? [{ debugName: "#pageSize" }] : []));
|
|
853
|
-
#totalElements = signal(
|
|
854
|
-
#filters = signal({}, ...(ngDevMode ? [{ debugName: "#filters" }] : []));
|
|
855
|
-
#query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : []));
|
|
856
|
-
#sort = signal([], ...(ngDevMode ? [{ debugName: "#sort" }] : []));
|
|
857
|
-
#routeParams = signal({}, ...(ngDevMode ? [{ debugName: "#routeParams" }] : []));
|
|
850
|
+
version = signal(0, ...(ngDevMode ? [{ debugName: "version" }] : /* istanbul ignore next */ []));
|
|
851
|
+
#page = signal(0, ...(ngDevMode ? [{ debugName: "#page" }] : /* istanbul ignore next */ []));
|
|
852
|
+
#pageSize = signal(20, ...(ngDevMode ? [{ debugName: "#pageSize" }] : /* istanbul ignore next */ []));
|
|
853
|
+
#totalElements = signal(undefined, ...(ngDevMode ? [{ debugName: "#totalElements" }] : /* istanbul ignore next */ []));
|
|
854
|
+
#filters = signal({}, ...(ngDevMode ? [{ debugName: "#filters" }] : /* istanbul ignore next */ []));
|
|
855
|
+
#query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : /* istanbul ignore next */ []));
|
|
856
|
+
#sort = signal([], ...(ngDevMode ? [{ debugName: "#sort" }] : /* istanbul ignore next */ []));
|
|
857
|
+
#routeParams = signal({}, ...(ngDevMode ? [{ debugName: "#routeParams" }] : /* istanbul ignore next */ []));
|
|
858
858
|
pageState = this.#page.asReadonly();
|
|
859
859
|
pageSizeState = this.#pageSize.asReadonly();
|
|
860
860
|
totalElementsState = this.#totalElements.asReadonly();
|
|
@@ -891,7 +891,7 @@ class PagedQueryStore {
|
|
|
891
891
|
get pageSize() {
|
|
892
892
|
return untracked(this.#pageSize);
|
|
893
893
|
}
|
|
894
|
-
/** Total number of elements reported by the server. */
|
|
894
|
+
/** Total number of elements reported by the server, or `undefined` when the total is unknown. */
|
|
895
895
|
get totalElements() {
|
|
896
896
|
return untracked(this.#totalElements);
|
|
897
897
|
}
|
|
@@ -931,6 +931,11 @@ class PagedQueryStore {
|
|
|
931
931
|
const nextFilters = filters == null ? this.filters : { ...this.filters, ...filters };
|
|
932
932
|
const nextQuery = query == null ? this.query : mergeQueryParams(this.query, query);
|
|
933
933
|
const nextSort = sort == null ? this.sort : this.normalizeSort(sort);
|
|
934
|
+
const shouldResetDataset = !deepEqual(this.filters, nextFilters) || !deepEqual(this.query, nextQuery) || !deepEqual(this.sort, nextSort);
|
|
935
|
+
if (shouldResetDataset) {
|
|
936
|
+
this.resetDataset();
|
|
937
|
+
return this.#fetchItems({ page: 0, filters: nextFilters, query: nextQuery, sort: nextSort });
|
|
938
|
+
}
|
|
934
939
|
return this.#fetchItems({ filters: nextFilters, query: nextQuery, sort: nextSort });
|
|
935
940
|
}
|
|
936
941
|
/**
|
|
@@ -1002,6 +1007,7 @@ class PagedQueryStore {
|
|
|
1002
1007
|
if (!isChanged) {
|
|
1003
1008
|
return;
|
|
1004
1009
|
}
|
|
1010
|
+
this.invalidatePageCache();
|
|
1005
1011
|
this.#routeParams.set(params);
|
|
1006
1012
|
if (opts.reset) {
|
|
1007
1013
|
this.resetDataset();
|
|
@@ -1023,6 +1029,7 @@ class PagedQueryStore {
|
|
|
1023
1029
|
updateConfig = (config) => {
|
|
1024
1030
|
this.config = { ...this.config, ...config };
|
|
1025
1031
|
this.#cache.limit = this.resolveCacheLimit();
|
|
1032
|
+
this.invalidatePageCache();
|
|
1026
1033
|
this.applyPresetMeta();
|
|
1027
1034
|
this.reinitTransport();
|
|
1028
1035
|
};
|
|
@@ -1031,6 +1038,7 @@ class PagedQueryStore {
|
|
|
1031
1038
|
*/
|
|
1032
1039
|
copy(store) {
|
|
1033
1040
|
this.applyConfig(store.config);
|
|
1041
|
+
this.invalidatePageCache();
|
|
1034
1042
|
this.applyPresetMeta();
|
|
1035
1043
|
this.reinitTransport();
|
|
1036
1044
|
}
|
|
@@ -1164,12 +1172,16 @@ class PagedQueryStore {
|
|
|
1164
1172
|
}
|
|
1165
1173
|
this.cached.set(flat);
|
|
1166
1174
|
};
|
|
1175
|
+
invalidatePageCache() {
|
|
1176
|
+
this.#cache.clear();
|
|
1177
|
+
this.cached.set([]);
|
|
1178
|
+
}
|
|
1167
1179
|
parseFlatArray(data) {
|
|
1168
1180
|
return { content: data, totalElements: data.length };
|
|
1169
1181
|
}
|
|
1170
1182
|
resetDataset() {
|
|
1171
1183
|
this.page = 0;
|
|
1172
|
-
this.totalElements =
|
|
1184
|
+
this.totalElements = undefined;
|
|
1173
1185
|
this.#cache.clear();
|
|
1174
1186
|
this.cached.set([]);
|
|
1175
1187
|
this.items.set([]);
|
|
@@ -1285,19 +1297,19 @@ class DictStore {
|
|
|
1285
1297
|
metaStorage;
|
|
1286
1298
|
ttlMs;
|
|
1287
1299
|
revalidate;
|
|
1288
|
-
cacheUpdatedAt = signal(null, ...(ngDevMode ? [{ debugName: "cacheUpdatedAt" }] : []));
|
|
1300
|
+
cacheUpdatedAt = signal(null, ...(ngDevMode ? [{ debugName: "cacheUpdatedAt" }] : /* istanbul ignore next */ []));
|
|
1289
1301
|
presetFilters = {};
|
|
1290
1302
|
/**
|
|
1291
1303
|
* Search text.
|
|
1292
1304
|
* With `fixed: true` filters the local cache; with `fixed: false` triggers server search.
|
|
1293
1305
|
*/
|
|
1294
|
-
searchText = signal('', ...(ngDevMode ? [{ debugName: "searchText" }] : []));
|
|
1306
|
+
searchText = signal('', ...(ngDevMode ? [{ debugName: "searchText" }] : /* istanbul ignore next */ []));
|
|
1295
1307
|
debouncedSearchText;
|
|
1296
1308
|
/**
|
|
1297
1309
|
* Additional filters for server request (or presets).
|
|
1298
1310
|
*/
|
|
1299
|
-
filters = signal({}, ...(ngDevMode ? [{ debugName: "filters" }] : []));
|
|
1300
|
-
cachedItems = signal([], ...(ngDevMode ? [{ debugName: "cachedItems" }] : []));
|
|
1311
|
+
filters = signal({}, ...(ngDevMode ? [{ debugName: "filters" }] : /* istanbul ignore next */ []));
|
|
1312
|
+
cachedItems = signal([], ...(ngDevMode ? [{ debugName: "cachedItems" }] : /* istanbul ignore next */ []));
|
|
1301
1313
|
/**
|
|
1302
1314
|
* Current list of dictionary items.
|
|
1303
1315
|
* Source — local cache (fixed=true) or data from `PagedQueryStore`.
|
|
@@ -1308,7 +1320,7 @@ class DictStore {
|
|
|
1308
1320
|
return this.debouncedSearchText() || !cached.length ? this.#helper.items() : cached;
|
|
1309
1321
|
}
|
|
1310
1322
|
return cached.length ? this.filterLocal() : this.#helper.items();
|
|
1311
|
-
}, ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1323
|
+
}, ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
1312
1324
|
/**
|
|
1313
1325
|
* Ready-to-use dropdown options: `{ label, value }`.
|
|
1314
1326
|
* Respects `maxOptionsSize` for truncating the list.
|
|
@@ -1316,9 +1328,9 @@ class DictStore {
|
|
|
1316
1328
|
options = computed(() => {
|
|
1317
1329
|
const options = this.items().map((it) => ({ label: String(it[this.labelKey] ?? ''), value: it[this.valueKey] }));
|
|
1318
1330
|
return this.maxOptionsSize ? options.slice(0, this.maxOptionsSize) : options;
|
|
1319
|
-
}, ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
1331
|
+
}, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
1320
1332
|
_lastPromise = null;
|
|
1321
|
-
_armed = signal(false, ...(ngDevMode ? [{ debugName: "_armed" }] : []));
|
|
1333
|
+
_armed = signal(false, ...(ngDevMode ? [{ debugName: "_armed" }] : /* istanbul ignore next */ []));
|
|
1322
1334
|
// todo add i18n support
|
|
1323
1335
|
/**
|
|
1324
1336
|
* @param apiUrl dictionary endpoint (e.g., `'/api/dicts/countries'`)
|
|
@@ -1574,8 +1586,8 @@ class DictLocalStore {
|
|
|
1574
1586
|
* Represents the full, unfiltered list of dictionary entries.
|
|
1575
1587
|
* Used as the base data set for search and option generation.
|
|
1576
1588
|
*/
|
|
1577
|
-
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1578
|
-
#draftItems = signal([], ...(ngDevMode ? [{ debugName: "#draftItems" }] : []));
|
|
1589
|
+
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
1590
|
+
#draftItems = signal([], ...(ngDevMode ? [{ debugName: "#draftItems" }] : /* istanbul ignore next */ []));
|
|
1579
1591
|
/**
|
|
1580
1592
|
* Computed list of options in `{ label, value }` format.
|
|
1581
1593
|
*
|
|
@@ -1590,7 +1602,7 @@ class DictLocalStore {
|
|
|
1590
1602
|
value: it[this.valueKey],
|
|
1591
1603
|
}));
|
|
1592
1604
|
return this.maxOptionsSize ? options.slice(0, this.maxOptionsSize) : options;
|
|
1593
|
-
}, ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
1605
|
+
}, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
1594
1606
|
labelKey;
|
|
1595
1607
|
valueKey;
|
|
1596
1608
|
maxOptionsSize;
|
|
@@ -1675,14 +1687,14 @@ class DictLocalStore {
|
|
|
1675
1687
|
class EntityStore {
|
|
1676
1688
|
idKey;
|
|
1677
1689
|
sortIds;
|
|
1678
|
-
byId = signal({}, ...(ngDevMode ? [{ debugName: "byId" }] : []));
|
|
1679
|
-
ids = signal([], ...(ngDevMode ? [{ debugName: "ids" }] : []));
|
|
1690
|
+
byId = signal({}, ...(ngDevMode ? [{ debugName: "byId" }] : /* istanbul ignore next */ []));
|
|
1691
|
+
ids = signal([], ...(ngDevMode ? [{ debugName: "ids" }] : /* istanbul ignore next */ []));
|
|
1680
1692
|
items = computed(() => {
|
|
1681
1693
|
const byId = this.byId();
|
|
1682
1694
|
return this.ids()
|
|
1683
1695
|
.map((id) => byId[String(id)])
|
|
1684
1696
|
.filter((item) => item !== undefined);
|
|
1685
|
-
}, ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1697
|
+
}, ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
1686
1698
|
constructor(config) {
|
|
1687
1699
|
this.idKey = config.idKey;
|
|
1688
1700
|
this.sortIds = config.sortIds;
|
package/package.json
CHANGED
|
@@ -505,7 +505,7 @@ declare class PagedQueryStore<ItemsType extends AnyDict, FilterType extends AnyD
|
|
|
505
505
|
version: WritableSignal<number>;
|
|
506
506
|
readonly pageState: _angular_core.Signal<number>;
|
|
507
507
|
readonly pageSizeState: _angular_core.Signal<number>;
|
|
508
|
-
readonly totalElementsState: _angular_core.Signal<number>;
|
|
508
|
+
readonly totalElementsState: _angular_core.Signal<number | undefined>;
|
|
509
509
|
readonly filtersState: _angular_core.Signal<Partial<FilterType>>;
|
|
510
510
|
readonly queryState: _angular_core.Signal<AnyDict>;
|
|
511
511
|
readonly sortState: _angular_core.Signal<QuerySortRule[]>;
|
|
@@ -523,15 +523,15 @@ declare class PagedQueryStore<ItemsType extends AnyDict, FilterType extends AnyD
|
|
|
523
523
|
get page(): number;
|
|
524
524
|
/** Default page size. */
|
|
525
525
|
get pageSize(): number;
|
|
526
|
-
/** Total number of elements reported by the server. */
|
|
527
|
-
get totalElements(): number;
|
|
526
|
+
/** Total number of elements reported by the server, or `undefined` when the total is unknown. */
|
|
527
|
+
get totalElements(): number | undefined;
|
|
528
528
|
/** Current sort rules. */
|
|
529
529
|
get sort(): QuerySortRule[];
|
|
530
530
|
set filters(value: Partial<FilterType>);
|
|
531
531
|
set query(value: AnyDict);
|
|
532
532
|
set page(value: number);
|
|
533
533
|
set pageSize(value: number);
|
|
534
|
-
set totalElements(value: number);
|
|
534
|
+
set totalElements(value: number | undefined);
|
|
535
535
|
/**
|
|
536
536
|
* Fetch data with explicit filters and query params from the first page.
|
|
537
537
|
* Replaces legacy `setFilters`.
|
|
@@ -600,6 +600,7 @@ declare class PagedQueryStore<ItemsType extends AnyDict, FilterType extends AnyD
|
|
|
600
600
|
private parseQuery;
|
|
601
601
|
private parseResponseData;
|
|
602
602
|
private updateCache;
|
|
603
|
+
private invalidatePageCache;
|
|
603
604
|
private parseFlatArray;
|
|
604
605
|
private resetDataset;
|
|
605
606
|
private bumpVersion;
|