diff --git a/src/Backend.ts b/src/Backend.ts index 225329f..4aa4b1e 100644 --- a/src/Backend.ts +++ b/src/Backend.ts @@ -5,6 +5,7 @@ import { PromiseIO } from "./PromiseIO/Server"; import * as T from './Types'; import * as U from './Utils'; import * as I from './Interfaces'; +import { BAD_CONFIG_PARAM, UNKNOWN_RPC_IDENTIFIER, UNKNOWN_RPC_SERVER } from './Strings'; export class RPCServer< InterfaceT extends T.RPCInterface = T.RPCInterface, @@ -45,7 +46,7 @@ export class RPCServer< if (conf.errorHandler) conf.errorHandler(socket, error, rpcName, args) else { if (forward) { - socket.call("$UNKNOWNRPC$", error) + socket.call(UNKNOWN_RPC_IDENTIFIER, error) } else { throw error } @@ -96,7 +97,7 @@ export class RPCServer< this.attach(undefined, options) } else { if (options) - console.warn("RPCServer options were passed to listen(..) after attach(..) was called. Please pass them to attach(..) instead. Ignoring.") + console.warn(BAD_CONFIG_PARAM) } this.pio.listen(port) return this @@ -114,7 +115,7 @@ export class RPCServer< clientSocket.on("*", (packet) => { if (!infos.some(i => i.uniqueName === packet.data[0])) { if (packet.data[0].startsWith('destroy_')) return - this.errorHandler(clientSocket, new Error(`Unknown RPC ${packet.data[0]}`), packet.data[0], [...packet.data].splice(1), true) + this.errorHandler(clientSocket, new Error(UNKNOWN_RPC_SERVER(packet.data[0])), packet.data[0], [...packet.data].splice(1), true) } }) } diff --git a/src/Frontend.ts b/src/Frontend.ts index 25e822e..95eda1c 100644 --- a/src/Frontend.ts +++ b/src/Frontend.ts @@ -4,6 +4,7 @@ import { PromiseIOClient, defaultClientConfig } from './PromiseIO/Client' import * as T from './Types'; import * as I from './Interfaces'; import { stripAfterEquals, appendComma } from './Utils'; +import { SOCKET_NOT_CONNECTED, UNKNOWN_RPC_IDENTIFIER, USER_DEFINED_TIMEOUT } from './Strings'; /** @@ -33,7 +34,7 @@ export class RPCSocket implements I */ constructor(public port: number, public address: string, private conf: T.ClientConfig = defaultClientConfig) { Object.defineProperty(this, 'socket', { value: undefined, writable: true }) - this.hook("$UNKNOWNRPC$", (err) => this.handlers['error'].forEach(handler => handler(err))) + this.hook(UNKNOWN_RPC_IDENTIFIER, (err) => this.handlers['error'].forEach(handler => handler(err))) } /** @@ -114,11 +115,19 @@ export class RPCSocket implements I * @param args other arguments */ public async call(rpcname: string, ...args: any[]): Promise { - if (!this.socket) throw new Error("The socket is not connected! Use socket.connect() first") + if (!this.socket) throw new Error(SOCKET_NOT_CONNECTED) + try { - const val = await this.socket.call.apply(this.socket, [rpcname, ...args]) - return val + if(!this.conf.callTimeoutMs || this.conf.callTimeoutMs <= 0) + return await this.socket.call.apply(this.socket, [rpcname, ...args]) + else + return await Promise.race([ + this.socket.call.apply(this.socket, [rpcname, ...args]), + new Promise((_, rej) => { + setTimeout(_ => rej(USER_DEFINED_TIMEOUT(this.conf.callTimeoutMs)), this.conf.callTimeoutMs) + }) + ]) } catch (e) { this.emit('error', e) throw e @@ -131,7 +140,7 @@ export class RPCSocket implements I * @param args other arguments */ public async fire(rpcname: string, ...args: any[]): Promise { - if (!this.socket) throw new Error("The socket is not connected! Use socket.connect() first") + if (!this.socket) throw new Error(SOCKET_NOT_CONNECTED) await this.socket.fire.apply(this.socket, [rpcname, ...args]) } @@ -181,7 +190,7 @@ export class RPCSocket implements I * Get a list of available RPCs from the server */ public async info(sesame?: string) { - if (!this.socket) throw new Error("The socket is not connected! Use socket.connect() first") + if (!this.socket) throw new Error(SOCKET_NOT_CONNECTED) return await this.socket.call('info', sesame) } diff --git a/src/Interfaces.ts b/src/Interfaces.ts index f34eae2..351df96 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -1,5 +1,4 @@ import * as T from "./Types"; -import * as I from "./Interfaces" /** * Interface for all classes that export RPCs diff --git a/src/PromiseIO/Client.ts b/src/PromiseIO/Client.ts index cd705cd..868353b 100644 --- a/src/PromiseIO/Client.ts +++ b/src/PromiseIO/Client.ts @@ -10,6 +10,7 @@ export const defaultClientConfig: ClientConfig = { reconnectionDelay: 200, timeout: 450, reconnection: false, + callTimeoutMs: 0, } export class PromiseIOClient { diff --git a/src/Strings.ts b/src/Strings.ts new file mode 100644 index 0000000..b1063ad --- /dev/null +++ b/src/Strings.ts @@ -0,0 +1,14 @@ +export const USER_DEFINED_TIMEOUT = ms => `User defined socket timout after ${ms}ms` +export const SOCKET_NOT_CONNECTED = "The socket is not connected! Use socket.connect() first" +export const UNKNOWN_RPC_IDENTIFIER = "$UNKNOWNRPC$" +export const UNKNOWN_RPC_SERVER = name => `Unknown RPC ${name}` +export const RPC_BAD_TYPE = type => `Bad socketIORPC type ${type}` +export const CALL_NOT_FOUND = callName => `Call not found: ${callName}. ; Zone: ; Task: Promise.then ; Value: Error: Call not found: ${callName}` +export const BAD_CONFIG_PARAM = "RPCServer options were passed to listen(..) after attach(..) was called. Please pass them to attach(..) instead. Ignoring." +export const RPC_NO_NAME = name => ` +RPC did not provide a name. +\nUse 'funtion name(..){ .. }' syntax instead. +\n +\n<------------OFFENDING RPC: +\n${name} +\n>------------OFFENDING RPC` \ No newline at end of file diff --git a/src/Types.ts b/src/Types.ts index 1aaa659..4b5dcc5 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -21,7 +21,8 @@ export type FrontEndHandlerType = { 'close' : () => void } export type ClientConfig = SocketIOClient.ConnectOpts & { - protocol?: 'http' | 'https' + protocol?: 'http' | 'https', + callTimeoutMs?: number } export type ExporterArray = I.RPCExporter, keyof InterfaceT>[] diff --git a/src/Utils.ts b/src/Utils.ts index 9e85c50..dd9857b 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -3,6 +3,7 @@ import * as uuidv4 from "uuid/v4" import * as T from "./Types"; import * as I from "./Interfaces"; import { Socket } from "socket.io" +import { CALL_NOT_FOUND, RPC_BAD_TYPE, RPC_NO_NAME } from "./Strings"; /** * Translate an RPC to RPCInfo for serialization. @@ -36,13 +37,7 @@ export const rpcToRpcinfo = (socket: I.Socket, rpc: T.RPC, owner: stri } } case "function": - if (!rpc.name) throw new Error(` -RPC did not provide a name. -\nUse 'funtion name(..){ .. }' syntax instead. -\n -\n<------------OFFENDING RPC: -\n${rpc.toString()} -\n>------------OFFENDING RPC`) + if (!rpc.name) throw new Error(RPC_NO_NAME(rpc.toString())) return { owner: owner, argNames: extractArgs(rpc), @@ -51,7 +46,7 @@ RPC did not provide a name. call: sesame ? async ($__sesame__$, ...args) => { if (sesame($__sesame__$)) return await rpc.apply({}, args); throw makeError(rpc.name) } : rpc, // check & remove sesame } } - throw new Error("Bad socketIORPC type " + typeof rpc) + throw new Error(RPC_BAD_TYPE(typeof rpc)) } /** @@ -141,7 +136,7 @@ const hookGenerator = (rpc: T.HookRPC, errorHandler: T.ErrorHandler, s return eval(hookStr) } -const makeError = (callName: string) => new Error(`Call not found: ${callName}. ; Zone: ; Task: Promise.then ; Value: Error: Call not found: ${callName}`) +const makeError = (callName: string) => new Error(CALL_NOT_FOUND(callName)) /** * Extract a string list of parameters from a function diff --git a/test/Test.ts b/test/Test.ts index 049a9bf..2431493 100644 --- a/test/Test.ts +++ b/test/Test.ts @@ -9,8 +9,12 @@ import * as fetch from 'node-fetch'; import { PromiseIO } from "../src/PromiseIO/Server"; import { PromiseIOClient } from "../src/PromiseIO/Client"; import { assert, expect } from 'chai'; +import { USER_DEFINED_TIMEOUT } from "../src/Strings"; var should = require('chai').should(); - +var chai = require("chai"); +var chaiAsPromised = require("chai-as-promised"); + +chai.use(chaiAsPromised); const noop = (...args) => { } const add = (...args: number[]) => { return args.reduce((a, b) => a + b, 0) } @@ -311,6 +315,7 @@ describe('can attach multiple RPCServers to same http server', () => { ], } ] + const callTimeout = 100; let client: RPCSocket, client2: RPCSocket, server: RPCServer, server2: RPCServer @@ -334,7 +339,7 @@ describe('can attach multiple RPCServers to same http server', () => { new RPCSocket(8080, 'localhost').connect().then(sock => { client = sock - new RPCSocket(8080, 'localhost', { path: "test" }).connect().then(sock2 => { + new RPCSocket(8080, 'localhost', { path: "test", callTimeoutMs: callTimeout }).connect().then(sock2 => { client2 = sock2 done() }) @@ -356,6 +361,13 @@ describe('can attach multiple RPCServers to same http server', () => { const res2 = await client2['Grp2'].test() expect(res2).to.equal('/test') }) + + it('server1 should answer after server2 is closed', async () => { + server2.close() + const res = await client['HelloWorldRPCGroup'].echo("test") + expect(res).to.equal('test') + return client2['Grp2'].test().should.eventually.be.rejectedWith(USER_DEFINED_TIMEOUT(callTimeout)) + }) }) describe("can attach second RPCServer if first is already running", () => {