mutts 1.0.2 → 1.0.3
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.
- package/README.md +14 -6
- package/dist/chunks/{_tslib-C-cuVLvZ.js → _tslib-BgjropY9.js} +9 -1
- package/dist/chunks/_tslib-BgjropY9.js.map +1 -0
- package/dist/chunks/{_tslib-CMEnd0VE.esm.js → _tslib-Mzh1rNsX.esm.js} +9 -2
- package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +1 -0
- package/dist/chunks/{decorator-D4DU97Zg.js → decorator-DLvrD0UF.js} +42 -19
- package/dist/chunks/decorator-DLvrD0UF.js.map +1 -0
- package/dist/chunks/{decorator-GnHw1Az7.esm.js → decorator-DqiszP7i.esm.js} +42 -19
- package/dist/chunks/decorator-DqiszP7i.esm.js.map +1 -0
- package/dist/chunks/index-DzUDtFc7.esm.js +4841 -0
- package/dist/chunks/index-DzUDtFc7.esm.js.map +1 -0
- package/dist/chunks/index-HNVqPzjz.js +4891 -0
- package/dist/chunks/index-HNVqPzjz.js.map +1 -0
- package/dist/decorator.esm.js +1 -1
- package/dist/decorator.js +1 -1
- package/dist/destroyable.d.ts +1 -1
- package/dist/destroyable.esm.js +1 -1
- package/dist/destroyable.esm.js.map +1 -1
- package/dist/destroyable.js +1 -1
- package/dist/destroyable.js.map +1 -1
- package/dist/devtools/devtools.html +9 -0
- package/dist/devtools/devtools.js +5 -0
- package/dist/devtools/devtools.js.map +1 -0
- package/dist/devtools/manifest.json +8 -0
- package/dist/devtools/panel.css +72 -0
- package/dist/devtools/panel.html +31 -0
- package/dist/devtools/panel.js +13048 -0
- package/dist/devtools/panel.js.map +1 -0
- package/dist/eventful.esm.js +1 -1
- package/dist/eventful.js +1 -1
- package/dist/index.d.ts +18 -63
- package/dist/index.esm.js +4 -4
- package/dist/index.js +36 -11
- package/dist/index.js.map +1 -1
- package/dist/indexable.d.ts +187 -1
- package/dist/indexable.esm.js +197 -3
- package/dist/indexable.esm.js.map +1 -1
- package/dist/indexable.js +198 -2
- package/dist/indexable.js.map +1 -1
- package/dist/mutts.umd.js +1 -1
- package/dist/mutts.umd.js.map +1 -1
- package/dist/mutts.umd.min.js +1 -1
- package/dist/mutts.umd.min.js.map +1 -1
- package/dist/promiseChain.esm.js.map +1 -1
- package/dist/promiseChain.js.map +1 -1
- package/dist/reactive.d.ts +601 -97
- package/dist/reactive.esm.js +3 -3
- package/dist/reactive.js +31 -10
- package/dist/reactive.js.map +1 -1
- package/dist/std-decorators.esm.js +1 -1
- package/dist/std-decorators.js +1 -1
- package/docs/ai/api-reference.md +133 -0
- package/docs/ai/manual.md +105 -0
- package/docs/iterableWeak.md +646 -0
- package/docs/reactive/advanced.md +1280 -0
- package/docs/reactive/collections.md +767 -0
- package/docs/reactive/core.md +973 -0
- package/docs/reactive.md +21 -9545
- package/package.json +18 -5
- package/src/decorator.ts +266 -0
- package/src/destroyable.ts +199 -0
- package/src/eventful.ts +77 -0
- package/src/index.d.ts +9 -0
- package/src/index.ts +9 -0
- package/src/indexable.ts +484 -0
- package/src/introspection.ts +59 -0
- package/src/iterableWeak.ts +233 -0
- package/src/mixins.ts +123 -0
- package/src/promiseChain.ts +110 -0
- package/src/reactive/array.ts +414 -0
- package/src/reactive/change.ts +134 -0
- package/src/reactive/debug.ts +517 -0
- package/src/reactive/deep-touch.ts +268 -0
- package/src/reactive/deep-watch-state.ts +82 -0
- package/src/reactive/deep-watch.ts +168 -0
- package/src/reactive/effect-context.ts +94 -0
- package/src/reactive/effects.ts +1333 -0
- package/src/reactive/index.ts +75 -0
- package/src/reactive/interface.ts +223 -0
- package/src/reactive/map.ts +171 -0
- package/src/reactive/mapped.ts +130 -0
- package/src/reactive/memoize.ts +107 -0
- package/src/reactive/non-reactive-state.ts +49 -0
- package/src/reactive/non-reactive.ts +43 -0
- package/src/reactive/project.project.md +93 -0
- package/src/reactive/project.ts +335 -0
- package/src/reactive/proxy-state.ts +27 -0
- package/src/reactive/proxy.ts +285 -0
- package/src/reactive/record.ts +196 -0
- package/src/reactive/register.ts +421 -0
- package/src/reactive/set.ts +144 -0
- package/src/reactive/tracking.ts +101 -0
- package/src/reactive/types.ts +358 -0
- package/src/reactive/zone.ts +208 -0
- package/src/std-decorators.ts +217 -0
- package/src/utils.ts +117 -0
- package/dist/chunks/_tslib-C-cuVLvZ.js.map +0 -1
- package/dist/chunks/_tslib-CMEnd0VE.esm.js.map +0 -1
- package/dist/chunks/decorator-D4DU97Zg.js.map +0 -1
- package/dist/chunks/decorator-GnHw1Az7.esm.js.map +0 -1
- package/dist/chunks/index-DBScoeCX.esm.js +0 -1960
- package/dist/chunks/index-DBScoeCX.esm.js.map +0 -1
- package/dist/chunks/index-DOTmXL89.js +0 -1983
- 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
|
+
}
|