Skip to content

Commit

Permalink
Explicitly spell out the state machines for Computed and Watcher (#…
Browse files Browse the repository at this point in the history
…217)

* Explicitly spell out the `Computed` state machine

* Explicitly spell out the `Watcher` state machine
  • Loading branch information
prophile committed May 13, 2024
1 parent f7c550b commit c4fa34c
Showing 1 changed file with 65 additions and 1 deletion.
66 changes: 65 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,12 +486,47 @@ Signal algorithms need to reference certain global state. This state is global f
1. Set the `state` of all `sinks` of this Signal to (if it is a Computed Signal) `~dirty~` if they were previously clean, or (if it is a Watcher) `~pending~` if it was previously `~watching~`.
1. Set the `state` of all of the sinks' Computed Signal dependencies (recursively) to `~checked~` if they were previously `~clean~` (that is, leave dirty markings in place), or for Watchers, `~pending~` if previously `~watching~`.
1. For each previously `~watching~` Watcher encountered in that recursive search, then in depth-first order,
1. Set `notifying` to true while calling their `notify` callback (saving aside any exception thrown, but ignoring the return value of `notify`), and then restore `notifying` to false.
1. Set `notifying` to true.
1. Calling their `notify` callback (saving aside any exception thrown, but ignoring the return value of `notify`).
1. Restore `notifying` to false.
1. Set the `state` of the Watcher to `~waiting~`.
1. If any exception was thrown from the `notify` callbacks, propagate it to the caller after all `notify` callbacks have run. If there are multiple exceptions, then package them up together into an AggregateError and throw that.
1. Return undefined.
### The `Signal.Computed` class
#### `Signal.Computed` State machine
The `state` of a Computed Signal may be one of the following:
- `~clean~`: The Signal's value is present and known not to be stale.
- `~checked~`: An (indirect) source of this Signal has changed; this Signal has a value but it _may_ be stale. Whether or it not is stale will be known only when all immediate sources have been evaluated.
- `~computing~`: This Signal's callback is currently being executed as a side-effect of a `.get()` call.
- `~dirty~`: Either this Signal has a value which is known to be stale, or it has never been evaluated.
The transition graph is as follows:
```mermaid
stateDiagram-v2
[*] --> dirty
dirty --> computing: [4]
computing --> clean: [5]
clean --> dirty: [2]
clean --> checked: [3]
checked --> clean: [6]
checked --> dirty: [1]
```
The transitions are:
| Number | From | To | Condition | Algorithm |
| ------ | ---- | -- | --------- | --------- |
| 1 | `~checked~` | `~dirty~` | An immediate source of this signal, which is a computed signal, has been evaluated, and its value has changed. | Algorithm: recalculate dirty computed Signal |
| 2 | `~clean~` | `~dirty~` | An immediate source of this signal, which is a State, has been set, with a value which is not equal to its previous value. | Method: `Signal.State.prototype.set(newValue)` |
| 3 | `~clean~` | `~checked~` | A recursive, but not immediate, source of this signal, which is a State, has been set, with a value which is not equal to its previous value. | Method: `Signal.State.prototype.set(newValue)` |
| 4 | `~dirty~` | `~computing~` | We are about to execute the `callback`. | Algorithm: recalculate dirty computed Signal |
| 5 | `~computing~` | `~clean~` | The `callback` has finished evaluating and either returned a value or thrown an exception. | Algorithm: recalculate dirty computed Signal |
| 6 | `~checked~` | `~clean~` | All immediate sources of this signal have been evaluated, and all have been discovered unchanged, so we are now known not to be stale. | Algorithm: recalculate dirty computed Signal |
#### `Signal.Computed` Internal slots
- `value`: The previous cached value of the Signal, or `~uninitialized~` for a never-read computed Signal. The value may be an exception which gets rethrown when the value is read. Always `undefined` for effect signals.
Expand Down Expand Up @@ -523,6 +558,33 @@ With [AsyncContext](https://github.com/tc39/proposal-async-context), the callbac
### The `Signal.subtle.Watcher` class
#### `Signal.subtle.Watcher` State machine
The `state` of a Watcher may be one of the following:
- `~waiting~`: The `notify` callback has been run, or the Watcher is new, but is not actively watching any signals.
- `~watching~`: The Watcher is actively watching signals, but no changes have yet happened which would necessitate a `notify` callback.
- `~pending~`: A dependency of the Watcher has changed, but the `notify` callback has not yet been run.
The transition graph is as follows:
```mermaid
stateDiagram-v2
[*] --> waiting
waiting --> watching: [1]
watching --> waiting: [2]
watching --> pending: [3]
pending --> waiting: [4]
```
The transitions are:
| Number | From | To | Condition | Algorithm |
| ------ | ---- | -- | --------- | --------- |
| 1 | `~waiting~` | `~watching~` | The Watcher's `watch` method has been called. | Method: `Signal.subtle.Watcher.prototype.watch(...signals)` |
| 2 | `~watching~` | `~waiting~` | The Watcher's `unwatch` method has been called, and the last watched signal has been removed. | Method: `Signal.subtle.Watcher.prototype.unwatch(...signals)` |
| 3 | `~watching~` | `~pending~` | A watched signal may have changed value. | Method: `Signal.State.prototype.set(newValue)` |
| 4 | `~pending~` | `~waiting~` | The `notify` callback has been run. | Method: `Signal.State.prototype.set(newValue)` |
#### `Signal.subtle.Watcher` internal slots
- `state`: May be `~watching~`, `~pending~` or `~waiting~`
Expand All @@ -543,13 +605,15 @@ With [AsyncContext](https://github.com/tc39/proposal-async-context), the callbac
1. Append all arguments to the end of this object's `signals`.
1. Add this watcher to each of the newly watched signals as a sink.
1. Add this watcher as a `sink` to each Signal. If this was the first sink, then recurse up to sources to add that signal as a sink, and call the `watched` callback if it exists.
1. If the Signal's `state` is `~waiting~`, then set it to `~watching~`.
#### Method: `Signal.subtle.Watcher.prototype.unwatch(...signals)`
1. If any of the arguments is not a signal, or is not being watched by this watcher, throw an exception.
1. Remove each element from signals from this object's `signals`.
1. Remove this Watcher from that Signal's `sink` set.
1. If any Signal's `sink` set is now empty, then remove itself as a sink from each of its sources, and call the `unwatched` callback if it exists
1. If the watcher now has no `signals`, and its `state` is `~watching~`, then set it to `~waiting~`.
#### Method: `Signal.subtle.Watcher.prototype.getPending()`
Expand Down

0 comments on commit c4fa34c

Please sign in to comment.