///
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
}
}
}