mutts 1.0.7 → 1.0.8

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 (132) hide show
  1. package/README.md +61 -23
  2. package/dist/async/browser.d.ts +2 -0
  3. package/dist/async/browser.d.ts.map +1 -0
  4. package/dist/async/index.d.ts +18 -0
  5. package/dist/async/index.d.ts.map +1 -0
  6. package/dist/async/node.d.ts +2 -0
  7. package/dist/async/node.d.ts.map +1 -0
  8. package/dist/{chunks/index-BFYK02LG.js → browser.cjs} +169 -60
  9. package/dist/browser.cjs.map +1 -0
  10. package/dist/browser.d.ts +1654 -1
  11. package/dist/browser.esm.js +260 -25
  12. package/dist/browser.esm.js.map +1 -1
  13. package/dist/chunks/async-browser-CA0jPWIi.cjs +304 -0
  14. package/dist/chunks/async-browser-CA0jPWIi.cjs.map +1 -0
  15. package/dist/chunks/async-core-UqHzvJ-S.cjs +25 -0
  16. package/dist/chunks/async-core-UqHzvJ-S.cjs.map +1 -0
  17. package/dist/chunks/async-node-BYHuGTni.cjs +103 -0
  18. package/dist/chunks/async-node-BYHuGTni.cjs.map +1 -0
  19. package/dist/chunks/{index-CNR6QRUl.esm.js → index-DhaOVusv.esm.js} +173 -53
  20. package/dist/chunks/index-DhaOVusv.esm.js.map +1 -0
  21. package/dist/decorator.d.ts +106 -0
  22. package/dist/decorator.d.ts.map +1 -0
  23. package/dist/destroyable.d.ts +87 -0
  24. package/dist/destroyable.d.ts.map +1 -0
  25. package/dist/devtools/devtool/devtools.d.ts +1 -0
  26. package/dist/devtools/devtool/devtools.d.ts.map +1 -0
  27. package/dist/devtools/devtool/panel.d.ts +2 -0
  28. package/dist/devtools/devtool/panel.d.ts.map +1 -0
  29. package/dist/entry-browser.d.ts +3 -0
  30. package/dist/entry-browser.d.ts.map +1 -0
  31. package/dist/entry-node.d.ts +3 -0
  32. package/dist/entry-node.d.ts.map +1 -0
  33. package/dist/eventful.d.ts +18 -0
  34. package/dist/eventful.d.ts.map +1 -0
  35. package/dist/index.d.ts +13 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/indexable.d.ts +243 -0
  38. package/dist/indexable.d.ts.map +1 -0
  39. package/dist/introspection.d.ts +27 -0
  40. package/dist/introspection.d.ts.map +1 -0
  41. package/dist/iterableWeak.d.ts +53 -0
  42. package/dist/iterableWeak.d.ts.map +1 -0
  43. package/dist/mixins.d.ts +25 -0
  44. package/dist/mixins.d.ts.map +1 -0
  45. package/dist/mutts.umd.js +1 -1
  46. package/dist/mutts.umd.js.map +1 -1
  47. package/dist/mutts.umd.min.js +1 -1
  48. package/dist/mutts.umd.min.js.map +1 -1
  49. package/dist/node.cjs +105 -0
  50. package/dist/node.cjs.map +1 -0
  51. package/dist/node.d.ts +1 -2
  52. package/dist/node.esm.js +91 -32
  53. package/dist/node.esm.js.map +1 -1
  54. package/dist/promiseChain.d.ts +20 -0
  55. package/dist/promiseChain.d.ts.map +1 -0
  56. package/dist/reactive/array.d.ts +49 -0
  57. package/dist/reactive/array.d.ts.map +1 -0
  58. package/dist/reactive/buffer.d.ts +44 -0
  59. package/dist/reactive/buffer.d.ts.map +1 -0
  60. package/dist/reactive/change.d.ts +29 -0
  61. package/dist/reactive/change.d.ts.map +1 -0
  62. package/dist/reactive/debug.d.ts +111 -0
  63. package/dist/reactive/debug.d.ts.map +1 -0
  64. package/dist/reactive/deep-touch.d.ts +28 -0
  65. package/dist/reactive/deep-touch.d.ts.map +1 -0
  66. package/dist/reactive/deep-watch-state.d.ts +25 -0
  67. package/dist/reactive/deep-watch-state.d.ts.map +1 -0
  68. package/dist/reactive/deep-watch.d.ts +19 -0
  69. package/dist/reactive/deep-watch.d.ts.map +1 -0
  70. package/dist/reactive/effect-context.d.ts +7 -0
  71. package/dist/reactive/effect-context.d.ts.map +1 -0
  72. package/dist/reactive/effects.d.ts +151 -0
  73. package/dist/reactive/effects.d.ts.map +1 -0
  74. package/dist/reactive/index.d.ts +20 -0
  75. package/dist/reactive/index.d.ts.map +1 -0
  76. package/dist/reactive/interface.d.ts +64 -0
  77. package/dist/reactive/interface.d.ts.map +1 -0
  78. package/dist/reactive/map.d.ts +30 -0
  79. package/dist/reactive/map.d.ts.map +1 -0
  80. package/dist/reactive/memoize.d.ts +5 -0
  81. package/dist/reactive/memoize.d.ts.map +1 -0
  82. package/dist/reactive/non-reactive-state.d.ts +9 -0
  83. package/dist/reactive/non-reactive-state.d.ts.map +1 -0
  84. package/dist/reactive/non-reactive.d.ts +11 -0
  85. package/dist/reactive/non-reactive.d.ts.map +1 -0
  86. package/dist/reactive/project.d.ts +41 -0
  87. package/dist/reactive/project.d.ts.map +1 -0
  88. package/dist/reactive/proxy-state.d.ts +8 -0
  89. package/dist/reactive/proxy-state.d.ts.map +1 -0
  90. package/dist/reactive/proxy.d.ts +23 -0
  91. package/dist/reactive/proxy.d.ts.map +1 -0
  92. package/dist/reactive/record.d.ts +116 -0
  93. package/dist/reactive/record.d.ts.map +1 -0
  94. package/dist/reactive/register.d.ts +64 -0
  95. package/dist/reactive/register.d.ts.map +1 -0
  96. package/dist/reactive/registry.d.ts +20 -0
  97. package/dist/reactive/registry.d.ts.map +1 -0
  98. package/dist/reactive/set.d.ts +28 -0
  99. package/dist/reactive/set.d.ts.map +1 -0
  100. package/dist/reactive/tracking.d.ts +7 -0
  101. package/dist/reactive/tracking.d.ts.map +1 -0
  102. package/dist/reactive/types.d.ts +376 -0
  103. package/dist/reactive/types.d.ts.map +1 -0
  104. package/dist/std-decorators.d.ts +50 -0
  105. package/dist/std-decorators.d.ts.map +1 -0
  106. package/dist/utils.d.ts +49 -0
  107. package/dist/utils.d.ts.map +1 -0
  108. package/dist/zone.d.ts +40 -0
  109. package/dist/zone.d.ts.map +1 -0
  110. package/docs/std-decorators.md +69 -1
  111. package/docs/zone.md +7 -0
  112. package/package.json +39 -27
  113. package/src/async/browser.ts +266 -34
  114. package/src/async/index.ts +17 -2
  115. package/src/async/node.ts +89 -31
  116. package/src/entry-browser.ts +5 -0
  117. package/src/entry-node.ts +5 -0
  118. package/src/index.d.ts +12 -9
  119. package/src/index.ts +1 -0
  120. package/src/reactive/array.ts +139 -52
  121. package/src/reactive/effect-context.ts +3 -3
  122. package/src/reactive/index.ts +2 -1
  123. package/src/reactive/map.ts +1 -1
  124. package/src/reactive/set.ts +1 -1
  125. package/src/utils.ts +1 -1
  126. package/src/zone.ts +19 -8
  127. package/dist/browser.js +0 -161
  128. package/dist/browser.js.map +0 -1
  129. package/dist/chunks/index-BFYK02LG.js.map +0 -1
  130. package/dist/chunks/index-CNR6QRUl.esm.js.map +0 -1
  131. package/dist/node.js +0 -136
  132. package/dist/node.js.map +0 -1
package/dist/zone.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ export declare abstract class AZone<T> {
2
+ abstract active?: T;
3
+ protected enter(value?: T): unknown;
4
+ protected leave(entered: unknown): void;
5
+ with<R>(value: T, fn: () => R): R;
6
+ root<R>(fn: () => R): R;
7
+ get zoned(): FunctionWrapper;
8
+ }
9
+ export type FunctionWrapper = <R>(fn?: () => R) => R;
10
+ export declare class Zone<T> extends AZone<T> {
11
+ active: T | undefined;
12
+ }
13
+ type HistoryValue<T> = {
14
+ present: T | undefined;
15
+ history: Set<T>;
16
+ };
17
+ export declare class ZoneHistory<T> extends AZone<HistoryValue<T>> {
18
+ private controlled;
19
+ private history;
20
+ readonly present: AZone<T>;
21
+ has(value: T): boolean;
22
+ some(predicate: (value: T) => boolean): boolean;
23
+ constructor(controlled?: AZone<T>);
24
+ get active(): HistoryValue<T> | undefined;
25
+ set active(value: HistoryValue<T> | undefined);
26
+ }
27
+ export declare class ZoneAggregator extends AZone<Map<AZone<unknown>, unknown>> {
28
+ #private;
29
+ constructor(...zones: AZone<unknown>[]);
30
+ get active(): Map<AZone<unknown>, unknown> | undefined;
31
+ set active(value: Map<AZone<unknown>, unknown> | undefined);
32
+ enter(value?: Map<AZone<unknown>, unknown> | undefined): Map<AZone<unknown>, unknown>;
33
+ leave(entered: Map<AZone<unknown>, unknown>): void;
34
+ add(z: AZone<unknown>): void;
35
+ delete(z: AZone<unknown>): void;
36
+ clear(): void;
37
+ }
38
+ export declare const asyncZone: ZoneAggregator;
39
+ export {};
40
+ //# sourceMappingURL=zone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zone.d.ts","sourceRoot":"","sources":["../src/zone.ts"],"names":[],"mappings":"AAUA,8BAAsB,KAAK,CAAC,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IACnB,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,OAAO;IAKnC,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAGvC,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC;IAYjC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC;IAQvB,IAAI,KAAK,IAAI,eAAe,CAG3B;CACD;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAA;AAEpD,qBAAa,IAAI,CAAC,CAAC,CAAE,SAAQ,KAAK,CAAC,CAAC,CAAC;IACpC,MAAM,EAAE,CAAC,GAAG,SAAS,CAAA;CACrB;AACD,KAAK,YAAY,CAAC,CAAC,IAAI;IAAC,OAAO,EAAE,CAAC,GAAG,SAAS,CAAC;IAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;CAAC,CAAA;AAChE,qBAAa,WAAW,CAAC,CAAC,CAAE,SAAQ,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAU7C,OAAO,CAAC,UAAU;IAT9B,OAAO,CAAC,OAAO,CAAe;IAC9B,SAAgB,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;IAC1B,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,OAAO;IAGtB,IAAI,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,GAAG,OAAO;gBAIlC,UAAU,GAAE,KAAK,CAAC,CAAC,CAAiB;IAqBxD,IAAI,MAAM,IAGQ,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAD5C;IACD,IAAI,MAAM,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,EAG5C;CACD;AAED,qBAAa,cAAe,SAAQ,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC;;gBAE1D,GAAG,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE;IAItC,IAAI,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,GAAG,SAAS,CAKrD;IACD,IAAI,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,GAAG,SAAS,EAEzD;IACD,KAAK,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,GAAG,SAAS;IAQtD,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,GAAG,IAAI;IAGlD,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC;IAGrB,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC;IAGxB,KAAK;CAGL;AAED,eAAO,MAAM,SAAS,gBAAqC,CAAA"}
@@ -1,4 +1,4 @@
1
- TODO: redo the doc here
1
+
2
2
  # Standard decorators
3
3
 
4
4
  A TypeScript library that provides standard decorators that should stop being re-implemented for the 50th time
@@ -551,6 +551,74 @@ deprecated.warn = (target, propertyKey, message?) => {
551
551
  4. **Document migration paths**: Provide clear upgrade instructions
552
552
  5. **Monitor usage**: Track which deprecated features are still being used
553
553
 
554
+ ## Debounce
555
+
556
+ The `debounce` decorator delays the execution of a method until a specified amount of time has passed since the last time it was called. This is useful for handling rapid events like keystrokes or window resizing.
557
+
558
+ ## API Reference
559
+
560
+ ### `@debounce(delay: number)`
561
+
562
+ A decorator that debounces a method.
563
+
564
+ **Parameters:**
565
+ - `delay`: The delay in milliseconds to wait before executing the method.
566
+
567
+ **Returns:** A method decorator.
568
+
569
+ ## Usage Example
570
+
571
+ ```typescript
572
+ import { debounce } from 'mutts/std-decorators'
573
+
574
+ class SearchComponent {
575
+ @debounce(300)
576
+ onSearch(query: string) {
577
+ // This will only run after 300ms of inactivity
578
+ console.log(`Searching for: ${query}`)
579
+ this.performApiCall(query)
580
+ }
581
+
582
+ performApiCall(query: string) {
583
+ // ...
584
+ }
585
+ }
586
+ ```
587
+
588
+ ## Throttle
589
+
590
+ The `throttle` decorator limits the execution of a method to at most once every specified amount of time. This is useful for rate-limiting expensive operations like scroll handlers or animations.
591
+
592
+ ## API Reference
593
+
594
+ ### `@throttle(delay: number)`
595
+
596
+ A decorator that throttles a method.
597
+
598
+ **Parameters:**
599
+ - `delay`: The time interval in milliseconds.
600
+
601
+ **Returns:** A method decorator.
602
+
603
+ ## Usage Example
604
+
605
+ ```typescript
606
+ import { throttle } from 'mutts/std-decorators'
607
+
608
+ class ScrollHandler {
609
+ @throttle(100)
610
+ onScroll(event: Event) {
611
+ // This will run at most once every 100ms
612
+ console.log('Scroll event processed')
613
+ this.updateUI()
614
+ }
615
+
616
+ updateUI() {
617
+ // ...
618
+ }
619
+ }
620
+ ```
621
+
554
622
  ## Related
555
623
 
556
624
  - [JavaScript Decorators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Decorators)
package/docs/zone.md CHANGED
@@ -42,6 +42,13 @@ requestId.with("req-123", async () => {
42
42
  });
43
43
  ```
44
44
 
45
+ > [!WARNING]
46
+ > TODO
47
+ > **Browser Limitations**: In browser environments (where `AsyncLocalStorage` is unavailable), `mutts` relies on monkey-patching global async primitives (Promise, setTimeout, etc.) to propagate zones. This is generally less robust than Node.js's `async_hooks` and may fail to track context across:
48
+ > * Native `async/await` boundaries in some modern browsers if not transpiled.
49
+ > * Concurrent modifications to global prototypes by other libraries.
50
+
51
+
45
52
  ## Core API
46
53
 
47
54
  ### `AZone<T>` (Abstract)
package/package.json CHANGED
@@ -1,50 +1,56 @@
1
1
  {
2
2
  "name": "mutts",
3
3
  "description": "Modern UTility TS: A collection of TypeScript utilities",
4
- "version": "1.0.7",
5
- "main": "dist/browser.js",
4
+ "version": "1.0.8",
5
+ "main": "dist/browser.cjs",
6
6
  "module": "dist/browser.esm.js",
7
7
  "types": "dist/browser.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
+ "test-node": {
11
+ "import": "./src/entry-node.ts"
12
+ },
13
+ "test-browser": {
14
+ "import": "./src/entry-browser.ts"
15
+ },
10
16
  "node": {
11
17
  "types": "./dist/node.d.ts",
12
18
  "import": "./dist/node.esm.js",
13
- "require": "./dist/node.js"
19
+ "require": "./dist/node.cjs"
14
20
  },
15
21
  "default": {
16
22
  "types": "./dist/browser.d.ts",
17
23
  "import": "./dist/browser.esm.js",
18
- "require": "./dist/browser.js"
24
+ "require": "./dist/browser.cjs"
19
25
  }
20
26
  },
21
27
  "./browser": {
22
28
  "types": "./dist/browser.d.ts",
23
29
  "import": "./dist/browser.esm.js",
24
- "require": "./dist/browser.js"
30
+ "require": "./dist/browser.cjs"
25
31
  },
26
32
  "./node": {
27
33
  "types": "./dist/node.d.ts",
28
34
  "import": "./dist/node.esm.js",
29
- "require": "./dist/node.js"
35
+ "require": "./dist/node.cjs"
30
36
  },
31
37
  "./src": {
32
38
  "node": {
33
- "types": "./src/async/node.ts",
34
- "import": "./src/async/node.ts"
39
+ "types": "./src/entry-node.ts",
40
+ "import": "./src/entry-node.ts"
35
41
  },
36
42
  "default": {
37
- "types": "./src/async/browser.ts",
38
- "import": "./src/async/browser.ts"
43
+ "types": "./src/entry-browser.ts",
44
+ "import": "./src/entry-browser.ts"
39
45
  }
40
46
  },
41
47
  "./src/browser": {
42
- "types": "./src/async/browser.ts",
43
- "import": "./src/async/browser.ts"
48
+ "types": "./src/entry-browser.ts",
49
+ "import": "./src/entry-browser.ts"
44
50
  },
45
51
  "./src/node": {
46
- "types": "./src/async/node.ts",
47
- "import": "./src/async/node.ts"
52
+ "types": "./src/entry-node.ts",
53
+ "import": "./src/entry-node.ts"
48
54
  }
49
55
  },
50
56
  "files": [
@@ -59,14 +65,20 @@
59
65
  "build": "npm run build:js && npm run build:devtools",
60
66
  "build:watch": "rollup -c --watch",
61
67
  "prepublishOnly": "npm run build",
62
- "test": "NODE_OPTIONS=--expose-gc jest",
63
- "test:coverage": "NODE_OPTIONS=--expose-gc jest --coverage",
64
- "test:coverage:watch": "NODE_OPTIONS=--expose-gc jest --coverage --watch",
65
- "test:legacy": "TSCONFIG=tsconfig.legacy.json jest --detectOpenHandles --testPathPatterns=decorator",
66
- "test:modern": "TSCONFIG=tsconfig.modern.json jest --detectOpenHandles --testPathPatterns=decorator",
67
- "test:profile": "RUN_PROFILING=1 NODE_OPTIONS=--expose-gc jest --testPathPatterns=profiling",
68
- "test:profile:benchmark": "RUN_PROFILING=1 NODE_OPTIONS=--expose-gc jest --testPathPatterns=profiling --testNamePattern=benchmark",
69
- "test:profile:detailed": "RUN_PROFILING=1 node --prof node_modules/jest/bin/jest.js --testPathPatterns=profiling --no-coverage",
68
+ "test": "npm run test:node && npm run test:browser",
69
+ "test:node": "TEST_ENV=node NODE_OPTIONS='--expose-gc' vitest run",
70
+ "test:browser": "TEST_ENV=browser vitest run --browser",
71
+ "test:zone:node": "TEST_ENV=node vitest run tests/zone.test.ts",
72
+ "test:zone:browser": "TEST_ENV=browser vitest run tests/zone.test.ts",
73
+ "test:async:node": "TEST_ENV=node vitest run tests/async-hook.test.ts",
74
+ "test:async:browser": "TEST_ENV=browser vitest run tests/async-hook.test.ts",
75
+ "test:coverage": "TEST_ENV=node vitest run --coverage",
76
+ "test:coverage:watch": "TEST_ENV=node vitest run --coverage --watch",
77
+ "test:legacy": "TEST_ENV=node TSCONFIG=tsconfig.legacy.json vitest run --detectOpenHandles",
78
+ "test:modern": "TEST_ENV=node TSCONFIG=tsconfig.modern.json vitest run --detectOpenHandles",
79
+ "test:profile": "RUN_PROFILING=1 NODE_OPTIONS=--expose-gc vitest run",
80
+ "test:profile:benchmark": "RUN_PROFILING=1 NODE_OPTIONS=--expose-gc vitest run -t benchmark",
81
+ "test:profile:detailed": "RUN_PROFILING=1 node --prof node_modules/vitest/vitest.mjs --no-coverage",
70
82
  "benchmark:save": "tsx tests/profiling/benchmark.ts save",
71
83
  "benchmark:compare": "tsx tests/profiling/benchmark.ts compare",
72
84
  "benchmark:list": "tsx tests/profiling/benchmark.ts list",
@@ -102,25 +114,25 @@
102
114
  },
103
115
  "devDependencies": {
104
116
  "@biomejs/biome": "^2.0.6",
105
- "@jest/globals": "^30.2.0",
106
117
  "@rollup/plugin-commonjs": "^28.0.6",
107
118
  "@rollup/plugin-json": "^6.1.0",
108
119
  "@rollup/plugin-node-resolve": "^16.0.1",
109
120
  "@rollup/plugin-terser": "^0.4.4",
110
121
  "@rollup/plugin-typescript": "^12.1.4",
111
- "@types/jest": "^30.0.0",
112
122
  "@types/node": "^22.10.10",
113
- "jest": "^30.0.4",
123
+ "@vitest/browser": "^4.0.18",
124
+ "@vitest/browser-playwright": "^4.0.18",
125
+ "playwright": "^1.58.1",
114
126
  "rollup": "^4.52.2",
115
127
  "rollup-plugin-copy": "^3.5.0",
116
128
  "rollup-plugin-dts": "^6.2.3",
117
129
  "rollup-plugin-typescript2": "^0.36.0",
118
- "ts-jest": "^29.4.0",
119
130
  "ts-node": "^10.9.2",
120
131
  "tslib": "^2.8.1",
121
132
  "tsx": "^4.20.4",
122
133
  "typescript": "^5.8.3",
123
- "vis-network": "^9.1.9"
134
+ "vis-network": "^9.1.9",
135
+ "vitest": "^4.0.18"
124
136
  },
125
137
  "packageManager": "pnpm@10.7.1+sha512.2d92c86b7928dc8284f53494fb4201f983da65f0fb4f0d40baafa5cf628fa31dae3e5968f12466f17df7e97310e30f343a648baea1b9b350685dafafffdf5808"
126
138
  }
@@ -1,6 +1,7 @@
1
1
  import { Hook, Restorer, asyncHooks } from '.'
2
2
 
3
3
  const hooks = new Set<Hook>()
4
+ const promiseContexts = new WeakMap<Promise<any>, Set<Restorer>>()
4
5
 
5
6
  asyncHooks.addHook = function (hook: Hook) {
6
7
  hooks.add(hook)
@@ -9,57 +10,288 @@ asyncHooks.addHook = function (hook: Hook) {
9
10
  }
10
11
  }
11
12
 
12
- export * from '../index'
13
+ // [HACK]: Sanitization
14
+ // If a Promise is created inside the zone, it carries the "Sticky" zone context.
15
+ // If returned to the outer scope, that context leaks. We wrap it in a new Promise
16
+ // created here (in the outer scope) to break the chain and sanitize the return value.
17
+ // See BROWSER_ASYNC_POLYFILL.md for full details.
18
+ asyncHooks.sanitizePromise = (res: any) => {
19
+ if (res && typeof (res as any).then === 'function') {
20
+ return new Promise((resolve, reject) => {
21
+ setTimeout(() => {
22
+ (res as any).then(resolve, reject)
23
+ }, 0)
24
+ })
25
+ }
26
+ return res
27
+ }
13
28
 
14
- function wrap<Args extends any[], R>(fn: ((...args: Args) => R) | null | undefined) {
15
- if (typeof fn !== 'function') return fn
29
+ function captureRestorers() {
16
30
  const restorers = new Set<Restorer>()
17
- for (const hook of hooks) restorers.add(hook())
31
+ for (const hook of hooks) {
32
+ const restorer = hook()
33
+ if (restorer) restorers.add(restorer)
34
+ }
35
+ return restorers
36
+ }
18
37
 
38
+ function wrap<Args extends any[], R>(fn: ((...args: Args) => R) | null | undefined, capturedRestorers?: Set<Restorer>) {
39
+ if (typeof fn !== 'function') return fn
40
+ const restorers = capturedRestorers || captureRestorers()
19
41
  return function (this: any, ...args: Args) {
20
- const undoers = new Set<() => void>()
21
- for (const restore of restorers) undoers.add(restore())
42
+ const undoers: (() => void)[] = []
43
+ for (const restore of restorers) undoers.push(restore())
22
44
  try {
23
45
  return fn.apply(this, args)
24
46
  } finally {
25
- for (const undo of undoers) undo()
47
+ if (originals.queueMicrotask) {
48
+ // Double microtask ensures we run after the first await resumption microtask
49
+ originals.queueMicrotask.call(globalThis, () => {
50
+ originals.queueMicrotask.call(globalThis, () => {
51
+ originals.queueMicrotask.call(globalThis, () => {
52
+ for (let i = undoers.length - 1; i >= 0; i--) undoers[i]()
53
+ })
54
+ })
55
+ })
56
+ } else {
57
+ for (let i = undoers.length - 1; i >= 0; i--) undoers[i]()
58
+ }
26
59
  }
27
60
  }
28
61
  }
29
62
 
30
- const originals = {
31
- then: Promise.prototype.then,
32
- catch: Promise.prototype.catch,
33
- finally: Promise.prototype.finally,
34
- setTimeout: globalThis.setTimeout,
35
- setInterval: globalThis.setInterval,
36
- setImmediate: globalThis.setImmediate,
37
- requestAnimationFrame: globalThis.requestAnimationFrame,
38
- queueMicrotask: globalThis.queueMicrotask,
63
+ const targetWrappers = new WeakMap<any, Map<string, WeakMap<Function, Function>>>()
64
+
65
+ function patchEventTarget(proto: any) {
66
+ if (!proto || !proto.addEventListener || !proto.removeEventListener) return
67
+ const nativeAdd = proto.addEventListener
68
+ const nativeRemove = proto.removeEventListener
69
+
70
+ proto.addEventListener = function (this: any, type: string, listener: any, options: any) {
71
+ if (typeof listener !== 'function') {
72
+ return nativeAdd.call(this, type, listener, options)
73
+ }
74
+
75
+ let types = targetWrappers.get(this)
76
+ if (!types) {
77
+ types = new Map()
78
+ targetWrappers.set(this, types)
79
+ }
80
+ let listeners = types.get(type)
81
+ if (!listeners) {
82
+ listeners = new WeakMap()
83
+ types.set(type, listeners)
84
+ }
85
+
86
+ let wrapped = listeners.get(listener)
87
+ if (!wrapped) {
88
+ wrapped = wrap(listener)
89
+ listeners.set(listener, wrapped)
90
+ }
91
+
92
+ return nativeAdd.call(this, type, wrapped, options)
93
+ }
94
+
95
+ proto.removeEventListener = function (this: any, type: string, listener: any, options: any) {
96
+ if (typeof listener !== 'function') {
97
+ return nativeRemove.call(this, type, listener, options)
98
+ }
99
+
100
+ const types = targetWrappers.get(this)
101
+ if (types) {
102
+ const listeners = types.get(type)
103
+ if (listeners) {
104
+ const wrapped = listeners.get(listener)
105
+ if (wrapped) {
106
+ return nativeRemove.call(this, type, wrapped, options)
107
+ }
108
+ }
109
+ }
110
+
111
+ return nativeRemove.call(this, type, listener, options)
112
+ }
39
113
  }
40
114
 
41
- Promise.prototype.then = function <T, R1, R2>(
42
- this: Promise<T>,
43
- onFulfilled?: ((value: T) => R1 | PromiseLike<R1>) | null,
44
- onRejected?: ((reason: any) => R2 | PromiseLike<R2>) | null
45
- ): Promise<R1 | R2> {
46
- return originals.then.call(this, wrap(onFulfilled), wrap(onRejected))
115
+ function patchOnProperties(proto: any) {
116
+ if (!proto) return
117
+ for (const prop of Object.getOwnPropertyNames(proto)) {
118
+ if (prop.startsWith('on')) {
119
+ const desc = Object.getOwnPropertyDescriptor(proto, prop)
120
+ if (desc && desc.set && desc.configurable) {
121
+ const nativeSet = desc.set
122
+ Object.defineProperty(proto, prop, {
123
+ ...desc,
124
+ set: function (this: any, fn: any) {
125
+ nativeSet.call(this, wrap(fn))
126
+ }
127
+ })
128
+ }
129
+ }
130
+ }
47
131
  }
48
132
 
49
- Promise.prototype.catch = function <T>(
50
- this: Promise<T>,
51
- onRejected?: ((reason: any) => T | PromiseLike<T>) | null
52
- ): Promise<T> {
53
- return originals.catch.call(this, wrap(onRejected))
133
+ if (typeof EventTarget !== 'undefined') {
134
+ patchEventTarget(EventTarget.prototype)
54
135
  }
55
136
 
56
- Promise.prototype.finally = function <T>(
57
- this: Promise<T>,
58
- onFinally?: (() => void) | null
59
- ): Promise<T> {
60
- return originals.finally.call(this, wrap(onFinally))
137
+ const prototypesToPatch = [
138
+ typeof EventTarget !== 'undefined' && EventTarget.prototype,
139
+ typeof HTMLElement !== 'undefined' && HTMLElement.prototype,
140
+ typeof Window !== 'undefined' && Window.prototype,
141
+ typeof Document !== 'undefined' && Document.prototype,
142
+ typeof MessagePort !== 'undefined' && MessagePort.prototype,
143
+ typeof XMLHttpRequest !== 'undefined' && XMLHttpRequest.prototype,
144
+ typeof IDBRequest !== 'undefined' && IDBRequest.prototype,
145
+ typeof IDBTransaction !== 'undefined' && IDBTransaction.prototype,
146
+ typeof IDBDatabase !== 'undefined' && IDBDatabase.prototype,
147
+ typeof FileReader !== 'undefined' && FileReader.prototype,
148
+ typeof AbortSignal !== 'undefined' && AbortSignal.prototype,
149
+ ]
150
+
151
+ for (const proto of prototypesToPatch) {
152
+ if (proto) {
153
+ patchOnProperties(proto)
154
+ }
61
155
  }
62
156
 
157
+ const GLOBAL_ORIGINALS = Symbol.for('mutts.originals');
158
+ const GLOBAL_PROMISE = Symbol.for('mutts.OriginalPromise');
159
+
160
+ let originals: any;
161
+ let OriginalPromise: any;
162
+
163
+ if ((globalThis as any)[GLOBAL_ORIGINALS]) {
164
+ originals = (globalThis as any)[GLOBAL_ORIGINALS];
165
+ OriginalPromise = (globalThis as any)[GLOBAL_PROMISE];
166
+ } else {
167
+ OriginalPromise = globalThis.Promise;
168
+ originals = {
169
+ then: OriginalPromise.prototype.then,
170
+ catch: OriginalPromise.prototype.catch,
171
+ finally: OriginalPromise.prototype.finally,
172
+ resolve: OriginalPromise.resolve,
173
+ reject: OriginalPromise.reject,
174
+ all: OriginalPromise.all,
175
+ allSettled: (OriginalPromise as any).allSettled,
176
+ race: OriginalPromise.race,
177
+ any: (OriginalPromise as any).any,
178
+ setTimeout: globalThis.setTimeout,
179
+ setInterval: globalThis.setInterval,
180
+ setImmediate: (globalThis as any).setImmediate,
181
+ requestAnimationFrame: (globalThis as any).requestAnimationFrame,
182
+ queueMicrotask: globalThis.queueMicrotask,
183
+ };
184
+ (globalThis as any)[GLOBAL_ORIGINALS] = originals;
185
+ (globalThis as any)[GLOBAL_PROMISE] = OriginalPromise;
186
+ }
187
+
188
+ // Ensure modern statics are captured even if originals was cached from an older version
189
+ if (!originals.allSettled) originals.allSettled = (OriginalPromise as any).allSettled
190
+ if (!originals.any) originals.any = (OriginalPromise as any).any
191
+ if (!originals.race) originals.race = OriginalPromise.race
192
+
193
+ function patchedThen(this: any, onFulfilled: any, onRejected: any) {
194
+ const context = promiseContexts.get(this) || captureRestorers()
195
+ const nextPromise = originals.then.call(this, wrap(onFulfilled, context), wrap(onRejected, context))
196
+ if (context.size > 0) promiseContexts.set(nextPromise, context)
197
+ return nextPromise
198
+ }
199
+
200
+ function patchedCatch(this: any, onRejected: any) {
201
+ const context = promiseContexts.get(this) || captureRestorers()
202
+ const nextPromise = originals.catch.call(this, wrap(onRejected, context))
203
+ if (context.size > 0) promiseContexts.set(nextPromise, context)
204
+ return nextPromise
205
+ }
206
+
207
+ function patchedFinally(this: any, onFinally: any) {
208
+ const context = promiseContexts.get(this) || captureRestorers()
209
+ const nextPromise = originals.finally.call(this, wrap(onFinally, context))
210
+ if (context.size > 0) promiseContexts.set(nextPromise, context)
211
+ return nextPromise
212
+ }
213
+
214
+ function PatchedPromise<T>(this: any, executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void) {
215
+ if (typeof executor === 'function') {
216
+ const p = new OriginalPromise((resolve, reject) => {
217
+ const wrappedResolve = wrap(resolve)
218
+ const wrappedReject = wrap(reject)
219
+ executor(wrappedResolve, wrappedReject)
220
+ })
221
+ const context = captureRestorers()
222
+ promiseContexts.set(p, context) // Always set, even if empty (Sticky Root)
223
+ return p
224
+ }
225
+ return new OriginalPromise(executor)
226
+ }
227
+
228
+ // Copy statics
229
+ Object.assign(PatchedPromise, (OriginalPromise as any))
230
+
231
+ // Inherit prototype for instanceof checks
232
+ PatchedPromise.prototype = OriginalPromise.prototype
233
+
234
+ PatchedPromise.resolve = function<T>(value?: T | PromiseLike<T>): Promise<T> {
235
+ const p = originals.resolve.call(OriginalPromise, value) as Promise<T>
236
+ const context = captureRestorers()
237
+ // Ensure we don't overwrite if it already has context (e.g. from constructor)
238
+ if (context.size > 0 && !promiseContexts.has(p)) promiseContexts.set(p, context)
239
+ return p
240
+ } as any
241
+
242
+ PatchedPromise.reject = function<T = never>(reason?: any): Promise<T> {
243
+ const p = originals.reject.call(OriginalPromise, reason) as Promise<T>
244
+ const context = captureRestorers()
245
+ if (context.size > 0) promiseContexts.set(p, context)
246
+ return p
247
+ } as any
248
+
249
+ PatchedPromise.all = function<T>(values: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>[]> {
250
+ const p = originals.all.call(OriginalPromise, values) as Promise<Awaited<T>[]>
251
+ const context = captureRestorers()
252
+ if (context.size > 0) promiseContexts.set(p, context)
253
+ return p
254
+ } as any
255
+
256
+ PatchedPromise.allSettled = function<T>(values: Iterable<T | PromiseLike<T>>): Promise<PromiseSettledResult<Awaited<T>>[]> {
257
+ const p = (originals.allSettled as any).call(OriginalPromise, values)
258
+ const context = captureRestorers()
259
+ if (context.size > 0) promiseContexts.set(p, context)
260
+ return p
261
+ } as any
262
+
263
+ PatchedPromise.race = function<T>(values: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>> {
264
+ const p = originals.race.call(OriginalPromise, values) as Promise<Awaited<T>>
265
+ const context = captureRestorers()
266
+ if (context.size > 0) promiseContexts.set(p, context)
267
+ return p
268
+ } as any
269
+
270
+ PatchedPromise.any = function<T>(values: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>> {
271
+ const p = (originals.any as any).call(OriginalPromise, values)
272
+ const context = captureRestorers()
273
+ if (context.size > 0) promiseContexts.set(p, context)
274
+ return p
275
+ } as any
276
+
277
+ // Only apply patches if not already applied (or re-apply safely)
278
+ // Note: OriginalPromise.prototype might be shared if we used the global one.
279
+ // We must ensure we don't patch it twice if it's the SAME object.
280
+ if (OriginalPromise.prototype.then !== patchedThen) {
281
+ OriginalPromise.prototype.then = patchedThen as any
282
+ OriginalPromise.prototype.catch = patchedCatch as any
283
+ OriginalPromise.prototype.finally = patchedFinally as any
284
+ }
285
+
286
+ try {
287
+ Object.defineProperty(OriginalPromise, Symbol.species, {
288
+ get: () => PatchedPromise,
289
+ configurable: true
290
+ })
291
+ } catch (e) {}
292
+
293
+ ;(globalThis as any).Promise = PatchedPromise
294
+
63
295
  globalThis.setTimeout = ((callback: Function, ...args: any[]) => {
64
296
  return originals.setTimeout.call(globalThis, wrap(callback as any), ...args)
65
297
  }) as any
@@ -69,7 +301,7 @@ globalThis.setInterval = ((callback: Function, ...args: any[]) => {
69
301
  }) as any
70
302
 
71
303
  if (originals.setImmediate) {
72
- globalThis.setImmediate = ((callback: Function, ...args: any[]) => {
304
+ ;(globalThis as any).setImmediate = ((callback: Function, ...args: any[]) => {
73
305
  return originals.setImmediate.call(globalThis, wrap(callback as any), ...args)
74
306
  }) as any
75
307
  }
@@ -84,4 +316,4 @@ if (originals.queueMicrotask) {
84
316
  globalThis.queueMicrotask = (callback: VoidFunction): void => {
85
317
  originals.queueMicrotask.call(globalThis, wrap(callback))
86
318
  }
87
- }
319
+ }
@@ -4,5 +4,20 @@ export type Hook = () => Restorer
4
4
  export const asyncHooks = {
5
5
  addHook(_hook: Hook): () => void {
6
6
  throw 'One must import the library from the server or the client side'
7
- }
8
- }
7
+ },
8
+ /**
9
+ * [Hack] Sanitize a promise (or value) to prevent context leaks.
10
+ * Default: Identity function.
11
+ * Browser: Uses Macrotask wrapping to break microtask chains.
12
+ */
13
+ sanitizePromise(p: any): any {
14
+ return p
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Register a hook that will be called whenever an asynchronous operation is initiated.
20
+ * The hook should return a restorer function which will be called just before the async callback runs.
21
+ * That restorer should in turn return an undoer function which will be called just after the async callback finishes.
22
+ */
23
+ export const asyncHook = (hook: Hook) => asyncHooks.addHook(hook)