-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathobject.ts
136 lines (109 loc) · 3.34 KB
/
object.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
134
135
136
import { Signal } from "signal-polyfill";
import { createStorage } from "./-private/util.ts";
/**
* Implementation based of tracked-built-ins' TrackedObject
* https://github.com/tracked-tools/tracked-built-ins/blob/master/addon/src/-private/object.js
*/
export class SignalObjectImpl {
static fromEntries<T = unknown>(
entries: Iterable<readonly [PropertyKey, T]>,
) {
return new SignalObjectImpl(Object.fromEntries(entries)) as T;
}
#storages = new Map<PropertyKey, Signal.State<null>>();
#collection = createStorage();
constructor(obj = {}) {
let proto = Object.getPrototypeOf(obj);
let descs = Object.getOwnPropertyDescriptors(obj);
let clone = Object.create(proto);
for (let prop in descs) {
// SAFETY: we just iterated over the property, so having to do an
// existence check here is a little silly
Object.defineProperty(clone, prop, descs[prop]!);
}
let self = this;
return new Proxy(clone, {
get(target, prop, receiver) {
// we don't use the signals directly
// because we don't know (nor care!) what the value would be
// and the value could be replaced
// (this is also important for supporting getters)
self.#readStorageFor(prop);
return Reflect.get(target, prop, receiver);
},
has(target, prop) {
self.#readStorageFor(prop);
return prop in target;
},
ownKeys(target) {
self.#collection.get();
return Reflect.ownKeys(target);
},
set(target, prop, value, receiver) {
let result = Reflect.set(target, prop, value, receiver);
self.#dirtyStorageFor(prop);
self.#dirtyCollection();
return result;
},
deleteProperty(target, prop) {
if (prop in target) {
delete target[prop];
self.#dirtyStorageFor(prop);
self.#dirtyCollection();
}
return true;
},
getPrototypeOf() {
return SignalObjectImpl.prototype;
},
});
}
#readStorageFor(key: PropertyKey) {
let storage = this.#storages.get(key);
if (storage === undefined) {
storage = createStorage();
this.#storages.set(key, storage);
}
storage.get();
}
#dirtyStorageFor(key: PropertyKey) {
const storage = this.#storages.get(key);
if (storage) {
storage.set(null);
}
}
#dirtyCollection() {
this.#collection.set(null);
}
}
interface SignalObject {
fromEntries<T = unknown>(
entries: Iterable<readonly [PropertyKey, T]>,
): { [k: string]: T };
new <T extends Record<PropertyKey, unknown> = Record<PropertyKey, unknown>>(
obj?: T,
): T;
}
// Types are too hard in proxy-implementation
// we want TS to think the SignalObject is Object-like
/**
* Create a reactive Object, backed by Signals, using a Proxy.
* This allows dynamic creation and deletion of signals using the object primitive
* APIs that most folks are familiar with -- the only difference is instantiation.
* ```js
* const obj = new SignalObject({ foo: 123 });
*
* obj.foo // 123
* obj.foo = 456
* obj.foo // 456
* obj.bar = 2
* obj.bar // 2
* ```
*/
export const SignalObject: SignalObject =
SignalObjectImpl as unknown as SignalObject;
export function signalObject<T extends Record<PropertyKey, unknown>>(
obj?: T | undefined,
) {
return new SignalObject(obj);
}