mutts 1.0.9 → 1.0.10

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 (204) hide show
  1. package/README.md +58 -33
  2. package/dist/browser.cjs +1371 -1206
  3. package/dist/browser.cjs.map +1 -1
  4. package/dist/browser.d.ts +2 -1392
  5. package/dist/browser.dev.cjs +110 -0
  6. package/dist/browser.dev.cjs.map +1 -0
  7. package/dist/browser.dev.d.ts +2 -0
  8. package/dist/browser.dev.esm.js +5 -0
  9. package/dist/browser.dev.esm.js.map +1 -0
  10. package/dist/browser.esm.js +6 -87
  11. package/dist/browser.esm.js.map +1 -1
  12. package/dist/chunks/{async-browser-lvzLOCgk.cjs → async-browser-BU_IfxYD.cjs} +4 -85
  13. package/dist/chunks/async-browser-BU_IfxYD.cjs.map +1 -0
  14. package/dist/chunks/{async-node-C3DeIb0y.cjs → async-node-3PrbVAbB.cjs} +3 -1
  15. package/dist/chunks/async-node-3PrbVAbB.cjs.map +1 -0
  16. package/dist/chunks/{index-2vea86wD.esm.js → index-CaaQQlPJ.esm.js} +1116 -953
  17. package/dist/chunks/index-CaaQQlPJ.esm.js.map +1 -0
  18. package/dist/chunks/node-nKJBk8iJ.esm.js +96 -0
  19. package/dist/chunks/node-nKJBk8iJ.esm.js.map +1 -0
  20. package/dist/chunks/{index-VTO-b2vR.cjs → proxy-Dtg-bJ3T.cjs} +2285 -2983
  21. package/dist/chunks/proxy-Dtg-bJ3T.cjs.map +1 -0
  22. package/dist/chunks/{index-CtA2AWl3.esm.js → proxy-r7lARftl.esm.js} +2273 -2948
  23. package/dist/chunks/proxy-r7lARftl.esm.js.map +1 -0
  24. package/dist/debug.cjs +973 -28
  25. package/dist/debug.cjs.map +1 -1
  26. package/dist/debug.d.ts +36 -37
  27. package/dist/debug.esm.js +949 -1
  28. package/dist/debug.esm.js.map +1 -1
  29. package/dist/devtools/manifest.json +1 -1
  30. package/dist/devtools/panel.html +1 -1
  31. package/dist/devtools/panel.js +107 -94
  32. package/dist/devtools/panel.js.map +1 -1
  33. package/dist/index.d.ts +1447 -1
  34. package/dist/mutts.umd.js +7167 -1
  35. package/dist/mutts.umd.js.map +1 -1
  36. package/dist/mutts.umd.min.js +1 -1
  37. package/dist/mutts.umd.min.js.map +1 -1
  38. package/dist/node.cjs +62 -60
  39. package/dist/node.cjs.map +1 -1
  40. package/dist/node.d.ts +2 -2
  41. package/dist/node.dev.cjs +110 -0
  42. package/dist/node.dev.cjs.map +1 -0
  43. package/dist/node.dev.d.ts +2 -0
  44. package/dist/node.dev.esm.js +6 -0
  45. package/dist/node.dev.esm.js.map +1 -0
  46. package/dist/node.esm.js +4 -97
  47. package/dist/node.esm.js.map +1 -1
  48. package/dist/{types-DaHFfhlN.d.ts → types-W5vD6m2n.d.ts} +99 -44
  49. package/docs/ai/api-reference.md +11 -18
  50. package/docs/ai/manual.md +308 -230
  51. package/docs/reactive/advanced.md +312 -2
  52. package/docs/reactive/attend.md +1 -2
  53. package/docs/reactive/collections.md +21 -39
  54. package/docs/reactive/core.md +7 -37
  55. package/docs/reactive/debugging.md +117 -10
  56. package/docs/reactive/error-handling.md +10 -10
  57. package/docs/reactive/resource.md +125 -0
  58. package/docs/reactive/scan.md +32 -1
  59. package/docs/reactive.md +3 -2
  60. package/docs/utils.md +70 -0
  61. package/package.json +59 -30
  62. package/dist/chunks/async-browser-lvzLOCgk.cjs.map +0 -1
  63. package/dist/chunks/async-node-C3DeIb0y.cjs.map +0 -1
  64. package/dist/chunks/index-2vea86wD.esm.js.map +0 -1
  65. package/dist/chunks/index-CtA2AWl3.esm.js.map +0 -1
  66. package/dist/chunks/index-VTO-b2vR.cjs.map +0 -1
  67. package/dist/debug/debug.d.ts +0 -122
  68. package/dist/debug/debug.d.ts.map +0 -1
  69. package/dist/debug/index.d.ts +0 -4
  70. package/dist/debug/index.d.ts.map +0 -1
  71. package/dist/debug/lineage-panel.d.ts +0 -5
  72. package/dist/debug/lineage-panel.d.ts.map +0 -1
  73. package/dist/debug/lineage.d.ts +0 -79
  74. package/dist/debug/lineage.d.ts.map +0 -1
  75. package/dist/src/async/browser.d.ts +0 -2
  76. package/dist/src/async/browser.d.ts.map +0 -1
  77. package/dist/src/async/index.d.ts +0 -19
  78. package/dist/src/async/index.d.ts.map +0 -1
  79. package/dist/src/async/node.d.ts +0 -2
  80. package/dist/src/async/node.d.ts.map +0 -1
  81. package/dist/src/decorator.d.ts +0 -106
  82. package/dist/src/decorator.d.ts.map +0 -1
  83. package/dist/src/destroyable.d.ts +0 -87
  84. package/dist/src/destroyable.d.ts.map +0 -1
  85. package/dist/src/entry-browser.d.ts +0 -3
  86. package/dist/src/entry-browser.d.ts.map +0 -1
  87. package/dist/src/entry-node.d.ts +0 -3
  88. package/dist/src/entry-node.d.ts.map +0 -1
  89. package/dist/src/eventful.d.ts +0 -20
  90. package/dist/src/eventful.d.ts.map +0 -1
  91. package/dist/src/flavored.d.ts +0 -33
  92. package/dist/src/flavored.d.ts.map +0 -1
  93. package/dist/src/index.d.ts +0 -14
  94. package/dist/src/index.d.ts.map +0 -1
  95. package/dist/src/indexable.d.ts +0 -243
  96. package/dist/src/indexable.d.ts.map +0 -1
  97. package/dist/src/introspection.d.ts +0 -27
  98. package/dist/src/introspection.d.ts.map +0 -1
  99. package/dist/src/iterableWeak.d.ts +0 -53
  100. package/dist/src/iterableWeak.d.ts.map +0 -1
  101. package/dist/src/mixins.d.ts +0 -25
  102. package/dist/src/mixins.d.ts.map +0 -1
  103. package/dist/src/promiseChain.d.ts +0 -20
  104. package/dist/src/promiseChain.d.ts.map +0 -1
  105. package/dist/src/reactive/array.d.ts +0 -48
  106. package/dist/src/reactive/array.d.ts.map +0 -1
  107. package/dist/src/reactive/buffer.d.ts +0 -120
  108. package/dist/src/reactive/buffer.d.ts.map +0 -1
  109. package/dist/src/reactive/change.d.ts +0 -29
  110. package/dist/src/reactive/change.d.ts.map +0 -1
  111. package/dist/src/reactive/deep-touch.d.ts +0 -28
  112. package/dist/src/reactive/deep-touch.d.ts.map +0 -1
  113. package/dist/src/reactive/deep-watch-state.d.ts +0 -25
  114. package/dist/src/reactive/deep-watch-state.d.ts.map +0 -1
  115. package/dist/src/reactive/deep-watch.d.ts +0 -20
  116. package/dist/src/reactive/deep-watch.d.ts.map +0 -1
  117. package/dist/src/reactive/describe.d.ts +0 -12
  118. package/dist/src/reactive/describe.d.ts.map +0 -1
  119. package/dist/src/reactive/effect-context.d.ts +0 -34
  120. package/dist/src/reactive/effect-context.d.ts.map +0 -1
  121. package/dist/src/reactive/effects.d.ts +0 -164
  122. package/dist/src/reactive/effects.d.ts.map +0 -1
  123. package/dist/src/reactive/index.d.ts +0 -19
  124. package/dist/src/reactive/index.d.ts.map +0 -1
  125. package/dist/src/reactive/map.d.ts +0 -28
  126. package/dist/src/reactive/map.d.ts.map +0 -1
  127. package/dist/src/reactive/memoize.d.ts +0 -28
  128. package/dist/src/reactive/memoize.d.ts.map +0 -1
  129. package/dist/src/reactive/non-reactive-state.d.ts +0 -9
  130. package/dist/src/reactive/non-reactive-state.d.ts.map +0 -1
  131. package/dist/src/reactive/non-reactive.d.ts +0 -11
  132. package/dist/src/reactive/non-reactive.d.ts.map +0 -1
  133. package/dist/src/reactive/project.d.ts +0 -40
  134. package/dist/src/reactive/project.d.ts.map +0 -1
  135. package/dist/src/reactive/proxy-state.d.ts +0 -8
  136. package/dist/src/reactive/proxy-state.d.ts.map +0 -1
  137. package/dist/src/reactive/proxy.d.ts +0 -23
  138. package/dist/src/reactive/proxy.d.ts.map +0 -1
  139. package/dist/src/reactive/record.d.ts +0 -115
  140. package/dist/src/reactive/record.d.ts.map +0 -1
  141. package/dist/src/reactive/register.d.ts +0 -125
  142. package/dist/src/reactive/register.d.ts.map +0 -1
  143. package/dist/src/reactive/registry.d.ts +0 -21
  144. package/dist/src/reactive/registry.d.ts.map +0 -1
  145. package/dist/src/reactive/set.d.ts +0 -26
  146. package/dist/src/reactive/set.d.ts.map +0 -1
  147. package/dist/src/reactive/tracking.d.ts +0 -7
  148. package/dist/src/reactive/tracking.d.ts.map +0 -1
  149. package/dist/src/reactive/types.d.ts +0 -424
  150. package/dist/src/reactive/types.d.ts.map +0 -1
  151. package/dist/src/reactive/watch.d.ts +0 -48
  152. package/dist/src/reactive/watch.d.ts.map +0 -1
  153. package/dist/src/std-decorators.d.ts +0 -45
  154. package/dist/src/std-decorators.d.ts.map +0 -1
  155. package/dist/src/utils.d.ts +0 -49
  156. package/dist/src/utils.d.ts.map +0 -1
  157. package/dist/src/zone.d.ts +0 -40
  158. package/dist/src/zone.d.ts.map +0 -1
  159. package/docs/reactive/describe.md +0 -85
  160. package/docs/reactive/project.md +0 -93
  161. package/src/async/browser.ts +0 -323
  162. package/src/async/index.ts +0 -27
  163. package/src/async/node.ts +0 -92
  164. package/src/decorator.ts +0 -272
  165. package/src/destroyable.ts +0 -199
  166. package/src/entry-browser.ts +0 -5
  167. package/src/entry-node.ts +0 -5
  168. package/src/eventful.ts +0 -110
  169. package/src/flavored.ts +0 -106
  170. package/src/index.d.ts +0 -12
  171. package/src/index.ts +0 -64
  172. package/src/indexable.ts +0 -526
  173. package/src/introspection.ts +0 -59
  174. package/src/iterableWeak.ts +0 -233
  175. package/src/mixins.ts +0 -123
  176. package/src/promiseChain.ts +0 -110
  177. package/src/reactive/array.ts +0 -500
  178. package/src/reactive/buffer.ts +0 -328
  179. package/src/reactive/change.ts +0 -131
  180. package/src/reactive/deep-touch.ts +0 -273
  181. package/src/reactive/deep-watch-state.ts +0 -82
  182. package/src/reactive/deep-watch.ts +0 -171
  183. package/src/reactive/describe.ts +0 -39
  184. package/src/reactive/effect-context.ts +0 -83
  185. package/src/reactive/effects.ts +0 -1434
  186. package/src/reactive/index.ts +0 -72
  187. package/src/reactive/map.ts +0 -142
  188. package/src/reactive/memoize.ts +0 -186
  189. package/src/reactive/non-reactive-state.ts +0 -49
  190. package/src/reactive/non-reactive.ts +0 -43
  191. package/src/reactive/project.md +0 -107
  192. package/src/reactive/project.ts +0 -430
  193. package/src/reactive/proxy-state.ts +0 -27
  194. package/src/reactive/proxy.ts +0 -282
  195. package/src/reactive/record.ts +0 -181
  196. package/src/reactive/register.ts +0 -538
  197. package/src/reactive/registry.ts +0 -72
  198. package/src/reactive/set.ts +0 -117
  199. package/src/reactive/tracking.ts +0 -41
  200. package/src/reactive/types.ts +0 -520
  201. package/src/reactive/watch.ts +0 -180
  202. package/src/std-decorators.ts +0 -256
  203. package/src/utils.ts +0 -300
  204. package/src/zone.ts +0 -142
@@ -1,85 +0,0 @@
1
- # Reactive Property Description (`describe`)
2
-
3
- The `describe` utility provides a reactive way to define or update properties on an object using a record of property descriptors.
4
-
5
- ## Overview
6
-
7
- Unlike `Object.defineProperties`, the reactive `describe` utility is alive:
8
- - It tracks the **keys** of the descriptors record.
9
- - When a new key is added to the descriptors record, the property is defined on the target object.
10
- - When a key is removed, the property is removed from the target object.
11
- - When a descriptor itself changes (e.g. its `value` or `get` function), the property definition is updated.
12
-
13
- This is particularly useful when projecting props or dynamically extending objects in a reactive way.
14
-
15
- Internally, `describe` is built on [`attend`](./attend.md), which provides the reactive key lifecycle.
16
-
17
- ## API
18
-
19
- ```typescript
20
- function describe<T extends object>(
21
- descriptors: Record<PropertyKey, PropertyDescriptor>,
22
- target: T = {} as T
23
- ): T
24
- ```
25
-
26
- ### Parameters
27
-
28
- - `descriptors`: A reactive record where each value is a standard `PropertyDescriptor`.
29
- - `target` (optional): The object to define properties on. If omitted, a new plain object is created and returned.
30
-
31
- ### Returns
32
-
33
- The `target` object with reactive property definitions.
34
-
35
- ## Basic Usage
36
-
37
- ```typescript
38
- import { reactive, describe, effect } from 'mutts/reactive'
39
-
40
- const target = {}
41
- const descriptors = reactive({
42
- foo: { value: 1, enumerable: true }
43
- })
44
-
45
- describe(descriptors, target)
46
-
47
- console.log(target.foo) // 1
48
-
49
- // Adding a property reactively
50
- descriptors.bar = { get: () => 42, enumerable: true }
51
- console.log(target.bar) // 42
52
-
53
- // Removing a property reactively
54
- delete descriptors.foo
55
- console.log('foo' in target) // false
56
- ```
57
-
58
- ## Integration with `project`
59
-
60
- `describe` is often used in conjunction with `project` to create dynamic projections of property descriptors:
61
-
62
- ```typescript
63
- import { project, describe, memoize } from 'mutts/reactive'
64
-
65
- function propsInto(props, into) {
66
- const descriptors = project(props, ({ key, value }) => ({
67
- get: memoize(() => (typeof value === 'function' ? value() : value)),
68
- enumerable: true,
69
- configurable: true
70
- }))
71
-
72
- return describe(descriptors, into)
73
- }
74
- ```
75
-
76
- ## Renaming Conflict
77
-
78
- Previously, `mutts` had a decorator named `describe`. To avoid confusion with this utility, the decorator has been renamed to `@descriptor`.
79
-
80
- ```typescript
81
- @descriptor({ enumerable: false })
82
- class MyClass {
83
- internal = 'secret'
84
- }
85
- ```
@@ -1,93 +0,0 @@
1
- # Reactive Register Memoization Notes
2
-
3
- ## Background
4
-
5
- - Register entries are stored in a reactive `Map`. Updating an entry via `Map.set` marks the entire value as changed.
6
- - Memoized computations that read through `map.get(key)` re-execute fully when the entry changes, even if only a nested property is touched.
7
- - The goal for rendering lists in a JSX/HTML engine is to avoid rebuilding DOM nodes; only the affected properties should update.
8
-
9
- ## Evolution: From `organized` to `project`
10
-
11
- ### Initial State
12
-
13
- - `memoize` caches results but invalidates on `Map.set`, so large effects still re-run.
14
- - `organized` (designed for `Record` sources) creates per-key effects so downstream work reruns only for the touched key; this matches the desired behaviour.
15
- - **Gap:** `organized` operates on plain objects: key enumeration relies on property iteration and `FoolProof.get/set`. Registers and other keyed collections (`Map`, `Register`, custom stores) need the same per-entry orchestration without converting to records.
16
-
17
- ### Completed Evolution: `project` Implementation
18
-
19
- We implemented `project` as a generalized transformation helper that works across arrays, records, and maps:
20
-
21
- **Key Design Decisions:**
22
- - **Unified API:** Single `project` function with runtime dispatch to `project.array`, `project.record`, or `project.map` based on source type.
23
- - **Access Pattern:** Callback receives a `ProjectAccess` object with `get()`, `set()`, `key`, `source`, and `value` (computed property) - similar to `organized` but returning a value instead of an effect.
24
- - **Automatic Target Creation:** The function always creates its own reactive target container (array, record, or map) - no `baseTarget` parameter needed.
25
- - **Per-Key Effects:** Each source key/index gets its own reactive effect that recomputes only when that specific entry changes, enabling granular updates for rendering pipelines.
26
-
27
- **Current API:**
28
- ```typescript
29
- project.array(source: readonly T[], apply: (access, target) => U): ProjectResult<U[]>
30
- project.record(source: Record<K, T>, apply: (access, target) => U): ProjectResult<Record<K, U>>
31
- project.map(source: Map<K, T>, apply: (access, target) => U): ProjectResult<Map<K, U>>
32
- ```
33
-
34
- **Current Behavior:**
35
- - Eager computation: all entries are computed immediately when keys are present.
36
- - One-way transformation: callback only handles "get" (read) operations; no write-back support.
37
- - Mutable results: returned arrays/records/maps are fully mutable.
38
- - `ProjectAccess.old` exposes the previously computed result for each entry, enabling incremental updates and state preservation.
39
-
40
- ## Future Evolutions
41
-
42
- ### Bidirectional Transformation (Set Callback)
43
-
44
- **Goal:** Support writing back to the source through the projected object.
45
-
46
- **Implementation:**
47
- - Add optional second callback parameter: `set: (access, newValue, target) => void | boolean`
48
- - When `set` is provided, mutations to the projected object trigger the set callback.
49
- - The callback receives the same `ProjectAccess` object plus the new value, allowing it to update the source.
50
- - If `set` returns `false` or throws, the mutation is rejected.
51
-
52
- **API Impact:**
53
- ```typescript
54
- project.array(
55
- source: readonly T[],
56
- apply: (access, target) => U,
57
- set?: (access, newValue: U, target) => void | boolean
58
- ): ProjectResult<U[]>
59
- ```
60
-
61
- ### Readonly Results When No Set Callback
62
-
63
- **Goal:** Make projected objects readonly when no write-back is supported.
64
-
65
- **Implementation:**
66
- - When `set` callback is not provided, wrap the result in a readonly proxy or use read-only array/record types.
67
- - Prevents accidental mutations that would have no effect on the source.
68
- - Type system should reflect readonly nature in return types.
69
-
70
- **API Impact:**
71
- - Return type becomes `ProjectResult<readonly U[]>` or similar when `set` is omitted.
72
- - Arrays use `ReactiveReadOnlyArray` (already exists in `mapped.ts`).
73
- - Records and maps need readonly wrappers or proxy-based protection.
74
-
75
- ## Implementation Notes for AI Agents
76
-
77
- **Current State:**
78
- - `project` is fully functional for arrays, records, maps, and registers with eager, one-way transformation.
79
- - Tests cover per-key reactivity, key addition/removal, automatic helper selection, and `access.old` value propagation.
80
- - The implementation follows the same per-key effect pattern as `organized` but returns computed values.
81
-
82
- **Future Work:**
83
- - Lazy computing requires careful effect lifecycle management to avoid memory leaks.
84
- - Bidirectional support needs to handle edge cases (concurrent reads/writes, validation).
85
- - Readonly enforcement should align with existing `ReactiveReadOnlyArray` patterns where possible.
86
- - Consider whether these features should be opt-in via options object or separate function variants.
87
-
88
- **Related Files:**
89
- - `src/reactive/project.ts` - Main implementation
90
- - `src/reactive/array.ts` - Reference for `ReactiveReadOnlyArray` pattern
91
- - `src/reactive/record.ts` - Reference for `organized` pattern (different use case)
92
- - `tests/reactive/project.test.ts` - Test coverage
93
-
@@ -1,323 +0,0 @@
1
- import { asyncHooks, hooks, type Restorer } from '.'
2
-
3
- const promiseContexts = new WeakMap<Promise<any>, Set<Restorer>>()
4
-
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
- })
17
- }
18
- return res
19
- }
20
-
21
- function captureRestorers() {
22
- const restorers = new Set<Restorer>()
23
- for (const hook of hooks) {
24
- const restorer = hook()
25
- if (restorer) restorers.add(restorer)
26
- }
27
- return restorers
28
- }
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()
36
- return function (this: any, ...args: Args) {
37
- const undoers: (() => void)[] = []
38
- for (const restore of restorers) undoers.push(restore())
39
- try {
40
- return fn.apply(this, args)
41
- } finally {
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
- }
104
- }
105
-
106
- return nativeRemove.call(this, type, listener, options)
107
- }
108
- }
109
-
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
- }
126
- }
127
-
128
- if (typeof EventTarget !== 'undefined') {
129
- patchEventTarget(EventTarget.prototype)
130
- }
131
-
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
- }
150
- }
151
-
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
181
- }
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
-
299
- globalThis.setTimeout = ((callback: Function, ...args: any[]) => {
300
- return originals.setTimeout.call(globalThis, wrap(callback as any), ...args)
301
- }) as any
302
-
303
- globalThis.setInterval = ((callback: Function, ...args: any[]) => {
304
- return originals.setInterval.call(globalThis, wrap(callback as any), ...args)
305
- }) as any
306
-
307
- if (originals.setImmediate) {
308
- ;(globalThis as any).setImmediate = ((callback: Function, ...args: any[]) => {
309
- return originals.setImmediate.call(globalThis, wrap(callback as any), ...args)
310
- }) as any
311
- }
312
-
313
- if (originals.requestAnimationFrame) {
314
- globalThis.requestAnimationFrame = (callback: FrameRequestCallback) => {
315
- return originals.requestAnimationFrame.call(globalThis, wrap(callback))
316
- }
317
- }
318
-
319
- if (originals.queueMicrotask) {
320
- globalThis.queueMicrotask = (callback: VoidFunction): void => {
321
- originals.queueMicrotask.call(globalThis, wrap(callback))
322
- }
323
- }
@@ -1,27 +0,0 @@
1
- export type Restorer = () => () => void
2
- export type Hook = () => Restorer
3
-
4
- // Queue for hooks registered before the environment is ready (circular dependency fix)
5
- export const hooks = new Set<Hook>()
6
-
7
- export const asyncHooks = {
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 DELETED
@@ -1,92 +0,0 @@
1
- import { createHook } from 'node:async_hooks'
2
- import { hooks, type Restorer } from '.'
3
-
4
- // 1. Generic async_hooks implementation for Hooks
5
- // This maintains support for 'asyncHooks.addHook' for generic use cases.
6
-
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]()
37
- }
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)
46
- },
47
- before(asyncId) {
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)
54
- }
55
- if (undoers.length > 0) activeUndoers.set(asyncId, undoers)
56
- },
57
- after(asyncId) {
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)
62
- },
63
- destroy(asyncId) {
64
- contexts.delete(asyncId)
65
- activeUndoers.delete(asyncId)
66
- },
67
- })
68
- hook.enable()
69
-
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,
81
- }
82
-
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