import type { ReadonlySignal, Signal } from "@preact-signals/unified-signals"; import { mutableHandlers, readonlyHandlers, shallowReactiveHandlers, shallowReadonlyHandlers, } from "./baseHandlers"; import { mutableCollectionHandlers, readonlyCollectionHandlers, shallowCollectionHandlers, shallowReadonlyCollectionHandlers, } from "./collectionHandlers"; import { ReactiveFlags } from "./constants"; import type { UnwrapSignalSimple } from "./deepSignal"; import { def, isObject, toRawType } from "./utils"; // maps rawVersions <-> wrapped versions export const deepReactiveMap = new WeakMap(); export const shallowReactiveMap = new WeakMap(); export const deepReadonlyMap = new WeakMap(); export const shallowReadonlyMap = new WeakMap(); // only unwrap nested ref export type UnwrapNestedSignals = T extends Signal ? T : T extends ReadonlySignal ? T : UnwrapSignalSimple; const enum TargetType { INVALID = 0, COMMON = 1, COLLECTION = 2, } function targetTypeMap(rawType: string) { switch (rawType) { case "Object": case "Array": return TargetType.COMMON; case "Map": case "Set": case "WeakMap": case "WeakSet": return TargetType.COLLECTION; default: return TargetType.INVALID; } } function getTargetType(value: Target) { return value[ReactiveFlags.SKIP] || !Object.isExtensible(value) ? TargetType.INVALID : targetTypeMap(toRawType(value)); } export interface Target { [ReactiveFlags.RAW]?: any; [ReactiveFlags.IS_REACTIVE]?: boolean; [ReactiveFlags.IS_READONLY]?: boolean; [ReactiveFlags.IS_SHALLOW]?: boolean; [ReactiveFlags.SKIP]?: boolean; } /** * Returns the raw, original object of a Vue-created proxy. * * `toRaw()` can return the original object from proxies created by * {@link deepReactive()}, {@link deepReadonly()}, {@link shallowReactive()} or * {@link shallowReadonly()}. * * This is an escape hatch that can be used to temporarily read without * incurring proxy access / tracking overhead or write without triggering * changes. It is **not** recommended to hold a persistent reference to the * original object. Use with caution. * * @example * ```js * const foo = {} * const reactiveFoo = reactive(foo) * * console.log(toRaw(reactiveFoo) === foo) // true * ``` * * @param observed - The object for which the "raw" value is requested. */ export function toRaw(observed: T): T { const raw = observed && (observed as Target)[ReactiveFlags.RAW]; return raw ? toRaw(raw) : observed; } /** * * Returns a reactive proxy of the object. * * The reactive conversion is "deep": it affects all nested properties. A * reactive object also deeply unwraps any properties that are refs while * maintaining reactivity. * * @example * ```js * const obj = reactive({ count: 0 }) * ``` * * @param target - The object to be made reactive. * @returns */ export const deepReactive = ( target: T, ): UnwrapNestedSignals => { if (target && (target as Target)[ReactiveFlags.RAW]) { return target as UnwrapNestedSignals; } return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers, deepReactiveMap, ); }; export declare const ShallowReactiveMarker: unique symbol; export type ShallowReactive = T & { [ShallowReactiveMarker]?: true }; /** * Shallow version of {@link deepReactive()}. * * Unlike {@link deepReactive()}, there is no deep conversion: only root-level * properties are reactive for a shallow reactive object. Property values are * stored and exposed as-is - this also means properties with ref values will * not be automatically unwrapped. * * @example * ```js * const state = shallowReactive({ * foo: 1, * nested: { * bar: 2 * } * }) * * // mutating state's own properties is reactive * state.foo++ * * // ...but does not convert nested objects * isReactive(state.nested) // false * * // NOT reactive * state.nested.bar++ * ``` * * @param target - The source object. */ export function shallowReactive( target: T, ): ShallowReactive { return createReactiveObject( target, false, shallowReactiveHandlers, shallowCollectionHandlers, shallowReactiveMap, ); } type Primitive = string | number | boolean | bigint | symbol | undefined | null; type Builtin = Primitive | Function | Date | Error | RegExp; export type DeepReadonly = T extends Builtin ? T : T extends Map ? ReadonlyMap, DeepReadonly> : T extends ReadonlyMap ? ReadonlyMap, DeepReadonly> : T extends WeakMap ? WeakMap, DeepReadonly> : T extends Set ? ReadonlySet> : T extends ReadonlySet ? ReadonlySet> : T extends WeakSet ? WeakSet> : T extends Promise ? Promise> : T extends Signal ? Readonly>> : T extends {} ? { readonly [K in keyof T]: DeepReadonly } : Readonly; /** * takes an object (reactive or plain) or a ref and returns a readonly proxy to * the original. * * a readonly proxy is deep: any nested property accessed will be readonly as * well. it also has the same ref-unwrapping behavior as {@link deepreactive()}, * except the unwrapped values will also be made readonly. * * @example * ```js * const original = reactive({ count: 0 }) * * const copy = readonly(original) * * watcheffect(() => { * // works for reactivity tracking * console.log(copy.count) * }) * * // mutating original will trigger watchers relying on the copy * original.count++ * * // mutating the copy will fail and result in a warning * copy.count++ // warning! * ``` * * @param target - The source object. */ export function deepReadonly( target: T, ): DeepReadonly> { return createReactiveObject( target, true, readonlyHandlers, readonlyCollectionHandlers, deepReadonlyMap, ); } /** * Shallow version of {@link deepReadonly()}. * * Unlike {@link deepReadonly()}, there is no deep conversion: only root-level * properties are made readonly. Property values are stored and exposed as-is - * this also means properties with ref values will not be automatically * unwrapped. * * @example * ```js * const state = shallowReadonly({ * foo: 1, * nested: { * bar: 2 * } * }) * * // mutating state's own properties will fail * state.foo++ * * // ...but works on nested objects * isReadonly(state.nested) // false * * // works * state.nested.bar++ * ``` * * @param target - The source object. */ export function shallowReadonly(target: T): Readonly { return createReactiveObject( target, true, shallowReadonlyHandlers, shallowReadonlyCollectionHandlers, shallowReadonlyMap, ); } function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler, collectionHandlers: ProxyHandler, proxyMap: WeakMap, ) { if (!isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`); } return target; } // target is already a Proxy, return it. // exception: calling readonly() on a reactive object if ( target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) { return target; } // target already has corresponding Proxy const existingProxy = proxyMap.get(target); if (existingProxy) { return existingProxy; } // only specific value types can be observed. const targetType = getTargetType(target); if (targetType === TargetType.INVALID) { return target; } const proxy = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers, ); proxyMap.set(target, proxy); return proxy; } /** * Checks if an object is a proxy created by {@link deepReactive()} or * {@link shallowReactive()} (or {@link ref()} in some cases). * * @example * ```js * isReactive(reactive({})) // => true * isReactive(readonly(reactive({}))) // => true * isReactive(ref({}).value) // => true * isReactive(readonly(ref({})).value) // => true * isReactive(ref(true)) // => false * isReactive(shallowRef({}).value) // => false * isReactive(shallowReactive({})) // => true * ``` * * @param value - The value to check. */ export function isReactive(value: unknown): boolean { if (isReadonly(value)) { return isReactive((value as Target)[ReactiveFlags.RAW]); } return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE]); } /** * Checks whether the passed value is a readonly object. The properties of a * readonly object can change, but they can't be assigned directly via the * passed object. * * The proxies created by {@link deepReadonly()} and {@link shallowReadonly()} are * both considered readonly, as is a computed ref without a set function. * * @param value - The value to check. */ export function isReadonly(value: unknown): boolean { return !!(value && (value as Target)[ReactiveFlags.IS_READONLY]); } export function isShallow(value: unknown): boolean { return !!(value && (value as Target)[ReactiveFlags.IS_SHALLOW]); } /** * Checks if an object is a proxy created by {@link deepReactive}, * {@link deepReadonly}, {@link shallowReactive} or {@link shallowReadonly()}. * * @param value - The value to check. */ export function isProxy(value: unknown): boolean { return isReactive(value) || isReadonly(value); } export declare const RawSymbol: unique symbol; export type Raw = T & { [RawSymbol]?: true }; /** * Marks an object so that it will never be converted to a proxy. Returns the * object itself. * * @example * ```js * const foo = markRaw({}) * console.log(isReactive(reactive(foo))) // false * * // also works when nested inside other reactive objects * const bar = reactive({ foo }) * console.log(isReactive(bar.foo)) // false * ``` * * **Warning:** `markRaw()` together with the shallow APIs such as * {@link shallowReactive()} allow you to selectively opt-out of the default * deep reactive/readonly conversion and embed raw, non-proxied objects in your * state graph. * * @param value - The object to be marked as "raw". */ export function markRaw(value: T): Raw { def(value, ReactiveFlags.SKIP, true); return value; } /** * Returns a reactive proxy of the given value (if possible). * * If the given value is not an object, the original value itself is returned. * * @param value - The value for which a reactive proxy shall be created. */ export const toDeepReactive = (value: T): T => isObject(value) ? deepReactive(value) : value; /** * Returns a readonly proxy of the given value (if possible). * * If the given value is not an object, the original value itself is returned. * * @param value - The value for which a readonly proxy shall be created. */ export const toDeepReadonly = (value: T) => (isObject(value) ? deepReadonly(value) : value) as T extends object ? DeepReadonly> : T;