@@ -2,15 +2,31 @@ export type Patch<T> = (value: T) => T;
2
2
export type ValueOrPatch < T > = T | Patch < T > ;
3
3
export type Handler < T > = ( nextValue : T , previousValue : T | undefined ) => void ;
4
4
export type Unsubscribe = ( ) => void ;
5
+ export type Unregister = Unsubscribe ;
6
+ export type Modifier < T > = ( nextValue : T , previousValue : T | undefined ) => void ;
5
7
6
8
export const isPatch = < T > ( value : ValueOrPatch < T > ) : value is Patch < T > =>
7
9
typeof value === 'function' ;
8
10
9
11
export class StateStore < T extends Record < string , unknown > > {
10
- private handlerSet = new Set < Handler < T > > ( ) ;
12
+ protected handlers = new Set < Handler < T > > ( ) ;
13
+ protected modifiers = new Set < Handler < T > > ( ) ;
11
14
12
15
constructor ( private value : T ) { }
13
16
17
+ /**
18
+ * Allows merging two stores only if their keys differ otherwise there's no way to ensure the data type stability.
19
+ */
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ public merge < Q extends StateStore < any > > (
22
+ stateStore : Q extends StateStore < infer L > ? ( Extract < keyof T , keyof L > extends never ? Q : never ) : never ,
23
+ ) {
24
+ return new MergedStateStore < T , Q extends StateStore < infer L > ? L : never > ( {
25
+ parentStore : this ,
26
+ mergedStore : stateStore ,
27
+ } ) ;
28
+ }
29
+
14
30
public next = ( newValueOrPatch : ValueOrPatch < T > ) : void => {
15
31
// newValue (or patch output) should never be mutated previous value
16
32
const newValue = isPatch ( newValueOrPatch )
@@ -20,24 +36,25 @@ export class StateStore<T extends Record<string, unknown>> {
20
36
// do not notify subscribers if the value hasn't changed
21
37
if ( newValue === this . value ) return ;
22
38
39
+ this . modifiers . forEach ( ( modifier ) => modifier ( newValue , this . value ) ) ;
40
+
23
41
const oldValue = this . value ;
24
42
this . value = newValue ;
25
43
26
- this . handlerSet . forEach ( ( handler ) => handler ( this . value , oldValue ) ) ;
44
+ this . handlers . forEach ( ( handler ) => handler ( this . value , oldValue ) ) ;
27
45
} ;
28
46
29
47
public partialNext = ( partial : Partial < T > ) : void =>
30
48
this . next ( ( current ) => ( { ...current , ...partial } ) ) ;
31
49
32
50
public getLatestValue = ( ) : T => this . value ;
33
-
34
- public subscribe = ( handler : Handler < T > ) : Unsubscribe => {
51
+ public subscribe ( handler : Handler < T > ) : Unsubscribe {
35
52
handler ( this . value , undefined ) ;
36
- this . handlerSet . add ( handler ) ;
53
+ this . handlers . add ( handler ) ;
37
54
return ( ) => {
38
- this . handlerSet . delete ( handler ) ;
55
+ this . handlers . delete ( handler ) ;
39
56
} ;
40
- } ;
57
+ }
41
58
42
59
public subscribeWithSelector = <
43
60
O extends Readonly < Record < string , unknown > > | Readonly < unknown [ ] > ,
@@ -69,4 +86,95 @@ export class StateStore<T extends Record<string, unknown>> {
69
86
70
87
return this . subscribe ( wrappedHandler ) ;
71
88
} ;
89
+
90
+ public registerModifier ( modifier : Modifier < T > ) : Unregister {
91
+ this . modifiers . add ( modifier ) ;
92
+
93
+ return ( ) => {
94
+ this . modifiers . delete ( modifier ) ;
95
+ } ;
96
+ }
72
97
}
98
+
99
+ class MergedStateStore < T extends Record < string , unknown > , L extends Record < string , unknown > > extends StateStore < T & L > {
100
+ private readonly parentStore : StateStore < T > ;
101
+ private readonly mergedStore : StateStore < L > ;
102
+
103
+ constructor ( { parentStore, mergedStore } : { mergedStore : StateStore < L > ; parentStore : StateStore < T > } ) {
104
+ super ( {
105
+ ...parentStore . getLatestValue ( ) ,
106
+ ...mergedStore . getLatestValue ( ) ,
107
+ } ) ;
108
+
109
+ this . parentStore = parentStore ;
110
+ this . mergedStore = mergedStore ;
111
+ }
112
+
113
+ public subscribe ( handler : Handler < T & L > ) {
114
+ const unsubscribeFunctions : Unsubscribe [ ] = [ ] ;
115
+
116
+ if ( ! this . handlers . size ) {
117
+ // FIXME: should we subscribe to the changes of the parent store or should we let it die
118
+ // and make MergedStateStore the next "parent"?
119
+ for ( const store of [ this . parentStore , this . mergedStore ] ) {
120
+ // TODO: maybe allow "resolver" (how the two states should be merged)
121
+ const unsubscribe = store . subscribe ( ( nv ) => {
122
+ this . next ( ( cv ) => ( {
123
+ ...cv ,
124
+ ...nv ,
125
+ } ) ) ;
126
+ } ) ;
127
+
128
+ unsubscribeFunctions . push ( unsubscribe ) ;
129
+ }
130
+ }
131
+
132
+ unsubscribeFunctions . push ( super . subscribe ( handler ) ) ;
133
+
134
+ return ( ) => {
135
+ unsubscribeFunctions . forEach ( ( f ) => f ( ) ) ;
136
+ } ;
137
+ }
138
+
139
+ // TODO: getLatestValue should remain the same unless either of the source values changed (cached)
140
+
141
+ // TODO: make `next` support only T type (maybe?) (only if we go with subscribing only to mergedStore)
142
+ }
143
+
144
+ // const a = new StateStore<{ a: string }>({ a: 'yooo' });
145
+ // const b = new StateStore<{ a: number, q: string; }>({ q: 'yooo', a: 2 });
146
+ // const c = new StateStore<{ q: string }>({ q: 'yooo' });
147
+
148
+ // const d = a.merge(b); // error
149
+ // const e = a.merge(c); // no error (keys differ)
150
+
151
+ // TODO: decide
152
+ // const d = a.merge(b); // state/type of `a` gets copied to the new merged state and gets garbage collected, `d` becomes new `a`
153
+
154
+ // l.subscribe(console.info);
155
+
156
+ // t.next({ a: 'poof' });
157
+ // b.next({ q: 'nah' });
158
+
159
+ const Uninitialized = Symbol ( 'uninitialized' ) ;
160
+
161
+ const a = new StateStore < { hasNext : boolean | typeof Uninitialized , next : string | null | typeof Uninitialized ; } > ( {
162
+ next : Uninitialized ,
163
+ hasNext : Uninitialized ,
164
+ } ) ;
165
+
166
+ a . registerModifier ( ( nextValue ) => {
167
+ if ( typeof nextValue . next === 'string' ) {
168
+ nextValue . hasNext = true ;
169
+ } else if ( nextValue . next === Uninitialized ) {
170
+ nextValue . hasNext = Uninitialized ;
171
+ } else {
172
+ nextValue . hasNext = false ;
173
+ }
174
+ } ) ;
175
+
176
+ a . subscribe ( ( ns ) => console . log ( ns ) ) ;
177
+
178
+ a . partialNext ( { next : 'sss' } ) ;
179
+ a . partialNext ( { next : null } ) ;
180
+ a . partialNext ( { next : Uninitialized } ) ;
0 commit comments