@v4fire/core
Version:
V4Fire core library
544 lines (426 loc) • 11.2 kB
text/typescript
/*!
* 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];
}
}
}