@@ -2,33 +2,41 @@ 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
+
5
6
export type Unregister = Unsubscribe ;
6
- export type Modifier < T > = ( nextValue : T , previousValue : T | undefined ) => void ;
7
+ export type Modifier < T > = Handler < T > ;
7
8
8
9
export const isPatch = < T > ( value : ValueOrPatch < T > ) : value is Patch < T > =>
9
10
typeof value === 'function' ;
10
11
12
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
13
+ const noop = ( ) => { } ;
14
+
11
15
export class StateStore < T extends Record < string , unknown > > {
12
16
protected handlers = new Set < Handler < T > > ( ) ;
13
17
protected modifiers = new Set < Handler < T > > ( ) ;
14
18
15
- constructor ( private value : T ) { }
19
+ constructor ( protected value : T ) { }
16
20
17
21
/**
18
22
* Allows merging two stores only if their keys differ otherwise there's no way to ensure the data type stability.
19
23
*/
20
24
// eslint-disable-next-line @typescript-eslint/no-explicit-any
21
25
public merge < Q extends StateStore < any > > (
22
- stateStore : Q extends StateStore < infer L > ? ( Extract < keyof T , keyof L > extends never ? Q : never ) : never ,
26
+ stateStore : Q extends StateStore < infer L >
27
+ ? Extract < keyof T , keyof L > extends never
28
+ ? Q
29
+ : never
30
+ : never ,
23
31
) {
24
32
return new MergedStateStore < T , Q extends StateStore < infer L > ? L : never > ( {
25
- parentStore : this ,
26
- mergedStore : stateStore ,
33
+ original : this ,
34
+ merged : stateStore ,
27
35
} ) ;
28
36
}
29
37
30
- public next = ( newValueOrPatch : ValueOrPatch < T > ) : void => {
31
- // newValue (or patch output) should never be mutated previous value
38
+ public next ( newValueOrPatch : ValueOrPatch < T > ) : void {
39
+ // newValue (or patch output) should never be a mutated previous value
32
40
const newValue = isPatch ( newValueOrPatch )
33
41
? newValueOrPatch ( this . value )
34
42
: newValueOrPatch ;
@@ -42,12 +50,15 @@ export class StateStore<T extends Record<string, unknown>> {
42
50
this . value = newValue ;
43
51
44
52
this . handlers . forEach ( ( handler ) => handler ( this . value , oldValue ) ) ;
45
- } ;
53
+ }
46
54
47
55
public partialNext = ( partial : Partial < T > ) : void =>
48
56
this . next ( ( current ) => ( { ...current , ...partial } ) ) ;
49
57
50
- public getLatestValue = ( ) : T => this . value ;
58
+ public getLatestValue ( ) : T {
59
+ return this . value ;
60
+ }
61
+
51
62
public subscribe ( handler : Handler < T > ) : Unsubscribe {
52
63
handler ( this . value , undefined ) ;
53
64
this . handlers . add ( handler ) ;
@@ -96,74 +107,131 @@ export class StateStore<T extends Record<string, unknown>> {
96
107
}
97
108
}
98
109
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 > ;
110
+ class MergedStateStore <
111
+ O extends Record < string , unknown > ,
112
+ M extends Record < string , unknown > ,
113
+ > extends StateStore < O & M > {
114
+ public readonly original : StateStore < O > ;
115
+ public readonly merged : StateStore < M > ;
116
+ private cachedOriginalValue : O ;
117
+ private cachedMergedValue : M ;
118
+
119
+ constructor ( { original, merged } : { original : StateStore < O > ; merged : StateStore < M > } ) {
120
+ const originalValue = original . getLatestValue ( ) ;
121
+ const mergedValue = merged . getLatestValue ( ) ;
102
122
103
- constructor ( { parentStore, mergedStore } : { mergedStore : StateStore < L > ; parentStore : StateStore < T > } ) {
104
123
super ( {
105
- ...parentStore . getLatestValue ( ) ,
106
- ...mergedStore . getLatestValue ( ) ,
124
+ ...originalValue ,
125
+ ...mergedValue ,
107
126
} ) ;
108
127
109
- this . parentStore = parentStore ;
110
- this . mergedStore = mergedStore ;
128
+ this . cachedOriginalValue = originalValue ;
129
+ this . cachedMergedValue = mergedValue ;
130
+
131
+ this . original = original ;
132
+ this . merged = merged ;
111
133
}
112
134
113
- public subscribe ( handler : Handler < T & L > ) {
135
+ public subscribe ( handler : Handler < O & M > ) {
114
136
const unsubscribeFunctions : Unsubscribe [ ] = [ ] ;
115
137
138
+ // first subscriber will also register helpers which listen to changes of the
139
+ // "original" and "merged" stores, combined outputs will be emitted through super.next
140
+ // whenever cached values do not equal (always apart from the initial subscription)
141
+ // since the actual handler subscription is registered after helpers, the actual
142
+ // handler will run only once
116
143
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
- }
144
+ const base = ( nextValue : O | M ) => {
145
+ super . next ( ( currentValue ) => ( {
146
+ ...currentValue ,
147
+ ...nextValue ,
148
+ } ) ) ;
149
+ } ;
150
+
151
+ unsubscribeFunctions . push (
152
+ this . original . subscribe ( ( nextValue ) => {
153
+ if ( nextValue === this . cachedOriginalValue ) return ;
154
+ this . cachedOriginalValue = nextValue ;
155
+ base ( nextValue ) ;
156
+ } ) ,
157
+ this . merged . subscribe ( ( nextValue ) => {
158
+ if ( nextValue === this . cachedMergedValue ) return ;
159
+ this . cachedMergedValue = nextValue ;
160
+ base ( nextValue ) ;
161
+ } ) ,
162
+ ) ;
130
163
}
131
164
132
165
unsubscribeFunctions . push ( super . subscribe ( handler ) ) ;
133
166
134
167
return ( ) => {
135
- unsubscribeFunctions . forEach ( ( f ) => f ( ) ) ;
168
+ unsubscribeFunctions . forEach ( ( unsubscribe ) => unsubscribe ( ) ) ;
136
169
} ;
137
170
}
138
171
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)
172
+ public getLatestValue ( ) {
173
+ // if there are no handlers registered to MergedStore then the local value might be out-of-sync
174
+ // pull latest and compare against cached - if they differ, cache latest and produce new combined
175
+ if ( ! this . handlers . size ) {
176
+ const originalValue = this . original . getLatestValue ( ) ;
177
+ const mergedValue = this . merged . getLatestValue ( ) ;
178
+
179
+ if (
180
+ originalValue !== this . cachedOriginalValue ||
181
+ mergedValue !== this . cachedMergedValue
182
+ ) {
183
+ this . value = {
184
+ ...originalValue ,
185
+ ...mergedValue ,
186
+ } ;
187
+ this . cachedMergedValue = mergedValue ;
188
+ this . cachedOriginalValue = originalValue ;
189
+ }
190
+ }
150
191
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`
192
+ return super . getLatestValue ( ) ;
193
+ }
153
194
154
- // l.subscribe(console.info);
195
+ // override original methods and "disable" them
196
+ public next = ( ) => {
197
+ console . warn (
198
+ `${ MergedStateStore . name } .next is disabled, call original.next or merged.next instead` ,
199
+ ) ;
200
+ } ;
201
+ public partialNext = ( ) => {
202
+ console . warn (
203
+ `${ MergedStateStore . name } .partialNext is disabled, call original.partialNext or merged.partialNext instead` ,
204
+ ) ;
205
+ } ;
206
+ public registerModifier ( ) {
207
+ console . warn (
208
+ `${ MergedStateStore . name } .registerModifier is disabled, call original.registerModifier or merged.registerModifier instead` ,
209
+ ) ;
210
+ return noop ;
211
+ }
212
+ }
155
213
156
- // t.next({ a: 'poof' });
157
- // b.next({ q: 'nah' });
214
+ // EXAMPLE:
158
215
159
216
const Uninitialized = Symbol ( 'uninitialized' ) ;
160
217
161
- const a = new StateStore < { hasNext : boolean | typeof Uninitialized , next : string | null | typeof Uninitialized ; } > ( {
218
+ const b = new StateStore < {
219
+ previous : string | null | symbol ;
220
+ hasPrevious : boolean | symbol ;
221
+ } > ( {
222
+ previous : Uninitialized ,
223
+ hasPrevious : Uninitialized ,
224
+ } ) ;
225
+
226
+ const a = new StateStore < {
227
+ hasNext : boolean | symbol ;
228
+ next : string | null | symbol ;
229
+ } > ( {
162
230
next : Uninitialized ,
163
231
hasNext : Uninitialized ,
164
- } ) ;
232
+ } ) . merge ( b ) ;
165
233
166
- a . registerModifier ( ( nextValue ) => {
234
+ a . original . registerModifier ( ( nextValue ) => {
167
235
if ( typeof nextValue . next === 'string' ) {
168
236
nextValue . hasNext = true ;
169
237
} else if ( nextValue . next === Uninitialized ) {
@@ -175,6 +243,6 @@ a.registerModifier((nextValue) => {
175
243
176
244
a . subscribe ( ( ns ) => console . log ( ns ) ) ;
177
245
178
- a . partialNext ( { next : 'sss ' } ) ;
179
- a . partialNext ( { next : null } ) ;
180
- a . partialNext ( { next : Uninitialized } ) ;
246
+ a . original . partialNext ( { next : 'next ' } ) ;
247
+ a . original . partialNext ( { next : null } ) ;
248
+ a . original . partialNext ( { next : Uninitialized } ) ;
0 commit comments