@dxos/util 0.8.4-main.ead640a → 0.8.4-main.f466a3d56e

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 (128) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +642 -524
  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 +642 -524
  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/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/id.d.ts +30 -0
  44. package/dist/types/src/id.d.ts.map +1 -0
  45. package/dist/types/src/id.test.d.ts +2 -0
  46. package/dist/types/src/id.test.d.ts.map +1 -0
  47. package/dist/types/src/index.d.ts +7 -0
  48. package/dist/types/src/index.d.ts.map +1 -1
  49. package/dist/types/src/instance-id.d.ts.map +1 -1
  50. package/dist/types/src/interval.d.ts.map +1 -1
  51. package/dist/types/src/join-tables.d.ts.map +1 -1
  52. package/dist/types/src/json.d.ts.map +1 -1
  53. package/dist/types/src/map-values.d.ts.map +1 -1
  54. package/dist/types/src/map.d.ts.map +1 -1
  55. package/dist/types/src/object.d.ts.map +1 -1
  56. package/dist/types/src/order-keys.d.ts.map +1 -1
  57. package/dist/types/src/order.d.ts.map +1 -1
  58. package/dist/types/src/pick.d.ts.map +1 -1
  59. package/dist/types/src/platform.d.ts +5 -2
  60. package/dist/types/src/platform.d.ts.map +1 -1
  61. package/dist/types/src/position.d.ts +5 -5
  62. package/dist/types/src/position.d.ts.map +1 -1
  63. package/dist/types/src/random.d.ts.map +1 -1
  64. package/dist/types/src/range.d.ts.map +1 -1
  65. package/dist/types/src/reducers.d.ts.map +1 -1
  66. package/dist/types/src/remove-undefined-keys.d.ts.map +1 -1
  67. package/dist/types/src/retry.d.ts +32 -0
  68. package/dist/types/src/retry.d.ts.map +1 -0
  69. package/dist/types/src/safe-await.d.ts.map +1 -1
  70. package/dist/types/src/safe-instanceof.d.ts.map +1 -1
  71. package/dist/types/src/safe-parse.d.ts +7 -4
  72. package/dist/types/src/safe-parse.d.ts.map +1 -1
  73. package/dist/types/src/safe-stringify.d.ts +22 -0
  74. package/dist/types/src/safe-stringify.d.ts.map +1 -0
  75. package/dist/types/src/safe-stringify.test.d.ts +2 -0
  76. package/dist/types/src/safe-stringify.test.d.ts.map +1 -0
  77. package/dist/types/src/sliding-window-summary.d.ts.map +1 -1
  78. package/dist/types/src/sort.d.ts.map +1 -1
  79. package/dist/types/src/string.d.ts.map +1 -1
  80. package/dist/types/src/sum.d.ts.map +1 -1
  81. package/dist/types/src/throw-unhandled-error.d.ts.map +1 -1
  82. package/dist/types/src/to-fallback.d.ts +18 -4
  83. package/dist/types/src/to-fallback.d.ts.map +1 -1
  84. package/dist/types/src/tracer.d.ts.map +1 -1
  85. package/dist/types/src/tree.d.ts.map +1 -1
  86. package/dist/types/src/types.d.ts +23 -5
  87. package/dist/types/src/types.d.ts.map +1 -1
  88. package/dist/types/src/uint8array.d.ts.map +1 -1
  89. package/dist/types/src/unit.d.ts +12 -13
  90. package/dist/types/src/unit.d.ts.map +1 -1
  91. package/dist/types/src/url.d.ts.map +1 -1
  92. package/dist/types/src/weak.d.ts +15 -0
  93. package/dist/types/src/weak.d.ts.map +1 -1
  94. package/dist/types/tsconfig.tsbuildinfo +1 -1
  95. package/package.json +13 -15
  96. package/src/array.ts +9 -2
  97. package/src/binder.ts +2 -2
  98. package/src/browser-storage.ts +60 -0
  99. package/src/circular-buffer.test.ts +26 -0
  100. package/src/circular-buffer.ts +5 -0
  101. package/src/complex.ts +32 -0
  102. package/src/composite-key.test.ts +31 -0
  103. package/src/composite-key.ts +16 -0
  104. package/src/deep.ts +2 -6
  105. package/src/defer.ts +1 -1
  106. package/src/di-key.ts +10 -8
  107. package/src/error-format.ts +24 -0
  108. package/src/filename.ts +16 -0
  109. package/src/id.test.ts +109 -0
  110. package/src/id.ts +48 -0
  111. package/src/index.ts +7 -0
  112. package/src/platform.ts +35 -3
  113. package/src/position.test.ts +38 -44
  114. package/src/position.ts +7 -11
  115. package/src/retry.ts +74 -0
  116. package/src/safe-parse.ts +27 -16
  117. package/src/safe-stringify.test.ts +96 -0
  118. package/src/safe-stringify.ts +153 -0
  119. package/src/string.ts +6 -2
  120. package/src/to-fallback.ts +39 -156
  121. package/src/types.test.ts +11 -1
  122. package/src/types.ts +39 -11
  123. package/src/unit.test.ts +1 -1
  124. package/src/unit.ts +63 -28
  125. package/src/weak.ts +52 -14
  126. package/dist/types/src/explicit-resource-management-polyfill.d.ts +0 -1
  127. package/dist/types/src/explicit-resource-management-polyfill.d.ts.map +0 -1
  128. package/src/explicit-resource-management-polyfill.ts +0 -13
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,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { describe, expect, it } from 'vitest';
5
+ import { describe, test } from 'vitest';
6
6
 
7
7
  import { type Position, byPosition } from './position';
8
8
 
@@ -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,34 +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
+ 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;
35
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
+ });
@@ -0,0 +1,153 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export const SKIP = Object.freeze({});
6
+
7
+ /**
8
+ * JSON.stringify replacer function.
9
+ */
10
+ export type StringifyReplacer = (key: string, value: any) => typeof SKIP | any;
11
+
12
+ /**
13
+ * Safely stringifies an object.
14
+ */
15
+ export function safeStringify(obj: any, filter: StringifyReplacer | undefined = defaultFilter, indent = 2) {
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
+ if (value != null && typeof value === 'object') {
25
+ seen.set(value, path);
26
+ }
27
+ return value;
28
+ } else if (this) {
29
+ const parentPath = seen.get(this);
30
+ path = parentPath ? `${parentPath}.${key}` : key;
31
+ }
32
+
33
+ // Null or undefined.
34
+ if (value == null) {
35
+ return value;
36
+ }
37
+
38
+ // Ignore functions.
39
+ if (typeof value === 'function') {
40
+ return undefined;
41
+ }
42
+
43
+ // Ignore exotic objects (non-plain objects like DOM elements, class instances, etc.)
44
+ if (typeof value === 'object' && Object.getPrototypeOf(value) !== Object.prototype && !Array.isArray(value)) {
45
+ return undefined;
46
+ }
47
+
48
+ // Check cycles.
49
+ if (typeof value === 'object' && value !== null) {
50
+ const exists = seen.get(value);
51
+ if (exists) {
52
+ return `[${path} => ${exists}]`;
53
+ }
54
+
55
+ seen.set(value, path);
56
+ }
57
+
58
+ if (filter) {
59
+ const filteredValue = filter?.(key, value);
60
+ if (filteredValue !== undefined) {
61
+ return filteredValue === SKIP ? undefined : filteredValue;
62
+ }
63
+ }
64
+
65
+ return value;
66
+ } catch (error: any) {
67
+ return `ERROR: ${error.message}`;
68
+ }
69
+ }
70
+
71
+ try {
72
+ return JSON.stringify(obj, replacer, indent);
73
+ } catch (error: any) {
74
+ return `ERROR: ${error.message}`;
75
+ }
76
+ }
77
+
78
+ export type CreateReplacerProps = {
79
+ omit?: string[];
80
+ parse?: string[]; // TODO(burdon): Parse JSON value.
81
+ maxDepth?: number;
82
+ maxArrayLen?: number;
83
+ maxStringLen?: number;
84
+ };
85
+
86
+ /**
87
+ * Construct JSON.stringify replacer.
88
+ */
89
+ // TODO(burdon): Change to composite effect.
90
+ export const createReplacer = ({
91
+ omit,
92
+ parse,
93
+ maxDepth,
94
+ maxArrayLen,
95
+ maxStringLen,
96
+ }: CreateReplacerProps = {}): StringifyReplacer => {
97
+ let currentDepth = 0;
98
+ const depthMap = new WeakMap<object, number>();
99
+
100
+ return function (this: any, key: string, value: any) {
101
+ // Track depth.
102
+ if (key === '') {
103
+ // Root.
104
+ currentDepth = 0;
105
+ } else if (this && typeof this === 'object') {
106
+ const parentDepth = depthMap.get(this) ?? 0;
107
+ currentDepth = parentDepth + 1;
108
+ }
109
+
110
+ // Skip functions.
111
+ if (typeof value === 'function') {
112
+ return SKIP;
113
+ }
114
+
115
+ // Show array length.
116
+ if (maxArrayLen != null && Array.isArray(value) && value.length > maxArrayLen) {
117
+ return `[length: ${value.length}]`;
118
+ }
119
+
120
+ // Store depth for this object.
121
+ if (value && typeof value === 'object') {
122
+ depthMap.set(value, currentDepth);
123
+
124
+ // Check max depth.
125
+ if (maxDepth != null && currentDepth >= maxDepth) {
126
+ return Array.isArray(value) ? `[{ length: ${value.length} }]` : `{ keys: ${Object.keys(value).length} }`;
127
+ }
128
+ }
129
+
130
+ // Apply other filters.
131
+ if (omit?.includes(key)) {
132
+ return SKIP;
133
+ }
134
+
135
+ // Parse JSON values.
136
+ if (parse?.includes(key) && typeof value === 'string') {
137
+ try {
138
+ return JSON.parse(value);
139
+ } catch {
140
+ return value;
141
+ }
142
+ }
143
+
144
+ // Truncate strings.
145
+ if (maxStringLen != null && typeof value === 'string' && value.length > maxStringLen) {
146
+ return value.slice(0, maxStringLen) + '...';
147
+ }
148
+
149
+ return value;
150
+ };
151
+ };
152
+
153
+ export const defaultFilter: StringifyReplacer = createReplacer();
package/src/string.ts CHANGED
@@ -32,8 +32,12 @@ export function trim(strings: TemplateStringsArray, ...values: any[]) {
32
32
  // Split into lines and trim leading/trailing blank lines.
33
33
  const lines = raw.split('\n');
34
34
 
35
- while (lines.length && !lines[0].trim()) lines.shift();
36
- while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
35
+ while (lines.length && !lines[0].trim()) {
36
+ lines.shift();
37
+ }
38
+ while (lines.length && !lines[lines.length - 1].trim()) {
39
+ lines.pop();
40
+ }
37
41
 
38
42
  // Find smallest indent across all non-blank lines.
39
43
  const minIndent = Math.min(...lines.filter((l) => l.trim()).map((l) => l.match(/^[ \t]*/)?.[0].length ?? 0));