mutts 1.0.7 → 1.0.9
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 +72 -17
- package/dist/browser.cjs +3111 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.ts +1391 -1
- package/dist/browser.esm.js +264 -35
- package/dist/browser.esm.js.map +1 -1
- package/dist/chunks/async-browser-lvzLOCgk.cjs +297 -0
- package/dist/chunks/async-browser-lvzLOCgk.cjs.map +1 -0
- package/dist/chunks/async-core-CRLKP3l-.cjs +29 -0
- package/dist/chunks/async-core-CRLKP3l-.cjs.map +1 -0
- package/dist/chunks/async-node-C3DeIb0y.cjs +96 -0
- package/dist/chunks/async-node-C3DeIb0y.cjs.map +1 -0
- package/dist/chunks/index-2vea86wD.esm.js +3011 -0
- package/dist/chunks/index-2vea86wD.esm.js.map +1 -0
- package/dist/chunks/index-CtA2AWl3.esm.js +4376 -0
- package/dist/chunks/index-CtA2AWl3.esm.js.map +1 -0
- package/dist/chunks/index-VTO-b2vR.cjs +4461 -0
- package/dist/chunks/index-VTO-b2vR.cjs.map +1 -0
- package/dist/debug/debug.d.ts +122 -0
- package/dist/debug/debug.d.ts.map +1 -0
- package/dist/debug/index.d.ts +4 -0
- package/dist/debug/index.d.ts.map +1 -0
- package/dist/debug/lineage-panel.d.ts +5 -0
- package/dist/debug/lineage-panel.d.ts.map +1 -0
- package/dist/debug/lineage.d.ts +79 -0
- package/dist/debug/lineage.d.ts.map +1 -0
- package/dist/debug.cjs +35 -0
- package/dist/debug.cjs.map +1 -0
- package/dist/debug.d.ts +209 -0
- package/dist/debug.esm.js +2 -0
- package/dist/debug.esm.js.map +1 -0
- package/dist/devtools/devtool/devtools.d.ts +1 -0
- package/dist/devtools/devtool/devtools.d.ts.map +1 -0
- package/dist/devtools/devtool/panel.d.ts +2 -0
- package/dist/devtools/devtool/panel.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- 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/node.cjs +107 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.ts +2 -2
- package/dist/node.esm.js +86 -33
- package/dist/node.esm.js.map +1 -1
- package/dist/src/async/browser.d.ts +2 -0
- package/dist/src/async/browser.d.ts.map +1 -0
- package/dist/src/async/index.d.ts +19 -0
- package/dist/src/async/index.d.ts.map +1 -0
- package/dist/src/async/node.d.ts +2 -0
- package/dist/src/async/node.d.ts.map +1 -0
- package/dist/src/decorator.d.ts +106 -0
- package/dist/src/decorator.d.ts.map +1 -0
- package/dist/src/destroyable.d.ts +87 -0
- package/dist/src/destroyable.d.ts.map +1 -0
- package/dist/src/entry-browser.d.ts +3 -0
- package/dist/src/entry-browser.d.ts.map +1 -0
- package/dist/src/entry-node.d.ts +3 -0
- package/dist/src/entry-node.d.ts.map +1 -0
- package/dist/src/eventful.d.ts +20 -0
- package/dist/src/eventful.d.ts.map +1 -0
- package/dist/src/flavored.d.ts +33 -0
- package/dist/src/flavored.d.ts.map +1 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/indexable.d.ts +243 -0
- package/dist/src/indexable.d.ts.map +1 -0
- package/dist/src/introspection.d.ts +27 -0
- package/dist/src/introspection.d.ts.map +1 -0
- package/dist/src/iterableWeak.d.ts +53 -0
- package/dist/src/iterableWeak.d.ts.map +1 -0
- package/dist/src/mixins.d.ts +25 -0
- package/dist/src/mixins.d.ts.map +1 -0
- package/dist/src/promiseChain.d.ts +20 -0
- package/dist/src/promiseChain.d.ts.map +1 -0
- package/dist/src/reactive/array.d.ts +48 -0
- package/dist/src/reactive/array.d.ts.map +1 -0
- package/dist/src/reactive/buffer.d.ts +120 -0
- package/dist/src/reactive/buffer.d.ts.map +1 -0
- package/dist/src/reactive/change.d.ts +29 -0
- package/dist/src/reactive/change.d.ts.map +1 -0
- package/dist/src/reactive/deep-touch.d.ts +28 -0
- package/dist/src/reactive/deep-touch.d.ts.map +1 -0
- package/dist/src/reactive/deep-watch-state.d.ts +25 -0
- package/dist/src/reactive/deep-watch-state.d.ts.map +1 -0
- package/dist/src/reactive/deep-watch.d.ts +20 -0
- package/dist/src/reactive/deep-watch.d.ts.map +1 -0
- package/dist/src/reactive/describe.d.ts +12 -0
- package/dist/src/reactive/describe.d.ts.map +1 -0
- package/dist/src/reactive/effect-context.d.ts +34 -0
- package/dist/src/reactive/effect-context.d.ts.map +1 -0
- package/dist/src/reactive/effects.d.ts +164 -0
- package/dist/src/reactive/effects.d.ts.map +1 -0
- package/dist/src/reactive/index.d.ts +19 -0
- package/dist/src/reactive/index.d.ts.map +1 -0
- package/dist/src/reactive/map.d.ts +28 -0
- package/dist/src/reactive/map.d.ts.map +1 -0
- package/dist/src/reactive/memoize.d.ts +28 -0
- package/dist/src/reactive/memoize.d.ts.map +1 -0
- package/dist/src/reactive/non-reactive-state.d.ts +9 -0
- package/dist/src/reactive/non-reactive-state.d.ts.map +1 -0
- package/dist/src/reactive/non-reactive.d.ts +11 -0
- package/dist/src/reactive/non-reactive.d.ts.map +1 -0
- package/dist/src/reactive/project.d.ts +40 -0
- package/dist/src/reactive/project.d.ts.map +1 -0
- package/dist/src/reactive/proxy-state.d.ts +8 -0
- package/dist/src/reactive/proxy-state.d.ts.map +1 -0
- package/dist/src/reactive/proxy.d.ts +23 -0
- package/dist/src/reactive/proxy.d.ts.map +1 -0
- package/dist/src/reactive/record.d.ts +115 -0
- package/dist/src/reactive/record.d.ts.map +1 -0
- package/dist/src/reactive/register.d.ts +125 -0
- package/dist/src/reactive/register.d.ts.map +1 -0
- package/dist/src/reactive/registry.d.ts +21 -0
- package/dist/src/reactive/registry.d.ts.map +1 -0
- package/dist/src/reactive/set.d.ts +26 -0
- package/dist/src/reactive/set.d.ts.map +1 -0
- package/dist/src/reactive/tracking.d.ts +7 -0
- package/dist/src/reactive/tracking.d.ts.map +1 -0
- package/dist/src/reactive/types.d.ts +424 -0
- package/dist/src/reactive/types.d.ts.map +1 -0
- package/dist/src/reactive/watch.d.ts +48 -0
- package/dist/src/reactive/watch.d.ts.map +1 -0
- package/dist/src/std-decorators.d.ts +45 -0
- package/dist/src/std-decorators.d.ts.map +1 -0
- package/dist/src/utils.d.ts +49 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/zone.d.ts +40 -0
- package/dist/src/zone.d.ts.map +1 -0
- package/dist/types-DaHFfhlN.d.ts +414 -0
- package/docs/ai/api-reference.md +1 -1
- package/docs/ai/manual.md +443 -17
- package/docs/events.md +57 -2
- package/docs/flavored.md +210 -0
- package/docs/reactive/advanced.md +2 -3
- package/docs/reactive/attend.md +143 -0
- package/docs/reactive/collections.md +55 -0
- package/docs/reactive/core.md +120 -4
- package/docs/reactive/debugging.md +8 -10
- package/docs/reactive/describe.md +85 -0
- package/docs/reactive/error-handling.md +121 -0
- package/docs/reactive/project.md +1 -1
- package/docs/reactive/scan.md +215 -0
- package/docs/reactive.md +2 -0
- package/docs/std-decorators.md +171 -52
- package/docs/zone.md +9 -6
- package/package.json +44 -27
- package/src/async/browser.ts +277 -41
- package/src/async/index.ts +23 -4
- package/src/async/node.ts +78 -32
- package/src/entry-browser.ts +5 -0
- package/src/entry-node.ts +5 -0
- package/src/eventful.ts +82 -49
- package/src/flavored.ts +106 -0
- package/src/index.d.ts +12 -9
- package/src/index.ts +2 -1
- package/src/introspection.ts +1 -1
- package/src/reactive/array.ts +160 -80
- package/src/reactive/buffer.ts +187 -27
- package/src/reactive/change.ts +16 -23
- package/src/reactive/deep-touch.ts +19 -14
- package/src/reactive/deep-watch-state.ts +3 -3
- package/src/reactive/deep-watch.ts +7 -4
- package/src/reactive/describe.ts +39 -0
- package/src/reactive/effect-context.ts +71 -6
- package/src/reactive/effects.ts +445 -393
- package/src/reactive/index.ts +12 -14
- package/src/reactive/map.ts +8 -24
- package/src/reactive/memoize.ts +26 -3
- package/src/reactive/project.md +107 -0
- package/src/reactive/project.ts +48 -71
- package/src/reactive/proxy.ts +21 -3
- package/src/reactive/record.ts +30 -45
- package/src/reactive/register.ts +138 -19
- package/src/reactive/registry.ts +22 -9
- package/src/reactive/set.ts +7 -20
- package/src/reactive/tracking.ts +9 -12
- package/src/reactive/types.ts +104 -24
- package/src/reactive/{interface.ts → watch.ts} +29 -59
- package/src/std-decorators.ts +56 -17
- package/src/utils.ts +3 -4
- package/src/zone.ts +43 -28
- package/dist/browser.js +0 -161
- package/dist/browser.js.map +0 -1
- package/dist/chunks/index-BFYK02LG.js +0 -6378
- package/dist/chunks/index-BFYK02LG.js.map +0 -1
- package/dist/chunks/index-CNR6QRUl.esm.js +0 -6291
- package/dist/chunks/index-CNR6QRUl.esm.js.map +0 -1
- package/dist/node.js +0 -136
- package/dist/node.js.map +0 -1
- package/src/reactive/debug.ts +0 -517
package/src/reactive/register.ts
CHANGED
|
@@ -1,14 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Eventful } from '../eventful'
|
|
2
2
|
import { ArrayReadForward, forwardArray, getAt, Indexable, setAt } from '../indexable'
|
|
3
|
+
import type { FunctionWrapper } from '../zone'
|
|
3
4
|
import { effect } from './effects'
|
|
4
|
-
import { unreactive } from './interface'
|
|
5
5
|
import { reactive } from './proxy'
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
// TODO: use register in a real-world crud situation, have "events" for add, delete, update
|
|
6
|
+
import type { EffectCleanup } from './types'
|
|
7
|
+
import { unreactive } from './watch'
|
|
9
8
|
|
|
10
9
|
type KeyFunction<T, K extends PropertyKey> = (item: T) => K
|
|
11
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Events emitted by the Register for CRUD operations
|
|
13
|
+
*/
|
|
14
|
+
export interface RegisterEvents<T, K extends PropertyKey> {
|
|
15
|
+
/**
|
|
16
|
+
* Emitted when a new item is added to the register
|
|
17
|
+
*/
|
|
18
|
+
add: (item: T, key: K, index: number) => void
|
|
19
|
+
/**
|
|
20
|
+
* Emitted when an item is removed from the register
|
|
21
|
+
*/
|
|
22
|
+
delete: (item: T, key: K, index: number) => void
|
|
23
|
+
/**
|
|
24
|
+
* Emitted when an item's value is updated (same key, new value)
|
|
25
|
+
*/
|
|
26
|
+
update: (oldItem: T, newItem: T, key: K, index: number) => void
|
|
27
|
+
/**
|
|
28
|
+
* Emitted when an item's key changes (rekey operation)
|
|
29
|
+
*/
|
|
30
|
+
rekey: (item: T, oldKey: K, newKey: K, index: number) => void
|
|
31
|
+
/**
|
|
32
|
+
* Index signature for EventsBase compatibility
|
|
33
|
+
*/
|
|
34
|
+
[key: string]: (...args: any[]) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
12
37
|
// Helper to work around TypeScript limitation: base class expressions cannot reference class type parameters
|
|
13
38
|
function getRegisterBase<T>() {
|
|
14
39
|
class RegisterBase extends Indexable(ArrayReadForward, {
|
|
@@ -37,7 +62,6 @@ function getRegisterBase<T>() {
|
|
|
37
62
|
interface RegisterInstance<T> extends ArrayReadForward<T> {
|
|
38
63
|
[index: number]: T
|
|
39
64
|
}
|
|
40
|
-
// TODO: What to do with prototype forwarding ?
|
|
41
65
|
@unreactive
|
|
42
66
|
class RegisterClass<T, K extends PropertyKey = PropertyKey>
|
|
43
67
|
extends getRegisterBase<any>()
|
|
@@ -50,9 +74,54 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
|
|
|
50
74
|
readonly #keys: K[]
|
|
51
75
|
readonly #values: Map<K, T>
|
|
52
76
|
readonly #usage = new Map<K, number>()
|
|
53
|
-
readonly #valueInfo = new Map<T, { key: K; stop?:
|
|
54
|
-
readonly #keyEffects = new Set<
|
|
77
|
+
readonly #valueInfo = new Map<T, { key: K; stop?: EffectCleanup }>()
|
|
78
|
+
readonly #keyEffects = new Set<EffectCleanup>()
|
|
55
79
|
readonly #ascend: FunctionWrapper
|
|
80
|
+
readonly #events = new Eventful<RegisterEvents<T, K>>()
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Register event listeners for CRUD operations
|
|
84
|
+
*/
|
|
85
|
+
on(events: Partial<RegisterEvents<T, K>>): void
|
|
86
|
+
on<EventType extends keyof RegisterEvents<T, K>>(
|
|
87
|
+
event: EventType,
|
|
88
|
+
cb: RegisterEvents<T, K>[EventType]
|
|
89
|
+
): () => void
|
|
90
|
+
on<EventType extends keyof RegisterEvents<T, K>>(
|
|
91
|
+
eventOrEvents: EventType | Partial<RegisterEvents<T, K>>,
|
|
92
|
+
cb?: RegisterEvents<T, K>[EventType]
|
|
93
|
+
): () => void {
|
|
94
|
+
// @ts-expect-error Delegate to Eventful
|
|
95
|
+
return this.#events.on(eventOrEvents, cb)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Remove event listeners
|
|
100
|
+
*/
|
|
101
|
+
off(events: Partial<RegisterEvents<T, K>>): void
|
|
102
|
+
off<EventType extends keyof RegisterEvents<T, K>>(
|
|
103
|
+
event: EventType,
|
|
104
|
+
cb?: RegisterEvents<T, K>[EventType]
|
|
105
|
+
): void
|
|
106
|
+
off<EventType extends keyof RegisterEvents<T, K>>(
|
|
107
|
+
eventOrEvents: EventType | Partial<RegisterEvents<T, K>>,
|
|
108
|
+
cb?: RegisterEvents<T, K>[EventType]
|
|
109
|
+
): void {
|
|
110
|
+
// @ts-expect-error Delegate to Eventful
|
|
111
|
+
this.#events.off(eventOrEvents, cb)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Register a global hook that receives all events
|
|
116
|
+
*/
|
|
117
|
+
hook(
|
|
118
|
+
cb: <EventType extends keyof RegisterEvents<T, K>>(
|
|
119
|
+
event: EventType,
|
|
120
|
+
...args: Parameters<RegisterEvents<T, K>[EventType]>
|
|
121
|
+
) => void
|
|
122
|
+
): () => void {
|
|
123
|
+
return this.#events.hook(cb)
|
|
124
|
+
}
|
|
56
125
|
|
|
57
126
|
constructor(keyFn: KeyFunction<T, K>, initial?: Iterable<T>) {
|
|
58
127
|
super()
|
|
@@ -84,7 +153,7 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
|
|
|
84
153
|
info!.key = nextKey
|
|
85
154
|
})
|
|
86
155
|
info!.stop = stop
|
|
87
|
-
this.#keyEffects.add(stop)
|
|
156
|
+
this.#keyEffects.add(stop as EffectCleanup)
|
|
88
157
|
})
|
|
89
158
|
if (info.key === undefined) throw new Error('Register key function must return a property key')
|
|
90
159
|
return info.key
|
|
@@ -96,10 +165,17 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
|
|
|
96
165
|
throw new Error('Register key function must return a property key')
|
|
97
166
|
}
|
|
98
167
|
|
|
99
|
-
private setKeyValue(key: K, value: T) {
|
|
168
|
+
private setKeyValue(key: K, value: T, index?: number) {
|
|
100
169
|
const existing = this.#values.get(key)
|
|
101
|
-
if (existing !== undefined && existing !== value)
|
|
102
|
-
|
|
170
|
+
if (existing !== undefined && existing !== value) {
|
|
171
|
+
this.cleanupValue(existing)
|
|
172
|
+
this.#values.set(key, value)
|
|
173
|
+
if (index !== undefined) {
|
|
174
|
+
this.#events.emit('update', existing, value, key, index)
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
this.#values.set(key, value)
|
|
178
|
+
}
|
|
103
179
|
}
|
|
104
180
|
|
|
105
181
|
private cleanupValue(value: T) {
|
|
@@ -128,13 +204,20 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
|
|
|
128
204
|
if (!count) return
|
|
129
205
|
const existingCount = this.#usage.get(newKey) ?? 0
|
|
130
206
|
this.setKeyValue(newKey, value)
|
|
207
|
+
let index = -1
|
|
131
208
|
for (let i = 0; i < this.#keys.length; i++)
|
|
132
|
-
if (Object.is(this.#keys[i], oldKey))
|
|
209
|
+
if (Object.is(this.#keys[i], oldKey)) {
|
|
210
|
+
this.#keys[i] = newKey
|
|
211
|
+
if (index === -1) index = i
|
|
212
|
+
}
|
|
133
213
|
this.#usage.set(newKey, existingCount + count)
|
|
134
214
|
this.#usage.delete(oldKey)
|
|
135
215
|
this.#values.delete(oldKey)
|
|
136
216
|
const updatedInfo = this.#valueInfo.get(value)
|
|
137
217
|
if (updatedInfo) updatedInfo.key = newKey
|
|
218
|
+
if (index !== -1) {
|
|
219
|
+
this.#events.emit('rekey', value, oldKey, newKey, index)
|
|
220
|
+
}
|
|
138
221
|
}
|
|
139
222
|
|
|
140
223
|
private incrementUsage(key: K) {
|
|
@@ -142,14 +225,18 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
|
|
|
142
225
|
this.#usage.set(key, count + 1)
|
|
143
226
|
}
|
|
144
227
|
|
|
145
|
-
private decrementUsage(key: K) {
|
|
228
|
+
private decrementUsage(key: K, index?: number) {
|
|
146
229
|
const count = this.#usage.get(key)
|
|
147
230
|
if (!count) return
|
|
148
231
|
if (count <= 1) {
|
|
149
232
|
const value = this.#values.get(key)
|
|
150
233
|
this.#usage.delete(key)
|
|
151
234
|
this.#values.delete(key)
|
|
152
|
-
if (value !== undefined)
|
|
235
|
+
if (value !== undefined) {
|
|
236
|
+
this.cleanupValue(value)
|
|
237
|
+
const idx = index ?? this.#keys.indexOf(key)
|
|
238
|
+
this.#events.emit('delete', value, key, idx)
|
|
239
|
+
}
|
|
153
240
|
} else {
|
|
154
241
|
this.#usage.set(key, count - 1)
|
|
155
242
|
}
|
|
@@ -170,19 +257,25 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
|
|
|
170
257
|
private assignAt(index: number, key: K, value: T) {
|
|
171
258
|
const oldKey = this.#keys[index]
|
|
172
259
|
if (oldKey !== undefined && Object.is(oldKey, key)) {
|
|
260
|
+
const oldValue = this.#values.get(key)
|
|
173
261
|
this.setKeyValue(key, value)
|
|
262
|
+
if (oldValue !== undefined && oldValue !== value) {
|
|
263
|
+
this.#events.emit('update', oldValue, value, key, index)
|
|
264
|
+
}
|
|
174
265
|
return
|
|
175
266
|
}
|
|
176
267
|
if (oldKey !== undefined) this.decrementUsage(oldKey as K)
|
|
177
268
|
this.#keys[index] = key
|
|
178
269
|
this.incrementUsage(key)
|
|
179
270
|
this.setKeyValue(key, value)
|
|
271
|
+
this.#events.emit('add', value, key, index)
|
|
180
272
|
}
|
|
181
273
|
|
|
182
274
|
private insertKeyValue(index: number, key: K, value: T) {
|
|
183
275
|
this.#keys.splice(index, 0, key)
|
|
184
276
|
this.incrementUsage(key)
|
|
185
277
|
this.setKeyValue(key, value)
|
|
278
|
+
this.#events.emit('add', value, key, index)
|
|
186
279
|
}
|
|
187
280
|
|
|
188
281
|
private rebuildFrom(values: T[]) {
|
|
@@ -255,10 +348,11 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
|
|
|
255
348
|
for (const item of items) keysToInsert.push(this.ensureKey(item))
|
|
256
349
|
const removedKeys = this.#keys.splice(normalizedStart, actualDelete, ...keysToInsert)
|
|
257
350
|
const removedValues: T[] = []
|
|
258
|
-
for (
|
|
351
|
+
for (let i = 0; i < removedKeys.length; i++) {
|
|
352
|
+
const key = removedKeys[i]
|
|
259
353
|
if (key === undefined) continue
|
|
260
354
|
const value = this.#values.get(key as K)
|
|
261
|
-
this.decrementUsage(key as K)
|
|
355
|
+
this.decrementUsage(key as K, normalizedStart + i)
|
|
262
356
|
removedValues.push(value as T)
|
|
263
357
|
}
|
|
264
358
|
for (let i = 0; i < keysToInsert.length; i++) {
|
|
@@ -296,7 +390,7 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
|
|
|
296
390
|
const [key] = this.#keys.splice(index, 1)
|
|
297
391
|
if (key === undefined) return undefined
|
|
298
392
|
const value = this.#values.get(key as K)
|
|
299
|
-
this.decrementUsage(key as K)
|
|
393
|
+
this.decrementUsage(key as K, index)
|
|
300
394
|
return value
|
|
301
395
|
}
|
|
302
396
|
|
|
@@ -336,7 +430,10 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
|
|
|
336
430
|
update(...values: T[]): void {
|
|
337
431
|
for (const value of values) {
|
|
338
432
|
const key = this.ensureKey(value)
|
|
339
|
-
if (this.#values.has(key))
|
|
433
|
+
if (this.#values.has(key)) {
|
|
434
|
+
const index = this.#keys.indexOf(key)
|
|
435
|
+
this.setKeyValue(key, value, index)
|
|
436
|
+
}
|
|
340
437
|
}
|
|
341
438
|
}
|
|
342
439
|
|
|
@@ -411,6 +508,28 @@ export const Register = RegisterClass as new <T, K extends PropertyKey = Propert
|
|
|
411
508
|
initial?: Iterable<T>
|
|
412
509
|
) => Register<T, K>
|
|
413
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Creates a reactive Register - an ordered, array-like collection with stable key-based identity.
|
|
513
|
+
*
|
|
514
|
+
* Register combines array semantics (indexable access, ordering, iteration) with Map-like
|
|
515
|
+
* key-based lookups. Items with the same key share the same value instance, making it ideal
|
|
516
|
+
* for UI lists keyed by IDs or when you need to preserve identity across reorders.
|
|
517
|
+
*
|
|
518
|
+
* @param keyFn - Function that extracts the key from each item
|
|
519
|
+
* @param initial - Optional initial items to populate the register
|
|
520
|
+
* @returns A reactive Register instance
|
|
521
|
+
*
|
|
522
|
+
* @example
|
|
523
|
+
* ```typescript
|
|
524
|
+
* const users = register(
|
|
525
|
+
* (user: User) => user.id,
|
|
526
|
+
* [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
|
|
527
|
+
* )
|
|
528
|
+
*
|
|
529
|
+
* users.push({ id: 3, name: 'Charlie' })
|
|
530
|
+
* const bob = users.get(2) // Get by key
|
|
531
|
+
* ```
|
|
532
|
+
*/
|
|
414
533
|
export function register<T, K extends PropertyKey = PropertyKey>(
|
|
415
534
|
keyFn: KeyFunction<T, K>,
|
|
416
535
|
initial?: Iterable<T>
|
package/src/reactive/registry.ts
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type EffectCleanup, type EffectNode, type EffectTrigger, rootFunction } from './types'
|
|
2
2
|
|
|
3
3
|
// Track which effects are watching which reactive objects for cleanup
|
|
4
|
-
export
|
|
4
|
+
export let effectToReactiveObjects = new WeakMap<EffectTrigger, Set<object>>()
|
|
5
5
|
|
|
6
6
|
// Track effects per reactive object and property
|
|
7
|
-
export
|
|
7
|
+
export let watchers = new WeakMap<object, Map<any, Set<EffectTrigger>>>()
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
export
|
|
9
|
+
// Track effect metadata and relationships
|
|
10
|
+
export let effectNodes = new WeakMap<EffectTrigger, EffectNode>()
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
export function getEffectNode(effect: EffectTrigger): EffectNode {
|
|
13
|
+
let node = effectNodes.get(effect)
|
|
14
|
+
if (!node) {
|
|
15
|
+
node = {}
|
|
16
|
+
effectNodes.set(effect, node)
|
|
17
|
+
}
|
|
18
|
+
return node
|
|
19
|
+
}
|
|
14
20
|
|
|
15
21
|
// Track reverse mapping to ensure unicity: One Root -> One Function
|
|
16
|
-
|
|
22
|
+
let reverseRoots = new WeakMap<any, WeakRef<Function>>()
|
|
23
|
+
|
|
24
|
+
export function resetRegistry() {
|
|
25
|
+
effectToReactiveObjects = new WeakMap()
|
|
26
|
+
watchers = new WeakMap()
|
|
27
|
+
effectNodes = new WeakMap()
|
|
28
|
+
reverseRoots = new WeakMap()
|
|
29
|
+
}
|
|
17
30
|
|
|
18
31
|
/**
|
|
19
32
|
* Marks a function with its root function for effect tracking
|
|
@@ -56,4 +69,4 @@ export function markWithRoot<T extends Function>(fn: T, root: any): T {
|
|
|
56
69
|
export function getRoot<T extends Function | undefined>(fn: T): T {
|
|
57
70
|
while (fn && rootFunction in fn) fn = fn[rootFunction] as T
|
|
58
71
|
return fn
|
|
59
|
-
}
|
|
72
|
+
}
|
package/src/reactive/set.ts
CHANGED
|
@@ -9,9 +9,6 @@ import { dependant } from './tracking'
|
|
|
9
9
|
* Only tracks individual value operations, no size tracking (WeakSet limitation)
|
|
10
10
|
*/
|
|
11
11
|
export abstract class ReactiveWeakSet<T extends object> extends WeakSet<T> {
|
|
12
|
-
get [Symbol.toStringTag]() {
|
|
13
|
-
return 'ReactiveWeakSet'
|
|
14
|
-
}
|
|
15
12
|
add(value: T): this {
|
|
16
13
|
const had = this.has(value)
|
|
17
14
|
this.add(value)
|
|
@@ -41,10 +38,6 @@ export abstract class ReactiveWeakSet<T extends object> extends WeakSet<T> {
|
|
|
41
38
|
* Tracks size changes, individual value operations, and collection-wide operations
|
|
42
39
|
*/
|
|
43
40
|
export abstract class ReactiveSet<T> extends Set<T> {
|
|
44
|
-
get [Symbol.toStringTag]() {
|
|
45
|
-
return 'ReactiveSet'
|
|
46
|
-
}
|
|
47
|
-
|
|
48
41
|
get size(): number {
|
|
49
42
|
// size depends on the wrapper instance, like Map counterpart
|
|
50
43
|
dependant(this, 'size')
|
|
@@ -112,19 +105,13 @@ export abstract class ReactiveSet<T> extends Set<T> {
|
|
|
112
105
|
|
|
113
106
|
[Symbol.iterator](): SetIterator<T> {
|
|
114
107
|
dependant(contentRef(this))
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
return { value: reactive(result.value), done: false }
|
|
123
|
-
},
|
|
124
|
-
[Symbol.iterator]() {
|
|
125
|
-
return this
|
|
126
|
-
},
|
|
127
|
-
[Symbol.dispose]() {},
|
|
108
|
+
const it: SetIterator<T> = Set.prototype[Symbol.iterator].call(this)
|
|
109
|
+
const nativeNext = it.next.bind(it)
|
|
110
|
+
it.next = () => {
|
|
111
|
+
const result = nativeNext()
|
|
112
|
+
if (result.done) return result
|
|
113
|
+
return { value: reactive(result.value), done: false }
|
|
128
114
|
}
|
|
115
|
+
return it
|
|
129
116
|
}
|
|
130
117
|
}
|
package/src/reactive/tracking.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { getActiveEffect } from './effect-context'
|
|
2
2
|
import { unwrap } from './proxy-state'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
watchers,
|
|
6
|
-
} from './registry'
|
|
7
|
-
import { allProps, type ScopedCallback } from './types'
|
|
3
|
+
import { effectToReactiveObjects, watchers } from './registry'
|
|
4
|
+
import { allProps, type EffectTrigger } from './types'
|
|
8
5
|
|
|
9
6
|
/**
|
|
10
7
|
* Marks a property as a dependency of the current effect
|
|
@@ -16,20 +13,20 @@ export function dependant(obj: any, prop: any = allProps) {
|
|
|
16
13
|
const currentActiveEffect = getActiveEffect()
|
|
17
14
|
|
|
18
15
|
// Early return if no active effect, tracking disabled, or invalid prop
|
|
19
|
-
if (
|
|
20
|
-
!currentActiveEffect ||
|
|
21
|
-
(typeof prop === 'symbol' && prop !== allProps)
|
|
22
|
-
)
|
|
23
|
-
return
|
|
16
|
+
if (!currentActiveEffect || (typeof prop === 'symbol' && prop !== allProps)) return
|
|
24
17
|
|
|
18
|
+
if ('dependencyHook' in currentActiveEffect) {
|
|
19
|
+
// @ts-expect-error We declared it nowhere - it's okay as it's really internal and for edge-case debug purpose only
|
|
20
|
+
currentActiveEffect.dependencyHook(obj, prop)
|
|
21
|
+
}
|
|
25
22
|
let objectWatchers = watchers.get(obj)
|
|
26
23
|
if (!objectWatchers) {
|
|
27
|
-
objectWatchers = new Map<PropertyKey, Set<
|
|
24
|
+
objectWatchers = new Map<PropertyKey, Set<EffectTrigger>>()
|
|
28
25
|
watchers.set(obj, objectWatchers)
|
|
29
26
|
}
|
|
30
27
|
let deps = objectWatchers.get(prop)
|
|
31
28
|
if (!deps) {
|
|
32
|
-
deps = new Set<
|
|
29
|
+
deps = new Set<EffectTrigger>()
|
|
33
30
|
objectWatchers.set(prop, deps)
|
|
34
31
|
}
|
|
35
32
|
deps.add(currentActiveEffect)
|
package/src/reactive/types.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
// biome-ignore-all lint/suspicious/noConfusingVoidType: Type 'void' is not assignable to type 'ScopedCallback | undefined'.
|
|
2
2
|
// Argument of type '() => void' is not assignable to parameter of type '(dep: DependencyFunction) => ScopedCallback | undefined'.
|
|
3
3
|
|
|
4
|
-
import { FunctionWrapper } from
|
|
4
|
+
import type { FunctionWrapper } from '../zone'
|
|
5
|
+
|
|
6
|
+
export type EffectAccessEvents = {
|
|
7
|
+
triggered(event: string, ...args: any[]): void
|
|
8
|
+
}
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
|
-
*
|
|
11
|
+
* Effect access passed to user callbacks within effects/watch
|
|
8
12
|
* Provides functions to track dependencies and information about the effect execution
|
|
9
13
|
*/
|
|
10
|
-
export interface
|
|
11
|
-
// TODO: remove tracked (async is managed)
|
|
12
|
-
// TODO: remove ascend (make a global like `untracked` who withEffect(parentEffect, () => {}))
|
|
14
|
+
export interface EffectAccess {
|
|
13
15
|
/**
|
|
14
16
|
* Tracks dependencies in the current effect context
|
|
15
17
|
* Use this for normal dependency tracking within the effect
|
|
@@ -64,10 +66,60 @@ export interface DependencyAccess {
|
|
|
64
66
|
// It automatically preserves effect context across Promise boundaries (.then, .catch, .finally)
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
|
-
*
|
|
69
|
+
* Base type for effect callbacks - simple function without additional properties
|
|
68
70
|
*/
|
|
69
71
|
export type ScopedCallback = () => void
|
|
70
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Type for effect cleanup functions with stopped state tracking
|
|
75
|
+
*/
|
|
76
|
+
export type EffectCleanup = ScopedCallback & {
|
|
77
|
+
[stopped]: boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Debug type for stack frames
|
|
81
|
+
export type StackFrame = {
|
|
82
|
+
functionName: string
|
|
83
|
+
fileName: string
|
|
84
|
+
lineNumber: number
|
|
85
|
+
columnNumber: number
|
|
86
|
+
raw: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type EffectTracking = (obj: any, evolution: Evolution, prop: any, effect: EffectTrigger) => void
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Centralized node for all effect metadata and relationships
|
|
93
|
+
*/
|
|
94
|
+
export interface EffectNode {
|
|
95
|
+
// Graph relationships
|
|
96
|
+
parent?: EffectTrigger
|
|
97
|
+
children?: Set<EffectCleanup>
|
|
98
|
+
|
|
99
|
+
// Lifecycle
|
|
100
|
+
cleanup?: ScopedCallback
|
|
101
|
+
stopped?: boolean
|
|
102
|
+
|
|
103
|
+
// Error handling
|
|
104
|
+
forwardThrow?: CatchFunction
|
|
105
|
+
catchers?: CatchFunction[]
|
|
106
|
+
|
|
107
|
+
// Debug / Metadata
|
|
108
|
+
creationStack?: StackFrame[]
|
|
109
|
+
dependencyHook?: (obj: any, prop: any) => void
|
|
110
|
+
|
|
111
|
+
// Tracking
|
|
112
|
+
trackers?: EffectTracking[]
|
|
113
|
+
|
|
114
|
+
// Configuration
|
|
115
|
+
isOpaque?: boolean
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Type for the `runEffect` function of an effect - argument-less function to call to trigger the effect
|
|
120
|
+
*/
|
|
121
|
+
export type EffectTrigger = ScopedCallback
|
|
122
|
+
|
|
71
123
|
/**
|
|
72
124
|
* Async execution mode for effects
|
|
73
125
|
* - `cancel`: Cancel previous async execution when dependencies change (default)
|
|
@@ -91,6 +143,14 @@ export interface EffectOptions {
|
|
|
91
143
|
* Use this for effects that depend on object identity (like memoize).
|
|
92
144
|
*/
|
|
93
145
|
opaque?: boolean
|
|
146
|
+
/**
|
|
147
|
+
* Used for debugging purpose. Provides a callback to be called every time a dependency is created.
|
|
148
|
+
*/
|
|
149
|
+
dependencyHook?: (obj: any, prop: any) => void
|
|
150
|
+
/**
|
|
151
|
+
* Used for debugging purpose. Provides a name for the effect.
|
|
152
|
+
*/
|
|
153
|
+
name?: string
|
|
94
154
|
}
|
|
95
155
|
|
|
96
156
|
/**
|
|
@@ -149,6 +209,12 @@ export const stopped = Symbol('stopped')
|
|
|
149
209
|
*/
|
|
150
210
|
export const cleanup = Symbol('cleanup')
|
|
151
211
|
|
|
212
|
+
export const forwardThrow = Symbol('throw')
|
|
213
|
+
|
|
214
|
+
export type EffectCloser = (error?: any) => void
|
|
215
|
+
//biome-ignore lint/suspicious/noConfusingVoidType: We have to
|
|
216
|
+
export type CatchFunction = (error: any) => EffectCloser | undefined | void
|
|
217
|
+
|
|
152
218
|
/**
|
|
153
219
|
* Context for a running projection item effect
|
|
154
220
|
*/
|
|
@@ -193,15 +259,11 @@ export type MaxReactionDebugInfo = {
|
|
|
193
259
|
effect: string
|
|
194
260
|
}
|
|
195
261
|
|
|
196
|
-
export type BrokenEffectsDebugInfo = {
|
|
197
|
-
code: ReactiveErrorCode.BrokenEffects
|
|
198
|
-
cause: any
|
|
199
|
-
}
|
|
200
|
-
|
|
201
262
|
export type GenericDebugInfo = {
|
|
202
263
|
code: ReactiveErrorCode
|
|
203
264
|
causalChain?: string[]
|
|
204
|
-
creationStack?: string
|
|
265
|
+
creationStack?: string | any[]
|
|
266
|
+
lineage?: any[]
|
|
205
267
|
[key: string]: any
|
|
206
268
|
}
|
|
207
269
|
|
|
@@ -209,7 +271,6 @@ export type ReactiveDebugInfo =
|
|
|
209
271
|
| CycleDebugInfo
|
|
210
272
|
| MaxDepthDebugInfo
|
|
211
273
|
| MaxReactionDebugInfo
|
|
212
|
-
| BrokenEffectsDebugInfo
|
|
213
274
|
| GenericDebugInfo
|
|
214
275
|
|
|
215
276
|
/**
|
|
@@ -271,13 +332,13 @@ export const options = {
|
|
|
271
332
|
* @param props - The properties that changed
|
|
272
333
|
* @param deps - The dependencies that changed
|
|
273
334
|
*/
|
|
274
|
-
touched: (_obj: any, _evolution: Evolution, _props?: any[], _deps?: Set<
|
|
335
|
+
touched: (_obj: any, _evolution: Evolution, _props?: any[], _deps?: Set<EffectTrigger>) => {},
|
|
275
336
|
/**
|
|
276
337
|
* Debug purpose: called when an effect is skipped because it's already running
|
|
277
338
|
* @param effect - The effect that is already running
|
|
278
339
|
* @param runningChain - The array of effects from the detected one to the currently running one
|
|
279
340
|
*/
|
|
280
|
-
skipRunningEffect: (_effect:
|
|
341
|
+
skipRunningEffect: (_effect: EffectTrigger) => {},
|
|
281
342
|
/**
|
|
282
343
|
* Debug purpose: maximum effect chain (like call stack max depth)
|
|
283
344
|
* Used to prevent infinite loops
|
|
@@ -327,20 +388,20 @@ export const options = {
|
|
|
327
388
|
/**
|
|
328
389
|
* How to handle cycles detected in effect batches.
|
|
329
390
|
*
|
|
330
|
-
* - `'
|
|
391
|
+
* - `'production'` (Default): High-performance mode. Disables dependency graph maintenance and
|
|
331
392
|
* Topological Sorting in favor of a simple FIFO queue. Use this for trustworthy, acyclic UI code.
|
|
332
|
-
* Cycle detection is heuristic (uses execution counts).
|
|
393
|
+
* Cycle detection is heuristic (uses maxEffectChain execution counts).
|
|
333
394
|
*
|
|
334
|
-
* - `'
|
|
335
|
-
*
|
|
395
|
+
* - `'development'`: Maintains direct dependency graph for early cycle detection during edge creation.
|
|
396
|
+
* Catches cycles before effects execute via DFS check when adding edges. Throws immediately with
|
|
397
|
+
* basic path information. Good balance of debugging help with moderate overhead.
|
|
336
398
|
*
|
|
337
|
-
* - `'
|
|
338
|
-
*
|
|
339
|
-
* - `'strict'`: Prevents cycle creation by checking the graph *during* dependency discovery.
|
|
399
|
+
* - `'debug'`: Full diagnostic mode with transitive closures and topological sorting.
|
|
400
|
+
* Provides detailed cycle path reporting. Highest overhead but most informative for bug hunting.
|
|
340
401
|
*
|
|
341
|
-
* @default '
|
|
402
|
+
* @default 'production'
|
|
342
403
|
*/
|
|
343
|
-
cycleHandling: '
|
|
404
|
+
cycleHandling: 'development' as 'production' | 'development' | 'debug',
|
|
344
405
|
/**
|
|
345
406
|
* Internal flag used by memoization discrepancy detector to avoid counting calls in tests
|
|
346
407
|
* @warning Do not modify this flag manually, this flag is given by the engine
|
|
@@ -437,4 +498,23 @@ export const options = {
|
|
|
437
498
|
}
|
|
438
499
|
// biome-ignore-end lint/correctness/noUnusedFunctionParameters: Interface declaration with empty defaults
|
|
439
500
|
|
|
501
|
+
type CallableOption = {
|
|
502
|
+
[K in keyof typeof options]: (typeof options)[K] extends ((...args: any[]) => any) | undefined
|
|
503
|
+
? K
|
|
504
|
+
: never
|
|
505
|
+
}[keyof typeof options]
|
|
506
|
+
|
|
507
|
+
export function optionCall<K extends CallableOption>(
|
|
508
|
+
name: K,
|
|
509
|
+
...args: NonNullable<(typeof options)[K]> extends (...a: infer A) => unknown ? A : never
|
|
510
|
+
): void {
|
|
511
|
+
const fn = options[name]
|
|
512
|
+
if (typeof fn !== 'function') return
|
|
513
|
+
try {
|
|
514
|
+
;(fn as Function)(...args)
|
|
515
|
+
} catch (error) {
|
|
516
|
+
options.warn(`options.${name} threw`, error)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
440
520
|
export { type State, nativeReactive, rootFunction }
|