mutts 1.0.2 → 1.0.4

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 (104) hide show
  1. package/README.md +14 -6
  2. package/dist/chunks/{_tslib-C-cuVLvZ.js → _tslib-BgjropY9.js} +9 -1
  3. package/dist/chunks/_tslib-BgjropY9.js.map +1 -0
  4. package/dist/chunks/{_tslib-CMEnd0VE.esm.js → _tslib-Mzh1rNsX.esm.js} +9 -2
  5. package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +1 -0
  6. package/dist/chunks/{decorator-D4DU97Zg.js → decorator-DLvrD0UF.js} +42 -19
  7. package/dist/chunks/decorator-DLvrD0UF.js.map +1 -0
  8. package/dist/chunks/{decorator-GnHw1Az7.esm.js → decorator-DqiszP7i.esm.js} +42 -19
  9. package/dist/chunks/decorator-DqiszP7i.esm.js.map +1 -0
  10. package/dist/chunks/index-79Kk8D6e.esm.js +4857 -0
  11. package/dist/chunks/index-79Kk8D6e.esm.js.map +1 -0
  12. package/dist/chunks/index-GRBSx0mB.js +4908 -0
  13. package/dist/chunks/index-GRBSx0mB.js.map +1 -0
  14. package/dist/decorator.esm.js +1 -1
  15. package/dist/decorator.js +1 -1
  16. package/dist/destroyable.d.ts +1 -1
  17. package/dist/destroyable.esm.js +1 -1
  18. package/dist/destroyable.esm.js.map +1 -1
  19. package/dist/destroyable.js +1 -1
  20. package/dist/destroyable.js.map +1 -1
  21. package/dist/devtools/devtools.html +9 -0
  22. package/dist/devtools/devtools.js +5 -0
  23. package/dist/devtools/devtools.js.map +1 -0
  24. package/dist/devtools/manifest.json +8 -0
  25. package/dist/devtools/panel.css +72 -0
  26. package/dist/devtools/panel.html +31 -0
  27. package/dist/devtools/panel.js +13048 -0
  28. package/dist/devtools/panel.js.map +1 -0
  29. package/dist/eventful.esm.js +1 -1
  30. package/dist/eventful.js +1 -1
  31. package/dist/index.d.ts +18 -63
  32. package/dist/index.esm.js +4 -4
  33. package/dist/index.js +37 -11
  34. package/dist/index.js.map +1 -1
  35. package/dist/indexable.d.ts +187 -1
  36. package/dist/indexable.esm.js +197 -3
  37. package/dist/indexable.esm.js.map +1 -1
  38. package/dist/indexable.js +198 -2
  39. package/dist/indexable.js.map +1 -1
  40. package/dist/mutts.umd.js +1 -1
  41. package/dist/mutts.umd.js.map +1 -1
  42. package/dist/mutts.umd.min.js +1 -1
  43. package/dist/mutts.umd.min.js.map +1 -1
  44. package/dist/promiseChain.esm.js.map +1 -1
  45. package/dist/promiseChain.js.map +1 -1
  46. package/dist/reactive.d.ts +602 -97
  47. package/dist/reactive.esm.js +3 -3
  48. package/dist/reactive.js +32 -10
  49. package/dist/reactive.js.map +1 -1
  50. package/dist/std-decorators.esm.js +1 -1
  51. package/dist/std-decorators.js +1 -1
  52. package/docs/ai/api-reference.md +133 -0
  53. package/docs/ai/manual.md +105 -0
  54. package/docs/iterableWeak.md +646 -0
  55. package/docs/reactive/advanced.md +1280 -0
  56. package/docs/reactive/collections.md +767 -0
  57. package/docs/reactive/core.md +973 -0
  58. package/docs/reactive.md +21 -9545
  59. package/package.json +18 -5
  60. package/src/decorator.ts +266 -0
  61. package/src/destroyable.ts +199 -0
  62. package/src/eventful.ts +77 -0
  63. package/src/index.d.ts +9 -0
  64. package/src/index.ts +9 -0
  65. package/src/indexable.ts +484 -0
  66. package/src/introspection.ts +59 -0
  67. package/src/iterableWeak.ts +233 -0
  68. package/src/mixins.ts +123 -0
  69. package/src/promiseChain.ts +110 -0
  70. package/src/reactive/array.ts +414 -0
  71. package/src/reactive/change.ts +134 -0
  72. package/src/reactive/debug.ts +517 -0
  73. package/src/reactive/deep-touch.ts +268 -0
  74. package/src/reactive/deep-watch-state.ts +82 -0
  75. package/src/reactive/deep-watch.ts +168 -0
  76. package/src/reactive/effect-context.ts +94 -0
  77. package/src/reactive/effects.ts +1345 -0
  78. package/src/reactive/index.ts +76 -0
  79. package/src/reactive/interface.ts +223 -0
  80. package/src/reactive/map.ts +171 -0
  81. package/src/reactive/mapped.ts +130 -0
  82. package/src/reactive/memoize.ts +107 -0
  83. package/src/reactive/non-reactive-state.ts +49 -0
  84. package/src/reactive/non-reactive.ts +43 -0
  85. package/src/reactive/project.project.md +93 -0
  86. package/src/reactive/project.ts +335 -0
  87. package/src/reactive/proxy-state.ts +27 -0
  88. package/src/reactive/proxy.ts +289 -0
  89. package/src/reactive/record.ts +196 -0
  90. package/src/reactive/register.ts +421 -0
  91. package/src/reactive/set.ts +144 -0
  92. package/src/reactive/tracking.ts +101 -0
  93. package/src/reactive/types.ts +358 -0
  94. package/src/reactive/zone.ts +208 -0
  95. package/src/std-decorators.ts +217 -0
  96. package/src/utils.ts +117 -0
  97. package/dist/chunks/_tslib-C-cuVLvZ.js.map +0 -1
  98. package/dist/chunks/_tslib-CMEnd0VE.esm.js.map +0 -1
  99. package/dist/chunks/decorator-D4DU97Zg.js.map +0 -1
  100. package/dist/chunks/decorator-GnHw1Az7.esm.js.map +0 -1
  101. package/dist/chunks/index-DBScoeCX.esm.js +0 -1960
  102. package/dist/chunks/index-DBScoeCX.esm.js.map +0 -1
  103. package/dist/chunks/index-DOTmXL89.js +0 -1983
  104. package/dist/chunks/index-DOTmXL89.js.map +0 -1
@@ -0,0 +1,358 @@
1
+ // biome-ignore-all lint/suspicious/noConfusingVoidType: Type 'void' is not assignable to type 'ScopedCallback | undefined'.
2
+ // Argument of type '() => void' is not assignable to parameter of type '(dep: DependencyFunction) => ScopedCallback | undefined'.
3
+
4
+ /**
5
+ * Function type for dependency tracking in effects
6
+ * Restores the active effect context for dependency tracking
7
+ */
8
+ export type DependencyFunction = <T>(cb: () => T) => T
9
+ /**
10
+ * Dependency access passed to user callbacks within effects/watch
11
+ * Provides functions to track dependencies and information about the effect execution
12
+ */
13
+ export interface DependencyAccess {
14
+ // TODO: remove tracked (async is managed)
15
+ // TODO: remove ascend (make a global like `untracked` who withEffect(parentEffect, () => {}))
16
+ /**
17
+ * Tracks dependencies in the current effect context
18
+ * Use this for normal dependency tracking within the effect
19
+ * @example
20
+ * ```typescript
21
+ * effect(({ tracked }) => {
22
+ * // In async context, use tracked to restore dependency tracking
23
+ * await someAsyncOperation()
24
+ * const value = tracked(() => state.count) // Tracks state.count in this effect
25
+ * })
26
+ * ```
27
+ */
28
+ tracked: DependencyFunction
29
+ /**
30
+ * Tracks dependencies in the parent effect context
31
+ * Use this when child effects should track dependencies in the parent,
32
+ * allowing parent cleanup to manage child effects while dependencies trigger the parent
33
+ * @example
34
+ * ```typescript
35
+ * effect(({ ascend }) => {
36
+ * const length = inputs.length
37
+ * if (length > 0) {
38
+ * ascend(() => {
39
+ * // Dependencies here are tracked in the parent effect
40
+ * inputs.forEach(item => console.log(item))
41
+ * })
42
+ * }
43
+ * })
44
+ * ```
45
+ */
46
+ ascend: DependencyFunction
47
+ /**
48
+ * Indicates whether the effect is running as a reaction (i.e. not the first call)
49
+ * - `false`: First execution when the effect is created
50
+ * - `true`: Subsequent executions triggered by dependency changes
51
+ * @example
52
+ * ```typescript
53
+ * effect(({ reaction }) => {
54
+ * if (!reaction) {
55
+ * console.log('Effect initialized')
56
+ * // Setup code that should only run once
57
+ * } else {
58
+ * console.log('Effect re-ran due to dependency change')
59
+ * // Code that runs on every update
60
+ * }
61
+ * })
62
+ * ```
63
+ */
64
+ reaction: boolean
65
+ }
66
+ // Zone-based async context preservation is implemented in zone.ts
67
+ // It automatically preserves effect context across Promise boundaries (.then, .catch, .finally)
68
+
69
+ /**
70
+ * Type for effect cleanup functions
71
+ */
72
+ export type ScopedCallback = () => void
73
+
74
+ /**
75
+ * Async execution mode for effects
76
+ * - `cancel`: Cancel previous async execution when dependencies change (default)
77
+ * - `queue`: Queue next execution to run after current completes
78
+ * - `ignore`: Ignore new executions while async work is running
79
+ */
80
+ export type AsyncExecutionMode = 'cancel' | 'queue' | 'ignore'
81
+
82
+ /**
83
+ * Options for effect creation
84
+ */
85
+ export interface EffectOptions {
86
+ /**
87
+ * How to handle async effect executions when dependencies change
88
+ * @default 'cancel'
89
+ */
90
+ asyncMode?: AsyncExecutionMode
91
+ /**
92
+ * If true, this effect is "opaque" to deep optimizations: it sees the object reference itself
93
+ * and must be notified when it changes, regardless of deep content similarity.
94
+ * Use this for effects that depend on object identity (like memoize).
95
+ */
96
+ opaque?: boolean
97
+ }
98
+
99
+ /**
100
+ * Type for property evolution events
101
+ */
102
+ export type PropEvolution = {
103
+ type: 'set' | 'del' | 'add' | 'invalidate'
104
+ prop: any
105
+ }
106
+
107
+ /**
108
+ * Type for collection operation evolution events
109
+ */
110
+ export type BunchEvolution = {
111
+ type: 'bunch'
112
+ method: string
113
+ }
114
+ export type Evolution = PropEvolution | BunchEvolution
115
+
116
+ type State =
117
+ | {
118
+ evolution: Evolution
119
+ next: State
120
+ }
121
+ | {}
122
+
123
+ // Track native reactivity
124
+ const nativeReactive = Symbol('native-reactive')
125
+
126
+ /**
127
+ * Symbol to mark individual objects as non-reactive
128
+ */
129
+ export const nonReactiveMark = Symbol('non-reactive')
130
+ /**
131
+ * Symbol to mark class properties as non-reactive
132
+ */
133
+ export const unreactiveProperties = Symbol('unreactive-properties')
134
+ /**
135
+ * Symbol for prototype forwarding in reactive objects
136
+ */
137
+ export const prototypeForwarding: unique symbol = Symbol('prototype-forwarding')
138
+
139
+ /**
140
+ * Symbol representing all properties in reactive tracking
141
+ */
142
+ export const allProps = Symbol('all-props')
143
+
144
+ // Symbol to mark functions with their root function
145
+ const rootFunction = Symbol('root-function')
146
+
147
+ /**
148
+ * Structured error codes for machine-readable diagnosis
149
+ */
150
+ export enum ReactiveErrorCode {
151
+ CycleDetected = 'CYCLE_DETECTED',
152
+ MaxDepthExceeded = 'MAX_DEPTH_EXCEEDED',
153
+ MaxReactionExceeded = 'MAX_REACTION_EXCEEDED',
154
+ WriteInComputed = 'WRITE_IN_COMPUTED',
155
+ TrackingError = 'TRACKING_ERROR',
156
+ }
157
+
158
+ export type CycleDebugInfo = {
159
+ code: ReactiveErrorCode.CycleDetected
160
+ cycle: string[]
161
+ details?: string
162
+ }
163
+
164
+ export type MaxDepthDebugInfo = {
165
+ code: ReactiveErrorCode.MaxDepthExceeded
166
+ depth: number
167
+ chain: string[]
168
+ }
169
+
170
+ export type MaxReactionDebugInfo = {
171
+ code: ReactiveErrorCode.MaxReactionExceeded
172
+ count: number
173
+ effect: string
174
+ }
175
+
176
+ export type GenericDebugInfo = {
177
+ code: ReactiveErrorCode
178
+ causalChain?: string[]
179
+ creationStack?: string
180
+ [key: string]: any
181
+ }
182
+
183
+ export type ReactiveDebugInfo =
184
+ | CycleDebugInfo
185
+ | MaxDepthDebugInfo
186
+ | MaxReactionDebugInfo
187
+ | GenericDebugInfo
188
+
189
+ /**
190
+ * Error class for reactive system errors
191
+ */
192
+ export class ReactiveError extends Error {
193
+ constructor(
194
+ message: string,
195
+ public debugInfo?: ReactiveDebugInfo
196
+ ) {
197
+ super(message)
198
+ this.name = 'ReactiveError'
199
+ }
200
+ }
201
+
202
+ // biome-ignore-start lint/correctness/noUnusedFunctionParameters: Interface declaration with empty defaults
203
+ /**
204
+ * Global options for the reactive system
205
+ */
206
+ export const options = {
207
+ /**
208
+ * Debug purpose: called when an effect is entered
209
+ * @param effect - The effect that is entered
210
+ */
211
+ enter: (_effect: Function) => {},
212
+ /**
213
+ * Debug purpose: called when an effect is left
214
+ * @param effect - The effect that is left
215
+ */
216
+ leave: (_effect: Function) => {},
217
+ /**
218
+ * Debug purpose: called when an effect is chained
219
+ * @param target - The effect that is being triggered
220
+ * @param caller - The effect that is calling the target
221
+ */
222
+ chain: (_targets: Function[], _caller?: Function) => {},
223
+ /**
224
+ * Debug purpose: called when an effect chain is started
225
+ * @param target - The effect that is being triggered
226
+ */
227
+ beginChain: (_targets: Function[]) => {},
228
+ /**
229
+ * Debug purpose: called when an effect chain is ended
230
+ */
231
+ endChain: () => {},
232
+ garbageCollected: (_fn: Function) => {},
233
+ /**
234
+ * Debug purpose: called when an object is touched
235
+ * @param obj - The object that is touched
236
+ * @param evolution - The type of change
237
+ * @param props - The properties that changed
238
+ * @param deps - The dependencies that changed
239
+ */
240
+ touched: (_obj: any, _evolution: Evolution, _props?: any[], _deps?: Set<ScopedCallback>) => {},
241
+ /**
242
+ * Debug purpose: called when an effect is skipped because it's already running
243
+ * @param effect - The effect that is already running
244
+ * @param runningChain - The array of effects from the detected one to the currently running one
245
+ */
246
+ skipRunningEffect: (_effect: ScopedCallback, _runningChain: ScopedCallback[]) => {},
247
+ /**
248
+ * Debug purpose: maximum effect chain (like call stack max depth)
249
+ * Used to prevent infinite loops
250
+ * @default 100
251
+ */
252
+ maxEffectChain: 100,
253
+ /**
254
+ * Debug purpose: maximum effect reaction (like call stack max depth)
255
+ * Used to prevent infinite loops
256
+ * @default 'throw'
257
+ */
258
+ maxEffectReaction: 'throw' as 'throw' | 'debug' | 'warn',
259
+ /**
260
+ * How to handle cycles detected in effect batches
261
+ * - 'throw': Throw an error with cycle information (default, recommended for development)
262
+ * - 'warn': Log a warning and break the cycle by executing one effect
263
+ * - 'break': Silently break the cycle by executing one effect (recommended for production)
264
+ * - 'strict': Prevent cycle creation by checking graph before execution (throws error)
265
+ * @default 'throw'
266
+ */
267
+ cycleHandling: 'throw' as 'throw' | 'warn' | 'break' | 'strict',
268
+ /**
269
+ * Maximum depth for deep watching traversal
270
+ * Used to prevent infinite recursion in circular references
271
+ * @default 100
272
+ */
273
+ maxDeepWatchDepth: 100,
274
+ /**
275
+ * Only react on instance members modification (not inherited properties)
276
+ * For instance, do not track class methods
277
+ * @default true
278
+ */
279
+ instanceMembers: true,
280
+ /**
281
+ * Ignore accessors (getters and setters) and only track direct properties
282
+ * @default true
283
+ */
284
+ ignoreAccessors: true,
285
+ /**
286
+ * Enable recursive touching when objects with the same prototype are replaced
287
+ * When enabled, replacing an object with another of the same prototype triggers
288
+ * recursive diffing instead of notifying parent effects
289
+ * @default true
290
+ */
291
+ recursiveTouching: true,
292
+ /**
293
+ * Default async execution mode for effects that return Promises
294
+ * - 'cancel': Cancel previous async execution when dependencies change (default, enables async zone)
295
+ * - 'queue': Queue next execution to run after current completes (enables async zone)
296
+ * - 'ignore': Ignore new executions while async work is running (enables async zone)
297
+ * - false: Disable async zone and async mode handling (effects run concurrently)
298
+ *
299
+ * **When truthy:** Enables async zone (Promise.prototype wrapping) for automatic context
300
+ * preservation in Promise callbacks. Warning: This modifies Promise.prototype globally.
301
+ * Only enable if no other library modifies Promise.prototype.
302
+ *
303
+ * **When false:** Async zone is disabled. Use `tracked()` manually in Promise callbacks.
304
+ *
305
+ * Can be overridden per-effect via EffectOptions
306
+ * @default 'cancel'
307
+ */
308
+ asyncMode: 'cancel' as AsyncExecutionMode | false,
309
+ // biome-ignore lint/suspicious/noConsole: This is the whole point here
310
+ warn: (...args: any[]) => console.warn(...args),
311
+
312
+ /**
313
+ * Configuration for the introspection system
314
+ */
315
+ introspection: {
316
+ /**
317
+ * Whether to keep a history of mutations for debugging
318
+ * @default false
319
+ */
320
+ enableHistory: false,
321
+ /**
322
+ * Number of mutations to keep in history
323
+ * @default 50
324
+ */
325
+ historySize: 50,
326
+ },
327
+
328
+ /**
329
+ * Configuration for zone hooks - control which async APIs are hooked
330
+ * Each option controls whether the corresponding async API is wrapped to preserve effect context
331
+ * Only applies when asyncMode is enabled (truthy)
332
+ */
333
+ zones: {
334
+ /**
335
+ * Hook setTimeout to preserve effect context
336
+ * @default true
337
+ */
338
+ setTimeout: true,
339
+ /**
340
+ * Hook setInterval to preserve effect context
341
+ * @default true
342
+ */
343
+ setInterval: true,
344
+ /**
345
+ * Hook requestAnimationFrame (runs in untracked context when hooked)
346
+ * @default true
347
+ */
348
+ requestAnimationFrame: true,
349
+ /**
350
+ * Hook queueMicrotask to preserve effect context
351
+ * @default true
352
+ */
353
+ queueMicrotask: true,
354
+ },
355
+ }
356
+ // biome-ignore-end lint/correctness/noUnusedFunctionParameters: Interface declaration with empty defaults
357
+
358
+ export { type State, nativeReactive, rootFunction }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Zone-like async context preservation for reactive effects
3
+ *
4
+ * Automatically preserves effect context across async boundaries:
5
+ * - Promise methods: .then(), .catch(), .finally()
6
+ * - Timers: setTimeout(), setInterval()
7
+ * - Animation: requestAnimationFrame() (if available) - runs in untracked context
8
+ * - Microtasks: queueMicrotask() (if available)
9
+ *
10
+ * **IMPORTANT:** This module is opt-in via `reactiveOptions.asyncMode` (truthy = enabled, false = disabled).
11
+ * By default, async zone is ENABLED with 'cancel' mode.
12
+ *
13
+ * When disabled (asyncMode = false), use `tracked()` manually in async callbacks.
14
+ * When enabled (asyncMode = 'cancel' | 'queue' | 'ignore'), async entry points are wrapped ONCE.
15
+ */
16
+
17
+ import { captureEffectStack, withEffectStack } from './effect-context'
18
+ import { options, ScopedCallback } from './types'
19
+
20
+ let zoneHooked = false
21
+
22
+ // Store original Promise methods at module load time (before any wrapping)
23
+ // This ensures we always have the true originals, even if wrapping happens multiple times
24
+ const originalPromiseThen =
25
+ Object.getOwnPropertyDescriptor(Promise.prototype, 'then')?.value || Promise.prototype.then
26
+ const originalPromiseCatch =
27
+ Object.getOwnPropertyDescriptor(Promise.prototype, 'catch')?.value || Promise.prototype.catch
28
+ const originalPromiseFinally =
29
+ Object.getOwnPropertyDescriptor(Promise.prototype, 'finally')?.value || Promise.prototype.finally
30
+
31
+ // Store original timer functions at module load time
32
+ const originalSetTimeout = globalThis.setTimeout
33
+ const originalSetInterval = globalThis.setInterval
34
+ const originalRequestAnimationFrame =
35
+ typeof globalThis.requestAnimationFrame !== 'undefined'
36
+ ? globalThis.requestAnimationFrame
37
+ : undefined
38
+ const originalQueueMicrotask =
39
+ typeof globalThis.queueMicrotask !== 'undefined' ? globalThis.queueMicrotask : undefined
40
+
41
+ // Store batch function to avoid circular dependency
42
+ let batchFn: ((cb: () => any, type: 'immediate') => any) | undefined
43
+
44
+ /**
45
+ * Check the asyncMode option and hook Promise.prototype once if enabled
46
+ * Called lazily on first effect creation
47
+ * asyncMode being truthy enables async zone, false disables it
48
+ *
49
+ * @param batch - Optional batch function injection from effects.ts to avoid circular dependency
50
+ */
51
+ export function ensureZoneHooked(batch?: (cb: () => any, type: 'immediate') => any) {
52
+ if (batch) batchFn = batch
53
+ if (zoneHooked || !options.asyncMode) return
54
+ hookZone()
55
+ zoneHooked = true
56
+ }
57
+
58
+ /**
59
+ * Hook Promise.prototype methods to preserve effect context
60
+ */
61
+ function hookZone() {
62
+ // biome-ignore lint/suspicious/noThenProperty: Intentional wrapping for zone functionality
63
+ Promise.prototype.then = function <T, R1, R2>(
64
+ this: Promise<T>,
65
+ onFulfilled?: ((value: T) => R1 | PromiseLike<R1>) | null,
66
+ onRejected?: ((reason: any) => R2 | PromiseLike<R2>) | null
67
+ ): Promise<R1 | R2> {
68
+ const capturedStack = captureEffectStack()
69
+ return originalPromiseThen.call(
70
+ this,
71
+ wrapCallback(onFulfilled, capturedStack),
72
+ wrapCallback(onRejected, capturedStack)
73
+ )
74
+ }
75
+
76
+ Promise.prototype.catch = function <T>(
77
+ this: Promise<T>,
78
+ onRejected?: ((reason: any) => T | PromiseLike<T>) | null
79
+ ): Promise<T> {
80
+ const capturedStack = captureEffectStack()
81
+ return originalPromiseCatch.call(this, wrapCallback(onRejected, capturedStack))
82
+ }
83
+
84
+ Promise.prototype.finally = function <T>(
85
+ this: Promise<T>,
86
+ onFinally?: (() => void) | null
87
+ ): Promise<T> {
88
+ const capturedStack = captureEffectStack()
89
+ return originalPromiseFinally.call(this, wrapCallback(onFinally, capturedStack))
90
+ }
91
+
92
+ // Hook setTimeout - preserve original function properties for Node.js compatibility
93
+ const wrappedSetTimeout = (<TArgs extends any[]>(
94
+ callback: (...args: TArgs) => void,
95
+ delay?: number,
96
+ ...args: TArgs
97
+ ): ReturnType<typeof originalSetTimeout> => {
98
+ const capturedStack = options.zones.setTimeout ? captureEffectStack() : undefined
99
+ return originalSetTimeout.apply(globalThis, [
100
+ wrapCallback(callback, capturedStack) as (...args: any[]) => void,
101
+ delay,
102
+ ...args,
103
+ ] as any)
104
+ }) as typeof originalSetTimeout
105
+ Object.assign(wrappedSetTimeout, originalSetTimeout)
106
+ globalThis.setTimeout = wrappedSetTimeout
107
+
108
+ // Hook setInterval - preserve original function properties for Node.js compatibility
109
+ const wrappedSetInterval = (<TArgs extends any[]>(
110
+ callback: (...args: TArgs) => void,
111
+ delay?: number,
112
+ ...args: TArgs
113
+ ): ReturnType<typeof originalSetInterval> => {
114
+ const capturedStack = options.zones.setInterval ? captureEffectStack() : undefined
115
+ return originalSetInterval.apply(globalThis, [
116
+ wrapCallback(callback, capturedStack) as (...args: any[]) => void,
117
+ delay,
118
+ ...args,
119
+ ] as any)
120
+ }) as typeof originalSetInterval
121
+ Object.assign(wrappedSetInterval, originalSetInterval)
122
+ globalThis.setInterval = wrappedSetInterval
123
+
124
+ // Hook requestAnimationFrame if available
125
+ if (originalRequestAnimationFrame) {
126
+ globalThis.requestAnimationFrame = ((
127
+ callback: FrameRequestCallback
128
+ ): ReturnType<typeof originalRequestAnimationFrame> => {
129
+ const capturedStack = options.zones.requestAnimationFrame ? captureEffectStack() : undefined
130
+ return originalRequestAnimationFrame.call(
131
+ globalThis,
132
+ wrapCallback(callback as any, capturedStack) as FrameRequestCallback
133
+ )
134
+ }) as typeof originalRequestAnimationFrame
135
+ }
136
+
137
+ // Hook queueMicrotask if available
138
+ if (originalQueueMicrotask) {
139
+ globalThis.queueMicrotask = ((callback: () => void): void => {
140
+ const capturedStack = options.zones.queueMicrotask ? captureEffectStack() : undefined
141
+ originalQueueMicrotask.call(globalThis, wrapCallback(callback, capturedStack) as () => void)
142
+ }) as typeof originalQueueMicrotask
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Wraps a callback to restore effect context and ensure batching
148
+ */
149
+ function wrapCallback<T extends (...args: any[]) => any>(
150
+ callback: T | null | undefined,
151
+ capturedStack: (ScopedCallback | undefined)[] | undefined
152
+ ): T | undefined {
153
+ if (!callback) return undefined
154
+
155
+ // If no stack to restore and no batch function, direct call (optimization)
156
+ if ((!capturedStack || !capturedStack.length) && !batchFn) {
157
+ return callback
158
+ }
159
+
160
+ return ((...args: any[]) => {
161
+ const execute = () => {
162
+ if (capturedStack?.length) {
163
+ return withEffectStack(capturedStack, () => callback(...args))
164
+ }
165
+ return callback(...args)
166
+ }
167
+
168
+ if (batchFn) {
169
+ return batchFn(execute, 'immediate')
170
+ }
171
+ return execute()
172
+ }) as T
173
+ }
174
+
175
+ /**
176
+ * Manually enable/disable the zone (for testing)
177
+ */
178
+ export function setZoneEnabled(enabled: boolean) {
179
+ if (enabled && !zoneHooked) {
180
+ hookZone()
181
+ zoneHooked = true
182
+ } else if (!enabled && zoneHooked) {
183
+ // Restore original Promise methods
184
+ // biome-ignore lint/suspicious/noThenProperty: Restoring original methods
185
+ Promise.prototype.then = originalPromiseThen
186
+ Promise.prototype.catch = originalPromiseCatch
187
+ Promise.prototype.finally = originalPromiseFinally
188
+
189
+ // Restore original timer functions
190
+ globalThis.setTimeout = originalSetTimeout
191
+ globalThis.setInterval = originalSetInterval
192
+ if (originalRequestAnimationFrame) {
193
+ globalThis.requestAnimationFrame = originalRequestAnimationFrame
194
+ }
195
+ if (originalQueueMicrotask) {
196
+ globalThis.queueMicrotask = originalQueueMicrotask
197
+ }
198
+
199
+ zoneHooked = false
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Check if zone is currently hooked
205
+ */
206
+ export function isZoneEnabled(): boolean {
207
+ return zoneHooked
208
+ }