@dxos/util 0.8.4-main.fbb7a13 → 0.8.4-main.fcfe5033a5
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/dist/lib/browser/index.mjs +123 -42
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +123 -42
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/circular-buffer.d.ts +1 -0
- package/dist/types/src/circular-buffer.d.ts.map +1 -1
- package/dist/types/src/complex.d.ts +15 -0
- package/dist/types/src/complex.d.ts.map +1 -1
- package/dist/types/src/composite-key.d.ts +10 -0
- package/dist/types/src/composite-key.d.ts.map +1 -0
- package/dist/types/src/composite-key.test.d.ts +2 -0
- package/dist/types/src/composite-key.test.d.ts.map +1 -0
- package/dist/types/src/deep.d.ts +0 -3
- package/dist/types/src/deep.d.ts.map +1 -1
- package/dist/types/src/di-key.d.ts +7 -5
- package/dist/types/src/di-key.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/platform.d.ts.map +1 -1
- package/dist/types/src/safe-parse.d.ts +1 -0
- package/dist/types/src/safe-parse.d.ts.map +1 -1
- package/dist/types/src/safe-stringify.d.ts +1 -1
- package/dist/types/src/safe-stringify.d.ts.map +1 -1
- package/dist/types/src/safe-stringify.test.d.ts +2 -0
- package/dist/types/src/safe-stringify.test.d.ts.map +1 -0
- package/dist/types/src/to-fallback.d.ts +18 -4
- package/dist/types/src/to-fallback.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +1 -1
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/weak.d.ts +15 -0
- package/dist/types/src/weak.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -10
- package/src/circular-buffer.test.ts +26 -0
- package/src/circular-buffer.ts +5 -0
- package/src/complex.ts +32 -0
- package/src/composite-key.test.ts +31 -0
- package/src/composite-key.ts +16 -0
- package/src/deep.ts +0 -6
- package/src/di-key.ts +10 -8
- package/src/index.ts +1 -0
- package/src/platform.ts +28 -6
- package/src/safe-parse.ts +8 -0
- package/src/safe-stringify.test.ts +96 -0
- package/src/safe-stringify.ts +15 -8
- package/src/to-fallback.ts +39 -156
- package/src/types.ts +1 -1
- package/src/weak.ts +52 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/util",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.fcfe5033a5",
|
|
4
4
|
"description": "Temporary bucket for misc functions, which should graduate into separate packages.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -32,17 +32,13 @@
|
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@hazae41/symbol-dispose-polyfill": "^1.0.2",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"@dxos/
|
|
38
|
-
"@dxos/
|
|
39
|
-
"@dxos/keys": "0.8.4-main.fbb7a13",
|
|
40
|
-
"@dxos/node-std": "0.8.4-main.fbb7a13"
|
|
35
|
+
"@dxos/debug": "0.8.4-main.fcfe5033a5",
|
|
36
|
+
"@dxos/invariant": "0.8.4-main.fcfe5033a5",
|
|
37
|
+
"@dxos/keys": "0.8.4-main.fcfe5033a5",
|
|
38
|
+
"@dxos/node-std": "0.8.4-main.fcfe5033a5"
|
|
41
39
|
},
|
|
42
40
|
"devDependencies": {
|
|
43
|
-
"@
|
|
44
|
-
"@types/lodash.set": "^4.3.9",
|
|
45
|
-
"@dxos/crypto": "0.8.4-main.fbb7a13"
|
|
41
|
+
"@dxos/crypto": "0.8.4-main.fcfe5033a5"
|
|
46
42
|
},
|
|
47
43
|
"publishConfig": {
|
|
48
44
|
"access": "public"
|
|
@@ -16,6 +16,32 @@ describe('CircularBuffer', () => {
|
|
|
16
16
|
}
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
+
test('clear resets buffer', () => {
|
|
20
|
+
const buffer = new CircularBuffer<number>(5);
|
|
21
|
+
buffer.push(1);
|
|
22
|
+
buffer.push(2);
|
|
23
|
+
buffer.push(3);
|
|
24
|
+
expect(buffer.elementCount).toBe(3);
|
|
25
|
+
|
|
26
|
+
buffer.clear();
|
|
27
|
+
expect(buffer.elementCount).toBe(0);
|
|
28
|
+
expect([...buffer]).toStrictEqual([]);
|
|
29
|
+
expect(buffer.getLast()).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('clear allows reuse', () => {
|
|
33
|
+
const buffer = new CircularBuffer<number>(3);
|
|
34
|
+
buffer.push(10);
|
|
35
|
+
buffer.push(20);
|
|
36
|
+
buffer.clear();
|
|
37
|
+
|
|
38
|
+
buffer.push(30);
|
|
39
|
+
buffer.push(40);
|
|
40
|
+
expect(buffer.elementCount).toBe(2);
|
|
41
|
+
expect([...buffer]).toStrictEqual([30, 40]);
|
|
42
|
+
expect(buffer.getLast()).toBe(40);
|
|
43
|
+
});
|
|
44
|
+
|
|
19
45
|
test('full cycle', () => {
|
|
20
46
|
const maxSize = 10;
|
|
21
47
|
const regularArray: number[] = [];
|
package/src/circular-buffer.ts
CHANGED
package/src/complex.ts
CHANGED
|
@@ -214,6 +214,38 @@ export class ComplexMap<K, V> implements Map<K, V> {
|
|
|
214
214
|
return this;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Returns the value for the given key if present, otherwise inserts and returns the default value.
|
|
219
|
+
* @param key - The key to look up or insert.
|
|
220
|
+
* @param defaultValue - The value to insert if the key is not present.
|
|
221
|
+
* @returns The existing or newly inserted value.
|
|
222
|
+
*/
|
|
223
|
+
getOrInsert(key: K, defaultValue: V): V {
|
|
224
|
+
const primitive = this._keyProjection(key);
|
|
225
|
+
if (this._values.has(primitive)) {
|
|
226
|
+
return this._values.get(primitive)!;
|
|
227
|
+
}
|
|
228
|
+
this.set(key, defaultValue);
|
|
229
|
+
return defaultValue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Returns the value for the given key if present, otherwise computes, inserts, and returns a new value.
|
|
234
|
+
* The callback is only invoked when the key is missing.
|
|
235
|
+
* @param key - The key to look up or insert.
|
|
236
|
+
* @param callbackfn - Function to compute the value if the key is not present.
|
|
237
|
+
* @returns The existing or newly computed value.
|
|
238
|
+
*/
|
|
239
|
+
getOrInsertComputed(key: K, callbackfn: (key: K) => V): V {
|
|
240
|
+
const primitive = this._keyProjection(key);
|
|
241
|
+
if (this._values.has(primitive)) {
|
|
242
|
+
return this._values.get(primitive)!;
|
|
243
|
+
}
|
|
244
|
+
const value = callbackfn(key);
|
|
245
|
+
this.set(key, value);
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
|
|
217
249
|
get size(): number {
|
|
218
250
|
return this._keys.size;
|
|
219
251
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { compositeKey, splitCompositeKey } from './composite-key';
|
|
8
|
+
|
|
9
|
+
describe('compositeKey', () => {
|
|
10
|
+
test('joins parts with colon', ({ expect }) => {
|
|
11
|
+
expect(compositeKey('a', 'b', 'c')).to.eq('a:b:c');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('single part returns as-is', ({ expect }) => {
|
|
15
|
+
expect(compositeKey('only')).to.eq('only');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('handles empty strings in parts', ({ expect }) => {
|
|
19
|
+
expect(compositeKey('', 'b')).to.eq(':b');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('splitCompositeKey', () => {
|
|
24
|
+
test('splits on colon', ({ expect }) => {
|
|
25
|
+
expect(splitCompositeKey('a:b:c')).to.deep.eq(['a', 'b', 'c']);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('single part returns array of one', ({ expect }) => {
|
|
29
|
+
expect(splitCompositeKey('only')).to.deep.eq(['only']);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
const SEPARATOR = ':';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build a composite key from string parts joined by a colon separator.
|
|
9
|
+
* Parts must not contain the separator character.
|
|
10
|
+
*/
|
|
11
|
+
export const compositeKey = (...parts: string[]): string => parts.join(SEPARATOR);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Split a composite key back into its parts.
|
|
15
|
+
*/
|
|
16
|
+
export const splitCompositeKey = (key: string): string[] => key.split(SEPARATOR);
|
package/src/deep.ts
CHANGED
|
@@ -2,14 +2,8 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import get from 'lodash.get';
|
|
6
|
-
import set from 'lodash.set';
|
|
7
|
-
|
|
8
5
|
import { invariant } from '@dxos/invariant';
|
|
9
6
|
|
|
10
|
-
// TODO(burdon): Re-export most common utils? (isEqual, defaultsDeep, merge, omit, pick, etc.)
|
|
11
|
-
export { get, set };
|
|
12
|
-
|
|
13
7
|
/**
|
|
14
8
|
* Initialize a deeply nested object.
|
|
15
9
|
* @returns The value of the prop after assignment.
|
package/src/di-key.ts
CHANGED
|
@@ -47,7 +47,7 @@ export type DiKey<T> =
|
|
|
47
47
|
}
|
|
48
48
|
| SymbolDiKey<T>;
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
class DiKeyConstructor {
|
|
51
51
|
/**
|
|
52
52
|
* Needed to ensure referential equality of combined keys.
|
|
53
53
|
*
|
|
@@ -57,7 +57,7 @@ export const DiKey = new (class DiKeyConstructor {
|
|
|
57
57
|
*/
|
|
58
58
|
// TODO(dmaretskyi): Disable private members lowering for dev env.
|
|
59
59
|
// TODO(dmaretskyi): Could be a weak map after a NodeJS upgrade.
|
|
60
|
-
|
|
60
|
+
private _combinedRegistry = new WeakMap();
|
|
61
61
|
|
|
62
62
|
define<T>(name: string): DiKey<T> {
|
|
63
63
|
return new SymbolDiKey(name);
|
|
@@ -88,24 +88,26 @@ export const DiKey = new (class DiKeyConstructor {
|
|
|
88
88
|
* Maintains referential equality: `DiKey.combine(A, B) === DiKey.combine(A, B)`
|
|
89
89
|
*/
|
|
90
90
|
combine(...ids: DiKey<any>[]) {
|
|
91
|
-
const map = this
|
|
92
|
-
return defaultMap(this
|
|
91
|
+
const map = this._lookupCombined(this._combinedRegistry, ids);
|
|
92
|
+
return defaultMap(this._combinedRegistry, map, () => this._combinedDescription(ids));
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
getSingletonFactory<T>(id: DiKey<T>): SingletonFactory<T> | undefined {
|
|
96
96
|
return (id as any)[symbolSingleton];
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
private _lookupCombined(map: WeakMap<any, any>, [first, ...rest]: DiKey<any>[]): WeakMap<any, any> {
|
|
100
100
|
const value = defaultMap(map, first, () => new Map());
|
|
101
101
|
if (rest.length > 0) {
|
|
102
|
-
return this
|
|
102
|
+
return this._lookupCombined(value, rest);
|
|
103
103
|
} else {
|
|
104
104
|
return value;
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
private _combinedDescription([first, ...rest]: DiKey<any>[]) {
|
|
109
109
|
return `${this.stringify(first)}<${rest.map(this.stringify).join(', ')}>`;
|
|
110
110
|
}
|
|
111
|
-
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const DiKey = new DiKeyConstructor();
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from './chunk-array';
|
|
|
14
14
|
export * from './circular-buffer';
|
|
15
15
|
export * from './clear-undefined';
|
|
16
16
|
export * from './complex';
|
|
17
|
+
export * from './composite-key';
|
|
17
18
|
export * from './deep';
|
|
18
19
|
export * from './defer-function';
|
|
19
20
|
export * from './defer';
|
package/src/platform.ts
CHANGED
|
@@ -13,14 +13,36 @@ export const isTauri = () => !!(globalThis as any).__TAURI__;
|
|
|
13
13
|
|
|
14
14
|
// From https://stackoverflow.com/a/11381730/2804332.
|
|
15
15
|
export const isMobile = () => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
let check = false;
|
|
17
|
+
(function (a) {
|
|
18
|
+
if (
|
|
19
|
+
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
|
|
20
|
+
a,
|
|
21
|
+
) ||
|
|
22
|
+
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
|
|
23
|
+
a.substr(0, 4),
|
|
24
|
+
)
|
|
25
|
+
) {
|
|
26
|
+
check = true;
|
|
27
|
+
}
|
|
28
|
+
})(navigator.userAgent || navigator.vendor || (window as any).opera);
|
|
29
|
+
return check;
|
|
30
|
+
};
|
|
20
31
|
|
|
21
32
|
export const isMobileOrTablet = () => {
|
|
22
33
|
let check = false;
|
|
23
|
-
((a) => {
|
|
34
|
+
((a) => {
|
|
35
|
+
if (
|
|
36
|
+
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(
|
|
37
|
+
a,
|
|
38
|
+
) ||
|
|
39
|
+
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
|
|
40
|
+
a.substr(0, 4),
|
|
41
|
+
)
|
|
42
|
+
) {
|
|
43
|
+
check = true;
|
|
44
|
+
}
|
|
45
|
+
})(navigator.userAgent || navigator.vendor || (window as any).opera);
|
|
24
46
|
return check;
|
|
25
47
|
};
|
|
26
48
|
|
|
@@ -44,7 +66,7 @@ export const safariCheck = () =>
|
|
|
44
66
|
*/
|
|
45
67
|
// From https://flaming.codes/posts/how-to-determine-os-in-browser.
|
|
46
68
|
export const getHostPlatform = () => {
|
|
47
|
-
if (!('navigator' in
|
|
69
|
+
if (!('navigator' in globalThis)) {
|
|
48
70
|
return 'unknown';
|
|
49
71
|
}
|
|
50
72
|
|
package/src/safe-parse.ts
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { SKIP, createReplacer, safeStringify } from './safe-stringify';
|
|
8
|
+
|
|
9
|
+
describe('safeStringify', () => {
|
|
10
|
+
test('stringifies plain nested objects', ({ expect }) => {
|
|
11
|
+
const data = { a: 1, b: { c: 'hello' } };
|
|
12
|
+
const result = safeStringify(data);
|
|
13
|
+
expect(result).to.include('"a": 1');
|
|
14
|
+
expect(result).to.include('"b"');
|
|
15
|
+
expect(result).to.include('"c": "hello"');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('handles circular references (Cycle story)', ({ expect }) => {
|
|
19
|
+
const cycle: Record<string, unknown> = { a: 1, b: [] };
|
|
20
|
+
(cycle.b as unknown[]).push(cycle);
|
|
21
|
+
|
|
22
|
+
const result = safeStringify(cycle);
|
|
23
|
+
expect(result).to.include('"a": 1');
|
|
24
|
+
expect(result).to.include('=> $]');
|
|
25
|
+
expect(result).to.match(/\[\$\.b\.\d+ => \$\]/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('handles multiple circular references', ({ expect }) => {
|
|
29
|
+
const obj: Record<string, unknown> = { x: 1 };
|
|
30
|
+
obj.self = obj;
|
|
31
|
+
|
|
32
|
+
const result = safeStringify(obj);
|
|
33
|
+
expect(result).to.include('"x": 1');
|
|
34
|
+
expect(result).to.include('[$.self => $]');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('filters functions', ({ expect }) => {
|
|
38
|
+
const data = { a: 1, fn: () => {} };
|
|
39
|
+
const result = safeStringify(data);
|
|
40
|
+
expect(result).to.include('"a": 1');
|
|
41
|
+
expect(result).not.to.include('fn');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('filters exotic objects', ({ expect }) => {
|
|
45
|
+
class Custom {}
|
|
46
|
+
const data = { a: 1, exotic: new Custom() };
|
|
47
|
+
const result = safeStringify(data);
|
|
48
|
+
expect(result).to.include('"a": 1');
|
|
49
|
+
expect(result).not.to.include('exotic');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('preserves arrays', ({ expect }) => {
|
|
53
|
+
const data = { arr: [1, 2, 3] };
|
|
54
|
+
const result = safeStringify(data);
|
|
55
|
+
expect(result).to.include('"arr"');
|
|
56
|
+
expect(result).to.include('1');
|
|
57
|
+
expect(result).to.include('2');
|
|
58
|
+
expect(result).to.include('3');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('createReplacer', () => {
|
|
63
|
+
test('maxArrayLen truncates long arrays (Large story)', ({ expect }) => {
|
|
64
|
+
const data = { arr: Array.from({ length: 15 }, (_, index) => index) };
|
|
65
|
+
const result = safeStringify(data, createReplacer({ maxArrayLen: 10 }));
|
|
66
|
+
expect(result).to.include('"[length: 15]"');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('maxStringLen truncates long strings (Large story)', ({ expect }) => {
|
|
70
|
+
const data = { str: 'a'.repeat(20) };
|
|
71
|
+
const result = safeStringify(data, createReplacer({ maxStringLen: 10 }));
|
|
72
|
+
expect(result).to.include('"aaaaaaaaaa..."');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('omit skips specified keys', ({ expect }) => {
|
|
76
|
+
const data = { a: 1, secret: 'hide', b: 2 };
|
|
77
|
+
const result = safeStringify(data, createReplacer({ omit: ['secret'] }));
|
|
78
|
+
expect(result).to.include('"a": 1');
|
|
79
|
+
expect(result).to.include('"b": 2');
|
|
80
|
+
expect(result).not.to.include('secret');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('parse parses JSON string values', ({ expect }) => {
|
|
84
|
+
const data = { nested: '{"x":1}' };
|
|
85
|
+
const result = safeStringify(data, createReplacer({ parse: ['nested'] }));
|
|
86
|
+
expect(result).to.include('"x": 1');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('SKIP return value removes key from output', ({ expect }) => {
|
|
90
|
+
const customReplacer = (_key: string, value: unknown) => (_key === 'skip' ? SKIP : value);
|
|
91
|
+
const data = { a: 1, skip: 'hidden' };
|
|
92
|
+
const result = safeStringify(data, customReplacer);
|
|
93
|
+
expect(result).to.include('"a": 1');
|
|
94
|
+
expect(result).not.to.include('skip');
|
|
95
|
+
});
|
|
96
|
+
});
|
package/src/safe-stringify.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type StringifyReplacer = (key: string, value: any) => typeof SKIP | any;
|
|
|
12
12
|
/**
|
|
13
13
|
* Safely stringifies an object.
|
|
14
14
|
*/
|
|
15
|
-
export function safeStringify(obj: any, filter: StringifyReplacer = defaultFilter, indent = 2) {
|
|
15
|
+
export function safeStringify(obj: any, filter: StringifyReplacer | undefined = defaultFilter, indent = 2) {
|
|
16
16
|
const seen = new WeakMap<object, string>();
|
|
17
17
|
|
|
18
18
|
// NOTE: Called for the root object with undefined key.
|
|
@@ -21,6 +21,9 @@ export function safeStringify(obj: any, filter: StringifyReplacer = defaultFilte
|
|
|
21
21
|
let path = key;
|
|
22
22
|
if (!key) {
|
|
23
23
|
path = '$';
|
|
24
|
+
if (value != null && typeof value === 'object') {
|
|
25
|
+
seen.set(value, path);
|
|
26
|
+
}
|
|
24
27
|
return value;
|
|
25
28
|
} else if (this) {
|
|
26
29
|
const parentPath = seen.get(this);
|
|
@@ -38,7 +41,7 @@ export function safeStringify(obj: any, filter: StringifyReplacer = defaultFilte
|
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
// Ignore exotic objects (non-plain objects like DOM elements, class instances, etc.)
|
|
41
|
-
if (typeof value === 'object' && Object.getPrototypeOf(value) !== Object.prototype) {
|
|
44
|
+
if (typeof value === 'object' && Object.getPrototypeOf(value) !== Object.prototype && !Array.isArray(value)) {
|
|
42
45
|
return undefined;
|
|
43
46
|
}
|
|
44
47
|
|
|
@@ -65,7 +68,11 @@ export function safeStringify(obj: any, filter: StringifyReplacer = defaultFilte
|
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
try {
|
|
72
|
+
return JSON.stringify(obj, replacer, indent);
|
|
73
|
+
} catch (error: any) {
|
|
74
|
+
return `ERROR: ${error.message}`;
|
|
75
|
+
}
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
export type CreateReplacerProps = {
|
|
@@ -105,6 +112,11 @@ export const createReplacer = ({
|
|
|
105
112
|
return SKIP;
|
|
106
113
|
}
|
|
107
114
|
|
|
115
|
+
// Show array length.
|
|
116
|
+
if (maxArrayLen != null && Array.isArray(value) && value.length > maxArrayLen) {
|
|
117
|
+
return `[length: ${value.length}]`;
|
|
118
|
+
}
|
|
119
|
+
|
|
108
120
|
// Store depth for this object.
|
|
109
121
|
if (value && typeof value === 'object') {
|
|
110
122
|
depthMap.set(value, currentDepth);
|
|
@@ -129,11 +141,6 @@ export const createReplacer = ({
|
|
|
129
141
|
}
|
|
130
142
|
}
|
|
131
143
|
|
|
132
|
-
// Show array length.
|
|
133
|
-
if (maxArrayLen != null && Array.isArray(value) && value.length > maxArrayLen) {
|
|
134
|
-
return `[length: ${value.length}]`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
144
|
// Truncate strings.
|
|
138
145
|
if (maxStringLen != null && typeof value === 'string' && value.length > maxStringLen) {
|
|
139
146
|
return value.slice(0, maxStringLen) + '...';
|