UNPKG

reactant-share

Version:

A framework for building shared web applications with Reactant

131 lines (129 loc) 4.14 kB
/* eslint-disable consistent-return */ import { containerKey, identifierKey, Service, watch } from 'reactant'; import { proxyClientActionName, proxyExecutorKey } from './constants'; import type { ProxyExec, ProxyExecutor } from './interfaces'; import { PortDetector } from './modules/portDetector'; /** * Proxy execute On the server side. * * ## Description * * `delegate()` is very similar to the actor model, * which transfers the corresponding module method to the server thread for execution and returns the result as response. * * Note: It does not create new threads, it always runs on the server thread that has already been created. * * ## Example * * ```tsx * import React from 'react'; * import { ViewModule, createApp, injectable, useConnector, action, state, delegate } from 'reactant-share'; * * @injectable({ name: 'counter'}) * class Counter { * @state * count = 0; * * @action * increase() { * this.count += 1; * } * } * * @injectable() * export class AppView extends ViewModule { * constructor(public counter: Counter) { * super(); * } * * component() { * const count = useConnector(() => this.counter.count); * return ( * <button type="button" onClick={() => delegate(this.counter, 'increase', [])}> * {count} * </button> * ); * } * } * ``` * reference: https://en.wikipedia.org/wiki/Actor_model */ export const delegate = ((module, key, args, options = {}) => { const method = module[key]; const _args = args ?? []; if (typeof key !== 'string') { throw new Error( `'delegate()' is valid only for method name with string type.` ); } if (typeof method !== 'function') { throw new Error( `The property '${key}'' must be a method in class '${module.constructor.name}'.` ); } if (!Array.isArray(_args)) { throw new Error(`The parameters of the method '${key}' must be an array.`); } const target: Service & ProxyExecutor = module; if (target[containerKey]?.isBound(PortDetector)) { const portDetector = target[containerKey]!.get(PortDetector); if (__DEV__) { const moduleName = target.constructor.name; if (/^@@reactant/.test(target[identifierKey]!)) { throw new Error( `The identifier '${target[identifierKey]}' is a temporary string, please set 'provide' for the module '${moduleName}' or the 'name' field of the module '${moduleName}'.` ); } } // If the method in main thread and use coworker, it should be proxied for execution to a coworker thread. if (target[proxyExecutorKey]) { return target[proxyExecutorKey]({ module: target[identifierKey]!, method: key, args: _args, }); } // If the port is not a client, it just run the method in server port. if (portDetector.isClient) { if (!portDetector.transports.client) { return Promise.reject( new Error( `Detected that the current client transport does not exist.` ) ); } return portDetector.transports.client .emit( { ...options, name: proxyClientActionName, }, { module: target[identifierKey]!, method: key, args: _args, hook: options._extra?.serverHook, } ) .then((response) => { // If the response is undefined, it means that the method is not executed. if (!response) return; const [sequence, result] = response; if (portDetector.lastAction.sequence >= sequence) { return result; } if (__DEV__) { console.warn( `The sequence of the action is not consistent in ${target[identifierKey]}.${key}.`, sequence, portDetector.lastAction.sequence ); } return portDetector .syncFullState({ forceSync: false }) .then(() => result); }); } } return (method as Function).apply(target, _args); }) as ProxyExec;