@esmx/class-state 3.0.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/connect.ts ADDED
@@ -0,0 +1,306 @@
1
+ /* eslint-disable @typescript-eslint/no-this-alias */
2
+ import { produce } from 'immer';
3
+
4
+ import { type State, type StateContext, getStateContext } from './create';
5
+ export type StoreParams = Record<string, any>;
6
+ export type StoreConstructor = new (cacheKey?: string) => any;
7
+
8
+ export type StoreInstance<T extends {}> = T & { $: StoreContext<T> };
9
+
10
+ let currentStateContext: StateContext | null = null;
11
+
12
+ /**
13
+ * class created
14
+ */
15
+ export const LIFE_CYCLE_CREATED = Symbol('class created');
16
+ /**
17
+ * class dispose
18
+ */
19
+ export const LIFE_CYCLE_DISPOSE = Symbol('class dispose');
20
+
21
+ export type StoreSubscribe = () => void;
22
+
23
+ // 订阅的id
24
+ let sid = 0;
25
+
26
+ function noon() {}
27
+
28
+ export class StoreContext<T extends {}> {
29
+ /**
30
+ * 全局的状态上下文
31
+ */
32
+ private _stateContext: StateContext | null;
33
+ /**
34
+ * 原始实例
35
+ */
36
+ private readonly _raw: T;
37
+ /**
38
+ * 原始实例的代理,每次状态变化时,代理都会更新
39
+ */
40
+ private _proxy: StoreInstance<T>;
41
+ /**
42
+ * 当前的 state 是否是草稿状态
43
+ */
44
+ private _drafting = false;
45
+ /**
46
+ * $ 函数的缓存对象
47
+ */
48
+ private readonly _cacheCommit = new Map<Function, Function>();
49
+ /**
50
+ * 当前的 store 的存储路径
51
+ */
52
+ public readonly keyPath: string;
53
+ /**
54
+ * 最新的状态
55
+ */
56
+ public state: Record<string, any>;
57
+ /**
58
+ * 当前的 store 的 state 是否已经连接到全局的 state 中
59
+ */
60
+ public connecting: boolean;
61
+ private readonly _subs: Array<{ sid: number; cb: StoreSubscribe }> = [];
62
+
63
+ public constructor(
64
+ stateContext: StateContext,
65
+ raw: T,
66
+ state: Record<string, any>,
67
+ keyPath: string
68
+ ) {
69
+ this._stateContext = stateContext;
70
+ stateContext.add(keyPath, this);
71
+
72
+ this._raw = raw;
73
+ this._proxy = this._createProxyClass();
74
+
75
+ this.state = state;
76
+ this.keyPath = keyPath;
77
+ this.connecting = stateContext.hasState(keyPath);
78
+
79
+ this.get = this.get.bind(this);
80
+ this.subscribe = this.subscribe.bind(this);
81
+ this.dispose = this.dispose.bind(this);
82
+ }
83
+
84
+ /**
85
+ * 获取当前代理实例,每次状态变化时,代理实例都会变化
86
+ * 已绑定 this
87
+ */
88
+ public get() {
89
+ this._depend();
90
+ return this._proxy;
91
+ }
92
+
93
+ /**
94
+ * 私有函数,外部调用不应该使用
95
+ */
96
+ public _setState(nextState: Record<string, any>) {
97
+ const { _stateContext, keyPath: fullPath, _subs } = this;
98
+ this.state = nextState;
99
+ if (_stateContext) {
100
+ _stateContext.updateState(fullPath, nextState);
101
+ }
102
+ this.connecting = !!_stateContext;
103
+ const store = this._createProxyClass();
104
+ this._proxy = store;
105
+
106
+ if (_subs.length) {
107
+ const subs = [..._subs];
108
+ subs.forEach((item) => {
109
+ item.cb();
110
+ });
111
+ }
112
+ }
113
+
114
+ /**
115
+ * 销毁实例,释放内存
116
+ * 已绑定 this
117
+ */
118
+ public dispose() {
119
+ const { _stateContext, _proxy } = this;
120
+ call(_proxy, LIFE_CYCLE_DISPOSE);
121
+ if (_stateContext) {
122
+ _stateContext.del(this.keyPath);
123
+ this._stateContext = null;
124
+ }
125
+ this._subs.splice(0);
126
+ this.dispose = noon;
127
+ }
128
+
129
+ /**
130
+ * 订阅状态变化
131
+ * @param cb 回调函数
132
+ * 已绑定 this
133
+ * @returns
134
+ */
135
+ public subscribe(cb: StoreSubscribe) {
136
+ const _sid = ++sid;
137
+ this._subs.push({
138
+ sid,
139
+ cb
140
+ });
141
+ return () => {
142
+ const index = this._subs.findIndex((item) => item.sid === _sid);
143
+ if (index > -1) {
144
+ this._subs.splice(index, 1);
145
+ }
146
+ };
147
+ }
148
+
149
+ private _depend() {
150
+ const stateContext = this._stateContext;
151
+ if (stateContext) {
152
+ if (this.connecting) {
153
+ stateContext.depend(this.keyPath);
154
+ } else {
155
+ stateContext.depend();
156
+ }
157
+ }
158
+ }
159
+
160
+ private _createProxyClass() {
161
+ const storeContext = this;
162
+ return new Proxy(this._raw, {
163
+ get(target, p, receiver) {
164
+ if (p === '$') {
165
+ return storeContext;
166
+ } else if (typeof p === 'string') {
167
+ const state = storeContext.state;
168
+ if (p in state) {
169
+ storeContext._depend();
170
+ return state[p];
171
+ }
172
+ }
173
+ const result = applyStateContext(
174
+ storeContext._stateContext,
175
+ () => Reflect.get(target, p, receiver)
176
+ );
177
+ if (
178
+ typeof result === 'function' &&
179
+ typeof p === 'string' &&
180
+ p.startsWith('$')
181
+ ) {
182
+ let func = storeContext._cacheCommit.get(result);
183
+ if (!func) {
184
+ func = storeContext._createProxyCommit(result);
185
+ storeContext._cacheCommit.set(result, func);
186
+ }
187
+ return func;
188
+ }
189
+
190
+ return result;
191
+ },
192
+ set(target, p, newValue, receiver) {
193
+ if (typeof p === 'string' && p in storeContext.state) {
194
+ if (storeContext._drafting) {
195
+ storeContext.state[p] = newValue;
196
+ return true;
197
+ }
198
+ throw new Error(
199
+ `Change the state in the agreed commit function, For example, $${p}('${String(newValue)}')`
200
+ );
201
+ }
202
+ return Reflect.set(target, p, newValue, receiver);
203
+ }
204
+ }) as any;
205
+ }
206
+
207
+ private _createProxyCommit(commitFunc: Function) {
208
+ const connectContext = this;
209
+ return function proxyCommit(...args: any) {
210
+ if (connectContext._drafting) {
211
+ return commitFunc.apply(connectContext._proxy, args);
212
+ }
213
+
214
+ const prevState = connectContext.state;
215
+ let result: unknown;
216
+ const nextState = produce(prevState, (draft) => {
217
+ connectContext._drafting = true;
218
+ connectContext.state = draft;
219
+ try {
220
+ result = commitFunc.apply(connectContext._proxy, args);
221
+ connectContext._drafting = false;
222
+ connectContext.state = prevState;
223
+ } catch (e) {
224
+ connectContext._drafting = false;
225
+ connectContext.state = prevState;
226
+ throw e;
227
+ }
228
+ });
229
+ connectContext._setState(nextState);
230
+ return result;
231
+ };
232
+ }
233
+ }
234
+
235
+ function getFullPath(name: string, cacheKey?: string) {
236
+ return typeof cacheKey === 'string' ? name + '/' + cacheKey : name;
237
+ }
238
+
239
+ export function connectState(state: State) {
240
+ const stateContext = getStateContext(state);
241
+ return <T extends StoreConstructor>(
242
+ Store: T,
243
+ name: string,
244
+ cacheKey?: string
245
+ ): StoreInstance<InstanceType<T>> => {
246
+ const fullPath = getFullPath(name, cacheKey);
247
+ let storeContext: StoreContext<InstanceType<T>> | null =
248
+ stateContext.get(fullPath);
249
+ if (!storeContext) {
250
+ const store = applyStateContext(
251
+ stateContext,
252
+ () => new Store(cacheKey)
253
+ );
254
+ let storeState: Record<string, any>;
255
+ if (fullPath in state.value) {
256
+ storeState = { ...store, ...state.value[fullPath] };
257
+ } else {
258
+ storeState = { ...store };
259
+ }
260
+ storeContext = new StoreContext<InstanceType<T>>(
261
+ stateContext,
262
+ store,
263
+ storeState,
264
+ fullPath
265
+ );
266
+ call(storeContext.get(), LIFE_CYCLE_CREATED);
267
+ }
268
+ return storeContext.get();
269
+ };
270
+ }
271
+ /**
272
+ * 查找外部的 store。如果没有找到则返回 null
273
+ */
274
+ export function foreignStore<T extends StoreConstructor>(
275
+ Store: T,
276
+ name: string,
277
+ cacheKey?: string
278
+ ): InstanceType<T> | null {
279
+ if (!currentStateContext) {
280
+ throw new Error('No state context found');
281
+ }
282
+ const fullPath = getFullPath(name, cacheKey);
283
+ const storeContext: StoreContext<InstanceType<T>> | null =
284
+ currentStateContext.get(fullPath);
285
+ if (storeContext) {
286
+ return storeContext.get();
287
+ }
288
+ return null;
289
+ }
290
+
291
+ function call(obj: any, key: symbol) {
292
+ if (typeof obj[key] === 'function') {
293
+ return obj[key]();
294
+ }
295
+ }
296
+
297
+ function applyStateContext<T = void>(
298
+ stateContext: StateContext | null,
299
+ cb: () => T
300
+ ) {
301
+ const prev = currentStateContext;
302
+ currentStateContext = stateContext;
303
+ const value = cb();
304
+ currentStateContext = prev;
305
+ return value;
306
+ }
package/src/create.ts ADDED
@@ -0,0 +1,83 @@
1
+ import type { StoreContext } from './connect';
2
+
3
+ export interface State {
4
+ value: Record<string, any>;
5
+ }
6
+ const rootMap = new WeakMap<State, any>();
7
+
8
+ export class StateContext {
9
+ public readonly state: State;
10
+ private readonly storeContext: Map<string, StoreContext<any>> = new Map<
11
+ string,
12
+ StoreContext<any>
13
+ >();
14
+
15
+ public constructor(state: State) {
16
+ this.state = state;
17
+ }
18
+
19
+ public depend(fullPath?: string): unknown {
20
+ if (fullPath) {
21
+ return this.state.value[fullPath];
22
+ }
23
+ return this.state.value;
24
+ }
25
+
26
+ public hasState(name: string): boolean {
27
+ return name in this.state.value;
28
+ }
29
+
30
+ public get(name: string): StoreContext<any> | null {
31
+ return this.storeContext.get(name) ?? null;
32
+ }
33
+
34
+ public add(name: string, storeContext: StoreContext<any>) {
35
+ this.storeContext.set(name, storeContext);
36
+ }
37
+
38
+ public updateState(name: string, nextState: any) {
39
+ const { state } = this;
40
+ if (name in state.value) {
41
+ state.value[name] = nextState;
42
+ } else {
43
+ state.value = {
44
+ ...state.value,
45
+ [name]: nextState
46
+ };
47
+ }
48
+ }
49
+
50
+ public del(name: string) {
51
+ const { state } = this;
52
+ this.storeContext.delete(name);
53
+ const newValue: Record<string, any> = {};
54
+ Object.keys(state.value).forEach((key) => {
55
+ if (key !== name) {
56
+ newValue[key] = state.value[key];
57
+ }
58
+ });
59
+ state.value = newValue;
60
+ }
61
+ }
62
+
63
+ function setStateContext(state: State, stateContext: StateContext) {
64
+ rootMap.set(state, stateContext);
65
+ }
66
+
67
+ export function getStateContext(state: State): StateContext {
68
+ let stateContext = rootMap.get(state);
69
+ if (stateContext) {
70
+ return stateContext;
71
+ } else {
72
+ stateContext = new StateContext(state);
73
+ setStateContext(state, stateContext);
74
+ }
75
+
76
+ return stateContext;
77
+ }
78
+
79
+ export function createState(state?: State): State {
80
+ return getStateContext(
81
+ state?.value && typeof state.value === 'object' ? state : { value: {} }
82
+ ).state;
83
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { createState, type State } from './create';
2
+ export {
3
+ connectState,
4
+ foreignStore,
5
+ type StoreConstructor,
6
+ type StoreContext,
7
+ type StoreInstance,
8
+ type StoreParams,
9
+ LIFE_CYCLE_CREATED,
10
+ LIFE_CYCLE_DISPOSE
11
+ } from './connect';
@@ -0,0 +1,83 @@
1
+ import { assert, test } from 'vitest';
2
+ import { nextTick, reactive, watch } from 'vue';
3
+
4
+ import { connectState } from './connect';
5
+ import { createState } from './create';
6
+
7
+ test('base', async () => {
8
+ const state = createState(reactive({ value: {} }));
9
+ const connectStore = connectState(state);
10
+ class User {
11
+ public name = '';
12
+ public $setName(name: string) {
13
+ this.name = name;
14
+ }
15
+ }
16
+ const user = connectStore(User, 'user');
17
+ let updateValue: string | undefined;
18
+ watch(
19
+ () => {
20
+ return user.name;
21
+ },
22
+ (name) => {
23
+ updateValue = name;
24
+ }
25
+ );
26
+ user.$setName('test');
27
+ await nextTick();
28
+ assert.equal(updateValue, 'test');
29
+
30
+ user.$setName('test2');
31
+ await nextTick();
32
+ assert.equal(updateValue, 'test2');
33
+ });
34
+
35
+ test('base2', async () => {
36
+ const state = createState(reactive({ value: {} }));
37
+ const connectStore = connectState(state);
38
+ class User {
39
+ public name = '';
40
+ public $setName(name: string) {
41
+ this.name = name;
42
+ }
43
+ }
44
+ const user = connectStore(User, 'user');
45
+ user.$setName('test');
46
+ let updateValue: string | undefined;
47
+ watch(
48
+ () => {
49
+ return user.name;
50
+ },
51
+ (name) => {
52
+ updateValue = name;
53
+ }
54
+ );
55
+ user.$setName('test2');
56
+ await nextTick();
57
+ assert.equal(updateValue, 'test2');
58
+ });
59
+
60
+ test('watch root', async () => {
61
+ const state = createState(reactive({ value: {} }));
62
+ const connectStore = connectState(state);
63
+ class User {
64
+ public name = '';
65
+ public $setName(name: string) {
66
+ this.name = name;
67
+ }
68
+ }
69
+ const user = connectStore(User, 'user');
70
+ let updateCount = 0;
71
+ watch(
72
+ () => {
73
+ return connectStore(User, 'user');
74
+ },
75
+ () => {
76
+ updateCount++;
77
+ }
78
+ );
79
+ assert.equal(user.$.connecting, false);
80
+ user.$setName('test2');
81
+ await nextTick();
82
+ assert.equal(updateCount, 1);
83
+ });