-
Notifications
You must be signed in to change notification settings - Fork 1
/
core.ts
96 lines (93 loc) · 3.44 KB
/
core.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import type { RootState, Selector, Store } from "@watchable/store";
import type { Controls, Follower, QueueHandler, ExitStatus } from "./types";
import { createQueue } from "@watchable/queue";
/**
* Configures a {@link @watchable/queue!MessageQueue} that will receive messages with
* every new value of a {@link @watchable/store!Selector} against a
* {@link @watchable/store!Store}. Passes the queue and the initial value from the
* Selector to `handleQueue` then waits for `handleQueue` to return, after which
* the queue is unsubscribed.
*
* @param store {@link @watchable/store!Store} to monitor
* @param selector a {@link @watchable/store!Selector} function to extract the selected value
* @param handleQueue {@link QueueHandler} function passed the initial selected value and queue
* @returns the value returned by `handleQueue`
*/
export async function withSelectorQueue<
State extends RootState,
Selected,
Ending
>(
store: Store<State>,
selector: Selector<State, Selected>,
handleQueue: QueueHandler<Selected, Ending>
): Promise<Ending> {
const queue = createQueue<Selected>();
// Could be hoisted as a 'SelectorWatchable'
let prevSelected: Selected = selector(store.read());
const selectedNotifier = (value: State) => {
const nextSelected = selector(value);
if (!Object.is(nextSelected, prevSelected)) {
prevSelected = nextSelected;
queue.send(nextSelected);
}
};
const unwatch = store.watch(selectedNotifier); // subscribe future states
selectedNotifier(store.read()); // notify the initial state
try {
return await handleQueue(queue, prevSelected);
} finally {
unwatch();
}
}
/**
* Invokes the {@link Follower | follower} once with the initial value of
* {@link @watchable/store!Selector | selector} and again every time
* {@link @watchable/store!Store | store} has a changed value of `Selector`. If follower is
* async, each invocation will be awaited before the next is called.
*
* The `follower` is passed the new value each time, and also a
* {@link Controls | control} object which can be used to exit the loop like
* `return control.exit(myValue)`. If `follower` doesn't return an exit
* instruction, its return value is ignored and it will be invoked again on the
* the next `Selector` change.
*
* @param store The store to follow
* @param selector The function to extract the selected value
* @param follower The callback to handle each changing value
* @returns Any `Ending` returned when exiting the loop
*/
export async function followSelector<State extends RootState, Selected, Ending>(
store: Store<State>,
selector: Selector<State, Selected>,
follower: Follower<Selected, Ending>
): Promise<Ending> {
return await withSelectorQueue(
store,
selector,
async function (queue, selected) {
const { receive } = queue;
let result: Ending;
let lastSelected: Selected | undefined;
const exitStatus: ExitStatus = ["exit"];
const controls: Controls<Selected, Ending> = {
exit(ending: Ending) {
result = ending;
return exitStatus;
},
lastSelected() {
return lastSelected;
},
};
for (;;) {
const ending = await follower(selected, controls);
if (ending === exitStatus) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return result!;
}
lastSelected = selected;
selected = await receive();
}
}
);
}