@naturalcycles/js-lib 14.213.0 → 14.215.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.
Files changed (48) hide show
  1. package/dist/array/array.util.d.ts +3 -0
  2. package/dist/array/array.util.js +3 -0
  3. package/dist/decorators/asyncMemo.decorator.d.ts +4 -14
  4. package/dist/decorators/asyncMemo.decorator.js +5 -11
  5. package/dist/decorators/memo.decorator.d.ts +0 -13
  6. package/dist/decorators/memo.decorator.js +1 -11
  7. package/dist/decorators/memoFn.js +1 -13
  8. package/dist/decorators/memoFnAsync.js +1 -13
  9. package/dist/decorators/memoSimple.decorator.d.ts +0 -3
  10. package/dist/decorators/memoSimple.decorator.js +1 -8
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +2 -0
  13. package/dist/object/deepEquals.d.ts +58 -6
  14. package/dist/object/deepEquals.js +146 -52
  15. package/dist/object/map2.d.ts +15 -0
  16. package/dist/object/map2.js +25 -0
  17. package/dist/object/object.util.d.ts +3 -1
  18. package/dist/object/set2.d.ts +11 -0
  19. package/dist/object/set2.js +19 -0
  20. package/dist/string/stringify.js +6 -0
  21. package/dist-esm/array/array.util.js +3 -0
  22. package/dist-esm/decorators/asyncMemo.decorator.js +6 -12
  23. package/dist-esm/decorators/memo.decorator.js +2 -12
  24. package/dist-esm/decorators/memoFn.js +1 -13
  25. package/dist-esm/decorators/memoFnAsync.js +1 -13
  26. package/dist-esm/decorators/memoSimple.decorator.js +2 -9
  27. package/dist-esm/index.js +2 -0
  28. package/dist-esm/object/deepEquals.js +142 -49
  29. package/dist-esm/object/map2.js +21 -0
  30. package/dist-esm/object/set2.js +15 -0
  31. package/dist-esm/string/stringify.js +6 -0
  32. package/package.json +1 -4
  33. package/src/array/array.util.ts +3 -0
  34. package/src/decorators/asyncMemo.decorator.ts +5 -42
  35. package/src/decorators/memo.decorator.ts +1 -37
  36. package/src/decorators/memoFn.ts +0 -18
  37. package/src/decorators/memoFnAsync.ts +0 -18
  38. package/src/decorators/memoSimple.decorator.ts +2 -20
  39. package/src/index.ts +2 -0
  40. package/src/object/deepEquals.ts +136 -45
  41. package/src/object/map2.ts +25 -0
  42. package/src/object/object.util.ts +3 -1
  43. package/src/object/set2.ts +18 -0
  44. package/src/string/stringify.ts +6 -0
  45. package/dist/lodash.types.d.ts +0 -4
  46. package/dist/lodash.types.js +0 -2
  47. package/dist-esm/lodash.types.js +0 -1
  48. package/src/lodash.types.ts +0 -6
@@ -1,4 +1,3 @@
1
- import type { PropertyPath } from '../lodash.types';
2
1
  import { KeyValueTuple, Reviver, SKIP } from '../types';
3
2
  import type { AnyObject, ObjectMapper, ObjectPredicate, ValueOf } from '../types';
4
3
  /**
@@ -146,6 +145,8 @@ export declare function _invertMap<K, V>(m: ReadonlyMap<K, V>): Map<V, K>;
146
145
  * _get(obj, 'unknown.path') // undefined
147
146
  */
148
147
  export declare function _get<T extends AnyObject>(obj?: T, path?: string): unknown;
148
+ type Many<T> = T | readonly T[];
149
+ type PropertyPath = Many<PropertyKey>;
149
150
  /**
150
151
  * Sets the value at path of object. If a portion of path doesn’t exist it’s created. Arrays are created for
151
152
  * missing index properties while objects are created for all other missing properties.
@@ -207,3 +208,4 @@ export declare function _deepFreeze(o: any): void;
207
208
  * To make mutation extra clear - function returns void (unlike Object.assign).
208
209
  */
209
210
  export declare function _objectAssignExact<T extends AnyObject>(target: T, source: T): void;
211
+ export {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Like Set, but serializes to JSON as an array.
3
+ *
4
+ * Fixes the "issue" of stock Set being json-serialized as `{}`.
5
+ *
6
+ * @experimental
7
+ */
8
+ export declare class Set2<T = any> extends Set<T> {
9
+ toArray(): T[];
10
+ toJSON(): T[];
11
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Set2 = void 0;
4
+ /**
5
+ * Like Set, but serializes to JSON as an array.
6
+ *
7
+ * Fixes the "issue" of stock Set being json-serialized as `{}`.
8
+ *
9
+ * @experimental
10
+ */
11
+ class Set2 extends Set {
12
+ toArray() {
13
+ return [...this];
14
+ }
15
+ toJSON() {
16
+ return [...this];
17
+ }
18
+ }
19
+ exports.Set2 = Set2;
@@ -111,6 +111,12 @@ function _stringify(obj, opt = {}) {
111
111
  //
112
112
  // Other
113
113
  //
114
+ if (obj instanceof Map) {
115
+ obj = Object.fromEntries(obj);
116
+ }
117
+ else if (obj instanceof Set) {
118
+ obj = [...obj];
119
+ }
114
120
  try {
115
121
  const { stringifyFn = globalStringifyFunction } = opt;
116
122
  s = stringifyFn(obj, undefined, 2);
@@ -160,6 +160,9 @@ export function _sortDescBy(items, mapper, mutate = false) {
160
160
  }
161
161
  /**
162
162
  * Like items.find(), but it tries to find from the END of the array.
163
+ *
164
+ * Node 18+ supports native array.findLast() - use that.
165
+ * iOS Safari only has it since 15.4
163
166
  */
164
167
  export function _findLast(items, predicate) {
165
168
  return [...items].reverse().find(predicate);
@@ -1,9 +1,12 @@
1
- import { _since } from '../time/time.util';
2
- import { _getArgsSignature, _getMethodSignature, _getTargetMethodSignature } from './decorator.util';
1
+ import { _getTargetMethodSignature } from './decorator.util';
3
2
  import { jsonMemoSerializer, MapMemoCache } from './memo.util';
4
3
  /**
5
4
  * Like @_Memo, but allowing async MemoCache implementation.
6
5
  *
6
+ * Important: it awaits the method to return the result before caching it.
7
+ *
8
+ * todo: test for "swarm requests", it should return "the same promise" and not cause a swarm origin hit
9
+ *
7
10
  * Method CANNOT return `undefined`, as undefined will always be treated as cache MISS and retried.
8
11
  * Return `null` instead (it'll be cached).
9
12
  */
@@ -15,7 +18,7 @@ export const _AsyncMemo = (opt = {}) => (target, key, descriptor) => {
15
18
  const originalFn = descriptor.value;
16
19
  // Map from "instance" of the Class where @_AsyncMemo is applied to AsyncMemoCache instance.
17
20
  const cache = new Map();
18
- const { logHit = false, logMiss = false, logArgs = true, logger = console, cacheFactory = () => new MapMemoCache(), cacheKeyFn = jsonMemoSerializer, cacheRejections = true, } = opt;
21
+ const { logger = console, cacheFactory = () => new MapMemoCache(), cacheKeyFn = jsonMemoSerializer, cacheRejections = true, } = opt;
19
22
  const keyStr = String(key);
20
23
  const methodSignature = _getTargetMethodSignature(target, keyStr);
21
24
  descriptor.value = async function (...args) {
@@ -36,16 +39,12 @@ export const _AsyncMemo = (opt = {}) => (target, key, descriptor) => {
36
39
  }
37
40
  if (value !== undefined) {
38
41
  // hit!
39
- if (logHit) {
40
- logger.log(`${_getMethodSignature(ctx, keyStr)}(${_getArgsSignature(args, logArgs)}) @_AsyncMemo hit`);
41
- }
42
42
  if (value instanceof Error) {
43
43
  throw value;
44
44
  }
45
45
  return value;
46
46
  }
47
47
  // Here we know it's a MISS, let's execute the real method
48
- const started = Date.now();
49
48
  try {
50
49
  value = await originalFn.apply(ctx, args);
51
50
  // Save the value in the Cache, without awaiting it
@@ -77,11 +76,6 @@ export const _AsyncMemo = (opt = {}) => (target, key, descriptor) => {
77
76
  }
78
77
  throw err;
79
78
  }
80
- finally {
81
- if (logMiss) {
82
- logger.log(`${_getMethodSignature(ctx, keyStr)}(${_getArgsSignature(args, logArgs)}) @_AsyncMemo miss (${_since(started)})`);
83
- }
84
- }
85
79
  };
86
80
  descriptor.value.dropCache = async () => {
87
81
  logger.log(`${methodSignature} @_AsyncMemo.dropCache()`);
@@ -1,5 +1,4 @@
1
- import { _since } from '../time/time.util';
2
- import { _getArgsSignature, _getMethodSignature, _getTargetMethodSignature } from './decorator.util';
1
+ import { _getTargetMethodSignature } from './decorator.util';
3
2
  import { jsonMemoSerializer, MapMemoCache } from './memo.util';
4
3
  /**
5
4
  * Memoizes the method of the class, so it caches the output and returns the cached version if the "key"
@@ -34,7 +33,7 @@ export const _Memo = (opt = {}) => (target, key, descriptor) => {
34
33
  // UPD: tests show that normal Map also doesn't leak (to be tested further)
35
34
  // Normal Map is needed to allow .dropCache()
36
35
  const cache = new Map();
37
- const { logHit = false, logMiss = false, logArgs = true, logger = console, cacheFactory = () => new MapMemoCache(), cacheKeyFn = jsonMemoSerializer, cacheErrors = true, } = opt;
36
+ const { logger = console, cacheFactory = () => new MapMemoCache(), cacheKeyFn = jsonMemoSerializer, cacheErrors = true, } = opt;
38
37
  const keyStr = String(key);
39
38
  const methodSignature = _getTargetMethodSignature(target, keyStr);
40
39
  descriptor.value = function (...args) {
@@ -45,16 +44,12 @@ export const _Memo = (opt = {}) => (target, key, descriptor) => {
45
44
  cache.set(ctx, cacheFactory());
46
45
  }
47
46
  else if (cache.get(ctx).has(cacheKey)) {
48
- if (logHit) {
49
- logger.log(`${_getMethodSignature(ctx, keyStr)}(${_getArgsSignature(args, logArgs)}) @_Memo hit`);
50
- }
51
47
  value = cache.get(ctx).get(cacheKey);
52
48
  if (value instanceof Error) {
53
49
  throw value;
54
50
  }
55
51
  return value;
56
52
  }
57
- const started = Date.now();
58
53
  try {
59
54
  value = originalFn.apply(ctx, args);
60
55
  try {
@@ -76,11 +71,6 @@ export const _Memo = (opt = {}) => (target, key, descriptor) => {
76
71
  }
77
72
  throw err;
78
73
  }
79
- finally {
80
- if (logMiss) {
81
- logger.log(`${_getMethodSignature(ctx, keyStr)}(${_getArgsSignature(args, logArgs)}) @_Memo miss (${_since(started)})`);
82
- }
83
- }
84
74
  };
85
75
  descriptor.value.dropCache = () => {
86
76
  logger.log(`${methodSignature} @_Memo.dropCache()`);
@@ -1,5 +1,3 @@
1
- import { _since } from '../time/time.util';
2
- import { _getArgsSignature } from './decorator.util';
3
1
  import { jsonMemoSerializer, MapMemoCache } from './memo.util';
4
2
  /**
5
3
  * Only supports Sync functions.
@@ -7,24 +5,19 @@ import { jsonMemoSerializer, MapMemoCache } from './memo.util';
7
5
  * Technically, you can use it with Async functions, but it'll return the Promise without awaiting it.
8
6
  */
9
7
  export function _memoFn(fn, opt = {}) {
10
- const { logHit = false, logMiss = false, logArgs = true, logger = console, cacheErrors = true, cacheFactory = () => new MapMemoCache(), cacheKeyFn = jsonMemoSerializer, } = opt;
8
+ const { logger = console, cacheErrors = true, cacheFactory = () => new MapMemoCache(), cacheKeyFn = jsonMemoSerializer, } = opt;
11
9
  const cache = cacheFactory();
12
- const fnName = fn.name;
13
10
  const memoizedFn = function (...args) {
14
11
  const ctx = this;
15
12
  const cacheKey = cacheKeyFn(args);
16
13
  let value;
17
14
  if (cache.has(cacheKey)) {
18
- if (logHit) {
19
- logger.log(`${fnName}(${_getArgsSignature(args, logArgs)}) memoFn hit`);
20
- }
21
15
  value = cache.get(cacheKey);
22
16
  if (value instanceof Error) {
23
17
  throw value;
24
18
  }
25
19
  return value;
26
20
  }
27
- const started = Date.now();
28
21
  try {
29
22
  value = fn.apply(ctx, args);
30
23
  try {
@@ -46,11 +39,6 @@ export function _memoFn(fn, opt = {}) {
46
39
  }
47
40
  throw err;
48
41
  }
49
- finally {
50
- if (logMiss) {
51
- logger.log(`${fnName}(${_getArgsSignature(args, logArgs)}) memoFn miss (${_since(started)})`);
52
- }
53
- }
54
42
  };
55
43
  Object.assign(memoizedFn, { cache });
56
44
  return memoizedFn;
@@ -1,14 +1,11 @@
1
- import { _since } from '../time/time.util';
2
- import { _getArgsSignature } from './decorator.util';
3
1
  import { jsonMemoSerializer, MapMemoCache } from './memo.util';
4
2
  /**
5
3
  * Only supports Sync functions.
6
4
  * To support Async functions - use _memoFnAsync
7
5
  */
8
6
  export function _memoFnAsync(fn, opt = {}) {
9
- const { logHit = false, logMiss = false, logArgs = true, logger = console, cacheRejections = true, cacheFactory = () => new MapMemoCache(), cacheKeyFn = jsonMemoSerializer, } = opt;
7
+ const { logger = console, cacheRejections = true, cacheFactory = () => new MapMemoCache(), cacheKeyFn = jsonMemoSerializer, } = opt;
10
8
  const cache = cacheFactory();
11
- const fnName = fn.name;
12
9
  const memoizedFn = async function (...args) {
13
10
  const ctx = this;
14
11
  const cacheKey = cacheKeyFn(args);
@@ -20,15 +17,11 @@ export function _memoFnAsync(fn, opt = {}) {
20
17
  logger.error(err);
21
18
  }
22
19
  if (value !== undefined) {
23
- if (logHit) {
24
- logger.log(`${fnName}(${_getArgsSignature(args, logArgs)}) memoFnAsync hit`);
25
- }
26
20
  if (value instanceof Error) {
27
21
  throw value;
28
22
  }
29
23
  return value;
30
24
  }
31
- const started = Date.now();
32
25
  try {
33
26
  value = await fn.apply(ctx, args);
34
27
  void (async () => {
@@ -54,11 +47,6 @@ export function _memoFnAsync(fn, opt = {}) {
54
47
  }
55
48
  throw err;
56
49
  }
57
- finally {
58
- if (logMiss) {
59
- logger.log(`${fnName}(${_getArgsSignature(args, logArgs)}) memoFnAsync miss (${_since(started)})`);
60
- }
61
- }
62
50
  };
63
51
  Object.assign(memoizedFn, { cache });
64
52
  return memoizedFn;
@@ -3,7 +3,7 @@
3
3
  // http://decodize.com/blog/2012/08/27/javascript-memoization-caching-results-for-better-performance/
4
4
  // http://inlehmansterms.net/2015/03/01/javascript-memoization/
5
5
  // https://community.risingstack.com/the-worlds-fastest-javascript-memoization-library/
6
- import { _getArgsSignature, _getTargetMethodSignature } from './decorator.util';
6
+ import { _getTargetMethodSignature } from './decorator.util';
7
7
  import { jsonMemoSerializer, MapMemoCache } from './memo.util';
8
8
  // memoSimple decorator is NOT exported. Only used in benchmarks currently
9
9
  /**
@@ -34,23 +34,16 @@ export const memoSimple = (opt = {}) => (target, key, descriptor) => {
34
34
  }
35
35
  */
36
36
  const cache = new MapMemoCache();
37
- const { logHit, logMiss, logArgs = true, logger = console } = opt;
37
+ const { logger = console } = opt;
38
38
  const keyStr = String(key);
39
39
  const methodSignature = _getTargetMethodSignature(target, keyStr);
40
40
  descriptor.value = function (...args) {
41
41
  const ctx = this;
42
42
  const cacheKey = jsonMemoSerializer(args);
43
43
  if (cache.has(cacheKey)) {
44
- if (logHit) {
45
- logger.log(`${methodSignature}(${_getArgsSignature(args, logArgs)}) @memo hit`);
46
- }
47
44
  return cache.get(cacheKey);
48
45
  }
49
- const d = Date.now();
50
46
  const res = originalFn.apply(ctx, args);
51
- if (logMiss) {
52
- logger.log(`${methodSignature}(${_getArgsSignature(args, logArgs)}) @memo miss (${Date.now() - d} ms)`);
53
- }
54
47
  cache.set(cacheKey, res);
55
48
  return res;
56
49
  };
package/dist-esm/index.js CHANGED
@@ -35,6 +35,8 @@ export * from './object/deepEquals';
35
35
  export * from './object/object.util';
36
36
  export * from './object/sortObject';
37
37
  export * from './object/sortObjectDeep';
38
+ export * from './object/map2';
39
+ export * from './object/set2';
38
40
  export * from './promise/pDefer';
39
41
  export * from './promise/pDelay';
40
42
  export * from './promise/pFilter';
@@ -1,70 +1,163 @@
1
- const isArray = Array.isArray;
2
- const keyList = Object.keys;
3
- const hasProp = Object.prototype.hasOwnProperty;
1
+ // Heavily inspired by https://github.com/epoberezkin/fast-deep-equal
4
2
  /**
5
- * deepEquals, but after JSON stringify/parse
6
- * E.g if object A has extra properties with value `undefined` -
7
- * it won't be _deepEquals true, but will be _deepJsonEquals true.
8
- * (because JSON.stringify removes undefined properties).
3
+ Returns true if a and b are deeply equal.
4
+
5
+ Equality is checked recursively, with the following rules/caveats:
6
+ - Primitive values are checked with ===
7
+ - NaN === NaN
8
+ - Array length should be the same, and every value should be equal
9
+ - Sets are checked similarly to arrays (but order doesn't matter in Sets)
10
+ - Objects and Maps are checked that all values match. Undefined values are treated the same as absent key (important!)
11
+ - Order of object/Map keys doesn't matter, unlike when comparing JSON.stringify(a) === JSON.stringify(b)
12
+ - Regex are compared by their source and flags
13
+ - Functions are compared by their `.toString`
14
+ - Any object that overrides `.toString()` is compared by that (e.g Function)
15
+ - Any object that overrides `.valueOf()` is compared by that (e.g Date)
16
+
17
+ What are the differences between various deep-equality functions?
18
+ There are:
19
+ - _deepEquals
20
+ - _deepJsonEquals
21
+ - _jsonEquals
22
+
23
+ _deepEquals uses "common sense" equality.
24
+ It tries to work "as you would expect it to".
25
+ With the important caveat that undefined values are treated the same as absent key.
26
+ So, _deepEquals should be the first choice.
27
+ It's also the most performant of 3.
28
+
29
+ _deepJsonEquals uses different logic, that's often not what you expect.
30
+ It should be used to compare objects of how they would look after "passing via JSON.stringify",
31
+ for example when you return it over the API to the Frontend,
32
+ or when you pass it to be saved to the Database.
33
+ If some object has custom .toJSON() implementation - it'll invoke that (similar to JSON.stringify).
34
+ For these cases - it can be better than _deepEquals.
35
+ And it's better than _jsonEquals, because it doesn't fail/depend on object key order.
36
+
37
+ _jsonEquals is simply JSON.stringify(a) === JSON.stringify(b).
38
+ It's the simplest implementation, but also the slowest of 3.
39
+
40
+ TLDR: _deepEquals should be useful in most of the cases, start there.
9
41
  */
10
- export function _deepJsonEquals(a, b) {
42
+ export function _deepEquals(a, b) {
11
43
  if (a === b)
12
44
  return true;
13
- const aj = JSON.stringify(a);
14
- const bj = JSON.stringify(b);
15
- if (aj === bj)
45
+ if (Number.isNaN(a)) {
46
+ return Number.isNaN(b);
47
+ }
48
+ if (a && b && typeof a === 'object' && typeof b === 'object') {
49
+ if (a.constructor !== b.constructor)
50
+ return false;
51
+ if (Array.isArray(a)) {
52
+ const length = a.length;
53
+ if (length !== b.length)
54
+ return false;
55
+ for (let i = length; i-- !== 0;) {
56
+ if (!_deepEquals(a[i], b[i]))
57
+ return false;
58
+ }
59
+ return true;
60
+ }
61
+ if (a instanceof Map && b instanceof Map) {
62
+ for (const key of new Set([...a.keys(), ...b.keys()])) {
63
+ if (!_deepEquals(a.get(key), b.get(key)))
64
+ return false;
65
+ }
66
+ return true;
67
+ }
68
+ if (a instanceof Set && b instanceof Set) {
69
+ if (a.size !== b.size)
70
+ return false;
71
+ for (const key of a) {
72
+ if (!b.has(key))
73
+ return false;
74
+ }
75
+ return true;
76
+ }
77
+ if (a.constructor === RegExp)
78
+ return a.source === b.source && a.flags === b.flags;
79
+ if (a.valueOf !== Object.prototype.valueOf)
80
+ return a.valueOf() === b.valueOf();
81
+ if (a.toString !== Object.prototype.toString)
82
+ return a.toString() === b.toString();
83
+ for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) {
84
+ if (!_deepEquals(a[key], b[key]))
85
+ return false;
86
+ }
16
87
  return true;
17
- return _deepEquals(JSON.parse(aj), JSON.parse(bj));
88
+ }
89
+ return a === b;
18
90
  }
19
91
  /**
20
- * Based on: https://github.com/epoberezkin/fast-deep-equal/
92
+ Returns true if a and b are deeply equal.
93
+
94
+ Equality is checked in the same way as if both arguments are processed via
95
+ JSON.stringify and JSON.parse:
96
+ - undefined values are removed, undefined values in an array are turned into `null`, etc.
97
+ - Any Regex, Map, Set, Function stringifies to {}.
98
+ - Date stringifies to its IsoDateTimeString representation.
99
+ - Any object that implements toJSON is compared by the output of its toJSON().
100
+ - NaN stringifies to null
101
+ - Order of object keys does not matter, unlike when comparing JSON.stringify(a) === JSON.stringify(b)
102
+
103
+ See _deepEquals docs for more details and comparison.
21
104
  */
22
- export function _deepEquals(a, b) {
105
+ export function _deepJsonEquals(a, b) {
23
106
  if (a === b)
24
107
  return true;
108
+ if (Number.isNaN(a)) {
109
+ a = null;
110
+ }
111
+ else if (typeof a === 'function') {
112
+ a = undefined;
113
+ }
114
+ else if (a && typeof a === 'object') {
115
+ if (a instanceof Date) {
116
+ a = a.valueOf();
117
+ }
118
+ else if ('toJSON' in a) {
119
+ a = a.toJSON();
120
+ }
121
+ }
122
+ if (Number.isNaN(b)) {
123
+ b = null;
124
+ }
125
+ else if (typeof b === 'function') {
126
+ b = undefined;
127
+ }
128
+ else if (b && typeof b === 'object') {
129
+ if (b instanceof Date) {
130
+ b = b.valueOf();
131
+ }
132
+ else if ('toJSON' in b) {
133
+ b = b.toJSON();
134
+ }
135
+ }
25
136
  if (a && b && typeof a === 'object' && typeof b === 'object') {
26
- const arrA = isArray(a);
27
- const arrB = isArray(b);
28
- let i;
29
- let length;
30
- let key;
31
- if (arrA !== arrB)
32
- return false;
33
- if (arrA && arrB) {
34
- length = a.length;
137
+ if (Array.isArray(a)) {
138
+ const length = a.length;
35
139
  if (length !== b.length)
36
140
  return false;
37
- for (i = length; i-- !== 0;)
38
- if (!_deepEquals(a[i], b[i]))
141
+ for (let i = length; i-- !== 0;) {
142
+ if (!_deepJsonEquals(a[i], b[i]))
39
143
  return false;
144
+ }
40
145
  return true;
41
146
  }
42
- const dateA = a instanceof Date;
43
- const dateB = b instanceof Date;
44
- if (dateA !== dateB)
45
- return false;
46
- if (dateA && dateB)
47
- return a.getTime() === b.getTime();
48
- const regexpA = a instanceof RegExp;
49
- const regexpB = b instanceof RegExp;
50
- if (regexpA !== regexpB)
51
- return false;
52
- if (regexpA && regexpB)
53
- return a.toString() === b.toString();
54
- const keys = keyList(a);
55
- length = keys.length;
56
- if (length !== keyList(b).length)
57
- return false;
58
- for (i = length; i-- !== 0;)
59
- if (!hasProp.call(b, keys[i]))
60
- return false;
61
- for (i = length; i-- !== 0;) {
62
- key = keys[i];
63
- if (!_deepEquals(a[key], b[key]))
147
+ for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) {
148
+ if (!_deepJsonEquals(a[key], b[key]))
64
149
  return false;
65
150
  }
66
151
  return true;
67
152
  }
68
- // eslint-disable-next-line no-self-compare
69
- return a !== a && b !== b;
153
+ return a === b;
154
+ }
155
+ /**
156
+ * Shortcut for JSON.stringify(a) === JSON.stringify(b)
157
+ *
158
+ * Simplest "deep equals" implementation, but also the slowest,
159
+ * and not robust, in the sense that it depends on the order of object keys.
160
+ */
161
+ export function _jsonEquals(a, b) {
162
+ return JSON.stringify(a) === JSON.stringify(b);
70
163
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Like Map, but serializes to JSON as an object.
3
+ *
4
+ * Fixes the "issue" of stock Map being json-serialized as `{}`.
5
+ *
6
+ * @experimental
7
+ */
8
+ export class Map2 extends Map {
9
+ /**
10
+ * Convenience way to create Map2 from object.
11
+ */
12
+ static of(obj) {
13
+ return new Map2(Object.entries(obj));
14
+ }
15
+ toObject() {
16
+ return Object.fromEntries(this);
17
+ }
18
+ toJSON() {
19
+ return Object.fromEntries(this);
20
+ }
21
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Like Set, but serializes to JSON as an array.
3
+ *
4
+ * Fixes the "issue" of stock Set being json-serialized as `{}`.
5
+ *
6
+ * @experimental
7
+ */
8
+ export class Set2 extends Set {
9
+ toArray() {
10
+ return [...this];
11
+ }
12
+ toJSON() {
13
+ return [...this];
14
+ }
15
+ }
@@ -107,6 +107,12 @@ export function _stringify(obj, opt = {}) {
107
107
  //
108
108
  // Other
109
109
  //
110
+ if (obj instanceof Map) {
111
+ obj = Object.fromEntries(obj);
112
+ }
113
+ else if (obj instanceof Set) {
114
+ obj = [...obj];
115
+ }
110
116
  try {
111
117
  const { stringifyFn = globalStringifyFunction } = opt;
112
118
  s = stringifyFn(obj, undefined, 2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.213.0",
3
+ "version": "14.215.0",
4
4
  "scripts": {
5
5
  "prepare": "husky",
6
6
  "build-prod": "build-prod-esm-cjs",
@@ -25,9 +25,6 @@
25
25
  "vitepress": "^1.0.0",
26
26
  "vue": "^3.2.45"
27
27
  },
28
- "resolutions": {
29
- "expect-type": "0.15.0"
30
- },
31
28
  "files": [
32
29
  "dist",
33
30
  "dist-esm",
@@ -196,6 +196,9 @@ export function _sortDescBy<T>(items: T[], mapper: Mapper<T, any>, mutate = fals
196
196
 
197
197
  /**
198
198
  * Like items.find(), but it tries to find from the END of the array.
199
+ *
200
+ * Node 18+ supports native array.findLast() - use that.
201
+ * iOS Safari only has it since 15.4
199
202
  */
200
203
  export function _findLast<T>(items: T[], predicate: Predicate<T>): T | undefined {
201
204
  return [...items].reverse().find(predicate)