UNPKG

value-object-cache

Version:

A value object cache that can be used to make value objects behave like primitive types, i.e. if two variables `a` and `b` point to instances of the same class and have the same value, then `a === b`, otherwise `a !== b`.

81 lines (79 loc) 4.89 kB
type Value = Primitive | ValueArray | ValueObject; type Primitive = string | number | boolean | symbol | bigint | null | undefined; interface ValueArray extends ReadonlyArray<Value> { } declare const VALUE_OBJECT_BRAND: unique symbol; declare abstract class ValueObject<T extends object = object> { protected readonly props: Readonly<T>; readonly [VALUE_OBJECT_BRAND] = true; protected constructor(props: Readonly<T>, values: ValueArray); } type ReadonlyValue<T extends Value> = T extends readonly [infer U extends Value, ...(infer R extends readonly Value[])] ? R extends [...never[]] ? readonly [ReadonlyValue<U>] : readonly [ReadonlyValue<U>, ...ReadonlyValue<R>] : T extends readonly (infer U extends Value)[] ? readonly ReadonlyValue<U>[] : T; declare function isPrimitive(x: unknown): x is Primitive; declare function isValueArray(x: unknown): x is ValueArray; declare function isValueObject(x: unknown): x is ValueObject; declare function isValue(x: unknown): x is Value; /** A cache tree node used to store an object {@link WeakRef} and a {@link Map} of child nodes, both optional. */ interface CacheTreeNode { children: Map<unknown, CacheTreeNode> | null; instanceRef: WeakRef<object> | null; } /** * A value object cache that can be used to make value objects behave like primitive types, i.e. if two variables `a` * and `b` point to an instance of the same class and have the same value, then `a === b`, otherwise `a !== b`. * * To achieve this, the cache can be queried with three arguments: a class constructor, an array of values, and a * factory function. Values represent the "identity" of an instance: all calls to the cache with the same constructor * and instance parameters (according to the [same-value-zero equality]( * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#same-value-zero_equality)) * will return the same instance. If the cache already contains an instance of this class with the same values, then it * is returned. Otherwise, the provided factory is called to create a new instance, which is then stored in the cache * and returned - all the following calls to the cache with the same constructor and values will now return this * instance until it is garbage-collected. Because, as the cache only stores weak references to the instances and their * constructors, they can still be garbage-collected once they become unreachable. * * While value objects aren't usually expected to have the same identity when they're equal, making sure they do can * make life easier in situations where specifying a custom equality function isn't practical or even doable, such as * when using React hooks like {@link useCallback}, {@link useMemo}, {@link useEffect}, etc. * * // TODO update doc * * @see https://en.wikipedia.org/wiki/Value_object * * @example * ```ts * abstract class Dimension<Unit extends string> { * constructor( * readonly scalar: number, * readonly unit: Unit, * ) { * Object.freeze(this); * return valueObjectCache.getInstance(this.constructor, [scalar, unit], () => this); * } * } * * type LengthUnit = 'mm' | 'm' | 'km'; * class Length extends Dimension<LengthUnit> {} * class OtherLength extends Dimension<LengthUnit> {} * * console.log(new Length(1, 'm') === new Length(1, 'm')); // outputs 'true' * console.log(new Length(1, 'm') === new Length(2, 'm')); // outputs 'false' * console.log(new Length(1, 'm') === new OtherLength(1, 'm')); // outputs 'false' * ``` */ declare const valueObjectCache: { readonly "__#1@#rootNode": CacheTreeNode; readonly "__#1@#finalizationRegistry": FinalizationRegistry<readonly unknown[]>; "__#1@#get"<T extends object>(constructor: Function, values: ValueArray, factory: () => T): T; /** Look for an instance of the provided class constructor matching the provided values. If a matching instance is * found then it is returned, otherwise the factory function is called to create a new instance, which is then stored * in the cache and returned - all future calls to this method made with the same constructor and values will return * this instance until it is garbage-collected. */ getObject<const T extends object>(constructor: Function, values: ValueArray, factory: () => T): T; /** Look for an {@link Array} containing a specific list of values in the cache. If a matching {@link Array} is found * then it is returned, otherwise a new {@link Array} is stored in the cache and returned. All returned arrays are * frozen (readonly). */ getArray<const T extends ValueArray>(values: T): ReadonlyValue<T>; getValue<const T extends Value>(value: T): ReadonlyValue<T>; }; export { type Primitive, type ReadonlyValue, type Value, type ValueArray, ValueObject, isPrimitive, isValue, isValueArray, isValueObject, valueObjectCache };