-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
122 lines (110 loc) · 4.09 KB
/
index.js
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
export default function (initialState, states = {}) {
/*
* Core Finite State Machine functionality
* - adheres to Svelte store contract (https://svelte.dev/docs#Store_contract)
* - invoked events are dispatched to handler of current state
* - transitions to returned state (or value if static property)
* - calls _exit() and _enter() methods if they are defined on exited/entered state
*/
const subscribers = new Set()
let state = null
function subscribe(callback) {
if (!(callback instanceof Function)) {
throw new TypeError('callback is not a function')
}
subscribers.add(callback)
callback(state)
return () => subscribers.delete(callback)
}
/*
* Proxy-based event invocation API:
* - return a proxy object with single native subscribe method
* - all other properties act as dynamic event invocation methods
* - event invokers also respond to .debounce(wait, ...args) (see above)
*/
const proxy = new Proxy(
{ subscribe },
{
get(target, property) {
if (!Reflect.has(target, property)) {
target[property] = invoke.bind(null, property)
target[property].debounce = debounce.bind(null, property)
}
return Reflect.get(target, property)
},
},
)
/*
* API change: subscribers are notified after _enter, not before, because eventless transitions
* might mean we settle in a new state. We may well transit through several states during the
* _enter() call.
*
* The logic here is intricate. The protocol is that there is always, internally, calls to
* _exit() followed by _enter(), with the same to and from arguments. The initial _exit()
* and the final _enter() will use the public event. All calls will have the original event
* and args -- since we never know in advance whether an event is a final one. If _enter()
* returns a new state, we then generate a new _exit() and _enter, moving from the previous
* state to the new one, and repeat. If it does not, then that _enter is the final call.
*/
function transition(newState, event, args) {
let metadata = { from: state, to: newState, event, args }
const startState = state
// Never exit the null state
if (state !== null) {
dispatch('_exit', metadata)
}
for (;;) {
state = metadata.to
const nextState = dispatch('_enter', metadata)
if (!nextState) {
break
}
metadata = { from: metadata.to, to: nextState, event, args }
dispatch('_exit', metadata)
}
// If (and only if) the final state is not the same as the initial state, then we
// inform the subscribers
if (state !== startState) {
for (const callback of subscribers) {
callback(state)
}
}
}
function dispatch(event, ...args) {
const action = states[state]?.[event] ?? states['*']?.[event]
return action instanceof Function ? action.apply(proxy, args) : action
}
function invoke(event, ...args) {
const newState = dispatch(event, ...args)?.valueOf()
if (['string', 'symbol'].includes(typeof newState) && newState !== state) {
transition(newState, event, args)
}
return state
}
/*
* Debounce functionality
* - `debounce` is lazily bound to dynamic event invoker methods (see Proxy section below)
* - `event.debounce(wait, ...args)` calls event with args after wait (unless called again first)
* - cancels all prior invocations made for the same event
* - cancels entirely when called with `wait` of `null`
*/
const timeout = {}
async function debounce(event, wait = 100, ...args) {
clearTimeout(timeout[event])
if (wait === null) {
return state
}
await new Promise((resolve) => {
timeout[event] = setTimeout(resolve, wait)
})
delete timeout[event]
return invoke(event, ...args)
}
/*
* `_enter` initial state and return the proxy object. Note that this may also
* involve eventless transitions to other states. Note, interestingly, that
* we are free to notify here, because there will never be subscribers.
*/
transition(initialState, null, [])
return proxy
}