pocket-state 0.0.2
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/package.json +14 -0
- package/src/globalState/event.ts +81 -0
- package/src/globalState/hooks.ts +26 -0
- package/src/globalState/shallowEqual.ts +74 -0
- package/src/globalState/store.ts +198 -0
- package/src/globalState/type-helper.d.ts +58 -0
- package/src/globalState/type.d.ts +219 -0
- package/src/index.tsx +22 -0
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pocket-state",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "tiny global store",
|
|
5
|
+
"main": "src/index",
|
|
6
|
+
"codegenConfig": {
|
|
7
|
+
"name": "TinystoreSpec",
|
|
8
|
+
"type": "modules",
|
|
9
|
+
"jsSrcsDir": "src"
|
|
10
|
+
},
|
|
11
|
+
"author": " <@kayda69> (nhh.tcp@gmail.com)",
|
|
12
|
+
"license": "ISC",
|
|
13
|
+
"homepage": "#readme"
|
|
14
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// event.ts
|
|
2
|
+
import {IEventEmitter, Listener} from './type';
|
|
3
|
+
|
|
4
|
+
export class EventEmitter implements IEventEmitter {
|
|
5
|
+
private events = new Map<string, Set<Listener<any>>>();
|
|
6
|
+
private onceWrappers = new Map<string, Map<Listener<any>, Listener<any>>>();
|
|
7
|
+
|
|
8
|
+
on<T>(event: string, listener: Listener<T>): void {
|
|
9
|
+
let set = this.events.get(event);
|
|
10
|
+
if (!set) {
|
|
11
|
+
set = new Set();
|
|
12
|
+
this.events.set(event, set);
|
|
13
|
+
}
|
|
14
|
+
set.add(listener as unknown as Listener<any>);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
once<T>(event: string, listener: Listener<T>): void {
|
|
18
|
+
const wrapper: Listener<T> = payload => {
|
|
19
|
+
this.off(event, wrapper);
|
|
20
|
+
this.onceWrappers
|
|
21
|
+
.get(event)
|
|
22
|
+
?.delete(listener as unknown as Listener<any>);
|
|
23
|
+
listener(payload);
|
|
24
|
+
};
|
|
25
|
+
if (!this.onceWrappers.has(event)) {
|
|
26
|
+
this.onceWrappers.set(event, new Map());
|
|
27
|
+
}
|
|
28
|
+
this.onceWrappers
|
|
29
|
+
.get(event)!
|
|
30
|
+
.set(
|
|
31
|
+
listener as unknown as Listener<any>,
|
|
32
|
+
wrapper as unknown as Listener<any>,
|
|
33
|
+
);
|
|
34
|
+
this.on(event, wrapper);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
emit<T>(event: string, payload: T): void {
|
|
38
|
+
const listeners = this.events.get(event);
|
|
39
|
+
if (!listeners || listeners.size === 0) return;
|
|
40
|
+
for (const l of listeners) {
|
|
41
|
+
try {
|
|
42
|
+
l(payload);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.warn(`Error in listener for '${event}':`, error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
off<T>(event: string, listener?: Listener<T>): void {
|
|
50
|
+
const set = this.events.get(event);
|
|
51
|
+
if (!set) return;
|
|
52
|
+
|
|
53
|
+
if (listener) {
|
|
54
|
+
const wrapped = this.onceWrappers
|
|
55
|
+
.get(event)
|
|
56
|
+
?.get(listener as unknown as Listener<any>);
|
|
57
|
+
if (wrapped) {
|
|
58
|
+
set.delete(wrapped as unknown as Listener<any>);
|
|
59
|
+
this.onceWrappers
|
|
60
|
+
.get(event)!
|
|
61
|
+
.delete(listener as unknown as Listener<any>);
|
|
62
|
+
if (this.onceWrappers.get(event)!.size === 0) {
|
|
63
|
+
this.onceWrappers.delete(event);
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
set.delete(listener as unknown as Listener<any>);
|
|
67
|
+
}
|
|
68
|
+
if (set.size === 0) {
|
|
69
|
+
this.events.delete(event);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.events.delete(event);
|
|
74
|
+
this.onceWrappers.delete(event);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
clear(): void {
|
|
78
|
+
this.events.clear();
|
|
79
|
+
this.onceWrappers.clear();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// useStore.ts
|
|
2
|
+
import {useCallback} from 'react';
|
|
3
|
+
import {useSyncExternalStore} from 'react';
|
|
4
|
+
import type {Store} from './type';
|
|
5
|
+
|
|
6
|
+
export function useStore<T>(store: Store<T>): T;
|
|
7
|
+
export function useStore<T, S>(store: Store<T>, selector: (state: T) => S): S;
|
|
8
|
+
|
|
9
|
+
export function useStore<T, S = T>(
|
|
10
|
+
store: Store<T>,
|
|
11
|
+
selector?: (state: T) => S,
|
|
12
|
+
): T | S {
|
|
13
|
+
const subscribe = useCallback(
|
|
14
|
+
(onChange: () => void) =>
|
|
15
|
+
selector
|
|
16
|
+
? store.subscribe(selector, () => onChange())
|
|
17
|
+
: store.subscribe(() => onChange()),
|
|
18
|
+
[store, selector],
|
|
19
|
+
);
|
|
20
|
+
const getSnapshot = useCallback(() => {
|
|
21
|
+
const s = store.getValue();
|
|
22
|
+
return selector ? selector(s) : (s as T);
|
|
23
|
+
}, [store, selector]);
|
|
24
|
+
|
|
25
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot) as T | S;
|
|
26
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const isIterable = (obj: object): obj is Iterable<unknown> =>
|
|
2
|
+
Symbol.iterator in obj;
|
|
3
|
+
|
|
4
|
+
const hasIterableEntries = (
|
|
5
|
+
value: Iterable<unknown>,
|
|
6
|
+
): value is Iterable<unknown> & {
|
|
7
|
+
entries(): Iterable<[unknown, unknown]>;
|
|
8
|
+
} =>
|
|
9
|
+
// HACK: avoid checking entries type
|
|
10
|
+
'entries' in value;
|
|
11
|
+
|
|
12
|
+
const compareEntries = (
|
|
13
|
+
valueA: {entries(): Iterable<[unknown, unknown]>},
|
|
14
|
+
valueB: {entries(): Iterable<[unknown, unknown]>},
|
|
15
|
+
) => {
|
|
16
|
+
const mapA = valueA instanceof Map ? valueA : new Map(valueA.entries());
|
|
17
|
+
const mapB = valueB instanceof Map ? valueB : new Map(valueB.entries());
|
|
18
|
+
if (mapA.size !== mapB.size) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
for (const [key, value] of mapA) {
|
|
22
|
+
if (!Object.is(value, mapB.get(key))) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Ordered iterables
|
|
30
|
+
const compareIterables = (
|
|
31
|
+
valueA: Iterable<unknown>,
|
|
32
|
+
valueB: Iterable<unknown>,
|
|
33
|
+
) => {
|
|
34
|
+
const iteratorA = valueA[Symbol.iterator]();
|
|
35
|
+
const iteratorB = valueB[Symbol.iterator]();
|
|
36
|
+
let nextA = iteratorA.next();
|
|
37
|
+
let nextB = iteratorB.next();
|
|
38
|
+
while (!nextA.done && !nextB.done) {
|
|
39
|
+
if (!Object.is(nextA.value, nextB.value)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
nextA = iteratorA.next();
|
|
43
|
+
nextB = iteratorB.next();
|
|
44
|
+
}
|
|
45
|
+
return !!nextA.done && !!nextB.done;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function shallow<T>(valueA: T, valueB: T): boolean {
|
|
49
|
+
if (Object.is(valueA, valueB)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (
|
|
53
|
+
typeof valueA !== 'object' ||
|
|
54
|
+
valueA === null ||
|
|
55
|
+
typeof valueB !== 'object' ||
|
|
56
|
+
valueB === null
|
|
57
|
+
) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (Object.getPrototypeOf(valueA) !== Object.getPrototypeOf(valueB)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (isIterable(valueA) && isIterable(valueB)) {
|
|
64
|
+
if (hasIterableEntries(valueA) && hasIterableEntries(valueB)) {
|
|
65
|
+
return compareEntries(valueA, valueB);
|
|
66
|
+
}
|
|
67
|
+
return compareIterables(valueA, valueB);
|
|
68
|
+
}
|
|
69
|
+
// assume plain objects
|
|
70
|
+
return compareEntries(
|
|
71
|
+
{entries: () => Object.entries(valueA)},
|
|
72
|
+
{entries: () => Object.entries(valueB)},
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// store.ts
|
|
2
|
+
// import deepEqual from 'fast-deep-equal';
|
|
3
|
+
import {IEventEmitter, Listener, Middleware, Store, UseStoreGet} from './type';
|
|
4
|
+
import {EventEmitter} from './event';
|
|
5
|
+
import {Draft, produce} from 'immer';
|
|
6
|
+
// import {shallow} from './shallowEqual';
|
|
7
|
+
import deepEqual from 'fast-deep-equal';
|
|
8
|
+
export function createStore<T>(
|
|
9
|
+
initialState: T,
|
|
10
|
+
middlewares: Middleware<T>[] = [],
|
|
11
|
+
): Store<T> {
|
|
12
|
+
const emitter: IEventEmitter = new EventEmitter();
|
|
13
|
+
let state = initialState;
|
|
14
|
+
|
|
15
|
+
// Coalesce emits trong cùng 1 microtask
|
|
16
|
+
let emitScheduled = false;
|
|
17
|
+
const emitState = () => {
|
|
18
|
+
if (emitScheduled) return;
|
|
19
|
+
emitScheduled = true;
|
|
20
|
+
queueMicrotask(() => {
|
|
21
|
+
emitScheduled = false;
|
|
22
|
+
emitter.emit('state', state);
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function baseSet(delta: Partial<T>) {
|
|
27
|
+
const nextState = Array.isArray(state)
|
|
28
|
+
? (delta as unknown as T) // với array: replace
|
|
29
|
+
: {...state, ...delta}; // với object: shallow merge
|
|
30
|
+
|
|
31
|
+
if (!deepEqual(state, nextState)) {
|
|
32
|
+
state = nextState;
|
|
33
|
+
|
|
34
|
+
// Dev guard: phát hiện mutate ngoài store
|
|
35
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
36
|
+
try {
|
|
37
|
+
Object.freeze(state as any);
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
emitState();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const setFn = middlewares.reduceRight(
|
|
46
|
+
(next, mw) => mw(next, () => state),
|
|
47
|
+
baseSet as (patch: Partial<T>) => void,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const getValue = ((key?: keyof T | (keyof T)[]) => {
|
|
51
|
+
if (key === undefined) return state;
|
|
52
|
+
if (Array.isArray(key)) {
|
|
53
|
+
const out = {} as Pick<T, (typeof key)[number]>;
|
|
54
|
+
for (const k of key) (out as any)[k] = (state as any)[k];
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
return (state as any)[key];
|
|
58
|
+
}) as UseStoreGet<T>;
|
|
59
|
+
|
|
60
|
+
function subscribe(selectorOrListener: any, maybeListener?: any) {
|
|
61
|
+
let wrapped: Listener<any>;
|
|
62
|
+
|
|
63
|
+
if (typeof maybeListener === 'function') {
|
|
64
|
+
// subscribe(selector, listener)
|
|
65
|
+
const selector: (s: T) => any = selectorOrListener;
|
|
66
|
+
let prevSlice = selector(state);
|
|
67
|
+
wrapped = (next: T) => {
|
|
68
|
+
const slice = selector(next);
|
|
69
|
+
if (!deepEqual(slice, prevSlice)) {
|
|
70
|
+
prevSlice = slice;
|
|
71
|
+
maybeListener(slice);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
} else {
|
|
75
|
+
// subscribe(listener)
|
|
76
|
+
const listener: Listener<T> = selectorOrListener;
|
|
77
|
+
let prev = state;
|
|
78
|
+
wrapped = (next: T) => {
|
|
79
|
+
if (!deepEqual(next, prev)) {
|
|
80
|
+
prev = next;
|
|
81
|
+
listener(next);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
emitter.on('state', wrapped);
|
|
87
|
+
|
|
88
|
+
// Lưu ý: KHÔNG gọi listener ngay khi subscribe
|
|
89
|
+
// (để tương thích useSyncExternalStore: initial snapshot lấy qua getSnapshot)
|
|
90
|
+
|
|
91
|
+
return () => emitter.off('state', wrapped);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function setValue(
|
|
95
|
+
patch: Partial<T> | ((state: T) => Partial<T> | Promise<Partial<T>>),
|
|
96
|
+
): void {
|
|
97
|
+
(async () => {
|
|
98
|
+
try {
|
|
99
|
+
const resolved =
|
|
100
|
+
typeof patch === 'function' ? await (patch as any)(state) : patch;
|
|
101
|
+
|
|
102
|
+
if (resolved && typeof resolved === 'object') {
|
|
103
|
+
setFn(resolved as Partial<T>);
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.warn('[store.setValue] patch error:', error);
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function setImmer(updater: (draft: Draft<T>) => void): void {
|
|
112
|
+
try {
|
|
113
|
+
const nextState = produce(state, updater);
|
|
114
|
+
if (deepEqual(state, nextState)) return;
|
|
115
|
+
|
|
116
|
+
if (Array.isArray(state)) {
|
|
117
|
+
setFn(nextState as unknown as Partial<T>);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const delta = {} as Partial<T>;
|
|
121
|
+
let changed = false;
|
|
122
|
+
for (const k in nextState as any) {
|
|
123
|
+
const nv = (nextState as any)[k];
|
|
124
|
+
const ov = (state as any)[k];
|
|
125
|
+
if (nv !== ov) {
|
|
126
|
+
(delta as any)[k] = nv;
|
|
127
|
+
changed = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const k in state as any) {
|
|
131
|
+
if (!(k in (nextState as any))) {
|
|
132
|
+
(delta as any)[k] = undefined;
|
|
133
|
+
changed = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (changed) setFn(delta);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.warn('[store.setImmer] error:', e);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function reset(): void;
|
|
143
|
+
function reset(initialValue?: T | Partial<T>): void;
|
|
144
|
+
function reset(initialValue?: T | Partial<T>) {
|
|
145
|
+
const isObj = (
|
|
146
|
+
v: unknown,
|
|
147
|
+
): v is Record<string | symbol | number, unknown> =>
|
|
148
|
+
typeof v === 'object' && v !== null;
|
|
149
|
+
|
|
150
|
+
const cloneShallow = <U>(src: U): U => {
|
|
151
|
+
if (Array.isArray(src)) return (src as any).slice();
|
|
152
|
+
if (isObj(src)) return {...(src as any)} as U;
|
|
153
|
+
return src;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
let next = cloneShallow(initialState) as T;
|
|
157
|
+
if (initialValue !== undefined) {
|
|
158
|
+
if (Array.isArray(initialValue)) {
|
|
159
|
+
next = (initialValue as any).slice();
|
|
160
|
+
} else if (isObj(initialValue)) {
|
|
161
|
+
Object.assign(next as any, initialValue);
|
|
162
|
+
} else {
|
|
163
|
+
const current = getValue();
|
|
164
|
+
if (!Object.is(current as any, initialValue as any)) {
|
|
165
|
+
setFn(initialValue as unknown as Partial<T>);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const current = getValue();
|
|
171
|
+
if (!deepEqual(current, next)) {
|
|
172
|
+
setFn(next as unknown as Partial<T>);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getInitialValue(): T {
|
|
177
|
+
if (Array.isArray(state)) {
|
|
178
|
+
return (state as any).slice();
|
|
179
|
+
}
|
|
180
|
+
if (state && typeof state === 'object') {
|
|
181
|
+
return {...(state as any)};
|
|
182
|
+
}
|
|
183
|
+
return state;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isDirty() {
|
|
187
|
+
return !deepEqual(state, initialState);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
getValue,
|
|
191
|
+
getInitialValue,
|
|
192
|
+
setValue,
|
|
193
|
+
setImmer,
|
|
194
|
+
reset,
|
|
195
|
+
subscribe,
|
|
196
|
+
isDirty,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// ---- Path utilities (zero-deps, RHF-like) ----
|
|
2
|
+
type Primitive =
|
|
3
|
+
| string
|
|
4
|
+
| number
|
|
5
|
+
| boolean
|
|
6
|
+
| bigint
|
|
7
|
+
| symbol
|
|
8
|
+
| null
|
|
9
|
+
| undefined
|
|
10
|
+
| Date
|
|
11
|
+
| RegExp
|
|
12
|
+
| Function;
|
|
13
|
+
|
|
14
|
+
type IsTuple<A extends readonly unknown[]> = number extends A['length']
|
|
15
|
+
? false
|
|
16
|
+
: true;
|
|
17
|
+
|
|
18
|
+
type IndexKeys<T> = Extract<keyof T, string>;
|
|
19
|
+
|
|
20
|
+
/** All dot/bracket-style paths into T (e.g. "user.name", "items.0.qty") */
|
|
21
|
+
export type Path<T> = T extends Primitive
|
|
22
|
+
? never
|
|
23
|
+
: T extends readonly (infer V)[]
|
|
24
|
+
? IsTuple<T> extends true
|
|
25
|
+
? {
|
|
26
|
+
[I in Extract<keyof T, `${number}`>]: `${I}` | `${I}.${Path<V>}`;
|
|
27
|
+
}[Extract<keyof T, `${number}`>]
|
|
28
|
+
: `${number}` | `${number}.${Path<V>}`
|
|
29
|
+
: {
|
|
30
|
+
[K in IndexKeys<T>]: T[K] extends Primitive
|
|
31
|
+
? `${K}`
|
|
32
|
+
: `${K}` | `${K}.${Path<T[K]>}`;
|
|
33
|
+
}[IndexKeys<T>];
|
|
34
|
+
|
|
35
|
+
/** Value type at path P */
|
|
36
|
+
export type PathValue<
|
|
37
|
+
T,
|
|
38
|
+
P extends string,
|
|
39
|
+
> = P extends `${infer K}.${infer Rest}`
|
|
40
|
+
? K extends keyof T
|
|
41
|
+
? PathValue<T[K], Rest>
|
|
42
|
+
: K extends `${number}`
|
|
43
|
+
? T extends readonly (infer V)[]
|
|
44
|
+
? PathValue<V, Rest>
|
|
45
|
+
: never
|
|
46
|
+
: never
|
|
47
|
+
: P extends keyof T
|
|
48
|
+
? T[P]
|
|
49
|
+
: P extends `${number}`
|
|
50
|
+
? T extends readonly (infer V)[]
|
|
51
|
+
? V
|
|
52
|
+
: never
|
|
53
|
+
: never;
|
|
54
|
+
|
|
55
|
+
/** Tuple of values for a tuple of paths */
|
|
56
|
+
export type PathValues<T, PS extends readonly string[]> = {
|
|
57
|
+
[I in keyof PS]: PS[I] extends string ? PathValue<T, PS[I]> : never;
|
|
58
|
+
};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {Draft} from 'immer';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A callback invoked when an event is emitted with a payload of type `T`.
|
|
5
|
+
* Keep listeners pure and fast. Long-running side effects should live in
|
|
6
|
+
* middleware/effects instead of listeners.
|
|
7
|
+
*/
|
|
8
|
+
export interface Listener<T = any> {
|
|
9
|
+
(payload: T): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Immer-style mutation function used for "mutable-looking" updates.
|
|
14
|
+
*
|
|
15
|
+
* - Receives a `draft` (a Proxy of your state).
|
|
16
|
+
* - You can mutate the draft; Immer will produce the next immutable state.
|
|
17
|
+
* - You may return a new value instead of mutating the draft (less common).
|
|
18
|
+
* - Can be async, but it's recommended to await first, then mutate, to avoid races.
|
|
19
|
+
*/
|
|
20
|
+
export type MutateFn<T> = (draft: Draft<T>) => void | T | Promise<void | T>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Store getter API.
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
* ```ts
|
|
27
|
+
* const all = getValues(); // → T (entire state)
|
|
28
|
+
* const count = getValues('count'); // → T['count'] (one key)
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Notes:
|
|
32
|
+
* - Results are expected to be reference-stable when state hasn't changed,
|
|
33
|
+
* which helps React avoid unnecessary renders.
|
|
34
|
+
* - If you want to support reading multiple keys at once, extend the type.
|
|
35
|
+
*/
|
|
36
|
+
export type UseStoreGet<T> = {
|
|
37
|
+
(): T;
|
|
38
|
+
<K extends keyof T>(key: K): T[K];
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Store setter API:
|
|
42
|
+
* - Accepts a `patch` (partial) to shallow-merge into current state,
|
|
43
|
+
* or a function that receives the current state and returns a partial.
|
|
44
|
+
* The function may be async.
|
|
45
|
+
*
|
|
46
|
+
* Examples:
|
|
47
|
+
* ```ts
|
|
48
|
+
* setValue({ flag: true });
|
|
49
|
+
* setValue(s => ({ count: s.count + 1 }));
|
|
50
|
+
* setValue(async s => {
|
|
51
|
+
* const user = await fetchUser();
|
|
52
|
+
* return { user };
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* Notes:
|
|
57
|
+
* - The store is responsible for equality checks and only emits on real changes.
|
|
58
|
+
* - Multiple updates in the same tick may be coalesced (implementation-dependent).
|
|
59
|
+
*
|
|
60
|
+
* `immer()` (if implemented) is a convenience to enable Immer-style updates.
|
|
61
|
+
* This type does not define its runtime behavior; see your implementation.
|
|
62
|
+
*/
|
|
63
|
+
export type UseStoreSet<T> = {
|
|
64
|
+
(patch: Partial<T> | ((state: T) => Partial<T> | Promise<Partial<T>>)): void;
|
|
65
|
+
immer(): void;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Minimal pub/sub event emitter contract.
|
|
70
|
+
*
|
|
71
|
+
* Expected behavior:
|
|
72
|
+
* - `on` registers a listener for an event name.
|
|
73
|
+
* - `emit` broadcasts a payload to all listeners of that event.
|
|
74
|
+
* - `off` removes a specific listener or all listeners for an event.
|
|
75
|
+
* - `once` registers a listener that runs exactly once, then unregisters itself.
|
|
76
|
+
* - `clear` removes listeners for all events.
|
|
77
|
+
*
|
|
78
|
+
* Implementation guidance:
|
|
79
|
+
* - Snapshot listeners before emitting to stay safe if `on/off` happens during emit.
|
|
80
|
+
* - `off(event, original)` should remove a `once` listener even if it's wrapped.
|
|
81
|
+
*/
|
|
82
|
+
export interface IEventEmitter {
|
|
83
|
+
/** Register a listener for an event name. */
|
|
84
|
+
on<T = any>(event: string, listener: Listener<T>): void;
|
|
85
|
+
|
|
86
|
+
/** Emit an event with a payload. */
|
|
87
|
+
emit<T = any>(event: string, payload: T): void;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Remove a listener or all listeners for an event.
|
|
91
|
+
* Omit `listener` to remove all listeners for the event.
|
|
92
|
+
*/
|
|
93
|
+
off<T = any>(event: string, listener?: Listener<T>): void;
|
|
94
|
+
|
|
95
|
+
/** Register a one-time listener that auto-unregisters after the first emit. */
|
|
96
|
+
once<T>(event: string, listener: Listener<T>): void;
|
|
97
|
+
|
|
98
|
+
/** Remove all listeners for all events. */
|
|
99
|
+
clear(): void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reactive key-value store interface.
|
|
104
|
+
* Supports:
|
|
105
|
+
* - Reading current state (`getValues`)
|
|
106
|
+
* - Updating via partials or functions (`setValue`)
|
|
107
|
+
* - Immer-style updates (`setImmer`) when available
|
|
108
|
+
* - Reset to the initial value (`reset`)
|
|
109
|
+
* - Subscribing to the whole state or to a slice via a selector
|
|
110
|
+
*
|
|
111
|
+
* Important notes:
|
|
112
|
+
* - Listeners are called only when the relevant data actually changes.
|
|
113
|
+
* - `subscribe` returns an unsubscribe function — call it on unmount to avoid leaks.
|
|
114
|
+
* - `subscribe` does **not** auto-invoke the listener initially (fits `useSyncExternalStore`);
|
|
115
|
+
* if you need an initial call, read the snapshot and invoke it yourself at the call site.
|
|
116
|
+
*/
|
|
117
|
+
export interface Store<T> {
|
|
118
|
+
/**
|
|
119
|
+
* Read the full state or a specific property by key.
|
|
120
|
+
* @param key Optional key within the state.
|
|
121
|
+
* @returns Without `key` → the full state `T`.
|
|
122
|
+
* With `key` → the value `T[K]`.
|
|
123
|
+
*/
|
|
124
|
+
getValue: UseStoreGet<T>;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Update state by shallow-merging `patch`, or by running a function
|
|
128
|
+
* that returns a `patch`. The function may be async.
|
|
129
|
+
*/
|
|
130
|
+
setValue(
|
|
131
|
+
patch: Partial<T> | ((state: T) => Partial<T> | Promise<Partial<T>>),
|
|
132
|
+
): void;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Returns the store's initial state value.
|
|
136
|
+
*
|
|
137
|
+
* - For arrays and objects → returns a shallow clone to avoid external mutations.
|
|
138
|
+
* - For primitive types → returns the value as-is.
|
|
139
|
+
* - The returned value represents the original initial state, not the current state.
|
|
140
|
+
*/
|
|
141
|
+
getInitialValue(): T;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Update state in Immer style.
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* store.setImmer(draft => {
|
|
148
|
+
* draft.user.name = 'Hiep';
|
|
149
|
+
* draft.items.push({ id: 'x' });
|
|
150
|
+
* });
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
setImmer(updater: (draft: Draft<T>) => void): void;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Resets the state to the initial value (`initialState`) with a new reference (shallow clone).
|
|
157
|
+
*
|
|
158
|
+
* - If no argument is provided → the state is reset to `initialState` (new reference).
|
|
159
|
+
* - If `initialValue` is provided:
|
|
160
|
+
* - If it's an object/array → shallow-merge it into a clone of `initialState`.
|
|
161
|
+
* - If it's a primitive → replace the state entirely with that value.
|
|
162
|
+
* - Always creates a new reference; only updates if the result is not shallow-equal to the current state.
|
|
163
|
+
*/
|
|
164
|
+
reset(initialValue?: T | Partial<T>): void;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Subscribe to the entire state.
|
|
168
|
+
* The listener is invoked **after commit** whenever state changes.
|
|
169
|
+
* @returns Unsubscribe function.
|
|
170
|
+
*/
|
|
171
|
+
subscribe(listener: Listener<T>): () => void;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Subscribe to a derived slice of the state.
|
|
175
|
+
* The listener fires only when the selector's result changes.
|
|
176
|
+
* @returns Unsubscribe function.
|
|
177
|
+
*/
|
|
178
|
+
subscribe<S>(selector: (state: T) => S, listener: Listener<S>): () => void;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Checks whether the current state differs from the initial state.
|
|
182
|
+
*
|
|
183
|
+
* - For primitive types → compared using strict equality (`===`).
|
|
184
|
+
* - For objects/arrays → compared using deep equality.
|
|
185
|
+
* - Returns `true` if the state is modified, otherwise `false`.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```ts
|
|
189
|
+
* const store = createStore({ count: 0 });
|
|
190
|
+
* store.isDirty(); // false
|
|
191
|
+
*
|
|
192
|
+
* store.setValue({ count: 1 });
|
|
193
|
+
* store.isDirty(); // true
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
isDirty(): boolean;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Middleware that intercepts and transforms a `patch` before it is applied.
|
|
201
|
+
*
|
|
202
|
+
* Contract:
|
|
203
|
+
* - Must call `next(patch)` to forward the update (similar to Redux middleware).
|
|
204
|
+
* - Can read the current state via `getState()`.
|
|
205
|
+
* - Useful for logging, validation, mapping, batching, devtools bridges, persistence, etc.
|
|
206
|
+
*
|
|
207
|
+
* @example Logging middleware:
|
|
208
|
+
* ```ts
|
|
209
|
+
* const logger: Middleware<State> = (next, get) => patch => {
|
|
210
|
+
* console.log('Before:', get());
|
|
211
|
+
* next(patch);
|
|
212
|
+
* console.log('After:', get());
|
|
213
|
+
* };
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
export type Middleware<T> = (
|
|
217
|
+
next: (patch: Partial<T>) => void,
|
|
218
|
+
getState: () => T,
|
|
219
|
+
) => (patch: Partial<T>) => void;
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {createStore} from './globalState/store';
|
|
2
|
+
import {useStore} from './globalState/hooks';
|
|
3
|
+
import type {
|
|
4
|
+
IEventEmitter,
|
|
5
|
+
Listener,
|
|
6
|
+
Middleware,
|
|
7
|
+
MutateFn,
|
|
8
|
+
UseStoreGet,
|
|
9
|
+
UseStoreSet,
|
|
10
|
+
Store,
|
|
11
|
+
} from './globalState/type';
|
|
12
|
+
|
|
13
|
+
export {createStore, useStore};
|
|
14
|
+
export type {
|
|
15
|
+
IEventEmitter,
|
|
16
|
+
Listener,
|
|
17
|
+
Middleware,
|
|
18
|
+
MutateFn,
|
|
19
|
+
UseStoreGet,
|
|
20
|
+
UseStoreSet,
|
|
21
|
+
Store,
|
|
22
|
+
};
|