-
Notifications
You must be signed in to change notification settings - Fork 0
/
typedefs.ts
280 lines (255 loc) · 15.9 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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
/** base type definitions <br>
* @module
*/
import type { Context } from "./context.ts"
import type { EffectSignal_Factory, SimpleSignal_Factory, StateSignal_Factory } from "./signal.ts"
type SimpleSignal = ReturnType<typeof SimpleSignal_Factory>
type StateSignal = ReturnType<typeof StateSignal_Factory>
type EffectSignal = ReturnType<typeof EffectSignal_Factory>
/** 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
/** a utility type that assigns the property `id` to the generic object `T`. <br>
* this type is commonly used by the return types of {@link SignalClass.create | signal create functions}, to make the {@link Accessor}s and {@link Setter}s identifiable.
*/
export type Identifiable<T> = T & { id: ID }
/** type definition for an incremental updater function. */
export type Updater<T> = (prev_value?: T) => T
/** type definition for a pure signal accessor (value getter) function. <br>
* it differs from the _actual_ returned {@link Accessor} type of a {@link SignalClass.create | signal create function}, in that it lacks the id information of its own signal. <br>
* use this version when annotating functions that do not read the id of dependency signals (quite common),
* otherwise opt out for {@link Accessor} if you do intend to read the ids of the dependency signals (common in dynamic signals, which can erase and add new dependencies cleanly).
*/
export type PureAccessor<T> = {
(observer_id?: TO_ID | UNTRACKED_ID): T
(get_self_id: typeof GET_ID): ID
}
/** type definition for a signal value setter function. <br>
* it differs from the _actual_ returned {@link Setter} type of a {@link SignalClass.create | signal create function}, in that it lacks the id information of its own signal. <br>
* use this version when annotating signal value setters who's ids are not read (quite common),
* otherwise opt out for {@link Setter} if you do intend to read the id of the signal setter (seldom used in dynamic signals).
*/
export type PureSetter<T> = ((new_value: T | Updater<T>) => boolean)
/** type definition for a signal accessor (value getter) function. <br>
* notice that it contains a custom `id` property assigned to it, so it is not just any anonymous function.
* to manually replicate it, you will also need to assign an `id` property to the replica anonymous function. <br>
* this type of accessor is what is returned by a {@link SignalClass.create | signal create function}, as opposed to {@link PureAccessor | pure accessor function}.
*
* why would the `id` be needed? <br>
* often times, a dynamic-dependency signal (such as one that is an array of other accessors) needs to know the `id`s of its dependencies so that it can remove them later on, when they are no longer a dependency.
*/
export interface Accessor<T> extends Identifiable<PureAccessor<T>> {
/** the id of the signal, from which this accessor originates from. <br>
* this property is statically inherited from {@link Signal.id}, when the accessor function is created by a {@link SignalClass.create | signal create function}.
*/
id: ID
}
/** type definition for a signal setter (value getter) function. <br>
* notice that it contains a custom `id` property assigned to it, so it is not just any anonymous function.
* to manually replicate it, you will also need to assign an `id` property to the replica anonymous function. <br>
* this type of setter is what is returned by a {@link SignalClass.create | signal create function}, as opposed to {@link PureSetter | pure setter function}.
*
* why would the `id` be needed? <br>
* good question. I don't know under what circumstances one would ever need this information.
* but for the sake of future proofing, and consistency with {@link Accessor}'s definition, the `id` is assigned as a property anyway.
*/
export interface Setter<T> extends Identifiable<PureSetter<T>> {
/** the id of the signal, from which this setter originates from. <br>
* this property is statically inherited from {@link Signal.id}, when the setter function is created by a {@link SignalClass.create | signal create function}.
*/
id: ID
}
/** type definition for an async signal value setter function. <br>
* TODO: should an `PureAsyncSetter<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 PureAsyncSetter<T> = (
new_value:
| (T | Promise<T | Updater<T>>)
| Updater<T | Promise<T | Updater<T>>>,
rejectable?: boolean,
) => Promise<T>
/** type definition for an async signal value setter function. */
export interface AsyncSetter<T> extends Identifiable<PureAsyncSetter<T>> { }
/** type definition for when a signal's _update_ function `run` is called by the signal update propagator `propagateSignalUpdate` inside of {@link 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} 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, the implementing class will follow the given sequence of actions, depending on what the `observer_id` is:
* - if it is `0` or `undefined` (i.e. {@link UNTRACKED_ID | untrackable id}), then this signal does not register it as a valid observer (no creation of dependency).
* - if it is `> 0` (i.e. {@link ID | positive id}), then this signal registers it as an observer (through the use of the context's {@link Context.addEdge} method).
* - if it is `< 0` (i.e. {@link ID | negative id}), then this signal unregisters it from observation (through the use of the context's {@link Context.delEdge} method). <br>
* as a result, the observer-signal will no longer be notified by this signal if this signal ever propagates an update.
*
* @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 {
* // if the observer's id is positive, then create a directed edge relation between this signal's `id` and the `observer_id`, in the dependency graph.
* if (observer_id > 0) { addEdge(this.id, observer_id) }
* // if the observer's id is negative, then delete the directed edge relation between this signal's `id` and the `observer_id`, in the dependency graph.
* else if (observer_id < 0) { addEdge(this.id, observer_id) }
* return this.value
* }
* // ...
* }
* }
* ```
*/
get(observer_id?: TO_ID | UNTRACKED_ID): T
get(get_self_id: typeof GET_ID): ID
/** 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.runId}.
* this is how {@link StateSignal}s and {@link 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 StateSignal} or {@link 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 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.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,
}
/** when this symbol is passed onto an {@link Accessor}, the accessor should return its associated signal's {@link Signal.id | `id`}, rather than returning its own {@link Signal.value | `value`}.
* finding out a signal's `id` through the use of its accessor function has many benefits when it comes to dynamic dependency management.
*
* @example
* ```ts
* const [idA, getA, setA] = createState<number>(55)
* console.assert(getA() === 55)
* console.assert(getA(GET_ID) === idA)
* ```
*
* @example
* ```ts
* declare ctx: Context
* const [idA, getA, setA] = createState<number>(55)
* const [idB, getB] = createMemo<number>((id) => {
* const double_value = getA(id) * 2
* console.log(double_value)
* return double_value
* }, { defer: false }) // the console will immediately log "110".
* setA(22) // this update will cause the memo signal to recompute and log "44" in the console.
* console.assert(getB() === getA() * 2)
*
* // now we remove the memo's dependency on the state signal.
* ctx.delEdge(getA(GET_ID), getB(GET_ID))
* // notice we didn't have to use `idA` nor `idB` variables.
* // this is because the ids were provided by the accessors when called with the `GET_ID` symbol parameter.
*
* setA(33) // this will no longer update the memo signal.
* console.assert(getB() !== getA() * 2)
* ```
*/
export const GET_ID = Symbol()