mutts 1.0.7 → 1.0.9

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 (191) hide show
  1. package/README.md +72 -17
  2. package/dist/browser.cjs +3111 -0
  3. package/dist/browser.cjs.map +1 -0
  4. package/dist/browser.d.ts +1391 -1
  5. package/dist/browser.esm.js +264 -35
  6. package/dist/browser.esm.js.map +1 -1
  7. package/dist/chunks/async-browser-lvzLOCgk.cjs +297 -0
  8. package/dist/chunks/async-browser-lvzLOCgk.cjs.map +1 -0
  9. package/dist/chunks/async-core-CRLKP3l-.cjs +29 -0
  10. package/dist/chunks/async-core-CRLKP3l-.cjs.map +1 -0
  11. package/dist/chunks/async-node-C3DeIb0y.cjs +96 -0
  12. package/dist/chunks/async-node-C3DeIb0y.cjs.map +1 -0
  13. package/dist/chunks/index-2vea86wD.esm.js +3011 -0
  14. package/dist/chunks/index-2vea86wD.esm.js.map +1 -0
  15. package/dist/chunks/index-CtA2AWl3.esm.js +4376 -0
  16. package/dist/chunks/index-CtA2AWl3.esm.js.map +1 -0
  17. package/dist/chunks/index-VTO-b2vR.cjs +4461 -0
  18. package/dist/chunks/index-VTO-b2vR.cjs.map +1 -0
  19. package/dist/debug/debug.d.ts +122 -0
  20. package/dist/debug/debug.d.ts.map +1 -0
  21. package/dist/debug/index.d.ts +4 -0
  22. package/dist/debug/index.d.ts.map +1 -0
  23. package/dist/debug/lineage-panel.d.ts +5 -0
  24. package/dist/debug/lineage-panel.d.ts.map +1 -0
  25. package/dist/debug/lineage.d.ts +79 -0
  26. package/dist/debug/lineage.d.ts.map +1 -0
  27. package/dist/debug.cjs +35 -0
  28. package/dist/debug.cjs.map +1 -0
  29. package/dist/debug.d.ts +209 -0
  30. package/dist/debug.esm.js +2 -0
  31. package/dist/debug.esm.js.map +1 -0
  32. package/dist/devtools/devtool/devtools.d.ts +1 -0
  33. package/dist/devtools/devtool/devtools.d.ts.map +1 -0
  34. package/dist/devtools/devtool/panel.d.ts +2 -0
  35. package/dist/devtools/devtool/panel.d.ts.map +1 -0
  36. package/dist/index.d.ts +1 -0
  37. package/dist/mutts.umd.js +1 -1
  38. package/dist/mutts.umd.js.map +1 -1
  39. package/dist/mutts.umd.min.js +1 -1
  40. package/dist/mutts.umd.min.js.map +1 -1
  41. package/dist/node.cjs +107 -0
  42. package/dist/node.cjs.map +1 -0
  43. package/dist/node.d.ts +2 -2
  44. package/dist/node.esm.js +86 -33
  45. package/dist/node.esm.js.map +1 -1
  46. package/dist/src/async/browser.d.ts +2 -0
  47. package/dist/src/async/browser.d.ts.map +1 -0
  48. package/dist/src/async/index.d.ts +19 -0
  49. package/dist/src/async/index.d.ts.map +1 -0
  50. package/dist/src/async/node.d.ts +2 -0
  51. package/dist/src/async/node.d.ts.map +1 -0
  52. package/dist/src/decorator.d.ts +106 -0
  53. package/dist/src/decorator.d.ts.map +1 -0
  54. package/dist/src/destroyable.d.ts +87 -0
  55. package/dist/src/destroyable.d.ts.map +1 -0
  56. package/dist/src/entry-browser.d.ts +3 -0
  57. package/dist/src/entry-browser.d.ts.map +1 -0
  58. package/dist/src/entry-node.d.ts +3 -0
  59. package/dist/src/entry-node.d.ts.map +1 -0
  60. package/dist/src/eventful.d.ts +20 -0
  61. package/dist/src/eventful.d.ts.map +1 -0
  62. package/dist/src/flavored.d.ts +33 -0
  63. package/dist/src/flavored.d.ts.map +1 -0
  64. package/dist/src/index.d.ts +14 -0
  65. package/dist/src/index.d.ts.map +1 -0
  66. package/dist/src/indexable.d.ts +243 -0
  67. package/dist/src/indexable.d.ts.map +1 -0
  68. package/dist/src/introspection.d.ts +27 -0
  69. package/dist/src/introspection.d.ts.map +1 -0
  70. package/dist/src/iterableWeak.d.ts +53 -0
  71. package/dist/src/iterableWeak.d.ts.map +1 -0
  72. package/dist/src/mixins.d.ts +25 -0
  73. package/dist/src/mixins.d.ts.map +1 -0
  74. package/dist/src/promiseChain.d.ts +20 -0
  75. package/dist/src/promiseChain.d.ts.map +1 -0
  76. package/dist/src/reactive/array.d.ts +48 -0
  77. package/dist/src/reactive/array.d.ts.map +1 -0
  78. package/dist/src/reactive/buffer.d.ts +120 -0
  79. package/dist/src/reactive/buffer.d.ts.map +1 -0
  80. package/dist/src/reactive/change.d.ts +29 -0
  81. package/dist/src/reactive/change.d.ts.map +1 -0
  82. package/dist/src/reactive/deep-touch.d.ts +28 -0
  83. package/dist/src/reactive/deep-touch.d.ts.map +1 -0
  84. package/dist/src/reactive/deep-watch-state.d.ts +25 -0
  85. package/dist/src/reactive/deep-watch-state.d.ts.map +1 -0
  86. package/dist/src/reactive/deep-watch.d.ts +20 -0
  87. package/dist/src/reactive/deep-watch.d.ts.map +1 -0
  88. package/dist/src/reactive/describe.d.ts +12 -0
  89. package/dist/src/reactive/describe.d.ts.map +1 -0
  90. package/dist/src/reactive/effect-context.d.ts +34 -0
  91. package/dist/src/reactive/effect-context.d.ts.map +1 -0
  92. package/dist/src/reactive/effects.d.ts +164 -0
  93. package/dist/src/reactive/effects.d.ts.map +1 -0
  94. package/dist/src/reactive/index.d.ts +19 -0
  95. package/dist/src/reactive/index.d.ts.map +1 -0
  96. package/dist/src/reactive/map.d.ts +28 -0
  97. package/dist/src/reactive/map.d.ts.map +1 -0
  98. package/dist/src/reactive/memoize.d.ts +28 -0
  99. package/dist/src/reactive/memoize.d.ts.map +1 -0
  100. package/dist/src/reactive/non-reactive-state.d.ts +9 -0
  101. package/dist/src/reactive/non-reactive-state.d.ts.map +1 -0
  102. package/dist/src/reactive/non-reactive.d.ts +11 -0
  103. package/dist/src/reactive/non-reactive.d.ts.map +1 -0
  104. package/dist/src/reactive/project.d.ts +40 -0
  105. package/dist/src/reactive/project.d.ts.map +1 -0
  106. package/dist/src/reactive/proxy-state.d.ts +8 -0
  107. package/dist/src/reactive/proxy-state.d.ts.map +1 -0
  108. package/dist/src/reactive/proxy.d.ts +23 -0
  109. package/dist/src/reactive/proxy.d.ts.map +1 -0
  110. package/dist/src/reactive/record.d.ts +115 -0
  111. package/dist/src/reactive/record.d.ts.map +1 -0
  112. package/dist/src/reactive/register.d.ts +125 -0
  113. package/dist/src/reactive/register.d.ts.map +1 -0
  114. package/dist/src/reactive/registry.d.ts +21 -0
  115. package/dist/src/reactive/registry.d.ts.map +1 -0
  116. package/dist/src/reactive/set.d.ts +26 -0
  117. package/dist/src/reactive/set.d.ts.map +1 -0
  118. package/dist/src/reactive/tracking.d.ts +7 -0
  119. package/dist/src/reactive/tracking.d.ts.map +1 -0
  120. package/dist/src/reactive/types.d.ts +424 -0
  121. package/dist/src/reactive/types.d.ts.map +1 -0
  122. package/dist/src/reactive/watch.d.ts +48 -0
  123. package/dist/src/reactive/watch.d.ts.map +1 -0
  124. package/dist/src/std-decorators.d.ts +45 -0
  125. package/dist/src/std-decorators.d.ts.map +1 -0
  126. package/dist/src/utils.d.ts +49 -0
  127. package/dist/src/utils.d.ts.map +1 -0
  128. package/dist/src/zone.d.ts +40 -0
  129. package/dist/src/zone.d.ts.map +1 -0
  130. package/dist/types-DaHFfhlN.d.ts +414 -0
  131. package/docs/ai/api-reference.md +1 -1
  132. package/docs/ai/manual.md +443 -17
  133. package/docs/events.md +57 -2
  134. package/docs/flavored.md +210 -0
  135. package/docs/reactive/advanced.md +2 -3
  136. package/docs/reactive/attend.md +143 -0
  137. package/docs/reactive/collections.md +55 -0
  138. package/docs/reactive/core.md +120 -4
  139. package/docs/reactive/debugging.md +8 -10
  140. package/docs/reactive/describe.md +85 -0
  141. package/docs/reactive/error-handling.md +121 -0
  142. package/docs/reactive/project.md +1 -1
  143. package/docs/reactive/scan.md +215 -0
  144. package/docs/reactive.md +2 -0
  145. package/docs/std-decorators.md +171 -52
  146. package/docs/zone.md +9 -6
  147. package/package.json +44 -27
  148. package/src/async/browser.ts +277 -41
  149. package/src/async/index.ts +23 -4
  150. package/src/async/node.ts +78 -32
  151. package/src/entry-browser.ts +5 -0
  152. package/src/entry-node.ts +5 -0
  153. package/src/eventful.ts +82 -49
  154. package/src/flavored.ts +106 -0
  155. package/src/index.d.ts +12 -9
  156. package/src/index.ts +2 -1
  157. package/src/introspection.ts +1 -1
  158. package/src/reactive/array.ts +160 -80
  159. package/src/reactive/buffer.ts +187 -27
  160. package/src/reactive/change.ts +16 -23
  161. package/src/reactive/deep-touch.ts +19 -14
  162. package/src/reactive/deep-watch-state.ts +3 -3
  163. package/src/reactive/deep-watch.ts +7 -4
  164. package/src/reactive/describe.ts +39 -0
  165. package/src/reactive/effect-context.ts +71 -6
  166. package/src/reactive/effects.ts +445 -393
  167. package/src/reactive/index.ts +12 -14
  168. package/src/reactive/map.ts +8 -24
  169. package/src/reactive/memoize.ts +26 -3
  170. package/src/reactive/project.md +107 -0
  171. package/src/reactive/project.ts +48 -71
  172. package/src/reactive/proxy.ts +21 -3
  173. package/src/reactive/record.ts +30 -45
  174. package/src/reactive/register.ts +138 -19
  175. package/src/reactive/registry.ts +22 -9
  176. package/src/reactive/set.ts +7 -20
  177. package/src/reactive/tracking.ts +9 -12
  178. package/src/reactive/types.ts +104 -24
  179. package/src/reactive/{interface.ts → watch.ts} +29 -59
  180. package/src/std-decorators.ts +56 -17
  181. package/src/utils.ts +3 -4
  182. package/src/zone.ts +43 -28
  183. package/dist/browser.js +0 -161
  184. package/dist/browser.js.map +0 -1
  185. package/dist/chunks/index-BFYK02LG.js +0 -6378
  186. package/dist/chunks/index-BFYK02LG.js.map +0 -1
  187. package/dist/chunks/index-CNR6QRUl.esm.js +0 -6291
  188. package/dist/chunks/index-CNR6QRUl.esm.js.map +0 -1
  189. package/dist/node.js +0 -136
  190. package/dist/node.js.map +0 -1
  191. package/src/reactive/debug.ts +0 -517
@@ -1,65 +1,301 @@
1
- import { Hook, Restorer, asyncHooks } from '.'
1
+ import { asyncHooks, hooks, type Restorer } from '.'
2
2
 
3
- const hooks = new Set<Hook>()
3
+ const promiseContexts = new WeakMap<Promise<any>, Set<Restorer>>()
4
4
 
5
- asyncHooks.addHook = function (hook: Hook) {
6
- hooks.add(hook)
7
- return () => {
8
- hooks.delete(hook)
5
+ // [HACK]: Sanitization
6
+ // If a Promise is created inside the zone, it carries the "Sticky" zone context.
7
+ // If returned to the outer scope, that context leaks. We wrap it in a new Promise
8
+ // created here (in the outer scope) to break the chain and sanitize the return value.
9
+ // See BROWSER_ASYNC_POLYFILL.md for full details.
10
+ asyncHooks.sanitizePromise = (res: any) => {
11
+ if (res && typeof (res as any).then === 'function') {
12
+ return new Promise((resolve, reject) => {
13
+ setTimeout(() => {
14
+ ;(res as any).then(resolve, reject)
15
+ }, 0)
16
+ })
9
17
  }
18
+ return res
10
19
  }
11
20
 
12
- export * from '../index'
13
-
14
- function wrap<Args extends any[], R>(fn: ((...args: Args) => R) | null | undefined) {
15
- if (typeof fn !== 'function') return fn
21
+ function captureRestorers() {
16
22
  const restorers = new Set<Restorer>()
17
- for (const hook of hooks) restorers.add(hook())
23
+ for (const hook of hooks) {
24
+ const restorer = hook()
25
+ if (restorer) restorers.add(restorer)
26
+ }
27
+ return restorers
28
+ }
18
29
 
30
+ function wrap<Args extends any[], R>(
31
+ fn: ((...args: Args) => R) | null | undefined,
32
+ capturedRestorers?: Set<Restorer>
33
+ ) {
34
+ if (typeof fn !== 'function') return fn
35
+ const restorers = capturedRestorers || captureRestorers()
19
36
  return function (this: any, ...args: Args) {
20
- const undoers = new Set<() => void>()
21
- for (const restore of restorers) undoers.add(restore())
37
+ const undoers: (() => void)[] = []
38
+ for (const restore of restorers) undoers.push(restore())
22
39
  try {
23
40
  return fn.apply(this, args)
24
41
  } finally {
25
- for (const undo of undoers) undo()
42
+ if (originals.queueMicrotask) {
43
+ // Double microtask ensures we run after the first await resumption microtask
44
+ originals.queueMicrotask.call(globalThis, () => {
45
+ originals.queueMicrotask.call(globalThis, () => {
46
+ originals.queueMicrotask.call(globalThis, () => {
47
+ for (let i = undoers.length - 1; i >= 0; i--) undoers[i]()
48
+ })
49
+ })
50
+ })
51
+ } else {
52
+ for (let i = undoers.length - 1; i >= 0; i--) undoers[i]()
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ const targetWrappers = new WeakMap<any, Map<string, WeakMap<Function, Function>>>()
59
+
60
+ function patchEventTarget(proto: any) {
61
+ if (!proto || !proto.addEventListener || !proto.removeEventListener) return
62
+ const nativeAdd = proto.addEventListener
63
+ const nativeRemove = proto.removeEventListener
64
+
65
+ proto.addEventListener = function (this: any, type: string, listener: any, options: any) {
66
+ if (typeof listener !== 'function') {
67
+ return nativeAdd.call(this, type, listener, options)
68
+ }
69
+
70
+ let types = targetWrappers.get(this)
71
+ if (!types) {
72
+ types = new Map()
73
+ targetWrappers.set(this, types)
74
+ }
75
+ let listeners = types.get(type)
76
+ if (!listeners) {
77
+ listeners = new WeakMap()
78
+ types.set(type, listeners)
79
+ }
80
+
81
+ let wrapped = listeners.get(listener)
82
+ if (!wrapped) {
83
+ wrapped = wrap(listener)
84
+ listeners.set(listener, wrapped)
85
+ }
86
+
87
+ return nativeAdd.call(this, type, wrapped, options)
88
+ }
89
+
90
+ proto.removeEventListener = function (this: any, type: string, listener: any, options: any) {
91
+ if (typeof listener !== 'function') {
92
+ return nativeRemove.call(this, type, listener, options)
93
+ }
94
+
95
+ const types = targetWrappers.get(this)
96
+ if (types) {
97
+ const listeners = types.get(type)
98
+ if (listeners) {
99
+ const wrapped = listeners.get(listener)
100
+ if (wrapped) {
101
+ return nativeRemove.call(this, type, wrapped, options)
102
+ }
103
+ }
26
104
  }
105
+
106
+ return nativeRemove.call(this, type, listener, options)
27
107
  }
28
108
  }
29
109
 
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,
110
+ function patchOnProperties(proto: any) {
111
+ if (!proto) return
112
+ for (const prop of Object.getOwnPropertyNames(proto)) {
113
+ if (prop.startsWith('on')) {
114
+ const desc = Object.getOwnPropertyDescriptor(proto, prop)
115
+ if (desc && desc.set && desc.configurable) {
116
+ const nativeSet = desc.set
117
+ Object.defineProperty(proto, prop, {
118
+ ...desc,
119
+ set: function (this: any, fn: any) {
120
+ nativeSet.call(this, wrap(fn))
121
+ },
122
+ })
123
+ }
124
+ }
125
+ }
39
126
  }
40
127
 
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))
128
+ if (typeof EventTarget !== 'undefined') {
129
+ patchEventTarget(EventTarget.prototype)
47
130
  }
48
131
 
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))
132
+ const prototypesToPatch = [
133
+ typeof EventTarget !== 'undefined' && EventTarget.prototype,
134
+ typeof HTMLElement !== 'undefined' && HTMLElement.prototype,
135
+ typeof Window !== 'undefined' && Window.prototype,
136
+ typeof Document !== 'undefined' && Document.prototype,
137
+ typeof MessagePort !== 'undefined' && MessagePort.prototype,
138
+ typeof XMLHttpRequest !== 'undefined' && XMLHttpRequest.prototype,
139
+ typeof IDBRequest !== 'undefined' && IDBRequest.prototype,
140
+ typeof IDBTransaction !== 'undefined' && IDBTransaction.prototype,
141
+ typeof IDBDatabase !== 'undefined' && IDBDatabase.prototype,
142
+ typeof FileReader !== 'undefined' && FileReader.prototype,
143
+ typeof AbortSignal !== 'undefined' && AbortSignal.prototype,
144
+ ]
145
+
146
+ for (const proto of prototypesToPatch) {
147
+ if (proto) {
148
+ patchOnProperties(proto)
149
+ }
54
150
  }
55
151
 
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))
152
+ const GLOBAL_ORIGINALS = Symbol.for('mutts.originals')
153
+ const GLOBAL_PROMISE = Symbol.for('mutts.OriginalPromise')
154
+
155
+ let originals: any
156
+ let OriginalPromise: any
157
+
158
+ if ((globalThis as any)[GLOBAL_ORIGINALS]) {
159
+ originals = (globalThis as any)[GLOBAL_ORIGINALS]
160
+ OriginalPromise = (globalThis as any)[GLOBAL_PROMISE]
161
+ } else {
162
+ OriginalPromise = globalThis.Promise
163
+ originals = {
164
+ then: OriginalPromise.prototype.then,
165
+ catch: OriginalPromise.prototype.catch,
166
+ finally: OriginalPromise.prototype.finally,
167
+ resolve: OriginalPromise.resolve,
168
+ reject: OriginalPromise.reject,
169
+ all: OriginalPromise.all,
170
+ allSettled: (OriginalPromise as any).allSettled,
171
+ race: OriginalPromise.race,
172
+ any: (OriginalPromise as any).any,
173
+ setTimeout: globalThis.setTimeout,
174
+ setInterval: globalThis.setInterval,
175
+ setImmediate: (globalThis as any).setImmediate,
176
+ requestAnimationFrame: (globalThis as any).requestAnimationFrame,
177
+ queueMicrotask: globalThis.queueMicrotask,
178
+ }
179
+ ;(globalThis as any)[GLOBAL_ORIGINALS] = originals
180
+ ;(globalThis as any)[GLOBAL_PROMISE] = OriginalPromise
61
181
  }
62
182
 
183
+ // Ensure modern statics are captured even if originals was cached from an older version
184
+ if (!originals.allSettled) originals.allSettled = (OriginalPromise as any).allSettled
185
+ if (!originals.any) originals.any = (OriginalPromise as any).any
186
+ if (!originals.race) originals.race = OriginalPromise.race
187
+
188
+ function patchedThen(this: any, onFulfilled: any, onRejected: any) {
189
+ const context = promiseContexts.get(this) || captureRestorers()
190
+ const nextPromise = originals.then.call(
191
+ this,
192
+ wrap(onFulfilled, context),
193
+ wrap(onRejected, context)
194
+ )
195
+ if (context.size > 0) promiseContexts.set(nextPromise, context)
196
+ return nextPromise
197
+ }
198
+
199
+ function patchedCatch(this: any, onRejected: any) {
200
+ const context = promiseContexts.get(this) || captureRestorers()
201
+ const nextPromise = originals.catch.call(this, wrap(onRejected, context))
202
+ if (context.size > 0) promiseContexts.set(nextPromise, context)
203
+ return nextPromise
204
+ }
205
+
206
+ function patchedFinally(this: any, onFinally: any) {
207
+ const context = promiseContexts.get(this) || captureRestorers()
208
+ const nextPromise = originals.finally.call(this, wrap(onFinally, context))
209
+ if (context.size > 0) promiseContexts.set(nextPromise, context)
210
+ return nextPromise
211
+ }
212
+
213
+ function PatchedPromise<T>(
214
+ this: any,
215
+ executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void
216
+ ) {
217
+ if (typeof executor === 'function') {
218
+ const p = new OriginalPromise((resolve, reject) => {
219
+ const wrappedResolve = wrap(resolve)
220
+ const wrappedReject = wrap(reject)
221
+ executor(wrappedResolve, wrappedReject)
222
+ })
223
+ const context = captureRestorers()
224
+ promiseContexts.set(p, context) // Always set, even if empty (Sticky Root)
225
+ return p
226
+ }
227
+ return new OriginalPromise(executor)
228
+ }
229
+
230
+ // Copy statics
231
+ Object.assign(PatchedPromise, OriginalPromise as any)
232
+
233
+ // Inherit prototype for instanceof checks
234
+ PatchedPromise.prototype = OriginalPromise.prototype
235
+
236
+ PatchedPromise.resolve = (<T>(value?: T | PromiseLike<T>): Promise<T> => {
237
+ const p = originals.resolve.call(OriginalPromise, value) as Promise<T>
238
+ const context = captureRestorers()
239
+ // Ensure we don't overwrite if it already has context (e.g. from constructor)
240
+ if (context.size > 0 && !promiseContexts.has(p)) promiseContexts.set(p, context)
241
+ return p
242
+ }) as any
243
+
244
+ PatchedPromise.reject = (<T = never>(reason?: any): Promise<T> => {
245
+ const p = originals.reject.call(OriginalPromise, reason) as Promise<T>
246
+ const context = captureRestorers()
247
+ if (context.size > 0) promiseContexts.set(p, context)
248
+ return p
249
+ }) as any
250
+
251
+ PatchedPromise.all = (<T>(values: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>[]> => {
252
+ const p = originals.all.call(OriginalPromise, values) as Promise<Awaited<T>[]>
253
+ const context = captureRestorers()
254
+ if (context.size > 0) promiseContexts.set(p, context)
255
+ return p
256
+ }) as any
257
+
258
+ PatchedPromise.allSettled = (<T>(
259
+ values: Iterable<T | PromiseLike<T>>
260
+ ): Promise<PromiseSettledResult<Awaited<T>>[]> => {
261
+ const p = (originals.allSettled as any).call(OriginalPromise, values)
262
+ const context = captureRestorers()
263
+ if (context.size > 0) promiseContexts.set(p, context)
264
+ return p
265
+ }) as any
266
+
267
+ PatchedPromise.race = (<T>(values: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>> => {
268
+ const p = originals.race.call(OriginalPromise, values) as Promise<Awaited<T>>
269
+ const context = captureRestorers()
270
+ if (context.size > 0) promiseContexts.set(p, context)
271
+ return p
272
+ }) as any
273
+
274
+ PatchedPromise.any = (<T>(values: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>> => {
275
+ const p = (originals.any as any).call(OriginalPromise, values)
276
+ const context = captureRestorers()
277
+ if (context.size > 0) promiseContexts.set(p, context)
278
+ return p
279
+ }) as any
280
+
281
+ // Only apply patches if not already applied (or re-apply safely)
282
+ // Note: OriginalPromise.prototype might be shared if we used the global one.
283
+ // We must ensure we don't patch it twice if it's the SAME object.
284
+ if (OriginalPromise.prototype.then !== patchedThen) {
285
+ OriginalPromise.prototype.then = patchedThen as any
286
+ OriginalPromise.prototype.catch = patchedCatch as any
287
+ OriginalPromise.prototype.finally = patchedFinally as any
288
+ }
289
+
290
+ try {
291
+ Object.defineProperty(OriginalPromise, Symbol.species, {
292
+ get: () => PatchedPromise,
293
+ configurable: true,
294
+ })
295
+ } catch (e) {}
296
+
297
+ ;(globalThis as any).Promise = PatchedPromise
298
+
63
299
  globalThis.setTimeout = ((callback: Function, ...args: any[]) => {
64
300
  return originals.setTimeout.call(globalThis, wrap(callback as any), ...args)
65
301
  }) as any
@@ -69,7 +305,7 @@ globalThis.setInterval = ((callback: Function, ...args: any[]) => {
69
305
  }) as any
70
306
 
71
307
  if (originals.setImmediate) {
72
- globalThis.setImmediate = ((callback: Function, ...args: any[]) => {
308
+ ;(globalThis as any).setImmediate = ((callback: Function, ...args: any[]) => {
73
309
  return originals.setImmediate.call(globalThis, wrap(callback as any), ...args)
74
310
  }) as any
75
311
  }
@@ -84,4 +320,4 @@ if (originals.queueMicrotask) {
84
320
  globalThis.queueMicrotask = (callback: VoidFunction): void => {
85
321
  originals.queueMicrotask.call(globalThis, wrap(callback))
86
322
  }
87
- }
323
+ }
@@ -1,8 +1,27 @@
1
1
  export type Restorer = () => () => void
2
2
  export type Hook = () => Restorer
3
3
 
4
+ // Queue for hooks registered before the environment is ready (circular dependency fix)
5
+ export const hooks = new Set<Hook>()
6
+
4
7
  export const asyncHooks = {
5
- addHook(_hook: Hook): () => void {
6
- throw 'One must import the library from the server or the client side'
7
- }
8
- }
8
+ addHook(hook: Hook): () => void {
9
+ hooks.add(hook)
10
+ return () => hooks.delete(hook)
11
+ },
12
+ /**
13
+ * [Hack] Sanitize a promise (or value) to prevent context leaks.
14
+ * Default: Identity function.
15
+ * Browser: Uses Macrotask wrapping to break microtask chains.
16
+ */
17
+ sanitizePromise(p: any): any {
18
+ return p
19
+ },
20
+ }
21
+
22
+ /**
23
+ * Register a hook that will be called whenever an asynchronous operation is initiated.
24
+ * The hook should return a restorer function which will be called just before the async callback runs.
25
+ * That restorer should in turn return an undoer function which will be called just after the async callback finishes.
26
+ */
27
+ export const asyncHook = (hook: Hook) => asyncHooks.addHook(hook)
package/src/async/node.ts CHANGED
@@ -1,46 +1,92 @@
1
1
  import { createHook } from 'node:async_hooks'
2
- import { Hook, Restorer, asyncHooks } from '.'
2
+ import { hooks, type Restorer } from '.'
3
3
 
4
- const hooks = new Set<Hook>()
5
- const restorersPerAsyncId = new Map<number, Set<Restorer>>()
6
- const undoersPerAsyncId = new Map<number, Set<() => void>>()
4
+ // 1. Generic async_hooks implementation for Hooks
5
+ // This maintains support for 'asyncHooks.addHook' for generic use cases.
7
6
 
8
- createHook({
9
- init(asyncId) {
10
- const restorers = new Set<Restorer>()
11
- for (const hook of hooks) {
12
- restorers.add(hook())
7
+ const contexts = new Map<number, Restorer[]>()
8
+ const activeUndoers = new Map<number, (() => void)[]>()
9
+
10
+ // Helper to capture current hooks state
11
+ function captureRestorers() {
12
+ if (hooks.size === 0) return []
13
+ const restorers: Restorer[] = []
14
+ for (const h of hooks) {
15
+ const r = h()
16
+ if (r) restorers.push(r)
17
+ }
18
+ return restorers
19
+ }
20
+
21
+ // Manual Wrap function to handle Promise callbacks
22
+ function wrap<Args extends any[], R>(fn: ((...args: Args) => R) | null | undefined) {
23
+ if (typeof fn !== 'function') return fn
24
+ const restorers = captureRestorers()
25
+ if (restorers.length === 0) return fn
26
+
27
+ return function (this: any, ...args: Args) {
28
+ const undoers: (() => void)[] = []
29
+ for (const restore of restorers) {
30
+ const u = restore()
31
+ if (u) undoers.push(u)
32
+ }
33
+ try {
34
+ return fn.apply(this, args)
35
+ } finally {
36
+ for (let i = undoers.length - 1; i >= 0; i--) undoers[i]()
13
37
  }
14
- restorersPerAsyncId.set(asyncId, restorers)
38
+ }
39
+ }
40
+
41
+ const hook = createHook({
42
+ init(asyncId, _type, _triggerId, _resource) {
43
+ // Used for native resources like Timers
44
+ const restorers = captureRestorers()
45
+ if (restorers.length > 0) contexts.set(asyncId, restorers)
15
46
  },
16
47
  before(asyncId) {
17
- const restorers = restorersPerAsyncId.get(asyncId)
18
- if (restorers) {
19
- const undoers = new Set<() => void>()
20
- for (const restore of restorers) {
21
- undoers.add(restore())
22
- }
23
- undoersPerAsyncId.set(asyncId, undoers)
48
+ const restorers = contexts.get(asyncId)
49
+ if (!restorers) return
50
+ const undoers: (() => void)[] = []
51
+ for (const restore of restorers) {
52
+ const u = restore()
53
+ if (u) undoers.push(u)
24
54
  }
55
+ if (undoers.length > 0) activeUndoers.set(asyncId, undoers)
25
56
  },
26
57
  after(asyncId) {
27
- const undoers = undoersPerAsyncId.get(asyncId)
28
- if (undoers) {
29
- for (const undo of undoers) undo()
30
- undoersPerAsyncId.delete(asyncId)
31
- }
58
+ const undoers = activeUndoers.get(asyncId)
59
+ if (!undoers) return
60
+ for (let i = undoers.length - 1; i >= 0; i--) undoers[i]()
61
+ activeUndoers.delete(asyncId)
32
62
  },
33
63
  destroy(asyncId) {
34
- restorersPerAsyncId.delete(asyncId)
35
- undoersPerAsyncId.delete(asyncId)
36
- }
37
- }).enable()
64
+ contexts.delete(asyncId)
65
+ activeUndoers.delete(asyncId)
66
+ },
67
+ })
68
+ hook.enable()
38
69
 
39
- asyncHooks.addHook = function (hook: Hook) {
40
- hooks.add(hook)
41
- return () => {
42
- hooks.delete(hook)
43
- }
70
+ // 2. Shadow Promise Implementation
71
+ // Ensures V8 await resumptions are visible as .then callbacks, wrapping them to restore context.
72
+
73
+ const OriginalPromise = globalThis.Promise
74
+ const originalMethods = {
75
+ then: OriginalPromise.prototype.then,
76
+ catch: OriginalPromise.prototype.catch,
77
+ finally: OriginalPromise.prototype.finally,
78
+ resolve: OriginalPromise.resolve,
79
+ reject: OriginalPromise.reject,
80
+ all: OriginalPromise.all,
44
81
  }
45
82
 
46
- export * from '../index'
83
+ // Patch prototype
84
+ OriginalPromise.prototype.then = function (onFulfilled, onRejected) {
85
+ return originalMethods.then.call(this, wrap(onFulfilled), wrap(onRejected))
86
+ } as any
87
+ OriginalPromise.prototype.catch = function (onRejected) {
88
+ return originalMethods.catch.call(this, wrap(onRejected))
89
+ } as any
90
+ OriginalPromise.prototype.finally = function (onFinally) {
91
+ return originalMethods.finally.call(this, wrap(onFinally))
92
+ } as any
@@ -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/eventful.ts CHANGED
@@ -2,76 +2,109 @@
2
2
  * Base type for event maps - all event handlers must be functions
3
3
  */
4
4
  export type EventsBase = Record<string, (...args: any[]) => void>
5
- /**
6
- * A type-safe event system that provides a clean API for event handling
7
- * @template Events - The event map defining event names and their handler signatures
8
- */
9
- export class Eventful<Events extends EventsBase> {
10
- readonly #events = new Map<keyof Events, ((...args: any[]) => void)[]>()
11
- readonly #hooks = [] as ((...args: any[]) => void)[]
12
5
 
13
- public hook(
14
- cb: <EventType extends keyof Events>(
15
- event: EventType,
16
- ...args: Parameters<Events[EventType]>
17
- ) => void
18
- ): () => void {
19
- if (!this.#hooks.includes(cb)) this.#hooks.push(cb)
20
- return () => {
21
- this.#hooks.splice(this.#hooks.indexOf(cb), 1)
22
- }
23
- }
6
+ const events = Symbol('events')
7
+ const hooks = Symbol('hooks')
24
8
 
25
- public on(events: Partial<Events>): void
26
- public on<EventType extends keyof Events>(event: EventType, cb: Events[EventType]): () => void
27
- public on<EventType extends keyof Events>(
28
- eventOrEvents: EventType | Partial<Events>,
29
- cb?: Events[EventType]
9
+ const eventBehavior = {
10
+ on<EventType extends keyof EventsBase>(
11
+ eventOrEvents: EventType | Partial<EventsBase>,
12
+ cb?: EventsBase[EventType]
30
13
  ): () => void {
31
14
  if (typeof eventOrEvents === 'object') {
32
- for (const e of Object.keys(eventOrEvents) as (keyof Events)[]) {
15
+ for (const e of Object.keys(eventOrEvents) as (keyof EventsBase)[]) {
33
16
  this.on(e, eventOrEvents[e]!)
34
17
  }
35
18
  } else if (cb !== undefined) {
36
- let callbacks = this.#events.get(eventOrEvents)
37
- if (!callbacks) {
38
- callbacks = []
39
- this.#events.set(eventOrEvents, callbacks)
40
- }
41
- callbacks.push(cb)
19
+ const callbacks = this[events].get(eventOrEvents) ?? new Set<EventsBase[EventType]>()
20
+ if (!callbacks.has(cb)) callbacks.add(cb)
21
+ this[events].set(eventOrEvents, callbacks)
42
22
  }
43
- // @ts-expect-error Generic case leads to generic case
44
23
  return () => this.off(eventOrEvents, cb)
45
- }
46
- public off(events: Partial<Events>): void
47
- public off<EventType extends keyof Events>(event: EventType, cb?: Events[EventType]): void
48
- public off<EventType extends keyof Events>(
49
- eventOrEvents: EventType | Partial<Events>,
50
- cb?: Events[EventType]
24
+ },
25
+ off<EventType extends keyof EventsBase>(
26
+ eventOrEvents: EventType | Partial<EventsBase>,
27
+ cb?: EventsBase[EventType]
51
28
  ): void {
52
29
  if (typeof eventOrEvents === 'object') {
53
- for (const e of Object.keys(eventOrEvents) as (keyof Events)[]) {
30
+ for (const e of Object.keys(eventOrEvents) as (keyof EventsBase)[]) {
54
31
  this.off(e, eventOrEvents[e])
55
32
  }
56
33
  } else if (cb !== null && cb !== undefined) {
57
- const callbacks = this.#events.get(eventOrEvents)
34
+ const callbacks = this[events].get(eventOrEvents)
58
35
  if (callbacks) {
59
- this.#events.set(
60
- eventOrEvents,
61
- callbacks.filter((c) => c !== cb)
62
- )
36
+ callbacks.delete(cb)
37
+ if (!callbacks.size) this[events].delete(eventOrEvents)
63
38
  }
64
39
  } else {
65
40
  // Remove all listeners for this event
66
- this.#events.delete(eventOrEvents)
41
+ this[events].delete(eventOrEvents)
67
42
  }
68
- }
69
- public emit<EventType extends keyof Events>(
43
+ },
44
+ emit<EventType extends keyof EventsBase>(
70
45
  event: EventType,
71
- ...args: Parameters<Events[EventType]>
46
+ ...args: Parameters<EventsBase[EventType]>
72
47
  ) {
73
- const callbacks = this.#events.get(event)
48
+ const callbacks = this[events].get(event)
74
49
  if (callbacks) for (const cb of callbacks) cb.apply(this, args)
75
- for (const cb of this.#hooks) cb.call(this, event, ...args)
50
+ for (const cb of this[hooks]) cb.call(this, event, ...args)
51
+ },
52
+ }
53
+
54
+ function perEvent(
55
+ eventful: Eventful<any>,
56
+ fct: (event: string, ...args: any[]) => void,
57
+ use?: 'use'
58
+ ) {
59
+ const cache = new Map<string, (...args: any[]) => any>()
60
+ return new Proxy(fct, {
61
+ get(target, prop: PropertyKey) {
62
+ if (typeof prop !== 'string') return target[prop]
63
+ if (use && !eventful[events].has(prop) && !eventful[hooks].size) return () => {}
64
+
65
+ // Return cached function or create and cache
66
+ let cached = cache.get(prop)
67
+ if (!cached) {
68
+ cached = (...args: any[]) => fct.apply(eventful, [prop, ...args])
69
+ cache.set(prop, cached)
70
+ }
71
+ return cached
72
+ },
73
+ })
74
+ }
75
+
76
+ /**
77
+ * A type-safe event system that provides a clean API for event handling
78
+ * @template Events - The event map defining event names and their handler signatures
79
+ */
80
+ export class Eventful<Events extends EventsBase> {
81
+ private readonly [events] = new Map<keyof Events, Set<(...args: any[]) => void>>()
82
+ private readonly [hooks] = new Set<(...args: any[]) => void>()
83
+
84
+ public hook(
85
+ cb: <EventType extends keyof Events>(
86
+ event: EventType,
87
+ ...args: Parameters<Events[EventType]>
88
+ ) => void
89
+ ): () => void {
90
+ this[hooks].add(cb)
91
+ return () => {
92
+ this[hooks].delete(cb)
93
+ }
76
94
  }
95
+
96
+ public on = perEvent(this, eventBehavior.on) as ((events: Partial<Events>) => void) &
97
+ (<EventType extends keyof Events>(event: EventType, cb: Events[EventType]) => () => void) & {
98
+ [event in keyof Events]: (cb: Events[event]) => () => void
99
+ }
100
+ public off = perEvent(this, eventBehavior.off) as ((events: Partial<Events>) => void) &
101
+ (<EventType extends keyof Events>(event: EventType, cb?: Events[EventType]) => void) & {
102
+ [event in keyof Events]: (cb?: Events[event]) => void
103
+ }
104
+
105
+ public emit = perEvent(this, eventBehavior.emit, 'use') as (<EventType extends keyof Events>(
106
+ event: EventType,
107
+ ...args: Parameters<Events[EventType]>
108
+ ) => void) &
109
+ Events
77
110
  }