@dxos/util 0.8.4-main.b97322e → 0.8.4-main.bd9b33e6c8
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 +425 -140
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +425 -140
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/array.d.ts +3 -2
- package/dist/types/src/array.d.ts.map +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/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/defer.d.ts +1 -1
- package/dist/types/src/defer.d.ts.map +1 -1
- package/dist/types/src/di-key.d.ts +3 -3
- package/dist/types/src/di-key.d.ts.map +1 -1
- package/dist/types/src/error-format.d.ts +5 -0
- package/dist/types/src/error-format.d.ts.map +1 -0
- package/dist/types/src/filename.d.ts +9 -0
- package/dist/types/src/filename.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +6 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/platform.d.ts +4 -1
- package/dist/types/src/platform.d.ts.map +1 -1
- package/dist/types/src/retry.d.ts +32 -0
- package/dist/types/src/retry.d.ts.map +1 -0
- package/dist/types/src/safe-parse.d.ts +7 -4
- package/dist/types/src/safe-parse.d.ts.map +1 -1
- package/dist/types/src/safe-stringify.d.ts +22 -0
- package/dist/types/src/safe-stringify.d.ts.map +1 -0
- 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/string.d.ts +5 -1
- package/dist/types/src/string.d.ts.map +1 -1
- 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 +24 -6
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/unit.d.ts +14 -0
- package/dist/types/src/unit.d.ts.map +1 -0
- package/dist/types/src/unit.test.d.ts +2 -0
- package/dist/types/src/unit.test.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -13
- package/src/array.test.ts +1 -1
- package/src/array.ts +9 -2
- package/src/binder.ts +2 -2
- package/src/circular-buffer.test.ts +27 -1
- package/src/circular-buffer.ts +5 -0
- package/src/complex.test.ts +1 -1
- package/src/composite-key.test.ts +31 -0
- package/src/composite-key.ts +16 -0
- package/src/deep.ts +2 -6
- package/src/defer.ts +1 -1
- package/src/error-format.ts +22 -0
- package/src/filename.ts +16 -0
- package/src/human-hash.test.ts +1 -1
- package/src/index.ts +6 -1
- package/src/platform.ts +35 -3
- package/src/position.test.ts +2 -2
- package/src/retry.ts +74 -0
- package/src/safe-parse.ts +29 -17
- package/src/safe-stringify.test.ts +96 -0
- package/src/safe-stringify.ts +153 -0
- package/src/sort.test.ts +1 -1
- package/src/string.test.ts +1 -1
- package/src/string.ts +32 -6
- package/src/to-fallback.ts +44 -160
- package/src/tree.test.ts +1 -1
- package/src/types.test.ts +11 -1
- package/src/types.ts +40 -12
- package/src/uint8array.test.ts +1 -1
- package/src/unit.test.ts +25 -0
- package/src/unit.ts +83 -0
- package/src/weak.test.ts +1 -1
- package/dist/types/src/explicit-resource-management-polyfill.d.ts +0 -1
- package/dist/types/src/explicit-resource-management-polyfill.d.ts.map +0 -1
- package/dist/types/src/first-two-chars.d.ts +0 -9
- package/dist/types/src/first-two-chars.d.ts.map +0 -1
- package/src/explicit-resource-management-polyfill.ts +0 -13
- package/src/first-two-chars.ts +0 -44
package/package.json
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/util",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.bd9b33e6c8",
|
|
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",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
7
11
|
"license": "MIT",
|
|
8
12
|
"author": "DXOS.org",
|
|
9
|
-
"sideEffects":
|
|
13
|
+
"sideEffects": false,
|
|
10
14
|
"type": "module",
|
|
11
15
|
"exports": {
|
|
12
16
|
".": {
|
|
17
|
+
"types": "./dist/types/src/index.d.ts",
|
|
13
18
|
"browser": "./dist/lib/browser/index.mjs",
|
|
14
19
|
"node": {
|
|
15
20
|
"require": "./dist/lib/node/index.cjs",
|
|
16
21
|
"default": "./dist/lib/node-esm/index.mjs"
|
|
17
|
-
}
|
|
18
|
-
"types": "./dist/types/src/index.d.ts"
|
|
22
|
+
}
|
|
19
23
|
}
|
|
20
24
|
},
|
|
21
25
|
"types": "dist/types/src/index.d.ts",
|
|
@@ -27,17 +31,14 @@
|
|
|
27
31
|
"src"
|
|
28
32
|
],
|
|
29
33
|
"dependencies": {
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"@dxos/
|
|
33
|
-
"@dxos/
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/node-std": "0.8.4-main.b97322e"
|
|
34
|
+
"@hazae41/symbol-dispose-polyfill": "^1.0.2",
|
|
35
|
+
"@dxos/invariant": "0.8.4-main.bd9b33e6c8",
|
|
36
|
+
"@dxos/keys": "0.8.4-main.bd9b33e6c8",
|
|
37
|
+
"@dxos/debug": "0.8.4-main.bd9b33e6c8",
|
|
38
|
+
"@dxos/node-std": "0.8.4-main.bd9b33e6c8"
|
|
36
39
|
},
|
|
37
40
|
"devDependencies": {
|
|
38
|
-
"@
|
|
39
|
-
"@types/lodash.set": "^4.3.9",
|
|
40
|
-
"@dxos/crypto": "0.8.4-main.b97322e"
|
|
41
|
+
"@dxos/crypto": "0.8.4-main.bd9b33e6c8"
|
|
41
42
|
},
|
|
42
43
|
"publishConfig": {
|
|
43
44
|
"access": "public"
|
package/src/array.test.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from 'vitest';
|
|
6
6
|
|
|
7
|
-
import { diff,
|
|
7
|
+
import { diff, distinctBy, intersection, partition } from './array';
|
|
8
8
|
|
|
9
9
|
describe('diff', () => {
|
|
10
10
|
test('returns the difference between two sets', () => {
|
package/src/array.ts
CHANGED
|
@@ -42,14 +42,14 @@ export const diff = <A, B = A>(
|
|
|
42
42
|
return result;
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
export const intersection = <A, B = A>(a: A[], b: B[], comparator: Comparator<A, B>): A[] =>
|
|
45
|
+
export const intersection = <A, B = A>(a: readonly A[], b: readonly B[], comparator: Comparator<A, B>): A[] =>
|
|
46
46
|
a.filter((a) => b.find((b) => comparator(a, b)) !== undefined);
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Returns a new array with only the first instance of each unique item
|
|
50
50
|
* based on a specified property.
|
|
51
51
|
*
|
|
52
|
-
* @
|
|
52
|
+
* @typeProp T - The type of items in the input array.
|
|
53
53
|
* @param array - The array to filter for distinct items.
|
|
54
54
|
* @param key - The property key to determine uniqueness for each item.
|
|
55
55
|
* @returns A new array with only distinct items based on the specified property.
|
|
@@ -130,3 +130,10 @@ export const intersectBy = <T, K>(arrays: T[][], selector: (item: T) => K): T[]
|
|
|
130
130
|
return lookups.every((lookup) => lookup.has(key));
|
|
131
131
|
});
|
|
132
132
|
};
|
|
133
|
+
|
|
134
|
+
export const coerceArray = <T>(arr: T | T[] | undefined): T[] => {
|
|
135
|
+
if (arr === undefined) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
return Array.isArray(arr) ? arr : [arr];
|
|
139
|
+
};
|
package/src/binder.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Function binder replaces pify.
|
|
@@ -10,5 +10,5 @@ import util from 'node:util';
|
|
|
10
10
|
// TODO(burdon): Replace pify everywhere.
|
|
11
11
|
export const createBinder = (obj: any) => ({
|
|
12
12
|
fn: (fn: Function) => fn.bind(obj),
|
|
13
|
-
async: (fn: Function) =>
|
|
13
|
+
async: (fn: Function) => promisify(fn.bind(obj)),
|
|
14
14
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { describe,
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { CircularBuffer } from './circular-buffer';
|
|
8
8
|
|
|
@@ -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.test.ts
CHANGED
|
@@ -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.
|
|
@@ -19,6 +13,8 @@ export const setDeep = <T>(obj: any, path: readonly (string | number)[], value:
|
|
|
19
13
|
let parent = obj;
|
|
20
14
|
for (const key of path.slice(0, -1)) {
|
|
21
15
|
if (parent[key] === undefined) {
|
|
16
|
+
// TODO(wittjosiah): This logic is flawed. This shouldn't be used for initializing arrays.
|
|
17
|
+
// Prefer `Obj.setValue` for ECHO objects.
|
|
22
18
|
const isArrayIndex = !isNaN(Number(key));
|
|
23
19
|
parent[key] = isArrayIndex ? [] : {};
|
|
24
20
|
}
|
package/src/defer.ts
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats an error with its cause chain.
|
|
3
|
+
*/
|
|
4
|
+
//
|
|
5
|
+
// Copyright 2025 DXOS.org
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
export const formatErrorWithCauses = (error: Error): string => {
|
|
9
|
+
const lines: string[] = [];
|
|
10
|
+
let current: Error | undefined = error;
|
|
11
|
+
let level = 0;
|
|
12
|
+
|
|
13
|
+
while (current) {
|
|
14
|
+
const prefix = level === 0 ? '' : `Caused by: `;
|
|
15
|
+
lines.push(prefix + (current.stack ?? String(current)));
|
|
16
|
+
if (!(current.cause instanceof Error)) break;
|
|
17
|
+
current = current.cause;
|
|
18
|
+
level += 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return lines.join('\n\n');
|
|
22
|
+
};
|
package/src/filename.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a unique filename.
|
|
7
|
+
*/
|
|
8
|
+
export const createFilename = ({
|
|
9
|
+
parts = [],
|
|
10
|
+
ext,
|
|
11
|
+
date = new Date(),
|
|
12
|
+
}: {
|
|
13
|
+
parts?: string[];
|
|
14
|
+
ext?: string;
|
|
15
|
+
date?: Date;
|
|
16
|
+
}) => [date.toISOString().replace(/[:.]/g, '-'), ...parts].join('_') + (ext ? `.${ext}` : '');
|
package/src/human-hash.test.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -14,11 +14,12 @@ 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';
|
|
20
21
|
export * from './entry';
|
|
21
|
-
export * from './
|
|
22
|
+
export * from './filename';
|
|
22
23
|
export * from './for-each-async';
|
|
23
24
|
export * from './human-hash';
|
|
24
25
|
export * from './instance-id';
|
|
@@ -41,6 +42,7 @@ export * from './remove-undefined-keys';
|
|
|
41
42
|
export * from './safe-await';
|
|
42
43
|
export * from './safe-instanceof';
|
|
43
44
|
export * from './safe-parse';
|
|
45
|
+
export * from './safe-stringify';
|
|
44
46
|
export * from './sliding-window-summary';
|
|
45
47
|
export * from './sort';
|
|
46
48
|
export * from './string';
|
|
@@ -51,5 +53,8 @@ export * from './tracer';
|
|
|
51
53
|
export * from './tree';
|
|
52
54
|
export * from './types';
|
|
53
55
|
export * from './uint8array';
|
|
56
|
+
export * from './unit';
|
|
54
57
|
export * from './url';
|
|
55
58
|
export * from './weak';
|
|
59
|
+
export * from './error-format';
|
|
60
|
+
export * from './retry';
|
package/src/platform.ts
CHANGED
|
@@ -5,12 +5,44 @@
|
|
|
5
5
|
// NOTE: `!=` is required.
|
|
6
6
|
export const isNode = () => typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
7
7
|
|
|
8
|
+
export const isBun = () => (globalThis as any).Bun !== undefined;
|
|
9
|
+
|
|
10
|
+
export const isTauri = () => !!(globalThis as any).__TAURI__;
|
|
11
|
+
|
|
8
12
|
/* eslint-disable */
|
|
9
13
|
|
|
10
14
|
// From https://stackoverflow.com/a/11381730/2804332.
|
|
11
|
-
export const
|
|
15
|
+
export const isMobile = () => {
|
|
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
|
+
};
|
|
31
|
+
|
|
32
|
+
export const isMobileOrTablet = () => {
|
|
12
33
|
let check = false;
|
|
13
|
-
((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);
|
|
14
46
|
return check;
|
|
15
47
|
};
|
|
16
48
|
|
|
@@ -34,7 +66,7 @@ export const safariCheck = () =>
|
|
|
34
66
|
*/
|
|
35
67
|
// From https://flaming.codes/posts/how-to-determine-os-in-browser.
|
|
36
68
|
export const getHostPlatform = () => {
|
|
37
|
-
if (!('navigator' in
|
|
69
|
+
if (!('navigator' in globalThis)) {
|
|
38
70
|
return 'unknown';
|
|
39
71
|
}
|
|
40
72
|
|
package/src/position.test.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { describe,
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { type Position, byPosition } from './position';
|
|
8
8
|
|
|
9
9
|
type TestItem = {
|
|
10
10
|
id: number;
|
package/src/retry.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
export interface RetryOptions<T> {
|
|
6
|
+
/**
|
|
7
|
+
* @default 3
|
|
8
|
+
*/
|
|
9
|
+
count?: number;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @default 100
|
|
13
|
+
*/
|
|
14
|
+
delayMs?: number;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Factor to increase delay by.
|
|
18
|
+
* @default 2
|
|
19
|
+
*/
|
|
20
|
+
exponent?: number;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Retry if error matches the predicate.
|
|
24
|
+
* @default () => true
|
|
25
|
+
*/
|
|
26
|
+
retryOnError?: (error: unknown) => Promise<boolean>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Retry if value matches the predicate.
|
|
30
|
+
* @default () => false
|
|
31
|
+
*/
|
|
32
|
+
retryOnValue?: (value: T) => Promise<boolean>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_RETRY_OPTIONS: Required<RetryOptions<unknown>> = {
|
|
36
|
+
count: 3,
|
|
37
|
+
delayMs: 100,
|
|
38
|
+
exponent: 2,
|
|
39
|
+
retryOnError: async () => true,
|
|
40
|
+
retryOnValue: async () => false,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Retries the operation a number of times.
|
|
45
|
+
* @returns The result of the succesfull invocation
|
|
46
|
+
* @throws Last error if all retries failed
|
|
47
|
+
*/
|
|
48
|
+
export const retry = async <T>(options: RetryOptions<T>, cb: () => Promise<T>): Promise<T> => {
|
|
49
|
+
const fullOptions: Required<RetryOptions<T>> = { ...DEFAULT_RETRY_OPTIONS, ...options };
|
|
50
|
+
|
|
51
|
+
let numRetries = 0,
|
|
52
|
+
currentDelay = fullOptions.delayMs;
|
|
53
|
+
while (true) {
|
|
54
|
+
let result: T;
|
|
55
|
+
try {
|
|
56
|
+
result = await cb();
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (numRetries > fullOptions.count || !(await fullOptions.retryOnError(err))) {
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, currentDelay));
|
|
62
|
+
currentDelay *= fullOptions.exponent;
|
|
63
|
+
numRetries++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!(await fullOptions.retryOnValue(result))) {
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, currentDelay));
|
|
70
|
+
currentDelay *= fullOptions.exponent;
|
|
71
|
+
numRetries++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
};
|
package/src/safe-parse.ts
CHANGED
|
@@ -2,33 +2,45 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
export function safeParseInt(str: string | undefined, defaultValue: number): number;
|
|
6
|
+
export function safeParseInt(str: string | undefined): number | undefined;
|
|
7
|
+
export function safeParseInt(str: string | undefined, defaultValue?: number): number | undefined {
|
|
6
8
|
try {
|
|
7
|
-
const
|
|
8
|
-
return isNaN(
|
|
9
|
-
} catch
|
|
9
|
+
const value = parseInt(str ?? '');
|
|
10
|
+
return isNaN(value) ? defaultValue : value;
|
|
11
|
+
} catch {
|
|
10
12
|
return defaultValue;
|
|
11
13
|
}
|
|
12
|
-
}
|
|
14
|
+
}
|
|
13
15
|
|
|
14
|
-
export
|
|
16
|
+
export function safeParseFloat(str: string | undefined, defaultValue: number): number;
|
|
17
|
+
export function safeParseFloat(str: string | undefined): number | undefined;
|
|
18
|
+
export function safeParseFloat(str: string | undefined, defaultValue?: number): number | undefined {
|
|
15
19
|
try {
|
|
16
|
-
|
|
20
|
+
const value = parseFloat(str ?? '');
|
|
21
|
+
return isNaN(value) ? defaultValue : value;
|
|
17
22
|
} catch {
|
|
18
|
-
return defaultValue
|
|
23
|
+
return defaultValue;
|
|
19
24
|
}
|
|
20
|
-
}
|
|
25
|
+
}
|
|
21
26
|
|
|
22
27
|
export const safeParseJson: {
|
|
23
|
-
<T extends object>(
|
|
24
|
-
<T extends object>(
|
|
25
|
-
} = <T extends object>(
|
|
26
|
-
if (
|
|
28
|
+
<T extends object>(str: string | undefined | null, defaultValue: T): T;
|
|
29
|
+
<T extends object>(str: string | undefined | null): T | undefined;
|
|
30
|
+
} = <T extends object = any>(str: string | undefined | null, defaultValue?: T): T | undefined => {
|
|
31
|
+
if (str && str.length > 0) {
|
|
27
32
|
try {
|
|
28
|
-
return JSON.parse(
|
|
29
|
-
} catch
|
|
30
|
-
// no-op.
|
|
31
|
-
}
|
|
33
|
+
return JSON.parse(str);
|
|
34
|
+
} catch {}
|
|
32
35
|
}
|
|
36
|
+
|
|
33
37
|
return defaultValue;
|
|
34
38
|
};
|
|
39
|
+
|
|
40
|
+
export const safeUrl = (str: string | undefined | null): URL | undefined => {
|
|
41
|
+
try {
|
|
42
|
+
return new URL(str ?? '');
|
|
43
|
+
} catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
@@ -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
|
+
});
|