@reforgium/statum 1.0.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/README.md +328 -0
- package/fesm2022/reforgium-statum.mjs +1401 -0
- package/fesm2022/reforgium-statum.mjs.map +1 -0
- package/package.json +42 -0
- package/types/reforgium-statum.d.ts +852 -0
- package/types/reforgium-statum.d.ts.map +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# @reforgium/statum
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@reforgium/internal)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
**Signals-first state stores and caching utilities for Angular (20+).**
|
|
7
|
+
|
|
8
|
+
`@reforgium/statum` provides a set of **API-oriented stores**, **cache strategies**, and a
|
|
9
|
+
**serialization layer** for building predictable data flows in Angular applications.
|
|
10
|
+
|
|
11
|
+
Designed for **application state**, not abstract reducers.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- Signals-first API (`signal()` everywhere)
|
|
18
|
+
- API-driven stores (resource / paginated / dictionaries)
|
|
19
|
+
- Built-in cache strategies (TTL, prefix grouping)
|
|
20
|
+
- Deterministic request lifecycle: loading / error / data
|
|
21
|
+
- Optional transport-level: dedupe, debounce/throttle, abort
|
|
22
|
+
- Explicit serialization / deserialization boundary
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @reforgium/statum
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Package Structure
|
|
35
|
+
|
|
36
|
+
- **Cache** — storage strategies for caching
|
|
37
|
+
- **Stores** — reusable state containers over HttpClient
|
|
38
|
+
- **Serializer** — configurable data transformation utility
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Cache
|
|
43
|
+
|
|
44
|
+
### cacheStrategy
|
|
45
|
+
|
|
46
|
+
`cacheStrategy<T>(kind)` is a factory that returns a cache storage implementing `StorageInterface<T>`
|
|
47
|
+
|
|
48
|
+
Available strategies:
|
|
49
|
+
|
|
50
|
+
- `persist` — browser localStorage
|
|
51
|
+
- `session` — browser sessionStorage
|
|
52
|
+
- `memory` — in-memory storage
|
|
53
|
+
- `lru` — size-limited in-memory cache
|
|
54
|
+
|
|
55
|
+
### StorageInterface
|
|
56
|
+
|
|
57
|
+
All cache strategies implement the same contract.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
interface StorageInterface<T> {
|
|
61
|
+
get(key: string): T | null;
|
|
62
|
+
set(key: string, value: T): void;
|
|
63
|
+
remove(key: string): void;
|
|
64
|
+
clear(): void;
|
|
65
|
+
get length(): number;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { cacheStrategy } from '@reforgium/statum';
|
|
73
|
+
|
|
74
|
+
type User = { id: number; name: string };
|
|
75
|
+
|
|
76
|
+
const storage = cacheStrategy<User>('persist');
|
|
77
|
+
|
|
78
|
+
storage.set('user:1', { id: 1, name: 'John' });
|
|
79
|
+
|
|
80
|
+
const user = storage.get('user:1'); // User | null
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## ResourceStore
|
|
86
|
+
|
|
87
|
+
Transport-level store over HttpClient with cache strategies, deduplication, abort, and rate-limiting.
|
|
88
|
+
|
|
89
|
+
### When to use
|
|
90
|
+
|
|
91
|
+
- CRUD and “resource” endpoints
|
|
92
|
+
- Centralized debounce/throttle
|
|
93
|
+
- Abort in-flight / delayed requests (latest-wins)
|
|
94
|
+
- Dedupe identical calls to one HTTP request
|
|
95
|
+
- Use as a transport engine for other stores (`promote: false`)
|
|
96
|
+
|
|
97
|
+
### Signals
|
|
98
|
+
|
|
99
|
+
| Signal | Type | Description |
|
|
100
|
+
|---------|-------------------|---------------------|
|
|
101
|
+
| value | `Signal<T | null>` | Last successful value |
|
|
102
|
+
| status | `'idle' | 'loading' | 'success' | 'error' | 'stale'` | Resource status |
|
|
103
|
+
| error | `Signal<unknown | null>` | Last error |
|
|
104
|
+
| loading | `Signal<boolean>` | Request in progress |
|
|
105
|
+
|
|
106
|
+
### Methods
|
|
107
|
+
|
|
108
|
+
| Method | Description |
|
|
109
|
+
|------|-------------|
|
|
110
|
+
| get | GET request |
|
|
111
|
+
| post | POST request |
|
|
112
|
+
| put | PUT request |
|
|
113
|
+
| patch | PATCH request |
|
|
114
|
+
| delete | DELETE request |
|
|
115
|
+
| abort | Abort a specific request |
|
|
116
|
+
| abortAll | Abort all requests |
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { ResourceStore } from '@reforgium/statum';
|
|
122
|
+
|
|
123
|
+
type User = { id: number; name: string };
|
|
124
|
+
|
|
125
|
+
const userStore = new ResourceStore<User>(
|
|
126
|
+
{ GET: '/users/:id' },
|
|
127
|
+
{ baseUrl: '/api', ttlMs: 60_000 }
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const user = await userStore.get(
|
|
131
|
+
{ params: { id: '42' }, query: {} },
|
|
132
|
+
{ dedupe: true }
|
|
133
|
+
);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## PaginatedDataStore
|
|
139
|
+
|
|
140
|
+
Lightweight store for server-side pagination with sorting, filtering and page cache.
|
|
141
|
+
|
|
142
|
+
### When to use
|
|
143
|
+
|
|
144
|
+
- Server-side pagination
|
|
145
|
+
- Noisy filters (debounce)
|
|
146
|
+
- Tables/grids (PrimeNG `onLazyLoad` and similar)
|
|
147
|
+
- Small local page cache with eviction
|
|
148
|
+
|
|
149
|
+
### State
|
|
150
|
+
|
|
151
|
+
| Field / Signal | Type | Description |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| items | `WritableSignal<T[]>` | Current page items |
|
|
154
|
+
| cached | `WritableSignal<T[]>` | Flattened cache of cached pages |
|
|
155
|
+
| loading | `WritableSignal<boolean>` | Loading indicator |
|
|
156
|
+
| page | `number` | Current page (0-based) |
|
|
157
|
+
| pageSize | `number` | Page size |
|
|
158
|
+
| totalElements | `number` | Total items on server |
|
|
159
|
+
| filters | `Partial<F>` | Active filters |
|
|
160
|
+
| sort | `string \| undefined` | Sort as `"field,asc|desc"` |
|
|
161
|
+
|
|
162
|
+
### Methods
|
|
163
|
+
|
|
164
|
+
| Method | Description |
|
|
165
|
+
|----------------|---------------------------------------------------|
|
|
166
|
+
| refresh | Repeat request with current params |
|
|
167
|
+
| updatePage | Change page (can use cache) |
|
|
168
|
+
| updatePageSize | Change page size (can use cache) |
|
|
169
|
+
| updateFilters | Merge filters, reset cache, go to page 0 |
|
|
170
|
+
| setFilters | Replace filters, reset cache + sort, go to page 0 |
|
|
171
|
+
| updateQuery | Map table events to `page`/`sort` |
|
|
172
|
+
| updateRoute | Change endpoint, reset meta/cache |
|
|
173
|
+
| updateConfig | Patch config and apply presets |
|
|
174
|
+
| copy | Copy config/meta from another store |
|
|
175
|
+
| destroy | Manual destroying of caches and abort requests |
|
|
176
|
+
|
|
177
|
+
Example:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { PaginatedDataStore } from '@reforgium/statum';
|
|
181
|
+
|
|
182
|
+
type User = { id: number; name: string };
|
|
183
|
+
|
|
184
|
+
const store = new PaginatedDataStore<User, { search?: string }>('api/users', {
|
|
185
|
+
method: 'GET',
|
|
186
|
+
debounceTime: 200,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
store.updateFilters({ search: 'neo' });
|
|
190
|
+
store.updatePage(1);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## DictStore
|
|
196
|
+
|
|
197
|
+
Helper for dictionaries/options (select/autocomplete) on top of PaginatedDataStore.
|
|
198
|
+
|
|
199
|
+
### When to use
|
|
200
|
+
|
|
201
|
+
- Select options / autocomplete hints
|
|
202
|
+
- Local search over previously loaded values (fixed mode)
|
|
203
|
+
- Cache across sessions in localStorage
|
|
204
|
+
- Need `{ label, value }` mapping
|
|
205
|
+
|
|
206
|
+
### Key behavior
|
|
207
|
+
|
|
208
|
+
- Two modes:
|
|
209
|
+
- `fixed: true` — first load fills local cache; search happens locally
|
|
210
|
+
- `fixed: false` — search goes to server (passes filter `name`)
|
|
211
|
+
- Local cache merges without duplicates and is size-limited
|
|
212
|
+
|
|
213
|
+
### Public API
|
|
214
|
+
|
|
215
|
+
Signals / fields:
|
|
216
|
+
|
|
217
|
+
| Field / Signal | Type | Description |
|
|
218
|
+
|----------------|--------------------------------------------------------|-----------------------|
|
|
219
|
+
| items | `Signal<T[]>` | Current visible items |
|
|
220
|
+
| options | `Signal<{ label: string; value: string \| number }[]>` | Options for selects |
|
|
221
|
+
| searchText | `Signal<string>` | Current search text |
|
|
222
|
+
| filters | `Signal<Record<string, any>>` | Current filters |
|
|
223
|
+
|
|
224
|
+
Methods:
|
|
225
|
+
|
|
226
|
+
| Method | Description |
|
|
227
|
+
|--------------|--------------------------------------------------------------------------------------------|
|
|
228
|
+
| search | Set search text and trigger load (fixed=false) or local filter (fixed=true) |
|
|
229
|
+
| findLabel | Resolve label by value from local cache (no network) |
|
|
230
|
+
| restoreCache | Restore cache from the selected storage (`persist`/`session`/`lru`/`memory`). (no network) |
|
|
231
|
+
|
|
232
|
+
Config:
|
|
233
|
+
|
|
234
|
+
| Option | Type | Default | Description |
|
|
235
|
+
|---------------|-----------------------------------------------|------------|---------------------------------------------|
|
|
236
|
+
| labelKey | `string` | `'label'` | Property name to use as option label |
|
|
237
|
+
| valueKey | `string` | `'value'` | Property name to use as option value |
|
|
238
|
+
| fixed | `boolean` | `false` | Use local search instead of server requests |
|
|
239
|
+
| method | `'GET' \| 'POST'` | `'GET'` | HTTP method for requests |
|
|
240
|
+
| cacheKey | `string \| null` | `null` | Storage key for persisting cache |
|
|
241
|
+
| debounceTime | `number` | `200` | Debounce time for search requests |
|
|
242
|
+
| pageSize | `number` | `50` | Page size for server requests |
|
|
243
|
+
| cacheLimit | `number` | `1000` | Maximum items in local cache |
|
|
244
|
+
| cacheStrategy | `'persist' \| 'session' \| 'memory' \| 'lru'` | `'memory'` | Cache storage strategy |
|
|
245
|
+
|
|
246
|
+
Example:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { DictStore } from '@reforgium/statum';
|
|
250
|
+
|
|
251
|
+
type Country = { code: string; name: string };
|
|
252
|
+
|
|
253
|
+
const dict = new DictStore<Country>('api/dictionaries/countries', 'countries', {
|
|
254
|
+
labelKey: 'name',
|
|
255
|
+
valueKey: 'code',
|
|
256
|
+
fixed: true,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
dict.search(''); // initial fill
|
|
260
|
+
dict.search('kir'); // local search over cache
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Serializer
|
|
266
|
+
|
|
267
|
+
### Serializer
|
|
268
|
+
|
|
269
|
+
Utility for serialization/deserialization between layers (UI ↔ API, objects ↔ query string).
|
|
270
|
+
|
|
271
|
+
Core API:
|
|
272
|
+
|
|
273
|
+
| Method | Description |
|
|
274
|
+
|---|---|
|
|
275
|
+
| serialize | Prepare data for transport |
|
|
276
|
+
| deserialize | Parse incoming data / query string |
|
|
277
|
+
| toQuery | Build query string |
|
|
278
|
+
| withConfig | Clone serializer with partial config overrides |
|
|
279
|
+
|
|
280
|
+
Config:
|
|
281
|
+
|
|
282
|
+
| Option | Type | Default | Description |
|
|
283
|
+
|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|--------------------------------|
|
|
284
|
+
| mapString | `{ trim?: boolean }` & ParseFormatConfig<string> | `{}` | String transformation options |
|
|
285
|
+
| mapNumber | `{ fromString?: boolean }` & ParseFormatConfig<number> | `{}` | Number parsing options |
|
|
286
|
+
| mapBoolean | `{ true?: string; false?: string }` & ParseFormatConfig<boolean> | `{}` | Boolean parsing options |
|
|
287
|
+
| mapDate | `{ dateFormat?: string }` & ParseFormatConfig<Date> | `{}` | Date parsing options |
|
|
288
|
+
| mapPeriod | `{ transformMode?: { mode: 'split'; dateFromKeyPostfix: string; dateToKeyPostfix: string } \| { mode: 'join'; concat: string }; dateFormat?: string }` & ParseFormatConfig<[Date \| null, Date \| null]> | `{}` | Period handling options |
|
|
289
|
+
| mapNullable | `{ remove?: boolean; replaceWith?: string; includeEmptyString?: boolean }` & ParseFormatConfig<null \| undefined> | `{}` | Null/undefined handling |
|
|
290
|
+
| mapArray | `{ concatType: 'comma' \| 'multi' \| 'json'; removeNullable?: boolean }` & `{ format?: (val: any[]) => any }` | `{}` | Array parsing options |
|
|
291
|
+
| mapObject | `{ deep: boolean }` & `{ format?: (val: Record<string, any>) => any }` | `{}` | Object transformation options |
|
|
292
|
+
| mapFields | `Record<string, { type: 'string'\|'number'\|'boolean'\|'array'\|'object'\|'date'\|'period'\|'nullable' } \| { parse: (val: any, data: Record<string, any>) => any; format: (val: any, data: Record<string, any>) => any }>` | `undefined` | Field-specific transformations |
|
|
293
|
+
|
|
294
|
+
Example:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
import { Serializer } from '@reforgium/statum';
|
|
298
|
+
|
|
299
|
+
const serializer = new Serializer({
|
|
300
|
+
mapString: { trim: true },
|
|
301
|
+
mapNullable: { remove: true },
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const body = serializer.serialize({ name: ' Vasya ', active: null });
|
|
305
|
+
// => { name: 'Vasya' }
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Source Structure
|
|
311
|
+
|
|
312
|
+
- Cache: `src/cache`
|
|
313
|
+
- Stores: `src/stores`
|
|
314
|
+
- Serializer: `src/serializer`
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Compatibility
|
|
319
|
+
|
|
320
|
+
- Angular: 18+
|
|
321
|
+
- Signals: required
|
|
322
|
+
- Zone.js: optional
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## License
|
|
327
|
+
|
|
328
|
+
MIT
|