mutts 1.0.1 → 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 +36 -6
- package/dist/chunks/_tslib-BgjropY9.js +81 -0
- package/dist/chunks/_tslib-BgjropY9.js.map +1 -0
- package/dist/chunks/_tslib-Mzh1rNsX.esm.js +75 -0
- package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +1 -0
- package/dist/chunks/{decorator-8qjFb7dw.js → decorator-DLvrD0UF.js} +103 -14
- package/dist/chunks/decorator-DLvrD0UF.js.map +1 -0
- package/dist/chunks/{decorator-AbRkXM5O.esm.js → decorator-DqiszP7i.esm.js} +100 -15
- 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.d.ts +57 -0
- package/dist/decorator.esm.js +1 -1
- package/dist/decorator.js +1 -1
- package/dist/destroyable.d.ts +43 -1
- package/dist/destroyable.esm.js +19 -1
- package/dist/destroyable.esm.js.map +1 -1
- package/dist/destroyable.js +19 -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.d.ts +10 -1
- package/dist/eventful.esm.js +5 -27
- package/dist/eventful.esm.js.map +1 -1
- package/dist/eventful.js +15 -37
- package/dist/eventful.js.map +1 -1
- package/dist/index.d.ts +18 -14
- package/dist/index.esm.js +4 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +44 -5
- package/dist/index.js.map +1 -1
- package/dist/indexable.d.ts +213 -1
- package/dist/indexable.esm.js +203 -3
- package/dist/indexable.esm.js.map +1 -1
- package/dist/indexable.js +204 -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.d.ts +10 -0
- package/dist/promiseChain.esm.js +6 -0
- package/dist/promiseChain.esm.js.map +1 -1
- package/dist/promiseChain.js +6 -0
- package/dist/promiseChain.js.map +1 -1
- package/dist/reactive.d.ts +774 -33
- package/dist/reactive.esm.js +4 -1458
- package/dist/reactive.esm.js.map +1 -1
- package/dist/reactive.js +53 -1474
- package/dist/reactive.js.map +1 -1
- package/dist/std-decorators.d.ts +35 -0
- package/dist/std-decorators.esm.js +36 -1
- package/dist/std-decorators.esm.js.map +1 -1
- package/dist/std-decorators.js +36 -1
- package/dist/std-decorators.js.map +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/mixin.md +229 -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 -2688
- 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/decorator-8qjFb7dw.js.map +0 -1
- package/dist/chunks/decorator-AbRkXM5O.esm.js.map +0 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { nativeReactive, nonReactiveMark } from './types'
|
|
2
|
+
|
|
3
|
+
export const nonReactiveObjects = new WeakSet<object>()
|
|
4
|
+
export const immutables = new Set<(tested: any) => boolean>()
|
|
5
|
+
export const absent = Symbol('absent')
|
|
6
|
+
|
|
7
|
+
function markNonReactive<T extends object[]>(...obj: T): T[0] {
|
|
8
|
+
for (const o of obj) {
|
|
9
|
+
try {
|
|
10
|
+
Object.defineProperty(o, nonReactiveMark, {
|
|
11
|
+
value: true,
|
|
12
|
+
writable: false,
|
|
13
|
+
enumerable: false,
|
|
14
|
+
configurable: false,
|
|
15
|
+
})
|
|
16
|
+
} catch {}
|
|
17
|
+
if (!(nonReactiveMark in (o as object))) nonReactiveObjects.add(o as object)
|
|
18
|
+
}
|
|
19
|
+
return obj[0]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function nonReactiveClass<T extends (new (...args: any[]) => any)[]>(...cls: T): T[0] {
|
|
23
|
+
for (const c of cls) if (c) (c.prototype as any)[nonReactiveMark] = true
|
|
24
|
+
return cls[0]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isNonReactive(obj: any): boolean {
|
|
28
|
+
if (obj === null || typeof obj !== 'object') return true
|
|
29
|
+
if (nonReactiveObjects.has(obj)) return true
|
|
30
|
+
if ((obj as any)[nonReactiveMark]) return true
|
|
31
|
+
for (const fn of immutables) if (fn(obj)) return true
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function registerNativeReactivity(
|
|
36
|
+
originalClass: new (...args: any[]) => any,
|
|
37
|
+
reactiveClass: new (...args: any[]) => any
|
|
38
|
+
) {
|
|
39
|
+
originalClass.prototype[nativeReactive] = reactiveClass
|
|
40
|
+
nonReactiveClass(reactiveClass)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
nonReactiveClass(Date, RegExp, Error, Promise, Function)
|
|
44
|
+
if (typeof window !== 'undefined') {
|
|
45
|
+
markNonReactive(window, document)
|
|
46
|
+
nonReactiveClass(Node, Element, HTMLElement, EventTarget)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { markNonReactive as nonReactive }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
absent,
|
|
3
|
+
immutables,
|
|
4
|
+
isNonReactive,
|
|
5
|
+
nonReactive,
|
|
6
|
+
nonReactiveClass,
|
|
7
|
+
nonReactiveObjects,
|
|
8
|
+
registerNativeReactivity,
|
|
9
|
+
} from './non-reactive-state'
|
|
10
|
+
import { reactive } from './proxy'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Converts an iterator to a generator that yields reactive values
|
|
14
|
+
*/
|
|
15
|
+
export function* makeReactiveIterator<T>(iterator: Iterator<T>): Generator<T> {
|
|
16
|
+
let result = iterator.next()
|
|
17
|
+
while (!result.done) {
|
|
18
|
+
yield reactive(result.value)
|
|
19
|
+
result = iterator.next()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Converts an iterator of key-value pairs to a generator that yields reactive key-value pairs
|
|
25
|
+
*/
|
|
26
|
+
export function* makeReactiveEntriesIterator<K, V>(iterator: Iterator<[K, V]>): Generator<[K, V]> {
|
|
27
|
+
let result = iterator.next()
|
|
28
|
+
while (!result.done) {
|
|
29
|
+
const [key, value] = result.value
|
|
30
|
+
yield [reactive(key), reactive(value)]
|
|
31
|
+
result = iterator.next()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
absent,
|
|
37
|
+
immutables,
|
|
38
|
+
isNonReactive,
|
|
39
|
+
nonReactive,
|
|
40
|
+
nonReactiveClass,
|
|
41
|
+
nonReactiveObjects,
|
|
42
|
+
registerNativeReactivity,
|
|
43
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Reactive Register Memoization Notes
|
|
2
|
+
|
|
3
|
+
## Background
|
|
4
|
+
|
|
5
|
+
- Register entries are stored in a reactive `Map`. Updating an entry via `Map.set` marks the entire value as changed.
|
|
6
|
+
- Memoized computations that read through `map.get(key)` re-execute fully when the entry changes, even if only a nested property is touched.
|
|
7
|
+
- The goal for rendering lists in a JSX/HTML engine is to avoid rebuilding DOM nodes; only the affected properties should update.
|
|
8
|
+
|
|
9
|
+
## Evolution: From `organized` to `project`
|
|
10
|
+
|
|
11
|
+
### Initial State
|
|
12
|
+
|
|
13
|
+
- `memoize` caches results but invalidates on `Map.set`, so large effects still re-run.
|
|
14
|
+
- `organized` (designed for `Record` sources) creates per-key effects so downstream work reruns only for the touched key; this matches the desired behaviour.
|
|
15
|
+
- **Gap:** `organized` operates on plain objects: key enumeration relies on property iteration and `ReflectGet/Set`. Registers and other keyed collections (`Map`, `Register`, custom stores) need the same per-entry orchestration without converting to records.
|
|
16
|
+
|
|
17
|
+
### Completed Evolution: `project` Implementation
|
|
18
|
+
|
|
19
|
+
We implemented `project` as a generalized transformation helper that works across arrays, records, and maps:
|
|
20
|
+
|
|
21
|
+
**Key Design Decisions:**
|
|
22
|
+
- **Unified API:** Single `project` function with runtime dispatch to `project.array`, `project.record`, or `project.map` based on source type.
|
|
23
|
+
- **Access Pattern:** Callback receives a `ProjectAccess` object with `get()`, `set()`, `key`, `source`, and `value` (computed property) - similar to `organized` but returning a value instead of an effect.
|
|
24
|
+
- **Automatic Target Creation:** The function always creates its own reactive target container (array, record, or map) - no `baseTarget` parameter needed.
|
|
25
|
+
- **Per-Key Effects:** Each source key/index gets its own reactive effect that recomputes only when that specific entry changes, enabling granular updates for rendering pipelines.
|
|
26
|
+
|
|
27
|
+
**Current API:**
|
|
28
|
+
```typescript
|
|
29
|
+
project.array(source: readonly T[], apply: (access, target) => U): ProjectResult<U[]>
|
|
30
|
+
project.record(source: Record<K, T>, apply: (access, target) => U): ProjectResult<Record<K, U>>
|
|
31
|
+
project.map(source: Map<K, T>, apply: (access, target) => U): ProjectResult<Map<K, U>>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Current Behavior:**
|
|
35
|
+
- Eager computation: all entries are computed immediately when keys are present.
|
|
36
|
+
- One-way transformation: callback only handles "get" (read) operations; no write-back support.
|
|
37
|
+
- Mutable results: returned arrays/records/maps are fully mutable.
|
|
38
|
+
- `ProjectAccess.old` exposes the previously computed result for each entry, enabling incremental updates and state preservation.
|
|
39
|
+
|
|
40
|
+
## Future Evolutions
|
|
41
|
+
|
|
42
|
+
### Bidirectional Transformation (Set Callback)
|
|
43
|
+
|
|
44
|
+
**Goal:** Support writing back to the source through the projected object.
|
|
45
|
+
|
|
46
|
+
**Implementation:**
|
|
47
|
+
- Add optional second callback parameter: `set: (access, newValue, target) => void | boolean`
|
|
48
|
+
- When `set` is provided, mutations to the projected object trigger the set callback.
|
|
49
|
+
- The callback receives the same `ProjectAccess` object plus the new value, allowing it to update the source.
|
|
50
|
+
- If `set` returns `false` or throws, the mutation is rejected.
|
|
51
|
+
|
|
52
|
+
**API Impact:**
|
|
53
|
+
```typescript
|
|
54
|
+
project.array(
|
|
55
|
+
source: readonly T[],
|
|
56
|
+
apply: (access, target) => U,
|
|
57
|
+
set?: (access, newValue: U, target) => void | boolean
|
|
58
|
+
): ProjectResult<U[]>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Readonly Results When No Set Callback
|
|
62
|
+
|
|
63
|
+
**Goal:** Make projected objects readonly when no write-back is supported.
|
|
64
|
+
|
|
65
|
+
**Implementation:**
|
|
66
|
+
- When `set` callback is not provided, wrap the result in a readonly proxy or use read-only array/record types.
|
|
67
|
+
- Prevents accidental mutations that would have no effect on the source.
|
|
68
|
+
- Type system should reflect readonly nature in return types.
|
|
69
|
+
|
|
70
|
+
**API Impact:**
|
|
71
|
+
- Return type becomes `ProjectResult<readonly U[]>` or similar when `set` is omitted.
|
|
72
|
+
- Arrays use `ReactiveReadOnlyArray` (already exists in `mapped.ts`).
|
|
73
|
+
- Records and maps need readonly wrappers or proxy-based protection.
|
|
74
|
+
|
|
75
|
+
## Implementation Notes for AI Agents
|
|
76
|
+
|
|
77
|
+
**Current State:**
|
|
78
|
+
- `project` is fully functional for arrays, records, maps, and registers with eager, one-way transformation.
|
|
79
|
+
- Tests cover per-key reactivity, key addition/removal, automatic helper selection, and `access.old` value propagation.
|
|
80
|
+
- The implementation follows the same per-key effect pattern as `organized` but returns computed values.
|
|
81
|
+
|
|
82
|
+
**Future Work:**
|
|
83
|
+
- Lazy computing requires careful effect lifecycle management to avoid memory leaks.
|
|
84
|
+
- Bidirectional support needs to handle edge cases (concurrent reads/writes, validation).
|
|
85
|
+
- Readonly enforcement should align with existing `ReactiveReadOnlyArray` patterns where possible.
|
|
86
|
+
- Consider whether these features should be opt-in via options object or separate function variants.
|
|
87
|
+
|
|
88
|
+
**Related Files:**
|
|
89
|
+
- `src/reactive/project.ts` - Main implementation
|
|
90
|
+
- `src/reactive/mapped.ts` - Reference for `ReactiveReadOnlyArray` pattern
|
|
91
|
+
- `src/reactive/record.ts` - Reference for `organized` pattern (different use case)
|
|
92
|
+
- `tests/reactive/project.test.ts` - Test coverage
|
|
93
|
+
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { ReflectGet, ReflectSet } from '../utils'
|
|
2
|
+
import { effect, untracked } from './effects'
|
|
3
|
+
import { cleanedBy, cleanup } from './interface'
|
|
4
|
+
import { reactive } from './proxy'
|
|
5
|
+
import { Register } from './register'
|
|
6
|
+
import { type ScopedCallback } from './types'
|
|
7
|
+
|
|
8
|
+
type ProjectOldValue<Target> = Target extends readonly (infer Item)[]
|
|
9
|
+
? Item
|
|
10
|
+
: Target extends Map<any, infer Item>
|
|
11
|
+
? Item
|
|
12
|
+
: Target extends Record<PropertyKey, infer Item>
|
|
13
|
+
? Item
|
|
14
|
+
: unknown
|
|
15
|
+
|
|
16
|
+
export type ProjectAccess<SourceValue, Key, SourceType, Target> = {
|
|
17
|
+
readonly key: Key
|
|
18
|
+
get(): SourceValue
|
|
19
|
+
set(value: SourceValue): boolean
|
|
20
|
+
readonly source: SourceType
|
|
21
|
+
readonly old?: ProjectOldValue<Target>
|
|
22
|
+
value: SourceValue
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type BivariantProjectCallback<Args extends any[], Return> = {
|
|
26
|
+
bivarianceHack(...args: Args): Return
|
|
27
|
+
}['bivarianceHack']
|
|
28
|
+
|
|
29
|
+
export type ProjectCallback<
|
|
30
|
+
SourceValue,
|
|
31
|
+
Key,
|
|
32
|
+
Target extends object,
|
|
33
|
+
SourceType,
|
|
34
|
+
Result,
|
|
35
|
+
> = BivariantProjectCallback<[ProjectAccess<SourceValue, Key, SourceType, Target>, Target], Result>
|
|
36
|
+
|
|
37
|
+
export type ProjectResult<Target extends object> = Target & { [cleanup]: ScopedCallback }
|
|
38
|
+
|
|
39
|
+
function defineAccessValue<Access extends { get(): unknown; set(value: unknown): boolean }>(
|
|
40
|
+
access: Access
|
|
41
|
+
) {
|
|
42
|
+
Object.defineProperty(access, 'value', {
|
|
43
|
+
get: access.get,
|
|
44
|
+
set: access.set,
|
|
45
|
+
configurable: true,
|
|
46
|
+
enumerable: true,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeCleanup<Result extends object>(
|
|
51
|
+
target: Result,
|
|
52
|
+
effectMap: Map<unknown, ScopedCallback>,
|
|
53
|
+
onDispose: () => void
|
|
54
|
+
): ProjectResult<Result> {
|
|
55
|
+
return cleanedBy(target, () => {
|
|
56
|
+
onDispose()
|
|
57
|
+
for (const stop of effectMap.values()) stop?.()
|
|
58
|
+
effectMap.clear()
|
|
59
|
+
}) as ProjectResult<Result>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function projectArray<SourceValue, ResultValue>(
|
|
63
|
+
source: readonly SourceValue[],
|
|
64
|
+
apply: ProjectCallback<SourceValue, number, ResultValue[], readonly SourceValue[], ResultValue>
|
|
65
|
+
): ProjectResult<ResultValue[]> {
|
|
66
|
+
const observedSource = reactive(source) as readonly SourceValue[]
|
|
67
|
+
const target = reactive([] as ResultValue[])
|
|
68
|
+
const indexEffects = new Map<number, ScopedCallback>()
|
|
69
|
+
|
|
70
|
+
function normalizeTargetLength(length: number) {
|
|
71
|
+
ReflectSet(target as unknown as object, 'length', length, target)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function disposeIndex(index: number) {
|
|
75
|
+
const stopEffect = indexEffects.get(index)
|
|
76
|
+
if (stopEffect) {
|
|
77
|
+
indexEffects.delete(index)
|
|
78
|
+
stopEffect()
|
|
79
|
+
Reflect.deleteProperty(target as unknown as object, index)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const cleanupLength = effect(function projectArrayLengthEffect({ ascend }) {
|
|
84
|
+
const length = observedSource.length
|
|
85
|
+
normalizeTargetLength(length)
|
|
86
|
+
const existing = Array.from(indexEffects.keys())
|
|
87
|
+
for (let i = 0; i < length; i++) {
|
|
88
|
+
if (indexEffects.has(i)) continue
|
|
89
|
+
ascend(() => {
|
|
90
|
+
const index = i
|
|
91
|
+
const stop = effect(function projectArrayIndexEffect() {
|
|
92
|
+
const previous = untracked(() => target[index])
|
|
93
|
+
const accessBase = {
|
|
94
|
+
key: index,
|
|
95
|
+
source: observedSource,
|
|
96
|
+
get: () => ReflectGet(observedSource as any, index, observedSource),
|
|
97
|
+
set: (value: SourceValue) =>
|
|
98
|
+
ReflectSet(observedSource as any, index, value, observedSource),
|
|
99
|
+
old: previous,
|
|
100
|
+
} as ProjectAccess<SourceValue, number, readonly SourceValue[], ResultValue[]>
|
|
101
|
+
defineAccessValue(accessBase)
|
|
102
|
+
const produced = apply(accessBase, target)
|
|
103
|
+
target[index] = produced
|
|
104
|
+
})
|
|
105
|
+
indexEffects.set(i, stop)
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
for (const index of existing) if (index >= length) disposeIndex(index)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return makeCleanup(target, indexEffects, () => cleanupLength())
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function projectRegister<Key extends PropertyKey, SourceValue, ResultValue>(
|
|
115
|
+
source: Register<SourceValue, Key>,
|
|
116
|
+
apply: ProjectCallback<
|
|
117
|
+
SourceValue,
|
|
118
|
+
Key,
|
|
119
|
+
Map<Key, ResultValue>,
|
|
120
|
+
Register<SourceValue, Key>,
|
|
121
|
+
ResultValue
|
|
122
|
+
>
|
|
123
|
+
): ProjectResult<Map<Key, ResultValue>> {
|
|
124
|
+
const observedSource = reactive(source) as Register<SourceValue, Key>
|
|
125
|
+
const rawTarget = new Map<Key, ResultValue>()
|
|
126
|
+
const target = reactive(rawTarget) as Map<Key, ResultValue>
|
|
127
|
+
const keyEffects = new Map<Key, ScopedCallback>()
|
|
128
|
+
|
|
129
|
+
function disposeKey(key: Key) {
|
|
130
|
+
const stopEffect = keyEffects.get(key)
|
|
131
|
+
if (stopEffect) {
|
|
132
|
+
stopEffect()
|
|
133
|
+
keyEffects.delete(key)
|
|
134
|
+
target.delete(key)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const cleanupKeys = effect(function projectRegisterEffect({ ascend }) {
|
|
139
|
+
const keys = new Set<Key>()
|
|
140
|
+
for (const key of observedSource.mapKeys()) keys.add(key)
|
|
141
|
+
|
|
142
|
+
for (const key of keys) {
|
|
143
|
+
if (keyEffects.has(key)) continue
|
|
144
|
+
ascend(() => {
|
|
145
|
+
const stop = effect(function projectRegisterKeyEffect() {
|
|
146
|
+
const previous = untracked(() => target.get(key))
|
|
147
|
+
const accessBase = {
|
|
148
|
+
key,
|
|
149
|
+
source: observedSource,
|
|
150
|
+
get: () => observedSource.get(key) as SourceValue,
|
|
151
|
+
set: (value: SourceValue) => {
|
|
152
|
+
observedSource.set(key, value)
|
|
153
|
+
return true
|
|
154
|
+
},
|
|
155
|
+
old: previous,
|
|
156
|
+
} as ProjectAccess<SourceValue, Key, Register<SourceValue, Key>, Map<Key, ResultValue>>
|
|
157
|
+
defineAccessValue(accessBase)
|
|
158
|
+
const produced = apply(accessBase, target)
|
|
159
|
+
target.set(key, produced)
|
|
160
|
+
})
|
|
161
|
+
keyEffects.set(key, stop)
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const key of Array.from(keyEffects.keys())) if (!keys.has(key)) disposeKey(key)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
return makeCleanup(target, keyEffects, () => cleanupKeys())
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function projectRecord<Source extends Record<PropertyKey, any>, ResultValue>(
|
|
172
|
+
source: Source,
|
|
173
|
+
apply: ProjectCallback<
|
|
174
|
+
Source[keyof Source],
|
|
175
|
+
keyof Source,
|
|
176
|
+
Record<keyof Source, ResultValue>,
|
|
177
|
+
Source,
|
|
178
|
+
ResultValue
|
|
179
|
+
>
|
|
180
|
+
): ProjectResult<Record<keyof Source, ResultValue>> {
|
|
181
|
+
const observedSource = reactive(source) as Source
|
|
182
|
+
const target = reactive({} as Record<keyof Source, ResultValue>)
|
|
183
|
+
const keyEffects = new Map<PropertyKey, ScopedCallback>()
|
|
184
|
+
|
|
185
|
+
function disposeKey(key: PropertyKey) {
|
|
186
|
+
const stopEffect = keyEffects.get(key)
|
|
187
|
+
if (stopEffect) {
|
|
188
|
+
stopEffect()
|
|
189
|
+
keyEffects.delete(key)
|
|
190
|
+
Reflect.deleteProperty(target as Record<PropertyKey, unknown>, key)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const cleanupKeys = effect(function projectRecordEffect({ ascend }) {
|
|
195
|
+
const keys = new Set<PropertyKey>()
|
|
196
|
+
for (const key in observedSource) keys.add(key)
|
|
197
|
+
const observed = Reflect.ownKeys(observedSource)
|
|
198
|
+
for (const key of observed) keys.add(key)
|
|
199
|
+
|
|
200
|
+
for (const key of keys) {
|
|
201
|
+
if (keyEffects.has(key)) continue
|
|
202
|
+
ascend(() => {
|
|
203
|
+
const stop = effect(function projectRecordKeyEffect() {
|
|
204
|
+
const sourceKey = key as keyof Source
|
|
205
|
+
const previous = untracked(
|
|
206
|
+
() => (target as Record<PropertyKey, ResultValue | undefined>)[key]
|
|
207
|
+
)
|
|
208
|
+
const accessBase = {
|
|
209
|
+
key: sourceKey,
|
|
210
|
+
source: observedSource,
|
|
211
|
+
get: () => ReflectGet(observedSource, sourceKey, observedSource),
|
|
212
|
+
set: (value: Source[typeof sourceKey]) =>
|
|
213
|
+
ReflectSet(observedSource, sourceKey, value, observedSource),
|
|
214
|
+
old: previous,
|
|
215
|
+
} as ProjectAccess<
|
|
216
|
+
Source[typeof sourceKey],
|
|
217
|
+
keyof Source,
|
|
218
|
+
Source,
|
|
219
|
+
Record<keyof Source, ResultValue>
|
|
220
|
+
>
|
|
221
|
+
defineAccessValue(accessBase)
|
|
222
|
+
const produced = apply(accessBase, target)
|
|
223
|
+
;(target as any)[sourceKey] = produced
|
|
224
|
+
})
|
|
225
|
+
keyEffects.set(key, stop)
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (const key of Array.from(keyEffects.keys())) if (!keys.has(key)) disposeKey(key)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
return makeCleanup(target, keyEffects, () => cleanupKeys())
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function projectMap<Key, Value, ResultValue>(
|
|
236
|
+
source: Map<Key, Value>,
|
|
237
|
+
apply: ProjectCallback<Value, Key, Map<Key, ResultValue>, Map<Key, Value>, ResultValue>
|
|
238
|
+
): ProjectResult<Map<Key, ResultValue>> {
|
|
239
|
+
const observedSource = reactive(source) as Map<Key, Value>
|
|
240
|
+
const rawTarget = new Map<Key, ResultValue>()
|
|
241
|
+
const target = reactive(rawTarget) as Map<Key, ResultValue>
|
|
242
|
+
const keyEffects = new Map<Key, ScopedCallback>()
|
|
243
|
+
|
|
244
|
+
function disposeKey(key: Key) {
|
|
245
|
+
const stopEffect = keyEffects.get(key)
|
|
246
|
+
if (stopEffect) {
|
|
247
|
+
stopEffect()
|
|
248
|
+
keyEffects.delete(key)
|
|
249
|
+
target.delete(key)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const cleanupKeys = effect(function projectMapEffect({ ascend }) {
|
|
254
|
+
const keys = new Set<Key>()
|
|
255
|
+
for (const key of observedSource.keys()) keys.add(key)
|
|
256
|
+
|
|
257
|
+
for (const key of keys) {
|
|
258
|
+
if (keyEffects.has(key)) continue
|
|
259
|
+
ascend(() => {
|
|
260
|
+
const stop = effect(function projectMapKeyEffect() {
|
|
261
|
+
const previous = untracked(() => target.get(key))
|
|
262
|
+
const accessBase = {
|
|
263
|
+
key,
|
|
264
|
+
source: observedSource,
|
|
265
|
+
get: () => observedSource.get(key) as Value,
|
|
266
|
+
set: (value: Value) => {
|
|
267
|
+
observedSource.set(key, value)
|
|
268
|
+
return true
|
|
269
|
+
},
|
|
270
|
+
old: previous,
|
|
271
|
+
} as ProjectAccess<Value, Key, Map<Key, Value>, Map<Key, ResultValue>>
|
|
272
|
+
defineAccessValue(accessBase)
|
|
273
|
+
const produced = apply(accessBase, target)
|
|
274
|
+
target.set(key, produced)
|
|
275
|
+
})
|
|
276
|
+
keyEffects.set(key, stop)
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const key of Array.from(keyEffects.keys())) if (!keys.has(key)) disposeKey(key)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
return makeCleanup(target, keyEffects, () => cleanupKeys())
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
type ProjectOverload = {
|
|
287
|
+
<SourceValue, ResultValue>(
|
|
288
|
+
source: readonly SourceValue[],
|
|
289
|
+
apply: ProjectCallback<SourceValue, number, ResultValue[], readonly SourceValue[], ResultValue>
|
|
290
|
+
): ProjectResult<ResultValue[]>
|
|
291
|
+
<Key extends PropertyKey, SourceValue, ResultValue>(
|
|
292
|
+
source: Register<SourceValue, Key>,
|
|
293
|
+
apply: ProjectCallback<
|
|
294
|
+
SourceValue,
|
|
295
|
+
Key,
|
|
296
|
+
Map<Key, ResultValue>,
|
|
297
|
+
Register<SourceValue, Key>,
|
|
298
|
+
ResultValue
|
|
299
|
+
>
|
|
300
|
+
): ProjectResult<Map<Key, ResultValue>>
|
|
301
|
+
<Source extends Record<PropertyKey, any>, ResultValue>(
|
|
302
|
+
source: Source,
|
|
303
|
+
apply: ProjectCallback<
|
|
304
|
+
Source[keyof Source],
|
|
305
|
+
keyof Source,
|
|
306
|
+
Record<keyof Source, ResultValue>,
|
|
307
|
+
Source,
|
|
308
|
+
ResultValue
|
|
309
|
+
>
|
|
310
|
+
): ProjectResult<Record<keyof Source, ResultValue>>
|
|
311
|
+
<Key, Value, ResultValue>(
|
|
312
|
+
source: Map<Key, Value>,
|
|
313
|
+
apply: ProjectCallback<Value, Key, Map<Key, ResultValue>, Map<Key, Value>, ResultValue>
|
|
314
|
+
): ProjectResult<Map<Key, ResultValue>>
|
|
315
|
+
array: typeof projectArray
|
|
316
|
+
register: typeof projectRegister
|
|
317
|
+
record: typeof projectRecord
|
|
318
|
+
map: typeof projectMap
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function projectCore(source: any, apply: any): ProjectResult<any> {
|
|
322
|
+
if (Array.isArray(source)) return projectArray(source, apply)
|
|
323
|
+
if (source instanceof Map) return projectMap(source, apply)
|
|
324
|
+
if (source instanceof Register) return projectRegister(source, apply)
|
|
325
|
+
if (source && (source.constructor === Object || source.constructor === undefined))
|
|
326
|
+
return projectRecord(source, apply)
|
|
327
|
+
throw new Error('Unsupported source type')
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export const project: ProjectOverload = Object.assign(projectCore, {
|
|
331
|
+
array: projectArray,
|
|
332
|
+
register: projectRegister,
|
|
333
|
+
record: projectRecord,
|
|
334
|
+
map: projectMap,
|
|
335
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const objectToProxy = new WeakMap<object, object>()
|
|
2
|
+
export const proxyToObject = new WeakMap<object, object>()
|
|
3
|
+
|
|
4
|
+
export function storeProxyRelationship(target: object, proxy: object) {
|
|
5
|
+
objectToProxy.set(target, proxy)
|
|
6
|
+
proxyToObject.set(proxy, target)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getExistingProxy<T extends object>(target: T): T | undefined {
|
|
10
|
+
return objectToProxy.get(target) as T | undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function trackProxyObject(proxy: object, target: object) {
|
|
14
|
+
proxyToObject.set(proxy, target)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function unwrap<T>(obj: T): T {
|
|
18
|
+
let current = obj
|
|
19
|
+
while (current && typeof current === 'object' && current !== null && proxyToObject.has(current)) {
|
|
20
|
+
current = proxyToObject.get(current) as T
|
|
21
|
+
}
|
|
22
|
+
return current
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isReactive(obj: any): boolean {
|
|
26
|
+
return proxyToObject.has(obj)
|
|
27
|
+
}
|