UNPKG

@v4fire/core

Version:
524 lines (414 loc) 11 kB
/*! * V4Fire Core * https://github.com/V4Fire/Core * * Released under the MIT license * https://github.com/V4Fire/Core/blob/master/LICENSE */ import { muteLabel, toProxyObject, toRootObject, toTopObject, toOriginalObject, watchPath, watchOptions, watchHandlers } from 'core/object/watch/const'; import { bindMutationHooks } from 'core/object/watch/wrap'; 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 accessors * * @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 accessors * * @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(): void { 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<object>(unwrappedObj, toProxyObject, handlers); if (proxy) { return returnProxy(unwrappedObj, proxy); } if (getProxyType(unwrappedObj) == null) { return returnProxy(unwrappedObj); } const fromProto = Boolean(opts.fromProto), resolvedPath = path ?? []; const wrapOpts = { root: resolvedRoot, top, path: resolvedPath, originalPath: resolvedPath, fromProto, watchOpts: opts }; if (Object.isArray(unwrappedObj)) { bindMutationHooks(unwrappedObj, wrapOpts, handlers); const arrayProxy = getOrCreateLabelValueByHandlers<unknown[]>( unwrappedObj, toProxyObject, handlers, unwrappedObj ); proxy = arrayProxy; for (let i = 0; i < arrayProxy.length; i++) { arrayProxy[i] = getProxyValue(arrayProxy[i], i, path, handlers, resolvedRoot, top, opts); } } else if (Object.isDictionary(unwrappedObj)) { proxy = getOrCreateLabelValueByHandlers<object>( unwrappedObj, toProxyObject, handlers, () => Object.create(unwrappedObj) ); // eslint-disable-next-line guard-for-in for (const key in unwrappedObj) { let propFromProto: boolean | 1 = fromProto; if (!Object.hasOwnProperty(unwrappedObj, key)) { propFromProto = !propFromProto ? 1 : true; if (Object.isTruly(opts.fromProto) && !opts.withProto) { continue; } } const watchOpts = Object.assign(Object.create(opts), {fromProto: propFromProto}); setWatchAccessors(unwrappedObj, key, path, handlers, resolvedRoot, top, watchOpts); } } else { bindMutationHooks(unwrappedObj, wrapOpts, handlers); proxy = getOrCreateLabelValueByHandlers(unwrappedObj, toProxyObject, handlers, unwrappedObj); } Object.defineProperty(proxy, watchPath, { configurable: true, value: path }); Object.defineProperty(proxy, watchHandlers, { configurable: true, value: handlers }); Object.defineProperty(proxy, toRootObject, { configurable: true, value: resolvedRoot }); Object.defineProperty(proxy, toTopObject, { configurable: true, value: top }); Object.defineProperty(proxy, toOriginalObject, { configurable: true, value: unwrappedObj }); 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 == null) { return; } const normalizedPath = Object.isArray(path) ? path : path.split('.'), prop = normalizedPath[normalizedPath.length - 1]; const ctxPath = obj[watchPath] ?? [], refPath = Array.concat([], ctxPath.slice(1), normalizedPath.slice(0, -1)), fullRefPath = Array.concat([], ctxPath.slice(0, 1), refPath); if (normalizedPath.length > 1 && Object.get(unwrappedObj, refPath) == null) { Object.set(unwrappedObj, refPath, {}, { setter: (ref, key, val) => { if (ref == null || typeof ref !== 'object') { return; } ref[muteLabel] = true; set(ref, [key], val, handlers); ref[muteLabel] = false; } }); } const proxy = getOrCreateLabelValueByHandlers<object>(unwrappedObj, toProxyObject, handlers); const root = proxy?.[toTopObject] ?? unwrappedObj, top = proxy?.[toTopObject] ?? unwrappedObj; const ref = Object.get<object>(top, refPath); if (ref == null) { throw new TypeError('Invalid data type to watch'); } switch (getProxyType(ref)) { case 'set': throw new TypeError('Invalid data type to watch'); case 'array': (<unknown[]>ref).splice(Number(prop), 1, value); break; case 'map': (<Map<unknown, unknown>>ref).set(prop, value); break; default: { const key = String(prop), hasPath = fullRefPath.length > 0; const resolvedPath = hasPath ? fullRefPath : undefined, resolvedRoot = hasPath ? root : unwrappedObj, resolvedTop = hasPath ? top : undefined; const refProxy = ref[toProxyObject]?.get(handlers) ?? Object.createDict(); // eslint-disable-next-line @typescript-eslint/unbound-method if (!Object.isFunction(Object.getOwnPropertyDescriptor(refProxy, key)?.get)) { ref[key] = refProxy[key]; } const resolvedProxy = setWatchAccessors( ref, key, resolvedPath, handlers, resolvedRoot, resolvedTop, {deep: true} ); resolvedProxy[key] = value; } } } /** * Unsets 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 = <string[]>(Object.isArray(path) ? path : path.split('.')), prop = normalizedPath[normalizedPath.length - 1]; const ctxPath = obj[watchPath] ?? [], refPath = Array.concat([], ctxPath.slice(1), normalizedPath.slice(0, -1)), fullRefPath = Array.concat([], ctxPath.slice(0, 1), refPath); const proxy = getOrCreateLabelValueByHandlers<object>(unwrappedObj, toProxyObject, handlers), root = proxy?.[toTopObject] ?? unwrappedObj, top = proxy?.[toTopObject] ?? unwrappedObj; const ref = <object>Object.get(top, refPath), type = getProxyType(ref); switch (type) { case null: return; case 'array': (<unknown[]>ref).splice(Number(prop), 1); break; case 'map': case 'set': (<Map<unknown, unknown>>ref).delete(prop); break; default: { const key = String(prop); const hasPath = fullRefPath.length > 0, resolvedPath = hasPath ? fullRefPath : undefined, resolvedRoot = hasPath ? root : unwrappedObj, resolvedTop = hasPath ? top : undefined; const resolvedProxy = setWatchAccessors( ref, key, resolvedPath, handlers, resolvedRoot, resolvedTop, {deep: true} ); resolvedProxy[key] = undefined; delete resolvedProxy[key]; } } } /** * Sets a pair of accessors to watch the specified property and returns a proxy object * * @param obj - object to watch * @param key - property key to watch * @param path - path to the object to watch from the root object * @param handlers - set of registered handlers * @param root - link to the root object of watching * @param [top] - link to the top object of watching * @param [opts] - additional watch options */ export function setWatchAccessors( obj: object, key: string, path: CanUndef<unknown[]>, handlers: WatchHandlersSet, root: object, top?: object, opts?: InternalWatchOptions ): Dictionary { const proxy = getOrCreateLabelValueByHandlers<Dictionary>( obj, toProxyObject, handlers, Object.create(obj) ); const descriptors = Object.getOwnPropertyDescriptor(obj, key); if (!descriptors || descriptors.configurable) { Object.defineProperty(proxy, key, { configurable: true, enumerable: true, get(): unknown { const val = obj[key]; if (root[muteLabel] === true) { return val; } return getProxyValue(val, key, path, handlers, root, top, opts); }, set(val: unknown): void { let fromProto = opts?.fromProto ?? false, oldVal = obj[key]; val = unwrap(val) ?? val; oldVal = unwrap(oldVal) ?? oldVal; if (oldVal !== val) { try { obj[key] = val; if (root[muteLabel] === true) { return; } if (fromProto === 1) { fromProto = false; if (opts != null) { opts.fromProto = fromProto; } } } catch { return; } handlers.forEach((handler) => { const resolvedPath = Array.concat([], path ?? [], key); handler(val, oldVal, { obj, root, top, path: resolvedPath, fromProto: Boolean(fromProto) }); }); } } }); } return proxy; }