@reforgium/statum 3.0.1 → 3.1.1
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 +218 -0
- package/LICENSE +21 -0
- package/README.md +33 -6
- package/fesm2022/reforgium-statum.mjs +137 -656
- package/package.json +17 -6
- package/types/reforgium-statum.d.ts +44 -310
|
@@ -1,380 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Serializer, fillUrlWithParams, mergeQueryParams, LruCache, deepEqual, isNullable, normalizeSortInput, sortInputToTokens, debounceSignal, storageStrategy } from '@reforgium/internal';
|
|
2
|
+
export { LocalStorage, LruCache, MemoryStorage, Serializer, SerializerFieldError, SessionStorage, storageStrategy } from '@reforgium/internal';
|
|
2
3
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
3
4
|
import { InjectionToken, makeEnvironmentProviders, inject, signal, computed, EnvironmentInjector, DestroyRef, runInInjectionContext, effect, untracked } from '@angular/core';
|
|
4
5
|
import { Subject, filter, timer, merge, map } from 'rxjs';
|
|
5
6
|
import { debounce, tap, throttle, finalize } from 'rxjs/operators';
|
|
6
7
|
|
|
7
|
-
const serializeString = (config, value) => config.mapString.format?.(value) ?? (config.mapString.trim ? value.trim() : value);
|
|
8
|
-
const serializeNumber = (config, value) => config.mapNumber.format?.(value) ?? Number(value);
|
|
9
|
-
const serializeBoolean = (config, value) => config.mapBoolean.format?.(value) ?? (value ? (config.mapBoolean.true ?? true) : (config.mapBoolean.false ?? false));
|
|
10
|
-
const serializeDate = (config, value) => config.mapDate.format?.(value) ?? formatDate(value, config.mapDate.dateFormat);
|
|
11
|
-
|
|
12
|
-
class SerializerFieldError extends Error {
|
|
13
|
-
field;
|
|
14
|
-
stage;
|
|
15
|
-
originalError;
|
|
16
|
-
constructor(field, stage, originalError) {
|
|
17
|
-
const originalMessage = originalError instanceof Error ? originalError.message : String(originalError);
|
|
18
|
-
super(`Serializer ${stage} error for field "${field}": ${originalMessage}`);
|
|
19
|
-
this.field = field;
|
|
20
|
-
this.stage = stage;
|
|
21
|
-
this.originalError = originalError;
|
|
22
|
-
this.name = 'SerializerFieldError';
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Universal serializer/deserializer for values used in forms, filters, and DTOs.
|
|
27
|
-
*
|
|
28
|
-
* Supports types: `string | number | boolean | Date | [Date, Date] (period) | array | object | nullable`.
|
|
29
|
-
* Capabilities:
|
|
30
|
-
* - normalize values according to config (trim strings, parse numbers from strings, boolean strings, etc.),
|
|
31
|
-
* - transform date periods into paired keys (`from/to`) or a single joined string,
|
|
32
|
-
* - build/parse a query string (or JSON) to and from an object.
|
|
33
|
-
*
|
|
34
|
-
* Example:
|
|
35
|
-
* ```ts
|
|
36
|
-
* type Filters = { q?: string; active?: boolean; created?: [Date, Date] | null };
|
|
37
|
-
* const s = new Serializer<Filters>({
|
|
38
|
-
* mapPeriod: { transformMode: { mode: 'split', dateFromKeyPostfix: 'From', dateToKeyPostfix: 'To' } }
|
|
39
|
-
* });
|
|
40
|
-
*
|
|
41
|
-
* // -> { q: 'john', createdFrom: '2025-01-01', createdTo: '2025-01-31' }
|
|
42
|
-
* const plain = s.serialize({
|
|
43
|
-
* q: ' john ',
|
|
44
|
-
* active: undefined,
|
|
45
|
-
* created: [new Date('2025-01-01'), new Date('2025-01-31')]
|
|
46
|
-
* });
|
|
47
|
-
*
|
|
48
|
-
* // -> 'q=john&createdFrom=2025-01-01&createdTo=2025-01-31'
|
|
49
|
-
* const qs = s.toQuery({ q: 'john', created: [new Date('2025-01-01'), new Date('2025-01-31')] });
|
|
50
|
-
*
|
|
51
|
-
* // <- { q: 'john', created: [Date, Date] }
|
|
52
|
-
* const parsed = s.deserialize('q=john&createdFrom=2025-01-01&createdTo=2025-01-31');
|
|
53
|
-
* ```
|
|
54
|
-
*/
|
|
55
|
-
class Serializer {
|
|
56
|
-
config;
|
|
57
|
-
/**
|
|
58
|
-
* Creates a serializer with a partially overridden configuration.
|
|
59
|
-
* Provide only the options you want to change (the rest are taken from defaults).
|
|
60
|
-
*
|
|
61
|
-
* @param config partial transformation configuration
|
|
62
|
-
*/
|
|
63
|
-
constructor(config = {}) {
|
|
64
|
-
this.config = this.mergeConfig({
|
|
65
|
-
mapString: { trim: true },
|
|
66
|
-
mapNumber: { fromString: false },
|
|
67
|
-
mapBoolean: {},
|
|
68
|
-
mapArray: { concatType: 'comma' },
|
|
69
|
-
mapObject: { deep: true },
|
|
70
|
-
mapDate: { dateFormat: 'yyyy-MM-dd' },
|
|
71
|
-
mapPeriod: {
|
|
72
|
-
dateFormat: 'yyyy-MM-dd',
|
|
73
|
-
transformMode: { mode: 'split', dateFromKeyPostfix: 'From', dateToKeyPostfix: 'To' },
|
|
74
|
-
},
|
|
75
|
-
mapNullable: { remove: true, includeEmptyString: false },
|
|
76
|
-
}, config);
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Converts a domain object into a flat serialized representation
|
|
80
|
-
* (ready to send to an API or build a query string).
|
|
81
|
-
*
|
|
82
|
-
* Rules are taken from `config`:
|
|
83
|
-
* — strings can be trimmed (if enabled),
|
|
84
|
-
* — numbers can be converted from strings/numbers,
|
|
85
|
-
* — boolean supports custom true/false representations,
|
|
86
|
-
* — dates are formatted by `dateFormat`,
|
|
87
|
-
* — date periods can be split/joined,
|
|
88
|
-
* — `nullable` can be removed from the result (`remove`) or formatted.
|
|
89
|
-
*
|
|
90
|
-
* @param obj source object
|
|
91
|
-
* @returns a flat dictionary with string/primitive values
|
|
92
|
-
*/
|
|
93
|
-
serialize(obj) {
|
|
94
|
-
const result = {};
|
|
95
|
-
for (const [key, value] of Object.entries(obj ?? {})) {
|
|
96
|
-
const fields = this.config.mapFields?.[key];
|
|
97
|
-
if (fields && 'format' in fields) {
|
|
98
|
-
try {
|
|
99
|
-
result[key] = fields.format(value, obj);
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
throw new SerializerFieldError(key, 'format', error);
|
|
103
|
-
}
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
if (fields?.type === 'nullable' || isNullable(value, this.config.mapNullable?.includeEmptyString)) {
|
|
107
|
-
if (this.config.mapNullable.remove) {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (fields?.type === 'period' || isDatePeriod(value)) {
|
|
112
|
-
const transform = this.config.mapPeriod.transformMode;
|
|
113
|
-
const [from, to] = value;
|
|
114
|
-
if (transform?.mode === 'split') {
|
|
115
|
-
result[`${key}${transform.dateFromKeyPostfix}`] = formatDate(from, this.config.mapPeriod.dateFormat);
|
|
116
|
-
result[`${key}${transform.dateToKeyPostfix}`] = formatDate(to, this.config.mapPeriod.dateFormat);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
result[key] = this.serializeElement(value, key);
|
|
121
|
-
}
|
|
122
|
-
return result;
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Parse serialized data into a domain object.
|
|
126
|
-
*
|
|
127
|
-
* Source can be:
|
|
128
|
-
* — a query string (`key=value&arr=1,2`) or `JSON.stringify(obj)`,
|
|
129
|
-
* — an already prepared flat object.
|
|
130
|
-
*
|
|
131
|
-
* Transformations are reverse of `serialize`: strings → number/boolean/Date/period,
|
|
132
|
-
* arrays are collected according to strategy (`comma`/`pipe`/`multi`), objects — deeply or as JSON.
|
|
133
|
-
*
|
|
134
|
-
* @param val query string or object
|
|
135
|
-
* @returns a domain object of the specified type
|
|
136
|
-
*/
|
|
137
|
-
deserialize = (val) => {
|
|
138
|
-
const data = typeof val === 'string' ? this.parseInputString(val) : val;
|
|
139
|
-
const result = {};
|
|
140
|
-
for (const [key, value] of Object.entries(data ?? {})) {
|
|
141
|
-
const field = this.config.mapFields?.[key];
|
|
142
|
-
if (field && 'parse' in field) {
|
|
143
|
-
try {
|
|
144
|
-
result[key] = field.parse(value, data);
|
|
145
|
-
}
|
|
146
|
-
catch (error) {
|
|
147
|
-
throw new SerializerFieldError(key, 'parse', error);
|
|
148
|
-
}
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (field?.type === 'nullable' ||
|
|
152
|
-
(field?.type !== 'array' && isNullable(value, this.config.mapNullable?.includeEmptyString))) {
|
|
153
|
-
if (this.config.mapNullable.remove) {
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const periodTransform = this.config.mapPeriod.transformMode;
|
|
158
|
-
if (periodTransform.mode === 'split') {
|
|
159
|
-
const isFrom = (key || '').endsWith(periodTransform.dateFromKeyPostfix);
|
|
160
|
-
const isTo = (key || '').endsWith(periodTransform.dateToKeyPostfix);
|
|
161
|
-
const keyJoint = (key || '')
|
|
162
|
-
.replace(periodTransform.dateFromKeyPostfix, '')
|
|
163
|
-
.replace(periodTransform.dateToKeyPostfix, '');
|
|
164
|
-
const field = this.config.mapFields?.[keyJoint];
|
|
165
|
-
const fieldType = field && 'type' in field ? field.type : undefined;
|
|
166
|
-
if (fieldType === 'period' && (isFrom || isTo)) {
|
|
167
|
-
result[keyJoint] ??= [null, null];
|
|
168
|
-
if (isFrom) {
|
|
169
|
-
result[keyJoint][0] = parseToDate(value, this.config.mapPeriod.dateFormat);
|
|
170
|
-
}
|
|
171
|
-
else if (isTo) {
|
|
172
|
-
result[keyJoint][1] = parseToDate(value, this.config.mapPeriod.dateFormat);
|
|
173
|
-
}
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
result[key] = this.deserializeElement(value, key);
|
|
178
|
-
}
|
|
179
|
-
return result;
|
|
180
|
-
};
|
|
181
|
-
/** Parse only query-string input. */
|
|
182
|
-
deserializeQuery = (query) => {
|
|
183
|
-
return this.deserialize(this.parseQuery(query));
|
|
184
|
-
};
|
|
185
|
-
/** Parse only JSON object input. */
|
|
186
|
-
deserializeJson = (json) => {
|
|
187
|
-
return this.deserialize(this.parseJsonObject(json));
|
|
188
|
-
};
|
|
189
|
-
/**
|
|
190
|
-
* Build a query string from a domain object using `serialize` rules
|
|
191
|
-
* and the array joining strategy (`concatType`).
|
|
192
|
-
*
|
|
193
|
-
* @param val domain object
|
|
194
|
-
* @returns query string (suitable for URL or history API)
|
|
195
|
-
*/
|
|
196
|
-
toQuery = (val) => {
|
|
197
|
-
return buildQueryParams(this.serialize(val), this.config.mapArray.concatType, this.resolveArrayFieldModes());
|
|
198
|
-
};
|
|
199
|
-
/**
|
|
200
|
-
* Returns a new serializer instance with a merged configuration.
|
|
201
|
-
* Useful for ad-hoc overrides for a specific call.
|
|
202
|
-
*
|
|
203
|
-
* @param config partial config changes
|
|
204
|
-
* @returns new `Serializer` with the provided `config` applied
|
|
205
|
-
*/
|
|
206
|
-
withConfig(config) {
|
|
207
|
-
return new Serializer(this.mergeConfig(this.config, config));
|
|
208
|
-
}
|
|
209
|
-
serializeElement(value, key) {
|
|
210
|
-
const fields = this.config.mapFields?.[key || ''];
|
|
211
|
-
if (fields && 'format' in fields) {
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
if (fields?.type === 'nullable' || isNullable(value, this.config.mapNullable?.includeEmptyString)) {
|
|
215
|
-
const nullableVal = value ?? null;
|
|
216
|
-
return this.config.mapNullable.format?.(nullableVal) || this.config.mapNullable.replaceWith || nullableVal;
|
|
217
|
-
}
|
|
218
|
-
if (fields?.type === 'string') {
|
|
219
|
-
return serializeString(this.config, value);
|
|
220
|
-
}
|
|
221
|
-
if (fields?.type === 'number' || isNumber(value, this.config.mapNumber.fromString)) {
|
|
222
|
-
return serializeNumber(this.config, value);
|
|
223
|
-
}
|
|
224
|
-
if (typeof value === 'string') {
|
|
225
|
-
return serializeString(this.config, value);
|
|
226
|
-
}
|
|
227
|
-
if (fields?.type === 'boolean' || typeof value === 'boolean') {
|
|
228
|
-
return serializeBoolean(this.config, value);
|
|
229
|
-
}
|
|
230
|
-
if (fields?.type === 'date' || value instanceof Date) {
|
|
231
|
-
return serializeDate(this.config, value);
|
|
232
|
-
}
|
|
233
|
-
if (fields?.type === 'period' || isDatePeriod(value)) {
|
|
234
|
-
const mapPeriod = this.config.mapPeriod;
|
|
235
|
-
if (mapPeriod.format) {
|
|
236
|
-
return mapPeriod.format(value);
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
const [from, to] = value;
|
|
240
|
-
const transform = mapPeriod.transformMode;
|
|
241
|
-
if (transform.mode === 'join') {
|
|
242
|
-
const period = [
|
|
243
|
-
formatDate(from, this.config.mapPeriod.dateFormat),
|
|
244
|
-
formatDate(to, this.config.mapPeriod.dateFormat),
|
|
245
|
-
];
|
|
246
|
-
return period.join(transform.concat);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
if (fields?.type === 'array' || Array.isArray(value)) {
|
|
251
|
-
return value.map((it) => this.serializeElement(it));
|
|
252
|
-
}
|
|
253
|
-
if (fields?.type === 'object' || isObject(value)) {
|
|
254
|
-
return (this.config.mapObject.format?.(value) ??
|
|
255
|
-
(this.config.mapObject.deep ? this.serialize(value) : JSON.stringify(value)));
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
deserializeElement(value, key) {
|
|
259
|
-
const field = this.config.mapFields?.[key || ''];
|
|
260
|
-
if (field && 'format' in field) {
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
if (field?.type === 'array' || Array.isArray(value)) {
|
|
264
|
-
const array = Array.isArray(value) ? value : [value];
|
|
265
|
-
if (this.config.mapArray.removeNullable) {
|
|
266
|
-
if (!isNullable(value, this.config.mapNullable?.includeEmptyString)) {
|
|
267
|
-
return array.map((it) => this.deserializeElement(it));
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
else {
|
|
274
|
-
return !value ? [] : array.map((it) => this.deserializeElement(it));
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
if (field?.type === 'object') {
|
|
278
|
-
try {
|
|
279
|
-
if (this.config.mapObject.deep) {
|
|
280
|
-
return isObject(value) ? this.deserialize(value) : value;
|
|
281
|
-
}
|
|
282
|
-
else {
|
|
283
|
-
return typeof value === 'string' ? JSON.parse(value) : value;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
catch {
|
|
287
|
-
return value;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
if (field?.type === 'nullable' || isNullable(value, this.config.mapNullable?.includeEmptyString)) {
|
|
291
|
-
return this.config.mapNullable.parse?.(value) || value;
|
|
292
|
-
}
|
|
293
|
-
if (field?.type === 'boolean' ||
|
|
294
|
-
typeof value === 'boolean' ||
|
|
295
|
-
value === this.config.mapBoolean.true ||
|
|
296
|
-
value === this.config.mapBoolean.false) {
|
|
297
|
-
return this.config.mapBoolean.parse?.(value) ?? (value === this.config.mapBoolean.true || value === true);
|
|
298
|
-
}
|
|
299
|
-
const maybeDate = parseToDate(value, this.config.mapDate.dateFormat);
|
|
300
|
-
if (field?.type === 'date' || maybeDate) {
|
|
301
|
-
return this.config.mapDate.parse?.(value) || maybeDate;
|
|
302
|
-
}
|
|
303
|
-
const periodTransform = this.config.mapPeriod.transformMode;
|
|
304
|
-
if (periodTransform.mode === 'join') {
|
|
305
|
-
const maybePeriod = parseToDatePeriod(value, this.config.mapPeriod.dateFormat);
|
|
306
|
-
if (field?.type === 'period' || (maybePeriod || []).some(Boolean)) {
|
|
307
|
-
return this.config.mapPeriod.parse?.(value) || maybePeriod;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
if (field?.type === 'number' || isNumber(value, this.config.mapNumber.fromString)) {
|
|
311
|
-
return this.config.mapNumber.parse?.(value) || Number(String(value).trim());
|
|
312
|
-
}
|
|
313
|
-
if (field?.type === 'string' || typeof value === 'string') {
|
|
314
|
-
const parsed = this.config.mapString.parse?.(value);
|
|
315
|
-
if (parsed !== undefined) {
|
|
316
|
-
return parsed;
|
|
317
|
-
}
|
|
318
|
-
return this.config.mapString.trim ? String(value).trim() : value;
|
|
319
|
-
}
|
|
320
|
-
return value;
|
|
321
|
-
}
|
|
322
|
-
parseQuery(val) {
|
|
323
|
-
return parseQueryParamsByMode(val, undefined, this.resolveArrayFieldModes());
|
|
324
|
-
}
|
|
325
|
-
parseJsonObject(val) {
|
|
326
|
-
try {
|
|
327
|
-
const parsed = JSON.parse(val);
|
|
328
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
329
|
-
return parsed;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
catch (error) {
|
|
333
|
-
throw new Error(`Invalid JSON input: ${error instanceof Error ? error.message : String(error)}`);
|
|
334
|
-
}
|
|
335
|
-
throw new Error('Invalid JSON input: expected a JSON object');
|
|
336
|
-
}
|
|
337
|
-
parseInputString(val) {
|
|
338
|
-
try {
|
|
339
|
-
return this.parseJsonObject(val);
|
|
340
|
-
}
|
|
341
|
-
catch {
|
|
342
|
-
return this.parseQuery(val);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
mergeConfig(base, override) {
|
|
346
|
-
return {
|
|
347
|
-
...base,
|
|
348
|
-
...override,
|
|
349
|
-
mapString: { ...base.mapString, ...override.mapString },
|
|
350
|
-
mapNumber: { ...base.mapNumber, ...override.mapNumber },
|
|
351
|
-
mapBoolean: { ...base.mapBoolean, ...override.mapBoolean },
|
|
352
|
-
mapDate: { ...base.mapDate, ...override.mapDate },
|
|
353
|
-
mapPeriod: {
|
|
354
|
-
...base.mapPeriod,
|
|
355
|
-
...override.mapPeriod,
|
|
356
|
-
transformMode: {
|
|
357
|
-
...base.mapPeriod.transformMode,
|
|
358
|
-
...override.mapPeriod?.transformMode,
|
|
359
|
-
},
|
|
360
|
-
},
|
|
361
|
-
mapNullable: { ...base.mapNullable, ...override.mapNullable },
|
|
362
|
-
mapArray: { ...base.mapArray, ...override.mapArray },
|
|
363
|
-
mapObject: { ...base.mapObject, ...override.mapObject },
|
|
364
|
-
mapFields: { ...(base.mapFields || {}), ...(override.mapFields || {}) },
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
resolveArrayFieldModes() {
|
|
368
|
-
const result = {};
|
|
369
|
-
Object.entries(this.config.mapFields || {}).forEach(([key, field]) => {
|
|
370
|
-
if ('type' in field && field.type === 'array') {
|
|
371
|
-
result[key] = field.concatType ?? this.config.mapArray.concatType;
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
return result;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
8
|
const createQuerySerializer = (config = {}) => {
|
|
379
9
|
return new Serializer({
|
|
380
10
|
mapArray: { concatType: 'comma' },
|
|
@@ -400,197 +30,6 @@ const createStrictSerializer = (config = {}) => {
|
|
|
400
30
|
});
|
|
401
31
|
};
|
|
402
32
|
|
|
403
|
-
class LruCache {
|
|
404
|
-
map = new Map();
|
|
405
|
-
_limit = 100;
|
|
406
|
-
constructor(limit = 100) {
|
|
407
|
-
this.limit = limit;
|
|
408
|
-
}
|
|
409
|
-
get limit() {
|
|
410
|
-
return this._limit;
|
|
411
|
-
}
|
|
412
|
-
get length() {
|
|
413
|
-
return this.map.size;
|
|
414
|
-
}
|
|
415
|
-
set limit(value) {
|
|
416
|
-
this._limit = Math.max(1, Math.floor(value || 0));
|
|
417
|
-
}
|
|
418
|
-
get(key) {
|
|
419
|
-
if (!this.map.has(key)) {
|
|
420
|
-
return null;
|
|
421
|
-
}
|
|
422
|
-
const val = this.map.get(key);
|
|
423
|
-
this.map.delete(key);
|
|
424
|
-
this.map.set(key, val);
|
|
425
|
-
return val;
|
|
426
|
-
}
|
|
427
|
-
set(key, value) {
|
|
428
|
-
if (this.map.has(key)) {
|
|
429
|
-
this.map.delete(key);
|
|
430
|
-
}
|
|
431
|
-
else if (this.map.size >= this.limit) {
|
|
432
|
-
const oldest = this.map.keys().next().value;
|
|
433
|
-
oldest !== undefined && this.map.delete(oldest);
|
|
434
|
-
}
|
|
435
|
-
this.map.set(key, value);
|
|
436
|
-
}
|
|
437
|
-
remove(key) {
|
|
438
|
-
return this.map.delete(key);
|
|
439
|
-
}
|
|
440
|
-
clear() {
|
|
441
|
-
this.map.clear();
|
|
442
|
-
}
|
|
443
|
-
has(key) {
|
|
444
|
-
return this.map.has(key);
|
|
445
|
-
}
|
|
446
|
-
keys() {
|
|
447
|
-
return Array.from(this.map.keys());
|
|
448
|
-
}
|
|
449
|
-
values() {
|
|
450
|
-
return Array.from(this.map.values());
|
|
451
|
-
}
|
|
452
|
-
entries() {
|
|
453
|
-
return Array.from(this.map.entries());
|
|
454
|
-
}
|
|
455
|
-
toArray() {
|
|
456
|
-
return Array.from(this.map.values());
|
|
457
|
-
}
|
|
458
|
-
fromArray(entries) {
|
|
459
|
-
this.map.clear();
|
|
460
|
-
for (const [k, v] of entries) {
|
|
461
|
-
this.set(k, v);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
class LocalStorage {
|
|
467
|
-
prefix;
|
|
468
|
-
constructor(prefix = 're') {
|
|
469
|
-
this.prefix = prefix;
|
|
470
|
-
}
|
|
471
|
-
get length() {
|
|
472
|
-
return Object.keys(localStorage).filter((key) => key.startsWith(this.safePrefix())).length;
|
|
473
|
-
}
|
|
474
|
-
get(key) {
|
|
475
|
-
const storageKey = this.getSafePrefix(key);
|
|
476
|
-
const raw = localStorage.getItem(storageKey);
|
|
477
|
-
if (raw == null) {
|
|
478
|
-
return null;
|
|
479
|
-
}
|
|
480
|
-
try {
|
|
481
|
-
const parsed = JSON.parse(raw);
|
|
482
|
-
return parsed ?? null;
|
|
483
|
-
}
|
|
484
|
-
catch {
|
|
485
|
-
localStorage.removeItem(storageKey);
|
|
486
|
-
return null;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
set(key, value) {
|
|
490
|
-
const str = JSON.stringify(value);
|
|
491
|
-
localStorage.setItem(this.getSafePrefix(key), str);
|
|
492
|
-
}
|
|
493
|
-
remove(key) {
|
|
494
|
-
return localStorage.removeItem(this.getSafePrefix(key));
|
|
495
|
-
}
|
|
496
|
-
clear() {
|
|
497
|
-
const keys = Object.keys(localStorage).filter((key) => key.startsWith(this.safePrefix()));
|
|
498
|
-
keys.forEach((key) => localStorage.removeItem(key));
|
|
499
|
-
}
|
|
500
|
-
getSafePrefix(key) {
|
|
501
|
-
return this.prefix ? `${this.prefix}:${key}` : String(key);
|
|
502
|
-
}
|
|
503
|
-
safePrefix() {
|
|
504
|
-
return this.prefix ? `${this.prefix}:` : '';
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
class MemoryStorage {
|
|
509
|
-
cache = new Map();
|
|
510
|
-
get length() {
|
|
511
|
-
return this.cache.size;
|
|
512
|
-
}
|
|
513
|
-
get(key) {
|
|
514
|
-
return this.cache.get(key) ?? null;
|
|
515
|
-
}
|
|
516
|
-
set(key, value) {
|
|
517
|
-
this.cache.set(key, value);
|
|
518
|
-
}
|
|
519
|
-
remove(key) {
|
|
520
|
-
this.cache.delete(key);
|
|
521
|
-
}
|
|
522
|
-
clear() {
|
|
523
|
-
this.cache.clear();
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
class SessionStorage {
|
|
528
|
-
prefix;
|
|
529
|
-
constructor(prefix = 're') {
|
|
530
|
-
this.prefix = prefix;
|
|
531
|
-
}
|
|
532
|
-
get length() {
|
|
533
|
-
return Object.keys(sessionStorage).filter((key) => key.startsWith(this.safePrefix())).length;
|
|
534
|
-
}
|
|
535
|
-
get(key) {
|
|
536
|
-
const storageKey = this.getSafePrefix(key);
|
|
537
|
-
const raw = sessionStorage.getItem(storageKey);
|
|
538
|
-
if (raw == null) {
|
|
539
|
-
return null;
|
|
540
|
-
}
|
|
541
|
-
try {
|
|
542
|
-
const parsed = JSON.parse(raw);
|
|
543
|
-
return parsed ?? null;
|
|
544
|
-
}
|
|
545
|
-
catch {
|
|
546
|
-
sessionStorage.removeItem(storageKey);
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
set(key, value) {
|
|
551
|
-
const str = JSON.stringify(value);
|
|
552
|
-
sessionStorage.setItem(this.getSafePrefix(key), str);
|
|
553
|
-
}
|
|
554
|
-
remove(key) {
|
|
555
|
-
return sessionStorage.removeItem(this.getSafePrefix(key));
|
|
556
|
-
}
|
|
557
|
-
clear() {
|
|
558
|
-
const keys = Object.keys(sessionStorage).filter((key) => key.startsWith(this.safePrefix()));
|
|
559
|
-
keys.forEach((key) => sessionStorage.removeItem(key));
|
|
560
|
-
}
|
|
561
|
-
getSafePrefix(key) {
|
|
562
|
-
return this.prefix ? `${this.prefix}:${key}` : String(key);
|
|
563
|
-
}
|
|
564
|
-
safePrefix() {
|
|
565
|
-
return this.prefix ? `${this.prefix}:` : '';
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Factory for data storage strategies.
|
|
571
|
-
*
|
|
572
|
-
* Returns a `StorageInterface` implementation depending on the selected strategy:
|
|
573
|
-
* - `'memory'` — in-memory storage (for the session lifetime);
|
|
574
|
-
* - `'session'` — `sessionStorage`, lives until the tab is closed;
|
|
575
|
-
* - `'persist'` — `localStorage`, persists between sessions;
|
|
576
|
-
* - `'lru'` — size-limited cache (Least Recently Used).
|
|
577
|
-
*
|
|
578
|
-
* Used to choose an appropriate storage implementation
|
|
579
|
-
* depending on the scenario: temporary data, long-term, cache, etc.
|
|
580
|
-
*
|
|
581
|
-
* @param strategy storage strategy type (`memory`, `session`, `persist`, `lru`)
|
|
582
|
-
* @returns instance implementing `StorageInterface<Key, Type>`
|
|
583
|
-
*/
|
|
584
|
-
const storageStrategy = (strategy, options = {}) => {
|
|
585
|
-
const fabrics = {
|
|
586
|
-
memory: () => new MemoryStorage(),
|
|
587
|
-
session: () => new SessionStorage(),
|
|
588
|
-
persist: () => new LocalStorage(),
|
|
589
|
-
lru: () => new LruCache(options.lruLimit ?? 100),
|
|
590
|
-
};
|
|
591
|
-
return fabrics[strategy]();
|
|
592
|
-
};
|
|
593
|
-
|
|
594
33
|
// noinspection ES6PreferShortImport
|
|
595
34
|
const STATUM_CONFIG = new InjectionToken('RE_STATUM_CONFIG');
|
|
596
35
|
const provideStatum = (config) => makeEnvironmentProviders([{ provide: STATUM_CONFIG, useValue: config }]);
|
|
@@ -638,6 +77,7 @@ class AbortError extends Error {
|
|
|
638
77
|
*/
|
|
639
78
|
function isAbort(e) {
|
|
640
79
|
return (e instanceof AbortError ||
|
|
80
|
+
(e instanceof DOMException && e.name === 'AbortError') ||
|
|
641
81
|
(typeof e === 'object' && e != null && e?.name === 'AbortError' && e?.message === 'aborted'));
|
|
642
82
|
}
|
|
643
83
|
function joinUrl(base, path) {
|
|
@@ -837,10 +277,10 @@ class KeyedScheduler {
|
|
|
837
277
|
class ResourceStore {
|
|
838
278
|
http = inject(HttpClient);
|
|
839
279
|
serializer;
|
|
840
|
-
#value = signal(null, ...(ngDevMode ? [{ debugName: "#value" }] : []));
|
|
841
|
-
#status = signal('idle', ...(ngDevMode ? [{ debugName: "#status" }] : []));
|
|
842
|
-
#error = signal(null, ...(ngDevMode ? [{ debugName: "#error" }] : []));
|
|
843
|
-
#activeRequests = signal(0, ...(ngDevMode ? [{ debugName: "#activeRequests" }] : []));
|
|
280
|
+
#value = signal(null, ...(ngDevMode ? [{ debugName: "#value" }] : /* istanbul ignore next */ []));
|
|
281
|
+
#status = signal('idle', ...(ngDevMode ? [{ debugName: "#status" }] : /* istanbul ignore next */ []));
|
|
282
|
+
#error = signal(null, ...(ngDevMode ? [{ debugName: "#error" }] : /* istanbul ignore next */ []));
|
|
283
|
+
#activeRequests = signal(0, ...(ngDevMode ? [{ debugName: "#activeRequests" }] : /* istanbul ignore next */ []));
|
|
844
284
|
/**
|
|
845
285
|
* Current resource value.
|
|
846
286
|
* Returns `null` if no data yet or the request failed.
|
|
@@ -858,7 +298,7 @@ class ResourceStore {
|
|
|
858
298
|
* Convenience loading flag: `true` when `loading` or `stale`.
|
|
859
299
|
* Useful for spinners and disabling buttons.
|
|
860
300
|
*/
|
|
861
|
-
loading = computed(() => this.#activeRequests() > 0 || this.#status() === 'stale', ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
301
|
+
loading = computed(() => this.#activeRequests() > 0 || this.#status() === 'stale', ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
862
302
|
routes;
|
|
863
303
|
opts;
|
|
864
304
|
maxEntries;
|
|
@@ -906,15 +346,20 @@ class ResourceStore {
|
|
|
906
346
|
catch (error) {
|
|
907
347
|
return Promise.reject(error);
|
|
908
348
|
}
|
|
909
|
-
const entry = this.ensureEntry(key);
|
|
910
349
|
const strategy = cfg.strategy ?? 'network-first';
|
|
911
350
|
const ttlMs = cfg.ttlMs ?? this.opts.ttlMs ?? 0;
|
|
912
|
-
|
|
913
|
-
if (
|
|
351
|
+
let entry = this.entries.get(key);
|
|
352
|
+
if (entry) {
|
|
353
|
+
// Keep hot keys at the end to approximate LRU order on cache reads too.
|
|
354
|
+
this.entries.delete(key);
|
|
355
|
+
this.entries.set(key, entry);
|
|
356
|
+
}
|
|
357
|
+
const fresh = entry?.updatedAt != null && Date.now() - entry.updatedAt < ttlMs;
|
|
358
|
+
if (cfg.dedupe && entry?.inflight) {
|
|
914
359
|
return entry.inflight;
|
|
915
360
|
}
|
|
916
361
|
if (strategy === 'cache-only') {
|
|
917
|
-
if (fresh && entry
|
|
362
|
+
if (fresh && entry?.data != null) {
|
|
918
363
|
this.trace({ type: 'cache-hit', method: 'GET', key, strategy });
|
|
919
364
|
this.promoteCurrent(entry, cfg.promote);
|
|
920
365
|
return Promise.resolve(entry.data);
|
|
@@ -922,29 +367,44 @@ class ResourceStore {
|
|
|
922
367
|
this.trace({ type: 'cache-miss', method: 'GET', key, strategy });
|
|
923
368
|
return Promise.reject(new CacheMissError(key));
|
|
924
369
|
}
|
|
925
|
-
if (strategy === 'cache-first' && fresh && !cfg.revalidate && entry
|
|
370
|
+
if (strategy === 'cache-first' && fresh && !cfg.revalidate && entry?.data != null) {
|
|
926
371
|
this.trace({ type: 'cache-hit', method: 'GET', key, strategy });
|
|
927
372
|
this.promoteCurrent(entry, cfg.promote);
|
|
928
373
|
return Promise.resolve(entry.data);
|
|
929
374
|
}
|
|
375
|
+
entry ??= this.ensureEntry(key);
|
|
930
376
|
const delay = cfg.delay ?? this.opts.delay ?? 0;
|
|
931
377
|
const mode = cfg.delayMode ?? this.opts.delayMode ?? 'debounce';
|
|
932
378
|
const isSWR = strategy === 'cache-first' && entry.data != null;
|
|
933
379
|
entry.status = isSWR ? 'stale' : 'loading';
|
|
380
|
+
if (!isSWR) {
|
|
381
|
+
entry.error = null;
|
|
382
|
+
}
|
|
934
383
|
this.promoteCurrent(entry, cfg.promote);
|
|
935
384
|
const retry = this.resolveRetryConfig(cfg.retry);
|
|
936
385
|
const taskWithRetry = this.runWithRetry((attempt) => {
|
|
937
386
|
this.trace({ type: 'request-start', method: 'GET', key, attempt });
|
|
938
|
-
|
|
939
|
-
|
|
387
|
+
entry.controller = new AbortController();
|
|
388
|
+
const options = {
|
|
389
|
+
params: query,
|
|
390
|
+
responseType: responseType,
|
|
391
|
+
observe: 'response',
|
|
392
|
+
signal: entry.controller.signal,
|
|
393
|
+
};
|
|
394
|
+
const scheduled = this.scheduler.schedule(key, mode, delay, this.exec$({
|
|
395
|
+
req$: this.http.get(url, options),
|
|
940
396
|
entry,
|
|
941
397
|
promote: cfg.promote,
|
|
942
398
|
parseFn: cfg.parseResponse,
|
|
399
|
+
observe: cfg.observe,
|
|
943
400
|
}));
|
|
401
|
+
void scheduled.catch(() => undefined);
|
|
402
|
+
return scheduled;
|
|
944
403
|
}, retry, {
|
|
945
404
|
method: 'GET',
|
|
946
405
|
key,
|
|
947
406
|
});
|
|
407
|
+
void taskWithRetry.catch(() => undefined);
|
|
948
408
|
const resolvedTask = taskWithRetry
|
|
949
409
|
.catch((error) => {
|
|
950
410
|
if (isAbort(error)) {
|
|
@@ -1036,9 +496,12 @@ class ResourceStore {
|
|
|
1036
496
|
const entry = this.entries.get(key);
|
|
1037
497
|
this.trace({ type: 'abort', method, key });
|
|
1038
498
|
if (entry) {
|
|
499
|
+
entry.controller?.abort(reason instanceof Error ? reason : new AbortError(typeof reason === 'string' ? reason : undefined));
|
|
500
|
+
entry.controller = undefined;
|
|
1039
501
|
entry.inflight = undefined;
|
|
1040
502
|
if (entry.status === 'loading' || entry.status === 'stale') {
|
|
1041
503
|
entry.status = 'idle';
|
|
504
|
+
this.promoteCurrent(entry, entry.promoted);
|
|
1042
505
|
}
|
|
1043
506
|
}
|
|
1044
507
|
this.scheduler.cancel?.(key, reason);
|
|
@@ -1050,10 +513,14 @@ class ResourceStore {
|
|
|
1050
513
|
*/
|
|
1051
514
|
abortAll(reason) {
|
|
1052
515
|
this.trace({ type: 'abort', method: 'GET', key: '*' });
|
|
516
|
+
const abortReason = reason instanceof Error ? reason : new AbortError(typeof reason === 'string' ? reason : undefined);
|
|
1053
517
|
this.entries.forEach((entry) => {
|
|
518
|
+
entry.controller?.abort(abortReason);
|
|
519
|
+
entry.controller = undefined;
|
|
1054
520
|
entry.inflight = undefined;
|
|
1055
521
|
if (entry.status === 'loading' || entry.status === 'stale') {
|
|
1056
522
|
entry.status = 'idle';
|
|
523
|
+
this.promoteCurrent(entry, entry.promoted);
|
|
1057
524
|
}
|
|
1058
525
|
});
|
|
1059
526
|
this.scheduler.cancelAll?.(reason);
|
|
@@ -1076,13 +543,18 @@ class ResourceStore {
|
|
|
1076
543
|
while (this.entries.size >= this.maxEntries) {
|
|
1077
544
|
let keyToDelete = null;
|
|
1078
545
|
for (const [key, value] of this.entries.entries()) {
|
|
1079
|
-
if (!value.inflight) {
|
|
546
|
+
if (!value.inflight && !value.controller) {
|
|
1080
547
|
keyToDelete = key;
|
|
1081
548
|
break;
|
|
1082
549
|
}
|
|
1083
550
|
}
|
|
1084
551
|
if (keyToDelete === null) {
|
|
552
|
+
// All entries have active requests — forced eviction. Dedupe for the evicted
|
|
553
|
+
// key will break: the in-flight request will finish but its result won't be cached.
|
|
1085
554
|
keyToDelete = this.entries.keys().next().value ?? null;
|
|
555
|
+
if (keyToDelete !== null) {
|
|
556
|
+
this.trace({ type: 'abort', method: 'GET', key: keyToDelete });
|
|
557
|
+
}
|
|
1086
558
|
}
|
|
1087
559
|
if (keyToDelete === null) {
|
|
1088
560
|
break;
|
|
@@ -1114,25 +586,35 @@ class ResourceStore {
|
|
|
1114
586
|
const mode = config.delayMode ?? this.opts.delayMode ?? 'debounce';
|
|
1115
587
|
const retry = this.resolveRetryConfig(config.retry);
|
|
1116
588
|
entry.status = 'loading';
|
|
589
|
+
entry.error = null;
|
|
1117
590
|
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
591
|
const task = this.runWithRetry((attempt) => {
|
|
1134
592
|
this.trace({ type: 'request-start', method, key, attempt });
|
|
1135
|
-
|
|
593
|
+
entry.controller = new AbortController();
|
|
594
|
+
const signal = entry.controller.signal;
|
|
595
|
+
let req$;
|
|
596
|
+
if (method === 'DELETE') {
|
|
597
|
+
// @ts-ignore
|
|
598
|
+
req$ = this.http.delete(url, {
|
|
599
|
+
body: payload,
|
|
600
|
+
params: query,
|
|
601
|
+
responseType: responseType,
|
|
602
|
+
observe: 'response',
|
|
603
|
+
signal,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
// @ts-ignore
|
|
608
|
+
req$ = this.mutationMethods[method](url, payload, {
|
|
609
|
+
params: query,
|
|
610
|
+
responseType: responseType,
|
|
611
|
+
observe: 'response',
|
|
612
|
+
signal,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
const scheduled = this.scheduler.schedule(key, mode, delay, this.exec$({ req$, entry, promote: config.promote, parseFn: config.parseResponse, observe: config.observe }));
|
|
616
|
+
void scheduled.catch(() => undefined);
|
|
617
|
+
return scheduled;
|
|
1136
618
|
}, retry, { method, key })
|
|
1137
619
|
.then((data) => {
|
|
1138
620
|
this.trace({ type: 'cache-write', method, key });
|
|
@@ -1181,6 +663,9 @@ class ResourceStore {
|
|
|
1181
663
|
});
|
|
1182
664
|
}
|
|
1183
665
|
preparePayload(payload) {
|
|
666
|
+
if (payload instanceof FormData || payload instanceof Blob || payload instanceof ArrayBuffer) {
|
|
667
|
+
return payload;
|
|
668
|
+
}
|
|
1184
669
|
const presetPayload = this.opts.presetPayload;
|
|
1185
670
|
if (!presetPayload && !payload) {
|
|
1186
671
|
return {};
|
|
@@ -1214,6 +699,7 @@ class ResourceStore {
|
|
|
1214
699
|
attempts: Math.max(0, source.attempts ?? 0),
|
|
1215
700
|
delayMs: Math.max(0, source.delayMs ?? 0),
|
|
1216
701
|
backoff: source.backoff ?? 'constant',
|
|
702
|
+
jitter: source.jitter ?? false,
|
|
1217
703
|
shouldRetry: source.shouldRetry ?? this.defaultShouldRetry,
|
|
1218
704
|
};
|
|
1219
705
|
}
|
|
@@ -1228,29 +714,41 @@ class ResourceStore {
|
|
|
1228
714
|
}
|
|
1229
715
|
return status === 0 || status >= 500;
|
|
1230
716
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
717
|
+
runWithRetry(exec, retry, context) {
|
|
718
|
+
const runAttempt = (attempt) => exec(attempt).catch((error) => {
|
|
719
|
+
const canRetry = attempt <= retry.attempts && retry.shouldRetry(error, attempt);
|
|
720
|
+
if (!canRetry) {
|
|
721
|
+
throw error;
|
|
1236
722
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
}
|
|
723
|
+
const baseDelay = retry.backoff === 'exponential' ? retry.delayMs * 2 ** (attempt - 1) : retry.delayMs;
|
|
724
|
+
const jitterOffset = retry.jitter ? (Math.random() * 0.5 - 0.25) * baseDelay : 0;
|
|
725
|
+
const delayMs = Math.max(0, Math.round(baseDelay + jitterOffset));
|
|
726
|
+
this.trace({ type: 'request-retry', method: context.method, key: context.key, attempt, error });
|
|
727
|
+
if (delayMs > 0) {
|
|
728
|
+
return new Promise((resolve, reject) => {
|
|
729
|
+
setTimeout(() => {
|
|
730
|
+
void runAttempt(attempt + 1).then(resolve, reject);
|
|
731
|
+
}, delayMs);
|
|
732
|
+
});
|
|
1248
733
|
}
|
|
1249
|
-
|
|
734
|
+
return runAttempt(attempt + 1);
|
|
735
|
+
});
|
|
736
|
+
return runAttempt(1);
|
|
1250
737
|
}
|
|
1251
|
-
exec$ = ({ req$, entry, promote = true, parseFn }) => () => {
|
|
738
|
+
exec$ = ({ req$, entry, promote = true, parseFn, observe }) => () => {
|
|
1252
739
|
promote && this.#activeRequests.update((n) => n + 1);
|
|
1253
|
-
return req$.pipe(map((
|
|
740
|
+
return req$.pipe(map((httpResponse) => {
|
|
741
|
+
try {
|
|
742
|
+
this.opts.onResponse?.(httpResponse);
|
|
743
|
+
}
|
|
744
|
+
catch {
|
|
745
|
+
/* noop */
|
|
746
|
+
}
|
|
747
|
+
const payload = observe === 'response'
|
|
748
|
+
? httpResponse
|
|
749
|
+
: httpResponse.body;
|
|
750
|
+
return parseFn ? parseFn(payload) : payload;
|
|
751
|
+
}), tap({
|
|
1254
752
|
next: (data) => {
|
|
1255
753
|
entry.data = data;
|
|
1256
754
|
entry.status = 'success';
|
|
@@ -1265,6 +763,7 @@ class ResourceStore {
|
|
|
1265
763
|
},
|
|
1266
764
|
}), finalize(() => {
|
|
1267
765
|
promote && this.#activeRequests.update((n) => Math.max(0, n - 1));
|
|
766
|
+
entry.controller = undefined;
|
|
1268
767
|
this.promoteCurrent(entry, promote);
|
|
1269
768
|
}));
|
|
1270
769
|
};
|
|
@@ -1272,6 +771,7 @@ class ResourceStore {
|
|
|
1272
771
|
if (!promote) {
|
|
1273
772
|
return;
|
|
1274
773
|
}
|
|
774
|
+
entry.promoted = true;
|
|
1275
775
|
this.#value.set(entry.data ?? null);
|
|
1276
776
|
this.#status.set(entry.status);
|
|
1277
777
|
this.#error.set(entry.error);
|
|
@@ -1339,22 +839,22 @@ class PagedQueryStore {
|
|
|
1339
839
|
#transport;
|
|
1340
840
|
#cache;
|
|
1341
841
|
/** Current page data (reactive). */
|
|
1342
|
-
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
842
|
+
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
1343
843
|
/** Merged cache of pages (flat list) handy for search/export. */
|
|
1344
|
-
cached = signal([], ...(ngDevMode ? [{ debugName: "cached" }] : []));
|
|
844
|
+
cached = signal([], ...(ngDevMode ? [{ debugName: "cached" }] : /* istanbul ignore next */ []));
|
|
1345
845
|
/** Loading flag of the current operation. */
|
|
1346
|
-
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
846
|
+
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
1347
847
|
/** Last request error (if any). */
|
|
1348
|
-
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
848
|
+
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
1349
849
|
/** Increments when the current dataset is reset/replaced. Useful for external consumers with local buffers. */
|
|
1350
|
-
version = signal(0, ...(ngDevMode ? [{ debugName: "version" }] : []));
|
|
1351
|
-
#page = signal(0, ...(ngDevMode ? [{ debugName: "#page" }] : []));
|
|
1352
|
-
#pageSize = signal(20, ...(ngDevMode ? [{ debugName: "#pageSize" }] : []));
|
|
1353
|
-
#totalElements = signal(0, ...(ngDevMode ? [{ debugName: "#totalElements" }] : []));
|
|
1354
|
-
#filters = signal({}, ...(ngDevMode ? [{ debugName: "#filters" }] : []));
|
|
1355
|
-
#query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : []));
|
|
1356
|
-
#sort = signal([], ...(ngDevMode ? [{ debugName: "#sort" }] : []));
|
|
1357
|
-
#routeParams = signal({}, ...(ngDevMode ? [{ debugName: "#routeParams" }] : []));
|
|
850
|
+
version = signal(0, ...(ngDevMode ? [{ debugName: "version" }] : /* istanbul ignore next */ []));
|
|
851
|
+
#page = signal(0, ...(ngDevMode ? [{ debugName: "#page" }] : /* istanbul ignore next */ []));
|
|
852
|
+
#pageSize = signal(20, ...(ngDevMode ? [{ debugName: "#pageSize" }] : /* istanbul ignore next */ []));
|
|
853
|
+
#totalElements = signal(0, ...(ngDevMode ? [{ debugName: "#totalElements" }] : /* istanbul ignore next */ []));
|
|
854
|
+
#filters = signal({}, ...(ngDevMode ? [{ debugName: "#filters" }] : /* istanbul ignore next */ []));
|
|
855
|
+
#query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : /* istanbul ignore next */ []));
|
|
856
|
+
#sort = signal([], ...(ngDevMode ? [{ debugName: "#sort" }] : /* istanbul ignore next */ []));
|
|
857
|
+
#routeParams = signal({}, ...(ngDevMode ? [{ debugName: "#routeParams" }] : /* istanbul ignore next */ []));
|
|
1358
858
|
pageState = this.#page.asReadonly();
|
|
1359
859
|
pageSizeState = this.#pageSize.asReadonly();
|
|
1360
860
|
totalElementsState = this.#totalElements.asReadonly();
|
|
@@ -1399,37 +899,18 @@ class PagedQueryStore {
|
|
|
1399
899
|
get sort() {
|
|
1400
900
|
return this.#sort();
|
|
1401
901
|
}
|
|
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
902
|
set filters(value) {
|
|
1407
903
|
this.#filters.set(value);
|
|
1408
904
|
}
|
|
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
905
|
set query(value) {
|
|
1414
906
|
this.#query.set(value);
|
|
1415
907
|
}
|
|
1416
|
-
/**
|
|
1417
|
-
* @deprecated Prefer `updatePage(...)`, `fetch(...)`, or `refetchWith(...)`.
|
|
1418
|
-
* Direct state mutation bypasses request semantics and should be treated as legacy.
|
|
1419
|
-
*/
|
|
1420
908
|
set page(value) {
|
|
1421
909
|
this.#page.set(value);
|
|
1422
910
|
}
|
|
1423
|
-
/**
|
|
1424
|
-
* @deprecated Prefer `updatePageSize(...)`.
|
|
1425
|
-
* Direct state mutation bypasses request semantics and should be treated as legacy.
|
|
1426
|
-
*/
|
|
1427
911
|
set pageSize(value) {
|
|
1428
912
|
this.#pageSize.set(value);
|
|
1429
913
|
}
|
|
1430
|
-
/**
|
|
1431
|
-
* @deprecated Managed by transport responses. Direct assignment should be treated as legacy.
|
|
1432
|
-
*/
|
|
1433
914
|
set totalElements(value) {
|
|
1434
915
|
this.#totalElements.set(value);
|
|
1435
916
|
}
|
|
@@ -1804,18 +1285,18 @@ class DictStore {
|
|
|
1804
1285
|
metaStorage;
|
|
1805
1286
|
ttlMs;
|
|
1806
1287
|
revalidate;
|
|
1807
|
-
cacheUpdatedAt = signal(null, ...(ngDevMode ? [{ debugName: "cacheUpdatedAt" }] : []));
|
|
1288
|
+
cacheUpdatedAt = signal(null, ...(ngDevMode ? [{ debugName: "cacheUpdatedAt" }] : /* istanbul ignore next */ []));
|
|
1808
1289
|
/**
|
|
1809
1290
|
* Search text.
|
|
1810
1291
|
* With `fixed: true` filters the local cache; with `fixed: false` triggers server search.
|
|
1811
1292
|
*/
|
|
1812
|
-
searchText = signal('', ...(ngDevMode ? [{ debugName: "searchText" }] : []));
|
|
1293
|
+
searchText = signal('', ...(ngDevMode ? [{ debugName: "searchText" }] : /* istanbul ignore next */ []));
|
|
1813
1294
|
debouncedSearchText;
|
|
1814
1295
|
/**
|
|
1815
1296
|
* Additional filters for server request (or presets).
|
|
1816
1297
|
*/
|
|
1817
|
-
filters = signal({}, ...(ngDevMode ? [{ debugName: "filters" }] : []));
|
|
1818
|
-
cachedItems = signal([], ...(ngDevMode ? [{ debugName: "cachedItems" }] : []));
|
|
1298
|
+
filters = signal({}, ...(ngDevMode ? [{ debugName: "filters" }] : /* istanbul ignore next */ []));
|
|
1299
|
+
cachedItems = signal([], ...(ngDevMode ? [{ debugName: "cachedItems" }] : /* istanbul ignore next */ []));
|
|
1819
1300
|
/**
|
|
1820
1301
|
* Current list of dictionary items.
|
|
1821
1302
|
* Source — local cache (fixed=true) or data from `PagedQueryStore`.
|
|
@@ -1826,7 +1307,7 @@ class DictStore {
|
|
|
1826
1307
|
return this.debouncedSearchText() || !cached.length ? this.#helper.items() : cached;
|
|
1827
1308
|
}
|
|
1828
1309
|
return cached.length ? this.filterLocal() : this.#helper.items();
|
|
1829
|
-
}, ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1310
|
+
}, ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
1830
1311
|
/**
|
|
1831
1312
|
* Ready-to-use dropdown options: `{ label, value }`.
|
|
1832
1313
|
* Respects `maxOptionsSize` for truncating the list.
|
|
@@ -1834,9 +1315,9 @@ class DictStore {
|
|
|
1834
1315
|
options = computed(() => {
|
|
1835
1316
|
const options = this.items().map((it) => ({ label: String(it[this.labelKey] ?? ''), value: it[this.valueKey] }));
|
|
1836
1317
|
return this.maxOptionsSize ? options.slice(0, this.maxOptionsSize) : options;
|
|
1837
|
-
}, ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
1318
|
+
}, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
1838
1319
|
_lastPromise = null;
|
|
1839
|
-
_armed = signal(false, ...(ngDevMode ? [{ debugName: "_armed" }] : []));
|
|
1320
|
+
_armed = signal(false, ...(ngDevMode ? [{ debugName: "_armed" }] : /* istanbul ignore next */ []));
|
|
1840
1321
|
// todo add i18n support
|
|
1841
1322
|
/**
|
|
1842
1323
|
* @param apiUrl dictionary endpoint (e.g., `'/api/dicts/countries'`)
|
|
@@ -2090,8 +1571,8 @@ class DictLocalStore {
|
|
|
2090
1571
|
* Represents the full, unfiltered list of dictionary entries.
|
|
2091
1572
|
* Used as the base data set for search and option generation.
|
|
2092
1573
|
*/
|
|
2093
|
-
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
2094
|
-
#draftItems = signal([], ...(ngDevMode ? [{ debugName: "#draftItems" }] : []));
|
|
1574
|
+
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
1575
|
+
#draftItems = signal([], ...(ngDevMode ? [{ debugName: "#draftItems" }] : /* istanbul ignore next */ []));
|
|
2095
1576
|
/**
|
|
2096
1577
|
* Computed list of options in `{ label, value }` format.
|
|
2097
1578
|
*
|
|
@@ -2106,7 +1587,7 @@ class DictLocalStore {
|
|
|
2106
1587
|
value: it[this.valueKey],
|
|
2107
1588
|
}));
|
|
2108
1589
|
return this.maxOptionsSize ? options.slice(0, this.maxOptionsSize) : options;
|
|
2109
|
-
}, ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
1590
|
+
}, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
2110
1591
|
labelKey;
|
|
2111
1592
|
valueKey;
|
|
2112
1593
|
maxOptionsSize;
|
|
@@ -2191,14 +1672,14 @@ class DictLocalStore {
|
|
|
2191
1672
|
class EntityStore {
|
|
2192
1673
|
idKey;
|
|
2193
1674
|
sortIds;
|
|
2194
|
-
byId = signal({}, ...(ngDevMode ? [{ debugName: "byId" }] : []));
|
|
2195
|
-
ids = signal([], ...(ngDevMode ? [{ debugName: "ids" }] : []));
|
|
1675
|
+
byId = signal({}, ...(ngDevMode ? [{ debugName: "byId" }] : /* istanbul ignore next */ []));
|
|
1676
|
+
ids = signal([], ...(ngDevMode ? [{ debugName: "ids" }] : /* istanbul ignore next */ []));
|
|
2196
1677
|
items = computed(() => {
|
|
2197
1678
|
const byId = this.byId();
|
|
2198
1679
|
return this.ids()
|
|
2199
1680
|
.map((id) => byId[String(id)])
|
|
2200
1681
|
.filter((item) => item !== undefined);
|
|
2201
|
-
}, ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1682
|
+
}, ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
2202
1683
|
constructor(config) {
|
|
2203
1684
|
this.idKey = config.idKey;
|
|
2204
1685
|
this.sortIds = config.sortIds;
|
|
@@ -2321,5 +1802,5 @@ class EntityStore {
|
|
|
2321
1802
|
* Generated bundle index. Do not edit.
|
|
2322
1803
|
*/
|
|
2323
1804
|
|
|
2324
|
-
export { AbortError, CacheMissError, DictLocalStore, DictStore, EntityStore,
|
|
1805
|
+
export { AbortError, CacheMissError, DictLocalStore, DictStore, EntityStore, PagedQueryStore, RESOURCE_PROFILES, ResourceStore, STATUM_CONFIG, createBodySerializer, createQuerySerializer, createResourceProfile, createStrictSerializer, isAbort, provideStatum };
|
|
2325
1806
|
//# sourceMappingURL=reforgium-statum.mjs.map
|