@dxos/util 0.8.4-main.7ace549 → 0.8.4-main.937b3ca

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 (41) hide show
  1. package/dist/lib/browser/index.mjs +126 -33
  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 +126 -33
  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.d.ts +2 -1
  8. package/dist/types/src/array.d.ts.map +1 -1
  9. package/dist/types/src/deep.d.ts.map +1 -1
  10. package/dist/types/src/defer.d.ts +1 -1
  11. package/dist/types/src/defer.d.ts.map +1 -1
  12. package/dist/types/src/filename.d.ts +9 -0
  13. package/dist/types/src/filename.d.ts.map +1 -0
  14. package/dist/types/src/index.d.ts +2 -0
  15. package/dist/types/src/index.d.ts.map +1 -1
  16. package/dist/types/src/platform.d.ts +4 -1
  17. package/dist/types/src/platform.d.ts.map +1 -1
  18. package/dist/types/src/retry.d.ts +32 -0
  19. package/dist/types/src/retry.d.ts.map +1 -0
  20. package/dist/types/src/safe-parse.d.ts +6 -4
  21. package/dist/types/src/safe-parse.d.ts.map +1 -1
  22. package/dist/types/src/safe-stringify.d.ts +9 -0
  23. package/dist/types/src/safe-stringify.d.ts.map +1 -1
  24. package/dist/types/src/types.d.ts +18 -4
  25. package/dist/types/src/types.d.ts.map +1 -1
  26. package/dist/types/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +11 -6
  28. package/src/array.ts +8 -1
  29. package/src/deep.ts +2 -0
  30. package/src/defer.ts +1 -1
  31. package/src/filename.ts +16 -0
  32. package/src/index.ts +2 -0
  33. package/src/platform.ts +11 -1
  34. package/src/retry.ts +74 -0
  35. package/src/safe-parse.ts +19 -16
  36. package/src/safe-stringify.ts +65 -23
  37. package/src/types.test.ts +11 -1
  38. package/src/types.ts +29 -9
  39. package/dist/types/src/explicit-resource-management-polyfill.d.ts +0 -1
  40. package/dist/types/src/explicit-resource-management-polyfill.d.ts.map +0 -1
  41. package/src/explicit-resource-management-polyfill.ts +0 -13
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@dxos/util",
3
- "version": "0.8.4-main.7ace549",
3
+ "version": "0.8.4-main.937b3ca",
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
13
  "sideEffects": false,
@@ -27,17 +31,18 @@
27
31
  "src"
28
32
  ],
29
33
  "dependencies": {
34
+ "@hazae41/symbol-dispose-polyfill": "^1.0.2",
30
35
  "lodash.get": "^4.4.2",
31
36
  "lodash.set": "^4.3.2",
32
- "@dxos/debug": "0.8.4-main.7ace549",
33
- "@dxos/invariant": "0.8.4-main.7ace549",
34
- "@dxos/keys": "0.8.4-main.7ace549",
35
- "@dxos/node-std": "0.8.4-main.7ace549"
37
+ "@dxos/debug": "0.8.4-main.937b3ca",
38
+ "@dxos/invariant": "0.8.4-main.937b3ca",
39
+ "@dxos/node-std": "0.8.4-main.937b3ca",
40
+ "@dxos/keys": "0.8.4-main.937b3ca"
36
41
  },
37
42
  "devDependencies": {
38
43
  "@types/lodash.get": "^4.4.9",
39
44
  "@types/lodash.set": "^4.3.9",
40
- "@dxos/crypto": "0.8.4-main.7ace549"
45
+ "@dxos/crypto": "0.8.4-main.937b3ca"
41
46
  },
42
47
  "publishConfig": {
43
48
  "access": "public"
package/src/array.ts CHANGED
@@ -49,7 +49,7 @@ export const intersection = <A, B = A>(a: A[], b: B[], comparator: Comparator<A,
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
- * @typeParam T - The type of items in the input array.
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/deep.ts CHANGED
@@ -19,6 +19,8 @@ export const setDeep = <T>(obj: any, path: readonly (string | number)[], value:
19
19
  let parent = obj;
20
20
  for (const key of path.slice(0, -1)) {
21
21
  if (parent[key] === undefined) {
22
+ // TODO(wittjosiah): This logic is flawed. This shouldn't be used for initializing arrays.
23
+ // Prefer `Obj.setValue` for ECHO objects.
22
24
  const isArrayIndex = !isNaN(Number(key));
23
25
  parent[key] = isArrayIndex ? [] : {};
24
26
  }
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.
@@ -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/index.ts CHANGED
@@ -18,6 +18,7 @@ export * from './deep';
18
18
  export * from './defer-function';
19
19
  export * from './defer';
20
20
  export * from './entry';
21
+ export * from './filename';
21
22
  export * from './for-each-async';
22
23
  export * from './human-hash';
23
24
  export * from './instance-id';
@@ -55,3 +56,4 @@ export * from './unit';
55
56
  export * from './url';
56
57
  export * from './weak';
57
58
  export * from './error-format';
59
+ export * from './retry';
package/src/platform.ts CHANGED
@@ -5,10 +5,20 @@
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){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/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);
18
+ return check;
19
+ };
20
+
21
+ export const isMobileOrTablet = () => {
12
22
  let check = false;
13
23
  ((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);
14
24
  return check;
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,36 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export const safeParseInt = (value: string | undefined, defaultValue?: number) => {
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 n = parseInt(value ?? '');
8
- return isNaN(n) ? defaultValue : n;
9
+ const value = parseInt(str ?? '');
10
+ return isNaN(value) ? defaultValue : value;
9
11
  } catch {
10
12
  return defaultValue;
11
13
  }
12
- };
14
+ }
13
15
 
14
- export const safeParseFloat = (str: string, defaultValue?: number): number | undefined => {
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
- return parseFloat(str);
20
+ const value = parseFloat(str ?? '');
21
+ return isNaN(value) ? defaultValue : value;
17
22
  } catch {
18
- return defaultValue ?? 0;
23
+ return defaultValue;
19
24
  }
20
- };
25
+ }
21
26
 
22
27
  export const safeParseJson: {
23
- <T extends object>(data: string | undefined | null, defaultValue: T): T;
24
- <T extends object>(data: string | undefined | null): T | undefined;
25
- } = <T extends object = any>(data: string | undefined | null, defaultValue?: T): T | undefined => {
26
- if (data && data.length > 0) {
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(data);
29
- } catch {
30
- // no-op.
31
- }
33
+ return JSON.parse(str);
34
+ } catch {}
32
35
  }
33
36
 
34
37
  return defaultValue;
@@ -4,48 +4,82 @@
4
4
 
5
5
  export const SKIP = Object.freeze({});
6
6
 
7
+ /**
8
+ * JSON.stringify replacer function.
9
+ */
7
10
  export type StringifyReplacer = (key: string, value: any) => typeof SKIP | any;
8
11
 
12
+ /**
13
+ * Safely stringifies an object.
14
+ */
9
15
  export function safeStringify(obj: any, filter: StringifyReplacer = defaultFilter, indent = 2) {
10
- const seen = new WeakSet();
16
+ const seen = new WeakMap<object, string>();
17
+
18
+ // NOTE: Called for the root object with undefined key.
19
+ function replacer(this: any, key: string, value: any) {
20
+ try {
21
+ let path = key;
22
+ if (!key) {
23
+ path = '$';
24
+ return value;
25
+ } else if (this) {
26
+ const parentPath = seen.get(this);
27
+ path = parentPath ? `${parentPath}.${key}` : key;
28
+ }
11
29
 
12
- function replacer(key: string, value: any) {
13
- if (typeof value === 'object' && value !== null) {
14
- if (seen.has(value)) {
15
- return '[Circular]';
30
+ // Null or undefined.
31
+ if (value == null) {
32
+ return value;
16
33
  }
17
34
 
18
- seen.add(value);
19
- }
35
+ // Ignore functions.
36
+ if (typeof value === 'function') {
37
+ return undefined;
38
+ }
20
39
 
21
- if (filter) {
22
- const v2 = filter?.(key, value);
23
- if (v2 !== undefined) {
24
- return v2 === SKIP ? undefined : v2;
40
+ // Ignore exotic objects (non-plain objects like DOM elements, class instances, etc.)
41
+ if (typeof value === 'object' && Object.getPrototypeOf(value) !== Object.prototype) {
42
+ return undefined;
25
43
  }
26
- }
27
44
 
28
- return value;
29
- }
45
+ // Check cycles.
46
+ if (typeof value === 'object' && value !== null) {
47
+ const exists = seen.get(value);
48
+ if (exists) {
49
+ return `[${path} => ${exists}]`;
50
+ }
30
51
 
31
- let result = '';
32
- try {
33
- result = JSON.stringify(obj, replacer, indent);
34
- } catch (error: any) {
35
- result = `Error: ${error.message}`;
52
+ seen.set(value, path);
53
+ }
54
+
55
+ if (filter) {
56
+ const filteredValue = filter?.(key, value);
57
+ if (filteredValue !== undefined) {
58
+ return filteredValue === SKIP ? undefined : filteredValue;
59
+ }
60
+ }
61
+
62
+ return value;
63
+ } catch (error: any) {
64
+ return `ERROR: ${error.message}`;
65
+ }
36
66
  }
37
67
 
38
- return result;
68
+ return JSON.stringify(obj, replacer, indent);
39
69
  }
40
70
 
41
71
  export type CreateReplacerProps = {
42
72
  omit?: string[];
43
- parse?: string[];
73
+ parse?: string[]; // TODO(burdon): Parse JSON value.
44
74
  maxDepth?: number;
45
75
  maxArrayLen?: number;
46
76
  maxStringLen?: number;
47
77
  };
48
78
 
79
+ /**
80
+ * Construct JSON.stringify replacer.
81
+ */
82
+ // TODO(burdon): Change to composite effect.
49
83
  export const createReplacer = ({
50
84
  omit,
51
85
  parse,
@@ -59,14 +93,16 @@ export const createReplacer = ({
59
93
  return function (this: any, key: string, value: any) {
60
94
  // Track depth.
61
95
  if (key === '') {
96
+ // Root.
62
97
  currentDepth = 0;
63
98
  } else if (this && typeof this === 'object') {
64
99
  const parentDepth = depthMap.get(this) ?? 0;
65
100
  currentDepth = parentDepth + 1;
66
101
  }
67
102
 
103
+ // Skip functions.
68
104
  if (typeof value === 'function') {
69
- return undefined;
105
+ return SKIP;
70
106
  }
71
107
 
72
108
  // Store depth for this object.
@@ -81,8 +117,10 @@ export const createReplacer = ({
81
117
 
82
118
  // Apply other filters.
83
119
  if (omit?.includes(key)) {
84
- return undefined;
120
+ return SKIP;
85
121
  }
122
+
123
+ // Parse JSON values.
86
124
  if (parse?.includes(key) && typeof value === 'string') {
87
125
  try {
88
126
  return JSON.parse(value);
@@ -90,9 +128,13 @@ export const createReplacer = ({
90
128
  return value;
91
129
  }
92
130
  }
131
+
132
+ // Show array length.
93
133
  if (maxArrayLen != null && Array.isArray(value) && value.length > maxArrayLen) {
94
134
  return `[length: ${value.length}]`;
95
135
  }
136
+
137
+ // Truncate strings.
96
138
  if (maxStringLen != null && typeof value === 'string' && value.length > maxStringLen) {
97
139
  return value.slice(0, maxStringLen) + '...';
98
140
  }
package/src/types.test.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { describe, expect, test } from 'vitest';
6
6
 
7
- import { isNonNullable } from './types';
7
+ import { arrayMove, arraySwap, isNonNullable } from './types';
8
8
 
9
9
  describe('types', () => {
10
10
  test('filter', async () => {
@@ -12,4 +12,14 @@ describe('types', () => {
12
12
  const filtered: number[] = values.filter(isNonNullable);
13
13
  expect(filtered).to.deep.equal([1, 2, 3, 4]);
14
14
  });
15
+
16
+ test('arrayMove', () => {
17
+ expect(arrayMove([1, 2, 3, 4], 0, 2)).to.deep.equal([2, 3, 1, 4]);
18
+ expect(arrayMove([1, 2, 3, 4], 2, 0)).to.deep.equal([3, 1, 2, 4]);
19
+ });
20
+
21
+ test('arraySwap', () => {
22
+ expect(arraySwap([1, 2, 3, 4], 0, 2)).to.deep.equal([3, 2, 1, 4]);
23
+ expect(arraySwap([1, 2, 3, 4], 2, 0)).to.deep.equal([3, 2, 1, 4]);
24
+ });
15
25
  });
package/src/types.ts CHANGED
@@ -12,6 +12,11 @@ export type MaybePromise<T> = T | Promise<T>;
12
12
 
13
13
  export type GuardedType<T> = T extends (value: any) => value is infer R ? R : never;
14
14
 
15
+ /**
16
+ * Removes readonly modifiers from top-level properties of T.
17
+ * Also converts readonly arrays at the top level to mutable arrays.
18
+ * For nested properties, mutability depends on the schema definition.
19
+ */
15
20
  export type ToMutable<T> = T extends object
16
21
  ? { -readonly [K in keyof T]: T[K] extends readonly (infer U)[] ? U[] : T[K] }
17
22
  : T;
@@ -20,13 +25,18 @@ export type Intersection<Types extends readonly unknown[]> = Types extends [infe
20
25
  ? First & Intersection<Rest>
21
26
  : unknown;
22
27
 
23
- export type DeepReadonly<T> = {
24
- readonly [P in keyof T]: T[P] extends Record<string, any>
25
- ? DeepReadonly<T[P]>
26
- : T[P] extends Array<infer U>
27
- ? ReadonlyArray<DeepReadonly<U>>
28
- : T[P];
29
- };
28
+ /**
29
+ * Recursively makes all properties of T readonly.
30
+ * Primitives (including branded types like `string & { __brand: true }`) are returned as-is.
31
+ * Arrays become ReadonlyArrays with deeply readonly elements.
32
+ */
33
+ export type DeepReadonly<T> = T extends string | number | boolean | bigint | symbol | null | undefined
34
+ ? T // Primitives (including branded primitives) stay as-is.
35
+ : T extends Array<infer U>
36
+ ? ReadonlyArray<DeepReadonly<U>>
37
+ : T extends object
38
+ ? { readonly [P in keyof T]: DeepReadonly<T[P]> }
39
+ : T;
30
40
 
31
41
  export type DeepWriteable<T> = { -readonly [K in keyof T]: T[K] extends object ? DeepWriteable<T[K]> : T[K] };
32
42
 
@@ -112,9 +122,19 @@ export const sortKeys = <T extends object>(obj: T): T =>
112
122
  }, {} as T);
113
123
 
114
124
  /**
115
- * Swap position of element within array.
125
+ * Move element within array.
116
126
  */
117
- export const arrayMove = <T>(array: T[], from: number, to: number): Array<T> => {
127
+ export const arrayMove = <T>(array: T[], from: number, to: number): T[] => {
118
128
  array.splice(to < 0 ? array.length + to : to, 0, array.splice(from, 1)[0]);
119
129
  return array;
120
130
  };
131
+
132
+ /**
133
+ * Swap position of element within array.
134
+ */
135
+ export function arraySwap<T>(array: T[], from: number, to: number): T[] {
136
+ const current = array[from];
137
+ array[from] = array[to];
138
+ array[to] = current;
139
+ return array;
140
+ }
@@ -1 +0,0 @@
1
- //# sourceMappingURL=explicit-resource-management-polyfill.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"explicit-resource-management-polyfill.d.ts","sourceRoot":"","sources":["../../../src/explicit-resource-management-polyfill.ts"],"names":[],"mappings":""}
@@ -1,13 +0,0 @@
1
- //
2
- // Copyright 2023 DXOS.org
3
- //
4
-
5
- // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html
6
-
7
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
8
- // @ts-ignore
9
- Symbol.dispose ??= Symbol('Symbol.dispose');
10
-
11
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
12
- // @ts-ignore
13
- Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose');