tgrid
Version:
Grid Computing Framework for TypeScript
287 lines (260 loc) • 8.94 kB
text/typescript
import { Singleton, is_node, sleep_until } from "tstl";
import { Communicator } from "../../components/Communicator";
import { Invoke } from "../../components/Invoke";
import { IHeaderWrapper } from "../internal/IHeaderWrapper";
import { IServer } from "../internal/IServer";
import { once } from "../internal/once";
import { IWorkerSystem } from "./internal/IWorkerSystem";
import { ProcessChannel } from "./internal/processes/ProcessChannel";
import { ThreadPort } from "./internal/threads/ThreadPort";
/**
* Worker Server.
*
* The `WorkerServer` is a class representing a `Worker` server which communicate
* with client ({@link WorkerConnector}), through the RPC (Remote Procedure Call).
*
* Unlike other servers, `WorkerServer` can accept only one client
* ({@link WorkerConnector}), because the `Worker` is dependent on its parent instance
* (web page, node or parent worker). Thus, `WorkerServer` does not have any acceptor
* and communicates with client (its parent) directly.
*
* To start communication with the client, call the {@link open} method
* with `Provider` instance. After your business, don't forget {@link close closing}
* this `Worker` instance. If the termination is performed by the
* {@link WorkerConnector}, you can wait the closing signal through the
* {@link join} method.
*
* Also, when declaring this `WorkerServer` 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 WorkerConnector} class
* (`Provider` and `Remote` must be reversed).
*
* For reference, the first `Header` type represents an initial data from the
* client after the connection. I recommend utilize it as an activation tool
* for security enhancement. The second generic argument `Provider` represents a
* provider from server to client, and the other `Remote` means a provider from the
* client to server.
*
* @template Header Type of the header containing initial data.
* @template Provider Type of features provided for the client.
* @template Remote Type of features supported by client.
* @author Jeongho Nam - https://github.com/samchon
*/
export class WorkerServer<
Header,
Provider extends object | null,
Remote extends object | null,
>
extends Communicator<Provider | undefined, Remote>
implements IWorkerSystem, IServer<WorkerServer.State>
{
/**
* @hidden
*/
private channel_: Singleton<Promise<IFeature>>;
/**
* @hidden
*/
private state_: WorkerServer.State;
/**
* @hidden
*/
private header_: Singleton<Promise<Header>>;
/* ----------------------------------------------------------------
CONSTRUCTOR
---------------------------------------------------------------- */
/**
* Default Constructor.
*
* @param type You can specify the worker mode when NodeJS. Default is "thread".
*/
public constructor() {
super(undefined);
this.channel_ = new Singleton(async () => {
// BROWSER CASE
if (is_node() === false) return (<any>self) as IFeature;
return (await ThreadPort.isWorkerThread())
? ((await ThreadPort()) as IFeature)
: (ProcessChannel as IFeature);
});
this.state_ = WorkerServer.State.NONE;
this.header_ = new Singleton(async () => {
(await this.channel_.get()).postMessage(WorkerServer.State.OPENING);
const data: string = await this._Handshake("getHeader");
const wrapper: IHeaderWrapper<Header> = JSON.parse(data);
return wrapper.header;
});
}
/**
* Open server with `Provider`.
*
* Open worker server and start communication with the client
* ({@link WorkerConnector}).
*
* Note that, after your business, you should terminate this worker to prevent
* waste of memory leak. Close this worker by yourself ({@link close}) or let
* client to close this worker ({@link WorkerConnector.close}).
*
* @param provider An object providing features for the client.
*/
public async open(provider: Provider): Promise<void> {
// TEST CONDITION
if (is_node() === false) {
if (self.document !== undefined)
throw new Error("Error on WorkerServer.open(): this is not Worker.");
} else if ((await this.channel_.get()).is_worker_server() === false)
throw new Error("Error on WorkerServer.open(): this is not Worker.");
else if (this.state_ !== WorkerServer.State.NONE)
throw new Error(
"Error on WorkerServer.open(): the server has been opened yet.",
);
// OPEN WORKER
this.state_ = WorkerServer.State.OPENING;
this.provider_ = provider;
// GET HEADERS
await this.header_.get();
// SUCCESS
const channel = await this.channel_.get();
channel.onmessage = (evt) => this._Handle_message(evt);
channel.postMessage(WorkerServer.State.OPEN);
this.state_ = WorkerServer.State.OPEN;
}
/**
* @inheritDoc
*/
public async close(): Promise<void> {
// TEST CONDITION
const error: Error | null = this.inspectReady();
if (error) throw error;
//----
// CLOSE WORKER
//----
this.state_ = WorkerServer.State.CLOSING;
{
// HANDLERS
await this.destructor();
// DO CLOSE
setTimeout(async () => {
const channel = await this.channel_.get();
channel.postMessage(WorkerServer.State.CLOSING);
channel.close();
});
}
this.state_ = WorkerServer.State.CLOSED;
}
/* ----------------------------------------------------------------
ACCESSORS
---------------------------------------------------------------- */
/**
* @inheritDoc
*/
public get state(): WorkerServer.State {
return this.state_;
}
/**
* Get header containing initialization data like activation.
*/
public getHeader(): Promise<Header> {
return this.header_.get();
}
/**
* @hidden
*/
private _Handshake(
method: string,
timeout?: number,
until?: Date,
): Promise<any> {
return new Promise(async (resolve, reject) => {
/* eslint-disable */
let completed: boolean = false;
/* eslint-disable */
let expired: boolean = false;
if (until !== undefined)
sleep_until(until)
.then(() => {
if (completed === false) {
reject(
new Error(
`Error on WorkerConnector.${method}(): target worker is not sending handshake data over ${timeout} milliseconds.`,
),
);
expired = true;
}
})
.catch(() => {});
(await this.channel_.get()).onmessage = once((evt) => {
if (expired === false) {
completed = true;
resolve(evt.data);
}
});
});
}
/* ----------------------------------------------------------------
COMMUNICATOR
---------------------------------------------------------------- */
/**
* @hidden
*/
protected async sendData(invoke: Invoke): Promise<void> {
(await this.channel_.get()).postMessage(JSON.stringify(invoke));
}
/**
* @hidden
*/
protected inspectReady(): Error | null {
// NO ERROR
if (this.state_ === WorkerServer.State.OPEN) return null;
// ERROR, ONE OF THEM
else if (this.state_ === WorkerServer.State.NONE)
return new Error(
"Error on WorkerServer.inspectReady(): server is not opened yet.",
);
else if (this.state_ === WorkerServer.State.OPENING)
return new Error(
"Error on WorkerServer.inspectReady(): server is on opening, wait for a sec.",
);
else if (this.state_ === WorkerServer.State.CLOSING)
return new Error(
"Error on WorkerServer.inspectReady(): server is on closing.",
);
// MAY NOT BE OCCURRED
else if (this.state_ === WorkerServer.State.CLOSED)
return new Error(
"Error on WorkerServer.inspectReady(): the server has been closed.",
);
else
return new Error(
"Error on WorkerServer.inspectReady(): unknown error, but not connected.",
);
}
/**
* @hidden
*/
private _Handle_message(evt: MessageEvent): void {
if (evt.data === WorkerServer.State.CLOSING) this.close();
else this.replyData(JSON.parse(evt.data));
}
}
/**
*
*/
export namespace WorkerServer {
/**
* Current state of the {@link WorkerServer}.
*/
export import State = IServer.State;
}
//----
// POLYFILL
//----
/**
* @hidden
*/
interface IFeature {
close(): void;
postMessage(message: any): void;
onmessage(event: MessageEvent): void;
is_worker_server(): boolean;
}