Skip to content

Commit

Permalink
Starting work on error recovery
Browse files Browse the repository at this point in the history
Co-authored-by: Godfrey Chan <[email protected]>

Revert "Starting work on error recovery"

This reverts commit 3e83045.

Added error recovery machine infrastructure

The infrastructure doesn't *do* anything yet, but it exists end-to-end.

Consolidate stack internals

Make the tests work with trace logging on

Remove fetchValue(machine register)

Clean up stack and debug logging

In progress updating stack checks

These changes will make it easier to iterate on the low-level VM, which
will be necessary in order to add error recovery in appropriate opcodes.

Finish overhauling debug logging

The logging should be significantly more useful, and the metadata is now
extensible to support arbitrary assertions.

I already added stack type assertions to the metadata, but these are
currently still implemented in terms of the old stack change check.

The improvements to metadata already improve the logs (because they
facilitate improved deserialization of immediate values in operands).

The next step is to assert the stack types and also log the (formatted)
stack parameters and return values in the log.

All of this facilitates quicker iteration on the in-progress error
recovery PR.

Cleanup

Tweak stack checks

chore(package.json): update devDependencies versions to latest

The devDependencies in package.json have been updated to their latest versions to ensure compatibility and take advantage of any bug fixes or new features. This includes updating various Babel plugins and presets, Rollup, ESLint, Prettier, Puppeteer, QUnit, rimraf, and TypeScript.

Update qunit

Update setup harness and add local types.

chore(vscode): update editor.codeActionsOnSave settings to prioritize source.fixAll over source.formatDocument
feat(vscode): add custom inline bookmark mapping for @fixme to highlight and style fixme comments in code
feat(vscode): add custom style for @fixme inline bookmarks to make them bold and display in red color

editor(vscode): Add commit message editor to vscode

refactor(debug): Make nulls and arrays in stack verification more accurate

Handle `null`s in the metadata more reliably, and add nullable arrays to the metadata.

chore: Clean up unused code

Also tweak code for better conformance to modern TypeScript.

infra(debugger): Improve and restore the debug tracing infra

- Make the debug infrastructure work again
- Have the "before" hook return the "after" hook so it can close
  over state it needs.
- Snapshot and formalize internal debug state.
- Properly handle errors in tracing so that log groups are properly
  closed.
- Improve the tracing output:
  - properly visualize the state of the element builder
  - properly visualize the block stack
  - improve visualization of the scope
- Streamline the interaction between the VM and debugger

The next commit uses all of these changes to significantly improve
stack verification and debugging metadata.

infra(debugger): Significant improvement to stack checks and trace logging

This commit overhauls opcode metadata:

Previously, many opcodes opted out of stack changes because their
changes were too dynamic (e.g. Pop reduces the stack size by a
parameter, which was previously too dynamic to be checked). This commit
enables dynamic stack checks, which are functions that take the runtime
op and debug VM state and dynamically determine the stack change.

This makes it possible to check the stack for every opcode except
`EnterList`, `Iterate`, `PrepareArgs` and `PopFrame` (the reasons for
these exceptions is documented in `opcode-metadata.ts`).

Dynamic checking is now used in:

- Concat (pops N from the stack based on an operand)
- Pop (same)
- BindDynamicScope (binds a number of names from an operand)

A previous commit enabled operand metadata. That infrastructure allows
the trace logger and compilation logger to print friendly versions of
opcodes.

This commit makes the metadata pervasively more accurate, piggy-backing
on the previous commit, which made nullable operands more accurate.

This deserialization process serves as a sort-of verification pass. If
an opcode is expecting an array, for example, and the operand
deserializes to a string, it will fail.

It currently fails confusingly (and not reliably) when the deserializer
fails to deserialize, but this can and should be improved in the future.

Enabling pervasive stack checks caused quite a few tests to fail.

For the most part, getting the tests to pass required improving the
accuracy of the opcode metadata.

style: Style tweaks

infra(opcodes): Generate opcode types and values

Previously, the source of truth for opcodes was an array literal and a
matching interface for that literal. All other types were generated
using TypeScript reflection.

However, the TypeScript reflection was fairly elaborate and slowed down
type feedback when working with the types.

This commit moves the source of truth to bin/opcodes.json and generates:

- @glimmer/interfaces/lib/generated/vm-opcodes.d.ts
- @glimmer/vm/lib/generated/opcodes.ts
- @glimmer/debug/lib/generated/op-list.ts

It adds a lint rule to avoid code casually importing directly from these
generated files, preferring import paths that re-export the generated
types and values.

The schema in `bin/opcodes/opcodes.schema.json` validates
`bin/opcodes.json`.

An npm script (`generate:opcodes`) updates all three files, but can also
be used to update a single file or review the generated files.

The generator script is written in TypeScript and this commit adds
`esyes` (https://github.com/starbeamjs/esyes) to generate the
`bin/opcodes.mts` file. (esyes requires node 21, but that doesn't
affect users of Glimmer, just contributors).

style: Style changes

refactor(typescript): Replace ContentType and CurriedType enums

There are better, more ergnomic, and more broadly supported ways to
handle enum-like patterns in TypeScript now.

TL;DR This commit replaces enums with literal types and type unions.

feat(error-boundary): Basic stack error recovery works

This commit also calls the specified handler asynchronously when an
error occurs.

refactor(opcodes): Nail down opcode behavior

Refactors the opcode metadata to be much more precise about how the
opcodes behave.

This is in service of properly accounting for possible throwing behavior
in opcodes.

refactor(tests): Prepare for error recovery tests

The plan is to put a no-op {{#-try}} around everything.

refactor(tests): Further trim down render delegate

To maximize the applicability of the no-op error recovery tests.

refactor(tests): Further trim down render delegate

Render delegates now only need to supply two pieces of DOM information:

- the document
- the initial element

Everything else is derived in the singular `RenderTest` class.

chore(checkpoint): Tests pass

fix(tests): Support else blocks in glimmer tests

Previously, the test suite skipped Glimmer-style templates for any tests
that yielded to `else` or `inverse`.

In general, the test suite uses a matrix test style to describe
templates and invocations abstractly (a "blueprint"), and then compiles
the blueprint into each of the styles.

It's possible to compile `else` blocks in Glimmer:

```hbs
<TestComponent>
  <:default as |block params|>

  </:default>
  <:else>

  </:else>
</TestComponent>
```

This is not purely pedantic: the current test suite fails to test that
`yield to="inverse"` actually yields to `<:else>` blocks and this fix
therefore improves coverage of the public API.

refactor(tests): Separate invoke & template styles

The test harness previously didn't provide a way to test attributes
passed to classic components.

This commit makes it possible to use the Glimmer invocation style, which
supports attributes, alongside templates implemented using classic
features.

Example:

```ts
@test({ invokeAs: 'glimmer' })
'with ariaRole specified as an outer binding'() {
  this.render.template(
    {
      layout: 'Here!',
      attributes: {
        id: '"aria-test"',
        class: '"foo"',
        role: 'this.ariaRole'
      },
    },
    { ariaRole: 'main' }
  );

  this.assertComponent('Here!', {
    id: 'aria-test',
    class: classes('ember-view foo'),
    role: 'main',
  });
  this.assertStableRerender();
}
```

This is a test that uses a classic-style component manager and
implementation strategy but uses a Glimmer invocation style. This allows
us to test that attributes passed to classic components are preserved.

test(error-recovery): Add error recovery suites

This commit makes it possible to take any existing test suite and add
tests for no-op error recovery.

This works by wrapping each template with `{{#-try this.handleError}}`
and adding minor infrastructure to merge the wrapping properties with
the original properties.

This commit adds error recovery testing to initial render tests, but
more tests will be added in future commits.

The implementation assumes that templates all funnel through
`this.renderTemplate` in the test delegate, but this may not be
accurate, especially in rehydration tests.

chore(eslint): Turn on stricter testing lints

Turn on (largely) the full gamut of lints we use in the codebase across
the workspace packages, **including in integration tests**.

Also restrict test packages in `packages/@glimmer-workspace` from
directly importing their parent package via relative imports.

feat(error-boundary): DOM clearing works

This commit finally gets to the meat of the matter.

This (passing) test is a good way to see where we're at.

```ts
@render
'error boundaries can happen in nested context'() {
  const actions = new Actions();

  class Woops {
    get woops() {
      actions.record('get woops');
      throw Error('woops');
    }
  }

  this.render.template(
    stripTight`
      <p>
        {{this.outer.before}}|
        {{#-try this.handler}}
          {{this.inner.before}}
          |message: [{{this.woops.woops}}]|
          {{this.inner.after}}
        {{/-try}}
        |{{this.outer.after}}
      </p>
    `,
    {
      woops: new Woops(),
      outer: {
        before: 'outer:before',
        after: 'outer:after',
      },
      inner: {
        before: 'inner:before',
        after: 'inner:after',
      },
      handler: (_err: unknown, _retry: () => void) => {
        actions.record('error handled');
      },
    }
  );

  actions.expect(['get woops', 'error handled']);
  this.assertHTML('<p>outer:before||outer:after</p>');
}
```

TL;DR when the error is handled, the surrounding try block is cleared,
and execution resumes after the try block.

If the error is not handled, the DOM is still cleared, but the error is
rethrown at the top-level once evaluation is complete.

Next steps:

- Make sure that other VM state is cleared properly (especially the
  block tracking state).
- Handle all errors that occur in user code. The current code handles
  errors thrown while appending content, but there are more cases. The
  good news is that the infrastructure for handling and propagating
  errors should Just Work for the remaining cases.
- Do a consistent job of making sure that errors that occur in the
  updating VM are handled properly. For example, if an error occurs in
  the updating part of an append opcode, that should consistently clear
  back to the nearest `#-try` block. At the moment, those blocks are not
  represented in the updating VM.

There's more work to do, but the crux of the conceptual and
prototyping work needed to support error boundaries is now done.

refactor(references): Bake errors in more deeply

This commit significantly overhauls the reference architecture to bake
errors in at a deeper level.

After this commit, references have an additional `error` field that can
be set to indicate that a reference is in the error state.

The internal reference API was overhauled to explicitly support error
cases, most notably via a new `readReactive` function, which returns a
`Result<T, UserException>`. If the reference is in the error state,
`readReactive` returns an `Err`.

There is also an `unwrapReactive` function, which throws an exception if
the the reference is in the error state. This replaces the `valueForRef`
function.

This commit adds new primitives and updates the terminology used in the
reactive API to reflect the error recovery changes (and also to align
with Starbeam's terminology).

- `MutableCell`: a new internal primitive that reflects a single piece
  of mutable data.
- `ReadonlyCell`: a new internal primitive that reflects a single piece
  of immutable data.
- `DeeplyConstant`: replaces the `unbound` collection of APIs. Where
  `ReadonlyCell` is shallowly immutable, `DeeplyConstant` values
  produce `DeeplyConstant` property reactives.
- `FallibleFormula`: A fallible formula is a read-only function that
  can throw errors. If the compute function throws an error, the
  `FallibleFormula` will be in an error state.
- `InfallibleFormula`: An infallible formula is a read-only function
  that cannot throw errors. If the compute function throws an error,
  the `InfallibleFormula` will be in an error state.
- `Accessor`: A read-write formula is an `InfallibleFormula` on the read
  side and also has an `update` side.

At the moment, virtually all uses of `valueForRef` have been
updated to use `unwrapReactive`. This is an acceptable (but not ideal)
transition path inside of combinators (such as `concatReference`)
because they can use `FallibleFormula` to handle the error.

The VM, on the other hand, must not use `unwrapReactive` directly (and
instead must use the previously committed `vm.deref` and `vm.unwrap`
methods to handle errors in a VM-aware way).

Ultimately, combinators should also not use `unwrapReactive`, to avoid
needing so many `try/catch`es, but that can come after the VM usages
have been updated.

refactor(test-infra): Restructure matrix tests

This commit introduces a new way to write matrix integration tests that
doesn't rely on JS inheritance. This makes it easier to mix-and-match
different parts of the tests and also makes it easier to add error
recovery tests.

feat(error-recovery): Recover more VM getErrorMessage

And write tests.

feat(error): Better rationalize unwindable state

feat(errors): Added checkpointing to `Stack`

The VM uses the `Stack` class (and friends) to manage its internal
state. This commit adds checkpointing to `Stack` so that the VM can
easily roll back to a point before a try frame began.

feat(errors): Closing in!

There are a few remaining open issues, but this commit gets us really
close to the finish line.

feat(errors): Test and clarify references

This commit cleans up and tests the behavior of references in the error
state. Documentation for the semantics is forthcoming.

feat(references): More fully test ref API

Also improve the labelling infrastructure to be more useful for
debugging tools such as the trace logger.

feat(debug): Better descriptions/devmode values

Improve the overall infrastructure of debug labels, and introduce a
minifier-friendly approach to dev-mode values that captures the notion
of values that should *always* be present in development but never in
production.

feat(errors): Support error recovery

This commit makes it possible for a code to reset the error state in a
reference in order to attempt error recovery.

When a reference's error is reset, its tag is invalidated, which will
result in consumers of that reference recomputing its value.

chore(imports): Strengthen and apply import sort rules

feat(errors): Get closer to final naming

docs(reactive): Document the new reactivity APIs

feat(errors): Make recover() work

feat(errors): Finish error recovery + tests

refactor(vm): Refactor and document VM

This commit removes gratuitous differences between different modes in an
attempt to clarify how the system works.

It also begins some documentation on how blocks and frames work.

feat(errors): Handle errors in more cases

This commit migrates more cases from unwrapReactive to readReactive.

Ideally we'd have explicit tests for each `readReactive` case, but there
are already some blanket error recovery tests and I'm moving forward
with these changes as long as all of the existing tests and blanket
error tests pass.

Workspaces need a name, especially nested ones

Krausest benchmark needs a lint config to properly configure sourceType

Delete dom-change-list

Cherry-pick files from 'master' branch

deps

packages

format

lint sync

reduce diff

eh

eh

reduceLock

eh

eh

Tell vite to use our custom meta.env variable

woo
  • Loading branch information
wycats authored and NullVoxPopuli committed Nov 22, 2023
1 parent e712ae3 commit c6f163d
Show file tree
Hide file tree
Showing 357 changed files with 22,772 additions and 15,383 deletions.
52 changes: 26 additions & 26 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,32 @@ module.exports = {
'**/fixtures',
'!**/.eslintrc.cjs',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
project: [],
},
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.js', '.cjs', '.mjs', '.mts', '.ts', '.d.ts'],
},
'import/resolver': {
typescript: {},
},
node: {
allowModules: ['@glimmer/debug', '@glimmer/local-debug-flags'],
tryExtensions: ['.js', '.ts', '.d.ts', '.json'],
},
},
plugins: [
'@typescript-eslint',
'prettier',
'qunit',
'simple-import-sort',
'unused-imports',
'prettier',
'n',
],
// parser: '@typescript-eslint/parser',
// parserOptions: {
// ecmaVersion: 'latest',
// project: [],
// },
// settings: {
// 'import/parsers': {
// '@typescript-eslint/parser': ['.js', '.cjs', '.mjs', '.mts', '.ts', '.d.ts'],
// },
// 'import/resolver': {
// typescript: {},
// },
// node: {
// allowModules: ['@glimmer/debug', '@glimmer/local-debug-flags'],
// tryExtensions: ['.js', '.ts', '.d.ts', '.json'],
// },
// },
// plugins: [
// '@typescript-eslint',
// 'prettier',
// 'qunit',
// 'simple-import-sort',
// 'unused-imports',
// 'prettier',
// 'n',
// ],

rules: {},
overrides: [
Expand Down
13 changes: 13 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
- [ ] Implement `#try` that takes handler
- [ ] internal `{{#if isClear}}{{!-- actual code --}}{{/if}}`
- [ ] if an error is encountered, unwind as-if isClear was false
during the render pass
- [ ] when you encounter the enter of the try, insert a marker in
every VM stack.
- [ ] every stack in the VM needs "unwind to nearest marker"
- [ ] when a render error is encountered, unwind all the stacks
- [ ] call the handler with the error
- [ ] no catch
- [ ] the handler has a way to clear the error
- [ ] deal with user destructors that should run even during render errors
- [ ] maintain the invariant that constructors and destructors are paired
4 changes: 2 additions & 2 deletions benchmark/benchmarks/krausest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
},
"dependencies": {
"@glimmer-workspace/benchmark-env": "workspace:^",
"@glimmer/compiler": "workspace:^",
"@glimmer/interfaces": "workspace:^",
"@simple-dom/document": "^1.4.0",
"@simple-dom/serializer": "^1.4.0",
"@simple-dom/void-map": "^1.4.0"
"@simple-dom/void-map": "^1.4.0",
"@glimmer/compiler": "workspace:^"
},
"devDependencies": {
"@types/node": "^20.9.4",
Expand Down
2 changes: 0 additions & 2 deletions bin/opcodes.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { dirname, resolve } from 'node:path';
import chalk from 'chalk';
import { execSync, spawnSync } from 'node:child_process';
import { Emitter } from './opcodes/utils.mjs';

const emitter = Emitter.argv('opcodes.json', import.meta);
Expand Down
4 changes: 2 additions & 2 deletions bin/opcodes/utils.mts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, relative, resolve } from 'node:path';
import chalk from 'chalk';
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, relative, resolve } from 'node:path';

const MISSING_INDEX = -1;
const REMOVE = 1;
Expand Down
12 changes: 6 additions & 6 deletions bin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
"test:types": "tsc --noEmit -p ./tsconfig.json"
},
"dependencies": {
"@types/glob": "^8.1.0",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.9.4",
"@types/puppeteer-chromium-resolver": "workspace:^",
"puppeteer-chromium-resolver": "^20.0.0",
"chalk": "^5.2.0",
"execa": "^7.1.1",
"glob": "^10.2.3",
"js-yaml": "^4.1.0",
"puppeteer-chromium-resolver": "^20.0.0"
"glob": "^10.2.3",
"@types/glob": "^8.1.0",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.9.4",
"@types/puppeteer-chromium-resolver": "workspace:^"
},
"devDependencies": {
"eslint": "^8.54.0",
Expand Down
2 changes: 1 addition & 1 deletion bin/update-package-json.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { resolve } from 'node:path';

import chalk from 'chalk';

import { type Package, packages } from './packages.mjs';
import { packages, type Package } from './packages.mjs';

const ROLLUP_CONFIG = [
`import { Package } from '@glimmer-workspace/build-support'`,
Expand Down
5 changes: 5 additions & 0 deletions guides/building-glimmer/01-introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Table of Contents

1. [Introduction](./01-introduction.md)
2. [Minification Assumptions](./02-minification.md)
3. [Dev Mode Patterns](./03-devmode-patterns.md)
14 changes: 14 additions & 0 deletions guides/building-glimmer/02-minification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Minification Assumptions

> 🚧 https://terser.org/docs/options/
- `import.meta.env.DEV`
- `keep_fargs: false`

## Issues

### Works better in terser than swc

- https://tiny.katz.zone/OfydPB
- https://tiny.katz.zone/XlJrqp
- https://tiny.katz.zone/6R5VdC (weird behavior involving new)
3 changes: 3 additions & 0 deletions guides/building-glimmer/03-devmode-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Dev Mode Patterns

## Case Study: `DevMode<Description>`
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@
4. [Resolver Delegate](./04-compile-time-resolver-delegate.md)
5. [Handles](./05-handles.md)
6. [Template Compilation](./06-templates.md)
7. [Frames and Blocks](./07-frames-and-blocks.md)
8. [References](./08-references.md)
9. [Error Recovery](./09-error-recovery/index.md)
10. [Trace Logging](./10-trace-logging.md)
55 changes: 55 additions & 0 deletions guides/internals/07-frames-and-blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Frames and Blocks

Initial:

```clj
[
(PushFrame)
[
(ReturnTo END)
(Push ...Args)
(Enter)
[
(Assertion) ; (JumpUnless) | (AssertSame)
...body
]
(Exit)
(Return)
; END
]
(PopFrame)
]
```

Update:

```clj
[
; restore state
(ReturnTo -1)
(PushArgs ...Captured)
(ReEnter)
; start evaluation here
[
(Assertion)
; (JumpUnless) | (AssertSame)
...body
]
(Exit)
(Return)
]
```

1. Initial
1. PushFrame
2. ReturnTo
3. Push captured args
4. Enter (optionally try frame)
5. Assertion
a. `JumpUnless` -> target
b. `AssertSame`
6. (body)
7. Exit
8. Return
9. PopFrame
2. Update (from 1.5)
139 changes: 139 additions & 0 deletions guides/internals/07-references.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Reactivity APIs

## Generic APIs

## `readReactive`

| Takes | Returns |
| ------------------ | ----------------------------- |
| 🟢🟡 Fallible Read | 🟡 Result _(throws ⚪ Never)_ |

The `readReactive` function takes a reactive value and returns a `ReactiveResult`.

## `unwrapReactive`

The `unwrapReactive` function takes a `ReactiveResult` and returns its value, throwing an exception
if the reactive produces an error (or was previously an error).

| Takes | Returns |
| ------------------ | ------------------------------------ |
| 🟢🟡 Fallible Read | 🟢 Value _(throws 🔴 UserException)_ |

## `updateReactive`

The `updateReactive` function takes a _mutable_ reactive and a new value and updates it. You cannot
pass an immutable reactive to this function.

| Takes | Updates |
| ---------- | ------------------------ |
| 📝 Mutable | 🟢 Value _(or 🔴 Error)_ |

## Cell APIs

### Constructors

```ts
function Cell(value: T): MutableCell<T>;
function ReadonlyCell(value: T): ReadonlyCell<T>;

type Cell<T> = MutableCell<T> | ReadonlyCell<T>;
```

| Type | Read | Write |
| ----------------- | -------- | ------------ |
| `Cell<T>` | 🟢 Value | 📝 Mutable |
| `ReadonlyCell<T>` | 🟢 Value | 🚫 Immutable |

### `readCell`

```ts
export function readCell<T>(cell: Cell<T>): T;
```

The `readCell` function takes a cell and returns its value. Since cells are infallible, you can use
this function to read from a cell without risking an exception (as with `unwrapReactive`).

### `writeCell`

```ts
export function writeCell<T>(cell: MutableCell<T>, value: T): void;
```

| Takes | Updates |
| ---------------- | -------- |
| 📝 Mutable Write | 🟢 Value |

The `writeCell` function writes a new value to a mutable cell. You can't write to a readonly cell.

## Formula APIs

| Type | Read | Write |
| ------------- | --------- | ------------ |
| `Accessor<T>` | 🟡 Result | 📝 Mutable |
| `Formula<T>` | 🟡 Result | 🚫 Immutable |

### Constructors

```ts
export function Formula<T>(compute: () => T): Formula<T>;
export function Accessor<T>(options: { get: () => T; set: (value: T) => void }): Accessor<T>;
```

If an accessor's `set` throws an error, the reactive value will become an error.

## External Markers

External markers are not reactive values themselves. Instead, they _stand in_ for external storage.

Here's an example using a `SimpleMap` class that uses a `Map` as its backing storage:

```ts
class SimpleMap<K, V> {
#markers = new Map<K, ExternalMarker>();
#values = new Map<K, V>();

get(key: K): V {
this.#initialized(key).consumed();
return this.#values.get(key);
}

has(key: K) {
this.#initialized(key).consumed();
return this.#values.has(key);
}

set(key: K, value: V) {
this.#initialized(key).updated();
this.#values.set(key, value);
}

#initialized(key: K) {
let marker = this.#markers.get(key);

if (!marker) {
marker = ExternalMarker();
this.#markers.set(key, marker);
}

return marker;
}
}
```

Now, reads from `has(key)` or `get(key)` will be invalidated whenever `set(key, value)` is called on
the same key.

The crux of the situation is that we don't want to store every value in a Cell and turn every
computation into a `Formula`. Instead, we want to store our data in normal JavaScript data
structures and notify the reactivity system whenever the data is accessed or modified.

## Internal Reactives

### `ComputedCell`

A `ComputedCell` behaves like a cell, but it uses a `compute` function to compute a new value. The
`compute` function must be infallible, and there is no error recovery if it fails.

| Type | Read | Write |
| ----------------- | -------- | ------------ |
| `ComputedCell<T>` | 🟢 Value | 🚫 Immutable |
Loading

0 comments on commit c6f163d

Please sign in to comment.