@v4fire/client
Version:
V4Fire client core library
506 lines (392 loc) • 11.8 kB
text/typescript
/*!
* 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;
}
};
}