@reforgium/statum 3.0.0-rc.1 → 3.0.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/README.md +342 -83
- package/fesm2022/reforgium-statum.mjs +416 -127
- package/package.json +3 -3
- package/types/reforgium-statum.d.ts +177 -60
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { formatDate, isNullable, isDatePeriod, parseToDate,
|
|
2
|
-
import { HttpClient } from '@angular/common/http';
|
|
3
|
-
import { InjectionToken, makeEnvironmentProviders, inject, signal, computed, DestroyRef, effect, untracked } from '@angular/core';
|
|
1
|
+
import { formatDate, isNullable, isDatePeriod, parseToDate, buildQueryParams, isNumber, isObject, parseToDatePeriod, parseQueryParamsByMode, fillUrlWithParams, mergeQueryParams, deepEqual, normalizeSortInput, sortInputToTokens, debounceSignal } from '@reforgium/internal';
|
|
2
|
+
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
3
|
+
import { InjectionToken, makeEnvironmentProviders, inject, signal, computed, EnvironmentInjector, DestroyRef, runInInjectionContext, effect, untracked } from '@angular/core';
|
|
4
4
|
import { Subject, filter, timer, merge, map } from 'rxjs';
|
|
5
5
|
import { debounce, tap, throttle, finalize } from 'rxjs/operators';
|
|
6
6
|
|
|
@@ -162,8 +162,8 @@ class Serializer {
|
|
|
162
162
|
.replace(periodTransform.dateFromKeyPostfix, '')
|
|
163
163
|
.replace(periodTransform.dateToKeyPostfix, '');
|
|
164
164
|
const field = this.config.mapFields?.[keyJoint];
|
|
165
|
-
|
|
166
|
-
if (
|
|
165
|
+
const fieldType = field && 'type' in field ? field.type : undefined;
|
|
166
|
+
if (fieldType === 'period' && (isFrom || isTo)) {
|
|
167
167
|
result[keyJoint] ??= [null, null];
|
|
168
168
|
if (isFrom) {
|
|
169
169
|
result[keyJoint][0] = parseToDate(value, this.config.mapPeriod.dateFormat);
|
|
@@ -194,7 +194,7 @@ class Serializer {
|
|
|
194
194
|
* @returns query string (suitable for URL or history API)
|
|
195
195
|
*/
|
|
196
196
|
toQuery = (val) => {
|
|
197
|
-
return
|
|
197
|
+
return buildQueryParams(this.serialize(val), this.config.mapArray.concatType, this.resolveArrayFieldModes());
|
|
198
198
|
};
|
|
199
199
|
/**
|
|
200
200
|
* Returns a new serializer instance with a merged configuration.
|
|
@@ -280,8 +280,7 @@ class Serializer {
|
|
|
280
280
|
return isObject(value) ? this.deserialize(value) : value;
|
|
281
281
|
}
|
|
282
282
|
else {
|
|
283
|
-
|
|
284
|
-
return JSON.parse(value);
|
|
283
|
+
return typeof value === 'string' ? JSON.parse(value) : value;
|
|
285
284
|
}
|
|
286
285
|
}
|
|
287
286
|
catch {
|
|
@@ -321,21 +320,7 @@ class Serializer {
|
|
|
321
320
|
return value;
|
|
322
321
|
}
|
|
323
322
|
parseQuery(val) {
|
|
324
|
-
|
|
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;
|
|
323
|
+
return parseQueryParamsByMode(val, undefined, this.resolveArrayFieldModes());
|
|
339
324
|
}
|
|
340
325
|
parseJsonObject(val) {
|
|
341
326
|
try {
|
|
@@ -379,6 +364,15 @@ class Serializer {
|
|
|
379
364
|
mapFields: { ...(base.mapFields || {}), ...(override.mapFields || {}) },
|
|
380
365
|
};
|
|
381
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
|
+
}
|
|
382
376
|
}
|
|
383
377
|
|
|
384
378
|
const createQuerySerializer = (config = {}) => {
|
|
@@ -408,19 +402,19 @@ const createStrictSerializer = (config = {}) => {
|
|
|
408
402
|
|
|
409
403
|
class LruCache {
|
|
410
404
|
map = new Map();
|
|
405
|
+
_limit = 100;
|
|
411
406
|
constructor(limit = 100) {
|
|
412
407
|
this.limit = limit;
|
|
413
408
|
}
|
|
414
|
-
_limit = 100;
|
|
415
409
|
get limit() {
|
|
416
410
|
return this._limit;
|
|
417
411
|
}
|
|
418
|
-
set limit(value) {
|
|
419
|
-
this._limit = Math.max(1, Math.floor(value || 0));
|
|
420
|
-
}
|
|
421
412
|
get length() {
|
|
422
413
|
return this.map.size;
|
|
423
414
|
}
|
|
415
|
+
set limit(value) {
|
|
416
|
+
this._limit = Math.max(1, Math.floor(value || 0));
|
|
417
|
+
}
|
|
424
418
|
get(key) {
|
|
425
419
|
if (!this.map.has(key)) {
|
|
426
420
|
return null;
|
|
@@ -647,7 +641,14 @@ function isAbort(e) {
|
|
|
647
641
|
(typeof e === 'object' && e != null && e?.name === 'AbortError' && e?.message === 'aborted'));
|
|
648
642
|
}
|
|
649
643
|
function joinUrl(base, path) {
|
|
650
|
-
|
|
644
|
+
if (!base) {
|
|
645
|
+
return path;
|
|
646
|
+
}
|
|
647
|
+
const normalizedBase = base.replace(/\/$/, '');
|
|
648
|
+
if (!path) {
|
|
649
|
+
return normalizedBase;
|
|
650
|
+
}
|
|
651
|
+
return `${normalizedBase}/${path.replace(/^\//, '')}`;
|
|
651
652
|
}
|
|
652
653
|
function buildKey(method, path, args) {
|
|
653
654
|
const params = stableStringify(args.params || {});
|
|
@@ -835,7 +836,7 @@ class KeyedScheduler {
|
|
|
835
836
|
*/
|
|
836
837
|
class ResourceStore {
|
|
837
838
|
http = inject(HttpClient);
|
|
838
|
-
serializer
|
|
839
|
+
serializer;
|
|
839
840
|
#value = signal(null, ...(ngDevMode ? [{ debugName: "#value" }] : []));
|
|
840
841
|
#status = signal('idle', ...(ngDevMode ? [{ debugName: "#status" }] : []));
|
|
841
842
|
#error = signal(null, ...(ngDevMode ? [{ debugName: "#error" }] : []));
|
|
@@ -863,6 +864,11 @@ class ResourceStore {
|
|
|
863
864
|
maxEntries;
|
|
864
865
|
entries = new Map();
|
|
865
866
|
scheduler = new KeyedScheduler();
|
|
867
|
+
mutationMethods = {
|
|
868
|
+
PATCH: this.http.patch.bind(this.http),
|
|
869
|
+
POST: this.http.post.bind(this.http),
|
|
870
|
+
PUT: this.http.put.bind(this.http),
|
|
871
|
+
};
|
|
866
872
|
/**
|
|
867
873
|
* @param routes Map of path templates for methods (`GET/POST/...` → `/users/:id`)
|
|
868
874
|
* @param opts Global options (baseUrl, ttlMs, delay, delayMode, presetQueries/payload, etc.)
|
|
@@ -871,6 +877,10 @@ class ResourceStore {
|
|
|
871
877
|
this.routes = routes;
|
|
872
878
|
this.opts = opts;
|
|
873
879
|
this.maxEntries = Math.max(1, opts.maxEntries ?? 1000);
|
|
880
|
+
this.serializer = new Serializer({
|
|
881
|
+
...(inject(STATUM_CONFIG, { optional: true })?.serializer ?? {}),
|
|
882
|
+
...(opts.serializer ?? {}),
|
|
883
|
+
});
|
|
874
884
|
}
|
|
875
885
|
/**
|
|
876
886
|
* Perform a GET request.
|
|
@@ -882,11 +892,20 @@ class ResourceStore {
|
|
|
882
892
|
* @param cfg Call settings (strategy, ttlMs, revalidate, parseResponse, delay, delayMode, dedupe, promote)
|
|
883
893
|
* @returns Deserialized `Data`
|
|
884
894
|
*/
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
895
|
+
get(args, cfg = {}) {
|
|
896
|
+
let url;
|
|
897
|
+
let key;
|
|
898
|
+
let query;
|
|
899
|
+
let responseType;
|
|
900
|
+
try {
|
|
901
|
+
url = this.buildUrl('GET', args);
|
|
902
|
+
key = buildKey('GET', url, args);
|
|
903
|
+
query = this.prepareQuery(args);
|
|
904
|
+
responseType = cfg.responseType ?? 'json';
|
|
905
|
+
}
|
|
906
|
+
catch (error) {
|
|
907
|
+
return Promise.reject(error);
|
|
908
|
+
}
|
|
890
909
|
const entry = this.ensureEntry(key);
|
|
891
910
|
const strategy = cfg.strategy ?? 'network-first';
|
|
892
911
|
const ttlMs = cfg.ttlMs ?? this.opts.ttlMs ?? 0;
|
|
@@ -898,15 +917,15 @@ class ResourceStore {
|
|
|
898
917
|
if (fresh && entry.data != null) {
|
|
899
918
|
this.trace({ type: 'cache-hit', method: 'GET', key, strategy });
|
|
900
919
|
this.promoteCurrent(entry, cfg.promote);
|
|
901
|
-
return entry.data;
|
|
920
|
+
return Promise.resolve(entry.data);
|
|
902
921
|
}
|
|
903
922
|
this.trace({ type: 'cache-miss', method: 'GET', key, strategy });
|
|
904
|
-
|
|
923
|
+
return Promise.reject(new CacheMissError(key));
|
|
905
924
|
}
|
|
906
925
|
if (strategy === 'cache-first' && fresh && !cfg.revalidate && entry.data != null) {
|
|
907
926
|
this.trace({ type: 'cache-hit', method: 'GET', key, strategy });
|
|
908
927
|
this.promoteCurrent(entry, cfg.promote);
|
|
909
|
-
return entry.data;
|
|
928
|
+
return Promise.resolve(entry.data);
|
|
910
929
|
}
|
|
911
930
|
const delay = cfg.delay ?? this.opts.delay ?? 0;
|
|
912
931
|
const mode = cfg.delayMode ?? this.opts.delayMode ?? 'debounce';
|
|
@@ -971,8 +990,8 @@ class ResourceStore {
|
|
|
971
990
|
* @param cfg Call settings (parseResponse, delay, delayMode, dedupe, promote)
|
|
972
991
|
* @returns API response (by default — as returned by the server)
|
|
973
992
|
*/
|
|
974
|
-
|
|
975
|
-
return
|
|
993
|
+
post(args, cfg = {}) {
|
|
994
|
+
return this.callApi('POST', args, cfg);
|
|
976
995
|
}
|
|
977
996
|
/**
|
|
978
997
|
* PUT request (full resource update).
|
|
@@ -981,8 +1000,8 @@ class ResourceStore {
|
|
|
981
1000
|
* @param cfg Call settings (parseResponse, delay, delayMode, dedupe, promote)
|
|
982
1001
|
* @returns API response (by default — as returned by the server)
|
|
983
1002
|
*/
|
|
984
|
-
|
|
985
|
-
return
|
|
1003
|
+
put(args, cfg = {}) {
|
|
1004
|
+
return this.callApi('PUT', args, cfg);
|
|
986
1005
|
}
|
|
987
1006
|
/**
|
|
988
1007
|
* PATCH request (partial update).
|
|
@@ -991,8 +1010,8 @@ class ResourceStore {
|
|
|
991
1010
|
* @param cfg Call settings (parseResponse, delay, delayMode, dedupe, promote)
|
|
992
1011
|
* @returns API response (by default — as returned by the server)
|
|
993
1012
|
*/
|
|
994
|
-
|
|
995
|
-
return
|
|
1013
|
+
patch(args, cfg = {}) {
|
|
1014
|
+
return this.callApi('PATCH', args, cfg);
|
|
996
1015
|
}
|
|
997
1016
|
/**
|
|
998
1017
|
* DELETE request.
|
|
@@ -1001,8 +1020,8 @@ class ResourceStore {
|
|
|
1001
1020
|
* @param cfg Call settings (parseResponse, delay, delayMode, dedupe, promote)
|
|
1002
1021
|
* @returns API response (by default — as returned by the server)
|
|
1003
1022
|
*/
|
|
1004
|
-
|
|
1005
|
-
return
|
|
1023
|
+
delete(args, cfg = {}) {
|
|
1024
|
+
return this.callApi('DELETE', args, cfg);
|
|
1006
1025
|
}
|
|
1007
1026
|
/**
|
|
1008
1027
|
* Cancel scheduled/running requests for a specific call.
|
|
@@ -1071,13 +1090,22 @@ class ResourceStore {
|
|
|
1071
1090
|
this.entries.delete(keyToDelete);
|
|
1072
1091
|
}
|
|
1073
1092
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1093
|
+
callApi(method, args, config = {}) {
|
|
1094
|
+
let url;
|
|
1095
|
+
let key;
|
|
1096
|
+
let query;
|
|
1097
|
+
let payload;
|
|
1098
|
+
let responseType;
|
|
1099
|
+
try {
|
|
1100
|
+
url = this.buildUrl(method, args);
|
|
1101
|
+
key = buildKey(method, url, args);
|
|
1102
|
+
query = this.prepareQuery(args);
|
|
1103
|
+
payload = this.preparePayload(args.payload);
|
|
1104
|
+
responseType = config.responseType ?? 'json';
|
|
1105
|
+
}
|
|
1106
|
+
catch (error) {
|
|
1107
|
+
return Promise.reject(error);
|
|
1108
|
+
}
|
|
1081
1109
|
const entry = this.ensureEntry(key);
|
|
1082
1110
|
if (config.dedupe && entry.inflight) {
|
|
1083
1111
|
return entry.inflight;
|
|
@@ -1090,14 +1118,14 @@ class ResourceStore {
|
|
|
1090
1118
|
let req$;
|
|
1091
1119
|
if (method === 'DELETE') {
|
|
1092
1120
|
req$ = this.http.delete(url, {
|
|
1093
|
-
body:
|
|
1121
|
+
body: payload,
|
|
1094
1122
|
params: query,
|
|
1095
1123
|
responseType: responseType,
|
|
1096
1124
|
});
|
|
1097
1125
|
}
|
|
1098
1126
|
else {
|
|
1099
1127
|
// @ts-ignore
|
|
1100
|
-
req$ = this.
|
|
1128
|
+
req$ = this.mutationMethods[method](url, payload, {
|
|
1101
1129
|
params: query,
|
|
1102
1130
|
responseType: responseType,
|
|
1103
1131
|
});
|
|
@@ -1132,15 +1160,45 @@ class ResourceStore {
|
|
|
1132
1160
|
}
|
|
1133
1161
|
buildUrl(method, args) {
|
|
1134
1162
|
const tpl = this.routes[method];
|
|
1135
|
-
if (
|
|
1163
|
+
if (tpl == null) {
|
|
1136
1164
|
throw new Error(`${method} route not configured`);
|
|
1137
1165
|
}
|
|
1138
1166
|
const path = fillUrlWithParams(tpl, args.params);
|
|
1139
1167
|
return joinUrl(this.opts.baseUrl, path);
|
|
1140
1168
|
}
|
|
1141
1169
|
prepareQuery(args) {
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1170
|
+
const presetQuery = this.opts.presetQueries;
|
|
1171
|
+
const requestQuery = args.query;
|
|
1172
|
+
if (!presetQuery && !requestQuery) {
|
|
1173
|
+
return new HttpParams();
|
|
1174
|
+
}
|
|
1175
|
+
const mergedQuery = mergeQueryParams(presetQuery, requestQuery);
|
|
1176
|
+
if (this.isEmptyObject(mergedQuery)) {
|
|
1177
|
+
return new HttpParams();
|
|
1178
|
+
}
|
|
1179
|
+
return new HttpParams({
|
|
1180
|
+
fromString: this.serializer.toQuery(mergedQuery),
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
preparePayload(payload) {
|
|
1184
|
+
const presetPayload = this.opts.presetPayload;
|
|
1185
|
+
if (!presetPayload && !payload) {
|
|
1186
|
+
return {};
|
|
1187
|
+
}
|
|
1188
|
+
let mergedPayload;
|
|
1189
|
+
if (!presetPayload) {
|
|
1190
|
+
mergedPayload = payload ?? {};
|
|
1191
|
+
}
|
|
1192
|
+
else if (!payload) {
|
|
1193
|
+
mergedPayload = presetPayload;
|
|
1194
|
+
}
|
|
1195
|
+
else {
|
|
1196
|
+
mergedPayload = { ...presetPayload, ...payload };
|
|
1197
|
+
}
|
|
1198
|
+
if (this.isEmptyObject(mergedPayload)) {
|
|
1199
|
+
return {};
|
|
1200
|
+
}
|
|
1201
|
+
return this.serializer.serialize(mergedPayload);
|
|
1144
1202
|
}
|
|
1145
1203
|
trace(event) {
|
|
1146
1204
|
try {
|
|
@@ -1218,6 +1276,14 @@ class ResourceStore {
|
|
|
1218
1276
|
this.#status.set(entry.status);
|
|
1219
1277
|
this.#error.set(entry.error);
|
|
1220
1278
|
}
|
|
1279
|
+
isEmptyObject(value) {
|
|
1280
|
+
for (const _key in value) {
|
|
1281
|
+
if (Object.prototype.hasOwnProperty.call(value, _key)) {
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return true;
|
|
1286
|
+
}
|
|
1221
1287
|
}
|
|
1222
1288
|
|
|
1223
1289
|
const RESOURCE_PROFILES = {
|
|
@@ -1265,26 +1331,37 @@ const createResourceProfile = (profile, overrides = {}) => ({
|
|
|
1265
1331
|
class PagedQueryStore {
|
|
1266
1332
|
route;
|
|
1267
1333
|
config;
|
|
1334
|
+
static DISABLED_CACHE_LIMIT = Number.MAX_SAFE_INTEGER;
|
|
1268
1335
|
defaultConfig = inject(STATUM_CONFIG, { optional: true })?.pagedQuery || {};
|
|
1336
|
+
injector = inject(EnvironmentInjector);
|
|
1337
|
+
#requestId = 0;
|
|
1338
|
+
#activeRequests = 0;
|
|
1269
1339
|
#transport;
|
|
1270
1340
|
#cache;
|
|
1271
1341
|
/** Current page data (reactive). */
|
|
1272
1342
|
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1273
|
-
/** Merged cache of pages (flat list)
|
|
1343
|
+
/** Merged cache of pages (flat list) handy for search/export. */
|
|
1274
1344
|
cached = signal([], ...(ngDevMode ? [{ debugName: "cached" }] : []));
|
|
1275
1345
|
/** Loading flag of the current operation. */
|
|
1276
1346
|
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
1277
|
-
/**
|
|
1278
|
-
|
|
1279
|
-
/**
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
routeParams = {};
|
|
1347
|
+
/** Last request error (if any). */
|
|
1348
|
+
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
1349
|
+
/** 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" }] : []));
|
|
1358
|
+
pageState = this.#page.asReadonly();
|
|
1359
|
+
pageSizeState = this.#pageSize.asReadonly();
|
|
1360
|
+
totalElementsState = this.#totalElements.asReadonly();
|
|
1361
|
+
filtersState = this.#filters.asReadonly();
|
|
1362
|
+
queryState = this.#query.asReadonly();
|
|
1363
|
+
sortState = this.#sort.asReadonly();
|
|
1364
|
+
routeParamsState = this.#routeParams.asReadonly();
|
|
1288
1365
|
/**
|
|
1289
1366
|
* @param route Resource URL pattern (e.g., `'/users'`)
|
|
1290
1367
|
* @param config Store behavior: method, cache, request/response parsers, debounce, presets, etc.
|
|
@@ -1293,45 +1370,105 @@ class PagedQueryStore {
|
|
|
1293
1370
|
this.route = route;
|
|
1294
1371
|
this.config = config;
|
|
1295
1372
|
this.applyConfig(config);
|
|
1296
|
-
this.#cache = new LruCache(this.
|
|
1373
|
+
this.#cache = new LruCache(this.resolveCacheLimit());
|
|
1297
1374
|
this.applyPresetMeta();
|
|
1298
1375
|
this.initTransport();
|
|
1299
1376
|
inject(DestroyRef).onDestroy(() => this.destroy());
|
|
1300
1377
|
}
|
|
1378
|
+
/** Current filters (applied to requests). */
|
|
1379
|
+
get filters() {
|
|
1380
|
+
return this.#filters();
|
|
1381
|
+
}
|
|
1382
|
+
/** Additional query params sent to transport requests. */
|
|
1383
|
+
get query() {
|
|
1384
|
+
return this.#query();
|
|
1385
|
+
}
|
|
1386
|
+
/** Current page index (0-based). */
|
|
1387
|
+
get page() {
|
|
1388
|
+
return this.#page();
|
|
1389
|
+
}
|
|
1390
|
+
/** Default page size. */
|
|
1391
|
+
get pageSize() {
|
|
1392
|
+
return this.#pageSize();
|
|
1393
|
+
}
|
|
1394
|
+
/** Total number of elements reported by the server. */
|
|
1395
|
+
get totalElements() {
|
|
1396
|
+
return this.#totalElements();
|
|
1397
|
+
}
|
|
1398
|
+
/** Current sort rules. */
|
|
1399
|
+
get sort() {
|
|
1400
|
+
return this.#sort();
|
|
1401
|
+
}
|
|
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
|
+
set filters(value) {
|
|
1407
|
+
this.#filters.set(value);
|
|
1408
|
+
}
|
|
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
|
+
set query(value) {
|
|
1414
|
+
this.#query.set(value);
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* @deprecated Prefer `updatePage(...)`, `fetch(...)`, or `refetchWith(...)`.
|
|
1418
|
+
* Direct state mutation bypasses request semantics and should be treated as legacy.
|
|
1419
|
+
*/
|
|
1420
|
+
set page(value) {
|
|
1421
|
+
this.#page.set(value);
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* @deprecated Prefer `updatePageSize(...)`.
|
|
1425
|
+
* Direct state mutation bypasses request semantics and should be treated as legacy.
|
|
1426
|
+
*/
|
|
1427
|
+
set pageSize(value) {
|
|
1428
|
+
this.#pageSize.set(value);
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* @deprecated Managed by transport responses. Direct assignment should be treated as legacy.
|
|
1432
|
+
*/
|
|
1433
|
+
set totalElements(value) {
|
|
1434
|
+
this.#totalElements.set(value);
|
|
1435
|
+
}
|
|
1301
1436
|
/**
|
|
1302
1437
|
* Fetch data with explicit filters and query params from the first page.
|
|
1303
1438
|
* Replaces legacy `setFilters`.
|
|
1304
1439
|
*/
|
|
1305
|
-
fetch = ({ filters = {}, query = {}, routeParams = {} } = {}) => {
|
|
1306
|
-
this
|
|
1307
|
-
this
|
|
1308
|
-
return this.#fetchItems({ page: 0, filters, query, routeParams });
|
|
1440
|
+
fetch = ({ filters = {}, query = {}, routeParams = {}, sort = this.sort } = {}) => {
|
|
1441
|
+
this.resetDataset();
|
|
1442
|
+
return this.#fetchItems({ page: 0, filters, query, routeParams, sort });
|
|
1309
1443
|
};
|
|
1310
1444
|
/**
|
|
1311
1445
|
* Force reload current data.
|
|
1312
1446
|
* Optional args merge into active filters/query.
|
|
1313
1447
|
* Route params are intentionally not changed from `refetchWith`.
|
|
1314
1448
|
*/
|
|
1315
|
-
refetchWith({ filters, query } = {}) {
|
|
1449
|
+
refetchWith({ filters, query, sort } = {}) {
|
|
1316
1450
|
const nextFilters = filters == null ? this.filters : { ...this.filters, ...filters };
|
|
1317
|
-
const nextQuery = query == null ? this.query :
|
|
1318
|
-
|
|
1451
|
+
const nextQuery = query == null ? this.query : mergeQueryParams(this.query, query);
|
|
1452
|
+
const nextSort = sort == null ? this.sort : this.normalizeSort(sort);
|
|
1453
|
+
return this.#fetchItems({ filters: nextFilters, query: nextQuery, sort: nextSort });
|
|
1319
1454
|
}
|
|
1320
1455
|
/**
|
|
1321
1456
|
* Switch page with a request.
|
|
1322
|
-
* If cache is enabled and the page is present in LRU
|
|
1323
|
-
* @param page page index (0-based)
|
|
1324
|
-
* @param
|
|
1457
|
+
* If cache is enabled and the page is present in LRU returns it from cache.
|
|
1458
|
+
* @param page page index (0-based) or object form: `{ page }`
|
|
1459
|
+
* @param options request options
|
|
1460
|
+
* @param options.ignoreCache ignore cache and fetch from network
|
|
1325
1461
|
*/
|
|
1326
1462
|
updatePage = (page = this.page, { ignoreCache = false } = {}) => {
|
|
1327
|
-
|
|
1328
|
-
|
|
1463
|
+
const normalizedPage = typeof page === 'number' ? page : page.page;
|
|
1464
|
+
if (this.config.hasCache && this.#cache.has(normalizedPage) && !ignoreCache) {
|
|
1465
|
+
const cached = this.#cache.get(normalizedPage);
|
|
1329
1466
|
this.items.set(cached);
|
|
1330
|
-
this.page =
|
|
1467
|
+
this.page = normalizedPage;
|
|
1331
1468
|
return Promise.resolve(cached);
|
|
1332
1469
|
}
|
|
1333
1470
|
else {
|
|
1334
|
-
return this.#fetchItems({ page });
|
|
1471
|
+
return this.#fetchItems({ page: normalizedPage });
|
|
1335
1472
|
}
|
|
1336
1473
|
};
|
|
1337
1474
|
/**
|
|
@@ -1339,17 +1476,27 @@ class PagedQueryStore {
|
|
|
1339
1476
|
* @param size new size (rows per page)
|
|
1340
1477
|
*/
|
|
1341
1478
|
updatePageSize = (size = this.pageSize) => {
|
|
1342
|
-
this
|
|
1343
|
-
this.cached.set([]);
|
|
1479
|
+
this.resetDataset();
|
|
1344
1480
|
return this.#fetchItems({ page: 0, size });
|
|
1345
1481
|
};
|
|
1346
1482
|
/**
|
|
1347
1483
|
* Helper for offset-based table events (PrimeNG, etc.) with `page/first/rows`.
|
|
1348
1484
|
*/
|
|
1349
|
-
updateByOffset = ({ page: pageNum, first = 0, rows = 0 } = {}, { query = this.query } = {}) => {
|
|
1485
|
+
updateByOffset = ({ page: pageNum, first = 0, rows = 0 } = {}, { query = this.query, sort = this.sort } = {}) => {
|
|
1350
1486
|
const page = (pageNum ?? (first && rows && Math.floor(first / rows))) || 0;
|
|
1351
1487
|
const size = rows || this.pageSize;
|
|
1352
|
-
return this.#fetchItems({ page, size, query });
|
|
1488
|
+
return this.#fetchItems({ page, size, query, sort });
|
|
1489
|
+
};
|
|
1490
|
+
setSort = (sort) => {
|
|
1491
|
+
this.#sort.set(this.normalizeSort(sort));
|
|
1492
|
+
};
|
|
1493
|
+
updateSort = (sort) => {
|
|
1494
|
+
return this.updateSorts(sort ? [sort] : []);
|
|
1495
|
+
};
|
|
1496
|
+
updateSorts = (sort) => {
|
|
1497
|
+
const normalizedSort = this.normalizeSort(sort);
|
|
1498
|
+
this.resetDataset();
|
|
1499
|
+
return this.#fetchItems({ page: 0, sort: normalizedSort });
|
|
1353
1500
|
};
|
|
1354
1501
|
/**
|
|
1355
1502
|
* Set route parameters (path variables) for the resource URL.
|
|
@@ -1370,20 +1517,17 @@ class PagedQueryStore {
|
|
|
1370
1517
|
* ```
|
|
1371
1518
|
*/
|
|
1372
1519
|
setRouteParams = (params = {}, opts = {}) => {
|
|
1373
|
-
const isChanged = !deepEqual(this
|
|
1520
|
+
const isChanged = !deepEqual(this.#routeParams(), params);
|
|
1374
1521
|
if (!isChanged) {
|
|
1375
1522
|
return;
|
|
1376
1523
|
}
|
|
1377
|
-
this.
|
|
1524
|
+
this.#routeParams.set(params);
|
|
1378
1525
|
if (opts.reset) {
|
|
1379
|
-
this.
|
|
1380
|
-
this.totalElements = 0;
|
|
1381
|
-
this.#cache.clear();
|
|
1382
|
-
this.cached.set([]);
|
|
1383
|
-
this.items.set([]);
|
|
1526
|
+
this.resetDataset();
|
|
1384
1527
|
}
|
|
1385
1528
|
if (opts.abort) {
|
|
1386
1529
|
try {
|
|
1530
|
+
this.invalidateRequests();
|
|
1387
1531
|
this.#transport?.abortAll?.('Route params changed');
|
|
1388
1532
|
}
|
|
1389
1533
|
finally {
|
|
@@ -1397,8 +1541,9 @@ class PagedQueryStore {
|
|
|
1397
1541
|
*/
|
|
1398
1542
|
updateConfig = (config) => {
|
|
1399
1543
|
this.config = { ...this.config, ...config };
|
|
1400
|
-
this.#cache.limit = this.
|
|
1544
|
+
this.#cache.limit = this.resolveCacheLimit();
|
|
1401
1545
|
this.applyPresetMeta();
|
|
1546
|
+
this.reinitTransport();
|
|
1402
1547
|
};
|
|
1403
1548
|
/**
|
|
1404
1549
|
* Copy configuration and presets from another store of the same type.
|
|
@@ -1406,10 +1551,12 @@ class PagedQueryStore {
|
|
|
1406
1551
|
copy(store) {
|
|
1407
1552
|
this.applyConfig(store.config);
|
|
1408
1553
|
this.applyPresetMeta();
|
|
1554
|
+
this.reinitTransport();
|
|
1409
1555
|
}
|
|
1410
1556
|
/** Clean up resources and cancel active requests. Automatically called onDestroy. */
|
|
1411
1557
|
destroy() {
|
|
1412
1558
|
try {
|
|
1559
|
+
this.invalidateRequests();
|
|
1413
1560
|
this.#transport?.abortAll?.('PagedQueryStore destroyed');
|
|
1414
1561
|
}
|
|
1415
1562
|
catch (e) {
|
|
@@ -1418,17 +1565,29 @@ class PagedQueryStore {
|
|
|
1418
1565
|
}
|
|
1419
1566
|
}
|
|
1420
1567
|
}
|
|
1421
|
-
#fetchItems = async ({ page = this.page, size = this.pageSize, filters = this.filters, query = this.query, routeParams = this
|
|
1422
|
-
const builtQuery = this.parseQuery({ page, size, filters, query });
|
|
1568
|
+
#fetchItems = async ({ page = this.page, size = this.pageSize, filters = this.filters, query = this.query, routeParams = this.#routeParams(), sort = this.sort, }) => {
|
|
1569
|
+
const builtQuery = this.parseQuery({ page, size, filters, query, sort });
|
|
1570
|
+
const requestId = ++this.#requestId;
|
|
1571
|
+
this.#activeRequests++;
|
|
1423
1572
|
this.loading.set(true);
|
|
1424
|
-
this.
|
|
1425
|
-
this.#
|
|
1573
|
+
this.error.set(null);
|
|
1574
|
+
this.#routeParams.set(routeParams);
|
|
1575
|
+
if (this.resolveConcurrency() === 'latest-wins') {
|
|
1576
|
+
this.#transport.abortAll();
|
|
1577
|
+
}
|
|
1426
1578
|
try {
|
|
1427
1579
|
const response = await this.runTransport(filters, builtQuery, routeParams);
|
|
1580
|
+
if (this.shouldIgnoreRequestResult(requestId)) {
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1428
1583
|
this.page = page;
|
|
1429
1584
|
this.filters = filters;
|
|
1430
1585
|
this.query = query;
|
|
1586
|
+
this.#sort.set(this.normalizeSort(sort));
|
|
1431
1587
|
const parsed = await this.parseResponseData(response);
|
|
1588
|
+
if (this.shouldIgnoreRequestResult(requestId)) {
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1432
1591
|
this.pageSize = parsed.pageable?.pageSize || size;
|
|
1433
1592
|
this.totalElements = parsed.totalElements;
|
|
1434
1593
|
this.items.set(parsed.content);
|
|
@@ -1439,42 +1598,62 @@ class PagedQueryStore {
|
|
|
1439
1598
|
if (isAbort(e)) {
|
|
1440
1599
|
return;
|
|
1441
1600
|
}
|
|
1601
|
+
if (!this.shouldIgnoreRequestResult(requestId)) {
|
|
1602
|
+
this.error.set(e);
|
|
1603
|
+
}
|
|
1442
1604
|
throw e;
|
|
1443
1605
|
}
|
|
1444
1606
|
finally {
|
|
1445
|
-
this.
|
|
1607
|
+
this.#activeRequests = Math.max(0, this.#activeRequests - 1);
|
|
1608
|
+
this.loading.set(this.#activeRequests > 0);
|
|
1446
1609
|
}
|
|
1447
1610
|
};
|
|
1448
1611
|
initTransport() {
|
|
1449
|
-
this.#transport = new ResourceStore({ [this.config.method || 'GET']: this.route }, {
|
|
1612
|
+
this.#transport = runInInjectionContext(this.injector, () => new ResourceStore({ [this.config.method || 'GET']: this.route }, {
|
|
1613
|
+
baseUrl: this.config.baseUrl,
|
|
1450
1614
|
delay: this.config.debounceTime,
|
|
1451
1615
|
delayMode: 'debounce',
|
|
1452
1616
|
presetQueries: { page: this.page, size: this.pageSize },
|
|
1453
1617
|
presetPayload: this.filters,
|
|
1454
|
-
|
|
1618
|
+
serializer: {
|
|
1619
|
+
mapFields: {
|
|
1620
|
+
[this.resolveSortQueryKey()]: {
|
|
1621
|
+
type: 'array',
|
|
1622
|
+
concatType: 'multi',
|
|
1623
|
+
},
|
|
1624
|
+
},
|
|
1625
|
+
},
|
|
1626
|
+
}));
|
|
1455
1627
|
}
|
|
1456
1628
|
async runTransport(payload, query, params) {
|
|
1457
1629
|
if (this.config.method === 'GET') {
|
|
1458
1630
|
return this.#transport.get({ query, params }, { promote: false, dedupe: true });
|
|
1459
1631
|
}
|
|
1460
|
-
const method = this.config.method?.toLowerCase() || 'post';
|
|
1632
|
+
const method = (this.config.method?.toLowerCase() || 'post');
|
|
1461
1633
|
// @ts-ignore
|
|
1462
1634
|
return this.#transport[method]({ query, payload, params }, { promote: false, dedupe: true });
|
|
1463
1635
|
}
|
|
1464
|
-
parseQuery({ page = 0, size, filters, query = {} }) {
|
|
1636
|
+
parseQuery({ page = 0, size, filters, query = {}, sort = this.sort }) {
|
|
1465
1637
|
const method = this.config.method || 'GET';
|
|
1466
|
-
const
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
...query,
|
|
1471
|
-
};
|
|
1472
|
-
const fallbackQuery = method === 'GET' ? requestPayload : { page, size, ...query };
|
|
1638
|
+
const normalizedSort = this.normalizeSort(sort);
|
|
1639
|
+
const sortQueryKey = this.resolveSortQueryKey();
|
|
1640
|
+
const requestPayload = mergeQueryParams(mergeQueryParams({ page, size }, filters), mergeQueryParams(query, normalizedSort.length ? { [sortQueryKey]: normalizedSort } : {}));
|
|
1641
|
+
const fallbackQuery = method === 'GET' ? requestPayload : mergeQueryParams({ page, size }, query);
|
|
1473
1642
|
const rawQueries = this.config.parseRequest?.(requestPayload) || fallbackQuery;
|
|
1474
1643
|
const queries = {};
|
|
1475
1644
|
Object.entries(rawQueries).forEach(([key, value]) => {
|
|
1476
|
-
if (Array.isArray(value)) {
|
|
1477
|
-
|
|
1645
|
+
if (key === sortQueryKey && Array.isArray(value)) {
|
|
1646
|
+
if (value.every((item) => typeof item === 'string')) {
|
|
1647
|
+
queries[key] = value;
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
const sortRules = this.normalizeSort(value);
|
|
1651
|
+
if (sortRules.length) {
|
|
1652
|
+
queries[key] = this.sortRulesToTokens(sortRules);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
else if (Array.isArray(value)) {
|
|
1656
|
+
queries[key] = value;
|
|
1478
1657
|
}
|
|
1479
1658
|
else if (!isNullable(value)) {
|
|
1480
1659
|
queries[key] = value;
|
|
@@ -1507,25 +1686,83 @@ class PagedQueryStore {
|
|
|
1507
1686
|
parseFlatArray(data) {
|
|
1508
1687
|
return { content: data, totalElements: data.length };
|
|
1509
1688
|
}
|
|
1689
|
+
resetDataset() {
|
|
1690
|
+
this.page = 0;
|
|
1691
|
+
this.totalElements = 0;
|
|
1692
|
+
this.#cache.clear();
|
|
1693
|
+
this.cached.set([]);
|
|
1694
|
+
this.items.set([]);
|
|
1695
|
+
this.error.set(null);
|
|
1696
|
+
this.bumpVersion();
|
|
1697
|
+
}
|
|
1698
|
+
bumpVersion() {
|
|
1699
|
+
this.version.update((current) => current + 1);
|
|
1700
|
+
}
|
|
1701
|
+
invalidateRequests() {
|
|
1702
|
+
this.#requestId++;
|
|
1703
|
+
}
|
|
1704
|
+
resolveConcurrency() {
|
|
1705
|
+
return this.config.concurrency || this.defaultConfig.defaultConcurrency || 'latest-wins';
|
|
1706
|
+
}
|
|
1707
|
+
shouldIgnoreRequestResult(requestId) {
|
|
1708
|
+
return this.resolveConcurrency() === 'latest-wins' && requestId !== this.#requestId;
|
|
1709
|
+
}
|
|
1510
1710
|
applyConfig(config) {
|
|
1511
1711
|
this.config = {
|
|
1512
1712
|
...config,
|
|
1713
|
+
baseUrl: config.baseUrl ?? this.defaultConfig.defaultBaseUrl,
|
|
1513
1714
|
method: config.method || this.defaultConfig.defaultMethod || 'POST',
|
|
1514
1715
|
presetQuery: config.presetQuery || this.defaultConfig.defaultQuery,
|
|
1515
1716
|
parseRequest: config.parseRequest || this.defaultConfig.defaultParseRequest,
|
|
1717
|
+
sorting: config.sorting || this.defaultConfig.defaultSorting,
|
|
1718
|
+
concurrency: config.concurrency || this.defaultConfig.defaultConcurrency || 'latest-wins',
|
|
1516
1719
|
debounceTime: config.debounceTime || 0,
|
|
1517
1720
|
hasCache: config.hasCache === undefined ? this.defaultConfig.defaultHasCache : config.hasCache,
|
|
1518
|
-
cacheSize: config.cacheSize
|
|
1721
|
+
cacheSize: config.cacheSize ?? this.defaultConfig.defaultCacheSize ?? 5,
|
|
1722
|
+
disableCacheLimit: config.disableCacheLimit === undefined
|
|
1723
|
+
? (this.defaultConfig.defaultDisableCacheLimit ?? false)
|
|
1724
|
+
: config.disableCacheLimit,
|
|
1519
1725
|
};
|
|
1520
1726
|
if (this.#cache) {
|
|
1521
|
-
this.#cache.limit = this.
|
|
1727
|
+
this.#cache.limit = this.resolveCacheLimit();
|
|
1522
1728
|
}
|
|
1523
1729
|
}
|
|
1730
|
+
resolveCacheLimit() {
|
|
1731
|
+
if (this.config.disableCacheLimit) {
|
|
1732
|
+
return PagedQueryStore.DISABLED_CACHE_LIMIT;
|
|
1733
|
+
}
|
|
1734
|
+
return this.config.cacheSize ?? this.defaultConfig.defaultCacheSize ?? 5;
|
|
1735
|
+
}
|
|
1524
1736
|
applyPresetMeta() {
|
|
1525
1737
|
this.filters = this.config.presetFilters || {};
|
|
1738
|
+
this.#sort.set(this.normalizeSort(this.config.presetSort));
|
|
1526
1739
|
this.pageSize = this.config.presetQuery?.pageSize || 20;
|
|
1527
1740
|
this.page = this.config.presetQuery?.page || 0;
|
|
1528
1741
|
}
|
|
1742
|
+
normalizeSort(sort) {
|
|
1743
|
+
return normalizeSortInput(sort);
|
|
1744
|
+
}
|
|
1745
|
+
resolveSortQueryKey() {
|
|
1746
|
+
return this.config.sorting?.queryKey || 'sort';
|
|
1747
|
+
}
|
|
1748
|
+
sortRulesToTokens(sort) {
|
|
1749
|
+
if (this.config.sorting?.serialize) {
|
|
1750
|
+
return sort.map((item) => this.config.sorting.serialize(item));
|
|
1751
|
+
}
|
|
1752
|
+
return sortInputToTokens(sort);
|
|
1753
|
+
}
|
|
1754
|
+
reinitTransport() {
|
|
1755
|
+
try {
|
|
1756
|
+
this.invalidateRequests();
|
|
1757
|
+
this.#transport?.abortAll?.('PagedQueryStore transport reconfigured');
|
|
1758
|
+
}
|
|
1759
|
+
catch (e) {
|
|
1760
|
+
if (!isAbort(e)) {
|
|
1761
|
+
throw e;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
this.initTransport();
|
|
1765
|
+
}
|
|
1529
1766
|
}
|
|
1530
1767
|
|
|
1531
1768
|
var _a;
|
|
@@ -1549,6 +1786,7 @@ class DictStore {
|
|
|
1549
1786
|
apiUrl;
|
|
1550
1787
|
storageKey;
|
|
1551
1788
|
static MAX_CACHE_SIZE = 400;
|
|
1789
|
+
static META_SUFFIX = ':meta';
|
|
1552
1790
|
/**
|
|
1553
1791
|
* Default dictionary configuration resolved from {@link STATUM_CONFIG}.
|
|
1554
1792
|
*
|
|
@@ -1563,6 +1801,10 @@ class DictStore {
|
|
|
1563
1801
|
valueKey;
|
|
1564
1802
|
maxOptionsSize;
|
|
1565
1803
|
storage;
|
|
1804
|
+
metaStorage;
|
|
1805
|
+
ttlMs;
|
|
1806
|
+
revalidate;
|
|
1807
|
+
cacheUpdatedAt = signal(null, ...(ngDevMode ? [{ debugName: "cacheUpdatedAt" }] : []));
|
|
1566
1808
|
/**
|
|
1567
1809
|
* Search text.
|
|
1568
1810
|
* With `fixed: true` filters the local cache; with `fixed: false` triggers server search.
|
|
@@ -1601,7 +1843,7 @@ class DictStore {
|
|
|
1601
1843
|
* @param storageKey key for saving cache in the selected strategy
|
|
1602
1844
|
* @param cfg behavior (fixed/search, parsers, label/value keys, cache strategy, etc.)
|
|
1603
1845
|
*/
|
|
1604
|
-
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', }) {
|
|
1846
|
+
constructor(apiUrl, storageKey, { autoLoad = true, method = this.defaultConfig.defaultRestMethod, presetFilters = this.defaultConfig.defaultPresetFilters, parseResponse, parseRequest, debounceTime = this.defaultConfig.defaultDebounceTime, ttlMs = this.defaultConfig.defaultTtlMs, revalidate = this.defaultConfig.defaultRevalidate ?? false, 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', }) {
|
|
1605
1847
|
this.apiUrl = apiUrl;
|
|
1606
1848
|
this.storageKey = storageKey;
|
|
1607
1849
|
const searchDebounce = debounceTime ?? 300;
|
|
@@ -1615,15 +1857,19 @@ class DictStore {
|
|
|
1615
1857
|
debounceTime: debounceTime,
|
|
1616
1858
|
});
|
|
1617
1859
|
this.fixed = fixed;
|
|
1860
|
+
this.ttlMs = ttlMs;
|
|
1861
|
+
this.revalidate = revalidate;
|
|
1618
1862
|
this.labelKey = labelKey;
|
|
1619
1863
|
this.valueKey = valueKey;
|
|
1620
1864
|
this.maxOptionsSize = maxOptionsSize;
|
|
1621
1865
|
if (cacheStrategy !== 'memory') {
|
|
1622
1866
|
this.storage = storageStrategy(cacheStrategy);
|
|
1623
1867
|
this.storage.prefix = keyPrefix;
|
|
1868
|
+
this.metaStorage = storageStrategy(cacheStrategy);
|
|
1869
|
+
this.metaStorage.prefix = keyPrefix;
|
|
1624
1870
|
}
|
|
1625
|
-
this.setAutoload(autoLoad);
|
|
1626
1871
|
this.restoreFromStorage();
|
|
1872
|
+
this.setAutoload(autoLoad);
|
|
1627
1873
|
this.#effectRefs.push(effect(() => {
|
|
1628
1874
|
const incoming = this.#helper.items();
|
|
1629
1875
|
if (incoming.length === 0) {
|
|
@@ -1642,9 +1888,12 @@ class DictStore {
|
|
|
1642
1888
|
this._lastPromise = this.#helper.fetch({ filters: { name: query, ...rest } });
|
|
1643
1889
|
});
|
|
1644
1890
|
}
|
|
1645
|
-
else if (
|
|
1891
|
+
else if (this.shouldFetchFixedCache()) {
|
|
1646
1892
|
untracked(() => {
|
|
1647
|
-
this._lastPromise = this.#helper.fetch({ filters: { name: '', ...rest } })
|
|
1893
|
+
this._lastPromise = this.#helper.fetch({ filters: { name: '', ...rest } }).then((items) => {
|
|
1894
|
+
items?.length && this.mergeIntoCache(items);
|
|
1895
|
+
return items;
|
|
1896
|
+
});
|
|
1648
1897
|
});
|
|
1649
1898
|
}
|
|
1650
1899
|
}));
|
|
@@ -1653,6 +1902,13 @@ class DictStore {
|
|
|
1653
1902
|
restoreCache() {
|
|
1654
1903
|
this.restoreFromStorage();
|
|
1655
1904
|
}
|
|
1905
|
+
clearCache() {
|
|
1906
|
+
this.#lru.clear();
|
|
1907
|
+
this.cachedItems.set([]);
|
|
1908
|
+
this.cacheUpdatedAt.set(null);
|
|
1909
|
+
this.storage?.remove(this.storageKey);
|
|
1910
|
+
this.metaStorage?.remove(this.metaKey());
|
|
1911
|
+
}
|
|
1656
1912
|
/**
|
|
1657
1913
|
* Set a search query and filters.
|
|
1658
1914
|
* With `fixed: false` initiates server search; with `fixed: true` — local filtering.
|
|
@@ -1701,8 +1957,7 @@ class DictStore {
|
|
|
1701
1957
|
}
|
|
1702
1958
|
if (changed || opts?.replace) {
|
|
1703
1959
|
const arr = this.#lru.toArray();
|
|
1704
|
-
this.
|
|
1705
|
-
this.storage?.set(this.storageKey, arr.slice(0, _a.MAX_CACHE_SIZE));
|
|
1960
|
+
this.persistCache(arr);
|
|
1706
1961
|
}
|
|
1707
1962
|
};
|
|
1708
1963
|
/** Release resources and stop internal effects. */
|
|
@@ -1727,8 +1982,7 @@ class DictStore {
|
|
|
1727
1982
|
}
|
|
1728
1983
|
if (changed > 0) {
|
|
1729
1984
|
const arr = this.#lru.toArray();
|
|
1730
|
-
this.
|
|
1731
|
-
this.storage?.set(this.storageKey, arr.slice(0, _a.MAX_CACHE_SIZE));
|
|
1985
|
+
this.persistCache(arr);
|
|
1732
1986
|
}
|
|
1733
1987
|
}
|
|
1734
1988
|
restoreFromStorage() {
|
|
@@ -1742,19 +1996,29 @@ class DictStore {
|
|
|
1742
1996
|
this.#lru.set(key, it);
|
|
1743
1997
|
}
|
|
1744
1998
|
this.cachedItems.set(this.#lru.toArray());
|
|
1999
|
+
this.cacheUpdatedAt.set(this.metaStorage?.get(this.metaKey())?.updatedAt ?? null);
|
|
1745
2000
|
}
|
|
1746
2001
|
catch {
|
|
1747
2002
|
try {
|
|
1748
2003
|
this.storage?.remove(this.storageKey);
|
|
2004
|
+
this.metaStorage?.remove(this.metaKey());
|
|
1749
2005
|
}
|
|
1750
2006
|
catch {
|
|
1751
2007
|
/* noop */
|
|
1752
2008
|
}
|
|
1753
2009
|
}
|
|
1754
2010
|
}
|
|
2011
|
+
persistCache(items) {
|
|
2012
|
+
const capped = items.slice(0, _a.MAX_CACHE_SIZE);
|
|
2013
|
+
const updatedAt = Date.now();
|
|
2014
|
+
this.cachedItems.set(items);
|
|
2015
|
+
this.cacheUpdatedAt.set(updatedAt);
|
|
2016
|
+
this.storage?.set(this.storageKey, capped);
|
|
2017
|
+
this.metaStorage?.set(this.metaKey(), { updatedAt });
|
|
2018
|
+
}
|
|
1755
2019
|
filterLocal() {
|
|
1756
2020
|
const list = this.cachedItems();
|
|
1757
|
-
const query =
|
|
2021
|
+
const query = this.searchText().toLowerCase();
|
|
1758
2022
|
if (!query) {
|
|
1759
2023
|
return list;
|
|
1760
2024
|
}
|
|
@@ -1777,13 +2041,38 @@ class DictStore {
|
|
|
1777
2041
|
}
|
|
1778
2042
|
setAutoload(autoLoad) {
|
|
1779
2043
|
if (autoLoad === 'whenEmpty') {
|
|
1780
|
-
const isEmpty = !this.
|
|
1781
|
-
this._armed.set(isEmpty);
|
|
2044
|
+
const isEmpty = !this.cachedItems().length;
|
|
2045
|
+
this._armed.set(isEmpty || this.shouldRevalidateCache());
|
|
1782
2046
|
}
|
|
1783
2047
|
else {
|
|
1784
2048
|
this._armed.set(autoLoad);
|
|
1785
2049
|
}
|
|
1786
2050
|
}
|
|
2051
|
+
shouldFetchFixedCache() {
|
|
2052
|
+
return !this.cachedItems().length || this.shouldRevalidateCache();
|
|
2053
|
+
}
|
|
2054
|
+
shouldRevalidateCache() {
|
|
2055
|
+
return this.revalidate && this.isCacheStale();
|
|
2056
|
+
}
|
|
2057
|
+
isCacheStale() {
|
|
2058
|
+
if (!this.cachedItems().length) {
|
|
2059
|
+
return false;
|
|
2060
|
+
}
|
|
2061
|
+
if (this.ttlMs == null) {
|
|
2062
|
+
return false;
|
|
2063
|
+
}
|
|
2064
|
+
if (this.ttlMs <= 0) {
|
|
2065
|
+
return true;
|
|
2066
|
+
}
|
|
2067
|
+
const updatedAt = this.cacheUpdatedAt();
|
|
2068
|
+
if (updatedAt == null) {
|
|
2069
|
+
return true;
|
|
2070
|
+
}
|
|
2071
|
+
return Date.now() - updatedAt >= this.ttlMs;
|
|
2072
|
+
}
|
|
2073
|
+
metaKey() {
|
|
2074
|
+
return `${this.storageKey}${_a.META_SUFFIX}`;
|
|
2075
|
+
}
|
|
1787
2076
|
}
|
|
1788
2077
|
_a = DictStore;
|
|
1789
2078
|
|