@reforgium/internal 2.0.5 → 2.0.7
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/fesm2022/reforgium-internal.mjs +337 -133
- package/package.json +9 -10
- package/types/reforgium-internal.d.ts +14 -19
|
@@ -6,38 +6,83 @@ class LocalStorage {
|
|
|
6
6
|
this.prefix = prefix;
|
|
7
7
|
}
|
|
8
8
|
get length() {
|
|
9
|
-
|
|
9
|
+
const storage = this.storage;
|
|
10
|
+
if (!storage) {
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
return Object.keys(storage).filter((key) => key.startsWith(this.safePrefix())).length;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
10
19
|
}
|
|
11
20
|
get(key) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
if (raw == null) {
|
|
21
|
+
const storage = this.storage;
|
|
22
|
+
if (!storage) {
|
|
15
23
|
return null;
|
|
16
24
|
}
|
|
25
|
+
const storageKey = this.getSafePrefix(key);
|
|
17
26
|
try {
|
|
18
|
-
const
|
|
19
|
-
|
|
27
|
+
const raw = storage.getItem(storageKey);
|
|
28
|
+
if (raw == null) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return JSON.parse(raw);
|
|
20
32
|
}
|
|
21
33
|
catch {
|
|
22
|
-
|
|
34
|
+
try {
|
|
35
|
+
storage.removeItem(storageKey);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* noop */
|
|
39
|
+
}
|
|
23
40
|
return null;
|
|
24
41
|
}
|
|
25
42
|
}
|
|
26
43
|
set(key, value) {
|
|
27
|
-
|
|
44
|
+
try {
|
|
45
|
+
this.storage?.setItem(this.getSafePrefix(key), JSON.stringify(value));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* Storage is optional and may reject writes (SSR, quota, privacy mode). */
|
|
49
|
+
}
|
|
28
50
|
}
|
|
29
51
|
remove(key) {
|
|
30
|
-
|
|
52
|
+
try {
|
|
53
|
+
this.storage?.removeItem(this.getSafePrefix(key));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* noop */
|
|
57
|
+
}
|
|
31
58
|
}
|
|
32
59
|
clear() {
|
|
33
|
-
const
|
|
34
|
-
|
|
60
|
+
const storage = this.storage;
|
|
61
|
+
if (!storage) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
Object.keys(storage)
|
|
66
|
+
.filter((key) => key.startsWith(this.safePrefix()))
|
|
67
|
+
.forEach((key) => storage.removeItem(key));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* noop */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
get storage() {
|
|
74
|
+
try {
|
|
75
|
+
return typeof localStorage === 'undefined' ? null : localStorage;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
35
80
|
}
|
|
36
81
|
getSafePrefix(key) {
|
|
37
|
-
return this.prefix ?
|
|
82
|
+
return this.prefix ? this.prefix + ':' + String(key) : String(key);
|
|
38
83
|
}
|
|
39
84
|
safePrefix() {
|
|
40
|
-
return this.prefix ?
|
|
85
|
+
return this.prefix ? this.prefix + ':' : '';
|
|
41
86
|
}
|
|
42
87
|
}
|
|
43
88
|
|
|
@@ -55,6 +100,13 @@ class LruCache {
|
|
|
55
100
|
}
|
|
56
101
|
set limit(value) {
|
|
57
102
|
this._limit = Math.max(1, Math.floor(value || 0));
|
|
103
|
+
while (this.map.size > this._limit) {
|
|
104
|
+
const oldest = this.map.keys().next().value;
|
|
105
|
+
if (oldest === undefined) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
this.map.delete(oldest);
|
|
109
|
+
}
|
|
58
110
|
}
|
|
59
111
|
get(key) {
|
|
60
112
|
if (!this.map.has(key)) {
|
|
@@ -129,38 +181,83 @@ class SessionStorage {
|
|
|
129
181
|
this.prefix = prefix;
|
|
130
182
|
}
|
|
131
183
|
get length() {
|
|
132
|
-
|
|
184
|
+
const storage = this.storage;
|
|
185
|
+
if (!storage) {
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
return Object.keys(storage).filter((key) => key.startsWith(this.safePrefix())).length;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
133
194
|
}
|
|
134
195
|
get(key) {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
if (raw == null) {
|
|
196
|
+
const storage = this.storage;
|
|
197
|
+
if (!storage) {
|
|
138
198
|
return null;
|
|
139
199
|
}
|
|
200
|
+
const storageKey = this.getSafePrefix(key);
|
|
140
201
|
try {
|
|
141
|
-
const
|
|
142
|
-
|
|
202
|
+
const raw = storage.getItem(storageKey);
|
|
203
|
+
if (raw == null) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return JSON.parse(raw);
|
|
143
207
|
}
|
|
144
208
|
catch {
|
|
145
|
-
|
|
209
|
+
try {
|
|
210
|
+
storage.removeItem(storageKey);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
/* noop */
|
|
214
|
+
}
|
|
146
215
|
return null;
|
|
147
216
|
}
|
|
148
217
|
}
|
|
149
218
|
set(key, value) {
|
|
150
|
-
|
|
219
|
+
try {
|
|
220
|
+
this.storage?.setItem(this.getSafePrefix(key), JSON.stringify(value));
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
/* Storage is optional and may reject writes (SSR, quota, privacy mode). */
|
|
224
|
+
}
|
|
151
225
|
}
|
|
152
226
|
remove(key) {
|
|
153
|
-
|
|
227
|
+
try {
|
|
228
|
+
this.storage?.removeItem(this.getSafePrefix(key));
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
/* noop */
|
|
232
|
+
}
|
|
154
233
|
}
|
|
155
234
|
clear() {
|
|
156
|
-
const
|
|
157
|
-
|
|
235
|
+
const storage = this.storage;
|
|
236
|
+
if (!storage) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
Object.keys(storage)
|
|
241
|
+
.filter((key) => key.startsWith(this.safePrefix()))
|
|
242
|
+
.forEach((key) => storage.removeItem(key));
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
/* noop */
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
get storage() {
|
|
249
|
+
try {
|
|
250
|
+
return typeof sessionStorage === 'undefined' ? null : sessionStorage;
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
158
255
|
}
|
|
159
256
|
getSafePrefix(key) {
|
|
160
|
-
return this.prefix ?
|
|
257
|
+
return this.prefix ? this.prefix + ':' + String(key) : String(key);
|
|
161
258
|
}
|
|
162
259
|
safePrefix() {
|
|
163
|
-
return this.prefix ?
|
|
260
|
+
return this.prefix ? this.prefix + ':' : '';
|
|
164
261
|
}
|
|
165
262
|
}
|
|
166
263
|
|
|
@@ -412,16 +509,28 @@ const debounceSignal = (src, ms = 150, opts) => {
|
|
|
412
509
|
*/
|
|
413
510
|
const throttleSignal = (src, ms = 100, opts) => {
|
|
414
511
|
const out = signal(src(), ...(ngDevMode ? [{ debugName: "out" }] : /* istanbul ignore next */ []));
|
|
415
|
-
let
|
|
416
|
-
let
|
|
512
|
+
let lastEmissionAt = null;
|
|
513
|
+
let queuedValue;
|
|
514
|
+
let hasQueuedValue = false;
|
|
417
515
|
let timeoutRef;
|
|
516
|
+
const clearPendingTimer = () => {
|
|
517
|
+
if (timeoutRef) {
|
|
518
|
+
clearTimeout(timeoutRef);
|
|
519
|
+
timeoutRef = null;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
const emitNow = (value) => {
|
|
523
|
+
out.set(value);
|
|
524
|
+
lastEmissionAt = performance.now();
|
|
525
|
+
hasQueuedValue = false;
|
|
526
|
+
clearPendingTimer();
|
|
527
|
+
};
|
|
418
528
|
const flush = () => {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
last = performance.now();
|
|
529
|
+
timeoutRef = null;
|
|
530
|
+
if (!hasQueuedValue) {
|
|
531
|
+
return;
|
|
423
532
|
}
|
|
424
|
-
|
|
533
|
+
emitNow(queuedValue);
|
|
425
534
|
};
|
|
426
535
|
effect((onCleanup) => {
|
|
427
536
|
const v = src();
|
|
@@ -430,24 +539,19 @@ const throttleSignal = (src, ms = 100, opts) => {
|
|
|
430
539
|
}
|
|
431
540
|
untracked(() => {
|
|
432
541
|
const now = performance.now();
|
|
433
|
-
if (now -
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
clearTimeout(timeoutRef);
|
|
437
|
-
timeoutRef = null;
|
|
438
|
-
queued = undefined;
|
|
542
|
+
if (lastEmissionAt == null || now - lastEmissionAt >= ms) {
|
|
543
|
+
emitNow(v);
|
|
544
|
+
return;
|
|
439
545
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
546
|
+
queuedValue = v;
|
|
547
|
+
hasQueuedValue = true;
|
|
548
|
+
if (!timeoutRef) {
|
|
549
|
+
timeoutRef = setTimeout(flush, ms - (now - lastEmissionAt));
|
|
443
550
|
}
|
|
444
551
|
});
|
|
445
552
|
onCleanup(() => {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
timeoutRef = null;
|
|
449
|
-
}
|
|
450
|
-
queued = undefined;
|
|
553
|
+
clearPendingTimer();
|
|
554
|
+
hasQueuedValue = false;
|
|
451
555
|
});
|
|
452
556
|
}, { injector: opts?.injector });
|
|
453
557
|
return out.asReadonly();
|
|
@@ -534,7 +638,12 @@ const parseToDate = (value, format = 'dd.MM.yyyy') => {
|
|
|
534
638
|
};
|
|
535
639
|
tokens.forEach((token, i) => map[token]?.(i));
|
|
536
640
|
const date = new Date(year, month, day, hours, minutes, seconds);
|
|
537
|
-
if (
|
|
641
|
+
if ((tokens.includes('yyyy') && date.getFullYear() !== year) ||
|
|
642
|
+
(tokens.includes('MM') && date.getMonth() !== month) ||
|
|
643
|
+
(tokens.includes('dd') && date.getDate() !== day) ||
|
|
644
|
+
(tokens.includes('HH') && date.getHours() !== hours) ||
|
|
645
|
+
(tokens.includes('mm') && date.getMinutes() !== minutes) ||
|
|
646
|
+
(tokens.includes('ss') && date.getSeconds() !== seconds)) {
|
|
538
647
|
return null;
|
|
539
648
|
}
|
|
540
649
|
return date;
|
|
@@ -606,7 +715,7 @@ const toDate = (v) => {
|
|
|
606
715
|
* Converts a date string from a given format to ISO (UTC).
|
|
607
716
|
* Example:
|
|
608
717
|
* reformatDateToISO("24.11.2025", "dd.MM.yyyy")
|
|
609
|
-
*
|
|
718
|
+
* > "2025-11-24T00:00:00.000Z"
|
|
610
719
|
*/
|
|
611
720
|
function reformatDateToISO(dateStr, mask) {
|
|
612
721
|
if (!dateStr || !mask) {
|
|
@@ -851,20 +960,25 @@ function getChainedValue(obj, path) {
|
|
|
851
960
|
}
|
|
852
961
|
|
|
853
962
|
const encodeSpacesAsPercent20 = (query) => query.replace(/\+/g, '%20');
|
|
963
|
+
const URL_PARTS_RE = /^([a-zA-Z][a-zA-Z\d+.-]*:)?(\/\/[^/?#]*)?([^?#]*)(\?[^#]*)?(#.*)?$/;
|
|
854
964
|
/**
|
|
855
965
|
* Normalizes a given URL by removing redundant slashes, resolving dot segments,
|
|
856
|
-
* and ensuring there are no trailing slashes at the end of the
|
|
966
|
+
* and ensuring there are no trailing slashes at the end of the pathname.
|
|
967
|
+
*
|
|
968
|
+
* Query strings and hashes are preserved byte-for-byte.
|
|
857
969
|
*
|
|
858
970
|
* @param {string} url - The URL string to normalize.
|
|
859
971
|
* @return {string} - The normalized URL string.
|
|
860
972
|
*/
|
|
861
973
|
function normalizeUrl(url) {
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
974
|
+
const match = url.match(URL_PARTS_RE);
|
|
975
|
+
if (!match) {
|
|
976
|
+
return url;
|
|
977
|
+
}
|
|
978
|
+
const [, protocol = '', authority = '', pathname = '', query = '', hash = ''] = match;
|
|
979
|
+
const normalizedPath = normalizePathname(pathname);
|
|
980
|
+
const withoutRootSlash = authority && normalizedPath === '/' ? '' : normalizedPath;
|
|
981
|
+
return `${protocol}${authority}${withoutRootSlash}${query}${hash}`;
|
|
868
982
|
}
|
|
869
983
|
/**
|
|
870
984
|
* Replaces placeholders in the provided URL template with corresponding values from the params object.
|
|
@@ -873,17 +987,59 @@ function normalizeUrl(url) {
|
|
|
873
987
|
* @param {string} template URL template containing placeholders to replace.
|
|
874
988
|
* @param {AnyDict} [params] Optional dictionary containing key-value pairs.
|
|
875
989
|
* Keys correspond to placeholders in the template, and values will replace them.
|
|
990
|
+
* @param {FillUrlWithParamsOptions} [options] Placeholder materialization options.
|
|
876
991
|
* @return {string} URL with placeholders replaced by corresponding values from the params object.
|
|
877
992
|
* Defaults to the original template if params is not provided.
|
|
878
993
|
*/
|
|
879
|
-
function fillUrlWithParams(template, params) {
|
|
994
|
+
function fillUrlWithParams(template, params, options = {}) {
|
|
880
995
|
if (!params) {
|
|
881
996
|
return template;
|
|
882
997
|
}
|
|
883
998
|
return template.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
884
|
-
|
|
999
|
+
const value = params[key];
|
|
1000
|
+
if (value == null) {
|
|
1001
|
+
switch (options.missingParam ?? 'throw') {
|
|
1002
|
+
case 'keep':
|
|
1003
|
+
return `:${key}`;
|
|
1004
|
+
case 'empty':
|
|
1005
|
+
return '';
|
|
1006
|
+
case 'throw':
|
|
1007
|
+
default:
|
|
1008
|
+
throw new Error(`Missing URL parameter "${key}"`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
const encoded = encodeURIComponent(String(value));
|
|
1012
|
+
return options.preserveSlash ? encoded.replaceAll('%2F', '/') : encoded;
|
|
885
1013
|
});
|
|
886
1014
|
}
|
|
1015
|
+
const normalizePathname = (pathname) => {
|
|
1016
|
+
if (!pathname) {
|
|
1017
|
+
return '';
|
|
1018
|
+
}
|
|
1019
|
+
const hasLeadingSlash = pathname.startsWith('/');
|
|
1020
|
+
const segments = pathname.split('/');
|
|
1021
|
+
const normalized = [];
|
|
1022
|
+
for (const segment of segments) {
|
|
1023
|
+
if (!segment || segment === '.') {
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
if (segment === '..') {
|
|
1027
|
+
if (normalized.length && normalized[normalized.length - 1] !== '..') {
|
|
1028
|
+
normalized.pop();
|
|
1029
|
+
}
|
|
1030
|
+
else if (!hasLeadingSlash) {
|
|
1031
|
+
normalized.push(segment);
|
|
1032
|
+
}
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
normalized.push(segment);
|
|
1036
|
+
}
|
|
1037
|
+
const nextPath = `${hasLeadingSlash ? '/' : ''}${normalized.join('/')}`;
|
|
1038
|
+
if (nextPath === '/') {
|
|
1039
|
+
return nextPath;
|
|
1040
|
+
}
|
|
1041
|
+
return nextPath.replace(/\/$/, '');
|
|
1042
|
+
};
|
|
887
1043
|
/**
|
|
888
1044
|
* Low-level URL helper for appending query parameters to an existing URL.
|
|
889
1045
|
*
|
|
@@ -1003,18 +1159,17 @@ const makeQuery = (object, concatType, resolveConcatType) => {
|
|
|
1003
1159
|
* @param {string} pureRoute - Template route path to match, which may include placeholders
|
|
1004
1160
|
* (e.g., `:id`).
|
|
1005
1161
|
* @param {boolean} [withSubroute=false] - Determines whether to allow subroute matching. If true,
|
|
1006
|
-
* actualRoute must
|
|
1162
|
+
* actualRoute must match the template exactly or continue on a segment boundary.
|
|
1007
1163
|
* @return {boolean} Returns true if the actual route matches the template
|
|
1008
1164
|
* route (and optional subroute condition if enabled), otherwise false.
|
|
1009
1165
|
*/
|
|
1010
1166
|
const compareRoutes = (actualRoute, pureRoute, withSubroute = false) => {
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
const filled = fillRouteTemplate(actualRoute, pureRoute);
|
|
1016
|
-
return withSubroute ? actualRoute.startsWith(filled) : filled === actualRoute;
|
|
1167
|
+
const actualSegments = splitRouteSegments(actualRoute);
|
|
1168
|
+
const templateSegments = splitRouteSegments(pureRoute);
|
|
1169
|
+
if ((!withSubroute && actualSegments.length !== templateSegments.length) || actualSegments.length < templateSegments.length) {
|
|
1170
|
+
return false;
|
|
1017
1171
|
}
|
|
1172
|
+
return templateSegments.every((segment, index) => segment.startsWith(':') || segment === actualSegments[index]);
|
|
1018
1173
|
};
|
|
1019
1174
|
/**
|
|
1020
1175
|
* Processes and materializes a route path by decoding and, if necessary, replacing route parameters.
|
|
@@ -1025,25 +1180,30 @@ const compareRoutes = (actualRoute, pureRoute, withSubroute = false) => {
|
|
|
1025
1180
|
* @returns {string} Fully materialized and decoded route path.
|
|
1026
1181
|
*/
|
|
1027
1182
|
const materializeRoutePath = (actualRoute, pureRoute) => {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
}
|
|
1031
|
-
else {
|
|
1032
|
-
return decodeURIComponent(fillRouteTemplate(actualRoute, pureRoute));
|
|
1033
|
-
}
|
|
1183
|
+
const materialized = pureRoute.includes(':') ? fillRouteTemplate(actualRoute, pureRoute) : pureRoute;
|
|
1184
|
+
return safeDecodeRoute(materialized);
|
|
1034
1185
|
};
|
|
1186
|
+
const splitRouteSegments = (route) => route.split('/').filter(Boolean);
|
|
1035
1187
|
const fillRouteTemplate = (actualRoute, pureRoute) => {
|
|
1036
|
-
const actualSegments = actualRoute
|
|
1037
|
-
const templateSegments = pureRoute
|
|
1188
|
+
const actualSegments = splitRouteSegments(actualRoute);
|
|
1189
|
+
const templateSegments = splitRouteSegments(pureRoute);
|
|
1038
1190
|
const prefix = actualRoute.startsWith('/') ? '/' : '';
|
|
1039
1191
|
const segments = [];
|
|
1040
1192
|
for (let i = 0; i < templateSegments.length; i++) {
|
|
1041
1193
|
const tempSegment = templateSegments[i];
|
|
1042
1194
|
const actSegment = actualSegments[i];
|
|
1043
|
-
segments.push(tempSegment.startsWith(':') ? actSegment : tempSegment);
|
|
1195
|
+
segments.push(tempSegment.startsWith(':') ? (actSegment ?? tempSegment) : tempSegment);
|
|
1044
1196
|
}
|
|
1045
1197
|
return prefix + segments.join('/');
|
|
1046
1198
|
};
|
|
1199
|
+
const safeDecodeRoute = (route) => {
|
|
1200
|
+
try {
|
|
1201
|
+
return decodeURIComponent(route);
|
|
1202
|
+
}
|
|
1203
|
+
catch {
|
|
1204
|
+
return route;
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1047
1207
|
|
|
1048
1208
|
/**
|
|
1049
1209
|
* Recommended high-level entrypoint for query-string work in `internal`.
|
|
@@ -1210,61 +1370,92 @@ function sortInputToTokens(sort) {
|
|
|
1210
1370
|
return normalizeSortInput(sort).map((item) => sortRuleToToken(item));
|
|
1211
1371
|
}
|
|
1212
1372
|
|
|
1373
|
+
const getComparableKind = (value) => {
|
|
1374
|
+
if (Array.isArray(value))
|
|
1375
|
+
return 'array';
|
|
1376
|
+
if (value instanceof Date)
|
|
1377
|
+
return 'date';
|
|
1378
|
+
if (value instanceof Map)
|
|
1379
|
+
return 'map';
|
|
1380
|
+
if (value instanceof Set)
|
|
1381
|
+
return 'set';
|
|
1382
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1383
|
+
return prototype === Object.prototype || prototype === null ? 'plain-object' : 'unsupported';
|
|
1384
|
+
};
|
|
1385
|
+
const getMemo = (memo, a, b) => memo.get(a)?.get(b);
|
|
1386
|
+
const setMemo = (memo, a, b, value) => {
|
|
1387
|
+
let comparisons = memo.get(a);
|
|
1388
|
+
if (!comparisons) {
|
|
1389
|
+
comparisons = new WeakMap();
|
|
1390
|
+
memo.set(a, comparisons);
|
|
1391
|
+
}
|
|
1392
|
+
comparisons.set(b, value);
|
|
1393
|
+
};
|
|
1394
|
+
const memoizePair = (memo, a, b, value) => {
|
|
1395
|
+
setMemo(memo, a, b, value);
|
|
1396
|
+
setMemo(memo, b, a, value);
|
|
1397
|
+
};
|
|
1213
1398
|
/**
|
|
1214
|
-
* Recursively
|
|
1215
|
-
* such as arrays, sets, maps, dates, and NaN.
|
|
1399
|
+
* Recursively compares primitives, arrays, plain objects, Date, Map, and Set values.
|
|
1216
1400
|
*
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1219
|
-
* a mathematical set-equivalence check.
|
|
1401
|
+
* Map and Set values are compared in iteration order. Object comparison includes own enumerable
|
|
1402
|
+
* string keys only. Symbol and non-enumerable object keys are intentionally excluded.
|
|
1220
1403
|
*
|
|
1221
|
-
* @param {AnyType} a - The first
|
|
1222
|
-
* @param {AnyType} b - The second
|
|
1223
|
-
* @
|
|
1224
|
-
* during deep equality check.
|
|
1225
|
-
* @return {boolean} - Returns true if the objects are deeply equal, otherwise returns false.
|
|
1404
|
+
* @param {AnyType} a - The first value to compare.
|
|
1405
|
+
* @param {AnyType} b - The second value to compare.
|
|
1406
|
+
* @return {boolean} - Returns true if the supported values are deeply equal, otherwise false.
|
|
1226
1407
|
*/
|
|
1227
|
-
|
|
1228
|
-
if (a
|
|
1408
|
+
const deepEqualInternal = (a, b, memo, activeLeftToRight, activeRightToLeft) => {
|
|
1409
|
+
if (Object.is(a, b))
|
|
1229
1410
|
return true;
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
if (
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1411
|
+
if (!a || !b || typeof a !== 'object' || typeof b !== 'object')
|
|
1412
|
+
return false;
|
|
1413
|
+
const kindA = getComparableKind(a);
|
|
1414
|
+
const kindB = getComparableKind(b);
|
|
1415
|
+
if (kindA !== kindB || kindA === 'unsupported')
|
|
1416
|
+
return false;
|
|
1417
|
+
const cached = getMemo(memo, a, b);
|
|
1418
|
+
if (cached !== undefined)
|
|
1419
|
+
return cached;
|
|
1420
|
+
const activeMatch = activeLeftToRight.get(a);
|
|
1421
|
+
if (activeMatch || activeRightToLeft.has(b)) {
|
|
1422
|
+
return activeMatch === b && activeRightToLeft.get(b) === a;
|
|
1423
|
+
}
|
|
1424
|
+
activeLeftToRight.set(a, b);
|
|
1425
|
+
activeRightToLeft.set(b, a);
|
|
1426
|
+
let result = false;
|
|
1427
|
+
switch (kindA) {
|
|
1428
|
+
case 'array':
|
|
1429
|
+
result =
|
|
1430
|
+
a.length === b.length &&
|
|
1431
|
+
a.every((item, index) => deepEqualInternal(item, b[index], memo, activeLeftToRight, activeRightToLeft));
|
|
1432
|
+
break;
|
|
1433
|
+
case 'date':
|
|
1434
|
+
result = Object.is(a.getTime(), b.getTime());
|
|
1435
|
+
break;
|
|
1436
|
+
case 'map':
|
|
1437
|
+
result = deepEqualInternal(Array.from(a.entries()), Array.from(b.entries()), memo, activeLeftToRight, activeRightToLeft);
|
|
1438
|
+
break;
|
|
1439
|
+
case 'set':
|
|
1440
|
+
result = deepEqualInternal(Array.from(a.values()), Array.from(b.values()), memo, activeLeftToRight, activeRightToLeft);
|
|
1441
|
+
break;
|
|
1442
|
+
case 'plain-object': {
|
|
1443
|
+
const keysA = Object.keys(a);
|
|
1444
|
+
const keysB = Object.keys(b);
|
|
1445
|
+
result =
|
|
1446
|
+
keysA.length === keysB.length &&
|
|
1447
|
+
keysA.every((key) => Object.prototype.hasOwnProperty.call(b, key) &&
|
|
1448
|
+
deepEqualInternal(a[key], b[key], memo, activeLeftToRight, activeRightToLeft));
|
|
1449
|
+
break;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
activeLeftToRight.delete(a);
|
|
1453
|
+
activeRightToLeft.delete(b);
|
|
1454
|
+
memoizePair(memo, a, b, result);
|
|
1455
|
+
return result;
|
|
1456
|
+
};
|
|
1457
|
+
function deepEqual(a, b) {
|
|
1458
|
+
return deepEqualInternal(a, b, new WeakMap(), new WeakMap(), new WeakMap());
|
|
1268
1459
|
}
|
|
1269
1460
|
|
|
1270
1461
|
/**
|
|
@@ -1423,7 +1614,7 @@ class Serializer {
|
|
|
1423
1614
|
}
|
|
1424
1615
|
if (typedField?.type === 'nullable' || isNullable(value, this.config.mapNullable?.includeEmptyString)) {
|
|
1425
1616
|
const nullableVal = value ?? null;
|
|
1426
|
-
return this.config.mapNullable.format?.(nullableVal)
|
|
1617
|
+
return this.config.mapNullable.format?.(nullableVal) ?? this.config.mapNullable.replaceWith ?? nullableVal;
|
|
1427
1618
|
}
|
|
1428
1619
|
if (typedField?.type === 'string') {
|
|
1429
1620
|
return serializeString(this.config, value);
|
|
@@ -1494,27 +1685,40 @@ class Serializer {
|
|
|
1494
1685
|
}
|
|
1495
1686
|
}
|
|
1496
1687
|
if (typedField?.type === 'nullable' || isNullable(value, this.config.mapNullable?.includeEmptyString)) {
|
|
1497
|
-
return this.config.mapNullable.parse?.(value)
|
|
1688
|
+
return this.config.mapNullable.parse?.(value) ?? value;
|
|
1689
|
+
}
|
|
1690
|
+
if (typedField?.type === 'boolean') {
|
|
1691
|
+
const parsed = this.config.mapBoolean.parse?.(value);
|
|
1692
|
+
if (parsed !== undefined) {
|
|
1693
|
+
return parsed;
|
|
1694
|
+
}
|
|
1695
|
+
if (value === true || value === false) {
|
|
1696
|
+
return value;
|
|
1697
|
+
}
|
|
1698
|
+
if (value === this.config.mapBoolean.true || value === 'true') {
|
|
1699
|
+
return true;
|
|
1700
|
+
}
|
|
1701
|
+
if (value === this.config.mapBoolean.false || value === 'false') {
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
throw new SerializerFieldError(key || '', 'parse', new Error(`Unknown boolean value: ${String(value)}`));
|
|
1498
1705
|
}
|
|
1499
|
-
if (
|
|
1500
|
-
typeof value === 'boolean' ||
|
|
1501
|
-
value === this.config.mapBoolean.true ||
|
|
1502
|
-
value === this.config.mapBoolean.false) {
|
|
1706
|
+
if (typeof value === 'boolean' || value === this.config.mapBoolean.true || value === this.config.mapBoolean.false) {
|
|
1503
1707
|
return this.config.mapBoolean.parse?.(value) ?? (value === this.config.mapBoolean.true || value === true);
|
|
1504
1708
|
}
|
|
1505
1709
|
const maybeDate = parseToDate(value, this.config.mapDate.dateFormat);
|
|
1506
1710
|
if (typedField?.type === 'date' || maybeDate) {
|
|
1507
|
-
return this.config.mapDate.parse?.(value)
|
|
1711
|
+
return this.config.mapDate.parse?.(value) ?? maybeDate;
|
|
1508
1712
|
}
|
|
1509
1713
|
const periodTransform = this.config.mapPeriod.transformMode;
|
|
1510
1714
|
if (periodTransform.mode === 'join') {
|
|
1511
1715
|
const maybePeriod = parseToDatePeriod(value, this.config.mapPeriod.dateFormat);
|
|
1512
1716
|
if (typedField?.type === 'period' || (maybePeriod || []).some(Boolean)) {
|
|
1513
|
-
return this.config.mapPeriod.parse?.(value)
|
|
1717
|
+
return this.config.mapPeriod.parse?.(value) ?? maybePeriod;
|
|
1514
1718
|
}
|
|
1515
1719
|
}
|
|
1516
1720
|
if (typedField?.type === 'number' || isNumber(value, this.config.mapNumber.fromString)) {
|
|
1517
|
-
return this.config.mapNumber.parse?.(value)
|
|
1721
|
+
return this.config.mapNumber.parse?.(value) ?? Number(String(value).trim());
|
|
1518
1722
|
}
|
|
1519
1723
|
if (typedField?.type === 'string' || typeof value === 'string') {
|
|
1520
1724
|
const parsed = this.config.mapString.parse?.(value);
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.0.
|
|
2
|
+
"version": "2.0.7",
|
|
3
3
|
"name": "@reforgium/internal",
|
|
4
4
|
"description": "Hidden Reforgium foundation package for shared primitives, tokens, and infrastructure.",
|
|
5
5
|
"author": "rtommievich",
|
|
@@ -25,9 +25,6 @@
|
|
|
25
25
|
"foundation",
|
|
26
26
|
"infrastructure"
|
|
27
27
|
],
|
|
28
|
-
"types": "./src/public-api.ts",
|
|
29
|
-
"typings": "types/reforgium-internal.d.ts",
|
|
30
|
-
"module": "fesm2022/reforgium-internal.mjs",
|
|
31
28
|
"files": [
|
|
32
29
|
"fesm2022/*.mjs",
|
|
33
30
|
"types/*.d.ts",
|
|
@@ -36,6 +33,14 @@
|
|
|
36
33
|
"CHANGELOG.md",
|
|
37
34
|
"LICENSE*"
|
|
38
35
|
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"tslib": "^2.8.1"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@angular/core": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"module": "fesm2022/reforgium-internal.mjs",
|
|
43
|
+
"typings": "types/reforgium-internal.d.ts",
|
|
39
44
|
"exports": {
|
|
40
45
|
"./package.json": {
|
|
41
46
|
"default": "./package.json"
|
|
@@ -44,11 +49,5 @@
|
|
|
44
49
|
"types": "./types/reforgium-internal.d.ts",
|
|
45
50
|
"default": "./fesm2022/reforgium-internal.mjs"
|
|
46
51
|
}
|
|
47
|
-
},
|
|
48
|
-
"dependencies": {
|
|
49
|
-
"tslib": "^2.8.1"
|
|
50
|
-
},
|
|
51
|
-
"peerDependencies": {
|
|
52
|
-
"@angular/core": ">=19.0.0"
|
|
53
52
|
}
|
|
54
53
|
}
|
|
@@ -271,6 +271,7 @@ declare class LocalStorage<Key = string, Type extends AnyType = AnyDict> impleme
|
|
|
271
271
|
set(key: Key, value: Type): void;
|
|
272
272
|
remove(key: Key): void;
|
|
273
273
|
clear(): void;
|
|
274
|
+
private get storage();
|
|
274
275
|
private getSafePrefix;
|
|
275
276
|
private safePrefix;
|
|
276
277
|
}
|
|
@@ -311,6 +312,7 @@ declare class SessionStorage<Key = string, Type extends AnyType = AnyDict> imple
|
|
|
311
312
|
set(key: Key, value: Type): void;
|
|
312
313
|
remove(key: Key): void;
|
|
313
314
|
clear(): void;
|
|
315
|
+
private get storage();
|
|
314
316
|
private getSafePrefix;
|
|
315
317
|
private safePrefix;
|
|
316
318
|
}
|
|
@@ -650,7 +652,7 @@ declare const toDate: (v: unknown) => Date;
|
|
|
650
652
|
* Converts a date string from a given format to ISO (UTC).
|
|
651
653
|
* Example:
|
|
652
654
|
* reformatDateToISO("24.11.2025", "dd.MM.yyyy")
|
|
653
|
-
*
|
|
655
|
+
* > "2025-11-24T00:00:00.000Z"
|
|
654
656
|
*/
|
|
655
657
|
declare function reformatDateToISO(dateStr?: string | null, mask?: string | null): string | null;
|
|
656
658
|
|
|
@@ -729,9 +731,15 @@ type ParseQueryParams = {
|
|
|
729
731
|
(query: string): Record<string, string>;
|
|
730
732
|
(query: string, arrayMode: QueryArrayMode): Record<string, string | string[]>;
|
|
731
733
|
};
|
|
734
|
+
type FillUrlWithParamsOptions = {
|
|
735
|
+
missingParam?: 'empty' | 'keep' | 'throw';
|
|
736
|
+
preserveSlash?: boolean;
|
|
737
|
+
};
|
|
732
738
|
/**
|
|
733
739
|
* Normalizes a given URL by removing redundant slashes, resolving dot segments,
|
|
734
|
-
* and ensuring there are no trailing slashes at the end of the
|
|
740
|
+
* and ensuring there are no trailing slashes at the end of the pathname.
|
|
741
|
+
*
|
|
742
|
+
* Query strings and hashes are preserved byte-for-byte.
|
|
735
743
|
*
|
|
736
744
|
* @param {string} url - The URL string to normalize.
|
|
737
745
|
* @return {string} - The normalized URL string.
|
|
@@ -744,10 +752,11 @@ declare function normalizeUrl(url: string): string;
|
|
|
744
752
|
* @param {string} template URL template containing placeholders to replace.
|
|
745
753
|
* @param {AnyDict} [params] Optional dictionary containing key-value pairs.
|
|
746
754
|
* Keys correspond to placeholders in the template, and values will replace them.
|
|
755
|
+
* @param {FillUrlWithParamsOptions} [options] Placeholder materialization options.
|
|
747
756
|
* @return {string} URL with placeholders replaced by corresponding values from the params object.
|
|
748
757
|
* Defaults to the original template if params is not provided.
|
|
749
758
|
*/
|
|
750
|
-
declare function fillUrlWithParams(template: string, params?: AnyDict): string;
|
|
759
|
+
declare function fillUrlWithParams(template: string, params?: AnyDict, options?: FillUrlWithParamsOptions): string;
|
|
751
760
|
/**
|
|
752
761
|
* Low-level URL helper for appending query parameters to an existing URL.
|
|
753
762
|
*
|
|
@@ -803,7 +812,7 @@ declare const makeQuery: (object: AnyDict, concatType: QueryArrayMode, resolveCo
|
|
|
803
812
|
* @param {string} pureRoute - Template route path to match, which may include placeholders
|
|
804
813
|
* (e.g., `:id`).
|
|
805
814
|
* @param {boolean} [withSubroute=false] - Determines whether to allow subroute matching. If true,
|
|
806
|
-
* actualRoute must
|
|
815
|
+
* actualRoute must match the template exactly or continue on a segment boundary.
|
|
807
816
|
* @return {boolean} Returns true if the actual route matches the template
|
|
808
817
|
* route (and optional subroute condition if enabled), otherwise false.
|
|
809
818
|
*/
|
|
@@ -899,21 +908,7 @@ declare function normalizeSortInput<Field extends string = string>(sort: SortInp
|
|
|
899
908
|
*/
|
|
900
909
|
declare function sortInputToTokens<Field extends string = string>(sort: SortInput<Field>): SortToken<Field>[];
|
|
901
910
|
|
|
902
|
-
|
|
903
|
-
* Recursively checks the equality of two objects, including nested structures and special cases
|
|
904
|
-
* such as arrays, sets, maps, dates, and NaN.
|
|
905
|
-
*
|
|
906
|
-
* `Map` and `Set` values are compared in iteration order because they are materialized into arrays
|
|
907
|
-
* before comparison. This keeps the helper predictable for transport/state snapshots, but it is not
|
|
908
|
-
* a mathematical set-equivalence check.
|
|
909
|
-
*
|
|
910
|
-
* @param {AnyType} a - The first object to compare.
|
|
911
|
-
* @param {AnyType} b - The second object to compare.
|
|
912
|
-
* @param {Map} [seen=new Map()] - A map used to detect cyclic references
|
|
913
|
-
* during deep equality check.
|
|
914
|
-
* @return {boolean} - Returns true if the objects are deeply equal, otherwise returns false.
|
|
915
|
-
*/
|
|
916
|
-
declare function deepEqual(a: AnyType, b: AnyType, seen?: Map<any, any>): boolean;
|
|
911
|
+
declare function deepEqual(a: AnyType, b: AnyType): boolean;
|
|
917
912
|
|
|
918
913
|
/**
|
|
919
914
|
* Generates a short pseudo-random identifier.
|