Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explicitly spell out the state machines for Computed and Watcher #217

Merged
merged 2 commits into from
May 13, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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