/
typedefs.ts
185 lines (166 loc) · 10.1 KB
/
typedefs.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
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
/** base type definitions <br>
* @module
*/
/** type definition for a value equality check function. */
export type EqualityFn<T> = (prev_value: T | undefined, new_value: T) => boolean
/** type definition for an equality check specification. <br>
* when `undefined`, javascript's regular `===` equality will be used. <br>
* when `false`, equality will always be evaluated to false, meaning that setting any value will always fire a signal, even if it's equal.
*/
export type EqualityCheck<T> = undefined | false | EqualityFn<T>
/** the {@link Signal.id | `id`} of a signal is simply a number. */
export type ID = number
/** another way of saying the dependency (or source) signal id. */
export type FROM_ID = ID
/** another way of saying the observer (or destination) signal id. */
export type TO_ID = ID
/** the {@link Signal.rid | runtime id (`rid`)} of a signal becomes zero after its first run (because it has captured all of its dependencies). */
export type UNTRACKED_ID = 0
/** the hash value generated by the dfs-signal-dag-traversal hash function. */
export type HASHED_IDS = number
/** type definition for an incremental updater function. */
export type Updater<T> = (prev_value?: T) => T
/** type definition for a signal accessor (value getter) function. */
export type Accessor<T> = ((observer_id?: TO_ID | UNTRACKED_ID) => T)
/** type definition for a signal value setter function. */
export type Setter<T> = ((new_value: T | Updater<T>) => boolean)
/** type definition for an async signal value setter function. <br>
* TODO: should an `AsyncSetter<T>` return a `Promise<T>` ? or should it return a `Promise<boolean>`, which should tell whether or not the value has changed (i.e. `!signal.equal(old_value, new_value)`)
*/
export type AsyncSetter<T> = (
new_value:
| (T | Promise<T | Updater<T>>)
| Updater<T | Promise<T | Updater<T>>>,
rejectable?: boolean,
) => Promise<T>
/** type definition for when a signal's _update_ function `run` is called by the signal update propagator `propagateSignalUpdate` inside of {@link context!Context}. <br>
* the return value should indicate whether this signal has:
* - updated ({@link SignalUpdateStatus.UPDATED} === 1), and therefore propagate to its observers to also run
* - unchanged ({@link SignalUpdateStatus.UNCHANGED} === 0), and therefore not propagate to its observers
* - aborted ({@link SignalUpdateStatus.ABORTED} === -1), and therefore force each of its observers to also become non propagating and inherit the {@link SignalUpdateStatus.ABORTED} status
*/
export type Runner = () => SignalUpdateStatus
/** the abstraction that defines what a signal is. */
export interface Signal<T> {
/** id of this signal in the {@link context!Context} in which it exists. */
id: number
/** runtime-id of this signal. <br>
* it equals to the {@link id} on the first, so that the dependencies of this signal can be notified of being observed.
* but once all dependencies have been notified after the first run, this runtime-id should become `0` ({@link UNTRACKED_ID}),
* so that the dependiencies do not have to re-register this signal as an observer.
*/
rid: ID | UNTRACKED_ID
/** give a name to this signal for debugging purposes */
name?: string
/** get the value of this signal, and handle any observing signal's id ({@link observer_id}). <br>
* typically, when `observer_id` is non-zero, this signal should handle it by registering it as an
* observer through the use of the context's {@link context!Context.addEdge} method.
*
* @example
* ```ts
* const MySignalClass_Factory = (ctx: Context) => {
* const addEdge = ctx.addEdge
* return class MySignal<T> implements Signal<T> {
* declare value: T
* // ...
* get(observer_id?: TO_ID | UNTRACKED_ID): T {
* // register this.id to observer (if non-zero) in the dependency graph as a directed edge
* if (observer_id) { addEdge(this.id, observer_id) }
* return this.value
* }
* // ...
* }
* }
* ```
*/
get(observer_id?: TO_ID | UNTRACKED_ID): T
/** set the value of this signal. <br>
* the meaning of setting a signal's value greatly varies from signal to signal, which is why it is so abstracted. <br>
* however, the returning value must always be a `boolean` describing whether or not this signal's value has changed compared to its previous value. <br>
* what makes use of the returned value again greatly varies from signal to signal.
* but it is typically used by the {@link run} method to decide whether or not this signal should propagate. <br>
* another purpose of the set method is typically to _initiate_ the ignition of an update cycle in a context, bu using {@link context!Context.runId}.
* this is how {@link signal!StateSignal}s and {@link signal!EffectSignal}s begin an update cycle when their values have changed from the prior value.
*
* @example
* ```ts
* const MySignalClass_Factory = (ctx: Context) => {
* const runId = ctx.runId
* return class MySignal<T> implements Signal<T> {
* declare value: T
* // ...
* set(new_value: T): boolean {
* const value_has_changed = new_value !== this.value
* if (value_has_changed) {
* runId(this.id)
* return true
* }
* return false
* }
* // ...
* }
* }
* ```
*/
set?(...args: any[]): boolean
/** specify actions that need to be taken __before__ an update cycle has even begun propagating. <br>
* TODO: CURRENTLY NOT IMPLEMENTED.
* ISSUE: what should the order in which prepruns run be? we do know the FULL set of signal ids that will be visited.
* but we do not know the subset of ids that WILL BE affected and ran, not until run time of the signal propagation.
* should ids that _might_ be affected also have their preruns ran? and in what order? because we cannot know the order until propagation runtime.
*/
prerun?(): void
/** run the actions taken by a signal when it is informed that its dependency signals have been modified/changed. <br>
* the return value should be of the numertic enum kind {@link SignalUpdateStatus}, which specifies that this signal has been either been:
* - ` 1`: updated, and therefore this signal's observers should be notified (i.e. _ran_ via their `run` method)
* - ` 0`: unchanged, and therefore this signal's observers should be notified if this is their only active dependency
* - `-1`: aborted, and therefore this signal's observer should also abort their `run` method's execution if they were queued
*
* this method may also accept an optional `forced` parameter, which tells the signal that it is
* being _forced_ to run, even though none of its dependency signals have been executed or changed.
* this information is useful when coding for signals that _can_ be fired independently, such as {@link signal!StateSignal} or {@link signal!EffectSignal}.
*
* @param forced was this signal _forced_ to run independently?
* @returns the update status of this signal, specifying whether or not it has changed, or if it has been aborted
*/
run(forced?: boolean): SignalUpdateStatus
/** specify actions that need to be taken __after__ an update cycle has fully propagated till the end. <br>
* the order in which `postrun`s will be executed will be in the reverse order in which they were first encountered (i.e: last in, last out).
* meaning that if all three signals `A`, `B`, and `C`, had `postrun` methods on them, and the order of execution was:
* `A -> B -> C`, then all `postrun`s will run in the order: `[C.postrun, B.postrun, A.postrun]` (similar to a stack popping).
*/
postrun?(): void
/** a utility method defined in {@link signal!SimpleSignal}, which allows one to bind a certain method (by name) to _this_ instance of a signal,
* and therefore make that method freeable/seperable from _this_ signal. <br>
* this method is used by all signal classes's static {@link SignalClass.create} method, which is supposed to construct a signal in
* a fashion similar to SolidJS, and return an array containing important control functions of the created signal.
* most, if not all, of these control function are generally plain old signal methods that have been bounded to the created signal instance.
*/
bindMethod<M extends keyof this>(method_name: M): this[M]
}
/** the abstraction that defines the static methods which must exist in a signal generating class */
export interface SignalClass {
new(...args: any[]): Signal<any>
create(...args: any[]): [id: ID, ...any[]]
}
/** the numbers used for relaying the status of a signal after it has been _ran_ via its {@link Signal.run | `run method`}. <br>
* these numbers convey the following instructions to the context's topological update cycle {@link context!Context.propagateSignalUpdate}:
* - ` 1`: this signal's value has been updated, and therefore its observers should be updated too.
* - ` 0`: this signal's value has not changed, and therefore its observers should be _not_ be updated.
* do note that an observer signal will still run if some _other_ of its dependency signal did update this cycle (i.e. had a status value of `1`)
* - `-1`: this signal has been aborted, and therefore its observers must abort execution as well.
* the observers will abort _even_ if they had a dependency that _did_ update (had a status value of `1`)
*
* to sum up, given a signal `D`, with dependencies: `A`, `B`, and `C` (all of which are mutually independent of each other).
* then the status of `D` will be as follows in the order of highest conditional priority to lowest:
* | status of D | status of D as enum | condition |
* |-----------------------|:-------------------------------------:|:-----------------------------------------------------------------------------:|
* | `status(D) = -1` | `ABORTED` | `∃X ∈ [A, B, C] such that status(X) === -1` |
* | `status(D) = D.run()` | `CHANGED` or `UNCHANGED` or `ABORTED` | `∃X ∈ [A, B, C] such that status(X) === 1` |
* | `status(D) = 0` | `UNCHANGED` | `∀X ∈ [A, B, C], status(X) === 0` |
*/
export const enum SignalUpdateStatus {
ABORTED = -1,
UNCHANGED = 0,
UPDATED = 1,
}