diff --git a/README.md b/README.md index b2b009d..b2754e0 100644 --- a/README.md +++ b/README.md @@ -12,33 +12,58 @@ rpclibrary is a simple to use websocket RPC library. npm i rpclibrary ``` -# Quickstart +# Quickstart (TL;DR) ```typescript import {RPCServer, RPCSocket} from 'rpclibrary' -// TL;DR const echo = (text) => text const add = (a, b) => a + b +const subtract = (a, b) => a - b -new RPCServer(20000, [{ - name: 'MyRPCGroup1', - exportRPCs: () => [ - echo, - add, - ] -}]) +new RPCServer([ + { + name: 'MyRPCGroup1', + RPCs: [ + echo, + add, + ] + }, + name: 'MyOtherRPCGroup', + RPCs: [ + subtract + ] + } +]).listen(20000) new RPCSocket(20000, 'localhost').connect().then(async sock => { try{ - const RPCs = sock['MyRPCGroup1'] - await RPCs.echo("hello!").then(console.log) - await RPCs.add(1, Math.PI).then(console.log) + + await sock['MyRPCGroup1'].echo("hello!").then(console.log) + const sum = await sock['MyRPCGroup1'].add(1, Math.PI) + const result = await sock['MyOtherRPCGroup'].subtract(sum, 4) + }catch(e){ console.log(String(e)) } }) ``` +# Working with http.Server and express +```typescript +import {RPCServer, RPCSocket} from 'rpclibrary' +import {Server as httpServer} from 'http' +import * as express from 'express' + +const app = express() +//...do express things... +const httpServer = new http.Server(app) + +const rpcServer = new RPCServer([ ... ]) +rpcServer.attach(httpServer) +rpcServer.listen(8080) //identical to httpServer.listen(8080) + +``` + # Async and Callbacks? rpclibrary offers full support for callbacks and Promises. @@ -69,16 +94,16 @@ const getCallback = (callback) => { return "Please wait for a callback :)" } -new RPCServer(20000, [{ +new RPCServer([{ name: 'MyRPCGroup1', - exportRPCs: () => [ + RPCs: [ getAsync, { name: 'getCallback', hook: getCallback, } ] -}]) +}]).listen(20000) new RPCSocket(20000, 'localhost').connect().then(async sock => { try{ @@ -97,12 +122,10 @@ new RPCSocket(20000, 'localhost').connect().then(async sock => { There are a many things you can hook into to manage your connections ```typescript -new RPCServer(20001, [{ +new RPCServer([{ name: 'MyRPCGroup1', - exportRPCs: () => [ + RPCs: [ echo, - add, - getAsync, { name: 'getCallback', hook: getCallback, @@ -111,11 +134,10 @@ new RPCServer(20001, [{ } ], }], { - visibility: '127.0.0.1', //0.0.0.0 closeHandler: (socket) => { /* global close handler */ }, connectionHandler: (socket) => { /* new connection made */ }, errorHandler: (socket, error, rpcname, argArr) => { /* An error occured inside a RPC */ }, -}) +}).listen(20001) const sock = new RPCSocket(20001, 'localhost') sock.on('error', (e) => { /* handle error */ }) @@ -132,184 +154,151 @@ sock.connect().then(_ => { /* ... */}) # Restricting access -rpclibrary offers some minimalistic permission management +rpclibrary offers some minimalistic permission management in the form of a parameter prefix. +The ```sesame``` option will decorate all functions with a check for the sesame password. If the wrong string is provided, the RPC will not be executed or respond. ```typescript - -//Restricting access -new RPCServer(20002, [{ - name: 'MyRPCGroup1', - exportRPCs: () => [ - echo, - add, - getAsync, - { - name: 'getCallback', - hook: getCallback, - } - ], -}], { +new RPCServer([ ... ], { sesame: "sesame open", - /* - OR check sesame dynamically - and refine permissioning with accessfilter (optional) - */ - - //sesame: (sesame) => true - //accessFilter: (sesame, exporter) => { return exporter.name === "MyRPCGroup1" && sesame === "sesame open" }, -}) +}).listen(20002) new RPCSocket(20002, 'localhost').connect("sesame open").then(async sock => { - try{ - const RPCs = sock['MyRPCGroup1'] - await RPCs.echo("hello!").then(console.log) - await RPCs.add(1, Math.PI).then(console.log) - await RPCs.getAsync().then(console.log) - await RPCs.getCallback(console.log).then(console.log) - }catch(e){ - console.log(String(e)) - } + /* use normally */ }) - +new RPCSocket(20002, 'localhost').connect("WRONG SESAME").then(async sock => { + const RPCs = sock['MyRPCGroup1'] //undefined +}) ``` +The permissioning can also be performed dynamically with the ```accessfilter``` option and ```sesame``` function. + +```typescript +new RPCServer([ ... ], { + sesame: (sesame) => checkIfSesameTokenShouldHaveAnyAccess(sesame), + accessFilter: (sesame, rpcGroupName) => checkIfSesameTokenShouldSeeRPCGroup(sesame, rpcGroupName), +}).listen(20002) +``` # Typescript support rpclibrary is a typescript-first project and offers full support for typing your RPCs. -**NOTE** that your function implementations have to be currectly typed to make the compiler agree. -Explicit typing is recommended. +**NOTE** that your function implementations and Group Names will usually have to be manually typed to make the compiler infer the correct types. -Example: +First apply types to your functions: ```typescript echo = (x) => x - /*becomes*/ +/*becomes*/ echo = (x:string) : string => x + +/* A faulty implementation */ +badEcho = (x: boolean) : number => 3 + ``` - +Next define a type matching the desired Group-to-RPC structure. This kind of type will be called ```pseudo-interface```: ```typescript -type MyInterface = { +type MyType = { MyRPCGroup1: { echo: (x: string) => string - add: (a: number, b: number) => number - getAsync: () => Promise<{ topic: string, message: string }> - getCallback: (callback:Function) => string - } + //...further RPC declarations + }, + //... further Group declarations }; +``` +If ```MyType``` is provided as generic parameter to ```RPCServer```, ```RPCs``` becomes type-safe. +TSC's stack traces may become very long when errors happen in this section. Don't be intimidated by this. +The relevant information is usually found at the end in a clear manner. -/* -exportRPCs is now type safe. Try swapping echo for badEcho. -Sadly TSC's stack traces aren't the best, but try to scroll to the bottom of them to find useful info like +```typescript +new RPCServer([{ + name: 'MyRPCGroup1' as 'MyRPCGroup1', //Otherwise the compiler may infer type 'string' + RPCs: [ badEcho ], //Type '(x: boolean) => number' is not assignable to type '(x: string) => string' +}]).listen(20003) +``` -Type '(x: boolean) => number' is not assignable to type '(x: string) => string' -*/ - -const badEcho = (x: boolean) : number => 3 - -new RPCServer(20003, [{ - name: 'MyRPCGroup1', - exportRPCs: () => [ - //badEcho, - echo, - add, - getAsync, - { - name: 'getCallback', - hook: getCallback, - } - ], -}]) - -new RPCSocket(20003, 'localhost').connect().then(async sock => { - try{ - await sock.MyRPCGroup1.echo("hello!").then(console.log) - await sock.MyRPCGroup1.add(1, Math.PI).then(console.log) - await sock.MyRPCGroup1.getAsync().then(console.log) - await sock.MyRPCGroup1.getCallback(console.log).then(console.log) - }catch(e){ - console.log(String(e)) - } +Finally also provide ```MyType``` to the client ```RPCSocket``` to gain type safety for the socket object. +```typescript +new RPCSocket(20003, 'localhost').connect().then(async sock => { + //TSC knows about MyRPCGroup1 and its members + await sock.MyRPCGroup1.echo("hello!").then(console.log) }) ``` -# A class-based scalable pattern for APIs - -because long lists of functions quickly become unwieldy, it is smart to break up the RPCs into chunks or features. -A pattern I found to be useful is as follows: +# A class-based pattern for modular APIs +Define a regular Interface. This will function as the source of truth for type definitions. ```typescript -interface IMyImplementation { +interface IComponentA { echo: (x: string) => string add: (a: number, b: number) => number +} + +interface IComponentB { getAsync: () => Promise<{ topic: string, message: string }> getCallback: (callback:Function) => string } +``` -type MyInterface = { - MyRPCGroup1: { - echo: IMyImplementation['echo'] - add: IMyImplementation['add'] - getAsync: IMyImplementation['getAsync'] - getCallback: IMyImplementation['getCallback'] +Create an RPC pseudo-interface type that references the proper interface. This will make sure the types declared by your RPCs are consistent with their implementations. +```typescript +type TComponentA = { + ComponentA: { + echo: IComponentA['echo'] + add: IComponentA['add'] } } -class MyImplementation implements IMyImplementation, RPCExporter{ - //"X" as "X" syntax is required to satisfy the type system (as it assumes string to be the true type) - name = "MyRpcGroup11" as "MyRPCGroup1" +type TComponentB = { + ComponentB: { + getAsync: IComponentB['getAsync'] + getCallback: IComponentB['getCallback'] + } +} +``` - //List the functions you declared in MyInterface - exportRPCs = () => [ +Create classes that implement the interfaces you declared alongside ```RPCExporter```, where MyType is the RPC pseudo-interface. +```typescript +class ComponentA implements IComponentA, RPCExporter { + name = "ComponentA" as "ComponentA" + + RPCs = [ this.echo, this.add, - this.getAsync, - this.getCallback ] - //Write your implementations as you normally would echo = (text: string) => text - add = (a: number, b: number) : number => a + b - - getAsync = async () : Promise<{topic: string, message:string}>=> await new Promise((res, _) => { - setTimeout(() => { - res({ - topic: "Hey!!", - message: "Hello World Async!" - }) - }, 250) - }) - - getCallback = (callback: Function) : string => { - setTimeout(() => { - try{ - callback({ - topic: "Hey!!", - message: "Hello World Callback!" - }) - }catch(e){ - console.log(String(e)) - } - }, 250) - return "Please wait for a callback :)" - } } -type ProjectInterface = MyInterface - //& MyOtherInterface - //& MyOtherOtherInterface - // ... -; +class ComponentB implements IComponentB, RPCExporter { + name = "ComponentB" as "ComponentB" -new RPCServer(20004, [new MyImplementation() /*, new MyOtherImplementation(), new MyOtherOtherImplementation() */]) + RPCs = [ + this.getAsync, + { + name: 'getCallback', + hook: this.getCallback + } + ] -new RPCSocket(20004, 'localhost').connect().then(async sock => { - // ... + getAsync = async () : Promise<{topic: string, message:string}> => {...} + getCallback = (callback: Function) : string => {...} +} +``` + +Finally, merge all RPC pseudo-interfaces into a project-wide one: +```typescript +type TProject = TComponentA + & TComponentB + +new RPCServer([new ComponentA(), new ComponentB()]).listen(port) + +new RPCSocket(port, host).connect().then(async sock => { + }) ``` diff --git a/demo.ts b/demo.ts deleted file mode 100644 index affb4d0..0000000 --- a/demo.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { RPCServer, RPCSocket } from './Index' -import { RPCExporter } from './src/Interfaces' -import { RPCInterface } from './src/Types' - -// TL;DR -const echo = (text: string) => text -const add = (a: number, b: number) : number => a + b -const getAsync = async () : Promise<{topic: string, message:string}>=> await new Promise((res, _) => { - setTimeout(() => { - res({ - topic: "Hey!!", - message: "Hello World Async!" - }) - }, 250) -}) -const getCallback = (callback: Function) : string => { - setTimeout(() => { - try{ - callback({ - topic: "Hey!!", - message: "Hello World Callback!" - }) - }catch(e){ - console.log(String(e)) - } - }, 250) - return "Please wait for a callback :)" -} -new RPCServer(20000, [{ - name: 'MyRPCGroup1', - exportRPCs: () => [ - echo, - add, - getAsync, - { - name: 'getCallback', - hook: getCallback, - onClose: (response, rpc) => { /* ... */ }, - onCallback: (...callbackArgs) => { /* ... */ } - } - ] -}]) - -new RPCSocket(20000, 'localhost').connect().then(async sock => { - try{ - const RPCs = sock['MyRPCGroup1'] - await RPCs.echo("hello!").then(console.log) - await RPCs.add(1, Math.PI).then(console.log) - await RPCs.getAsync().then(console.log) - await RPCs.getCallback(console.log).then(console.log) - }catch(e){ - console.log(String(e)) - } -}) - -//Hooks and events -new RPCServer(20001, [{ - name: 'MyRPCGroup1', - exportRPCs: () => [ - echo, - add, - getAsync, - { - name: 'getCallback', - hook: getCallback, - onClose: (response, rpc) => { /* client disconnected */ }, - onCallback: (...callbackArgs) => { /* callback triggered */ } - } - ], -}], { - visibility: '127.0.0.1', //0.0.0.0 - closeHandler: (socket) => { /* global close handler */ }, - connectionHandler: (socket) => { /* new connection made */ }, - errorHandler: (socket, error, rpcname, argArr) => { /* An error occured inside a RPC */ }, -}) - -const sock = new RPCSocket(20001, 'localhost') -sock.on('error', (e) => { /* handle error */ }) -sock.on('close', () => { /* handle close event */ }) - -sock.hook('RPCName', (/* arg0, arg1, ..., argN */) => { /* bind client-side RPCs */ }) -//Retrieve the socket from connectionHandler (Server-side) and trigger with -//socket.call('RPCName', arg0, arg1, ..., argN) - -sock.connect().then(_ => { /* ... */}) - -//Restricting access -new RPCServer(20002, [{ - name: 'MyRPCGroup1', - exportRPCs: () => [ - echo, - add, - getAsync, - { - name: 'getCallback', - hook: getCallback, - } - ], -}], { - sesame: "sesame open", - /* - OR check sesame dynamically - and refine permissioning with accessfilter (optional) - */ - - //sesame: (sesame) => true - //accessFilter: (sesame, exporter) => { return exporter.name === "MyRPCGroup1" && sesame === "sesame open" }, -}) - -new RPCSocket(20002, 'localhost').connect("sesame open").then(async sock => { - try{ - const RPCs = sock['MyRPCGroup1'] - await RPCs.echo("hello!").then(console.log) - await RPCs.add(1, Math.PI).then(console.log) - await RPCs.getAsync().then(console.log) - await RPCs.getCallback(console.log).then(console.log) - }catch(e){ - console.log(String(e)) - } -}) - - -//TypeScript and pseudo-interfaces - -type MyInterface = { - MyRPCGroup1: { - echo: (x: string) => string - add: (a: number, b: number) => number - getAsync: () => Promise<{ topic: string, message: string }> - getCallback: (callback:Function) => string - } -}; - - -/* -exportRPCs is now type safe. Try swapping echo for badEcho. -Sadly TSC's stack traces aren't the best, but try to scroll to the bottom of them to find useful info like - -Type '(x: boolean) => number' is not assignable to type '(x: string) => string' -*/ - -const badEcho = (x: boolean) : number => 3 - -new RPCServer(20003, [{ - name: 'MyRPCGroup1', - exportRPCs: () => [ - //badEcho, - echo, - add, - getAsync, - { - name: 'getCallback', - hook: getCallback, - } - ], -}]) - -new RPCSocket(20003, 'localhost').connect().then(async sock => { - try{ - await sock.MyRPCGroup1.echo("hello!").then(console.log) - await sock.MyRPCGroup1.add(1, Math.PI).then(console.log) - await sock.MyRPCGroup1.getAsync().then(console.log) - await sock.MyRPCGroup1.getCallback(console.log).then(console.log) - }catch(e){ - console.log(String(e)) - } -}) - - -//Class-based pattern - -interface IMyImplementation { - echo: (x: string) => string - add: (a: number, b: number) => number - getAsync: () => Promise<{ topic: string, message: string }> - getCallback: (callback:Function) => string -} - -type IfcGenerator = { - [name in Name]: { - [key in keys] : Interface[key] - } -} - -type MyIfc = IfcGenerator<"MyRPCGroup1", IMyImplementation> - -class MyImplementation implements IMyImplementation, RPCExporter{ - //"X" as "X" syntax is required to satisfy the type system (as it assumes string) - name = "MyRpcGroup11" as "MyRPCGroup1" - - //List the functions you declared in MyIfc - exportRPCs = () => [ - this.echo, - this.add, - this.getAsync, - this.getCallback - ] - - //Write your implementations as you normally would - echo = (text: string) => text - - add = (a: number, b: number) : number => a + b - - getAsync = async () : Promise<{topic: string, message:string}>=> await new Promise((res, _) => { - setTimeout(() => { - res({ - topic: "Hey!!", - message: "Hello World Async!" - }) - }, 250) - }) - - getCallback = (callback: Function) : string => { - setTimeout(() => { - try{ - callback({ - topic: "Hey!!", - message: "Hello World Callback!" - }) - }catch(e){ - console.log(String(e)) - } - }, 250) - return "Please wait for a callback :)" - } -} - -new RPCServer(20004, [new MyImplementation(), /* ... other RPCExporters */]) - -new RPCSocket(20004, 'localhost').connect().then(async sock => { - try{ - await sock.MyRPCGroup1.echo("hello!").then(console.log) - await sock.MyRPCGroup1.add(1, Math.PI).then(console.log) - await sock.MyRPCGroup1.getAsync().then(console.log) - await sock.MyRPCGroup1.getCallback(console.log).then(console.log) - }catch(e){ - console.log(String(e)) - } -}) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b677f28..bda20d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "rpclibrary", - "version": "1.10.2", + "version": "1.12.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -631,6 +631,12 @@ "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", "dev": true }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", @@ -837,6 +843,41 @@ "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", "dev": true }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1001,6 +1042,12 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, "cacache": { "version": "12.0.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", @@ -1314,6 +1361,21 @@ "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", "dev": true }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, "convert-source-map": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", @@ -1328,6 +1390,12 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, "copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -1522,6 +1590,12 @@ } } }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, "des.js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", @@ -1532,6 +1606,12 @@ "minimalistic-assert": "^1.0.0" } }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, "detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", @@ -1573,6 +1653,12 @@ "stream-shift": "^1.0.0" } }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, "elliptic": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz", @@ -1600,6 +1686,12 @@ "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", "dev": true }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -1735,6 +1827,12 @@ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -1778,6 +1876,12 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, "events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", @@ -1868,6 +1972,67 @@ "homedir-polyfill": "^1.0.1" } }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -1995,6 +2160,38 @@ } } }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, "find-cache-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", @@ -2105,6 +2302,12 @@ } } }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -2114,6 +2317,12 @@ "map-cache": "^0.2.2" } }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, "from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -3008,12 +3217,42 @@ "resolved": "https://registry.npmjs.org/http/-/http-0.0.0.tgz", "integrity": "sha1-huYybSnF0Dnen6xYSkVon5KfT3I=" }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, "https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", @@ -3093,6 +3332,12 @@ "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", "dev": true }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -3721,6 +3966,12 @@ "safe-buffer": "^5.1.2" } }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, "mem": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", @@ -3742,6 +3993,18 @@ "readable-stream": "^2.0.1" } }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -3773,6 +4036,12 @@ "brorand": "^1.0.1" } }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, "mime-db": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", @@ -3970,6 +4239,12 @@ "semver": "^5.7.0" } }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", + "dev": true + }, "node-libs-browser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", @@ -4383,6 +4658,15 @@ "isobject": "^3.0.1" } }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4535,6 +4819,12 @@ "better-assert": "~1.0.0" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -4577,6 +4867,12 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, "pbkdf2": { "version": "3.0.17", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", @@ -4644,6 +4940,16 @@ "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", "dev": true }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "dev": true, + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -4703,6 +5009,12 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", @@ -4734,6 +5046,24 @@ "safe-buffer": "^5.1.0" } }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", @@ -4920,6 +5250,12 @@ "ret": "~0.1.10" } }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -4937,12 +5273,64 @@ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + } + } + }, "serialize-javascript": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", "dev": true }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -4978,6 +5366,12 @@ "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", "dev": true }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, "sha.js": { "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", @@ -5421,6 +5815,12 @@ } } }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, "stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", @@ -5697,6 +6097,12 @@ "repeat-string": "^1.6.1" } }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, "ts-loader": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-5.4.5.tgz", @@ -5784,6 +6190,16 @@ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -5915,6 +6331,12 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -6023,6 +6445,12 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, "uuid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", @@ -6034,6 +6462,12 @@ "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==", "dev": true }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, "vm-browserify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", diff --git a/package.json b/package.json index 70fefb8..1bb162a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "rpclibrary", - "version": "1.12.1", - "description": "rpclibrary is a websocket on steroids!", + "version": "2.0.0", + "description": "rpclibrary is a websocket RPC library", "main": "./js/Index.js", "repository": { "type": "git", @@ -38,7 +38,9 @@ "@types/expect": "^1.20.4", "@types/mocha": "^5.2.7", "@types/node": "^11.13.19", + "express": "^4.17.1", "mocha": "^6.2.0", + "node-fetch": "^2.6.0", "nyc": "^15.0.0", "ts-loader": "^5.3.3", "ts-mocha": "^6.0.0", diff --git a/scratchpad.ts b/scratchpad.ts deleted file mode 100644 index c751fce..0000000 --- a/scratchpad.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { RPCSocket, RPCServer } from "./Index" - - -// TL;DR -const echo = (text: string) => {throw new Error("XD")} -const add = (a: number, b: number) : number => a + b -const getAsync = async () : Promise<{topic: string, message:string}>=> await new Promise((res, _) => { - setTimeout(() => { - res({ - topic: "Hey!!", - message: "Hello World Async!" - }) - }, 250) -}) -const getCallback = (cbb: Function) : string => { - setTimeout(() => { - try{ - cbb({ - topic: "Hey!!", - message: "Hello World Callback!" - }) - }catch(e){ - console.log(String(e)) - } - }, 250) - return "Please wait for a callback :)" -} -new RPCServer(20000, [{ - name: 'MyRPCGroup1', - exportRPCs: () => [ - echo, - add, - getAsync, - { - name: 'getCallback', - hook: getCallback, - onClose: (response, rpc) => { }, - onCallback: (...callbackArgs) => { } - } - ] -}]) - - -new RPCSocket(20000, 'localhost').connect() -.then(async sock => { - try{ - const val = await sock.call('ABCD', 12345) - console.log("VAL",val); - - }catch(e){ - console.log("RPC error "+e) - } -}).catch(e => console.log("connect err: "+e)) diff --git a/src/Backend.ts b/src/Backend.ts index 28a16e3..f901837 100644 --- a/src/Backend.ts +++ b/src/Backend.ts @@ -11,12 +11,12 @@ export class RPCServer< > { private pio = PromiseIO.createServer() - private visibility: T.Visibility private closeHandler: T.CloseHandler private errorHandler: T.ErrorHandler private connectionHandler: T.ConnectionHandler private sesame?: T.SesameFunction private accessFilter: T.AccessFilter + private attached = false /** * @throws On RPC with no name @@ -25,13 +25,9 @@ export class RPCServer< * @param conf A {@link SocketConf} object with optional settings */ constructor( - private port: number, private exporters: T.ExporterArray = [], - private conf: T.ServerConf = {}, - private ws = http.createServer() + conf: T.ServerConf = {}, ) { - if (!conf.visibility) this.visibility = "0.0.0.0" - if (conf.sesame) { this.sesame = U.makeSesameFunction(conf.sesame) } @@ -57,7 +53,7 @@ export class RPCServer< exporters.forEach(U.fixNames) //TSC for some reason doesn't preserve name properties of methods - let badRPC = exporters.flatMap(ex => ex.exportRPCs()).find(rpc => !rpc.name) + let badRPC = exporters.flatMap(ex => typeof ex.RPCs === "function"?ex.RPCs():(ex as any)).find(rpc => !rpc.name) if (badRPC) { throw new Error(` RPC did not provide a name. @@ -67,32 +63,32 @@ export class RPCServer< \n`+ badRPC.toString() + ` \n>------------OFFENDING RPC`) } - - this.startWebsocket() - } - - private startWebsocket() { try { - this.pio.attach(this.ws) - this.pio.on('socket', (socket: I.Socket) => { socket.on('error', (err) => this.errorHandler(socket, err, "system", [])) socket.on('close', () => this.closeHandler(socket)) this.connectionHandler(socket) this.initRPCs(socket) }) - - if(this.conf.selfStart == null || this.conf.selfStart == true) - this.pio.listen(this.port) - } catch (e) { this.errorHandler(this.pio, e, 'system', []) } } + public attach = (httpServer = new http.Server()) : RPCServer => { + this.pio.attach(httpServer) + this.attached = true + return this + } + + public listen(port:number) : RPCServer{ + if(!this.attached) this.attach() + this.pio.listen(port) + return this + } + protected initRPCs(socket: I.Socket) { - socket.hook('info', async (sesame?: string) => { const rpcs = await Promise.all(this.exporters.map(async exp => { const allowed = await this.accessFilter(sesame, exp) @@ -105,6 +101,5 @@ export class RPCServer< close(): void { this.pio.close() - this.ws.close() } } \ No newline at end of file diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 8e09549..973ceac 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -9,13 +9,12 @@ export type RPCExporter< Name extends keyof Ifc = keyof Ifc, > = { name: Name - exportRPCs() : T.RPCDefinitions[Name] + RPCs : T.RPCDefinitions[Name] | (() => T.RPCDefinitions[Name]) } export interface Socket { id?: string bind: (name: string, listener: T.PioBindListener) => void - hook: (rpcname: string, handler: T.PioHookListener) => void unhook: (rpcname: string, listener?:T.AnyFunction) => void call: (rpcname: string, ...args: any[]) => Promise diff --git a/src/PromiseIO/Server.ts b/src/PromiseIO/Server.ts index 2fa3d0c..1a5cd16 100644 --- a/src/PromiseIO/Server.ts +++ b/src/PromiseIO/Server.ts @@ -6,6 +6,7 @@ const socketio = require('socket.io') export class PromiseIO { io?: Server + httpServer: httpServer private listeners: { [eventName in string]: ((...args: any) => void)[] } = { socket: [], connect: [] @@ -16,10 +17,8 @@ export class PromiseIO { } attach(httpServer: httpServer) { + this.httpServer = httpServer this.io = socketio(httpServer) - } - - listen(port: number) { this.io!.on('connection', (sock: Socket) => { const pioSock = U.makePioSocket(sock) this.listeners['socket'].forEach(listener => listener(pioSock)) @@ -36,7 +35,10 @@ export class PromiseIO { pioSock.on('reconnecting', ()=>console.log('reconnecting')); */ }) - this.io!.listen(port) + } + + listen(port: number) { + this.httpServer!.listen(port) } on(eventName: string, listener: T.AnyFunction) { diff --git a/src/Types.ts b/src/Types.ts index 4100a79..236ba7b 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -28,12 +28,10 @@ export type ExporterArray = I.RP export type ConnectedSocket = RPCSocket & AsyncIfc export type ServerConf = { - selfStart?: boolean accessFilter?: AccessFilter connectionHandler?: ConnectionHandler errorHandler?: ErrorHandler closeHandler?: CloseHandler - visibility?: Visibility } & SesameConf export type SocketConf = { diff --git a/src/Utils.ts b/src/Utils.ts index 4e7213f..114356a 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -61,7 +61,7 @@ RPC did not provide a name. */ export function rpcHooker(socket: I.Socket, exporter: I.RPCExporter, errorHandler: T.ErrorHandler, sesame?: T.SesameFunction, makeUnique = true): T.ExtendedRpcInfo[] { const owner = exporter.name - const RPCs = exporter.exportRPCs() + const RPCs = typeof exporter.RPCs === "function" ? exporter.RPCs() : exporter.RPCs return RPCs.map(rpc => rpcToRpcinfo(socket, rpc, owner, errorHandler, sesame)) .map(info => { @@ -262,7 +262,7 @@ export const makePioSocket = (socket: any): I.Socket => { close: () => { socket socket.disconnect(true) - }, + } } } diff --git a/test/Test.ts b/test/Test.ts index 0611a72..7206ace 100644 --- a/test/Test.ts +++ b/test/Test.ts @@ -3,13 +3,16 @@ import { RPCServer, RPCSocket } from '../Index' import { RPCExporter } from "../src/Interfaces"; import { ConnectedSocket } from "../src/Types"; import * as log from 'why-is-node-running'; +import * as http from 'http'; +import * as express from 'express'; +import * as fetch from 'node-fetch'; const add = (...args: number[]) => { return args.reduce((a, b) => a + b, 0) } function makeServer() { let subcallback - return new RPCServer(21010, [{ + const sv = new RPCServer([{ name: 'test', - exportRPCs: () => [ + RPCs: [ { name: 'echo', call: async (s: string) => s, @@ -37,12 +40,15 @@ function makeServer() { add, function triggerCallback(...messages: any[]): number { return subcallback.apply({}, messages) }, ] - }], { + }], + { connectionHandler: (socket) => { }, closeHandler: (socket) => { }, errorHandler: (socket, err) => { throw err } }) + sv.listen(21010) + return sv } @@ -51,9 +57,9 @@ describe('RPCServer', () => { const echo = (x) => x before(done => { - server = new RPCServer(21003, [{ + server = new RPCServer([{ name: 'HelloWorldRPCGroup', - exportRPCs: () => [ + RPCs: () => [ echo, //named function variable function echof(x) { return x }, //named function { @@ -62,11 +68,11 @@ describe('RPCServer', () => { } ] }]) - + server.listen(21003) client = new RPCSocket(21003, 'localhost') done() }) - + after(done => { client.close() server.close() @@ -83,7 +89,7 @@ describe('RPCServer', () => { if (r0 === 'Hello' && r1 === 'World' && r2.join('') === 'RPC!') { done() - }else{ + } else { done(new Error("Bad response")) } }) @@ -91,12 +97,13 @@ describe('RPCServer', () => { it('new RPCServer() should fail on bad RPC', (done) => { try { - new RPCServer(20001, [{ + const sv = new RPCServer([{ name: 'bad', - exportRPCs: () => [ + RPCs: () => [ (aaa, bbb, ccc) => { return aaa + bbb + ccc } ] }]) + sv.listen(20001) done(new Error("Didn't fail with bad RPC")) } catch (badRPCError) { done() @@ -104,6 +111,91 @@ describe('RPCServer', () => { }) }) +describe('RPCServer with premade http server', () => { + const echo = (x) => x + const RPCs = [ + echo, //named function variable + function echof(x) { return x }, //named function + { + name: 'echoExplicit', //describing object + call: async (x, y, z) => [x, y, z] + } + ] + + const RPCExporters = [ + { + name: 'HelloWorldRPCGroup', + RPCs: RPCs, + } + ] + + const RPCExporters2 = [ + { + name: 'Grp2', + RPCs: [ + function test(){ return "test" } + ], + } + ] + + let client:RPCSocket, server:RPCServer + + before(done => { + const expressServer = express() + const httpServer = new http.Server(expressServer) + + expressServer.get('/REST_ping', (req, res)=>{ + return res + .send('REST_pong') + .status(200) + }) + + server = new RPCServer( + RPCExporters, + ) + server.attach(httpServer) + httpServer.listen(8080) + + client = new RPCSocket(8080, 'localhost') + + done() + }) + + after(done => { + client.close() + server.close() + + done() + }) + + it('should serve REST', (done) => { + fetch('http://localhost:8080/REST_ping').then(response => { + response.text().then(text => { + if(text === "REST_pong") + done() + else + done(new Error("REST repsonse was "+text)) + }) + }) + }) + + + it('should be able to use all kinds of RPC definitions', (done) => { + client.connect().then(async () => { + const r0 = await client['HelloWorldRPCGroup'].echo('Hello') + const r1 = await client['HelloWorldRPCGroup'].echof('World') + const r2 = await client['HelloWorldRPCGroup'].echoExplicit('R', 'P', 'C!') + + + if (r0 === 'Hello' && r1 === 'World' && r2.join('') === 'RPC!') { + done() + } else { + done(new Error("Bad response")) + } + }) + }) +}) + describe('RPCSocket', () => { let client: RPCSocket @@ -111,6 +203,7 @@ describe('RPCSocket', () => { before(async () => { server = makeServer() + client = new RPCSocket(21010, "localhost") return await client.connect() }) @@ -183,9 +276,9 @@ describe('It should do unhook', () => { let server: RPCServer before(async () => { - server = new RPCServer(21010, [{ + server = new RPCServer([{ name: "test", - exportRPCs: () => [{ + RPCs: () => [{ name: 'subscribe', hook: async (callback): Promise => { cb = callback @@ -211,11 +304,13 @@ describe('It should do unhook', () => { function publish(): string { cb(candy); return candy }, function unsubscribe(): string { candy = noCandy; cb(candy); cb = () => { }; return candy } ] - }], { + }], + { connectionHandler: (socket) => { }, closeHandler: (socket) => { }, errorHandler: (socket, err) => { throw err } }) + server.listen(21010) client = new RPCSocket(21010, "localhost") return await client.connect() }) @@ -238,15 +333,15 @@ describe('It should do unhook', () => { const expected = [yesCandy, noCandy, noCandy, noCandy] it('Unhook+unsubscribe should stop callbacks', (done) => { - - client['test'].subscribe(function myCallback(c){ - if(run == 1) - (myCallback as any).destroy() + + client['test'].subscribe(function myCallback(c) { + if (run == 1) + (myCallback as any).destroy() if (c !== expected[run++]) { done(new Error(`Wrong candy '${c}' in iteration '${run - 1}'`)) } - }).then(async function(res){ + }).then(async function (res) { const r1 = await client['test'].publish() const r3 = await client['test'].unsubscribe() const r2 = await client['test'].publish() @@ -266,11 +361,11 @@ type SesameTestIfc = { test: { checkCandy: () => Promise subscribe: (callback: Function) => Promise - manyParams: (a:A, b:B, c:C, d:D) => Promise<[A, B, C, D]> + manyParams: (a: A, b: B, c: C, d: D) => Promise<[A, B, C, D]> } other: { - echo: (x:any) => Promise + echo: (x: any) => Promise } } @@ -281,9 +376,9 @@ describe('Sesame should unlock the socket', () => { let cb: Function = (...args) => { } before((done) => { - server = new RPCServer(21004, [{ + server = new RPCServer([{ name: "test", - exportRPCs: () => [ + RPCs: () => [ { name: 'subscribe', hook: async (callback) => { @@ -297,15 +392,16 @@ describe('Sesame should unlock the socket', () => { async function checkCandy() { cb(candy); cb = () => { }; return candy }, async function manyParams(a, b, c, d) { return [a, b, c, d] } ], - },{ + }, { name: 'other', - exportRPCs: () => [ - async function echo(x){return x} + RPCs: () => [ + async function echo(x) { return x } ] - + }], { sesame: (_sesame) => _sesame === 'sesame!' }) + server.listen(21004) const sock = new RPCSocket(21004, "localhost") sock.connect('sesame!').then(cli => { client = cli @@ -378,15 +474,14 @@ describe('Error handling', () => { } it("RPC throws on client without handler", (done) => { - let server = new RPCServer(21004, [{ + let server = new RPCServer([{ name: "createUser", - exportRPCs: () => [{ + RPCs: () => [{ name: 'createUser' as 'createUser', call: createUser }] - }], { - - }) + }]) + server.listen(21004) let sock = new RPCSocket(21004, 'localhost') sock.connect().then((cli) => { @@ -413,9 +508,9 @@ describe('Error handling', () => { }) it("RPC throws on server with handler", (done) => { - let server = new RPCServer(21004, [{ + let server = new RPCServer([{ name: "createUser", - exportRPCs: () => [{ + RPCs: () => [{ name: 'createUser' as 'createUser', call: createUser }] @@ -424,6 +519,7 @@ describe('Error handling', () => { done() } }) + server.listen(21004) let sock = new RPCSocket(21004, 'localhost') sock.connect().then((cli) => { @@ -457,9 +553,9 @@ describe("Errorhandler functionality", () => { } it("correct values are passed to the handler", (done) => { - let server = new RPCServer(21004, [{ + let server = new RPCServer([{ name: "createUser", - exportRPCs: () => [{ + RPCs: () => [{ name: 'createUser' as 'createUser', call: createUser }] @@ -469,6 +565,7 @@ describe("Errorhandler functionality", () => { done() } }) + server.listen(21004) let sock = new RPCSocket(21004, 'localhost') sock.connect().then((cli) => { @@ -493,9 +590,9 @@ describe("Errorhandler functionality", () => { it("handler sees sesame", (done) => { let sesame = "AAAAAAAAAAAAAAA" - let server = new RPCServer(21004, [{ + let server = new RPCServer([{ name: "createUser" as "createUser", - exportRPCs: () => [{ + RPCs: () => [{ name: 'createUser' as 'createUser', call: createUser }] @@ -507,6 +604,7 @@ describe("Errorhandler functionality", () => { } }) + server.listen(21004) let sock = new RPCSocket(21004, 'localhost') sock.connect(sesame).then((cli) => { @@ -547,7 +645,7 @@ describe("Class binding", () => { class MyExporter implements RPCExporter{ name = "MyExporter" as "MyExporter" - exportRPCs = () => [ + RPCs = () => [ this.myRPC ] @@ -559,7 +657,7 @@ describe("Class binding", () => { class MyOtherExporter implements RPCExporter{ name = "MyExporter" as "MyExporter" - exportRPCs = () => [ + RPCs = () => [ this.myRPC ] @@ -571,18 +669,21 @@ describe("Class binding", () => { before(done => { exporter1 = new MyExporter() - serv = new RPCServer(21004, [exporter1], { - accessFilter: async (sesame, exporter) => { - if(exporter.name === 'MyExporter'){ - if (!allowed) return false - allowed = false - return sesame === 'xxx'; - }else{ - return false - } - }, - sesame: "xxx" - }) + serv = new RPCServer( + [exporter1], + { + accessFilter: async (sesame, exporter) => { + if (exporter.name === 'MyExporter') { + if (!allowed) return false + allowed = false + return sesame === 'xxx'; + } else { + return false + } + }, + sesame: "xxx" + }) + serv.listen(21004) done() }) @@ -630,7 +731,7 @@ describe("Class binding", () => { it("use sesameFilter for available", (done) => { - if (sock['MyExporter']){ + if (sock['MyExporter']) { allowed = false done() } @@ -722,6 +823,6 @@ describe("attaching handlers before connecting", () => { describe('finally', () => { it('print open handles (Ignore `DNSCHANNEL` and `Immediate`)', () => { - log() + //log(console) }) }) \ No newline at end of file