/// namespace Leaf { export type RPCId = number export const RPCErrors = Leaf.ErrorDoc.create() .define("APINotExists") .define("Timeout") .define("Conflict") .define("BrokenConnection") .define("Abort") .generate() as ErrorsOf<{ APINotExists Timeout Conflict BrokenConnection Abort }> export interface RPCCallback { (error: Error, result: TResult): void } export interface RPCInvoke { (option?: TOption, callback?: RPCCallback): void RPC?: RPCDeclare } export interface RPCOption { name: string timeout?: number } export function RPCMethod() { return (target, propertyKey: string, descriptor: PropertyDescriptor) => { if (typeof descriptor.value == "function") { descriptor.value["__isRPCMethod"] = true } } } export interface RPCMethodHandler { (data: any, callback: { (err?: Error, data?: any): void }): void } export class RPCComposer { public declares: any = {} public requestOffset: number = 10000 public methods: { [index: string]: RPCMethodHandler } = {} public remoteEvents = new EventEmitter<{ "*": { [K in keyof E]: E[K] } } & { [P in keyof E]: E[P] }>() isOpen() { return !!this.connection } protected connection: RPCConnection private responseHandlers: RPCResponseHandler[] = [] public customNormalize: { (data: any): string } public customDenormalize: { (data: string): any } private allocateId() { return this.requestOffset++ } install(obj: any = this, decorator?: (fn: Function, name?: string) => Function) { for (let key in obj) { let v = obj[key] if (typeof v !== "function") continue if (v["__isRPCMethod"]) { if (v.length !== 2) { console.error(`RPCMethod ${key} must accept 2 arguments (option,callback), which accepts ${v.length}.`) continue } if (this.methods[key]) { console.error("Duplicate RPCMethod ${key}") continue } let method = v.bind(obj) if (decorator) { method = decorator(method, key) } this.registerRPCMethod(key, method) } } } constructor(public option: { defaultTimeout?: number } = {}) { if (!(typeof this.option.defaultTimeout === "number")) { this.option.defaultTimeout = 10000 } } public setConnection(con: RPCConnection) { this.connection = con this.connection.events.listenBy(this, "message", (message: string) => { this.handleMessage(message) }) this.connection.events.listenBy(this, "close", () => { this.unsetConnection() }) this.events.emit("open") } events = new Leaf.EventEmitter<{ open close timeout response: { method: string id: number error: Error value: any } }>() public unsetConnection() { if (!this.connection) return this.connection.events.stopListenBy(this) this.connection = null for (let handler of this.responseHandlers) { this.finishHandler(handler, new RPCErrors.Abort) } this.responseHandlers.length = 0 this.events.emit("close") } public revokeRPCMethod(name: string) { delete this.methods[name] } public registerRPCMethod(name: string, handler: RPCMethodHandler) { if (this.methods[name]) { throw new RPCErrors.Conflict(`RPC Method "${name}" already exists`) } this.methods[name] = handler } public createRPCDeclare(option: RPCOption): RPCDeclare { let api = new RPCDeclare(this, option) this.declares[option.name] = api return api } public createRPCInvokeInterface(option: RPCOption): RPCInvoke { let api = this.createRPCDeclare(option) this[option.name] = api return api.getFunction() } public emitRemoteEvent(name: string, data?: any) { let blob: RPCProtocolEventBlob = { name: name , data: data , type: RPCProtocolBlobType.EVENT } this.send(this.normalize(blob)) } public invoke(name: string, data: any, callback?: Function, option: { timeout?: number } = {}) { if (!callback) { callback = () => { } } if (!this.connection) { callback(new RPCErrors.BrokenConnection("Connection not exists")) return } let handler: RPCResponseHandler = { id: this.allocateId(), callback: callback, finished: false, timer: null, method: name } let timeout = option.timeout || this.option.defaultTimeout if (timeout > 0) { handler.timer = setTimeout(() => { if (!handler.finished) { this.finishHandlerById(handler.id, new RPCErrors.Timeout(`RPC timeout after ${timeout || this.option.defaultTimeout}`)) } this.events.emit("timeout") }, timeout) } this.responseHandlers.push(handler) let blob: RPCProtocolInvokeBlob = { id: handler.id, method: name, data: data, type: RPCProtocolBlobType.INVOKE } this.send(this.normalize(blob)) } protected serializer = new Serializer() protected serialize(data): string { return this.serializer.serialize(data) } protected deserialize(content: string): any { return this.serializer.deserialize(content) } private send(data: any) { if (!this.connection) return false if (typeof data !== "string") { data = this.serialize(data) } this.connection.send(data) return true } private handleMessage(rawString: string) { let data: any = null try { let raw = this.deserialize(rawString) data = this.denormalize(raw) } catch (e) { return } if (data.type === RPCProtocolBlobType.INVOKE) { this.handleInvoke(data) } else if (data.type === RPCProtocolBlobType.RESPONSE) { this.handleResponse(data) } else if (data.type === RPCProtocolBlobType.EVENT) { this.handleEvent(data) } } private handleInvoke(req: RPCProtocolInvokeBlob) { let response: RPCProtocolResponseBlob = { id: req.id, type: RPCProtocolBlobType.RESPONSE } if (!this.methods[req.method]) { response.error = new RPCErrors.APINotExists(`Requesting API ${req.method} not exists`) this.send(this.normalize(response)) return } let call = this.methods[req.method] let res = (err, data) => { response.error = err response.data = data this.send(this.normalize(response)) } try { call(req.data, res) } catch (e) { res(e, null) } } private handleResponse(res: RPCProtocolResponseBlob) { this.finishHandlerById(res.id, res.error, res.data) } private handleEvent(event: RPCProtocolEventBlob) { setTimeout(() => { this.remoteEvents.emit(event.name as any, event.data as any) this.remoteEvents.emit("*", event as any) }, 0) } private transformObject(data: any, handler: Function): any { if (typeof data !== "object") { return data } if (Array.isArray(data)) { return data.map((item) => { return this.transformObject(item, handler) }) } if (!data) return data let norm = data if (handler) { norm = handler(norm) } // changed by custom norm if (norm && norm !== data) { return norm } for (let prop in data) { data[prop] = this.transformObject(data[prop], handler) } return data } private normalize(data: any): any { return this.transformObject(data, this.customNormalize) } private denormalize(data: any): any { return this.transformObject(data, this.customDenormalize) } private finishHandlerById(id: RPCId, err?: Error, value?: any) { this.responseHandlers = this.responseHandlers.filter((handler) => { if (handler.id === id) { this.finishHandler(handler, err, value) return false } return true }) } private finishHandler(handler: RPCResponseHandler, err?: Error, value?: any) { handler.finished = true if (handler.timer) { clearTimeout(handler.timer) handler.timer = null } setTimeout(() => { this.events.emit("response", { method: handler.method, id: handler.id, error: err, value }) handler.callback(err, value) }, 0) } } export class RPCDeclare { constructor(private center: RPCComposer, private option: RPCOption) { } public getFunction(): RPCInvoke { let fn: RPCInvoke = this.invoke.bind(this) fn.RPC = this return fn } public invoke(data: TOption = {} as any, callback?: RPCCallback) { this.center.invoke(this.option.name, data, callback, { timeout: this.option.timeout || 0 }) } } export type RPCBaseConnectionMessage = string export interface RPCConnection { events: EventEmitter<{ message: string close: null open: null }> describe?(): string isOpen(): boolean send(data: string) close() } export abstract class RPCConnectionBase implements RPCConnection { events = new EventEmitter<{ message: string close: null open: null }>() public abstract send(data: string): void; public abstract close() public abstract isOpen(): boolean } export enum RPCProtocolBlobType { INVOKE = 1 , RESPONSE , EVENT } export interface RPCResponseHandler { id: RPCId callback: Function timer: any // timer for timeout the info finished: boolean method: string } export interface RPCProtocolBlob { type: RPCProtocolBlobType } export interface RPCProtocolResponseBlob extends RPCProtocolBlob { id: RPCId data?: any error?: any type: RPCProtocolBlobType } export interface RPCProtocolInvokeBlob extends RPCProtocolBlob { id: RPCId method: string data: any type: RPCProtocolBlobType } export interface RPCProtocolEventBlob extends RPCProtocolBlob { name: string data: any type: RPCProtocolBlobType } export class RPCBrowserWebSocketConnection extends RPCConnectionBase implements RPCConnection { constructor(public connection: WebSocket) { super() this.connection.addEventListener("open", (msg) => { this.events.emit("open") }) this.connection.addEventListener("message", (msg) => { this.events.emit("message", msg.data) }) this.connection.addEventListener("close", () => { this.events.emit("close") }) } send(content: string) { this.connection.send(content) } isOpen() { return this.connection.readyState == this.connection.OPEN } close() { this.connection.close() } describe() { return this.connection.url } } }