UNPKG

tgrid

Version:

Grid Computing Framework for TypeScript

288 lines (261 loc) 9.14 kB
import { sleep_until } from "tstl"; import { Invoke } from "../../components/Invoke"; import { ConnectorBase } from "../internal/ConnectorBase"; import { IHeaderWrapper } from "../internal/IHeaderWrapper"; import { once } from "../internal/once"; import { IReject } from "./internal/IReject"; import { IWorkerSystem } from "./internal/IWorkerSystem"; import { WebWorkerCompiler } from "./internal/WebWorkerCompiler"; /** * SharedWorker Connector * * - available only in the Web Browser. * * The `SharedWorkerConnector` is a communicator class which connects to an * `SharedWorker` instance, and interacts with it through RFC (Remote Function Call) * concept. * * You can connect to the {@link SharedWorkerServer} using {@link connect} method. * The interaction would be started if the server accepts your connection by calling * the {@link SharedWorkerAcceptor.accept} method. If the remote server rejects your * connection through {@link SharedWorkerAcceptor.reject} method, the exception * would be thrown. * * After the connection, don't forget to {@link closing} the connection, if your * business logics have been completed, to clean up the resources. Otherwise, the * closing must be performed by the remote shared worker server, you can wait the * remote server's closing signal through the {@link join} method. * * Also, when declaring this `SharedWorkerConnector` type, you've to define three * generic arguments; `Header`, `Provider` and `Remote`. Those generic arguments must * be same with the ones defined in the target {@link WebSocketServer} and * {@link SharedWorkerAcceptor} classes (`Provider` and `Remote` must be reversed). * * For reference, the first `Header` type represents an initial data from the * remote client after the connection. I recommend utilize it as an activation tool * for security enhancement. The second generic argument `Provider` represents a * provider from client to server, and the other `Remote` means a provider from the * remote server to client. * * @template Header Type of the header containing initial data. * @template Provider Type of features provided for the remote server. * @template Remote Type of features supported by remote server. */ export class SharedWorkerConnector< Header, Provider extends object | null, Remote extends object | null, > extends ConnectorBase<Header, Provider, Remote> implements IWorkerSystem { /** * @hidden */ private port_?: MessagePort; /* ---------------------------------------------------------------- CONNECTIONS ---------------------------------------------------------------- */ /** * Connect to remote server. * * The {@link connect}() method tries to connect an `SharedWorker` instance. If the * `SharedWorker` instance is not created yet, the `SharedWorker` instance would be newly * created. After the creation, the `SharedWorker` program must open that server using * the {@link SharedWorkerServer.open}() method. * * After you business has been completed, you've to close the `SharedWorker` using one of * them below. If you don't close that, vulnerable memory usage and communication channel * would not be destroyed and it may cause the memory leak: * * - {@link close}() * - {@link ShareDWorkerAcceptor.close}() * - {@link SharedWorkerServer.close}() * * @param jsFile JS File to be {@link SharedWorkerServer}. * @param options Detailed options like timeout. */ public async connect( jsFile: string, options: Partial<SharedWorkerConnector.IConnectOptions> = {}, ): Promise<void> { // TEST CONDITION if (this.port_ && this.state_ !== SharedWorkerConnector.State.CLOSED) { if (this.state_ === SharedWorkerConnector.State.CONNECTING) throw new Error( "Error on SharedWorkerConnector.connect(): on connecting.", ); else if (this.state_ === SharedWorkerConnector.State.OPEN) throw new Error( "Error on SharedWorkerConnector.connect(): already connected.", ); else throw new Error("Error on SharedWorkerConnector.connect(): closing."); } //---- // CONNECTION //---- // TIME LIMIT const at: Date | undefined = options.timeout !== undefined ? new Date(Date.now() + options.timeout) : undefined; // SET CURRENT STATE this.state_ = SharedWorkerConnector.State.CONNECTING; try { // EXECUTE THE WORKER const worker: SharedWorker = new SharedWorker(jsFile); this.port_ = worker.port as MessagePort; // WAIT THE WORKER TO BE READY if ( (await this._Handshake(options.timeout, at)) !== SharedWorkerConnector.State.CONNECTING ) throw new Error( `Error on SharedWorkerConnector.connect(): target shared-worker may not be opened by TGrid. It's not following the TGrid's own handshake rule when connecting.`, ); // SEND HEADERS this.port_.postMessage(JSON.stringify(IHeaderWrapper.wrap(this.header))); // WAIT ACCESSION OR REJECTION const last: string | SharedWorkerConnector.State.OPEN = await this._Handshake(options.timeout, at); if (last === SharedWorkerConnector.State.OPEN) { // ACCEPTED this.state_ = SharedWorkerConnector.State.OPEN; this.port_.onmessage = this._Handle_message.bind(this); this.port_.onmessageerror = () => {}; } else { // REJECT OR HANDSHAKE ERROR /* eslint-disable */ let reject: IReject | null = null; try { reject = JSON.parse(last); } catch {} if ( reject && reject.name === "reject" && typeof reject.message === "string" ) throw new Error(reject.message); else throw new Error( `Error on SharedWorkerConnector.connect(): target shared-worker may not be opened by TGrid. It's not following the TGrid's own handshake rule.`, ); } } catch (exp) { try { if (this.port_) this.port_.close(); } catch {} this.state_ = SharedWorkerConnector.State.NONE; throw exp; } } /** * @hidden */ private _Handshake(timeout?: number, at?: Date): Promise<any> { return new Promise((resolve, reject) => { let completed: boolean = false; let expired: boolean = false; if (at !== undefined) sleep_until(at).then(() => { if (completed === false) { reject( new Error( `Error on SharedWorkerConnector.connect(): target shared-worker is not sending handshake data over ${timeout} milliseconds.`, ), ); expired = true; } }); this.port_!.onmessage = once((evt) => { if (expired === false) { completed = true; resolve(evt.data); } }); }); } /** * @inheritDoc */ public async close(): Promise<void> { // TEST CONDITION const error: Error | null = this.inspectReady("close"); if (error) throw error; //---- // CLOSE WITH JOIN //---- // PROMISE RETURN const ret: Promise<void> = this.join(); // REQUEST CLOSE TO SERVER this.state_ = SharedWorkerConnector.State.CLOSING; this.port_!.postMessage(SharedWorkerConnector.State.CLOSING); // LAZY RETURN await ret; } /* ---------------------------------------------------------------- COMMUNICATOR ---------------------------------------------------------------- */ /** * @hidden */ protected async sendData(invoke: Invoke): Promise<void> { this.port_!.postMessage(JSON.stringify(invoke)); } /** * @hidden */ private _Handle_message(evt: MessageEvent): void { if (evt.data === SharedWorkerConnector.State.CLOSING) this._Handle_close(); // RFC OR REJECT else { const data: Invoke = JSON.parse(evt.data); this.replyData(data as Invoke); } } /** * @hidden */ private async _Handle_close(): Promise<void> { await this.destructor(); this.state_ = SharedWorkerConnector.State.CLOSED; } } /** * */ export namespace SharedWorkerConnector { /** * Current state of the {@link SharedWorkerConnector}. */ export import State = ConnectorBase.State; /** * Connection options for the {@link SharedWorkerConnector.connect}. */ export interface IConnectOptions { /** * Milliseconds to wait the shared-worker server to accept or reject it. If omitted, the waiting would be forever. */ timeout: number; } /** * Compile JS source code. * * @param content Source code * @return Temporary URL. */ export async function compile(content: string): Promise<string> { const { compile } = await WebWorkerCompiler(); return compile(content); } /** * Remove compiled JS file. * * @param url Temporary URL. */ export async function remove(url: string): Promise<void> { const { remove } = await WebWorkerCompiler(); await remove(url); } }