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..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/validator/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..5e67c448910 100644 --- a/package.json +++ b/package.json @@ -369,7 +369,6 @@ "@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..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 { CURRENT_TAG, validateTag, valueForTag } from '@glimmer/validator/lib/validators'; +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 = valueForTag(CURRENT_TAG); + this.#lastRevision = writeCount(); } this.#inRenderTransaction = false; } @@ -342,7 +342,7 @@ export class RendererState { root.render(); } - this.#lastRevision = valueForTag(CURRENT_TAG); + 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. * - * `CURRENT_TAG` stands for every 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 || validateTag(CURRENT_TAG, this.#lastRevision); + 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 c7e1027ca35..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,15 @@ 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, -} from '@glimmer/validator/lib/tracking'; -import { validateTag, valueForTag } from '@glimmer/validator/lib/validators'; + isFrameStale, +} from '@glimmer/signals/lib/tags'; import type Component from '../component'; import type { DynamicScope } from '../renderer'; import type RuntimeResolver from '../resolver'; @@ -275,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); @@ -341,7 +345,7 @@ export default class CurlyComponentManager let bucket = new ComponentStateBucket( component, capturedArgs, - argsTag, + argsFrame, finalizer, hasWrappedElement, isInteractive @@ -362,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; @@ -440,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); @@ -468,7 +470,7 @@ export default class CurlyComponentManager endUntrackFrame(); - consumeTag(argsTag); + consumeFrame(argsFrame); consumeTag(component[DIRTY_TAG]); } diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/root.ts b/packages/@ember/-internals/glimmer/lib/component-managers/root.ts index abcb5383a05..1fafc6fa95d 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 { 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'; @@ -66,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/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..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,10 +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 type { Revision } from '@glimmer/validator/lib/validators'; -import type { Tag } from '@glimmer/interfaces'; -import { beginUntrackFrame, endUntrackFrame } from '@glimmer/validator/lib/tracking'; -import { valueForTag } from '@glimmer/validator/lib/validators'; +import { beginUntrackFrame, endUntrackFrame } from '@glimmer/signals/lib/tags'; +import type { TagNode } from '@glimmer/signals/lib/tags'; import type Component from '../component'; type Finalizer = () => void; @@ -30,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/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..e8656feb3ad 100644 --- a/packages/@ember/-internals/meta/lib/meta.ts +++ b/packages/@ember/-internals/meta/lib/meta.ts @@ -3,7 +3,6 @@ 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 { UpdatableTag } from '@glimmer/interfaces'; type ObjMap = { [key: string]: T }; @@ -99,7 +98,7 @@ export class Meta { /** @internal */ _values: ObjMap | undefined; /** @internal */ - _revisions: ObjMap | undefined; + _cached: ObjMap | undefined; /** @internal */ source: object; /** @internal */ @@ -127,7 +126,7 @@ export class Meta { this._mixins = undefined; this._lazyChains = undefined; this._values = undefined; - this._revisions = undefined; + this._cached = undefined; // initial value for all flags right now is false // see FLAGS const for detailed list of flags used @@ -170,7 +169,7 @@ export class Meta { } /** @internal */ - _getOrCreateOwnMap(key: '_values' | '_revisions' | '_lazyChains') { + _getOrCreateOwnMap(key: '_values' | '_cached' | '_lazyChains') { return this[key] || (this[key] = Object.create(null)); } @@ -221,18 +220,22 @@ export class Meta { values[key] = value; } - /** @internal */ - revisionFor(key: string): Revision | undefined { - let revisions = this._revisions; + /** + * `true` when `valueFor` holds a computed value for the key. + * + * The value is valid while the tag of the property is fresh. + * + * @internal + */ + hasCacheFor(key: string): boolean { + let cached = this._cached; - return revisions !== undefined ? revisions[key] : undefined; + return cached !== undefined && cached[key] === true; } /** @internal */ - setRevisionFor(key: string, revision: Revision | undefined) { - let revisions = this._getOrCreateOwnMap('_revisions'); - - revisions[key] = revision; + setCachedFor(key: string, isCached: boolean) { + this._getOrCreateOwnMap('_cached')[key] = isCached; } /** @internal */ 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/packages/@ember/-internals/metal/lib/alias.ts b/packages/@ember/-internals/metal/lib/alias.ts index 7f5915725bb..882cd0cd093 100644 --- a/packages/@ember/-internals/metal/lib/alias.ts +++ b/packages/@ember/-internals/metal/lib/alias.ts @@ -3,13 +3,8 @@ 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, - validateTag, - valueForTag, -} from '@glimmer/validator/lib/validators'; +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'; import { @@ -89,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.hasCacheFor(keyName) || !isTagFresh(propertyTag)) { updateTag(propertyTag, getChainTagsForKey(obj, this.altKey, tagMeta, meta)); - meta.setRevisionFor(keyName, valueForTag(propertyTag)); + meta.setCachedFor(keyName, true); 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/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..421cdc98e9e 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 { createTag, isTagFresh, updateTag } from '@glimmer/signals/lib/tags'; +import { tagMetaFor } from '@glimmer/signals/lib/meta'; import { objectAt } from './object-at'; import { tagForProperty } from './tags'; @@ -44,7 +39,7 @@ export function getChainTagsForKeys( getChainTags(tags, obj, key, tagMeta, meta); } - return combine(tags); + return tags; } export function getChainTagsForKey( @@ -53,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( @@ -176,16 +171,14 @@ 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.hasCacheFor(segment) && isTagFresh(propertyTag)) { current = instanceMeta.valueFor(segment); } else { // use metaFor here to ensure we have the meta for the instance 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..30af4e7a167 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, - validateTag, - valueForTag, -} from '@glimmer/validator/lib/validators'; -import { consumeTag, track, untrack } from '@glimmer/validator/lib/tracking'; -import { tagFor, tagMetaFor } from '@glimmer/validator/lib/meta'; + 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 { ExtendedMethodDecorator, @@ -402,9 +402,7 @@ export class ComputedProperty extends ComputedDescriptor { let ret; - let revision = meta.revisionFor(keyName); - - if (revision !== undefined && validateTag(propertyTag, revision)) { + 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 @@ -423,14 +421,12 @@ export class ComputedProperty extends ComputedDescriptor { if (_dependentKeys !== undefined) { updateTag(propertyTag, getChainTagsForKeys(obj, _dependentKeys, tagMeta, meta)); - - if (DEBUG) { - ALLOW_CYCLES!.set(propertyTag, true); - } + } else { + freshenTag(propertyTag); } meta.setValueFor(keyName, ret); - meta.setRevisionFor(keyName, valueForTag(propertyTag)); + meta.setCachedFor(keyName, true); finishLazyChains(meta, keyName, ret); } @@ -501,13 +497,11 @@ export class ComputedProperty extends ComputedDescriptor { if (_dependentKeys !== undefined) { updateTag(propertyTag, getChainTagsForKeys(obj, _dependentKeys, tagMeta, meta)); - - if (DEBUG) { - ALLOW_CYCLES!.set(propertyTag, true); - } + } else { + freshenTag(propertyTag); } - meta.setRevisionFor(keyName, valueForTag(propertyTag)); + meta.setCachedFor(keyName, true); } finally { endPropertyChanges(); } @@ -520,7 +514,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; @@ -548,8 +542,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.setCachedFor(keyName, false); meta.setValueFor(keyName, undefined); } @@ -566,9 +560,7 @@ class AutoComputedProperty extends ComputedProperty { let ret; - let revision = meta.revisionFor(keyName); - - if (revision !== undefined && validateTag(propertyTag, revision)) { + if (meta.hasCacheFor(keyName) && isTagFresh(propertyTag)) { ret = meta.valueFor(keyName); } else { assert( @@ -579,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.setRevisionFor(keyName, valueForTag(propertyTag)); + meta.setCachedFor(keyName, true); finishLazyChains(meta, keyName, ret); } diff --git a/packages/@ember/-internals/metal/lib/observer.ts b/packages/@ember/-internals/metal/lib/observer.ts index 08c5148cd9c..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 { CURRENT_TAG, validateTag, valueForTag } from '@glimmer/validator/lib/validators'; -import { tagMetaFor } from '@glimmer/validator/lib/meta'; +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 currentRevision = valueForTag(CURRENT_TAG); - if (lastKnownRevision === currentRevision) { + let writes = writeCount(); + if (lastKnownWrites === writes) { return; } - lastKnownRevision = currentRevision; + 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/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..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/validator'; +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 3f693666e99..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/validator'; +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 ed8be57bf7a..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/validator'; +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 0d781938072..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/validator'; +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 e2eef6ed310..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/validator'; +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/-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..00b9c393440 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 { 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)); @@ -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/-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..eac929c0a7f 100644 --- a/packages/@ember/array/proxy.ts +++ b/packages/@ember/array/proxy.ts @@ -20,14 +20,9 @@ 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, - validateTag, - valueForTag, - type Revision, -} from '@glimmer/validator/lib/validators'; -import { consumeTag } from '@glimmer/validator/lib/tracking'; -import { tagFor } from '@glimmer/validator/lib/meta'; +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'; function isMutable(obj: T[] | EmberArray): obj is T[] | MutableArray { @@ -192,9 +187,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 +376,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,12 +391,14 @@ 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')]); - 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/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..0f8e239f15d 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, trackInto } 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; @@ -32,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; }; @@ -130,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/@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..b9e533fb7f7 100644 --- a/packages/@glimmer/interfaces/lib/tags.d.ts +++ b/packages/@glimmer/interfaces/lib/tags.d.ts @@ -1,57 +1,18 @@ -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; - /** - * 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; -} - -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 TagNode { + readonly kind: number; } -export interface ConstantTag extends MonomorphicTag { - readonly [TYPE]: CONSTANT_TAG_ID; -} +/** + * One node, or a list of tags. A subscriber to a list subscribes to each member. + */ +export type Tag = TagNode | readonly Tag[]; -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..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/validator/lib/tracking'; +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/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..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/validator'; +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/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..a1e123aa292 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 { + CONSTANT_TAG, + consumeTag, + createFrame, + isFrameStale, + watchTag, +} 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'; @@ -319,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 = valueForTag(tag ?? CURRENT_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); @@ -375,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/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..dc68aeec745 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 { trackInto } from '@glimmer/signals/lib/tags'; import DebugRenderTree from './debug-render-tree'; import { DOMChangesImpl, DOMTreeConstruction } from './dom/helper'; @@ -64,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); } @@ -81,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/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..1e348b678dc 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(true); + 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(true)); + 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..01941a548bd 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(true)); 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/signals/index.ts b/packages/@glimmer/signals/index.ts new file mode 100644 index 00000000000..1936c039e74 --- /dev/null +++ b/packages/@glimmer/signals/index.ts @@ -0,0 +1,67 @@ +const GLIMMER_SIGNALS_REGISTRATION = Symbol('GLIMMER_SIGNALS_REGISTRATION'); + +if (Reflect.has(globalThis, GLIMMER_SIGNALS_REGISTRATION)) { + throw new 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_SIGNALS_REGISTRATION, true); + +export { trackedArray } from './lib/collections/array'; +export { trackedMap } from './lib/collections/map'; +export { trackedObject } from './lib/collections/object'; +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, releaseTagsFor, tagFor, type TagMeta, tagMetaFor } from './lib/meta'; +export { trackedData } from './lib/tracked-data'; +export { + type Reactive, + type ReadOnlyReactive, + TrackedValue, + trackedValue, +} from './lib/tracked-value'; +export { + abandonFrame, + beginFrame, + beginUntrackFrame, + type Cache, + CONSTANT_TAG, + consumeFrame, + consumeTag, + createCache, + createComputed, + createFrame, + createTag, + dirtyTag, + disposeFrame, + freshenTag, + endFrame, + endUntrackFrame, + getValue, + isConst, + isConstComputed, + isConstTag, + isFrameStale, + isTagFresh, + isTracking, + readComputed, + releaseTag, + resetTracking, + TagNode, + track, + trackInto, + untrack, + updateTag, + watchTag, + writeCount, +} from './lib/tags'; +export type { + CombinatorTag, + ConstantTag, + DirtyableTag, + Tag, + UpdatableTag, +} from '@glimmer/interfaces'; 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 92% rename from packages/@glimmer/validator/lib/debug.ts rename to packages/@glimmer/signals/lib/debug.ts index 154acaee33a..10fa7fb341a 100644 --- a/packages/@glimmer/validator/lib/debug.ts +++ b/packages/@glimmer/signals/lib/debug.ts @@ -178,21 +178,28 @@ if (DEBUG) { }; debug.markTagAsConsumed = (_tag: Tag) => { - if (!CONSUMED_TAGS || CONSUMED_TAGS.has(_tag)) return; + if (!CONSUMED_TAGS) return; - CONSUMED_TAGS.set(_tag, getLast(asPresentArray(TRANSACTION_STACK))); + let mark = debug.markTagAsConsumed!; + + if (Array.isArray(_tag)) { + _tag.forEach(mark); + return; + } - // 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 (CONSUMED_TAGS.has(_tag)) return; + + CONSUMED_TAGS.set(_tag, getLast(asPresentArray(TRANSACTION_STACK))); - 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 { deps: { dep: Tag; nextDep: unknown } | undefined }; - if (Array.isArray(subtag)) { - subtag.forEach(debug.markTagAsConsumed); - } else { - debug.markTagAsConsumed(subtag); + for (let dep = node.deps; dep !== undefined; dep = dep.nextDep as typeof dep) { + mark(dep.dep); } }; 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..594a38ef130 --- /dev/null +++ b/packages/@glimmer/signals/lib/tags.ts @@ -0,0 +1,666 @@ +import { DEBUG } from '@glimmer/env'; +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'; + +import { debug } from './debug'; +import { unwrap } from './utils'; + +let writes = 0; + +/** + * The number of writes to tags since the start of the program. + * + * A caller can keep this number to learn later if any write occurred. + */ +export function writeCount(): number { + return writes; +} + +/** + * 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. A TAG can follow other tags (`updateTag`). + * - COMPUTED: a cached function (`createCache`, compute references). + * - FRAME: a subscriber that its owner runs again, or links again, when it is stale. + */ +const TAG_KIND = 0; +const COMPUTED_KIND = 2; +const FRAME_KIND = 3; + +/** + * One class for all kinds keeps the graph walk of alien-signals monomorphic. + */ +export class TagNode implements ReactiveNode, TagNodeInterface { + deps: Link | undefined = undefined; + depsTail: Link | undefined = undefined; + subs: Link | undefined = undefined; + subsTail: Link | undefined = undefined; + flags: number; + kind: number; + + /** + * The number of the last walk that passed this node. + */ + mark = 0; + + /** + * 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. + */ + 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`. + * + * Such a frame keeps its links when its last subscriber goes away. + */ + owned = false; + + 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; + +/** + * 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({ + /** + * `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; + }, + + /** + * `propagate` calls this for a TAG that follows other tags. + */ + notify(tag: ReactiveNode) { + (tag as TagNode).stale = true; + }, + + unwatched(node: ReactiveNode) { + let tag = node as TagNode; + + if (tag.kind === COMPUTED_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; + } + }, +}); + +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); +} + +/** + * 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 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); + } + + writes++; + tag.stale = true; + + let subs = tag.subs; + + if (subs !== undefined) { + propagate(subs, false); + } + + scheduleRevalidate(); +} + +/** + * 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 { + let tag = _tag as TagNode; + + if (DEBUG && tag.kind !== TAG_KIND) { + throw new Error('Attempted to update a tag that was not updatable'); + } + + cycle++; + walk++; + tag.depsTail = undefined; + linkLeaves(tag, source); + purgeDeps(tag); + settle(tag); +} + +/** + * `false` after a write to the tag, or to a tag that it follows. + * + * `updateTag`, `freshenTag`, and a run between `beginFrame` and `endFrame` + * make the tag fresh again. + */ +export function isTagFresh(tag: Tag): boolean { + return !(tag as TagNode).stale; +} + +export function freshenTag(tag: Tag): void { + (tag as TagNode).stale = false; +} + +/** + * Removes the links from other tags to `tag`. + * + * A tag that stays linked keeps memory and slows each write to its sources. + */ +export function releaseTag(tag: Tag): void { + disposeDeps(tag as TagNode); +} + +/** + * `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; + } + + link(tag, sub, cycle); +} + +/** + * 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); + } + return; + } + + let node = tag as TagNode; + + if (node === sub) return; + + if (node.kind === TAG_KIND) { + linkLeaf(sub, node); + return; + } + + if (node.mark === walk) return; + + node.mark = walk; + + if (node.forced || (node.deps === undefined && node.flags !== MUTABLE)) { + sub.forced = true; + } + + 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; + } + + let node = tag as TagNode; + + if (node === sub) return; + + 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 consumeTag(_tag: Tag): void { + let sub = activeSub; + + if (sub === undefined) return; + + if (DEBUG) { + unwrap(debug.markTagAsConsumed)(_tag); + } + + linkTag(sub, _tag); +} + +////////// + +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.stale = true; + sub.flags = sub.flags === 0 ? 0 : MUTABLE | DIRTY; + } +} + +export function isTracking(): boolean { + return activeSub !== undefined; +} + +/** + * 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(); + + beginFrame(frame, debugLabel); + + try { + block(); + } finally { + endFrame(); + } + + return frame; +} + +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(owned = false): TagNode { + let frame = new TagNode(FRAME_KIND, MUTABLE); + frame.owned = owned; + return frame; +} + +export function beginFrame(frame: TagNode, debuggingContext?: string | false): void { + SUB_STACK.push(activeSub); + + cycle++; + frame.depsTail = undefined; + frame.forced = 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 === undefined || frame.kind === COMPUTED_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) { + walk++; + + for (let dep = node.deps; dep !== undefined; dep = dep.nextDep) { + let tag = dep.dep as TagNode; + + if ((tag.flags & STALE) !== 0) { + linkLeaves(node, tag); + } + } + } + + node.stale = false; + node.flags = node.kind === TAG_KIND && node.deps !== undefined ? MUTABLE | WATCHING : 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.forced; +} + +/** + * Makes the open frame depend on `frame`. + */ +export function consumeFrame(frame: TagNode): void { + let sub = activeSub; + + if (sub === undefined || (frame.deps === undefined && !frame.forced)) return; + + if (DEBUG) { + unwrap(debug.markTagAsConsumed)(frame); + } + + if (frame.forced) sub.forced = true; + if (frame.deps !== undefined) link(frame, sub, cycle); +} + +export function disposeFrame(frame: TagNode): void { + disposeDeps(frame); + frame.flags = MUTABLE | DIRTY; +} + +////////// + +/** + * Makes `frame` depend on `tag` only, and makes it not stale. + * + * 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); + 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 flags = node.flags; + + 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.forced) + ) { + if (DEBUG) { + unwrap(debug.markTagAsConsumed)(node); + } + + if (node.forced) sub.forced = true; + if (node.deps !== undefined) link(node, sub, cycle); + } + + return node.value as T; +} + +function evaluate(node: TagNode): void { + let first = node.flags === 0; + let done = false; + + SUB_STACK.push(activeSub); + + cycle++; + node.forced = false; + node.depsTail = undefined; + node.flags = MUTABLE | RECURSED_CHECK; + activeSub = node; + + 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) { + if (DEBUG) { + unwrap(debug.endTrackingTransaction)(); + } + + activeSub = SUB_STACK.pop(); + } + + purgeDeps(node); + + if (done) { + settle(node); + } else { + node.flags = first ? 0 : MUTABLE | DIRTY; + } + } +} + +export function isConstComputed(node: TagNode): boolean { + return node.flags !== 0 && node.deps === undefined && !node.forced; +} + +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..070789b2e08 --- /dev/null +++ b/packages/@glimmer/signals/test/graph-test.ts @@ -0,0 +1,381 @@ +import { + beginFrame, + consumeFrame, + consumeTag, + createCache, + createFrame, + createTag, + dirtyTag, + disposeFrame, + endFrame, + getValue, + isFrameStale, + isTagFresh, + releaseTag, + type TagNode, + track, + trackInto, + updateTag, + watchTag, +} 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 keeps its value until a write', (assert) => { + let tag = createTag(); + let count = 0; + let cache = createCache(() => { + consumeTag(tag); + return ++count; + }); + + assert.strictEqual(getValue(cache), 1); + assert.strictEqual(getValue(cache), 1); + + dirtyTag(tag); + + assert.strictEqual(getValue(cache), 2); + }); + + test('a cache that loses its last subscriber removes its links', (assert) => { + let tag = createTag(); + let count = 0; + let cache = createCache(() => { + consumeTag(tag); + return ++count; + }); + let frame = createFrame(); + + run(frame, () => assert.strictEqual(getValue(cache), 1)); + + assert.true(hasSubscribers(tag)); + + disposeFrame(frame); + + assert.false(hasSubscribers(tag)); + assert.strictEqual(getValue(cache), 2, 'the cache runs again, because it had no links'); + }); + + test('a frame is stale after a write below a nested cache', (assert) => { + let tag = createTag(); + let inner = createCache(() => consumeTag(tag)); + let outer = createCache(() => getValue(inner)); + let frame = createFrame(); + + run(frame, () => getValue(outer)); + dirtyTag(tag); + + assert.true(isFrameStale(frame)); + }); + + 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 frame = createFrame(); + + watchTag(frame, tag); + dirtyTag(first); + assert.true(isFrameStale(frame)); + + watchTag(frame, tag); + assert.false(isFrameStale(frame)); + + dirtyTag(second); + assert.true(isFrameStale(frame), 'the stale cache does not hide the second write'); + }); + }); + + module('updateTag', () => { + test('the tag changes for each write to the source', (assert) => { + let tag = createTag(); + let source = createTag(); + + updateTag(tag, source); + + let frame = createFrame(); + watchTag(frame, tag); + dirtyTag(source); + assert.true(isFrameStale(frame)); + + watchTag(frame, tag); + dirtyTag(source); + assert.true(isFrameStale(frame)); + }); + + 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 frame = createFrame(); + watchTag(frame, tag); + dirtyTag(leaf); + assert.true(isFrameStale(frame)); + + watchTag(frame, tag); + dirtyTag(leaf); + 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) => { + 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 frame = createFrame(); + watchTag(frame, first); + dirtyTag(third); + + assert.true(isFrameStale(frame)); + }); + + 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 frame = createFrame(); + watchTag(frame, tag); + + dirtyTag(oldSource); + assert.false(isFrameStale(frame)); + assert.false(hasSubscribers(oldSource)); + + dirtyTag(newSource); + 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(); + + updateTag(tag, source); + assert.true(hasSubscribers(source)); + + releaseTag(tag); + assert.false(hasSubscribers(source)); + }); + }); +}); diff --git a/packages/@glimmer/signals/test/meta-test.ts b/packages/@glimmer/signals/test/meta-test.ts new file mode 100644 index 00000000000..c179d322f73 --- /dev/null +++ b/packages/@glimmer/signals/test/meta-test.ts @@ -0,0 +1,35 @@ +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 = {}; + let tag = tagFor(obj, 'foo'); + assert.strictEqual(tagFor(obj, 'foo'), tag); + }); + + test('it can dirty the tag for a property on a given object', (assert) => { + let obj = {}; + let tag = tagFor(obj, 'foo'); + + let snapshot = watch(tag); + dirtyTagFor(obj, 'foo'); + + assert.notOk(isValid(snapshot)); + }); +}); 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 69% rename from packages/@glimmer/validator/test/tracked-value-test.ts rename to packages/@glimmer/signals/test/tracked-value-test.ts index b7962546ab7..209d7bae1b5 100644 --- a/packages/@glimmer/validator/test/tracked-value-test.ts +++ b/packages/@glimmer/signals/test/tracked-value-test.ts @@ -1,8 +1,22 @@ -import { track, trackedValue, validateTag, valueForTag } from '@glimmer/validator'; +import type { Tag, TagNode } from '@glimmer/signals'; +import { createFrame, isFrameStale, track, trackedValue, watchTag } from '@glimmer/signals'; import { module, test } from './-utils'; -module('@glimmer/validator: trackedValue()', () => { +/** + * 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/validator: 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/validator/test/tracking-test.ts b/packages/@glimmer/signals/test/tracking-test.ts similarity index 83% rename from packages/@glimmer/validator/test/tracking-test.ts rename to packages/@glimmer/signals/test/tracking-test.ts index 40d8d57f18a..4b0d6641e61 100644 --- a/packages/@glimmer/validator/test/tracking-test.ts +++ b/packages/@glimmer/signals/test/tracking-test.ts @@ -1,27 +1,55 @@ import { DEBUG } from '@glimmer/env'; +import type { Tag, TagNode } from '@glimmer/signals'; import { - beginTrackFrame, + beginFrame, beginUntrackFrame, consumeTag, createCache, + createFrame, createTag, debug, dirtyTag, - endTrackFrame, + endFrame, getValue, isConst, + isFrameStale, isTracking, resetTracking, track, trackedData, untrack, - validateTag, - valueForTag, -} from '@glimmer/validator'; + watchTag, +} from '@glimmer/signals'; import { module, test } from './-utils'; -module('@glimmer/validator: tracking', () => { +/** + * 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; +} + +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); +} + +module('@glimmer/signals: tracking', () => { module('track', () => { test('it combines tags that are consumed within a track frame', (assert) => { let tag1 = createTag(); @@ -32,13 +60,13 @@ module('@glimmer/validator: 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 +81,13 @@ module('@glimmer/validator: 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 +102,13 @@ module('@glimmer/validator: 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 +125,13 @@ module('@glimmer/validator: 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 +194,13 @@ module('@glimmer/validator: 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 +217,13 @@ module('@glimmer/validator: 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 +244,13 @@ module('@glimmer/validator: 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,24 +273,13 @@ module('@glimmer/validator: 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)); - }); - - 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); + assert.notOk(isValid(snapshot)); }); test('it keeps a tag that a nested frame consumed between two consumptions', (assert) => { @@ -276,35 +293,19 @@ module('@glimmer/validator: 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) => { @@ -315,18 +316,24 @@ module('@glimmer/validator: 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(); } @@ -334,6 +341,7 @@ module('@glimmer/validator: tracking', () => { beginTrackFrame(); resetTracking(); + OPEN_FRAMES.length = 0; assert.notOk(isTracking()); }); @@ -350,10 +358,7 @@ module('@glimmer/validator: 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); }); } }); @@ -565,10 +570,10 @@ module('@glimmer/validator: 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 new file mode 100644 index 00000000000..d8e2bbab52c --- /dev/null +++ b/packages/@glimmer/signals/test/validators-test.ts @@ -0,0 +1,237 @@ +import { DEBUG } from '@glimmer/env'; +import { testOverrideGlobalContext } from '@glimmer/global-context'; +import type { Tag, TagNode } from '@glimmer/signals'; +import { + CONSTANT_TAG, + createFrame, + createTag, + writeCount, + dirtyTag, + isFrameStale, + updateTag, + 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'); + } + + return value; +} + +module('@glimmer/signals: validators', () => { + module('DirtyableTag', () => { + test('it can be dirtied', (assert) => { + let tag = createTag(); + let snapshot = watch(tag); + + assert.ok(isValid(snapshot)); + + dirtyTag(tag); + assert.notOk(isValid(snapshot)); + + snapshot = watch(tag); + assert.ok(isValid(snapshot)); + }); + + if (DEBUG) { + test('it calls scheduleRevalidate', (assert) => { + let originalContext = unwrap(testOverrideGlobalContext)({ + scheduleRevalidate() { + assert.step('scheduleRevalidate'); + assert.ok(true, 'called'); + }, + }); + + try { + let tag = createTag(); + + dirtyTag(tag); + } finally { + unwrap(testOverrideGlobalContext)(originalContext); + } + + assert.verifySteps(['scheduleRevalidate']); + }); + } + }); + + module('UpdatableTag', () => { + test('it can be dirtied', (assert) => { + let tag = createTag(); + let snapshot = watch(tag); + + assert.ok(isValid(snapshot)); + + dirtyTag(tag); + assert.notOk(isValid(snapshot)); + + snapshot = watch(tag); + assert.ok(isValid(snapshot)); + }); + + test('it can be updated', (assert) => { + let tag = createTag(); + let subtag = createTag(); + + updateTag(tag, subtag); + + let snapshot = watch(tag); + assert.ok(isValid(snapshot)); + + dirtyTag(subtag); + assert.notOk(isValid(snapshot)); + + snapshot = watch(tag); + assert.ok(isValid(snapshot)); + }); + + test('it correctly buffers updates when subtag has a less recent value', (assert) => { + let tag = createTag(); + let subtag = createTag(); + + // First, we dirty the parent tag so it is more recent than the subtag + dirtyTag(tag); + + // Then, we get a snapshot of the parent + let snapshot = watch(tag); + + // Now, we update the parent tag with the subtag, and revalidate it + updateTag(tag, subtag); + + 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(isValid(snapshot), 'tag is invalid after subtag is dirtied again'); + }); + + test('it correctly buffers updates when subtag has a more recent value', (assert) => { + let tag = createTag(); + let subtag = createTag(); + + // First, we get a snapshot of the parent + let snapshot = watch(tag); + + // Then we dirty the currently unrelated subtag + dirtyTag(subtag); + + // Now, we update the parent tag with the subtag, and revalidate it + updateTag(tag, subtag); + + 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(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 = watch(tag); + let subtagSnapshot = watch(subtag); + + updateTag(tag, subtag); + updateTag(subtag, tag); + + dirtyTag(tag); + + assert.notOk(isValid(snapshot)); + assert.notOk(isValid(subtagSnapshot)); + }); + }); + + module('CombinatorTag', () => { + test('it can combine multiple tags', (assert) => { + let tag1 = createTag(); + let tag2 = createTag(); + + let combined = [tag1, tag2]; + + let snapshot = watch(combined); + dirtyTag(tag1); + assert.notOk(isValid(snapshot)); + + snapshot = watch(combined); + dirtyTag(tag2); + assert.notOk(isValid(snapshot)); + }); + + if (DEBUG) { + test('it cannot be dirtied', (assert) => { + let tag1 = createTag(); + let tag2 = createTag(); + + let combined = [tag1, tag2]; + + assert.throws( + () => dirtyTag(combined), + /Error: Attempted to dirty a tag that was not dirtyable/u + ); + }); + + test('it cannot be updated', (assert) => { + let tag1 = createTag(); + let tag2 = createTag(); + + let combined = [tag1, tag2]; + + assert.throws( + () => updateTag(combined, tag1), + /Error: Attempted to update a tag that was not updatable/u + ); + }); + } + }); + + module('ConstantTag', () => { + if (DEBUG) { + test('it cannot be dirtied', (assert) => { + assert.throws( + () => dirtyTag(CONSTANT_TAG), + /Error: Attempted to dirty a tag that was not dirtyable/u + ); + }); + + test('it cannot be updated', (assert) => { + let subtag = createTag(); + + assert.throws( + () => updateTag(CONSTANT_TAG, subtag), + /Error: Attempted to update a tag that was not updatable/u + ); + }); + } + }); + + module('writeCount', () => { + test('it changes when a tag is dirtied', (assert) => { + let snapshot = writeCount(); + + let tag = createTag(); + dirtyTag(tag); + + assert.notStrictEqual(writeCount(), snapshot); + }); + }); +}); 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/index.ts b/packages/@glimmer/validator/index.ts deleted file mode 100644 index 920b34148d7..00000000000 --- a/packages/@glimmer/validator/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -const GLIMMER_VALIDATOR_REGISTRATION = Symbol('GLIMMER_VALIDATOR_REGISTRATION'); - -if (Reflect.has(globalThis, GLIMMER_VALIDATOR_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.' - ); -} - -Reflect.set(globalThis, GLIMMER_VALIDATOR_REGISTRATION, true); - -export { trackedArray } from './lib/collections/array'; -export { trackedMap } from './lib/collections/map'; -export { trackedObject } from './lib/collections/object'; -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 { trackedData } from './lib/tracked-data'; -export { - type Reactive, - type ReadOnlyReactive, - TrackedValue, - trackedValue, -} from './lib/tracked-value'; -export { - beginTrackFrame, - beginUntrackFrame, - type Cache, - consumeTag, - createCache, - endTrackFrame, - endUntrackFrame, - getValue, - isConst, - isTracking, - resetTracking, - 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, - validateTag, - valueForTag, - VOLATILE, - VOLATILE_TAG, - VolatileTag, -} from './lib/validators'; -export type { - CombinatorTag, - ConstantTag, - DirtyableTag, - Tag, - UpdatableTag, -} from '@glimmer/interfaces'; 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/@glimmer/validator/test/meta-test.ts b/packages/@glimmer/validator/test/meta-test.ts deleted file mode 100644 index cf6a739c74f..00000000000 --- a/packages/@glimmer/validator/test/meta-test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { dirtyTagFor, tagFor, validateTag, valueForTag } from '@glimmer/validator'; - -import { module, test } from './-utils'; - -module('@glimmer/validator: meta', () => { - test('it creates a unique tag for a property on a given object', (assert) => { - let obj = {}; - let tag = tagFor(obj, 'foo'); - assert.strictEqual(tagFor(obj, 'foo'), tag); - }); - - test('it can dirty the tag for a property on a given object', (assert) => { - let obj = {}; - let tag = tagFor(obj, 'foo'); - - let snapshot = valueForTag(tag); - dirtyTagFor(obj, 'foo'); - - assert.notOk(validateTag(tag, snapshot)); - }); -}); diff --git a/packages/@glimmer/validator/test/validators-test.ts b/packages/@glimmer/validator/test/validators-test.ts deleted file mode 100644 index 9c963a1c928..00000000000 --- a/packages/@glimmer/validator/test/validators-test.ts +++ /dev/null @@ -1,331 +0,0 @@ -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, - dirtyTag, - updateTag, - validateTag, - valueForTag, - VOLATILE_TAG, -} from '@glimmer/validator'; - -import { module, test } from './-utils'; - -function unwrap(value: T | null | undefined): T { - if (value === null || value === undefined) { - throw new Error('unexpected null or undefined value'); - } - - return value; -} - -module('@glimmer/validator: validators', () => { - module('DirtyableTag', () => { - test('it can be dirtied', (assert) => { - let tag = createTag(); - let snapshot = valueForTag(tag); - - assert.ok(validateTag(tag, snapshot)); - - dirtyTag(tag); - assert.notOk(validateTag(tag, snapshot)); - - snapshot = valueForTag(tag); - assert.ok(validateTag(tag, snapshot)); - }); - - if (DEBUG) { - test('it calls scheduleRevalidate', (assert) => { - let originalContext = unwrap(testOverrideGlobalContext)({ - scheduleRevalidate() { - assert.step('scheduleRevalidate'); - assert.ok(true, 'called'); - }, - }); - - try { - let tag = createTag(); - - dirtyTag(tag); - } finally { - unwrap(testOverrideGlobalContext)(originalContext); - } - - 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 snapshot = valueForTag(tag); - - assert.ok(validateTag(tag, snapshot)); - - dirtyTag(tag); - assert.notOk(validateTag(tag, snapshot)); - - snapshot = valueForTag(tag); - assert.ok(validateTag(tag, snapshot)); - }); - - test('it can be updated', (assert) => { - let tag = createUpdatableTag(); - let subtag = createUpdatableTag(); - - updateTag(tag, subtag); - - let snapshot = valueForTag(tag); - assert.ok(validateTag(tag, snapshot)); - - dirtyTag(subtag); - assert.notOk(validateTag(tag, snapshot)); - - snapshot = valueForTag(tag); - assert.ok(validateTag(tag, snapshot)); - }); - - test('it correctly buffers updates when subtag has a less recent value', (assert) => { - let tag = createUpdatableTag(); - let subtag = createUpdatableTag(); - - // First, we dirty the parent tag so it is more recent than the subtag - dirtyTag(tag); - - // Then, we get a snapshot of the parent - let snapshot = valueForTag(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'); - - // 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'); - }); - - test('it correctly buffers updates when subtag has a more recent value', (assert) => { - let tag = createUpdatableTag(); - let subtag = createUpdatableTag(); - - // First, we get a snapshot of the parent - let snapshot = valueForTag(tag); - - // Then we dirty the currently unrelated subtag - dirtyTag(subtag); - - // 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'); - - // 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'); - }); - - 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); - - unwrap(ALLOW_CYCLES).set(tag, true); - unwrap(ALLOW_CYCLES).set(subtag, true); - - updateTag(tag, subtag); - updateTag(subtag, tag); - - dirtyTag(tag); - - assert.notOk(validateTag(tag, snapshot)); - }); - } - }); - - module('CombinatorTag', () => { - test('it can combine multiple tags', (assert) => { - let tag1 = createTag(); - let tag2 = createTag(); - - let combined = combine([tag1, tag2]); - - let snapshot = valueForTag(combined); - dirtyTag(tag1); - assert.notOk(validateTag(combined, snapshot)); - - snapshot = valueForTag(combined); - dirtyTag(tag2); - assert.notOk(validateTag(combined, snapshot)); - }); - - if (DEBUG) { - test('it cannot be dirtied', (assert) => { - let tag1 = createTag(); - let tag2 = createTag(); - - 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 - ); - }); - - test('it cannot be updated', (assert) => { - let tag1 = createTag(); - let tag2 = createTag(); - - 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 - ); - }); - } - }); - - module('ConstantTag', () => { - 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 - ); - }); - - test('it cannot be updated', (assert) => { - 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 - ); - }); - } - }); - - 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( - // @ts-expect-error this is an error condition - () => 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( - // @ts-expect-error this is an error condition - () => updateTag(VOLATILE_TAG, subtag), - /Error: Attempted to update a tag that was not updatable/u - ); - }); - } - }); - - module('CurrentTag', () => { - test('it is always the current revision', (assert) => { - let snapshot = valueForTag(CURRENT_TAG); - assert.ok(validateTag(CURRENT_TAG, snapshot)); - - let tag = createTag(); - dirtyTag(tag); - - assert.notOk(validateTag(CURRENT_TAG, 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/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..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'), @@ -291,7 +307,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 +326,13 @@ 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' + ), ...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..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, createUpdatableTag, dirtyTag } from '@glimmer/validator'; +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 = createUpdatableTag(); + 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 6d111af1ffa..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/validator'; - 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 15398187f46..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 { valueForTag, validateTag } from '@glimmer/validator'; + 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,10 +75,10 @@ export const stableDecoratorFiles = { @tracked count = 0; } let obj = new Example(); - let tag = tagForObject(obj); - let snapshot = valueForTag(tag); + let cache = countRuns(obj); + assert.strictEqual(getValue(cache), 1); obj.count = 1; - assert.false(validateTag(tag, snapshot)); + assert.strictEqual(getValue(cache), 2); }); test('setting a tracked accessor dirties the object tag', function(assert) { @@ -77,10 +86,10 @@ export const stableDecoratorFiles = { @tracked accessor count = 0; } let obj = new Example(); - let tag = tagForObject(obj); - let snapshot = valueForTag(tag); + let cache = countRuns(obj); + assert.strictEqual(getValue(cache), 1); obj.count = 1; - assert.false(validateTag(tag, snapshot)); + assert.strictEqual(getValue(cache), 2); }); }); `, diff --git a/tests/node-vitest/tree-shakability.test.js b/tests/node-vitest/tree-shakability.test.js index a26056a90ee..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/validator/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/validator/index.js", "@handlebars/parser/lib/parse.js", "@handlebars/parser/lib/printer.js", "@handlebars/parser/lib/whitespace-control.js",