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.
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-DzUDtFc7.esm.js +4841 -0
  11. package/dist/chunks/index-DzUDtFc7.esm.js.map +1 -0
  12. package/dist/chunks/index-HNVqPzjz.js +4891 -0
  13. package/dist/chunks/index-HNVqPzjz.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 +36 -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 +601 -97
  47. package/dist/reactive.esm.js +3 -3
  48. package/dist/reactive.js +31 -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 +1333 -0
  78. package/src/reactive/index.ts +75 -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 +285 -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,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
+ })