-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcomplex-signal.ts
133 lines (118 loc) · 3.37 KB
/
complex-signal.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import { batch, Signal, signal } from '@preact/signals-core'
const batchTheseArrayMethods = new Set([
'clear',
'copyWithin',
'fill',
'pop',
'push',
'reverse',
'shift',
'sort',
'splice',
'unshift'
])
const mapSetMutationMethods = new Set([
'add',
'clear',
'delete',
'set'
])
export function complexSignal<T extends object>(initialValue: T): Signal<T> {
// NOTE: we use inner here so the object identity of the original object will
// remain the same, we can update the signal and trigger effects by creating
// a new outer object
const sig = signal<{ inner: T }>({ inner: initialValue })
// deno-lint-ignore no-explicit-any
const proxies: WeakSet<typeof Proxy<any>> = new WeakSet()
// NOTE: trigger an update by creating a new outer object
const update = (): void => {
sig.value = { inner: sig.value.inner }
}
// deno-lint-ignore no-explicit-any
const handler: ProxyHandler<any> = {
get(target, prop, receiver) {
const original = Reflect.get(target, prop, receiver)
// NOTE: return nested signals as-is
if (original instanceof Signal) {
return original
}
// NOTE: wrap nested objects
if (typeof original === 'object' && !proxies.has(original)) {
// NOTE: cache the proxy so we only ever make a proxy once per object
return target[prop] = wrap(original)
}
// NOTE: batch array mutation methods so we only trigger effects once
if (
Array.isArray(target)
&& typeof prop === 'string'
&& batchTheseArrayMethods.has(prop)
&& typeof original === 'function'
) {
// NOTE: some array methods (like splice) make many assignments so we batch them
// deno-lint-ignore no-explicit-any
return (...args: any[]) => {
return batch(() => {
return original.call(receiver, ...args)
})
}
}
// NOTE: arrays are good now
if (Array.isArray(target)) {
return original
}
// NOTE: maps and sets are kinda annoying
if (
typeof original === 'function' && typeof prop === 'string'
&& (target instanceof Map || target instanceof Set)
) {
if (mapSetMutationMethods.has(prop)) {
// deno-lint-ignore no-explicit-any
return (...args: any) => {
update()
return original.bind(target)(...args)
}
}
if (typeof original === 'function') {
return original.bind(target)
}
}
// NOTE: I don't think this is necessary, but why not
// if (typeof original === 'function') {
// return original.bind(target)
// }
return original
},
set(target, prop, newValue, receiver) {
Reflect.set(target, prop, newValue, receiver)
update()
return true
}
}
function wrap<W extends object>(obj: W) {
const proxy = new Proxy(obj, handler)
proxies.add(proxy)
return proxy
}
sig.value.inner = wrap(sig.value.inner)
return {
...sig,
get value() {
return sig.value.inner
},
set value(newValue: T) {
sig.value = { inner: wrap(newValue) }
},
valueOf() {
return sig.valueOf().inner
},
toJSON() {
return sig.toJSON().inner
},
peek() {
return sig.peek().inner
},
subscribe(cb) {
return sig.subscribe(({ inner }) => cb(inner))
}
}
}