UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

1,028 lines (946 loc) 31.8 kB
import { scheduleRevalidate } from '../global-context/index.js'; /* eslint-disable @typescript-eslint/no-non-null-assertion -- @fixme */ const debug = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any function unwrap(val) { if (val === null || val === undefined) throw new Error(`Expected value to be present`); return val; } ////////// const CONSTANT = 0; const INITIAL = 1; const VOLATILE = NaN; let $REVISION = INITIAL; function bump() { $REVISION++; } ////////// const DIRYTABLE_TAG_ID = 0; const UPDATABLE_TAG_ID = 1; const COMBINATOR_TAG_ID = 2; const CONSTANT_TAG_ID = 3; ////////// const COMPUTE = Symbol('TAG_COMPUTE'); Reflect.set(globalThis, 'COMPUTE_SYMBOL', COMPUTE); ////////// /** * `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 */ function valueForTag(tag) { 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 */ function validateTag(tag, snapshot) { return snapshot >= tag[COMPUTE](); } ////////// const TYPE = Symbol('TAG_TYPE'); // this is basically a const let ALLOW_CYCLES; class MonomorphicTagImpl { static combine(tags) { switch (tags.length) { case 0: return CONSTANT_TAG; case 1: return tags[0]; default: { let tag = new MonomorphicTagImpl(COMBINATOR_TAG_ID); tag.subtag = tags; return tag; } } } revision = INITIAL; lastChecked = INITIAL; lastValue = INITIAL; isUpdating = false; subtag = null; subtagBufferCache = null; constructor(type) { this[TYPE] = type; } [COMPUTE]() { let { lastChecked } = this; if (this.isUpdating) { 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 (const tag of subtag) { let value = tag[COMPUTE](); revision = Math.max(value, 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(_tag, _subtag) { // TODO: TS 3.7 should allow us to do this via assertion let tag = _tag; let subtag = _subtag; 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(tag, disableConsumptionAssertion) { tag.revision = ++$REVISION; scheduleRevalidate(); } } const DIRTY_TAG = MonomorphicTagImpl.dirtyTag; const UPDATE_TAG = MonomorphicTagImpl.updateTag; ////////// function createTag() { return new MonomorphicTagImpl(DIRYTABLE_TAG_ID); } function createUpdatableTag() { return new MonomorphicTagImpl(UPDATABLE_TAG_ID); } ////////// const CONSTANT_TAG = new MonomorphicTagImpl(CONSTANT_TAG_ID); function isConstTag(tag) { return tag === CONSTANT_TAG; } ////////// const VOLATILE_TAG_ID = 100; class VolatileTag { [TYPE] = VOLATILE_TAG_ID; [COMPUTE]() { return VOLATILE; } } const VOLATILE_TAG = new VolatileTag(); ////////// const CURRENT_TAG_ID = 101; class CurrentTag { [TYPE] = CURRENT_TAG_ID; [COMPUTE]() { return $REVISION; } } const CURRENT_TAG = new CurrentTag(); ////////// 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); /** * An object that that tracks @tracked properties that were consumed. */ class Tracker { tags = new Set(); last = null; add(tag) { if (tag === CONSTANT_TAG) return; this.tags.add(tag); this.last = tag; } combine() { let { tags } = this; if (tags.size === 0) { return CONSTANT_TAG; } else if (tags.size === 1) { return this.last; } else { return combine(Array.from(this.tags)); } } } /** * 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 = null; const OPEN_TRACK_FRAMES = []; function beginTrackFrame(debuggingContext) { OPEN_TRACK_FRAMES.push(CURRENT_TRACKER); CURRENT_TRACKER = new Tracker(); } function endTrackFrame() { let current = CURRENT_TRACKER; CURRENT_TRACKER = OPEN_TRACK_FRAMES.pop() || null; return unwrap(current).combine(); } function beginUntrackFrame() { OPEN_TRACK_FRAMES.push(CURRENT_TRACKER); CURRENT_TRACKER = null; } function endUntrackFrame() { CURRENT_TRACKER = OPEN_TRACK_FRAMES.pop() || null; } // This function is only for handling errors and resetting to a valid state function resetTracking() { while (OPEN_TRACK_FRAMES.length > 0) { OPEN_TRACK_FRAMES.pop(); } CURRENT_TRACKER = null; } function isTracking() { return CURRENT_TRACKER !== null; } function consumeTag(tag) { if (CURRENT_TRACKER !== null) { CURRENT_TRACKER.add(tag); } } // public interface const FN = Symbol('FN'); const LAST_VALUE = Symbol('LAST_VALUE'); const TAG = Symbol('TAG'); const SNAPSHOT = Symbol('SNAPSHOT'); function createCache(fn, debuggingLabel) { let cache = { [FN]: fn, [LAST_VALUE]: undefined, [TAG]: undefined, [SNAPSHOT]: -1 }; return cache; } function getValue(cache) { 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]; } function isConst(cache) { let tag = cache[TAG]; return isConstTag(tag); } ////////// // 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. function track(block, debugLabel) { beginTrackFrame(); 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. function untrack(callback) { beginUntrackFrame(); try { return callback(); } finally { endUntrackFrame(); } } /* eslint-disable @typescript-eslint/no-explicit-any */ // Unfortunately, TypeScript's ability to do inference *or* type-checking in a // `Proxy`'s body is very limited, so we have to use a number of casts `as any` // to make the internal accesses work. The type safety of these is guaranteed at // the *call site* instead of within the body: you cannot do `Array.blah` in TS, // 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. const ARRAY_GETTER_METHODS = new Set([Symbol.iterator, 'concat', 'entries', 'every', 'filter', 'find', 'findIndex', 'flat', 'flatMap', 'forEach', 'includes', 'indexOf', 'join', 'keys', 'lastIndexOf', 'map', 'reduce', 'reduceRight', 'slice', 'some', 'values']); // For these methods, `Array` itself immediately gets the `.length` to return // after invoking them. const ARRAY_WRITE_THEN_READ_METHODS = new Set(['fill', 'push', 'unshift']); function convertToInt(prop) { if (typeof prop === 'symbol') return null; const num = Number(prop); if (isNaN(num)) return null; return num % 1 === 0 ? num : null; } // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging class TrackedArray { #options; constructor(arr, options) { this.#options = options; const clone = arr.slice(); // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; const boundFns = new Map(); /** Flag to track whether we have *just* intercepted a call to `.push()` or `.unshift()`, since in those cases (and only those cases!) the `Array` itself checks `.length` to return from the function call. */ let nativelyAccessingLengthFromWriteMethod = false; return new Proxy(clone, { get(target, prop /*, _receiver */) { const index = convertToInt(prop); if (index !== null) { self.#readStorageFor(index); consumeTag(self.#collection); return target[index]; } if (prop === 'length') { // If we are reading `.length`, it may be a normal user-triggered // read, or it may be a read triggered by Array itself. In the latter // case, it is because we have just done `.push()` or `.unshift()`; in // that case it is safe not to mark this as a *read* operation, since // calling `.push()` or `.unshift()` cannot otherwise be part of a // "read" operation safely, and if done during an *existing* read // (e.g. if the user has already checked `.length` *prior* to this), // that will still trigger the mutation-after-consumption assertion. if (nativelyAccessingLengthFromWriteMethod) { nativelyAccessingLengthFromWriteMethod = false; } else { consumeTag(self.#collection); } return target[prop]; } // Here, track that we are doing a `.push()` or `.unshift()` by setting // the flag to `true` so that when the `.length` is read by `Array` (see // immediately above), it knows not to dirty the collection. if (ARRAY_WRITE_THEN_READ_METHODS.has(prop)) { nativelyAccessingLengthFromWriteMethod = true; } if (ARRAY_GETTER_METHODS.has(prop)) { let fn = boundFns.get(prop); if (fn === undefined) { fn = (...args) => { consumeTag(self.#collection); // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access return target[prop](...args); }; boundFns.set(prop, fn); } return fn; } // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access return target[prop]; }, set(target, prop, value /*, _receiver */) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument let isUnchanged = self.#options.equals(target[prop], value); if (isUnchanged) return true; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access target[prop] = value; const index = convertToInt(prop); if (index !== null) { self.#dirtyStorageFor(index); self.#dirtyCollection(); } else if (prop === 'length') { self.#dirtyCollection(); } return true; }, getPrototypeOf() { return TrackedArray.prototype; } }); } #collection = createUpdatableTag(); #storages = new Map(); #readStorageFor(index) { let storage = this.#storages.get(index); if (storage === undefined) { storage = createUpdatableTag(); this.#storages.set(index, storage); } consumeTag(storage); } #dirtyStorageFor(index) { const storage = this.#storages.get(index); if (storage) { DIRTY_TAG(storage); } } #dirtyCollection() { DIRTY_TAG(this.#collection); this.#storages.clear(); } } // This rule is correct in the general case, but it doesn't understand // declaration merging, which is how we're using the interface here. This says // `TrackedArray` acts just like `Array<T>`, but also has the properties // declared via the `class` declaration above -- but without the cost of a // subclass, which is much slower that the proxied array behavior. That is: a // `TrackedArray` *is* an `Array`, just with a proxy in front of accessors and // setters, rather than a subclass of an `Array` which would be de-optimized by // the browsers. // // eslint-disable-next-line @typescript-eslint/no-empty-object-type // Ensure instanceof works correctly Object.setPrototypeOf(TrackedArray.prototype, Array.prototype); function trackedArray(data, options) { return new TrackedArray(data ?? [], { equals: options?.equals ?? Object.is, description: options?.description }); } /* eslint-disable @typescript-eslint/no-explicit-any */ // Using a Proxy-based approach so that any new methods added to the Map // interface (like getOrInsert, getOrInsertComputed, etc.) are automatically // supported without needing to manually re-implement each one. function trackedMap(data, options) { const equals = options?.equals ?? Object.is; // TypeScript doesn't correctly resolve the overloads for calling the `Map` // constructor for the no-value constructor. This resolves that. const target = data instanceof Map ? new Map(data.entries()) : new Map(data ?? []); const collection = createUpdatableTag(); const storages = new Map(); function storageFor(key) { let storage = storages.get(key); if (storage === undefined) { storage = createUpdatableTag(); storages.set(key, storage); } return storage; } function dirtyStorageFor(key) { const storage = storages.get(key); if (storage) { DIRTY_TAG(storage); } } const proxy = new Proxy(target, { get(target, prop, receiver) { if (prop === 'set') { return function (key, value) { const hasExisting = target.has(key); if (hasExisting) { const isUnchanged = equals(target.get(key), value); if (isUnchanged) return proxy; } dirtyStorageFor(key); DIRTY_TAG(collection); target.set(key, value); return proxy; }; } if (prop === 'delete') { return function (key) { if (!target.has(key)) return false; dirtyStorageFor(key); DIRTY_TAG(collection); storages.delete(key); return target.delete(key); }; } if (prop === 'clear') { return function () { if (target.size === 0) return; storages.forEach(s => DIRTY_TAG(s)); storages.clear(); DIRTY_TAG(collection); target.clear(); }; } if (prop === 'get') { return function (key) { consumeTag(storageFor(key)); return target.get(key); }; } if (prop === 'has') { return function (key) { consumeTag(storageFor(key)); return target.has(key); }; } if (prop === 'size') { consumeTag(collection); return target.size; } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const value = Reflect.get(target, prop, receiver); if (typeof value === 'function') { return function (...args) { consumeTag(collection); // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call return value.apply(target, args); }; } // eslint-disable-next-line @typescript-eslint/no-unsafe-return return value; } }); return proxy; } class TrackedObject { #options; #storages = new Map(); #collection = createUpdatableTag(); #readStorageFor(key) { let storage = this.#storages.get(key); if (storage === undefined) { storage = createUpdatableTag(); this.#storages.set(key, storage); } consumeTag(storage); } #dirtyStorageFor(key) { const storage = this.#storages.get(key); if (storage) { DIRTY_TAG(storage); } } #dirtyCollection() { DIRTY_TAG(this.#collection); } /** * This implementation of trackedObject is far too dynamic for TS to be happy with */ constructor(obj, options) { this.#options = options; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const proto = Object.getPrototypeOf(obj); const descs = Object.getOwnPropertyDescriptors(obj); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const clone = Object.create(proto); for (const prop in descs) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion Object.defineProperty(clone, prop, descs[prop]); } // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; return new Proxy(clone, { get(target, prop) { self.#readStorageFor(prop); return target[prop]; }, has(target, prop) { self.#readStorageFor(prop); return prop in target; }, ownKeys(target) { consumeTag(self.#collection); return Reflect.ownKeys(target); }, set(target, prop, value) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument let isUnchanged = self.#options.equals(target[prop], value); if (isUnchanged) { return true; } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment target[prop] = value; self.#dirtyStorageFor(prop); self.#dirtyCollection(); return true; }, deleteProperty(target, prop) { if (prop in target) { delete target[prop]; self.#dirtyStorageFor(prop); self.#storages.delete(prop); self.#dirtyCollection(); } return true; }, getPrototypeOf() { return TrackedObject.prototype; } }); } } function trackedObject(data, options) { return new TrackedObject(data ?? {}, { equals: options?.equals ?? Object.is, description: options?.description /** * SAFETY: we are trying to mimic the same behavior as a plain object, so if anything about * the object that is returned behaves differently from a native object in a surprising * way, we should fix that and make the behavior match native objects. */ }); } /* eslint-disable @typescript-eslint/no-explicit-any */ // Using a Proxy-based approach so that any new methods added to the Set // interface are automatically supported without needing to manually // re-implement each one. function trackedSet(data, options) { const equals = options?.equals ?? Object.is; const target = new Set(data ?? []); const collection = createUpdatableTag(); const storages = new Map(); function storageFor(key) { let storage = storages.get(key); if (storage === undefined) { storage = createUpdatableTag(); storages.set(key, storage); } return storage; } function dirtyStorageFor(key) { const storage = storages.get(key); if (storage) { DIRTY_TAG(storage); } } const proxy = new Proxy(target, { get(target, prop, receiver) { if (prop === 'add') { return function (value) { if (target.has(value)) { const isUnchanged = equals(value, value); if (isUnchanged) return proxy; } else { DIRTY_TAG(collection); } dirtyStorageFor(value); target.add(value); return proxy; }; } if (prop === 'delete') { return function (value) { if (!target.has(value)) return false; dirtyStorageFor(value); DIRTY_TAG(collection); storages.delete(value); return target.delete(value); }; } if (prop === 'clear') { return function () { if (target.size === 0) return; storages.forEach(s => DIRTY_TAG(s)); DIRTY_TAG(collection); storages.clear(); target.clear(); }; } if (prop === 'has') { return function (value) { consumeTag(storageFor(value)); return target.has(value); }; } if (prop === 'size') { consumeTag(collection); return target.size; } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const value = Reflect.get(target, prop, receiver); if (typeof value === 'function') { return function (...args) { consumeTag(collection); // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call return value.apply(target, args); }; } // eslint-disable-next-line @typescript-eslint/no-unsafe-return return value; } }); return proxy; } // Using a Proxy-based approach so that any new methods added to the WeakMap // interface (like getOrInsert, getOrInsertComputed, etc.) are automatically // supported without needing to manually re-implement each one. function trackedWeakMap(data, options) { const equals = options?.equals ?? Object.is; const existing = data ?? []; /** * SAFETY: note that when passing in an existing weak map, we can't * clone it as it is not iterable and not a supported type of structuredClone */ const target = existing instanceof WeakMap ? existing : new WeakMap(existing); const storages = new WeakMap(); function storageFor(key) { let storage = storages.get(key); if (storage === undefined) { storage = createUpdatableTag(); storages.set(key, storage); } return storage; } function dirtyStorageFor(key) { const storage = storages.get(key); if (storage) { DIRTY_TAG(storage); } } const proxy = new Proxy(target, { get(target, prop, receiver) { if (prop === 'set') { return function (key, value) { const hasExisting = target.has(key); if (hasExisting) { const isUnchanged = equals(target.get(key), value); if (isUnchanged) return proxy; } dirtyStorageFor(key); target.set(key, value); return proxy; }; } if (prop === 'delete') { return function (key) { if (!target.has(key)) return false; dirtyStorageFor(key); storages.delete(key); return target.delete(key); }; } if (prop === 'get') { return function (key) { consumeTag(storageFor(key)); return target.get(key); }; } if (prop === 'has') { return function (key) { consumeTag(storageFor(key)); return target.has(key); }; } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const value = Reflect.get(target, prop, receiver); if (typeof value === 'function') { // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call return value.bind(target); } // eslint-disable-next-line @typescript-eslint/no-unsafe-return return value; } }); return proxy; } // Using a Proxy-based approach so that any new methods added to the WeakSet // interface are automatically supported without needing to manually // re-implement each one. /** * NOTE: we cannot pass a WeakSet because WeakSets are not iterable */ /** * Creates an instanceof WeakSet from an optional list of entries * */ function trackedWeakSet(data, options) { const equals = options?.equals ?? Object.is; const target = new WeakSet(data ?? []); const storages = new WeakMap(); function storageFor(key) { let storage = storages.get(key); if (storage === undefined) { storage = createUpdatableTag(); storages.set(key, storage); } return storage; } function dirtyStorageFor(key) { const storage = storages.get(key); if (storage) { DIRTY_TAG(storage); } } const proxy = new Proxy(target, { get(target, prop, receiver) { if (prop === 'add') { return function (value) { /** * In a WeakSet, there is no `.get()`, but if there was, * we could assume it's the same value as what we passed. * * So for a WeakSet, if we try to add something that already exists * we no-op. * * WeakSet already does this internally for us, * but we want the ability for the reactive behavior to reflect the same behavior. * * i.e.: doing weakSet.add(value) should never dirty with the defaults * if the `value` is already in the weakSet */ if (target.has(value)) { /** * This looks a little silly, where a always will === b, * but see the note above. */ const isUnchanged = equals(value, value); if (isUnchanged) return proxy; } // Add to vals first to get better error message target.add(value); dirtyStorageFor(value); return proxy; }; } if (prop === 'delete') { return function (value) { if (!target.has(value)) return false; dirtyStorageFor(value); storages.delete(value); return target.delete(value); }; } if (prop === 'has') { return function (value) { consumeTag(storageFor(value)); return target.has(value); }; } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const value = Reflect.get(target, prop, receiver); if (typeof value === 'function') { // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call return value.bind(target); } // eslint-disable-next-line @typescript-eslint/no-unsafe-return return value; } }); return proxy; } /////////// const TRACKED_TAGS = new WeakMap(); function dirtyTagFor(obj, key, meta) { let tags = meta === undefined ? TRACKED_TAGS.get(obj) : meta; // No tags have been setup for this object yet, return if (tags === undefined) return; // Dirty the tag for the specific property if it exists let propertyTag = tags.get(key); if (propertyTag !== undefined) { DIRTY_TAG(propertyTag, true); } } function tagMetaFor(obj) { let tags = TRACKED_TAGS.get(obj); if (tags === undefined) { tags = new Map(); TRACKED_TAGS.set(obj, tags); } return tags; } function tagFor(obj, key, meta) { let tags = meta === undefined ? tagMetaFor(obj) : meta; let tag = tags.get(key); if (tag === undefined) { tag = createUpdatableTag(); tags.set(key, tag); } return tag; } function trackedData(key, initializer) { let values = new WeakMap(); let hasInitializer = typeof initializer === 'function'; function getter(self) { consumeTag(tagFor(self, key)); let value; // If the field has never been initialized, we should initialize it if (hasInitializer && !values.has(self)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme value = initializer.call(self); values.set(self, value); } else { value = values.get(self); } return value; } function setter(self, value) { dirtyTagFor(self, key); values.set(self, value); } return { getter, setter }; } 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 { ALLOW_CYCLES, COMPUTE, CONSTANT, CONSTANT_TAG, CURRENT_TAG, CurrentTag, INITIAL, VOLATILE, VOLATILE_TAG, VolatileTag, beginTrackFrame, beginUntrackFrame, bump, combine, consumeTag, createCache, createTag, createUpdatableTag, debug, DIRTY_TAG as dirtyTag, dirtyTagFor, endTrackFrame, endUntrackFrame, getValue, isConst, isConstTag, isTracking, resetTracking, tagFor, tagMetaFor, track, trackedArray, trackedData, trackedMap, trackedObject, trackedSet, trackedWeakMap, trackedWeakSet, untrack, UPDATE_TAG as updateTag, validateTag, valueForTag };