@reforgium/statum 3.1.6 → 3.3.0
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 +69 -28
- package/README.md +64 -15
- package/fesm2022/reforgium-statum.mjs +460 -14
- package/package.json +1 -1
- package/types/reforgium-statum.d.ts +168 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,39 +1,80 @@
|
|
|
1
|
-
## [3.
|
|
1
|
+
## [3.3.0]: 2026-05-29
|
|
2
2
|
|
|
3
|
-
###
|
|
4
|
-
- `
|
|
5
|
-
|
|
6
|
-
### Test:
|
|
7
|
-
- updated flat-array regression coverage to assert unknown-total semantics instead of the old inferred-length behavior
|
|
8
|
-
|
|
9
|
-
### Docs:
|
|
10
|
-
- `README`: clarified that bare array responses are treated as unknown-total datasets and that exact totals must come from the backend or a custom `parseResponse(...)`
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## [3.1.5]: 2026-04-14
|
|
15
|
-
|
|
16
|
-
### Fix:
|
|
17
|
-
- `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
|
|
18
|
-
- `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
|
|
3
|
+
### Feat:
|
|
4
|
+
- `PagedQueryLocalStore`: added a public local/in-memory counterpart of `PagedQueryStore` with the same page/sort/version surface, so consumer apps can mock paged datasets or work with locally assembled collections without losing `DataGrid` source compatibility
|
|
5
|
+
- `ResourceMockStore`: added a public handler-driven in-memory counterpart of `ResourceStore` with the same `value/status/error/loading` signals and `get/post/put/patch/delete` entry points for API-less feature work and service mocking
|
|
19
6
|
|
|
20
7
|
### Test:
|
|
21
|
-
- added regression coverage for
|
|
8
|
+
- added regression coverage for local page slicing, local multi-sort, custom local resolvers, seeded resource reads, cache-first mock reads, and handler-driven mock mutations
|
|
22
9
|
|
|
23
10
|
---
|
|
24
11
|
|
|
25
|
-
## [3.1
|
|
12
|
+
## [3.2.1]: 2026-05-11
|
|
26
13
|
|
|
27
14
|
### Fix:
|
|
28
|
-
- `PagedQueryStore`: `
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
15
|
+
- `PagedQueryStore`: fixed `parseRequest(...)` input typing to match actual runtime behavior; the hook now explicitly receives pre-serialization sort rules (`QuerySortRule[]`) instead of a transport-level `sort: string | string[]` contract
|
|
16
|
+
|
|
17
|
+
### Test:
|
|
18
|
+
- added regression coverage proving that `parseRequest(...)` receives object sort rules while the outgoing HTTP query still serializes them as repeated `sort=name,asc` params
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [3.2.0]: 2026-04-24
|
|
23
|
+
|
|
24
|
+
### Feat:
|
|
25
|
+
- `DictStore`: added `autoLoad: 'onDemand'` and `ensureLoaded()` for controlled lazy startup; dictionaries can now stay passive on construction and fetch only when a consumer explicitly needs them
|
|
26
|
+
- `DictStore`: added `autoLoad: 'onAccess'` as an explicitly dirty opt-in mode for UI consumers that want the first read of `items()` / `options()` to lazily schedule loading
|
|
27
|
+
- `DictStoreProviderConfig`: added `defaultAutoLoad` so the default startup policy can be set once at provider level instead of repeating `autoLoad` on every dictionary instance
|
|
28
|
+
|
|
29
|
+
### Fix:
|
|
30
|
+
- `DictStore`: `ttlMs` now enables stale-cache revalidation by default when `revalidate` is omitted; stale dictionaries no longer look like they have TTL configured while silently skipping refresh
|
|
31
|
+
- `DictStore`: stale-cache refresh now depends only on `ttlMs`; deprecated `revalidate` no longer changes runtime behavior, so TTL-based freshness is no longer split across two partially overlapping flags
|
|
32
|
+
|
|
33
|
+
### Test:
|
|
34
|
+
- added regression coverage for deprecated `revalidate: false` compatibility, on-demand loading, and on-access lazy activation
|
|
35
|
+
|
|
36
|
+
### Docs:
|
|
37
|
+
- `README`: clarified that `ttlMs` implies background revalidation unless `revalidate` is set to `false` explicitly
|
|
38
|
+
- `README`: documented `autoLoad: 'onDemand'`, `autoLoad: 'onAccess'`, `ensureLoaded()`, provider-level `defaultAutoLoad`, and the deprecation of `revalidate`
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## [3.1.6]: 2026-04-17
|
|
43
|
+
|
|
44
|
+
### Fix:
|
|
45
|
+
- `PagedQueryStore`: plain array responses (`T[]`) no longer synthesize `totalElements = data.length`; `parseFlatArray()` now keeps `totalElements` as `undefined` unless the backend or a custom `parseResponse(...)` provides an exact total explicitly
|
|
46
|
+
|
|
47
|
+
### Test:
|
|
48
|
+
- updated flat-array regression coverage to assert unknown-total semantics instead of the old inferred-length behavior
|
|
49
|
+
|
|
50
|
+
### Docs:
|
|
51
|
+
- `README`: clarified that bare array responses are treated as unknown-total datasets and that exact totals must come from the backend or a custom `parseResponse(...)`
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## [3.1.5]: 2026-04-14
|
|
56
|
+
|
|
57
|
+
### Fix:
|
|
58
|
+
- `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
|
|
59
|
+
- `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
|
|
60
|
+
|
|
61
|
+
### Test:
|
|
62
|
+
- added regression coverage for cache invalidation on route-param session changes without store destruction
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## [3.1.4]: 2026-04-10
|
|
67
|
+
|
|
68
|
+
### Fix:
|
|
69
|
+
- `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.
|
|
70
|
+
- `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.
|
|
71
|
+
|
|
72
|
+
### Test:
|
|
73
|
+
- added regression coverage for omitted `totalElements`, reset/refetch clearing, and empty-result unknown-total flows
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## [3.1.3]: 2026-04-06
|
|
37
78
|
|
|
38
79
|
### Fix:
|
|
39
80
|
- `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
|
@@ -18,11 +18,12 @@ It is not a reduced framework and does not try to replace NgRx-style global even
|
|
|
18
18
|
- Signals-first API (`signal()` everywhere)
|
|
19
19
|
- API-driven stores (resource / paginated / dictionaries)
|
|
20
20
|
- Normalized entity store (`EntityStore`)
|
|
21
|
-
- Built-in cache strategies (TTL, prefix grouping)
|
|
22
|
-
- Deterministic request lifecycle: loading / error / data
|
|
23
|
-
- Optional transport-level: dedupe, debounce/throttle, abort
|
|
24
|
-
- Unified retry and trace hooks in `ResourceStore`
|
|
25
|
-
-
|
|
21
|
+
- Built-in cache strategies (TTL, prefix grouping)
|
|
22
|
+
- Deterministic request lifecycle: loading / error / data
|
|
23
|
+
- Optional transport-level: dedupe, debounce/throttle, abort
|
|
24
|
+
- Unified retry and trace hooks in `ResourceStore`
|
|
25
|
+
- Public local/mock store counterparts for API-less flows
|
|
26
|
+
- Explicit serialization / deserialization boundary
|
|
26
27
|
|
|
27
28
|
## Best Fit
|
|
28
29
|
|
|
@@ -38,13 +39,18 @@ Use `statum` when you need:
|
|
|
38
39
|
- plain `HttpClient` services that have started to accumulate state logic
|
|
39
40
|
- full application state frameworks where global reducers/effects are too expensive for the problem
|
|
40
41
|
|
|
41
|
-
`statum` is usually not the right fit when:
|
|
42
|
+
`statum` is usually not the right fit when:
|
|
42
43
|
|
|
43
44
|
- you need cross-page event sourcing, time-travel tooling, or centralized action logs
|
|
44
45
|
- your app is mostly local UI state with little backend orchestration
|
|
45
|
-
- you want a framework-agnostic data layer
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
- you want a framework-agnostic data layer
|
|
47
|
+
|
|
48
|
+
For API-less feature work and staged backend delivery:
|
|
49
|
+
|
|
50
|
+
- use `PagedQueryLocalStore` when the consumer still wants a `PagedQueryStore`-like source for `DataGrid`
|
|
51
|
+
- use `ResourceMockStore` when the consumer wants `ResourceStore`-like signals and CRUD entry points backed by local handlers instead of HTTP
|
|
52
|
+
|
|
53
|
+
---
|
|
48
54
|
|
|
49
55
|
## Installation
|
|
50
56
|
|
|
@@ -616,17 +622,60 @@ import { DictStore } from '@reforgium/statum';
|
|
|
616
622
|
|
|
617
623
|
type Country = { code: string; name: string };
|
|
618
624
|
|
|
619
|
-
const countries = new DictStore<Country>('/api/dictionaries/countries', 'countries', {
|
|
620
|
-
fixed: true,
|
|
621
|
-
labelKey: 'name',
|
|
622
|
-
valueKey: 'code',
|
|
623
|
-
});
|
|
625
|
+
const countries = new DictStore<Country>('/api/dictionaries/countries', 'countries', {
|
|
626
|
+
fixed: true,
|
|
627
|
+
labelKey: 'name',
|
|
628
|
+
valueKey: 'code',
|
|
629
|
+
});
|
|
624
630
|
|
|
625
631
|
await countries.search('');
|
|
626
632
|
countries.search('kir');
|
|
627
633
|
```
|
|
628
634
|
|
|
629
|
-
Use `fixed: true` when the dataset is small enough to keep locally and you want immediate repeated searches without extra network calls.
|
|
635
|
+
Use `fixed: true` when the dataset is small enough to keep locally and you want immediate repeated searches without extra network calls.
|
|
636
|
+
|
|
637
|
+
`DictStore` cache freshness notes:
|
|
638
|
+
|
|
639
|
+
- `ttlMs` marks persisted dictionary cache as stale after the configured window
|
|
640
|
+
- stale cache is refreshed automatically when `ttlMs` is set
|
|
641
|
+
- `revalidate` is deprecated and no longer changes runtime behavior
|
|
642
|
+
- storage `updatedAt` changes when cache is actually persisted again, not just because wall-clock time passed
|
|
643
|
+
|
|
644
|
+
For controlled lazy startup, use `autoLoad: 'onDemand'` and trigger the first fetch explicitly:
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
const countries = new DictStore<Country>('/api/dictionaries/countries', 'countries', {
|
|
648
|
+
fixed: true,
|
|
649
|
+
autoLoad: 'onDemand',
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
countries.ensureLoaded(); // first request only when the UI actually needs the dictionary
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
There is also a dirtier opt-in mode for select-like consumers that want loading to start on the first read:
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
const countries = new DictStore<Country>('/api/dictionaries/countries', 'countries', {
|
|
659
|
+
fixed: true,
|
|
660
|
+
autoLoad: 'onAccess',
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
countries.options(); // first read schedules loading lazily
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
Use `onAccess` only when you explicitly accept read-triggered loading semantics. It is guarded against same-turn request storms, but it is still intentionally less pure than `onDemand`.
|
|
667
|
+
|
|
668
|
+
The same policy can be moved to the global provider:
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
provideStatum({
|
|
672
|
+
dict: {
|
|
673
|
+
defaultAutoLoad: 'onDemand',
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
`ensureLoaded()` is not required for every `DictStore`. It is only needed when the effective auto-load mode is `'onDemand'` and you want the first fetch to happen explicitly. In `true`, `false`, or `'whenEmpty'` modes it remains an optional manual trigger.
|
|
630
679
|
|
|
631
680
|
### Normalize Paged Data For Detail Mutations
|
|
632
681
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Serializer, fillUrlWithParams, mergeQueryParams,
|
|
1
|
+
import { Serializer, fillUrlWithParams, mergeQueryParams, deepEqual, normalizeSortInput, LruCache, isNullable, sortInputToTokens, debounceSignal, storageStrategy } from '@reforgium/internal';
|
|
2
2
|
export { LocalStorage, LruCache, MemoryStorage, Serializer, SerializerFieldError, SessionStorage, storageStrategy } from '@reforgium/internal';
|
|
3
|
+
import { signal, computed, InjectionToken, makeEnvironmentProviders, inject, untracked, EnvironmentInjector, DestroyRef, runInInjectionContext, effect } from '@angular/core';
|
|
3
4
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
4
|
-
import { InjectionToken, makeEnvironmentProviders, inject, signal, computed, EnvironmentInjector, DestroyRef, untracked, runInInjectionContext, effect } from '@angular/core';
|
|
5
5
|
import { Subject, filter, timer, merge, map } from 'rxjs';
|
|
6
6
|
import { debounce, tap, throttle, finalize } from 'rxjs/operators';
|
|
7
7
|
|
|
@@ -30,10 +30,6 @@ const createStrictSerializer = (config = {}) => {
|
|
|
30
30
|
});
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
// noinspection ES6PreferShortImport
|
|
34
|
-
const STATUM_CONFIG = new InjectionToken('RE_STATUM_CONFIG');
|
|
35
|
-
const provideStatum = (config) => makeEnvironmentProviders([{ provide: STATUM_CONFIG, useValue: config }]);
|
|
36
|
-
|
|
37
33
|
/**
|
|
38
34
|
* Error thrown when requested data is missing in the cache.
|
|
39
35
|
*
|
|
@@ -119,6 +115,170 @@ function stableStringify(value) {
|
|
|
119
115
|
return String(value);
|
|
120
116
|
}
|
|
121
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Lightweight in-memory counterpart of `ResourceStore`.
|
|
120
|
+
*
|
|
121
|
+
* It preserves the result-facing API (`value/status/error/loading` + CRUD methods),
|
|
122
|
+
* but does not try to reproduce transport, retry, or per-key cache internals.
|
|
123
|
+
*/
|
|
124
|
+
class ResourceMockStore {
|
|
125
|
+
#value = signal(null, ...(ngDevMode ? [{ debugName: "#value" }] : /* istanbul ignore next */ []));
|
|
126
|
+
#status = signal('idle', ...(ngDevMode ? [{ debugName: "#status" }] : /* istanbul ignore next */ []));
|
|
127
|
+
#error = signal(null, ...(ngDevMode ? [{ debugName: "#error" }] : /* istanbul ignore next */ []));
|
|
128
|
+
#activeRequests = signal(0, ...(ngDevMode ? [{ debugName: "#activeRequests" }] : /* istanbul ignore next */ []));
|
|
129
|
+
value = this.#value.asReadonly();
|
|
130
|
+
status = this.#status.asReadonly();
|
|
131
|
+
error = this.#error.asReadonly();
|
|
132
|
+
loading = computed(() => this.#activeRequests() > 0 || this.#status() === 'stale', ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
133
|
+
routes;
|
|
134
|
+
opts;
|
|
135
|
+
constructor(routes = {}, opts = {}) {
|
|
136
|
+
this.routes = routes;
|
|
137
|
+
this.opts = opts;
|
|
138
|
+
if (opts.seed !== undefined) {
|
|
139
|
+
this.#value.set(opts.seed);
|
|
140
|
+
this.#status.set(opts.seed == null ? 'idle' : 'success');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
setValue(value) {
|
|
144
|
+
this.#value.set(value);
|
|
145
|
+
this.#status.set(value == null ? 'idle' : 'success');
|
|
146
|
+
this.#error.set(null);
|
|
147
|
+
}
|
|
148
|
+
patchValue(patch) {
|
|
149
|
+
const current = this.#value();
|
|
150
|
+
if (!current || typeof current !== 'object') {
|
|
151
|
+
this.#value.set(patch);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
this.#value.set({ ...current, ...patch });
|
|
155
|
+
}
|
|
156
|
+
this.#status.set('success');
|
|
157
|
+
this.#error.set(null);
|
|
158
|
+
}
|
|
159
|
+
reset(value = this.opts.seed ?? null) {
|
|
160
|
+
this.#value.set(value);
|
|
161
|
+
this.#status.set(value == null ? 'idle' : 'success');
|
|
162
|
+
this.#error.set(null);
|
|
163
|
+
}
|
|
164
|
+
get(args, cfg = {}) {
|
|
165
|
+
const strategy = cfg.strategy ?? 'network-first';
|
|
166
|
+
if (strategy === 'cache-only' && this.#value() == null && !this.opts.handlers?.GET) {
|
|
167
|
+
return Promise.reject(new CacheMissError(this.buildUrl('GET', args)));
|
|
168
|
+
}
|
|
169
|
+
if (strategy === 'cache-first' && this.#value() != null && !cfg.revalidate) {
|
|
170
|
+
return Promise.resolve(this.#value());
|
|
171
|
+
}
|
|
172
|
+
return this.run('GET', args, async () => {
|
|
173
|
+
const url = this.buildUrl('GET', args);
|
|
174
|
+
const handler = this.opts.handlers?.GET;
|
|
175
|
+
const result = handler
|
|
176
|
+
? await handler({ method: 'GET', route: this.routes.GET, url, args, current: this.#value() })
|
|
177
|
+
: this.#value();
|
|
178
|
+
if (result == null) {
|
|
179
|
+
throw new Error('ResourceMockStore GET has no handler result and no current value.');
|
|
180
|
+
}
|
|
181
|
+
const parsed = cfg.parseResponse ? cfg.parseResponse(result) : result;
|
|
182
|
+
this.#value.set(parsed);
|
|
183
|
+
this.#status.set('success');
|
|
184
|
+
this.#error.set(null);
|
|
185
|
+
return parsed;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
post(args, cfg = {}) {
|
|
189
|
+
return this.runMutation('POST', args, cfg);
|
|
190
|
+
}
|
|
191
|
+
put(args, cfg = {}) {
|
|
192
|
+
return this.runMutation('PUT', args, cfg);
|
|
193
|
+
}
|
|
194
|
+
patch(args, cfg = {}) {
|
|
195
|
+
return this.runMutation('PATCH', args, cfg);
|
|
196
|
+
}
|
|
197
|
+
delete(args, cfg = {}) {
|
|
198
|
+
return this.runMutation('DELETE', args, cfg);
|
|
199
|
+
}
|
|
200
|
+
abort() { }
|
|
201
|
+
abortAll() { }
|
|
202
|
+
runMutation(method, args, cfg) {
|
|
203
|
+
return this.run(method, args, async () => {
|
|
204
|
+
const result = await this.resolveMutation(method, args);
|
|
205
|
+
const parsed = cfg.parseResponse ? cfg.parseResponse(result) : result;
|
|
206
|
+
this.#value.set(parsed ?? null);
|
|
207
|
+
this.#status.set('success');
|
|
208
|
+
this.#error.set(null);
|
|
209
|
+
return parsed;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async resolveMutation(method, args) {
|
|
213
|
+
const url = this.buildUrl(method, args);
|
|
214
|
+
const handler = this.opts.handlers?.[method];
|
|
215
|
+
if (handler) {
|
|
216
|
+
return handler({
|
|
217
|
+
method,
|
|
218
|
+
route: this.routes[method],
|
|
219
|
+
url,
|
|
220
|
+
args: args,
|
|
221
|
+
current: this.#value(),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (method === 'DELETE') {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
if (method === 'PATCH') {
|
|
228
|
+
const current = this.#value();
|
|
229
|
+
const patch = args.payload;
|
|
230
|
+
if (current && typeof current === 'object' && patch && typeof patch === 'object' && !Array.isArray(patch)) {
|
|
231
|
+
return { ...current, ...patch };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return args.payload ?? this.#value();
|
|
235
|
+
}
|
|
236
|
+
run(method, args, exec) {
|
|
237
|
+
const url = this.buildUrl(method, args);
|
|
238
|
+
const delay = this.opts.delay ?? 0;
|
|
239
|
+
this.#activeRequests.update((count) => count + 1);
|
|
240
|
+
this.#status.set(this.#value() == null ? 'loading' : 'stale');
|
|
241
|
+
this.trace({ type: 'request-start', method, key: url, attempt: 1 });
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
setTimeout(() => {
|
|
244
|
+
void exec()
|
|
245
|
+
.then((result) => {
|
|
246
|
+
this.trace({ type: 'request-success', method, key: url });
|
|
247
|
+
resolve(result);
|
|
248
|
+
})
|
|
249
|
+
.catch((error) => {
|
|
250
|
+
this.#status.set('error');
|
|
251
|
+
this.#error.set(error);
|
|
252
|
+
this.trace({ type: 'request-error', method, key: url, error });
|
|
253
|
+
reject(error);
|
|
254
|
+
})
|
|
255
|
+
.finally(() => {
|
|
256
|
+
this.#activeRequests.update((count) => Math.max(0, count - 1));
|
|
257
|
+
});
|
|
258
|
+
}, delay);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
buildUrl(method, args) {
|
|
262
|
+
const template = this.routes[method];
|
|
263
|
+
if (template == null) {
|
|
264
|
+
throw new Error(`${method} route not configured`);
|
|
265
|
+
}
|
|
266
|
+
return joinUrl(this.opts.baseUrl, fillUrlWithParams(template, args.params));
|
|
267
|
+
}
|
|
268
|
+
trace(event) {
|
|
269
|
+
try {
|
|
270
|
+
this.opts.onTrace?.(event);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
/* noop */
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// noinspection ES6PreferShortImport
|
|
279
|
+
const STATUM_CONFIG = new InjectionToken('RE_STATUM_CONFIG');
|
|
280
|
+
const provideStatum = (config) => makeEnvironmentProviders([{ provide: STATUM_CONFIG, useValue: config }]);
|
|
281
|
+
|
|
122
282
|
/**
|
|
123
283
|
* Per-key task scheduler with `debounce`/`throttle` and result deduplication.
|
|
124
284
|
*
|
|
@@ -811,6 +971,251 @@ const createResourceProfile = (profile, overrides = {}) => ({
|
|
|
811
971
|
...overrides,
|
|
812
972
|
});
|
|
813
973
|
|
|
974
|
+
/**
|
|
975
|
+
* Lightweight local counterpart of `PagedQueryStore`.
|
|
976
|
+
*
|
|
977
|
+
* It keeps the same result-facing shape needed by consumers and `DataGrid source`,
|
|
978
|
+
* but resolves pages from an in-memory collection instead of HTTP.
|
|
979
|
+
*/
|
|
980
|
+
class PagedQueryLocalStore {
|
|
981
|
+
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
982
|
+
cached = signal([], ...(ngDevMode ? [{ debugName: "cached" }] : /* istanbul ignore next */ []));
|
|
983
|
+
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
984
|
+
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
985
|
+
version = signal(0, ...(ngDevMode ? [{ debugName: "version" }] : /* istanbul ignore next */ []));
|
|
986
|
+
allItemsState = signal([], ...(ngDevMode ? [{ debugName: "allItemsState" }] : /* istanbul ignore next */ []));
|
|
987
|
+
pageState = signal(0, ...(ngDevMode ? [{ debugName: "pageState" }] : /* istanbul ignore next */ []));
|
|
988
|
+
pageSizeState = signal(20, ...(ngDevMode ? [{ debugName: "pageSizeState" }] : /* istanbul ignore next */ []));
|
|
989
|
+
totalElementsState = signal(undefined, ...(ngDevMode ? [{ debugName: "totalElementsState" }] : /* istanbul ignore next */ []));
|
|
990
|
+
filtersState = signal({}, ...(ngDevMode ? [{ debugName: "filtersState" }] : /* istanbul ignore next */ []));
|
|
991
|
+
queryState = signal({}, ...(ngDevMode ? [{ debugName: "queryState" }] : /* istanbul ignore next */ []));
|
|
992
|
+
sortState = signal([], ...(ngDevMode ? [{ debugName: "sortState" }] : /* istanbul ignore next */ []));
|
|
993
|
+
routeParamsState = signal({}, ...(ngDevMode ? [{ debugName: "routeParamsState" }] : /* istanbul ignore next */ []));
|
|
994
|
+
config;
|
|
995
|
+
prefetchMode;
|
|
996
|
+
constructor(items = [], config = {}) {
|
|
997
|
+
this.config = config;
|
|
998
|
+
this.prefetchMode = config.prefetchMode ?? 'sequential';
|
|
999
|
+
this.allItemsState.set(items);
|
|
1000
|
+
this.filtersState.set(config.presetFilters ?? {});
|
|
1001
|
+
this.sortState.set(this.normalizeSort(config.presetSort));
|
|
1002
|
+
this.routeParamsState.set(config.presetRouteParams ?? {});
|
|
1003
|
+
this.pageState.set(config.presetQuery?.page ?? 0);
|
|
1004
|
+
this.pageSizeState.set(config.presetQuery?.pageSize ?? 20);
|
|
1005
|
+
void this.refresh();
|
|
1006
|
+
}
|
|
1007
|
+
get page() {
|
|
1008
|
+
return untracked(this.pageState);
|
|
1009
|
+
}
|
|
1010
|
+
get pageSize() {
|
|
1011
|
+
return untracked(this.pageSizeState);
|
|
1012
|
+
}
|
|
1013
|
+
get totalElements() {
|
|
1014
|
+
return untracked(this.totalElementsState);
|
|
1015
|
+
}
|
|
1016
|
+
get filters() {
|
|
1017
|
+
return untracked(this.filtersState);
|
|
1018
|
+
}
|
|
1019
|
+
get query() {
|
|
1020
|
+
return untracked(this.queryState);
|
|
1021
|
+
}
|
|
1022
|
+
get sort() {
|
|
1023
|
+
return untracked(this.sortState);
|
|
1024
|
+
}
|
|
1025
|
+
get routeParams() {
|
|
1026
|
+
return untracked(this.routeParamsState);
|
|
1027
|
+
}
|
|
1028
|
+
set page(value) {
|
|
1029
|
+
this.pageState.set(value);
|
|
1030
|
+
}
|
|
1031
|
+
set pageSize(value) {
|
|
1032
|
+
this.pageSizeState.set(value);
|
|
1033
|
+
}
|
|
1034
|
+
set totalElements(value) {
|
|
1035
|
+
this.totalElementsState.set(value);
|
|
1036
|
+
}
|
|
1037
|
+
set filters(value) {
|
|
1038
|
+
this.filtersState.set(value);
|
|
1039
|
+
}
|
|
1040
|
+
set query(value) {
|
|
1041
|
+
this.queryState.set(value);
|
|
1042
|
+
}
|
|
1043
|
+
setData = (items, opts) => {
|
|
1044
|
+
const nextItems = opts?.replace === false ? [...this.allItemsState(), ...items] : [...items];
|
|
1045
|
+
this.allItemsState.set(nextItems);
|
|
1046
|
+
this.bumpVersion();
|
|
1047
|
+
return this.refresh({ page: 0 });
|
|
1048
|
+
};
|
|
1049
|
+
preload = this.setData;
|
|
1050
|
+
fetch = ({ filters = {}, query = {}, routeParams = {}, sort = this.sort } = {}) => {
|
|
1051
|
+
this.filtersState.set(filters);
|
|
1052
|
+
this.queryState.set(query);
|
|
1053
|
+
this.routeParamsState.set(routeParams);
|
|
1054
|
+
this.sortState.set(this.normalizeSort(sort));
|
|
1055
|
+
this.pageState.set(0);
|
|
1056
|
+
this.bumpVersion();
|
|
1057
|
+
return this.refresh();
|
|
1058
|
+
};
|
|
1059
|
+
refetchWith({ filters, query, sort } = {}) {
|
|
1060
|
+
const nextFilters = filters == null ? this.filters : { ...this.filters, ...filters };
|
|
1061
|
+
const nextQuery = query == null ? this.query : { ...this.query, ...query };
|
|
1062
|
+
const nextSort = sort == null ? this.sort : this.normalizeSort(sort);
|
|
1063
|
+
if (!deepEqual(this.filters, nextFilters) || !deepEqual(this.query, nextQuery) || !deepEqual(this.sort, nextSort)) {
|
|
1064
|
+
this.pageState.set(0);
|
|
1065
|
+
this.bumpVersion();
|
|
1066
|
+
}
|
|
1067
|
+
this.filtersState.set(nextFilters);
|
|
1068
|
+
this.queryState.set(nextQuery);
|
|
1069
|
+
this.sortState.set(nextSort);
|
|
1070
|
+
return this.refresh();
|
|
1071
|
+
}
|
|
1072
|
+
updatePage = (page = this.page, _options) => {
|
|
1073
|
+
this.pageState.set(typeof page === 'number' ? page : page.page);
|
|
1074
|
+
return this.refresh();
|
|
1075
|
+
};
|
|
1076
|
+
updatePageSize = (size = this.pageSize) => {
|
|
1077
|
+
this.pageSizeState.set(size);
|
|
1078
|
+
this.pageState.set(0);
|
|
1079
|
+
this.bumpVersion();
|
|
1080
|
+
return this.refresh();
|
|
1081
|
+
};
|
|
1082
|
+
updateByOffset = ({ page: pageNum, first = 0, rows = 0 } = {}, { query = this.query, sort = this.sort } = {}) => {
|
|
1083
|
+
const page = (pageNum ?? (first && rows && Math.floor(first / rows))) || 0;
|
|
1084
|
+
this.pageState.set(page);
|
|
1085
|
+
this.pageSizeState.set(rows || this.pageSize);
|
|
1086
|
+
this.queryState.set(query);
|
|
1087
|
+
this.sortState.set(this.normalizeSort(sort));
|
|
1088
|
+
return this.refresh();
|
|
1089
|
+
};
|
|
1090
|
+
setSort = (sort) => {
|
|
1091
|
+
this.sortState.set(this.normalizeSort(sort));
|
|
1092
|
+
};
|
|
1093
|
+
updateSort = (sort) => this.updateSorts(sort ? [sort] : []);
|
|
1094
|
+
updateSorts = (sort) => {
|
|
1095
|
+
this.sortState.set(this.normalizeSort(sort));
|
|
1096
|
+
this.pageState.set(0);
|
|
1097
|
+
this.bumpVersion();
|
|
1098
|
+
return this.refresh();
|
|
1099
|
+
};
|
|
1100
|
+
setRouteParams = (params = {}, opts = {}) => {
|
|
1101
|
+
this.routeParamsState.set(params);
|
|
1102
|
+
if (opts.reset) {
|
|
1103
|
+
this.pageState.set(0);
|
|
1104
|
+
this.bumpVersion();
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
updateConfig = (config) => {
|
|
1108
|
+
this.config = { ...this.config, ...config };
|
|
1109
|
+
this.prefetchMode = this.config.prefetchMode ?? 'sequential';
|
|
1110
|
+
if (config.presetFilters) {
|
|
1111
|
+
this.filtersState.set(config.presetFilters);
|
|
1112
|
+
}
|
|
1113
|
+
if (config.presetSort) {
|
|
1114
|
+
this.sortState.set(this.normalizeSort(config.presetSort));
|
|
1115
|
+
}
|
|
1116
|
+
if (config.presetRouteParams) {
|
|
1117
|
+
this.routeParamsState.set(config.presetRouteParams);
|
|
1118
|
+
}
|
|
1119
|
+
if (config.presetQuery?.page != null) {
|
|
1120
|
+
this.pageState.set(config.presetQuery.page);
|
|
1121
|
+
}
|
|
1122
|
+
if (config.presetQuery?.pageSize != null) {
|
|
1123
|
+
this.pageSizeState.set(config.presetQuery.pageSize);
|
|
1124
|
+
}
|
|
1125
|
+
void this.refresh();
|
|
1126
|
+
};
|
|
1127
|
+
copy(store) {
|
|
1128
|
+
this.config = { ...store.config };
|
|
1129
|
+
this.prefetchMode = store.prefetchMode;
|
|
1130
|
+
this.allItemsState.set(store.allItemsState());
|
|
1131
|
+
this.filtersState.set(store.filters);
|
|
1132
|
+
this.queryState.set(store.query);
|
|
1133
|
+
this.sortState.set([...store.sort]);
|
|
1134
|
+
this.routeParamsState.set(store.routeParams);
|
|
1135
|
+
this.pageState.set(store.page);
|
|
1136
|
+
this.pageSizeState.set(store.pageSize);
|
|
1137
|
+
this.totalElementsState.set(store.totalElements);
|
|
1138
|
+
this.cached.set(store.cached());
|
|
1139
|
+
this.items.set(store.items());
|
|
1140
|
+
this.bumpVersion();
|
|
1141
|
+
}
|
|
1142
|
+
destroy() { }
|
|
1143
|
+
async refresh(override) {
|
|
1144
|
+
const page = override?.page ?? this.page;
|
|
1145
|
+
const size = this.pageSize;
|
|
1146
|
+
const sort = this.sort;
|
|
1147
|
+
try {
|
|
1148
|
+
const resolved = await this.resolveItems({
|
|
1149
|
+
page,
|
|
1150
|
+
size,
|
|
1151
|
+
...this.filters,
|
|
1152
|
+
...this.query,
|
|
1153
|
+
sort,
|
|
1154
|
+
items: this.allItemsState(),
|
|
1155
|
+
routeParams: this.routeParams,
|
|
1156
|
+
});
|
|
1157
|
+
const sorted = this.applyClientSort(resolved, sort);
|
|
1158
|
+
const start = Math.max(0, page) * Math.max(1, size);
|
|
1159
|
+
const pageItems = sorted.slice(start, start + Math.max(1, size));
|
|
1160
|
+
this.pageState.set(page);
|
|
1161
|
+
this.totalElementsState.set(sorted.length);
|
|
1162
|
+
this.cached.set(sorted);
|
|
1163
|
+
this.items.set(pageItems);
|
|
1164
|
+
return pageItems;
|
|
1165
|
+
}
|
|
1166
|
+
catch (error) {
|
|
1167
|
+
this.error.set(error);
|
|
1168
|
+
throw error;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
resolveItems(input) {
|
|
1172
|
+
if (this.config.resolveItems) {
|
|
1173
|
+
return this.config.resolveItems(input);
|
|
1174
|
+
}
|
|
1175
|
+
return input.items;
|
|
1176
|
+
}
|
|
1177
|
+
applyClientSort(items, sort) {
|
|
1178
|
+
const clone = [...items];
|
|
1179
|
+
if (!sort.length) {
|
|
1180
|
+
return clone;
|
|
1181
|
+
}
|
|
1182
|
+
clone.sort((left, right) => {
|
|
1183
|
+
for (const rule of sort) {
|
|
1184
|
+
const result = this.compareValues(left[rule.sort], right[rule.sort]);
|
|
1185
|
+
if (result !== 0) {
|
|
1186
|
+
return rule.order === 'desc' ? -result : result;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return 0;
|
|
1190
|
+
});
|
|
1191
|
+
return clone;
|
|
1192
|
+
}
|
|
1193
|
+
compareValues(left, right) {
|
|
1194
|
+
if (left == null && right == null) {
|
|
1195
|
+
return 0;
|
|
1196
|
+
}
|
|
1197
|
+
if (left == null) {
|
|
1198
|
+
return -1;
|
|
1199
|
+
}
|
|
1200
|
+
if (right == null) {
|
|
1201
|
+
return 1;
|
|
1202
|
+
}
|
|
1203
|
+
if (typeof left === 'number' && typeof right === 'number') {
|
|
1204
|
+
return left - right;
|
|
1205
|
+
}
|
|
1206
|
+
if (left instanceof Date && right instanceof Date) {
|
|
1207
|
+
return left.getTime() - right.getTime();
|
|
1208
|
+
}
|
|
1209
|
+
return String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: 'base' });
|
|
1210
|
+
}
|
|
1211
|
+
normalizeSort(sort) {
|
|
1212
|
+
return normalizeSortInput(sort);
|
|
1213
|
+
}
|
|
1214
|
+
bumpVersion() {
|
|
1215
|
+
this.version.update((current) => current + 1);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
814
1219
|
// noinspection ES6PreferShortImport
|
|
815
1220
|
/**
|
|
816
1221
|
* Store for paginated data (tables/lists) with per-page cache and unified requests.
|
|
@@ -1126,6 +1531,7 @@ class PagedQueryStore {
|
|
|
1126
1531
|
const method = this.config.method || 'GET';
|
|
1127
1532
|
const normalizedSort = this.normalizeSort(sort);
|
|
1128
1533
|
const sortQueryKey = this.resolveSortQueryKey();
|
|
1534
|
+
// parseRequest works on the pre-serialization request model, so sort is still object-based here.
|
|
1129
1535
|
const requestPayload = mergeQueryParams(mergeQueryParams({ page, size }, filters), mergeQueryParams(query, normalizedSort.length ? { [sortQueryKey]: normalizedSort } : {}));
|
|
1130
1536
|
const fallbackQuery = method === 'GET' ? requestPayload : mergeQueryParams({ page, size }, query);
|
|
1131
1537
|
const rawQueries = this.config.parseRequest?.(requestPayload) || fallbackQuery;
|
|
@@ -1296,9 +1702,10 @@ class DictStore {
|
|
|
1296
1702
|
storage;
|
|
1297
1703
|
metaStorage;
|
|
1298
1704
|
ttlMs;
|
|
1299
|
-
|
|
1705
|
+
accessAutoLoadMode;
|
|
1300
1706
|
cacheUpdatedAt = signal(null, ...(ngDevMode ? [{ debugName: "cacheUpdatedAt" }] : /* istanbul ignore next */ []));
|
|
1301
1707
|
presetFilters = {};
|
|
1708
|
+
accessLoadQueued = false;
|
|
1302
1709
|
/**
|
|
1303
1710
|
* Search text.
|
|
1304
1711
|
* With `fixed: true` filters the local cache; with `fixed: false` triggers server search.
|
|
@@ -1315,6 +1722,7 @@ class DictStore {
|
|
|
1315
1722
|
* Source — local cache (fixed=true) or data from `PagedQueryStore`.
|
|
1316
1723
|
*/
|
|
1317
1724
|
items = computed(() => {
|
|
1725
|
+
this.maybeScheduleAccessLoad();
|
|
1318
1726
|
const cached = this.cachedItems();
|
|
1319
1727
|
if (!this.fixed) {
|
|
1320
1728
|
return this.debouncedSearchText() || !cached.length ? this.#helper.items() : cached;
|
|
@@ -1337,7 +1745,7 @@ class DictStore {
|
|
|
1337
1745
|
* @param storageKey key for saving cache in the selected strategy
|
|
1338
1746
|
* @param cfg behavior (fixed/search, parsers, label/value keys, cache strategy, etc.)
|
|
1339
1747
|
*/
|
|
1340
|
-
constructor(apiUrl, storageKey, { autoLoad = true, method = this.defaultConfig.defaultRestMethod, presetFilters = this.defaultConfig.defaultPresetFilters, parseResponse, parseRequest, debounceTime = this.defaultConfig.defaultDebounceTime, ttlMs = this.defaultConfig.defaultTtlMs,
|
|
1748
|
+
constructor(apiUrl, storageKey, { autoLoad = this.defaultConfig.defaultAutoLoad ?? true, method = this.defaultConfig.defaultRestMethod, presetFilters = this.defaultConfig.defaultPresetFilters, parseResponse, parseRequest, debounceTime = this.defaultConfig.defaultDebounceTime, ttlMs = this.defaultConfig.defaultTtlMs, fixed = true, maxOptionsSize = this.defaultConfig.defaultMaxOptionsSize, labelKey = this.defaultConfig.defaultLabelKey || 'name', valueKey = this.defaultConfig.defaultValueKey || 'code', keyPrefix = this.defaultConfig.defaultPrefix || 're', cacheStrategy = this.defaultConfig.defaultCacheStrategy || 'persist', }) {
|
|
1341
1749
|
this.apiUrl = apiUrl;
|
|
1342
1750
|
this.storageKey = storageKey;
|
|
1343
1751
|
const searchDebounce = debounceTime ?? 300;
|
|
@@ -1352,7 +1760,7 @@ class DictStore {
|
|
|
1352
1760
|
});
|
|
1353
1761
|
this.fixed = fixed;
|
|
1354
1762
|
this.ttlMs = ttlMs;
|
|
1355
|
-
this.
|
|
1763
|
+
this.accessAutoLoadMode = autoLoad === 'onAccess';
|
|
1356
1764
|
this.labelKey = labelKey;
|
|
1357
1765
|
this.valueKey = valueKey;
|
|
1358
1766
|
this.maxOptionsSize = maxOptionsSize;
|
|
@@ -1414,6 +1822,16 @@ class DictStore {
|
|
|
1414
1822
|
this.searchText.set(name);
|
|
1415
1823
|
this.filters.set(filters);
|
|
1416
1824
|
};
|
|
1825
|
+
/**
|
|
1826
|
+
* Explicitly arms the store and loads data only when it is actually needed.
|
|
1827
|
+
*
|
|
1828
|
+
* Useful for dropdown/popup flows where eager constructor-time requests would
|
|
1829
|
+
* create too many parallel dictionary loads.
|
|
1830
|
+
*/
|
|
1831
|
+
ensureLoaded = (filters = this.filters()) => {
|
|
1832
|
+
this._armed.set(true);
|
|
1833
|
+
this.filters.set(filters);
|
|
1834
|
+
};
|
|
1417
1835
|
/**
|
|
1418
1836
|
* Find display label by value (typically for reverse binding).
|
|
1419
1837
|
* @returns label string or `undefined` if not found
|
|
@@ -1538,17 +1956,45 @@ class DictStore {
|
|
|
1538
1956
|
setAutoload(autoLoad) {
|
|
1539
1957
|
if (autoLoad === 'whenEmpty') {
|
|
1540
1958
|
const isEmpty = !this.cachedItems().length;
|
|
1541
|
-
this._armed.set(isEmpty || this.
|
|
1959
|
+
this._armed.set(isEmpty || this.isCacheStale());
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
if (autoLoad === 'onDemand' || autoLoad === 'onAccess') {
|
|
1963
|
+
this._armed.set(false);
|
|
1964
|
+
return;
|
|
1542
1965
|
}
|
|
1543
1966
|
else {
|
|
1544
1967
|
this._armed.set(autoLoad);
|
|
1545
1968
|
}
|
|
1546
1969
|
}
|
|
1547
1970
|
shouldFetchFixedCache() {
|
|
1548
|
-
return !this.cachedItems().length || this.
|
|
1971
|
+
return !this.cachedItems().length || this.isCacheStale();
|
|
1549
1972
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1973
|
+
maybeScheduleAccessLoad() {
|
|
1974
|
+
if (!this.accessAutoLoadMode || this._armed() || this.accessLoadQueued) {
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
if (!this.shouldFetchOnAccessRead()) {
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
this.accessLoadQueued = true;
|
|
1981
|
+
// Note: this is an intentionally dirty opt-in mode.
|
|
1982
|
+
// We never mutate `_armed` synchronously inside `computed()`, because reads should stay pure by default.
|
|
1983
|
+
// Instead, `autoLoad: 'onAccess'` schedules a single deferred activation after the first read of
|
|
1984
|
+
// `items()` / `options()`. This keeps the hack isolated and prevents request storms from repeated reads
|
|
1985
|
+
// during the same render/effect turn.
|
|
1986
|
+
queueMicrotask(() => {
|
|
1987
|
+
this.accessLoadQueued = false;
|
|
1988
|
+
if (!this._armed() && this.shouldFetchOnAccessRead()) {
|
|
1989
|
+
this.ensureLoaded();
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
shouldFetchOnAccessRead() {
|
|
1994
|
+
if (this.fixed) {
|
|
1995
|
+
return this.shouldFetchFixedCache();
|
|
1996
|
+
}
|
|
1997
|
+
return this.#helper.items().length === 0 && !this.cachedItems().length;
|
|
1552
1998
|
}
|
|
1553
1999
|
isCacheStale() {
|
|
1554
2000
|
if (!this.cachedItems().length) {
|
|
@@ -1817,5 +2263,5 @@ class EntityStore {
|
|
|
1817
2263
|
* Generated bundle index. Do not edit.
|
|
1818
2264
|
*/
|
|
1819
2265
|
|
|
1820
|
-
export { AbortError, CacheMissError, DictLocalStore, DictStore, EntityStore, PagedQueryStore, RESOURCE_PROFILES, ResourceStore, STATUM_CONFIG, createBodySerializer, createQuerySerializer, createResourceProfile, createStrictSerializer, isAbort, provideStatum };
|
|
2266
|
+
export { AbortError, CacheMissError, DictLocalStore, DictStore, EntityStore, PagedQueryLocalStore, PagedQueryStore, RESOURCE_PROFILES, ResourceMockStore, ResourceStore, STATUM_CONFIG, createBodySerializer, createQuerySerializer, createResourceProfile, createStrictSerializer, isAbort, provideStatum };
|
|
1821
2267
|
//# sourceMappingURL=reforgium-statum.mjs.map
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DataType, SerializerConfig, Serializer, RestMethods, AnyDict, AnyType, QueryParams,
|
|
1
|
+
import { DataType, SerializerConfig, Serializer, RestMethods, AnyDict, AnyType, QueryParams, PageableResponse, StorageStrategy } from '@reforgium/internal';
|
|
2
2
|
export { DataType, FieldConcatType, FieldConfig, FormatConfig, LocalStorage, LruCache, MemoryStorage, ParseFormatConfig, SerializedType, Serializer, SerializerConfig, SerializerFieldError, SessionStorage, StorageInterface, StorageStrategy, StorageStrategyOptions, Types, storageStrategy } from '@reforgium/internal';
|
|
3
3
|
import * as _angular_core from '@angular/core';
|
|
4
4
|
import { Signal, WritableSignal, EnvironmentProviders, InjectionToken } from '@angular/core';
|
|
@@ -168,6 +168,51 @@ type ResourceTraceEvent = {
|
|
|
168
168
|
error?: unknown;
|
|
169
169
|
};
|
|
170
170
|
|
|
171
|
+
type ResourceMockHandlerContext<Data, Param extends SimpleDict = SimpleDict, Query extends QueryDict = QueryDict, Payload extends PayloadData = PayloadData> = {
|
|
172
|
+
method: RestMethods;
|
|
173
|
+
route?: string;
|
|
174
|
+
url: string;
|
|
175
|
+
args: CallArgs<Param, Query, Payload>;
|
|
176
|
+
current: Data | null;
|
|
177
|
+
};
|
|
178
|
+
type ResourceMockHandler<Data, Result = Data, Param extends SimpleDict = SimpleDict, Query extends QueryDict = QueryDict, Payload extends PayloadData = PayloadData> = (ctx: ResourceMockHandlerContext<Data, Param, Query, Payload>) => Result | Promise<Result>;
|
|
179
|
+
type ResourceMockHandlers<Data> = Partial<Record<RestMethods, ResourceMockHandler<Data, AnyType>>>;
|
|
180
|
+
type ResourceMockStoreOptions<Data> = Pick<ResourceStoreOptions, 'baseUrl' | 'delay' | 'onTrace'> & {
|
|
181
|
+
seed?: Data | null;
|
|
182
|
+
handlers?: ResourceMockHandlers<Data>;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Lightweight in-memory counterpart of `ResourceStore`.
|
|
186
|
+
*
|
|
187
|
+
* It preserves the result-facing API (`value/status/error/loading` + CRUD methods),
|
|
188
|
+
* but does not try to reproduce transport, retry, or per-key cache internals.
|
|
189
|
+
*/
|
|
190
|
+
declare class ResourceMockStore<Data> {
|
|
191
|
+
#private;
|
|
192
|
+
readonly value: Signal<Data | null>;
|
|
193
|
+
readonly status: Signal<ResourceStatus>;
|
|
194
|
+
readonly error: Signal<unknown | null>;
|
|
195
|
+
readonly loading: Signal<boolean>;
|
|
196
|
+
private readonly routes;
|
|
197
|
+
private readonly opts;
|
|
198
|
+
constructor(routes?: ResourceRoutesMap, opts?: ResourceMockStoreOptions<Data>);
|
|
199
|
+
setValue(value: Data | null): void;
|
|
200
|
+
patchValue(patch: Partial<Data>): void;
|
|
201
|
+
reset(value?: Data | null): void;
|
|
202
|
+
get<Param extends SimpleDict = SimpleDict, Query extends QueryDict = QueryDict, Response extends AnyType = Data>(args: CallArgs<Param, Query, never>, cfg?: GetCallConfig<Response, Data>): Promise<Data>;
|
|
203
|
+
post<Param extends SimpleDict = SimpleDict, Payload extends PayloadData = PayloadData, Query extends QueryDict = QueryDict, Response extends AnyType = AnyType>(args: CallArgs<Param, Query, Payload>, cfg?: CallConfig<Response, Response>): Promise<Response>;
|
|
204
|
+
put<Param extends SimpleDict = SimpleDict, Payload extends PayloadData = PayloadData, Query extends QueryDict = QueryDict, Response extends AnyType = AnyType>(args: CallArgs<Param, Query, Partial<Payload>>, cfg?: CallConfig<Response, Response>): Promise<Response>;
|
|
205
|
+
patch<Param extends SimpleDict = SimpleDict, Payload extends PayloadData = PayloadData, Query extends QueryDict = QueryDict, Response extends AnyType = AnyType>(args: CallArgs<Param, Query, Partial<Payload>>, cfg?: CallConfig<Response, Response>): Promise<Response>;
|
|
206
|
+
delete<Param extends SimpleDict = SimpleDict, Payload extends PayloadData = PayloadData, Query extends QueryDict = QueryDict, Response extends AnyType = AnyType>(args: CallArgs<Param, Query, Payload>, cfg?: CallConfig<Response, Response>): Promise<Response>;
|
|
207
|
+
abort(): void;
|
|
208
|
+
abortAll(): void;
|
|
209
|
+
private runMutation;
|
|
210
|
+
private resolveMutation;
|
|
211
|
+
private run;
|
|
212
|
+
private buildUrl;
|
|
213
|
+
private trace;
|
|
214
|
+
}
|
|
215
|
+
|
|
171
216
|
/**
|
|
172
217
|
* Store for REST resources with caching and request deduplication.
|
|
173
218
|
*
|
|
@@ -358,6 +403,10 @@ type QuerySortRule<Key extends string = string> = {
|
|
|
358
403
|
order: QuerySortOrder;
|
|
359
404
|
};
|
|
360
405
|
type QuerySortInput<Key extends string = string> = QuerySortRule<Key> | ReadonlyArray<QuerySortRule<Key>> | null | undefined;
|
|
406
|
+
type PagedQueryRequestInput<FilterType extends AnyDict = AnyDict> = Omit<QueryParams<FilterType>, 'page' | 'sort'> & {
|
|
407
|
+
page: number;
|
|
408
|
+
sort?: QuerySortInput;
|
|
409
|
+
} & Partial<FilterType> & AnyDict;
|
|
361
410
|
type PagedQueryConcurrency = 'latest-wins' | 'parallel';
|
|
362
411
|
type OffsetPaginationType = {
|
|
363
412
|
page?: number;
|
|
@@ -433,7 +482,7 @@ type PagedQueryStoreConfig<ItemsType extends object, FilterType extends AnyDict
|
|
|
433
482
|
* Useful for mapping `page/pageSize` and selected filter fields to API-specific query keys.
|
|
434
483
|
* Returned object can contain nullable values; they are filtered before the transport call.
|
|
435
484
|
*/
|
|
436
|
-
parseRequest?: BivariantCallback<
|
|
485
|
+
parseRequest?: BivariantCallback<PagedQueryRequestInput<FilterType>, AnyDict>;
|
|
437
486
|
/**
|
|
438
487
|
* Custom parser of API response into unified `PageableResponse<ItemsType>`.
|
|
439
488
|
* Use if the server returns an array or a non-standard structure.
|
|
@@ -470,6 +519,85 @@ type PagedQueryStoreProviderConfig = {
|
|
|
470
519
|
defaultDisableCacheLimit?: boolean;
|
|
471
520
|
};
|
|
472
521
|
|
|
522
|
+
type PagedQueryLocalResolverInput<ItemsType extends AnyDict, FilterType extends AnyDict = AnyDict> = PagedQueryRequestInput<FilterType> & {
|
|
523
|
+
items: readonly ItemsType[];
|
|
524
|
+
routeParams: AnyDict;
|
|
525
|
+
};
|
|
526
|
+
type PagedQueryLocalResolverResult<ItemsType extends AnyDict> = readonly ItemsType[] | Promise<readonly ItemsType[]>;
|
|
527
|
+
type PagedQueryLocalStoreConfig<ItemsType extends AnyDict, FilterType extends AnyDict = AnyDict> = {
|
|
528
|
+
presetQuery?: {
|
|
529
|
+
page?: number;
|
|
530
|
+
pageSize?: number;
|
|
531
|
+
};
|
|
532
|
+
presetFilters?: Partial<FilterType>;
|
|
533
|
+
presetSort?: ReadonlyArray<QuerySortRule>;
|
|
534
|
+
presetRouteParams?: AnyDict;
|
|
535
|
+
prefetchMode?: 'sequential' | 'parallel';
|
|
536
|
+
resolveItems?: (input: PagedQueryLocalResolverInput<ItemsType, FilterType>) => PagedQueryLocalResolverResult<ItemsType>;
|
|
537
|
+
};
|
|
538
|
+
/**
|
|
539
|
+
* Lightweight local counterpart of `PagedQueryStore`.
|
|
540
|
+
*
|
|
541
|
+
* It keeps the same result-facing shape needed by consumers and `DataGrid source`,
|
|
542
|
+
* but resolves pages from an in-memory collection instead of HTTP.
|
|
543
|
+
*/
|
|
544
|
+
declare class PagedQueryLocalStore<ItemsType extends AnyDict, FilterType extends AnyDict = AnyDict> {
|
|
545
|
+
readonly items: _angular_core.WritableSignal<ItemsType[]>;
|
|
546
|
+
readonly cached: _angular_core.WritableSignal<ItemsType[]>;
|
|
547
|
+
readonly loading: _angular_core.WritableSignal<boolean>;
|
|
548
|
+
readonly error: _angular_core.WritableSignal<unknown>;
|
|
549
|
+
readonly version: _angular_core.WritableSignal<number>;
|
|
550
|
+
readonly allItemsState: _angular_core.WritableSignal<readonly ItemsType[]>;
|
|
551
|
+
readonly pageState: _angular_core.WritableSignal<number>;
|
|
552
|
+
readonly pageSizeState: _angular_core.WritableSignal<number>;
|
|
553
|
+
readonly totalElementsState: _angular_core.WritableSignal<number | undefined>;
|
|
554
|
+
readonly filtersState: _angular_core.WritableSignal<Partial<FilterType>>;
|
|
555
|
+
readonly queryState: _angular_core.WritableSignal<AnyDict>;
|
|
556
|
+
readonly sortState: _angular_core.WritableSignal<QuerySortRule[]>;
|
|
557
|
+
readonly routeParamsState: _angular_core.WritableSignal<AnyDict>;
|
|
558
|
+
config: PagedQueryLocalStoreConfig<ItemsType, FilterType>;
|
|
559
|
+
prefetchMode: 'sequential' | 'parallel';
|
|
560
|
+
constructor(items?: readonly ItemsType[], config?: PagedQueryLocalStoreConfig<ItemsType, FilterType>);
|
|
561
|
+
get page(): number;
|
|
562
|
+
get pageSize(): number;
|
|
563
|
+
get totalElements(): number | undefined;
|
|
564
|
+
get filters(): Partial<FilterType>;
|
|
565
|
+
get query(): AnyDict;
|
|
566
|
+
get sort(): QuerySortRule[];
|
|
567
|
+
get routeParams(): AnyDict;
|
|
568
|
+
set page(value: number);
|
|
569
|
+
set pageSize(value: number);
|
|
570
|
+
set totalElements(value: number | undefined);
|
|
571
|
+
set filters(value: Partial<FilterType>);
|
|
572
|
+
set query(value: AnyDict);
|
|
573
|
+
setData: (items: readonly ItemsType[], opts?: {
|
|
574
|
+
replace?: boolean;
|
|
575
|
+
}) => Promise<ItemsType[]>;
|
|
576
|
+
preload: (items: readonly ItemsType[], opts?: {
|
|
577
|
+
replace?: boolean;
|
|
578
|
+
}) => Promise<ItemsType[]>;
|
|
579
|
+
fetch: ({ filters, query, routeParams, sort }?: FetchInput<FilterType>) => Promise<ItemsType[]>;
|
|
580
|
+
refetchWith({ filters, query, sort }?: RefetchWithInput<FilterType>): Promise<ItemsType[]>;
|
|
581
|
+
updatePage: (page?: number | {
|
|
582
|
+
page: number;
|
|
583
|
+
}, _options?: UpdatePageOptions) => Promise<ItemsType[]>;
|
|
584
|
+
updatePageSize: (size?: number) => Promise<ItemsType[]>;
|
|
585
|
+
updateByOffset: ({ page: pageNum, first, rows }?: OffsetPaginationType, { query, sort }?: UpdateByOffsetOptions) => Promise<ItemsType[]>;
|
|
586
|
+
setSort: (sort?: QuerySortInput) => void;
|
|
587
|
+
updateSort: (sort?: QuerySortRule | null) => Promise<ItemsType[]>;
|
|
588
|
+
updateSorts: (sort?: ReadonlyArray<QuerySortRule> | null) => Promise<ItemsType[]>;
|
|
589
|
+
setRouteParams: (params?: AnyDict, opts?: SetRouteParamsOptions) => void;
|
|
590
|
+
updateConfig: (config: PagedQueryLocalStoreConfig<ItemsType, FilterType>) => void;
|
|
591
|
+
copy(store: PagedQueryLocalStore<ItemsType, FilterType>): void;
|
|
592
|
+
destroy(): void;
|
|
593
|
+
private refresh;
|
|
594
|
+
private resolveItems;
|
|
595
|
+
private applyClientSort;
|
|
596
|
+
private compareValues;
|
|
597
|
+
private normalizeSort;
|
|
598
|
+
private bumpVersion;
|
|
599
|
+
}
|
|
600
|
+
|
|
473
601
|
/**
|
|
474
602
|
* Store for paginated data (tables/lists) with per-page cache and unified requests.
|
|
475
603
|
*
|
|
@@ -644,13 +772,21 @@ type DictStoreConfig<ItemsType extends object> = {
|
|
|
644
772
|
keyPrefix?: string;
|
|
645
773
|
/** Initial filters (added to the first request/filtering). */
|
|
646
774
|
presetFilters?: Record<string, string>;
|
|
647
|
-
/**
|
|
648
|
-
|
|
775
|
+
/**
|
|
776
|
+
* Autoload data on initialization (`true` by default).
|
|
777
|
+
*
|
|
778
|
+
* - `true`: arm immediately on construction
|
|
779
|
+
* - `false`: stay passive until `search(...)` or `ensureLoaded()`
|
|
780
|
+
* - `'whenEmpty'`: arm on init only when cache is empty or stale
|
|
781
|
+
* - `'onDemand'`: never arm on init; load only after an explicit `ensureLoaded()` / `search(...)`
|
|
782
|
+
* - `'onAccess'`: dirty opt-in mode; first read of `items()` / `options()` lazily schedules loading
|
|
783
|
+
*/
|
|
784
|
+
autoLoad?: boolean | 'whenEmpty' | 'onDemand' | 'onAccess';
|
|
649
785
|
/**
|
|
650
786
|
* Custom mapper of pagination/sort request into query params.
|
|
651
787
|
* Useful if the API expects non-standard field names.
|
|
652
788
|
*/
|
|
653
|
-
parseRequest?: (data:
|
|
789
|
+
parseRequest?: (data: PagedQueryRequestInput<FilterType>) => AnyDict;
|
|
654
790
|
/**
|
|
655
791
|
* Custom parser of server response into a unified PageableResponse.
|
|
656
792
|
* Needed if the API returns an array or a "non-standard" structure.
|
|
@@ -662,7 +798,10 @@ type DictStoreConfig<ItemsType extends object> = {
|
|
|
662
798
|
debounceTime?: number;
|
|
663
799
|
/** Cache freshness window (ms). Undefined means restored cache does not expire. */
|
|
664
800
|
ttlMs?: number;
|
|
665
|
-
/**
|
|
801
|
+
/**
|
|
802
|
+
* @deprecated `DictStore` now revalidates stale cache whenever `ttlMs` is set.
|
|
803
|
+
* Pass `ttlMs: undefined` to disable time-based refresh semantics entirely.
|
|
804
|
+
*/
|
|
666
805
|
revalidate?: boolean;
|
|
667
806
|
/** Maximum number of options exposed (truncates `options`). */
|
|
668
807
|
maxOptionsSize?: number;
|
|
@@ -708,13 +847,18 @@ type DictStoreProviderConfig = {
|
|
|
708
847
|
defaultPrefix?: string;
|
|
709
848
|
/** Initial filters (added to the first request/filtering). */
|
|
710
849
|
defaultPresetFilters?: DictStoreConfig<AnyType>['presetFilters'];
|
|
850
|
+
/** Default autoload policy for dictionary stores. */
|
|
851
|
+
defaultAutoLoad?: DictStoreConfig<AnyType>['autoLoad'];
|
|
711
852
|
/** Transport method for fetching the dictionary. Defaults to 'POST'. */
|
|
712
853
|
defaultRestMethod?: DictStoreConfig<AnyType>['method'];
|
|
713
854
|
/** Debounce delay before request (ms) — for frequent input/search. */
|
|
714
855
|
defaultDebounceTime?: DictStoreConfig<AnyType>['debounceTime'];
|
|
715
856
|
/** Cache freshness window (ms). Undefined means restored cache does not expire. */
|
|
716
857
|
defaultTtlMs?: DictStoreConfig<AnyType>['ttlMs'];
|
|
717
|
-
/**
|
|
858
|
+
/**
|
|
859
|
+
* @deprecated `DictStore` now revalidates stale cache whenever `defaultTtlMs` is set.
|
|
860
|
+
* Keep this field only for backward compatibility.
|
|
861
|
+
*/
|
|
718
862
|
defaultRevalidate?: DictStoreConfig<AnyType>['revalidate'];
|
|
719
863
|
/** Maximum number of options exposed (truncates `options`). */
|
|
720
864
|
defaultMaxOptionsSize?: DictStoreConfig<AnyType>['maxOptionsSize'];
|
|
@@ -724,6 +868,9 @@ type DictStoreProviderConfig = {
|
|
|
724
868
|
defaultValueKey?: DictStoreConfig<AnyType>['valueKey'];
|
|
725
869
|
};
|
|
726
870
|
type ValueType = string[] | string | number | null | undefined;
|
|
871
|
+
type FilterType = {
|
|
872
|
+
name: string;
|
|
873
|
+
};
|
|
727
874
|
|
|
728
875
|
/**
|
|
729
876
|
* Dictionary store (select/options) with local LRU cache and optional persistence.
|
|
@@ -760,9 +907,10 @@ declare class DictStore<Type extends AnyDict> {
|
|
|
760
907
|
private readonly storage?;
|
|
761
908
|
private readonly metaStorage?;
|
|
762
909
|
private readonly ttlMs?;
|
|
763
|
-
private readonly
|
|
910
|
+
private readonly accessAutoLoadMode;
|
|
764
911
|
private readonly cacheUpdatedAt;
|
|
765
912
|
private readonly presetFilters;
|
|
913
|
+
private accessLoadQueued;
|
|
766
914
|
/**
|
|
767
915
|
* Search text.
|
|
768
916
|
* With `fixed: true` filters the local cache; with `fixed: false` triggers server search.
|
|
@@ -794,7 +942,7 @@ declare class DictStore<Type extends AnyDict> {
|
|
|
794
942
|
* @param storageKey key for saving cache in the selected strategy
|
|
795
943
|
* @param cfg behavior (fixed/search, parsers, label/value keys, cache strategy, etc.)
|
|
796
944
|
*/
|
|
797
|
-
constructor(apiUrl: string, storageKey: string, { autoLoad, method, presetFilters, parseResponse, parseRequest, debounceTime, ttlMs,
|
|
945
|
+
constructor(apiUrl: string, storageKey: string, { autoLoad, method, presetFilters, parseResponse, parseRequest, debounceTime, ttlMs, fixed, maxOptionsSize, labelKey, valueKey, keyPrefix, cacheStrategy, }: DictStoreConfig<Type>);
|
|
798
946
|
/** Restore cache from the selected storage (`persist`/`session`/`lru`/`memory`). */
|
|
799
947
|
restoreCache(): void;
|
|
800
948
|
clearCache(): void;
|
|
@@ -803,6 +951,13 @@ declare class DictStore<Type extends AnyDict> {
|
|
|
803
951
|
* With `fixed: false` initiates server search; with `fixed: true` — local filtering.
|
|
804
952
|
*/
|
|
805
953
|
search: (name?: string, filters?: AnyDict) => void;
|
|
954
|
+
/**
|
|
955
|
+
* Explicitly arms the store and loads data only when it is actually needed.
|
|
956
|
+
*
|
|
957
|
+
* Useful for dropdown/popup flows where eager constructor-time requests would
|
|
958
|
+
* create too many parallel dictionary loads.
|
|
959
|
+
*/
|
|
960
|
+
ensureLoaded: (filters?: AnyDict) => void;
|
|
806
961
|
/**
|
|
807
962
|
* Find display label by value (typically for reverse binding).
|
|
808
963
|
* @returns label string or `undefined` if not found
|
|
@@ -829,7 +984,8 @@ declare class DictStore<Type extends AnyDict> {
|
|
|
829
984
|
private keyOf;
|
|
830
985
|
private setAutoload;
|
|
831
986
|
private shouldFetchFixedCache;
|
|
832
|
-
private
|
|
987
|
+
private maybeScheduleAccessLoad;
|
|
988
|
+
private shouldFetchOnAccessRead;
|
|
833
989
|
private isCacheStale;
|
|
834
990
|
private metaKey;
|
|
835
991
|
}
|
|
@@ -951,6 +1107,6 @@ type StatumConfig = PagedQueryProviderConfig & SerializerProviderConfig & DictPr
|
|
|
951
1107
|
declare const STATUM_CONFIG: InjectionToken<StatumConfig>;
|
|
952
1108
|
declare const provideStatum: (config: StatumConfig) => EnvironmentProviders;
|
|
953
1109
|
|
|
954
|
-
export { AbortError, CacheMissError, DictLocalStore, DictStore, EntityStore, PagedQueryStore, RESOURCE_PROFILES, ResourceStore, STATUM_CONFIG, createBodySerializer, createQuerySerializer, createResourceProfile, createStrictSerializer, isAbort, provideStatum };
|
|
955
|
-
export type { DictLocalConfig, DictStoreConfig, DictStoreProviderConfig, EntityId, EntityStoreConfig, FetchInput, FetchParams, OffsetPaginationType, PagedQueryStoreConfig, PagedQueryStoreProviderConfig, QuerySortInput, QuerySortOrder, QuerySortRule, RefetchWithInput, ResourceProfileName, ResourceRoutesMap, ResourceStatus, ResourceStoreOptions, ResourceTraceEvent, RetryConfig, SetRouteParamsOptions, StatumConfig, UpdateByOffsetOptions, UpdatePageInput, UpdatePageOptions };
|
|
1110
|
+
export { AbortError, CacheMissError, DictLocalStore, DictStore, EntityStore, PagedQueryLocalStore, PagedQueryStore, RESOURCE_PROFILES, ResourceMockStore, ResourceStore, STATUM_CONFIG, createBodySerializer, createQuerySerializer, createResourceProfile, createStrictSerializer, isAbort, provideStatum };
|
|
1111
|
+
export type { DictLocalConfig, DictStoreConfig, DictStoreProviderConfig, EntityId, EntityStoreConfig, FetchInput, FetchParams, OffsetPaginationType, PagedQueryLocalResolverInput, PagedQueryLocalResolverResult, PagedQueryLocalStoreConfig, PagedQueryRequestInput, PagedQueryStoreConfig, PagedQueryStoreProviderConfig, QuerySortInput, QuerySortOrder, QuerySortRule, RefetchWithInput, ResourceMockHandler, ResourceMockHandlerContext, ResourceMockHandlers, ResourceMockStoreOptions, ResourceProfileName, ResourceRoutesMap, ResourceStatus, ResourceStoreOptions, ResourceTraceEvent, RetryConfig, SetRouteParamsOptions, StatumConfig, UpdateByOffsetOptions, UpdatePageInput, UpdatePageOptions };
|
|
956
1112
|
//# sourceMappingURL=reforgium-statum.d.ts.map
|