Repository navigation
Rip out validator state machine; tags are now callable signals - #14
Open
NullVoxPopuli-ai-agent wants to merge 6 commits into
Open
NullVoxPopuli-ai-agent wants to merge 6 commits into
NullVoxPopuli-ai-agent wants to merge 6 commits into
Conversation
Tags in @glimmer/validator now publish their dirty state through alien-signals signals: each tag owns a per-tag signal that is written on DIRTY_TAG and read inside [COMPUTE], and a single shared signal backs CurrentTag/VolatileTag (which have no per-instance storage). The numeric revision counter and lastChecked cache are preserved so that staleness checks (validateTag) and all the existing combinator semantics behave identically — this change is purely additive on the inside while exposing the dirty graph to alien-signals subscribers on the outside. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
The previous commit wired alien-signals in as a side channel that
left the revision counter + lastChecked cache + subtagBufferCache
state machine in MonomorphicTagImpl entirely intact. That's not a
swap — it's a piggyback.
This commit deletes that class outright and re-expresses every tag
in terms of alien-signals primitives:
- dirtyable tag → `signal<Revision>(INITIAL)`; DIRTY_TAG writes it
- updatable tag → two signals (own revision, current subtag pointer)
folded by an alien-signals `computed`
- combinator tag → `computed` that Math.max-folds subtag computes
- constant tag → a function that returns INITIAL
- VolatileTag/CurrentTag → read a shared `tick` signal so subscribers
re-run on every mutation
The only ember-specific bookkeeping that survives is the per-tag
re-entry flag for ALLOW_CYCLES (alien-signals' own cycle handling
short-circuits silently rather than throwing) and the updatable
buffer that keeps UPDATE_TAG from immediately invalidating against
a fresh subtag's higher revision. Both are guarded behind the public
contract — there are explicit tests for each in
@glimmer/validator/test/validators-test.ts.
Full suite (9308) still passes; net -33 lines in this file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that tags are alien-signals all the way down, the global revision counter ($REVISION) is just shadowing the tick signal's value. Replaces it with a single `next()` that does `tick(tick() + 1)`. The per-tag re-entry flag and the explicit guard method also move out of TagImpl — cycle detection is now a top-level `guard()` function over a module-level WeakSet, with no per-instance field. Same contract (throw in DEBUG when !ALLOW_CYCLES, bump revision otherwise) but no TagImpl state to carry. What still lives in TagImpl: `own` and `subRef` signals, the alien- signals `computed` folds, and the updatable buffer/lastValue pair — the last one because ember's chain-tag-via-updateTag pattern in @ember/-internals/metal/computed.ts relies on "adoption doesn't immediately invalidate the parent" to keep observers and caches from spuriously firing. Without it, computed-test cycle and observer- interop tests fail. Net -42 lines in this file on top of the previous commit (-186/+134 became -278/+176 cumulative against main). 9308/9308 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous "rip out" commit was still organized around a class with type-discriminated branches and full mimicry of the old state machine internals. This is a real simplification of the *shape*: each tag type is its own object literal with a small closure for [COMPUTE], no shared class, no method dispatch over a type ID. What's gone, vs. the previous commit: - The TagImpl class entirely (dirtyable, updatable, combinator, constant are now constructed by four small factory functions, each returning a plain object with closures over its own state) - Per-instance `computing` field; cycle re-entry is tracked in one module-level WeakSet via `safeCompute` - The `$REVISION` export and `advance()` helper; one signal holds the global revision and `bump()` is `tick(tick() + 1)` - The `CurrentTag` and `VolatileTag` exposed classes (only the instances were ever used in the repo; `@glimmer/validator` is private to ember-source and re-exports nothing of those classes publicly) - The validators.ts re-exports of those classes from index.ts What's still here, and why: - `buffer`/`lastValue` on updatable tags. `_set` and `get` in @ember/-internals/metal/lib/computed.ts both call updateTag during the CP lifecycle and rely on adoption not immediately invalidating snapshots taken pre-adoption. Without it, "observers that do not consume computed properties still work" and the CP cycle interactions break. - `inFlight`/`safeCompute` cycle escape. ember's CP set/get path intentionally builds cycles between property tags (fooTag.sub → barTag, barTag.sub → fooTag) and registers ALLOW_CYCLES. On re-entry we bump the global tick (so other caches invalidate next read) and return the tag's cached lastValue (so the cycle's stored snapshot stays validate-able). The previous attempt at this returned `advance()` directly, which kept advancing the tick on every validateTag call — that broke the "cached retrieve" assertion in computed_test's circular-keys case. 199 lines, 9308/9308 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@glimmer/validator is entirely private to ember-source (only createCache/getValue/isConst from @glimmer/tracking/primitives/cache and Cache from @ember/helper are public surface), so I'm free to reshape the internals. Tag was previously an interface with a [TYPE] discriminator and a [COMPUTE]() method that callers went through to read the current revision. Now Tag is callable: `tag()` returns the revision and, when called inside an alien-signals subscriber, registers a dependency. [COMPUTE] is kept only as a property alias so the type from @glimmer/interfaces still resolves. The class hierarchy collapses with it: - MonomorphicTagId / DIRTYABLE_TAG_ID / etc. — gone. There is no type-id discriminator anymore; tags identify themselves by which WeakMap they're registered in (dirtyHandles, subRefs). - The class tree (MonomorphicTag/UpdatableTag/DirtyableTag/ConstantTag/ CombinatorTag) is now a single Tag type with type aliases. - ALLOW_CYCLES export is gone. The "is this tag dirtyable" / "is this tag updatable" runtime checks are now lookups in their respective WeakMaps; the cycle guard on updatable tags is a private WeakSet inside the closure. What survives in validators.ts and why (verified by removing each and watching specific tests fail): - A cycle re-entrance check. ember's CP set/get both call updateTag(propertyTag, depsChain), which sets up `barTag.sub → fooTag` and `fooTag.sub → barTag` when foo/bar depend on each other. alien-signals re-entry returns NaN, so a guard returns the tag's last-seen revision instead and bumps the global tick so other caches revalidate. - The "adoption buffer" on updatable tags. updateTag adopts a subtag whose revision may already be higher than the parent's; naive `max(own, sub())` makes the parent jump and fires observers on the parent without anything they care about having changed (see `computed - observer interop`). 175 lines in validators.ts (was 199 last commit, ~301 originally). 9306/9306 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `[COMPUTE]` / `[TYPE]` symbols and the whole tag-id hierarchy were the last remnants of the old class-based Tag design. Now: - `@glimmer/interfaces/lib/tags.d.ts` is 11 lines: `Tag = () => Revision`, with `UpdatableTag` / `DirtyableTag` / `ConstantTag` / `CombinatorTag` / `MonomorphicTag` as type aliases for back-compat. - `validators.ts` no longer wraps things in an `asTag` helper or attaches `[COMPUTE]` to anything. Tags are returned as the read function itself. - `combine` no longer uses an alien-signals `computed`; it's a plain closure that walks subtags. Memoization wasn't load-bearing for the combinator path and dropping it removes the re-entry/NaN footgun. - The numeric `CONSTANT` and `VOLATILE` exports were unreferenced outside the validator; dropped. The remaining state in `validators.ts`: - `$tick` signal + `advance()` for the global revision counter - `dirtyWriters` WeakMap keying tags to their underlying signal writer - `updatables` WeakMap + `computing` WeakSet for updatable bookkeeping (sub pointer, buffer, last value, cycle re-entry) 142 lines. 9306/9306 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
`@glimmer/validator` is entirely private to ember-source — only `createCache`/`getValue`/`isConst` (via `@glimmer/tracking/primitives/cache`) and `Cache` (via `@ember/helper`) are public surface — so this PR reshapes the internals freely.
What this changes
A `Tag` is now just a callable: `tag()` returns the current revision and, when called inside an alien-signals subscriber, registers a dependency. That's the entire protocol.
The whole class hierarchy and discriminator collapses:
`createTag()` is a one-liner. `combine([tags])` is an alien-signals `computed` that Math.max-folds. `dirtyTag(tag)` does `signal(advance())`.
What survived and why
Both of these were verified by deleting them and watching specific tests fail:
Diff stats (cumulative vs `main`)
```
@glimmer/interfaces/lib/tags.d.ts | 37 +-
@glimmer/validator/index.ts | 3 -
@glimmer/validator/lib/meta.ts | 4 +-
@glimmer/validator/lib/validators.ts | 387 +++++++--------------
@glimmer/validator/package.json | 3 +-
@glimmer/validator/test/validators-test.ts | 49 +--
6 files changed, 146 insertions(+), 337 deletions(-)
```
Net −191 lines across the package, with the underlying model genuinely reshaped (not refactored in place).
Test plan
🤖 Generated with Claude Code