tgrid
Version:
Grid Computing Framework for TypeScript
288 lines (261 loc) • 9.43 kB
text/typescript
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);
}
}