Skip to content

Commit de5f0e7

Browse files
committed
feat: proxyStore
1 parent abfdc0d commit de5f0e7

File tree

3 files changed

+532
-0
lines changed

3 files changed

+532
-0
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export { batch } from './internal/batch';
4949
export { equal } from './internal/equal';
5050
export { symbolObservable } from './internal/exposeRawStores';
5151
export { untrack } from './internal/untrack';
52+
export { proxyStore } from './internal/proxy';
5253
export type * from './types';
5354

5455
/**

src/internal/proxy.ts

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import { batch } from './batch';
2+
import { RawStoreComputed } from './storeComputed';
3+
import { RawStoreWritable } from './storeWritable';
4+
import { activeConsumer } from './untrack';
5+
6+
const returnFalse = () => false;
7+
const arrayEquals = <T>(a: T[], b: T[]) => {
8+
const aLength = a.length;
9+
if (aLength !== b.length) {
10+
return false;
11+
}
12+
for (let i = 0; i < aLength; i++) {
13+
if (a[i] !== b[i]) {
14+
return false;
15+
}
16+
}
17+
return true;
18+
};
19+
20+
const objectProto = Object.prototype;
21+
const arrayProto = Array.prototype;
22+
23+
class TansuArrayStore extends Array {}
24+
class TansuObjectStore {}
25+
26+
const wrapInBatch = <T, U extends any[], V>(originalFn: (this: T, ...args: U) => V) =>
27+
function (this: T, ...args: U) {
28+
return batch(() => originalFn.call(this, ...args));
29+
};
30+
31+
const tansuArrayProto = TansuArrayStore.prototype;
32+
for (const fnName of ['fill', 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift']) {
33+
const fn = (arrayProto as any)[fnName];
34+
if (typeof fn === 'function') {
35+
(tansuArrayProto as any)[fnName] = wrapInBatch(fn);
36+
}
37+
}
38+
39+
const createProxyStore = <T extends object>(initialValue: T): T => {
40+
const isArray = Array.isArray(initialValue);
41+
const constructor: any = isArray ? TansuArrayStore : TansuObjectStore;
42+
const proto = constructor.prototype;
43+
const proxyTarget = new constructor();
44+
const proxyTarget$ = new RawStoreWritable(proxyTarget);
45+
proxyTarget$.equalFn = returnFalse;
46+
const keysInfo: Record<string | symbol, KeyInfo<any>> = Object.create(null);
47+
let length: RawStoreComputed<number> | undefined;
48+
let ownKeys: RawStoreComputed<(string | symbol)[]> | undefined;
49+
50+
class KeyInfo<U> {
51+
private store: RawStoreWritable<U | undefined>;
52+
private existsStore?: RawStoreWritable<boolean>;
53+
constructor(
54+
private key: string | symbol,
55+
initialValue?: U,
56+
private existsValue = false
57+
) {
58+
this.store = new RawStoreWritable(initialValue);
59+
this.store.equalFn = Object.is;
60+
}
61+
62+
get = () => this.store.get();
63+
set = (value: U | undefined) =>
64+
batch(() => {
65+
value = proxyStore(value);
66+
proxyTarget[this.key] = value;
67+
if (!this.existsValue) {
68+
proxyTarget$.set(proxyTarget);
69+
this.existsStore?.set(true);
70+
this.existsValue = true;
71+
}
72+
this.store.set(value);
73+
});
74+
75+
exists() {
76+
let existsStore = this.existsStore;
77+
if (!existsStore) {
78+
existsStore = new RawStoreWritable(this.existsValue);
79+
this.existsStore = existsStore;
80+
}
81+
return existsStore.get();
82+
}
83+
84+
remove() {
85+
if (this.existsValue) {
86+
batch(() => {
87+
delete proxyTarget[this.key];
88+
proxyTarget$.set(proxyTarget);
89+
this.store.set(undefined);
90+
this.existsValue = false;
91+
this.existsStore?.set(false);
92+
});
93+
}
94+
}
95+
}
96+
97+
for (const key of Reflect.ownKeys(initialValue)) {
98+
if (isArray && key === 'length') {
99+
proxyTarget.length = initialValue.length;
100+
continue;
101+
}
102+
const value = proxyStore((initialValue as any)[key]);
103+
proxyTarget[key] = value;
104+
keysInfo[key] = new KeyInfo(key, value, true);
105+
}
106+
const getKeyInfo = (key: any) => {
107+
let keyInfo = keysInfo[key];
108+
if (!keyInfo) {
109+
keyInfo = new KeyInfo(key);
110+
keysInfo[key] = keyInfo;
111+
}
112+
return keyInfo;
113+
};
114+
return new Proxy(proxyTarget as T, {
115+
get(target, key) {
116+
if (!activeConsumer) {
117+
// quick path
118+
return proxyTarget[key];
119+
}
120+
if (isArray && key === 'length') {
121+
if (!length) {
122+
length = new RawStoreComputed(() => proxyTarget$.get().length);
123+
}
124+
return length.get();
125+
}
126+
return getKeyInfo(key).get() ?? proto[key];
127+
},
128+
set(target, key, value) {
129+
if (isArray && key === 'length') {
130+
batch(() => {
131+
const prevLength = proxyTarget.length;
132+
proxyTarget.length = value;
133+
const newLength = proxyTarget.length;
134+
if (newLength !== prevLength) {
135+
proxyTarget$.set(proxyTarget);
136+
}
137+
for (let i = newLength; i < prevLength; i++) {
138+
keysInfo[i]?.remove();
139+
}
140+
});
141+
return true;
142+
}
143+
getKeyInfo(key).set(value);
144+
return true;
145+
},
146+
deleteProperty(target, key) {
147+
if (isArray && key === 'length') {
148+
return false;
149+
}
150+
keysInfo[key]?.remove();
151+
return true;
152+
},
153+
has(target, key) {
154+
if (!activeConsumer) {
155+
// quick path
156+
return key in proxyTarget;
157+
}
158+
return getKeyInfo(key).exists() || key in proto;
159+
},
160+
ownKeys() {
161+
if (!ownKeys) {
162+
ownKeys = new RawStoreComputed(() => Reflect.ownKeys(proxyTarget$.get()));
163+
ownKeys.equalFn = arrayEquals;
164+
}
165+
return [...ownKeys.get()];
166+
},
167+
getOwnPropertyDescriptor(target, key) {
168+
if (!activeConsumer || (isArray && key === 'length')) {
169+
return Reflect.getOwnPropertyDescriptor(proxyTarget, key);
170+
}
171+
const keyInfo = getKeyInfo(key);
172+
if (!keyInfo.exists()) {
173+
return undefined;
174+
}
175+
return {
176+
configurable: true,
177+
enumerable: true,
178+
get: keyInfo.get,
179+
set: keyInfo.set,
180+
};
181+
},
182+
// unsupported features:
183+
preventExtensions() {
184+
return false;
185+
},
186+
defineProperty() {
187+
return false;
188+
},
189+
setPrototypeOf() {
190+
return false;
191+
},
192+
});
193+
};
194+
195+
/**
196+
* Create a {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy|Proxy} object
197+
* which recursively implements each of its properties as a Tansu store.
198+
*
199+
* @example
200+
* ```ts
201+
* const myStore = proxyStore({ a: 1, b: 2 });
202+
* const aTimesB = computed(() => myStore.a * myStore.b);
203+
* aTimesB.subscribe((value) => console.log(value)); // logs: 2
204+
* myStore.a = 2; // logs: 4
205+
* myStore.b = 3; // logs: 6
206+
* ```
207+
*
208+
* @param initialValue - The initial value of the object. It is copied and wrapped in Tansu stores.
209+
* @returns The proxy store object.
210+
*/
211+
export const proxyStore = <T>(initialValue: T): T => {
212+
if (typeof initialValue !== 'object' || initialValue == null) {
213+
return initialValue;
214+
}
215+
const proto = Object.getPrototypeOf(initialValue);
216+
if (proto === objectProto || proto === arrayProto) {
217+
return createProxyStore(initialValue);
218+
}
219+
return initialValue;
220+
};

0 commit comments

Comments
 (0)