@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 ADDED
@@ -0,0 +1,328 @@
1
+ # @reforgium/statum
2
+
3
+ [![npm version](https://badge.fury.io/js/%40recrafted%2Finternal.svg)](https://www.npmjs.com/package/@reforgium/internal)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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