@reforgium/statum 2.0.0 → 2.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/README.md +139 -74
- package/fesm2022/reforgium-statum.mjs +561 -95
- package/fesm2022/reforgium-statum.mjs.map +1 -1
- package/package.json +1 -1
- package/types/reforgium-statum.d.ts +90 -9
- package/types/reforgium-statum.d.ts.map +1 -1
|
@@ -9,6 +9,19 @@ const serializeNumber = (config, value) => config.mapNumber.format?.(value) ?? N
|
|
|
9
9
|
const serializeBoolean = (config, value) => config.mapBoolean.format?.(value) ?? (value ? (config.mapBoolean.true ?? true) : (config.mapBoolean.false ?? false));
|
|
10
10
|
const serializeDate = (config, value) => config.mapDate.format?.(value) ?? formatDate(value, config.mapDate.dateFormat);
|
|
11
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
|
+
}
|
|
12
25
|
/**
|
|
13
26
|
* Universal serializer/deserializer for values used in forms, filters, and DTOs.
|
|
14
27
|
*
|
|
@@ -47,8 +60,8 @@ class Serializer {
|
|
|
47
60
|
*
|
|
48
61
|
* @param config partial transformation configuration
|
|
49
62
|
*/
|
|
50
|
-
constructor(config) {
|
|
51
|
-
this.config = {
|
|
63
|
+
constructor(config = {}) {
|
|
64
|
+
this.config = this.mergeConfig({
|
|
52
65
|
mapString: { trim: true },
|
|
53
66
|
mapNumber: { fromString: false },
|
|
54
67
|
mapBoolean: {},
|
|
@@ -60,8 +73,7 @@ class Serializer {
|
|
|
60
73
|
transformMode: { mode: 'split', dateFromKeyPostfix: 'From', dateToKeyPostfix: 'To' },
|
|
61
74
|
},
|
|
62
75
|
mapNullable: { remove: true, includeEmptyString: false },
|
|
63
|
-
|
|
64
|
-
};
|
|
76
|
+
}, config);
|
|
65
77
|
}
|
|
66
78
|
/**
|
|
67
79
|
* Converts a domain object into a flat serialized representation
|
|
@@ -83,7 +95,12 @@ class Serializer {
|
|
|
83
95
|
for (const [key, value] of Object.entries(obj ?? {})) {
|
|
84
96
|
const fields = this.config.mapFields?.[key];
|
|
85
97
|
if (fields && 'format' in fields) {
|
|
86
|
-
|
|
98
|
+
try {
|
|
99
|
+
result[key] = fields.format(value, obj);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
throw new SerializerFieldError(key, 'format', error);
|
|
103
|
+
}
|
|
87
104
|
continue;
|
|
88
105
|
}
|
|
89
106
|
if (fields?.type === 'nullable' || isNullable(value, this.config.mapNullable?.includeEmptyString)) {
|
|
@@ -118,12 +135,17 @@ class Serializer {
|
|
|
118
135
|
* @returns a domain object of the specified type
|
|
119
136
|
*/
|
|
120
137
|
deserialize = (val) => {
|
|
121
|
-
const data = typeof val === 'string' ? this.
|
|
138
|
+
const data = typeof val === 'string' ? this.parseInputString(val) : val;
|
|
122
139
|
const result = {};
|
|
123
140
|
for (const [key, value] of Object.entries(data ?? {})) {
|
|
124
141
|
const field = this.config.mapFields?.[key];
|
|
125
142
|
if (field && 'parse' in field) {
|
|
126
|
-
|
|
143
|
+
try {
|
|
144
|
+
result[key] = field.parse(value, data);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
throw new SerializerFieldError(key, 'parse', error);
|
|
148
|
+
}
|
|
127
149
|
continue;
|
|
128
150
|
}
|
|
129
151
|
if (field?.type === 'nullable' ||
|
|
@@ -156,6 +178,14 @@ class Serializer {
|
|
|
156
178
|
}
|
|
157
179
|
return result;
|
|
158
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
|
+
};
|
|
159
189
|
/**
|
|
160
190
|
* Build a query string from a domain object using `serialize` rules
|
|
161
191
|
* and the array joining strategy (`concatType`).
|
|
@@ -174,7 +204,7 @@ class Serializer {
|
|
|
174
204
|
* @returns new `Serializer` with the provided `config` applied
|
|
175
205
|
*/
|
|
176
206
|
withConfig(config) {
|
|
177
|
-
return new Serializer(
|
|
207
|
+
return new Serializer(this.mergeConfig(this.config, config));
|
|
178
208
|
}
|
|
179
209
|
serializeElement(value, key) {
|
|
180
210
|
const fields = this.config.mapFields?.[key || ''];
|
|
@@ -182,7 +212,7 @@ class Serializer {
|
|
|
182
212
|
return;
|
|
183
213
|
}
|
|
184
214
|
if (fields?.type === 'nullable' || isNullable(value, this.config.mapNullable?.includeEmptyString)) {
|
|
185
|
-
const nullableVal = value
|
|
215
|
+
const nullableVal = value ?? null;
|
|
186
216
|
return this.config.mapNullable.format?.(nullableVal) || this.config.mapNullable.replaceWith || nullableVal;
|
|
187
217
|
}
|
|
188
218
|
if (fields?.type === 'string') {
|
|
@@ -247,8 +277,7 @@ class Serializer {
|
|
|
247
277
|
if (field?.type === 'object') {
|
|
248
278
|
try {
|
|
249
279
|
if (this.config.mapObject.deep) {
|
|
250
|
-
|
|
251
|
-
return this.deserializeElement(value);
|
|
280
|
+
return isObject(value) ? this.deserialize(value) : value;
|
|
252
281
|
}
|
|
253
282
|
else {
|
|
254
283
|
// @ts-ignore
|
|
@@ -283,40 +312,112 @@ class Serializer {
|
|
|
283
312
|
return this.config.mapNumber.parse?.(value) || Number(String(value).trim());
|
|
284
313
|
}
|
|
285
314
|
if (field?.type === 'string' || typeof value === 'string') {
|
|
286
|
-
|
|
315
|
+
const parsed = this.config.mapString.parse?.(value);
|
|
316
|
+
if (parsed !== undefined) {
|
|
317
|
+
return parsed;
|
|
318
|
+
}
|
|
319
|
+
return this.config.mapString.trim ? String(value).trim() : value;
|
|
287
320
|
}
|
|
288
321
|
return value;
|
|
289
322
|
}
|
|
290
323
|
parseQuery(val) {
|
|
324
|
+
const params = new URLSearchParams(val);
|
|
325
|
+
const data = {};
|
|
326
|
+
params.forEach((value, key) => {
|
|
327
|
+
const field = this.config.mapFields?.[key] || {};
|
|
328
|
+
const concatType = this.config.mapArray.concatType;
|
|
329
|
+
if ('type' in field && field.type === 'array') {
|
|
330
|
+
data[key] ??= [];
|
|
331
|
+
concatType !== 'multi' && (data[key] = parseQueryArray(value, concatType, key));
|
|
332
|
+
concatType === 'multi' && data[key].push(value);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
data[key] = value;
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
return data;
|
|
339
|
+
}
|
|
340
|
+
parseJsonObject(val) {
|
|
341
|
+
try {
|
|
342
|
+
const parsed = JSON.parse(val);
|
|
343
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
344
|
+
return parsed;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
throw new Error(`Invalid JSON input: ${error instanceof Error ? error.message : String(error)}`);
|
|
349
|
+
}
|
|
350
|
+
throw new Error('Invalid JSON input: expected a JSON object');
|
|
351
|
+
}
|
|
352
|
+
parseInputString(val) {
|
|
291
353
|
try {
|
|
292
|
-
return
|
|
354
|
+
return this.parseJsonObject(val);
|
|
293
355
|
}
|
|
294
356
|
catch {
|
|
295
|
-
|
|
296
|
-
const data = {};
|
|
297
|
-
params.forEach((value, key) => {
|
|
298
|
-
const field = this.config.mapFields?.[key] || {};
|
|
299
|
-
const concatType = this.config.mapArray.concatType;
|
|
300
|
-
if ('type' in field && field.type === 'array') {
|
|
301
|
-
data[key] ??= [];
|
|
302
|
-
concatType !== 'multi' && (data[key] = parseQueryArray(value, concatType, key));
|
|
303
|
-
concatType === 'multi' && data[key].push(value);
|
|
304
|
-
}
|
|
305
|
-
else {
|
|
306
|
-
data[key] = value;
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
return data;
|
|
357
|
+
return this.parseQuery(val);
|
|
310
358
|
}
|
|
311
359
|
}
|
|
360
|
+
mergeConfig(base, override) {
|
|
361
|
+
return {
|
|
362
|
+
...base,
|
|
363
|
+
...override,
|
|
364
|
+
mapString: { ...base.mapString, ...override.mapString },
|
|
365
|
+
mapNumber: { ...base.mapNumber, ...override.mapNumber },
|
|
366
|
+
mapBoolean: { ...base.mapBoolean, ...override.mapBoolean },
|
|
367
|
+
mapDate: { ...base.mapDate, ...override.mapDate },
|
|
368
|
+
mapPeriod: {
|
|
369
|
+
...base.mapPeriod,
|
|
370
|
+
...override.mapPeriod,
|
|
371
|
+
transformMode: {
|
|
372
|
+
...base.mapPeriod.transformMode,
|
|
373
|
+
...override.mapPeriod?.transformMode,
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
mapNullable: { ...base.mapNullable, ...override.mapNullable },
|
|
377
|
+
mapArray: { ...base.mapArray, ...override.mapArray },
|
|
378
|
+
mapObject: { ...base.mapObject, ...override.mapObject },
|
|
379
|
+
mapFields: { ...(base.mapFields || {}), ...(override.mapFields || {}) },
|
|
380
|
+
};
|
|
381
|
+
}
|
|
312
382
|
}
|
|
313
383
|
|
|
384
|
+
const createQuerySerializer = (config = {}) => {
|
|
385
|
+
return new Serializer({
|
|
386
|
+
mapArray: { concatType: 'comma' },
|
|
387
|
+
mapNullable: { remove: true, includeEmptyString: false },
|
|
388
|
+
mapObject: { deep: false },
|
|
389
|
+
...config,
|
|
390
|
+
});
|
|
391
|
+
};
|
|
392
|
+
const createBodySerializer = (config = {}) => {
|
|
393
|
+
return new Serializer({
|
|
394
|
+
mapArray: { concatType: 'json' },
|
|
395
|
+
mapNullable: { remove: false },
|
|
396
|
+
mapObject: { deep: true },
|
|
397
|
+
...config,
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
const createStrictSerializer = (config = {}) => {
|
|
401
|
+
return new Serializer({
|
|
402
|
+
mapString: { trim: true },
|
|
403
|
+
mapNumber: { fromString: true },
|
|
404
|
+
mapNullable: { remove: true, includeEmptyString: true },
|
|
405
|
+
...config,
|
|
406
|
+
});
|
|
407
|
+
};
|
|
408
|
+
|
|
314
409
|
class LruCache {
|
|
315
|
-
limit;
|
|
316
410
|
map = new Map();
|
|
317
411
|
constructor(limit = 100) {
|
|
318
412
|
this.limit = limit;
|
|
319
413
|
}
|
|
414
|
+
_limit = 100;
|
|
415
|
+
get limit() {
|
|
416
|
+
return this._limit;
|
|
417
|
+
}
|
|
418
|
+
set limit(value) {
|
|
419
|
+
this._limit = Math.max(1, Math.floor(value || 0));
|
|
420
|
+
}
|
|
320
421
|
get length() {
|
|
321
422
|
return this.map.size;
|
|
322
423
|
}
|
|
@@ -333,7 +434,7 @@ class LruCache {
|
|
|
333
434
|
if (this.map.has(key)) {
|
|
334
435
|
this.map.delete(key);
|
|
335
436
|
}
|
|
336
|
-
else if (this.map.size
|
|
437
|
+
else if (this.map.size >= this.limit) {
|
|
337
438
|
const oldest = this.map.keys().next().value;
|
|
338
439
|
oldest !== undefined && this.map.delete(oldest);
|
|
339
440
|
}
|
|
@@ -354,6 +455,9 @@ class LruCache {
|
|
|
354
455
|
values() {
|
|
355
456
|
return Array.from(this.map.values());
|
|
356
457
|
}
|
|
458
|
+
entries() {
|
|
459
|
+
return Array.from(this.map.entries());
|
|
460
|
+
}
|
|
357
461
|
toArray() {
|
|
358
462
|
return Array.from(this.map.values());
|
|
359
463
|
}
|
|
@@ -371,12 +475,22 @@ class LocalStorage {
|
|
|
371
475
|
this.prefix = prefix;
|
|
372
476
|
}
|
|
373
477
|
get length() {
|
|
374
|
-
return Object.keys(localStorage).filter((key) => key.startsWith(this.
|
|
478
|
+
return Object.keys(localStorage).filter((key) => key.startsWith(this.safePrefix())).length;
|
|
375
479
|
}
|
|
376
480
|
get(key) {
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
-
|
|
481
|
+
const storageKey = this.getSafePrefix(key);
|
|
482
|
+
const raw = localStorage.getItem(storageKey);
|
|
483
|
+
if (raw == null) {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const parsed = JSON.parse(raw);
|
|
488
|
+
return parsed ?? null;
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
localStorage.removeItem(storageKey);
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
380
494
|
}
|
|
381
495
|
set(key, value) {
|
|
382
496
|
const str = JSON.stringify(value);
|
|
@@ -386,12 +500,15 @@ class LocalStorage {
|
|
|
386
500
|
return localStorage.removeItem(this.getSafePrefix(key));
|
|
387
501
|
}
|
|
388
502
|
clear() {
|
|
389
|
-
const keys = Object.keys(localStorage).filter((key) => key.startsWith(this.
|
|
503
|
+
const keys = Object.keys(localStorage).filter((key) => key.startsWith(this.safePrefix()));
|
|
390
504
|
keys.forEach((key) => localStorage.removeItem(key));
|
|
391
505
|
}
|
|
392
506
|
getSafePrefix(key) {
|
|
393
507
|
return this.prefix ? `${this.prefix}:${key}` : String(key);
|
|
394
508
|
}
|
|
509
|
+
safePrefix() {
|
|
510
|
+
return this.prefix ? `${this.prefix}:` : '';
|
|
511
|
+
}
|
|
395
512
|
}
|
|
396
513
|
|
|
397
514
|
class MemoryStorage {
|
|
@@ -419,12 +536,22 @@ class SessionStorage {
|
|
|
419
536
|
this.prefix = prefix;
|
|
420
537
|
}
|
|
421
538
|
get length() {
|
|
422
|
-
return Object.keys(sessionStorage).filter((key) => key.startsWith(this.
|
|
539
|
+
return Object.keys(sessionStorage).filter((key) => key.startsWith(this.safePrefix())).length;
|
|
423
540
|
}
|
|
424
541
|
get(key) {
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
|
|
542
|
+
const storageKey = this.getSafePrefix(key);
|
|
543
|
+
const raw = sessionStorage.getItem(storageKey);
|
|
544
|
+
if (raw == null) {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
const parsed = JSON.parse(raw);
|
|
549
|
+
return parsed ?? null;
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
sessionStorage.removeItem(storageKey);
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
428
555
|
}
|
|
429
556
|
set(key, value) {
|
|
430
557
|
const str = JSON.stringify(value);
|
|
@@ -434,12 +561,15 @@ class SessionStorage {
|
|
|
434
561
|
return sessionStorage.removeItem(this.getSafePrefix(key));
|
|
435
562
|
}
|
|
436
563
|
clear() {
|
|
437
|
-
const keys = Object.keys(sessionStorage).filter((key) => key.startsWith(this.
|
|
564
|
+
const keys = Object.keys(sessionStorage).filter((key) => key.startsWith(this.safePrefix()));
|
|
438
565
|
keys.forEach((key) => sessionStorage.removeItem(key));
|
|
439
566
|
}
|
|
440
567
|
getSafePrefix(key) {
|
|
441
568
|
return this.prefix ? `${this.prefix}:${key}` : String(key);
|
|
442
569
|
}
|
|
570
|
+
safePrefix() {
|
|
571
|
+
return this.prefix ? `${this.prefix}:` : '';
|
|
572
|
+
}
|
|
443
573
|
}
|
|
444
574
|
|
|
445
575
|
/**
|
|
@@ -457,12 +587,12 @@ class SessionStorage {
|
|
|
457
587
|
* @param strategy storage strategy type (`memory`, `session`, `persist`, `lru`)
|
|
458
588
|
* @returns instance implementing `StorageInterface<Key, Type>`
|
|
459
589
|
*/
|
|
460
|
-
const storageStrategy = (strategy) => {
|
|
590
|
+
const storageStrategy = (strategy, options = {}) => {
|
|
461
591
|
const fabrics = {
|
|
462
592
|
memory: () => new MemoryStorage(),
|
|
463
593
|
session: () => new SessionStorage(),
|
|
464
594
|
persist: () => new LocalStorage(),
|
|
465
|
-
lru: () => new LruCache(),
|
|
595
|
+
lru: () => new LruCache(options.lruLimit ?? 100),
|
|
466
596
|
};
|
|
467
597
|
return fabrics[strategy]();
|
|
468
598
|
};
|
|
@@ -519,13 +649,32 @@ function joinUrl(base, path) {
|
|
|
519
649
|
return base ? `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}` : path;
|
|
520
650
|
}
|
|
521
651
|
function buildKey(method, path, args) {
|
|
522
|
-
const params =
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
652
|
+
const params = stableStringify(args.params || {});
|
|
653
|
+
const query = stableStringify(args.query || {});
|
|
654
|
+
const payload = stableStringify(args.payload || {});
|
|
655
|
+
return `${method}|${path}|${params}|${query}|${payload}`;
|
|
656
|
+
}
|
|
657
|
+
function stableStringify(value) {
|
|
658
|
+
if (value === null || value === undefined) {
|
|
659
|
+
return '';
|
|
660
|
+
}
|
|
661
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
662
|
+
return String(value);
|
|
663
|
+
}
|
|
664
|
+
if (value instanceof Date) {
|
|
665
|
+
return value.toISOString();
|
|
666
|
+
}
|
|
667
|
+
if (Array.isArray(value)) {
|
|
668
|
+
return `[${value.map((entry) => stableStringify(entry)).join(',')}]`;
|
|
669
|
+
}
|
|
670
|
+
if (typeof value === 'object') {
|
|
671
|
+
const entries = Object.entries(value)
|
|
672
|
+
.filter(([, v]) => v !== undefined)
|
|
673
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
674
|
+
.map(([k, v]) => `${k}:${stableStringify(v)}`);
|
|
675
|
+
return `{${entries.join(',')}}`;
|
|
676
|
+
}
|
|
677
|
+
return String(value);
|
|
529
678
|
}
|
|
530
679
|
|
|
531
680
|
/**
|
|
@@ -568,6 +717,10 @@ class KeyedScheduler {
|
|
|
568
717
|
const promise = new Promise((resolve, reject) => {
|
|
569
718
|
channel.waiters.push({ resolve, reject });
|
|
570
719
|
});
|
|
720
|
+
// The scheduler can reject synchronously from `cancel()`.
|
|
721
|
+
// Mark the promise as handled to avoid host-level unhandled rejection noise
|
|
722
|
+
// when consumer handlers are attached on chained promises.
|
|
723
|
+
void promise.catch(() => undefined);
|
|
571
724
|
channel.lastExec = exec;
|
|
572
725
|
if (delay <= 0) {
|
|
573
726
|
this.startExecution(channel);
|
|
@@ -589,13 +742,9 @@ class KeyedScheduler {
|
|
|
589
742
|
if (!ch) {
|
|
590
743
|
return;
|
|
591
744
|
}
|
|
592
|
-
ch.timerSub?.unsubscribe();
|
|
593
|
-
ch.inflight?.unsubscribe();
|
|
594
|
-
ch.inflight = undefined;
|
|
595
|
-
ch.subject.complete();
|
|
596
745
|
const waiters = ch.waiters.splice(0);
|
|
597
746
|
waiters.forEach((w) => w.reject(reason));
|
|
598
|
-
this.
|
|
747
|
+
this.cleanupChannel(ch);
|
|
599
748
|
}
|
|
600
749
|
/**
|
|
601
750
|
* Cancels all channels and their pending tasks.
|
|
@@ -613,7 +762,7 @@ class KeyedScheduler {
|
|
|
613
762
|
return ch;
|
|
614
763
|
}
|
|
615
764
|
const subject = new Subject();
|
|
616
|
-
ch = { subject, waiters: [] };
|
|
765
|
+
ch = { key, subject, waiters: [] };
|
|
617
766
|
this.channels.set(key, ch);
|
|
618
767
|
const debounced$ = subject.pipe(filter((t) => t.mode === 'debounce'), debounce((t) => timer(t.delay)), tap(() => this.startExecution(ch)));
|
|
619
768
|
const throttled$ = subject.pipe(filter((t) => t.mode === 'throttle'), throttle((t) => timer(t.delay), { leading: false, trailing: true }), tap(() => this.startExecution(ch)));
|
|
@@ -636,10 +785,31 @@ class KeyedScheduler {
|
|
|
636
785
|
error: (err) => {
|
|
637
786
|
const waiters = ch.waiters.splice(0);
|
|
638
787
|
waiters.forEach((w) => w.reject(err));
|
|
788
|
+
ch.inflight = undefined;
|
|
789
|
+
this.cleanupIfIdle(ch);
|
|
790
|
+
},
|
|
791
|
+
complete: () => {
|
|
792
|
+
ch.inflight = undefined;
|
|
793
|
+
this.cleanupIfIdle(ch);
|
|
639
794
|
},
|
|
640
|
-
complete: () => (ch.inflight = undefined),
|
|
641
795
|
});
|
|
642
796
|
}
|
|
797
|
+
cleanupIfIdle(ch) {
|
|
798
|
+
if (ch.waiters.length > 0 || ch.inflight) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
this.cleanupChannel(ch);
|
|
802
|
+
}
|
|
803
|
+
cleanupChannel(ch) {
|
|
804
|
+
if (this.channels.get(ch.key) !== ch) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
ch.timerSub?.unsubscribe();
|
|
808
|
+
ch.inflight?.unsubscribe();
|
|
809
|
+
ch.inflight = undefined;
|
|
810
|
+
ch.subject.complete();
|
|
811
|
+
this.channels.delete(ch.key);
|
|
812
|
+
}
|
|
643
813
|
}
|
|
644
814
|
|
|
645
815
|
/**
|
|
@@ -689,6 +859,7 @@ class ResourceStore {
|
|
|
689
859
|
loading = computed(() => this.#activeRequests() > 0 || this.#status() === 'stale', ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
690
860
|
routes;
|
|
691
861
|
opts;
|
|
862
|
+
maxEntries;
|
|
692
863
|
entries = new Map();
|
|
693
864
|
scheduler = new KeyedScheduler();
|
|
694
865
|
/**
|
|
@@ -698,6 +869,7 @@ class ResourceStore {
|
|
|
698
869
|
constructor(routes, opts = {}) {
|
|
699
870
|
this.routes = routes;
|
|
700
871
|
this.opts = opts;
|
|
872
|
+
this.maxEntries = Math.max(1, opts.maxEntries ?? 1000);
|
|
701
873
|
}
|
|
702
874
|
/**
|
|
703
875
|
* Perform a GET request.
|
|
@@ -713,6 +885,7 @@ class ResourceStore {
|
|
|
713
885
|
const url = this.buildUrl('GET', args);
|
|
714
886
|
const key = buildKey('GET', url, args);
|
|
715
887
|
const query = this.prepareQuery(args);
|
|
888
|
+
const responseType = cfg.responseType ?? 'json';
|
|
716
889
|
const entry = this.ensureEntry(key);
|
|
717
890
|
const strategy = cfg.strategy ?? 'network-first';
|
|
718
891
|
const ttlMs = cfg.ttlMs ?? this.opts.ttlMs ?? 0;
|
|
@@ -722,12 +895,15 @@ class ResourceStore {
|
|
|
722
895
|
}
|
|
723
896
|
if (strategy === 'cache-only') {
|
|
724
897
|
if (fresh && entry.data != null) {
|
|
898
|
+
this.trace({ type: 'cache-hit', method: 'GET', key, strategy });
|
|
725
899
|
this.promoteCurrent(entry, cfg.promote);
|
|
726
900
|
return entry.data;
|
|
727
901
|
}
|
|
902
|
+
this.trace({ type: 'cache-miss', method: 'GET', key, strategy });
|
|
728
903
|
throw new CacheMissError(key);
|
|
729
904
|
}
|
|
730
905
|
if (strategy === 'cache-first' && fresh && !cfg.revalidate && entry.data != null) {
|
|
906
|
+
this.trace({ type: 'cache-hit', method: 'GET', key, strategy });
|
|
731
907
|
this.promoteCurrent(entry, cfg.promote);
|
|
732
908
|
return entry.data;
|
|
733
909
|
}
|
|
@@ -736,19 +912,56 @@ class ResourceStore {
|
|
|
736
912
|
const isSWR = strategy === 'cache-first' && entry.data != null;
|
|
737
913
|
entry.status = isSWR ? 'stale' : 'loading';
|
|
738
914
|
this.promoteCurrent(entry, cfg.promote);
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
915
|
+
const retry = this.resolveRetryConfig(cfg.retry);
|
|
916
|
+
const taskWithRetry = this.runWithRetry((attempt) => {
|
|
917
|
+
this.trace({ type: 'request-start', method: 'GET', key, attempt });
|
|
918
|
+
return this.scheduler.schedule(key, mode, delay, this.exec$({
|
|
919
|
+
req$: this.http.get(url, { params: query, responseType: responseType }),
|
|
920
|
+
entry,
|
|
921
|
+
promote: cfg.promote,
|
|
922
|
+
parseFn: cfg.parseResponse,
|
|
923
|
+
}));
|
|
924
|
+
}, retry, {
|
|
925
|
+
method: 'GET',
|
|
926
|
+
key,
|
|
927
|
+
});
|
|
928
|
+
const resolvedTask = taskWithRetry
|
|
929
|
+
.catch((error) => {
|
|
930
|
+
if (isAbort(error)) {
|
|
931
|
+
throw error;
|
|
749
932
|
}
|
|
933
|
+
if (strategy === 'network-first' && entry.data != null) {
|
|
934
|
+
entry.status = 'success';
|
|
935
|
+
entry.error = null;
|
|
936
|
+
this.promoteCurrent(entry, cfg.promote);
|
|
937
|
+
this.trace({ type: 'cache-fallback', method: 'GET', key, strategy });
|
|
938
|
+
return entry.data;
|
|
939
|
+
}
|
|
940
|
+
throw error;
|
|
941
|
+
})
|
|
942
|
+
.then((data) => {
|
|
943
|
+
this.trace({ type: 'cache-write', method: 'GET', key, strategy });
|
|
944
|
+
this.trace({ type: 'request-success', method: 'GET', key });
|
|
945
|
+
return data;
|
|
946
|
+
})
|
|
947
|
+
.catch((error) => {
|
|
948
|
+
if (!isAbort(error)) {
|
|
949
|
+
this.trace({ type: 'request-error', method: 'GET', key, error });
|
|
950
|
+
}
|
|
951
|
+
throw error;
|
|
750
952
|
});
|
|
751
|
-
|
|
953
|
+
if (cfg.dedupe) {
|
|
954
|
+
entry.inflight = resolvedTask;
|
|
955
|
+
void resolvedTask.finally(() => {
|
|
956
|
+
entry.inflight === resolvedTask && (entry.inflight = undefined);
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
void resolvedTask.catch((error) => {
|
|
960
|
+
if (isAbort(error)) {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
return resolvedTask;
|
|
752
965
|
}
|
|
753
966
|
/**
|
|
754
967
|
* POST request.
|
|
@@ -800,6 +1013,7 @@ class ResourceStore {
|
|
|
800
1013
|
abort(method, args, reason) {
|
|
801
1014
|
const url = this.buildUrl(method, args);
|
|
802
1015
|
const key = buildKey(method, url, args);
|
|
1016
|
+
this.trace({ type: 'abort', method, key });
|
|
803
1017
|
this.scheduler.cancel?.(key, reason);
|
|
804
1018
|
}
|
|
805
1019
|
/**
|
|
@@ -808,43 +1022,96 @@ class ResourceStore {
|
|
|
808
1022
|
* @param reason Cancellation reason (optional)
|
|
809
1023
|
*/
|
|
810
1024
|
abortAll(reason) {
|
|
1025
|
+
this.trace({ type: 'abort', method: 'GET', key: '*' });
|
|
811
1026
|
this.scheduler.cancelAll?.(reason);
|
|
812
1027
|
}
|
|
813
1028
|
ensureEntry(key) {
|
|
814
1029
|
let entry = this.entries.get(key);
|
|
815
1030
|
if (!entry) {
|
|
1031
|
+
this.evictEntriesIfNeeded();
|
|
816
1032
|
entry = { data: null, status: 'idle', error: null, updatedAt: null };
|
|
817
1033
|
this.entries.set(key, entry);
|
|
818
1034
|
}
|
|
1035
|
+
else {
|
|
1036
|
+
// Keep hot keys at the end to approximate LRU order.
|
|
1037
|
+
this.entries.delete(key);
|
|
1038
|
+
this.entries.set(key, entry);
|
|
1039
|
+
}
|
|
819
1040
|
return entry;
|
|
820
1041
|
}
|
|
1042
|
+
evictEntriesIfNeeded() {
|
|
1043
|
+
while (this.entries.size >= this.maxEntries) {
|
|
1044
|
+
let keyToDelete = null;
|
|
1045
|
+
for (const [key, value] of this.entries.entries()) {
|
|
1046
|
+
if (!value.inflight) {
|
|
1047
|
+
keyToDelete = key;
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (keyToDelete === null) {
|
|
1052
|
+
keyToDelete = this.entries.keys().next().value ?? null;
|
|
1053
|
+
}
|
|
1054
|
+
if (keyToDelete === null) {
|
|
1055
|
+
break;
|
|
1056
|
+
}
|
|
1057
|
+
this.entries.delete(keyToDelete);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
821
1060
|
async callApi(method, args, config = {}) {
|
|
822
1061
|
const url = this.buildUrl(method, args);
|
|
823
1062
|
const key = buildKey(method, url, args);
|
|
824
1063
|
const query = this.prepareQuery(args);
|
|
825
1064
|
const payload = { ...(this.opts.presetPayload || {}), ...(args.payload || {}) };
|
|
826
1065
|
const serializedPayload = this.serializer.serialize(payload);
|
|
1066
|
+
const responseType = config.responseType ?? 'json';
|
|
827
1067
|
const entry = this.ensureEntry(key);
|
|
828
1068
|
if (config.dedupe && entry.inflight) {
|
|
829
1069
|
return entry.inflight;
|
|
830
1070
|
}
|
|
831
1071
|
const delay = config.delay ?? this.opts.delay ?? 0;
|
|
832
1072
|
const mode = config.delayMode ?? this.opts.delayMode ?? 'debounce';
|
|
1073
|
+
const retry = this.resolveRetryConfig(config.retry);
|
|
833
1074
|
entry.status = 'loading';
|
|
834
1075
|
this.promoteCurrent(entry, config.promote);
|
|
835
1076
|
let req$;
|
|
836
1077
|
if (method === 'DELETE') {
|
|
837
|
-
req$ = this.http.delete(url, {
|
|
1078
|
+
req$ = this.http.delete(url, {
|
|
1079
|
+
body: serializedPayload,
|
|
1080
|
+
params: query,
|
|
1081
|
+
responseType: responseType,
|
|
1082
|
+
});
|
|
838
1083
|
}
|
|
839
1084
|
else {
|
|
840
1085
|
// @ts-ignore
|
|
841
|
-
req$ = this.http[method.toLowerCase()](url, serializedPayload, {
|
|
1086
|
+
req$ = this.http[method.toLowerCase()](url, serializedPayload, {
|
|
1087
|
+
params: query,
|
|
1088
|
+
responseType: responseType,
|
|
1089
|
+
});
|
|
842
1090
|
}
|
|
843
|
-
const task = this.
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1091
|
+
const task = this.runWithRetry((attempt) => {
|
|
1092
|
+
this.trace({ type: 'request-start', method, key, attempt });
|
|
1093
|
+
return this.scheduler.schedule(key, mode, delay, this.exec$({ req$, entry, promote: config.promote, parseFn: config.parseResponse }));
|
|
1094
|
+
}, retry, { method, key })
|
|
1095
|
+
.then((data) => {
|
|
1096
|
+
this.trace({ type: 'cache-write', method, key });
|
|
1097
|
+
this.trace({ type: 'request-success', method, key });
|
|
1098
|
+
return data;
|
|
1099
|
+
})
|
|
1100
|
+
.catch((error) => {
|
|
1101
|
+
if (!isAbort(error)) {
|
|
1102
|
+
this.trace({ type: 'request-error', method, key, error });
|
|
1103
|
+
}
|
|
1104
|
+
throw error;
|
|
1105
|
+
});
|
|
1106
|
+
if (config.dedupe) {
|
|
1107
|
+
entry.inflight = task;
|
|
1108
|
+
void task.finally(() => {
|
|
1109
|
+
entry.inflight === task && (entry.inflight = undefined);
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
void task.catch((error) => {
|
|
1113
|
+
if (isAbort(error)) {
|
|
1114
|
+
return;
|
|
848
1115
|
}
|
|
849
1116
|
});
|
|
850
1117
|
return task;
|
|
@@ -861,11 +1128,59 @@ class ResourceStore {
|
|
|
861
1128
|
const mergedQuery = { ...(this.opts.presetQueries || {}), ...(args.query || {}) };
|
|
862
1129
|
return this.serializer.serialize(mergedQuery);
|
|
863
1130
|
}
|
|
1131
|
+
trace(event) {
|
|
1132
|
+
try {
|
|
1133
|
+
this.opts.onTrace?.(event);
|
|
1134
|
+
}
|
|
1135
|
+
catch {
|
|
1136
|
+
/* noop */
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
resolveRetryConfig(override) {
|
|
1140
|
+
const source = { ...(this.opts.retry || {}), ...(override || {}) };
|
|
1141
|
+
return {
|
|
1142
|
+
attempts: Math.max(0, source.attempts ?? 0),
|
|
1143
|
+
delayMs: Math.max(0, source.delayMs ?? 0),
|
|
1144
|
+
backoff: source.backoff ?? 'constant',
|
|
1145
|
+
shouldRetry: source.shouldRetry ?? this.defaultShouldRetry,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
defaultShouldRetry(error) {
|
|
1149
|
+
if (isAbort(error)) {
|
|
1150
|
+
return false;
|
|
1151
|
+
}
|
|
1152
|
+
const maybe = error;
|
|
1153
|
+
const status = maybe?.status;
|
|
1154
|
+
if (status == null) {
|
|
1155
|
+
return true;
|
|
1156
|
+
}
|
|
1157
|
+
return status === 0 || status >= 500;
|
|
1158
|
+
}
|
|
1159
|
+
async runWithRetry(exec, retry, context) {
|
|
1160
|
+
let attempt = 1;
|
|
1161
|
+
while (true) {
|
|
1162
|
+
try {
|
|
1163
|
+
return await exec(attempt);
|
|
1164
|
+
}
|
|
1165
|
+
catch (error) {
|
|
1166
|
+
const canRetry = attempt <= retry.attempts && retry.shouldRetry(error, attempt);
|
|
1167
|
+
if (!canRetry) {
|
|
1168
|
+
throw error;
|
|
1169
|
+
}
|
|
1170
|
+
const delayMs = retry.backoff === 'exponential' ? retry.delayMs * 2 ** (attempt - 1) : retry.delayMs;
|
|
1171
|
+
this.trace({ type: 'request-retry', method: context.method, key: context.key, attempt, error });
|
|
1172
|
+
attempt++;
|
|
1173
|
+
if (delayMs > 0) {
|
|
1174
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
864
1179
|
exec$ = ({ req$, entry, promote = true, parseFn }) => () => {
|
|
865
1180
|
promote && this.#activeRequests.update((n) => n + 1);
|
|
866
1181
|
return req$.pipe(map((data) => (parseFn ? parseFn(data) : data)), tap({
|
|
867
1182
|
next: (data) => {
|
|
868
|
-
|
|
1183
|
+
entry.data = data;
|
|
869
1184
|
entry.status = 'success';
|
|
870
1185
|
entry.updatedAt = Date.now();
|
|
871
1186
|
entry.error = null;
|
|
@@ -877,7 +1192,6 @@ class ResourceStore {
|
|
|
877
1192
|
this.promoteCurrent(entry, promote);
|
|
878
1193
|
},
|
|
879
1194
|
}), finalize(() => {
|
|
880
|
-
entry.inflight = undefined;
|
|
881
1195
|
promote && this.#activeRequests.update((n) => Math.max(0, n - 1));
|
|
882
1196
|
this.promoteCurrent(entry, promote);
|
|
883
1197
|
}));
|
|
@@ -939,8 +1253,8 @@ class PaginatedDataStore {
|
|
|
939
1253
|
constructor(route, config = {}) {
|
|
940
1254
|
this.route = route;
|
|
941
1255
|
this.config = config;
|
|
942
|
-
this.#cache = new LruCache(config.cacheSize);
|
|
943
1256
|
this.applyConfig(config);
|
|
1257
|
+
this.#cache = new LruCache(this.config.cacheSize);
|
|
944
1258
|
this.applyPresetMeta();
|
|
945
1259
|
this.initTransport();
|
|
946
1260
|
inject(DestroyRef).onDestroy(() => this.destroy());
|
|
@@ -1019,7 +1333,7 @@ class PaginatedDataStore {
|
|
|
1019
1333
|
*
|
|
1020
1334
|
* @param params Dictionary of route parameters (e.g., `{ id: '123' }`)
|
|
1021
1335
|
* @param opts Options object
|
|
1022
|
-
* @param opts.reset If `true
|
|
1336
|
+
* @param opts.reset If `true`, resets page to 0, clears cache, total elements count, and items
|
|
1023
1337
|
* @param opts.abort If `true`, aborts all active transport requests and sets loading to false
|
|
1024
1338
|
*
|
|
1025
1339
|
* @example
|
|
@@ -1059,6 +1373,7 @@ class PaginatedDataStore {
|
|
|
1059
1373
|
*/
|
|
1060
1374
|
updateConfig = (config) => {
|
|
1061
1375
|
this.config = { ...this.config, ...config };
|
|
1376
|
+
this.#cache.limit = this.config.cacheSize || this.defaultConfig.defaultCacheSize || 5;
|
|
1062
1377
|
this.applyPresetMeta();
|
|
1063
1378
|
};
|
|
1064
1379
|
/**
|
|
@@ -1149,10 +1464,16 @@ class PaginatedDataStore {
|
|
|
1149
1464
|
return data;
|
|
1150
1465
|
};
|
|
1151
1466
|
updateCache = (data) => {
|
|
1152
|
-
if (this.config.hasCache) {
|
|
1153
|
-
this
|
|
1467
|
+
if (!this.config.hasCache) {
|
|
1468
|
+
this.cached.set([...data]);
|
|
1469
|
+
return;
|
|
1154
1470
|
}
|
|
1155
|
-
this.
|
|
1471
|
+
this.#cache.set(this.page, data);
|
|
1472
|
+
const flat = [];
|
|
1473
|
+
for (const pageItems of this.#cache.values()) {
|
|
1474
|
+
flat.push(...pageItems);
|
|
1475
|
+
}
|
|
1476
|
+
this.cached.set(flat);
|
|
1156
1477
|
};
|
|
1157
1478
|
parseFlatArray(data) {
|
|
1158
1479
|
return { content: data, totalElements: data.length };
|
|
@@ -1167,6 +1488,9 @@ class PaginatedDataStore {
|
|
|
1167
1488
|
hasCache: config.hasCache === undefined ? this.defaultConfig.defaultHasCache : config.hasCache,
|
|
1168
1489
|
cacheSize: config.cacheSize || this.defaultConfig.defaultCacheSize || 5,
|
|
1169
1490
|
};
|
|
1491
|
+
if (this.#cache) {
|
|
1492
|
+
this.#cache.limit = this.config.cacheSize;
|
|
1493
|
+
}
|
|
1170
1494
|
}
|
|
1171
1495
|
applyPresetMeta() {
|
|
1172
1496
|
this.filters = this.config.presetFilters || {};
|
|
@@ -1216,7 +1540,7 @@ class DictStore {
|
|
|
1216
1540
|
* With `fixed: true` filters the local cache; with `fixed: false` triggers server search.
|
|
1217
1541
|
*/
|
|
1218
1542
|
searchText = signal('', ...(ngDevMode ? [{ debugName: "searchText" }] : []));
|
|
1219
|
-
debouncedSearchText
|
|
1543
|
+
debouncedSearchText;
|
|
1220
1544
|
/**
|
|
1221
1545
|
* Additional filters for server request (or presets).
|
|
1222
1546
|
*/
|
|
@@ -1252,6 +1576,8 @@ class DictStore {
|
|
|
1252
1576
|
constructor(apiUrl, storageKey, { autoLoad = true, method = this.defaultConfig.defaultRestMethod, presetFilters = this.defaultConfig.defaultPresetFilters, parseResponse, parseRequest, debounceTime = this.defaultConfig.defaultDebounceTime, fixed = true, maxOptionsSize = this.defaultConfig.defaultMaxOptionsSize, labelKey = this.defaultConfig.defaultLabelKey || 'name', valueKey = this.defaultConfig.defaultValueKey || 'code', keyPrefix = this.defaultConfig.defaultPrefix || 're', cacheStrategy = this.defaultConfig.defaultCacheStrategy || 'persist', }) {
|
|
1253
1577
|
this.apiUrl = apiUrl;
|
|
1254
1578
|
this.storageKey = storageKey;
|
|
1579
|
+
const searchDebounce = debounceTime ?? 300;
|
|
1580
|
+
this.debouncedSearchText = debounceSignal(this.searchText, searchDebounce);
|
|
1255
1581
|
this.#helper = new PaginatedDataStore(this.apiUrl, {
|
|
1256
1582
|
method: method,
|
|
1257
1583
|
hasCache: false,
|
|
@@ -1313,19 +1639,22 @@ class DictStore {
|
|
|
1313
1639
|
* @returns label string or `undefined` if not found
|
|
1314
1640
|
*/
|
|
1315
1641
|
findLabel = (value) => {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
return String(item[this.labelKey] ?? undefined);
|
|
1319
|
-
}
|
|
1642
|
+
if (value == null) {
|
|
1643
|
+
return undefined;
|
|
1320
1644
|
}
|
|
1321
|
-
|
|
1645
|
+
const key = typeof value === 'string' ? value : String(value);
|
|
1646
|
+
if (!key) {
|
|
1647
|
+
return undefined;
|
|
1648
|
+
}
|
|
1649
|
+
const item = this.#lru.get(key);
|
|
1650
|
+
return item ? String(item[this.labelKey] ?? undefined) : undefined;
|
|
1322
1651
|
};
|
|
1323
1652
|
/**
|
|
1324
1653
|
* Preload dictionary items into a local cache.
|
|
1325
1654
|
* Useful for SSR/static lists/quick presets.
|
|
1326
1655
|
*
|
|
1327
1656
|
* @param items list of items
|
|
1328
|
-
* @param opts `{ replace?: true }`
|
|
1657
|
+
* @param opts `{ replace?: true }` - completely replace current cache
|
|
1329
1658
|
*/
|
|
1330
1659
|
preload = (items, opts) => {
|
|
1331
1660
|
opts?.replace && this.#lru.clear();
|
|
@@ -1335,9 +1664,12 @@ class DictStore {
|
|
|
1335
1664
|
if (!key) {
|
|
1336
1665
|
continue;
|
|
1337
1666
|
}
|
|
1338
|
-
const
|
|
1667
|
+
const prev = this.#lru.get(key);
|
|
1668
|
+
const existed = prev !== null;
|
|
1339
1669
|
this.#lru.set(key, it);
|
|
1340
|
-
!existed
|
|
1670
|
+
if (!existed || prev !== it) {
|
|
1671
|
+
changed = true;
|
|
1672
|
+
}
|
|
1341
1673
|
}
|
|
1342
1674
|
if (changed || opts?.replace) {
|
|
1343
1675
|
const arr = this.#lru.toArray();
|
|
@@ -1358,9 +1690,12 @@ class DictStore {
|
|
|
1358
1690
|
let changed = 0;
|
|
1359
1691
|
for (const it of items) {
|
|
1360
1692
|
const key = this.keyOf(it);
|
|
1361
|
-
const
|
|
1693
|
+
const prev = this.#lru.get(key);
|
|
1694
|
+
const before = prev !== null;
|
|
1362
1695
|
this.#lru.set(key, it);
|
|
1363
|
-
!before
|
|
1696
|
+
if (!before || prev !== it) {
|
|
1697
|
+
changed++;
|
|
1698
|
+
}
|
|
1364
1699
|
}
|
|
1365
1700
|
if (changed > 0) {
|
|
1366
1701
|
const arr = this.#lru.toArray();
|
|
@@ -1399,7 +1734,9 @@ class DictStore {
|
|
|
1399
1734
|
const out = [];
|
|
1400
1735
|
for (let i = 0; i < list.length; i++) {
|
|
1401
1736
|
const val = list[i][key];
|
|
1402
|
-
val
|
|
1737
|
+
String(val ?? '')
|
|
1738
|
+
.toLowerCase()
|
|
1739
|
+
.includes(query) && out.push(list[i]);
|
|
1403
1740
|
}
|
|
1404
1741
|
return out;
|
|
1405
1742
|
}
|
|
@@ -1459,7 +1796,7 @@ class DictLocalStore {
|
|
|
1459
1796
|
constructor(items, config) {
|
|
1460
1797
|
this.labelKey = config?.labelKey ?? (this.defaultConfig.defaultLabelKey || 'name');
|
|
1461
1798
|
this.valueKey = config?.valueKey ?? (this.defaultConfig.defaultValueKey || 'code');
|
|
1462
|
-
this.maxOptionsSize = config?.maxOptionsSize ?? 1000;
|
|
1799
|
+
this.maxOptionsSize = config?.maxOptionsSize ?? this.defaultConfig.defaultMaxOptionsSize ?? 1000;
|
|
1463
1800
|
this.items.set(items);
|
|
1464
1801
|
this.#draftItems.set(items);
|
|
1465
1802
|
}
|
|
@@ -1534,9 +1871,138 @@ class DictLocalStore {
|
|
|
1534
1871
|
};
|
|
1535
1872
|
}
|
|
1536
1873
|
|
|
1874
|
+
class EntityStore {
|
|
1875
|
+
idKey;
|
|
1876
|
+
sortIds;
|
|
1877
|
+
byId = signal({}, ...(ngDevMode ? [{ debugName: "byId" }] : []));
|
|
1878
|
+
ids = signal([], ...(ngDevMode ? [{ debugName: "ids" }] : []));
|
|
1879
|
+
items = computed(() => {
|
|
1880
|
+
const byId = this.byId();
|
|
1881
|
+
return this.ids()
|
|
1882
|
+
.map((id) => byId[String(id)])
|
|
1883
|
+
.filter((item) => item !== undefined);
|
|
1884
|
+
}, ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1885
|
+
constructor(config) {
|
|
1886
|
+
this.idKey = config.idKey;
|
|
1887
|
+
this.sortIds = config.sortIds;
|
|
1888
|
+
}
|
|
1889
|
+
clear() {
|
|
1890
|
+
this.byId.set({});
|
|
1891
|
+
this.ids.set([]);
|
|
1892
|
+
}
|
|
1893
|
+
setAll(items) {
|
|
1894
|
+
const byId = {};
|
|
1895
|
+
const ids = [];
|
|
1896
|
+
for (const item of items) {
|
|
1897
|
+
const id = item[this.idKey];
|
|
1898
|
+
if (!this.isValidId(id)) {
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
1901
|
+
byId[String(id)] = item;
|
|
1902
|
+
ids.push(id);
|
|
1903
|
+
}
|
|
1904
|
+
this.byId.set(byId);
|
|
1905
|
+
this.ids.set(this.sortIds ? [...ids].sort(this.sortIds) : ids);
|
|
1906
|
+
}
|
|
1907
|
+
upsertOne(item) {
|
|
1908
|
+
const id = item[this.idKey];
|
|
1909
|
+
if (!this.isValidId(id)) {
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
const key = String(id);
|
|
1913
|
+
this.byId.update((prev) => ({ ...prev, [key]: item }));
|
|
1914
|
+
this.ids.update((prev) => {
|
|
1915
|
+
const has = prev.some((currentId) => currentId === id);
|
|
1916
|
+
const next = has ? [...prev] : [...prev, id];
|
|
1917
|
+
return this.sortIds ? next.sort(this.sortIds) : next;
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
upsertMany(items) {
|
|
1921
|
+
if (!items.length) {
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
this.byId.update((prev) => {
|
|
1925
|
+
const next = { ...prev };
|
|
1926
|
+
for (const item of items) {
|
|
1927
|
+
const id = item[this.idKey];
|
|
1928
|
+
if (!this.isValidId(id)) {
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
next[String(id)] = item;
|
|
1932
|
+
}
|
|
1933
|
+
return next;
|
|
1934
|
+
});
|
|
1935
|
+
this.ids.update((prev) => {
|
|
1936
|
+
const next = [...prev];
|
|
1937
|
+
const keySet = new Set(next.map((id) => String(id)));
|
|
1938
|
+
for (const item of items) {
|
|
1939
|
+
const id = item[this.idKey];
|
|
1940
|
+
if (!this.isValidId(id) || keySet.has(String(id))) {
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1943
|
+
keySet.add(String(id));
|
|
1944
|
+
next.push(id);
|
|
1945
|
+
}
|
|
1946
|
+
return this.sortIds ? next.sort(this.sortIds) : next;
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
removeOne(id) {
|
|
1950
|
+
if (!this.isValidId(id)) {
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
const key = String(id);
|
|
1954
|
+
this.byId.update((prev) => {
|
|
1955
|
+
const next = { ...prev };
|
|
1956
|
+
delete next[key];
|
|
1957
|
+
return next;
|
|
1958
|
+
});
|
|
1959
|
+
this.ids.update((prev) => prev.filter((it) => it !== id));
|
|
1960
|
+
}
|
|
1961
|
+
removeMany(ids) {
|
|
1962
|
+
if (!ids.length) {
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
const idSet = new Set(ids.map((id) => String(id)));
|
|
1966
|
+
this.byId.update((prev) => {
|
|
1967
|
+
const next = { ...prev };
|
|
1968
|
+
for (const id of idSet) {
|
|
1969
|
+
delete next[id];
|
|
1970
|
+
}
|
|
1971
|
+
return next;
|
|
1972
|
+
});
|
|
1973
|
+
this.ids.update((prev) => prev.filter((id) => !idSet.has(String(id))));
|
|
1974
|
+
}
|
|
1975
|
+
patchOne(id, patch) {
|
|
1976
|
+
if (!this.isValidId(id)) {
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
const key = String(id);
|
|
1980
|
+
const current = this.byId()[key];
|
|
1981
|
+
if (!current) {
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
this.byId.update((prev) => ({
|
|
1985
|
+
...prev,
|
|
1986
|
+
[key]: { ...current, ...patch },
|
|
1987
|
+
}));
|
|
1988
|
+
}
|
|
1989
|
+
getById(id) {
|
|
1990
|
+
if (!this.isValidId(id)) {
|
|
1991
|
+
return null;
|
|
1992
|
+
}
|
|
1993
|
+
return this.byId()[String(id)] ?? null;
|
|
1994
|
+
}
|
|
1995
|
+
has(id) {
|
|
1996
|
+
return this.getById(id) !== null;
|
|
1997
|
+
}
|
|
1998
|
+
isValidId(value) {
|
|
1999
|
+
return typeof value === 'string' || typeof value === 'number';
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1537
2003
|
/**
|
|
1538
2004
|
* Generated bundle index. Do not edit.
|
|
1539
2005
|
*/
|
|
1540
2006
|
|
|
1541
|
-
export { AbortError, CacheMissError, DictLocalStore, DictStore, LruCache, PaginatedDataStore, ResourceStore, STATUM_CONFIG, Serializer, isAbort, storageStrategy };
|
|
2007
|
+
export { AbortError, CacheMissError, DictLocalStore, DictStore, EntityStore, LruCache, PaginatedDataStore, ResourceStore, STATUM_CONFIG, Serializer, SerializerFieldError, createBodySerializer, createQuerySerializer, createStrictSerializer, isAbort, storageStrategy };
|
|
1542
2008
|
//# sourceMappingURL=reforgium-statum.mjs.map
|