From 8ae08a65f2ccb2238619a307efb495fab8b7f1c3 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Fri, 18 May 2018 18:57:02 -0500 Subject: [PATCH] Release 1.0.0-beta.1 :rocket: GitHub Milestone: https://github.com/onixjs/core/milestone/2?closed=1 - Fix: https://github.com/onixjs/core/issues/43 - Fix: https://github.com/onixjs/core/issues/42 - Fix: https://github.com/onixjs/core/issues/41 - Fix: https://github.com/onixjs/core/issues/47 --- package.json | 6 +- src/core/acl.group.match.ts | 2 +- src/core/acl.groups.ts | 3 +- src/core/app.factory.ts | 186 +++---------------- src/core/app.route.ts | 250 ++++++++++++++++++++++++++ src/core/app.server.ts | 70 +++++--- src/core/call.connect.ts | 22 ++- src/core/call.responser.ts | 8 +- src/core/call.streamer.ts | 5 +- src/core/host.broker.ts | 202 +++++++++++++++++---- src/core/index.ts | 1 + src/core/lifecycle.ts | 20 +-- src/core/notify.events.ts | 9 + src/decorators/rpc.ts | 8 +- src/index.ts | 57 +++--- src/interfaces/index.ts | 72 ++------ test/bob.app/modules/bob.component.ts | 4 +- test/decorators.unit.ts | 2 + test/onixjs.acceptance.ts | 45 +++-- test/onixjs.core.unit.ts | 152 +++++++++------- test/todo.shared/todo.module.ts | 3 +- 21 files changed, 710 insertions(+), 417 deletions(-) create mode 100644 src/core/app.route.ts create mode 100644 src/core/notify.events.ts diff --git a/package.json b/package.json index 2cbfadc..3ddf18d 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,12 @@ "build:watch": "tsc --watch", "build:docs": "typedoc --out ./documentation ./src", "clean": "rm -rf dist dist6", - "lint": "npm run prettier:check && npm run lint:fix && npm run tslint", + "lint": "npm run prettier:fix && npm run prettier:check && npm run lint:fix && npm run tslint", "lint:fix": "npm run prettier:fix", "prettier:cli": "prettier \"**/*.ts\" \"**/*.js\"", "prettier:check": "npm run prettier:cli -- -l", "prettier:fix": "npm run prettier:cli -- --write", - "tslint": "tslint -c tslint.full.json --project tsconfig.json --type-check", + "tslint": "tslint -c tslint.full.json --project tsconfig.json", "tslint:fix": "npm run lint -- --fix", "prepublish": "npm run lint:fix && npm run clean && npm run build", "pretest": "npm run build", @@ -49,7 +49,7 @@ }, "dependencies": { "@onixjs/enumerable": "1.0.0-alpha.7", - "@onixjs/sdk": "^1.0.0-alpha.14", + "@onixjs/sdk": "^1.0.0-beta.1", "finalhandler": "^1.1.1", "reflect-metadata": "^0.1.12", "router": "^1.3.2" diff --git a/src/core/acl.group.match.ts b/src/core/acl.group.match.ts index 073b28c..c66458b 100644 --- a/src/core/acl.group.match.ts +++ b/src/core/acl.group.match.ts @@ -1,5 +1,4 @@ import { - IAppOperation, IComponentConfig, promiseSeries, IACLRule, @@ -7,6 +6,7 @@ import { Injector, IModuleConfig, } from '..'; +import {IAppOperation} from '@onixjs/sdk'; /** * @class GroupMatch * @author Joanthan Casarrubias diff --git a/src/core/acl.groups.ts b/src/core/acl.groups.ts index 8ae77bf..da8b590 100644 --- a/src/core/acl.groups.ts +++ b/src/core/acl.groups.ts @@ -1,4 +1,5 @@ -import {IRequest, AccessType, IGroup} from '../interfaces'; +import {AccessType, IGroup} from '../interfaces'; +import {IRequest} from '@onixjs/sdk'; /** * @namespace Groups * @author Jonathan Casarrubias diff --git a/src/core/app.factory.ts b/src/core/app.factory.ts index 8d5b927..ac4b312 100644 --- a/src/core/app.factory.ts +++ b/src/core/app.factory.ts @@ -10,13 +10,11 @@ import { RouterTypes, } from '../interfaces'; import {Injector} from '../core'; -import {getObjectMethods, promiseSeries, AsyncWalk} from '../utils'; -import {AppNotifier, IMiddleware} from '..'; +import {getObjectMethods, promiseSeries} from '../utils'; +import {AppNotifier} from '..'; import * as Router from 'router'; -import * as fs from 'fs'; import * as path from 'path'; -import {Utils} from '@onixjs/sdk/dist/utils'; -import {promisify} from 'util'; +import {AppRoute} from './app.route'; /** * @class AppFactory * @author Jonathan Casarrubias @@ -184,25 +182,18 @@ export class AppFactory { /** * @method routing * @param instance - * @description - * Keep routin' routin' routin' routin' (what?) - * Keep routin' routin' routin' routin' (come on) - * Keep routin' routin' routin' routin' (yeah) - * Keep routin' routin' routin' routin' - * Now I know why'all be lovin' this shit right here - * O.N.I.X Onix is right here - * People in the house put them hands in the air - * 'Cause if you don't care, then we don't care - * -------------------------------------------------- - * /W Love: Jon + * @description This method will configure every decorated + * method using any of the provided @Router decorators. + * Those having decorated configs, will load the associated + * class Route class. */ - routing(instance, router: Router): void { + routing(component, router: Router): void { // Iterate over component methods - getObjectMethods(instance).forEach((method: string) => { + getObjectMethods(component).forEach((method: string) => { // Try to get middleware config now - const config: IMiddleware = Reflect.getMetadata( + const config = Reflect.getMetadata( ReflectionKeys.MIDDLEWARE, - instance, + component, method, ); // Verify we actually got a middleware config @@ -211,165 +202,42 @@ export class AppFactory { case RouterTypes.HTTP: case RouterTypes.USE: case RouterTypes.ALL: - if (config.endpoint) { - this.router[config.method.toLowerCase()]( - config.endpoint, - async (req, res, next) => - await this.routeWrapper(instance, method, req, res, next), - ); - } else { - this.router[config.method.toLowerCase()]( - async (req, res, next) => - await this.routeWrapper(instance, method, req, res, next), - ); - } + new AppRoute.Default(component, method, this.router, config); break; case RouterTypes.PARAM: this.router.param( config.param!.name, async (req, res, next, param) => { - req[config.param!.as] = await instance[method](req, param); + req[config.param!.as] = await component[method](req, param); next(); }, ); break; case RouterTypes.STATIC: - this.router.use(async (req, res, next) => { - const pathname: string = path.join( + new AppRoute.Static( + component, + method, + path.join( this.config.cwd || process.cwd(), config.endpoint || '/', - ); - try { - const AsyncLStat = promisify(fs.lstat); - const stats: fs.Stats = await AsyncLStat(pathname); - let match: string | undefined; - if (stats.isDirectory()) { - const files: string[] = await AsyncWalk(pathname); - match = files - .filter((file: string) => - file.match(new RegExp('\\b' + req.url + '\\b', 'g')), - ) - .pop(); - } - await this.view( - instance, - method, - config, - match || pathname, - req, - res, - next, - ); - } catch (e) { - next(); - } - }); + ), + this.router, + config, + ); break; case RouterTypes.VIEW: - this.router[config.method.toLowerCase()]( - config.endpoint || config.file, - async (req, res, next) => - await this.view( - instance, - method, - config, - path.join( - this.config.cwd || process.cwd(), - config.file || '', - ), - req, - res, - next, - ), + new AppRoute.View( + component, + method, + path.join(this.config.cwd || process.cwd(), config.file || ''), + this.router, + config, ); break; } } }); } - /** - * @method routeWrapper - * @param instance - * @param method - * @param err - * @param req - * @param res - * @param next - * @description This handler will provide shared functionality - * when registering different type of route features. - */ - async routeWrapper(instance, method, req, res, next) { - // Potentially register LifeCycles in here. - const result = await instance[method](req, res, next); - // If the method returned a value, otherwise they might response their selves - if (result) { - // Send result to the requester - res.end(Utils.IsJsonString(result) ? JSON.stringify(result) : result); - } - } - /** - * @method view - * @param ctx - * @param req - * @param res - * Will server view files - */ - private async view(instance, method, config, pathname, req, res, next) { - if (req.url && req.method) { - // Try to get a file extension - const ext = path.parse(pathname).ext; - // maps file extention to MIME typere - const map = { - '.ico': 'image/x-icon', - '.html': 'text/html', - '.js': 'text/javascript', - '.json': 'application/json', - '.css': 'text/css', - '.png': 'image/png', - '.jpg': 'image/jpeg', - '.wav': 'audio/wav', - '.mp3': 'audio/mpeg', - '.svg': 'image/svg+xml', - '.pdf': 'application/pdf', - '.doc': 'application/msword', - '.woff': 'application/font-woff', - '.ttf': 'application/font-ttf', - '.eot': 'application/vnd.ms-fontobject', - '.otf': 'application/font-otf', - }; - // Promisify exists and readfile - const AsyncExists = promisify(fs.exists); - const AsyncReadFile = promisify(fs.readFile); - try { - // Verify the pathname exists - const exist: boolean = await AsyncExists(pathname); - // If not, return 404 code - if (!exist) { - // if the file is not found, return 404 - res.statusCode = 404; - return res.end( - JSON.stringify({ - code: res.statusCode, - message: `Oops!!! something went wrong.`, - }), - ); - } - try { - // read file from file system - const data = await AsyncReadFile(pathname); - // Potentially get cookies and headers - const result = await instance[method](req, data); - // Set response headers - res.setHeader('Content-type', map[ext] || 'text/plain'); - res.end(Utils.IsJsonString(result) ? JSON.stringify(result) : result); - } catch (e) { - next(); - } - } catch (e) { - next(); - } - } - } /** * @method schema * @description This method will build an app schema that will be exposed diff --git a/src/core/app.route.ts b/src/core/app.route.ts new file mode 100644 index 0000000..24e00e8 --- /dev/null +++ b/src/core/app.route.ts @@ -0,0 +1,250 @@ +import * as Router from 'router'; +import {IMiddleware, IComponent} from '..'; +import {promisify} from 'util'; +import * as fs from 'fs'; +import {AsyncWalk} from '../utils'; +import * as path from 'path'; +import {Utils} from '@onixjs/sdk/dist/utils'; +/** + * @namespace AppRoute + * @author Jonathan Casarrubias + * @description Provides a set of classes that will configure + * specific app routes. + */ +export namespace AppRoute { + /** + * @class Default + * @author Jonathan Casarrubias + * @description This class will configure most of the middlewares, + * basically creating a route using the endpoint -if provided- and + * the provided HTTP Method. + * + * Once these routes are executed it will load the component method + * associated with the current request. + * + * Developers are provided with 2 options to terminate a middleware + * process. + * + * 1.- (Recommended) just return the result within the component method. + * 2.- (Advanced) sometimes a middleware based component requires to end + * a request, then a req and res objects are provided within the route. + * A module or developer ending a middleware process, will execute + * the req.end() method and no other middlewares will be executd. + */ + export class Default { + constructor( + component: IComponent, + method: string, + router: Router, + config: IMiddleware, + ) { + if (config.endpoint) { + router[config.method.toLowerCase()]( + config.endpoint, + async (req, res, next) => + await this.wrapper(component, method, req, res, next), + ); + } else { + router[config.method.toLowerCase()]( + async (req, res, next) => + await this.wrapper(component, method, req, res, next), + ); + } + } + /** + * @method wrapper + * @param instance + * @param method + * @param req + * @param res + * @param next + * @description This handler will provide shared functionality + * when registering different type of route features. + */ + async wrapper(instance, method, req, res, next) { + // Potentially register LifeCycles in here. + const result = await instance[method](req, res, next); + // If the method returned a value, otherwise they might response their selves + if (result) { + // Send result to the requester + res.end(typeof result === 'object' ? JSON.stringify(result) : result); + } + } + } + /** + * @class Static + * @author Jonathan Casarrubias + * @description this class will register a new static + * route, it will verify if the provided pathname is + * a static file otherwise if a directory it will verify + * the req url to see if the file exist in the path directory. + * + * Objectives: + * + * 1.- It can register a static file like index.html + * 2.- It can register a directory, if a request includes a + * file contained within that registered directory, then + * this class will dynamically load that file. + * e.g. /assets directory + */ + export class Static { + constructor( + component: IComponent, + method: string, + pathname: string, + router: Router, + config: IMiddleware, + ) { + router.use(async function(req, res, next) { + // Verify if this static middleware should treat + // this request, otherwise just call the next one + if (!pathname.includes(req.url)) { + return next(); + } + // Ok just make sure we got a filename + try { + const AsyncLStat = promisify(fs.lstat); + const stats: fs.Stats = await AsyncLStat(pathname); + let match: string | undefined; + if (stats.isDirectory()) { + const files: string[] = await AsyncWalk(pathname); + match = files + .filter((file: string) => + file.match(new RegExp('\\b' + req.url + '\\b', 'g')), + ) + .pop(); + } + // Load file and method for this route. + await AppRoute.Load.file( + component, + method, + config, + match || pathname, + req, + res, + next, + ); + } catch (e) { + next(); + } + }); + } + } + /** + * @class View + * @author Jonathan Casarrubias + * @description This class will register a view endpoint. + * Similar to registering using AppRoute.Static class, it + * can also load a static file, being the AppRoute.View + * a little more advance since it also allows to register + * an alias path. + * + * Objectives: + * + * 1.- Register static files e.g. /my/awesome/file.html + * 2.- Register static files with endpoint associated + * e.g. endpoint: /cool/path, file: /my/hidden/path/file.html + * + * Usage: + * + * It has to be used through the @Router.View decorator. + * + * Examples: + * + * - 1.- @Router.View({ file: '/my/awesome/file.html' }) + * - 2.- @Router.View({ + * endpoint: '/cool/path', + * file: '/my/hidden/path/file.html' + * }) + */ + export class View { + constructor( + component: IComponent, + method: string, + pathname: string, + router: Router, + config: IMiddleware, + ) { + router[config.method.toLowerCase()]( + config.endpoint || config.file, + async (req, res, next) => + await AppRoute.Load.file( + component, + method, + config, + pathname, + req, + res, + next, + ), + ); + } + } + /** + * @class Load + * @author Jonathan Casarrubias + * @description This class will load static files from the provided + * configuration, then it will execute the component method that + * registered this middleware, in order to be used and/or modified. + */ + export class Load { + static async file(instance, method, config, pathname, req, res, next) { + if (req.url && req.method) { + // Try to get a file extension + const ext = path.parse(pathname).ext; + // maps file extention to MIME typere + const map = { + '.ico': 'image/x-icon', + '.html': 'text/html', + '.js': 'text/javascript', + '.json': 'application/json', + '.css': 'text/css', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.wav': 'audio/wav', + '.mp3': 'audio/mpeg', + '.svg': 'image/svg+xml', + '.pdf': 'application/pdf', + '.doc': 'application/msword', + '.woff': 'application/font-woff', + '.ttf': 'application/font-ttf', + '.eot': 'application/vnd.ms-fontobject', + '.otf': 'application/font-otf', + }; + // Promisify exists and readfile + const AsyncExists = promisify(fs.exists); + const AsyncReadFile = promisify(fs.readFile); + try { + // Verify the pathname exists + const exist: boolean = await AsyncExists(pathname); + // If not, return 404 code + if (!exist) { + // if the file is not found, return 404 + res.statusCode = 404; + return res.end( + JSON.stringify({ + code: res.statusCode, + message: `Oops!!! something went wrong.`, + }), + ); + } + try { + // read file from file system + const data = await AsyncReadFile(pathname); + // Potentially get cookies and headers + const result = await instance[method](req, data); + // Set response headers + res.setHeader('Content-type', map[ext] || 'text/plain'); + res.end( + Utils.IsJsonString(result) ? JSON.stringify(result) : result, + ); + } catch (e) { + next(); + } + } catch (e) { + next(); + } + } + } + } +} diff --git a/src/core/app.server.ts b/src/core/app.server.ts index 2b4c48c..475f6d8 100644 --- a/src/core/app.server.ts +++ b/src/core/app.server.ts @@ -1,10 +1,5 @@ import 'reflect-metadata'; -import { - OperationType, - IAppOperation, - IAppConfig, - AppConstructor, -} from '../index'; +import {IAppConfig, AppConstructor} from '../index'; import {AppFactory} from './app.factory'; import {CallResponser} from './call.responser'; import {CallStreamer} from './call.streamer'; @@ -14,6 +9,8 @@ import * as fs from 'fs'; import * as http from 'http'; import * as https from 'https'; import * as finalhandler from 'finalhandler'; +import {IAppOperation, OperationType} from '@onixjs/sdk'; +import {NotifyEvents} from './notify.events'; /** * @function AppServer * @author Jonathan Casarrubias @@ -121,17 +118,24 @@ export class AppServer { // Setup responser and streamer this.responser = new CallResponser(this.factory); this.streamer = new CallStreamer(this.factory); - // Return IO Stream Message - if (process.send) { - process.send({ - uuid: operation.uuid, - type: OperationType.APP_CREATE_RESPONSE, - message: { - request: { - payload: schema, + try { + // Return IO Stream Message + if (process.send) { + process.send({ + uuid: operation.uuid, + type: OperationType.APP_CREATE_RESPONSE, + message: { + request: { + payload: schema, + }, }, - }, - }); + }); + } + } catch (e) { + console.log( + 'ONIXJS: HostBroker is not available, this process is going down.', + ); + process.kill(1); } break; // Event sent from the broker when starting a project @@ -150,6 +154,32 @@ export class AppServer { if (process.send) process.send({type: OperationType.APP_START_RESPONSE}); break; + // Event sent from the broker when a client has been disconnected + case OperationType.ONIX_REMOTE_UNREGISTER_CLIENT: + this.notifier.emit( + NotifyEvents.CLIENT_CLOSED, + operation.message.request.metadata.subscription, + ); + // Send back result + if (process.send) + process.send({ + uuid: operation.uuid, + type: OperationType.ONIX_REMOTE_UNREGISTER_CLIENT_RESPONSE, + }); + break; + // Event sent from the broker when a client has been disconnected + case OperationType.ONIX_REMOTE_CALL_STREAM_UNSUBSCRIBE: + this.notifier.emit( + NotifyEvents.CLIENT_UNSUBSCRIBED, + operation.message.request.payload.uuid, + ); + // Send back result + if (process.send) + process.send({ + uuid: operation.uuid, + type: OperationType.ONIX_REMOTE_CALL_STREAM_UNSUBSCRIBE_RESPONSE, + }); + break; // Event sent from the broker when stoping a project case OperationType.APP_STOP: // If network enabled, turn off the server @@ -177,9 +207,7 @@ export class AppServer { message: { rpc: operation.message.rpc, request: { - metadata: { - /* HERE We might want to return server-side metadata*/ - }, + metadata: operation.message.request.metadata, payload: chunk, }, }, @@ -196,9 +224,7 @@ export class AppServer { message: { rpc: operation.message.rpc, request: { - metadata: { - /* HERE We might want to return server-side metadata*/ - }, + metadata: operation.message.request.metadata, payload: result, }, }, diff --git a/src/core/call.connect.ts b/src/core/call.connect.ts index 3ce79c7..05ba5c0 100644 --- a/src/core/call.connect.ts +++ b/src/core/call.connect.ts @@ -1,12 +1,7 @@ -import { - IAppOperation, - OperationType, - IMetaData, - ICallConfig, - IOnixSchema, -} from '../interfaces'; +import {ICallConfig, IOnixSchema} from '../interfaces'; import {Utils} from '@onixjs/sdk/dist/utils'; import {NodeJS} from '@onixjs/sdk/dist/adapters/node.adapters'; +import {IAppOperation, OperationType, IMetaData} from '@onixjs/sdk'; /** * @class CallConnect * @author Jonathan Casarrubias @@ -66,7 +61,13 @@ export class CallConnect { * execute a RPC, that can be either on this SOA Service or within * any other SOA Service living in the same cluster. */ - async call(payload: T, metadata: IMetaData = {stream: false}): Promise { + async call( + payload: T, + metadata: IMetaData = { + stream: false, + subscription: '$anonymous', + }, + ): Promise { // Hard copy the configuration const config: ICallConfig = JSON.parse(JSON.stringify(this.config)); // Make sure everything is well configured in order to make this call @@ -118,7 +119,10 @@ export class CallConnect { */ async stream( handler: (payload: T, metadata: IMetaData) => void, - metadata: IMetaData = {stream: true}, + metadata: IMetaData = { + stream: true, + subscription: '$anonymous', + }, ) { // Hard copy the configuration const config: ICallConfig = JSON.parse(JSON.stringify(this.config)); diff --git a/src/core/call.responser.ts b/src/core/call.responser.ts index 8633cf7..0900bc5 100644 --- a/src/core/call.responser.ts +++ b/src/core/call.responser.ts @@ -1,11 +1,7 @@ -import { - ReflectionKeys, - IAppOperation, - IComponentConfig, - IModuleConfig, -} from '../interfaces'; +import {ReflectionKeys, IComponentConfig, IModuleConfig} from '../interfaces'; import {AppFactory, LifeCycle} from '../core'; import {GroupMatch} from './acl.group.match'; +import {IAppOperation} from '@onixjs/sdk'; //import { RoleMatch } from './roles'; /** * @class CallResponse diff --git a/src/core/call.streamer.ts b/src/core/call.streamer.ts index 64cefb5..ede4caf 100644 --- a/src/core/call.streamer.ts +++ b/src/core/call.streamer.ts @@ -1,8 +1,9 @@ import {AppFactory} from './app.factory'; -import {IAppOperation, IComponentConfig, IModuleConfig} from '../interfaces'; +import {IComponentConfig, IModuleConfig} from '../interfaces'; import {LifeCycle} from '.'; import {ReflectionKeys} from '..'; import {GroupMatch} from './acl.group.match'; +import {IAppOperation} from '@onixjs/sdk'; export class CallStreamer { /** @@ -118,6 +119,7 @@ export class CallStreamer { scope, data => handler(slaveSubHandler(masterSubHandler(data))), operation.message.request.metadata, + operation.uuid, ) : null; }, @@ -129,6 +131,7 @@ export class CallStreamer { scope, data => handler(masterSubHandler(data)), operation.message.request.metadata, + operation.uuid, ) : null; } diff --git a/src/core/host.broker.ts b/src/core/host.broker.ts index 497e1bf..d4a530b 100644 --- a/src/core/host.broker.ts +++ b/src/core/host.broker.ts @@ -1,13 +1,9 @@ import * as http from 'http'; import * as https from 'https'; -import { - IAppOperation, - IAppDirectory, - WebSocketAdapter, - OperationType, -} from '../interfaces'; +import {IAppDirectory, WebSocketAdapter} from '../interfaces'; import {ChildProcess} from 'child_process'; import {Utils} from '@onixjs/sdk/dist/utils'; +import {ListenerCollection, IAppOperation, OperationType} from '@onixjs/sdk'; /** * @class HostBroker * @author Jonathan Casarrubias @@ -18,6 +14,19 @@ import {Utils} from '@onixjs/sdk/dist/utils'; * server instead of multiple servers. */ export class HostBroker { + private uuid: string = Utils.uuid(); + + private subscriptions: { + [key: string]: ListenerCollection; + } = { + [this.uuid]: new ListenerCollection(), + }; + /** + * @prop listeners + * @description Instance of ListenerCollection which provides + * features to easily handle event listeners. + */ + //private listeners: ListenerCollection = new ListenerCollection(); /** * @constructor * @author Jonathan Casarrubias @@ -31,24 +40,53 @@ export class HostBroker { private websocket: WebSocketAdapter, private apps: IAppDirectory, ) { + // Create WebSocket Server + const wss = this.websocket.WebSocket(this.server); // Listen for WebSocket Requests - this.websocket.WebSocket(this.server).on('connection', ws => { - ws.on('message', (data: string) => { - this.wsHandler(ws, Utils.IsJsonString(data) ? JSON.parse(data) : data); - }); + wss.on('connection', ws => { + // Register a message event listener + ws.on('message', (data: string) => + this.wsHandler(ws, Utils.IsJsonString(data) ? JSON.parse(data) : data), + ); + // Add On Close Listener + ws.onclose = async () => await this.close(ws); + // Add On Error Listener + ws.onerror = async () => await this.close(ws); + // Add Pong Listener + ws.on('pong', () => (ws.isAlive = true)); }); // Listen for IO STD Streams from any app Object.keys(this.apps).forEach((app: string) => { - this.apps[app].process.on('message', (operation: IAppOperation) => { - // Verify the operation is some sort of communication (RPC or Stream) - if ( - operation.type === OperationType.ONIX_REMOTE_CALL_PROCEDURE || - operation.type === OperationType.ONIX_REMOTE_CALL_STREAM - ) { - this.ioHandler(this.apps[app].process, operation); - } - }); + // Add Coordination Listener + this.subscriptions[this.uuid] + .namespace(app) + .add((operation: IAppOperation) => { + // Verify the operation is some sort of communication (RPC or Stream) + if ( + operation.type === OperationType.ONIX_REMOTE_CALL_PROCEDURE || + operation.type === OperationType.ONIX_REMOTE_CALL_STREAM || + operation.type === OperationType.ONIX_REMOTE_CALL_STREAM_UNSUBSCRIBE + ) { + this.ioHandler(this.apps[app].process, operation); + } + }); + // The only event emitter for this app. + // Will broadcast to all listeners registered under + // the same namespace. + this.apps[app].process.on('message', data => + Object.keys(this.subscriptions).forEach(index => + this.subscriptions[index].namespace(app).broadcast(data), + ), + ); }); + // Setup Ping Pong Events + setInterval(() => { + wss.clients.forEach(ws => { + if (ws.isAlive === false) return ws.terminate(); + ws.isAlive = false; + ws.ping(() => {}); + }); + }, 3000); } /** * @method wsHandler @@ -57,15 +95,33 @@ export class HostBroker { * a valid AppOperation. */ wsHandler(ws: WebSocket, operation: IAppOperation) { + // Add a handler for client registrations + if (operation.type === OperationType.ONIX_REMOTE_REGISTER_CLIENT) { + return (ws['onix.uuid'] = this.register(operation, response => + ws.send(JSON.stringify(operation)), + )); + } // Route Message to the right application const callee: string = operation.message.rpc.split('.').shift() || ''; if (this.apps[callee]) { - this.apps[callee].process.on('message', (response: IAppOperation) => { - if (operation.uuid === response.uuid) { - // Send application response to websocket client - ws.send(JSON.stringify(response)); - } - }); + const index: number = this.subscriptions[ + operation.message.request.metadata.subscription + ] + .namespace(callee) + .add((response: IAppOperation) => { + if (operation.uuid === response.uuid) { + // Send application response to websocket client + ws.send(JSON.stringify(response)); + // If RPC Call remove the listener + if (!operation.message.request.metadata.stream) { + this.subscriptions[ + operation.message.request.metadata.subscription + ] + .namespace(callee) + .remove(index); + } + } + }); // Route incoming message to the right application // through std io stream this.apps[callee].process.send(operation); @@ -80,15 +136,42 @@ export class HostBroker { * a valid AppOperation. */ ioHandler(process: ChildProcess, operation: IAppOperation) { + // Add a handler for client registrations + if (operation.type === OperationType.ONIX_REMOTE_REGISTER_CLIENT) { + return this.register(operation, response => process.send(operation)); + } // Route Message to the right application const callee: string = operation.message.rpc.split('.').shift() || ''; if (this.apps[callee]) { - this.apps[callee].process.on('message', (response: IAppOperation) => { - if (operation.uuid === response.uuid) { - // Send application response to websocket client - process.send(response); - } - }); + // If not validating a valid subscription + if ( + !this.subscriptions[operation.message.request.metadata.subscription] + ) { + operation.message.request.payload = { + code: 404, + message: + 'Unable to find subscription for this call, try passing incoming metadata.', + }; + return process.send(operation); + } + const index: number = this.subscriptions[ + operation.message.request.metadata.subscription + ] + .namespace(callee) + .add((response: IAppOperation) => { + if (operation.uuid === response.uuid) { + // Send application response to websocket client + process.send(response); + // If RPC Call remove the listener + if (!operation.message.request.metadata.stream) { + this.subscriptions[ + operation.message.request.metadata.subscription + ] + .namespace(callee) + .remove(index); + } + } + }); // Route incoming message to the right application // through std io stream this.apps[callee].process.send(operation); @@ -96,4 +179,61 @@ export class HostBroker { throw new Error('Unable to find callee application'); } } + + private register( + operation: IAppOperation, + callback: (response: IAppOperation) => void, + ): string { + const uuid: string = operation.uuid; + this.subscriptions[uuid] = new ListenerCollection(); + operation.type = OperationType.ONIX_REMOTE_REGISTER_CLIENT_RESPONSE; + callback(operation); + return uuid; + } + + private async close(ws: WebSocket) { + // Notify Components that this client subscription + // Has been terminated, therefore listeners needs to be removed. + // Create Unregistration operation to signal apps for cleaning up. + const operation: IAppOperation = { + uuid: Utils.uuid(), + type: OperationType.ONIX_REMOTE_UNREGISTER_CLIENT, + message: { + rpc: 'unsubscribe', + request: { + metadata: { + stream: false, + subscription: ws['onix.uuid'], + }, + payload: {}, + }, + }, + }; + // Send a client closing signal + await Promise.all( + this.subscriptions[ws['onix.uuid']].namespaces().map( + (app: string) => + new Promise((resolve, reject) => { + const index: number = this.subscriptions[ws['onix.uuid']] + .namespace(app) + .add((response: IAppOperation) => { + if ( + response.uuid === operation.uuid && + response.type === + OperationType.ONIX_REMOTE_UNREGISTER_CLIENT_RESPONSE + ) { + this.subscriptions[ws['onix.uuid']].remove(index); + resolve(); + } + }); + // Send close signal + this.apps[app].process.send(operation); + }), + ), + ); + // Everything is cool now, remove any reference + // For this subscription + delete this.subscriptions[ws['onix.uuid']]; + console.log('A Client has been disconnected'); + } } diff --git a/src/core/index.ts b/src/core/index.ts index 5cb8aa1..a3e7f62 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -8,3 +8,4 @@ export * from './app.notifier'; export * from './call.connect'; export * from './acl.groups'; export * from './acl.everyone'; +export * from './notify.events'; diff --git a/src/core/lifecycle.ts b/src/core/lifecycle.ts index 1ca46b4..3f5c0ca 100644 --- a/src/core/lifecycle.ts +++ b/src/core/lifecycle.ts @@ -1,5 +1,5 @@ -import {IApp} from '../index'; -import {OnixMessage} from '../interfaces'; +import {OnixMethod, ModelProvider} from '../index'; +import {OnixMessage} from '@onixjs/sdk'; /** * @class LifeCycle * @author Jonathan Casarrubias @@ -19,9 +19,9 @@ export class LifeCycle { * Application. */ async onAppMethodCall( - app: IApp, + models: ModelProvider, message: OnixMessage, - method: () => Promise, + method: OnixMethod, ): Promise { // Before Method Call const result: any = await method(); @@ -39,9 +39,9 @@ export class LifeCycle { * Application. */ async onModuleMethodCall( - app: IApp, + models: ModelProvider, message: OnixMessage, - method: () => Promise, + method: OnixMethod, ): Promise { // Before Method Call const result: any = await method(); @@ -59,9 +59,9 @@ export class LifeCycle { * Application. */ async onComponentMethodCall( - app: IApp, + models: ModelProvider, message: OnixMessage, - method: () => Promise, + method: OnixMethod, ): Promise { // Before Method Call const result: any = await method(); @@ -79,7 +79,7 @@ export class LifeCycle { * Application. */ async onModuleMethodStream( - app: IApp, + models: ModelProvider, message: OnixMessage, stream: (handler: (data) => any) => any, ): Promise { @@ -100,7 +100,7 @@ export class LifeCycle { * Application. */ async onComponentMethodStream( - app: IApp, + models: ModelProvider, message: OnixMessage, stream: (handler: (data) => any) => any, ): Promise { diff --git a/src/core/notify.events.ts b/src/core/notify.events.ts new file mode 100644 index 0000000..131c889 --- /dev/null +++ b/src/core/notify.events.ts @@ -0,0 +1,9 @@ +/** + * @class NotifyEvents + * @description This class provides some namespaces + * for events + */ +export class NotifyEvents { + static CLIENT_CLOSED = 'onixjs:client:closed'; + static CLIENT_UNSUBSCRIBED = 'onixjs:client:unsubscribed'; +} diff --git a/src/decorators/rpc.ts b/src/decorators/rpc.ts index ef5055c..52fd773 100644 --- a/src/decorators/rpc.ts +++ b/src/decorators/rpc.ts @@ -1,4 +1,4 @@ -import {ReflectionKeys} from '../index'; +import {ReflectionKeys, RPCMethod, IComponent} from '../index'; /** * @function RPC * @author Jonathan Casarrubias @@ -6,7 +6,11 @@ import {ReflectionKeys} from '../index'; * @description This decorator will expose component methods */ export function RPC() { - return function(target: object, propertyKey: string) { + return function( + target: IComponent, + propertyKey: string, + descriptor: TypedPropertyDescriptor, + ) { Reflect.defineMetadata( ReflectionKeys.RPC_METHOD, true, diff --git a/src/index.ts b/src/index.ts index b1b3ec1..14452f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,5 @@ import {fork, ChildProcess} from 'child_process'; -import { - IAppDirectory, - IAppConfig, - OperationType, - IAppOperation, - OnixMessage, - IRequest, - OnixConfig, -} from './interfaces'; +import {IAppDirectory, IAppConfig, OnixConfig} from './interfaces'; import {SchemaProvider} from './core/schema.provider'; export * from './core'; export * from './utils'; @@ -23,6 +15,7 @@ import {Utils} from '@onixjs/sdk/dist/utils'; import {HostBroker} from './core/host.broker'; import {promisify} from 'util'; import {WSAdapter} from './adapters/ws.adapter'; +import {IAppOperation, OperationType, IRequest, OnixMessage} from '@onixjs/sdk'; /** * @class OnixJS * @author Jonathan Casarrubias @@ -81,6 +74,13 @@ export class OnixJS { }); // Log Onix Version console.info('Loading Onix Server Version: ', this.version); + // Kill Childs On Exit + process.on('exit', () => + Object.keys(this._apps).forEach((key: string) => { + console.log(`ONIXJS: Killing child process "${key}"`); + this._apps[key].process.kill(); + }), + ); } /** * @method ping @@ -97,14 +97,16 @@ export class OnixJS { throw new Error( `OnixJS Error: Trying to ping unexisting app "${name}".`, ); + const uuid: string = Utils.uuid(); const operation: IAppOperation = { - uuid: Utils.uuid(), + uuid, type: OperationType.APP_PING, message: { rpc: 'ping', request: { metadata: { stream: false, + subscription: uuid, }, payload: {}, }, @@ -136,14 +138,16 @@ export class OnixJS { Object.keys(this._apps).map( (name: string) => new Promise((resolve, reject) => { + const uuid: string = Utils.uuid(); const operation: IAppOperation = { - uuid: Utils.uuid(), + uuid, type: OperationType.APP_GREET, message: { rpc: '[apps].isAlive', // [apps] will be overriden inside each app request: { metadata: { stream: false, + subscription: uuid, }, payload: apps, }, @@ -257,8 +261,9 @@ export class OnixJS { */ coordinate(rpc: string, request: IRequest) { return new Promise((resolve, reject) => { + const uuid: string = Utils.uuid(); const operation: IAppOperation = { - uuid: Utils.uuid(), + uuid, type: OperationType.ONIX_REMOTE_CALL_PROCEDURE, message: { rpc, @@ -289,7 +294,7 @@ export class OnixJS { * all the loaded child applications. */ async start(): Promise { - return Promise.all( + const result = await Promise.all( // Concatenate an array of promises, starting from Onix Server, // Then map each app reference to create promises for start operation. Object.keys(this._apps).map( @@ -306,15 +311,9 @@ export class OnixJS { this._apps[name].process.send({type: OperationType.APP_START}); }), ), - ).then( - (res: OperationType.APP_START_RESPONSE[]) => - new Promise( - async (resolve, reject) => { - await this.startSystemServer(); - resolve(res); - }, - ), ); + await this.startSystemServer(); + return result; } /** * @method start @@ -325,7 +324,7 @@ export class OnixJS { * all the loaded child applications. */ async stop(): Promise { - return Promise.all( + const result = await Promise.all( // Concatenate an array of promises, starting from Onix Server, // Then map each app reference to create promises for start operation. Object.keys(this._apps).map((name: string) => { @@ -344,18 +343,10 @@ export class OnixJS { }, ); }), - ).then( - (res: OperationType.APP_STOP_RESPONSE[]) => - new Promise((resolve, reject) => { - this.server.close(() => { - // Kill'em all right now. - Object.keys(this._apps).forEach(reference => - this._apps[reference].process.kill('SIGHUP'), - ); - resolve(res); - }); - }), ); + // Close the server + this.server.close(); + return result; } /** * @method startSystemServer diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 7181145..975d916 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -2,6 +2,7 @@ import {ChildProcess} from 'child_process'; import * as http from 'http'; import * as https from 'https'; import {AppNotifier} from '..'; +import {OnixMessage, IRequest, IMetaData} from '@onixjs/sdk'; /** * @interface IAppConfig * @author Jonathan Casarrubias @@ -133,35 +134,6 @@ export interface IApp { stop(): Promise; isAlive(): boolean; } -/** - * @author Jonathan Casarrubias - * @interface IAppOperation - * @description Internal system operation, executed when - * RPC calls are made. - */ -export interface IAppOperation { - uuid: string; - type: OperationType; - message: OnixMessage; -} -/** - * @interface OnixMessage - * @author Jonathan Casarrubias - * @description OnixMessage Contract - */ -export interface OnixMessage { - rpc: string; - request: IRequest; -} -/** - * @interface IRequest - * @author Jonathan Casarrubias - * @description IRequest inteface - */ -export interface IRequest { - metadata: IMetaData; - payload: any; -} /** * @interface ICallConfig * @author Jonathan Casarrubias @@ -196,28 +168,6 @@ export interface IOnixSchema { }; }; } -/** - * @author Jonathan Casarrubias - * @enum OperationType - * @description Enum used for system level operations. - */ -export enum OperationType { - /*0*/ APP_CREATE, - /*1*/ APP_CREATE_RESPONSE, - /*2*/ APP_PING, - /*3*/ APP_PING_RESPONSE, - /*4*/ APP_START, - /*5*/ APP_START_RESPONSE, - /*6*/ APP_STOP, - /*7*/ APP_STOP_RESPONSE, - /*8*/ APP_DESTROY, - /*9*/ APP_DESTROY_RESPONSE, - /*10*/ APP_GREET, - /*11*/ APP_GREET_RESPONSE, - /*12*/ ONIX_REMOTE_CALL_STREAM, - /*13*/ ONIX_REMOTE_CALL_PROCEDURE, - /*14*/ ONIX_REMOTE_CALL_PROCEDURE_RESPONSE, -} /** * @author Jonathan Casarrubias * @interface IAppDirectory @@ -239,17 +189,6 @@ export interface IAppDirectory { export interface IComponentDirectory { [key: string]: any; } -/** - * @interface IMetaData - * @author Jonathan Casarrubias - * @description Interface used as generic IMetaData class. - */ -export interface IMetaData { - [key: string]: any; - sub?: string; - token?: string; - stream: boolean; -} /** * @interface Constructor * @author Jonathan Casarrubias @@ -467,6 +406,15 @@ export interface OnixHTTPRequest extends http.IncomingMessage { export interface IViewRenderer { process(view: string, args: Directory): string; } + +export interface RPCMethod { + (payload: any, metadata: IMetaData); +} + +export interface StreamMethod { + (payload: T, metadata: IMetaData, uuid: string): Promise; +} + /** * @author Jonathan Casarrubias * @enum ReflectionKeys diff --git a/test/bob.app/modules/bob.component.ts b/test/bob.app/modules/bob.component.ts index 881eae2..cc2b2ff 100644 --- a/test/bob.app/modules/bob.component.ts +++ b/test/bob.app/modules/bob.component.ts @@ -32,10 +32,10 @@ export class BobComponent implements IComponent { destroy() {} @RPC() - async exposedCall(payload) { + async exposedCall(payload, metadata) { try { this.connect.config.method = 'callMe'; - const result = await this.connect.call(payload); + const result = await this.connect.call(payload, metadata); return result; } catch (e) { return e.message; diff --git a/test/decorators.unit.ts b/test/decorators.unit.ts index 0760136..a31304e 100644 --- a/test/decorators.unit.ts +++ b/test/decorators.unit.ts @@ -226,6 +226,8 @@ test('@RPC Method should be decorated.', t => { class MyClass { @RPC() myRPC() {} + init() {} + destroy() {} } const instance: MyClass = new MyClass(); const enabled: boolean = Reflect.getMetadata( diff --git a/test/onixjs.acceptance.ts b/test/onixjs.acceptance.ts index 292bfe5..8471053 100644 --- a/test/onixjs.acceptance.ts +++ b/test/onixjs.acceptance.ts @@ -1,18 +1,25 @@ import {test} from 'ava'; import * as path from 'path'; -import {OnixJS, OperationType, IAppOperation, IRequest} from '../src/index'; +import {OnixJS} from '../src/index'; import {TodoModel} from './todo.shared/todo.model'; const pkg = require('../../package.json'); const cwd = path.join(process.cwd(), 'dist', 'test'); -import * as WebSocket from 'ws'; +//import * as WebSocket from 'ws'; import {IAppConfig} from '../src/interfaces'; import {Utils} from '@onixjs/sdk/dist/utils'; -import {OnixClient, AppReference, ComponentReference} from '@onixjs/sdk'; +import { + OnixClient, + AppReference, + ComponentReference, + OperationType, + IAppOperation, + IRequest, +} from '@onixjs/sdk'; import {NodeJS} from '@onixjs/sdk/dist/adapters/node.adapters'; import {WSAdapter} from '../src/adapters/ws.adapter'; // Test Onix Version -test('Onix version', t => { +test('Acceptance: Onix version', t => { const onix: OnixJS = new OnixJS({ cwd, port: 8085, @@ -21,7 +28,7 @@ test('Onix version', t => { t.is(onix.version, pkg.version); }); //Test Onix App Starter -test('Onix app starter', async t => { +test('Acceptance: Onix app starter', async t => { const onix: OnixJS = new OnixJS({ cwd, port: 8083, @@ -37,7 +44,7 @@ test('Onix app starter', async t => { }); //Test Onix App Pinger -test('Onix app pinger', async t => { +test('Acceptance: Onix app pinger', async t => { const onix: OnixJS = new OnixJS({ cwd, port: 8084, @@ -48,7 +55,7 @@ test('Onix app pinger', async t => { t.true(config.network!.disabled); }); //Test Onix Apps Say Hello -test('Onix app greeter', async t => { +test('Acceptance: Onix app greeter', async t => { const onix: OnixJS = new OnixJS({cwd, adapters: {websocket: WSAdapter}}); await onix.load('BobApp@bob.app'); await onix.load('AliceApp@alice.app'); @@ -62,7 +69,7 @@ test('Onix app greeter', async t => { }); //Test Onix RPC component methods -test('Onix rpc component methods from server', async t => { +test('Acceptance: Onix rpc component methods from server', async t => { const onix: OnixJS = new OnixJS({ cwd, port: 8085, @@ -75,7 +82,12 @@ test('Onix rpc component methods from server', async t => { const operation: IAppOperation = await onix.coordinate( 'TodoApp.TodoModule.TodoComponent.addTodo', { - metadata: {stream: false, caller: 'tester', token: 'dummytoken'}, + metadata: { + stream: false, + caller: 'tester', + token: 'dummytoken', + subscription: Utils.uuid(), + }, payload: todo, }, ); @@ -87,7 +99,7 @@ test('Onix rpc component methods from server', async t => { t.truthy(result._id); }); -//Test Onix RPC component methods +/*Test Onix RPC component methods test('Onix rpc component methods from client', async t => { const onix: OnixJS = new OnixJS({ cwd, @@ -122,17 +134,22 @@ test('Onix rpc component methods from client', async t => { message: { rpc: 'TodoApp.TodoModule.TodoComponent.addTodo', request: { - metadata: {stream: false, caller: 'tester', token: 'dummytoken'}, + metadata: { + stream: false, + caller: 'tester', + token: 'dummytoken', + subscription: Utils.uuid() + }, payload: todo, }, }, }), ); }); -}); +});*/ //Test Onix RPC component stream* -test('Onix rpc component stream', async t => { +test('Acceptance: Onix rpc component stream', async t => { const text: string = 'Hello SDK World'; // Host Port 8087 const onix: OnixJS = new OnixJS({ @@ -176,7 +193,7 @@ test('Onix rpc component stream', async t => { }); //Test Onix Call Connect RPC* -test('Onix Call Connect RPC', async t => { +test('Acceptance: Onix Call Connect RPC', async t => { const text: string = 'Hello Connected World'; // Host Port 2999 const onix: OnixJS = new OnixJS({ diff --git a/test/onixjs.core.unit.ts b/test/onixjs.core.unit.ts index 42fd5b5..a0fc604 100644 --- a/test/onixjs.core.unit.ts +++ b/test/onixjs.core.unit.ts @@ -5,12 +5,9 @@ import {AppNotifier} from '../src/core/app.notifier'; import {AppServer} from '../src/core/app.server'; import { OnixJS, - IRequest, RPC, Stream, Module, - OperationType, - IAppOperation, Component, Service, Inject, @@ -19,8 +16,6 @@ import { IModel, Model, Property, - IApp, - IModuleDirectory, OnixHTTPRequest, IViewRenderer, Directory, @@ -49,7 +44,7 @@ import {Mongoose, Schema} from 'mongoose'; import {GroupMatch} from '../src/core/acl.group.match'; import {AllowEveryone} from '../src/core/acl.everyone'; import {WSAdapter} from '../src/adapters/ws.adapter'; -import {OnixMessage} from '@onixjs/sdk'; +import {OnixMessage, IRequest, OperationType, IAppOperation} from '@onixjs/sdk'; const cwd = path.join(process.cwd(), 'dist', 'test'); test('Core: AppFactory creates an Application.', async t => { @@ -136,6 +131,8 @@ test('Core: OnixJS schema builder.', async t => { testRPC() {} @Stream() testSTREAM() {} + init() {} + destroy() {} } @Module({ models: [], @@ -159,6 +156,8 @@ test('Core: CallResponser invalid call.', async t => { testRPC() {} @Stream() testSTREAM() {} + init() {} + destroy() {} } @Module({ models: [], @@ -198,6 +197,8 @@ test('Core: CallResponser not authorized call.', async t => { } @Stream() testSTREAM() {} + init() {} + destroy() {} } @Module({ models: [], @@ -234,6 +235,8 @@ test('Core: CallResponser valid call.', async t => { } @Stream() testSTREAM() {} + init() {} + destroy() {} } @Module({ models: [], @@ -265,6 +268,8 @@ test('Core: CallResponser invalid call.', async t => { testRPC() {} @Stream() testSTREAM() {} + init() {} + destroy() {} } @Module({ models: [], @@ -312,6 +317,8 @@ test('Core: CallResponser Hooks.', async t => { test(payload) { return payload; } + init() {} + destroy() {} } @Module({ models: [], @@ -326,13 +333,14 @@ test('Core: CallResponser Hooks.', async t => { factory.notifier = new AppNotifier(); await factory.setup(); const responser: CallResponser = new CallResponser(factory); + const uuid: string = Utils.uuid(); const result = await responser.process({ - uuid: Utils.uuid(), + uuid, type: OperationType.ONIX_REMOTE_CALL_PROCEDURE, message: { rpc: 'MyApp.MyModule.MyComponent.test', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: { text: 'Hello Responser', }, @@ -881,22 +889,11 @@ test('Core: Inject Throws Uninstalled Injectable.', async t => { // Test Main Life Cycle test('Core: main lifecycle.', async t => { const result: boolean = true; - class MyApp implements IApp { - modules: IModuleDirectory; - async start(): Promise { - return result; - } - async stop(): Promise { - return result; - } - isAlive(): boolean { - return result; - } - } - const instance: MyApp = new MyApp(); const lifecycle: LifeCycle = new LifeCycle(); const r1 = await lifecycle.onAppMethodCall( - instance, + (name: string): T => { + return newFunction(); + }, { rpc: 'somerpc', request: { @@ -907,7 +904,9 @@ test('Core: main lifecycle.', async t => { async () => result, ); const r2 = await lifecycle.onModuleMethodCall( - instance, + (name: string): T => { + return newFunction(); + }, { rpc: 'somerpc', request: { @@ -918,7 +917,9 @@ test('Core: main lifecycle.', async t => { async () => result, ); const r3 = await lifecycle.onComponentMethodCall( - instance, + (name: string): T => { + return newFunction(); + }, { rpc: 'somerpc', request: { @@ -938,7 +939,7 @@ test('Core: Component Router REST Endpoint.', async t => { class StaticComponent { @Router.Get('/test/get') async test(req, res) { - res.end(JSON.stringify({HELLO: 'WORLD'})); + return {HELLO: 'WORLD'}; } } // Declare Module @@ -956,26 +957,28 @@ test('Core: Component Router REST Endpoint.', async t => { port: 7950, modules: [StaticModule], }); + let uuid: string = Utils.uuid(); // Create Application await server.operation({ - uuid: Utils.uuid(), + uuid, type: OperationType.APP_CREATE, message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, }); + uuid = Utils.uuid(); // Start Application await server.operation({ - uuid: Utils.uuid(), + uuid, type: OperationType.APP_START, message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, @@ -987,14 +990,15 @@ test('Core: Component Router REST Endpoint.', async t => { const result: any = await client.get('http://127.0.0.1:7950/test/get'); // Test Service t.is(result.HELLO, 'WORLD'); + uuid = Utils.uuid(); // Start Application await server.operation({ - uuid: Utils.uuid(), + uuid, type: OperationType.APP_STOP, message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, @@ -1040,14 +1044,15 @@ test('Core: Component Router Param Hook.', async t => { port: 7951, modules: [StaticModule], }); + const uuid: string = Utils.uuid(); // Create Application await server.operation({ - uuid: Utils.uuid(), + uuid, type: OperationType.APP_CREATE, message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, @@ -1059,7 +1064,7 @@ test('Core: Component Router Param Hook.', async t => { message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, @@ -1084,7 +1089,7 @@ test('Core: Component Router Param Hook.', async t => { message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, @@ -1098,6 +1103,11 @@ test('Core: Component Static.', async t => { async test(req: OnixHTTPRequest, buffer: Buffer) { return buffer.toString(); } + @Router.Static('test/no.exist.json') + async thrower(req: OnixHTTPRequest, buffer: Buffer) { + // Wont be executed since path file does not exist + return buffer.toString(); + } } // Declare Module @Module({ @@ -1114,26 +1124,28 @@ test('Core: Component Static.', async t => { port: 6950, modules: [StaticModule], }); + let uuid: string = Utils.uuid(); // Create Application await server.operation({ - uuid: Utils.uuid(), + uuid, type: OperationType.APP_CREATE, message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, }); + uuid = Utils.uuid(); // Start Application await server.operation({ - uuid: Utils.uuid(), + uuid, type: OperationType.APP_START, message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, @@ -1147,6 +1159,11 @@ test('Core: Component Static.', async t => { ); // Test Service t.is(JSON.parse(result.toString()).hello, 'world'); + // Thrower/ + const noexist: String = await client.get( + 'http://127.0.0.1:6950/test/no.exist.json', + ); + t.true(noexist.includes('Cannot GET /test/no.exist.json')); // Start Application await server.operation({ uuid: Utils.uuid(), @@ -1154,7 +1171,7 @@ test('Core: Component Static.', async t => { message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, @@ -1187,26 +1204,28 @@ test('Core: Component View.', async t => { port: 6050, modules: [StaticModule], }); + let uuid: string = Utils.uuid(); // Create Application await server.operation({ - uuid: Utils.uuid(), + uuid, type: OperationType.APP_CREATE, message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, }); + uuid = Utils.uuid(); // Start Application await server.operation({ - uuid: Utils.uuid(), + uuid, type: OperationType.APP_START, message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, @@ -1227,7 +1246,7 @@ test('Core: Component View.', async t => { message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, @@ -1260,18 +1279,20 @@ test('Core: Component View Immutable.', async t => { port: 6150, modules: [StaticModule], }); + let uuid: string = Utils.uuid(); // Create Application await server.operation({ - uuid: Utils.uuid(), + uuid, type: OperationType.APP_CREATE, message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, }); + uuid = Utils.uuid(); // Start Application await server.operation({ uuid: Utils.uuid(), @@ -1279,7 +1300,7 @@ test('Core: Component View Immutable.', async t => { message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, @@ -1300,7 +1321,7 @@ test('Core: Component View Immutable.', async t => { message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, @@ -1333,26 +1354,28 @@ test('Core: Component View FilePath Not Exist.', async t => { port: 6350, modules: [StaticModule], }); + let uuid: string = Utils.uuid(); // Create Application await server.operation({ - uuid: Utils.uuid(), + uuid, type: OperationType.APP_CREATE, message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, }); + uuid = Utils.uuid(); // Start Application await server.operation({ - uuid: Utils.uuid(), + uuid, type: OperationType.APP_START, message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, @@ -1364,14 +1387,15 @@ test('Core: Component View FilePath Not Exist.', async t => { // Test Service t.is(result.code, 404); t.is(result.message, 'Oops!!! something went wrong.'); + uuid = Utils.uuid(); // Start Application await server.operation({ - uuid: Utils.uuid(), + uuid, type: OperationType.APP_STOP, message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, @@ -1419,18 +1443,20 @@ test('Core: View Renderer.', async t => { port: 6060, modules: [DynamicModule], }); + let uuid: string = Utils.uuid(); // Create Application await server.operation({ - uuid: Utils.uuid(), + uuid, type: OperationType.APP_CREATE, message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, }); + uuid = Utils.uuid(); // Start Application await server.operation({ uuid: Utils.uuid(), @@ -1438,7 +1464,7 @@ test('Core: View Renderer.', async t => { message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, @@ -1458,7 +1484,7 @@ test('Core: View Renderer.', async t => { message: { rpc: '', request: { - metadata: {stream: false}, + metadata: {stream: false, subscription: uuid}, payload: '', }, }, @@ -1503,15 +1529,17 @@ test('Core: Notifier.', async t => { test('CORE: ACL Group Match', async t => { // SOME DUMMY METHOD NAME const name: string = 'somemethod'; + const uuid: string = Utils.uuid(); // SOME DUMMY OPERATION const operation: IAppOperation = { - uuid: Utils.uuid(), + uuid, type: OperationType.ONIX_REMOTE_CALL_PROCEDURE, message: { rpc: name, request: { metadata: { stream: false, + subscription: uuid, }, payload: {}, }, @@ -1537,3 +1565,7 @@ test('CORE: ACL Group Match', async t => { // TEST IF HAS ACCESS t.true(hasAccess); }); + +function newFunction(): T { + return {}; +} diff --git a/test/todo.shared/todo.module.ts b/test/todo.shared/todo.module.ts index 6d3d88f..8eb3891 100644 --- a/test/todo.shared/todo.module.ts +++ b/test/todo.shared/todo.module.ts @@ -2,7 +2,8 @@ import {Module} from '../../src/index'; import {TodoComponent} from './todo.component'; import {TodoModel} from './todo.model'; import {TodoService} from './todo.service'; -import {OnixMessage, ModelProvider, OnixMethod} from '../../src/interfaces'; +import {ModelProvider, OnixMethod} from '../../src/interfaces'; +import {OnixMessage} from '@onixjs/sdk'; /** * @class TodoModule * @author Jonathan Casarrubias