Skip to content

Commit 4fac3d3

Browse files
committed
feat: proxyStore
1 parent abfdc0d commit 4fac3d3

File tree

3 files changed

+534
-0
lines changed

3 files changed

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

0 commit comments

Comments
 (0)