@reforgium/statum 3.0.0-rc.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +343 -83
- package/fesm2022/reforgium-statum.mjs +423 -133
- package/package.json +3 -3
- package/types/reforgium-statum.d.ts +181 -58
|
@@ -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
|
-
* Route params are intentionally not changed from `
|
|
1447
|
+
* Route params are intentionally not changed from `refetchWith`.
|
|
1314
1448
|
*/
|
|
1315
|
-
|
|
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,50 +1476,58 @@ 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.
|
|
1356
|
-
* Does not trigger loading automatically - call `
|
|
1503
|
+
* Does not trigger loading automatically - call `refetchWith()` or `updatePage()` after.
|
|
1357
1504
|
*
|
|
1358
1505
|
* @param params Dictionary of route parameters (e.g., `{ id: '123' }`)
|
|
1506
|
+
* @param opts
|
|
1359
1507
|
* @param opts.reset If `true`, resets page to 0, clears cache, total elements count, and items
|
|
1360
1508
|
* @param opts.abort If `true`, aborts all active transport requests and sets loading to false
|
|
1361
1509
|
*
|
|
1362
1510
|
* @example
|
|
1363
1511
|
* ```ts
|
|
1364
1512
|
* store.setRouteParams({ userId: '42' });
|
|
1365
|
-
* await store.
|
|
1513
|
+
* await store.refetchWith(); // fetch with new params
|
|
1366
1514
|
*
|
|
1367
1515
|
* // Or with options:
|
|
1368
1516
|
* store.setRouteParams({ userId: '42' }, { reset: false, abort: true });
|
|
1369
1517
|
* ```
|
|
1370
1518
|
*/
|
|
1371
|
-
setRouteParams = (params = {},
|
|
1372
|
-
const isChanged = !deepEqual(this
|
|
1519
|
+
setRouteParams = (params = {}, opts = {}) => {
|
|
1520
|
+
const isChanged = !deepEqual(this.#routeParams(), params);
|
|
1373
1521
|
if (!isChanged) {
|
|
1374
1522
|
return;
|
|
1375
1523
|
}
|
|
1376
|
-
this.
|
|
1377
|
-
if (reset) {
|
|
1378
|
-
this.
|
|
1379
|
-
this.totalElements = 0;
|
|
1380
|
-
this.#cache.clear();
|
|
1381
|
-
this.cached.set([]);
|
|
1382
|
-
this.items.set([]);
|
|
1524
|
+
this.#routeParams.set(params);
|
|
1525
|
+
if (opts.reset) {
|
|
1526
|
+
this.resetDataset();
|
|
1383
1527
|
}
|
|
1384
|
-
if (abort) {
|
|
1528
|
+
if (opts.abort) {
|
|
1385
1529
|
try {
|
|
1530
|
+
this.invalidateRequests();
|
|
1386
1531
|
this.#transport?.abortAll?.('Route params changed');
|
|
1387
1532
|
}
|
|
1388
1533
|
finally {
|
|
@@ -1396,8 +1541,9 @@ class PagedQueryStore {
|
|
|
1396
1541
|
*/
|
|
1397
1542
|
updateConfig = (config) => {
|
|
1398
1543
|
this.config = { ...this.config, ...config };
|
|
1399
|
-
this.#cache.limit = this.
|
|
1544
|
+
this.#cache.limit = this.resolveCacheLimit();
|
|
1400
1545
|
this.applyPresetMeta();
|
|
1546
|
+
this.reinitTransport();
|
|
1401
1547
|
};
|
|
1402
1548
|
/**
|
|
1403
1549
|
* Copy configuration and presets from another store of the same type.
|
|
@@ -1405,10 +1551,12 @@ class PagedQueryStore {
|
|
|
1405
1551
|
copy(store) {
|
|
1406
1552
|
this.applyConfig(store.config);
|
|
1407
1553
|
this.applyPresetMeta();
|
|
1554
|
+
this.reinitTransport();
|
|
1408
1555
|
}
|
|
1409
1556
|
/** Clean up resources and cancel active requests. Automatically called onDestroy. */
|
|
1410
1557
|
destroy() {
|
|
1411
1558
|
try {
|
|
1559
|
+
this.invalidateRequests();
|
|
1412
1560
|
this.#transport?.abortAll?.('PagedQueryStore destroyed');
|
|
1413
1561
|
}
|
|
1414
1562
|
catch (e) {
|
|
@@ -1417,17 +1565,29 @@ class PagedQueryStore {
|
|
|
1417
1565
|
}
|
|
1418
1566
|
}
|
|
1419
1567
|
}
|
|
1420
|
-
#fetchItems = async ({ page = this.page, size = this.pageSize, filters = this.filters, query = this.query, routeParams = this
|
|
1421
|
-
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++;
|
|
1422
1572
|
this.loading.set(true);
|
|
1423
|
-
this.
|
|
1424
|
-
this.#
|
|
1573
|
+
this.error.set(null);
|
|
1574
|
+
this.#routeParams.set(routeParams);
|
|
1575
|
+
if (this.resolveConcurrency() === 'latest-wins') {
|
|
1576
|
+
this.#transport.abortAll();
|
|
1577
|
+
}
|
|
1425
1578
|
try {
|
|
1426
1579
|
const response = await this.runTransport(filters, builtQuery, routeParams);
|
|
1580
|
+
if (this.shouldIgnoreRequestResult(requestId)) {
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1427
1583
|
this.page = page;
|
|
1428
1584
|
this.filters = filters;
|
|
1429
1585
|
this.query = query;
|
|
1586
|
+
this.#sort.set(this.normalizeSort(sort));
|
|
1430
1587
|
const parsed = await this.parseResponseData(response);
|
|
1588
|
+
if (this.shouldIgnoreRequestResult(requestId)) {
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1431
1591
|
this.pageSize = parsed.pageable?.pageSize || size;
|
|
1432
1592
|
this.totalElements = parsed.totalElements;
|
|
1433
1593
|
this.items.set(parsed.content);
|
|
@@ -1438,42 +1598,62 @@ class PagedQueryStore {
|
|
|
1438
1598
|
if (isAbort(e)) {
|
|
1439
1599
|
return;
|
|
1440
1600
|
}
|
|
1601
|
+
if (!this.shouldIgnoreRequestResult(requestId)) {
|
|
1602
|
+
this.error.set(e);
|
|
1603
|
+
}
|
|
1441
1604
|
throw e;
|
|
1442
1605
|
}
|
|
1443
1606
|
finally {
|
|
1444
|
-
this.
|
|
1607
|
+
this.#activeRequests = Math.max(0, this.#activeRequests - 1);
|
|
1608
|
+
this.loading.set(this.#activeRequests > 0);
|
|
1445
1609
|
}
|
|
1446
1610
|
};
|
|
1447
1611
|
initTransport() {
|
|
1448
|
-
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,
|
|
1449
1614
|
delay: this.config.debounceTime,
|
|
1450
1615
|
delayMode: 'debounce',
|
|
1451
1616
|
presetQueries: { page: this.page, size: this.pageSize },
|
|
1452
1617
|
presetPayload: this.filters,
|
|
1453
|
-
|
|
1618
|
+
serializer: {
|
|
1619
|
+
mapFields: {
|
|
1620
|
+
[this.resolveSortQueryKey()]: {
|
|
1621
|
+
type: 'array',
|
|
1622
|
+
concatType: 'multi',
|
|
1623
|
+
},
|
|
1624
|
+
},
|
|
1625
|
+
},
|
|
1626
|
+
}));
|
|
1454
1627
|
}
|
|
1455
1628
|
async runTransport(payload, query, params) {
|
|
1456
1629
|
if (this.config.method === 'GET') {
|
|
1457
1630
|
return this.#transport.get({ query, params }, { promote: false, dedupe: true });
|
|
1458
1631
|
}
|
|
1459
|
-
const method = this.config.method?.toLowerCase() || 'post';
|
|
1632
|
+
const method = (this.config.method?.toLowerCase() || 'post');
|
|
1460
1633
|
// @ts-ignore
|
|
1461
1634
|
return this.#transport[method]({ query, payload, params }, { promote: false, dedupe: true });
|
|
1462
1635
|
}
|
|
1463
|
-
parseQuery({ page = 0, size, filters, query = {} }) {
|
|
1636
|
+
parseQuery({ page = 0, size, filters, query = {}, sort = this.sort }) {
|
|
1464
1637
|
const method = this.config.method || 'GET';
|
|
1465
|
-
const
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
...query,
|
|
1470
|
-
};
|
|
1471
|
-
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);
|
|
1472
1642
|
const rawQueries = this.config.parseRequest?.(requestPayload) || fallbackQuery;
|
|
1473
1643
|
const queries = {};
|
|
1474
1644
|
Object.entries(rawQueries).forEach(([key, value]) => {
|
|
1475
|
-
if (Array.isArray(value)) {
|
|
1476
|
-
|
|
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;
|
|
1477
1657
|
}
|
|
1478
1658
|
else if (!isNullable(value)) {
|
|
1479
1659
|
queries[key] = value;
|
|
@@ -1506,25 +1686,83 @@ class PagedQueryStore {
|
|
|
1506
1686
|
parseFlatArray(data) {
|
|
1507
1687
|
return { content: data, totalElements: data.length };
|
|
1508
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
|
+
}
|
|
1509
1710
|
applyConfig(config) {
|
|
1510
1711
|
this.config = {
|
|
1511
1712
|
...config,
|
|
1713
|
+
baseUrl: config.baseUrl ?? this.defaultConfig.defaultBaseUrl,
|
|
1512
1714
|
method: config.method || this.defaultConfig.defaultMethod || 'POST',
|
|
1513
1715
|
presetQuery: config.presetQuery || this.defaultConfig.defaultQuery,
|
|
1514
1716
|
parseRequest: config.parseRequest || this.defaultConfig.defaultParseRequest,
|
|
1717
|
+
sorting: config.sorting || this.defaultConfig.defaultSorting,
|
|
1718
|
+
concurrency: config.concurrency || this.defaultConfig.defaultConcurrency || 'latest-wins',
|
|
1515
1719
|
debounceTime: config.debounceTime || 0,
|
|
1516
1720
|
hasCache: config.hasCache === undefined ? this.defaultConfig.defaultHasCache : config.hasCache,
|
|
1517
|
-
cacheSize: config.cacheSize
|
|
1721
|
+
cacheSize: config.cacheSize ?? this.defaultConfig.defaultCacheSize ?? 5,
|
|
1722
|
+
disableCacheLimit: config.disableCacheLimit === undefined
|
|
1723
|
+
? (this.defaultConfig.defaultDisableCacheLimit ?? false)
|
|
1724
|
+
: config.disableCacheLimit,
|
|
1518
1725
|
};
|
|
1519
1726
|
if (this.#cache) {
|
|
1520
|
-
this.#cache.limit = this.
|
|
1727
|
+
this.#cache.limit = this.resolveCacheLimit();
|
|
1521
1728
|
}
|
|
1522
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
|
+
}
|
|
1523
1736
|
applyPresetMeta() {
|
|
1524
1737
|
this.filters = this.config.presetFilters || {};
|
|
1738
|
+
this.#sort.set(this.normalizeSort(this.config.presetSort));
|
|
1525
1739
|
this.pageSize = this.config.presetQuery?.pageSize || 20;
|
|
1526
1740
|
this.page = this.config.presetQuery?.page || 0;
|
|
1527
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
|
+
}
|
|
1528
1766
|
}
|
|
1529
1767
|
|
|
1530
1768
|
var _a;
|
|
@@ -1548,6 +1786,7 @@ class DictStore {
|
|
|
1548
1786
|
apiUrl;
|
|
1549
1787
|
storageKey;
|
|
1550
1788
|
static MAX_CACHE_SIZE = 400;
|
|
1789
|
+
static META_SUFFIX = ':meta';
|
|
1551
1790
|
/**
|
|
1552
1791
|
* Default dictionary configuration resolved from {@link STATUM_CONFIG}.
|
|
1553
1792
|
*
|
|
@@ -1562,6 +1801,10 @@ class DictStore {
|
|
|
1562
1801
|
valueKey;
|
|
1563
1802
|
maxOptionsSize;
|
|
1564
1803
|
storage;
|
|
1804
|
+
metaStorage;
|
|
1805
|
+
ttlMs;
|
|
1806
|
+
revalidate;
|
|
1807
|
+
cacheUpdatedAt = signal(null, ...(ngDevMode ? [{ debugName: "cacheUpdatedAt" }] : []));
|
|
1565
1808
|
/**
|
|
1566
1809
|
* Search text.
|
|
1567
1810
|
* With `fixed: true` filters the local cache; with `fixed: false` triggers server search.
|
|
@@ -1600,7 +1843,7 @@ class DictStore {
|
|
|
1600
1843
|
* @param storageKey key for saving cache in the selected strategy
|
|
1601
1844
|
* @param cfg behavior (fixed/search, parsers, label/value keys, cache strategy, etc.)
|
|
1602
1845
|
*/
|
|
1603
|
-
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', }) {
|
|
1604
1847
|
this.apiUrl = apiUrl;
|
|
1605
1848
|
this.storageKey = storageKey;
|
|
1606
1849
|
const searchDebounce = debounceTime ?? 300;
|
|
@@ -1614,15 +1857,19 @@ class DictStore {
|
|
|
1614
1857
|
debounceTime: debounceTime,
|
|
1615
1858
|
});
|
|
1616
1859
|
this.fixed = fixed;
|
|
1860
|
+
this.ttlMs = ttlMs;
|
|
1861
|
+
this.revalidate = revalidate;
|
|
1617
1862
|
this.labelKey = labelKey;
|
|
1618
1863
|
this.valueKey = valueKey;
|
|
1619
1864
|
this.maxOptionsSize = maxOptionsSize;
|
|
1620
1865
|
if (cacheStrategy !== 'memory') {
|
|
1621
1866
|
this.storage = storageStrategy(cacheStrategy);
|
|
1622
1867
|
this.storage.prefix = keyPrefix;
|
|
1868
|
+
this.metaStorage = storageStrategy(cacheStrategy);
|
|
1869
|
+
this.metaStorage.prefix = keyPrefix;
|
|
1623
1870
|
}
|
|
1624
|
-
this.setAutoload(autoLoad);
|
|
1625
1871
|
this.restoreFromStorage();
|
|
1872
|
+
this.setAutoload(autoLoad);
|
|
1626
1873
|
this.#effectRefs.push(effect(() => {
|
|
1627
1874
|
const incoming = this.#helper.items();
|
|
1628
1875
|
if (incoming.length === 0) {
|
|
@@ -1641,9 +1888,12 @@ class DictStore {
|
|
|
1641
1888
|
this._lastPromise = this.#helper.fetch({ filters: { name: query, ...rest } });
|
|
1642
1889
|
});
|
|
1643
1890
|
}
|
|
1644
|
-
else if (
|
|
1891
|
+
else if (this.shouldFetchFixedCache()) {
|
|
1645
1892
|
untracked(() => {
|
|
1646
|
-
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
|
+
});
|
|
1647
1897
|
});
|
|
1648
1898
|
}
|
|
1649
1899
|
}));
|
|
@@ -1652,6 +1902,13 @@ class DictStore {
|
|
|
1652
1902
|
restoreCache() {
|
|
1653
1903
|
this.restoreFromStorage();
|
|
1654
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
|
+
}
|
|
1655
1912
|
/**
|
|
1656
1913
|
* Set a search query and filters.
|
|
1657
1914
|
* With `fixed: false` initiates server search; with `fixed: true` — local filtering.
|
|
@@ -1700,8 +1957,7 @@ class DictStore {
|
|
|
1700
1957
|
}
|
|
1701
1958
|
if (changed || opts?.replace) {
|
|
1702
1959
|
const arr = this.#lru.toArray();
|
|
1703
|
-
this.
|
|
1704
|
-
this.storage?.set(this.storageKey, arr.slice(0, _a.MAX_CACHE_SIZE));
|
|
1960
|
+
this.persistCache(arr);
|
|
1705
1961
|
}
|
|
1706
1962
|
};
|
|
1707
1963
|
/** Release resources and stop internal effects. */
|
|
@@ -1726,8 +1982,7 @@ class DictStore {
|
|
|
1726
1982
|
}
|
|
1727
1983
|
if (changed > 0) {
|
|
1728
1984
|
const arr = this.#lru.toArray();
|
|
1729
|
-
this.
|
|
1730
|
-
this.storage?.set(this.storageKey, arr.slice(0, _a.MAX_CACHE_SIZE));
|
|
1985
|
+
this.persistCache(arr);
|
|
1731
1986
|
}
|
|
1732
1987
|
}
|
|
1733
1988
|
restoreFromStorage() {
|
|
@@ -1741,19 +1996,29 @@ class DictStore {
|
|
|
1741
1996
|
this.#lru.set(key, it);
|
|
1742
1997
|
}
|
|
1743
1998
|
this.cachedItems.set(this.#lru.toArray());
|
|
1999
|
+
this.cacheUpdatedAt.set(this.metaStorage?.get(this.metaKey())?.updatedAt ?? null);
|
|
1744
2000
|
}
|
|
1745
2001
|
catch {
|
|
1746
2002
|
try {
|
|
1747
2003
|
this.storage?.remove(this.storageKey);
|
|
2004
|
+
this.metaStorage?.remove(this.metaKey());
|
|
1748
2005
|
}
|
|
1749
2006
|
catch {
|
|
1750
2007
|
/* noop */
|
|
1751
2008
|
}
|
|
1752
2009
|
}
|
|
1753
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
|
+
}
|
|
1754
2019
|
filterLocal() {
|
|
1755
2020
|
const list = this.cachedItems();
|
|
1756
|
-
const query =
|
|
2021
|
+
const query = this.searchText().toLowerCase();
|
|
1757
2022
|
if (!query) {
|
|
1758
2023
|
return list;
|
|
1759
2024
|
}
|
|
@@ -1776,13 +2041,38 @@ class DictStore {
|
|
|
1776
2041
|
}
|
|
1777
2042
|
setAutoload(autoLoad) {
|
|
1778
2043
|
if (autoLoad === 'whenEmpty') {
|
|
1779
|
-
const isEmpty = !this.
|
|
1780
|
-
this._armed.set(isEmpty);
|
|
2044
|
+
const isEmpty = !this.cachedItems().length;
|
|
2045
|
+
this._armed.set(isEmpty || this.shouldRevalidateCache());
|
|
1781
2046
|
}
|
|
1782
2047
|
else {
|
|
1783
2048
|
this._armed.set(autoLoad);
|
|
1784
2049
|
}
|
|
1785
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
|
+
}
|
|
1786
2076
|
}
|
|
1787
2077
|
_a = DictStore;
|
|
1788
2078
|
|