@v4fire/client
Version:
V4Fire client core library
1,128 lines (948 loc) • 25.6 kB
text/typescript
/*!
* V4Fire Client Core
* https://github.com/V4Fire/Client
*
* Released under the MIT license
* https://github.com/V4Fire/Client/blob/master/LICENSE
*/
/**
* [[include:super/i-block/modules/sync/README.md]]
* @packageDocumentation
*/
import { isProxy } from 'core/object/watch';
import {
bindingRgxp,
customWatcherRgxp,
getPropertyInfo,
PropertyInfo,
SyncLinkCache
} from 'core/component';
import type iBlock from 'super/i-block/i-block';
import { statuses } from 'super/i-block/const';
import Friend from 'super/i-block/modules/friend';
import type {
LinkDecl,
PropLinks,
Link,
LinkWrapper,
ModValueConverter,
AsyncWatchOptions
} from 'super/i-block/modules/sync/interface';
export * from 'super/i-block/modules/sync/interface';
/**
* Class provides API to organize a "link" from one component property to another
*/
export default class Sync extends Friend {
/**
* Cache of functions to synchronize modifiers
*/
readonly syncModCache!: Dictionary<Function>;
/** @see [[iBlock.$syncLinkCache]] */
protected get syncLinkCache(): SyncLinkCache {
return this.ctx.$syncLinkCache;
}
/** @see [[iBlock.$syncLinkCache]] */
protected set syncLinkCache(value: SyncLinkCache) {
Object.set(this.ctx, '$syncLinkCache', value);
}
/**
* Cache for links
*/
protected readonly linksCache!: Dictionary<Dictionary>;
constructor(component: iBlock) {
super(component);
this.linksCache = Object.createDict();
this.syncLinkCache = new Map();
this.syncModCache = Object.createDict();
}
/**
* Sets a link to a property that logically connected to the current property.
*
* The link is mean every time a value by the link is changed or linked event is fired
* a value that refers to the link will be also changed.
*
* Logical connection is based on a name convention: properties that match the pattern
* "${property} -> ${property}Prop | ${property}Store -> ${property}Prop"
* are connected with each other.
*
* Mind, this method can be used only within a property decorator.
*
* @param [optsOrWrapper] - additional options or a wrapper
*
* @example
* ```typescript
* @component()
* class Foo extends iBlock {
* @prop()
* blaProp: number = 0;
*
* @field((ctx) => ctx.sync.link())
* bla!: number;
* }
* ```
*/
link<D = unknown, R = D>(optsOrWrapper?: AsyncWatchOptions | LinkWrapper<this['C'], D, R>): CanUndef<R>;
/**
* Sets a link to a property that logically connected to the current property.
*
* The link is mean every time a value by the link is changed or linked event is fired
* a value that refers to the link will be also changed.
*
* Logical connection is based on a name convention:
* properties that match the pattern "${property} -> ${property}Prop" are connected with each other.
*
* Mind, this method can be used only within a property decorator.
*
* @param opts - additional options
* @param [wrapper]
*
* @example
* ```typescript
* @component()
* class Foo extends iBlock {
* @prop()
* blaProp: number = 0;
*
* @field((ctx) => ctx.sync.link({deep: true}, (val) => val + 1}))
* bla!: number;
* }
* ```
*/
link<D = unknown, R = D>(opts: AsyncWatchOptions, wrapper?: LinkWrapper<this['C'], D, R>): CanUndef<R>;
/**
* Sets a link to a component/object property or event by the specified path.
*
* The link is mean every time a value by the link is changed or linked event is fired
* a value that refers to the link will be also changed.
*
* To listen an event you need to use the special delimiter ":" within a path.
* Also, you can specify an event emitter to listen by writing a link before ":".
*
* @see [[iBlock.watch]]
* @param path - path to a property/event that we are referring or
* [path to a property that contains a link, path to a property/event that we are referring]
*
* @param [optsOrWrapper] - additional options or a wrapper
*
* @example
* ```typescript
* @component()
* class Foo extends iBlock {
* @prop()
* bla: number = 0;
*
* @field((ctx) => ctx.sync.link('bla'))
* baz!: number;
*
* @field((ctx) => ctx.sync.link({ctx: remoteObject, path: 'bla'}))
* ban!: number;
* }
* ```
*
* ```typescript
* @component()
* class Foo extends iBlock {
* @prop()
* bla: number = 0;
*
* @field()
* baz!: number;
*
* @field()
* ban!: number;
*
* created() {
* this.baz = this.sync.link(['baz', 'bla']);
* this.ban = this.sync.link(['ban', remoteObject]);
* }
* }
* ```
*/
link<D = unknown, R = D>(
path: LinkDecl,
optsOrWrapper?: AsyncWatchOptions | LinkWrapper<this['C'], D, R>
): CanUndef<R>;
/**
* Sets a link to a component/object property or event by the specified path.
*
* The link is mean every time a value by the link is changed or linked event is fired
* a value that refers to the link will be also changed.
*
* To listen an event you need to use the special delimiter ":" within a path.
* Also, you can specify an event emitter to listen by writing a link before ":".
*
* @see [[iBlock.watch]]
* @param path - path to a property/event that we are referring or
* [path to a property that contains a link, path to a property/event that we are referring]
*
* @param opts - additional options
* @param [wrapper]
*
* @example
* ```typescript
* @component()
* class Foo extends iBlock {
* @prop()
* bla: number = 0;
*
* @field((ctx) => ctx.sync.link('bla', {deep: true}, (val) => val + 1))
* baz!: number;
*
* @field((ctx) => ctx.sync.link({ctx: remoteObject, path: 'bla'}, {deep: true}, (val) => val + 1)))
* ban!: number;
* }
* ```
*
* ```typescript
* @component()
* class Foo extends iBlock {
* @prop()
* bla: number = 0;
*
* @field()
* baz!: number;
*
* @field()
* ban!: number;
*
* created() {
* this.baz = this.sync.link(['baz', 'bla'], {deep: true}, (val) => val + 1));
* this.ban = this.sync.link(['ban', remoteObject], {deep: true}, (val) => val + 1));
* }
* }
* ```
*/
link<D = unknown, R = D>(
path: LinkDecl,
opts: AsyncWatchOptions,
wrapper?: LinkWrapper<this['C'], D, R>
): CanUndef<R>;
link<D = unknown, R = D>(
path?: LinkDecl | AsyncWatchOptions | LinkWrapper<this['C'], D>,
opts?: AsyncWatchOptions | LinkWrapper<this['C'], D>,
wrapper?: LinkWrapper<this['C'], D>
): CanUndef<R> {
let
destPath,
resolvedPath: CanUndef<LinkDecl>;
if (Object.isArray(path)) {
destPath = path[0];
path = path[1];
} else {
destPath = this.activeField;
if (Object.isFunction(path)) {
wrapper = path;
path = undefined;
}
}
if (Object.isFunction(opts)) {
wrapper = opts;
}
if (destPath == null) {
throw new Error('A path to the property that is contained a link is not defined');
}
const {
meta,
ctx,
linksCache,
syncLinkCache
} = this;
if (linksCache[destPath] != null) {
return;
}
let
resolvedOpts: AsyncWatchOptions = {};
if (path == null) {
resolvedPath = `${destPath.replace(bindingRgxp, '')}Prop`;
} else if (Object.isString(path) || isProxy(path) || 'ctx' in path) {
resolvedPath = path;
} else if (Object.isDictionary(path)) {
resolvedOpts = path;
}
if (Object.isDictionary(opts)) {
resolvedOpts = opts;
}
if (resolvedPath == null) {
throw new ReferenceError('A path or object to watch is not specified');
}
let
info,
normalizedPath: CanUndef<ObjectPropertyPath>,
topPathIndex = 1;
let
isMountedWatcher = false,
isCustomWatcher = false;
if (!Object.isString(resolvedPath)) {
isMountedWatcher = true;
if (isProxy(resolvedPath)) {
info = {ctx: resolvedPath};
normalizedPath = undefined;
} else {
info = resolvedPath;
normalizedPath = info.path;
topPathIndex = 0;
}
} else {
normalizedPath = resolvedPath;
if (RegExp.test(customWatcherRgxp, normalizedPath)) {
isCustomWatcher = true;
} else {
info = getPropertyInfo(normalizedPath, this.ctx);
if (info.type === 'mounted') {
isMountedWatcher = true;
normalizedPath = info.path;
topPathIndex = Object.size(info.path) > 0 ? 0 : 1;
}
}
}
const isAccessor = info != null ?
Boolean(info.type === 'accessor' || info.type === 'computed' || info.accessor) :
false;
if (isAccessor) {
resolvedOpts.immediate = resolvedOpts.immediate !== false;
}
if (!isCustomWatcher) {
if (
normalizedPath != null && (
Object.isArray(normalizedPath) && normalizedPath.length > topPathIndex ||
Object.isString(normalizedPath) && normalizedPath.split('.', 2).length > topPathIndex
)
) {
if (!resolvedOpts.deep && !resolvedOpts.collapse) {
resolvedOpts.collapse = false;
}
} else if (resolvedOpts.deep !== false && resolvedOpts.collapse !== false) {
resolvedOpts.deep = true;
resolvedOpts.collapse = true;
}
}
linksCache[destPath] = {};
const sync = (val?, oldVal?) => {
const res = wrapper ? wrapper.call(this.component, val, oldVal) : val;
this.field.set(destPath, res);
return res;
};
if (wrapper != null && (wrapper.length > 1 || wrapper['originalLength'] > 1)) {
ctx.watch(info ?? normalizedPath, resolvedOpts, (val, oldVal, ...args) => {
if (isCustomWatcher) {
oldVal = undefined;
} else {
if (args.length === 0 && Object.isArray(val) && val.length > 0) {
const
mutation = <[unknown, unknown]>val[val.length - 1];
val = mutation[0];
oldVal = mutation[1];
}
if (this.fastCompare(val, oldVal, destPath, resolvedOpts)) {
return;
}
}
sync(val, oldVal);
});
} else {
ctx.watch(info ?? normalizedPath, resolvedOpts, (val, ...args) => {
let
oldVal: unknown = undefined;
if (!isCustomWatcher) {
if (args.length === 0 && Object.isArray(val) && val.length > 0) {
const
mutation = <[unknown, unknown]>val[val.length - 1];
val = mutation[0];
oldVal = mutation[1];
} else {
oldVal ??= args[0];
}
if (this.fastCompare(val, oldVal, destPath, resolvedOpts)) {
return;
}
}
sync(val, oldVal);
});
}
{
let
key;
if (isMountedWatcher) {
const o = info?.originalPath;
key = Object.isString(o) ? o : info?.ctx ?? normalizedPath;
} else {
key = normalizedPath;
}
syncLinkCache.set(key, Object.assign(syncLinkCache.get(key) ?? {}, {
[destPath]: {
path: destPath,
sync
}
}));
}
if (isCustomWatcher) {
return sync();
}
const
needCollapse = resolvedOpts.collapse !== false;
if (isMountedWatcher) {
const
obj = info?.ctx;
if (needCollapse || Object.size(normalizedPath) === 0) {
return sync(obj);
}
return sync(Object.get(obj, normalizedPath!));
}
const initSync = () => sync(
this.field.get(needCollapse ? info.originalTopPath : info.originalPath)
);
if (this.lfc.isBeforeCreate('beforeDataCreate')) {
const
name = '[[SYNC]]',
hooks = meta.hooks.beforeDataCreate;
let
pos = 0;
for (let i = 0; i < hooks.length; i++) {
if (hooks[i].name === name) {
pos = i + 1;
}
}
hooks.splice(pos, 0, {fn: initSync, name});
return;
}
return initSync();
}
/**
* Creates an object where all keys are referring to another properties/events as links.
*
* The link is mean every time a value by the link is changed or linked event is fired
* a value that refers to the link will be also changed.
*
* To listen an event you need to use the special delimiter ":" within a path.
* Also, you can specify an event emitter to listen by writing a link before ":".
*
* Mind, this method can be used only within a property decorator.
*
* @see [[iBlock.watch]]
* @param decl - declaration of object properties
*
* @example
* ```typescript
* @component()
* class Foo extends iBlock {
* @field()
* bla: number = 0;
*
* @field()
* bar: number = 0;
*
* @field()
* bad: number = 0;
*
* @field((ctx) => ctx.sync.object([
* 'bla',
* ['barAlias', 'bar'],
* ['bad', String]
* ]))
*
* baz!: {bla: number; barAlias: number; bad: string}};
* }
* ```
*/
object(decl: PropLinks): Dictionary;
/**
* Creates an object where all keys refer to another properties/events as links.
*
* The link is mean every time a value by the link is changed or linked event is fired
* a value that refers to the link will be also changed.
*
* To listen an event you need to use the special delimiter ":" within a path.
* Also, you can specify an event emitter to listen by writing a link before ":".
*
* Mind, this method can be used only within a property decorator.
*
* @see [[iBlock.watch]]
* @param opts - additional options
* @param fields - declaration of object properties
*
* @example
* ```typescript
* @component()
* class Foo extends iBlock {
* @field()
* bla: number = 0;
*
* @field()
* bar: number = 0;
*
* @field()
* bad: number = 0;
*
* @field((ctx) => ctx.sync.object({deep: true}, [
* 'bla',
* ['barAlias', 'bar'],
* ['bad', String]
* ]))
*
* baz!: {bla: number; barAlias: number; bad: string}};
* }
* ```
*/
object(opts: AsyncWatchOptions, fields: PropLinks): Dictionary;
/**
* Creates an object where all keys refer to another properties/events as links.
*
* The link is mean every time a value by the link is changed or linked event is fired
* a value that refers to the link will be also changed.
*
* To listen an event you need to use the special delimiter ":" within a path.
* Also, you can specify an event emitter to listen by writing a link before ":".
*
* @see [[iBlock.watch]]
* @param path - path to a property that contains the result object
* (if the method is used within a property decorator, this value will be concatenated to an active field name)
*
* @param fields - declaration of object properties
*
* @example
* ```typescript
* @component()
* class Foo extends iBlock {
* @field()
* bla: number = 0;
*
* @field()
* bar: number = 0;
*
* @field()
* bad: number = 0;
*
* @field((ctx) => ctx.sync.object('links', [
* 'bla',
* ['barAlias', 'bar'],
* ['bad', String]
* ]))
*
* baz: {links: {bla: number; barAlias: number; bad: string}}}
* }
* ```
*/
// eslint-disable-next-line @typescript-eslint/unified-signatures
object(path: Link, fields: PropLinks): Dictionary;
/**
* Creates an object where all keys refer to another properties/events as links.
*
* The link is mean every time a value by the link is changed or linked event is fired
* a value that refers to the link will be also changed.
*
* To listen an event you need to use the special delimiter ":" within a path.
* Also, you can specify an event emitter to listen by writing a link before ":".
*
* @see [[iBlock.watch]]
* @param path - path to a property that contains the result object
* (if the method is used within a property decorator, this value will be concatenated to an active field name)
*
* @param opts - additional options
* @param fields - declaration of object properties
*
* @example
* ```typescript
* @component()
* class Foo extends iBlock {
* @field()
* bla: number = 0;
*
* @field()
* bar: number = 0;
*
* @field()
* bad: number = 0;
*
* @field((ctx) => ctx.sync.object('links', {deep: true}, [
* 'bla',
* ['barAlias', 'bar'],
* ['bad', String]
* ]))
*
* baz: {links: {bla: number; barAlias: number; bad: string}}}
* }
* ```
*/
object(
path: Link,
opts: AsyncWatchOptions,
fields: PropLinks
): Dictionary;
object(
path: Link | AsyncWatchOptions | PropLinks,
opts?: AsyncWatchOptions | PropLinks,
fields?: PropLinks
): Dictionary {
if (Object.isString(path)) {
if (Object.isArray(opts)) {
fields = opts;
opts = undefined;
}
} else {
if (Object.isArray(path)) {
fields = path;
opts = undefined;
} else {
if (Object.isArray(opts)) {
fields = opts;
}
opts = path;
}
path = '';
}
let
destHead = this.activeField;
if (destHead != null) {
if (Object.size(path) > 0) {
path = [destHead, path].join('.');
} else {
path = destHead;
}
} else {
destHead = path.split('.', 1)[0];
}
const
localPath = path.split('.').slice(1);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (destHead == null) {
throw new ReferenceError('A path to a property that is contained the final object is not defined');
}
const {
ctx,
syncLinkCache,
linksCache,
meta: {hooks: {beforeDataCreate: hooks}}
} = this;
const
resObj = {};
if (!Object.isArray(fields)) {
return resObj;
}
if (localPath.length > 0) {
Object.set(resObj, localPath, {});
}
const
cursor = Object.get<StrictDictionary>(resObj, localPath);
const attachWatcher = (watchPath, destPath, getVal, wrapper?) => {
Object.set(linksCache, destPath, true);
let
info,
isMountedWatcher = false,
isCustomWatcher = false,
topPathIndex = 1;
if (!Object.isString(watchPath)) {
isMountedWatcher = true;
if (isProxy(watchPath)) {
info = {ctx: watchPath};
watchPath = undefined;
} else {
info = watchPath;
watchPath = info.path;
topPathIndex = 0;
}
} else if (RegExp.test(customWatcherRgxp, watchPath)) {
isCustomWatcher = true;
} else {
info = getPropertyInfo(watchPath, this.ctx);
if (info.type === 'mounted') {
isMountedWatcher = true;
watchPath = info.path;
topPathIndex = Object.size(info.path) > 0 ? 0 : 1;
}
}
const isAccessor = info != null ?
Boolean(info.type === 'accessor' || info.type === 'computed' || info.accessor) :
false;
const
sync = (val?, oldVal?, init?) => this.field.set(destPath, getVal(val, oldVal, init)),
isolatedOpts = <AsyncWatchOptions>{...opts};
if (isAccessor) {
isolatedOpts.immediate = isolatedOpts.immediate !== false;
}
if (!isCustomWatcher) {
if (
watchPath != null && (
Object.isArray(watchPath) && watchPath.length > topPathIndex ||
Object.isString(watchPath) && watchPath.split('.', 2).length > topPathIndex
)
) {
if (!isolatedOpts.deep && !isolatedOpts.collapse) {
isolatedOpts.collapse = false;
}
} else if (isolatedOpts.deep !== false && isolatedOpts.collapse !== false) {
isolatedOpts.deep = true;
isolatedOpts.collapse = true;
}
}
if (wrapper != null && (wrapper.length > 1 || wrapper['originalLength'] > 1)) {
ctx.watch(info ?? watchPath, isolatedOpts, (val, oldVal, ...args) => {
if (isCustomWatcher) {
oldVal = undefined;
} else {
if (args.length === 0 && Object.isArray(val) && val.length > 0) {
const
mutation = <[unknown, unknown]>val[val.length - 1];
val = mutation[0];
oldVal = mutation[1];
}
if (this.fastCompare(val, oldVal, destPath, isolatedOpts)) {
return;
}
}
sync(val, oldVal);
});
} else {
ctx.watch(info ?? watchPath, isolatedOpts, (val, ...args) => {
let
oldVal: unknown = undefined;
if (!isCustomWatcher) {
if (args.length === 0 && Object.isArray(val) && val.length > 0) {
const
mutation = <[unknown, unknown]>val[val.length - 1];
val = mutation[0];
oldVal = mutation[1];
} else {
oldVal ??= args[0];
}
if (this.fastCompare(val, oldVal, destPath, isolatedOpts)) {
return;
}
}
sync(val, oldVal);
});
}
{
let
key;
if (isMountedWatcher) {
const o = info?.originalPath;
key = Object.isString(o) ? o : info?.ctx ?? watchPath;
} else {
key = watchPath;
}
syncLinkCache.set(key, Object.assign(syncLinkCache.get(key) ?? {}, {
[destPath]: {
path: destPath,
sync
}
}));
}
if (isCustomWatcher) {
return ['custom', isolatedOpts];
}
if (isMountedWatcher) {
return ['mounted', isolatedOpts, info];
}
if (this.lfc.isBeforeCreate('beforeDataCreate')) {
hooks.push({fn: () => sync(null, null, true)});
}
return ['regular', isolatedOpts, info];
};
for (let i = 0; i < fields.length; i++) {
const
el = fields[i];
let
type: string,
opts: AsyncWatchOptions,
info: PropertyInfo;
const createGetVal = (watchPath, wrapper) => (val?, oldVal?, init?: boolean) => {
if (init) {
switch (type) {
case 'regular':
val = this.field.get(opts.collapse ? info.originalTopPath : watchPath);
break;
case 'mounted': {
const
obj = info.ctx;
if (opts.collapse || Object.size(info.path) === 0) {
val = obj;
} else {
val = Object.get(obj, info.path);
}
break;
}
default:
val = undefined;
break;
}
}
if (wrapper == null) {
return val;
}
return wrapper.call(this.component, val, oldVal);
};
let
wrapper,
watchPath,
savePath;
if (Object.isArray(el)) {
if (el.length === 3) {
watchPath = el[1];
wrapper = el[2];
} else if (Object.isFunction(el[1])) {
watchPath = el[0];
wrapper = el[1];
} else {
watchPath = el[1];
}
savePath = el[0];
} else {
watchPath = el;
savePath = el;
}
const
destPath = [path, savePath].join('.');
if (Object.get(linksCache, destPath) == null) {
const getVal = createGetVal(watchPath, wrapper);
[type, opts, info] = attachWatcher(watchPath, destPath, getVal);
Object.set(cursor, savePath, getVal(null, null, true));
}
}
this.field.set(path, cursor);
return resObj;
}
/**
* Synchronizes component link values with values they are linked
*
* @param path - path to a property/event that we are referring or
* [path to a property that contains a link, path to a property/event that we are referring]
*
* @param [value] - value to synchronize links
*/
syncLinks(path?: LinkDecl, value?: unknown): void {
let
linkPath,
storePath;
if (Object.isArray(path)) {
storePath = path[0];
linkPath = path[1];
} else {
linkPath = path;
}
const
cache = this.syncLinkCache;
const sync = (linkName) => {
const
o = cache.get(linkName);
if (o == null) {
return;
}
for (let keys = Object.keys(o), i = 0; i < keys.length; i++) {
const
key = keys[i],
el = o[key];
if (el == null) {
continue;
}
if (storePath == null || key === storePath) {
el.sync(value ?? this.field.get(linkName));
}
}
};
if (linkPath != null) {
sync(linkPath);
} else {
for (let o = cache.keys(), el = o.next(); !el.done; el = o.next()) {
sync(el.value);
}
}
}
/**
* Binds a modifier to a property by the specified path
*
* @param modName
* @param path
* @param [converter] - converter function
*/
mod<D = unknown, R = unknown>(
modName: string,
path: string,
converter?: ModValueConverter<this['C'], D, R>
): void;
/**
* Binds a modifier to a property by the specified path
*
* @param modName
* @param path
* @param opts - additional options
* @param [converter] - converter function
*/
mod<D = unknown, R = unknown>(
modName: string,
path: string,
opts: AsyncWatchOptions,
converter?: ModValueConverter<this['C'], D, R>
): void;
mod<D = unknown, R = unknown>(
modName: string,
path: string,
optsOrConverter?: AsyncWatchOptions | ModValueConverter<this['C'], D, R>,
converter: ModValueConverter<this['C'], D, R> = (v) => v != null ? Boolean(v) : undefined
): void {
modName = modName.camelize(false);
let
opts;
if (Object.isFunction(optsOrConverter)) {
converter = optsOrConverter;
} else {
opts = optsOrConverter;
}
const
{ctx} = this;
const setWatcher = () => {
const wrapper = (val, ...args) => {
val = converter.call(this.component, val, ...args);
if (val !== undefined) {
void this.ctx.setMod(modName, val);
}
};
if (converter.length > 1) {
ctx.watch(path, opts, (val, oldVal) => wrapper(val, oldVal));
} else {
ctx.watch(path, opts, wrapper);
}
};
if (this.lfc.isBeforeCreate()) {
const sync = () => {
const
v = converter.call(this.component, this.field.get(path));
if (v !== undefined) {
ctx.mods[modName] = String(v);
}
};
this.syncModCache[modName] = sync;
if (ctx.hook !== 'beforeDataCreate') {
this.meta.hooks.beforeDataCreate.push({
fn: sync
});
} else {
sync();
}
setWatcher();
} else if (statuses[ctx.componentStatus] >= 1) {
setWatcher();
}
}
/**
* Wrapper of `Object.fastCompare` to compare watchable values
*
* @param value
* @param oldValue
* @param destPath - path to the property
* @param opts - watch options
*/
protected fastCompare(
value: unknown,
oldValue: unknown,
destPath: string,
opts: AsyncWatchOptions
): boolean {
if (opts.collapse === false) {
return value === oldValue;
}
return !opts.withProto && (
Object.fastCompare(value, oldValue) &&
Object.fastCompare(value, this.field.get(destPath))
);
}
}