From 2f4f13324280fb8db8869aa54318fdc50e2f0165 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 6 Oct 2026 00:25:27 -0400 Subject: [PATCH 1/5] Replace @glimmer/validator with a reactive graph on alien-signals This is a spike. It removes the @glimmer/validator package. The new private package @glimmer/signals builds on alien-signals/system. - References and createCache are computed nodes of the graph. - Each cache group of the VM is a frame node. A render has a root frame. - A destroyed block or render removes its links from the tags. - A cache without a subscriber keeps a list of tags and a revision, not links. - updateTag uses a watcher node that forwards writes to the tag. - Classic computed properties and observers keep revision snapshots. Co-Authored-By: Claude Opus 5.5 (1M context) --- eslint.config.mjs | 2 +- lib/index.cjs | 2 +- package.json | 2 +- .../-internals/glimmer/lib/base-renderer.ts | 10 +- .../glimmer/lib/component-managers/curly.ts | 5 +- .../glimmer/lib/component-managers/root.ts | 3 +- .../-internals/glimmer/lib/component.ts | 2 +- .../glimmer/lib/components/input.ts | 2 +- .../glimmer/lib/components/internal.ts | 2 +- .../glimmer/lib/components/link-to.ts | 4 +- .../-internals/glimmer/lib/environment.ts | 2 +- .../@ember/-internals/glimmer/lib/helper.ts | 3 +- .../glimmer/lib/helpers/-track-array.ts | 2 +- .../-internals/glimmer/lib/helpers/each-in.ts | 2 +- .../lib/utils/curly-component-state-bucket.ts | 9 +- .../-internals/glimmer/lib/utils/iterator.ts | 4 +- .../-internals/glimmer/lib/utils/to-bool.ts | 2 +- .../integration/helpers/invoke-helper-test.js | 2 +- .../integration/helpers/unique-id-test.js | 2 +- packages/@ember/-internals/meta/lib/meta.ts | 2 +- packages/@ember/-internals/metal/lib/alias.ts | 9 +- packages/@ember/-internals/metal/lib/cache.ts | 2 +- .../@ember/-internals/metal/lib/cached.ts | 2 +- .../@ember/-internals/metal/lib/chain-tags.ts | 13 +- .../@ember/-internals/metal/lib/computed.ts | 20 +- .../@ember/-internals/metal/lib/observer.ts | 10 +- .../-internals/metal/lib/property_get.ts | 4 +- packages/@ember/-internals/metal/lib/tags.ts | 6 +- .../@ember/-internals/metal/lib/tracked.ts | 8 +- .../-internals/metal/tests/alias_test.js | 2 +- .../tests/tracked/classic_classes_test.js | 2 +- .../metal/tests/tracked/options_test.js | 2 +- .../metal/tests/tracked/standalone_test.js | 2 +- .../metal/tests/tracked/validation_test.js | 2 +- packages/@ember/-internals/package.json | 2 +- .../routing/route-managers/root-outlet.ts | 3 +- .../-internals/runtime/lib/mixins/-proxy.ts | 6 +- .../tests/system/array_proxy/length_test.js | 2 +- .../runtime/tests/system/core_object_test.js | 2 +- packages/@ember/application/package.json | 2 +- packages/@ember/array/package.json | 2 +- packages/@ember/array/proxy.ts | 6 +- packages/@ember/controller/package.json | 2 +- packages/@ember/debug/data-adapter.ts | 5 +- packages/@ember/debug/package.json | 2 +- packages/@ember/engine/package.json | 2 +- packages/@ember/enumerable/package.json | 2 +- packages/@ember/helper/package.json | 2 +- .../helper/type-tests/invoke-helper.test.ts | 2 +- packages/@ember/instrumentation/package.json | 2 +- packages/@ember/object/compat.ts | 5 +- packages/@ember/object/core.ts | 2 + packages/@ember/object/package.json | 2 +- packages/@ember/reactive/collections.ts | 12 +- packages/@ember/reactive/package.json | 2 +- packages/@ember/routing/package.json | 2 +- packages/@ember/routing/router-service.ts | 4 +- packages/@ember/runloop/package.json | 2 +- packages/@ember/service/package.json | 2 +- packages/@ember/utils/package.json | 2 +- .../lib/components/emberish-curly.ts | 4 +- .../integration-tests/lib/modifiers.ts | 6 +- .../integration-tests/lib/render-test.ts | 2 +- .../integration-tests/lib/setup-harness.ts | 2 +- .../integration-tests/lib/suites/each.ts | 2 +- .../lib/test-helpers/tracked-object.ts | 2 +- .../integration-tests/package.json | 2 +- .../test/collections/array-test.ts | 2 +- .../test/collections/map-test.ts | 2 +- .../test/collections/object-test.ts | 2 +- .../test/collections/set-test.ts | 2 +- .../test/collections/weak-map-test.ts | 2 +- .../test/collections/weak-set-test.ts | 2 +- .../test/graph-links-test.ts | 74 ++ .../integration-tests/test/package.json | 2 +- .../test/tracked-value-test.ts | 2 +- .../integration-tests/test/updating-test.ts | 2 +- .../@glimmer/interfaces/lib/references.d.ts | 4 +- packages/@glimmer/interfaces/lib/tags.d.ts | 60 +- .../@glimmer/manager/lib/public/modifier.ts | 5 +- .../@glimmer/manager/lib/util/args-proxy.ts | 2 +- packages/@glimmer/manager/package.json | 2 +- .../@glimmer/manager/test/managers-test.ts | 4 +- packages/@glimmer/manager/test/package.json | 2 +- packages/@glimmer/reference/lib/iterable.ts | 3 +- packages/@glimmer/reference/lib/reference.ts | 84 +- packages/@glimmer/reference/package.json | 2 +- .../@glimmer/reference/test/iterable-test.ts | 2 +- packages/@glimmer/reference/test/package.json | 2 +- .../reference/test/references-test.ts | 2 +- .../lib/compiled/opcodes/-debug-strip.ts | 3 +- .../runtime/lib/compiled/opcodes/dom.ts | 12 +- .../runtime/lib/compiled/opcodes/vm.ts | 43 +- packages/@glimmer/runtime/lib/environment.ts | 3 +- .../@glimmer/runtime/lib/helpers/invoke.ts | 3 +- packages/@glimmer/runtime/lib/modifiers/on.ts | 4 +- packages/@glimmer/runtime/lib/render.ts | 69 +- packages/@glimmer/runtime/lib/vm/append.ts | 11 +- packages/@glimmer/runtime/lib/vm/arguments.ts | 2 +- .../@glimmer/runtime/lib/vm/render-result.ts | 40 +- packages/@glimmer/runtime/lib/vm/update.ts | 4 +- packages/@glimmer/runtime/package.json | 2 +- .../{validator => signals}/.npmignore | 0 .../@glimmer/{validator => signals}/index.ts | 53 +- .../lib/collections/array.ts | 13 +- .../lib/collections/map.ts | 19 +- .../lib/collections/object.ts | 13 +- .../lib/collections/set.ts | 19 +- .../lib/collections/types.ts | 0 .../lib/collections/weak-map.ts | 9 +- .../lib/collections/weak-set.ts | 9 +- .../{validator => signals}/lib/debug.ts | 31 +- .../{validator => signals}/lib/meta.ts | 26 +- packages/@glimmer/signals/lib/tags.ts | 920 ++++++++++++++++++ .../lib/tracked-data.ts | 2 +- .../lib/tracked-value.ts | 11 +- .../{validator => signals}/lib/utils.ts | 0 .../{validator => signals}/package.json | 9 +- .../{validator => signals}/test/-utils.ts | 0 .../test/collections/array-test.ts | 4 +- .../test/collections/map-test.ts | 4 +- .../test/collections/object-test.ts | 4 +- .../test/collections/set-test.ts | 4 +- .../test/collections/weak-map-test.ts | 4 +- .../test/collections/weak-set-test.ts | 4 +- packages/@glimmer/signals/test/graph-test.ts | 368 +++++++ .../{validator => signals}/test/meta-test.ts | 4 +- .../{validator => signals}/test/package.json | 4 +- .../test/tracked-value-test.ts | 4 +- .../test/tracking-test.ts | 4 +- .../test/validators-test.ts | 120 +-- packages/@glimmer/tracking/index.ts | 6 +- packages/@glimmer/tracking/package.json | 2 +- .../@glimmer/tracking/primitives/cache.ts | 2 +- packages/@glimmer/validator/lib/tracking.ts | 397 -------- packages/@glimmer/validator/lib/validators.ts | 319 ------ packages/ember-template-compiler/package.json | 2 +- packages/ember-testing/package.json | 2 +- packages/ember/package.json | 2 +- .../lib/ember-dev/setup-qunit.ts | 2 +- packages/internal-test-helpers/package.json | 2 +- pnpm-lock.yaml | 250 ++--- rollup.config.mjs | 5 +- .../benchmark-app/app/services/cell.js | 4 +- smoke-tests/scenarios/basic-test.ts | 2 +- .../scenarios/stable-decorator-files.ts | 2 +- tests/node-vitest/tree-shakability.test.js | 4 +- 147 files changed, 1997 insertions(+), 1377 deletions(-) create mode 100644 packages/@glimmer-workspace/integration-tests/test/graph-links-test.ts rename packages/@glimmer/{validator => signals}/.npmignore (100%) rename packages/@glimmer/{validator => signals}/index.ts (52%) rename packages/@glimmer/{validator => signals}/lib/collections/array.ts (95%) rename packages/@glimmer/{validator => signals}/lib/collections/map.ts (88%) rename packages/@glimmer/{validator => signals}/lib/collections/object.ts (91%) rename packages/@glimmer/{validator => signals}/lib/collections/set.ts (86%) rename packages/@glimmer/{validator => signals}/lib/collections/types.ts (100%) rename packages/@glimmer/{validator => signals}/lib/collections/weak-map.ts (92%) rename packages/@glimmer/{validator => signals}/lib/collections/weak-set.ts (93%) rename packages/@glimmer/{validator => signals}/lib/debug.ts (91%) rename packages/@glimmer/{validator => signals}/lib/meta.ts (71%) create mode 100644 packages/@glimmer/signals/lib/tags.ts rename packages/@glimmer/{validator => signals}/lib/tracked-data.ts (96%) rename packages/@glimmer/{validator => signals}/lib/tracked-value.ts (93%) rename packages/@glimmer/{validator => signals}/lib/utils.ts (100%) rename packages/@glimmer/{validator => signals}/package.json (82%) rename packages/@glimmer/{validator => signals}/test/-utils.ts (100%) rename packages/@glimmer/{validator => signals}/test/collections/array-test.ts (98%) rename packages/@glimmer/{validator => signals}/test/collections/map-test.ts (97%) rename packages/@glimmer/{validator => signals}/test/collections/object-test.ts (92%) rename packages/@glimmer/{validator => signals}/test/collections/set-test.ts (98%) rename packages/@glimmer/{validator => signals}/test/collections/weak-map-test.ts (94%) rename packages/@glimmer/{validator => signals}/test/collections/weak-set-test.ts (94%) create mode 100644 packages/@glimmer/signals/test/graph-test.ts rename packages/@glimmer/{validator => signals}/test/meta-test.ts (89%) rename packages/@glimmer/{validator => signals}/test/package.json (76%) rename packages/@glimmer/{validator => signals}/test/tracked-value-test.ts (98%) rename packages/@glimmer/{validator => signals}/test/tracking-test.ts (99%) rename packages/@glimmer/{validator => signals}/test/validators-test.ts (65%) delete mode 100644 packages/@glimmer/validator/lib/tracking.ts delete mode 100644 packages/@glimmer/validator/lib/validators.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index 4969204fcc5..3e16491af86 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -450,7 +450,7 @@ export default [ }, }, { - files: ['packages/@glimmer/validator/**/*.ts'], + files: ['packages/@glimmer/signals/**/*.ts'], rules: { '@typescript-eslint/no-deprecated': 'off', '@typescript-eslint/no-unnecessary-condition': 'off', diff --git a/lib/index.cjs b/lib/index.cjs index 1aab0ad0389..2ccc8caedc0 100644 --- a/lib/index.cjs +++ b/lib/index.cjs @@ -147,7 +147,7 @@ const shim = addonV1Shim(path.join(__dirname, '..'), { './dist/dev/packages/@glimmer/tracking/index.js', './dist/dev/packages/@glimmer/tracking/primitives/cache/index.js', './dist/dev/packages/@glimmer/util/index.js', - './dist/dev/packages/@glimmer/validator/index.js', + './dist/dev/packages/@glimmer/signals/index.js', './dist/dev/packages/@glimmer/vm/index.js', './dist/dev/packages/@glimmer/wire-format/index.js', './dist/dev/packages/@simple-dom/document/index.js', diff --git a/package.json b/package.json index ee5d718b1b7..dcb1d159257 100644 --- a/package.json +++ b/package.json @@ -366,10 +366,10 @@ "@glimmer/program/index.js": "ember-source/@glimmer/program/index.js", "@glimmer/reference/index.js": "ember-source/@glimmer/reference/index.js", "@glimmer/runtime/index.js": "ember-source/@glimmer/runtime/index.js", + "@glimmer/signals/index.js": "ember-source/@glimmer/signals/index.js", "@glimmer/tracking/index.js": "ember-source/@glimmer/tracking/index.js", "@glimmer/tracking/primitives/cache/index.js": "ember-source/@glimmer/tracking/primitives/cache/index.js", "@glimmer/util/index.js": "ember-source/@glimmer/util/index.js", - "@glimmer/validator/index.js": "ember-source/@glimmer/validator/index.js", "@glimmer/vm/index.js": "ember-source/@glimmer/vm/index.js", "@glimmer/wire-format/index.js": "ember-source/@glimmer/wire-format/index.js", "@simple-dom/document/index.js": "ember-source/@simple-dom/document/index.js", diff --git a/packages/@ember/-internals/glimmer/lib/base-renderer.ts b/packages/@ember/-internals/glimmer/lib/base-renderer.ts index 5776f3831ed..2e684dae778 100644 --- a/packages/@ember/-internals/glimmer/lib/base-renderer.ts +++ b/packages/@ember/-internals/glimmer/lib/base-renderer.ts @@ -25,7 +25,7 @@ import { RuntimeOpImpl } from '@glimmer/program/lib/opcode'; import { clientBuilder } from '@glimmer/runtime/lib/vm/element-builder'; import { inTransaction, runtimeOptions } from '@glimmer/runtime/lib/environment'; import { renderComponent as glimmerRenderComponent } from '@glimmer/runtime/lib/render'; -import { CURRENT_TAG, validateTag, valueForTag } from '@glimmer/validator/lib/validators'; +import { currentRevision } from '@glimmer/signals/lib/tags'; import type { SimpleDocument, SimpleElement } from '@simple-dom/interface'; import { hasDOM } from '../../browser-environment'; import { EmberEnvironmentDelegate } from './environment'; @@ -295,7 +295,7 @@ export class RendererState { * and that run loop throws the same error again. */ if (!completedWithoutError) { - this.#lastRevision = valueForTag(CURRENT_TAG); + this.#lastRevision = currentRevision(); } this.#inRenderTransaction = false; } @@ -342,7 +342,7 @@ export class RendererState { root.render(); } - this.#lastRevision = valueForTag(CURRENT_TAG); + this.#lastRevision = currentRevision(); }); } while (roots.length > initialRootsLength); @@ -386,12 +386,12 @@ export class RendererState { * True when there is nothing to render: * no roots, or no tracked value changed after the last render. * - * `CURRENT_TAG` stands for every tag, + * `currentRevision()` changes with a write to any tag, * so one changed value anywhere in the app makes every renderer invalid. * Each root then finds out for itself if the change applies to it. */ isValid(): boolean { - return this.#roots.length === 0 || validateTag(CURRENT_TAG, this.#lastRevision); + return this.#roots.length === 0 || this.#lastRevision === currentRevision(); } revalidate(): void { diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts index c7e1027ca35..c3031f8a257 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts @@ -46,8 +46,9 @@ import { consumeTag, endTrackFrame, endUntrackFrame, -} from '@glimmer/validator/lib/tracking'; -import { validateTag, valueForTag } from '@glimmer/validator/lib/validators'; + validateTag, + valueForTag, +} from '@glimmer/signals/lib/tags'; import type Component from '../component'; import type { DynamicScope } from '../renderer'; import type RuntimeResolver from '../resolver'; diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/root.ts b/packages/@ember/-internals/glimmer/lib/component-managers/root.ts index abcb5383a05..4e1a0d14636 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/root.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/root.ts @@ -11,8 +11,7 @@ import type { } from '@glimmer/interfaces'; import type { Nullable } from '@ember/-internals/utility-types'; import { capabilityFlagsFrom } from '@glimmer/manager/lib/util/capabilities'; -import { CONSTANT_TAG } from '@glimmer/validator/lib/validators'; -import { consumeTag } from '@glimmer/validator/lib/tracking'; +import { CONSTANT_TAG, consumeTag } from '@glimmer/signals/lib/tags'; import type Component from '../component'; import type { DynamicScope } from '../renderer'; import ComponentStateBucket from '../utils/curly-component-state-bucket'; diff --git a/packages/@ember/-internals/glimmer/lib/component.ts b/packages/@ember/-internals/glimmer/lib/component.ts index 5c0735cd780..5d4d3865c62 100644 --- a/packages/@ember/-internals/glimmer/lib/component.ts +++ b/packages/@ember/-internals/glimmer/lib/component.ts @@ -22,7 +22,7 @@ import { setInternalComponentManager } from '@glimmer/manager/lib/internal/api'; import { isUpdatableRef, updateRef } from '@glimmer/reference/lib/reference'; import { normalizeProperty } from '@glimmer/runtime/lib/dom/props'; import type { DirtyableTag } from '@glimmer/interfaces'; -import { createTag, DIRTY_TAG as dirtyTag } from '@glimmer/validator/lib/validators'; +import { createTag, dirtyTag } from '@glimmer/signals/lib/tags'; import type { SimpleElement } from '@simple-dom/interface'; import { BOUNDS, diff --git a/packages/@ember/-internals/glimmer/lib/components/input.ts b/packages/@ember/-internals/glimmer/lib/components/input.ts index 0b029328148..6c0cdbb975f 100644 --- a/packages/@ember/-internals/glimmer/lib/components/input.ts +++ b/packages/@ember/-internals/glimmer/lib/components/input.ts @@ -6,7 +6,7 @@ import { type Opaque } from '@ember/-internals/utility-types'; import { assert, warn } from '@ember/debug'; import { action } from '@ember/object'; import { valueForRef } from '@glimmer/reference/lib/reference'; -import { untrack } from '@glimmer/validator/lib/tracking'; +import { untrack } from '@glimmer/signals/lib/tags'; import InputTemplate from '../templates/input'; import AbstractInput, { valueFrom } from './abstract-input'; import { type OpaqueInternalComponentConstructor, opaquify } from './internal'; diff --git a/packages/@ember/-internals/glimmer/lib/components/internal.ts b/packages/@ember/-internals/glimmer/lib/components/internal.ts index cd5571b35dc..df0e0aa7d82 100644 --- a/packages/@ember/-internals/glimmer/lib/components/internal.ts +++ b/packages/@ember/-internals/glimmer/lib/components/internal.ts @@ -17,7 +17,7 @@ import { setComponentTemplate } from '@glimmer/manager/lib/public/template'; import { setInternalComponentManager } from '@glimmer/manager/lib/internal/api'; import type { Reference } from '@glimmer/reference/lib/reference'; import { createConstRef, isConstRef, valueForRef } from '@glimmer/reference/lib/reference'; -import { untrack } from '@glimmer/validator/lib/tracking'; +import { untrack } from '@glimmer/signals/lib/tags'; function NOOP(): void {} diff --git a/packages/@ember/-internals/glimmer/lib/components/link-to.ts b/packages/@ember/-internals/glimmer/lib/components/link-to.ts index d47ddd8b7ea..30f7a02f102 100644 --- a/packages/@ember/-internals/glimmer/lib/components/link-to.ts +++ b/packages/@ember/-internals/glimmer/lib/components/link-to.ts @@ -11,8 +11,8 @@ import { service } from '@ember/service'; import { DEBUG } from '@glimmer/env'; import type { Maybe } from '@glimmer/interfaces'; import type { Nullable } from '@ember/-internals/utility-types'; -import { consumeTag, createCache, getValue, untrack } from '@glimmer/validator/lib/tracking'; -import { tagFor } from '@glimmer/validator/lib/meta'; +import { consumeTag, createCache, getValue, untrack } from '@glimmer/signals/lib/tags'; +import { tagFor } from '@glimmer/signals/lib/meta'; import type { Transition } from 'router_js'; import LinkToTemplate from '../templates/link-to'; import InternalComponent, { type OpaqueInternalComponentConstructor, opaquify } from './internal'; diff --git a/packages/@ember/-internals/glimmer/lib/environment.ts b/packages/@ember/-internals/glimmer/lib/environment.ts index fc2d99f4f4d..b96329b21a2 100644 --- a/packages/@ember/-internals/glimmer/lib/environment.ts +++ b/packages/@ember/-internals/glimmer/lib/environment.ts @@ -10,7 +10,7 @@ import { schedule, _backburner } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; import setGlobalContext from '@glimmer/global-context'; import type { EnvironmentDelegate } from '@glimmer/runtime/lib/environment'; -import { debug } from '@glimmer/validator/lib/debug'; +import { debug } from '@glimmer/signals/lib/debug'; import toIterator from './utils/iterator'; import { isHTMLSafe } from './utils/string'; import toBool from './utils/to-bool'; diff --git a/packages/@ember/-internals/glimmer/lib/helper.ts b/packages/@ember/-internals/glimmer/lib/helper.ts index 81e59cca7e0..af82aecf1dc 100644 --- a/packages/@ember/-internals/glimmer/lib/helper.ts +++ b/packages/@ember/-internals/glimmer/lib/helper.ts @@ -13,8 +13,7 @@ import type { Arguments, HelperManager } from '@glimmer/interfaces'; import { helperCapabilities } from '@glimmer/manager/lib/public/helper'; import { setHelperManager } from '@glimmer/manager/lib/public/api'; import type { DirtyableTag } from '@glimmer/interfaces'; -import { consumeTag } from '@glimmer/validator/lib/tracking'; -import { createTag, DIRTY_TAG as dirtyTag } from '@glimmer/validator/lib/validators'; +import { consumeTag, createTag, dirtyTag } from '@glimmer/signals/lib/tags'; import { IS_CLASSIC_HELPER } from './helper-brand'; export { isClassicHelper } from './helper-brand'; diff --git a/packages/@ember/-internals/glimmer/lib/helpers/-track-array.ts b/packages/@ember/-internals/glimmer/lib/helpers/-track-array.ts index 607015a4a55..da33ffe696a 100644 --- a/packages/@ember/-internals/glimmer/lib/helpers/-track-array.ts +++ b/packages/@ember/-internals/glimmer/lib/helpers/-track-array.ts @@ -6,7 +6,7 @@ import { isObject } from '@ember/-internals/utils/lib/spec'; import { assert } from '@ember/debug'; import type { CapturedArguments } from '@glimmer/interfaces'; import { createComputeRef, valueForRef } from '@glimmer/reference/lib/reference'; -import { consumeTag } from '@glimmer/validator/lib/tracking'; +import { consumeTag } from '@glimmer/signals/lib/tags'; import { internalHelper } from './internal-helper'; /** diff --git a/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts b/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts index 745442038d9..667bf1b1e6e 100644 --- a/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts +++ b/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts @@ -7,7 +7,7 @@ import { isProxy } from '@ember/-internals/utils/lib/is_proxy'; import { assert } from '@ember/debug'; import type { CapturedArguments } from '@glimmer/interfaces'; import { createComputeRef, valueForRef } from '@glimmer/reference/lib/reference'; -import { consumeTag } from '@glimmer/validator/lib/tracking'; +import { consumeTag } from '@glimmer/signals/lib/tags'; import { internalHelper } from './internal-helper'; /** diff --git a/packages/@ember/-internals/glimmer/lib/utils/curly-component-state-bucket.ts b/packages/@ember/-internals/glimmer/lib/utils/curly-component-state-bucket.ts index 1780214877f..f6952e077fe 100644 --- a/packages/@ember/-internals/glimmer/lib/utils/curly-component-state-bucket.ts +++ b/packages/@ember/-internals/glimmer/lib/utils/curly-component-state-bucket.ts @@ -8,10 +8,13 @@ import { registerDestructor } from '@glimmer/destroyable'; import type { CapturedNamedArguments } from '@glimmer/interfaces'; import type { Reference } from '@glimmer/reference/lib/reference'; import { createConstRef } from '@glimmer/reference/lib/reference'; -import type { Revision } from '@glimmer/validator/lib/validators'; +import { + beginUntrackFrame, + endUntrackFrame, + valueForTag, + type Revision, +} from '@glimmer/signals/lib/tags'; import type { Tag } from '@glimmer/interfaces'; -import { beginUntrackFrame, endUntrackFrame } from '@glimmer/validator/lib/tracking'; -import { valueForTag } from '@glimmer/validator/lib/validators'; import type Component from '../component'; type Finalizer = () => void; diff --git a/packages/@ember/-internals/glimmer/lib/utils/iterator.ts b/packages/@ember/-internals/glimmer/lib/utils/iterator.ts index a497c8a4a73..78be85e56c3 100644 --- a/packages/@ember/-internals/glimmer/lib/utils/iterator.ts +++ b/packages/@ember/-internals/glimmer/lib/utils/iterator.ts @@ -4,8 +4,8 @@ import { isEmberArray } from '@ember/array/-internals'; import { isObject } from '@ember/-internals/utils/lib/spec'; import type { Nullable } from '@ember/-internals/utility-types'; import type { IteratorDelegate } from '@glimmer/reference/lib/iterable'; -import { consumeTag, isTracking } from '@glimmer/validator/lib/tracking'; -import { tagFor } from '@glimmer/validator/lib/meta'; +import { consumeTag, isTracking } from '@glimmer/signals/lib/tags'; +import { tagFor } from '@glimmer/signals/lib/meta'; import { EachInWrapper } from '../helpers/each-in'; import type { NativeArray } from '@ember/array'; diff --git a/packages/@ember/-internals/glimmer/lib/utils/to-bool.ts b/packages/@ember/-internals/glimmer/lib/utils/to-bool.ts index dbec5d9f76c..34060392c5f 100644 --- a/packages/@ember/-internals/glimmer/lib/utils/to-bool.ts +++ b/packages/@ember/-internals/glimmer/lib/utils/to-bool.ts @@ -3,7 +3,7 @@ import { get } from '@ember/-internals/metal/lib/property_get'; import { tagForProperty } from '@ember/-internals/metal/lib/tags'; import isArray from '@ember/array/lib/is-array'; import { isProxy } from '@ember/-internals/utils/lib/is_proxy'; -import { consumeTag } from '@glimmer/validator/lib/tracking'; +import { consumeTag } from '@glimmer/signals/lib/tags'; export default function toBool(predicate: unknown): boolean { if (isProxy(predicate)) { diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/invoke-helper-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/invoke-helper-test.js index 8b4fc637c16..e8fd4f617ba 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/invoke-helper-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/invoke-helper-test.js @@ -8,7 +8,7 @@ import { set } from '@ember/object'; import { getOwner } from '@ember/-internals/owner'; import Service, { service } from '@ember/service'; import { DEBUG } from '@glimmer/env'; -import { getValue } from '@glimmer/validator'; +import { getValue } from '@glimmer/signals'; import { destroy, isDestroyed, registerDestructor } from '@glimmer/destroyable'; import { invokeHelper } from '@glimmer/runtime'; diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/unique-id-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/unique-id-test.js index 85d1d68df8e..55cd621d9e4 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/unique-id-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/unique-id-test.js @@ -7,7 +7,7 @@ import { } from 'internal-test-helpers'; import { setProperties } from '@ember/object'; import { uniqueId, invokeHelper } from '@ember/helper'; -import { getValue } from '@glimmer/validator'; +import { getValue } from '@glimmer/signals'; moduleFor( 'Helpers test: {{unique-id}} JS', diff --git a/packages/@ember/-internals/meta/lib/meta.ts b/packages/@ember/-internals/meta/lib/meta.ts index 781566d20bd..17650c54279 100644 --- a/packages/@ember/-internals/meta/lib/meta.ts +++ b/packages/@ember/-internals/meta/lib/meta.ts @@ -3,7 +3,7 @@ import toString from '@ember/-internals/utils/lib/to-string'; import { assert } from '@ember/debug'; import { isDestroyed } from '@glimmer/destroyable'; import { DEBUG } from '@glimmer/env'; -import type { Revision } from '@glimmer/validator/lib/validators'; +import type { Revision } from '@glimmer/signals/lib/tags'; import type { UpdatableTag } from '@glimmer/interfaces'; type ObjMap = { [key: string]: T }; diff --git a/packages/@ember/-internals/metal/lib/alias.ts b/packages/@ember/-internals/metal/lib/alias.ts index 7f5915725bb..927a079f10d 100644 --- a/packages/@ember/-internals/metal/lib/alias.ts +++ b/packages/@ember/-internals/metal/lib/alias.ts @@ -3,13 +3,14 @@ import { meta as metaFor } from '@ember/-internals/meta/lib/meta'; import inspect from '@ember/debug/lib/inspect'; import { assert } from '@ember/debug'; import type { UpdatableTag } from '@glimmer/interfaces'; -import { consumeTag, untrack } from '@glimmer/validator/lib/tracking'; -import { tagFor, tagMetaFor } from '@glimmer/validator/lib/meta'; import { - UPDATE_TAG as updateTag, + consumeTag, + untrack, + updateTag, validateTag, valueForTag, -} from '@glimmer/validator/lib/validators'; +} from '@glimmer/signals/lib/tags'; +import { tagFor, tagMetaFor } from '@glimmer/signals/lib/meta'; import { CHAIN_PASS_THROUGH, finishLazyChains, getChainTagsForKey } from './chain-tags'; import type { ExtendedMethodDecorator } from './decorator'; import { diff --git a/packages/@ember/-internals/metal/lib/cache.ts b/packages/@ember/-internals/metal/lib/cache.ts index f6349c9c469..8e6f3c89a40 100644 --- a/packages/@ember/-internals/metal/lib/cache.ts +++ b/packages/@ember/-internals/metal/lib/cache.ts @@ -1,4 +1,4 @@ -export { createCache, getValue, isConst } from '@glimmer/validator/lib/tracking'; +export { createCache, getValue, isConst } from '@glimmer/signals/lib/tags'; /** Ember uses caching based on trackable values to avoid updating large portions diff --git a/packages/@ember/-internals/metal/lib/cached.ts b/packages/@ember/-internals/metal/lib/cached.ts index 6d4895dbfe0..f536011b94d 100644 --- a/packages/@ember/-internals/metal/lib/cached.ts +++ b/packages/@ember/-internals/metal/lib/cached.ts @@ -2,7 +2,7 @@ // Both glimmerjs/glimmer.js and emberjs/ember.js have the exact same implementation // of @cached, so any changes made to one should also be made to the other import { DEBUG } from '@glimmer/env'; -import { createCache, getValue } from '@glimmer/validator/lib/tracking'; +import { createCache, getValue } from '@glimmer/signals/lib/tags'; import { type Decorator, identifyModernDecoratorArgs, diff --git a/packages/@ember/-internals/metal/lib/chain-tags.ts b/packages/@ember/-internals/metal/lib/chain-tags.ts index 9827cf1cf6b..3662f8c2655 100644 --- a/packages/@ember/-internals/metal/lib/chain-tags.ts +++ b/packages/@ember/-internals/metal/lib/chain-tags.ts @@ -3,14 +3,9 @@ import { meta as metaFor, peekMeta } from '@ember/-internals/meta/lib/meta'; import { isObject } from '@ember/-internals/utils/lib/spec'; import { assert } from '@ember/debug'; import type { Tag } from '@glimmer/interfaces'; -import type { TagMeta } from '@glimmer/validator/lib/meta'; -import { - combine, - createUpdatableTag, - UPDATE_TAG as updateTag, - validateTag, -} from '@glimmer/validator/lib/validators'; -import { tagMetaFor } from '@glimmer/validator/lib/meta'; +import type { TagMeta } from '@glimmer/signals/lib/meta'; +import { combine, createTag, updateTag, validateTag } from '@glimmer/signals/lib/tags'; +import { tagMetaFor } from '@glimmer/signals/lib/meta'; import { objectAt } from './object-at'; import { tagForProperty } from './tags'; @@ -185,7 +180,7 @@ function getChainTags( let lazyChains = instanceMeta.writableLazyChainsFor(segment); let rest = path.substring(segmentEnd + 1); - let placeholderTag = createUpdatableTag(); + let placeholderTag = createTag(); lazyChains.push([placeholderTag, rest]); chainTags.push(placeholderTag); diff --git a/packages/@ember/-internals/metal/lib/computed.ts b/packages/@ember/-internals/metal/lib/computed.ts index e1b8fd9474b..f2797fe1efe 100644 --- a/packages/@ember/-internals/metal/lib/computed.ts +++ b/packages/@ember/-internals/metal/lib/computed.ts @@ -4,16 +4,16 @@ import toString from '@ember/-internals/utils/lib/to-string'; import inspect from '@ember/debug/lib/inspect'; import { assert } from '@ember/debug'; import { isDestroyed } from '@glimmer/destroyable'; -import { DEBUG } from '@glimmer/env'; import type { UpdatableTag } from '@glimmer/interfaces'; import { - ALLOW_CYCLES, - UPDATE_TAG as updateTag, + consumeTag, + track, + untrack, + updateTag, validateTag, valueForTag, -} from '@glimmer/validator/lib/validators'; -import { consumeTag, track, untrack } from '@glimmer/validator/lib/tracking'; -import { tagFor, tagMetaFor } from '@glimmer/validator/lib/meta'; +} from '@glimmer/signals/lib/tags'; +import { tagFor, tagMetaFor } from '@glimmer/signals/lib/meta'; import { finishLazyChains, getChainTagsForKeys } from './chain-tags'; import type { ExtendedMethodDecorator, @@ -423,10 +423,6 @@ export class ComputedProperty extends ComputedDescriptor { if (_dependentKeys !== undefined) { updateTag(propertyTag, getChainTagsForKeys(obj, _dependentKeys, tagMeta, meta)); - - if (DEBUG) { - ALLOW_CYCLES!.set(propertyTag, true); - } } meta.setValueFor(keyName, ret); @@ -501,10 +497,6 @@ export class ComputedProperty extends ComputedDescriptor { if (_dependentKeys !== undefined) { updateTag(propertyTag, getChainTagsForKeys(obj, _dependentKeys, tagMeta, meta)); - - if (DEBUG) { - ALLOW_CYCLES!.set(propertyTag, true); - } } meta.setRevisionFor(keyName, valueForTag(propertyTag)); diff --git a/packages/@ember/-internals/metal/lib/observer.ts b/packages/@ember/-internals/metal/lib/observer.ts index 08c5148cd9c..714f504acbd 100644 --- a/packages/@ember/-internals/metal/lib/observer.ts +++ b/packages/@ember/-internals/metal/lib/observer.ts @@ -3,8 +3,8 @@ import { peekMeta } from '@ember/-internals/meta/lib/meta'; import type { schedule } from '@ember/runloop'; import { registerDestructor } from '@glimmer/destroyable'; import type { Tag } from '@glimmer/interfaces'; -import { CURRENT_TAG, validateTag, valueForTag } from '@glimmer/validator/lib/validators'; -import { tagMetaFor } from '@glimmer/validator/lib/meta'; +import { currentRevision, validateTag, valueForTag } from '@glimmer/signals/lib/tags'; +import { tagMetaFor } from '@glimmer/signals/lib/meta'; import { getChainTagsForKey } from './chain-tags'; import changeEvent from './change_event'; import { addListener, removeListener, sendEvent } from './events'; @@ -189,11 +189,11 @@ export function revalidateObservers(target: object) { let lastKnownRevision = 0; export function flushAsyncObservers(_schedule: typeof schedule | false) { - let currentRevision = valueForTag(CURRENT_TAG); - if (lastKnownRevision === currentRevision) { + let revision = currentRevision(); + if (lastKnownRevision === revision) { return; } - lastKnownRevision = currentRevision; + lastKnownRevision = revision; ASYNC_OBSERVERS.forEach((activeObservers, target) => { let meta = peekMeta(target); diff --git a/packages/@ember/-internals/metal/lib/property_get.ts b/packages/@ember/-internals/metal/lib/property_get.ts index e76c9c14064..42eabf3ccd6 100644 --- a/packages/@ember/-internals/metal/lib/property_get.ts +++ b/packages/@ember/-internals/metal/lib/property_get.ts @@ -6,8 +6,8 @@ import { setProxy } from '@ember/-internals/utils/lib/is_proxy'; import { isEmberArray } from '@ember/array/-internals'; import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; -import { consumeTag, isTracking, track } from '@glimmer/validator/lib/tracking'; -import { tagFor } from '@glimmer/validator/lib/meta'; +import { consumeTag, isTracking, track } from '@glimmer/signals/lib/tags'; +import { tagFor } from '@glimmer/signals/lib/meta'; import { isPath } from './path_cache'; export const PROXY_CONTENT = Symbol('PROXY_CONTENT'); diff --git a/packages/@ember/-internals/metal/lib/tags.ts b/packages/@ember/-internals/metal/lib/tags.ts index 41ab6860a53..9ae3fd007a7 100644 --- a/packages/@ember/-internals/metal/lib/tags.ts +++ b/packages/@ember/-internals/metal/lib/tags.ts @@ -6,9 +6,9 @@ import { isDestroyed } from '@glimmer/destroyable'; import { DEBUG } from '@glimmer/env'; import { getCustomTagFor } from '@glimmer/manager/lib/util/args-proxy'; import type { Tag } from '@glimmer/interfaces'; -import type { TagMeta } from '@glimmer/validator/lib/meta'; -import { CONSTANT_TAG } from '@glimmer/validator/lib/validators'; -import { dirtyTagFor, tagFor } from '@glimmer/validator/lib/meta'; +import type { TagMeta } from '@glimmer/signals/lib/meta'; +import { CONSTANT_TAG } from '@glimmer/signals/lib/tags'; +import { dirtyTagFor, tagFor } from '@glimmer/signals/lib/meta'; ///////// diff --git a/packages/@ember/-internals/metal/lib/tracked.ts b/packages/@ember/-internals/metal/lib/tracked.ts index 67b09e2aa60..f6d922a7b85 100644 --- a/packages/@ember/-internals/metal/lib/tracked.ts +++ b/packages/@ember/-internals/metal/lib/tracked.ts @@ -2,10 +2,10 @@ import { meta as metaFor } from '@ember/-internals/meta/lib/meta'; import { isEmberArray } from '@ember/array/-internals'; import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; -import { consumeTag, untrack } from '@glimmer/validator/lib/tracking'; -import { dirtyTagFor, tagFor } from '@glimmer/validator/lib/meta'; -import { trackedData } from '@glimmer/validator/lib/tracked-data'; -import { trackedValue, type TrackedValue } from '@glimmer/validator/lib/tracked-value'; +import { consumeTag, untrack } from '@glimmer/signals/lib/tags'; +import { dirtyTagFor, tagFor } from '@glimmer/signals/lib/meta'; +import { trackedData } from '@glimmer/signals/lib/tracked-data'; +import { trackedValue, type TrackedValue } from '@glimmer/signals/lib/tracked-value'; import type { ElementDescriptor } from '..'; import { CHAIN_PASS_THROUGH } from './chain-tags'; import type { ExtendedMethodDecorator, DecoratorPropertyDescriptor } from './decorator'; diff --git a/packages/@ember/-internals/metal/tests/alias_test.js b/packages/@ember/-internals/metal/tests/alias_test.js index a9c4a2635ce..6f29037c82e 100644 --- a/packages/@ember/-internals/metal/tests/alias_test.js +++ b/packages/@ember/-internals/metal/tests/alias_test.js @@ -12,7 +12,7 @@ import EmberObject from '@ember/object'; import { A } from '@ember/array'; import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { destroy } from '@glimmer/destroyable'; -import { valueForTag, validateTag } from '@glimmer/validator'; +import { valueForTag, validateTag } from '@glimmer/signals'; let obj, count; diff --git a/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js b/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js index 3f693666e99..4606d82ce7f 100644 --- a/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js +++ b/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js @@ -1,7 +1,7 @@ import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; import { defineProperty, tracked, nativeDescDecorator } from '../..'; -import { track, valueForTag, validateTag } from '@glimmer/validator'; +import { track, valueForTag, validateTag } from '@glimmer/signals'; moduleFor( '@tracked decorator - classic classes', diff --git a/packages/@ember/-internals/metal/tests/tracked/options_test.js b/packages/@ember/-internals/metal/tests/tracked/options_test.js index ed8be57bf7a..bd79be39274 100644 --- a/packages/@ember/-internals/metal/tests/tracked/options_test.js +++ b/packages/@ember/-internals/metal/tests/tracked/options_test.js @@ -1,7 +1,7 @@ import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; import { tracked } from '../..'; -import { track, valueForTag, validateTag } from '@glimmer/validator'; +import { track, valueForTag, validateTag } from '@glimmer/signals'; moduleFor( '@tracked decorator - options', diff --git a/packages/@ember/-internals/metal/tests/tracked/standalone_test.js b/packages/@ember/-internals/metal/tests/tracked/standalone_test.js index 0d781938072..86beccbca52 100644 --- a/packages/@ember/-internals/metal/tests/tracked/standalone_test.js +++ b/packages/@ember/-internals/metal/tests/tracked/standalone_test.js @@ -1,7 +1,7 @@ import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; import { tracked } from '../..'; -import { track, valueForTag, validateTag } from '@glimmer/validator'; +import { track, valueForTag, validateTag } from '@glimmer/signals'; moduleFor( 'tracked() - standalone usage', diff --git a/packages/@ember/-internals/metal/tests/tracked/validation_test.js b/packages/@ember/-internals/metal/tests/tracked/validation_test.js index e2eef6ed310..2cc1a690617 100644 --- a/packages/@ember/-internals/metal/tests/tracked/validation_test.js +++ b/packages/@ember/-internals/metal/tests/tracked/validation_test.js @@ -9,7 +9,7 @@ import { } from '../..'; import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; -import { track, valueForTag, validateTag } from '@glimmer/validator'; +import { track, valueForTag, validateTag } from '@glimmer/signals'; moduleFor( '@tracked get validation', diff --git a/packages/@ember/-internals/package.json b/packages/@ember/-internals/package.json index 7f5f825722b..4077067bfa8 100644 --- a/packages/@ember/-internals/package.json +++ b/packages/@ember/-internals/package.json @@ -62,7 +62,7 @@ "@glimmer/syntax": "workspace:*", "@glimmer/tracking": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "@glimmer/vm": "workspace:*", "@simple-dom/interface": "^1.4.0", "backburner.js": "^2.7.0", diff --git a/packages/@ember/-internals/routing/route-managers/root-outlet.ts b/packages/@ember/-internals/routing/route-managers/root-outlet.ts index df69300fa52..b6cf2d0deab 100644 --- a/packages/@ember/-internals/routing/route-managers/root-outlet.ts +++ b/packages/@ember/-internals/routing/route-managers/root-outlet.ts @@ -16,8 +16,7 @@ import { setComponentTemplate } from '@glimmer/manager/lib/public/template'; import { precompileTemplate } from '@ember/template-compilation'; import { DEBUG } from '@glimmer/env'; import type { OutletParent, OutletState } from './outlet-state'; -import { consumeTag } from '@glimmer/validator/lib/tracking'; -import { createTag, DIRTY_TAG as dirtyTag } from '@glimmer/validator/lib/validators'; +import { consumeTag, createTag, dirtyTag } from '@glimmer/signals/lib/tags'; // `this` == <@outlet />; returned by `getSelf` const ROOT_OUTLET_TEMPLATE = precompileTemplate('{{this}}', { diff --git a/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts b/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts index cf3e23234e6..b0d339598be 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts @@ -17,12 +17,12 @@ import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; import { setCustomTagFor } from '@glimmer/manager/lib/util/args-proxy'; import type { UpdatableTag, Tag } from '@glimmer/interfaces'; -import { combine, UPDATE_TAG as updateTag } from '@glimmer/validator/lib/validators'; -import { tagFor, tagMetaFor } from '@glimmer/validator/lib/meta'; +import { combine, updateTag } from '@glimmer/signals/lib/tags'; +import { tagFor, tagMetaFor } from '@glimmer/signals/lib/meta'; export function contentFor(proxy: ProxyMixin): T | null { let content = get(proxy, 'content'); - // SAFETY: Ideally we'd assert instead of casting, but @glimmer/validator doesn't give us + // SAFETY: Ideally we'd assert instead of casting, but @glimmer/signals doesn't give us // sufficient public types for this. Previously this code was .js and worked correctly so // hopefully this is sufficiently reliable. updateTag(tagForObject(proxy) as UpdatableTag, tagForObject(content)); diff --git a/packages/@ember/-internals/runtime/tests/system/array_proxy/length_test.js b/packages/@ember/-internals/runtime/tests/system/array_proxy/length_test.js index 13408d36ee7..52f48288794 100644 --- a/packages/@ember/-internals/runtime/tests/system/array_proxy/length_test.js +++ b/packages/@ember/-internals/runtime/tests/system/array_proxy/length_test.js @@ -11,7 +11,7 @@ import { testUnless, } from 'internal-test-helpers'; import { set, get } from '@ember/object'; -import { createCache, getValue } from '@glimmer/validator'; +import { createCache, getValue } from '@glimmer/signals'; import { DEPRECATIONS } from '@ember/-internals/deprecations'; moduleFor( diff --git a/packages/@ember/-internals/runtime/tests/system/core_object_test.js b/packages/@ember/-internals/runtime/tests/system/core_object_test.js index 1649c657a4d..3b61eb4803c 100644 --- a/packages/@ember/-internals/runtime/tests/system/core_object_test.js +++ b/packages/@ember/-internals/runtime/tests/system/core_object_test.js @@ -8,7 +8,7 @@ import { runDestroy, runLoopSettled, } from 'internal-test-helpers'; -import { track } from '@glimmer/validator'; +import { track } from '@glimmer/signals'; import { destroy } from '@glimmer/destroyable'; import { run } from '@ember/runloop'; diff --git a/packages/@ember/application/package.json b/packages/@ember/application/package.json index aa0f4bfb648..7480127352a 100644 --- a/packages/@ember/application/package.json +++ b/packages/@ember/application/package.json @@ -24,7 +24,7 @@ "@glimmer/env": "workspace:*", "@glimmer/owner": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "backburner.js": "^2.7.0", "dag-map": "^2.0.2", "ember": "workspace:*", diff --git a/packages/@ember/array/package.json b/packages/@ember/array/package.json index 8585c775652..8ec6882effe 100644 --- a/packages/@ember/array/package.json +++ b/packages/@ember/array/package.json @@ -24,7 +24,7 @@ "@glimmer/manager": "workspace:*", "@glimmer/owner": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "expect-type": "^0.15.0", "internal-test-helpers": "workspace:*" } diff --git a/packages/@ember/array/proxy.ts b/packages/@ember/array/proxy.ts index 192c55215ee..e4e9c259658 100644 --- a/packages/@ember/array/proxy.ts +++ b/packages/@ember/array/proxy.ts @@ -22,12 +22,12 @@ import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; import { setCustomTagFor } from '@glimmer/manager/lib/util/args-proxy'; import { combine, + consumeTag, validateTag, valueForTag, type Revision, -} from '@glimmer/validator/lib/validators'; -import { consumeTag } from '@glimmer/validator/lib/tracking'; -import { tagFor } from '@glimmer/validator/lib/meta'; +} from '@glimmer/signals/lib/tags'; +import { tagFor } from '@glimmer/signals/lib/meta'; import type { Tag } from '@glimmer/interfaces'; function isMutable(obj: T[] | EmberArray): obj is T[] | MutableArray { diff --git a/packages/@ember/controller/package.json b/packages/@ember/controller/package.json index 90e42a88425..d82e52a68e2 100644 --- a/packages/@ember/controller/package.json +++ b/packages/@ember/controller/package.json @@ -17,7 +17,7 @@ "@glimmer/env": "workspace:*", "@glimmer/owner": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "expect-type": "^0.15.0", "internal-test-helpers": "workspace:*" }, diff --git a/packages/@ember/debug/data-adapter.ts b/packages/@ember/debug/data-adapter.ts index 4b37faaff4b..5ba4603f9ec 100644 --- a/packages/@ember/debug/data-adapter.ts +++ b/packages/@ember/debug/data-adapter.ts @@ -7,9 +7,8 @@ import Namespace from '@ember/application/namespace'; import type { NativeArray } from '@ember/array'; import EmberObject from '@ember/object'; import { A as emberA } from '@ember/array'; -import type { Cache } from '@glimmer/validator/lib/tracking'; -import { consumeTag, createCache, getValue, untrack } from '@glimmer/validator/lib/tracking'; -import { tagFor } from '@glimmer/validator/lib/meta'; +import { consumeTag, createCache, getValue, untrack, type Cache } from '@glimmer/signals/lib/tags'; +import { tagFor } from '@glimmer/signals/lib/meta'; import type ContainerDebugAdapter from '@ember/debug/container-debug-adapter'; import { assert } from '.'; diff --git a/packages/@ember/debug/package.json b/packages/@ember/debug/package.json index 0d271bb0f79..949296cdaa3 100644 --- a/packages/@ember/debug/package.json +++ b/packages/@ember/debug/package.json @@ -25,7 +25,7 @@ "@glimmer/manager": "workspace:*", "@glimmer/owner": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "backburner.js": "^2.7.0", "expect-type": "^0.15.0", "internal-test-helpers": "workspace:*" diff --git a/packages/@ember/engine/package.json b/packages/@ember/engine/package.json index 1756d1d1cdc..0c4de63cc7f 100644 --- a/packages/@ember/engine/package.json +++ b/packages/@ember/engine/package.json @@ -24,7 +24,7 @@ "@glimmer/env": "workspace:*", "@glimmer/owner": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "dag-map": "^2.0.2", "expect-type": "^0.15.0", "internal-test-helpers": "workspace:*", diff --git a/packages/@ember/enumerable/package.json b/packages/@ember/enumerable/package.json index a12c717454d..ffb71691c32 100644 --- a/packages/@ember/enumerable/package.json +++ b/packages/@ember/enumerable/package.json @@ -16,7 +16,7 @@ "@glimmer/env": "workspace:*", "@glimmer/owner": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "internal-test-helpers": "workspace:*" } } diff --git a/packages/@ember/helper/package.json b/packages/@ember/helper/package.json index 8884ff2d17a..2d38bcab463 100644 --- a/packages/@ember/helper/package.json +++ b/packages/@ember/helper/package.json @@ -12,7 +12,7 @@ "@glimmer/interfaces": "workspace:*", "@glimmer/manager": "workspace:*", "@glimmer/runtime": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "expect-type": "^0.15.0" } } diff --git a/packages/@ember/helper/type-tests/invoke-helper.test.ts b/packages/@ember/helper/type-tests/invoke-helper.test.ts index ceb8ed895f4..f350fdbefae 100644 --- a/packages/@ember/helper/type-tests/invoke-helper.test.ts +++ b/packages/@ember/helper/type-tests/invoke-helper.test.ts @@ -2,7 +2,7 @@ import Component from '@ember/component'; import { getValue } from '@ember/-internals/metal'; import Helper from '@ember/component/helper'; import { invokeHelper } from '@ember/helper'; -import type { Cache } from '@glimmer/validator'; +import type { Cache } from '@glimmer/signals'; import { expectTypeOf } from 'expect-type'; // NOTE: The types should probably be stricter, but they're from glimmer itself diff --git a/packages/@ember/instrumentation/package.json b/packages/@ember/instrumentation/package.json index b0e5097cebd..27cbb96218a 100644 --- a/packages/@ember/instrumentation/package.json +++ b/packages/@ember/instrumentation/package.json @@ -15,7 +15,7 @@ "@ember/runloop": "workspace:*", "@ember/utils": "workspace:*", "@glimmer/env": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "internal-test-helpers": "workspace:*" } } diff --git a/packages/@ember/object/compat.ts b/packages/@ember/object/compat.ts index aeba7b106b4..f0934ccfd25 100644 --- a/packages/@ember/object/compat.ts +++ b/packages/@ember/object/compat.ts @@ -15,9 +15,8 @@ import { } from '@ember/-internals/metal/lib/decorator-util'; import { assert } from '@ember/debug'; import type { UpdatableTag } from '@glimmer/interfaces'; -import { consumeTag, track } from '@glimmer/validator/lib/tracking'; -import { tagFor } from '@glimmer/validator/lib/meta'; -import { UPDATE_TAG as updateTag } from '@glimmer/validator/lib/validators'; +import { consumeTag, track, updateTag } from '@glimmer/signals/lib/tags'; +import { tagFor } from '@glimmer/signals/lib/meta'; let wrapGetterSetter = function (target: object, key: string, desc: PropertyDescriptor) { let { get: originalGet } = desc; diff --git a/packages/@ember/object/core.ts b/packages/@ember/object/core.ts index 2dcec80ec1c..59d9943632a 100644 --- a/packages/@ember/object/core.ts +++ b/packages/@ember/object/core.ts @@ -23,6 +23,7 @@ import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; import { destroy, isDestroying, isDestroyed, registerDestructor } from '@glimmer/destroyable'; import { OWNER } from '@glimmer/owner'; +import { releaseTagsFor } from '@glimmer/signals/lib/meta'; type EmberClassConstructor = new (owner?: Owner) => T; @@ -299,6 +300,7 @@ class CoreObject { const destroyable = self; registerDestructor(self, ensureDestroyCalled, true); registerDestructor(self, () => destroyable.willDestroy()); + registerDestructor(self, releaseTagsFor); // disable chains let m = meta(self); diff --git a/packages/@ember/object/package.json b/packages/@ember/object/package.json index e0c4ce1ff36..cacf167405e 100644 --- a/packages/@ember/object/package.json +++ b/packages/@ember/object/package.json @@ -33,7 +33,7 @@ "@glimmer/manager": "workspace:*", "@glimmer/owner": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "expect-type": "^0.15.0", "internal-test-helpers": "workspace:*" }, diff --git a/packages/@ember/reactive/collections.ts b/packages/@ember/reactive/collections.ts index 70dfb92f642..3765adcb9f3 100644 --- a/packages/@ember/reactive/collections.ts +++ b/packages/@ember/reactive/collections.ts @@ -45,7 +45,7 @@ * @returns {Array} A tracked array that updates reactively * @public */ -export { trackedArray } from '@glimmer/validator/lib/collections/array'; +export { trackedArray } from '@glimmer/signals/lib/collections/array'; /** * A utility for creating tracked objects, copying the original data so that @@ -87,7 +87,7 @@ export { trackedArray } from '@glimmer/validator/lib/collections/array'; * @returns {Object} A tracked object that updates reactively * @public */ -export { trackedObject } from '@glimmer/validator/lib/collections/object'; +export { trackedObject } from '@glimmer/signals/lib/collections/object'; /** * A utility for creating tracked sets, copying the original data so that @@ -130,7 +130,7 @@ export { trackedObject } from '@glimmer/validator/lib/collections/object'; * @returns {Set} A tracked Set that updates reactively * @public */ -export { trackedSet } from '@glimmer/validator/lib/collections/set'; +export { trackedSet } from '@glimmer/signals/lib/collections/set'; /** * A utility for creating tracked weak sets, copying the original data so that @@ -170,7 +170,7 @@ export { trackedSet } from '@glimmer/validator/lib/collections/set'; * @returns {WeakSet} A tracked WeakSet that updates reactively * @public */ -export { trackedWeakSet } from '@glimmer/validator/lib/collections/weak-set'; +export { trackedWeakSet } from '@glimmer/signals/lib/collections/weak-set'; /** * A utility for creating tracked maps, copying the original data so that @@ -213,7 +213,7 @@ export { trackedWeakSet } from '@glimmer/validator/lib/collections/weak-set'; * @returns {Map} A tracked Map that updates reactively * @public */ -export { trackedMap } from '@glimmer/validator/lib/collections/map'; +export { trackedMap } from '@glimmer/signals/lib/collections/map'; /** * A utility for creating tracked weak maps, copying the original data so that @@ -253,4 +253,4 @@ export { trackedMap } from '@glimmer/validator/lib/collections/map'; * @returns {WeakMap} A tracked WeakMap that updates reactively * @public */ -export { trackedWeakMap } from '@glimmer/validator/lib/collections/weak-map'; +export { trackedWeakMap } from '@glimmer/signals/lib/collections/weak-map'; diff --git a/packages/@ember/reactive/package.json b/packages/@ember/reactive/package.json index 7a624600d98..79e4239ff00 100644 --- a/packages/@ember/reactive/package.json +++ b/packages/@ember/reactive/package.json @@ -8,6 +8,6 @@ "./*": "./*.ts" }, "dependencies": { - "@glimmer/validator": "workspace:*" + "@glimmer/signals": "workspace:*" } } diff --git a/packages/@ember/routing/package.json b/packages/@ember/routing/package.json index 5a956191f63..8fa002770aa 100644 --- a/packages/@ember/routing/package.json +++ b/packages/@ember/routing/package.json @@ -33,7 +33,7 @@ "@glimmer/manager": "workspace:*", "@glimmer/owner": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "backburner.js": "^2.7.0", "dag-map": "^2.0.2", "expect-type": "^0.15.0", diff --git a/packages/@ember/routing/router-service.ts b/packages/@ember/routing/router-service.ts index 0e4b6cae977..f50de8ac233 100644 --- a/packages/@ember/routing/router-service.ts +++ b/packages/@ember/routing/router-service.ts @@ -5,8 +5,8 @@ import { getOwner } from '@ember/-internals/owner'; import { assert } from '@ember/debug'; import { readOnly } from '@ember/object/computed'; import Service from '@ember/service'; -import { consumeTag } from '@glimmer/validator/lib/tracking'; -import { tagFor } from '@glimmer/validator/lib/meta'; +import { consumeTag } from '@glimmer/signals/lib/tags'; +import { tagFor } from '@glimmer/signals/lib/meta'; import type { Transition } from 'router_js'; import type { ModelFor } from '@ember/routing/route'; import type Route from '@ember/routing/route'; diff --git a/packages/@ember/runloop/package.json b/packages/@ember/runloop/package.json index e0b194a2af7..907bded110f 100644 --- a/packages/@ember/runloop/package.json +++ b/packages/@ember/runloop/package.json @@ -14,7 +14,7 @@ "@ember/object": "workspace:*", "@ember/utils": "workspace:*", "@glimmer/env": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "backburner.js": "^2.7.0", "expect-type": "^0.15.0", "internal-test-helpers": "workspace:*" diff --git a/packages/@ember/service/package.json b/packages/@ember/service/package.json index 4af5a789601..445f0cbe8ba 100644 --- a/packages/@ember/service/package.json +++ b/packages/@ember/service/package.json @@ -15,7 +15,7 @@ "@glimmer/env": "workspace:*", "@glimmer/owner": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "expect-type": "^0.15.0", "internal-test-helpers": "workspace:*" }, diff --git a/packages/@ember/utils/package.json b/packages/@ember/utils/package.json index a26dab58aae..c9404e1db68 100644 --- a/packages/@ember/utils/package.json +++ b/packages/@ember/utils/package.json @@ -19,7 +19,7 @@ "@glimmer/manager": "workspace:*", "@glimmer/owner": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "expect-type": "^0.15.0", "internal-test-helpers": "workspace:*" } diff --git a/packages/@glimmer-workspace/integration-tests/lib/components/emberish-curly.ts b/packages/@glimmer-workspace/integration-tests/lib/components/emberish-curly.ts index 61560bb3931..714f54f58fb 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/components/emberish-curly.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/components/emberish-curly.ts @@ -18,7 +18,7 @@ import type { WithDynamicLayout, WithDynamicTagName, } from '@glimmer/interfaces'; -import type { DirtyableTag } from '@glimmer/validator'; +import type { DirtyableTag } from '@glimmer/signals'; import { unwrapTemplate } from '@glimmer/debug-util'; import { registerDestructor } from '@glimmer/destroyable'; import { setInternalComponentManager } from '@glimmer/manager'; @@ -31,7 +31,7 @@ import { } from '@glimmer/reference'; import { reifyNamed, reifyPositional } from '@glimmer/runtime'; import { assign, EMPTY_ARRAY, keys } from '@glimmer/util'; -import { consumeTag, createTag, dirtyTag, dirtyTagFor } from '@glimmer/validator'; +import { consumeTag, createTag, dirtyTag, dirtyTagFor } from '@glimmer/signals'; import type { TestJitRuntimeResolver } from '../modes/jit/resolver'; import type { TestComponentConstructor } from './types'; diff --git a/packages/@glimmer-workspace/integration-tests/lib/modifiers.ts b/packages/@glimmer-workspace/integration-tests/lib/modifiers.ts index 210b1daa804..5565915d4bd 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/modifiers.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/modifiers.ts @@ -7,10 +7,10 @@ import type { Owner, SimpleElement, } from '@glimmer/interfaces'; -import type { UpdatableTag } from '@glimmer/validator'; +import type { UpdatableTag } from '@glimmer/signals'; import { registerDestructor } from '@glimmer/destroyable'; import { reifyNamed, reifyPositional } from '@glimmer/runtime'; -import { createUpdatableTag } from '@glimmer/validator'; +import { createTag } from '@glimmer/signals'; export interface TestModifierConstructor { new (): TestModifierInstance; @@ -84,7 +84,7 @@ export class TestModifierManager implements InternalModifierManager< } export class TestModifier { - public tag = createUpdatableTag(); + public tag = createTag(); constructor( public element: SimpleElement, diff --git a/packages/@glimmer-workspace/integration-tests/lib/render-test.ts b/packages/@glimmer-workspace/integration-tests/lib/render-test.ts index fcb040348a2..86d6a5d36e9 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/render-test.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/render-test.ts @@ -15,7 +15,7 @@ import { expect, isPresent, assert, unwrap } from '@glimmer/debug-util'; import { destroy } from '@glimmer/destroyable'; import { inTransaction } from '@glimmer/runtime'; import { clearElement, dict } from '@glimmer/util'; -import { dirtyTagFor } from '@glimmer/validator'; +import { dirtyTagFor } from '@glimmer/signals'; import { run } from '@ember/runloop'; import type { ComponentBlueprint, ComponentKind, ComponentTypes } from './components'; diff --git a/packages/@glimmer-workspace/integration-tests/lib/setup-harness.ts b/packages/@glimmer-workspace/integration-tests/lib/setup-harness.ts index f5cf0704f77..0536c656fe8 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/setup-harness.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/setup-harness.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import type { Expand } from '@glimmer/interfaces'; import type { Runner } from 'js-reporters'; -import { debug } from '@glimmer/validator'; +import { debug } from '@glimmer/signals'; export async function runTests(packages: Record Promise>) { const { smokeTest } = await setupQunit(); diff --git a/packages/@glimmer-workspace/integration-tests/lib/suites/each.ts b/packages/@glimmer-workspace/integration-tests/lib/suites/each.ts index 576e8f81d94..90fca3ccfa2 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/suites/each.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/suites/each.ts @@ -1,6 +1,6 @@ import { LOCAL_DEBUG } from '@glimmer/local-debug-flags'; import { beginTestSteps, endTestSteps, verifySteps } from '@glimmer/util'; -import { consumeTag, createTag, dirtyTag } from '@glimmer/validator'; +import { consumeTag, createTag, dirtyTag } from '@glimmer/signals'; import { RenderTest } from '../render-test'; import { test } from '../test-decorator'; diff --git a/packages/@glimmer-workspace/integration-tests/lib/test-helpers/tracked-object.ts b/packages/@glimmer-workspace/integration-tests/lib/test-helpers/tracked-object.ts index d0d653802ca..f23f279cf62 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/test-helpers/tracked-object.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/test-helpers/tracked-object.ts @@ -1,4 +1,4 @@ -import { consumeTag, dirtyTagFor, tagFor } from '@glimmer/validator'; +import { consumeTag, dirtyTagFor, tagFor } from '@glimmer/signals'; export function trackedObj>( obj: T = {} as T diff --git a/packages/@glimmer-workspace/integration-tests/package.json b/packages/@glimmer-workspace/integration-tests/package.json index c303372f5d5..63257410b96 100644 --- a/packages/@glimmer-workspace/integration-tests/package.json +++ b/packages/@glimmer-workspace/integration-tests/package.json @@ -29,7 +29,7 @@ "@glimmer/syntax": "workspace:*", "@glimmer/tracking": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "@glimmer/vm": "workspace:*", "@glimmer/wire-format": "workspace:*", "@simple-dom/document": "^1.4.0", diff --git a/packages/@glimmer-workspace/integration-tests/test/collections/array-test.ts b/packages/@glimmer-workspace/integration-tests/test/collections/array-test.ts index 5d9ca795f78..fe9d472320c 100644 --- a/packages/@glimmer-workspace/integration-tests/test/collections/array-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/collections/array-test.ts @@ -1,5 +1,5 @@ import type { Dict, Owner } from '@glimmer/interfaces'; -import { trackedArray } from '@glimmer/validator'; +import { trackedArray } from '@glimmer/signals'; import { GlimmerishComponent as Component, jitSuite, diff --git a/packages/@glimmer-workspace/integration-tests/test/collections/map-test.ts b/packages/@glimmer-workspace/integration-tests/test/collections/map-test.ts index 02acb7d5b68..2123c4b4062 100644 --- a/packages/@glimmer-workspace/integration-tests/test/collections/map-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/collections/map-test.ts @@ -1,4 +1,4 @@ -import { trackedMap } from '@glimmer/validator'; +import { trackedMap } from '@glimmer/signals'; import { defineComponent, GlimmerishComponent as Component, diff --git a/packages/@glimmer-workspace/integration-tests/test/collections/object-test.ts b/packages/@glimmer-workspace/integration-tests/test/collections/object-test.ts index 7e0bfa8b14e..8253f32689f 100644 --- a/packages/@glimmer-workspace/integration-tests/test/collections/object-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/collections/object-test.ts @@ -1,4 +1,4 @@ -import { trackedObject } from '@glimmer/validator'; +import { trackedObject } from '@glimmer/signals'; import { defineComponent, GlimmerishComponent as Component, diff --git a/packages/@glimmer-workspace/integration-tests/test/collections/set-test.ts b/packages/@glimmer-workspace/integration-tests/test/collections/set-test.ts index 205b87947fa..bc360e7841d 100644 --- a/packages/@glimmer-workspace/integration-tests/test/collections/set-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/collections/set-test.ts @@ -1,4 +1,4 @@ -import { trackedSet } from '@glimmer/validator'; +import { trackedSet } from '@glimmer/signals'; import { defineComponent, GlimmerishComponent as Component, diff --git a/packages/@glimmer-workspace/integration-tests/test/collections/weak-map-test.ts b/packages/@glimmer-workspace/integration-tests/test/collections/weak-map-test.ts index bb9de80053e..d73f9f972d9 100644 --- a/packages/@glimmer-workspace/integration-tests/test/collections/weak-map-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/collections/weak-map-test.ts @@ -1,4 +1,4 @@ -import { trackedWeakMap } from '@glimmer/validator'; +import { trackedWeakMap } from '@glimmer/signals'; import { defineComponent, GlimmerishComponent as Component, diff --git a/packages/@glimmer-workspace/integration-tests/test/collections/weak-set-test.ts b/packages/@glimmer-workspace/integration-tests/test/collections/weak-set-test.ts index 16127f3f3c2..1f7c95440f6 100644 --- a/packages/@glimmer-workspace/integration-tests/test/collections/weak-set-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/collections/weak-set-test.ts @@ -1,4 +1,4 @@ -import { trackedWeakSet } from '@glimmer/validator'; +import { trackedWeakSet } from '@glimmer/signals'; import { defineComponent, GlimmerishComponent as Component, diff --git a/packages/@glimmer-workspace/integration-tests/test/graph-links-test.ts b/packages/@glimmer-workspace/integration-tests/test/graph-links-test.ts new file mode 100644 index 00000000000..0a8217a11e8 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/graph-links-test.ts @@ -0,0 +1,74 @@ +import { consumeTag, createTag, dirtyTag, type TagNode } from '@glimmer/signals'; +import { + defineComponent, + GlimmerishComponent as Component, + jitSuite, + RenderTest, + test, +} from '@glimmer-workspace/integration-tests'; + +function hasSubscribers(tag: TagNode) { + return tag.subs !== undefined; +} + +class GraphLinksTest extends RenderTest { + static suiteName = `links of the reactive graph (rendering)`; + + @test + 'a block that is removed leaves no link on the tags that it read'(assert: Assert) { + const tag = createTag(); + const read = () => { + consumeTag(tag); + return 'value'; + }; + + const Inner = defineComponent({ read }, '

{{ (read) }}

'); + + this.render('{{#if this.show}}{{/if}}', { show: true, Inner }); + + this.assertHTML('

value

'); + assert.true(hasSubscribers(tag), 'the render subscribes to the tag'); + + this.rerender({ show: false }); + + this.assertHTML(''); + assert.false(hasSubscribers(tag), 'the removed block has no link'); + + this.rerender({ show: true }); + + this.assertHTML('

value

'); + assert.true(hasSubscribers(tag)); + } + + @test + 'a render that is destroyed leaves no link on the tags that it read'(assert: Assert) { + const tag = createTag(); + + class Reader extends Component { + get value() { + consumeTag(tag); + return 'value'; + } + } + + const Inner = defineComponent({}, '

{{this.value}}

', { definition: Reader }); + + this.render('{{#each this.list key="@index" as |item|}}{{item}}{{/each}}', { + list: [1, 2, 3], + Inner, + }); + + assert.true(hasSubscribers(tag), 'the render subscribes to the tag'); + + dirtyTag(tag); + this.rerender(); + + assert.true(hasSubscribers(tag), 'the render subscribes after an update'); + + this.destroy(); + + assert.false(hasSubscribers(tag), 'the destroyed render has no link'); + } +} + +jitSuite(GraphLinksTest); diff --git a/packages/@glimmer-workspace/integration-tests/test/package.json b/packages/@glimmer-workspace/integration-tests/test/package.json index 3db79747f72..e50eb80b691 100644 --- a/packages/@glimmer-workspace/integration-tests/test/package.json +++ b/packages/@glimmer-workspace/integration-tests/test/package.json @@ -19,7 +19,7 @@ "@glimmer/runtime": "workspace:*", "@glimmer/syntax": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "@glimmer/wire-format": "workspace:*", "@ember/helper": "workspace:*", "@ember/modifier": "workspace:*", diff --git a/packages/@glimmer-workspace/integration-tests/test/tracked-value-test.ts b/packages/@glimmer-workspace/integration-tests/test/tracked-value-test.ts index 58e6a7604cd..8c39e662cef 100644 --- a/packages/@glimmer-workspace/integration-tests/test/tracked-value-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/tracked-value-test.ts @@ -1,4 +1,4 @@ -import { trackedValue } from '@glimmer/validator'; +import { trackedValue } from '@glimmer/signals'; import { defineComponent, GlimmerishComponent as Component, diff --git a/packages/@glimmer-workspace/integration-tests/test/updating-test.ts b/packages/@glimmer-workspace/integration-tests/test/updating-test.ts index 46f2c627834..fbe622d1e8a 100644 --- a/packages/@glimmer-workspace/integration-tests/test/updating-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/updating-test.ts @@ -4,7 +4,7 @@ import type { JitRenderDelegate } from '@glimmer-workspace/integration-tests'; import { expect } from '@glimmer/debug-util'; import { associateDestroyableChild, registerDestructor } from '@glimmer/destroyable'; import { createComputeRef, createConstRef, createPrimitiveRef } from '@glimmer/reference'; -import { consumeTag, createTag, dirtyTag } from '@glimmer/validator'; +import { consumeTag, createTag, dirtyTag } from '@glimmer/signals'; import { assertNodeTagName, getElementByClassName, diff --git a/packages/@glimmer/interfaces/lib/references.d.ts b/packages/@glimmer/interfaces/lib/references.d.ts index 9bb1415eb57..6be78c9f850 100644 --- a/packages/@glimmer/interfaces/lib/references.d.ts +++ b/packages/@glimmer/interfaces/lib/references.d.ts @@ -1,5 +1,3 @@ -import type { Nullable } from './core.js'; - export type ConstantReference = 0; export type ComputeReference = 1; export type UnboundReference = 2; @@ -24,6 +22,6 @@ export type ReferenceSymbol = typeof REFERENCE; export interface Reference { [REFERENCE]: ReferenceType; debugLabel?: string | false | undefined; - compute: Nullable<() => T>; + value: T | undefined; children: null | Map; } diff --git a/packages/@glimmer/interfaces/lib/tags.d.ts b/packages/@glimmer/interfaces/lib/tags.d.ts index 00bf6227d27..4e77323b1ab 100644 --- a/packages/@glimmer/interfaces/lib/tags.d.ts +++ b/packages/@glimmer/interfaces/lib/tags.d.ts @@ -1,57 +1,15 @@ -declare const TYPE: unique symbol; -export type TagTypeSymbol = typeof TYPE; - -declare const COMPUTE: unique symbol; -export type TagComputeSymbol = typeof COMPUTE; - -export type DIRTYABLE_TAG_ID = 0; -export type UPDATABLE_TAG_ID = 1; -export type COMBINATOR_TAG_ID = 2; -export type CONSTANT_TAG_ID = 3; +export type Revision = number; /** - * This union represents all of the possible tag types for the monomorphic tag class. - * Other custom tag classes can exist, such as CurrentTag and VolatileTag, but for - * performance reasons, any type of tag that is meant to be used frequently should - * be added to the monomorphic tag. + * A node of the reactive graph. + * + * `@glimmer/signals` owns the fields. Other packages use its functions. */ -export type MonomorphicTagId = - | DIRTYABLE_TAG_ID - | UPDATABLE_TAG_ID - | COMBINATOR_TAG_ID - | CONSTANT_TAG_ID; - -export type VOLATILE_TAG_ID = 100; -export type CURRENT_TAG_ID = 101; - -export type PolymorphicTagId = VOLATILE_TAG_ID | CURRENT_TAG_ID; - -export type TagId = MonomorphicTagId | PolymorphicTagId; - -export type Revision = number; - export interface Tag { - readonly [TYPE]: TagId; - readonly subtag?: Tag | Tag[] | null | undefined; - [COMPUTE](): Revision; + readonly kind: number; } -export interface MonomorphicTag extends Tag { - readonly [TYPE]: MonomorphicTagId; -} - -export interface UpdatableTag extends MonomorphicTag { - readonly [TYPE]: UPDATABLE_TAG_ID; -} - -export interface DirtyableTag extends MonomorphicTag { - readonly [TYPE]: DIRTYABLE_TAG_ID; -} - -export interface ConstantTag extends MonomorphicTag { - readonly [TYPE]: CONSTANT_TAG_ID; -} - -export interface CombinatorTag extends MonomorphicTag { - readonly [TYPE]: COMBINATOR_TAG_ID; -} +export type UpdatableTag = Tag; +export type DirtyableTag = Tag; +export type ConstantTag = Tag; +export type CombinatorTag = Tag; diff --git a/packages/@glimmer/manager/lib/public/modifier.ts b/packages/@glimmer/manager/lib/public/modifier.ts index 84af7f9ee32..16a2270b946 100644 --- a/packages/@glimmer/manager/lib/public/modifier.ts +++ b/packages/@glimmer/manager/lib/public/modifier.ts @@ -15,8 +15,7 @@ import { registerDestructor } from '@glimmer/destroyable'; import { debugAssert } from '@glimmer/global-context'; import { valueForRef } from '@glimmer/reference/lib/reference'; import { dict } from '@glimmer/util/lib/collections'; -import { createUpdatableTag } from '@glimmer/validator/lib/validators'; -import { untrack } from '@glimmer/validator/lib/tracking'; +import { createTag, untrack } from '@glimmer/signals/lib/tags'; import type { ManagerFactory } from '.'; @@ -110,7 +109,7 @@ export class CustomModifierManager< let args = argsProxyFor(capturedArgs, 'modifier'); let instance: ModifierInstance = delegate.createModifier(definition, args); - let tag = createUpdatableTag(); + let tag = createTag(); let state: CustomModifierState; state = { diff --git a/packages/@glimmer/manager/lib/util/args-proxy.ts b/packages/@glimmer/manager/lib/util/args-proxy.ts index 88ec478855c..ed0b9923790 100644 --- a/packages/@glimmer/manager/lib/util/args-proxy.ts +++ b/packages/@glimmer/manager/lib/util/args-proxy.ts @@ -7,7 +7,7 @@ import type { } from '@glimmer/interfaces'; import type { Tag } from '@glimmer/interfaces'; import { valueForRef } from '@glimmer/reference/lib/reference'; -import { track } from '@glimmer/validator/lib/tracking'; +import { track } from '@glimmer/signals/lib/tags'; const CUSTOM_TAG_FOR = new WeakMap Tag>(); diff --git a/packages/@glimmer/manager/package.json b/packages/@glimmer/manager/package.json index 8e1631b5478..939551af8e2 100644 --- a/packages/@glimmer/manager/package.json +++ b/packages/@glimmer/manager/package.json @@ -42,7 +42,7 @@ "@glimmer/interfaces": "workspace:*", "@glimmer/reference": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "@glimmer/vm": "workspace:*" }, "devDependencies": { diff --git a/packages/@glimmer/manager/test/managers-test.ts b/packages/@glimmer/manager/test/managers-test.ts index 2d28b602635..369fcb8e46d 100644 --- a/packages/@glimmer/manager/test/managers-test.ts +++ b/packages/@glimmer/manager/test/managers-test.ts @@ -24,7 +24,7 @@ import { setModifierManager, } from '@glimmer/manager'; import { UNDEFINED_REFERENCE } from '@glimmer/reference'; -import { createUpdatableTag } from '@glimmer/validator'; +import { createTag } from '@glimmer/signals'; const { module, test } = QUnit; @@ -299,7 +299,7 @@ module('Managers', () => { } getTag() { - return createUpdatableTag(); + return createTag(); } install() {} diff --git a/packages/@glimmer/manager/test/package.json b/packages/@glimmer/manager/test/package.json index 9b53b62fc14..1e025515c3d 100644 --- a/packages/@glimmer/manager/test/package.json +++ b/packages/@glimmer/manager/test/package.json @@ -10,7 +10,7 @@ "@glimmer/interfaces": "workspace:*", "@glimmer/manager": "workspace:*", "@glimmer/reference": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "@glimmer/vm": "workspace:*" } } diff --git a/packages/@glimmer/reference/lib/iterable.ts b/packages/@glimmer/reference/lib/iterable.ts index 2c566c87520..cda86e161f4 100644 --- a/packages/@glimmer/reference/lib/iterable.ts +++ b/packages/@glimmer/reference/lib/iterable.ts @@ -3,8 +3,7 @@ import type { Nullable } from '@glimmer/interfaces'; import { getPath, toIterator } from '@glimmer/global-context'; import { EMPTY_ARRAY } from '@glimmer/util/lib/array-utils'; import { isIndexable } from '@glimmer/util/lib/collections'; -import { consumeTag } from '@glimmer/validator/lib/tracking'; -import { createTag, DIRTY_TAG as dirtyTag } from '@glimmer/validator/lib/validators'; +import { consumeTag, createTag, dirtyTag } from '@glimmer/signals/lib/tags'; import type { Reference, ReferenceEnvironment } from './reference'; diff --git a/packages/@glimmer/reference/lib/reference.ts b/packages/@glimmer/reference/lib/reference.ts index 6aacbaa1c08..4e75094ed13 100644 --- a/packages/@glimmer/reference/lib/reference.ts +++ b/packages/@glimmer/reference/lib/reference.ts @@ -9,14 +9,11 @@ import type { ReferenceType, UnboundReference, } from '@glimmer/interfaces'; -import type { Revision } from '@glimmer/validator/lib/validators'; -import type { Tag } from '@glimmer/interfaces'; +import { isConstComputed, readComputed, TagNode } from '@glimmer/signals/lib/tags'; import { expect } from '@glimmer/debug-util/lib/platform-utils'; import { DESTROYABLE_META_KEY } from '@glimmer/util/lib/destroyable-key'; import { getProp, setProp } from '@glimmer/global-context'; import { isDict } from '@glimmer/util/lib/collections'; -import { CONSTANT_TAG, INITIAL, validateTag, valueForTag } from '@glimmer/validator/lib/validators'; -import { consumeTag, track } from '@glimmer/validator/lib/tracking'; export const REFERENCE: ReferenceSymbol = Symbol('REFERENCE') as ReferenceSymbol; @@ -35,21 +32,33 @@ export interface ReferenceEnvironment { setProp(obj: unknown, path: string, value: unknown): unknown; } -class ReferenceImpl implements Reference { +/** + * The value of `kind` for a COMPUTED node, and the flags of a node that has a + * valid value. See `@glimmer/signals/lib/tags`. + */ +const COMPUTED_NODE = 2; +const NOT_EVALUATED = 0; +const EVALUATED = 1; + +/** + * A reference is a node of the reactive graph. + * + * - `fn` computes the value. + * - `value` holds the last value. + */ +class ReferenceImpl extends TagNode implements Reference { [DESTROYABLE_META_KEY]: object | undefined; [REFERENCE]: ReferenceType; - public tag: Nullable = null; - public lastRevision: Revision = INITIAL; - public lastValue?: T; public children: Nullable> = null; - public compute: Nullable<() => T> = null; public update: Nullable<(val: T) => void> = null; - public debugLabel?: string; + declare public debugLabel?: string; + declare public value: T | undefined; - constructor(type: ReferenceType) { + constructor(type: ReferenceType, flags: number) { + super(COMPUTED_NODE, flags); this[REFERENCE] = type; } } @@ -57,10 +66,9 @@ class ReferenceImpl implements Reference { export function createPrimitiveRef( value: T ): Reference { - const ref = new ReferenceImpl(UNBOUND); + const ref = new ReferenceImpl(UNBOUND, EVALUATED); - ref.tag = CONSTANT_TAG; - ref.lastValue = value; + ref.value = value; if (DEBUG) { ref.debugLabel = String(value); @@ -75,10 +83,9 @@ export const TRUE_REFERENCE = createPrimitiveRef(true as const); export const FALSE_REFERENCE = createPrimitiveRef(false as const); export function createConstRef(value: T, debugLabel: false | string): Reference { - const ref = new ReferenceImpl(CONSTANT); + const ref = new ReferenceImpl(CONSTANT, EVALUATED); - ref.lastValue = value; - ref.tag = CONSTANT_TAG; + ref.value = value; if (DEBUG) { ref.debugLabel = debugLabel as string; @@ -88,10 +95,9 @@ export function createConstRef(value: T, debugLabel: false | string): Referen } export function createUnboundRef(value: T, debugLabel: false | string): Reference { - const ref = new ReferenceImpl(UNBOUND); + const ref = new ReferenceImpl(UNBOUND, EVALUATED); - ref.lastValue = value; - ref.tag = CONSTANT_TAG; + ref.value = value; if (DEBUG) { ref.debugLabel = debugLabel as string; @@ -105,9 +111,9 @@ export function createComputeRef( update: Nullable<(value: T) => void> = null, debugLabel: false | string = 'unknown' ): Reference { - const ref = new ReferenceImpl(COMPUTE); + const ref = new ReferenceImpl(COMPUTE, NOT_EVALUATED); - ref.compute = compute; + ref.fn = compute; ref.update = update; if (DEBUG) { @@ -139,9 +145,7 @@ export function createInvokableRef(inner: Reference): Reference { } export function isConstRef(_ref: Reference) { - const ref = _ref as ReferenceImpl; - - return ref.tag === CONSTANT_TAG; + return isConstComputed(_ref as ReferenceImpl); } export function isUpdatableRef(_ref: Reference) { @@ -151,35 +155,7 @@ export function isUpdatableRef(_ref: Reference) { } export function valueForRef(_ref: Reference): T { - const ref = _ref as ReferenceImpl; - - let { tag } = ref; - - if (tag === CONSTANT_TAG) { - return ref.lastValue as T; - } - - const { lastRevision } = ref; - let lastValue; - - if (tag === null || !validateTag(tag, lastRevision)) { - const { compute } = ref; - - const newTag = track(() => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme - lastValue = ref.lastValue = compute!(); - }, DEBUG && ref.debugLabel); - - tag = ref.tag = newTag; - - ref.lastRevision = valueForTag(newTag); - } else { - lastValue = ref.lastValue; - } - - consumeTag(tag); - - return lastValue as T; + return readComputed(_ref as ReferenceImpl); } export function updateRef(_ref: Reference, value: unknown) { diff --git a/packages/@glimmer/reference/package.json b/packages/@glimmer/reference/package.json index 0722c6d7b84..c32dad82b80 100644 --- a/packages/@glimmer/reference/package.json +++ b/packages/@glimmer/reference/package.json @@ -41,7 +41,7 @@ "@glimmer/global-context": "workspace:*", "@glimmer/interfaces": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*" + "@glimmer/signals": "workspace:*" }, "devDependencies": { "@glimmer/debug-util": "workspace:*", diff --git a/packages/@glimmer/reference/test/iterable-test.ts b/packages/@glimmer/reference/test/iterable-test.ts index 5e97c466029..f209a0f5b6d 100644 --- a/packages/@glimmer/reference/test/iterable-test.ts +++ b/packages/@glimmer/reference/test/iterable-test.ts @@ -1,6 +1,6 @@ import type { OpaqueIterationItem, Reference } from '@glimmer/reference'; import { createComputeRef, createIteratorRef, valueForRef } from '@glimmer/reference'; -import { consumeTag, VOLATILE_TAG } from '@glimmer/validator'; +import { consumeTag, VOLATILE_TAG } from '@glimmer/signals'; import { module, test } from './utils/qunit'; diff --git a/packages/@glimmer/reference/test/package.json b/packages/@glimmer/reference/test/package.json index 2e4a5b8e507..d0652eb8867 100644 --- a/packages/@glimmer/reference/test/package.json +++ b/packages/@glimmer/reference/test/package.json @@ -7,7 +7,7 @@ "@glimmer/global-context": "workspace:*", "@glimmer/reference": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*" + "@glimmer/signals": "workspace:*" }, "devDependencies": { "@glimmer/debug-util": "workspace:*" diff --git a/packages/@glimmer/reference/test/references-test.ts b/packages/@glimmer/reference/test/references-test.ts index 7d46ac70c5d..72ceb542341 100644 --- a/packages/@glimmer/reference/test/references-test.ts +++ b/packages/@glimmer/reference/test/references-test.ts @@ -16,7 +16,7 @@ import { valueForRef, } from '@glimmer/reference'; import { dict } from '@glimmer/util'; -import { consumeTag, createTag, dirtyTag } from '@glimmer/validator'; +import { consumeTag, createTag, dirtyTag } from '@glimmer/signals'; import { tracked } from './support'; diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/-debug-strip.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/-debug-strip.ts index 0795d1c1507..7060b54875a 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/-debug-strip.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/-debug-strip.ts @@ -35,14 +35,13 @@ import { wrap, } from '@glimmer/debug/lib/stack-check'; import { REFERENCE, UNDEFINED_REFERENCE } from '@glimmer/reference/lib/reference'; -import { COMPUTE } from '@glimmer/validator/lib/validators'; import { ScopeImpl } from '../../scope'; import { VMArgumentsImpl } from '../../vm/arguments'; import { ComponentElementOperations } from './component'; export const CheckTag: Checker = CheckInterface({ - [COMPUTE]: CheckFunction, + kind: CheckNumber, }); export const CheckOperations: Checker> = wrap(() => diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts index 1976f099867..8c4f7aad34c 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts @@ -11,7 +11,13 @@ import type { UpdatingVM, } from '@glimmer/interfaces'; import type { Reference } from '@glimmer/reference/lib/reference'; -import type { Revision } from '@glimmer/validator/lib/validators'; +import { + consumeTag, + validateTag, + valueForTag, + type Revision, + INITIAL, +} from '@glimmer/signals/lib/tags'; import type { Tag } from '@glimmer/interfaces'; import { CURRIED_MODIFIER } from '@glimmer/constants/lib/curried'; import { @@ -42,8 +48,6 @@ import { associateDestroyableChild, destroy, registerDestructor } from '@glimmer import { getInternalModifierManager } from '@glimmer/manager/lib/internal/api'; import { createComputeRef, isConstRef, valueForRef } from '@glimmer/reference/lib/reference'; import { isIndexable } from '@glimmer/util/lib/collections'; -import { consumeTag } from '@glimmer/validator/lib/tracking'; -import { CURRENT_TAG, validateTag, valueForTag } from '@glimmer/validator/lib/validators'; import { $t0 } from '@glimmer/vm/lib/registers'; import type { CurriedValue } from '../../curried-value'; @@ -348,7 +352,7 @@ export class UpdateDynamicModifierOpcode implements UpdatingOpcode { private instance: ModifierInstance | undefined, private instanceRef: Reference ) { - this.lastUpdated = valueForTag(tag ?? CURRENT_TAG); + this.lastUpdated = tag === null ? INITIAL : valueForTag(tag); } evaluate(vm: UpdatingVM) { diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/vm.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/vm.ts index a505a3ff5b6..baa4ab4c3d5 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/vm.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/vm.ts @@ -1,7 +1,12 @@ import type { CompilableTemplate, Nullable, UpdatingOpcode } from '@glimmer/interfaces'; import type { Reference } from '@glimmer/reference/lib/reference'; -import type { Revision } from '@glimmer/validator/lib/validators'; -import type { Tag } from '@glimmer/interfaces'; +import { + beginFrame, + consumeFrame, + createFrame, + endFrame, + isFrameStale, +} from '@glimmer/signals/lib/tags'; import { decodeHandle, decodeImmediate, isHandle } from '@glimmer/constants/lib/immediate'; import { VM_ASSERT_SAME_OP, @@ -53,8 +58,6 @@ import { UNDEFINED_REFERENCE, valueForRef, } from '@glimmer/reference/lib/reference'; -import { beginTrackFrame, consumeTag, endTrackFrame } from '@glimmer/validator/lib/tracking'; -import { CONSTANT_TAG, INITIAL, validateTag, valueForTag } from '@glimmer/validator/lib/validators'; import type { UpdatingVM } from '../../vm'; import type { VM } from '../../vm/append'; @@ -284,36 +287,34 @@ export class AssertFilter implements UpdatingOpcode { } export class JumpIfNotModifiedOpcode implements UpdatingOpcode { - private tag: Tag = CONSTANT_TAG; - private lastRevision: Revision = INITIAL; + /** + * The subscriber for the opcodes between this opcode and `target`. + */ + readonly frame = createFrame(); private target?: number; - finalize(tag: Tag, target: number) { + finalize(target: number) { this.target = target; - this.didModify(tag); } evaluate(vm: UpdatingVM) { - let { tag, target, lastRevision } = this; + let { frame, target } = this; - if (!vm.alwaysRevalidate && validateTag(tag, lastRevision)) { - consumeTag(tag); + if (!vm.alwaysRevalidate && !isFrameStale(frame)) { + consumeFrame(frame); vm.goto(expect(target, 'VM BUG: Target must be set before attempting to jump')); } } - - didModify(tag: Tag) { - this.tag = tag; - this.lastRevision = valueForTag(this.tag); - consumeTag(tag); - } } export class BeginTrackFrameOpcode implements UpdatingOpcode { - constructor(private debugLabel?: string) {} + constructor( + private target: JumpIfNotModifiedOpcode, + private debugLabel?: string + ) {} evaluate() { - beginTrackFrame(this.debugLabel); + beginFrame(this.target.frame, this.debugLabel); } } @@ -321,7 +322,7 @@ export class EndTrackFrameOpcode implements UpdatingOpcode { constructor(private target: JumpIfNotModifiedOpcode) {} evaluate() { - let tag = endTrackFrame(); - this.target.didModify(tag); + endFrame(); + consumeFrame(this.target.frame); } } diff --git a/packages/@glimmer/runtime/lib/environment.ts b/packages/@glimmer/runtime/lib/environment.ts index bafbefe2f82..b6d0d1b3b85 100644 --- a/packages/@glimmer/runtime/lib/environment.ts +++ b/packages/@glimmer/runtime/lib/environment.ts @@ -16,8 +16,7 @@ import type { import { expect } from '@glimmer/debug-util/lib/platform-utils'; import assert from '@glimmer/debug-util/lib/assert'; import { ProgramImpl } from '@glimmer/program/lib/program'; -import { track } from '@glimmer/validator/lib/tracking'; -import { UPDATE_TAG as updateTag } from '@glimmer/validator/lib/validators'; +import { track, updateTag } from '@glimmer/signals/lib/tags'; import DebugRenderTree from './debug-render-tree'; import { DOMChangesImpl, DOMTreeConstruction } from './dom/helper'; diff --git a/packages/@glimmer/runtime/lib/helpers/invoke.ts b/packages/@glimmer/runtime/lib/helpers/invoke.ts index e82e44e28c3..34d8cf8b3c5 100644 --- a/packages/@glimmer/runtime/lib/helpers/invoke.ts +++ b/packages/@glimmer/runtime/lib/helpers/invoke.ts @@ -1,11 +1,10 @@ import { DEBUG } from '@glimmer/env'; import type { Arguments, InternalHelperManager } from '@glimmer/interfaces'; -import type { Cache } from '@glimmer/validator/lib/tracking'; +import { createCache, getValue, type Cache } from '@glimmer/signals/lib/tags'; import { associateDestroyableChild, isDestroyed, isDestroying } from '@glimmer/destroyable'; import { getInternalHelperManager } from '@glimmer/manager/lib/internal/api'; import { hasDestroyable, hasValue } from '@glimmer/manager/lib/public/helper'; import { getOwner } from '@glimmer/owner'; -import { createCache, getValue } from '@glimmer/validator/lib/tracking'; import { EMPTY_ARGS, EMPTY_NAMED, EMPTY_POSITIONAL } from '../vm/arguments'; diff --git a/packages/@glimmer/runtime/lib/modifiers/on.ts b/packages/@glimmer/runtime/lib/modifiers/on.ts index e8c38554895..9742a6a9cf5 100644 --- a/packages/@glimmer/runtime/lib/modifiers/on.ts +++ b/packages/@glimmer/runtime/lib/modifiers/on.ts @@ -18,7 +18,7 @@ import buildUntouchableThis from '@glimmer/debug-util/lib/untouchable-this'; import { registerDestructor } from '@glimmer/destroyable'; import { setInternalModifierManager } from '@glimmer/manager/lib/internal/api'; import { valueForRef } from '@glimmer/reference/lib/reference'; -import { createUpdatableTag } from '@glimmer/validator/lib/validators'; +import { createTag } from '@glimmer/signals/lib/tags'; import { reifyNamed } from '../vm/arguments'; @@ -35,7 +35,7 @@ interface Listener { } export class OnModifierState { - public tag = createUpdatableTag(); + public tag = createTag(); public element: Element; public args: CapturedArguments; public listener: Listener | null = null; diff --git a/packages/@glimmer/runtime/lib/render.ts b/packages/@glimmer/runtime/lib/render.ts index 7061473ebe7..80a9d841d25 100644 --- a/packages/@glimmer/runtime/lib/render.ts +++ b/packages/@glimmer/runtime/lib/render.ts @@ -16,25 +16,80 @@ import { dev, expect } from '@glimmer/debug-util/lib/platform-utils'; import { unwrapHandle } from '@glimmer/debug-util/lib/template'; import { LOCAL_DEBUG } from '@glimmer/local-debug-flags'; import { childRefFor, createConstRef } from '@glimmer/reference/lib/reference'; -import { debug } from '@glimmer/validator/lib/debug'; +import { debug } from '@glimmer/signals/lib/debug'; +import { + abandonFrame, + beginFrame, + consumeFrame, + createFrame, + endFrame, +} from '@glimmer/signals/lib/tags'; +import type { TagNode } from '@glimmer/signals/lib/tags'; + +import type RenderResultImpl from './vm/render-result'; import { inTransaction } from './environment'; import { DynamicScopeImpl } from './scope'; import { VM } from './vm/append'; class TemplateIteratorImpl implements TemplateIterator { + /** + * The frame for the initial render. The render result keeps it. + */ + #root: TagNode | undefined = undefined; + constructor(private vm: VM) {} + next(): RichIteratorResult { - return this.vm.next(); + let root = this.#root; + + if (root === undefined) { + root = this.#root = createFrame(); + beginFrame(root); + } + + let done = false; + + try { + let result = this.vm.next(); + done = true; + + if (result.done) { + endFrame(); + consumeFrame(root); + (result.value as RenderResultImpl).root = root; + } + + return result; + } finally { + if (!done) abandonFrame(root); + } } sync(): RenderResult { - if (DEBUG) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme - return debug.runInTrackingTransaction!(() => this.vm.execute(), '- While rendering:'); - } else { - return this.vm.execute(); + let root = (this.#root = createFrame()); + let result: RenderResult | undefined; + + beginFrame(root); + + try { + if (DEBUG) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme + result = debug.runInTrackingTransaction!(() => this.vm.execute(), '- While rendering:'); + } else { + result = this.vm.execute(); + } + } finally { + if (result === undefined) { + abandonFrame(root); + } else { + endFrame(); + consumeFrame(root); + (result as RenderResultImpl).root = root; + } } + + return result; } } diff --git a/packages/@glimmer/runtime/lib/vm/append.ts b/packages/@glimmer/runtime/lib/vm/append.ts index b27b704785b..352bce90978 100644 --- a/packages/@glimmer/runtime/lib/vm/append.ts +++ b/packages/@glimmer/runtime/lib/vm/append.ts @@ -34,7 +34,7 @@ import { UNDEFINED_REFERENCE } from '@glimmer/reference/lib/reference'; import { reverse } from '@glimmer/util/lib/array-utils'; import { StackImpl as Stack } from '@glimmer/util/lib/collections'; import { LOCAL_LOGGER } from '@glimmer/util'; -import { beginTrackFrame, endTrackFrame, resetTracking } from '@glimmer/validator/lib/tracking'; +import { beginFrame, consumeFrame, endFrame, resetTracking } from '@glimmer/signals/lib/tags'; import { $pc, isLowLevelRegister } from '@glimmer/vm/lib/registers'; import type { ScopeOptions } from '../scope'; @@ -353,10 +353,10 @@ export class VM { let guard = new JumpIfNotModifiedOpcode(); opcodes.push(guard); - opcodes.push(new BeginTrackFrameOpcode(name)); + opcodes.push(new BeginTrackFrameOpcode(guard, name)); this.#stacks.cache.push(guard); - beginTrackFrame(name); + beginFrame(guard.frame, name); } /** @@ -377,10 +377,11 @@ export class VM { let opcodes = this.updating(); let guard = expect(this.#stacks.cache.pop(), 'VM BUG: Expected a cache group'); - let tag = endTrackFrame(); + endFrame(); + consumeFrame(guard.frame); opcodes.push(new EndTrackFrameOpcode(guard)); - guard.finalize(tag, opcodes.length); + guard.finalize(opcodes.length); } /** diff --git a/packages/@glimmer/runtime/lib/vm/arguments.ts b/packages/@glimmer/runtime/lib/vm/arguments.ts index f09b81c5ead..316353b214d 100644 --- a/packages/@glimmer/runtime/lib/vm/arguments.ts +++ b/packages/@glimmer/runtime/lib/vm/arguments.ts @@ -35,7 +35,7 @@ import { } from '@glimmer/reference/lib/reference'; import { dict } from '@glimmer/util/lib/collections'; import { EMPTY_STRING_ARRAY, emptyArray, enumerate } from '@glimmer/util/lib/array-utils'; -import { CONSTANT_TAG } from '@glimmer/validator/lib/validators'; +import { CONSTANT_TAG } from '@glimmer/signals/lib/tags'; import { $sp } from '@glimmer/vm/lib/registers'; import type { EvaluationStack } from './stack'; diff --git a/packages/@glimmer/runtime/lib/vm/render-result.ts b/packages/@glimmer/runtime/lib/vm/render-result.ts index a2696846909..479e330b9fa 100644 --- a/packages/@glimmer/runtime/lib/vm/render-result.ts +++ b/packages/@glimmer/runtime/lib/vm/render-result.ts @@ -9,6 +9,15 @@ import type { import { unreachable } from '@glimmer/debug-util/lib/platform-utils'; import { associateDestroyableChild, registerDestructor } from '@glimmer/destroyable'; import { DESTROYABLE_META_KEY } from '@glimmer/util/lib/destroyable-key'; +import { + abandonFrame, + beginFrame, + consumeFrame, + createFrame, + disposeFrame, + endFrame, +} from '@glimmer/signals/lib/tags'; +import type { TagNode } from '@glimmer/signals/lib/tags'; import { clear } from '../bounds'; import { UpdatingVM } from './update'; @@ -16,6 +25,15 @@ import { UpdatingVM } from './update'; export default class RenderResultImpl implements RenderResult { [DESTROYABLE_META_KEY]: object | undefined; + /** + * The subscriber for all reads of this render. + * + * Each block and each reference of the render has a path of links to this + * frame. When the result is destroyed, the frame drops its links, and that + * removes the links from the tags of the application to the render. + */ + root: TagNode | undefined = undefined; + constructor( public env: Environment, private updating: UpdatingOpcode[], @@ -23,13 +41,31 @@ export default class RenderResultImpl implements RenderResult { readonly drop: object ) { associateDestroyableChild(this, drop); - registerDestructor(this, () => clear(this.bounds)); + registerDestructor(this, () => { + clear(this.bounds); + if (this.root !== undefined) disposeFrame(this.root); + }); } rerender({ alwaysRevalidate = false } = { alwaysRevalidate: false }) { let { env, updating } = this; + let root = (this.root ??= createFrame()); let vm = new UpdatingVM(env, { alwaysRevalidate }); - vm.execute(updating, this); + let done = false; + + beginFrame(root); + + try { + vm.execute(updating, this); + done = true; + } finally { + if (done) { + endFrame(); + consumeFrame(root); + } else { + abandonFrame(root); + } + } } parentElement(): SimpleElement { diff --git a/packages/@glimmer/runtime/lib/vm/update.ts b/packages/@glimmer/runtime/lib/vm/update.ts index ca239649fd3..330d7d27179 100644 --- a/packages/@glimmer/runtime/lib/vm/update.ts +++ b/packages/@glimmer/runtime/lib/vm/update.ts @@ -23,8 +23,8 @@ import { LOCAL_DEBUG } from '@glimmer/local-debug-flags'; import { updateRef, valueForRef } from '@glimmer/reference/lib/reference'; import { logStep } from '@glimmer/util/lib/debug-steps'; import { StackImpl as Stack } from '@glimmer/util/lib/collections'; -import { debug } from '@glimmer/validator/lib/debug'; -import { resetTracking } from '@glimmer/validator/lib/tracking'; +import { debug } from '@glimmer/signals/lib/debug'; +import { resetTracking } from '@glimmer/signals/lib/tags'; import type { Closure } from './append'; import type { AppendingBlockList } from './element-builder'; diff --git a/packages/@glimmer/runtime/package.json b/packages/@glimmer/runtime/package.json index 58059fdb7f7..cc92af8f069 100644 --- a/packages/@glimmer/runtime/package.json +++ b/packages/@glimmer/runtime/package.json @@ -46,7 +46,7 @@ "@glimmer/program": "workspace:*", "@glimmer/reference": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "@glimmer/vm": "workspace:*" }, "devDependencies": { diff --git a/packages/@glimmer/validator/.npmignore b/packages/@glimmer/signals/.npmignore similarity index 100% rename from packages/@glimmer/validator/.npmignore rename to packages/@glimmer/signals/.npmignore diff --git a/packages/@glimmer/validator/index.ts b/packages/@glimmer/signals/index.ts similarity index 52% rename from packages/@glimmer/validator/index.ts rename to packages/@glimmer/signals/index.ts index 920b34148d7..261cc7fcafb 100644 --- a/packages/@glimmer/validator/index.ts +++ b/packages/@glimmer/signals/index.ts @@ -1,12 +1,12 @@ -const GLIMMER_VALIDATOR_REGISTRATION = Symbol('GLIMMER_VALIDATOR_REGISTRATION'); +const GLIMMER_SIGNALS_REGISTRATION = Symbol('GLIMMER_SIGNALS_REGISTRATION'); -if (Reflect.has(globalThis, GLIMMER_VALIDATOR_REGISTRATION)) { +if (Reflect.has(globalThis, GLIMMER_SIGNALS_REGISTRATION)) { throw new Error( - 'The `@glimmer/validator` library has been included twice in this application. It could be different versions of the package, or the same version included twice by mistake. `@glimmer/validator` depends on having a single copy of the package in use at any time in an application, even if they are the same version. You must dedupe your build to remove the duplicate packages in order to prevent this error.' + 'The `@glimmer/signals` library has been included twice in this application. It could be different versions of the package, or the same version included twice by mistake. `@glimmer/signals` depends on having a single copy of the package in use at any time in an application, even if they are the same version. You must dedupe your build to remove the duplicate packages in order to prevent this error.' ); } -Reflect.set(globalThis, GLIMMER_VALIDATOR_REGISTRATION, true); +Reflect.set(globalThis, GLIMMER_SIGNALS_REGISTRATION, true); export { trackedArray } from './lib/collections/array'; export { trackedMap } from './lib/collections/map'; @@ -15,7 +15,7 @@ export { trackedSet } from './lib/collections/set'; export { trackedWeakMap } from './lib/collections/weak-map'; export { trackedWeakSet } from './lib/collections/weak-set'; export { debug } from './lib/debug'; -export { dirtyTagFor, tagFor, type TagMeta, tagMetaFor } from './lib/meta'; +export { dirtyTagFor, releaseTagsFor, tagFor, type TagMeta, tagMetaFor } from './lib/meta'; export { trackedData } from './lib/tracked-data'; export { type Reactive, @@ -24,42 +24,47 @@ export { trackedValue, } from './lib/tracked-value'; export { + abandonFrame, + beginFrame, beginTrackFrame, beginUntrackFrame, + bump, type Cache, + combine, + CONSTANT, + CONSTANT_TAG, + consumeFrame, consumeTag, createCache, + createComputed, + createFrame, + createTag, + currentRevision, + dirtyTag, + disposeFrame, + endFrame, endTrackFrame, endUntrackFrame, getValue, + INITIAL, isConst, + isConstComputed, + isConstTag, + isFrameStale, isTracking, + readComputed, + releaseTag, resetTracking, + type Revision, + TagNode, track, untrack, -} from './lib/tracking'; -export { - ALLOW_CYCLES, - bump, - combine, - COMPUTE, - CONSTANT, - CONSTANT_TAG, - createTag, - createUpdatableTag, - CURRENT_TAG, - CurrentTag, - DIRTY_TAG as dirtyTag, - INITIAL, - isConstTag, - type Revision, - UPDATE_TAG as updateTag, + updateTag, validateTag, valueForTag, VOLATILE, VOLATILE_TAG, - VolatileTag, -} from './lib/validators'; +} from './lib/tags'; export type { CombinatorTag, ConstantTag, diff --git a/packages/@glimmer/validator/lib/collections/array.ts b/packages/@glimmer/signals/lib/collections/array.ts similarity index 95% rename from packages/@glimmer/validator/lib/collections/array.ts rename to packages/@glimmer/signals/lib/collections/array.ts index 325acad042d..2eff583301f 100644 --- a/packages/@glimmer/validator/lib/collections/array.ts +++ b/packages/@glimmer/signals/lib/collections/array.ts @@ -6,8 +6,7 @@ // and it will blow up in JS in exactly the same way, so it is safe to assume // that properties within the getter have the correct type in TS. -import { consumeTag } from '../tracking'; -import { createUpdatableTag, DIRTY_TAG } from '../validators'; +import { consumeTag, createTag, dirtyTag } from '../tags'; const ARRAY_GETTER_METHODS = new Set([ Symbol.iterator, @@ -152,15 +151,15 @@ class TrackedArray { }) as TrackedArray; } - #collection = createUpdatableTag(); + #collection = createTag(); - #storages = new Map>(); + #storages = new Map>(); #readStorageFor(index: number) { let storage = this.#storages.get(index); if (storage === undefined) { - storage = createUpdatableTag(); + storage = createTag(); this.#storages.set(index, storage); } @@ -171,12 +170,12 @@ class TrackedArray { const storage = this.#storages.get(index); if (storage) { - DIRTY_TAG(storage); + dirtyTag(storage); } } #dirtyCollection() { - DIRTY_TAG(this.#collection); + dirtyTag(this.#collection); this.#storages.clear(); } } diff --git a/packages/@glimmer/validator/lib/collections/map.ts b/packages/@glimmer/signals/lib/collections/map.ts similarity index 88% rename from packages/@glimmer/validator/lib/collections/map.ts rename to packages/@glimmer/signals/lib/collections/map.ts index feee8099d8f..2161d664e22 100644 --- a/packages/@glimmer/validator/lib/collections/map.ts +++ b/packages/@glimmer/signals/lib/collections/map.ts @@ -3,10 +3,9 @@ // interface (like getOrInsert, getOrInsertComputed, etc.) are automatically // supported without needing to manually re-implement each one. -import { consumeTag } from '../tracking'; -import { createUpdatableTag, DIRTY_TAG } from '../validators'; +import { consumeTag, createTag, dirtyTag } from '../tags'; -type Tag = ReturnType; +type Tag = ReturnType; export function trackedMap( data?: @@ -21,14 +20,14 @@ export function trackedMap( // constructor for the no-value constructor. This resolves that. const target: Map = data instanceof Map ? new Map(data.entries()) : new Map(data ?? []); - const collection = createUpdatableTag(); + const collection = createTag(); const storages = new Map(); function storageFor(key: Key): Tag { let storage = storages.get(key); if (storage === undefined) { - storage = createUpdatableTag(); + storage = createTag(); storages.set(key, storage); } @@ -39,7 +38,7 @@ export function trackedMap( const storage = storages.get(key); if (storage) { - DIRTY_TAG(storage); + dirtyTag(storage); } } @@ -58,7 +57,7 @@ export function trackedMap( } dirtyStorageFor(key); - DIRTY_TAG(collection); + dirtyTag(collection); target.set(key, value); @@ -71,7 +70,7 @@ export function trackedMap( if (!target.has(key)) return false; dirtyStorageFor(key); - DIRTY_TAG(collection); + dirtyTag(collection); storages.delete(key); return target.delete(key); @@ -82,10 +81,10 @@ export function trackedMap( return function (): void { if (target.size === 0) return; - storages.forEach((s) => DIRTY_TAG(s)); + storages.forEach((s) => dirtyTag(s)); storages.clear(); - DIRTY_TAG(collection); + dirtyTag(collection); target.clear(); }; } diff --git a/packages/@glimmer/validator/lib/collections/object.ts b/packages/@glimmer/signals/lib/collections/object.ts similarity index 91% rename from packages/@glimmer/validator/lib/collections/object.ts rename to packages/@glimmer/signals/lib/collections/object.ts index 2c9948db36e..229aa9f76cb 100644 --- a/packages/@glimmer/validator/lib/collections/object.ts +++ b/packages/@glimmer/signals/lib/collections/object.ts @@ -1,18 +1,17 @@ import type { ReactiveOptions } from './types'; -import { consumeTag } from '../tracking'; -import { createUpdatableTag, DIRTY_TAG } from '../validators'; +import { consumeTag, createTag, dirtyTag } from '../tags'; class TrackedObject> { #options: ReactiveOptions; - #storages = new Map>(); - #collection = createUpdatableTag(); + #storages = new Map>(); + #collection = createTag(); #readStorageFor(key: PropertyKey) { let storage = this.#storages.get(key); if (storage === undefined) { - storage = createUpdatableTag(); + storage = createTag(); this.#storages.set(key, storage); } @@ -23,12 +22,12 @@ class TrackedObject> { const storage = this.#storages.get(key); if (storage) { - DIRTY_TAG(storage); + dirtyTag(storage); } } #dirtyCollection() { - DIRTY_TAG(this.#collection); + dirtyTag(this.#collection); } /** diff --git a/packages/@glimmer/validator/lib/collections/set.ts b/packages/@glimmer/signals/lib/collections/set.ts similarity index 86% rename from packages/@glimmer/validator/lib/collections/set.ts rename to packages/@glimmer/signals/lib/collections/set.ts index ad912509fe5..6f40ba227d6 100644 --- a/packages/@glimmer/validator/lib/collections/set.ts +++ b/packages/@glimmer/signals/lib/collections/set.ts @@ -3,10 +3,9 @@ // interface are automatically supported without needing to manually // re-implement each one. -import { consumeTag } from '../tracking'; -import { createUpdatableTag, DIRTY_TAG } from '../validators'; +import { consumeTag, createTag, dirtyTag } from '../tags'; -type Tag = ReturnType; +type Tag = ReturnType; export function trackedSet( data?: Set | Value[] | Iterable | null, @@ -14,14 +13,14 @@ export function trackedSet( ): Set { const equals = options?.equals ?? Object.is; const target = new Set(data ?? []); - const collection = createUpdatableTag(); + const collection = createTag(); const storages = new Map(); function storageFor(key: Value): Tag { let storage = storages.get(key); if (storage === undefined) { - storage = createUpdatableTag(); + storage = createTag(); storages.set(key, storage); } @@ -32,7 +31,7 @@ export function trackedSet( const storage = storages.get(key); if (storage) { - DIRTY_TAG(storage); + dirtyTag(storage); } } @@ -44,7 +43,7 @@ export function trackedSet( const isUnchanged = equals(value, value); if (isUnchanged) return proxy; } else { - DIRTY_TAG(collection); + dirtyTag(collection); } dirtyStorageFor(value); @@ -60,7 +59,7 @@ export function trackedSet( if (!target.has(value)) return false; dirtyStorageFor(value); - DIRTY_TAG(collection); + dirtyTag(collection); storages.delete(value); return target.delete(value); @@ -71,8 +70,8 @@ export function trackedSet( return function (): void { if (target.size === 0) return; - storages.forEach((s) => DIRTY_TAG(s)); - DIRTY_TAG(collection); + storages.forEach((s) => dirtyTag(s)); + dirtyTag(collection); storages.clear(); target.clear(); diff --git a/packages/@glimmer/validator/lib/collections/types.ts b/packages/@glimmer/signals/lib/collections/types.ts similarity index 100% rename from packages/@glimmer/validator/lib/collections/types.ts rename to packages/@glimmer/signals/lib/collections/types.ts diff --git a/packages/@glimmer/validator/lib/collections/weak-map.ts b/packages/@glimmer/signals/lib/collections/weak-map.ts similarity index 92% rename from packages/@glimmer/validator/lib/collections/weak-map.ts rename to packages/@glimmer/signals/lib/collections/weak-map.ts index b4e174647b3..685d6e1fb1a 100644 --- a/packages/@glimmer/validator/lib/collections/weak-map.ts +++ b/packages/@glimmer/signals/lib/collections/weak-map.ts @@ -2,10 +2,9 @@ // interface (like getOrInsert, getOrInsertComputed, etc.) are automatically // supported without needing to manually re-implement each one. -import { consumeTag } from '../tracking'; -import { createUpdatableTag, DIRTY_TAG } from '../validators'; +import { consumeTag, createTag, dirtyTag } from '../tags'; -type Tag = ReturnType; +type Tag = ReturnType; export function trackedWeakMap( data?: WeakMap | [Key, Value][] | Iterable | null, @@ -25,7 +24,7 @@ export function trackedWeakMap( let storage = storages.get(key); if (storage === undefined) { - storage = createUpdatableTag(); + storage = createTag(); storages.set(key, storage); } @@ -36,7 +35,7 @@ export function trackedWeakMap( const storage = storages.get(key); if (storage) { - DIRTY_TAG(storage); + dirtyTag(storage); } } diff --git a/packages/@glimmer/validator/lib/collections/weak-set.ts b/packages/@glimmer/signals/lib/collections/weak-set.ts similarity index 93% rename from packages/@glimmer/validator/lib/collections/weak-set.ts rename to packages/@glimmer/signals/lib/collections/weak-set.ts index bcc64a82766..1056df10c37 100644 --- a/packages/@glimmer/validator/lib/collections/weak-set.ts +++ b/packages/@glimmer/signals/lib/collections/weak-set.ts @@ -2,10 +2,9 @@ // interface are automatically supported without needing to manually // re-implement each one. -import { consumeTag } from '../tracking'; -import { createUpdatableTag, DIRTY_TAG } from '../validators'; +import { consumeTag, createTag, dirtyTag } from '../tags'; -type Tag = ReturnType; +type Tag = ReturnType; /** * NOTE: we cannot pass a WeakSet because WeakSets are not iterable @@ -26,7 +25,7 @@ export function trackedWeakSet( let storage = storages.get(key); if (storage === undefined) { - storage = createUpdatableTag(); + storage = createTag(); storages.set(key, storage); } @@ -37,7 +36,7 @@ export function trackedWeakSet( const storage = storages.get(key); if (storage) { - DIRTY_TAG(storage); + dirtyTag(storage); } } diff --git a/packages/@glimmer/validator/lib/debug.ts b/packages/@glimmer/signals/lib/debug.ts similarity index 91% rename from packages/@glimmer/validator/lib/debug.ts rename to packages/@glimmer/signals/lib/debug.ts index 154acaee33a..e191f505b0f 100644 --- a/packages/@glimmer/validator/lib/debug.ts +++ b/packages/@glimmer/signals/lib/debug.ts @@ -182,17 +182,28 @@ if (DEBUG) { CONSUMED_TAGS.set(_tag, getLast(asPresentArray(TRANSACTION_STACK))); - // We need to mark the tag and all of its subtags as consumed, so we need to - // cast it and access its internals. In the future this shouldn't be necessary, - // this is only for computed properties. - let subtag = (_tag as unknown as { subtag: Tag | Tag[] | null }).subtag; - - if (!subtag || !debug.markTagAsConsumed) return; + /** + * A write to a tag below `_tag` also counts as a write after a read. + * + * The fields belong to `TagNode` in `tags.ts`. + */ + let node = _tag as unknown as { + kind: number; + tags: Tag[] | undefined; + deps: { dep: Tag; nextDep: unknown } | undefined; + peer: Tag | undefined; + }; + + let mark = debug.markTagAsConsumed!; + + node.tags?.forEach(mark); + + for (let dep = node.deps; dep !== undefined; dep = dep.nextDep as typeof dep) { + mark(dep.dep); + } - if (Array.isArray(subtag)) { - subtag.forEach(debug.markTagAsConsumed); - } else { - debug.markTagAsConsumed(subtag); + if (node.kind === 0 && node.peer !== undefined) { + mark(node.peer); } }; diff --git a/packages/@glimmer/validator/lib/meta.ts b/packages/@glimmer/signals/lib/meta.ts similarity index 71% rename from packages/@glimmer/validator/lib/meta.ts rename to packages/@glimmer/signals/lib/meta.ts index 56e30cc7be1..99240828207 100644 --- a/packages/@glimmer/validator/lib/meta.ts +++ b/packages/@glimmer/signals/lib/meta.ts @@ -1,11 +1,11 @@ import { DEBUG } from '@glimmer/env'; -import type { ConstantTag, UpdatableTag } from '@glimmer/interfaces'; +import type { Tag } from '@glimmer/interfaces'; import type { Indexable } from './utils'; import { debug } from './debug'; import { unwrap } from './utils'; -import { createUpdatableTag, DIRTY_TAG } from './validators'; +import { createTag, dirtyTag, releaseTag } from './tags'; function isObjectLike(u: T): u is Indexable & T { return (typeof u === 'object' && u !== null) || typeof u === 'function'; @@ -13,7 +13,7 @@ function isObjectLike(u: T): u is Indexable & T { /////////// -export type TagMeta = Map; +export type TagMeta = Map; const TRACKED_TAGS = new WeakMap(); @@ -39,7 +39,7 @@ export function dirtyTagFor( unwrap(debug.assertTagNotConsumed)(propertyTag, obj, key); } - DIRTY_TAG(propertyTag, true); + dirtyTag(propertyTag, true); } } @@ -59,14 +59,28 @@ export function tagFor( obj: T, key: keyof T | string | symbol, meta?: TagMeta -): UpdatableTag | ConstantTag { +): Tag { let tags = meta === undefined ? tagMetaFor(obj) : meta; let tag = tags.get(key); if (tag === undefined) { - tag = createUpdatableTag(); + tag = createTag(); tags.set(key, tag); } return tag; } + +/** + * Call this function when `obj` will get no more reads. + * + * A tag of `obj` that follows other tags has links from those tags. The links + * keep memory, and each write to those tags visits them. + */ +export function releaseTagsFor(obj: object): void { + let tags = TRACKED_TAGS.get(obj); + + if (tags !== undefined) { + tags.forEach(releaseTag); + } +} diff --git a/packages/@glimmer/signals/lib/tags.ts b/packages/@glimmer/signals/lib/tags.ts new file mode 100644 index 00000000000..2f21c22fefd --- /dev/null +++ b/packages/@glimmer/signals/lib/tags.ts @@ -0,0 +1,920 @@ +import { DEBUG } from '@glimmer/env'; +import type { Tag } from '@glimmer/interfaces'; +import { scheduleRevalidate } from '@glimmer/global-context'; +import { createReactiveSystem } from 'alien-signals/system'; +import type { Link, ReactiveNode } from 'alien-signals/system'; + +import { debug } from './debug'; +import { unwrap } from './utils'; + +export type Revision = number; + +export const CONSTANT: Revision = 0; +export const INITIAL: Revision = 1; +export const VOLATILE: Revision = NaN; + +let $REVISION = INITIAL; + +/** + * The revision of the last write to a tag. + * + * A caller can keep this number to learn later if a write occurred. + */ +export function currentRevision(): Revision { + return $REVISION; +} + +export function bump(): void { + $REVISION++; +} + +/** + * The flag values of `alien-signals/system`. + * + * The library exports them as an object. Constants let the minifier inline them. + */ +const MUTABLE = 1; +const WATCHING = 2; +const RECURSED_CHECK = 4; +const DIRTY = 16; +const PENDING = 32; +const STALE = DIRTY | PENDING; + +/** + * - TAG: a value that code can write. It has subscribers and no dependencies. + * - LIST: a set of tags. It is not in the graph. A check reads the revisions. + * - COMPUTED: a cached function (`createCache`, compute references). + * - FRAME: a block of updating opcodes that the VM runs again when it is stale. + * - WATCHER: forwards the writes of its dependencies to one TAG (`updateTag`). + * - COLLECTOR: an open `track()` frame. It becomes a LIST at the end. + */ +const TAG_KIND = 0; +const LIST_KIND = 1; +const COMPUTED_KIND = 2; +const FRAME_KIND = 3; +const WATCHER_KIND = 4; +const COLLECTOR_KIND = 5; +const CONSTANT_KIND = 6; +const VOLATILE_KIND = 7; + +/** + * One class for all kinds keeps the graph walk of alien-signals monomorphic. + */ +export class TagNode implements ReactiveNode, Tag { + deps: Link | undefined = undefined; + depsTail: Link | undefined = undefined; + subs: Link | undefined = undefined; + subsTail: Link | undefined = undefined; + flags: number; + kind: number; + + /** + * - TAG: the revision of the last write. + * - LIST: the revision that the last check found. + * - COMPUTED without subscribers: the revision at which the value was valid. + */ + revision: Revision = INITIAL; + + /** + * - LIST, COLLECTOR: the members. + * - COMPUTED without subscribers: the tags that the last run read. + */ + tags: TagNode[] | undefined = undefined; + + /** + * - TAG: its watcher. + * - WATCHER: its tag. + */ + peer: TagNode | undefined = undefined; + + /** + * - COLLECTOR: its id. + * - other kinds: the id of the last collector that took this tag. + * - LIST: the revision of the last check. + */ + mark = 0; + + /** + * `true` when the last run read `VOLATILE_TAG`. Such a node is always stale. + */ + volatile = false; + + /** + * COMPUTED, FRAME: the answer of the last `valueForTag`, and its revision. + * + * Without it, each cache in a chain walks the full chain, + * when nobody subscribes to the chain. + */ + checked = -1; + max: Revision = CONSTANT; + + fn: (() => unknown) | undefined = undefined; + value: unknown = undefined; + debugLabel?: string | false | undefined; + + constructor(kind: number, flags: number) { + this.kind = kind; + this.flags = flags; + } +} + +let activeSub: TagNode | undefined = undefined; +const SUB_STACK: (TagNode | undefined)[] = []; + +/** + * alien-signals uses this number to find a dependency that a run links twice. + */ +let cycle = 0; + +let collectorId = 0; + +const FIRED: TagNode[] = []; + +const { link, unlink, propagate } = createReactiveSystem({ + /** + * `checkDirty` calls this function, and this module does not call `checkDirty`. + * + * No node has an equality check, so a pending node is always a stale node. + */ + update() { + return true; + }, + + notify(watcher: ReactiveNode) { + FIRED.push(watcher as TagNode); + }, + + unwatched(node: ReactiveNode) { + let tag = node as TagNode; + + if (tag.kind === COMPUTED_KIND) { + park(tag); + } else if (tag.kind === FRAME_KIND) { + disposeDeps(tag); + tag.flags = MUTABLE | DIRTY; + } + }, +}); + +function disposeDeps(sub: TagNode): void { + let current = sub.depsTail; + + while (current !== undefined) { + let prev = current.prevDep; + unlink(current, sub); + current = prev; + } +} + +function purgeDeps(sub: TagNode): void { + let depsTail = sub.depsTail; + let dep = depsTail !== undefined ? depsTail.nextDep : sub.deps; + + while (dep !== undefined) { + dep = unlink(dep, sub); + } +} + +////////// + +export function createTag(): TagNode { + return new TagNode(TAG_KIND, MUTABLE); +} + +export const CONSTANT_TAG = new TagNode(CONSTANT_KIND, MUTABLE); +CONSTANT_TAG.revision = CONSTANT; + +export const VOLATILE_TAG = new TagNode(VOLATILE_KIND, MUTABLE); +VOLATILE_TAG.revision = VOLATILE; +VOLATILE_TAG.volatile = true; + +export function isConstTag(tag: Tag): boolean { + return tag === CONSTANT_TAG; +} + +/** + * Returns a number for the present state of the tag. + * + * Give the number to `validateTag` later to learn if the tag changed. + */ +export function valueForTag(_tag: Tag): Revision { + let tag = _tag as TagNode; + + switch (tag.kind) { + case TAG_KIND: + case CONSTANT_KIND: + case VOLATILE_KIND: + return tag.revision; + + case LIST_KIND: { + if (tag.mark !== $REVISION) { + tag.mark = $REVISION; + tag.revision = maxRevision(unwrap(tag.tags)); + } + + return tag.revision; + } + + default: { + if (tag.volatile) { + return VOLATILE; + } + + if (tag.checked === $REVISION) { + return tag.max; + } + + let revision = CONSTANT; + + if (tag.tags !== undefined) { + revision = maxRevision(tag.tags); + } else { + for (let dep = tag.deps; dep !== undefined; dep = dep.nextDep) { + revision = Math.max(revision, valueForTag(dep.dep as TagNode)); + } + } + + tag.checked = $REVISION; + tag.max = revision; + + return revision; + } + } +} + +function maxRevision(tags: TagNode[]): Revision { + let revision = CONSTANT; + + for (let i = 0; i < tags.length; i++) { + revision = Math.max(revision, valueForTag(tags[i] as TagNode)); + } + + return revision; +} + +export function validateTag(tag: Tag, snapshot: Revision): boolean { + return snapshot >= valueForTag(tag); +} + +export function combine(tags: Tag[]): Tag { + switch (tags.length) { + case 0: + return CONSTANT_TAG; + case 1: + return tags[0] as Tag; + default: { + let list = new TagNode(LIST_KIND, MUTABLE); + list.tags = tags as TagNode[]; + list.mark = -1; + list.volatile = (tags as TagNode[]).some((tag) => tag.volatile); + return list; + } + } +} + +export function dirtyTag(_tag: Tag, disableConsumptionAssertion?: boolean): void { + let tag = _tag as TagNode; + + if (DEBUG && tag.kind !== TAG_KIND) { + throw new Error('Attempted to dirty a tag that was not dirtyable'); + } + + if (DEBUG && disableConsumptionAssertion !== true) { + unwrap(debug.assertTagNotConsumed)(tag); + } + + tag.revision = ++$REVISION; + + let subs = tag.subs; + + if (subs !== undefined) { + propagate(subs, false); + + if (FIRED.length > 0) { + flushWatchers(); + } + } + + scheduleRevalidate(); +} + +/** + * Writes the tag of each watcher that the last `propagate` reached. + * + * A watcher fires one time for each write, so two tags that forward to each + * other do not loop. + */ +function flushWatchers(): void { + for (let i = 0; i < FIRED.length; i++) { + let tag = (FIRED[i] as TagNode).peer; + + if (tag !== undefined) { + tag.revision = $REVISION; + + let subs = tag.subs; + + if (subs !== undefined) { + propagate(subs, false); + } + } + } + + for (let i = 0; i < FIRED.length; i++) { + let watcher = FIRED[i] as TagNode; + watcher.flags = watcher.peer === undefined ? 0 : WATCHING; + } + + FIRED.length = 0; +} + +/** + * Makes `tag` change when `source` changes. + * + * A later call replaces the source of the tag. + */ +export function updateTag(_tag: Tag, _source: Tag): void { + let tag = _tag as TagNode; + let source = _source as TagNode; + + if (DEBUG && tag.kind !== TAG_KIND) { + throw new Error('Attempted to update a tag that was not updatable'); + } + + let watcher = tag.peer; + + if (watcher === undefined) { + if (source === CONSTANT_TAG) return; + + watcher = tag.peer = new TagNode(WATCHER_KIND, WATCHING); + watcher.peer = tag; + } + + cycle++; + watcher.depsTail = undefined; + linkLeaves(watcher, source); + purgeDeps(watcher); + watcher.flags = WATCHING; +} + +/** + * Removes the links from other tags to the watcher of `tag`. + * + * A watcher that stays linked keeps memory and slows each write of its sources. + */ +export function releaseTag(_tag: Tag): void { + let watcher = (_tag as TagNode).peer; + + if (watcher !== undefined) { + disposeDeps(watcher); + } +} + +/** + * Links `sub` to each TAG that `tag` reads at this time. + * + * A stale COMPUTED between a TAG and `sub` stops `propagate`, + * so the links go to the TAGs directly. + */ +function linkLeaves(sub: TagNode, tag: TagNode): void { + switch (tag.kind) { + case TAG_KIND: + link(tag, sub, cycle); + break; + + case CONSTANT_KIND: + break; + + case VOLATILE_KIND: + sub.volatile = true; + break; + + default: { + let tags = tag.tags; + + if (tag.volatile) sub.volatile = true; + + if (tags !== undefined) { + for (let i = 0; i < tags.length; i++) { + linkLeaves(sub, tags[i] as TagNode); + } + } else { + for (let dep = tag.deps; dep !== undefined; dep = dep.nextDep) { + linkLeaves(sub, dep.dep as TagNode); + } + } + } + } +} + +function linkTag(sub: TagNode, tag: TagNode): void { + switch (tag.kind) { + case TAG_KIND: + link(tag, sub, cycle); + break; + + case LIST_KIND: { + let tags = unwrap(tag.tags); + + for (let i = 0; i < tags.length; i++) { + linkTag(sub, tags[i] as TagNode); + } + break; + } + + case COMPUTED_KIND: + case FRAME_KIND: + if (tag.tags === undefined && tag.flags === MUTABLE) { + if (tag.volatile) sub.volatile = true; + if (tag.deps !== undefined) link(tag, sub, cycle); + } else { + linkLeaves(sub, tag); + } + break; + + case VOLATILE_KIND: + /** + * `sub` can never be valid, and no write will tell it so. + */ + sub.volatile = true; + break; + } +} + +function collect(collector: TagNode, tag: TagNode): void { + if (tag.volatile) collector.volatile = true; + + /** + * A LIST uses `mark` for its own check, so it can go in two times. + */ + if (tag.kind === LIST_KIND) { + unwrap(collector.tags).push(tag); + } else if (tag.mark !== collector.mark) { + tag.mark = collector.mark; + unwrap(collector.tags).push(tag); + } +} + +export function consumeTag(_tag: Tag): void { + let sub = activeSub; + let tag = _tag as TagNode; + + if (sub === undefined || tag === CONSTANT_TAG) return; + + if (DEBUG) { + unwrap(debug.markTagAsConsumed)(tag); + } + + if (sub.kind === COLLECTOR_KIND) { + collect(sub, tag); + } else { + linkTag(sub, tag); + } +} + +////////// + +export function beginTrackFrame(debuggingContext?: string | false): void { + SUB_STACK.push(activeSub); + + let collector = new TagNode(COLLECTOR_KIND, MUTABLE); + collector.tags = []; + collector.mark = ++collectorId; + activeSub = collector; + + if (DEBUG) { + unwrap(debug.beginTrackingTransaction)(debuggingContext); + } +} + +export function endTrackFrame(): Tag { + let collector = activeSub; + + if (DEBUG) { + if (SUB_STACK.length === 0 || collector?.kind !== COLLECTOR_KIND) { + throw new Error('attempted to close a tracking frame, but one was not open'); + } + + unwrap(debug.endTrackingTransaction)(); + } + + activeSub = SUB_STACK.pop(); + + return collectorToTag(collector as TagNode); +} + +function collectorToTag(collector: TagNode): TagNode { + let tags = collector.tags as TagNode[]; + + if (tags.length === 0) { + return CONSTANT_TAG; + } else if (tags.length === 1) { + return tags[0] as TagNode; + } + + collector.kind = LIST_KIND; + collector.mark = -1; + + return collector; +} + +export function beginUntrackFrame(): void { + SUB_STACK.push(activeSub); + activeSub = undefined; +} + +export function endUntrackFrame(): void { + if (DEBUG && SUB_STACK.length === 0) { + throw new Error('attempted to close a tracking frame, but one was not open'); + } + + activeSub = SUB_STACK.pop(); +} + +/** + * Use this function only after an error, to get back to a valid state. + */ +export function resetTracking(): string | void { + while (activeSub !== undefined || SUB_STACK.length > 0) { + abandon(activeSub); + activeSub = SUB_STACK.pop(); + } + + if (DEBUG) { + return unwrap(debug.resetTrackingTransaction)(); + } +} + +function abandon(sub: TagNode | undefined): void { + if (sub !== undefined && (sub.kind === FRAME_KIND || sub.kind === COMPUTED_KIND)) { + sub.flags = sub.flags === 0 ? 0 : MUTABLE | DIRTY; + } +} + +export function isTracking(): boolean { + return activeSub !== undefined; +} + +export function track(block: () => void, debugLabel?: string | false): Tag { + beginTrackFrame(debugLabel); + + let tag; + + try { + block(); + } finally { + tag = endTrackFrame(); + } + + return tag; +} + +export function untrack(callback: () => T): T { + beginUntrackFrame(); + + try { + return callback(); + } finally { + endUntrackFrame(); + } +} + +////////// + +/** + * A frame is the subscriber for a block of updating opcodes. + * + * The owner runs the block between `beginFrame` and `endFrame`. + * When `isFrameStale` answers `true`, the owner runs the block again. + */ +export function createFrame(): TagNode { + return new TagNode(FRAME_KIND, MUTABLE); +} + +export function beginFrame(frame: TagNode, debuggingContext?: string | false): void { + SUB_STACK.push(activeSub); + + cycle++; + frame.depsTail = undefined; + frame.checked = -1; + frame.volatile = false; + frame.flags = MUTABLE | RECURSED_CHECK; + activeSub = frame; + + if (DEBUG) { + unwrap(debug.beginTrackingTransaction)(debuggingContext); + } +} + +export function endFrame(): void { + let frame = activeSub; + + if (DEBUG) { + if (frame?.kind !== FRAME_KIND) { + throw new Error('attempted to close a frame, but one was not open'); + } + + unwrap(debug.endTrackingTransaction)(); + } + + activeSub = SUB_STACK.pop(); + + purgeDeps(frame as TagNode); + settle(frame as TagNode); +} + +/** + * Ends a run of a FRAME or a COMPUTED. + * + * A write to a tag that this run read earlier does not make the node stale. + * + * Such a write can leave a dependency stale. + * A stale dependency stops `propagate`. + * Links to the TAGs below it keep later writes visible. + */ +function settle(node: TagNode): void { + if ((node.flags & PENDING) !== 0) { + for (let dep = node.deps; dep !== undefined; dep = dep.nextDep) { + let tag = dep.dep as TagNode; + + if (tag.kind !== TAG_KIND && (tag.flags & STALE) !== 0) { + linkLeaves(node, tag); + } + } + } + + node.flags = MUTABLE; +} + +/** + * Closes `frame` after an error stopped its run. + */ +export function abandonFrame(frame: TagNode): void { + while (activeSub !== undefined || SUB_STACK.length > 0) { + let sub = activeSub; + activeSub = SUB_STACK.pop(); + + if (sub === frame) break; + } + + frame.flags = MUTABLE | DIRTY; +} + +export function isFrameStale(frame: TagNode): boolean { + return (frame.flags & STALE) !== 0 || frame.volatile; +} + +/** + * Makes the open frame depend on `frame`. + */ +export function consumeFrame(frame: TagNode): void { + let sub = activeSub; + + if (sub === undefined || (frame.deps === undefined && !frame.volatile)) return; + + if (DEBUG) { + unwrap(debug.markTagAsConsumed)(frame); + } + + if (sub.kind === COLLECTOR_KIND) { + collect(sub, frame); + } else { + if (frame.volatile) sub.volatile = true; + if (frame.deps !== undefined) link(frame, sub, cycle); + } +} + +export function disposeFrame(frame: TagNode): void { + disposeDeps(frame); + frame.flags = MUTABLE | DIRTY; +} + +////////// + +/** + * Reads a COMPUTED node, and runs its function first if the value is stale. + * + * The node has links to its dependencies only while it has a subscriber. + * Without a subscriber, it keeps a list of the tags and a revision. + * + * Thus a long-lived tag does not keep each cache that read it one time. + */ +export function readComputed(node: TagNode): T { + let sub = activeSub; + let watched = (sub !== undefined && sub.kind !== COLLECTOR_KIND) || node.subs !== undefined; + let flags = node.flags; + + if (flags === 0) { + evaluate(node, watched); + } else if (node.tags !== undefined) { + if (node.volatile || (node.revision !== $REVISION && !(node.revision >= valueForTag(node)))) { + evaluate(node, watched); + } else { + node.revision = $REVISION; + if (watched) unpark(node); + } + } else if ((flags & STALE) !== 0 || node.volatile) { + evaluate(node, watched); + } + + /** + * A node that reads itself during its run gets no link to itself. + */ + if ( + sub !== undefined && + (node.flags & RECURSED_CHECK) === 0 && + (node.deps !== undefined || node.tags !== undefined || node.volatile) + ) { + if (DEBUG) { + unwrap(debug.markTagAsConsumed)(node); + } + + if (sub.kind === COLLECTOR_KIND) { + collect(sub, node); + } else { + if (node.volatile) sub.volatile = true; + if (node.deps !== undefined) link(node, sub, cycle); + } + } + + return node.value as T; +} + +function evaluate(node: TagNode, watched: boolean): void { + let first = node.flags === 0; + let done = false; + let collector: TagNode | undefined; + + SUB_STACK.push(activeSub); + + node.volatile = false; + node.checked = -1; + + if (watched) { + node.tags = undefined; + cycle++; + node.depsTail = undefined; + node.flags = MUTABLE | RECURSED_CHECK; + activeSub = node; + } else { + if (node.deps !== undefined) disposeDeps(node); + + node.flags = MUTABLE | RECURSED_CHECK; + + collector = new TagNode(COLLECTOR_KIND, MUTABLE); + collector.tags = []; + collector.mark = ++collectorId; + activeSub = collector; + } + + if (DEBUG) { + unwrap(debug.beginTrackingTransaction)(node.debugLabel); + } + + try { + node.value = (node.fn as () => unknown)(); + done = true; + } finally { + /** + * `resetTracking` can empty the stack before this point, after an error. + */ + if (activeSub === node || activeSub === collector) { + if (DEBUG) { + unwrap(debug.endTrackingTransaction)(); + } + + activeSub = SUB_STACK.pop(); + } + + if (collector === undefined) { + purgeDeps(node); + } else if (done) { + let tags = collector.tags as TagNode[]; + + node.tags = tags.length === 0 ? undefined : tags; + node.volatile = collector.volatile; + node.revision = $REVISION; + } else { + node.tags = undefined; + } + + if (done) { + settle(node); + } else { + node.flags = first ? 0 : MUTABLE | DIRTY; + } + } +} + +/** + * Replaces the links of a COMPUTED that lost its last subscriber by a list. + */ +function park(node: TagNode): void { + if (node.deps === undefined) return; + + let tags: TagNode[] = []; + + for (let dep: Link | undefined = node.deps; dep !== undefined; dep = dep.nextDep) { + tags.push(dep.dep as TagNode); + } + + node.tags = tags; + node.checked = -1; + node.revision = (node.flags & STALE) !== 0 ? -1 : $REVISION; + node.flags = MUTABLE; + + disposeDeps(node); +} + +/** + * Gives a valid COMPUTED its links back, because a subscriber reads it now. + */ +function unpark(node: TagNode): void { + let tags = node.tags as TagNode[]; + + node.tags = undefined; + node.depsTail = undefined; + cycle++; + + for (let i = 0; i < tags.length; i++) { + linkSource(node, tags[i] as TagNode); + } +} + +function linkSource(sub: TagNode, tag: TagNode): void { + if (tag.kind === COMPUTED_KIND) { + if (tag.tags !== undefined) { + unpark(tag); + } + + if (tag.flags === MUTABLE) { + if (tag.volatile) sub.volatile = true; + if (tag.deps !== undefined) link(tag, sub, cycle); + return; + } + } + + linkTag(sub, tag); +} + +export function isConstComputed(node: TagNode): boolean { + return node.flags !== 0 && node.deps === undefined && node.tags === undefined && !node.volatile; +} + +export function createComputed(fn: () => T, debugLabel?: string | false): TagNode { + let node = new TagNode(COMPUTED_KIND, 0); + node.fn = fn; + + if (DEBUG) { + node.debugLabel = debugLabel; + } + + return node; +} + +////////// + +declare const CACHE_KEY: unique symbol; + +export interface Cache { + [CACHE_KEY]: T; +} + +export function createCache(fn: () => T, debuggingLabel?: string | false): Cache { + if (DEBUG && !(typeof fn === 'function')) { + throw new Error( + `createCache() must be passed a function as its first parameter. Called with: ${String(fn)}` + ); + } + + return createComputed(fn, debuggingLabel) as unknown as Cache; +} + +export function getValue(cache: Cache): T | undefined { + assertCache(cache, 'getValue'); + + return readComputed(cache); +} + +export function isConst(cache: Cache): boolean { + assertCache(cache, 'isConst'); + + if (DEBUG && cache.flags === 0) { + throw new Error( + `isConst() can only be used on a cache once getValue() has been called at least once. Called with cache function:\n\n${String( + cache.fn + )}` + ); + } + + return isConstComputed(cache); +} + +function assertCache(value: unknown, fnName: string): asserts value is TagNode { + if (DEBUG && !(value instanceof TagNode && value.kind === COMPUTED_KIND)) { + throw new Error( + `${fnName}() can only be used on an instance of a cache created with createCache(). Called with: ${String( + value + )}` + ); + } +} diff --git a/packages/@glimmer/validator/lib/tracked-data.ts b/packages/@glimmer/signals/lib/tracked-data.ts similarity index 96% rename from packages/@glimmer/validator/lib/tracked-data.ts rename to packages/@glimmer/signals/lib/tracked-data.ts index 560f6d71a54..64e591fb53e 100644 --- a/packages/@glimmer/validator/lib/tracked-data.ts +++ b/packages/@glimmer/signals/lib/tracked-data.ts @@ -1,5 +1,5 @@ import { dirtyTagFor, tagFor } from './meta'; -import { consumeTag } from './tracking'; +import { consumeTag } from './tags'; export type Getter = (self: T) => T[K] | undefined; export type Setter = (self: T, value: T[K]) => void; diff --git a/packages/@glimmer/validator/lib/tracked-value.ts b/packages/@glimmer/signals/lib/tracked-value.ts similarity index 93% rename from packages/@glimmer/validator/lib/tracked-value.ts rename to packages/@glimmer/signals/lib/tracked-value.ts index a59a1000665..1c4c57904bb 100644 --- a/packages/@glimmer/validator/lib/tracked-value.ts +++ b/packages/@glimmer/signals/lib/tracked-value.ts @@ -1,9 +1,8 @@ -import type { UpdatableTag } from '@glimmer/interfaces'; +import type { Tag } from '@glimmer/interfaces'; import type { ReactiveOptions } from './collections/types'; -import { consumeTag } from './tracking'; -import { createUpdatableTag, DIRTY_TAG } from './validators'; +import { consumeTag, createTag, dirtyTag } from './tags'; /** * A mutable reactive value. @@ -26,7 +25,7 @@ export class TrackedValue implements Reactive { #isFrozen = false; #value: Value; readonly #options: ReactiveOptions; - readonly #tag: UpdatableTag; + readonly #tag: Tag; /** * `get`, `set`, `update` and `freeze` are bound to the instance, @@ -58,7 +57,7 @@ export class TrackedValue implements Reactive { this.#value = 0 as Value; this.#value = value; this.#options = options; - this.#tag = createUpdatableTag(); + this.#tag = createTag(); } /** @@ -135,7 +134,7 @@ export class TrackedValue implements Reactive { this.#value = value; - DIRTY_TAG(this.#tag); + dirtyTag(this.#tag); return true; } diff --git a/packages/@glimmer/validator/lib/utils.ts b/packages/@glimmer/signals/lib/utils.ts similarity index 100% rename from packages/@glimmer/validator/lib/utils.ts rename to packages/@glimmer/signals/lib/utils.ts diff --git a/packages/@glimmer/validator/package.json b/packages/@glimmer/signals/package.json similarity index 82% rename from packages/@glimmer/validator/package.json rename to packages/@glimmer/signals/package.json index b6f0237dae1..4f6a9a06f69 100644 --- a/packages/@glimmer/validator/package.json +++ b/packages/@glimmer/signals/package.json @@ -1,13 +1,13 @@ { - "name": "@glimmer/validator", + "name": "@glimmer/signals", "version": "0.95.0", "license": "MIT", "private": true, - "description": "Objects used to track values and their dirtiness in Glimmer", + "description": "The reactive graph of Glimmer, on top of alien-signals", "repository": { "type": "git", "url": "git+https://github.com/glimmerjs/glimmer-vm.git", - "directory": "packages/@glimmer/validator" + "directory": "packages/@glimmer/signals" }, "type": "module", "exports": { @@ -39,7 +39,8 @@ }, "dependencies": { "@glimmer/global-context": "workspace:*", - "@glimmer/interfaces": "workspace:*" + "@glimmer/interfaces": "workspace:*", + "alien-signals": "^3.2.1" }, "devDependencies": { "@glimmer/debug-util": "workspace:*", diff --git a/packages/@glimmer/validator/test/-utils.ts b/packages/@glimmer/signals/test/-utils.ts similarity index 100% rename from packages/@glimmer/validator/test/-utils.ts rename to packages/@glimmer/signals/test/-utils.ts diff --git a/packages/@glimmer/validator/test/collections/array-test.ts b/packages/@glimmer/signals/test/collections/array-test.ts similarity index 98% rename from packages/@glimmer/validator/test/collections/array-test.ts rename to packages/@glimmer/signals/test/collections/array-test.ts index 060928978cd..6b7f321b23b 100644 --- a/packages/@glimmer/validator/test/collections/array-test.ts +++ b/packages/@glimmer/signals/test/collections/array-test.ts @@ -1,11 +1,11 @@ -import { trackedArray } from '@glimmer/validator'; +import { trackedArray } from '@glimmer/signals'; import { expectTypeOf } from 'expect-type'; import { module, test } from '../-utils'; expectTypeOf>().toMatchTypeOf>(); -module('@glimmer/validator: trackedArray()', () => { +module('@glimmer/signals: trackedArray()', () => { test('Can get values on array directly', (assert) => { let arr = trackedArray(['foo']); diff --git a/packages/@glimmer/validator/test/collections/map-test.ts b/packages/@glimmer/signals/test/collections/map-test.ts similarity index 97% rename from packages/@glimmer/validator/test/collections/map-test.ts rename to packages/@glimmer/signals/test/collections/map-test.ts index 23a4be6e8ce..91e256057bc 100644 --- a/packages/@glimmer/validator/test/collections/map-test.ts +++ b/packages/@glimmer/signals/test/collections/map-test.ts @@ -1,11 +1,11 @@ -import { trackedMap } from '@glimmer/validator'; +import { trackedMap } from '@glimmer/signals'; import { expectTypeOf } from 'expect-type'; import { module, test } from '../-utils'; expectTypeOf>>().toMatchTypeOf>(); -module('@glimmer/validator: trackedMap', function () { +module('@glimmer/signals: trackedMap', function () { test('constructor', (assert) => { const map = trackedMap([['foo', 123]]); diff --git a/packages/@glimmer/validator/test/collections/object-test.ts b/packages/@glimmer/signals/test/collections/object-test.ts similarity index 92% rename from packages/@glimmer/validator/test/collections/object-test.ts rename to packages/@glimmer/signals/test/collections/object-test.ts index 3d75ebf5107..7883af6a6d9 100644 --- a/packages/@glimmer/validator/test/collections/object-test.ts +++ b/packages/@glimmer/signals/test/collections/object-test.ts @@ -1,4 +1,4 @@ -import { trackedObject } from '@glimmer/validator'; +import { trackedObject } from '@glimmer/signals'; import { expectTypeOf } from 'expect-type'; import { module, test } from '../-utils'; @@ -12,7 +12,7 @@ trackedObject<{ foo: number }>({}); // Optional keys should not require a value trackedObject<{ foo?: number }>(); -module('@glimmer/validator: trackedObject', function () { +module('@glimmer/signals: trackedObject', function () { test('basic usage', (assert) => { let original = { foo: 123 }; let obj = trackedObject(original); diff --git a/packages/@glimmer/validator/test/collections/set-test.ts b/packages/@glimmer/signals/test/collections/set-test.ts similarity index 98% rename from packages/@glimmer/validator/test/collections/set-test.ts rename to packages/@glimmer/signals/test/collections/set-test.ts index 6ef5f661270..88594af06d7 100644 --- a/packages/@glimmer/validator/test/collections/set-test.ts +++ b/packages/@glimmer/signals/test/collections/set-test.ts @@ -1,4 +1,4 @@ -import { trackedSet } from '@glimmer/validator'; +import { trackedSet } from '@glimmer/signals'; import { expectTypeOf } from 'expect-type'; import { module, test } from '../-utils'; @@ -7,7 +7,7 @@ expectTypeOf>>().toMatchTypeOf> type AnyFn = (...args: any[]) => any; -module('@glimmer/validator: trackedSet', function () { +module('@glimmer/signals: trackedSet', function () { test('constructor', (assert) => { const set = trackedSet(['foo', 123]); diff --git a/packages/@glimmer/validator/test/collections/weak-map-test.ts b/packages/@glimmer/signals/test/collections/weak-map-test.ts similarity index 94% rename from packages/@glimmer/validator/test/collections/weak-map-test.ts rename to packages/@glimmer/signals/test/collections/weak-map-test.ts index c0931a3c68b..09955c3b2b3 100644 --- a/packages/@glimmer/validator/test/collections/weak-map-test.ts +++ b/packages/@glimmer/signals/test/collections/weak-map-test.ts @@ -1,4 +1,4 @@ -import { trackedWeakMap } from '@glimmer/validator'; +import { trackedWeakMap } from '@glimmer/signals'; import { expectTypeOf } from 'expect-type'; import { module, test } from '../-utils'; @@ -7,7 +7,7 @@ expectTypeOf>>().toMatchTypeOf< WeakMap >(); -module('@glimmer/validator: trackedWeakMap()', function () { +module('@glimmer/signals: trackedWeakMap()', function () { test('constructor', (assert) => { const obj = {}; const map = trackedWeakMap([[obj, 123]]); diff --git a/packages/@glimmer/validator/test/collections/weak-set-test.ts b/packages/@glimmer/signals/test/collections/weak-set-test.ts similarity index 94% rename from packages/@glimmer/validator/test/collections/weak-set-test.ts rename to packages/@glimmer/signals/test/collections/weak-set-test.ts index c605f239e2a..f995a60aace 100644 --- a/packages/@glimmer/validator/test/collections/weak-set-test.ts +++ b/packages/@glimmer/signals/test/collections/weak-set-test.ts @@ -1,11 +1,11 @@ -import { trackedWeakSet } from '@glimmer/validator'; +import { trackedWeakSet } from '@glimmer/signals'; import { expectTypeOf } from 'expect-type'; import { module, test } from '../-utils'; expectTypeOf>>().toMatchTypeOf>(); -module('@glimmer/validator: trackedWeakSet()', function () { +module('@glimmer/signals: trackedWeakSet()', function () { test('constructor', (assert) => { const obj = {}; const set = trackedWeakSet([obj]); diff --git a/packages/@glimmer/signals/test/graph-test.ts b/packages/@glimmer/signals/test/graph-test.ts new file mode 100644 index 00000000000..238d40f86c0 --- /dev/null +++ b/packages/@glimmer/signals/test/graph-test.ts @@ -0,0 +1,368 @@ +import { + beginFrame, + consumeFrame, + consumeTag, + createCache, + createFrame, + createTag, + dirtyTag, + disposeFrame, + endFrame, + getValue, + isFrameStale, + releaseTag, + type TagNode, + track, + updateTag, + validateTag, + valueForTag, +} from '@glimmer/signals'; + +import { module, test } from './-utils'; + +function run(frame: TagNode, block: () => void) { + beginFrame(frame); + + try { + block(); + } finally { + endFrame(); + } +} + +function hasSubscribers(tag: TagNode) { + return tag.subs !== undefined; +} + +module('@glimmer/signals: graph', () => { + module('frames', () => { + test('a write to a tag makes the frame stale', (assert) => { + let tag = createTag(); + let frame = createFrame(); + + run(frame, () => consumeTag(tag)); + + assert.false(isFrameStale(frame)); + + dirtyTag(tag); + + assert.true(isFrameStale(frame)); + + run(frame, () => consumeTag(tag)); + + assert.false(isFrameStale(frame)); + }); + + test('a frame without reads is never stale', (assert) => { + let tag = createTag(); + let frame = createFrame(); + + run(frame, () => {}); + dirtyTag(tag); + + assert.false(isFrameStale(frame)); + }); + + test('a write below a child frame makes the parent frame stale', (assert) => { + let tag = createTag(); + let parent = createFrame(); + let child = createFrame(); + + run(parent, () => { + run(child, () => consumeTag(tag)); + consumeFrame(child); + }); + + dirtyTag(tag); + + assert.true(isFrameStale(child)); + assert.true(isFrameStale(parent)); + }); + + test('a parent that does not use a child frame again removes its links', (assert) => { + let tag = createTag(); + let parent = createFrame(); + let child = createFrame(); + + run(parent, () => { + run(child, () => consumeTag(tag)); + consumeFrame(child); + }); + + assert.true(hasSubscribers(tag)); + + run(parent, () => {}); + + assert.false(hasSubscribers(tag)); + }); + + test('a frame drops a dependency that the next run does not read', (assert) => { + let first = createTag(); + let second = createTag(); + let frame = createFrame(); + let useFirst = true; + + let block = () => consumeTag(useFirst ? first : second); + + run(frame, block); + useFirst = false; + run(frame, block); + + dirtyTag(first); + assert.false(isFrameStale(frame)); + assert.false(hasSubscribers(first)); + + dirtyTag(second); + assert.true(isFrameStale(frame)); + }); + + test('a write to a tag that the open run read does not make the frame stale', (assert) => { + let tag = createTag(); + let frame = createFrame(); + + run(frame, () => { + consumeTag(tag); + dirtyTag(tag, true); + }); + + assert.false(isFrameStale(frame)); + + dirtyTag(tag); + + assert.true(isFrameStale(frame)); + }); + + test('a later write is visible when a write in the run made a cache stale', (assert) => { + let tag = createTag(); + let cache = createCache(() => consumeTag(tag)); + let frame = createFrame(); + + run(frame, () => { + getValue(cache); + dirtyTag(tag, true); + }); + + assert.false(isFrameStale(frame)); + + dirtyTag(tag); + + assert.true(isFrameStale(frame)); + }); + + test('disposeFrame removes the links of all that the frame read', (assert) => { + let tag = createTag(); + let cache = createCache(() => consumeTag(tag)); + let root = createFrame(); + let child = createFrame(); + + run(root, () => { + run(child, () => getValue(cache)); + consumeFrame(child); + }); + + assert.true(hasSubscribers(tag)); + + disposeFrame(root); + + assert.false(hasSubscribers(tag)); + }); + }); + + module('caches', () => { + test('a cache without a subscriber has no links from its tags', (assert) => { + let tag = createTag(); + let count = 0; + let cache = createCache(() => { + consumeTag(tag); + return ++count; + }); + + assert.strictEqual(getValue(cache), 1); + assert.strictEqual(getValue(cache), 1); + assert.false(hasSubscribers(tag)); + + dirtyTag(tag); + + assert.strictEqual(getValue(cache), 2); + assert.false(hasSubscribers(tag)); + }); + + test('a cache keeps its value when it gets a subscriber and when it loses it', (assert) => { + let tag = createTag(); + let count = 0; + let cache = createCache(() => { + consumeTag(tag); + return ++count; + }); + let frame = createFrame(); + + assert.strictEqual(getValue(cache), 1); + + run(frame, () => assert.strictEqual(getValue(cache), 1)); + + assert.true(hasSubscribers(tag)); + + disposeFrame(frame); + + assert.false(hasSubscribers(tag)); + assert.strictEqual(getValue(cache), 1); + + dirtyTag(tag); + + assert.strictEqual(getValue(cache), 2); + }); + + test('a nested cache gets its links back together with the outer cache', (assert) => { + let tag = createTag(); + let innerCount = 0; + let inner = createCache(() => { + consumeTag(tag); + return ++innerCount; + }); + let outer = createCache(() => getValue(inner)); + let frame = createFrame(); + + assert.strictEqual(getValue(outer), 1); + assert.false(hasSubscribers(tag)); + + run(frame, () => getValue(outer)); + + assert.true(hasSubscribers(tag)); + assert.strictEqual(innerCount, 1); + + dirtyTag(tag); + + assert.true(isFrameStale(frame)); + assert.strictEqual(getValue(outer), 2); + }); + + test('a cache that lost its subscriber while stale runs again', (assert) => { + let tag = createTag(); + let count = 0; + let cache = createCache(() => { + consumeTag(tag); + return ++count; + }); + let frame = createFrame(); + + run(frame, () => getValue(cache)); + dirtyTag(tag); + disposeFrame(frame); + + assert.strictEqual(getValue(cache), 2); + }); + + test('a tag from track() that holds a cache sees each write', (assert) => { + let first = createTag(); + let second = createTag(); + let cache = createCache(() => { + consumeTag(first); + consumeTag(second); + }); + + let tag = track(() => getValue(cache)); + let snapshot = valueForTag(tag); + + dirtyTag(first); + assert.false(validateTag(tag, snapshot)); + + snapshot = valueForTag(tag); + assert.true(validateTag(tag, snapshot)); + + dirtyTag(second); + assert.false(validateTag(tag, snapshot)); + }); + }); + + module('updateTag', () => { + test('the tag changes for each write to the source', (assert) => { + let tag = createTag(); + let source = createTag(); + + updateTag(tag, source); + + let snapshot = valueForTag(tag); + dirtyTag(source); + assert.false(validateTag(tag, snapshot)); + + snapshot = valueForTag(tag); + dirtyTag(source); + assert.false(validateTag(tag, snapshot)); + }); + + test('the tag changes for each write below a cache in the source', (assert) => { + let tag = createTag(); + let leaf = createTag(); + let cache = createCache(() => consumeTag(leaf)); + + updateTag( + tag, + track(() => getValue(cache)) + ); + + let snapshot = valueForTag(tag); + dirtyTag(leaf); + assert.false(validateTag(tag, snapshot)); + + snapshot = valueForTag(tag); + dirtyTag(leaf); + assert.false(validateTag(tag, snapshot), 'the stale cache does not hide the second write'); + }); + + test('a frame that read the tag is stale after a write to the source', (assert) => { + let tag = createTag(); + let source = createTag(); + let frame = createFrame(); + + updateTag(tag, source); + run(frame, () => consumeTag(tag)); + + dirtyTag(source); + + assert.true(isFrameStale(frame)); + }); + + test('a chain of tags forwards a write', (assert) => { + let first = createTag(); + let second = createTag(); + let third = createTag(); + + updateTag(first, second); + updateTag(second, third); + + let snapshot = valueForTag(first); + dirtyTag(third); + + assert.false(validateTag(first, snapshot)); + }); + + test('a new source replaces the old source', (assert) => { + let tag = createTag(); + let oldSource = createTag(); + let newSource = createTag(); + + updateTag(tag, oldSource); + updateTag(tag, newSource); + + let snapshot = valueForTag(tag); + + dirtyTag(oldSource); + assert.true(validateTag(tag, snapshot)); + assert.false(hasSubscribers(oldSource)); + + dirtyTag(newSource); + assert.false(validateTag(tag, snapshot)); + }); + + test('releaseTag removes the links from the source', (assert) => { + let tag = createTag(); + let source = createTag(); + + updateTag(tag, source); + assert.true(hasSubscribers(source)); + + releaseTag(tag); + assert.false(hasSubscribers(source)); + }); + }); +}); diff --git a/packages/@glimmer/validator/test/meta-test.ts b/packages/@glimmer/signals/test/meta-test.ts similarity index 89% rename from packages/@glimmer/validator/test/meta-test.ts rename to packages/@glimmer/signals/test/meta-test.ts index cf6a739c74f..317cf23332d 100644 --- a/packages/@glimmer/validator/test/meta-test.ts +++ b/packages/@glimmer/signals/test/meta-test.ts @@ -1,8 +1,8 @@ -import { dirtyTagFor, tagFor, validateTag, valueForTag } from '@glimmer/validator'; +import { dirtyTagFor, tagFor, validateTag, valueForTag } from '@glimmer/signals'; import { module, test } from './-utils'; -module('@glimmer/validator: meta', () => { +module('@glimmer/signals: meta', () => { test('it creates a unique tag for a property on a given object', (assert) => { let obj = {}; let tag = tagFor(obj, 'foo'); diff --git a/packages/@glimmer/validator/test/package.json b/packages/@glimmer/signals/test/package.json similarity index 76% rename from packages/@glimmer/validator/test/package.json rename to packages/@glimmer/signals/test/package.json index f7f0977584d..537ea164e4c 100644 --- a/packages/@glimmer/validator/test/package.json +++ b/packages/@glimmer/signals/test/package.json @@ -1,6 +1,6 @@ { "private": true, - "name": "@glimmer-test/validator", + "name": "@glimmer-test/signals", "repo-meta": { "strictness": "strict" }, @@ -9,7 +9,7 @@ "dependencies": { "@glimmer/global-context": "workspace:*", "@glimmer/interfaces": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "expect-type": "^1.1.0" } } diff --git a/packages/@glimmer/validator/test/tracked-value-test.ts b/packages/@glimmer/signals/test/tracked-value-test.ts similarity index 98% rename from packages/@glimmer/validator/test/tracked-value-test.ts rename to packages/@glimmer/signals/test/tracked-value-test.ts index b7962546ab7..43ec821ce0e 100644 --- a/packages/@glimmer/validator/test/tracked-value-test.ts +++ b/packages/@glimmer/signals/test/tracked-value-test.ts @@ -1,8 +1,8 @@ -import { track, trackedValue, validateTag, valueForTag } from '@glimmer/validator'; +import { track, trackedValue, validateTag, valueForTag } from '@glimmer/signals'; import { module, test } from './-utils'; -module('@glimmer/validator: trackedValue()', () => { +module('@glimmer/signals: trackedValue()', () => { test('creates reactive storage', (assert) => { const x = trackedValue('hello'); diff --git a/packages/@glimmer/validator/test/tracking-test.ts b/packages/@glimmer/signals/test/tracking-test.ts similarity index 99% rename from packages/@glimmer/validator/test/tracking-test.ts rename to packages/@glimmer/signals/test/tracking-test.ts index 40d8d57f18a..a708a422bba 100644 --- a/packages/@glimmer/validator/test/tracking-test.ts +++ b/packages/@glimmer/signals/test/tracking-test.ts @@ -17,11 +17,11 @@ import { untrack, validateTag, valueForTag, -} from '@glimmer/validator'; +} from '@glimmer/signals'; import { module, test } from './-utils'; -module('@glimmer/validator: tracking', () => { +module('@glimmer/signals: tracking', () => { module('track', () => { test('it combines tags that are consumed within a track frame', (assert) => { let tag1 = createTag(); diff --git a/packages/@glimmer/validator/test/validators-test.ts b/packages/@glimmer/signals/test/validators-test.ts similarity index 65% rename from packages/@glimmer/validator/test/validators-test.ts rename to packages/@glimmer/signals/test/validators-test.ts index 9c963a1c928..0be3b5e9c15 100644 --- a/packages/@glimmer/validator/test/validators-test.ts +++ b/packages/@glimmer/signals/test/validators-test.ts @@ -1,20 +1,17 @@ import { DEBUG } from '@glimmer/env'; -import type { UpdatableTag } from '@glimmer/interfaces'; import { testOverrideGlobalContext } from '@glimmer/global-context'; import { - ALLOW_CYCLES, bump, combine, CONSTANT_TAG, createTag, - createUpdatableTag, - CURRENT_TAG, + currentRevision, dirtyTag, updateTag, validateTag, valueForTag, VOLATILE_TAG, -} from '@glimmer/validator'; +} from '@glimmer/signals'; import { module, test } from './-utils'; @@ -26,7 +23,7 @@ function unwrap(value: T | null | undefined): T { return value; } -module('@glimmer/validator: validators', () => { +module('@glimmer/signals: validators', () => { module('DirtyableTag', () => { test('it can be dirtied', (assert) => { let tag = createTag(); @@ -60,22 +57,12 @@ module('@glimmer/validator: validators', () => { assert.verifySteps(['scheduleRevalidate']); }); - - test('it cannot be updated', (assert) => { - let tag = createTag(); - let subtag = createTag(); - - assert.throws( - () => updateTag(tag as unknown as UpdatableTag, subtag), - /Error: Attempted to update a tag that was not updatable/u - ); - }); } }); module('UpdatableTag', () => { test('it can be dirtied', (assert) => { - let tag = createUpdatableTag(); + let tag = createTag(); let snapshot = valueForTag(tag); assert.ok(validateTag(tag, snapshot)); @@ -88,8 +75,8 @@ module('@glimmer/validator: validators', () => { }); test('it can be updated', (assert) => { - let tag = createUpdatableTag(); - let subtag = createUpdatableTag(); + let tag = createTag(); + let subtag = createTag(); updateTag(tag, subtag); @@ -104,8 +91,8 @@ module('@glimmer/validator: validators', () => { }); test('it correctly buffers updates when subtag has a less recent value', (assert) => { - let tag = createUpdatableTag(); - let subtag = createUpdatableTag(); + let tag = createTag(); + let subtag = createTag(); // First, we dirty the parent tag so it is more recent than the subtag dirtyTag(tag); @@ -125,8 +112,8 @@ module('@glimmer/validator: validators', () => { }); test('it correctly buffers updates when subtag has a more recent value', (assert) => { - let tag = createUpdatableTag(); - let subtag = createUpdatableTag(); + let tag = createTag(); + let subtag = createTag(); // First, we get a snapshot of the parent let snapshot = valueForTag(tag); @@ -145,38 +132,21 @@ module('@glimmer/validator: validators', () => { assert.notOk(validateTag(tag, snapshot), 'tag is invalid after subtag is dirtied again'); }); - if (DEBUG) { - test('does not allow cycles on tags that have not been marked with ALLOW_CYCLES', (assert) => { - let tag = createUpdatableTag(); - let subtag = createUpdatableTag(); - - let snapshot = valueForTag(tag); - - updateTag(tag, subtag); - updateTag(subtag, tag); - - dirtyTag(tag); - - assert.throws(() => validateTag(tag, snapshot)); - }); - - test('does allow cycles on tags that have been marked with ALLOW_CYCLES', (assert) => { - let tag = createUpdatableTag(); - let subtag = createUpdatableTag(); - - let snapshot = valueForTag(tag); + test('two tags can follow each other', (assert) => { + let tag = createTag(); + let subtag = createTag(); - unwrap(ALLOW_CYCLES).set(tag, true); - unwrap(ALLOW_CYCLES).set(subtag, true); + let snapshot = valueForTag(tag); + let subtagSnapshot = valueForTag(subtag); - updateTag(tag, subtag); - updateTag(subtag, tag); + updateTag(tag, subtag); + updateTag(subtag, tag); - dirtyTag(tag); + dirtyTag(tag); - assert.notOk(validateTag(tag, snapshot)); - }); - } + assert.notOk(validateTag(tag, snapshot)); + assert.notOk(validateTag(subtag, subtagSnapshot)); + }); }); module('CombinatorTag', () => { @@ -203,7 +173,6 @@ module('@glimmer/validator: validators', () => { let combined = combine([tag1, tag2]); assert.throws( - // @ts-expect-error this is an error condition () => dirtyTag(combined), /Error: Attempted to dirty a tag that was not dirtyable/u ); @@ -216,7 +185,6 @@ module('@glimmer/validator: validators', () => { let combined = combine([tag1, tag2]); assert.throws( - // @ts-expect-error this is an error condition () => updateTag(combined, tag1), /Error: Attempted to update a tag that was not updatable/u ); @@ -228,7 +196,6 @@ module('@glimmer/validator: validators', () => { if (DEBUG) { test('it cannot be dirtied', (assert) => { assert.throws( - // @ts-expect-error this is an error condition () => dirtyTag(CONSTANT_TAG), /Error: Attempted to dirty a tag that was not dirtyable/u ); @@ -238,7 +205,6 @@ module('@glimmer/validator: validators', () => { let subtag = createTag(); assert.throws( - // @ts-expect-error this is an error condition () => updateTag(CONSTANT_TAG, subtag), /Error: Attempted to update a tag that was not updatable/u ); @@ -266,7 +232,6 @@ module('@glimmer/validator: validators', () => { if (DEBUG) { test('it cannot be dirtied', (assert) => { assert.throws( - // @ts-expect-error this is an error condition () => dirtyTag(VOLATILE_TAG), /Error: Attempted to dirty a tag that was not dirtyable/u ); @@ -276,7 +241,6 @@ module('@glimmer/validator: validators', () => { let subtag = createTag(); assert.throws( - // @ts-expect-error this is an error condition () => updateTag(VOLATILE_TAG, subtag), /Error: Attempted to update a tag that was not updatable/u ); @@ -284,48 +248,14 @@ module('@glimmer/validator: validators', () => { } }); - module('CurrentTag', () => { - test('it is always the current revision', (assert) => { - let snapshot = valueForTag(CURRENT_TAG); - assert.ok(validateTag(CURRENT_TAG, snapshot)); + module('currentRevision', () => { + test('it changes when a tag is dirtied', (assert) => { + let snapshot = currentRevision(); let tag = createTag(); dirtyTag(tag); - assert.notOk(validateTag(CURRENT_TAG, snapshot)); + assert.notStrictEqual(currentRevision(), snapshot); }); - - test('it ensures that any tags which it is combined with are also always the current revision', (assert) => { - let tag2 = createTag(); - let combined = combine([CURRENT_TAG, tag2]); - - let snapshot = valueForTag(combined); - assert.ok(validateTag(combined, snapshot)); - - let otherTag = createTag(); - dirtyTag(otherTag); - - assert.notOk(validateTag(combined, snapshot)); - }); - - if (DEBUG) { - test('it cannot be dirtied', (assert) => { - assert.throws( - // @ts-expect-error this is an error condition - () => dirtyTag(CURRENT_TAG), - /Error: Attempted to dirty a tag that was not dirtyable/u - ); - }); - - test('it cannot be updated', (assert) => { - let subtag = createTag(); - - assert.throws( - // @ts-expect-error this is an error condition - () => updateTag(CURRENT_TAG, subtag), - /Error: Attempted to update a tag that was not updatable/u - ); - }); - } }); }); diff --git a/packages/@glimmer/tracking/index.ts b/packages/@glimmer/tracking/index.ts index 7cb01700dda..5aeff6d01dc 100644 --- a/packages/@glimmer/tracking/index.ts +++ b/packages/@glimmer/tracking/index.ts @@ -1,11 +1,7 @@ export { tracked } from '@ember/-internals/metal/lib/tracked'; export { cached } from '@ember/-internals/metal/lib/cached'; -export type { - Reactive, - ReadOnlyReactive, - TrackedValue, -} from '@glimmer/validator/lib/tracked-value'; +export type { Reactive, ReadOnlyReactive, TrackedValue } from '@glimmer/signals/lib/tracked-value'; /** In order to tell Ember a value might change, we need to mark it as trackable. diff --git a/packages/@glimmer/tracking/package.json b/packages/@glimmer/tracking/package.json index 82d03c11687..6a02ea83528 100644 --- a/packages/@glimmer/tracking/package.json +++ b/packages/@glimmer/tracking/package.json @@ -9,6 +9,6 @@ }, "dependencies": { "@ember/-internals": "workspace:*", - "@glimmer/validator": "workspace:*" + "@glimmer/signals": "workspace:*" } } diff --git a/packages/@glimmer/tracking/primitives/cache.ts b/packages/@glimmer/tracking/primitives/cache.ts index 772099baf8d..c64f4b04156 100644 --- a/packages/@glimmer/tracking/primitives/cache.ts +++ b/packages/@glimmer/tracking/primitives/cache.ts @@ -1 +1 @@ -export { createCache, getValue, isConst } from '@glimmer/validator/lib/tracking'; +export { createCache, getValue, isConst } from '@glimmer/signals/lib/tags'; diff --git a/packages/@glimmer/validator/lib/tracking.ts b/packages/@glimmer/validator/lib/tracking.ts deleted file mode 100644 index 1ae354a32c6..00000000000 --- a/packages/@glimmer/validator/lib/tracking.ts +++ /dev/null @@ -1,397 +0,0 @@ -import { DEBUG } from '@glimmer/env'; -import type { Tag } from '@glimmer/interfaces'; - -import type { Revision } from './validators'; - -import { debug } from './debug'; -import { unwrap } from './utils'; -import { combine, CONSTANT_TAG, isConstTag, validateTag, valueForTag } from './validators'; - -/** - * A tag with the `slot` field that `Tracker#add` reads and writes. - * - * Each tag class of this package declares the field, - * so a tag has it from its construction. - */ -interface ConsumedTag extends Tag { - slot: number; -} - -/** - * An object that tracks @tracked properties that were consumed. - * - * A tracker collects the tags that one tracking frame consumes. - * When the frame ends, `combine()` makes one tag from them. - * - * A tracker is not made for each frame. - * `beginTrackFrame` takes it from `TRACKER_POOL`, so the next frame - * at the same depth uses the same tracker again. - */ -class Tracker { - /** - * The tags that the current frame consumed. - * Each tag is at the position of its first consumption. - * `size` is the number of tags. - * - * tags: [ a, b, c, null, null ] - * size: 3 - * - * The entries from `size` up are `null`. - * They are left from an earlier frame that consumed more tags. - * - * The array never shrinks. - * A write to `length` is a slow path in V8, and this code runs for every frame. - */ - private tags: (Tag | null)[] = []; - private size = 0; - - /** - * Adds a tag to the frame, one time. - * - * A frame can consume one tag many times, - * so the tracker must find out if it has the tag already. - * It does that with no `Set`: the tag keeps the index - * at which a tracker put it last, in `tag.slot`. - * - * consume a tags: [ a ] a.slot = 0 - * consume b tags: [ a, b ] b.slot = 1 - * consume a tags[a.slot] is a, so the frame has it. No change. - * - * The check is `tags[tag.slot] === tag`. - * It compares the entry with the tag, so a `slot` that is out of date - * cannot hide a tag. The worst case is a tag that is in the array two times. - * - * That case needs a nested frame. - * The nested frame has its own tracker, and that tracker also writes `slot`: - * - * 1. outer frame consumes a, b outer tags: [ a, b ] b.slot = 1 - * 2. inner frame consumes b inner tags: [ b ] b.slot = 0 - * 3. outer frame consumes b outer tags[0] is a, not b. - * outer tags: [ a, b, b ] b.slot = 2 - * - * The duplicate does no harm. - * The revision of a combined tag is the highest revision of its tags, - * and a tag that is there two times does not change the highest. - * - * If the inner frame puts the tag at the index that the outer frame used, - * the check of the outer frame still passes, and there is no duplicate. - */ - add(tag: Tag) { - if (tag === CONSTANT_TAG) return; - - if (DEBUG) { - unwrap(debug.markTagAsConsumed)(tag); - } - - let { tags, size } = this; - - if (tags[(tag as ConsumedTag).slot] === tag) return; - - (tag as ConsumedTag).slot = size; - tags[size] = tag; - this.size = size + 1; - } - - combine(): Tag { - let { tags, size } = this; - let result: Tag; - - if (size === 0) { - result = CONSTANT_TAG; - } else if (size === 1) { - result = tags[0] as Tag; - } else { - result = combine(tags.slice(0, size) as Tag[]); - } - - this.clear(); - - return result; - } - - /** - * Empties the tracker for the next frame at the same depth. - * - * before: tags: [ a, b, c ] size: 3 - * after: tags: [ null, null, null ] size: 0 - * - * The entries must be `null`, and not only ignored: - * - * - an entry that stays is a match for `tags[tag.slot] === tag`, - * so the next frame would not add that tag - * - an entry that stays keeps its tag alive after the frame - */ - clear(): void { - let { tags, size } = this; - - for (let i = 0; i < size; i++) { - tags[i] = null; - } - - this.size = 0; - } -} - -/** - * Whenever a tracked computed property is entered, the current tracker is - * saved off and a new tracker is replaced. - * - * Any tracked properties consumed are added to the current tracker. - * - * When a tracked computed property is exited, the tracker's tags are - * combined and added to the parent tracker. - * - * The consequence is that each tracked computed property has a tag - * that corresponds to the tracked properties consumed inside of - * itself, including child tracked computed properties. - */ -let CURRENT_TRACKER: Tracker | null = null; - -const OPEN_TRACK_FRAMES: (Tracker | null)[] = []; - -/** - * The trackers, by the depth of the frame that uses them. - * The depth of a frame is the number of frames that are open around it. - * - * Frames are strictly nested: a frame ends before the frame around it ends. - * So two frames at the same depth are never open at the same time, - * and one tracker for each depth is enough. - * - * frame A depth 0 TRACKER_POOL[0] - * |- frame B depth 1 TRACKER_POOL[1] - * | `- frame C depth 2 TRACKER_POOL[2] - * `- frame D depth 1 TRACKER_POOL[1], which B used before - * - * A tracker is made the first time that a frame opens at its depth. - * After that, a frame at that depth allocates no tracker. - * - * An untrack frame takes a depth but no tracker, so the pool can have holes: - * - * untrack frame depth 0 no tracker - * `- frame E depth 1 TRACKER_POOL[1] - * - * TRACKER_POOL: [ , tracker ] - */ -const TRACKER_POOL: (Tracker | undefined)[] = []; - -export function beginTrackFrame(debuggingContext?: string | false): void { - let depth = OPEN_TRACK_FRAMES.length; - - OPEN_TRACK_FRAMES.push(CURRENT_TRACKER); - - let tracker = TRACKER_POOL[depth]; - - if (tracker === undefined) { - tracker = TRACKER_POOL[depth] = new Tracker(); - } - - CURRENT_TRACKER = tracker; - - if (DEBUG) { - unwrap(debug.beginTrackingTransaction)(debuggingContext); - } -} - -export function endTrackFrame(): Tag { - let current = CURRENT_TRACKER; - - if (DEBUG) { - if (OPEN_TRACK_FRAMES.length === 0) { - throw new Error('attempted to close a tracking frame, but one was not open'); - } - - unwrap(debug.endTrackingTransaction)(); - } - - CURRENT_TRACKER = OPEN_TRACK_FRAMES.pop() || null; - - return unwrap(current).combine(); -} - -export function beginUntrackFrame(): void { - OPEN_TRACK_FRAMES.push(CURRENT_TRACKER); - CURRENT_TRACKER = null; -} - -export function endUntrackFrame(): void { - if (DEBUG && OPEN_TRACK_FRAMES.length === 0) { - throw new Error('attempted to close a tracking frame, but one was not open'); - } - - CURRENT_TRACKER = OPEN_TRACK_FRAMES.pop() || null; -} - -// This function is only for handling errors and resetting to a valid state -export function resetTracking(): string | void { - while (OPEN_TRACK_FRAMES.length > 0) { - OPEN_TRACK_FRAMES.pop(); - } - - /** - * A frame that did not end left its tags in its tracker. - * The next frame at that depth must start with no tag. - * - * The pool can have holes, see `TRACKER_POOL`. - */ - for (let tracker of TRACKER_POOL) { - if (tracker !== undefined) { - tracker.clear(); - } - } - - CURRENT_TRACKER = null; - - if (DEBUG) { - return unwrap(debug.resetTrackingTransaction)(); - } -} - -export function isTracking(): boolean { - return CURRENT_TRACKER !== null; -} - -export function consumeTag(tag: Tag): void { - if (CURRENT_TRACKER !== null) { - CURRENT_TRACKER.add(tag); - } -} - -////////// - -const CACHE_KEY = Symbol('CACHE_KEY'); - -// public interface -export interface Cache { - [CACHE_KEY]: T; -} - -const FN = Symbol('FN'); -const LAST_VALUE = Symbol('LAST_VALUE'); -const TAG = Symbol('TAG'); -const SNAPSHOT = Symbol('SNAPSHOT'); -const DEBUG_LABEL = Symbol('DEBUG_LABEL'); - -interface InternalCache { - [FN]: (...args: unknown[]) => T; - [LAST_VALUE]: T | undefined; - [TAG]: Tag | undefined; - [SNAPSHOT]: Revision; - [DEBUG_LABEL]?: string | false | undefined; -} - -export function createCache(fn: () => T, debuggingLabel?: string | false): Cache { - if (DEBUG && !(typeof fn === 'function')) { - throw new Error( - `createCache() must be passed a function as its first parameter. Called with: ${String(fn)}` - ); - } - - let cache: InternalCache = { - [FN]: fn, - [LAST_VALUE]: undefined, - [TAG]: undefined, - [SNAPSHOT]: -1, - }; - - if (DEBUG) { - cache[DEBUG_LABEL] = debuggingLabel; - } - - return cache as unknown as Cache; -} - -export function getValue(cache: Cache): T | undefined { - assertCache(cache, 'getValue'); - - let fn = cache[FN]; - let tag = cache[TAG]; - let snapshot = cache[SNAPSHOT]; - - if (tag === undefined || !validateTag(tag, snapshot)) { - beginTrackFrame(); - - try { - cache[LAST_VALUE] = fn(); - } finally { - tag = endTrackFrame(); - cache[TAG] = tag; - cache[SNAPSHOT] = valueForTag(tag); - consumeTag(tag); - } - } else { - consumeTag(tag); - } - - return cache[LAST_VALUE]; -} - -export function isConst(cache: Cache): boolean { - assertCache(cache, 'isConst'); - - let tag = cache[TAG]; - - assertTag(tag, cache); - - return isConstTag(tag); -} - -function assertCache( - value: Cache | InternalCache, - fnName: string -): asserts value is InternalCache { - if (DEBUG && !(typeof value === 'object' && FN in value)) { - throw new Error( - `${fnName}() can only be used on an instance of a cache created with createCache(). Called with: ${String( - // eslint-disable-next-line @typescript-eslint/no-base-to-string -- @fixme - value - )}` - ); - } -} - -// replace this with `expect` when we can -function assertTag(tag: Tag | undefined, cache: InternalCache): asserts tag is Tag { - if (DEBUG && tag === undefined) { - throw new Error( - `isConst() can only be used on a cache once getValue() has been called at least once. Called with cache function:\n\n${String( - cache[FN] - )}` - ); - } -} - -////////// - -// Legacy tracking APIs - -// track() shouldn't be necessary at all in the VM once the autotracking -// refactors are merged, and we should generally be moving away from it. It may -// be necessary in Ember for a while longer, but I think we'll be able to drop -// it in favor of cache sooner rather than later. -export function track(block: () => void, debugLabel?: string | false): Tag { - beginTrackFrame(debugLabel); - - let tag; - - try { - block(); - } finally { - tag = endTrackFrame(); - } - - return tag; -} - -// untrack() is currently mainly used to handle places that were previously not -// tracked, and that tracking now would cause backtracking rerender assertions. -// I think once we move everyone forward onto modern APIs, we'll probably be -// able to remove it, but I'm not sure yet. -export function untrack(callback: () => T): T { - beginUntrackFrame(); - - try { - return callback(); - } finally { - endUntrackFrame(); - } -} diff --git a/packages/@glimmer/validator/lib/validators.ts b/packages/@glimmer/validator/lib/validators.ts deleted file mode 100644 index 9745468c398..00000000000 --- a/packages/@glimmer/validator/lib/validators.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { DEBUG } from '@glimmer/env'; -import type { - COMBINATOR_TAG_ID as ICOMBINATOR_TAG_ID, - CONSTANT_TAG_ID as ICONSTANT_TAG_ID, - ConstantTag, - CURRENT_TAG_ID as ICURRENT_TAG_ID, - DIRTYABLE_TAG_ID as IDIRTYABLE_TAG_ID, - DirtyableTag, - MonomorphicTagId, - Tag, - TagComputeSymbol, - TagTypeSymbol, - UPDATABLE_TAG_ID as IUPDATABLE_TAG_ID, - UpdatableTag, - VOLATILE_TAG_ID as IVOLATILE_TAG_ID, -} from '@glimmer/interfaces'; -import { scheduleRevalidate } from '@glimmer/global-context'; - -import { debug } from './debug'; -import { unwrap } from './utils'; - -////////// - -export type Revision = number; - -export const CONSTANT: Revision = 0; -export const INITIAL: Revision = 1; -export const VOLATILE: Revision = NaN; - -export let $REVISION = INITIAL; - -export function bump(): void { - $REVISION++; -} - -////////// - -const DIRYTABLE_TAG_ID: IDIRTYABLE_TAG_ID = 0; -const UPDATABLE_TAG_ID: IUPDATABLE_TAG_ID = 1; -const COMBINATOR_TAG_ID: ICOMBINATOR_TAG_ID = 2; -const CONSTANT_TAG_ID: ICONSTANT_TAG_ID = 3; - -////////// - -export const COMPUTE: TagComputeSymbol = Symbol('TAG_COMPUTE') as TagComputeSymbol; - -////////// - -/** - * `value` receives a tag and returns an opaque Revision based on that tag. This - * snapshot can then later be passed to `validate` with the same tag to - * determine if the tag has changed at all since the time that `value` was - * called. - * - * @param tag - */ -export function valueForTag(tag: Tag): Revision { - return tag[COMPUTE](); -} - -/** - * `validate` receives a tag and a snapshot from a previous call to `value` with - * the same tag, and determines if the tag is still valid compared to the - * snapshot. If the tag's state has changed at all since then, `validate` will - * return false, otherwise it will return true. This is used to determine if a - * calculation related to the tags should be rerun. - * - * @param tag - * @param snapshot - */ -export function validateTag(tag: Tag, snapshot: Revision): boolean { - return snapshot >= tag[COMPUTE](); -} - -////////// - -const TYPE: TagTypeSymbol = Symbol('TAG_TYPE') as TagTypeSymbol; - -// this is basically a const -export let ALLOW_CYCLES: WeakMap | undefined; - -if (DEBUG) { - ALLOW_CYCLES = new WeakMap(); -} - -function allowsCycles(tag: Tag): boolean { - if (ALLOW_CYCLES === undefined) { - return true; - } else { - return ALLOW_CYCLES.has(tag); - } -} - -class MonomorphicTagImpl { - static combine(this: void, tags: Tag[]): Tag { - switch (tags.length) { - case 0: - return CONSTANT_TAG; - case 1: - return tags[0] as Tag; - default: { - let tag: MonomorphicTagImpl = new MonomorphicTagImpl(COMBINATOR_TAG_ID); - tag.subtag = tags; - return tag; - } - } - } - - private revision = INITIAL; - private lastChecked = INITIAL; - private lastValue = INITIAL; - - private isUpdating = false; - public subtag: Tag | Tag[] | null = null; - private subtagBufferCache: Revision | null = null; - - /** - * The index at which a tracker put this tag last. - * `Tracker#add` uses it to find out if a tracking frame has the tag already. - * - * The start value 0 is safe: - * the tracker compares its entry at that index with the tag. - */ - public slot = 0; - - declare [TYPE]: T; - - constructor(type: T) { - this[TYPE] = type; - } - - [COMPUTE](): Revision { - let { lastChecked } = this; - - if (this.isUpdating) { - if (DEBUG && !allowsCycles(this)) { - throw new Error('Cycles in tags are not allowed'); - } - - this.lastChecked = ++$REVISION; - } else if (lastChecked !== $REVISION) { - this.isUpdating = true; - this.lastChecked = $REVISION; - - try { - let { subtag, revision } = this; - - if (subtag !== null) { - if (Array.isArray(subtag)) { - for (let i = 0; i < subtag.length; i++) { - revision = Math.max((subtag[i] as Tag)[COMPUTE](), revision); - } - } else { - let subtagValue = subtag[COMPUTE](); - - if (subtagValue === this.subtagBufferCache) { - revision = Math.max(revision, this.lastValue); - } else { - // Clear the temporary buffer cache - this.subtagBufferCache = null; - revision = Math.max(revision, subtagValue); - } - } - } - - this.lastValue = revision; - } finally { - this.isUpdating = false; - } - } - - return this.lastValue; - } - - static updateTag(this: void, _tag: UpdatableTag, _subtag: Tag) { - // catch bug by non-TS users - - if (DEBUG && _tag[TYPE] !== UPDATABLE_TAG_ID) { - throw new Error('Attempted to update a tag that was not updatable'); - } - - // TODO: TS 3.7 should allow us to do this via assertion - let tag = _tag as MonomorphicTagImpl; - let subtag = _subtag as MonomorphicTagImpl; - - if (subtag === CONSTANT_TAG) { - tag.subtag = null; - } else { - // There are two different possibilities when updating a subtag: - // - // 1. subtag[COMPUTE]() <= tag[COMPUTE](); - // 2. subtag[COMPUTE]() > tag[COMPUTE](); - // - // The first possibility is completely fine within our caching model, but - // the second possibility presents a problem. If the parent tag has - // already been read, then it's value is cached and will not update to - // reflect the subtag's greater value. Next time the cache is busted, the - // subtag's value _will_ be read, and it's value will be _greater_ than - // the saved snapshot of the parent, causing the resulting calculation to - // be rerun erroneously. - // - // In order to prevent this, when we first update to a new subtag we store - // its computed value, and then check against that computed value on - // subsequent updates. If its value hasn't changed, then we return the - // parent's previous value. Once the subtag changes for the first time, - // we clear the cache and everything is finally in sync with the parent. - tag.subtagBufferCache = subtag[COMPUTE](); - tag.subtag = subtag; - } - } - - static dirtyTag( - this: void, - tag: DirtyableTag | UpdatableTag, - disableConsumptionAssertion?: boolean - ) { - if ( - DEBUG && - // catch bug by non-TS users - - !(tag[TYPE] === UPDATABLE_TAG_ID || tag[TYPE] === DIRYTABLE_TAG_ID) - ) { - throw new Error('Attempted to dirty a tag that was not dirtyable'); - } - - if (DEBUG && disableConsumptionAssertion !== true) { - // Usually by this point, we've already asserted with better error information, - // but this is our last line of defense. - unwrap(debug.assertTagNotConsumed)(tag); - } - - (tag as MonomorphicTagImpl).revision = ++$REVISION; - - scheduleRevalidate(); - } -} - -export const DIRTY_TAG = MonomorphicTagImpl.dirtyTag; -export const UPDATE_TAG = MonomorphicTagImpl.updateTag; - -////////// - -export function createTag(): DirtyableTag { - return new MonomorphicTagImpl(DIRYTABLE_TAG_ID); -} - -export function createUpdatableTag(): UpdatableTag { - return new MonomorphicTagImpl(UPDATABLE_TAG_ID); -} - -////////// - -export const CONSTANT_TAG: ConstantTag = new MonomorphicTagImpl(CONSTANT_TAG_ID); - -export function isConstTag(tag: Tag): tag is ConstantTag { - return tag === CONSTANT_TAG; -} - -////////// - -const VOLATILE_TAG_ID: IVOLATILE_TAG_ID = 100; - -export class VolatileTag implements Tag { - readonly [TYPE] = VOLATILE_TAG_ID; - - /** - * See `slot` of `MonomorphicTagImpl`. - */ - slot = 0; - - [COMPUTE](): Revision { - return VOLATILE; - } -} - -export const VOLATILE_TAG = new VolatileTag(); - -////////// - -const CURRENT_TAG_ID: ICURRENT_TAG_ID = 101; - -export class CurrentTag implements Tag { - readonly [TYPE] = CURRENT_TAG_ID; - - /** - * See `slot` of `MonomorphicTagImpl`. - */ - slot = 0; - - [COMPUTE](): Revision { - return $REVISION; - } -} - -export const CURRENT_TAG = new CurrentTag(); - -////////// - -export const combine = MonomorphicTagImpl.combine; - -// Warm - -let tag1 = createUpdatableTag(); -let tag2 = createUpdatableTag(); -let tag3 = createUpdatableTag(); - -valueForTag(tag1); -DIRTY_TAG(tag1); -valueForTag(tag1); -UPDATE_TAG(tag1, combine([tag2, tag3])); -valueForTag(tag1); -DIRTY_TAG(tag2); -valueForTag(tag1); -DIRTY_TAG(tag3); -valueForTag(tag1); -UPDATE_TAG(tag1, tag3); -valueForTag(tag1); -DIRTY_TAG(tag3); -valueForTag(tag1); diff --git a/packages/ember-template-compiler/package.json b/packages/ember-template-compiler/package.json index 6bb122fbb56..2c81370ea1c 100644 --- a/packages/ember-template-compiler/package.json +++ b/packages/ember-template-compiler/package.json @@ -32,7 +32,7 @@ "@glimmer/manager": "workspace:*", "@glimmer/runtime": "workspace:*", "@glimmer/syntax": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "backburner.js": "^2.7.0", "ember": "workspace:*", "internal-test-helpers": "workspace:*" diff --git a/packages/ember-testing/package.json b/packages/ember-testing/package.json index d2ceb6b5435..8e3d6b459ca 100644 --- a/packages/ember-testing/package.json +++ b/packages/ember-testing/package.json @@ -23,7 +23,7 @@ "@glimmer/env": "workspace:*", "@glimmer/owner": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "backburner.js": "^2.7.0", "ember": "workspace:*", "internal-test-helpers": "workspace:*", diff --git a/packages/ember/package.json b/packages/ember/package.json index 03a84ccd1e7..41273186270 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -38,7 +38,7 @@ "@glimmer/runtime": "workspace:*", "@glimmer/tracking": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "backburner.js": "^2.7.0", "dag-map": "^2.0.2", "ember-template-compiler": "workspace:*", diff --git a/packages/internal-test-helpers/lib/ember-dev/setup-qunit.ts b/packages/internal-test-helpers/lib/ember-dev/setup-qunit.ts index 78423b64470..87ab176155f 100644 --- a/packages/internal-test-helpers/lib/ember-dev/setup-qunit.ts +++ b/packages/internal-test-helpers/lib/ember-dev/setup-qunit.ts @@ -1,6 +1,6 @@ import { getOnerror, setOnerror } from '@ember/-internals/error-handling'; import { DEBUG } from '@glimmer/env'; -import { resetTracking } from '@glimmer/validator'; +import { resetTracking } from '@glimmer/signals'; declare global { interface Assert { diff --git a/packages/internal-test-helpers/package.json b/packages/internal-test-helpers/package.json index d09f8ef17c0..754943ea174 100644 --- a/packages/internal-test-helpers/package.json +++ b/packages/internal-test-helpers/package.json @@ -34,7 +34,7 @@ "@glimmer/runtime": "workspace:*", "@glimmer/syntax": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", + "@glimmer/signals": "workspace:*", "backburner.js": "^2.7.0", "dag-map": "^2.0.2", "ember": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fef94be9d6..746af03d8a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -373,6 +373,9 @@ importers: '@glimmer/runtime': specifier: workspace:* version: link:../../@glimmer/runtime + '@glimmer/signals': + specifier: workspace:* + version: link:../../@glimmer/signals '@glimmer/syntax': specifier: workspace:* version: link:../../@glimmer/syntax @@ -382,9 +385,6 @@ importers: '@glimmer/util': specifier: workspace:* version: link:../../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../../@glimmer/validator '@glimmer/vm': specifier: workspace:* version: link:../../@glimmer/vm @@ -467,12 +467,12 @@ importers: '@glimmer/owner': specifier: workspace:* version: link:../../@glimmer/owner + '@glimmer/signals': + specifier: workspace:* + version: link:../../@glimmer/signals '@glimmer/util': specifier: workspace:* version: link:../../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../../@glimmer/validator backburner.js: specifier: ^2.7.0 version: 2.8.0 @@ -533,12 +533,12 @@ importers: '@glimmer/owner': specifier: workspace:* version: link:../../@glimmer/owner + '@glimmer/signals': + specifier: workspace:* + version: link:../../@glimmer/signals '@glimmer/util': specifier: workspace:* version: link:../../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../../@glimmer/validator expect-type: specifier: ^0.15.0 version: 0.15.0 @@ -609,12 +609,12 @@ importers: '@glimmer/owner': specifier: workspace:* version: link:../../@glimmer/owner + '@glimmer/signals': + specifier: workspace:* + version: link:../../@glimmer/signals '@glimmer/util': specifier: workspace:* version: link:../../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../../@glimmer/validator expect-type: specifier: ^0.15.0 version: 0.15.0 @@ -673,12 +673,12 @@ importers: '@glimmer/owner': specifier: workspace:* version: link:../../@glimmer/owner + '@glimmer/signals': + specifier: workspace:* + version: link:../../@glimmer/signals '@glimmer/util': specifier: workspace:* version: link:../../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../../@glimmer/validator backburner.js: specifier: ^2.7.0 version: 2.8.0 @@ -744,12 +744,12 @@ importers: '@glimmer/owner': specifier: workspace:* version: link:../../@glimmer/owner + '@glimmer/signals': + specifier: workspace:* + version: link:../../@glimmer/signals '@glimmer/util': specifier: workspace:* version: link:../../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../../@glimmer/validator dag-map: specifier: ^2.0.2 version: 2.0.2 @@ -786,12 +786,12 @@ importers: '@glimmer/owner': specifier: workspace:* version: link:../../@glimmer/owner + '@glimmer/signals': + specifier: workspace:* + version: link:../../@glimmer/signals '@glimmer/util': specifier: workspace:* version: link:../../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../../@glimmer/validator internal-test-helpers: specifier: workspace:* version: link:../../internal-test-helpers @@ -813,9 +813,9 @@ importers: '@glimmer/runtime': specifier: workspace:* version: link:../../@glimmer/runtime - '@glimmer/validator': + '@glimmer/signals': specifier: workspace:* - version: link:../../@glimmer/validator + version: link:../../@glimmer/signals expect-type: specifier: ^0.15.0 version: 0.15.0 @@ -846,9 +846,9 @@ importers: '@glimmer/env': specifier: workspace:* version: link:../../@glimmer/env - '@glimmer/validator': + '@glimmer/signals': specifier: workspace:* - version: link:../../@glimmer/validator + version: link:../../@glimmer/signals internal-test-helpers: specifier: workspace:* version: link:../../internal-test-helpers @@ -912,12 +912,12 @@ importers: '@glimmer/owner': specifier: workspace:* version: link:../../@glimmer/owner + '@glimmer/signals': + specifier: workspace:* + version: link:../../@glimmer/signals '@glimmer/util': specifier: workspace:* version: link:../../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../../@glimmer/validator expect-type: specifier: ^0.15.0 version: 0.15.0 @@ -964,9 +964,9 @@ importers: packages/@ember/reactive: dependencies: - '@glimmer/validator': + '@glimmer/signals': specifier: workspace:* - version: link:../../@glimmer/validator + version: link:../../@glimmer/signals packages/@ember/renderer: dependencies: @@ -1027,12 +1027,12 @@ importers: '@glimmer/owner': specifier: workspace:* version: link:../../@glimmer/owner + '@glimmer/signals': + specifier: workspace:* + version: link:../../@glimmer/signals '@glimmer/util': specifier: workspace:* version: link:../../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../../@glimmer/validator backburner.js: specifier: ^2.7.0 version: 2.8.0 @@ -1072,9 +1072,9 @@ importers: '@glimmer/env': specifier: workspace:* version: link:../../@glimmer/env - '@glimmer/validator': + '@glimmer/signals': specifier: workspace:* - version: link:../../@glimmer/validator + version: link:../../@glimmer/signals backburner.js: specifier: ^2.7.0 version: 2.8.0 @@ -1108,12 +1108,12 @@ importers: '@glimmer/owner': specifier: workspace:* version: link:../../@glimmer/owner + '@glimmer/signals': + specifier: workspace:* + version: link:../../@glimmer/signals '@glimmer/util': specifier: workspace:* version: link:../../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../../@glimmer/validator expect-type: specifier: ^0.15.0 version: 0.15.0 @@ -1254,12 +1254,12 @@ importers: '@glimmer/owner': specifier: workspace:* version: link:../../@glimmer/owner + '@glimmer/signals': + specifier: workspace:* + version: link:../../@glimmer/signals '@glimmer/util': specifier: workspace:* version: link:../../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../../@glimmer/validator expect-type: specifier: ^0.15.0 version: 0.15.0 @@ -1373,6 +1373,9 @@ importers: '@glimmer/runtime': specifier: workspace:* version: link:../../@glimmer/runtime + '@glimmer/signals': + specifier: workspace:* + version: link:../../@glimmer/signals '@glimmer/syntax': specifier: workspace:* version: link:../../@glimmer/syntax @@ -1382,9 +1385,6 @@ importers: '@glimmer/util': specifier: workspace:* version: link:../../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../../@glimmer/validator '@glimmer/vm': specifier: workspace:* version: link:../../@glimmer/vm @@ -1485,15 +1485,15 @@ importers: '@glimmer/runtime': specifier: workspace:* version: link:../../../@glimmer/runtime + '@glimmer/signals': + specifier: workspace:* + version: link:../../../@glimmer/signals '@glimmer/syntax': specifier: workspace:* version: link:../../../@glimmer/syntax '@glimmer/util': specifier: workspace:* version: link:../../../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../../../@glimmer/validator '@glimmer/wire-format': specifier: workspace:* version: link:../../../@glimmer/wire-format @@ -1844,12 +1844,12 @@ importers: '@glimmer/reference': specifier: workspace:* version: link:../reference + '@glimmer/signals': + specifier: workspace:* + version: link:../signals '@glimmer/util': specifier: workspace:* version: link:../util - '@glimmer/validator': - specifier: workspace:* - version: link:../validator '@glimmer/vm': specifier: workspace:* version: link:../vm @@ -1887,9 +1887,9 @@ importers: '@glimmer/reference': specifier: workspace:* version: link:../../reference - '@glimmer/validator': + '@glimmer/signals': specifier: workspace:* - version: link:../../validator + version: link:../../signals '@glimmer/vm': specifier: workspace:* version: link:../../vm @@ -2058,12 +2058,12 @@ importers: '@glimmer/interfaces': specifier: workspace:* version: link:../interfaces + '@glimmer/signals': + specifier: workspace:* + version: link:../signals '@glimmer/util': specifier: workspace:* version: link:../util - '@glimmer/validator': - specifier: workspace:* - version: link:../validator devDependencies: '@glimmer/debug-util': specifier: workspace:* @@ -2095,12 +2095,12 @@ importers: '@glimmer/reference': specifier: workspace:* version: link:.. + '@glimmer/signals': + specifier: workspace:* + version: link:../../signals '@glimmer/util': specifier: workspace:* version: link:../../util - '@glimmer/validator': - specifier: workspace:* - version: link:../../validator devDependencies: '@glimmer/debug-util': specifier: workspace:* @@ -2129,12 +2129,12 @@ importers: '@glimmer/reference': specifier: workspace:* version: link:../reference + '@glimmer/signals': + specifier: workspace:* + version: link:../signals '@glimmer/util': specifier: workspace:* version: link:../util - '@glimmer/validator': - specifier: workspace:* - version: link:../validator '@glimmer/vm': specifier: workspace:* version: link:../vm @@ -2167,6 +2167,55 @@ importers: specifier: ^5.7.3 version: 5.9.3 + packages/@glimmer/signals: + dependencies: + '@glimmer/global-context': + specifier: workspace:* + version: link:../global-context + '@glimmer/interfaces': + specifier: workspace:* + version: link:../interfaces + alien-signals: + specifier: ^3.2.1 + version: 3.2.1 + devDependencies: + '@glimmer/debug-util': + specifier: workspace:* + version: link:../debug-util + '@glimmer/env': + specifier: workspace:* + version: link:../env + eslint: + specifier: ^9.20.1 + version: 9.39.5 + expect-type: + specifier: ^1.1.0 + version: 1.3.0 + publint: + specifier: ^0.3.2 + version: 0.3.21 + rollup: + specifier: ^4.2.0 + version: 4.61.0 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + packages/@glimmer/signals/test: + dependencies: + '@glimmer/global-context': + specifier: workspace:* + version: link:../../global-context + '@glimmer/interfaces': + specifier: workspace:* + version: link:../../interfaces + '@glimmer/signals': + specifier: workspace:* + version: link:.. + expect-type: + specifier: ^1.1.0 + version: 1.3.0 + packages/@glimmer/syntax: dependencies: simple-html-tokenizer: @@ -2234,9 +2283,9 @@ importers: '@ember/-internals': specifier: workspace:* version: link:../../@ember/-internals - '@glimmer/validator': + '@glimmer/signals': specifier: workspace:* - version: link:../validator + version: link:../signals packages/@glimmer/util: dependencies: @@ -2272,52 +2321,6 @@ importers: specifier: workspace:* version: link:.. - packages/@glimmer/validator: - dependencies: - '@glimmer/global-context': - specifier: workspace:* - version: link:../global-context - '@glimmer/interfaces': - specifier: workspace:* - version: link:../interfaces - devDependencies: - '@glimmer/debug-util': - specifier: workspace:* - version: link:../debug-util - '@glimmer/env': - specifier: workspace:* - version: link:../env - eslint: - specifier: ^9.20.1 - version: 9.39.4 - expect-type: - specifier: ^1.1.0 - version: 1.3.0 - publint: - specifier: ^0.3.2 - version: 0.3.21 - rollup: - specifier: ^4.2.0 - version: 4.61.0 - typescript: - specifier: ^5.7.3 - version: 5.9.3 - - packages/@glimmer/validator/test: - dependencies: - '@glimmer/global-context': - specifier: workspace:* - version: link:../../global-context - '@glimmer/interfaces': - specifier: workspace:* - version: link:../../interfaces - '@glimmer/validator': - specifier: workspace:* - version: link:.. - expect-type: - specifier: ^1.1.0 - version: 1.3.0 - packages/@glimmer/vm: dependencies: '@glimmer/interfaces': @@ -2465,15 +2468,15 @@ importers: '@glimmer/runtime': specifier: workspace:* version: link:../@glimmer/runtime + '@glimmer/signals': + specifier: workspace:* + version: link:../@glimmer/signals '@glimmer/tracking': specifier: workspace:* version: link:../@glimmer/tracking '@glimmer/util': specifier: workspace:* version: link:../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../@glimmer/validator backburner.js: specifier: ^2.7.0 version: 2.8.0 @@ -2570,12 +2573,12 @@ importers: '@glimmer/runtime': specifier: workspace:* version: link:../@glimmer/runtime + '@glimmer/signals': + specifier: workspace:* + version: link:../@glimmer/signals '@glimmer/syntax': specifier: workspace:* version: link:../@glimmer/syntax - '@glimmer/validator': - specifier: workspace:* - version: link:../@glimmer/validator backburner.js: specifier: ^2.7.0 version: 2.8.0 @@ -2636,12 +2639,12 @@ importers: '@glimmer/owner': specifier: workspace:* version: link:../@glimmer/owner + '@glimmer/signals': + specifier: workspace:* + version: link:../@glimmer/signals '@glimmer/util': specifier: workspace:* version: link:../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../@glimmer/validator backburner.js: specifier: ^2.7.0 version: 2.8.0 @@ -2732,15 +2735,15 @@ importers: '@glimmer/runtime': specifier: workspace:* version: link:../@glimmer/runtime + '@glimmer/signals': + specifier: workspace:* + version: link:../@glimmer/signals '@glimmer/syntax': specifier: workspace:* version: link:../@glimmer/syntax '@glimmer/util': specifier: workspace:* version: link:../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../@glimmer/validator backburner.js: specifier: ^2.7.0 version: 2.8.0 @@ -6166,6 +6169,9 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + alien-signals@3.2.1: + resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} + amd-name-resolver@1.3.1: resolution: {integrity: sha512-26qTEWqZQ+cxSYygZ4Cf8tsjDBLceJahhtewxtKZA3SRa4PluuqYCuheemDQD+7Mf5B7sr+zhTDWAHDh02a1Dw==} engines: {node: 6.* || 8.* || >= 10.*} @@ -16774,6 +16780,8 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + alien-signals@3.2.1: {} + amd-name-resolver@1.3.1: dependencies: ensure-posix-path: 1.1.1 diff --git a/rollup.config.mjs b/rollup.config.mjs index 1162c7291b1..3c28099f0d6 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -291,7 +291,6 @@ export function exposedDependencies() { '@glimmer/owner', '@glimmer/opcode-compiler', '@glimmer/runtime', - '@glimmer/validator', ]), '@glimmer/tracking': resolve(packageCache.appRoot, 'packages/@glimmer/tracking/index.ts'), '@glimmer/tracking/primitives/cache': resolve( @@ -311,6 +310,10 @@ export function hiddenDependencies() { 'module' ).path, '@handlebars/parser': resolve(packageCache.appRoot, 'packages/@handlebars/parser/lib/index.js'), + 'alien-signals/system': resolve( + findFromProject('@glimmer/signals', 'alien-signals').root, + 'esm/system.mjs' + ), ...walkGlimmerDeps(['@glimmer/compiler']), ...(process.env.VITE_STABLE_DECORATORS ? {} diff --git a/smoke-tests/benchmark-app/app/services/cell.js b/smoke-tests/benchmark-app/app/services/cell.js index 5ca32a6c56a..9b70e3c90e5 100644 --- a/smoke-tests/benchmark-app/app/services/cell.js +++ b/smoke-tests/benchmark-app/app/services/cell.js @@ -4,7 +4,7 @@ * * Needed for equality-based dirty checking, rather than identity-based. */ -import { consumeTag, createUpdatableTag, dirtyTag } from '@glimmer/validator'; +import { consumeTag, createTag, dirtyTag } from '@glimmer/signals'; export function cell(initial, options = { equals: Object.is }) { return new Cell(initial, options); @@ -18,7 +18,7 @@ class Cell { constructor(value, options) { this.#value = value; this.#equals = options.equals; - this.#tag = createUpdatableTag(); + this.#tag = createTag(); } get current() { diff --git a/smoke-tests/scenarios/basic-test.ts b/smoke-tests/scenarios/basic-test.ts index 6d111af1ffa..11937fffc77 100644 --- a/smoke-tests/scenarios/basic-test.ts +++ b/smoke-tests/scenarios/basic-test.ts @@ -912,7 +912,7 @@ function basicTest(scenarios: Scenarios, appName: string) { 'index.js': 'module.exports = { name: "v1-addon-without-eai" }', addon: { 'index.js': ` - import { consumeTag } from '@glimmer/validator'; + import { consumeTag } from '@glimmer/signals'; export function accessGlimmerValidator() { if (typeof consumeTag === 'function') { return "it works" diff --git a/smoke-tests/scenarios/stable-decorator-files.ts b/smoke-tests/scenarios/stable-decorator-files.ts index 15398187f46..5b7839afe4d 100644 --- a/smoke-tests/scenarios/stable-decorator-files.ts +++ b/smoke-tests/scenarios/stable-decorator-files.ts @@ -56,7 +56,7 @@ export const stableDecoratorFiles = { import { module, test } from 'qunit'; import { tracked } from '@glimmer/tracking'; import { tagForObject } from '@ember/-internals/metal'; - import { valueForTag, validateTag } from '@glimmer/validator'; + import { valueForTag, validateTag } from '@glimmer/signals'; // {{#each-in}} and ObjectProxy depend on the object's own tag, which // every form of @tracked must dirty when it is set. diff --git a/tests/node-vitest/tree-shakability.test.js b/tests/node-vitest/tree-shakability.test.js index a26056a90ee..520af9054d3 100644 --- a/tests/node-vitest/tree-shakability.test.js +++ b/tests/node-vitest/tree-shakability.test.js @@ -35,7 +35,7 @@ it('[dev] source modules with side effects', () => { "@glimmer/runtime/lib/compiled/opcodes/expressions.js", "@glimmer/runtime/lib/compiled/opcodes/lists.js", "@glimmer/runtime/lib/compiled/opcodes/vm.js", - "@glimmer/validator/index.js", + "@glimmer/signals/index.js", "@handlebars/parser/lib/parse.js", "@handlebars/parser/lib/printer.js", "@handlebars/parser/lib/whitespace-control.js", @@ -61,7 +61,7 @@ it('[prod] source modules with side effects', () => { "@glimmer/runtime/lib/compiled/opcodes/expressions.js", "@glimmer/runtime/lib/compiled/opcodes/lists.js", "@glimmer/runtime/lib/compiled/opcodes/vm.js", - "@glimmer/validator/index.js", + "@glimmer/signals/index.js", "@handlebars/parser/lib/parse.js", "@handlebars/parser/lib/printer.js", "@handlebars/parser/lib/whitespace-control.js", From e382c71ccc86e00f199067c0a2c444698e327650 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:52:27 -0400 Subject: [PATCH 2/5] Remove the revision model No tag has a revision number, and valueForTag and validateTag are gone. Each caller that held a tag and a revision now owns a subscriber node: - a classic computed property and an alias, in the meta of the object - an observer - the arguments of a curly component - a modifier opcode - ArrayProxy for its arranged content A cache always has links to its tags. A cache that loses its last subscriber removes its links and runs again on the next read, as a computed of alien-signals does. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../-internals/glimmer/lib/base-renderer.ts | 10 +- .../glimmer/lib/component-managers/curly.ts | 31 +- .../lib/utils/curly-component-state-bucket.ts | 13 +- packages/@ember/-internals/meta/lib/meta.ts | 58 +++- packages/@ember/-internals/metal/lib/alias.ts | 14 +- .../-internals/metal/lib/array_events.ts | 4 +- .../@ember/-internals/metal/lib/chain-tags.ts | 6 +- .../@ember/-internals/metal/lib/computed.ts | 29 +- .../@ember/-internals/metal/lib/observer.ts | 88 +++-- .../-internals/metal/tests/alias_test.js | 24 +- .../tests/tracked/classic_classes_test.js | 36 +- .../metal/tests/tracked/options_test.js | 22 +- .../metal/tests/tracked/standalone_test.js | 34 +- .../metal/tests/tracked/validation_test.js | 128 +++---- packages/@ember/array/proxy.ts | 26 +- packages/@glimmer/interfaces/lib/tags.d.ts | 2 - .../@glimmer/reference/test/iterable-test.ts | 8 +- .../runtime/lib/compiled/opcodes/dom.ts | 35 +- packages/@glimmer/runtime/lib/render.ts | 4 +- .../@glimmer/runtime/lib/vm/render-result.ts | 2 +- packages/@glimmer/signals/index.ts | 11 +- packages/@glimmer/signals/lib/tags.ts | 324 ++++-------------- packages/@glimmer/signals/test/graph-test.ts | 90 ++--- packages/@glimmer/signals/test/meta-test.ts | 20 +- .../signals/test/tracked-value-test.ts | 44 ++- .../@glimmer/signals/test/tracking-test.ts | 87 +++-- .../@glimmer/signals/test/validators-test.ts | 123 +++---- .../scenarios/stable-decorator-files.ts | 12 +- 28 files changed, 571 insertions(+), 714 deletions(-) diff --git a/packages/@ember/-internals/glimmer/lib/base-renderer.ts b/packages/@ember/-internals/glimmer/lib/base-renderer.ts index 2e684dae778..b7707400c3f 100644 --- a/packages/@ember/-internals/glimmer/lib/base-renderer.ts +++ b/packages/@ember/-internals/glimmer/lib/base-renderer.ts @@ -25,7 +25,7 @@ import { RuntimeOpImpl } from '@glimmer/program/lib/opcode'; import { clientBuilder } from '@glimmer/runtime/lib/vm/element-builder'; import { inTransaction, runtimeOptions } from '@glimmer/runtime/lib/environment'; import { renderComponent as glimmerRenderComponent } from '@glimmer/runtime/lib/render'; -import { currentRevision } from '@glimmer/signals/lib/tags'; +import { writeCount } from '@glimmer/signals/lib/tags'; import type { SimpleDocument, SimpleElement } from '@simple-dom/interface'; import { hasDOM } from '../../browser-environment'; import { EmberEnvironmentDelegate } from './environment'; @@ -295,7 +295,7 @@ export class RendererState { * and that run loop throws the same error again. */ if (!completedWithoutError) { - this.#lastRevision = currentRevision(); + this.#lastRevision = writeCount(); } this.#inRenderTransaction = false; } @@ -342,7 +342,7 @@ export class RendererState { root.render(); } - this.#lastRevision = currentRevision(); + this.#lastRevision = writeCount(); }); } while (roots.length > initialRootsLength); @@ -386,12 +386,12 @@ export class RendererState { * True when there is nothing to render: * no roots, or no tracked value changed after the last render. * - * `currentRevision()` changes with a write to any tag, + * `writeCount()` changes with a write to any tag, * so one changed value anywhere in the app makes every renderer invalid. * Each root then finds out for itself if the change applies to it. */ isValid(): boolean { - return this.#roots.length === 0 || this.#lastRevision === currentRevision(); + return this.#roots.length === 0 || this.#lastRevision === writeCount(); } revalidate(): void { diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts index c3031f8a257..4e18ca917b7 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts @@ -41,13 +41,14 @@ import { reifyPositional } from '@glimmer/runtime/lib/vm/arguments'; import { EMPTY_ARRAY } from '@glimmer/util/lib/array-utils'; import { unwrapTemplate } from './unwrap-template'; import { - beginTrackFrame, + beginFrame, beginUntrackFrame, + consumeFrame, consumeTag, - endTrackFrame, + createFrame, + endFrame, endUntrackFrame, - validateTag, - valueForTag, + isFrameStale, } from '@glimmer/signals/lib/tags'; import type Component from '../component'; import type { DynamicScope } from '../renderer'; @@ -276,9 +277,11 @@ export default class CurlyComponentManager // copy of the Arguments object that is safe to hold on to between renders. let capturedArgs = args.named.capture(); - beginTrackFrame(); + let argsFrame = createFrame(); + + beginFrame(argsFrame); let props = processComponentArgs(capturedArgs); - let argsTag = endTrackFrame(); + endFrame(); // Alias `id` argument to `elementId` property on the component instance. aliasIdToElementId(args, props); @@ -342,7 +345,7 @@ export default class CurlyComponentManager let bucket = new ComponentStateBucket( component, capturedArgs, - argsTag, + argsFrame, finalizer, hasWrappedElement, isInteractive @@ -363,7 +366,7 @@ export default class CurlyComponentManager endUntrackFrame(); // consume every argument so we always run again - consumeTag(bucket.argsTag); + consumeFrame(bucket.argsFrame); consumeTag(component[DIRTY_TAG]); return bucket; @@ -441,18 +444,16 @@ export default class CurlyComponentManager } update(bucket: ComponentStateBucket): void { - let { component, args, argsTag, argsRevision, isInteractive } = bucket; + let { component, args, argsFrame, isInteractive } = bucket; bucket.finalizer = _instrumentStart('render.component', rerenderInstrumentDetails, component); beginUntrackFrame(); - if (args !== null && !validateTag(argsTag, argsRevision)) { - beginTrackFrame(); + if (args !== null && isFrameStale(argsFrame)) { + beginFrame(argsFrame); let props = processComponentArgs(args); - argsTag = bucket.argsTag = endTrackFrame(); - - bucket.argsRevision = valueForTag(argsTag); + endFrame(); component[IS_DISPATCHING_ATTRS] = true; component.setProperties(props); @@ -469,7 +470,7 @@ export default class CurlyComponentManager endUntrackFrame(); - consumeTag(argsTag); + consumeFrame(argsFrame); consumeTag(component[DIRTY_TAG]); } diff --git a/packages/@ember/-internals/glimmer/lib/utils/curly-component-state-bucket.ts b/packages/@ember/-internals/glimmer/lib/utils/curly-component-state-bucket.ts index f6952e077fe..7a4760769b0 100644 --- a/packages/@ember/-internals/glimmer/lib/utils/curly-component-state-bucket.ts +++ b/packages/@ember/-internals/glimmer/lib/utils/curly-component-state-bucket.ts @@ -8,13 +8,8 @@ import { registerDestructor } from '@glimmer/destroyable'; import type { CapturedNamedArguments } from '@glimmer/interfaces'; import type { Reference } from '@glimmer/reference/lib/reference'; import { createConstRef } from '@glimmer/reference/lib/reference'; -import { - beginUntrackFrame, - endUntrackFrame, - valueForTag, - type Revision, -} from '@glimmer/signals/lib/tags'; -import type { Tag } from '@glimmer/interfaces'; +import { beginUntrackFrame, endUntrackFrame } from '@glimmer/signals/lib/tags'; +import type { TagNode } from '@glimmer/signals/lib/tags'; import type Component from '../component'; type Finalizer = () => void; @@ -33,18 +28,16 @@ function NOOP() {} export default class ComponentStateBucket { public classRef: Reference | null = null; public rootRef: Reference; - public argsRevision: Revision; constructor( public component: Component, public args: CapturedNamedArguments | null, - public argsTag: Tag, + public argsFrame: TagNode, public finalizer: Finalizer, public hasWrappedElement: boolean, public isInteractive: boolean ) { this.classRef = null; - this.argsRevision = args === null ? 0 : valueForTag(argsTag); this.rootRef = createConstRef(component, 'this'); registerDestructor(this, () => this.willDestroy(), true); diff --git a/packages/@ember/-internals/meta/lib/meta.ts b/packages/@ember/-internals/meta/lib/meta.ts index 17650c54279..6a0c422906a 100644 --- a/packages/@ember/-internals/meta/lib/meta.ts +++ b/packages/@ember/-internals/meta/lib/meta.ts @@ -3,8 +3,9 @@ import toString from '@ember/-internals/utils/lib/to-string'; import { assert } from '@ember/debug'; import { isDestroyed } from '@glimmer/destroyable'; import { DEBUG } from '@glimmer/env'; -import type { Revision } from '@glimmer/signals/lib/tags'; -import type { UpdatableTag } from '@glimmer/interfaces'; +import { createFrame, disposeFrame, isFrameStale, watchTag } from '@glimmer/signals/lib/tags'; +import type { TagNode } from '@glimmer/signals/lib/tags'; +import type { Tag, UpdatableTag } from '@glimmer/interfaces'; type ObjMap = { [key: string]: T }; @@ -99,7 +100,7 @@ export class Meta { /** @internal */ _values: ObjMap | undefined; /** @internal */ - _revisions: ObjMap | undefined; + _watches: ObjMap | undefined; /** @internal */ source: object; /** @internal */ @@ -127,7 +128,7 @@ export class Meta { this._mixins = undefined; this._lazyChains = undefined; this._values = undefined; - this._revisions = undefined; + this._watches = undefined; // initial value for all flags right now is false // see FLAGS const for detailed list of flags used @@ -170,7 +171,7 @@ export class Meta { } /** @internal */ - _getOrCreateOwnMap(key: '_values' | '_revisions' | '_lazyChains') { + _getOrCreateOwnMap(key: '_values' | '_watches' | '_lazyChains') { return this[key] || (this[key] = Object.create(null)); } @@ -221,18 +222,51 @@ export class Meta { values[key] = value; } - /** @internal */ - revisionFor(key: string): Revision | undefined { - let revisions = this._revisions; + /** + * `true` when `setCachedFor` ran for the key, also when the value is stale. + * + * @internal + */ + hasCacheFor(key: string): boolean { + let watches = this._watches; + + return watches !== undefined && watches[key] !== undefined; + } + + /** + * `true` when the value from `valueFor` is valid: + * no write reached the tag since `setCachedFor`. + * + * @internal + */ + isCachedFor(key: string): boolean { + let watches = this._watches; + let watch = watches !== undefined ? watches[key] : undefined; - return revisions !== undefined ? revisions[key] : undefined; + return watch !== undefined && !isFrameStale(watch); + } + + /** + * Marks the value for the key as valid until the next write to `tag`. + * + * @internal + */ + setCachedFor(key: string, tag: Tag) { + let watches = this._getOrCreateOwnMap('_watches'); + let watch = (watches[key] ??= createFrame()); + + watchTag(watch, tag); } /** @internal */ - setRevisionFor(key: string, revision: Revision | undefined) { - let revisions = this._getOrCreateOwnMap('_revisions'); + clearCachedFor(key: string) { + let watches = this._watches; + let watch = watches !== undefined ? watches[key] : undefined; - revisions[key] = revision; + if (watch !== undefined) { + disposeFrame(watch); + watches![key] = undefined; + } } /** @internal */ diff --git a/packages/@ember/-internals/metal/lib/alias.ts b/packages/@ember/-internals/metal/lib/alias.ts index 927a079f10d..436f883ef2c 100644 --- a/packages/@ember/-internals/metal/lib/alias.ts +++ b/packages/@ember/-internals/metal/lib/alias.ts @@ -3,13 +3,7 @@ import { meta as metaFor } from '@ember/-internals/meta/lib/meta'; import inspect from '@ember/debug/lib/inspect'; import { assert } from '@ember/debug'; import type { UpdatableTag } from '@glimmer/interfaces'; -import { - consumeTag, - untrack, - updateTag, - validateTag, - valueForTag, -} from '@glimmer/signals/lib/tags'; +import { consumeTag, untrack, updateTag } from '@glimmer/signals/lib/tags'; import { tagFor, tagMetaFor } from '@glimmer/signals/lib/meta'; import { CHAIN_PASS_THROUGH, finishLazyChains, getChainTagsForKey } from './chain-tags'; import type { ExtendedMethodDecorator } from './decorator'; @@ -90,11 +84,9 @@ class AliasedProperty extends ComputedDescriptor { ret = get(obj, this.altKey); }); - let lastRevision = meta.revisionFor(keyName); - - if (lastRevision === undefined || !validateTag(propertyTag, lastRevision)) { + if (!meta.isCachedFor(keyName)) { updateTag(propertyTag, getChainTagsForKey(obj, this.altKey, tagMeta, meta)); - meta.setRevisionFor(keyName, valueForTag(propertyTag)); + meta.setCachedFor(keyName, propertyTag); finishLazyChains(meta, keyName, ret); } diff --git a/packages/@ember/-internals/metal/lib/array_events.ts b/packages/@ember/-internals/metal/lib/array_events.ts index 61fd6072501..8dd4509c067 100644 --- a/packages/@ember/-internals/metal/lib/array_events.ts +++ b/packages/@ember/-internals/metal/lib/array_events.ts @@ -68,11 +68,11 @@ export function arrayContentDidChange( let previousLength = length - delta; let normalStartIdx = startIdx < 0 ? previousLength + startIdx : startIdx; - if (meta.revisionFor('firstObject') !== undefined && normalStartIdx === 0) { + if (meta.hasCacheFor('firstObject') && normalStartIdx === 0) { notifyPropertyChange(array, 'firstObject', meta); } - if (meta.revisionFor('lastObject') !== undefined) { + if (meta.hasCacheFor('lastObject')) { let previousLastIndex = previousLength - 1; let lastAffectedIndex = normalStartIdx + removedAmount; if (previousLastIndex < lastAffectedIndex) { diff --git a/packages/@ember/-internals/metal/lib/chain-tags.ts b/packages/@ember/-internals/metal/lib/chain-tags.ts index 3662f8c2655..8867c923f7d 100644 --- a/packages/@ember/-internals/metal/lib/chain-tags.ts +++ b/packages/@ember/-internals/metal/lib/chain-tags.ts @@ -4,7 +4,7 @@ import { isObject } from '@ember/-internals/utils/lib/spec'; import { assert } from '@ember/debug'; import type { Tag } from '@glimmer/interfaces'; import type { TagMeta } from '@glimmer/signals/lib/meta'; -import { combine, createTag, updateTag, validateTag } from '@glimmer/signals/lib/tags'; +import { combine, createTag, updateTag } from '@glimmer/signals/lib/tags'; import { tagMetaFor } from '@glimmer/signals/lib/meta'; import { objectAt } from './object-at'; import { tagForProperty } from './tags'; @@ -171,9 +171,7 @@ function getChainTags( // we create a lazy chain lookup, and the next time the CP is calculated, // it will update that lazy chain. let instanceMeta = currentMeta!.source === current ? currentMeta! : metaFor(current); - let lastRevision = instanceMeta.revisionFor(segment); - - if (lastRevision !== undefined && validateTag(propertyTag, lastRevision)) { + if (instanceMeta.isCachedFor(segment)) { current = instanceMeta.valueFor(segment); } else { // use metaFor here to ensure we have the meta for the instance diff --git a/packages/@ember/-internals/metal/lib/computed.ts b/packages/@ember/-internals/metal/lib/computed.ts index f2797fe1efe..a24aef176f8 100644 --- a/packages/@ember/-internals/metal/lib/computed.ts +++ b/packages/@ember/-internals/metal/lib/computed.ts @@ -5,14 +5,7 @@ import inspect from '@ember/debug/lib/inspect'; import { assert } from '@ember/debug'; import { isDestroyed } from '@glimmer/destroyable'; import type { UpdatableTag } from '@glimmer/interfaces'; -import { - consumeTag, - track, - untrack, - updateTag, - validateTag, - valueForTag, -} from '@glimmer/signals/lib/tags'; +import { consumeTag, track, untrack, updateTag } from '@glimmer/signals/lib/tags'; import { tagFor, tagMetaFor } from '@glimmer/signals/lib/meta'; import { finishLazyChains, getChainTagsForKeys } from './chain-tags'; import type { @@ -402,9 +395,7 @@ export class ComputedProperty extends ComputedDescriptor { let ret; - let revision = meta.revisionFor(keyName); - - if (revision !== undefined && validateTag(propertyTag, revision)) { + if (meta.isCachedFor(keyName)) { ret = meta.valueFor(keyName); } else { // For backwards compatibility, we only throw if the CP has any dependencies. CPs without dependencies @@ -426,7 +417,7 @@ export class ComputedProperty extends ComputedDescriptor { } meta.setValueFor(keyName, ret); - meta.setRevisionFor(keyName, valueForTag(propertyTag)); + meta.setCachedFor(keyName, propertyTag); finishLazyChains(meta, keyName, ret); } @@ -499,7 +490,7 @@ export class ComputedProperty extends ComputedDescriptor { updateTag(propertyTag, getChainTagsForKeys(obj, _dependentKeys, tagMeta, meta)); } - meta.setRevisionFor(keyName, valueForTag(propertyTag)); + meta.setCachedFor(keyName, propertyTag); } finally { endPropertyChanges(); } @@ -512,7 +503,7 @@ export class ComputedProperty extends ComputedDescriptor { } _set(obj: object, keyName: string, value: unknown, meta: Meta): unknown { - let hadCachedValue = meta.revisionFor(keyName) !== undefined; + let hadCachedValue = meta.hasCacheFor(keyName); let cachedValue = meta.valueFor(keyName); let ret; @@ -540,8 +531,8 @@ export class ComputedProperty extends ComputedDescriptor { /* called before property is overridden */ teardown(obj: object, keyName: string, meta: Meta): void { - if (meta.revisionFor(keyName) !== undefined) { - meta.setRevisionFor(keyName, undefined); + if (meta.hasCacheFor(keyName)) { + meta.clearCachedFor(keyName); meta.setValueFor(keyName, undefined); } @@ -558,9 +549,7 @@ class AutoComputedProperty extends ComputedProperty { let ret; - let revision = meta.revisionFor(keyName); - - if (revision !== undefined && validateTag(propertyTag, revision)) { + if (meta.isCachedFor(keyName)) { ret = meta.valueFor(keyName); } else { assert( @@ -578,7 +567,7 @@ class AutoComputedProperty extends ComputedProperty { updateTag(propertyTag, tag); meta.setValueFor(keyName, ret); - meta.setRevisionFor(keyName, valueForTag(propertyTag)); + meta.setCachedFor(keyName, propertyTag); finishLazyChains(meta, keyName, ret); } diff --git a/packages/@ember/-internals/metal/lib/observer.ts b/packages/@ember/-internals/metal/lib/observer.ts index 714f504acbd..7b80cf9be62 100644 --- a/packages/@ember/-internals/metal/lib/observer.ts +++ b/packages/@ember/-internals/metal/lib/observer.ts @@ -2,17 +2,25 @@ import { ENV } from '@ember/-internals/environment/lib/env'; import { peekMeta } from '@ember/-internals/meta/lib/meta'; import type { schedule } from '@ember/runloop'; import { registerDestructor } from '@glimmer/destroyable'; -import type { Tag } from '@glimmer/interfaces'; -import { currentRevision, validateTag, valueForTag } from '@glimmer/signals/lib/tags'; +import { + createFrame, + disposeFrame, + isFrameStale, + watchTag, + writeCount, +} from '@glimmer/signals/lib/tags'; +import type { TagNode } from '@glimmer/signals/lib/tags'; import { tagMetaFor } from '@glimmer/signals/lib/meta'; import { getChainTagsForKey } from './chain-tags'; import changeEvent from './change_event'; import { addListener, removeListener, sendEvent } from './events'; interface ActiveObserver { - tag: Tag; + /** + * The subscriber for the tags of `path`. It is stale after a write to one of them. + */ + watch: TagNode; path: string; - lastRevision: number; count: number; suspended: boolean; } @@ -99,15 +107,10 @@ export function activateObserver(target: object, eventName: string, sync = false activeObservers.get(eventName)!.count++; } else { let path = eventName.substring(0, eventName.lastIndexOf(':')); - let tag = getChainTagsForKey(target, path, tagMetaFor(target), peekMeta(target)); - - activeObservers.set(eventName, { - count: 1, - path, - tag, - lastRevision: valueForTag(tag), - suspended: false, - }); + let observer = { count: 1, path, watch: createFrame(), suspended: false }; + + rewatch(target, observer); + activeObservers.set(eventName, observer); } } @@ -130,6 +133,7 @@ function deactivateObserver(target: object, eventName: string, sync = false) { observer.count--; if (observer.count === 0) { + disposeFrame(observer.watch); activeObservers.delete(eventName); if (activeObservers.size === 0) { @@ -163,54 +167,36 @@ export function resumeObserverDeactivation() { export function revalidateObservers(target: object) { if (ASYNC_OBSERVERS.has(target)) { ASYNC_OBSERVERS.get(target)!.forEach((observer) => { - observer.tag = getChainTagsForKey( - target, - observer.path, - tagMetaFor(target), - peekMeta(target) - ); - observer.lastRevision = valueForTag(observer.tag); + rewatch(target, observer); }); } if (SYNC_OBSERVERS.has(target)) { SYNC_OBSERVERS.get(target)!.forEach((observer) => { - observer.tag = getChainTagsForKey( - target, - observer.path, - tagMetaFor(target), - peekMeta(target) - ); - observer.lastRevision = valueForTag(observer.tag); + rewatch(target, observer); }); } } -let lastKnownRevision = 0; +let lastKnownWrites = 0; export function flushAsyncObservers(_schedule: typeof schedule | false) { - let revision = currentRevision(); - if (lastKnownRevision === revision) { + let writes = writeCount(); + if (lastKnownWrites === writes) { return; } - lastKnownRevision = revision; + lastKnownWrites = writes; ASYNC_OBSERVERS.forEach((activeObservers, target) => { let meta = peekMeta(target); activeObservers.forEach((observer, eventName) => { - if (!validateTag(observer.tag, observer.lastRevision)) { + if (isFrameStale(observer.watch)) { let sendObserver = () => { try { sendEvent(target, eventName, [target, observer.path], undefined, meta); } finally { - observer.tag = getChainTagsForKey( - target, - observer.path, - tagMetaFor(target), - peekMeta(target) - ); - observer.lastRevision = valueForTag(observer.tag); + rewatch(target, observer); } }; @@ -233,18 +219,12 @@ export function flushSyncObservers() { let meta = peekMeta(target); activeObservers.forEach((observer, eventName) => { - if (!observer.suspended && !validateTag(observer.tag, observer.lastRevision)) { + if (!observer.suspended && isFrameStale(observer.watch)) { try { observer.suspended = true; sendEvent(target, eventName, [target, observer.path], undefined, meta); } finally { - observer.tag = getChainTagsForKey( - target, - observer.path, - tagMetaFor(target), - peekMeta(target) - ); - observer.lastRevision = valueForTag(observer.tag); + rewatch(target, observer); observer.suspended = false; } } @@ -266,7 +246,21 @@ export function setObserverSuspended(target: object, property: string, suspended } } +function rewatch(target: object, observer: ActiveObserver) { + watchTag( + observer.watch, + getChainTagsForKey(target, observer.path, tagMetaFor(target), peekMeta(target)) + ); +} + function destroyObservers(target: object) { + SYNC_OBSERVERS.get(target)?.forEach(disposeWatch); + ASYNC_OBSERVERS.get(target)?.forEach(disposeWatch); + if (SYNC_OBSERVERS.size > 0) SYNC_OBSERVERS.delete(target); if (ASYNC_OBSERVERS.size > 0) ASYNC_OBSERVERS.delete(target); } + +function disposeWatch(observer: ActiveObserver) { + disposeFrame(observer.watch); +} diff --git a/packages/@ember/-internals/metal/tests/alias_test.js b/packages/@ember/-internals/metal/tests/alias_test.js index 6f29037c82e..a54a8d45479 100644 --- a/packages/@ember/-internals/metal/tests/alias_test.js +++ b/packages/@ember/-internals/metal/tests/alias_test.js @@ -12,7 +12,17 @@ import EmberObject from '@ember/object'; import { A } from '@ember/array'; import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { destroy } from '@glimmer/destroyable'; -import { valueForTag, validateTag } from '@glimmer/signals'; +import { createFrame, isFrameStale, watchTag } from '@glimmer/signals'; + +function watch(tag) { + let frame = createFrame(); + watchTag(frame, tag); + return frame; +} + +function isValid(frame) { + return !isFrameStale(frame); +} let obj, count; @@ -146,10 +156,10 @@ moduleFor( get(obj, 'bar'); let tag = tagForProperty(obj, 'bar'); - let tagValue = valueForTag(tag); + let tagValue = watch(tag); set(obj, 'foo.faz', 'BAR'); - assert.ok(!validateTag(tag, tagValue), 'setting the aliased key should dirty the object'); + assert.ok(!isValid(tagValue), 'setting the aliased key should dirty the object'); } ['@test setting alias on self should fail assertion']() { @@ -162,20 +172,20 @@ moduleFor( ['@test property tags are bumped when the source changes [GH#17243]'](assert) { function assertPropertyTagChanged(obj, keyName, callback) { let tag = tagForProperty(obj, keyName); - let before = valueForTag(tag); + let before = watch(tag); callback(); - assert.notOk(validateTag(tag, before), `tagForProperty ${keyName} should change`); + assert.notOk(isValid(before), `tagForProperty ${keyName} should change`); } function assertPropertyTagUnchanged(obj, keyName, callback) { let tag = tagForProperty(obj, keyName); - let before = valueForTag(tag); + let before = watch(tag); callback(); - assert.ok(validateTag(tag, before), `tagForProperty ${keyName} should not change`); + assert.ok(isValid(before), `tagForProperty ${keyName} should not change`); } defineProperty(obj, 'bar', alias('foo.faz')); diff --git a/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js b/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js index 4606d82ce7f..1cf41f0488f 100644 --- a/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js +++ b/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js @@ -1,7 +1,17 @@ import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; import { defineProperty, tracked, nativeDescDecorator } from '../..'; -import { track, valueForTag, validateTag } from '@glimmer/signals'; +import { createFrame, isFrameStale, track, watchTag } from '@glimmer/signals'; + +function watch(tag) { + let frame = createFrame(); + watchTag(frame, tag); + return frame; +} + +function isValid(frame) { + return !isFrameStale(frame); +} moduleFor( '@tracked decorator - classic classes', @@ -35,24 +45,24 @@ moduleFor( obj.last = 'Dale'; let tag = track(() => obj.full); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); assert.equal(obj.full, 'Tom Dale', 'The full name starts correct'); - assert.equal(validateTag(tag, snapshot), true); + assert.equal(isValid(snapshot), true); - snapshot = valueForTag(tag); - assert.equal(validateTag(tag, snapshot), true); + snapshot = watch(tag); + assert.equal(isValid(snapshot), true); obj.full = 'Melanie Sumner'; - assert.equal(validateTag(tag, snapshot), false); + assert.equal(isValid(snapshot), false); assert.equal(obj.full, 'Melanie Sumner'); assert.equal(obj.first, 'Melanie'); assert.equal(obj.last, 'Sumner'); - snapshot = valueForTag(tag); + snapshot = watch(tag); - assert.equal(validateTag(tag, snapshot), true); + assert.equal(isValid(snapshot), true); } [`@test can pass a default value to the tracked decorator`](assert) { @@ -113,15 +123,15 @@ moduleFor( let obj = new Tracked(); let tag = track(() => obj.first); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); assert.equal(obj.first, 'Tom', 'default value is assigned'); obj.first = 'Tom'; - assert.equal(validateTag(tag, snapshot), true, 'setting an equal value does not invalidate'); + assert.equal(isValid(snapshot), true, 'setting an equal value does not invalidate'); obj.first = 'Thomas'; - assert.equal(validateTag(tag, snapshot), false, 'setting a new value invalidates'); + assert.equal(isValid(snapshot), false, 'setting a new value invalidates'); } } ); @@ -151,13 +161,13 @@ moduleFor( let obj = new Tracked(); let tag = track(() => obj.value); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); assert.equal(obj.value, 1, 'initial value is assigned'); obj.value = 2; - assert.equal(validateTag(tag, snapshot), false, 'setting a value invalidates'); + assert.equal(isValid(snapshot), false, 'setting a value invalidates'); assert.equal(obj.value, 2); } diff --git a/packages/@ember/-internals/metal/tests/tracked/options_test.js b/packages/@ember/-internals/metal/tests/tracked/options_test.js index bd79be39274..fd252f2b9e1 100644 --- a/packages/@ember/-internals/metal/tests/tracked/options_test.js +++ b/packages/@ember/-internals/metal/tests/tracked/options_test.js @@ -1,7 +1,17 @@ import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; import { tracked } from '../..'; -import { track, valueForTag, validateTag } from '@glimmer/signals'; +import { createFrame, isFrameStale, track, watchTag } from '@glimmer/signals'; + +function watch(tag) { + let frame = createFrame(); + watchTag(frame, tag); + return frame; +} + +function isValid(frame) { + return !isFrameStale(frame); +} moduleFor( '@tracked decorator - options', @@ -14,15 +24,15 @@ moduleFor( let obj = new Tracked(); let tag = track(() => obj.value); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); assert.strictEqual(obj.value, 0, 'initializer ran'); obj.value = 0; - assert.true(validateTag(tag, snapshot), 'setting an equal value does not invalidate'); + assert.true(isValid(snapshot), 'setting an equal value does not invalidate'); obj.value = 1; - assert.false(validateTag(tag, snapshot), 'setting a new value invalidates'); + assert.false(isValid(snapshot), 'setting a new value invalidates'); assert.strictEqual(obj.value, 1); } @@ -34,11 +44,11 @@ moduleFor( let obj = new Tracked(); let tag = track(() => obj.value); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); let current = obj.value; obj.value = current; - assert.false(validateTag(tag, snapshot), 'a no-op set invalidates'); + assert.false(isValid(snapshot), 'a no-op set invalidates'); } ['@test equals option is compared against the initial value before any read'](assert) { diff --git a/packages/@ember/-internals/metal/tests/tracked/standalone_test.js b/packages/@ember/-internals/metal/tests/tracked/standalone_test.js index 86beccbca52..afe807886e5 100644 --- a/packages/@ember/-internals/metal/tests/tracked/standalone_test.js +++ b/packages/@ember/-internals/metal/tests/tracked/standalone_test.js @@ -1,7 +1,17 @@ import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; import { tracked } from '../..'; -import { track, valueForTag, validateTag } from '@glimmer/signals'; +import { createFrame, isFrameStale, track, watchTag } from '@glimmer/signals'; + +function watch(tag) { + let frame = createFrame(); + watchTag(frame, tag); + return frame; +} + +function isValid(frame) { + return !isFrameStale(frame); +} moduleFor( 'tracked() - standalone usage', @@ -26,48 +36,48 @@ moduleFor( let count = tracked(0); let tag = track(() => count.value); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); - assert.true(validateTag(tag, snapshot), 'tag is valid before a change'); + assert.true(isValid(snapshot), 'tag is valid before a change'); count.value = 1; - assert.false(validateTag(tag, snapshot), 'tag is invalidated by a change'); + assert.false(isValid(snapshot), 'tag is invalidated by a change'); } ['@test default equality is Object.is'](assert) { let count = tracked(0); let tag = track(() => count.value); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); count.value = 0; - assert.true(validateTag(tag, snapshot), 'setting an equal value does not invalidate'); + assert.true(isValid(snapshot), 'setting an equal value does not invalidate'); count.value = 1; - assert.false(validateTag(tag, snapshot), 'setting a new value invalidates'); + assert.false(isValid(snapshot), 'setting a new value invalidates'); } ['@test equality can be customized'](assert) { let state = tracked({ id: 1 }, { equals: (a, b) => a.id === b.id }); let tag = track(() => state.value); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); state.value = { id: 1 }; - assert.true(validateTag(tag, snapshot), 'setting an equal value does not invalidate'); + assert.true(isValid(snapshot), 'setting an equal value does not invalidate'); state.value = { id: 2 }; - assert.false(validateTag(tag, snapshot), 'setting a different value invalidates'); + assert.false(isValid(snapshot), 'setting a different value invalidates'); } ['@test always-dirty equality'](assert) { let count = tracked(0, { equals: () => false }); let tag = track(() => count.value); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); count.value = 0; - assert.false(validateTag(tag, snapshot), 'a no-op set invalidates'); + assert.false(isValid(snapshot), 'a no-op set invalidates'); } ['@test freeze() prevents further updates'](assert) { diff --git a/packages/@ember/-internals/metal/tests/tracked/validation_test.js b/packages/@ember/-internals/metal/tests/tracked/validation_test.js index 2cc1a690617..a63625d8804 100644 --- a/packages/@ember/-internals/metal/tests/tracked/validation_test.js +++ b/packages/@ember/-internals/metal/tests/tracked/validation_test.js @@ -9,7 +9,17 @@ import { } from '../..'; import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; -import { track, valueForTag, validateTag } from '@glimmer/signals'; +import { createFrame, isFrameStale, track, watchTag } from '@glimmer/signals'; + +function watch(tag) { + let frame = createFrame(); + watchTag(frame, tag); + return frame; +} + +function isValid(frame) { + return !isFrameStale(frame); +} moduleFor( '@tracked get validation', @@ -25,22 +35,22 @@ moduleFor( let obj = new Tracked('Tom', 'Dale'); let tag = track(() => obj.first); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); assert.equal(obj.first, 'Tom', 'The full name starts correct'); - assert.equal(validateTag(tag, snapshot), true); + assert.equal(isValid(snapshot), true); - snapshot = valueForTag(tag); - assert.equal(validateTag(tag, snapshot), true); + snapshot = watch(tag); + assert.equal(isValid(snapshot), true); obj.first = 'Thomas'; - assert.equal(validateTag(tag, snapshot), false); + assert.equal(isValid(snapshot), false); assert.equal(obj.first, 'Thomas'); - snapshot = valueForTag(tag); + snapshot = watch(tag); - assert.equal(validateTag(tag, snapshot), true); + assert.equal(isValid(snapshot), true); } [`@test autotracking should work with initializers (${import.meta.env.VITE_STABLE_DECORATORS ? 'stable' : 'legacy'} decorators)`]( @@ -54,17 +64,17 @@ moduleFor( let obj = new Tracked(); let tag = track(() => obj.first); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); let expectedInitialValue = import.meta.env.VITE_STABLE_DECORATORS ? 'first: undefined' : 'first: second'; assert.equal(obj.first, expectedInitialValue, 'The value initializes correctly'); - assert.equal(validateTag(tag, snapshot), true); + assert.equal(isValid(snapshot), true); - snapshot = valueForTag(tag); - assert.equal(validateTag(tag, snapshot), true); + snapshot = watch(tag); + assert.equal(isValid(snapshot), true); obj.second = '2nd'; @@ -72,18 +82,18 @@ moduleFor( // assert.equal(validate(tag, snapshot), true); assert.equal(obj.first, expectedInitialValue, 'The value stays the same once initialized'); - snapshot = valueForTag(tag); + snapshot = watch(tag); - assert.equal(validateTag(tag, snapshot), true); + assert.equal(isValid(snapshot), true); obj.first = 'FIRST!!!'; - assert.equal(validateTag(tag, snapshot), false); + assert.equal(isValid(snapshot), false); assert.equal(obj.first, 'FIRST!!!'); - snapshot = valueForTag(tag); + snapshot = watch(tag); - assert.equal(validateTag(tag, snapshot), true); + assert.equal(isValid(snapshot), true); } [`@test autotracking should work with native getters`](assert) { @@ -103,22 +113,22 @@ moduleFor( let obj = new Tracked('Tom', 'Dale'); let tag = track(() => obj.full); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); assert.equal(obj.full, 'Tom Dale', 'The full name starts correct'); - assert.equal(validateTag(tag, snapshot), true); + assert.equal(isValid(snapshot), true); - snapshot = valueForTag(tag); - assert.equal(validateTag(tag, snapshot), true); + snapshot = watch(tag); + assert.equal(isValid(snapshot), true); obj.first = 'Thomas'; - assert.equal(validateTag(tag, snapshot), false); + assert.equal(isValid(snapshot), false); assert.equal(obj.full, 'Thomas Dale'); - snapshot = valueForTag(tag); + snapshot = watch(tag); - assert.equal(validateTag(tag, snapshot), true); + assert.equal(isValid(snapshot), true); } [`@test autotracking should work with native setters`](assert) { @@ -145,24 +155,24 @@ moduleFor( let obj = new Tracked('Tom', 'Dale'); let tag = track(() => obj.full); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); assert.equal(obj.full, 'Tom Dale', 'The full name starts correct'); - assert.equal(validateTag(tag, snapshot), true); + assert.equal(isValid(snapshot), true); - snapshot = valueForTag(tag); - assert.equal(validateTag(tag, snapshot), true); + snapshot = watch(tag); + assert.equal(isValid(snapshot), true); obj.full = 'Melanie Sumner'; - assert.equal(validateTag(tag, snapshot), false); + assert.equal(isValid(snapshot), false); assert.equal(obj.full, 'Melanie Sumner'); assert.equal(obj.first, 'Melanie'); assert.equal(obj.last, 'Sumner'); - snapshot = valueForTag(tag); + snapshot = watch(tag); - assert.equal(validateTag(tag, snapshot), true); + assert.equal(isValid(snapshot), true); } [`@test interaction with Ember object model (tracked property depending on Ember property)`]( @@ -185,25 +195,25 @@ moduleFor( let tag = track(() => obj.full); assert.equal(obj.full, 'Tom Dale'); - let snapshot = valueForTag(tag); - assert.equal(validateTag(tag, snapshot), true); + let snapshot = watch(tag); + assert.equal(isValid(snapshot), true); set(tom, 'first', 'Thomas'); - assert.equal(validateTag(tag, snapshot), false, 'invalid after setting with Ember set'); + assert.equal(isValid(snapshot), false, 'invalid after setting with Ember set'); assert.equal(obj.full, 'Thomas Dale'); - snapshot = valueForTag(tag); + snapshot = watch(tag); - assert.equal(validateTag(tag, snapshot), true); + assert.equal(isValid(snapshot), true); set(obj, 'name', { first: 'Ricardo', last: 'Mendes' }); - assert.equal(validateTag(tag, snapshot), false, 'invalid after setting with Ember set'); + assert.equal(isValid(snapshot), false, 'invalid after setting with Ember set'); assert.equal(obj.full, 'Ricardo Mendes'); - snapshot = valueForTag(tag); + snapshot = watch(tag); - assert.equal(validateTag(tag, snapshot), true); + assert.equal(isValid(snapshot), true); } [`@test interaction with Ember object model (Ember computed property depending on tracked property)`]( @@ -241,20 +251,16 @@ moduleFor( let full = get(obj, 'full'); assert.equal(full, 'Tom Dale'); - let snapshot = valueForTag(tag); - assert.equal(validateTag(tag, snapshot), true); + let snapshot = watch(tag); + assert.equal(isValid(snapshot), true); tom.first = 'Thomas'; - assert.equal( - validateTag(tag, snapshot), - false, - 'invalid after setting with tracked properties' - ); + assert.equal(isValid(snapshot), false, 'invalid after setting with tracked properties'); assert.equal(get(obj, 'full'), 'Thomas Dale'); - snapshot = valueForTag(tag); + snapshot = watch(tag); - assert.equal(validateTag(tag, snapshot), true); + assert.equal(isValid(snapshot), true); } ['@test interaction with the Ember object model (paths going through tracked properties)']( @@ -302,23 +308,23 @@ moduleFor( let full = get(obj, 'full'); assert.equal(full, 'Tom Dale'); - let snapshot = valueForTag(tag); - assert.equal(validateTag(tag, snapshot), true); + let snapshot = watch(tag); + assert.equal(isValid(snapshot), true); set(tom, 'first', 'Thomas'); - assert.equal(validateTag(tag, snapshot), false, 'invalid after setting with Ember.set'); + assert.equal(isValid(snapshot), false, 'invalid after setting with Ember.set'); assert.equal(get(obj, 'full'), 'Thomas Dale'); - snapshot = valueForTag(tag); + snapshot = watch(tag); tom = contact.name = new EmberName('T', 'Dale'); - assert.equal(validateTag(tag, snapshot), false, 'invalid after setting with Ember.set'); + assert.equal(isValid(snapshot), false, 'invalid after setting with Ember.set'); assert.equal(get(obj, 'full'), 'T Dale'); - snapshot = valueForTag(tag); + snapshot = watch(tag); set(tom, 'first', 'Tizzle'); - assert.equal(validateTag(tag, snapshot), false, 'invalid after setting with Ember.set'); + assert.equal(isValid(snapshot), false, 'invalid after setting with Ember.set'); assert.equal(get(obj, 'full'), 'Tizzle Dale'); } @@ -332,15 +338,15 @@ moduleFor( let array; let tag = track(() => (array = get(obj, 'array'))); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); assert.deepEqual(array, []); - assert.equal(validateTag(tag, snapshot), true); + assert.equal(isValid(snapshot), true); array.push(1); notifyPropertyChange(array, '[]'); assert.equal( - validateTag(tag, snapshot), + isValid(snapshot), false, 'invalid after pushing an object and notifying on the array' ); @@ -355,15 +361,15 @@ moduleFor( let array; let tag = track(() => (array = obj.array)); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); assert.deepEqual(array, []); - assert.equal(validateTag(tag, snapshot), true); + assert.equal(isValid(snapshot), true); array.push(1); notifyPropertyChange(array, '[]'); assert.equal( - validateTag(tag, snapshot), + isValid(snapshot), false, 'invalid after pushing an object and notifying on the array' ); diff --git a/packages/@ember/array/proxy.ts b/packages/@ember/array/proxy.ts index e4e9c259658..307407224e6 100644 --- a/packages/@ember/array/proxy.ts +++ b/packages/@ember/array/proxy.ts @@ -23,10 +23,11 @@ import { setCustomTagFor } from '@glimmer/manager/lib/util/args-proxy'; import { combine, consumeTag, - validateTag, - valueForTag, - type Revision, + createFrame, + isFrameStale, + watchTag, } from '@glimmer/signals/lib/tags'; +import type { TagNode } from '@glimmer/signals/lib/tags'; import { tagFor } from '@glimmer/signals/lib/meta'; import type { Tag } from '@glimmer/interfaces'; @@ -192,9 +193,7 @@ class ArrayProxy extends EmberObject implements PropertyDidChange { /** @internal */ _arrangedContentIsUpdating = false; /** @internal */ - _arrangedContentTag: Tag | null = null; - /** @internal */ - _arrangedContentRevision: Revision | null = null; + _arrangedContentWatch: TagNode | null = null; /** @internal */ _lengthTag: Tag | null = null; /** @internal */ @@ -383,13 +382,12 @@ class ArrayProxy extends EmberObject implements PropertyDidChange { _revalidate() { if (this._arrangedContentIsUpdating === true) return; - if ( - this._arrangedContentTag === null || - !validateTag(this._arrangedContentTag, this._arrangedContentRevision!) - ) { + let watch = this._arrangedContentWatch; + + if (watch === null || isFrameStale(watch)) { let arrangedContent = this.get('arrangedContent'); - if (this._arrangedContentTag === null) { + if (watch === null) { // This is the first time the proxy has been setup, only add the observer // don't trigger any events this._addArrangedContentArrayObserver(arrangedContent); @@ -399,8 +397,10 @@ class ArrayProxy extends EmberObject implements PropertyDidChange { this._arrangedContentIsUpdating = false; } - let arrangedContentTag = (this._arrangedContentTag = tagFor(this, 'arrangedContent')); - this._arrangedContentRevision = valueForTag(this._arrangedContentTag); + let arrangedContentTag = tagFor(this, 'arrangedContent'); + + watch = this._arrangedContentWatch ??= createFrame(); + watchTag(watch, arrangedContentTag); if (isObject(arrangedContent)) { this._lengthTag = combine([arrangedContentTag, tagForProperty(arrangedContent, 'length')]); diff --git a/packages/@glimmer/interfaces/lib/tags.d.ts b/packages/@glimmer/interfaces/lib/tags.d.ts index 4e77323b1ab..7e08127480d 100644 --- a/packages/@glimmer/interfaces/lib/tags.d.ts +++ b/packages/@glimmer/interfaces/lib/tags.d.ts @@ -1,5 +1,3 @@ -export type Revision = number; - /** * A node of the reactive graph. * diff --git a/packages/@glimmer/reference/test/iterable-test.ts b/packages/@glimmer/reference/test/iterable-test.ts index f209a0f5b6d..5301fe284dd 100644 --- a/packages/@glimmer/reference/test/iterable-test.ts +++ b/packages/@glimmer/reference/test/iterable-test.ts @@ -1,15 +1,16 @@ import type { OpaqueIterationItem, Reference } from '@glimmer/reference'; import { createComputeRef, createIteratorRef, valueForRef } from '@glimmer/reference'; -import { consumeTag, VOLATILE_TAG } from '@glimmer/signals'; +import { consumeTag, createTag, dirtyTag } from '@glimmer/signals'; import { module, test } from './utils/qunit'; class IterableWrapper { private iterable: Reference<{ next(): OpaqueIterationItem | null }>; + private tag = createTag(); constructor(obj: unknown, key = '@identity') { let valueRef = createComputeRef(() => { - consumeTag(VOLATILE_TAG); + consumeTag(this.tag); return obj; }); this.iterable = createIteratorRef(valueRef, key); @@ -18,6 +19,9 @@ class IterableWrapper { private iterate() { let result: OpaqueIterationItem[] = []; + // the object has no tags of its own, so each iteration starts with a write + dirtyTag(this.tag); + // bootstrap let iterator = valueForRef(this.iterable); let item = iterator.next(); diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts index 8c4f7aad34c..a1e123aa292 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts @@ -12,11 +12,11 @@ import type { } from '@glimmer/interfaces'; import type { Reference } from '@glimmer/reference/lib/reference'; import { + CONSTANT_TAG, consumeTag, - validateTag, - valueForTag, - type Revision, - INITIAL, + createFrame, + isFrameStale, + watchTag, } from '@glimmer/signals/lib/tags'; import type { Tag } from '@glimmer/interfaces'; import { CURRIED_MODIFIER } from '@glimmer/constants/lib/curried'; @@ -323,40 +323,43 @@ APPEND_OPCODES.add(VM_DYNAMIC_MODIFIER_OP, (vm) => { }); export class UpdateModifierOpcode implements UpdatingOpcode { - private lastUpdated: Revision; + /** + * Stale after a write to `tag`. + */ + private watch = createFrame(); constructor( private tag: Tag, private modifier: ModifierInstance ) { - this.lastUpdated = valueForTag(tag); + watchTag(this.watch, tag); } evaluate(vm: UpdatingVM) { - let { modifier, tag, lastUpdated } = this; + let { modifier, tag, watch } = this; consumeTag(tag); - if (!validateTag(tag, lastUpdated)) { + if (isFrameStale(watch)) { vm.env.scheduleUpdateModifier(modifier); - this.lastUpdated = valueForTag(tag); + watchTag(watch, tag); } } } export class UpdateDynamicModifierOpcode implements UpdatingOpcode { - private lastUpdated: Revision; + private watch = createFrame(); constructor( private tag: Tag | null, private instance: ModifierInstance | undefined, private instanceRef: Reference ) { - this.lastUpdated = tag === null ? INITIAL : valueForTag(tag); + if (tag !== null) watchTag(this.watch, tag); } evaluate(vm: UpdatingVM) { - let { tag, lastUpdated, instance, instanceRef } = this; + let { tag, watch, instance, instanceRef } = this; let newInstance = valueForRef(instanceRef); @@ -379,19 +382,17 @@ export class UpdateDynamicModifierOpcode implements UpdatingOpcode { tag = manager.getTag(state); - if (tag !== null) { - this.lastUpdated = valueForTag(tag); - } + watchTag(watch, tag ?? CONSTANT_TAG); this.tag = tag; vm.env.scheduleInstallModifier(newInstance); } this.instance = newInstance; - } else if (tag !== null && !validateTag(tag, lastUpdated)) { + } else if (tag !== null && isFrameStale(watch)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme vm.env.scheduleUpdateModifier(instance!); - this.lastUpdated = valueForTag(tag); + watchTag(watch, tag); } if (tag !== null) { diff --git a/packages/@glimmer/runtime/lib/render.ts b/packages/@glimmer/runtime/lib/render.ts index 80a9d841d25..1e348b678dc 100644 --- a/packages/@glimmer/runtime/lib/render.ts +++ b/packages/@glimmer/runtime/lib/render.ts @@ -44,7 +44,7 @@ class TemplateIteratorImpl implements TemplateIterator { let root = this.#root; if (root === undefined) { - root = this.#root = createFrame(); + root = this.#root = createFrame(true); beginFrame(root); } @@ -67,7 +67,7 @@ class TemplateIteratorImpl implements TemplateIterator { } sync(): RenderResult { - let root = (this.#root = createFrame()); + let root = (this.#root = createFrame(true)); let result: RenderResult | undefined; beginFrame(root); diff --git a/packages/@glimmer/runtime/lib/vm/render-result.ts b/packages/@glimmer/runtime/lib/vm/render-result.ts index 479e330b9fa..01941a548bd 100644 --- a/packages/@glimmer/runtime/lib/vm/render-result.ts +++ b/packages/@glimmer/runtime/lib/vm/render-result.ts @@ -49,7 +49,7 @@ export default class RenderResultImpl implements RenderResult { rerender({ alwaysRevalidate = false } = { alwaysRevalidate: false }) { let { env, updating } = this; - let root = (this.root ??= createFrame()); + let root = (this.root ??= createFrame(true)); let vm = new UpdatingVM(env, { alwaysRevalidate }); let done = false; diff --git a/packages/@glimmer/signals/index.ts b/packages/@glimmer/signals/index.ts index 261cc7fcafb..a79fecb6026 100644 --- a/packages/@glimmer/signals/index.ts +++ b/packages/@glimmer/signals/index.ts @@ -28,10 +28,8 @@ export { beginFrame, beginTrackFrame, beginUntrackFrame, - bump, type Cache, combine, - CONSTANT, CONSTANT_TAG, consumeFrame, consumeTag, @@ -39,14 +37,12 @@ export { createComputed, createFrame, createTag, - currentRevision, dirtyTag, disposeFrame, endFrame, endTrackFrame, endUntrackFrame, getValue, - INITIAL, isConst, isConstComputed, isConstTag, @@ -55,15 +51,12 @@ export { readComputed, releaseTag, resetTracking, - type Revision, TagNode, track, untrack, updateTag, - validateTag, - valueForTag, - VOLATILE, - VOLATILE_TAG, + watchTag, + writeCount, } from './lib/tags'; export type { CombinatorTag, diff --git a/packages/@glimmer/signals/lib/tags.ts b/packages/@glimmer/signals/lib/tags.ts index 2f21c22fefd..847e9c5dfee 100644 --- a/packages/@glimmer/signals/lib/tags.ts +++ b/packages/@glimmer/signals/lib/tags.ts @@ -7,25 +7,15 @@ import type { Link, ReactiveNode } from 'alien-signals/system'; import { debug } from './debug'; import { unwrap } from './utils'; -export type Revision = number; - -export const CONSTANT: Revision = 0; -export const INITIAL: Revision = 1; -export const VOLATILE: Revision = NaN; - -let $REVISION = INITIAL; +let writes = 0; /** - * The revision of the last write to a tag. + * The number of writes to tags since the start of the program. * - * A caller can keep this number to learn later if a write occurred. + * A caller can keep this number to learn later if any write occurred. */ -export function currentRevision(): Revision { - return $REVISION; -} - -export function bump(): void { - $REVISION++; +export function writeCount(): number { + return writes; } /** @@ -42,7 +32,7 @@ const STALE = DIRTY | PENDING; /** * - TAG: a value that code can write. It has subscribers and no dependencies. - * - LIST: a set of tags. It is not in the graph. A check reads the revisions. + * - LIST: a set of tags. It is not in the graph. A subscriber links to its members. * - COMPUTED: a cached function (`createCache`, compute references). * - FRAME: a block of updating opcodes that the VM runs again when it is stale. * - WATCHER: forwards the writes of its dependencies to one TAG (`updateTag`). @@ -55,7 +45,6 @@ const FRAME_KIND = 3; const WATCHER_KIND = 4; const COLLECTOR_KIND = 5; const CONSTANT_KIND = 6; -const VOLATILE_KIND = 7; /** * One class for all kinds keeps the graph walk of alien-signals monomorphic. @@ -69,15 +58,7 @@ export class TagNode implements ReactiveNode, Tag { kind: number; /** - * - TAG: the revision of the last write. - * - LIST: the revision that the last check found. - * - COMPUTED without subscribers: the revision at which the value was valid. - */ - revision: Revision = INITIAL; - - /** - * - LIST, COLLECTOR: the members. - * - COMPUTED without subscribers: the tags that the last run read. + * LIST, COLLECTOR: the members. */ tags: TagNode[] | undefined = undefined; @@ -90,23 +71,22 @@ export class TagNode implements ReactiveNode, Tag { /** * - COLLECTOR: its id. * - other kinds: the id of the last collector that took this tag. - * - LIST: the revision of the last check. */ mark = 0; /** - * `true` when the last run read `VOLATILE_TAG`. Such a node is always stale. + * FRAME, COMPUTED: `true` when the run read a cache that had lost its links. + * + * No write can reach such a node, so it is stale until its next run. */ - volatile = false; + forced = false; /** - * COMPUTED, FRAME: the answer of the last `valueForTag`, and its revision. + * FRAME: `true` when an owner removes the links with `disposeFrame`. * - * Without it, each cache in a chain walks the full chain, - * when nobody subscribes to the chain. + * Such a frame keeps its links when its last subscriber goes away. */ - checked = -1; - max: Revision = CONSTANT; + owned = false; fn: (() => unknown) | undefined = undefined; value: unknown = undefined; @@ -148,8 +128,14 @@ const { link, unlink, propagate } = createReactiveSystem({ let tag = node as TagNode; if (tag.kind === COMPUTED_KIND) { - park(tag); - } else if (tag.kind === FRAME_KIND) { + /** + * The next read runs the function again. + */ + if (tag.deps !== undefined) { + disposeDeps(tag); + tag.flags = MUTABLE | DIRTY; + } + } else if (tag.kind === FRAME_KIND && !tag.owned) { disposeDeps(tag); tag.flags = MUTABLE | DIRTY; } @@ -182,80 +168,11 @@ export function createTag(): TagNode { } export const CONSTANT_TAG = new TagNode(CONSTANT_KIND, MUTABLE); -CONSTANT_TAG.revision = CONSTANT; - -export const VOLATILE_TAG = new TagNode(VOLATILE_KIND, MUTABLE); -VOLATILE_TAG.revision = VOLATILE; -VOLATILE_TAG.volatile = true; export function isConstTag(tag: Tag): boolean { return tag === CONSTANT_TAG; } -/** - * Returns a number for the present state of the tag. - * - * Give the number to `validateTag` later to learn if the tag changed. - */ -export function valueForTag(_tag: Tag): Revision { - let tag = _tag as TagNode; - - switch (tag.kind) { - case TAG_KIND: - case CONSTANT_KIND: - case VOLATILE_KIND: - return tag.revision; - - case LIST_KIND: { - if (tag.mark !== $REVISION) { - tag.mark = $REVISION; - tag.revision = maxRevision(unwrap(tag.tags)); - } - - return tag.revision; - } - - default: { - if (tag.volatile) { - return VOLATILE; - } - - if (tag.checked === $REVISION) { - return tag.max; - } - - let revision = CONSTANT; - - if (tag.tags !== undefined) { - revision = maxRevision(tag.tags); - } else { - for (let dep = tag.deps; dep !== undefined; dep = dep.nextDep) { - revision = Math.max(revision, valueForTag(dep.dep as TagNode)); - } - } - - tag.checked = $REVISION; - tag.max = revision; - - return revision; - } - } -} - -function maxRevision(tags: TagNode[]): Revision { - let revision = CONSTANT; - - for (let i = 0; i < tags.length; i++) { - revision = Math.max(revision, valueForTag(tags[i] as TagNode)); - } - - return revision; -} - -export function validateTag(tag: Tag, snapshot: Revision): boolean { - return snapshot >= valueForTag(tag); -} - export function combine(tags: Tag[]): Tag { switch (tags.length) { case 0: @@ -265,8 +182,6 @@ export function combine(tags: Tag[]): Tag { default: { let list = new TagNode(LIST_KIND, MUTABLE); list.tags = tags as TagNode[]; - list.mark = -1; - list.volatile = (tags as TagNode[]).some((tag) => tag.volatile); return list; } } @@ -283,7 +198,7 @@ export function dirtyTag(_tag: Tag, disableConsumptionAssertion?: boolean): void unwrap(debug.assertTagNotConsumed)(tag); } - tag.revision = ++$REVISION; + writes++; let subs = tag.subs; @@ -308,14 +223,10 @@ function flushWatchers(): void { for (let i = 0; i < FIRED.length; i++) { let tag = (FIRED[i] as TagNode).peer; - if (tag !== undefined) { - tag.revision = $REVISION; - - let subs = tag.subs; + let subs = tag?.subs; - if (subs !== undefined) { - propagate(subs, false); - } + if (subs !== undefined) { + propagate(subs, false); } } @@ -384,14 +295,15 @@ function linkLeaves(sub: TagNode, tag: TagNode): void { case CONSTANT_KIND: break; - case VOLATILE_KIND: - sub.volatile = true; - break; - default: { let tags = tag.tags; - if (tag.volatile) sub.volatile = true; + if ( + tag.forced || + (tag.kind === COMPUTED_KIND && tag.deps === undefined && tag.flags !== MUTABLE) + ) { + sub.forced = true; + } if (tags !== undefined) { for (let i = 0; i < tags.length; i++) { @@ -423,32 +335,17 @@ function linkTag(sub: TagNode, tag: TagNode): void { case COMPUTED_KIND: case FRAME_KIND: - if (tag.tags === undefined && tag.flags === MUTABLE) { - if (tag.volatile) sub.volatile = true; + if (tag.flags === MUTABLE && !tag.forced) { if (tag.deps !== undefined) link(tag, sub, cycle); } else { linkLeaves(sub, tag); } break; - - case VOLATILE_KIND: - /** - * `sub` can never be valid, and no write will tell it so. - */ - sub.volatile = true; - break; } } function collect(collector: TagNode, tag: TagNode): void { - if (tag.volatile) collector.volatile = true; - - /** - * A LIST uses `mark` for its own check, so it can go in two times. - */ - if (tag.kind === LIST_KIND) { - unwrap(collector.tags).push(tag); - } else if (tag.mark !== collector.mark) { + if (tag.mark !== collector.mark) { tag.mark = collector.mark; unwrap(collector.tags).push(tag); } @@ -512,7 +409,6 @@ function collectorToTag(collector: TagNode): TagNode { } collector.kind = LIST_KIND; - collector.mark = -1; return collector; } @@ -586,8 +482,10 @@ export function untrack(callback: () => T): T { * The owner runs the block between `beginFrame` and `endFrame`. * When `isFrameStale` answers `true`, the owner runs the block again. */ -export function createFrame(): TagNode { - return new TagNode(FRAME_KIND, MUTABLE); +export function createFrame(owned = false): TagNode { + let frame = new TagNode(FRAME_KIND, MUTABLE); + frame.owned = owned; + return frame; } export function beginFrame(frame: TagNode, debuggingContext?: string | false): void { @@ -595,8 +493,7 @@ export function beginFrame(frame: TagNode, debuggingContext?: string | false): v cycle++; frame.depsTail = undefined; - frame.checked = -1; - frame.volatile = false; + frame.forced = false; frame.flags = MUTABLE | RECURSED_CHECK; activeSub = frame; @@ -660,7 +557,7 @@ export function abandonFrame(frame: TagNode): void { } export function isFrameStale(frame: TagNode): boolean { - return (frame.flags & STALE) !== 0 || frame.volatile; + return (frame.flags & STALE) !== 0 || frame.forced; } /** @@ -669,7 +566,7 @@ export function isFrameStale(frame: TagNode): boolean { export function consumeFrame(frame: TagNode): void { let sub = activeSub; - if (sub === undefined || (frame.deps === undefined && !frame.volatile)) return; + if (sub === undefined || (frame.deps === undefined && !frame.forced)) return; if (DEBUG) { unwrap(debug.markTagAsConsumed)(frame); @@ -678,7 +575,7 @@ export function consumeFrame(frame: TagNode): void { if (sub.kind === COLLECTOR_KIND) { collect(sub, frame); } else { - if (frame.volatile) sub.volatile = true; + if (frame.forced) sub.forced = true; if (frame.deps !== undefined) link(frame, sub, cycle); } } @@ -691,38 +588,41 @@ export function disposeFrame(frame: TagNode): void { ////////// /** - * Reads a COMPUTED node, and runs its function first if the value is stale. + * Makes `frame` depend on `tag` only, and makes it not stale. * - * The node has links to its dependencies only while it has a subscriber. - * Without a subscriber, it keeps a list of the tags and a revision. - * - * Thus a long-lived tag does not keep each cache that read it one time. + * Use it for a subscriber that has no block to run: + * the owner asks `isFrameStale` later, and then calls this function again. + */ +export function watchTag(frame: TagNode, _tag: Tag): void { + cycle++; + frame.depsTail = undefined; + frame.forced = false; + linkTag(frame, _tag as TagNode); + purgeDeps(frame); + frame.flags = MUTABLE; +} + +////////// + +/** + * Reads a COMPUTED node, and runs its function first if the value is stale. */ export function readComputed(node: TagNode): T { - let sub = activeSub; - let watched = (sub !== undefined && sub.kind !== COLLECTOR_KIND) || node.subs !== undefined; let flags = node.flags; - if (flags === 0) { - evaluate(node, watched); - } else if (node.tags !== undefined) { - if (node.volatile || (node.revision !== $REVISION && !(node.revision >= valueForTag(node)))) { - evaluate(node, watched); - } else { - node.revision = $REVISION; - if (watched) unpark(node); - } - } else if ((flags & STALE) !== 0 || node.volatile) { - evaluate(node, watched); + if (flags === 0 || (flags & STALE) !== 0 || node.forced) { + evaluate(node); } + let sub = activeSub; + /** * A node that reads itself during its run gets no link to itself. */ if ( sub !== undefined && (node.flags & RECURSED_CHECK) === 0 && - (node.deps !== undefined || node.tags !== undefined || node.volatile) + (node.deps !== undefined || node.forced) ) { if (DEBUG) { unwrap(debug.markTagAsConsumed)(node); @@ -731,7 +631,7 @@ export function readComputed(node: TagNode): T { if (sub.kind === COLLECTOR_KIND) { collect(sub, node); } else { - if (node.volatile) sub.volatile = true; + if (node.forced) sub.forced = true; if (node.deps !== undefined) link(node, sub, cycle); } } @@ -739,32 +639,17 @@ export function readComputed(node: TagNode): T { return node.value as T; } -function evaluate(node: TagNode, watched: boolean): void { +function evaluate(node: TagNode): void { let first = node.flags === 0; let done = false; - let collector: TagNode | undefined; SUB_STACK.push(activeSub); - node.volatile = false; - node.checked = -1; - - if (watched) { - node.tags = undefined; - cycle++; - node.depsTail = undefined; - node.flags = MUTABLE | RECURSED_CHECK; - activeSub = node; - } else { - if (node.deps !== undefined) disposeDeps(node); - - node.flags = MUTABLE | RECURSED_CHECK; - - collector = new TagNode(COLLECTOR_KIND, MUTABLE); - collector.tags = []; - collector.mark = ++collectorId; - activeSub = collector; - } + cycle++; + node.forced = false; + node.depsTail = undefined; + node.flags = MUTABLE | RECURSED_CHECK; + activeSub = node; if (DEBUG) { unwrap(debug.beginTrackingTransaction)(node.debugLabel); @@ -777,7 +662,7 @@ function evaluate(node: TagNode, watched: boolean): void { /** * `resetTracking` can empty the stack before this point, after an error. */ - if (activeSub === node || activeSub === collector) { + if (activeSub === node) { if (DEBUG) { unwrap(debug.endTrackingTransaction)(); } @@ -785,17 +670,7 @@ function evaluate(node: TagNode, watched: boolean): void { activeSub = SUB_STACK.pop(); } - if (collector === undefined) { - purgeDeps(node); - } else if (done) { - let tags = collector.tags as TagNode[]; - - node.tags = tags.length === 0 ? undefined : tags; - node.volatile = collector.volatile; - node.revision = $REVISION; - } else { - node.tags = undefined; - } + purgeDeps(node); if (done) { settle(node); @@ -805,59 +680,8 @@ function evaluate(node: TagNode, watched: boolean): void { } } -/** - * Replaces the links of a COMPUTED that lost its last subscriber by a list. - */ -function park(node: TagNode): void { - if (node.deps === undefined) return; - - let tags: TagNode[] = []; - - for (let dep: Link | undefined = node.deps; dep !== undefined; dep = dep.nextDep) { - tags.push(dep.dep as TagNode); - } - - node.tags = tags; - node.checked = -1; - node.revision = (node.flags & STALE) !== 0 ? -1 : $REVISION; - node.flags = MUTABLE; - - disposeDeps(node); -} - -/** - * Gives a valid COMPUTED its links back, because a subscriber reads it now. - */ -function unpark(node: TagNode): void { - let tags = node.tags as TagNode[]; - - node.tags = undefined; - node.depsTail = undefined; - cycle++; - - for (let i = 0; i < tags.length; i++) { - linkSource(node, tags[i] as TagNode); - } -} - -function linkSource(sub: TagNode, tag: TagNode): void { - if (tag.kind === COMPUTED_KIND) { - if (tag.tags !== undefined) { - unpark(tag); - } - - if (tag.flags === MUTABLE) { - if (tag.volatile) sub.volatile = true; - if (tag.deps !== undefined) link(tag, sub, cycle); - return; - } - } - - linkTag(sub, tag); -} - export function isConstComputed(node: TagNode): boolean { - return node.flags !== 0 && node.deps === undefined && node.tags === undefined && !node.volatile; + return node.flags !== 0 && node.deps === undefined && !node.forced; } export function createComputed(fn: () => T, debugLabel?: string | false): TagNode { diff --git a/packages/@glimmer/signals/test/graph-test.ts b/packages/@glimmer/signals/test/graph-test.ts index 238d40f86c0..57f5ad63c46 100644 --- a/packages/@glimmer/signals/test/graph-test.ts +++ b/packages/@glimmer/signals/test/graph-test.ts @@ -14,8 +14,7 @@ import { type TagNode, track, updateTag, - validateTag, - valueForTag, + watchTag, } from '@glimmer/signals'; import { module, test } from './-utils'; @@ -169,7 +168,7 @@ module('@glimmer/signals: graph', () => { }); module('caches', () => { - test('a cache without a subscriber has no links from its tags', (assert) => { + test('a cache keeps its value until a write', (assert) => { let tag = createTag(); let count = 0; let cache = createCache(() => { @@ -179,15 +178,13 @@ module('@glimmer/signals: graph', () => { assert.strictEqual(getValue(cache), 1); assert.strictEqual(getValue(cache), 1); - assert.false(hasSubscribers(tag)); dirtyTag(tag); assert.strictEqual(getValue(cache), 2); - assert.false(hasSubscribers(tag)); }); - test('a cache keeps its value when it gets a subscriber and when it loses it', (assert) => { + test('a cache that loses its last subscriber removes its links', (assert) => { let tag = createTag(); let count = 0; let cache = createCache(() => { @@ -196,8 +193,6 @@ module('@glimmer/signals: graph', () => { }); let frame = createFrame(); - assert.strictEqual(getValue(cache), 1); - run(frame, () => assert.strictEqual(getValue(cache), 1)); assert.true(hasSubscribers(tag)); @@ -205,51 +200,19 @@ module('@glimmer/signals: graph', () => { disposeFrame(frame); assert.false(hasSubscribers(tag)); - assert.strictEqual(getValue(cache), 1); - - dirtyTag(tag); - - assert.strictEqual(getValue(cache), 2); + assert.strictEqual(getValue(cache), 2, 'the cache runs again, because it had no links'); }); - test('a nested cache gets its links back together with the outer cache', (assert) => { + test('a frame is stale after a write below a nested cache', (assert) => { let tag = createTag(); - let innerCount = 0; - let inner = createCache(() => { - consumeTag(tag); - return ++innerCount; - }); + let inner = createCache(() => consumeTag(tag)); let outer = createCache(() => getValue(inner)); let frame = createFrame(); - assert.strictEqual(getValue(outer), 1); - assert.false(hasSubscribers(tag)); - run(frame, () => getValue(outer)); - - assert.true(hasSubscribers(tag)); - assert.strictEqual(innerCount, 1); - dirtyTag(tag); assert.true(isFrameStale(frame)); - assert.strictEqual(getValue(outer), 2); - }); - - test('a cache that lost its subscriber while stale runs again', (assert) => { - let tag = createTag(); - let count = 0; - let cache = createCache(() => { - consumeTag(tag); - return ++count; - }); - let frame = createFrame(); - - run(frame, () => getValue(cache)); - dirtyTag(tag); - disposeFrame(frame); - - assert.strictEqual(getValue(cache), 2); }); test('a tag from track() that holds a cache sees each write', (assert) => { @@ -261,16 +224,17 @@ module('@glimmer/signals: graph', () => { }); let tag = track(() => getValue(cache)); - let snapshot = valueForTag(tag); + let frame = createFrame(); + watchTag(frame, tag); dirtyTag(first); - assert.false(validateTag(tag, snapshot)); + assert.true(isFrameStale(frame)); - snapshot = valueForTag(tag); - assert.true(validateTag(tag, snapshot)); + watchTag(frame, tag); + assert.false(isFrameStale(frame)); dirtyTag(second); - assert.false(validateTag(tag, snapshot)); + assert.true(isFrameStale(frame), 'the stale cache does not hide the second write'); }); }); @@ -281,13 +245,14 @@ module('@glimmer/signals: graph', () => { updateTag(tag, source); - let snapshot = valueForTag(tag); + let frame = createFrame(); + watchTag(frame, tag); dirtyTag(source); - assert.false(validateTag(tag, snapshot)); + assert.true(isFrameStale(frame)); - snapshot = valueForTag(tag); + watchTag(frame, tag); dirtyTag(source); - assert.false(validateTag(tag, snapshot)); + assert.true(isFrameStale(frame)); }); test('the tag changes for each write below a cache in the source', (assert) => { @@ -300,13 +265,14 @@ module('@glimmer/signals: graph', () => { track(() => getValue(cache)) ); - let snapshot = valueForTag(tag); + let frame = createFrame(); + watchTag(frame, tag); dirtyTag(leaf); - assert.false(validateTag(tag, snapshot)); + assert.true(isFrameStale(frame)); - snapshot = valueForTag(tag); + watchTag(frame, tag); dirtyTag(leaf); - assert.false(validateTag(tag, snapshot), 'the stale cache does not hide the second write'); + assert.true(isFrameStale(frame), 'the stale cache does not hide the second write'); }); test('a frame that read the tag is stale after a write to the source', (assert) => { @@ -330,10 +296,11 @@ module('@glimmer/signals: graph', () => { updateTag(first, second); updateTag(second, third); - let snapshot = valueForTag(first); + let frame = createFrame(); + watchTag(frame, first); dirtyTag(third); - assert.false(validateTag(first, snapshot)); + assert.true(isFrameStale(frame)); }); test('a new source replaces the old source', (assert) => { @@ -344,14 +311,15 @@ module('@glimmer/signals: graph', () => { updateTag(tag, oldSource); updateTag(tag, newSource); - let snapshot = valueForTag(tag); + let frame = createFrame(); + watchTag(frame, tag); dirtyTag(oldSource); - assert.true(validateTag(tag, snapshot)); + assert.false(isFrameStale(frame)); assert.false(hasSubscribers(oldSource)); dirtyTag(newSource); - assert.false(validateTag(tag, snapshot)); + assert.true(isFrameStale(frame)); }); test('releaseTag removes the links from the source', (assert) => { diff --git a/packages/@glimmer/signals/test/meta-test.ts b/packages/@glimmer/signals/test/meta-test.ts index 317cf23332d..c179d322f73 100644 --- a/packages/@glimmer/signals/test/meta-test.ts +++ b/packages/@glimmer/signals/test/meta-test.ts @@ -1,7 +1,21 @@ -import { dirtyTagFor, tagFor, validateTag, valueForTag } from '@glimmer/signals'; +import type { Tag, TagNode } from '@glimmer/signals'; +import { createFrame, dirtyTagFor, isFrameStale, tagFor, watchTag } from '@glimmer/signals'; import { module, test } from './-utils'; +/** + * A subscriber for `tag`. `isValid` answers `false` after a write to the tag. + */ +function watch(tag: Tag) { + let frame = createFrame(); + watchTag(frame, tag); + return frame; +} + +function isValid(frame: TagNode) { + return !isFrameStale(frame); +} + module('@glimmer/signals: meta', () => { test('it creates a unique tag for a property on a given object', (assert) => { let obj = {}; @@ -13,9 +27,9 @@ module('@glimmer/signals: meta', () => { let obj = {}; let tag = tagFor(obj, 'foo'); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); dirtyTagFor(obj, 'foo'); - assert.notOk(validateTag(tag, snapshot)); + assert.notOk(isValid(snapshot)); }); }); diff --git a/packages/@glimmer/signals/test/tracked-value-test.ts b/packages/@glimmer/signals/test/tracked-value-test.ts index 43ec821ce0e..209d7bae1b5 100644 --- a/packages/@glimmer/signals/test/tracked-value-test.ts +++ b/packages/@glimmer/signals/test/tracked-value-test.ts @@ -1,7 +1,21 @@ -import { track, trackedValue, validateTag, valueForTag } from '@glimmer/signals'; +import type { Tag, TagNode } from '@glimmer/signals'; +import { createFrame, isFrameStale, track, trackedValue, watchTag } from '@glimmer/signals'; import { module, test } from './-utils'; +/** + * A subscriber for `tag`. `isValid` answers `false` after a write to the tag. + */ +function watch(tag: Tag) { + let frame = createFrame(); + watchTag(frame, tag); + return frame; +} + +function isValid(frame: TagNode) { + return !isFrameStale(frame); +} + module('@glimmer/signals: trackedValue()', () => { test('creates reactive storage', (assert) => { const x = trackedValue('hello'); @@ -47,58 +61,58 @@ module('@glimmer/signals: trackedValue()', () => { const x = trackedValue(0); const tag = track(() => x.value); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); - assert.true(validateTag(tag, snapshot), 'tag is valid before a change'); + assert.true(isValid(snapshot), 'tag is valid before a change'); x.value = 1; - assert.false(validateTag(tag, snapshot), 'tag is invalidated by a change'); + assert.false(isValid(snapshot), 'tag is invalidated by a change'); - snapshot = valueForTag(tag); - assert.true(validateTag(tag, snapshot), 'tag is valid after snapshotting'); + snapshot = watch(tag); + assert.true(isValid(snapshot), 'tag is valid after snapshotting'); }); test('default equals (Object.is) does not dirty on no-op changes', (assert) => { const x = trackedValue(0); const tag = track(() => x.value); - const snapshot = valueForTag(tag); + const snapshot = watch(tag); x.value = 0; - assert.true(validateTag(tag, snapshot), 'tag is still valid after setting an equal value'); + assert.true(isValid(snapshot), 'tag is still valid after setting an equal value'); }); test('options.equals: () => false dirties on every set', (assert) => { const x = trackedValue(0, { equals: () => false }); const tag = track(() => x.value); - const snapshot = valueForTag(tag); + const snapshot = watch(tag); x.value = 0; - assert.false(validateTag(tag, snapshot), 'tag is invalidated by a no-op set'); + assert.false(isValid(snapshot), 'tag is invalidated by a no-op set'); }); test('options.equals: custom comparisons are respected', (assert) => { const x = trackedValue({ id: 1 }, { equals: (a, b) => a.id === b.id }); const tag = track(() => x.value); - const snapshot = valueForTag(tag); + const snapshot = watch(tag); x.value = { id: 1 }; - assert.true(validateTag(tag, snapshot), 'tag is still valid after setting an equal value'); + assert.true(isValid(snapshot), 'tag is still valid after setting an equal value'); x.value = { id: 2 }; - assert.false(validateTag(tag, snapshot), 'tag is invalidated by a different value'); + assert.false(isValid(snapshot), 'tag is invalidated by a different value'); }); test('update() reads without consuming', (assert) => { const x = trackedValue(0); const tag = track(() => x.update((value) => value)); - const snapshot = valueForTag(tag); + const snapshot = watch(tag); x.value = 1; - assert.true(validateTag(tag, snapshot), 'update() did not entangle with the value'); + assert.true(isValid(snapshot), 'update() did not entangle with the value'); }); test('each method is the same function on each read', (assert) => { diff --git a/packages/@glimmer/signals/test/tracking-test.ts b/packages/@glimmer/signals/test/tracking-test.ts index a708a422bba..d25bbf53949 100644 --- a/packages/@glimmer/signals/test/tracking-test.ts +++ b/packages/@glimmer/signals/test/tracking-test.ts @@ -1,26 +1,41 @@ import { DEBUG } from '@glimmer/env'; +import type { Tag, TagNode } from '@glimmer/signals'; import { beginTrackFrame, beginUntrackFrame, consumeTag, createCache, + createFrame, createTag, debug, dirtyTag, endTrackFrame, getValue, isConst, + isFrameStale, isTracking, resetTracking, track, trackedData, untrack, - validateTag, - valueForTag, + watchTag, } from '@glimmer/signals'; import { module, test } from './-utils'; +/** + * A subscriber for `tag`. `isValid` answers `false` after a write to the tag. + */ +function watch(tag: Tag) { + let frame = createFrame(); + watchTag(frame, tag); + return frame; +} + +function isValid(frame: TagNode) { + return !isFrameStale(frame); +} + module('@glimmer/signals: tracking', () => { module('track', () => { test('it combines tags that are consumed within a track frame', (assert) => { @@ -32,13 +47,13 @@ module('@glimmer/signals: tracking', () => { consumeTag(tag2); }); - let snapshot = valueForTag(combined); + let snapshot = watch(combined); dirtyTag(tag1); - assert.notOk(validateTag(combined, snapshot)); + assert.notOk(isValid(snapshot)); - snapshot = valueForTag(combined); + snapshot = watch(combined); dirtyTag(tag2); - assert.notOk(validateTag(combined, snapshot)); + assert.notOk(isValid(snapshot)); }); test('it ignores tags consumed within an untrack frame', (assert) => { @@ -53,13 +68,13 @@ module('@glimmer/signals: tracking', () => { }); }); - let snapshot = valueForTag(combined); + let snapshot = watch(combined); dirtyTag(tag1); - assert.notOk(validateTag(combined, snapshot)); + assert.notOk(isValid(snapshot)); - snapshot = valueForTag(combined); + snapshot = watch(combined); dirtyTag(tag2); - assert.ok(validateTag(combined, snapshot)); + assert.ok(isValid(snapshot)); }); test('it does not automatically consume tags in nested tracking frames', (assert) => { @@ -74,13 +89,13 @@ module('@glimmer/signals: tracking', () => { }); }); - let snapshot = valueForTag(combined); + let snapshot = watch(combined); dirtyTag(tag1); - assert.notOk(validateTag(combined, snapshot)); + assert.notOk(isValid(snapshot)); - snapshot = valueForTag(combined); + snapshot = watch(combined); dirtyTag(tag2); - assert.ok(validateTag(combined, snapshot)); + assert.ok(isValid(snapshot)); }); test('it works for nested tags', (assert) => { @@ -97,13 +112,13 @@ module('@glimmer/signals: tracking', () => { consumeTag(tag3); }); - let snapshot = valueForTag(combined); + let snapshot = watch(combined); dirtyTag(tag1); - assert.notOk(validateTag(combined, snapshot)); + assert.notOk(isValid(snapshot)); - snapshot = valueForTag(combined); + snapshot = watch(combined); dirtyTag(tag2); - assert.notOk(validateTag(combined, snapshot)); + assert.notOk(isValid(snapshot)); }); test('isTracking works within a track and untrack frame', (assert) => { @@ -166,13 +181,13 @@ module('@glimmer/signals: tracking', () => { let combined = endTrackFrame(); - let snapshot = valueForTag(combined); + let snapshot = watch(combined); dirtyTag(tag1); - assert.notOk(validateTag(combined, snapshot)); + assert.notOk(isValid(snapshot)); - snapshot = valueForTag(combined); + snapshot = watch(combined); dirtyTag(tag2); - assert.notOk(validateTag(combined, snapshot)); + assert.notOk(isValid(snapshot)); }); test('it ignores tags consumed within an untrack frame', (assert) => { @@ -189,13 +204,13 @@ module('@glimmer/signals: tracking', () => { let combined = endTrackFrame(); - let snapshot = valueForTag(combined); + let snapshot = watch(combined); dirtyTag(tag1); - assert.notOk(validateTag(combined, snapshot)); + assert.notOk(isValid(snapshot)); - snapshot = valueForTag(combined); + snapshot = watch(combined); dirtyTag(tag2); - assert.ok(validateTag(combined, snapshot)); + assert.ok(isValid(snapshot)); }); test('it does not automatically consume tags in nested tracking frames', (assert) => { @@ -216,13 +231,13 @@ module('@glimmer/signals: tracking', () => { let combined = endTrackFrame(); - let snapshot = valueForTag(combined); + let snapshot = watch(combined); dirtyTag(tag1); - assert.notOk(validateTag(combined, snapshot)); + assert.notOk(isValid(snapshot)); - snapshot = valueForTag(combined); + snapshot = watch(combined); dirtyTag(tag2); - assert.ok(validateTag(combined, snapshot)); + assert.ok(isValid(snapshot)); }); test('it works for nested tags', (assert) => { @@ -245,13 +260,13 @@ module('@glimmer/signals: tracking', () => { let combined = endTrackFrame(); - let snapshot = valueForTag(combined); + let snapshot = watch(combined); dirtyTag(tag1); - assert.notOk(validateTag(combined, snapshot)); + assert.notOk(isValid(snapshot)); - snapshot = valueForTag(combined); + snapshot = watch(combined); dirtyTag(tag2); - assert.notOk(validateTag(combined, snapshot)); + assert.notOk(isValid(snapshot)); }); test('it returns the tag itself if the frame consumed one tag many times', (assert) => { @@ -565,10 +580,10 @@ module('@glimmer/signals: tracking', () => { assert.strictEqual(getter(foo), 456, 'value is set correctly'); }); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); setter(foo, 789); - assert.notOk(validateTag(tag, snapshot)); + assert.notOk(isValid(snapshot)); }); if (DEBUG) { diff --git a/packages/@glimmer/signals/test/validators-test.ts b/packages/@glimmer/signals/test/validators-test.ts index 0be3b5e9c15..5530b383c41 100644 --- a/packages/@glimmer/signals/test/validators-test.ts +++ b/packages/@glimmer/signals/test/validators-test.ts @@ -1,20 +1,33 @@ import { DEBUG } from '@glimmer/env'; import { testOverrideGlobalContext } from '@glimmer/global-context'; +import type { Tag, TagNode } from '@glimmer/signals'; import { - bump, combine, CONSTANT_TAG, + createFrame, createTag, - currentRevision, + writeCount, dirtyTag, + isFrameStale, updateTag, - validateTag, - valueForTag, - VOLATILE_TAG, + watchTag, } from '@glimmer/signals'; import { module, test } from './-utils'; +/** + * A subscriber for `tag`. `isValid` answers `false` after a write to the tag. + */ +function watch(tag: Tag) { + let frame = createFrame(); + watchTag(frame, tag); + return frame; +} + +function isValid(frame: TagNode) { + return !isFrameStale(frame); +} + function unwrap(value: T | null | undefined): T { if (value === null || value === undefined) { throw new Error('unexpected null or undefined value'); @@ -27,15 +40,15 @@ module('@glimmer/signals: validators', () => { module('DirtyableTag', () => { test('it can be dirtied', (assert) => { let tag = createTag(); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); - assert.ok(validateTag(tag, snapshot)); + assert.ok(isValid(snapshot)); dirtyTag(tag); - assert.notOk(validateTag(tag, snapshot)); + assert.notOk(isValid(snapshot)); - snapshot = valueForTag(tag); - assert.ok(validateTag(tag, snapshot)); + snapshot = watch(tag); + assert.ok(isValid(snapshot)); }); if (DEBUG) { @@ -63,15 +76,15 @@ module('@glimmer/signals: validators', () => { module('UpdatableTag', () => { test('it can be dirtied', (assert) => { let tag = createTag(); - let snapshot = valueForTag(tag); + let snapshot = watch(tag); - assert.ok(validateTag(tag, snapshot)); + assert.ok(isValid(snapshot)); dirtyTag(tag); - assert.notOk(validateTag(tag, snapshot)); + assert.notOk(isValid(snapshot)); - snapshot = valueForTag(tag); - assert.ok(validateTag(tag, snapshot)); + snapshot = watch(tag); + assert.ok(isValid(snapshot)); }); test('it can be updated', (assert) => { @@ -80,14 +93,14 @@ module('@glimmer/signals: validators', () => { updateTag(tag, subtag); - let snapshot = valueForTag(tag); - assert.ok(validateTag(tag, snapshot)); + let snapshot = watch(tag); + assert.ok(isValid(snapshot)); dirtyTag(subtag); - assert.notOk(validateTag(tag, snapshot)); + assert.notOk(isValid(snapshot)); - snapshot = valueForTag(tag); - assert.ok(validateTag(tag, snapshot)); + snapshot = watch(tag); + assert.ok(isValid(snapshot)); }); test('it correctly buffers updates when subtag has a less recent value', (assert) => { @@ -98,17 +111,17 @@ module('@glimmer/signals: validators', () => { dirtyTag(tag); // Then, we get a snapshot of the parent - let snapshot = valueForTag(tag); + let snapshot = watch(tag); // Now, we update the parent tag with the subtag, and revalidate it updateTag(tag, subtag); - assert.ok(validateTag(tag, snapshot), 'tag is still valid after being updated'); + assert.ok(isValid(snapshot), 'tag is still valid after being updated'); // Finally, dirty the subtag one final time to bust the buffer cache dirtyTag(subtag); - assert.notOk(validateTag(tag, snapshot), 'tag is invalid after subtag is dirtied again'); + assert.notOk(isValid(snapshot), 'tag is invalid after subtag is dirtied again'); }); test('it correctly buffers updates when subtag has a more recent value', (assert) => { @@ -116,7 +129,7 @@ module('@glimmer/signals: validators', () => { let subtag = createTag(); // First, we get a snapshot of the parent - let snapshot = valueForTag(tag); + let snapshot = watch(tag); // Then we dirty the currently unrelated subtag dirtyTag(subtag); @@ -124,28 +137,28 @@ module('@glimmer/signals: validators', () => { // Now, we update the parent tag with the subtag, and revalidate it updateTag(tag, subtag); - assert.ok(validateTag(tag, snapshot), 'tag is still valid after being updated'); + assert.ok(isValid(snapshot), 'tag is still valid after being updated'); // Finally, dirty the subtag one final time to bust the buffer cache dirtyTag(subtag); - assert.notOk(validateTag(tag, snapshot), 'tag is invalid after subtag is dirtied again'); + assert.notOk(isValid(snapshot), 'tag is invalid after subtag is dirtied again'); }); test('two tags can follow each other', (assert) => { let tag = createTag(); let subtag = createTag(); - let snapshot = valueForTag(tag); - let subtagSnapshot = valueForTag(subtag); + let snapshot = watch(tag); + let subtagSnapshot = watch(subtag); updateTag(tag, subtag); updateTag(subtag, tag); dirtyTag(tag); - assert.notOk(validateTag(tag, snapshot)); - assert.notOk(validateTag(subtag, subtagSnapshot)); + assert.notOk(isValid(snapshot)); + assert.notOk(isValid(subtagSnapshot)); }); }); @@ -156,13 +169,13 @@ module('@glimmer/signals: validators', () => { let combined = combine([tag1, tag2]); - let snapshot = valueForTag(combined); + let snapshot = watch(combined); dirtyTag(tag1); - assert.notOk(validateTag(combined, snapshot)); + assert.notOk(isValid(snapshot)); - snapshot = valueForTag(combined); + snapshot = watch(combined); dirtyTag(tag2); - assert.notOk(validateTag(combined, snapshot)); + assert.notOk(isValid(snapshot)); }); if (DEBUG) { @@ -212,50 +225,14 @@ module('@glimmer/signals: validators', () => { } }); - module('VolatileTag', () => { - test('it is always invalid', (assert) => { - let snapshot = valueForTag(VOLATILE_TAG); - assert.notOk(validateTag(VOLATILE_TAG, snapshot)); - }); - - test('it ensures that any tags which it is combined with are also always invalid', (assert) => { - let tag2 = createTag(); - - let combined = combine([VOLATILE_TAG, tag2]); - - bump(); - - let snapshot = valueForTag(combined); - assert.notOk(validateTag(combined, snapshot)); - }); - - if (DEBUG) { - test('it cannot be dirtied', (assert) => { - assert.throws( - () => dirtyTag(VOLATILE_TAG), - /Error: Attempted to dirty a tag that was not dirtyable/u - ); - }); - - test('it cannot be updated', (assert) => { - let subtag = createTag(); - - assert.throws( - () => updateTag(VOLATILE_TAG, subtag), - /Error: Attempted to update a tag that was not updatable/u - ); - }); - } - }); - - module('currentRevision', () => { + module('writeCount', () => { test('it changes when a tag is dirtied', (assert) => { - let snapshot = currentRevision(); + let snapshot = writeCount(); let tag = createTag(); dirtyTag(tag); - assert.notStrictEqual(currentRevision(), snapshot); + assert.notStrictEqual(writeCount(), snapshot); }); }); }); diff --git a/smoke-tests/scenarios/stable-decorator-files.ts b/smoke-tests/scenarios/stable-decorator-files.ts index 5b7839afe4d..8b9bd3ba415 100644 --- a/smoke-tests/scenarios/stable-decorator-files.ts +++ b/smoke-tests/scenarios/stable-decorator-files.ts @@ -56,7 +56,7 @@ export const stableDecoratorFiles = { import { module, test } from 'qunit'; import { tracked } from '@glimmer/tracking'; import { tagForObject } from '@ember/-internals/metal'; - import { valueForTag, validateTag } from '@glimmer/signals'; + import { createFrame, isFrameStale, watchTag } from '@glimmer/signals'; // {{#each-in}} and ObjectProxy depend on the object's own tag, which // every form of @tracked must dirty when it is set. @@ -67,9 +67,10 @@ export const stableDecoratorFiles = { } let obj = new Example(); let tag = tagForObject(obj); - let snapshot = valueForTag(tag); + let frame = createFrame(); + watchTag(frame, tag); obj.count = 1; - assert.false(validateTag(tag, snapshot)); + assert.true(isFrameStale(frame)); }); test('setting a tracked accessor dirties the object tag', function(assert) { @@ -78,9 +79,10 @@ export const stableDecoratorFiles = { } let obj = new Example(); let tag = tagForObject(obj); - let snapshot = valueForTag(tag); + let frame = createFrame(); + watchTag(frame, tag); obj.count = 1; - assert.false(validateTag(tag, snapshot)); + assert.true(isFrameStale(frame)); }); }); `, From ce5807c641c48938a978b51c96d66dca0ec94ab9 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:43:03 -0400 Subject: [PATCH 3/5] Make @glimmer/signals a private package of the build The package is no longer an entrypoint, so it is not in ember-addon.renamed-modules and no app or addon can import it. - The benchmark app builds its cell on tracked() from @glimmer/tracking. The control build of the perf job uses main, which has no @glimmer/signals. - The smoke test for a v1 addon imports @glimmer/destroyable. - The smoke test for the object tag reads it through a cache. Co-Authored-By: Claude Opus 5.5 (1M context) --- lib/index.cjs | 1 - package.json | 1 - packages/@ember/-internals/metal/index.ts | 1 + rollup.config.mjs | 19 ++++++++++++ .../benchmark-app/app/services/cell.js | 30 +++++-------------- smoke-tests/scenarios/basic-test.ts | 10 +++---- .../scenarios/stable-decorator-files.ts | 27 ++++++++++------- tests/node-vitest/tree-shakability.test.js | 2 -- 8 files changed, 49 insertions(+), 42 deletions(-) diff --git a/lib/index.cjs b/lib/index.cjs index 2ccc8caedc0..9cfadd5283e 100644 --- a/lib/index.cjs +++ b/lib/index.cjs @@ -147,7 +147,6 @@ const shim = addonV1Shim(path.join(__dirname, '..'), { './dist/dev/packages/@glimmer/tracking/index.js', './dist/dev/packages/@glimmer/tracking/primitives/cache/index.js', './dist/dev/packages/@glimmer/util/index.js', - './dist/dev/packages/@glimmer/signals/index.js', './dist/dev/packages/@glimmer/vm/index.js', './dist/dev/packages/@glimmer/wire-format/index.js', './dist/dev/packages/@simple-dom/document/index.js', diff --git a/package.json b/package.json index dcb1d159257..5e67c448910 100644 --- a/package.json +++ b/package.json @@ -366,7 +366,6 @@ "@glimmer/program/index.js": "ember-source/@glimmer/program/index.js", "@glimmer/reference/index.js": "ember-source/@glimmer/reference/index.js", "@glimmer/runtime/index.js": "ember-source/@glimmer/runtime/index.js", - "@glimmer/signals/index.js": "ember-source/@glimmer/signals/index.js", "@glimmer/tracking/index.js": "ember-source/@glimmer/tracking/index.js", "@glimmer/tracking/primitives/cache/index.js": "ember-source/@glimmer/tracking/primitives/cache/index.js", "@glimmer/util/index.js": "ember-source/@glimmer/util/index.js", diff --git a/packages/@ember/-internals/metal/index.ts b/packages/@ember/-internals/metal/index.ts index f0c9be2187b..0d3d5211039 100644 --- a/packages/@ember/-internals/metal/index.ts +++ b/packages/@ember/-internals/metal/index.ts @@ -71,6 +71,7 @@ export { } from './lib/observer'; export { default as inject, DEBUG_INJECTION_FUNCTIONS } from './lib/injected_property'; export { tagForProperty, tagForObject, markObjectAsDirty } from './lib/tags'; +export { consumeTag } from '@glimmer/signals/lib/tags'; export { tracked, TrackedDescriptor } from './lib/tracked'; export { cached } from './lib/cached'; export { createCache, getValue, isConst } from './lib/cache'; diff --git a/rollup.config.mjs b/rollup.config.mjs index 3c28099f0d6..4dc9efff461 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -277,6 +277,22 @@ function rolledUpPackages() { // ember-source. That is, other packages could actually depend on the copies of // these that we publish. export function exposedDependencies() { + let deps = exposedAndPrivateDependencies(); + + for (let name of privatePackages()) { + delete deps[name]; + } + + return deps; +} + +// these packages are dependencies of the exposed ones, and no code outside of +// ember-source can import them. +function privatePackages() { + return ['@glimmer/signals']; +} + +function exposedAndPrivateDependencies() { return { 'backburner.js': require.resolve('backburner.js/dist/es6/backburner.js'), rsvp: require.resolve('rsvp/lib/rsvp.js'), @@ -310,6 +326,9 @@ export function hiddenDependencies() { 'module' ).path, '@handlebars/parser': resolve(packageCache.appRoot, 'packages/@handlebars/parser/lib/index.js'), + ...Object.fromEntries( + privatePackages().map((name) => [name, exposedAndPrivateDependencies()[name]]) + ), 'alien-signals/system': resolve( findFromProject('@glimmer/signals', 'alien-signals').root, 'esm/system.mjs' diff --git a/smoke-tests/benchmark-app/app/services/cell.js b/smoke-tests/benchmark-app/app/services/cell.js index 9b70e3c90e5..c46f2e26c5a 100644 --- a/smoke-tests/benchmark-app/app/services/cell.js +++ b/smoke-tests/benchmark-app/app/services/cell.js @@ -4,7 +4,7 @@ * * Needed for equality-based dirty checking, rather than identity-based. */ -import { consumeTag, createTag, dirtyTag } from '@glimmer/signals'; +import { tracked } from '@glimmer/tracking'; export function cell(initial, options = { equals: Object.is }) { return new Cell(initial, options); @@ -12,40 +12,24 @@ export function cell(initial, options = { equals: Object.is }) { class Cell { #value; - #equals; - #tag; constructor(value, options) { - this.#value = value; - this.#equals = options.equals; - this.#tag = createTag(); + this.#value = tracked(value, options); } get current() { - consumeTag(this.#tag); - - return this.#value; + return this.#value.value; } read() { - consumeTag(this.#tag); - - return this.#value; + return this.#value.value; } set(value) { - if (this.#equals?.(this.#value, value)) { - return false; - } - - this.#value = value; - - dirtyTag(this.#tag); - - return true; + return this.#value.set(value); } update(updater) { - this.set(updater(this.#value)); + this.#value.update(updater); } -} \ No newline at end of file +} diff --git a/smoke-tests/scenarios/basic-test.ts b/smoke-tests/scenarios/basic-test.ts index 11937fffc77..49e8e0fa617 100644 --- a/smoke-tests/scenarios/basic-test.ts +++ b/smoke-tests/scenarios/basic-test.ts @@ -187,13 +187,13 @@ function basicTest(scenarios: Scenarios, appName: string) { unit: { 'v1-addon-without-eai-test.js': ` import { module, test } from 'qunit'; - import { accessGlimmerValidator } from 'v1-addon-without-eai'; + import { accessGlimmerPackage } from 'v1-addon-without-eai'; module('Acceptance | v1-addon-without-eai', function (hooks) { // a v1 addon without ember-auto-import needs to maintain access // to all the backward-compatible ember-provided packages, regardless // of our build environment and optional-features. test('can access things from ember', function(assert) { - assert.strictEqual(accessGlimmerValidator(), 'it works'); + assert.strictEqual(accessGlimmerPackage(), 'it works'); }) }); `, @@ -912,9 +912,9 @@ function basicTest(scenarios: Scenarios, appName: string) { 'index.js': 'module.exports = { name: "v1-addon-without-eai" }', addon: { 'index.js': ` - import { consumeTag } from '@glimmer/signals'; - export function accessGlimmerValidator() { - if (typeof consumeTag === 'function') { + import { destroy } from '@glimmer/destroyable'; + export function accessGlimmerPackage() { + if (typeof destroy === 'function') { return "it works" } } diff --git a/smoke-tests/scenarios/stable-decorator-files.ts b/smoke-tests/scenarios/stable-decorator-files.ts index 8b9bd3ba415..bc3d2197ff5 100644 --- a/smoke-tests/scenarios/stable-decorator-files.ts +++ b/smoke-tests/scenarios/stable-decorator-files.ts @@ -55,8 +55,17 @@ export const stableDecoratorFiles = { 'tracked-object-tag-test.js': ` import { module, test } from 'qunit'; import { tracked } from '@glimmer/tracking'; - import { tagForObject } from '@ember/-internals/metal'; - import { createFrame, isFrameStale, watchTag } from '@glimmer/signals'; + import { createCache, getValue } from '@glimmer/tracking/primitives/cache'; + import { consumeTag, tagForObject } from '@ember/-internals/metal'; + + function countRuns(obj) { + let runs = 0; + + return createCache(() => { + consumeTag(tagForObject(obj)); + return ++runs; + }); + } // {{#each-in}} and ObjectProxy depend on the object's own tag, which // every form of @tracked must dirty when it is set. @@ -66,11 +75,10 @@ export const stableDecoratorFiles = { @tracked count = 0; } let obj = new Example(); - let tag = tagForObject(obj); - let frame = createFrame(); - watchTag(frame, tag); + let cache = countRuns(obj); + assert.strictEqual(getValue(cache), 1); obj.count = 1; - assert.true(isFrameStale(frame)); + assert.strictEqual(getValue(cache), 2); }); test('setting a tracked accessor dirties the object tag', function(assert) { @@ -78,11 +86,10 @@ export const stableDecoratorFiles = { @tracked accessor count = 0; } let obj = new Example(); - let tag = tagForObject(obj); - let frame = createFrame(); - watchTag(frame, tag); + let cache = countRuns(obj); + assert.strictEqual(getValue(cache), 1); obj.count = 1; - assert.true(isFrameStale(frame)); + assert.strictEqual(getValue(cache), 2); }); }); `, diff --git a/tests/node-vitest/tree-shakability.test.js b/tests/node-vitest/tree-shakability.test.js index 520af9054d3..0d8d82462aa 100644 --- a/tests/node-vitest/tree-shakability.test.js +++ b/tests/node-vitest/tree-shakability.test.js @@ -35,7 +35,6 @@ it('[dev] source modules with side effects', () => { "@glimmer/runtime/lib/compiled/opcodes/expressions.js", "@glimmer/runtime/lib/compiled/opcodes/lists.js", "@glimmer/runtime/lib/compiled/opcodes/vm.js", - "@glimmer/signals/index.js", "@handlebars/parser/lib/parse.js", "@handlebars/parser/lib/printer.js", "@handlebars/parser/lib/whitespace-control.js", @@ -61,7 +60,6 @@ it('[prod] source modules with side effects', () => { "@glimmer/runtime/lib/compiled/opcodes/expressions.js", "@glimmer/runtime/lib/compiled/opcodes/lists.js", "@glimmer/runtime/lib/compiled/opcodes/vm.js", - "@glimmer/signals/index.js", "@handlebars/parser/lib/parse.js", "@handlebars/parser/lib/printer.js", "@handlebars/parser/lib/whitespace-control.js", From 8190c5790decf55b6a680d9cad66636e06c9d4eb Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:58:03 -0400 Subject: [PATCH 4/5] Remove tag lists, collectors, and watcher nodes - A classic computed property is one node: its property tag. The tag links to the tags that the getter or the dependent keys read, and a stale bit on the tag says if the cached value is valid. Meta keeps only a flag that a value is cached. - trackInto(tag, fn) runs fn with the tag as the subscriber. It replaces track() followed by updateTag(). - A Tag is one node or an array of tags. combine() is gone. - track() returns a frame. beginTrackFrame and endTrackFrame are gone. - A tag that follows other tags never stops propagate. A new subscriber clears its flags, and the stale bit keeps the fact. - The tag of an argument is its reference. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../glimmer/lib/component-managers/root.ts | 4 +- packages/@ember/-internals/meta/lib/meta.ts | 53 +-- packages/@ember/-internals/metal/lib/alias.ts | 6 +- .../@ember/-internals/metal/lib/chain-tags.ts | 8 +- .../@ember/-internals/metal/lib/computed.ts | 29 +- .../-internals/runtime/lib/mixins/-proxy.ts | 4 +- packages/@ember/array/proxy.ts | 12 +- packages/@ember/object/compat.ts | 12 +- packages/@glimmer/interfaces/lib/tags.d.ts | 7 +- .../@glimmer/manager/lib/util/args-proxy.ts | 41 +- packages/@glimmer/runtime/lib/environment.ts | 10 +- packages/@glimmer/signals/index.ts | 6 +- packages/@glimmer/signals/lib/debug.ts | 26 +- packages/@glimmer/signals/lib/tags.ts | 362 +++++++----------- packages/@glimmer/signals/test/graph-test.ts | 45 +++ .../@glimmer/signals/test/tracking-test.ts | 22 +- .../@glimmer/signals/test/validators-test.ts | 7 +- 17 files changed, 302 insertions(+), 352 deletions(-) diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/root.ts b/packages/@ember/-internals/glimmer/lib/component-managers/root.ts index 4e1a0d14636..1fafc6fa95d 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/root.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/root.ts @@ -11,7 +11,7 @@ import type { } from '@glimmer/interfaces'; import type { Nullable } from '@ember/-internals/utility-types'; import { capabilityFlagsFrom } from '@glimmer/manager/lib/util/capabilities'; -import { CONSTANT_TAG, consumeTag } from '@glimmer/signals/lib/tags'; +import { consumeTag, createFrame } from '@glimmer/signals/lib/tags'; import type Component from '../component'; import type { DynamicScope } from '../renderer'; import ComponentStateBucket from '../utils/curly-component-state-bucket'; @@ -65,7 +65,7 @@ class RootComponentManager extends CurlyComponentManager { let bucket = new ComponentStateBucket( component, null, - CONSTANT_TAG, + createFrame(), finalizer, hasWrappedElement, isInteractive diff --git a/packages/@ember/-internals/meta/lib/meta.ts b/packages/@ember/-internals/meta/lib/meta.ts index 6a0c422906a..e8656feb3ad 100644 --- a/packages/@ember/-internals/meta/lib/meta.ts +++ b/packages/@ember/-internals/meta/lib/meta.ts @@ -3,9 +3,7 @@ import toString from '@ember/-internals/utils/lib/to-string'; import { assert } from '@ember/debug'; import { isDestroyed } from '@glimmer/destroyable'; import { DEBUG } from '@glimmer/env'; -import { createFrame, disposeFrame, isFrameStale, watchTag } from '@glimmer/signals/lib/tags'; -import type { TagNode } from '@glimmer/signals/lib/tags'; -import type { Tag, UpdatableTag } from '@glimmer/interfaces'; +import type { UpdatableTag } from '@glimmer/interfaces'; type ObjMap = { [key: string]: T }; @@ -100,7 +98,7 @@ export class Meta { /** @internal */ _values: ObjMap | undefined; /** @internal */ - _watches: ObjMap | undefined; + _cached: ObjMap | undefined; /** @internal */ source: object; /** @internal */ @@ -128,7 +126,7 @@ export class Meta { this._mixins = undefined; this._lazyChains = undefined; this._values = undefined; - this._watches = undefined; + this._cached = undefined; // initial value for all flags right now is false // see FLAGS const for detailed list of flags used @@ -171,7 +169,7 @@ export class Meta { } /** @internal */ - _getOrCreateOwnMap(key: '_values' | '_watches' | '_lazyChains') { + _getOrCreateOwnMap(key: '_values' | '_cached' | '_lazyChains') { return this[key] || (this[key] = Object.create(null)); } @@ -223,50 +221,21 @@ export class Meta { } /** - * `true` when `setCachedFor` ran for the key, also when the value is stale. + * `true` when `valueFor` holds a computed value for the key. * - * @internal - */ - hasCacheFor(key: string): boolean { - let watches = this._watches; - - return watches !== undefined && watches[key] !== undefined; - } - - /** - * `true` when the value from `valueFor` is valid: - * no write reached the tag since `setCachedFor`. + * The value is valid while the tag of the property is fresh. * * @internal */ - isCachedFor(key: string): boolean { - let watches = this._watches; - let watch = watches !== undefined ? watches[key] : undefined; - - return watch !== undefined && !isFrameStale(watch); - } - - /** - * Marks the value for the key as valid until the next write to `tag`. - * - * @internal - */ - setCachedFor(key: string, tag: Tag) { - let watches = this._getOrCreateOwnMap('_watches'); - let watch = (watches[key] ??= createFrame()); + hasCacheFor(key: string): boolean { + let cached = this._cached; - watchTag(watch, tag); + return cached !== undefined && cached[key] === true; } /** @internal */ - clearCachedFor(key: string) { - let watches = this._watches; - let watch = watches !== undefined ? watches[key] : undefined; - - if (watch !== undefined) { - disposeFrame(watch); - watches![key] = undefined; - } + setCachedFor(key: string, isCached: boolean) { + this._getOrCreateOwnMap('_cached')[key] = isCached; } /** @internal */ diff --git a/packages/@ember/-internals/metal/lib/alias.ts b/packages/@ember/-internals/metal/lib/alias.ts index 436f883ef2c..882cd0cd093 100644 --- a/packages/@ember/-internals/metal/lib/alias.ts +++ b/packages/@ember/-internals/metal/lib/alias.ts @@ -3,7 +3,7 @@ import { meta as metaFor } from '@ember/-internals/meta/lib/meta'; import inspect from '@ember/debug/lib/inspect'; import { assert } from '@ember/debug'; import type { UpdatableTag } from '@glimmer/interfaces'; -import { consumeTag, untrack, updateTag } from '@glimmer/signals/lib/tags'; +import { consumeTag, isTagFresh, untrack, updateTag } from '@glimmer/signals/lib/tags'; import { tagFor, tagMetaFor } from '@glimmer/signals/lib/meta'; import { CHAIN_PASS_THROUGH, finishLazyChains, getChainTagsForKey } from './chain-tags'; import type { ExtendedMethodDecorator } from './decorator'; @@ -84,9 +84,9 @@ class AliasedProperty extends ComputedDescriptor { ret = get(obj, this.altKey); }); - if (!meta.isCachedFor(keyName)) { + if (!meta.hasCacheFor(keyName) || !isTagFresh(propertyTag)) { updateTag(propertyTag, getChainTagsForKey(obj, this.altKey, tagMeta, meta)); - meta.setCachedFor(keyName, propertyTag); + meta.setCachedFor(keyName, true); finishLazyChains(meta, keyName, ret); } diff --git a/packages/@ember/-internals/metal/lib/chain-tags.ts b/packages/@ember/-internals/metal/lib/chain-tags.ts index 8867c923f7d..421cdc98e9e 100644 --- a/packages/@ember/-internals/metal/lib/chain-tags.ts +++ b/packages/@ember/-internals/metal/lib/chain-tags.ts @@ -4,7 +4,7 @@ import { isObject } from '@ember/-internals/utils/lib/spec'; import { assert } from '@ember/debug'; import type { Tag } from '@glimmer/interfaces'; import type { TagMeta } from '@glimmer/signals/lib/meta'; -import { combine, createTag, updateTag } from '@glimmer/signals/lib/tags'; +import { createTag, isTagFresh, updateTag } from '@glimmer/signals/lib/tags'; import { tagMetaFor } from '@glimmer/signals/lib/meta'; import { objectAt } from './object-at'; import { tagForProperty } from './tags'; @@ -39,7 +39,7 @@ export function getChainTagsForKeys( getChainTags(tags, obj, key, tagMeta, meta); } - return combine(tags); + return tags; } export function getChainTagsForKey( @@ -48,7 +48,7 @@ export function getChainTagsForKey( tagMeta: TagMeta, meta: Meta | null ): Tag { - return combine(getChainTags([], obj, key, tagMeta, meta)); + return getChainTags([], obj, key, tagMeta, meta); } function getChainTags( @@ -171,7 +171,7 @@ function getChainTags( // we create a lazy chain lookup, and the next time the CP is calculated, // it will update that lazy chain. let instanceMeta = currentMeta!.source === current ? currentMeta! : metaFor(current); - if (instanceMeta.isCachedFor(segment)) { + if (instanceMeta.hasCacheFor(segment) && isTagFresh(propertyTag)) { current = instanceMeta.valueFor(segment); } else { // use metaFor here to ensure we have the meta for the instance diff --git a/packages/@ember/-internals/metal/lib/computed.ts b/packages/@ember/-internals/metal/lib/computed.ts index a24aef176f8..30af4e7a167 100644 --- a/packages/@ember/-internals/metal/lib/computed.ts +++ b/packages/@ember/-internals/metal/lib/computed.ts @@ -5,7 +5,14 @@ import inspect from '@ember/debug/lib/inspect'; import { assert } from '@ember/debug'; import { isDestroyed } from '@glimmer/destroyable'; import type { UpdatableTag } from '@glimmer/interfaces'; -import { consumeTag, track, untrack, updateTag } from '@glimmer/signals/lib/tags'; +import { + consumeTag, + freshenTag, + isTagFresh, + trackInto, + untrack, + updateTag, +} from '@glimmer/signals/lib/tags'; import { tagFor, tagMetaFor } from '@glimmer/signals/lib/meta'; import { finishLazyChains, getChainTagsForKeys } from './chain-tags'; import type { @@ -395,7 +402,7 @@ export class ComputedProperty extends ComputedDescriptor { let ret; - if (meta.isCachedFor(keyName)) { + if (meta.hasCacheFor(keyName) && isTagFresh(propertyTag)) { ret = meta.valueFor(keyName); } else { // For backwards compatibility, we only throw if the CP has any dependencies. CPs without dependencies @@ -414,10 +421,12 @@ export class ComputedProperty extends ComputedDescriptor { if (_dependentKeys !== undefined) { updateTag(propertyTag, getChainTagsForKeys(obj, _dependentKeys, tagMeta, meta)); + } else { + freshenTag(propertyTag); } meta.setValueFor(keyName, ret); - meta.setCachedFor(keyName, propertyTag); + meta.setCachedFor(keyName, true); finishLazyChains(meta, keyName, ret); } @@ -488,9 +497,11 @@ export class ComputedProperty extends ComputedDescriptor { if (_dependentKeys !== undefined) { updateTag(propertyTag, getChainTagsForKeys(obj, _dependentKeys, tagMeta, meta)); + } else { + freshenTag(propertyTag); } - meta.setCachedFor(keyName, propertyTag); + meta.setCachedFor(keyName, true); } finally { endPropertyChanges(); } @@ -532,7 +543,7 @@ export class ComputedProperty extends ComputedDescriptor { /* called before property is overridden */ teardown(obj: object, keyName: string, meta: Meta): void { if (meta.hasCacheFor(keyName)) { - meta.clearCachedFor(keyName); + meta.setCachedFor(keyName, false); meta.setValueFor(keyName, undefined); } @@ -549,7 +560,7 @@ class AutoComputedProperty extends ComputedProperty { let ret; - if (meta.isCachedFor(keyName)) { + if (meta.hasCacheFor(keyName) && isTagFresh(propertyTag)) { ret = meta.valueFor(keyName); } else { assert( @@ -560,14 +571,12 @@ class AutoComputedProperty extends ComputedProperty { let { _getter } = this; // Create a tracker that absorbs any trackable actions inside the CP - let tag = track(() => { + trackInto(propertyTag, () => { ret = _getter!.call(obj, keyName); }); - updateTag(propertyTag, tag); - meta.setValueFor(keyName, ret); - meta.setCachedFor(keyName, propertyTag); + meta.setCachedFor(keyName, true); finishLazyChains(meta, keyName, ret); } diff --git a/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts b/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts index b0d339598be..00b9c393440 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts @@ -17,7 +17,7 @@ import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; import { setCustomTagFor } from '@glimmer/manager/lib/util/args-proxy'; import type { UpdatableTag, Tag } from '@glimmer/interfaces'; -import { combine, updateTag } from '@glimmer/signals/lib/tags'; +import { updateTag } from '@glimmer/signals/lib/tags'; import { tagFor, tagMetaFor } from '@glimmer/signals/lib/meta'; export function contentFor(proxy: ProxyMixin): T | null { @@ -57,7 +57,7 @@ function customTagForProxy(proxy: object, key: string, addMandatorySetter?: bool tags.push(tagForProperty(content, key, addMandatorySetter)); } - return combine(tags); + return tags; } } diff --git a/packages/@ember/array/proxy.ts b/packages/@ember/array/proxy.ts index 307407224e6..eac929c0a7f 100644 --- a/packages/@ember/array/proxy.ts +++ b/packages/@ember/array/proxy.ts @@ -20,13 +20,7 @@ import MutableArray from '@ember/array/mutable'; import { assert } from '@ember/debug'; import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; import { setCustomTagFor } from '@glimmer/manager/lib/util/args-proxy'; -import { - combine, - consumeTag, - createFrame, - isFrameStale, - watchTag, -} from '@glimmer/signals/lib/tags'; +import { consumeTag, createFrame, isFrameStale, watchTag } from '@glimmer/signals/lib/tags'; import type { TagNode } from '@glimmer/signals/lib/tags'; import { tagFor } from '@glimmer/signals/lib/meta'; import type { Tag } from '@glimmer/interfaces'; @@ -403,8 +397,8 @@ class ArrayProxy extends EmberObject implements PropertyDidChange { watchTag(watch, arrangedContentTag); if (isObject(arrangedContent)) { - this._lengthTag = combine([arrangedContentTag, tagForProperty(arrangedContent, 'length')]); - this._arrTag = combine([arrangedContentTag, tagForProperty(arrangedContent, '[]')]); + this._lengthTag = [arrangedContentTag, tagForProperty(arrangedContent, 'length')]; + this._arrTag = [arrangedContentTag, tagForProperty(arrangedContent, '[]')]; } else { this._lengthTag = this._arrTag = arrangedContentTag; } diff --git a/packages/@ember/object/compat.ts b/packages/@ember/object/compat.ts index f0934ccfd25..0f8e239f15d 100644 --- a/packages/@ember/object/compat.ts +++ b/packages/@ember/object/compat.ts @@ -15,7 +15,7 @@ import { } from '@ember/-internals/metal/lib/decorator-util'; import { assert } from '@ember/debug'; import type { UpdatableTag } from '@glimmer/interfaces'; -import { consumeTag, track, updateTag } from '@glimmer/signals/lib/tags'; +import { consumeTag, trackInto } from '@glimmer/signals/lib/tags'; import { tagFor } from '@glimmer/signals/lib/meta'; let wrapGetterSetter = function (target: object, key: string, desc: PropertyDescriptor) { @@ -31,12 +31,11 @@ let wrapGetterSetter = function (target: object, key: string, desc: PropertyDesc let propertyTag = tagFor(this, key) as UpdatableTag; let ret; - let tag = track(() => { + trackInto(propertyTag, () => { ret = originalGet!.call(this); }); - updateTag(propertyTag, tag); - consumeTag(tag); + consumeTag(propertyTag); return ret; }; @@ -129,12 +128,11 @@ export function dependentKeyCompat( let propertyTag = tagFor(this, dec.context.name as string) as UpdatableTag; let ret; - let tag = track(() => { + trackInto(propertyTag, () => { ret = dec.value.call(this); }); - updateTag(propertyTag, tag); - consumeTag(tag); + consumeTag(propertyTag); return ret; }; diff --git a/packages/@glimmer/interfaces/lib/tags.d.ts b/packages/@glimmer/interfaces/lib/tags.d.ts index 7e08127480d..b9e533fb7f7 100644 --- a/packages/@glimmer/interfaces/lib/tags.d.ts +++ b/packages/@glimmer/interfaces/lib/tags.d.ts @@ -3,10 +3,15 @@ * * `@glimmer/signals` owns the fields. Other packages use its functions. */ -export interface Tag { +export interface TagNode { readonly kind: number; } +/** + * One node, or a list of tags. A subscriber to a list subscribes to each member. + */ +export type Tag = TagNode | readonly Tag[]; + export type UpdatableTag = Tag; export type DirtyableTag = Tag; export type ConstantTag = Tag; diff --git a/packages/@glimmer/manager/lib/util/args-proxy.ts b/packages/@glimmer/manager/lib/util/args-proxy.ts index ed0b9923790..35359c42731 100644 --- a/packages/@glimmer/manager/lib/util/args-proxy.ts +++ b/packages/@glimmer/manager/lib/util/args-proxy.ts @@ -1,3 +1,4 @@ +import type { Reference } from '@glimmer/reference/lib/reference'; import { DEBUG } from '@glimmer/env'; import type { Arguments, @@ -7,7 +8,7 @@ import type { } from '@glimmer/interfaces'; import type { Tag } from '@glimmer/interfaces'; import { valueForRef } from '@glimmer/reference/lib/reference'; -import { track } from '@glimmer/signals/lib/tags'; +import { CONSTANT_TAG, untrack } from '@glimmer/signals/lib/tags'; const CUSTOM_TAG_FOR = new WeakMap Tag>(); @@ -29,30 +30,32 @@ function convertToInt(prop: number | string | symbol): number | null { return num % 1 === 0 ? num : null; } +/** + * A reference is a node of the reactive graph, so it is the tag of its argument. + * + * The read makes sure that the reference has its dependencies. + */ +function tagForRef(ref: Reference): Tag { + untrack(() => valueForRef(ref)); + + return ref as unknown as Tag; +} + function tagForNamedArg(namedArgs: CapturedNamedArguments, key: string): Tag { - return track(() => { - if (key in namedArgs) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme - valueForRef(namedArgs[key]!); - } - }); + let ref = namedArgs[key]; + + return ref === undefined ? CONSTANT_TAG : tagForRef(ref); } function tagForPositionalArg(positionalArgs: CapturedPositionalArguments, key: string): Tag { - return track(() => { - if (key === '[]') { - // consume all of the tags in the positional array - positionalArgs.forEach(valueForRef); - } + if (key === '[]') { + return positionalArgs.map(tagForRef); + } - const parsed = convertToInt(key); + const parsed = convertToInt(key); + const ref = parsed === null ? undefined : positionalArgs[parsed]; - if (parsed !== null && parsed >= 0 && parsed < positionalArgs.length) { - // consume the tag of the referenced index - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme - valueForRef(positionalArgs[parsed]!); - } - }); + return ref === undefined ? CONSTANT_TAG : tagForRef(ref); } // eslint-disable-next-line @typescript-eslint/no-empty-object-type -- @fixme diff --git a/packages/@glimmer/runtime/lib/environment.ts b/packages/@glimmer/runtime/lib/environment.ts index b6d0d1b3b85..dc68aeec745 100644 --- a/packages/@glimmer/runtime/lib/environment.ts +++ b/packages/@glimmer/runtime/lib/environment.ts @@ -16,7 +16,7 @@ import type { import { expect } from '@glimmer/debug-util/lib/platform-utils'; import assert from '@glimmer/debug-util/lib/assert'; import { ProgramImpl } from '@glimmer/program/lib/program'; -import { track, updateTag } from '@glimmer/signals/lib/tags'; +import { trackInto } from '@glimmer/signals/lib/tags'; import DebugRenderTree from './debug-render-tree'; import { DOMChangesImpl, DOMTreeConstruction } from './dom/helper'; @@ -63,14 +63,14 @@ class TransactionImpl implements Transaction { let modifierTag = manager.getTag(state); if (modifierTag !== null) { - let tag = track( + trackInto( + modifierTag, () => manager.install(state), DEBUG && `- While rendering:\n (instance of a \`${ definition.resolvedName || manager.getDebugName(definition.state) }\` modifier)` ); - updateTag(modifierTag, tag); } else { manager.install(state); } @@ -80,14 +80,14 @@ class TransactionImpl implements Transaction { let modifierTag = manager.getTag(state); if (modifierTag !== null) { - let tag = track( + trackInto( + modifierTag, () => manager.update(state), DEBUG && `- While rendering:\n (instance of a \`${ definition.resolvedName || manager.getDebugName(definition.state) }\` modifier)` ); - updateTag(modifierTag, tag); } else { manager.update(state); } diff --git a/packages/@glimmer/signals/index.ts b/packages/@glimmer/signals/index.ts index a79fecb6026..1936c039e74 100644 --- a/packages/@glimmer/signals/index.ts +++ b/packages/@glimmer/signals/index.ts @@ -26,10 +26,8 @@ export { export { abandonFrame, beginFrame, - beginTrackFrame, beginUntrackFrame, type Cache, - combine, CONSTANT_TAG, consumeFrame, consumeTag, @@ -39,20 +37,22 @@ export { createTag, dirtyTag, disposeFrame, + freshenTag, endFrame, - endTrackFrame, endUntrackFrame, getValue, isConst, isConstComputed, isConstTag, isFrameStale, + isTagFresh, isTracking, readComputed, releaseTag, resetTracking, TagNode, track, + trackInto, untrack, updateTag, watchTag, diff --git a/packages/@glimmer/signals/lib/debug.ts b/packages/@glimmer/signals/lib/debug.ts index e191f505b0f..10fa7fb341a 100644 --- a/packages/@glimmer/signals/lib/debug.ts +++ b/packages/@glimmer/signals/lib/debug.ts @@ -178,7 +178,16 @@ if (DEBUG) { }; debug.markTagAsConsumed = (_tag: Tag) => { - if (!CONSUMED_TAGS || CONSUMED_TAGS.has(_tag)) return; + if (!CONSUMED_TAGS) return; + + let mark = debug.markTagAsConsumed!; + + if (Array.isArray(_tag)) { + _tag.forEach(mark); + return; + } + + if (CONSUMED_TAGS.has(_tag)) return; CONSUMED_TAGS.set(_tag, getLast(asPresentArray(TRANSACTION_STACK))); @@ -187,24 +196,11 @@ if (DEBUG) { * * The fields belong to `TagNode` in `tags.ts`. */ - let node = _tag as unknown as { - kind: number; - tags: Tag[] | undefined; - deps: { dep: Tag; nextDep: unknown } | undefined; - peer: Tag | undefined; - }; - - let mark = debug.markTagAsConsumed!; - - node.tags?.forEach(mark); + let node = _tag as unknown as { deps: { dep: Tag; nextDep: unknown } | undefined }; for (let dep = node.deps; dep !== undefined; dep = dep.nextDep as typeof dep) { mark(dep.dep); } - - if (node.kind === 0 && node.peer !== undefined) { - mark(node.peer); - } }; debug.assertTagNotConsumed = (tag: Tag, obj?: T, keyName?: keyof T | string | symbol) => { diff --git a/packages/@glimmer/signals/lib/tags.ts b/packages/@glimmer/signals/lib/tags.ts index 847e9c5dfee..594a38ef130 100644 --- a/packages/@glimmer/signals/lib/tags.ts +++ b/packages/@glimmer/signals/lib/tags.ts @@ -1,5 +1,5 @@ import { DEBUG } from '@glimmer/env'; -import type { Tag } from '@glimmer/interfaces'; +import type { Tag, TagNode as TagNodeInterface } from '@glimmer/interfaces'; import { scheduleRevalidate } from '@glimmer/global-context'; import { createReactiveSystem } from 'alien-signals/system'; import type { Link, ReactiveNode } from 'alien-signals/system'; @@ -31,25 +31,18 @@ const PENDING = 32; const STALE = DIRTY | PENDING; /** - * - TAG: a value that code can write. It has subscribers and no dependencies. - * - LIST: a set of tags. It is not in the graph. A subscriber links to its members. + * - TAG: a value that code can write. A TAG can follow other tags (`updateTag`). * - COMPUTED: a cached function (`createCache`, compute references). - * - FRAME: a block of updating opcodes that the VM runs again when it is stale. - * - WATCHER: forwards the writes of its dependencies to one TAG (`updateTag`). - * - COLLECTOR: an open `track()` frame. It becomes a LIST at the end. + * - FRAME: a subscriber that its owner runs again, or links again, when it is stale. */ const TAG_KIND = 0; -const LIST_KIND = 1; const COMPUTED_KIND = 2; const FRAME_KIND = 3; -const WATCHER_KIND = 4; -const COLLECTOR_KIND = 5; -const CONSTANT_KIND = 6; /** * One class for all kinds keeps the graph walk of alien-signals monomorphic. */ -export class TagNode implements ReactiveNode, Tag { +export class TagNode implements ReactiveNode, TagNodeInterface { deps: Link | undefined = undefined; depsTail: Link | undefined = undefined; subs: Link | undefined = undefined; @@ -58,19 +51,7 @@ export class TagNode implements ReactiveNode, Tag { kind: number; /** - * LIST, COLLECTOR: the members. - */ - tags: TagNode[] | undefined = undefined; - - /** - * - TAG: its watcher. - * - WATCHER: its tag. - */ - peer: TagNode | undefined = undefined; - - /** - * - COLLECTOR: its id. - * - other kinds: the id of the last collector that took this tag. + * The number of the last walk that passed this node. */ mark = 0; @@ -81,6 +62,11 @@ export class TagNode implements ReactiveNode, Tag { */ forced = false; + /** + * TAG: `true` after a write to the tag, or to a tag that it follows. + */ + stale = false; + /** * FRAME: `true` when an owner removes the links with `disposeFrame`. * @@ -106,9 +92,11 @@ const SUB_STACK: (TagNode | undefined)[] = []; */ let cycle = 0; -let collectorId = 0; - -const FIRED: TagNode[] = []; +/** + * A walk over the dependencies of a node skips a node that it passed before. + * Two tags can follow each other, so a walk can come back to its start. + */ +let walk = 0; const { link, unlink, propagate } = createReactiveSystem({ /** @@ -120,8 +108,11 @@ const { link, unlink, propagate } = createReactiveSystem({ return true; }, - notify(watcher: ReactiveNode) { - FIRED.push(watcher as TagNode); + /** + * `propagate` calls this for a TAG that follows other tags. + */ + notify(tag: ReactiveNode) { + (tag as TagNode).stale = true; }, unwatched(node: ReactiveNode) { @@ -167,26 +158,15 @@ export function createTag(): TagNode { return new TagNode(TAG_KIND, MUTABLE); } -export const CONSTANT_TAG = new TagNode(CONSTANT_KIND, MUTABLE); +/** + * A tag that never changes: a list of tags with no member. + */ +export const CONSTANT_TAG: Tag = Object.freeze([]); export function isConstTag(tag: Tag): boolean { return tag === CONSTANT_TAG; } -export function combine(tags: Tag[]): Tag { - switch (tags.length) { - case 0: - return CONSTANT_TAG; - case 1: - return tags[0] as Tag; - default: { - let list = new TagNode(LIST_KIND, MUTABLE); - list.tags = tags as TagNode[]; - return list; - } - } -} - export function dirtyTag(_tag: Tag, disableConsumptionAssertion?: boolean): void { let tag = _tag as TagNode; @@ -199,219 +179,147 @@ export function dirtyTag(_tag: Tag, disableConsumptionAssertion?: boolean): void } writes++; + tag.stale = true; let subs = tag.subs; if (subs !== undefined) { propagate(subs, false); - - if (FIRED.length > 0) { - flushWatchers(); - } } scheduleRevalidate(); } /** - * Writes the tag of each watcher that the last `propagate` reached. - * - * A watcher fires one time for each write, so two tags that forward to each - * other do not loop. - */ -function flushWatchers(): void { - for (let i = 0; i < FIRED.length; i++) { - let tag = (FIRED[i] as TagNode).peer; - - let subs = tag?.subs; - - if (subs !== undefined) { - propagate(subs, false); - } - } - - for (let i = 0; i < FIRED.length; i++) { - let watcher = FIRED[i] as TagNode; - watcher.flags = watcher.peer === undefined ? 0 : WATCHING; - } - - FIRED.length = 0; -} - -/** - * Makes `tag` change when `source` changes. + * Makes `tag` change when `source` changes, and makes `tag` fresh. * * A later call replaces the source of the tag. */ -export function updateTag(_tag: Tag, _source: Tag): void { +export function updateTag(_tag: Tag, source: Tag): void { let tag = _tag as TagNode; - let source = _source as TagNode; if (DEBUG && tag.kind !== TAG_KIND) { throw new Error('Attempted to update a tag that was not updatable'); } - let watcher = tag.peer; - - if (watcher === undefined) { - if (source === CONSTANT_TAG) return; - - watcher = tag.peer = new TagNode(WATCHER_KIND, WATCHING); - watcher.peer = tag; - } - cycle++; - watcher.depsTail = undefined; - linkLeaves(watcher, source); - purgeDeps(watcher); - watcher.flags = WATCHING; + walk++; + tag.depsTail = undefined; + linkLeaves(tag, source); + purgeDeps(tag); + settle(tag); } /** - * Removes the links from other tags to the watcher of `tag`. + * `false` after a write to the tag, or to a tag that it follows. * - * A watcher that stays linked keeps memory and slows each write of its sources. + * `updateTag`, `freshenTag`, and a run between `beginFrame` and `endFrame` + * make the tag fresh again. */ -export function releaseTag(_tag: Tag): void { - let watcher = (_tag as TagNode).peer; +export function isTagFresh(tag: Tag): boolean { + return !(tag as TagNode).stale; +} - if (watcher !== undefined) { - disposeDeps(watcher); - } +export function freshenTag(tag: Tag): void { + (tag as TagNode).stale = false; } /** - * Links `sub` to each TAG that `tag` reads at this time. + * Removes the links from other tags to `tag`. * - * A stale COMPUTED between a TAG and `sub` stops `propagate`, - * so the links go to the TAGs directly. + * A tag that stays linked keeps memory and slows each write to its sources. */ -function linkLeaves(sub: TagNode, tag: TagNode): void { - switch (tag.kind) { - case TAG_KIND: - link(tag, sub, cycle); - break; - - case CONSTANT_KIND: - break; - - default: { - let tags = tag.tags; - - if ( - tag.forced || - (tag.kind === COMPUTED_KIND && tag.deps === undefined && tag.flags !== MUTABLE) - ) { - sub.forced = true; - } - - if (tags !== undefined) { - for (let i = 0; i < tags.length; i++) { - linkLeaves(sub, tags[i] as TagNode); - } - } else { - for (let dep = tag.deps; dep !== undefined; dep = dep.nextDep) { - linkLeaves(sub, dep.dep as TagNode); - } - } - } - } +export function releaseTag(tag: Tag): void { + disposeDeps(tag as TagNode); } -function linkTag(sub: TagNode, tag: TagNode): void { - switch (tag.kind) { - case TAG_KIND: - link(tag, sub, cycle); - break; +/** + * `propagate` stops at a node that it marked before. + * + * A TAG has no function to run again, so it can let the next write through at once. + * Its `stale` field keeps the fact that a write reached it. + */ +function linkLeaf(sub: TagNode, tag: TagNode): void { + if ((tag.flags & STALE) !== 0) { + tag.flags = MUTABLE | WATCHING; + } - case LIST_KIND: { - let tags = unwrap(tag.tags); + link(tag, sub, cycle); +} - for (let i = 0; i < tags.length; i++) { - linkTag(sub, tags[i] as TagNode); - } - break; +/** + * Links `sub` to each TAG that `tag` depends on at this time. + * + * A stale COMPUTED or FRAME between a TAG and `sub` stops `propagate`, + * so `sub` needs its own link to each TAG below it. + * + * The caller increments `walk` first. + */ +function linkLeaves(sub: TagNode, tag: Tag): void { + if (Array.isArray(tag)) { + for (let i = 0; i < tag.length; i++) { + linkLeaves(sub, tag[i] as Tag); } - - case COMPUTED_KIND: - case FRAME_KIND: - if (tag.flags === MUTABLE && !tag.forced) { - if (tag.deps !== undefined) link(tag, sub, cycle); - } else { - linkLeaves(sub, tag); - } - break; + return; } -} -function collect(collector: TagNode, tag: TagNode): void { - if (tag.mark !== collector.mark) { - tag.mark = collector.mark; - unwrap(collector.tags).push(tag); + let node = tag as TagNode; + + if (node === sub) return; + + if (node.kind === TAG_KIND) { + linkLeaf(sub, node); + return; } -} -export function consumeTag(_tag: Tag): void { - let sub = activeSub; - let tag = _tag as TagNode; + if (node.mark === walk) return; - if (sub === undefined || tag === CONSTANT_TAG) return; + node.mark = walk; - if (DEBUG) { - unwrap(debug.markTagAsConsumed)(tag); + if (node.forced || (node.deps === undefined && node.flags !== MUTABLE)) { + sub.forced = true; } - if (sub.kind === COLLECTOR_KIND) { - collect(sub, tag); - } else { - linkTag(sub, tag); + for (let dep = node.deps; dep !== undefined; dep = dep.nextDep) { + linkLeaves(sub, dep.dep as TagNode); } } -////////// +function linkTag(sub: TagNode, tag: Tag): void { + if (Array.isArray(tag)) { + for (let i = 0; i < tag.length; i++) { + linkTag(sub, tag[i] as Tag); + } + return; + } -export function beginTrackFrame(debuggingContext?: string | false): void { - SUB_STACK.push(activeSub); + let node = tag as TagNode; - let collector = new TagNode(COLLECTOR_KIND, MUTABLE); - collector.tags = []; - collector.mark = ++collectorId; - activeSub = collector; + if (node === sub) return; - if (DEBUG) { - unwrap(debug.beginTrackingTransaction)(debuggingContext); + if (node.kind === TAG_KIND) { + linkLeaf(sub, node); + } else if (node.flags === MUTABLE && !node.forced) { + if (node.deps !== undefined) link(node, sub, cycle); + } else { + walk++; + linkLeaves(sub, node); } } -export function endTrackFrame(): Tag { - let collector = activeSub; +export function consumeTag(_tag: Tag): void { + let sub = activeSub; - if (DEBUG) { - if (SUB_STACK.length === 0 || collector?.kind !== COLLECTOR_KIND) { - throw new Error('attempted to close a tracking frame, but one was not open'); - } + if (sub === undefined) return; - unwrap(debug.endTrackingTransaction)(); + if (DEBUG) { + unwrap(debug.markTagAsConsumed)(_tag); } - activeSub = SUB_STACK.pop(); - - return collectorToTag(collector as TagNode); + linkTag(sub, _tag); } -function collectorToTag(collector: TagNode): TagNode { - let tags = collector.tags as TagNode[]; - - if (tags.length === 0) { - return CONSTANT_TAG; - } else if (tags.length === 1) { - return tags[0] as TagNode; - } - - collector.kind = LIST_KIND; - - return collector; -} +////////// export function beginUntrackFrame(): void { SUB_STACK.push(activeSub); @@ -441,7 +349,8 @@ export function resetTracking(): string | void { } function abandon(sub: TagNode | undefined): void { - if (sub !== undefined && (sub.kind === FRAME_KIND || sub.kind === COMPUTED_KIND)) { + if (sub !== undefined) { + sub.stale = true; sub.flags = sub.flags === 0 ? 0 : MUTABLE | DIRTY; } } @@ -450,18 +359,36 @@ export function isTracking(): boolean { return activeSub !== undefined; } -export function track(block: () => void, debugLabel?: string | false): Tag { - beginTrackFrame(debugLabel); +/** + * Runs `block` with `tag` as the subscriber. + * + * After the run, `tag` is fresh, and it changes when a tag that `block` read changes. + */ +export function trackInto(tag: Tag, block: () => void, debugLabel?: string | false): void { + beginFrame(tag as TagNode, debugLabel); + + try { + block(); + } finally { + endFrame(); + } +} + +/** + * Runs `block` in a new frame, and returns the frame. + */ +export function track(block: () => void, debugLabel?: string | false): TagNode { + let frame = createFrame(); - let tag; + beginFrame(frame, debugLabel); try { block(); } finally { - tag = endTrackFrame(); + endFrame(); } - return tag; + return frame; } export function untrack(callback: () => T): T { @@ -506,7 +433,7 @@ export function endFrame(): void { let frame = activeSub; if (DEBUG) { - if (frame?.kind !== FRAME_KIND) { + if (frame === undefined || frame.kind === COMPUTED_KIND) { throw new Error('attempted to close a frame, but one was not open'); } @@ -530,16 +457,19 @@ export function endFrame(): void { */ function settle(node: TagNode): void { if ((node.flags & PENDING) !== 0) { + walk++; + for (let dep = node.deps; dep !== undefined; dep = dep.nextDep) { let tag = dep.dep as TagNode; - if (tag.kind !== TAG_KIND && (tag.flags & STALE) !== 0) { + if ((tag.flags & STALE) !== 0) { linkLeaves(node, tag); } } } - node.flags = MUTABLE; + node.stale = false; + node.flags = node.kind === TAG_KIND && node.deps !== undefined ? MUTABLE | WATCHING : MUTABLE; } /** @@ -572,12 +502,8 @@ export function consumeFrame(frame: TagNode): void { unwrap(debug.markTagAsConsumed)(frame); } - if (sub.kind === COLLECTOR_KIND) { - collect(sub, frame); - } else { - if (frame.forced) sub.forced = true; - if (frame.deps !== undefined) link(frame, sub, cycle); - } + if (frame.forced) sub.forced = true; + if (frame.deps !== undefined) link(frame, sub, cycle); } export function disposeFrame(frame: TagNode): void { @@ -593,11 +519,11 @@ export function disposeFrame(frame: TagNode): void { * Use it for a subscriber that has no block to run: * the owner asks `isFrameStale` later, and then calls this function again. */ -export function watchTag(frame: TagNode, _tag: Tag): void { +export function watchTag(frame: TagNode, tag: Tag): void { cycle++; frame.depsTail = undefined; frame.forced = false; - linkTag(frame, _tag as TagNode); + linkTag(frame, tag); purgeDeps(frame); frame.flags = MUTABLE; } @@ -628,12 +554,8 @@ export function readComputed(node: TagNode): T { unwrap(debug.markTagAsConsumed)(node); } - if (sub.kind === COLLECTOR_KIND) { - collect(sub, node); - } else { - if (node.forced) sub.forced = true; - if (node.deps !== undefined) link(node, sub, cycle); - } + if (node.forced) sub.forced = true; + if (node.deps !== undefined) link(node, sub, cycle); } return node.value as T; diff --git a/packages/@glimmer/signals/test/graph-test.ts b/packages/@glimmer/signals/test/graph-test.ts index 57f5ad63c46..070789b2e08 100644 --- a/packages/@glimmer/signals/test/graph-test.ts +++ b/packages/@glimmer/signals/test/graph-test.ts @@ -10,9 +10,11 @@ import { endFrame, getValue, isFrameStale, + isTagFresh, releaseTag, type TagNode, track, + trackInto, updateTag, watchTag, } from '@glimmer/signals'; @@ -322,6 +324,49 @@ module('@glimmer/signals: graph', () => { assert.true(isFrameStale(frame)); }); + test('a subscriber that links to a stale tag follows the next source only', (assert) => { + let tag = createTag(); + let first = createTag(); + let second = createTag(); + let frame = createFrame(); + + trackInto(tag, () => consumeTag(first)); + watchTag(frame, tag); + + dirtyTag(first); + assert.true(isFrameStale(frame)); + assert.false(isTagFresh(tag)); + + watchTag(frame, tag); + trackInto(tag, () => consumeTag(second)); + + assert.true(isTagFresh(tag)); + + dirtyTag(first); + assert.false(isFrameStale(frame), 'the old source has no link'); + + dirtyTag(second); + assert.true(isFrameStale(frame)); + }); + + test('a subscriber sees a second write when the tag stays stale', (assert) => { + let tag = createTag(); + let source = createTag(); + let frame = createFrame(); + + updateTag(tag, source); + watchTag(frame, tag); + + dirtyTag(source); + assert.true(isFrameStale(frame)); + + watchTag(frame, tag); + assert.false(isFrameStale(frame)); + + dirtyTag(source); + assert.true(isFrameStale(frame)); + }); + test('releaseTag removes the links from the source', (assert) => { let tag = createTag(); let source = createTag(); diff --git a/packages/@glimmer/signals/test/tracking-test.ts b/packages/@glimmer/signals/test/tracking-test.ts index d25bbf53949..9c19907defb 100644 --- a/packages/@glimmer/signals/test/tracking-test.ts +++ b/packages/@glimmer/signals/test/tracking-test.ts @@ -1,7 +1,7 @@ import { DEBUG } from '@glimmer/env'; import type { Tag, TagNode } from '@glimmer/signals'; import { - beginTrackFrame, + beginFrame, beginUntrackFrame, consumeTag, createCache, @@ -9,7 +9,7 @@ import { createTag, debug, dirtyTag, - endTrackFrame, + endFrame, getValue, isConst, isFrameStale, @@ -32,6 +32,19 @@ function watch(tag: Tag) { return frame; } +const OPEN_FRAMES: TagNode[] = []; + +function beginTrackFrame() { + let frame = createFrame(); + OPEN_FRAMES.push(frame); + beginFrame(frame); +} + +function endTrackFrame() { + endFrame(); + return OPEN_FRAMES.pop() as TagNode; +} + function isValid(frame: TagNode) { return !isFrameStale(frame); } @@ -365,10 +378,7 @@ module('@glimmer/signals: tracking', () => { if (DEBUG) { test('asserts if track frame was ended without one existing', (assert) => { - assert.throws( - () => endTrackFrame(), - /attempted to close a tracking frame, but one was not open/u - ); + assert.throws(() => endTrackFrame(), /attempted to close a frame, but one was not open/u); }); } }); diff --git a/packages/@glimmer/signals/test/validators-test.ts b/packages/@glimmer/signals/test/validators-test.ts index 5530b383c41..d8e2bbab52c 100644 --- a/packages/@glimmer/signals/test/validators-test.ts +++ b/packages/@glimmer/signals/test/validators-test.ts @@ -2,7 +2,6 @@ import { DEBUG } from '@glimmer/env'; import { testOverrideGlobalContext } from '@glimmer/global-context'; import type { Tag, TagNode } from '@glimmer/signals'; import { - combine, CONSTANT_TAG, createFrame, createTag, @@ -167,7 +166,7 @@ module('@glimmer/signals: validators', () => { let tag1 = createTag(); let tag2 = createTag(); - let combined = combine([tag1, tag2]); + let combined = [tag1, tag2]; let snapshot = watch(combined); dirtyTag(tag1); @@ -183,7 +182,7 @@ module('@glimmer/signals: validators', () => { let tag1 = createTag(); let tag2 = createTag(); - let combined = combine([tag1, tag2]); + let combined = [tag1, tag2]; assert.throws( () => dirtyTag(combined), @@ -195,7 +194,7 @@ module('@glimmer/signals: validators', () => { let tag1 = createTag(); let tag2 = createTag(); - let combined = combine([tag1, tag2]); + let combined = [tag1, tag2]; assert.throws( () => updateTag(combined, tag1), From 57457412c6aa404f014ababdc2bb8cf03050020a Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:27:24 -0400 Subject: [PATCH 5/5] Port the tracker tests of main to frames The rebase brought in the tests that main added for the tracker pool. A frame of this spike returns a frame node, not a tag, so: - the two tests that compare the result of a frame with a tag are gone. They test the tracker that this spike deletes. - the other tests use the `watch` and `isValid` helpers of this file. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../@glimmer/signals/test/tracking-test.ts | 54 ++++++------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/packages/@glimmer/signals/test/tracking-test.ts b/packages/@glimmer/signals/test/tracking-test.ts index 9c19907defb..4b0d6641e61 100644 --- a/packages/@glimmer/signals/test/tracking-test.ts +++ b/packages/@glimmer/signals/test/tracking-test.ts @@ -282,17 +282,6 @@ module('@glimmer/signals: tracking', () => { assert.notOk(isValid(snapshot)); }); - test('it returns the tag itself if the frame consumed one tag many times', (assert) => { - let tag = createTag(); - - beginTrackFrame(); - - consumeTag(tag); - consumeTag(tag); - - assert.strictEqual(endTrackFrame(), tag); - }); - test('it keeps a tag that a nested frame consumed between two consumptions', (assert) => { let tag1 = createTag(); let tag2 = createTag(); @@ -304,35 +293,19 @@ module('@glimmer/signals: tracking', () => { beginTrackFrame(); consumeTag(tag1); - let inner = endTrackFrame(); + endTrackFrame(); consumeTag(tag1); let outer = endTrackFrame(); - assert.strictEqual(inner, tag1); - - let snapshot = valueForTag(outer); + let snapshot = watch(outer); dirtyTag(tag1); - assert.notOk(validateTag(outer, snapshot)); + assert.notOk(isValid(snapshot)); - snapshot = valueForTag(outer); + snapshot = watch(outer); dirtyTag(tag2); - assert.notOk(validateTag(outer, snapshot)); - }); - - test('it takes a tag one time if nested frames consume it at the same index', (assert) => { - let tag = createTag(); - - beginTrackFrame(); - - for (let i = 0; i < 3; i++) { - beginTrackFrame(); - consumeTag(tag); - consumeTag(endTrackFrame()); - } - - assert.strictEqual(endTrackFrame(), tag); + assert.notOk(isValid(snapshot)); }); test('it does not keep the tags of a frame that did not end', (assert) => { @@ -343,18 +316,24 @@ module('@glimmer/signals: tracking', () => { consumeTag(tag1); resetTracking(); + OPEN_FRAMES.length = 0; beginTrackFrame(); consumeTag(tag2); - assert.strictEqual(endTrackFrame(), tag2); + let snapshot = watch(endTrackFrame()); + + dirtyTag(tag1); + assert.ok( + isValid(snapshot), + 'the frame does not follow the tag of the frame that did not end' + ); + + dirtyTag(tag2); + assert.notOk(isValid(snapshot)); }); test('it resets after a frame that began inside untrack frames', (assert) => { - /** - * Deeper than any other test goes, - * so these depths have no tracker yet. - */ for (let i = 0; i < 100; i++) { beginUntrackFrame(); } @@ -362,6 +341,7 @@ module('@glimmer/signals: tracking', () => { beginTrackFrame(); resetTracking(); + OPEN_FRAMES.length = 0; assert.notOk(isTracking()); });