Skip to content

Commit a0364c5

Browse files
committed
feat: proxyStore
1 parent abfdc0d commit a0364c5

File tree

3 files changed

+541
-0
lines changed

3 files changed

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

0 commit comments

Comments
 (0)