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,75 @@
|
|
|
1
|
+
export { getState, touched, touched1 } from './change'
|
|
2
|
+
export type { ReactivityGraph } from './debug'
|
|
3
|
+
export {
|
|
4
|
+
buildReactivityGraph,
|
|
5
|
+
enableDevTools,
|
|
6
|
+
isDevtoolsEnabled,
|
|
7
|
+
registerEffectForDebug,
|
|
8
|
+
registerObjectForDebug,
|
|
9
|
+
setEffectName,
|
|
10
|
+
setObjectName,
|
|
11
|
+
} from './debug'
|
|
12
|
+
export { deepWatch } from './deep-watch'
|
|
13
|
+
export {
|
|
14
|
+
addBatchCleanup,
|
|
15
|
+
atomic,
|
|
16
|
+
biDi,
|
|
17
|
+
defer,
|
|
18
|
+
effect,
|
|
19
|
+
getActiveEffect,
|
|
20
|
+
root,
|
|
21
|
+
trackEffect,
|
|
22
|
+
untracked,
|
|
23
|
+
} from './effects'
|
|
24
|
+
export { cleanedBy, cleanup, derived, unreactive, watch } from './interface'
|
|
25
|
+
export { mapped, ReadOnlyError, reduced } from './mapped'
|
|
26
|
+
export { type Memoizable, memoize } from './memoize'
|
|
27
|
+
export { immutables, isNonReactive, registerNativeReactivity } from './non-reactive'
|
|
28
|
+
export { project } from './project'
|
|
29
|
+
export { isReactive, ReactiveBase, reactive, unwrap } from './proxy'
|
|
30
|
+
export { organize, organized } from './record'
|
|
31
|
+
export { Register, register } from './register'
|
|
32
|
+
export {
|
|
33
|
+
type DependencyAccess,
|
|
34
|
+
type DependencyFunction,
|
|
35
|
+
type Evolution,
|
|
36
|
+
options as reactiveOptions,
|
|
37
|
+
ReactiveError,
|
|
38
|
+
type ScopedCallback,
|
|
39
|
+
} from './types'
|
|
40
|
+
export { isZoneEnabled, setZoneEnabled } from './zone'
|
|
41
|
+
|
|
42
|
+
import { ReactiveArray } from './array'
|
|
43
|
+
import {
|
|
44
|
+
deepWatchers,
|
|
45
|
+
effectToDeepWatchedObjects,
|
|
46
|
+
objectParents,
|
|
47
|
+
objectsWithDeepWatchers,
|
|
48
|
+
} from './deep-watch'
|
|
49
|
+
import { ReactiveMap, ReactiveWeakMap } from './map'
|
|
50
|
+
import { nonReactiveObjects, registerNativeReactivity } from './non-reactive-state'
|
|
51
|
+
import { objectToProxy, proxyToObject } from './proxy'
|
|
52
|
+
import { ReactiveSet, ReactiveWeakSet } from './set'
|
|
53
|
+
import { effectToReactiveObjects, watchers } from './tracking'
|
|
54
|
+
|
|
55
|
+
// Register native collection types to use specialized reactive wrappers
|
|
56
|
+
registerNativeReactivity(WeakMap, ReactiveWeakMap)
|
|
57
|
+
registerNativeReactivity(Map, ReactiveMap)
|
|
58
|
+
registerNativeReactivity(WeakSet, ReactiveWeakSet)
|
|
59
|
+
registerNativeReactivity(Set, ReactiveSet)
|
|
60
|
+
registerNativeReactivity(Array, ReactiveArray)
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Object containing internal reactive system state for debugging and profiling
|
|
64
|
+
*/
|
|
65
|
+
export const profileInfo: any = {
|
|
66
|
+
objectToProxy,
|
|
67
|
+
proxyToObject,
|
|
68
|
+
effectToReactiveObjects,
|
|
69
|
+
watchers,
|
|
70
|
+
objectParents,
|
|
71
|
+
objectsWithDeepWatchers,
|
|
72
|
+
deepWatchers,
|
|
73
|
+
effectToDeepWatchedObjects,
|
|
74
|
+
nonReactiveObjects,
|
|
75
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { decorator, GenericClassDecorator } from '../decorator'
|
|
2
|
+
import { deepWatch } from './deep-watch'
|
|
3
|
+
import { withEffect } from './effect-context'
|
|
4
|
+
import { effect, getActiveEffect, untracked } from './effects'
|
|
5
|
+
import { isNonReactive, nonReactiveClass, nonReactiveObjects } from './non-reactive-state'
|
|
6
|
+
import { unwrap } from './proxy-state'
|
|
7
|
+
import { dependant, markWithRoot } from './tracking'
|
|
8
|
+
import {
|
|
9
|
+
type DependencyAccess,
|
|
10
|
+
nonReactiveMark,
|
|
11
|
+
type ScopedCallback,
|
|
12
|
+
unreactiveProperties,
|
|
13
|
+
} from './types'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Symbol for accessing the cleanup function on cleaned objects
|
|
17
|
+
*/
|
|
18
|
+
export const cleanup = Symbol('cleanup')
|
|
19
|
+
|
|
20
|
+
//#region watch
|
|
21
|
+
|
|
22
|
+
const unsetYet = Symbol('unset-yet')
|
|
23
|
+
/**
|
|
24
|
+
* Options for the watch function
|
|
25
|
+
*/
|
|
26
|
+
export interface WatchOptions {
|
|
27
|
+
/** Whether to call the callback immediately */
|
|
28
|
+
immediate?: boolean
|
|
29
|
+
/** Whether to watch nested properties */
|
|
30
|
+
deep?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Watches a reactive value and calls a callback when it changes
|
|
35
|
+
* @param value - Function that returns the value to watch
|
|
36
|
+
* @param changed - Callback to call when the value changes
|
|
37
|
+
* @param options - Watch options
|
|
38
|
+
* @returns Cleanup function to stop watching
|
|
39
|
+
*/
|
|
40
|
+
export function watch<T>(
|
|
41
|
+
value: (dep: DependencyAccess) => T,
|
|
42
|
+
changed: (value: T, oldValue?: T) => void,
|
|
43
|
+
options?: Omit<WatchOptions, 'deep'> & { deep?: false }
|
|
44
|
+
): ScopedCallback
|
|
45
|
+
/**
|
|
46
|
+
* Watches a reactive value with deep watching enabled
|
|
47
|
+
* @param value - Function that returns the value to watch
|
|
48
|
+
* @param changed - Callback to call when the value changes
|
|
49
|
+
* @param options - Watch options with deep watching enabled
|
|
50
|
+
* @returns Cleanup function to stop watching
|
|
51
|
+
*/
|
|
52
|
+
export function watch<T extends object | any[]>(
|
|
53
|
+
value: (dep: DependencyAccess) => T,
|
|
54
|
+
changed: (value: T, oldValue?: T) => void,
|
|
55
|
+
options?: Omit<WatchOptions, 'deep'> & { deep: true }
|
|
56
|
+
): ScopedCallback
|
|
57
|
+
/**
|
|
58
|
+
* Watches a reactive object directly
|
|
59
|
+
* @param value - The reactive object to watch
|
|
60
|
+
* @param changed - Callback to call when the object changes
|
|
61
|
+
* @param options - Watch options
|
|
62
|
+
* @returns Cleanup function to stop watching
|
|
63
|
+
*/
|
|
64
|
+
export function watch<T extends object | any[]>(
|
|
65
|
+
value: T,
|
|
66
|
+
changed: (value: T) => void,
|
|
67
|
+
options?: WatchOptions
|
|
68
|
+
): ScopedCallback
|
|
69
|
+
|
|
70
|
+
export function watch(
|
|
71
|
+
value: any, //object | ((dep: DependencyAccess) => object),
|
|
72
|
+
changed: (value?: object, oldValue?: object) => void,
|
|
73
|
+
options: any = {}
|
|
74
|
+
) {
|
|
75
|
+
return typeof value === 'function'
|
|
76
|
+
? watchCallBack(value, changed, options)
|
|
77
|
+
: typeof value === 'object' && value !== null
|
|
78
|
+
? watchObject(value, changed, options)
|
|
79
|
+
: (() => {
|
|
80
|
+
throw new Error('watch: value must be a function or an object')
|
|
81
|
+
})()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function watchObject(
|
|
85
|
+
value: object,
|
|
86
|
+
changed: (value: object) => void,
|
|
87
|
+
{ immediate = false, deep = false } = {}
|
|
88
|
+
): ScopedCallback {
|
|
89
|
+
const myParentEffect = getActiveEffect()
|
|
90
|
+
if (deep) return deepWatch(value, changed, { immediate })!
|
|
91
|
+
return effect(
|
|
92
|
+
markWithRoot(function watchObjectEffect() {
|
|
93
|
+
dependant(value)
|
|
94
|
+
if (immediate) withEffect(myParentEffect, () => changed(value))
|
|
95
|
+
immediate = true
|
|
96
|
+
}, changed)
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function watchCallBack<T>(
|
|
101
|
+
value: (dep: DependencyAccess) => T,
|
|
102
|
+
changed: (value: T, oldValue?: T) => void,
|
|
103
|
+
{ immediate = false, deep = false } = {}
|
|
104
|
+
): ScopedCallback {
|
|
105
|
+
const myParentEffect = getActiveEffect()
|
|
106
|
+
let oldValue: T | typeof unsetYet = unsetYet
|
|
107
|
+
let deepCleanup: ScopedCallback | undefined
|
|
108
|
+
const cbCleanup = effect(
|
|
109
|
+
markWithRoot(function watchCallBackEffect(access) {
|
|
110
|
+
const newValue = value(access)
|
|
111
|
+
if (oldValue !== newValue)
|
|
112
|
+
withEffect(
|
|
113
|
+
myParentEffect,
|
|
114
|
+
markWithRoot(() => {
|
|
115
|
+
if (oldValue === unsetYet) {
|
|
116
|
+
if (immediate) changed(newValue)
|
|
117
|
+
} else changed(newValue, oldValue)
|
|
118
|
+
oldValue = newValue
|
|
119
|
+
if (deep) {
|
|
120
|
+
if (deepCleanup) deepCleanup()
|
|
121
|
+
deepCleanup = deepWatch(
|
|
122
|
+
newValue as object,
|
|
123
|
+
markWithRoot((value) => changed(value as T, value as T), changed)
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
}, changed)
|
|
127
|
+
)
|
|
128
|
+
}, value)
|
|
129
|
+
)
|
|
130
|
+
return () => {
|
|
131
|
+
cbCleanup()
|
|
132
|
+
if (deepCleanup) deepCleanup()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
//#endregion
|
|
137
|
+
|
|
138
|
+
//#region nonReactive
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Mark an object as non-reactive. This object and all its properties will never be made reactive.
|
|
142
|
+
* @param obj - The object to mark as non-reactive
|
|
143
|
+
*/
|
|
144
|
+
function deepNonReactive<T>(obj: T): T {
|
|
145
|
+
obj = unwrap(obj)
|
|
146
|
+
if (isNonReactive(obj)) return obj
|
|
147
|
+
try {
|
|
148
|
+
Object.defineProperty(obj as object, nonReactiveMark, {
|
|
149
|
+
value: true,
|
|
150
|
+
writable: false,
|
|
151
|
+
enumerable: false,
|
|
152
|
+
configurable: true,
|
|
153
|
+
})
|
|
154
|
+
} catch {}
|
|
155
|
+
if (!(nonReactiveMark in (obj as object))) nonReactiveObjects.add(obj as object)
|
|
156
|
+
//for (const key in obj) deepNonReactive(obj[key])
|
|
157
|
+
return obj
|
|
158
|
+
}
|
|
159
|
+
function unreactiveApplication<T extends object>(...args: (keyof T)[]): GenericClassDecorator<T>
|
|
160
|
+
function unreactiveApplication<T extends object>(obj: T): T
|
|
161
|
+
function unreactiveApplication<T extends object>(
|
|
162
|
+
arg1: T | keyof T,
|
|
163
|
+
...args: (keyof T)[]
|
|
164
|
+
): GenericClassDecorator<T> | T {
|
|
165
|
+
return typeof arg1 === 'object'
|
|
166
|
+
? deepNonReactive(arg1)
|
|
167
|
+
: (((original) => {
|
|
168
|
+
// Copy the parent's unreactive properties if they exist
|
|
169
|
+
original.prototype[unreactiveProperties] = new Set<PropertyKey>(
|
|
170
|
+
original.prototype[unreactiveProperties] || []
|
|
171
|
+
)
|
|
172
|
+
// Add all arguments (including the first one)
|
|
173
|
+
original.prototype[unreactiveProperties].add(arg1)
|
|
174
|
+
for (const arg of args) original.prototype[unreactiveProperties].add(arg)
|
|
175
|
+
return original // Return the class
|
|
176
|
+
}) as GenericClassDecorator<T>)
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Decorator that marks classes or properties as non-reactive
|
|
180
|
+
* Prevents objects from being made reactive
|
|
181
|
+
*/
|
|
182
|
+
export const unreactive = decorator({
|
|
183
|
+
class(original) {
|
|
184
|
+
// Called without arguments, mark entire class as non-reactive
|
|
185
|
+
nonReactiveClass(original)
|
|
186
|
+
},
|
|
187
|
+
default: unreactiveApplication,
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
//#endregion
|
|
191
|
+
|
|
192
|
+
export function cleanedBy<T extends object>(obj: T, cleanupFn: ScopedCallback) {
|
|
193
|
+
return Object.defineProperty(obj, cleanup, {
|
|
194
|
+
value: cleanupFn,
|
|
195
|
+
writable: false,
|
|
196
|
+
enumerable: false,
|
|
197
|
+
configurable: true,
|
|
198
|
+
}) as T & { [cleanup]: ScopedCallback }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
//#region greedy caching
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Creates a derived value that automatically recomputes when dependencies change
|
|
205
|
+
* @param compute - Function that computes the derived value
|
|
206
|
+
* @returns Object with value and cleanup function
|
|
207
|
+
*/
|
|
208
|
+
export function derived<T>(compute: (dep: DependencyAccess) => T): {
|
|
209
|
+
value: T
|
|
210
|
+
[cleanup]: ScopedCallback
|
|
211
|
+
} {
|
|
212
|
+
const rv = { value: undefined as unknown as T }
|
|
213
|
+
return cleanedBy(
|
|
214
|
+
rv,
|
|
215
|
+
untracked(() =>
|
|
216
|
+
effect(
|
|
217
|
+
markWithRoot(function derivedEffect(access) {
|
|
218
|
+
rv.value = compute(access)
|
|
219
|
+
}, compute)
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { touched, touched1 } from './change'
|
|
2
|
+
import { notifyPropertyChange } from './deep-touch'
|
|
3
|
+
import { makeReactiveEntriesIterator, makeReactiveIterator } from './non-reactive'
|
|
4
|
+
import { reactive } from './proxy'
|
|
5
|
+
import { dependant } from './tracking'
|
|
6
|
+
import { prototypeForwarding } from './types'
|
|
7
|
+
|
|
8
|
+
const native = Symbol('native')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Reactive wrapper around JavaScript's WeakMap class
|
|
12
|
+
* Only tracks individual key operations, no size tracking (WeakMap limitation)
|
|
13
|
+
*/
|
|
14
|
+
export class ReactiveWeakMap<K extends object, V> {
|
|
15
|
+
declare readonly [native]: WeakMap<K, V>
|
|
16
|
+
declare readonly content: symbol
|
|
17
|
+
constructor(original: WeakMap<K, V>) {
|
|
18
|
+
Object.defineProperties(this, {
|
|
19
|
+
[native]: { value: original },
|
|
20
|
+
[prototypeForwarding]: { value: original },
|
|
21
|
+
content: { value: Symbol('content') },
|
|
22
|
+
[Symbol.toStringTag]: { value: 'ReactiveWeakMap' },
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Implement WeakMap interface methods with reactivity
|
|
27
|
+
delete(key: K): boolean {
|
|
28
|
+
const hadKey = this[native].has(key)
|
|
29
|
+
const result = this[native].delete(key)
|
|
30
|
+
|
|
31
|
+
if (hadKey) touched1(this.content, { type: 'del', prop: key }, key)
|
|
32
|
+
|
|
33
|
+
return result
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get(key: K): V | undefined {
|
|
37
|
+
dependant(this.content, key)
|
|
38
|
+
return reactive(this[native].get(key))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
has(key: K): boolean {
|
|
42
|
+
dependant(this.content, key)
|
|
43
|
+
return this[native].has(key)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
set(key: K, value: V): this {
|
|
47
|
+
const hadKey = this[native].has(key)
|
|
48
|
+
const oldValue = this[native].get(key)
|
|
49
|
+
const reactiveValue = reactive(value)
|
|
50
|
+
this[native].set(key, reactiveValue)
|
|
51
|
+
|
|
52
|
+
if (!hadKey || oldValue !== reactiveValue) {
|
|
53
|
+
notifyPropertyChange(this.content, key, oldValue, reactiveValue, hadKey)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return this
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reactive wrapper around JavaScript's Map class
|
|
62
|
+
* Tracks size changes, individual key operations, and collection-wide operations
|
|
63
|
+
*/
|
|
64
|
+
export class ReactiveMap<K, V> {
|
|
65
|
+
declare readonly [native]: Map<K, V>
|
|
66
|
+
declare readonly content: symbol
|
|
67
|
+
|
|
68
|
+
constructor(original: Map<K, V>) {
|
|
69
|
+
Object.defineProperties(this, {
|
|
70
|
+
[native]: { value: original },
|
|
71
|
+
[prototypeForwarding]: { value: original },
|
|
72
|
+
content: { value: Symbol('content') },
|
|
73
|
+
[Symbol.toStringTag]: { value: 'ReactiveMap' },
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Implement Map interface methods with reactivity
|
|
78
|
+
get size(): number {
|
|
79
|
+
dependant(this, 'size') // The ReactiveMap instance still goes through proxy
|
|
80
|
+
return this[native].size
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
clear(): void {
|
|
84
|
+
const hadEntries = this[native].size > 0
|
|
85
|
+
this[native].clear()
|
|
86
|
+
|
|
87
|
+
if (hadEntries) {
|
|
88
|
+
const evolution = { type: 'bunch', method: 'clear' } as const
|
|
89
|
+
// Clear triggers all effects since all keys are affected
|
|
90
|
+
touched1(this, evolution, 'size')
|
|
91
|
+
touched(this.content, evolution)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
entries(): Generator<[K, V]> {
|
|
96
|
+
dependant(this.content)
|
|
97
|
+
return makeReactiveEntriesIterator(this[native].entries())
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void {
|
|
101
|
+
dependant(this.content)
|
|
102
|
+
this[native].forEach(callbackfn, thisArg)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
keys(): MapIterator<K> {
|
|
106
|
+
dependant(this.content)
|
|
107
|
+
return this[native].keys()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
values(): Generator<V> {
|
|
111
|
+
dependant(this.content)
|
|
112
|
+
return makeReactiveIterator(this[native].values())
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
[Symbol.iterator](): Iterator<[K, V]> {
|
|
116
|
+
dependant(this.content)
|
|
117
|
+
const nativeIterator = this[native][Symbol.iterator]()
|
|
118
|
+
return {
|
|
119
|
+
next() {
|
|
120
|
+
const result = nativeIterator.next()
|
|
121
|
+
if (result.done) {
|
|
122
|
+
return result
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
value: [result.value[0], reactive(result.value[1])],
|
|
126
|
+
done: false,
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Implement Map methods with reactivity
|
|
133
|
+
delete(key: K): boolean {
|
|
134
|
+
const hadKey = this[native].has(key)
|
|
135
|
+
const result = this[native].delete(key)
|
|
136
|
+
|
|
137
|
+
if (hadKey) {
|
|
138
|
+
const evolution = { type: 'del', prop: key } as const
|
|
139
|
+
touched1(this.content, evolution, key)
|
|
140
|
+
touched1(this, evolution, 'size')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return result
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get(key: K): V | undefined {
|
|
147
|
+
dependant(this.content, key)
|
|
148
|
+
return reactive(this[native].get(key))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
has(key: K): boolean {
|
|
152
|
+
dependant(this.content, key)
|
|
153
|
+
return this[native].has(key)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
set(key: K, value: V): this {
|
|
157
|
+
const hadKey = this[native].has(key)
|
|
158
|
+
const oldValue = this[native].get(key)
|
|
159
|
+
const reactiveValue = reactive(value)
|
|
160
|
+
this[native].set(key, reactiveValue)
|
|
161
|
+
|
|
162
|
+
if (!hadKey || oldValue !== reactiveValue) {
|
|
163
|
+
notifyPropertyChange(this.content, key, oldValue, reactiveValue, hadKey)
|
|
164
|
+
// Also notify size change for Map (WeakMap doesn't track size)
|
|
165
|
+
const evolution = { type: hadKey ? 'set' : 'add', prop: key } as const
|
|
166
|
+
touched1(this, evolution, 'size')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return this
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Indexable } from '../indexable'
|
|
2
|
+
import { native, ReactiveBaseArray } from './array'
|
|
3
|
+
import { touched, touched1 } from './change'
|
|
4
|
+
import { effect, untracked } from './effects'
|
|
5
|
+
import { cleanedBy } from './interface'
|
|
6
|
+
import { reactive } from './proxy'
|
|
7
|
+
import { dependant } from './tracking'
|
|
8
|
+
import { prototypeForwarding, ScopedCallback } from './types'
|
|
9
|
+
|
|
10
|
+
// TODO: Lazy reactivity ?
|
|
11
|
+
export class ReadOnlyError extends Error {}
|
|
12
|
+
/**
|
|
13
|
+
* Reactive wrapper around JavaScript's Array class with full array method support
|
|
14
|
+
* Tracks length changes, individual index operations, and collection-wide operations
|
|
15
|
+
*/
|
|
16
|
+
class ReactiveReadOnlyArrayClass extends Indexable(ReactiveBaseArray, {
|
|
17
|
+
get(i: number): any {
|
|
18
|
+
dependant(this, i)
|
|
19
|
+
return reactive(this[native][i])
|
|
20
|
+
},
|
|
21
|
+
set(i: number, _value: any) {
|
|
22
|
+
throw new ReadOnlyError(`Setting index ${i} on a read-only array`)
|
|
23
|
+
},
|
|
24
|
+
getLength() {
|
|
25
|
+
dependant(this, 'length')
|
|
26
|
+
return this[native].length
|
|
27
|
+
},
|
|
28
|
+
setLength(value: number) {
|
|
29
|
+
throw new ReadOnlyError(`Setting length to ${value} on a read-only array`)
|
|
30
|
+
},
|
|
31
|
+
}) {
|
|
32
|
+
declare length: number
|
|
33
|
+
constructor(original: any[]) {
|
|
34
|
+
super()
|
|
35
|
+
Object.defineProperties(this, {
|
|
36
|
+
// We have to make it double, as [native] must be `unique symbol` - impossible through import
|
|
37
|
+
[native]: { value: original },
|
|
38
|
+
[prototypeForwarding]: { value: original },
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
push(..._items: any[]) {
|
|
43
|
+
throw new ReadOnlyError(`Pushing items to a read-only array`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pop() {
|
|
47
|
+
throw new ReadOnlyError(`Popping from a read-only array`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
shift() {
|
|
51
|
+
throw new ReadOnlyError(`Shifting from a read-only array`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
unshift(..._items: any[]) {
|
|
55
|
+
throw new ReadOnlyError(`Unshifting items to a read-only array`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
splice(_start: number, _deleteCount?: number, ..._items: any[]) {
|
|
59
|
+
throw new ReadOnlyError(`Splice from a read-only array`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
reverse() {
|
|
63
|
+
throw new ReadOnlyError(`Reversing a read-only array`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
sort(_compareFn?: (a: any, b: any) => number) {
|
|
67
|
+
throw new ReadOnlyError(`Sorting a read-only array`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fill(_value: any, _start?: number, _end?: number) {
|
|
71
|
+
throw new ReadOnlyError(`Filling a read-only array`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
copyWithin(_target: number, _start: number, _end?: number) {
|
|
75
|
+
throw new ReadOnlyError(`Copying within a read-only array`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const ReactiveReadOnlyArray = reactive(ReactiveReadOnlyArrayClass)
|
|
80
|
+
export type ReactiveReadOnlyArray<T> = readonly T[]
|
|
81
|
+
export function mapped<T, U>(
|
|
82
|
+
inputs: readonly T[],
|
|
83
|
+
compute: (input: T, index: number, output: U[]) => U,
|
|
84
|
+
resize?: (newLength: number, oldLength: number) => void
|
|
85
|
+
): readonly U[] {
|
|
86
|
+
const result: U[] = []
|
|
87
|
+
const resultReactive = new ReactiveReadOnlyArray(result)
|
|
88
|
+
const cleanups: ScopedCallback[] = []
|
|
89
|
+
function input(index: number) {
|
|
90
|
+
return effect(function computedIndexedMapInputEffect() {
|
|
91
|
+
result[index] = compute(inputs[index], index, resultReactive)
|
|
92
|
+
touched1(resultReactive, { type: 'set', prop: index }, index)
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
const cleanupLength = effect(function computedMapLengthEffect({ ascend }) {
|
|
96
|
+
const length = inputs.length
|
|
97
|
+
const resultLength = untracked(() => result.length)
|
|
98
|
+
resize?.(length, resultLength)
|
|
99
|
+
touched1(resultReactive, { type: 'set', prop: 'length' }, 'length')
|
|
100
|
+
if (length < resultLength) {
|
|
101
|
+
const toCleanup = cleanups.splice(length)
|
|
102
|
+
for (const cleanup of toCleanup) cleanup()
|
|
103
|
+
result.length = length
|
|
104
|
+
} else if (length > resultLength)
|
|
105
|
+
// the input effects will be registered as the call's children, so they will remain not cleaned with this effect on length
|
|
106
|
+
ascend(function computedMapNewElements() {
|
|
107
|
+
for (let i = resultLength; i < length; i++) cleanups.push(input(i))
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
return cleanedBy(resultReactive, () => {
|
|
111
|
+
for (const cleanup of cleanups) cleanup()
|
|
112
|
+
cleanups.length = 0
|
|
113
|
+
cleanupLength()
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function reduced<T, U, R extends object = any>(
|
|
118
|
+
inputs: readonly T[],
|
|
119
|
+
compute: (input: T, factor: R) => readonly U[]
|
|
120
|
+
): readonly U[] {
|
|
121
|
+
const result: U[] = []
|
|
122
|
+
const resultReactive = new ReactiveReadOnlyArray(result)
|
|
123
|
+
const cleanupFactor = effect(function computedReducedFactorEffect() {
|
|
124
|
+
const factor: R = {} as R
|
|
125
|
+
result.length = 0
|
|
126
|
+
for (const input of inputs) result.push(...compute(input, factor))
|
|
127
|
+
touched(resultReactive, { type: 'invalidate', prop: 'reduced' })
|
|
128
|
+
})
|
|
129
|
+
return cleanedBy(resultReactive, cleanupFactor)
|
|
130
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { decorator } from '../decorator'
|
|
2
|
+
import { renamed } from '../utils'
|
|
3
|
+
import { touched1 } from './change'
|
|
4
|
+
import { effect, root } from './effects'
|
|
5
|
+
import { dependant, getRoot, markWithRoot } from './tracking'
|
|
6
|
+
|
|
7
|
+
export type Memoizable = object | any[] | symbol | ((...args: any[]) => any)
|
|
8
|
+
|
|
9
|
+
type MemoCacheTree<Result> = {
|
|
10
|
+
result?: Result
|
|
11
|
+
cleanup?: () => void
|
|
12
|
+
branches?: WeakMap<Memoizable, MemoCacheTree<Result>>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const memoizedRegistry = new WeakMap<Function, Function>()
|
|
16
|
+
|
|
17
|
+
function getBranch<Result>(tree: MemoCacheTree<Result>, key: Memoizable): MemoCacheTree<Result> {
|
|
18
|
+
tree.branches ??= new WeakMap()
|
|
19
|
+
let branch = tree.branches.get(key)
|
|
20
|
+
if (!branch) {
|
|
21
|
+
branch = {}
|
|
22
|
+
tree.branches.set(key, branch)
|
|
23
|
+
}
|
|
24
|
+
return branch
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function memoizeFunction<Result, Args extends Memoizable[]>(
|
|
28
|
+
fn: (...args: Args) => Result
|
|
29
|
+
): (...args: Args) => Result {
|
|
30
|
+
const fnRoot = getRoot(fn)
|
|
31
|
+
const existing = memoizedRegistry.get(fnRoot)
|
|
32
|
+
if (existing) return existing as (...args: Args) => Result
|
|
33
|
+
|
|
34
|
+
const cacheRoot: MemoCacheTree<Result> = {}
|
|
35
|
+
const memoized = markWithRoot((...args: Args): Result => {
|
|
36
|
+
const localArgs = args //: Args = maxArgs !== undefined ? (args.slice(0, maxArgs) as Args) : args
|
|
37
|
+
if (localArgs.some((arg) => !(arg && ['object', 'symbol', 'function'].includes(typeof arg))))
|
|
38
|
+
throw new Error('memoize expects non-null object arguments')
|
|
39
|
+
|
|
40
|
+
let node: MemoCacheTree<Result> = cacheRoot
|
|
41
|
+
for (const arg of localArgs) {
|
|
42
|
+
node = getBranch(node, arg)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
dependant(node, 'memoize')
|
|
46
|
+
if ('result' in node) return node.result!
|
|
47
|
+
|
|
48
|
+
// Create memoize internal effect to track dependencies and invalidate cache
|
|
49
|
+
// Use untracked to prevent the effect creation from being affected by parent effects
|
|
50
|
+
node.cleanup = root(() =>
|
|
51
|
+
effect(
|
|
52
|
+
markWithRoot(() => {
|
|
53
|
+
// Execute the function and track its dependencies
|
|
54
|
+
// The function execution will automatically track dependencies on reactive objects
|
|
55
|
+
node.result = fn(...localArgs)
|
|
56
|
+
return () => {
|
|
57
|
+
// When dependencies change, clear the cache and notify consumers
|
|
58
|
+
delete node.result
|
|
59
|
+
touched1(node, { type: 'invalidate', prop: localArgs }, 'memoize')
|
|
60
|
+
}
|
|
61
|
+
}, fnRoot),
|
|
62
|
+
{ opaque: true }
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
return node.result!
|
|
66
|
+
}, fn)
|
|
67
|
+
|
|
68
|
+
memoizedRegistry.set(fnRoot, memoized)
|
|
69
|
+
memoizedRegistry.set(memoized, memoized)
|
|
70
|
+
return memoized as (...args: Args) => Result
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const memoize = decorator({
|
|
74
|
+
getter(original, propertyKey) {
|
|
75
|
+
const memoized = memoizeFunction(
|
|
76
|
+
markWithRoot(
|
|
77
|
+
renamed(
|
|
78
|
+
(that: object) => {
|
|
79
|
+
return original.call(that)
|
|
80
|
+
},
|
|
81
|
+
`${String(this.constructor.name)}.${String(propertyKey)}`
|
|
82
|
+
),
|
|
83
|
+
original
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
return function (this: any) {
|
|
87
|
+
return memoized(this)
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
method(original, name) {
|
|
91
|
+
const memoized = memoizeFunction(
|
|
92
|
+
markWithRoot(
|
|
93
|
+
renamed(
|
|
94
|
+
(that: object, ...args: object[]) => {
|
|
95
|
+
return original.call(that, ...args)
|
|
96
|
+
},
|
|
97
|
+
`${String(this.constructor.name)}.${String(name)}`
|
|
98
|
+
),
|
|
99
|
+
original
|
|
100
|
+
)
|
|
101
|
+
) as (...args: object[]) => unknown
|
|
102
|
+
return function (this: any, ...args: object[]) {
|
|
103
|
+
return memoized(this, ...args)
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
default: memoizeFunction,
|
|
107
|
+
})
|