UNPKG

@v4fire/core

Version:
544 lines (426 loc) 11.2 kB
/*! * V4Fire Core * https://github.com/V4Fire/Core * * Released under the MIT license * https://github.com/V4Fire/Core/blob/master/LICENSE */ import { muteLabel, blackList, toProxyObject, toRootObject, toTopObject, toOriginalObject, watchPath, watchOptions, watchHandlers } from 'core/object/watch/const'; import { bindMutationHooks } from 'core/object/watch/wrap'; import { isValueCanBeArrayIndex } from 'core/object/watch/helpers'; import { unwrap, getProxyType, getProxyValue, getOrCreateLabelValueByHandlers } from 'core/object/watch/engines/helpers'; import type { Watcher, WatchPath, RawWatchHandler, WatchHandlersSet, WatchOptions, InternalWatchOptions } from 'core/object/watch/interface'; /** * Watches for changes of the specified object by using Proxy objects * * @param obj * @param path - base path to object properties: it is provided to a watch handler with parameters * @param handler - callback that is invoked on every mutation hook * @param handlers - set of registered handlers * @param [opts] - additional options */ export function watch<T extends object>( obj: T, path: CanUndef<unknown[]>, handler: Nullable<RawWatchHandler>, handlers: WatchHandlersSet, opts?: WatchOptions ): Watcher<T>; /** * Watches for changes of the specified object by using Proxy objects * * @param obj * @param path - base path to object properties: it is provided to a watch handler with parameters * @param handler - callback that is invoked on every mutation hook * @param handlers - set of registered handlers * @param opts - additional options * @param root - link to the root object of watching * @param top - link to the top object of watching */ export function watch<T extends object>( obj: T, path: CanUndef<unknown[]>, handler: Nullable<RawWatchHandler>, handlers: WatchHandlersSet, opts: CanUndef<InternalWatchOptions>, root: object, top: object ): T; export function watch<T extends object>( obj: T, path: CanUndef<unknown[]>, handler: Nullable<RawWatchHandler>, handlers: WatchHandlersSet, opts?: InternalWatchOptions, root?: object, top?: object ): Watcher<T> | T { opts ??= {}; const unwrappedObj = unwrap(obj), resolvedRoot = root ?? unwrappedObj; const returnProxy = (obj, proxy?) => { if (proxy != null && handler != null && (!top || !handlers.has(handler))) { handlers.add(handler); } if (top) { return proxy ?? obj; } return { proxy: proxy ?? obj, set: (path, value) => { set(obj, path, value, handlers); }, delete: (path) => { unset(obj, path, handlers); }, unwatch: () => { if (handler != null) { handlers.delete(handler); } } }; }; if (unwrappedObj == null || resolvedRoot == null) { return returnProxy(obj); } if (!top) { const tmpOpts = getOrCreateLabelValueByHandlers<InternalWatchOptions>( unwrappedObj, watchOptions, handlers, {...opts} ); if (opts.deep) { tmpOpts.deep = true; } if (opts.withProto) { tmpOpts.withProto = true; } opts = tmpOpts; } let proxy = getOrCreateLabelValueByHandlers(unwrappedObj, toProxyObject, handlers); if (proxy != null) { return returnProxy(unwrappedObj, proxy); } if (getProxyType(unwrappedObj) == null) { return returnProxy(unwrappedObj); } const fromProto = Boolean(opts.fromProto), resolvedPath = path ?? []; if (!Object.isDictionary(unwrappedObj)) { const wrapOpts = { root: resolvedRoot, top, path: resolvedPath, originalPath: resolvedPath, fromProto, watchOpts: opts }; bindMutationHooks(unwrappedObj, wrapOpts, handlers); } const frozenKeys = Object.createDict<Dictionary<boolean>>({ [toRootObject]: true, [toTopObject]: true, [toOriginalObject]: true, [watchHandlers]: true, [watchPath]: true }); const blackListStore = new Set(); let lastSetKey; proxy = new Proxy(unwrappedObj, { get: (target, key) => { switch (key) { case toRootObject: return resolvedRoot; case toTopObject: return top; case toOriginalObject: return target; case watchHandlers: return handlers; case watchPath: return path; default: // Do nothing } const val = target[key]; if (Object.isPrimitive(val) || resolvedRoot[muteLabel] === true) { return val; } const isArray = Object.isArray(target), isCustomObject = isArray || Object.isCustomObject(target); if (isArray && !Reflect.has(target, Symbol.isConcatSpreadable)) { Object.defineSymbol(target, Symbol.isConcatSpreadable, true); } if (Object.isSymbol(key) || blackListStore.has(key)) { if (isCustomObject) { return val; } } else if (isCustomObject) { let propFromProto = fromProto, normalizedKey; if (isArray && isValueCanBeArrayIndex(key)) { normalizedKey = Number(key); } else { normalizedKey = key; const desc = Reflect.getOwnPropertyDescriptor(target, key); // Readonly non-configurable values can't be wrapped due Proxy API limitations if (desc?.writable === false && desc.configurable === false) { return val; } } if (propFromProto || !isArray && !Object.hasOwnProperty(target, key)) { propFromProto = true; } const watchOpts = Object.assign(Object.create(opts!), {fromProto: propFromProto}); return getProxyValue(val, normalizedKey, path, handlers, resolvedRoot, top, watchOpts); } return Object.isFunction(val) ? val.bind(target) : val; }, set: (target, key, val, receiver) => { if (frozenKeys[key]) { return false; } lastSetKey = key; val = unwrap(val) ?? val; const isArray = Object.isArray(target), isCustomObj = isArray || Object.isCustomObject(target), set = () => Reflect.set(target, key, val, isCustomObj ? receiver : target); const canSetWithoutEmit = Object.isSymbol(key) || resolvedRoot[muteLabel] === true || blackListStore.has(key); if (canSetWithoutEmit) { return set(); } let normalizedKey; if (isArray && isValueCanBeArrayIndex(key)) { normalizedKey = Number(key); } else { normalizedKey = key; } let oldVal = Reflect.get(target, normalizedKey, isCustomObj ? receiver : target); oldVal = unwrap(oldVal) ?? oldVal; if (oldVal !== val && set()) { if (!opts!.withProto && (fromProto || !isArray && !Object.hasOwnProperty(target, key))) { return true; } handlers.forEach((handler) => { const path = resolvedPath.concat(normalizedKey); handler(val, oldVal, { obj: unwrappedObj, root: resolvedRoot, top, fromProto, path }); }); } return true; }, defineProperty: (target: object, key, desc) => { if (frozenKeys[key]) { return false; } const define = (desc) => Reflect.defineProperty(target, key, desc); if (lastSetKey === key) { lastSetKey = undefined; return define(desc); } const canDefineWithoutEmit = Object.isSymbol(key) || resolvedRoot[muteLabel] === true || blackListStore.has(key); if (canDefineWithoutEmit) { return define(desc); } const { configurable, writable } = desc; const mergedDesc = {...desc}; let valToDefine; const needRedefineValue = desc.get == null && desc.set == null && 'value' in desc && desc.value !== Reflect.get(target, key, proxy); if (needRedefineValue) { valToDefine = desc.value; mergedDesc.value = undefined; mergedDesc.configurable = true; mergedDesc.writable = true; } const res = define(mergedDesc); if (res) { if (valToDefine !== undefined) { Object.cast<Dictionary>(proxy)[key] = valToDefine; define({configurable, writable}); } return true; } return false; }, deleteProperty: (target, key) => { if (frozenKeys[key]) { return false; } if (Reflect.deleteProperty(target, key)) { if (resolvedRoot[muteLabel] === true) { return true; } if (Object.isDictionary(target) || Object.isMap(target) || Object.isWeakMap(target)) { blackListStore.add(key); } return true; } return false; }, has: (target, key) => { if (frozenKeys[key]) { return true; } if (blackListStore.has(key)) { return false; } return Reflect.has(target, key); } }); getOrCreateLabelValueByHandlers(unwrappedObj, blackList, handlers, blackListStore); getOrCreateLabelValueByHandlers(unwrappedObj, toProxyObject, handlers, proxy); return returnProxy(unwrappedObj, proxy); } /** * Sets a new watchable value for an object by the specified path * * @param obj * @param path * @param value * @param handlers - set of registered handlers */ export function set(obj: object, path: WatchPath, value: unknown, handlers: WatchHandlersSet): void { const unwrappedObj = unwrap(obj); if (!unwrappedObj) { return; } const normalizedPath = Object.isArray(path) ? path : path.split('.'); const prop = normalizedPath[normalizedPath.length - 1], refPath = normalizedPath.slice(0, -1); if (normalizedPath.length > 1 && Object.get(obj, refPath) == null) { unwrappedObj[muteLabel] = true; Object.set(obj, refPath, {}); unwrappedObj[muteLabel] = false; } const ref = Object.get( getOrCreateLabelValueByHandlers(unwrappedObj, toProxyObject, handlers) ?? unwrappedObj, refPath ); const blackListStore = getOrCreateLabelValueByHandlers<Set<unknown>>(unwrappedObj, blackList, handlers), type = getProxyType(ref); switch (type) { case 'set': throw new TypeError('Invalid data type to watch'); case 'array': (<unknown[]>ref).splice(Number(prop), 1, value); break; case 'map': blackListStore?.delete(prop); (<Map<unknown, unknown>>ref).set(prop, value); break; default: { const key = String(prop), store = <Dictionary>ref; blackListStore?.delete(key); store[key] = value; } } } /** * Deletes a watchable value for an object by the specified path * * @param obj * @param path * @param handlers - set of registered handlers */ export function unset(obj: object, path: WatchPath, handlers: WatchHandlersSet): void { const unwrappedObj = unwrap(obj); if (!unwrappedObj) { return; } const normalizedPath = Object.isArray(path) ? path : path.split('.'); const prop = normalizedPath[normalizedPath.length - 1], refPath = normalizedPath.slice(0, -1); const ref = Object.get( getOrCreateLabelValueByHandlers(unwrappedObj, toProxyObject, handlers) ?? unwrappedObj, refPath ); const blackListStore = getOrCreateLabelValueByHandlers<Set<unknown>>(unwrappedObj, blackList, handlers), type = getProxyType(ref); switch (type) { case null: return; case 'array': (<unknown[]>ref).splice(Number(prop), 1); break; case 'map': case 'set': blackListStore?.delete(prop); (<Map<unknown, unknown>>ref).delete(prop); break; default: { const key = String(prop), store = <Dictionary>ref; blackListStore?.delete(key); store[key] = undefined; delete store[key]; } } }