UNPKG

@omnicajs/vue-remote

Version:

Proxy renderer for Vue.js based on @remote-ui

483 lines (397 loc) 11.9 kB
import type { Channel } from '@/dom/common/channel' import type { RemoteComponent, RemoteComponentOption, RemoteFragment, RemoteRoot, RemoteRootOptions, SchemaOf, SupportedBy, UnknownChild, UnknownNode, UnknownParent, } from '@/dom/remote/tree' import { isRemoteFragment } from '@/dom/remote/tree' import { addMethod, capture, } from '@/common/scaffolding' import { ACTION_INSERT_CHILD, ACTION_REMOVE_CHILD, ACTION_INVOKE, } from '@/dom/common/channel' import { KIND_COMPONENT, KIND_FRAGMENT, KIND_ROOT, } from '@/dom/common/tree' export interface RemoteComponentData { properties: { original: { readonly [key: string]: unknown }; serializable: { readonly [key: string]: unknown }; }; children: ReadonlyArray<UnknownChild>; } export interface RemoteFragmentData { children: ReadonlyArray<UnknownChild>; } export interface TreeData<R extends RemoteRoot = RemoteRoot> { strict: boolean; mounted: boolean; channel: Channel; nodes: WeakSet<UnknownNode>; progenitors: WeakMap<UnknownNode, UnknownParent>; parents: WeakMap<UnknownNode, UnknownParent>; components: WeakMap<RemoteComponent<SupportedBy<R>, R>, RemoteComponentData>; fragments: WeakMap< RemoteFragment<R>, RemoteFragmentData >; children: R['children']; } export interface TreeContext<R extends RemoteRoot = RemoteRoot> extends TreeData<R> { nextId (): string; collect (node: UnknownNode): void attach (parent: UnknownParent, node: UnknownNode): void; detach (node: UnknownNode): void; append (parent: UnknownParent, children: UnknownChild[]): void; insert ( parent: UnknownParent, child: UnknownChild, before: UnknownChild | undefined | null ): void; update ( node: UnknownChild | UnknownParent, remote: (channel: Channel) => void | Promise<void>, local: () => void ): void; replace ( parent: UnknownParent, children: UnknownChild[] ): void; removeChild ( parent: UnknownParent, child: UnknownChild ): void; invoke ( node: RemoteComponent<SupportedBy<R>, R>, method: string, payload: unknown[] ): Promise<unknown> } const traverse = (element: UnknownNode, each: (item: UnknownNode) => void) => { const _traverse = (element: UnknownNode) => { if ('children' in element) { for (const child of element.children) { each(child) _traverse(child) } } } _traverse(element) } function attach ( context: TreeData, parent: UnknownParent, node: UnknownNode ) { const { progenitors, parents } = context const progenitor = parent.kind === KIND_ROOT ? parent : parent.progenitor if (progenitor) { progenitors.set(node, progenitor) } parents.set(node, parent) attachFragments(context, node) traverse(node, (descendant) => { if (progenitor) { progenitors.set(descendant, progenitor) } attachFragments(context, descendant) }) } function attachFragments (context: TreeData, node: UnknownNode) { if (node.kind === KIND_COMPONENT) { Object.values(node.properties).forEach(prop => { if (isRemoteFragment(prop)) { attach(context, node, prop) } }) } } function detach (context: TreeData, node: UnknownNode) { const { progenitors, parents } = context progenitors.delete(node) parents.delete(node) traverse(node, (descendant) => { progenitors.delete(descendant) detachFragments(context, descendant) }) detachFragments(context, node) } function detachFragments (context: TreeData, node: UnknownNode) { if (node.kind !== KIND_COMPONENT) { return } const properties = node.properties for (const key of Object.keys(properties)) { const p = properties[key] if (isRemoteFragment(p)) { detach(context, p) } } } const update = ( context: TreeData, node: UnknownChild | UnknownParent, remote: (channel: Channel) => void | Promise<void>, local: () => void ) => { if (context.mounted && (node.kind === KIND_ROOT || node.progenitor?.kind === KIND_ROOT)) { // should only create context once async queue is cleared remote(context.channel) // technically, we should be waiting for the remote update to apply, // then apply it locally. The implementation below is too naive because // it allows local updates to get out of sync with remote ones. // if (remoteResult == null || !('then' in remoteResult)) { // local(); // return; // } else { // return remoteResult.then(() => { // local(); // }); // } } local() } type ParentData = TreeData | RemoteComponentData | RemoteFragmentData function dataOf (context: TreeData, parent: UnknownParent): ParentData | undefined { switch (parent?.kind) { case KIND_COMPONENT: return context.components.get(parent) case KIND_FRAGMENT: return context.fragments.get(parent) case KIND_ROOT: return context } } const insertToArray = <T>(target: T[], el: T, before: T | undefined | null) => { if (before == null) { target.push(el) } else { target.splice(target.indexOf(before), 0, el) } return target } const remoteFromArray = <T>(target: T[] | readonly T[], index: number) => { const result = [...target] result.splice(index, 1) return result } const insert = ( context: TreeData, parent: UnknownParent, child: UnknownChild, before: UnknownChild | undefined | null = null ) => { const currentParent = child.parent const currentIndex = currentParent?.children.indexOf(child) ?? -1 attach(context, parent, child) let newChildren: UnknownChild[] const parentData = dataOf(context, parent)! if (currentParent) { const currentParentData = dataOf(context, currentParent)! const currentChildren = remoteFromArray(currentParentData.children, currentIndex) if (currentParent === parent) { newChildren = currentChildren } else { currentParentData.children = capture(currentChildren, context.strict) newChildren = [...parentData.children] } } else { newChildren = [...parentData.children] } parentData.children = capture(insertToArray(newChildren, child, before), context.strict) } const appendChild = ( context: TreeData, parent: UnknownParent, child: UnknownChild ) => { if (!context.nodes.has(child)) { throw new Error('Cannot append a node that was not created by this remote root') } const currentParent = child.parent const currentIndex = currentParent?.children.indexOf(child) ?? -1 return update(context, parent, (channel) => { channel( ACTION_INSERT_CHILD, parent.id, currentIndex < 0 ? parent.children.length : parent.children.length - 1, child.serialize(), currentParent ? currentParent.id : false ) }, () => insert(context, parent, child)) } const insertBefore = ( context: TreeData, parent: UnknownParent, child: UnknownChild, before: UnknownChild | undefined | null ) => { if (!context.nodes.has(child)) { throw new Error('Cannot insert a node that was not created by this remote root') } if (before && before.id === child.id) return if (before && !parent.children.includes(before)) { throw new DOMException( 'Cannot add a child before an element that is not a child of the target parent.', 'HierarchyRequestError' ) } const oldIndex = parent.children.indexOf(child) ?? -1 const oldParent = child.parent const beforeIndex = before ? parent.children.indexOf(before) : -1 return update(context, parent, (channel) => channel( ACTION_INSERT_CHILD, parent.id, beforeIndex < 0 ? parent.children.length : oldIndex < 0 || oldIndex > beforeIndex ? beforeIndex : beforeIndex - 1, child.serialize(), oldParent ? oldParent.id : false ), () => insert(context, parent, child, before)) } /** @TODO: Объединение удаления нескольких узлов в один запрос */ const removeChild = ( context: TreeData, parent: UnknownParent, child: UnknownChild ) => { const data = dataOf(context, parent)! return update(context, parent, (channel) => channel( ACTION_REMOVE_CHILD, parent.id, parent.children.indexOf(child) ), () => { detach(context, child) data.children = capture(remoteFromArray( data.children, data.children.indexOf(child) ), context.strict) }) } const addAttachMethod = (context: TreeData) => addMethod(context, 'attach', ( parent: UnknownParent, node: UnknownNode ) => attach(context, parent, node)) const addDetachMethod = (context: TreeData) => addMethod(context, 'detach', ( node: UnknownNode ) => detach(context, node)) const addAppendMethod = (context: TreeData) => addMethod(context, 'append', ( parent: UnknownParent, children: UnknownChild[] ) => { for (const child of children) { appendChild(context, parent, child) } }) const addUpdateMethod = (context: TreeData) => addMethod(context, 'update', ( node: UnknownChild | UnknownParent, remote: (channel: Channel) => void | Promise<void>, local: () => void ) => update(context, node, remote, local)) const addReplaceMethod = (context: TreeData) => addMethod(context, 'replace', ( parent: UnknownParent, children: UnknownChild[] ) => { for (const child of parent.children) { removeChild(context, parent, child) } for (const child of children) { appendChild(context, parent, child) } }) type CollectMethod = TreeContext['collect'] const addCollectMethod = (context: TreeData) => addMethod<CollectMethod>(context, 'collect', node => { if (context.nodes.has(node)) { return } context.nodes.add(node) Object.defineProperty(node, 'parent', { get: () => context.parents.get(node) ?? null, configurable: true, enumerable: true, }) Object.defineProperty(node, 'progenitor', { get: () => context.progenitors.get(node) ?? null, configurable: true, enumerable: true, }) }) type InvokeMethod = TreeContext['invoke'] const addInvokeMethod = (context: TreeData) => addMethod<InvokeMethod>(context, 'invoke', ( node, method, payload ) => { if (!context.nodes.has(node)) { throw new Error('Cannot invoke method for a node that was not created by this remote root') } return new Promise((resolve, reject) => { context.channel( ACTION_INVOKE, node.id, method, payload, resolve, reject ) }) }) const createRemoteRootData = <S extends RemoteComponentOption = RemoteComponentOption>( channel: Channel, { components, strict = true }: RemoteRootOptions<S> = {} ) => { if (strict) { Object.freeze(components) } return { strict, mounted: false, channel, children: [], nodes: new WeakSet(), parents: new WeakMap(), progenitors: new WeakMap(), components: new WeakMap(), fragments: new WeakMap(), } as TreeData<RemoteRoot<SchemaOf<S>>> } export const createTreeContext = <S extends RemoteComponentOption = RemoteComponentOption>( channel: Channel, options: RemoteRootOptions<S> = {} ): TreeContext<RemoteRoot<SchemaOf<S>>> => { const context = createRemoteRootData(channel, options) let lastId = 0 addMethod(context, 'nextId', () => `${++lastId}`) addCollectMethod(context) addAttachMethod(context) addDetachMethod(context) addAppendMethod(context) addUpdateMethod(context) addMethod(context, 'insert', ( parent: UnknownParent, child: UnknownChild, before: UnknownChild | undefined | null ) => insertBefore(context, parent, child, before)) addMethod(context, 'removeChild', ( parent: UnknownParent, child: UnknownChild ) => removeChild(context, parent, child)) addReplaceMethod(context) addInvokeMethod(context) return context as TreeContext<RemoteRoot<SchemaOf<S>>> }