@dxos/util 0.8.4-main.84f28bd → 0.8.4-main.8baae0fced

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 (153) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +637 -228
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +637 -228
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/array-to-hex.d.ts.map +1 -1
  9. package/dist/types/src/array.d.ts +3 -2
  10. package/dist/types/src/array.d.ts.map +1 -1
  11. package/dist/types/src/assume.d.ts.map +1 -1
  12. package/dist/types/src/binder.d.ts.map +1 -1
  13. package/dist/types/src/bitfield.d.ts.map +1 -1
  14. package/dist/types/src/browser-storage.d.ts +9 -0
  15. package/dist/types/src/browser-storage.d.ts.map +1 -0
  16. package/dist/types/src/callback-collection.d.ts.map +1 -1
  17. package/dist/types/src/callback.d.ts.map +1 -1
  18. package/dist/types/src/case.d.ts.map +1 -1
  19. package/dist/types/src/chunk-array.d.ts.map +1 -1
  20. package/dist/types/src/circular-buffer.d.ts +1 -0
  21. package/dist/types/src/circular-buffer.d.ts.map +1 -1
  22. package/dist/types/src/clear-undefined.d.ts.map +1 -1
  23. package/dist/types/src/complex.d.ts +15 -0
  24. package/dist/types/src/complex.d.ts.map +1 -1
  25. package/dist/types/src/composite-key.d.ts +10 -0
  26. package/dist/types/src/composite-key.d.ts.map +1 -0
  27. package/dist/types/src/composite-key.test.d.ts +2 -0
  28. package/dist/types/src/composite-key.test.d.ts.map +1 -0
  29. package/dist/types/src/deep.d.ts +0 -3
  30. package/dist/types/src/deep.d.ts.map +1 -1
  31. package/dist/types/src/defer-function.d.ts.map +1 -1
  32. package/dist/types/src/defer.d.ts +1 -1
  33. package/dist/types/src/defer.d.ts.map +1 -1
  34. package/dist/types/src/di-container.d.ts.map +1 -1
  35. package/dist/types/src/di-key.d.ts +7 -5
  36. package/dist/types/src/di-key.d.ts.map +1 -1
  37. package/dist/types/src/entry.d.ts.map +1 -1
  38. package/dist/types/src/error-format.d.ts +5 -0
  39. package/dist/types/src/error-format.d.ts.map +1 -0
  40. package/dist/types/src/filename.d.ts +9 -0
  41. package/dist/types/src/filename.d.ts.map +1 -0
  42. package/dist/types/src/for-each-async.d.ts.map +1 -1
  43. package/dist/types/src/human-hash.d.ts.map +1 -1
  44. package/dist/types/src/id.d.ts +30 -0
  45. package/dist/types/src/id.d.ts.map +1 -0
  46. package/dist/types/src/id.test.d.ts +2 -0
  47. package/dist/types/src/id.test.d.ts.map +1 -0
  48. package/dist/types/src/index.d.ts +8 -2
  49. package/dist/types/src/index.d.ts.map +1 -1
  50. package/dist/types/src/instance-id.d.ts.map +1 -1
  51. package/dist/types/src/interval.d.ts.map +1 -1
  52. package/dist/types/src/join-tables.d.ts.map +1 -1
  53. package/dist/types/src/json.d.ts +1 -1
  54. package/dist/types/src/json.d.ts.map +1 -1
  55. package/dist/types/src/map-values.d.ts.map +1 -1
  56. package/dist/types/src/map.d.ts.map +1 -1
  57. package/dist/types/src/object.d.ts +3 -0
  58. package/dist/types/src/object.d.ts.map +1 -1
  59. package/dist/types/src/order-keys.d.ts.map +1 -1
  60. package/dist/types/src/order.d.ts.map +1 -1
  61. package/dist/types/src/pick.d.ts.map +1 -1
  62. package/dist/types/src/platform.d.ts +5 -2
  63. package/dist/types/src/platform.d.ts.map +1 -1
  64. package/dist/types/src/position.d.ts +5 -5
  65. package/dist/types/src/position.d.ts.map +1 -1
  66. package/dist/types/src/random.d.ts.map +1 -1
  67. package/dist/types/src/range.d.ts.map +1 -1
  68. package/dist/types/src/reducers.d.ts.map +1 -1
  69. package/dist/types/src/remove-undefined-keys.d.ts.map +1 -1
  70. package/dist/types/src/retry.d.ts +32 -0
  71. package/dist/types/src/retry.d.ts.map +1 -0
  72. package/dist/types/src/safe-await.d.ts.map +1 -1
  73. package/dist/types/src/safe-instanceof.d.ts.map +1 -1
  74. package/dist/types/src/safe-parse.d.ts +7 -4
  75. package/dist/types/src/safe-parse.d.ts.map +1 -1
  76. package/dist/types/src/safe-stringify.d.ts +22 -0
  77. package/dist/types/src/safe-stringify.d.ts.map +1 -0
  78. package/dist/types/src/safe-stringify.test.d.ts +2 -0
  79. package/dist/types/src/safe-stringify.test.d.ts.map +1 -0
  80. package/dist/types/src/sliding-window-summary.d.ts.map +1 -1
  81. package/dist/types/src/sort.d.ts.map +1 -1
  82. package/dist/types/src/string.d.ts +8 -0
  83. package/dist/types/src/string.d.ts.map +1 -1
  84. package/dist/types/src/string.test.d.ts +2 -0
  85. package/dist/types/src/string.test.d.ts.map +1 -0
  86. package/dist/types/src/sum.d.ts.map +1 -1
  87. package/dist/types/src/throw-unhandled-error.d.ts.map +1 -1
  88. package/dist/types/src/to-fallback.d.ts +18 -4
  89. package/dist/types/src/to-fallback.d.ts.map +1 -1
  90. package/dist/types/src/tracer.d.ts.map +1 -1
  91. package/dist/types/src/tree.d.ts.map +1 -1
  92. package/dist/types/src/types.d.ts +24 -6
  93. package/dist/types/src/types.d.ts.map +1 -1
  94. package/dist/types/src/uint8array.d.ts +27 -0
  95. package/dist/types/src/uint8array.d.ts.map +1 -1
  96. package/dist/types/src/unit.d.ts +14 -0
  97. package/dist/types/src/unit.d.ts.map +1 -0
  98. package/dist/types/src/unit.test.d.ts +2 -0
  99. package/dist/types/src/unit.test.d.ts.map +1 -0
  100. package/dist/types/src/url.d.ts.map +1 -1
  101. package/dist/types/src/weak.d.ts +15 -0
  102. package/dist/types/src/weak.d.ts.map +1 -1
  103. package/dist/types/tsconfig.tsbuildinfo +1 -1
  104. package/package.json +15 -17
  105. package/src/array.test.ts +1 -1
  106. package/src/array.ts +9 -4
  107. package/src/assume.ts +0 -1
  108. package/src/binder.ts +2 -2
  109. package/src/browser-storage.ts +60 -0
  110. package/src/circular-buffer.test.ts +27 -1
  111. package/src/circular-buffer.ts +5 -0
  112. package/src/complex.test.ts +1 -1
  113. package/src/complex.ts +32 -0
  114. package/src/composite-key.test.ts +31 -0
  115. package/src/composite-key.ts +16 -0
  116. package/src/deep.ts +2 -6
  117. package/src/defer.ts +1 -1
  118. package/src/di-key.ts +10 -8
  119. package/src/error-format.ts +24 -0
  120. package/src/filename.ts +16 -0
  121. package/src/human-hash.test.ts +1 -1
  122. package/src/id.test.ts +109 -0
  123. package/src/id.ts +48 -0
  124. package/src/index.ts +8 -2
  125. package/src/json.ts +2 -7
  126. package/src/map-values.ts +6 -0
  127. package/src/object.ts +3 -0
  128. package/src/platform.ts +35 -3
  129. package/src/position.test.ts +39 -45
  130. package/src/position.ts +7 -11
  131. package/src/retry.ts +74 -0
  132. package/src/safe-parse.ts +29 -15
  133. package/src/safe-stringify.test.ts +96 -0
  134. package/src/safe-stringify.ts +153 -0
  135. package/src/sort.test.ts +1 -1
  136. package/src/string.test.ts +19 -0
  137. package/src/string.ts +40 -0
  138. package/src/to-fallback.ts +44 -160
  139. package/src/tree.test.ts +1 -1
  140. package/src/types.test.ts +11 -1
  141. package/src/types.ts +40 -12
  142. package/src/uint8array.test.ts +1 -1
  143. package/src/uint8array.ts +44 -0
  144. package/src/unit.test.ts +25 -0
  145. package/src/unit.ts +87 -0
  146. package/src/weak.test.ts +1 -1
  147. package/src/weak.ts +52 -14
  148. package/dist/types/src/explicit-resource-management-polyfill.d.ts +0 -1
  149. package/dist/types/src/explicit-resource-management-polyfill.d.ts.map +0 -1
  150. package/dist/types/src/first-two-chars.d.ts +0 -9
  151. package/dist/types/src/first-two-chars.d.ts.map +0 -1
  152. package/src/explicit-resource-management-polyfill.ts +0 -13
  153. package/src/first-two-chars.ts +0 -44
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,13 +15,16 @@ 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';
27
+ export * from './id';
24
28
  export * from './instance-id';
25
29
  export * from './interval';
26
30
  export * from './join-tables';
@@ -38,10 +42,11 @@ export * from './random';
38
42
  export * from './range';
39
43
  export * from './reducers';
40
44
  export * from './remove-undefined-keys';
45
+ export * from './retry';
41
46
  export * from './safe-await';
42
47
  export * from './safe-instanceof';
43
48
  export * from './safe-parse';
44
- export * from './sliding-window-summary';
49
+ export * from './safe-stringify';
45
50
  export * from './sliding-window-summary';
46
51
  export * from './sort';
47
52
  export * from './string';
@@ -52,5 +57,6 @@ export * from './tracer';
52
57
  export * from './tree';
53
58
  export * from './types';
54
59
  export * from './uint8array';
60
+ export * from './unit';
55
61
  export * from './url';
56
62
  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/map-values.ts CHANGED
@@ -52,6 +52,9 @@ class DeepMapper {
52
52
  res[i] = this._map(value[i], i);
53
53
  }
54
54
  return res;
55
+ } else if (ArrayBuffer.isView(value)) {
56
+ // Typed arrays (Uint8Array, etc.) are opaque leaf values, not POJOs to recurse into.
57
+ return value;
55
58
  } else if (value !== null && typeof value === 'object') {
56
59
  const res: any = {};
57
60
  this._cyclic.set(value, res);
@@ -112,6 +115,9 @@ class DeepMapperAsync {
112
115
  res[i] = await this._map(value[i], i);
113
116
  }
114
117
  return res;
118
+ } else if (ArrayBuffer.isView(value)) {
119
+ // Typed arrays (Uint8Array, etc.) are opaque leaf values, not POJOs to recurse into.
120
+ return value;
115
121
  } else if (value !== null && typeof value === 'object') {
116
122
  const res: any = {};
117
123
  this._cyclic.set(value, res);
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, test } 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;
@@ -12,14 +12,14 @@ type TestItem = {
12
12
  };
13
13
 
14
14
  describe('byPosition', () => {
15
- it('should keep items with same position in their original order', () => {
15
+ test('should keep items with same position in their original order', ({ expect }) => {
16
16
  const items: TestItem[] = [
17
- { id: 1, position: 'static' },
18
- { id: 2, position: 'static' },
19
- { id: 3, position: 'hoist' },
20
- { id: 4, position: 'hoist' },
21
- { id: 5, position: 'fallback' },
22
- { id: 6, position: 'fallback' },
17
+ { id: 1 },
18
+ { id: 2 },
19
+ { id: 3, position: 'first' },
20
+ { id: 4, position: 'first' },
21
+ { id: 5, position: 'last' },
22
+ { id: 6, position: 'last' },
23
23
  ];
24
24
 
25
25
  const sorted = [...items].sort(byPosition);
@@ -30,69 +30,63 @@ describe('byPosition', () => {
30
30
  expect(sorted.findIndex((item) => item.id === 5)).toBeLessThan(sorted.findIndex((item) => item.id === 6));
31
31
  });
32
32
 
33
- it('should place "hoist" items before "static" items', () => {
34
- const items: TestItem[] = [
35
- { id: 1, position: 'static' },
36
- { id: 2, position: 'hoist' },
37
- ];
33
+ test('should place "first" items before items in natural order', ({ expect }) => {
34
+ const items: TestItem[] = [{ id: 1 }, { id: 2, position: 'first' }];
38
35
 
39
36
  const sorted = [...items].sort(byPosition);
40
- expect(sorted[0].position).toBe('hoist');
41
- expect(sorted[1].position).toBe('static');
37
+ expect(sorted[0].position).toBe('first');
38
+ expect(sorted[1].position).toBeUndefined();
42
39
  });
43
40
 
44
- it('should place "fallback" items after "static" items', () => {
45
- const items: TestItem[] = [
46
- { id: 1, position: 'fallback' },
47
- { id: 2, position: 'static' },
48
- ];
41
+ test('should place "last" items after items in natural order', ({ expect }) => {
42
+ const items: TestItem[] = [{ id: 1, position: 'last' }, { id: 2 }];
49
43
 
50
44
  const sorted = [...items].sort(byPosition);
51
- expect(sorted[0].position).toBe('static');
52
- expect(sorted[1].position).toBe('fallback');
45
+ expect(sorted[0].position).toBeUndefined();
46
+ expect(sorted[1].position).toBe('last');
53
47
  });
54
48
 
55
- it('should treat items without position as "static"', () => {
56
- const items: TestItem[] = [{ id: 1 }, { id: 2, position: 'hoist' }, { id: 3, position: 'fallback' }];
49
+ test('should treat items without position as natural order', ({ expect }) => {
50
+ const items: TestItem[] = [{ id: 1 }, { id: 2, position: 'first' }, { id: 3, position: 'last' }];
57
51
 
58
52
  const sorted = [...items].sort(byPosition);
59
- expect(sorted[0].position).toBe('hoist');
53
+ expect(sorted[0].position).toBe('first');
60
54
  expect(sorted[1].position).toBeUndefined();
61
- expect(sorted[2].position).toBe('fallback');
55
+ expect(sorted[2].position).toBe('last');
62
56
  });
63
57
 
64
- it('should correctly sort mixed positions', () => {
58
+ test('should correctly sort mixed positions', ({ expect }) => {
65
59
  const items: TestItem[] = [
66
- { id: 1, position: 'fallback' },
67
- { id: 2, position: 'static' },
68
- { id: 3, position: 'hoist' },
69
- { id: 4 }, // implicit static
70
- { id: 5, position: 'hoist' },
71
- { id: 6, position: 'fallback' },
60
+ { id: 1, position: 'last' },
61
+ { id: 2 },
62
+ { id: 3, position: 'first' },
63
+ { id: 4 },
64
+ { id: 5, position: 'first' },
65
+ { id: 6, position: 'last' },
72
66
  ];
73
67
 
74
68
  const sorted = [...items].sort(byPosition);
75
69
 
76
- // All hoisted items should come first
77
- expect(sorted[0].position).toBe('hoist');
78
- expect(sorted[1].position).toBe('hoist');
70
+ // All "first" items should come first
71
+ expect(sorted[0].position).toBe('first');
72
+ expect(sorted[1].position).toBe('first');
79
73
 
80
- // Static items (including undefined) should be in the middle
81
- expect(sorted[2].position).toBe('static');
74
+ // Natural-order items (undefined) should be in the middle
75
+ expect(sorted[2].position).toBeUndefined();
82
76
  expect(sorted[3].position).toBeUndefined();
83
77
 
84
- // Fallback items should be last
85
- expect(sorted[4].position).toBe('fallback');
86
- expect(sorted[5].position).toBe('fallback');
78
+ // "last" items should be at the end
79
+ expect(sorted[4].position).toBe('last');
80
+ expect(sorted[5].position).toBe('last');
87
81
  });
88
82
 
89
- it('should handle empty arrays', () => {
83
+ test('should handle empty arrays', ({ expect }) => {
90
84
  const items: TestItem[] = [];
91
85
  expect(() => [...items].sort(byPosition)).not.toThrow();
92
86
  });
93
87
 
94
- it('should handle single item arrays', () => {
95
- const items: TestItem[] = [{ id: 1, position: 'static' }];
88
+ test('should handle single item arrays', ({ expect }) => {
89
+ const items: TestItem[] = [{ id: 1 }];
96
90
  expect(() => [...items].sort(byPosition)).not.toThrow();
97
91
  });
98
92
  });
package/src/position.ts CHANGED
@@ -6,25 +6,21 @@
6
6
 
7
7
  /**
8
8
  * Determines priority order:
9
- * - `static` - Remain in natural order.
10
- * - `hoist` - Placed before `static`.
11
- * - `fallback` - Placed after `static`.
9
+ * - `undefined` - Remain in natural order.
10
+ * - `first` - Placed before items in natural order.
11
+ * - `last` - Placed after items in natural order.
12
12
  */
13
- // TODO(wittjosiah): Change to 'static' | 'start' | 'end'.
14
- export type Position = 'static' | 'hoist' | 'fallback';
13
+ export type Position = 'first' | 'last';
15
14
 
16
15
  /**
17
16
  * Sorting function for sorting by position.
18
17
  */
19
- export const byPosition = <T extends { position?: Position }>(
20
- { position: a = 'static' }: T,
21
- { position: b = 'static' }: T,
22
- ) => {
18
+ export const byPosition = <T extends { position?: Position }>({ position: a }: T, { position: b }: T) => {
23
19
  if (a === b) {
24
20
  return 0;
25
- } else if (a === 'hoist' || b === 'fallback') {
21
+ } else if (a === 'first' || b === 'last') {
26
22
  return -1;
27
- } else if (b === 'hoist' || a === 'fallback') {
23
+ } else if (b === 'first' || a === 'last') {
28
24
  return 1;
29
25
  } else {
30
26
  return 0;
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,31 +2,45 @@
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
- } catch (err) {
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 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>(data: string | undefined | null, defaultValue?: T) => {
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 (err) {}
33
+ return JSON.parse(str);
34
+ } catch {}
30
35
  }
36
+
31
37
  return defaultValue;
32
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
+ });