@reforgium/internal 2.0.4 → 2.0.6
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 +339 -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,15 +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
|
-
return template.replace(/:([a-zA-Z0-9_]+)/g, (_,
|
|
998
|
+
return template.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
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;
|
|
1013
|
+
});
|
|
884
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
|
+
};
|
|
885
1043
|
/**
|
|
886
1044
|
* Low-level URL helper for appending query parameters to an existing URL.
|
|
887
1045
|
*
|
|
@@ -1001,18 +1159,17 @@ const makeQuery = (object, concatType, resolveConcatType) => {
|
|
|
1001
1159
|
* @param {string} pureRoute - Template route path to match, which may include placeholders
|
|
1002
1160
|
* (e.g., `:id`).
|
|
1003
1161
|
* @param {boolean} [withSubroute=false] - Determines whether to allow subroute matching. If true,
|
|
1004
|
-
* actualRoute must
|
|
1162
|
+
* actualRoute must match the template exactly or continue on a segment boundary.
|
|
1005
1163
|
* @return {boolean} Returns true if the actual route matches the template
|
|
1006
1164
|
* route (and optional subroute condition if enabled), otherwise false.
|
|
1007
1165
|
*/
|
|
1008
1166
|
const compareRoutes = (actualRoute, pureRoute, withSubroute = false) => {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
const filled = fillRouteTemplate(actualRoute, pureRoute);
|
|
1014
|
-
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;
|
|
1015
1171
|
}
|
|
1172
|
+
return templateSegments.every((segment, index) => segment.startsWith(':') || segment === actualSegments[index]);
|
|
1016
1173
|
};
|
|
1017
1174
|
/**
|
|
1018
1175
|
* Processes and materializes a route path by decoding and, if necessary, replacing route parameters.
|
|
@@ -1023,25 +1180,30 @@ const compareRoutes = (actualRoute, pureRoute, withSubroute = false) => {
|
|
|
1023
1180
|
* @returns {string} Fully materialized and decoded route path.
|
|
1024
1181
|
*/
|
|
1025
1182
|
const materializeRoutePath = (actualRoute, pureRoute) => {
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
}
|
|
1029
|
-
else {
|
|
1030
|
-
return decodeURIComponent(fillRouteTemplate(actualRoute, pureRoute));
|
|
1031
|
-
}
|
|
1183
|
+
const materialized = pureRoute.includes(':') ? fillRouteTemplate(actualRoute, pureRoute) : pureRoute;
|
|
1184
|
+
return safeDecodeRoute(materialized);
|
|
1032
1185
|
};
|
|
1186
|
+
const splitRouteSegments = (route) => route.split('/').filter(Boolean);
|
|
1033
1187
|
const fillRouteTemplate = (actualRoute, pureRoute) => {
|
|
1034
|
-
const actualSegments = actualRoute
|
|
1035
|
-
const templateSegments = pureRoute
|
|
1188
|
+
const actualSegments = splitRouteSegments(actualRoute);
|
|
1189
|
+
const templateSegments = splitRouteSegments(pureRoute);
|
|
1036
1190
|
const prefix = actualRoute.startsWith('/') ? '/' : '';
|
|
1037
1191
|
const segments = [];
|
|
1038
1192
|
for (let i = 0; i < templateSegments.length; i++) {
|
|
1039
1193
|
const tempSegment = templateSegments[i];
|
|
1040
1194
|
const actSegment = actualSegments[i];
|
|
1041
|
-
segments.push(tempSegment.startsWith(':') ? actSegment : tempSegment);
|
|
1195
|
+
segments.push(tempSegment.startsWith(':') ? (actSegment ?? tempSegment) : tempSegment);
|
|
1042
1196
|
}
|
|
1043
1197
|
return prefix + segments.join('/');
|
|
1044
1198
|
};
|
|
1199
|
+
const safeDecodeRoute = (route) => {
|
|
1200
|
+
try {
|
|
1201
|
+
return decodeURIComponent(route);
|
|
1202
|
+
}
|
|
1203
|
+
catch {
|
|
1204
|
+
return route;
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1045
1207
|
|
|
1046
1208
|
/**
|
|
1047
1209
|
* Recommended high-level entrypoint for query-string work in `internal`.
|
|
@@ -1208,61 +1370,92 @@ function sortInputToTokens(sort) {
|
|
|
1208
1370
|
return normalizeSortInput(sort).map((item) => sortRuleToToken(item));
|
|
1209
1371
|
}
|
|
1210
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
|
+
};
|
|
1211
1398
|
/**
|
|
1212
|
-
* Recursively
|
|
1213
|
-
* such as arrays, sets, maps, dates, and NaN.
|
|
1399
|
+
* Recursively compares primitives, arrays, plain objects, Date, Map, and Set values.
|
|
1214
1400
|
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
* 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.
|
|
1218
1403
|
*
|
|
1219
|
-
* @param {AnyType} a - The first
|
|
1220
|
-
* @param {AnyType} b - The second
|
|
1221
|
-
* @
|
|
1222
|
-
* during deep equality check.
|
|
1223
|
-
* @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.
|
|
1224
1407
|
*/
|
|
1225
|
-
|
|
1226
|
-
if (a
|
|
1227
|
-
return true;
|
|
1228
|
-
}
|
|
1229
|
-
if (Number.isNaN(a) && Number.isNaN(b)) {
|
|
1230
|
-
return true;
|
|
1231
|
-
}
|
|
1232
|
-
if (a && b && typeof a === 'object' && typeof b === 'object') {
|
|
1233
|
-
if (seen.get(a) === b) {
|
|
1234
|
-
return true;
|
|
1235
|
-
}
|
|
1236
|
-
seen.set(a, b);
|
|
1237
|
-
if (Array.isArray(a)) {
|
|
1238
|
-
if (!Array.isArray(b) || a.length !== b.length) {
|
|
1239
|
-
return false;
|
|
1240
|
-
}
|
|
1241
|
-
for (let i = 0; i < a.length; i++) {
|
|
1242
|
-
if (!deepEqual(a[i], b[i], seen)) {
|
|
1243
|
-
return false;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
return true;
|
|
1247
|
-
}
|
|
1248
|
-
if (a instanceof Date || a instanceof RegExp) {
|
|
1249
|
-
return String(a) === String(b);
|
|
1250
|
-
}
|
|
1251
|
-
if (a instanceof Map) {
|
|
1252
|
-
return deepEqual(Array.from(a.entries()), Array.from(b.entries()), seen);
|
|
1253
|
-
}
|
|
1254
|
-
if (a instanceof Set) {
|
|
1255
|
-
return deepEqual(Array.from(a.values()), Array.from(b.values()), seen);
|
|
1256
|
-
}
|
|
1257
|
-
const keysA = Object.keys(a);
|
|
1258
|
-
if (keysA.length !== Object.keys(b).length)
|
|
1259
|
-
return false;
|
|
1260
|
-
for (const k of keysA)
|
|
1261
|
-
if (!Object.prototype.hasOwnProperty.call(b, k) || !deepEqual(a[k], b[k], seen))
|
|
1262
|
-
return false;
|
|
1408
|
+
const deepEqualInternal = (a, b, memo, activeLeftToRight, activeRightToLeft) => {
|
|
1409
|
+
if (Object.is(a, b))
|
|
1263
1410
|
return true;
|
|
1264
|
-
|
|
1265
|
-
|
|
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());
|
|
1266
1459
|
}
|
|
1267
1460
|
|
|
1268
1461
|
/**
|
|
@@ -1421,7 +1614,7 @@ class Serializer {
|
|
|
1421
1614
|
}
|
|
1422
1615
|
if (typedField?.type === 'nullable' || isNullable(value, this.config.mapNullable?.includeEmptyString)) {
|
|
1423
1616
|
const nullableVal = value ?? null;
|
|
1424
|
-
return this.config.mapNullable.format?.(nullableVal)
|
|
1617
|
+
return this.config.mapNullable.format?.(nullableVal) ?? this.config.mapNullable.replaceWith ?? nullableVal;
|
|
1425
1618
|
}
|
|
1426
1619
|
if (typedField?.type === 'string') {
|
|
1427
1620
|
return serializeString(this.config, value);
|
|
@@ -1492,27 +1685,40 @@ class Serializer {
|
|
|
1492
1685
|
}
|
|
1493
1686
|
}
|
|
1494
1687
|
if (typedField?.type === 'nullable' || isNullable(value, this.config.mapNullable?.includeEmptyString)) {
|
|
1495
|
-
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)}`));
|
|
1496
1705
|
}
|
|
1497
|
-
if (
|
|
1498
|
-
typeof value === 'boolean' ||
|
|
1499
|
-
value === this.config.mapBoolean.true ||
|
|
1500
|
-
value === this.config.mapBoolean.false) {
|
|
1706
|
+
if (typeof value === 'boolean' || value === this.config.mapBoolean.true || value === this.config.mapBoolean.false) {
|
|
1501
1707
|
return this.config.mapBoolean.parse?.(value) ?? (value === this.config.mapBoolean.true || value === true);
|
|
1502
1708
|
}
|
|
1503
1709
|
const maybeDate = parseToDate(value, this.config.mapDate.dateFormat);
|
|
1504
1710
|
if (typedField?.type === 'date' || maybeDate) {
|
|
1505
|
-
return this.config.mapDate.parse?.(value)
|
|
1711
|
+
return this.config.mapDate.parse?.(value) ?? maybeDate;
|
|
1506
1712
|
}
|
|
1507
1713
|
const periodTransform = this.config.mapPeriod.transformMode;
|
|
1508
1714
|
if (periodTransform.mode === 'join') {
|
|
1509
1715
|
const maybePeriod = parseToDatePeriod(value, this.config.mapPeriod.dateFormat);
|
|
1510
1716
|
if (typedField?.type === 'period' || (maybePeriod || []).some(Boolean)) {
|
|
1511
|
-
return this.config.mapPeriod.parse?.(value)
|
|
1717
|
+
return this.config.mapPeriod.parse?.(value) ?? maybePeriod;
|
|
1512
1718
|
}
|
|
1513
1719
|
}
|
|
1514
1720
|
if (typedField?.type === 'number' || isNumber(value, this.config.mapNumber.fromString)) {
|
|
1515
|
-
return this.config.mapNumber.parse?.(value)
|
|
1721
|
+
return this.config.mapNumber.parse?.(value) ?? Number(String(value).trim());
|
|
1516
1722
|
}
|
|
1517
1723
|
if (typedField?.type === 'string' || typeof value === 'string') {
|
|
1518
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.6",
|
|
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.
|