|
1 | | -export type Subscriber<T> = (val: T) => void; |
| 1 | +import { Keyof as K } from "./util.ts"; |
2 | 2 |
|
3 | | -export class ReadonlyRef<T = any> { |
| 3 | +type StateEntries<Obj extends Record<string, State>> = { [k in K<Obj>]: { key: k; value: Obj[k]["value"] } }[K<Obj>]; |
| 4 | +type MapEntries<Rs extends State[]> = { [k in keyof Rs]: { key: k; value: Rs[k]["value"] } }[number]; |
| 5 | + |
| 6 | +export type Subscriber<T> = (value: T, unused?: unknown) => void; |
| 7 | +export type KeyedSubscriber<K extends { key: unknown; value: unknown }> = (value: K["value"], key: K["key"]) => void; |
| 8 | +export type StateType<R extends State> = R extends State<infer U> ? U : never; |
| 9 | + |
| 10 | +export class ReadonlyState<T = any> { |
4 | 11 | protected subscribers: Subscriber<T>[] = []; |
5 | 12 |
|
6 | 13 | constructor(public value: T) {} |
7 | 14 |
|
8 | | - listen(f: Subscriber<T>) { |
9 | | - this.subscribers.push(f); |
| 15 | + static isState<X>(x: X): x is Extract<X, State | ReadonlyState> { |
| 16 | + return x instanceof ReadonlyState; |
| 17 | + } |
| 18 | + |
| 19 | + listen(listener: Subscriber<T>) { |
| 20 | + this.subscribers.push(listener); |
10 | 21 | } |
11 | 22 |
|
12 | 23 | map<U>(mapper: (t: T) => U) { |
13 | | - const s = new Ref(mapper(this.value)); |
| 24 | + const s = new State(mapper(this.value)); |
14 | 25 | // publish mapped changes when value changes |
15 | 26 | this.listen(value => s.publish(mapper(value))); |
16 | 27 | // return readonly so mapped state can't be published into |
17 | 28 | return s.readonly(); |
18 | 29 | } |
19 | 30 |
|
20 | | - effect(effector: (t: T) => void) { |
21 | | - // trigger effect when value changes |
22 | | - this.listen(effector); |
23 | | - } |
24 | | - |
25 | | - into(state: Ref<T>) { |
| 31 | + into(state: State<T>) { |
26 | 32 | this.listen(value => state.publish(value)); |
27 | 33 | } |
28 | 34 | } |
29 | 35 |
|
30 | | -export class Ref<T = any> extends ReadonlyRef<T> { |
| 36 | +export class State<T = any> extends ReadonlyState<T> { |
31 | 37 | constructor(value: T) { |
32 | 38 | super(value); |
33 | 39 | } |
34 | 40 |
|
35 | | - static isRef<X>(x: X): x is Extract<X, Ref | ReadonlyRef> { |
36 | | - return x instanceof ReadonlyRef; |
| 41 | + /** |
| 42 | + * Merge multiple refs into a single Ref |
| 43 | + */ |
| 44 | + static merge<T>(...states: [State<T>, ...State<T>[]]): MergedState<MapEntries<State<T>[]>>; |
| 45 | + |
| 46 | + static merge<RefMap extends { [k: string]: State }>(refs: RefMap): MergedState<StateEntries<RefMap>>; |
| 47 | + |
| 48 | + static merge<T, RefMap extends { [k: string]: State }>( |
| 49 | + ...refs: [State<T> | RefMap, ...State<T>[]] |
| 50 | + ): State<T> | MergedState<MapEntries<State<T>[]>> | MergedState<StateEntries<RefMap>> { |
| 51 | + if (State.isState(refs[0])) { |
| 52 | + const ref = new MergedState<MapEntries<State<T>[]>>(refs[0].value); |
| 53 | + for (let i = 0; i < refs.length; i++) { |
| 54 | + const r = refs[i] as State<T>; |
| 55 | + r.listen(x => ref.publish(x, i)); |
| 56 | + } |
| 57 | + return ref; |
| 58 | + } else { |
| 59 | + const ref = new MergedState<StateEntries<RefMap>>(null); |
| 60 | + const rs = refs[0]; |
| 61 | + for (const r in rs) rs[r].listen(c => ref.publish(c, r)); |
| 62 | + return ref; |
| 63 | + } |
37 | 64 | } |
38 | 65 |
|
39 | | - publish(next: T | Promise<T>) { |
| 66 | + publish(next: T | Promise<T>, unused?: unknown) { |
40 | 67 | return Promise.resolve(next).then(val => { |
41 | 68 | this.value = val; |
42 | 69 | this.subscribers.forEach(subscriber => subscriber(val)); |
43 | 70 | }); |
44 | 71 | } |
45 | 72 |
|
46 | 73 | readonly() { |
47 | | - return new ReadonlyRef(this.value); |
| 74 | + return new ReadonlyState(this.value); |
| 75 | + } |
| 76 | +} |
| 77 | + |
| 78 | +export class MergedState<T extends { key: string | number; value: unknown }> extends State<T["value"]> { |
| 79 | + // @ts-expect-error |
| 80 | + protected subscribers: KeyedSubscriber<T>[]; |
| 81 | + |
| 82 | + listen(listener: KeyedSubscriber<T>): void { |
| 83 | + this.subscribers.push(listener); |
| 84 | + } |
| 85 | + |
| 86 | + publish(value: T["value"] | Promise<T["value"]>, key: T["key"]) { |
| 87 | + return Promise.resolve(value).then(val => { |
| 88 | + this.value = val; |
| 89 | + this.subscribers.forEach(subscriber => subscriber(val, key)); |
| 90 | + }); |
48 | 91 | } |
49 | 92 | } |
0 commit comments