@reforgium/statum 3.0.1 → 3.1.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 +203 -0
- package/LICENSE +21 -0
- package/README.md +23 -1
- package/fesm2022/reforgium-statum.mjs +118 -67
- package/package.json +1 -1
- package/types/reforgium-statum.d.ts +41 -35
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
## [3.1.0]: 2026-04-01
|
|
2
|
+
|
|
3
|
+
### Feat:
|
|
4
|
+
- `ResourceStore`: added `observe` option on per-call config; set `observe: 'response'` to pass the full `HttpResponse<T>` to `parseResponse` instead of just the body
|
|
5
|
+
- `ResourceStore`: added request-body pass-through for `FormData`, `Blob`, and `ArrayBuffer`
|
|
6
|
+
- `ResourceStore`: added optional retry jitter (`+-25%`) on top of constant/exponential backoff
|
|
7
|
+
|
|
8
|
+
### Refactor:
|
|
9
|
+
- `PagedQueryStore`: removed `@deprecated` annotations from direct state setters (`page`, `pageSize`, `filters`, `query`, `totalElements`); setters remain available for low-level integration scenarios
|
|
10
|
+
|
|
11
|
+
### Fix:
|
|
12
|
+
- `ResourceStore`: aligned query generics with actual transport behavior; query args now support booleans and arrays without reusing payload constraints
|
|
13
|
+
- `ResourceStore`: GET cache lookups now refresh LRU order, while `cache-only` misses no longer create empty cache entries
|
|
14
|
+
- `ResourceStore`: abort and abortAll now cancel underlying `HttpClient` requests via `AbortController` and correctly recognize browser `AbortError`
|
|
15
|
+
- `ResourceStore`: request state promotion and loading cleanup were stabilized for abort and retry flows, including non-promoted entries
|
|
16
|
+
- `PagedQueryStore`: `presetQuery.page` is now optional and defaults to `0`
|
|
17
|
+
- `PagedQueryStore`: `parseRequest` typing now accepts concrete filter dictionaries without conflicting with `AnyDict` call sites
|
|
18
|
+
- `Serializer`: deep object serialization now skips circular references instead of throwing
|
|
19
|
+
|
|
20
|
+
### Test:
|
|
21
|
+
- `ResourceStore`: added `observe` coverage for cache-first hit, HTTP error propagation, and PUT/PATCH/DELETE methods
|
|
22
|
+
- `ResourceStore`: added coverage for retry backoff/jitter, payload pass-through, abort edge cases, forced eviction, and hot-key LRU behavior
|
|
23
|
+
- `DictStore`, `DictLocalStore`, and `EntityStore`: added regression coverage for uncovered edge scenarios
|
|
24
|
+
- `Serializer`: added regression coverage for direct and indirect circular references plus repeated shared-object serialization
|
|
25
|
+
|
|
26
|
+
### Docs:
|
|
27
|
+
- `README`: documented `observe: 'response'` behavior and clarified direct setter usage in `PagedQueryStore`
|
|
28
|
+
|
|
29
|
+
### Chore:
|
|
30
|
+
- package assets now include `CHANGELOG.md` and `LICENSE`
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## [3.0.1]: 2026-03-31
|
|
35
|
+
|
|
36
|
+
### Feat:
|
|
37
|
+
- `ResourceStore`: added `observe` option on per-call config - set `observe: 'response'` to pass the full `HttpResponse<T>` to `parseResponse` instead of just the body; useful for reading response headers or status codes
|
|
38
|
+
|
|
39
|
+
### Refactor:
|
|
40
|
+
- `PagedQueryStore`: removed `@deprecated` annotations from direct state setters (`page`, `pageSize`, `filters`, `query`, `totalElements`); setters remain available for low-level integration scenarios (e.g., external data-grid source contracts)
|
|
41
|
+
|
|
42
|
+
### Test:
|
|
43
|
+
- `ResourceStore`: added `observe` coverage for cache-first hit, HTTP error propagation, and PUT/PATCH/DELETE methods
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## [3.0.0]: 2026-03-17
|
|
48
|
+
|
|
49
|
+
### Feat:
|
|
50
|
+
- `PagedQueryStore`: added first-class sorting state and methods (`setSort`, `updateSort`, `updateSorts`) with standard repeated query serialization (`sort=a,asc&sort=b,desc`)
|
|
51
|
+
- `PagedQueryStore`: added reactive metadata signals (`pageState`, `pageSizeState`, `totalElementsState`, `filtersState`, `queryState`, `sortState`, `routeParamsState`)
|
|
52
|
+
- `PagedQueryStore`: added reactive `error` signal
|
|
53
|
+
- `PagedQueryStore`: added configurable request concurrency (`latest-wins` by default, optional `parallel`)
|
|
54
|
+
- `PagedQueryStore`: added `baseUrl` config option and `defaultBaseUrl` provider default for routing requests through a shared API prefix
|
|
55
|
+
- `PagedQueryStore`: added `disableCacheLimit` config flag to disable LRU eviction and keep all loaded pages in memory
|
|
56
|
+
- `PagedQueryStoreProviderConfig`: added `defaultDisableCacheLimit` global default
|
|
57
|
+
- `DictStore`: added cache freshness controls (`ttlMs`, `revalidate`) and provider defaults (`defaultTtlMs`, `defaultRevalidate`)
|
|
58
|
+
- `DictStore`: added `clearCache()`
|
|
59
|
+
- `Serializer`: added per-field array query serialization override via `mapFields.<key>.concatType`
|
|
60
|
+
|
|
61
|
+
### Refactor:
|
|
62
|
+
- `PagedQueryStore`: route params are now exposed only via `routeParamsState`; direct `routeParams` getter was removed
|
|
63
|
+
- `PagedQueryStore`: direct `sort` / `routeParams` mutation setters were removed; remaining direct state setters are now deprecated
|
|
64
|
+
- `ResourceStore`: GET setup path no longer uses the extra local rethrow branch while preserving dedupe/cache behavior
|
|
65
|
+
|
|
66
|
+
### Fix:
|
|
67
|
+
- `PagedQueryStore`: `latest-wins` request bursts now keep `loading` stable until the newest request finishes
|
|
68
|
+
- `PagedQueryStore`: stale async `parseResponse` results are now ignored after a newer request starts, transport is reinitialized, or the store is destroyed
|
|
69
|
+
- `PagedQueryStore.updateConfig(...)` / `copy(...)`: transport is now reinitialized so updated request settings apply immediately
|
|
70
|
+
- `ResourceStore`: query arrays are now serialized as comma-separated values (`a=1,2,3`) instead of collapsing to a single last value
|
|
71
|
+
- `PagedQueryStore`: cache limit resolution is now consistent across constructor/config apply/`updateConfig`, including cache-size updates at runtime
|
|
72
|
+
- `ResourceStore`: empty-string routes are now treated as valid and `baseUrl + ''` no longer produces a trailing slash
|
|
73
|
+
- `DictStore`: stale local cache can now stay visible while background refresh runs, with timestamped storage metadata
|
|
74
|
+
- `LruCache`: limit setter ordering/typing was normalized to keep runtime behavior predictable
|
|
75
|
+
- `Serializer`: query parse/build now respects per-field array concat modes
|
|
76
|
+
|
|
77
|
+
### Test:
|
|
78
|
+
- added expanded `PagedQueryStore` regression coverage for sorting, cache limits, concurrency, config updates, and source-mode integration
|
|
79
|
+
- added `ResourceStore` regression coverage for query-array serialization and empty-string routes
|
|
80
|
+
- added `DictStore` regression coverage for TTL/revalidate/clear-cache flows
|
|
81
|
+
- added perf/stress suites and report benchmarks for main stores
|
|
82
|
+
- stabilized test setup / Vitest library configuration for the v3 suite
|
|
83
|
+
|
|
84
|
+
### Docs:
|
|
85
|
+
- `README`: updated positioning, API stability policy, behavioral guarantees, recipes, and `PagedQueryStore` / `data-grid` integration guidance
|
|
86
|
+
- `MIGRATION.md`: added v3 stability note and expanded migration guidance
|
|
87
|
+
- `PERF.md`: added checked-in benchmark/report baseline for main stores
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## [3.0.0-rc.1]: 2026-02-18
|
|
92
|
+
|
|
93
|
+
### Feat:
|
|
94
|
+
- `PagedQueryStore`: normalized public API to object-style contracts (`fetch`, `refetchWith`, `updatePage`, `updatePageSize`, `updateByOffset`, `setRouteParams`, `updateConfig`, `copy`)
|
|
95
|
+
- `PagedQueryStore`: `fetch({ filters, query, routeParams })` performs clean first-page request with cache reset
|
|
96
|
+
- `ResourceStore`: added reusable option presets via `RESOURCE_PROFILES` and `createResourceProfile(...)`
|
|
97
|
+
- `Statum`: added `provideStatum(...)` provider helper for DI configuration
|
|
98
|
+
|
|
99
|
+
### Refactor:
|
|
100
|
+
- `PagedQueryStore`: removed built-in sorting state/transport mapping from store internals (sorting stays at consumer side)
|
|
101
|
+
- `PagedQueryStore`: removed legacy APIs `updateFilters(...)`, `updateQuery(...)`, `setRoute(...)`
|
|
102
|
+
- `DictStore`: migrated internal helper calls to new `PagedQueryStore.fetch(...)` contract
|
|
103
|
+
|
|
104
|
+
### Fix:
|
|
105
|
+
- `ResourceStore`: `abort(...)` / `abortAll(...)` now clear dedupe inflight references immediately
|
|
106
|
+
- `Serializer` tests: aligned `multi` query-array expectation with current query builder behavior
|
|
107
|
+
|
|
108
|
+
### Docs:
|
|
109
|
+
- `README`: updated `PagedQueryStore` v3 API and cache behavior matrix
|
|
110
|
+
- `MIGRATION.md`: added v3 old-to-new mapping with concrete replacement examples
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## [2.1.0]: 2026-02-14
|
|
115
|
+
|
|
116
|
+
### Feat:
|
|
117
|
+
- `EntityStore`: added normalized entity store (`byId` + `ids`) with `setAll`, `upsert`, `remove`, `patch`, and computed `items`
|
|
118
|
+
- `ResourceStore`: added unified retry policy (`attempts`, `delayMs`, `backoff`, `shouldRetry`) on store and per-call levels
|
|
119
|
+
- `ResourceStore`: added trace hook (`onTrace`) for cache/request lifecycle (`cache-hit/miss/fallback/write`, `request-start/success/error/retry`, `abort`)
|
|
120
|
+
- `Serializer`: added presets export (`serializer.presets`) for quicker setup of common mapping configurations
|
|
121
|
+
- `Stores`: added `resource.scheduler` public test coverage and scheduler stability improvements
|
|
122
|
+
- `Stores`: added integration tests for composed flows (`ResourceStore` + `PaginatedDataStore` + `EntityStore`)
|
|
123
|
+
- `Stores`: added perf/stress test suite for main stores
|
|
124
|
+
|
|
125
|
+
### Fix:
|
|
126
|
+
- `ResourceStore + KeyedScheduler`: stabilized cancel/reject behavior for abort/debounce scenarios in tests
|
|
127
|
+
- `EntityStore`: fixed generic type narrowing an edge case (`TS2677`) by removing invalid predicate narrowing in `removeMany`
|
|
128
|
+
- `ResourceStore`: expanded models/exports and aligned runtime behavior for retry + tracing hooks
|
|
129
|
+
- `PaginatedDataStore`: refined config/cache behavior and updated tests around cache/refresh flows
|
|
130
|
+
- `DictStore`: refined store behavior and updated coverage for edge scenarios
|
|
131
|
+
- `CacheStrategy` + storages (`local/session/lru`): safety and behavior fixes with updated tests
|
|
132
|
+
- `Serializer`: parsing/normalization fixes and stronger test coverage for edge cases
|
|
133
|
+
|
|
134
|
+
### Docs:
|
|
135
|
+
- `README`: added composition patterns and updated usage examples for `EntityStore` and `ResourceStore` retry/trace
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## [2.0.1]: 2026-02-06
|
|
140
|
+
|
|
141
|
+
### Fix:
|
|
142
|
+
- `ResourceStore`: stable request keys now include payload; use `responseType` in HttpClient requests; avoid unhandled rejections
|
|
143
|
+
- `PaginatedDataStore`: initialize LRU cache size from resolved config; keep cache limit in sync after config changes; clarify `setRouteParams` reset behavior
|
|
144
|
+
- `DictStore`: debounce respects configured `debounceTime`; local filter handles non-string values safely
|
|
145
|
+
- `DictLocalStore`: respects global default `maxOptionsSize`
|
|
146
|
+
- `Serializer`: honor `mapString.parse` without precedence bugs; deep object deserialize; nullable handling preserves falsy values
|
|
147
|
+
|
|
148
|
+
### Chore:
|
|
149
|
+
- `LruCache`: added `entries()` helper
|
|
150
|
+
- `README`: updated defaults and fixed broken formatting/encoding
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## [2.0.0]: 2026-01-22
|
|
155
|
+
|
|
156
|
+
### Feat:
|
|
157
|
+
- `PaginatedDataStore`: added route params update method
|
|
158
|
+
- `DictStore`: added debounce for search
|
|
159
|
+
|
|
160
|
+
### Fix:
|
|
161
|
+
- `ResourceStore`: improved internal key uniqueness
|
|
162
|
+
- `ResourceStore`: fixed `loading` handling for concurrent requests
|
|
163
|
+
- `LocalStorage`/`SessionStorage`: prefix-based safety for clear operations
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## [1.0.2]: 2026-01-09
|
|
168
|
+
|
|
169
|
+
### Chore:
|
|
170
|
+
- improved docs
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## [1.0.1]: 2026-01-09
|
|
175
|
+
|
|
176
|
+
### Fix:
|
|
177
|
+
- `DictStore`: fixed search effect handling
|
|
178
|
+
- `PaginatedDataStore`: reordered filter caching
|
|
179
|
+
|
|
180
|
+
### Feat:
|
|
181
|
+
- `PaginatedDataStore`: made `parseResponse` async
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## [1.0.0]: 2025-12-24
|
|
186
|
+
|
|
187
|
+
### Feat:
|
|
188
|
+
- base data grid functionality
|
|
189
|
+
- virtual scrolling for large datasets
|
|
190
|
+
- single-column sorting
|
|
191
|
+
- pagination and infinite scroll
|
|
192
|
+
- customizable cell and header templates
|
|
193
|
+
- pin rows to top and bottom
|
|
194
|
+
- sticky columns left and right
|
|
195
|
+
- text alignment configuration per column
|
|
196
|
+
- min/max column widths
|
|
197
|
+
- single and multiple row selection
|
|
198
|
+
- index column support
|
|
199
|
+
- empty/loading state customization
|
|
200
|
+
- flexible data typing system
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rtommievich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -226,6 +226,28 @@ Transport-level store over HttpClient with cache strategies, deduplication, abor
|
|
|
226
226
|
| abort | Abort a specific request |
|
|
227
227
|
| abortAll | Abort all requests |
|
|
228
228
|
|
|
229
|
+
### observe
|
|
230
|
+
|
|
231
|
+
By default, `parseResponse` (and the resolved Promise value) receive the **response body**.
|
|
232
|
+
Pass `observe: 'response'` to receive the full `HttpResponse` instead — useful for reading response headers or status codes inside `parseResponse`.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const result = await store.get(
|
|
236
|
+
{ params: { id: '1' }, query: {} },
|
|
237
|
+
{
|
|
238
|
+
observe: 'response',
|
|
239
|
+
parseResponse: (res) => ({
|
|
240
|
+
data: res.body,
|
|
241
|
+
etag: res.headers.get('ETag'),
|
|
242
|
+
}),
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`onResponse` (store-level hook) always fires with the full `HttpResponse` regardless of `observe`, and is intended for side-effects only (logging, header extraction).
|
|
248
|
+
|
|
249
|
+
> Cache hits bypass the network entirely. When `observe: 'response'` is used with `strategy: 'cache-first'` or `'cache-only'` and a cached value exists, `parseResponse` is not called — the cached result is returned as-is.
|
|
250
|
+
|
|
229
251
|
Example:
|
|
230
252
|
|
|
231
253
|
```ts
|
|
@@ -397,7 +419,7 @@ store.updateSort({ sort: 'name', order: 'asc' });
|
|
|
397
419
|
|
|
398
420
|
`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.
|
|
399
421
|
|
|
400
|
-
Direct state mutation setters for `page`, `pageSize`, `filters`, `query`, and `totalElements`
|
|
422
|
+
`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.
|
|
401
423
|
|
|
402
424
|
### PagedQueryStore + DataGrid source mode
|
|
403
425
|
|
|
@@ -90,8 +90,11 @@ class Serializer {
|
|
|
90
90
|
* @param obj source object
|
|
91
91
|
* @returns a flat dictionary with string/primitive values
|
|
92
92
|
*/
|
|
93
|
-
serialize(obj) {
|
|
93
|
+
serialize(obj, _seen = new WeakSet()) {
|
|
94
94
|
const result = {};
|
|
95
|
+
if (obj != null && typeof obj === 'object') {
|
|
96
|
+
_seen.add(obj);
|
|
97
|
+
}
|
|
95
98
|
for (const [key, value] of Object.entries(obj ?? {})) {
|
|
96
99
|
const fields = this.config.mapFields?.[key];
|
|
97
100
|
if (fields && 'format' in fields) {
|
|
@@ -117,7 +120,10 @@ class Serializer {
|
|
|
117
120
|
continue;
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
|
-
result[key] = this.serializeElement(value, key);
|
|
123
|
+
result[key] = this.serializeElement(value, key, _seen);
|
|
124
|
+
}
|
|
125
|
+
if (obj != null && typeof obj === 'object') {
|
|
126
|
+
_seen.delete(obj);
|
|
121
127
|
}
|
|
122
128
|
return result;
|
|
123
129
|
}
|
|
@@ -206,7 +212,7 @@ class Serializer {
|
|
|
206
212
|
withConfig(config) {
|
|
207
213
|
return new Serializer(this.mergeConfig(this.config, config));
|
|
208
214
|
}
|
|
209
|
-
serializeElement(value, key) {
|
|
215
|
+
serializeElement(value, key, _seen = new WeakSet()) {
|
|
210
216
|
const fields = this.config.mapFields?.[key || ''];
|
|
211
217
|
if (fields && 'format' in fields) {
|
|
212
218
|
return;
|
|
@@ -248,11 +254,14 @@ class Serializer {
|
|
|
248
254
|
}
|
|
249
255
|
}
|
|
250
256
|
if (fields?.type === 'array' || Array.isArray(value)) {
|
|
251
|
-
return value.map((it) => this.serializeElement(it));
|
|
257
|
+
return value.map((it) => this.serializeElement(it, undefined, _seen));
|
|
252
258
|
}
|
|
253
259
|
if (fields?.type === 'object' || isObject(value)) {
|
|
260
|
+
if (this.config.mapObject.deep && !this.config.mapObject.format && value != null && _seen.has(value)) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
254
263
|
return (this.config.mapObject.format?.(value) ??
|
|
255
|
-
(this.config.mapObject.deep ? this.serialize(value) : JSON.stringify(value)));
|
|
264
|
+
(this.config.mapObject.deep ? this.serialize(value, _seen) : JSON.stringify(value)));
|
|
256
265
|
}
|
|
257
266
|
}
|
|
258
267
|
deserializeElement(value, key) {
|
|
@@ -638,6 +647,7 @@ class AbortError extends Error {
|
|
|
638
647
|
*/
|
|
639
648
|
function isAbort(e) {
|
|
640
649
|
return (e instanceof AbortError ||
|
|
650
|
+
(e instanceof DOMException && e.name === 'AbortError') ||
|
|
641
651
|
(typeof e === 'object' && e != null && e?.name === 'AbortError' && e?.message === 'aborted'));
|
|
642
652
|
}
|
|
643
653
|
function joinUrl(base, path) {
|
|
@@ -906,15 +916,20 @@ class ResourceStore {
|
|
|
906
916
|
catch (error) {
|
|
907
917
|
return Promise.reject(error);
|
|
908
918
|
}
|
|
909
|
-
const entry = this.ensureEntry(key);
|
|
910
919
|
const strategy = cfg.strategy ?? 'network-first';
|
|
911
920
|
const ttlMs = cfg.ttlMs ?? this.opts.ttlMs ?? 0;
|
|
912
|
-
|
|
913
|
-
if (
|
|
921
|
+
let entry = this.entries.get(key);
|
|
922
|
+
if (entry) {
|
|
923
|
+
// Keep hot keys at the end to approximate LRU order on cache reads too.
|
|
924
|
+
this.entries.delete(key);
|
|
925
|
+
this.entries.set(key, entry);
|
|
926
|
+
}
|
|
927
|
+
const fresh = entry?.updatedAt != null && Date.now() - entry.updatedAt < ttlMs;
|
|
928
|
+
if (cfg.dedupe && entry?.inflight) {
|
|
914
929
|
return entry.inflight;
|
|
915
930
|
}
|
|
916
931
|
if (strategy === 'cache-only') {
|
|
917
|
-
if (fresh && entry
|
|
932
|
+
if (fresh && entry?.data != null) {
|
|
918
933
|
this.trace({ type: 'cache-hit', method: 'GET', key, strategy });
|
|
919
934
|
this.promoteCurrent(entry, cfg.promote);
|
|
920
935
|
return Promise.resolve(entry.data);
|
|
@@ -922,29 +937,44 @@ class ResourceStore {
|
|
|
922
937
|
this.trace({ type: 'cache-miss', method: 'GET', key, strategy });
|
|
923
938
|
return Promise.reject(new CacheMissError(key));
|
|
924
939
|
}
|
|
925
|
-
if (strategy === 'cache-first' && fresh && !cfg.revalidate && entry
|
|
940
|
+
if (strategy === 'cache-first' && fresh && !cfg.revalidate && entry?.data != null) {
|
|
926
941
|
this.trace({ type: 'cache-hit', method: 'GET', key, strategy });
|
|
927
942
|
this.promoteCurrent(entry, cfg.promote);
|
|
928
943
|
return Promise.resolve(entry.data);
|
|
929
944
|
}
|
|
945
|
+
entry ??= this.ensureEntry(key);
|
|
930
946
|
const delay = cfg.delay ?? this.opts.delay ?? 0;
|
|
931
947
|
const mode = cfg.delayMode ?? this.opts.delayMode ?? 'debounce';
|
|
932
948
|
const isSWR = strategy === 'cache-first' && entry.data != null;
|
|
933
949
|
entry.status = isSWR ? 'stale' : 'loading';
|
|
950
|
+
if (!isSWR) {
|
|
951
|
+
entry.error = null;
|
|
952
|
+
}
|
|
934
953
|
this.promoteCurrent(entry, cfg.promote);
|
|
935
954
|
const retry = this.resolveRetryConfig(cfg.retry);
|
|
936
955
|
const taskWithRetry = this.runWithRetry((attempt) => {
|
|
937
956
|
this.trace({ type: 'request-start', method: 'GET', key, attempt });
|
|
938
|
-
|
|
939
|
-
|
|
957
|
+
entry.controller = new AbortController();
|
|
958
|
+
const options = {
|
|
959
|
+
params: query,
|
|
960
|
+
responseType: responseType,
|
|
961
|
+
observe: 'response',
|
|
962
|
+
signal: entry.controller.signal,
|
|
963
|
+
};
|
|
964
|
+
const scheduled = this.scheduler.schedule(key, mode, delay, this.exec$({
|
|
965
|
+
req$: this.http.get(url, options),
|
|
940
966
|
entry,
|
|
941
967
|
promote: cfg.promote,
|
|
942
968
|
parseFn: cfg.parseResponse,
|
|
969
|
+
observe: cfg.observe,
|
|
943
970
|
}));
|
|
971
|
+
void scheduled.catch(() => undefined);
|
|
972
|
+
return scheduled;
|
|
944
973
|
}, retry, {
|
|
945
974
|
method: 'GET',
|
|
946
975
|
key,
|
|
947
976
|
});
|
|
977
|
+
void taskWithRetry.catch(() => undefined);
|
|
948
978
|
const resolvedTask = taskWithRetry
|
|
949
979
|
.catch((error) => {
|
|
950
980
|
if (isAbort(error)) {
|
|
@@ -1036,9 +1066,12 @@ class ResourceStore {
|
|
|
1036
1066
|
const entry = this.entries.get(key);
|
|
1037
1067
|
this.trace({ type: 'abort', method, key });
|
|
1038
1068
|
if (entry) {
|
|
1069
|
+
entry.controller?.abort(reason instanceof Error ? reason : new AbortError(typeof reason === 'string' ? reason : undefined));
|
|
1070
|
+
entry.controller = undefined;
|
|
1039
1071
|
entry.inflight = undefined;
|
|
1040
1072
|
if (entry.status === 'loading' || entry.status === 'stale') {
|
|
1041
1073
|
entry.status = 'idle';
|
|
1074
|
+
this.promoteCurrent(entry, entry.promoted);
|
|
1042
1075
|
}
|
|
1043
1076
|
}
|
|
1044
1077
|
this.scheduler.cancel?.(key, reason);
|
|
@@ -1050,10 +1083,14 @@ class ResourceStore {
|
|
|
1050
1083
|
*/
|
|
1051
1084
|
abortAll(reason) {
|
|
1052
1085
|
this.trace({ type: 'abort', method: 'GET', key: '*' });
|
|
1086
|
+
const abortReason = reason instanceof Error ? reason : new AbortError(typeof reason === 'string' ? reason : undefined);
|
|
1053
1087
|
this.entries.forEach((entry) => {
|
|
1088
|
+
entry.controller?.abort(abortReason);
|
|
1089
|
+
entry.controller = undefined;
|
|
1054
1090
|
entry.inflight = undefined;
|
|
1055
1091
|
if (entry.status === 'loading' || entry.status === 'stale') {
|
|
1056
1092
|
entry.status = 'idle';
|
|
1093
|
+
this.promoteCurrent(entry, entry.promoted);
|
|
1057
1094
|
}
|
|
1058
1095
|
});
|
|
1059
1096
|
this.scheduler.cancelAll?.(reason);
|
|
@@ -1076,13 +1113,18 @@ class ResourceStore {
|
|
|
1076
1113
|
while (this.entries.size >= this.maxEntries) {
|
|
1077
1114
|
let keyToDelete = null;
|
|
1078
1115
|
for (const [key, value] of this.entries.entries()) {
|
|
1079
|
-
if (!value.inflight) {
|
|
1116
|
+
if (!value.inflight && !value.controller) {
|
|
1080
1117
|
keyToDelete = key;
|
|
1081
1118
|
break;
|
|
1082
1119
|
}
|
|
1083
1120
|
}
|
|
1084
1121
|
if (keyToDelete === null) {
|
|
1122
|
+
// All entries have active requests — forced eviction. Dedupe for the evicted
|
|
1123
|
+
// key will break: the in-flight request will finish but its result won't be cached.
|
|
1085
1124
|
keyToDelete = this.entries.keys().next().value ?? null;
|
|
1125
|
+
if (keyToDelete !== null) {
|
|
1126
|
+
this.trace({ type: 'abort', method: 'GET', key: keyToDelete });
|
|
1127
|
+
}
|
|
1086
1128
|
}
|
|
1087
1129
|
if (keyToDelete === null) {
|
|
1088
1130
|
break;
|
|
@@ -1114,25 +1156,35 @@ class ResourceStore {
|
|
|
1114
1156
|
const mode = config.delayMode ?? this.opts.delayMode ?? 'debounce';
|
|
1115
1157
|
const retry = this.resolveRetryConfig(config.retry);
|
|
1116
1158
|
entry.status = 'loading';
|
|
1159
|
+
entry.error = null;
|
|
1117
1160
|
this.promoteCurrent(entry, config.promote);
|
|
1118
|
-
let req$;
|
|
1119
|
-
if (method === 'DELETE') {
|
|
1120
|
-
req$ = this.http.delete(url, {
|
|
1121
|
-
body: payload,
|
|
1122
|
-
params: query,
|
|
1123
|
-
responseType: responseType,
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1126
|
-
else {
|
|
1127
|
-
// @ts-ignore
|
|
1128
|
-
req$ = this.mutationMethods[method](url, payload, {
|
|
1129
|
-
params: query,
|
|
1130
|
-
responseType: responseType,
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
1161
|
const task = this.runWithRetry((attempt) => {
|
|
1134
1162
|
this.trace({ type: 'request-start', method, key, attempt });
|
|
1135
|
-
|
|
1163
|
+
entry.controller = new AbortController();
|
|
1164
|
+
const signal = entry.controller.signal;
|
|
1165
|
+
let req$;
|
|
1166
|
+
if (method === 'DELETE') {
|
|
1167
|
+
// @ts-ignore
|
|
1168
|
+
req$ = this.http.delete(url, {
|
|
1169
|
+
body: payload,
|
|
1170
|
+
params: query,
|
|
1171
|
+
responseType: responseType,
|
|
1172
|
+
observe: 'response',
|
|
1173
|
+
signal,
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
else {
|
|
1177
|
+
// @ts-ignore
|
|
1178
|
+
req$ = this.mutationMethods[method](url, payload, {
|
|
1179
|
+
params: query,
|
|
1180
|
+
responseType: responseType,
|
|
1181
|
+
observe: 'response',
|
|
1182
|
+
signal,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
const scheduled = this.scheduler.schedule(key, mode, delay, this.exec$({ req$, entry, promote: config.promote, parseFn: config.parseResponse, observe: config.observe }));
|
|
1186
|
+
void scheduled.catch(() => undefined);
|
|
1187
|
+
return scheduled;
|
|
1136
1188
|
}, retry, { method, key })
|
|
1137
1189
|
.then((data) => {
|
|
1138
1190
|
this.trace({ type: 'cache-write', method, key });
|
|
@@ -1181,6 +1233,9 @@ class ResourceStore {
|
|
|
1181
1233
|
});
|
|
1182
1234
|
}
|
|
1183
1235
|
preparePayload(payload) {
|
|
1236
|
+
if (payload instanceof FormData || payload instanceof Blob || payload instanceof ArrayBuffer) {
|
|
1237
|
+
return payload;
|
|
1238
|
+
}
|
|
1184
1239
|
const presetPayload = this.opts.presetPayload;
|
|
1185
1240
|
if (!presetPayload && !payload) {
|
|
1186
1241
|
return {};
|
|
@@ -1214,6 +1269,7 @@ class ResourceStore {
|
|
|
1214
1269
|
attempts: Math.max(0, source.attempts ?? 0),
|
|
1215
1270
|
delayMs: Math.max(0, source.delayMs ?? 0),
|
|
1216
1271
|
backoff: source.backoff ?? 'constant',
|
|
1272
|
+
jitter: source.jitter ?? false,
|
|
1217
1273
|
shouldRetry: source.shouldRetry ?? this.defaultShouldRetry,
|
|
1218
1274
|
};
|
|
1219
1275
|
}
|
|
@@ -1228,29 +1284,41 @@ class ResourceStore {
|
|
|
1228
1284
|
}
|
|
1229
1285
|
return status === 0 || status >= 500;
|
|
1230
1286
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1287
|
+
runWithRetry(exec, retry, context) {
|
|
1288
|
+
const runAttempt = (attempt) => exec(attempt).catch((error) => {
|
|
1289
|
+
const canRetry = attempt <= retry.attempts && retry.shouldRetry(error, attempt);
|
|
1290
|
+
if (!canRetry) {
|
|
1291
|
+
throw error;
|
|
1236
1292
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
}
|
|
1293
|
+
const baseDelay = retry.backoff === 'exponential' ? retry.delayMs * 2 ** (attempt - 1) : retry.delayMs;
|
|
1294
|
+
const jitterOffset = retry.jitter ? (Math.random() * 0.5 - 0.25) * baseDelay : 0;
|
|
1295
|
+
const delayMs = Math.max(0, Math.round(baseDelay + jitterOffset));
|
|
1296
|
+
this.trace({ type: 'request-retry', method: context.method, key: context.key, attempt, error });
|
|
1297
|
+
if (delayMs > 0) {
|
|
1298
|
+
return new Promise((resolve, reject) => {
|
|
1299
|
+
setTimeout(() => {
|
|
1300
|
+
void runAttempt(attempt + 1).then(resolve, reject);
|
|
1301
|
+
}, delayMs);
|
|
1302
|
+
});
|
|
1248
1303
|
}
|
|
1249
|
-
|
|
1304
|
+
return runAttempt(attempt + 1);
|
|
1305
|
+
});
|
|
1306
|
+
return runAttempt(1);
|
|
1250
1307
|
}
|
|
1251
|
-
exec$ = ({ req$, entry, promote = true, parseFn }) => () => {
|
|
1308
|
+
exec$ = ({ req$, entry, promote = true, parseFn, observe }) => () => {
|
|
1252
1309
|
promote && this.#activeRequests.update((n) => n + 1);
|
|
1253
|
-
return req$.pipe(map((
|
|
1310
|
+
return req$.pipe(map((httpResponse) => {
|
|
1311
|
+
try {
|
|
1312
|
+
this.opts.onResponse?.(httpResponse);
|
|
1313
|
+
}
|
|
1314
|
+
catch {
|
|
1315
|
+
/* noop */
|
|
1316
|
+
}
|
|
1317
|
+
const payload = observe === 'response'
|
|
1318
|
+
? httpResponse
|
|
1319
|
+
: httpResponse.body;
|
|
1320
|
+
return parseFn ? parseFn(payload) : payload;
|
|
1321
|
+
}), tap({
|
|
1254
1322
|
next: (data) => {
|
|
1255
1323
|
entry.data = data;
|
|
1256
1324
|
entry.status = 'success';
|
|
@@ -1265,6 +1333,7 @@ class ResourceStore {
|
|
|
1265
1333
|
},
|
|
1266
1334
|
}), finalize(() => {
|
|
1267
1335
|
promote && this.#activeRequests.update((n) => Math.max(0, n - 1));
|
|
1336
|
+
entry.controller = undefined;
|
|
1268
1337
|
this.promoteCurrent(entry, promote);
|
|
1269
1338
|
}));
|
|
1270
1339
|
};
|
|
@@ -1272,6 +1341,7 @@ class ResourceStore {
|
|
|
1272
1341
|
if (!promote) {
|
|
1273
1342
|
return;
|
|
1274
1343
|
}
|
|
1344
|
+
entry.promoted = true;
|
|
1275
1345
|
this.#value.set(entry.data ?? null);
|
|
1276
1346
|
this.#status.set(entry.status);
|
|
1277
1347
|
this.#error.set(entry.error);
|
|
@@ -1399,37 +1469,18 @@ class PagedQueryStore {
|
|
|
1399
1469
|
get sort() {
|
|
1400
1470
|
return this.#sort();
|
|
1401
1471
|
}
|
|
1402
|
-
/**
|
|
1403
|
-
* @deprecated Prefer `fetch(...)`, `refetchWith(...)`, or a dedicated setter method.
|
|
1404
|
-
* Direct state mutation bypasses request semantics and should be treated as legacy.
|
|
1405
|
-
*/
|
|
1406
1472
|
set filters(value) {
|
|
1407
1473
|
this.#filters.set(value);
|
|
1408
1474
|
}
|
|
1409
|
-
/**
|
|
1410
|
-
* @deprecated Prefer `fetch(...)`, `refetchWith(...)`, or a dedicated setter method.
|
|
1411
|
-
* Direct state mutation bypasses request semantics and should be treated as legacy.
|
|
1412
|
-
*/
|
|
1413
1475
|
set query(value) {
|
|
1414
1476
|
this.#query.set(value);
|
|
1415
1477
|
}
|
|
1416
|
-
/**
|
|
1417
|
-
* @deprecated Prefer `updatePage(...)`, `fetch(...)`, or `refetchWith(...)`.
|
|
1418
|
-
* Direct state mutation bypasses request semantics and should be treated as legacy.
|
|
1419
|
-
*/
|
|
1420
1478
|
set page(value) {
|
|
1421
1479
|
this.#page.set(value);
|
|
1422
1480
|
}
|
|
1423
|
-
/**
|
|
1424
|
-
* @deprecated Prefer `updatePageSize(...)`.
|
|
1425
|
-
* Direct state mutation bypasses request semantics and should be treated as legacy.
|
|
1426
|
-
*/
|
|
1427
1481
|
set pageSize(value) {
|
|
1428
1482
|
this.#pageSize.set(value);
|
|
1429
1483
|
}
|
|
1430
|
-
/**
|
|
1431
|
-
* @deprecated Managed by transport responses. Direct assignment should be treated as legacy.
|
|
1432
|
-
*/
|
|
1433
1484
|
set totalElements(value) {
|
|
1434
1485
|
this.#totalElements.set(value);
|
|
1435
1486
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AnyType, AnyDict, RestMethods, QueryParams, PageableRequest, PageableResponse } from '@reforgium/internal';
|
|
2
2
|
import * as _angular_core from '@angular/core';
|
|
3
3
|
import { Signal, WritableSignal, EnvironmentProviders, InjectionToken } from '@angular/core';
|
|
4
|
+
import { HttpResponse } from '@angular/common/http';
|
|
4
5
|
import * as _reforgium_statum from '@reforgium/statum';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -182,7 +183,7 @@ declare class Serializer<EntityType extends DataType> {
|
|
|
182
183
|
* @param obj source object
|
|
183
184
|
* @returns a flat dictionary with string/primitive values
|
|
184
185
|
*/
|
|
185
|
-
serialize(obj: EntityType): SerializedType;
|
|
186
|
+
serialize(obj: EntityType, _seen?: WeakSet<object>): SerializedType;
|
|
186
187
|
/**
|
|
187
188
|
* Parse serialized data into a domain object.
|
|
188
189
|
*
|
|
@@ -283,13 +284,17 @@ declare const storageStrategy: <Key = string, Type extends AnyType = AnyDict>(st
|
|
|
283
284
|
/**
|
|
284
285
|
* Object for request body (payload).
|
|
285
286
|
* Commonly used with POST/PUT/PATCH/DELETE.
|
|
287
|
+
* `FormData`, `Blob`, and `ArrayBuffer` are passed through as-is without serialization.
|
|
286
288
|
*/
|
|
287
|
-
type PayloadData = AnyDict;
|
|
289
|
+
type PayloadData = AnyDict | FormData | Blob | ArrayBuffer;
|
|
288
290
|
/**
|
|
289
|
-
* Simple parameters dictionary (string/number).
|
|
290
|
-
* Suitable for path params and query.
|
|
291
|
+
* Simple path parameters dictionary (string/number).
|
|
291
292
|
*/
|
|
292
293
|
type SimpleDict = Record<string, string | number>;
|
|
294
|
+
/**
|
|
295
|
+
* Query parameters dictionary with scalar and array support.
|
|
296
|
+
*/
|
|
297
|
+
type QueryDict = Record<string, string | number | boolean | ReadonlyArray<string | number | boolean>>;
|
|
293
298
|
/**
|
|
294
299
|
* Resource status in `ResourceStore`:
|
|
295
300
|
* - `idle` — not loaded yet
|
|
@@ -310,6 +315,11 @@ type RetryConfig = {
|
|
|
310
315
|
attempts?: number;
|
|
311
316
|
delayMs?: number;
|
|
312
317
|
backoff?: RetryBackoff;
|
|
318
|
+
/**
|
|
319
|
+
* Add ±25% random jitter to the retry delay to avoid synchronized retries
|
|
320
|
+
* across multiple clients hitting the same endpoint simultaneously.
|
|
321
|
+
*/
|
|
322
|
+
jitter?: boolean;
|
|
313
323
|
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
314
324
|
};
|
|
315
325
|
/**
|
|
@@ -347,6 +357,12 @@ type ResourceStoreOptions = {
|
|
|
347
357
|
retry?: RetryConfig;
|
|
348
358
|
/** Trace hook for request/cache lifecycle events. */
|
|
349
359
|
onTrace?: (event: ResourceTraceEvent) => void;
|
|
360
|
+
/**
|
|
361
|
+
* Side-effect hook called with the full `HttpResponse` for every completed request.
|
|
362
|
+
* Useful for reading response headers (e.g. `X-Total-Count`, `Link`, correlation IDs).
|
|
363
|
+
* Errors thrown here are silently swallowed.
|
|
364
|
+
*/
|
|
365
|
+
onResponse?: (response: HttpResponse<unknown>) => void;
|
|
350
366
|
serializer?: Partial<SerializerConfig>;
|
|
351
367
|
};
|
|
352
368
|
/**
|
|
@@ -355,7 +371,7 @@ type ResourceStoreOptions = {
|
|
|
355
371
|
* - `query` — query string parameters
|
|
356
372
|
* - `payload` — request body (for mutations)
|
|
357
373
|
*/
|
|
358
|
-
type CallArgs<Params extends SimpleDict = SimpleDict, Query extends
|
|
374
|
+
type CallArgs<Params extends SimpleDict = SimpleDict, Query extends QueryDict = QueryDict, Payload extends PayloadData = PayloadData> = {
|
|
359
375
|
params?: Params;
|
|
360
376
|
query?: Query;
|
|
361
377
|
payload?: Payload;
|
|
@@ -385,6 +401,12 @@ type CallConfig<Response, Type> = {
|
|
|
385
401
|
* - 'arraybuffer' - response as an ArrayBuffer
|
|
386
402
|
*/
|
|
387
403
|
responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
|
|
404
|
+
/**
|
|
405
|
+
* Controls what is passed to `parseResponse`:
|
|
406
|
+
* - `'body'` (default) — receives the response body (`Response`)
|
|
407
|
+
* - `'response'` — receives the full `HttpResponse<Response>` (headers, status, body)
|
|
408
|
+
*/
|
|
409
|
+
observe?: 'body' | 'response';
|
|
388
410
|
/**
|
|
389
411
|
* Custom response parser.
|
|
390
412
|
* Allows converting server `Response` to the domain type `Type`.
|
|
@@ -481,7 +503,7 @@ declare class ResourceStore<Data> {
|
|
|
481
503
|
* @param cfg Call settings (strategy, ttlMs, revalidate, parseResponse, delay, delayMode, dedupe, promote)
|
|
482
504
|
* @returns Deserialized `Data`
|
|
483
505
|
*/
|
|
484
|
-
get<Param extends SimpleDict = SimpleDict, Query extends
|
|
506
|
+
get<Param extends SimpleDict = SimpleDict, Query extends QueryDict = QueryDict, Response extends AnyType = Data>(args: CallArgs<Param, Query, never>, cfg?: GetCallConfig<Response, Data>): Promise<Data>;
|
|
485
507
|
/**
|
|
486
508
|
* POST request.
|
|
487
509
|
*
|
|
@@ -489,7 +511,7 @@ declare class ResourceStore<Data> {
|
|
|
489
511
|
* @param cfg Call settings (parseResponse, delay, delayMode, dedupe, promote)
|
|
490
512
|
* @returns API response (by default — as returned by the server)
|
|
491
513
|
*/
|
|
492
|
-
post<Param extends SimpleDict = SimpleDict, Payload extends PayloadData = PayloadData, Query extends
|
|
514
|
+
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>;
|
|
493
515
|
/**
|
|
494
516
|
* PUT request (full resource update).
|
|
495
517
|
*
|
|
@@ -497,7 +519,7 @@ declare class ResourceStore<Data> {
|
|
|
497
519
|
* @param cfg Call settings (parseResponse, delay, delayMode, dedupe, promote)
|
|
498
520
|
* @returns API response (by default — as returned by the server)
|
|
499
521
|
*/
|
|
500
|
-
put<Param extends SimpleDict = SimpleDict, Payload extends PayloadData = PayloadData, Query extends
|
|
522
|
+
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>;
|
|
501
523
|
/**
|
|
502
524
|
* PATCH request (partial update).
|
|
503
525
|
*
|
|
@@ -505,7 +527,7 @@ declare class ResourceStore<Data> {
|
|
|
505
527
|
* @param cfg Call settings (parseResponse, delay, delayMode, dedupe, promote)
|
|
506
528
|
* @returns API response (by default — as returned by the server)
|
|
507
529
|
*/
|
|
508
|
-
patch<Param extends SimpleDict = SimpleDict, Payload extends PayloadData = PayloadData, Query extends
|
|
530
|
+
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>;
|
|
509
531
|
/**
|
|
510
532
|
* DELETE request.
|
|
511
533
|
*
|
|
@@ -513,7 +535,7 @@ declare class ResourceStore<Data> {
|
|
|
513
535
|
* @param cfg Call settings (parseResponse, delay, delayMode, dedupe, promote)
|
|
514
536
|
* @returns API response (by default — as returned by the server)
|
|
515
537
|
*/
|
|
516
|
-
delete<Param extends SimpleDict = SimpleDict, Payload extends PayloadData = PayloadData, Query extends
|
|
538
|
+
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>;
|
|
517
539
|
/**
|
|
518
540
|
* Cancel scheduled/running requests for a specific call.
|
|
519
541
|
*
|
|
@@ -521,7 +543,7 @@ declare class ResourceStore<Data> {
|
|
|
521
543
|
* @param args Arguments used to build the URL (params, query)
|
|
522
544
|
* @param reason Cancellation reason (optional)
|
|
523
545
|
*/
|
|
524
|
-
abort<Param extends SimpleDict = SimpleDict, Query extends
|
|
546
|
+
abort<Param extends SimpleDict = SimpleDict, Query extends QueryDict = QueryDict>(method: RestMethods, args: CallArgs<Param, Query>, reason?: string | Error): void;
|
|
525
547
|
/**
|
|
526
548
|
* Cancel all scheduled/running requests for this store.
|
|
527
549
|
*
|
|
@@ -645,13 +667,16 @@ type SetRouteParamsOptions = {
|
|
|
645
667
|
abort?: boolean;
|
|
646
668
|
};
|
|
647
669
|
|
|
670
|
+
type BivariantCallback<Args, Return> = {
|
|
671
|
+
bivarianceHack(data: Args): Return;
|
|
672
|
+
}['bivarianceHack'];
|
|
648
673
|
/**
|
|
649
674
|
* Configuration for the paginated data store.
|
|
650
675
|
*
|
|
651
676
|
* Controls request method, page cache, debounce delay, concurrency policy, and
|
|
652
677
|
* request/response transformations.
|
|
653
678
|
*/
|
|
654
|
-
type PagedQueryStoreConfig<ItemsType extends object, FilterType =
|
|
679
|
+
type PagedQueryStoreConfig<ItemsType extends object, FilterType extends AnyDict = AnyDict> = {
|
|
655
680
|
/** Optional base URL prepended to the route before the request is sent. */
|
|
656
681
|
baseUrl?: string;
|
|
657
682
|
/** Transport HTTP method: `GET`/`POST`/`PATCH`/`PUT`/`DELETE`. Defaults to global config or `POST`. */
|
|
@@ -666,9 +691,9 @@ type PagedQueryStoreConfig<ItemsType extends object, FilterType = unknown> = {
|
|
|
666
691
|
debounceTime?: number;
|
|
667
692
|
/** Concurrency policy for overlapping requests. Defaults to `latest-wins`. */
|
|
668
693
|
concurrency?: PagedQueryConcurrency;
|
|
669
|
-
/** Initial pagination params. `page`
|
|
694
|
+
/** Initial pagination params. Omitted `page` defaults to `0`. */
|
|
670
695
|
presetQuery?: {
|
|
671
|
-
page
|
|
696
|
+
page?: number;
|
|
672
697
|
pageSize?: number;
|
|
673
698
|
};
|
|
674
699
|
/** Initial filters. Will be sent with the first request. */
|
|
@@ -680,7 +705,7 @@ type PagedQueryStoreConfig<ItemsType extends object, FilterType = unknown> = {
|
|
|
680
705
|
* Useful for mapping `page/pageSize` and selected filter fields to API-specific query keys.
|
|
681
706
|
* Returned object can contain nullable values; they are filtered before the transport call.
|
|
682
707
|
*/
|
|
683
|
-
parseRequest?:
|
|
708
|
+
parseRequest?: BivariantCallback<PageableRequest & Partial<FilterType> & AnyDict, AnyDict>;
|
|
684
709
|
/**
|
|
685
710
|
* Custom parser of API response into unified `PageableResponse<ItemsType>`.
|
|
686
711
|
* Use if the server returns an array or a non-standard structure.
|
|
@@ -733,7 +758,7 @@ type PagedQueryStoreProviderConfig = {
|
|
|
733
758
|
* effect(() => console.log(ds.items(), ds.loading()));
|
|
734
759
|
* ```
|
|
735
760
|
*/
|
|
736
|
-
declare class PagedQueryStore<ItemsType extends AnyDict, FilterType = AnyDict> {
|
|
761
|
+
declare class PagedQueryStore<ItemsType extends AnyDict, FilterType extends AnyDict = AnyDict> {
|
|
737
762
|
#private;
|
|
738
763
|
private route;
|
|
739
764
|
config: PagedQueryStoreConfig<ItemsType, FilterType>;
|
|
@@ -774,29 +799,10 @@ declare class PagedQueryStore<ItemsType extends AnyDict, FilterType = AnyDict> {
|
|
|
774
799
|
get totalElements(): number;
|
|
775
800
|
/** Current sort rules. */
|
|
776
801
|
get sort(): QuerySortRule[];
|
|
777
|
-
/**
|
|
778
|
-
* @deprecated Prefer `fetch(...)`, `refetchWith(...)`, or a dedicated setter method.
|
|
779
|
-
* Direct state mutation bypasses request semantics and should be treated as legacy.
|
|
780
|
-
*/
|
|
781
802
|
set filters(value: Partial<FilterType>);
|
|
782
|
-
/**
|
|
783
|
-
* @deprecated Prefer `fetch(...)`, `refetchWith(...)`, or a dedicated setter method.
|
|
784
|
-
* Direct state mutation bypasses request semantics and should be treated as legacy.
|
|
785
|
-
*/
|
|
786
803
|
set query(value: AnyDict);
|
|
787
|
-
/**
|
|
788
|
-
* @deprecated Prefer `updatePage(...)`, `fetch(...)`, or `refetchWith(...)`.
|
|
789
|
-
* Direct state mutation bypasses request semantics and should be treated as legacy.
|
|
790
|
-
*/
|
|
791
804
|
set page(value: number);
|
|
792
|
-
/**
|
|
793
|
-
* @deprecated Prefer `updatePageSize(...)`.
|
|
794
|
-
* Direct state mutation bypasses request semantics and should be treated as legacy.
|
|
795
|
-
*/
|
|
796
805
|
set pageSize(value: number);
|
|
797
|
-
/**
|
|
798
|
-
* @deprecated Managed by transport responses. Direct assignment should be treated as legacy.
|
|
799
|
-
*/
|
|
800
806
|
set totalElements(value: number);
|
|
801
807
|
/**
|
|
802
808
|
* Fetch data with explicit filters and query params from the first page.
|