Skip to content

Commit

Permalink
Merge pull request #4 from jovinjijo/feature-websockets
Browse files Browse the repository at this point in the history
Realtime updates in UI
  • Loading branch information
jovinjijo authored Oct 25, 2020
2 parents 638c371 + 0031cb6 commit fbece56
Show file tree
Hide file tree
Showing 26 changed files with 496 additions and 73 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
root = true

[*]
end_of_line = lf
end_of_line = crlf
insert_final_newline = true

[*.{js,json,ts,tsx,.yml}]
Expand Down
13 changes: 9 additions & 4 deletions packages/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import app from './src/app';
import { SocketService } from './src/services/SocketService';
import { initMarket } from './src/util/Methods';
import { createServer } from 'http';

initMarket();
const server = createServer(app);
const socketService = new SocketService(server);

initMarket(socketService);

/**
* Start Express server.
*/
const server = app.listen(app.get('port'), () => {
server.listen(app.get('port'), () => {
console.log(' App is running at http://localhost:%d in %s mode', app.get('port'), app.get('env'));
console.log(' Press CTRL-C to stop\n');
});

export { server };
export { UserStoreItemDetails } from './src/models/User';
export { UserStoreItemDetails, UserDetails } from './src/models/User';
export { OrderStoreDetails } from './src/models/Order';
export { LtpUpdate, OrderUpdate } from './src/services/NotificationService';
7 changes: 5 additions & 2 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"test": "jest --collectCoverage --testTimeout 20000",
"lint": "eslint '**/*.{js,ts}' --fix",
"precommit": "lint-staged && yarn test",
"start": "nodemon build/server.js"
"start": "nodemon build/index.js"
},
"keywords": [
"stockexchange",
Expand All @@ -28,18 +28,21 @@
"compression": "^1.7.4",
"express": "^4.17.1",
"express-session": "^1.17.1",
"express-socket.io-session": "^1.3.5",
"express-validator": "^6.6.1",
"method-override": "^3.0.0",
"socket.io": "^2.3.0"
},
"devDependencies": {
"@types/jest": "^26.0.13",
"@types/bcrypt": "^3.0.0",
"@types/compression": "^1.7.0",
"@types/express": "^4.17.7",
"@types/express-session": "^1.17.0",
"@types/express-socket.io-session": "^1.3.2",
"@types/jest": "^26.0.13",
"@types/method-override": "^0.0.31",
"@types/newman": "^5.1.1",
"@types/socket.io": "^2.1.11",
"@typescript-eslint/eslint-plugin": "^3.9.0",
"@typescript-eslint/parser": "^3.9.0",
"eslint": "^7.6.0",
Expand Down
16 changes: 8 additions & 8 deletions packages/api/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import express from 'express';
import compression from 'compression';
import bodyParser from 'body-parser';
import session from 'express-session';
import expressSession from 'express-session';
import path from 'path';
import methodOverride from 'method-override';

Expand All @@ -20,13 +20,12 @@ app.use(compression());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.use(
session({
resave: true,
saveUninitialized: true,
secret: process.env['SESSION_SECRET'] || '12345678',
}),
);
const session = expressSession({
resave: true,
saveUninitialized: true,
secret: process.env['SESSION_SECRET'] || '12345678',
});
app.use(session);

// If a user is logged in, fill up req.user with user details
app.use(fillUserData);
Expand All @@ -52,3 +51,4 @@ app.use(methodOverride());
app.use(errorHandler);

export default app;
export { session };
56 changes: 28 additions & 28 deletions packages/api/src/models/Order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,25 @@ import {
ID,
} from '@se/core';

type TruncatedOrderDetails =
| OrderInput
| {
id: ID;
time: Date;
};
type TruncatedOrderDetails = Omit<OrderInput, 'user'> & {
id: ID;
time: Date;
};

type ConfirmedOrderDetails =
| TruncatedOrderDetails
| {
status: OrderStatus.Confirmed;
avgSettledPrice: Amount;
settledTime: Date;
};
type ConfirmedOrderDetails = TruncatedOrderDetails & {
status: OrderStatus.Confirmed;
avgSettledPrice: Amount;
settledTime: Date;
};

type PlacedOrderDetails =
| TruncatedOrderDetails
| {
status: OrderStatus.Placed;
};
type PlacedOrderDetails = TruncatedOrderDetails & {
status: OrderStatus.Placed;
};

type PartiallyFilledOrderDetails =
| TruncatedOrderDetails
| {
status: OrderStatus.PartiallyFilled;
quantityFilled: Quantity;
};
type PartiallyFilledOrderDetails = TruncatedOrderDetails & {
status: OrderStatus.PartiallyFilled;
quantityFilled: Quantity;
};

export type OrderDetails = ConfirmedOrderDetails | PlacedOrderDetails | PartiallyFilledOrderDetails;

Expand Down Expand Up @@ -81,12 +73,20 @@ export class OrderRepository {
}

public static getOrderDetails(order: Order): OrderDetails {
const orderInput: OrderInput = {
...{
quantity: order.quantity,
symbol: order.symbol,
type: order.type,
user: order.user,
},
...(order.additionalType === AdditionalOrderType.Market
? { additionalType: AdditionalOrderType.Market }
: { additionalType: AdditionalOrderType.Limit, price: order.price }),
};
const orderDetails: TruncatedOrderDetails = {
...{ ...orderInput, user: undefined },
id: order.id,
price: order.price,
quantity: order.quantity,
symbol: order.symbol,
type: order.type,
time: order.time,
};
if (order.status === OrderStatus.Confirmed) {
Expand Down
20 changes: 18 additions & 2 deletions packages/api/src/models/User.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { User, Amount, HoldingsData, IUser } from '@se/core';
import * as bcrypt from 'bcrypt';
import { Socket } from 'socket.io';
import { OrderRepository, OrderStoreDetails } from './Order';

const saltRounds = 10;

export interface WalletDetails {
margin: Amount;
}

export interface UserDetails extends Omit<IUser, 'orders' | 'holdings' | 'name' | 'wallet'> {
orders: OrderStoreDetails;
holdings: HoldingsData;
wallet: { margin: Amount };
wallet: WalletDetails;
}

export interface UserStoreItem {
user: User;
username: string;
socket?: Socket;
}

export interface UserStoreItemDetails extends UserDetails {
Expand All @@ -23,7 +29,7 @@ export interface UserStoreItemSensitive extends UserStoreItem {
password: string;
}
export class UserStore {
static users: Map<string, UserStoreItemSensitive> = new Map<string, UserStoreItemSensitive>();
private static users: Map<string, UserStoreItemSensitive> = new Map<string, UserStoreItemSensitive>();

public static async addUser(
username: string,
Expand All @@ -49,6 +55,7 @@ export class UserStore {
return {
user: user.user,
username: user.username,
socket: user.socket,
};
}
throw new Error('User not found');
Expand Down Expand Up @@ -80,4 +87,13 @@ export class UserStore {
username: user.username,
};
}

public static setSocketForUser(username: string, socket: Socket): void {
const user = this.users.get(username);
if (user) {
user.socket = socket;
} else {
throw new Error('User not found');
}
}
}
36 changes: 36 additions & 0 deletions packages/api/src/services/NotificationService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Amount, Notification, Order, Stock, User } from '@se/core';
import { OrderDetails, OrderRepository } from '../models/Order';
import { UserDetails, UserStore } from '../models/User';
import { SocketService } from './SocketService';

export interface LtpUpdate {
stock: Stock;
lastTradePrice: Amount;
time: Date;
}

export interface OrderUpdate {
user: UserDetails;
order: OrderDetails;
}

export class NotificationService implements Notification {
socketService: SocketService;
constructor(socketService: SocketService) {
this.socketService = socketService;
}

notifyLtpUpdate(stock: Stock, lastTradePrice: Amount, time: Date): void {
this.socketService.broadcast('ltpUpdate', { stock, lastTradePrice, time });
}

notifyOrderUpdate(user: User, order: Order): void {
const socket = UserStore.findUserByUsername(user.name)?.socket;
if (socket) {
this.socketService.send(socket, 'orderUpdate', {
user: UserStore.getUserDetails(user),
order: OrderRepository.getOrderDetails(order),
});
}
}
}
27 changes: 27 additions & 0 deletions packages/api/src/services/SocketService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { SocketServer } from '../util/SocketServer';
import { Server } from 'http';
import { Socket } from 'socket.io';
import { UserStore } from '../models/User';
import { Market } from '@se/core';

export class SocketService extends SocketServer {
constructor(server: Server) {
super(server, SocketService.onNewConnection);
}

static onNewConnection(socket: Socket): void {
try {
if (!socket.handshake.session) {
throw new Error('User not logged in');
} else {
const username = socket.handshake.session.userId;
if (username) {
UserStore.setSocketForUser(username, socket);
socket.emit('ltpMap', Market.getInstance().getLtpForOrderStores());
}
}
} catch (ex) {
socket.disconnect();
}
}
}
6 changes: 5 additions & 1 deletion packages/api/src/util/Methods.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { Market, Stock } from '@se/core';
import { NotificationService } from '../services/NotificationService';
import { SocketService } from '../services/SocketService';

function initMarket(): void {
function initMarket(socketService: SocketService): void {
Market.getInstance().addOrderStore(Stock.TSLA, 500);
Market.getInstance().addOrderStore(Stock.AMZN, 3000);
Market.getInstance().addOrderStore(Stock.RIL, 2000);

Market.getInstance().attachNotification(new NotificationService(socketService));
}

export { initMarket };
24 changes: 24 additions & 0 deletions packages/api/src/util/SocketServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Server } from 'http';
import io, { Socket } from 'socket.io';
import sharedSession from 'express-socket.io-session';
import { session } from '../app';

export class SocketServer {
private socket: SocketIO.Server;

constructor(server: Server, onNewConnection?: (socket: Socket) => void) {
this.socket = io(server);
this.socket.use(sharedSession(session, { autoSave: true }));
this.socket.on('connection', (socket: Socket) => {
if (onNewConnection) onNewConnection(socket);
});
}

send(socket: Socket, type: string, data: unknown): void {
socket.emit(type, data);
}

broadcast(type: string, data: unknown): void {
this.socket.emit(type, data);
}
}
10 changes: 7 additions & 3 deletions packages/api/tests/user.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import * as newman from 'newman';
import collection from '../docs/se_api.postman_collection.json';
import { Server } from 'http';
import { Server, createServer } from 'http';

import app from '../src/app';
import { initMarket } from '../src/util/Methods';
import { SocketService } from '../src/services/SocketService';

let server: Server;

beforeAll((done) => {
initMarket();
server = app.listen(app.get('port'), () => {
server = createServer(app);
const socketService = new SocketService(server);

initMarket(socketService);
server.listen(app.get('port'), () => {
done();
});
});
Expand Down
Loading

0 comments on commit fbece56

Please sign in to comment.