@dxos/util 0.8.4-main.f9ba587 → 0.8.4-main.fcc0d83b33

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 (142) hide show
  1. package/dist/lib/browser/index.mjs +569 -203
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +569 -203
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/array-to-hex.d.ts.map +1 -1
  8. package/dist/types/src/array.d.ts +3 -2
  9. package/dist/types/src/array.d.ts.map +1 -1
  10. package/dist/types/src/assume.d.ts.map +1 -1
  11. package/dist/types/src/binder.d.ts.map +1 -1
  12. package/dist/types/src/bitfield.d.ts.map +1 -1
  13. package/dist/types/src/browser-storage.d.ts +9 -0
  14. package/dist/types/src/browser-storage.d.ts.map +1 -0
  15. package/dist/types/src/callback-collection.d.ts.map +1 -1
  16. package/dist/types/src/callback.d.ts.map +1 -1
  17. package/dist/types/src/case.d.ts.map +1 -1
  18. package/dist/types/src/chunk-array.d.ts.map +1 -1
  19. package/dist/types/src/circular-buffer.d.ts +1 -0
  20. package/dist/types/src/circular-buffer.d.ts.map +1 -1
  21. package/dist/types/src/clear-undefined.d.ts.map +1 -1
  22. package/dist/types/src/complex.d.ts +15 -0
  23. package/dist/types/src/complex.d.ts.map +1 -1
  24. package/dist/types/src/composite-key.d.ts +10 -0
  25. package/dist/types/src/composite-key.d.ts.map +1 -0
  26. package/dist/types/src/composite-key.test.d.ts +2 -0
  27. package/dist/types/src/composite-key.test.d.ts.map +1 -0
  28. package/dist/types/src/deep.d.ts +0 -3
  29. package/dist/types/src/deep.d.ts.map +1 -1
  30. package/dist/types/src/defer-function.d.ts.map +1 -1
  31. package/dist/types/src/defer.d.ts +1 -1
  32. package/dist/types/src/defer.d.ts.map +1 -1
  33. package/dist/types/src/di-container.d.ts.map +1 -1
  34. package/dist/types/src/di-key.d.ts +7 -5
  35. package/dist/types/src/di-key.d.ts.map +1 -1
  36. package/dist/types/src/entry.d.ts.map +1 -1
  37. package/dist/types/src/error-format.d.ts +5 -0
  38. package/dist/types/src/error-format.d.ts.map +1 -0
  39. package/dist/types/src/filename.d.ts +9 -0
  40. package/dist/types/src/filename.d.ts.map +1 -0
  41. package/dist/types/src/for-each-async.d.ts.map +1 -1
  42. package/dist/types/src/human-hash.d.ts.map +1 -1
  43. package/dist/types/src/index.d.ts +7 -2
  44. package/dist/types/src/index.d.ts.map +1 -1
  45. package/dist/types/src/instance-id.d.ts.map +1 -1
  46. package/dist/types/src/interval.d.ts.map +1 -1
  47. package/dist/types/src/join-tables.d.ts.map +1 -1
  48. package/dist/types/src/json.d.ts +1 -1
  49. package/dist/types/src/json.d.ts.map +1 -1
  50. package/dist/types/src/map-values.d.ts.map +1 -1
  51. package/dist/types/src/map.d.ts.map +1 -1
  52. package/dist/types/src/object.d.ts +3 -0
  53. package/dist/types/src/object.d.ts.map +1 -1
  54. package/dist/types/src/order-keys.d.ts.map +1 -1
  55. package/dist/types/src/order.d.ts.map +1 -1
  56. package/dist/types/src/pick.d.ts.map +1 -1
  57. package/dist/types/src/platform.d.ts +5 -2
  58. package/dist/types/src/platform.d.ts.map +1 -1
  59. package/dist/types/src/position.d.ts +1 -1
  60. package/dist/types/src/position.d.ts.map +1 -1
  61. package/dist/types/src/random.d.ts.map +1 -1
  62. package/dist/types/src/range.d.ts.map +1 -1
  63. package/dist/types/src/reducers.d.ts.map +1 -1
  64. package/dist/types/src/remove-undefined-keys.d.ts.map +1 -1
  65. package/dist/types/src/retry.d.ts +32 -0
  66. package/dist/types/src/retry.d.ts.map +1 -0
  67. package/dist/types/src/safe-await.d.ts.map +1 -1
  68. package/dist/types/src/safe-instanceof.d.ts.map +1 -1
  69. package/dist/types/src/safe-parse.d.ts +7 -4
  70. package/dist/types/src/safe-parse.d.ts.map +1 -1
  71. package/dist/types/src/safe-stringify.d.ts +22 -0
  72. package/dist/types/src/safe-stringify.d.ts.map +1 -0
  73. package/dist/types/src/safe-stringify.test.d.ts +2 -0
  74. package/dist/types/src/safe-stringify.test.d.ts.map +1 -0
  75. package/dist/types/src/sliding-window-summary.d.ts.map +1 -1
  76. package/dist/types/src/sort.d.ts.map +1 -1
  77. package/dist/types/src/string.d.ts +8 -0
  78. package/dist/types/src/string.d.ts.map +1 -1
  79. package/dist/types/src/string.test.d.ts +2 -0
  80. package/dist/types/src/string.test.d.ts.map +1 -0
  81. package/dist/types/src/sum.d.ts.map +1 -1
  82. package/dist/types/src/throw-unhandled-error.d.ts.map +1 -1
  83. package/dist/types/src/to-fallback.d.ts +18 -4
  84. package/dist/types/src/to-fallback.d.ts.map +1 -1
  85. package/dist/types/src/tracer.d.ts.map +1 -1
  86. package/dist/types/src/tree.d.ts.map +1 -1
  87. package/dist/types/src/types.d.ts +24 -6
  88. package/dist/types/src/types.d.ts.map +1 -1
  89. package/dist/types/src/uint8array.d.ts.map +1 -1
  90. package/dist/types/src/unit.d.ts +14 -0
  91. package/dist/types/src/unit.d.ts.map +1 -0
  92. package/dist/types/src/unit.test.d.ts +2 -0
  93. package/dist/types/src/unit.test.d.ts.map +1 -0
  94. package/dist/types/src/url.d.ts.map +1 -1
  95. package/dist/types/src/weak.d.ts +15 -0
  96. package/dist/types/src/weak.d.ts.map +1 -1
  97. package/dist/types/tsconfig.tsbuildinfo +1 -1
  98. package/package.json +14 -16
  99. package/src/array.test.ts +1 -1
  100. package/src/array.ts +9 -4
  101. package/src/assume.ts +0 -1
  102. package/src/binder.ts +2 -2
  103. package/src/browser-storage.ts +60 -0
  104. package/src/circular-buffer.test.ts +27 -1
  105. package/src/circular-buffer.ts +5 -0
  106. package/src/complex.test.ts +1 -1
  107. package/src/complex.ts +32 -0
  108. package/src/composite-key.test.ts +31 -0
  109. package/src/composite-key.ts +16 -0
  110. package/src/deep.ts +2 -6
  111. package/src/defer.ts +1 -1
  112. package/src/di-key.ts +10 -8
  113. package/src/error-format.ts +24 -0
  114. package/src/filename.ts +16 -0
  115. package/src/human-hash.test.ts +1 -1
  116. package/src/index.ts +7 -2
  117. package/src/json.ts +2 -7
  118. package/src/object.ts +3 -0
  119. package/src/platform.ts +35 -3
  120. package/src/position.test.ts +2 -2
  121. package/src/retry.ts +74 -0
  122. package/src/safe-parse.ts +29 -15
  123. package/src/safe-stringify.test.ts +96 -0
  124. package/src/safe-stringify.ts +153 -0
  125. package/src/sort.test.ts +1 -1
  126. package/src/string.test.ts +19 -0
  127. package/src/string.ts +40 -0
  128. package/src/to-fallback.ts +44 -160
  129. package/src/tree.test.ts +1 -1
  130. package/src/types.test.ts +11 -1
  131. package/src/types.ts +40 -12
  132. package/src/uint8array.test.ts +1 -1
  133. package/src/unit.test.ts +25 -0
  134. package/src/unit.ts +87 -0
  135. package/src/weak.test.ts +1 -1
  136. package/src/weak.ts +52 -14
  137. package/dist/types/src/explicit-resource-management-polyfill.d.ts +0 -1
  138. package/dist/types/src/explicit-resource-management-polyfill.d.ts.map +0 -1
  139. package/dist/types/src/first-two-chars.d.ts +0 -9
  140. package/dist/types/src/first-two-chars.d.ts.map +0 -1
  141. package/src/explicit-resource-management-polyfill.ts +0 -13
  142. package/src/first-two-chars.ts +0 -44
package/package.json CHANGED
@@ -1,43 +1,41 @@
1
1
  {
2
2
  "name": "@dxos/util",
3
- "version": "0.8.4-main.f9ba587",
3
+ "version": "0.8.4-main.fcc0d83b33",
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": true,
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",
22
- "typesVersions": {
23
- "*": {}
24
- },
25
26
  "files": [
26
27
  "dist",
27
28
  "src"
28
29
  ],
29
30
  "dependencies": {
30
- "lodash.get": "^4.4.2",
31
- "lodash.set": "^4.3.2",
32
- "@dxos/debug": "0.8.4-main.f9ba587",
33
- "@dxos/invariant": "0.8.4-main.f9ba587",
34
- "@dxos/keys": "0.8.4-main.f9ba587",
35
- "@dxos/node-std": "0.8.4-main.f9ba587"
31
+ "@hazae41/symbol-dispose-polyfill": "^1.0.2",
32
+ "@dxos/invariant": "0.8.4-main.fcc0d83b33",
33
+ "@dxos/keys": "0.8.4-main.fcc0d83b33",
34
+ "@dxos/debug": "0.8.4-main.fcc0d83b33",
35
+ "@dxos/node-std": "0.8.4-main.fcc0d83b33"
36
36
  },
37
37
  "devDependencies": {
38
- "@types/lodash.get": "^4.4.9",
39
- "@types/lodash.set": "^4.3.9",
40
- "@dxos/crypto": "0.8.4-main.f9ba587"
38
+ "@dxos/crypto": "0.8.4-main.fcc0d83b33"
41
39
  },
42
40
  "publishConfig": {
43
41
  "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, intersection, distinctBy, partition } from './array';
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
@@ -16,7 +16,6 @@ export type DiffResult<A, B = A> = {
16
16
  * @param next
17
17
  * @param comparator
18
18
  */
19
- // TODO(burdon): Factor out.
20
19
  export const diff = <A, B = A>(
21
20
  previous: readonly A[],
22
21
  next: readonly B[],
@@ -43,15 +42,14 @@ export const diff = <A, B = A>(
43
42
  return result;
44
43
  };
45
44
 
46
- // TODO(burdon): Factor out.
47
- 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[] =>
48
46
  a.filter((a) => b.find((b) => comparator(a, b)) !== undefined);
49
47
 
50
48
  /**
51
49
  * Returns a new array with only the first instance of each unique item
52
50
  * based on a specified property.
53
51
  *
54
- * @typeParam T - The type of items in the input array.
52
+ * @typeProp T - The type of items in the input array.
55
53
  * @param array - The array to filter for distinct items.
56
54
  * @param key - The property key to determine uniqueness for each item.
57
55
  * @returns A new array with only distinct items based on the specified property.
@@ -132,3 +130,10 @@ export const intersectBy = <T, K>(arrays: T[][], selector: (item: T) => K): T[]
132
130
  return lookups.every((lookup) => lookup.has(key));
133
131
  });
134
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/assume.ts CHANGED
@@ -16,7 +16,6 @@
16
16
  * ```
17
17
  */
18
18
  // NOTE: Keep as `function` to avoid type inference issues.
19
- // eslint-disable-next-line @stayradiated/prefer-arrow-functions/prefer-arrow-functions
20
19
  export function assumeType<T>(value: unknown): asserts value is T {
21
20
  // No-op.
22
21
  }
package/src/binder.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import util from 'node:util';
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) => util.promisify(fn.bind(obj)),
13
+ async: (fn: Function) => promisify(fn.bind(obj)),
14
14
  });
@@ -0,0 +1,60 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ /** Delete all IndexedDB databases for this origin. */
6
+ export const clearIndexedDB = async (): Promise<void> => {
7
+ const dbs = await indexedDB.databases();
8
+ const results = await Promise.allSettled(
9
+ dbs
10
+ .filter((db) => db.name != null)
11
+ .map(
12
+ (db) =>
13
+ new Promise<void>((resolve, reject) => {
14
+ const request = indexedDB.deleteDatabase(db.name!);
15
+ request.onsuccess = () => resolve();
16
+ request.onerror = () => reject(request.error);
17
+ request.onblocked = () => reject(new Error(`IndexedDB deletion blocked: ${db.name}`));
18
+ }),
19
+ ),
20
+ );
21
+ const failures = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected');
22
+ if (failures.length > 0) {
23
+ throw new AggregateError(
24
+ failures.map((r) => r.reason),
25
+ 'Failed to delete some IndexedDB databases',
26
+ );
27
+ }
28
+ };
29
+
30
+ /** Remove all entries from the Origin Private File System. */
31
+ export const clearOPFS = async (): Promise<void> => {
32
+ const root = await navigator.storage.getDirectory();
33
+ const errors: unknown[] = [];
34
+ for await (const [name] of root.entries()) {
35
+ try {
36
+ await root.removeEntry(name, { recursive: true });
37
+ } catch (err) {
38
+ errors.push(err);
39
+ }
40
+ }
41
+ if (errors.length > 0) {
42
+ throw new AggregateError(errors, 'Failed to remove some OPFS entries');
43
+ }
44
+ };
45
+
46
+ /** Unregister all service workers for this origin. */
47
+ export const clearServiceWorkers = async (): Promise<void> => {
48
+ const regs = await navigator.serviceWorker.getRegistrations();
49
+ for (const reg of regs) {
50
+ await reg.unregister();
51
+ }
52
+ };
53
+
54
+ /** Delete all HTTP caches for this origin. */
55
+ export const clearCaches = async (): Promise<void> => {
56
+ const keys = await caches.keys();
57
+ for (const key of keys) {
58
+ await caches.delete(key);
59
+ }
60
+ };
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { describe, test, expect } from 'vitest';
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[] = [];
@@ -26,6 +26,11 @@ export class CircularBuffer<T> {
26
26
  return this._elementCount;
27
27
  }
28
28
 
29
+ public clear(): void {
30
+ this._nextIndex = 0;
31
+ this._elementCount = 0;
32
+ }
33
+
29
34
  public getLast(): T | undefined {
30
35
  if (this._elementCount === 0) {
31
36
  return undefined;
@@ -6,7 +6,7 @@ import { expect, test } from 'vitest';
6
6
 
7
7
  import { PublicKey } from '@dxos/keys';
8
8
 
9
- import { makeSet, makeMap } from './complex';
9
+ import { makeMap, makeSet } from './complex';
10
10
 
11
11
  const PulicKeySet = makeSet<PublicKey>(PublicKey.hash);
12
12
 
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.
@@ -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
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import './explicit-resource-management-polyfill';
5
+ import '@hazae41/symbol-dispose-polyfill';
6
6
 
7
7
  /**
8
8
  * Run function on scope exit. Go style.
package/src/di-key.ts CHANGED
@@ -47,7 +47,7 @@ export type DiKey<T> =
47
47
  }
48
48
  | SymbolDiKey<T>;
49
49
 
50
- export const DiKey = new (class DiKeyConstructor {
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
- #combinedRegistry = new WeakMap();
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.#lookupCombined(this.#combinedRegistry, ids);
92
- return defaultMap(this.#combinedRegistry, map, () => this.#combinedDescription(ids));
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
- #lookupCombined(map: WeakMap<any, any>, [first, ...rest]: DiKey<any>[]): WeakMap<any, any> {
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.#lookupCombined(value, rest);
102
+ return this._lookupCombined(value, rest);
103
103
  } else {
104
104
  return value;
105
105
  }
106
106
  }
107
107
 
108
- #combinedDescription([first, ...rest]: DiKey<any>[]) {
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();
@@ -0,0 +1,24 @@
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)) {
17
+ break;
18
+ }
19
+ current = current.cause;
20
+ level += 1;
21
+ }
22
+
23
+ return lines.join('\n\n');
24
+ };
@@ -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}` : '');
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { expect, test } from 'vitest';
6
6
 
7
- import { createKeyPair, createId } from '@dxos/crypto';
7
+ import { createId, createKeyPair } from '@dxos/crypto';
8
8
  import { PublicKey } from '@dxos/keys';
9
9
 
10
10
  import { humanize } from './human-hash';
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from './array';
7
7
  export * from './assume';
8
8
  export * from './binder';
9
9
  export * from './bitfield';
10
+ export * from './browser-storage';
10
11
  export * from './callback-collection';
11
12
  export * from './callback';
12
13
  export * from './case';
@@ -14,11 +15,13 @@ export * from './chunk-array';
14
15
  export * from './circular-buffer';
15
16
  export * from './clear-undefined';
16
17
  export * from './complex';
18
+ export * from './composite-key';
17
19
  export * from './deep';
18
20
  export * from './defer-function';
19
21
  export * from './defer';
20
22
  export * from './entry';
21
- export * from './first-two-chars';
23
+ export * from './error-format';
24
+ export * from './filename';
22
25
  export * from './for-each-async';
23
26
  export * from './human-hash';
24
27
  export * from './instance-id';
@@ -38,10 +41,11 @@ export * from './random';
38
41
  export * from './range';
39
42
  export * from './reducers';
40
43
  export * from './remove-undefined-keys';
44
+ export * from './retry';
41
45
  export * from './safe-await';
42
46
  export * from './safe-instanceof';
43
47
  export * from './safe-parse';
44
- export * from './sliding-window-summary';
48
+ export * from './safe-stringify';
45
49
  export * from './sliding-window-summary';
46
50
  export * from './sort';
47
51
  export * from './string';
@@ -52,5 +56,6 @@ export * from './tracer';
52
56
  export * from './tree';
53
57
  export * from './types';
54
58
  export * from './uint8array';
59
+ export * from './unit';
55
60
  export * from './url';
56
61
  export * from './weak';
package/src/json.ts CHANGED
@@ -17,7 +17,7 @@ const LOG_MAX_DEPTH = 7;
17
17
  /**
18
18
  * JSON.stringify replacer.
19
19
  */
20
- export function jsonReplacer(this: any, key: string, value: any): any {
20
+ export const jsonReplacer = (key: string, value: any): any => {
21
21
  // TODO(burdon): Why is this represented as `{ type: 'Buffer', data }`.
22
22
  if (value !== null && typeof value === 'object' && typeof value[inspect.custom] === 'function') {
23
23
  return value[inspect.custom]();
@@ -32,13 +32,8 @@ export function jsonReplacer(this: any, key: string, value: any): any {
32
32
  }
33
33
  }
34
34
 
35
- // TODO(burdon): Option.
36
- // code if (Array.isArray(value)) {
37
- // code return value.length;
38
- // code } else {
39
35
  return value;
40
- // code }
41
- }
36
+ };
42
37
 
43
38
  /**
44
39
  * Recursively converts an object into a JSON-compatible object.
package/src/object.ts CHANGED
@@ -2,6 +2,9 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ /**
6
+ * @deprecated Use `Struct.keys` from effect.
7
+ */
5
8
  export const keys = <R>(obj: R): (keyof R)[] => Object.keys(obj as any) as (keyof R)[];
6
9
 
7
10
  export const entries = <R>(obj: R): [keyof R, R[keyof R]][] => Object.entries(obj as any) as [keyof R, any][];
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 mobileAndTabletCheck = () => {
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) => { if (/(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(a) || /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(a.substr(0,4))) { check = true; } })(navigator.userAgent || navigator.vendor || (window as any).opera);
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 window)) {
69
+ if (!('navigator' in globalThis)) {
38
70
  return 'unknown';
39
71
  }
40
72
 
@@ -2,9 +2,9 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { describe, it, expect } from 'vitest';
5
+ import { describe, expect, it } from 'vitest';
6
6
 
7
- import { byPosition, type Position } from './position';
7
+ import { type Position, byPosition } from './position';
8
8
 
9
9
  type TestItem = {
10
10
  id: number;