mutts 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-79Kk8D6e.esm.js +4857 -0
- package/dist/chunks/index-79Kk8D6e.esm.js.map +1 -0
- package/dist/chunks/index-GRBSx0mB.js +4908 -0
- package/dist/chunks/index-GRBSx0mB.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 +37 -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 +602 -97
- package/dist/reactive.esm.js +3 -3
- package/dist/reactive.js +32 -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 +1345 -0
- package/src/reactive/index.ts +76 -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 +289 -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,289 @@
|
|
|
1
|
+
import { decorator } from '../decorator'
|
|
2
|
+
import { mixin } from '../mixins'
|
|
3
|
+
import { isOwnAccessor, ReflectGet, ReflectSet } from '../utils'
|
|
4
|
+
import { touched1 } from './change'
|
|
5
|
+
import { notifyPropertyChange } from './deep-touch'
|
|
6
|
+
import {
|
|
7
|
+
addBackReference,
|
|
8
|
+
bubbleUpChange,
|
|
9
|
+
needsBackReferences,
|
|
10
|
+
objectsWithDeepWatchers,
|
|
11
|
+
removeBackReference,
|
|
12
|
+
} from './deep-watch-state'
|
|
13
|
+
import { withEffect } from './effect-context'
|
|
14
|
+
import { absent, isNonReactive } from './non-reactive-state'
|
|
15
|
+
import {
|
|
16
|
+
getExistingProxy,
|
|
17
|
+
proxyToObject,
|
|
18
|
+
storeProxyRelationship,
|
|
19
|
+
trackProxyObject,
|
|
20
|
+
unwrap,
|
|
21
|
+
} from './proxy-state'
|
|
22
|
+
import { dependant } from './tracking'
|
|
23
|
+
import {
|
|
24
|
+
allProps,
|
|
25
|
+
nativeReactive,
|
|
26
|
+
nonReactiveMark,
|
|
27
|
+
options,
|
|
28
|
+
prototypeForwarding,
|
|
29
|
+
ReactiveError,
|
|
30
|
+
ReactiveErrorCode,
|
|
31
|
+
unreactiveProperties,
|
|
32
|
+
} from './types'
|
|
33
|
+
|
|
34
|
+
const hasReentry: any[] = []
|
|
35
|
+
const reactiveHandlers = {
|
|
36
|
+
[Symbol.toStringTag]: 'MutTs Reactive',
|
|
37
|
+
get(obj: any, prop: PropertyKey, receiver: any) {
|
|
38
|
+
if (prop === nonReactiveMark) return false
|
|
39
|
+
const unwrappedObj = unwrap(obj)
|
|
40
|
+
// Check if this property is marked as unreactive
|
|
41
|
+
if (unwrappedObj[unreactiveProperties]?.has(prop) || typeof prop === 'symbol')
|
|
42
|
+
return ReflectGet(obj, prop, receiver)
|
|
43
|
+
|
|
44
|
+
// Special-case: array wrappers use prototype forwarding + numeric accessors.
|
|
45
|
+
// With options.instanceMembers=true, inherited reads are normally not tracked, which breaks
|
|
46
|
+
// reactivity for array indices/length (they appear inherited on the proxy).
|
|
47
|
+
const isArrayCase =
|
|
48
|
+
prototypeForwarding in obj &&
|
|
49
|
+
// biome-ignore lint/suspicious/useIsArray: This is the whole point here
|
|
50
|
+
obj[prototypeForwarding] instanceof Array &&
|
|
51
|
+
typeof prop === 'string' &&
|
|
52
|
+
(prop === 'length' || !Number.isNaN(Number(prop)))
|
|
53
|
+
if (isArrayCase) {
|
|
54
|
+
dependant(obj, prop === 'length' ? 'length' : Number(prop))
|
|
55
|
+
}
|
|
56
|
+
// Check if property exists and if it's an own property (cached for later use)
|
|
57
|
+
const hasProp = Reflect.has(receiver, prop)
|
|
58
|
+
const isOwnProp = hasProp && Object.hasOwn(receiver, prop)
|
|
59
|
+
const isInheritedAccess = hasProp && !isOwnProp
|
|
60
|
+
|
|
61
|
+
// For accessor properties, check the unwrapped object to see if it's an accessor
|
|
62
|
+
// This ensures ignoreAccessors works correctly even after operations like Object.setPrototypeOf
|
|
63
|
+
const shouldIgnoreAccessor =
|
|
64
|
+
options.ignoreAccessors &&
|
|
65
|
+
isOwnProp &&
|
|
66
|
+
(isOwnAccessor(receiver, prop) || isOwnAccessor(unwrappedObj, prop))
|
|
67
|
+
|
|
68
|
+
// Depend if...
|
|
69
|
+
if (
|
|
70
|
+
!hasProp ||
|
|
71
|
+
(!(options.instanceMembers && isInheritedAccess && obj instanceof Object) &&
|
|
72
|
+
!shouldIgnoreAccessor)
|
|
73
|
+
)
|
|
74
|
+
dependant(obj, prop)
|
|
75
|
+
|
|
76
|
+
// Watch the whole prototype chain when requested or for null-proto objects
|
|
77
|
+
if (isInheritedAccess && (!options.instanceMembers || !(obj instanceof Object))) {
|
|
78
|
+
let current = reactiveObject(Object.getPrototypeOf(obj))
|
|
79
|
+
while (current && current !== Object.prototype) {
|
|
80
|
+
dependant(current, prop)
|
|
81
|
+
if (Object.hasOwn(current, prop)) break
|
|
82
|
+
let next = reactiveObject(Object.getPrototypeOf(current))
|
|
83
|
+
if (next === current) {
|
|
84
|
+
next = reactiveObject(Object.getPrototypeOf(unwrap(current)))
|
|
85
|
+
}
|
|
86
|
+
current = next
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const value = ReflectGet(obj, prop, receiver)
|
|
90
|
+
if (typeof value === 'object' && value !== null) {
|
|
91
|
+
const reactiveValue = reactiveObject(value)
|
|
92
|
+
|
|
93
|
+
// Only create back-references if this object needs them
|
|
94
|
+
if (needsBackReferences(obj)) {
|
|
95
|
+
addBackReference(reactiveValue, obj, prop)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return reactiveValue
|
|
99
|
+
}
|
|
100
|
+
return value
|
|
101
|
+
},
|
|
102
|
+
set(obj: any, prop: PropertyKey, value: any, receiver: any): boolean {
|
|
103
|
+
// Read old value directly from unwrapped object to avoid triggering dependency tracking
|
|
104
|
+
const unwrappedObj = unwrap(obj)
|
|
105
|
+
const unwrappedReceiver = unwrap(receiver)
|
|
106
|
+
|
|
107
|
+
// Check if this property is marked as unreactive
|
|
108
|
+
if (unwrappedObj[unreactiveProperties]?.has(prop) || unwrappedObj !== unwrappedReceiver)
|
|
109
|
+
return ReflectSet(obj, prop, value, receiver)
|
|
110
|
+
// Really specific case for when Array is forwarder, in order to let it manage the reactivity
|
|
111
|
+
const isArrayCase =
|
|
112
|
+
prototypeForwarding in obj &&
|
|
113
|
+
// biome-ignore lint/suspicious/useIsArray: This is the whole point here
|
|
114
|
+
obj[prototypeForwarding] instanceof Array &&
|
|
115
|
+
(!Number.isNaN(Number(prop)) || prop === 'length')
|
|
116
|
+
const newValue = unwrap(value)
|
|
117
|
+
|
|
118
|
+
if (isArrayCase) {
|
|
119
|
+
;(obj as any)[prop] = newValue
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
// Read old value, using withEffect(undefined, ...) for getter-only accessors to avoid
|
|
123
|
+
// breaking memoization dependency tracking during SET operations
|
|
124
|
+
let oldVal = absent
|
|
125
|
+
if (Reflect.has(unwrappedReceiver, prop)) {
|
|
126
|
+
// Check descriptor on both receiver and target to handle proxy cases
|
|
127
|
+
const receiverDesc = Object.getOwnPropertyDescriptor(unwrappedReceiver, prop)
|
|
128
|
+
const targetDesc = Object.getOwnPropertyDescriptor(unwrappedObj, prop)
|
|
129
|
+
const desc = receiverDesc || targetDesc
|
|
130
|
+
// If it's a getter-only accessor (has getter but no setter), read without tracking
|
|
131
|
+
// to avoid breaking memoization invalidation when the getter calls memoized functions
|
|
132
|
+
if (desc?.get && !desc?.set) {
|
|
133
|
+
oldVal = withEffect(undefined, () => Reflect.get(unwrappedObj, prop, unwrappedReceiver))
|
|
134
|
+
} else {
|
|
135
|
+
oldVal = Reflect.get(unwrappedObj, prop, unwrappedReceiver)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (objectsWithDeepWatchers.has(obj)) {
|
|
139
|
+
if (typeof oldVal === 'object' && oldVal !== null) {
|
|
140
|
+
removeBackReference(oldVal, obj, prop)
|
|
141
|
+
}
|
|
142
|
+
if (typeof newValue === 'object' && newValue !== null) {
|
|
143
|
+
const reactiveValue = reactiveObject(newValue)
|
|
144
|
+
addBackReference(reactiveValue, obj, prop)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (oldVal !== newValue) {
|
|
149
|
+
// For getter-only accessors, Reflect.set() may fail, but we still return true
|
|
150
|
+
// to avoid throwing errors. Only proceed with change notifications if set succeeded.
|
|
151
|
+
if (ReflectSet(obj, prop, newValue, receiver)) {
|
|
152
|
+
notifyPropertyChange(obj, prop, oldVal, newValue, oldVal !== absent)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return true
|
|
156
|
+
},
|
|
157
|
+
has(obj: any, prop: PropertyKey): boolean {
|
|
158
|
+
if (hasReentry.includes(obj))
|
|
159
|
+
throw new ReactiveError(
|
|
160
|
+
`[reactive] Circular dependency detected in 'has' check for property '${String(prop)}'`,
|
|
161
|
+
{
|
|
162
|
+
code: ReactiveErrorCode.CycleDetected,
|
|
163
|
+
cycle: [], // We don't have the full cycle here, but we know it involves obj
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
hasReentry.push(obj)
|
|
167
|
+
dependant(obj, prop)
|
|
168
|
+
const rv = Reflect.has(obj, prop)
|
|
169
|
+
hasReentry.pop()
|
|
170
|
+
return rv
|
|
171
|
+
},
|
|
172
|
+
deleteProperty(obj: any, prop: PropertyKey): boolean {
|
|
173
|
+
if (!Object.hasOwn(obj, prop)) return false
|
|
174
|
+
|
|
175
|
+
const oldVal = (obj as any)[prop]
|
|
176
|
+
|
|
177
|
+
// Remove back-references if this object has deep watchers
|
|
178
|
+
if (objectsWithDeepWatchers.has(obj) && typeof oldVal === 'object' && oldVal !== null) {
|
|
179
|
+
removeBackReference(oldVal, obj, prop)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
delete (obj as any)[prop]
|
|
183
|
+
touched1(obj, { type: 'del', prop }, prop)
|
|
184
|
+
|
|
185
|
+
// Bubble up changes if this object has deep watchers
|
|
186
|
+
if (objectsWithDeepWatchers.has(obj)) {
|
|
187
|
+
bubbleUpChange(obj, { type: 'del', prop })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return true
|
|
191
|
+
},
|
|
192
|
+
getPrototypeOf(obj: any): object | null {
|
|
193
|
+
if (prototypeForwarding in obj) return obj[prototypeForwarding]
|
|
194
|
+
return Object.getPrototypeOf(obj)
|
|
195
|
+
},
|
|
196
|
+
setPrototypeOf(obj: any, proto: object | null): boolean {
|
|
197
|
+
if (prototypeForwarding in obj) return false
|
|
198
|
+
Object.setPrototypeOf(obj, proto)
|
|
199
|
+
return true
|
|
200
|
+
},
|
|
201
|
+
ownKeys(obj: any): (string | symbol)[] {
|
|
202
|
+
dependant(obj, allProps)
|
|
203
|
+
return Reflect.ownKeys(obj)
|
|
204
|
+
},
|
|
205
|
+
} as const
|
|
206
|
+
|
|
207
|
+
const reactiveClasses = new WeakSet<Function>()
|
|
208
|
+
|
|
209
|
+
// Create the ReactiveBase mixin
|
|
210
|
+
/**
|
|
211
|
+
* Base mixin for reactive classes that provides proper constructor reactivity
|
|
212
|
+
* Solves constructor reactivity issues in complex inheritance trees
|
|
213
|
+
*/
|
|
214
|
+
export const ReactiveBase = mixin((base) => {
|
|
215
|
+
class ReactiveMixin extends base {
|
|
216
|
+
constructor(...args: any[]) {
|
|
217
|
+
super(...args)
|
|
218
|
+
// Only apply reactive transformation if the class is marked with @reactive
|
|
219
|
+
// This allows the mixin to work properly with method inheritance
|
|
220
|
+
// biome-ignore lint/correctness/noConstructorReturn: This is the whole point here
|
|
221
|
+
return reactiveClasses.has(new.target) ? reactive(this) : this
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return ReactiveMixin
|
|
225
|
+
})
|
|
226
|
+
function reactiveObject<T>(anyTarget: T): T {
|
|
227
|
+
if (!anyTarget || typeof anyTarget !== 'object') return anyTarget
|
|
228
|
+
const target = anyTarget as any
|
|
229
|
+
// If target is already a proxy, return it
|
|
230
|
+
if (isNonReactive(target)) return target as T
|
|
231
|
+
const isProxy = proxyToObject.has(target)
|
|
232
|
+
if (isProxy) return target as T
|
|
233
|
+
|
|
234
|
+
// If we already have a proxy for this object, return it (optimized: get returns undefined if not found)
|
|
235
|
+
const existing = getExistingProxy(target)
|
|
236
|
+
if (existing !== undefined) return existing as T
|
|
237
|
+
|
|
238
|
+
const proxied =
|
|
239
|
+
nativeReactive in target && !(target instanceof target[nativeReactive])
|
|
240
|
+
? new target[nativeReactive](target)
|
|
241
|
+
: target
|
|
242
|
+
if (proxied !== target) trackProxyObject(proxied, target)
|
|
243
|
+
const proxy = new Proxy(proxied, reactiveHandlers)
|
|
244
|
+
|
|
245
|
+
// Store the relationships
|
|
246
|
+
storeProxyRelationship(target, proxy)
|
|
247
|
+
return proxy as T
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Main decorator for making classes reactive
|
|
252
|
+
* Automatically makes class instances reactive when created
|
|
253
|
+
*/
|
|
254
|
+
export const reactive = decorator({
|
|
255
|
+
class(original) {
|
|
256
|
+
if (original.prototype instanceof ReactiveBase) {
|
|
257
|
+
reactiveClasses.add(original)
|
|
258
|
+
return original
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
class Reactive extends original {
|
|
262
|
+
constructor(...args: any[]) {
|
|
263
|
+
super(...args)
|
|
264
|
+
if (new.target !== Reactive && !reactiveClasses.has(new.target))
|
|
265
|
+
options.warn(
|
|
266
|
+
`${(original as any).name} has been inherited by ${this.constructor.name} that is not reactive.
|
|
267
|
+
@reactive decorator must be applied to the leaf class OR classes have to extend ReactiveBase.`
|
|
268
|
+
)
|
|
269
|
+
// biome-ignore lint/correctness/noConstructorReturn: This is the whole point here
|
|
270
|
+
return reactive(this)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
Object.defineProperty(Reactive, 'name', {
|
|
274
|
+
value: `Reactive<${original.name}>`,
|
|
275
|
+
})
|
|
276
|
+
return Reactive as any
|
|
277
|
+
},
|
|
278
|
+
get(original: any) {
|
|
279
|
+
return reactiveObject(original)
|
|
280
|
+
},
|
|
281
|
+
default: reactiveObject,
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Gets the original, non-reactive object from a reactive proxy
|
|
286
|
+
* @param proxy - The reactive proxy
|
|
287
|
+
* @returns The original object
|
|
288
|
+
*/
|
|
289
|
+
export { isReactive, objectToProxy, proxyToObject, unwrap } from './proxy-state'
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { ReflectGet, ReflectSet } from '../utils'
|
|
2
|
+
import { touched1 } from './change'
|
|
3
|
+
import { effect } from './effects'
|
|
4
|
+
import { cleanedBy, cleanup } from './interface'
|
|
5
|
+
import { reactive } from './proxy'
|
|
6
|
+
import { type ScopedCallback } from './types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Provides type-safe access to a source object's property within the organized callback.
|
|
10
|
+
* @template Source - The type of the source object
|
|
11
|
+
* @template Key - The type of the property key in the source object
|
|
12
|
+
*/
|
|
13
|
+
export type OrganizedAccess<Source extends Record<PropertyKey, any>, Key extends keyof Source> = {
|
|
14
|
+
/** The property key being accessed */
|
|
15
|
+
readonly key: Key
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Gets the current value of the property from the source object
|
|
19
|
+
* @returns The current value of the property
|
|
20
|
+
*/
|
|
21
|
+
get(): Source[Key]
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Updates the property value in the source object
|
|
25
|
+
* @param value - The new value to set
|
|
26
|
+
* @returns {boolean} True if the update was successful
|
|
27
|
+
*/
|
|
28
|
+
set(value: Source[Key]): boolean
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The current value of the property (equivalent to using get()/set() directly)
|
|
32
|
+
*/
|
|
33
|
+
value: Source[Key]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Callback function type for the organized function that processes each source property.
|
|
38
|
+
* @template Source - The type of the source object
|
|
39
|
+
* @template Target - The type of the target object
|
|
40
|
+
*/
|
|
41
|
+
export type OrganizedCallback<Source extends Record<PropertyKey, any>, Target extends object> = <
|
|
42
|
+
Key extends keyof Source,
|
|
43
|
+
>(
|
|
44
|
+
/**
|
|
45
|
+
* Accessor object for the current source property
|
|
46
|
+
*/
|
|
47
|
+
access: OrganizedAccess<Source, Key>,
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The target object where organized data will be stored
|
|
51
|
+
*/
|
|
52
|
+
target: Target
|
|
53
|
+
) => ScopedCallback | undefined
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The result type of the organized function, combining the target object with cleanup capability.
|
|
57
|
+
* @template Target - The type of the target object
|
|
58
|
+
*/
|
|
59
|
+
export type OrganizedResult<Target extends object> = Target & {
|
|
60
|
+
/**
|
|
61
|
+
* Cleanup function to dispose of all reactive bindings created by organized().
|
|
62
|
+
* This is automatically called when the effect that created the organized binding is disposed.
|
|
63
|
+
*/
|
|
64
|
+
[cleanup]: ScopedCallback
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Organizes a source object's properties into a target object using a callback function.
|
|
69
|
+
* This creates a reactive mapping between source properties and a target object,
|
|
70
|
+
* automatically handling property additions, updates, and removals.
|
|
71
|
+
*
|
|
72
|
+
* @template Source - The type of the source object
|
|
73
|
+
* @template Target - The type of the target object (defaults to Record<PropertyKey, any>)
|
|
74
|
+
*
|
|
75
|
+
* @param {Source} source - The source object to organize
|
|
76
|
+
* @param {OrganizedCallback<Source, Target>} apply - Callback function that defines how each source property is mapped to the target
|
|
77
|
+
* @param {Target} [baseTarget={}] - Optional base target object to use (will be made reactive if not already)
|
|
78
|
+
*
|
|
79
|
+
* @returns {OrganizedResult<Target>} The target object with cleanup capability
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* // Organize user permissions into role-based access
|
|
83
|
+
* const user = reactive({ isAdmin: true, canEdit: false });
|
|
84
|
+
* const permissions = organized(
|
|
85
|
+
* user,
|
|
86
|
+
* (access, target) => {
|
|
87
|
+
* if (access.key === 'isAdmin') {
|
|
88
|
+
* target.hasFullAccess = access.value;
|
|
89
|
+
* }
|
|
90
|
+
* target[`can${access.key.charAt(0).toUpperCase() + access.key.slice(1)}`] = access.value;
|
|
91
|
+
* }
|
|
92
|
+
* );
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* // Transform object structure with cleanup
|
|
96
|
+
* const source = reactive({ firstName: 'John', lastName: 'Doe' });
|
|
97
|
+
* const formatted = organized(
|
|
98
|
+
* source,
|
|
99
|
+
* (access, target) => {
|
|
100
|
+
* if (access.key === 'firstName' || access.key === 'lastName') {
|
|
101
|
+
* target.fullName = `${source.firstName} ${source.lastName}`.trim();
|
|
102
|
+
* }
|
|
103
|
+
* }
|
|
104
|
+
* );
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* // Using with cleanup in a component
|
|
108
|
+
* effect(() => {
|
|
109
|
+
* const data = fetchData();
|
|
110
|
+
* const organizedData = organized(data, (access, target) => {
|
|
111
|
+
* // Transform data
|
|
112
|
+
* });
|
|
113
|
+
*
|
|
114
|
+
* // The cleanup will be called automatically when the effect is disposed
|
|
115
|
+
* return () => organizedData[cleanup]();
|
|
116
|
+
* });
|
|
117
|
+
*/
|
|
118
|
+
export function organized<
|
|
119
|
+
Source extends Record<PropertyKey, any>,
|
|
120
|
+
Target extends object = Record<PropertyKey, any>,
|
|
121
|
+
>(
|
|
122
|
+
source: Source,
|
|
123
|
+
apply: OrganizedCallback<Source, Target>,
|
|
124
|
+
baseTarget: Target = {} as Target
|
|
125
|
+
): OrganizedResult<Target> {
|
|
126
|
+
const observedSource = reactive(source) as Source
|
|
127
|
+
const target = reactive(baseTarget) as Target
|
|
128
|
+
const keyEffects = new Map<PropertyKey, ScopedCallback>()
|
|
129
|
+
|
|
130
|
+
function disposeKey(key: PropertyKey) {
|
|
131
|
+
const stopEffect = keyEffects.get(key)
|
|
132
|
+
if (stopEffect) {
|
|
133
|
+
keyEffects.delete(key)
|
|
134
|
+
stopEffect()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const cleanupKeys = effect(function organizedKeysEffect({ ascend }) {
|
|
139
|
+
//const keys = Reflect.ownKeys(observedSource) as PropertyKey[]
|
|
140
|
+
const keys = new Set<PropertyKey>()
|
|
141
|
+
for (const key in observedSource) keys.add(key)
|
|
142
|
+
|
|
143
|
+
for (const key of keys) {
|
|
144
|
+
if (keyEffects.has(key)) continue
|
|
145
|
+
ascend(() => {
|
|
146
|
+
const stop = effect(function organizedKeyEffect() {
|
|
147
|
+
const sourceKey = key as keyof Source
|
|
148
|
+
const accessBase = {
|
|
149
|
+
key: sourceKey,
|
|
150
|
+
get: () => ReflectGet(observedSource, sourceKey, observedSource),
|
|
151
|
+
set: (value: Source[typeof sourceKey]) =>
|
|
152
|
+
ReflectSet(observedSource, sourceKey, value, observedSource),
|
|
153
|
+
}
|
|
154
|
+
Object.defineProperty(accessBase, 'value', {
|
|
155
|
+
get: accessBase.get,
|
|
156
|
+
set: accessBase.set,
|
|
157
|
+
configurable: true,
|
|
158
|
+
enumerable: true,
|
|
159
|
+
})
|
|
160
|
+
return apply(accessBase as OrganizedAccess<Source, typeof sourceKey>, target)
|
|
161
|
+
})
|
|
162
|
+
keyEffects.set(key, stop)
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const key of Array.from(keyEffects.keys())) if (!keys.has(key)) disposeKey(key)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
return cleanedBy(target, () => {
|
|
170
|
+
cleanupKeys()
|
|
171
|
+
for (const key of Array.from(keyEffects.keys())) disposeKey(key)
|
|
172
|
+
}) as OrganizedResult<Target>
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Organizes a property on a target object
|
|
177
|
+
* Shortcut for defineProperty/delete with touched signal
|
|
178
|
+
* @param target - The target object
|
|
179
|
+
* @param property - The property to organize
|
|
180
|
+
* @param access - The access object
|
|
181
|
+
* @returns The property descriptor
|
|
182
|
+
*/
|
|
183
|
+
export function organize<T>(
|
|
184
|
+
target: object,
|
|
185
|
+
property: PropertyKey,
|
|
186
|
+
access: { get?(): T; set?(value: T): boolean }
|
|
187
|
+
) {
|
|
188
|
+
Object.defineProperty(target, property, {
|
|
189
|
+
get: access.get,
|
|
190
|
+
set: access.set,
|
|
191
|
+
configurable: true,
|
|
192
|
+
enumerable: true,
|
|
193
|
+
})
|
|
194
|
+
touched1(target, { type: 'set', prop: property }, property)
|
|
195
|
+
return () => delete (target as any)[property]
|
|
196
|
+
}
|