mutts 1.0.6 → 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 (196) 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-CDCOjzTy.js → browser.cjs} +5913 -4382
  9. package/dist/browser.cjs.map +1 -0
  10. package/dist/browser.d.ts +1655 -0
  11. package/dist/browser.esm.js +305 -0
  12. package/dist/browser.esm.js.map +1 -0
  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-DiP0RXoZ.esm.js → index-DhaOVusv.esm.js} +5851 -4345
  20. package/dist/chunks/index-DhaOVusv.esm.js.map +1 -0
  21. package/dist/decorator.d.ts +17 -18
  22. package/dist/decorator.d.ts.map +1 -0
  23. package/dist/destroyable.d.ts +12 -15
  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/devtools/panel.js.map +1 -1
  30. package/dist/entry-browser.d.ts +3 -0
  31. package/dist/entry-browser.d.ts.map +1 -0
  32. package/dist/entry-node.d.ts +3 -0
  33. package/dist/entry-node.d.ts.map +1 -0
  34. package/dist/eventful.d.ts +3 -5
  35. package/dist/eventful.d.ts.map +1 -0
  36. package/dist/index.d.ts +13 -19
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/indexable.d.ts +10 -10
  39. package/dist/indexable.d.ts.map +1 -0
  40. package/dist/introspection.d.ts +27 -0
  41. package/dist/introspection.d.ts.map +1 -0
  42. package/dist/iterableWeak.d.ts +53 -0
  43. package/dist/iterableWeak.d.ts.map +1 -0
  44. package/dist/mixins.d.ts +25 -0
  45. package/dist/mixins.d.ts.map +1 -0
  46. package/dist/mutts.umd.js +1 -1
  47. package/dist/mutts.umd.js.map +1 -1
  48. package/dist/mutts.umd.min.js +1 -1
  49. package/dist/mutts.umd.min.js.map +1 -1
  50. package/dist/node.cjs +105 -0
  51. package/dist/node.cjs.map +1 -0
  52. package/dist/node.d.ts +1 -0
  53. package/dist/node.esm.js +104 -0
  54. package/dist/node.esm.js.map +1 -0
  55. package/dist/promiseChain.d.ts +4 -5
  56. package/dist/promiseChain.d.ts.map +1 -0
  57. package/dist/reactive/array.d.ts +49 -0
  58. package/dist/reactive/array.d.ts.map +1 -0
  59. package/dist/reactive/buffer.d.ts +44 -0
  60. package/dist/reactive/buffer.d.ts.map +1 -0
  61. package/dist/reactive/change.d.ts +29 -0
  62. package/dist/reactive/change.d.ts.map +1 -0
  63. package/dist/reactive/debug.d.ts +111 -0
  64. package/dist/reactive/debug.d.ts.map +1 -0
  65. package/dist/reactive/deep-touch.d.ts +28 -0
  66. package/dist/reactive/deep-touch.d.ts.map +1 -0
  67. package/dist/reactive/deep-watch-state.d.ts +25 -0
  68. package/dist/reactive/deep-watch-state.d.ts.map +1 -0
  69. package/dist/reactive/deep-watch.d.ts +19 -0
  70. package/dist/reactive/deep-watch.d.ts.map +1 -0
  71. package/dist/reactive/effect-context.d.ts +7 -0
  72. package/dist/reactive/effect-context.d.ts.map +1 -0
  73. package/dist/reactive/effects.d.ts +151 -0
  74. package/dist/reactive/effects.d.ts.map +1 -0
  75. package/dist/reactive/index.d.ts +20 -0
  76. package/dist/reactive/index.d.ts.map +1 -0
  77. package/dist/reactive/interface.d.ts +64 -0
  78. package/dist/reactive/interface.d.ts.map +1 -0
  79. package/dist/reactive/map.d.ts +30 -0
  80. package/dist/reactive/map.d.ts.map +1 -0
  81. package/dist/reactive/memoize.d.ts +5 -0
  82. package/dist/reactive/memoize.d.ts.map +1 -0
  83. package/dist/reactive/non-reactive-state.d.ts +9 -0
  84. package/dist/reactive/non-reactive-state.d.ts.map +1 -0
  85. package/dist/reactive/non-reactive.d.ts +11 -0
  86. package/dist/reactive/non-reactive.d.ts.map +1 -0
  87. package/dist/reactive/project.d.ts +41 -0
  88. package/dist/reactive/project.d.ts.map +1 -0
  89. package/dist/reactive/proxy-state.d.ts +8 -0
  90. package/dist/reactive/proxy-state.d.ts.map +1 -0
  91. package/dist/reactive/proxy.d.ts +23 -0
  92. package/dist/reactive/proxy.d.ts.map +1 -0
  93. package/dist/reactive/record.d.ts +116 -0
  94. package/dist/reactive/record.d.ts.map +1 -0
  95. package/dist/reactive/register.d.ts +64 -0
  96. package/dist/reactive/register.d.ts.map +1 -0
  97. package/dist/reactive/registry.d.ts +20 -0
  98. package/dist/reactive/registry.d.ts.map +1 -0
  99. package/dist/reactive/set.d.ts +28 -0
  100. package/dist/reactive/set.d.ts.map +1 -0
  101. package/dist/reactive/tracking.d.ts +7 -0
  102. package/dist/reactive/tracking.d.ts.map +1 -0
  103. package/dist/reactive/types.d.ts +376 -0
  104. package/dist/reactive/types.d.ts.map +1 -0
  105. package/dist/std-decorators.d.ts +9 -11
  106. package/dist/std-decorators.d.ts.map +1 -0
  107. package/dist/utils.d.ts +49 -0
  108. package/dist/utils.d.ts.map +1 -0
  109. package/dist/zone.d.ts +40 -0
  110. package/dist/zone.d.ts.map +1 -0
  111. package/docs/ai/api-reference.md +0 -2
  112. package/docs/reactive/advanced.md +2 -5
  113. package/docs/reactive/collections.md +0 -125
  114. package/docs/reactive/core.md +27 -24
  115. package/docs/reactive/debugging.md +12 -2
  116. package/docs/reactive/project.md +1 -1
  117. package/docs/reactive/scan.md +78 -0
  118. package/docs/reactive.md +2 -1
  119. package/docs/std-decorators.md +69 -0
  120. package/docs/zone.md +95 -0
  121. package/package.json +67 -23
  122. package/src/async/browser.ts +319 -0
  123. package/src/async/index.ts +23 -0
  124. package/src/async/node.ts +104 -0
  125. package/src/decorator.ts +5 -1
  126. package/src/destroyable.ts +1 -1
  127. package/src/entry-browser.ts +5 -0
  128. package/src/entry-node.ts +5 -0
  129. package/src/index.d.ts +12 -9
  130. package/src/index.ts +23 -14
  131. package/src/indexable.ts +42 -0
  132. package/src/mixins.ts +2 -2
  133. package/src/reactive/array.ts +274 -179
  134. package/src/reactive/buffer.ts +168 -0
  135. package/src/reactive/change.ts +2 -2
  136. package/src/reactive/effect-context.ts +15 -91
  137. package/src/reactive/effects.ts +119 -179
  138. package/src/reactive/index.ts +11 -13
  139. package/src/reactive/interface.ts +19 -33
  140. package/src/reactive/map.ts +49 -62
  141. package/src/reactive/memoize.ts +19 -9
  142. package/src/reactive/project.ts +43 -22
  143. package/src/reactive/proxy.ts +16 -41
  144. package/src/reactive/record.ts +3 -3
  145. package/src/reactive/register.ts +5 -7
  146. package/src/reactive/registry.ts +9 -17
  147. package/src/reactive/set.ts +43 -57
  148. package/src/reactive/tracking.ts +1 -29
  149. package/src/reactive/types.ts +46 -23
  150. package/src/utils.ts +80 -37
  151. package/src/zone.ts +138 -0
  152. package/dist/chunks/_tslib-BgjropY9.js +0 -81
  153. package/dist/chunks/_tslib-BgjropY9.js.map +0 -1
  154. package/dist/chunks/_tslib-MCKDzsSq.esm.js +0 -75
  155. package/dist/chunks/_tslib-MCKDzsSq.esm.js.map +0 -1
  156. package/dist/chunks/decorator-BGILvPtN.esm.js +0 -627
  157. package/dist/chunks/decorator-BGILvPtN.esm.js.map +0 -1
  158. package/dist/chunks/decorator-BQ2eBTCj.js +0 -651
  159. package/dist/chunks/decorator-BQ2eBTCj.js.map +0 -1
  160. package/dist/chunks/index-CDCOjzTy.js.map +0 -1
  161. package/dist/chunks/index-DiP0RXoZ.esm.js.map +0 -1
  162. package/dist/decorator.esm.js +0 -2
  163. package/dist/decorator.esm.js.map +0 -1
  164. package/dist/decorator.js +0 -11
  165. package/dist/decorator.js.map +0 -1
  166. package/dist/destroyable.esm.js +0 -109
  167. package/dist/destroyable.esm.js.map +0 -1
  168. package/dist/destroyable.js +0 -116
  169. package/dist/destroyable.js.map +0 -1
  170. package/dist/eventful.esm.js +0 -66
  171. package/dist/eventful.esm.js.map +0 -1
  172. package/dist/eventful.js +0 -68
  173. package/dist/eventful.js.map +0 -1
  174. package/dist/index.esm.js +0 -53
  175. package/dist/index.esm.js.map +0 -1
  176. package/dist/index.js +0 -139
  177. package/dist/index.js.map +0 -1
  178. package/dist/indexable.esm.js +0 -285
  179. package/dist/indexable.esm.js.map +0 -1
  180. package/dist/indexable.js +0 -291
  181. package/dist/indexable.js.map +0 -1
  182. package/dist/promiseChain.esm.js +0 -78
  183. package/dist/promiseChain.esm.js.map +0 -1
  184. package/dist/promiseChain.js +0 -80
  185. package/dist/promiseChain.js.map +0 -1
  186. package/dist/reactive.d.ts +0 -910
  187. package/dist/reactive.esm.js +0 -5
  188. package/dist/reactive.esm.js.map +0 -1
  189. package/dist/reactive.js +0 -59
  190. package/dist/reactive.js.map +0 -1
  191. package/dist/std-decorators.esm.js +0 -196
  192. package/dist/std-decorators.esm.js.map +0 -1
  193. package/dist/std-decorators.js +0 -204
  194. package/dist/std-decorators.js.map +0 -1
  195. package/src/reactive/mapped.ts +0 -129
  196. package/src/reactive/zone.ts +0 -208
package/package.json CHANGED
@@ -1,19 +1,57 @@
1
1
  {
2
2
  "name": "mutts",
3
3
  "description": "Modern UTility TS: A collection of TypeScript utilities",
4
- "version": "1.0.6",
5
- "main": "src/index.ts",
6
- "module": "src/index.ts",
7
- "types": "src/index.ts",
4
+ "version": "1.0.8",
5
+ "main": "dist/browser.cjs",
6
+ "module": "dist/browser.esm.js",
7
+ "types": "dist/browser.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./dist/index.d.ts",
11
- "source": "./src/index.ts",
12
- "import": "./dist/index.esm.js",
13
- "require": "./dist/index.js",
14
- "script": "./dist/mutts.umd.min.js"
10
+ "test-node": {
11
+ "import": "./src/entry-node.ts"
12
+ },
13
+ "test-browser": {
14
+ "import": "./src/entry-browser.ts"
15
+ },
16
+ "node": {
17
+ "types": "./dist/node.d.ts",
18
+ "import": "./dist/node.esm.js",
19
+ "require": "./dist/node.cjs"
20
+ },
21
+ "default": {
22
+ "types": "./dist/browser.d.ts",
23
+ "import": "./dist/browser.esm.js",
24
+ "require": "./dist/browser.cjs"
25
+ }
15
26
  },
16
- "./src/*": "./src/*"
27
+ "./browser": {
28
+ "types": "./dist/browser.d.ts",
29
+ "import": "./dist/browser.esm.js",
30
+ "require": "./dist/browser.cjs"
31
+ },
32
+ "./node": {
33
+ "types": "./dist/node.d.ts",
34
+ "import": "./dist/node.esm.js",
35
+ "require": "./dist/node.cjs"
36
+ },
37
+ "./src": {
38
+ "node": {
39
+ "types": "./src/entry-node.ts",
40
+ "import": "./src/entry-node.ts"
41
+ },
42
+ "default": {
43
+ "types": "./src/entry-browser.ts",
44
+ "import": "./src/entry-browser.ts"
45
+ }
46
+ },
47
+ "./src/browser": {
48
+ "types": "./src/entry-browser.ts",
49
+ "import": "./src/entry-browser.ts"
50
+ },
51
+ "./src/node": {
52
+ "types": "./src/entry-node.ts",
53
+ "import": "./src/entry-node.ts"
54
+ }
17
55
  },
18
56
  "files": [
19
57
  "dist",
@@ -27,14 +65,20 @@
27
65
  "build": "npm run build:js && npm run build:devtools",
28
66
  "build:watch": "rollup -c --watch",
29
67
  "prepublishOnly": "npm run build",
30
- "test": "NODE_OPTIONS=--expose-gc jest",
31
- "test:coverage": "NODE_OPTIONS=--expose-gc jest --coverage",
32
- "test:coverage:watch": "NODE_OPTIONS=--expose-gc jest --coverage --watch",
33
- "test:legacy": "TSCONFIG=tsconfig.legacy.json jest --detectOpenHandles --testPathPatterns=decorator",
34
- "test:modern": "TSCONFIG=tsconfig.modern.json jest --detectOpenHandles --testPathPatterns=decorator",
35
- "test:profile": "RUN_PROFILING=1 NODE_OPTIONS=--expose-gc jest --testPathPatterns=profiling",
36
- "test:profile:benchmark": "RUN_PROFILING=1 NODE_OPTIONS=--expose-gc jest --testPathPatterns=profiling --testNamePattern=benchmark",
37
- "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",
38
82
  "benchmark:save": "tsx tests/profiling/benchmark.ts save",
39
83
  "benchmark:compare": "tsx tests/profiling/benchmark.ts compare",
40
84
  "benchmark:list": "tsx tests/profiling/benchmark.ts list",
@@ -70,25 +114,25 @@
70
114
  },
71
115
  "devDependencies": {
72
116
  "@biomejs/biome": "^2.0.6",
73
- "@jest/globals": "^30.2.0",
74
117
  "@rollup/plugin-commonjs": "^28.0.6",
75
118
  "@rollup/plugin-json": "^6.1.0",
76
119
  "@rollup/plugin-node-resolve": "^16.0.1",
77
120
  "@rollup/plugin-terser": "^0.4.4",
78
121
  "@rollup/plugin-typescript": "^12.1.4",
79
- "@types/jest": "^30.0.0",
80
122
  "@types/node": "^22.10.10",
81
- "jest": "^30.0.4",
123
+ "@vitest/browser": "^4.0.18",
124
+ "@vitest/browser-playwright": "^4.0.18",
125
+ "playwright": "^1.58.1",
82
126
  "rollup": "^4.52.2",
83
127
  "rollup-plugin-copy": "^3.5.0",
84
128
  "rollup-plugin-dts": "^6.2.3",
85
129
  "rollup-plugin-typescript2": "^0.36.0",
86
- "ts-jest": "^29.4.0",
87
130
  "ts-node": "^10.9.2",
88
131
  "tslib": "^2.8.1",
89
132
  "tsx": "^4.20.4",
90
133
  "typescript": "^5.8.3",
91
- "vis-network": "^9.1.9"
134
+ "vis-network": "^9.1.9",
135
+ "vitest": "^4.0.18"
92
136
  },
93
137
  "packageManager": "pnpm@10.7.1+sha512.2d92c86b7928dc8284f53494fb4201f983da65f0fb4f0d40baafa5cf628fa31dae3e5968f12466f17df7e97310e30f343a648baea1b9b350685dafafffdf5808"
94
138
  }
@@ -0,0 +1,319 @@
1
+ import { Hook, Restorer, asyncHooks } from '.'
2
+
3
+ const hooks = new Set<Hook>()
4
+ const promiseContexts = new WeakMap<Promise<any>, Set<Restorer>>()
5
+
6
+ asyncHooks.addHook = function (hook: Hook) {
7
+ hooks.add(hook)
8
+ return () => {
9
+ hooks.delete(hook)
10
+ }
11
+ }
12
+
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
+ }
28
+
29
+ function captureRestorers() {
30
+ const restorers = new Set<Restorer>()
31
+ for (const hook of hooks) {
32
+ const restorer = hook()
33
+ if (restorer) restorers.add(restorer)
34
+ }
35
+ return restorers
36
+ }
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()
41
+ return function (this: any, ...args: Args) {
42
+ const undoers: (() => void)[] = []
43
+ for (const restore of restorers) undoers.push(restore())
44
+ try {
45
+ return fn.apply(this, args)
46
+ } finally {
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
+ }
59
+ }
60
+ }
61
+ }
62
+
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
+ }
113
+ }
114
+
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
+ }
131
+ }
132
+
133
+ if (typeof EventTarget !== 'undefined') {
134
+ patchEventTarget(EventTarget.prototype)
135
+ }
136
+
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
+ }
155
+ }
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
+
295
+ globalThis.setTimeout = ((callback: Function, ...args: any[]) => {
296
+ return originals.setTimeout.call(globalThis, wrap(callback as any), ...args)
297
+ }) as any
298
+
299
+ globalThis.setInterval = ((callback: Function, ...args: any[]) => {
300
+ return originals.setInterval.call(globalThis, wrap(callback as any), ...args)
301
+ }) as any
302
+
303
+ if (originals.setImmediate) {
304
+ ;(globalThis as any).setImmediate = ((callback: Function, ...args: any[]) => {
305
+ return originals.setImmediate.call(globalThis, wrap(callback as any), ...args)
306
+ }) as any
307
+ }
308
+
309
+ if (originals.requestAnimationFrame) {
310
+ globalThis.requestAnimationFrame = (callback: FrameRequestCallback) => {
311
+ return originals.requestAnimationFrame.call(globalThis, wrap(callback))
312
+ }
313
+ }
314
+
315
+ if (originals.queueMicrotask) {
316
+ globalThis.queueMicrotask = (callback: VoidFunction): void => {
317
+ originals.queueMicrotask.call(globalThis, wrap(callback))
318
+ }
319
+ }
@@ -0,0 +1,23 @@
1
+ export type Restorer = () => () => void
2
+ export type Hook = () => Restorer
3
+
4
+ export const asyncHooks = {
5
+ addHook(_hook: Hook): () => void {
6
+ throw 'One must import the library from the server or the client side'
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)
@@ -0,0 +1,104 @@
1
+ import { createHook } from 'node:async_hooks'
2
+ import { Hook, Restorer, asyncHooks } from '.'
3
+
4
+ // 1. Generic async_hooks implementation for Hooks
5
+ // This maintains support for 'asyncHooks.addHook' for generic use cases.
6
+
7
+ const hooks = new Set<Hook>()
8
+ asyncHooks.addHook = function (hook: Hook) {
9
+ hooks.add(hook)
10
+ return () => {
11
+ hooks.delete(hook)
12
+ }
13
+ }
14
+
15
+ const contexts = new Map<number, Restorer[]>()
16
+ const activeUndoers = new Map<number, (() => void)[]>()
17
+
18
+ // Helper to capture current hooks state
19
+ function captureRestorers() {
20
+ if (hooks.size === 0) return []
21
+ const restorers: Restorer[] = []
22
+ for (const h of hooks) {
23
+ const r = h()
24
+ if (r) restorers.push(r)
25
+ }
26
+ return restorers
27
+ }
28
+
29
+ // Manual Wrap function to handle Promise callbacks
30
+ function wrap<Args extends any[], R>(fn: ((...args: Args) => R) | null | undefined) {
31
+ if (typeof fn !== 'function') return fn
32
+ const restorers = captureRestorers()
33
+ if (restorers.length === 0) return fn
34
+
35
+ return function (this: any, ...args: Args) {
36
+ const undoers: (() => void)[] = []
37
+ for (const restore of restorers) {
38
+ const u = restore()
39
+ if (u) undoers.push(u)
40
+ }
41
+ try {
42
+ return fn.apply(this, args)
43
+ } finally {
44
+ for (let i = undoers.length - 1; i >= 0; i--) undoers[i]()
45
+ }
46
+ }
47
+ }
48
+
49
+ const hook = createHook({
50
+ init(asyncId, type, triggerId, resource) {
51
+ // Used for native resources like Timers
52
+ const restorers = captureRestorers()
53
+ if (restorers.length > 0) contexts.set(asyncId, restorers)
54
+ },
55
+ before(asyncId) {
56
+ const restorers = contexts.get(asyncId)
57
+ if (!restorers) return
58
+ const undoers: (() => void)[] = []
59
+ for (const restore of restorers) {
60
+ const u = restore()
61
+ if (u) undoers.push(u)
62
+ }
63
+ if (undoers.length > 0) activeUndoers.set(asyncId, undoers)
64
+ },
65
+ after(asyncId) {
66
+ const undoers = activeUndoers.get(asyncId)
67
+ if (!undoers) return
68
+ for (let i = undoers.length - 1; i >= 0; i--) undoers[i]()
69
+ activeUndoers.delete(asyncId)
70
+ },
71
+ destroy(asyncId) {
72
+ contexts.delete(asyncId)
73
+ activeUndoers.delete(asyncId)
74
+ },
75
+ })
76
+ hook.enable()
77
+
78
+ // 2. Shadow Promise Implementation
79
+ // Ensures V8 await resumptions are visible as .then callbacks, wrapping them to restore context.
80
+
81
+ const OriginalPromise = globalThis.Promise
82
+ const originalMethods = {
83
+ then: OriginalPromise.prototype.then,
84
+ catch: OriginalPromise.prototype.catch,
85
+ finally: OriginalPromise.prototype.finally,
86
+ resolve: OriginalPromise.resolve,
87
+ reject: OriginalPromise.reject,
88
+ all: OriginalPromise.all,
89
+ }
90
+
91
+
92
+
93
+ // Patch prototype
94
+ OriginalPromise.prototype.then = function(onFulfilled, onRejected) {
95
+ return originalMethods.then.call(this, wrap(onFulfilled), wrap(onRejected))
96
+ } as any
97
+ OriginalPromise.prototype.catch = function(onRejected) {
98
+ return originalMethods.catch.call(this, wrap(onRejected))
99
+ } as any
100
+ OriginalPromise.prototype.finally = function(onFinally) {
101
+ return originalMethods.finally.call(this, wrap(onFinally))
102
+ } as any
103
+
104
+
package/src/decorator.ts CHANGED
@@ -62,7 +62,11 @@ type DDMethod<T> = (
62
62
  name: PropertyKey
63
63
  ) => ((this: T, ...args: any[]) => any) | void
64
64
 
65
- type DDGetter<T> = (original: (this: T) => any, target: any, name: PropertyKey) => ((this: T) => any) | void
65
+ type DDGetter<T> = (
66
+ original: (this: T) => any,
67
+ target: any,
68
+ name: PropertyKey
69
+ ) => ((this: T) => any) | void
66
70
 
67
71
  type DDSetter<T> = (
68
72
  original: (this: T, value: any) => void,
@@ -117,7 +117,7 @@ export function Destroyable<
117
117
  base = undefined
118
118
  }
119
119
  if (!base) {
120
- base = class { } as T
120
+ base = class {} as T
121
121
  }
122
122
 
123
123
  return class Destroyable extends (base as T) {
@@ -0,0 +1,5 @@
1
+ // Import environment-specific patches first (SIDE EFFECTS)
2
+ import './async/browser'
3
+
4
+ // Then export the library
5
+ export * from './index'
@@ -0,0 +1,5 @@
1
+ // Import environment-specific patches first (SIDE EFFECTS)
2
+ import './async/node'
3
+
4
+ // Then export the library
5
+ export * from './index'
package/src/index.d.ts CHANGED
@@ -1,9 +1,12 @@
1
- // Augment Array.isArray to properly handle readonly arrays in type narrowing
2
- interface ArrayConstructor {
3
- /**
4
- * Determines whether an object is an array.
5
- * @param arg Any value to test.
6
- * @returns True if the value is an array (mutable or readonly), false otherwise.
7
- */
8
- isArray(arg: any): arg is any[] | readonly any[]
9
- }
1
+ export * from './decorator';
2
+ export * from './destroyable';
3
+ export * from './eventful';
4
+ export * from './indexable';
5
+ export * from './iterableWeak';
6
+ export * from './mixins';
7
+ export * from './promiseChain';
8
+ export * from './reactive';
9
+ export * from './std-decorators';
10
+ export * from './utils';
11
+ export * from './zone';
12
+ export * from './async';