UNPKG

@v4fire/client

Version:

V4Fire client core library

506 lines (392 loc) • 11.8 kB
/*! * V4Fire Client Core * https://github.com/V4Fire/Client * * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ import watch, { mute, unmute, unwrap, getProxyType, isProxy, WatchHandlerParams } from 'core/object/watch'; import { getPropertyInfo, PropertyInfo } from 'core/component/reflection'; import type { ComponentInterface, WatchOptions, RawWatchHandler } from 'core/component/interface'; import { tiedWatchers, watcherInitializer, fakeCopyLabel } from 'core/component/watch/const'; import { cloneWatchValue } from 'core/component/watch/clone'; import { attachDynamicWatcher } from 'core/component/watch/helpers'; /** * Creates a function to watch changes from the specified component instance and returns it * @param component */ // eslint-disable-next-line max-lines-per-function export function createWatchFn(component: ComponentInterface): ComponentInterface['$watch'] { const watchCache = new Map(); // eslint-disable-next-line @typescript-eslint/typedef,max-lines-per-function return function watchFn(this: unknown, path, optsOrHandler, rawHandler?) { if (component.isFlyweight) { return null; } let handler: RawWatchHandler, opts: WatchOptions; if (Object.isFunction(optsOrHandler)) { handler = optsOrHandler; opts = {}; } else { handler = rawHandler; opts = optsOrHandler ?? {}; } let info: PropertyInfo; if (Object.isString(path)) { info = getPropertyInfo(path, component); } else { if (isProxy(path)) { // @ts-ignore (lazy binding) info = {ctx: path}; } else { info = path; } if (isProxy(info.ctx)) { info.type = 'mounted'; info.originalPath = info.path; info.fullPath = info.path; } } let meta, isRoot = false, isFunctional = false; if (info.type !== 'mounted') { const propCtx = info.ctx.unsafe, ctxParams = propCtx.meta.params; meta = propCtx.meta; isRoot = Boolean(ctxParams.root); isFunctional = !isRoot && ctxParams.functional === true; } let canSkipWatching = (isRoot || isFunctional) && (info.type === 'prop' || info.type === 'attr'); if (!canSkipWatching && isFunctional) { let f; switch (info.type) { case 'system': f = meta.systemFields[info.name]; break; case 'field': f = meta.fields[info.name]; break; default: // Do nothing } if (f != null) { canSkipWatching = f.functional === false || f.functionalWatching === false; } } const isAccessor = Boolean(info.type === 'accessor' || info.type === 'computed' || info.accessor), isMountedWatcher = info.type === 'mounted'; const isDefinedPath = Object.size(info.path) > 0, watchInfo = isAccessor ? null : component.$renderEngine.proxyGetters[info.type]?.(info.ctx); const normalizedOpts = <WatchOptions>{ collapse: true, ...opts, ...watchInfo?.opts }; const needCollapse = normalizedOpts.collapse, needImmediate = normalizedOpts.immediate, needCache = (handler['originalLength'] ?? handler.length) > 1 && needCollapse; if (canSkipWatching && !needImmediate) { return null; } const originalHandler = handler; let oldVal; if (needCache) { let cacheKey; if (Object.isString(info.originalPath)) { cacheKey = [info.originalPath]; } else { cacheKey = Array.concat([info.ctx], info.path); } if (Object.has(watchCache, cacheKey)) { oldVal = Object.get(watchCache, cacheKey); } else { oldVal = needImmediate || !isAccessor ? cloneWatchValue(getVal(), normalizedOpts) : undefined; Object.set(watchCache, cacheKey, oldVal); } handler = (val, _, i) => { if (!isDefinedPath && Object.isArray(val) && val.length > 0) { i = (<[unknown, unknown, PropertyInfo]>val[val.length - 1])[2]; } if (isMountedWatcher) { val = info.ctx; patchPath(i); } else if (isAccessor) { val = Object.get(info.ctx, info.accessor ?? info.name); } const res = originalHandler.call(this, val, oldVal, i); oldVal = cloneWatchValue(isDefinedPath ? val : getVal(), normalizedOpts); Object.set(watchCache, cacheKey, oldVal); return res; }; handler[tiedWatchers] = originalHandler[tiedWatchers]; if (needImmediate) { const val = oldVal; oldVal = undefined; handler.call(component, val, undefined, undefined); } } else { if (isMountedWatcher) { handler = (val, ...args) => { let oldVal = args[0], handlerParams = args[1]; if (!isDefinedPath && needCollapse && Object.isArray(val) && val.length > 0) { handlerParams = (<[unknown, unknown, PropertyInfo]>val[val.length - 1])[2]; } else if (args.length === 0) { return originalHandler.call(this, val.map(([val, oldVal, i]) => { patchPath(i); return [val, oldVal, i]; })); } if (needCollapse) { val = info.ctx; oldVal = val; } patchPath(handlerParams); return originalHandler.call(this, val, oldVal, handlerParams); }; } else if (isAccessor) { handler = (val, _, i) => { if (needCollapse) { val = Object.get(info.ctx, info.accessor ?? info.name); } else { val = Object.get(component, info.originalPath); } if (!isDefinedPath && Object.isArray(i?.path)) { oldVal = Object.get(oldVal, [i.path[0]]); } const res = originalHandler.call(this, val, oldVal, i); oldVal = isDefinedPath ? val : getVal(); return res; }; } if (needImmediate) { handler.call(component, getVal(), undefined, undefined); } } if (canSkipWatching) { return null; } let proxy = watchInfo?.value; if (proxy != null) { if (watchInfo == null) { return null; } switch (info.type) { case 'field': case 'system': { const propCtx = info.ctx.unsafe; if (!Object.getOwnPropertyDescriptor(propCtx, info.name)?.get) { proxy[watcherInitializer]?.(); proxy = watchInfo.value; mute(proxy); if (info.type === 'system') { propCtx.$set(proxy, info.name, propCtx[info.name]); } unmute(proxy); Object.defineProperty(propCtx, info.name, { enumerable: true, configurable: true, get: () => proxy[info.name], set: (val) => { propCtx.$set(proxy, info.name, val); } }); } break; } case 'attr': { const attr = info.name; let unwatch; if ('watch' in watchInfo) { unwatch = watchInfo.watch(attr, (value, oldValue) => { const info = { obj: component, root: component, path: [attr], originalPath: [attr], top: value, fromProto: false }; handler.call(this, value, oldValue, info); }); } else { // eslint-disable-next-line @typescript-eslint/unbound-method unwatch = watch(proxy, info.path, normalizedOpts, handler).unwatch; } return wrapDestructor(unwatch); } case 'prop': { const prop = info.name, pathChunks = info.path.split('.'), slicedPathChunks = pathChunks.slice(1); const destructors = <Function[]>[]; const watchHandler = (value, oldValue, info) => { for (let i = destructors.length; --i > 0;) { destructors[i](); destructors.pop(); } // eslint-disable-next-line @typescript-eslint/no-use-before-define attachDeepProxy(); if (value?.[fakeCopyLabel] === true) { return; } let valueByPath = Object.get(value, slicedPathChunks); valueByPath = unwrap(valueByPath) ?? valueByPath; let oldValueByPath = Object.get(oldValue, slicedPathChunks); oldValueByPath = unwrap(oldValueByPath) ?? oldValueByPath; if (valueByPath !== oldValueByPath) { if (needCollapse) { handler.call(this, value, oldValue, info); } else { handler.call(this, valueByPath, oldValueByPath, info); } } }; let unwatch; if ('watch' in watchInfo) { unwatch = watchInfo.watch(prop, (value, oldValue) => { const info = { obj: component, root: component, path: [prop], originalPath: [prop], top: value, fromProto: false }; const tiedLinks = handler[tiedWatchers]; if (Object.isArray(tiedLinks)) { for (let i = 0; i < tiedLinks.length; i++) { const modifiedInfo = { ...info, path: tiedLinks[i], parent: {value, oldValue, info} }; watchHandler(value, oldValue, modifiedInfo); } } else { watchHandler(value, oldValue, info); } }); } else { const topOpts = { ...normalizedOpts, deep: false, collapse: true }; // eslint-disable-next-line @typescript-eslint/unbound-method unwatch = watch(proxy, prop, topOpts, Object.cast(watchHandler)).unwatch; } destructors.push(unwatch); const attachDeepProxy = () => { const propVal = proxy[prop]; if (getProxyType(propVal) != null) { const parent = component.$parent; if (parent == null) { return; } const normalizedOpts = { collapse: true, ...opts, pathModifier: (path) => { if (parent[path[0]] === propVal) { return [pathChunks[0], ...path.slice(1)]; } return path; } }; const watchHandler = (...args) => { if (args.length === 1) { args = args[0][args[0].length - 1]; } const [val, oldVal, mutInfo] = args; if (mutInfo.originalPath.length > 1) { handler.call(this, val, oldVal, mutInfo); } }; // eslint-disable-next-line @typescript-eslint/unbound-method const {unwatch} = watch(<object>propVal, info.path, normalizedOpts, watchHandler); destructors.push(unwatch); } }; attachDeepProxy(); return wrapDestructor(() => { for (let i = 0; i < destructors.length; i++) { destructors[i](); } }); } default: // Loopback } // eslint-disable-next-line @typescript-eslint/unbound-method const {unwatch} = isDefinedPath ? watch(proxy, info.path, normalizedOpts, handler) : watch(proxy, normalizedOpts, handler); return wrapDestructor(unwatch); } return attachDynamicWatcher(component, info, opts, handler); function patchPath(params?: WatchHandlerParams) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (params == null || info.name == null) { return; } if (needCollapse) { params.path = [info.name]; params.originalPath = params.path; } else { params.path.unshift(info.name); if (params.path !== params.originalPath) { params.originalPath.unshift(info.name); } } } function getVal(): unknown { if (info.type !== 'mounted') { return Object.get(component, needCollapse ? info.originalTopPath : info.originalPath); } if (!isDefinedPath || needCollapse) { return info.ctx; } return Object.get(component, info.path); } function wrapDestructor<T>(destructor: T): T { if (Object.isFunction(destructor)) { // Every worker that passed to async have a counter with number of consumers of this worker, // but in this case this behaviour is redundant and can produce an error, // that why we wrap original destructor with a new function component.unsafe.$async.worker(() => destructor()); } return destructor; } }; }