Skip to content

Commit

Permalink
internal messages / sales dashboard impl
Browse files Browse the repository at this point in the history
  • Loading branch information
floodfx committed Jan 22, 2022
1 parent 5d25d62 commit 83b7f3e
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 32 deletions.
13 changes: 10 additions & 3 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import jwt from "jsonwebtoken";
import session, {MemoryStore} from "express-session";
import { nanoid } from "nanoid";
import { wsServer } from "./socket/websocket_server";
import { PhxSocket } from "./socket/types";

// pull in websocket server to listen for events
wsServer
Expand Down Expand Up @@ -49,11 +50,17 @@ declare module 'express-session' {
// register each route path to the component to be rendered
Object.keys(router).forEach(key => {
app.get(key, (req: Request, res: Response) => {
// console.log("req.path", req.path)

// new LiveViewId per HTTP requess?
const liveViewId = nanoid();
const phxSocket: PhxSocket = {
id: liveViewId,
connected: false, // http request
}

// render the component
const component = router[key];
const ctx = component.mount({}, {}, {});
const ctx = component.mount({}, {}, phxSocket);
const view = component.render(ctx);

// lookup / gen csrf token for this session
Expand All @@ -65,7 +72,7 @@ Object.keys(router).forEach(key => {
res.render("index", {
page_title: "Live View",
csrf_meta_tag: req.session.csrfToken,
liveViewId: nanoid(), // new LiveViewId per HTTP requess?
liveViewId,
session: jwt.sign(JSON.stringify(req.session), SIGNING_SECRET),
statics: jwt.sign(JSON.stringify(view.statics), SIGNING_SECRET),
inner_content: view.toString()
Expand Down
15 changes: 5 additions & 10 deletions src/server/live/license_liveview.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import escapeHtml from "../liveview/templates";
import { LiveViewComponent, LiveViewContext, LiveViewExternalEventListener } from "../liveview/types";
import { PhxSocket } from "../socket/types";
import { numberToCurrency } from "../utils";
import { LightContext } from "./light_liveview";

export interface LicenseContext {
Expand All @@ -15,7 +17,7 @@ export class LicenseLiveViewComponent implements
{


mount(params: any, session: any, socket: any) {
mount(params: any, session: any, socket: PhxSocket) {
// store this somewhere durable
const seats = 2;
const amount = calculateLicenseAmount(seats);
Expand Down Expand Up @@ -52,8 +54,8 @@ export class LicenseLiveViewComponent implements
`
};

handleEvent(event: "update", params: {seats: number}, socket: any) {
console.log("event:", event, params, socket);
handleEvent(event: "update", params: {seats: number}, socket: PhxSocket) {
// console.log("event:", event, params, socket);
const { seats } = params;
const amount = calculateLicenseAmount(seats);
return { data: { seats, amount} };
Expand All @@ -69,10 +71,3 @@ function calculateLicenseAmount(seats: number): number {
}
}

function numberToCurrency(amount: number) {
var formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
});
return formatter.format(amount);
}
5 changes: 3 additions & 2 deletions src/server/live/light_liveview.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import escapeHtml from "../liveview/templates";
import { LiveViewComponent, LiveViewContext, LiveViewExternalEventListener } from "../liveview/types";
import { PhxSocket } from "../socket/types";

export interface LightContext {
brightness: number;
Expand All @@ -15,7 +16,7 @@ export class LightLiveViewComponent implements
LiveViewExternalEventListener<LightContext, "off", any> {


mount(params: any, session: any, socket: any) {
mount(params: any, session: any, socket: PhxSocket) {
// store this somewhere durable
const ctx: LightContext = { brightness: 10 };
_db[socket.id] = ctx;
Expand Down Expand Up @@ -51,7 +52,7 @@ export class LightLiveViewComponent implements
`
};

handleEvent(event: LightEvent, params: any, socket: any) {
handleEvent(event: LightEvent, params: any, socket: PhxSocket) {
const ctx = _db[socket.id];
console.log("event:", event, socket, ctx);
switch (event) {
Expand Down
4 changes: 3 additions & 1 deletion src/server/live/router.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { LiveViewRouter } from "../liveview/types";
import { LicenseLiveViewComponent } from "./license_liveview";
import { LightLiveViewComponent } from "./light_liveview";
import { SalesDashboardLiveViewComponent } from "./sales_dashboard_liveview";

export const router: LiveViewRouter = {
"/license": new LicenseLiveViewComponent(),
"/light": new LightLiveViewComponent()
"/light": new LightLiveViewComponent(),
'/sales-dashboard': new SalesDashboardLiveViewComponent()
}
108 changes: 108 additions & 0 deletions src/server/live/sales_dashboard_liveview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import escapeHtml from "../liveview/templates";
import { LiveViewComponent, LiveViewContext, LiveViewExternalEventListener, LiveViewInternalEventListener } from "../liveview/types";
import { PhxSocket } from "../socket/types";
import { sendInternalMessage } from "../socket/websocket_server";
import { numberToCurrency } from "../utils";

// generate a random number between min and max
const random = (min: number, max: number): () => number => {
return () => Math.floor(Math.random() * (max - min + 1)) + min;
}

const randomSalesAmount = random(100, 1000);
const randomNewOrders = random(5, 20);
const randomSatisfaction = random(95, 100);


export interface SalesDashboardContext {
newOrders: number;
salesAmount: number;
satisfaction: number;
}

export class SalesDashboardLiveViewComponent implements
LiveViewComponent<SalesDashboardContext>,
LiveViewExternalEventListener<SalesDashboardContext, "refresh", any>,
LiveViewInternalEventListener<SalesDashboardContext, "tick">
{

mount(params: any, session: any, socket: PhxSocket) {
if(socket.connected) {
const intervalId = setInterval(() => {
sendInternalMessage(socket, this, "tick");
}, 1000);
}
return {
data: {
...generateSalesDashboardContext()
}
}
};

render(context: LiveViewContext<SalesDashboardContext>) {
return escapeHtml`
<h1>Sales Dashboard</h1>
<div id="dashboard">
<div class="stats">
<div class="stat">
<span class="value">
${context.data.newOrders}
</span>
<span class="name">
New Orders
</span>
</div>
<div class="stat">
<span class="value">
${ numberToCurrency(context.data.salesAmount)}
</span>
<span class="name">
Sales Amount
</span>
</div>
<div class="stat">
<span class="value">
${context.data.satisfaction}
</span>
<span class="name">
Satisfaction
</span>
</div>
</div>
<button phx-click="refresh">
<img src="images/refresh.svg" />
Refresh
</button>
</div>
`
}

handleEvent(event: "refresh", params: any, socket: any): LiveViewContext<SalesDashboardContext> {
return {
data: {
...generateSalesDashboardContext()
}
}
}

handleInfo(event: "tick", socket: PhxSocket): LiveViewContext<SalesDashboardContext> {
return {
data: {
...generateSalesDashboardContext()
}
}
}

}

function generateSalesDashboardContext(): SalesDashboardContext {
return {
newOrders: randomNewOrders(),
salesAmount: randomSalesAmount(),
satisfaction: randomSatisfaction()
}
}




9 changes: 7 additions & 2 deletions src/server/liveview/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PhxSocket } from "../socket/types";
import escapeHtml, { HtmlSafeString } from "./templates";


Expand All @@ -10,13 +11,17 @@ export interface LiveViewTemplate extends HtmlSafeString {

export interface LiveViewComponent<T> {

mount: (params: any, session: any, socket: any) => LiveViewContext<T>;
mount: (params: any, session: any, socket: PhxSocket) => LiveViewContext<T>;
render: (context: LiveViewContext<T>) => LiveViewTemplate;

}

export interface LiveViewExternalEventListener<T, E extends string, P> {
handleEvent: (event: Lowercase<E>, params: P, socket: any) => LiveViewContext<T>;
handleEvent: (event: Lowercase<E>, params: P, socket: PhxSocket) => LiveViewContext<T>;
}

export interface LiveViewInternalEventListener<T, E extends string> {
handleInfo: (event: Lowercase<E>, socket: PhxSocket) => LiveViewContext<T>;
}

export interface LiveViewRouter {
Expand Down
13 changes: 11 additions & 2 deletions src/server/socket/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import WebSocket from 'ws';

export interface PhxSocket {
id: string;
connected: boolean; // true for websocket, false for http request
socket?: WebSocket;
}

export enum PhxSocketProtocolNames {
joinRef = 0,
messageRef,
Expand All @@ -16,9 +24,9 @@ export type PhxIncomingMessage<Payload> = [

export type PhxOutgoingMessage<Payload> = [
joinRef: string | null, // number
messageRef: string, // number
messageRef: string | null, // number
topic: "phoenix" | string,
event: "phx_reply",
event: "phx_reply" | "diff",
payload: Payload
]

Expand All @@ -45,6 +53,7 @@ export interface PhxReplyPayload {
}

export type PhxReply = PhxOutgoingMessage<PhxReplyPayload>;
export type PhxDiffReply = PhxOutgoingMessage<Dynamics>;

export interface PhxEventPayload<Type extends string,Value> {
type: Type,
Expand Down
68 changes: 56 additions & 12 deletions src/server/socket/websocket_server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { PhxReply, PhxSocketProtocolNames, RenderedNode, PhxOutgoingMessage, newHeartbeatReply, PhxJoinIncoming, PhxHeartbeatIncoming, PhxClickEvent, PhxFormEvent, PhxIncomingMessage, PhxClickPayload, PhxFormPayload } from './types';
import ws from 'ws';
import { PhxReply, PhxSocketProtocolNames, RenderedNode, PhxOutgoingMessage, newHeartbeatReply, PhxJoinIncoming, PhxHeartbeatIncoming, PhxClickEvent, PhxFormEvent, PhxIncomingMessage, PhxClickPayload, PhxFormPayload, PhxSocket, PhxDiffReply } from './types';
import WebSocket from 'ws';
import { router } from '../live/router';
import qs from 'querystring';
import { URLSearchParams } from 'url';
import { LiveViewComponent } from '../liveview/types';

const wsServer = new ws.Server({
const wsServer = new WebSocket.Server({
port: 3003,
});

Expand All @@ -14,7 +14,16 @@ wsServer.on('connection', socket => {
// console.log("socket connected", socket);

socket.on('message', message => {
console.log("message", String(message));

onMessage(socket, message);

});
});


function onMessage(socket: WebSocket, message: WebSocket.RawData) {

console.log("message", String(message));
// get raw message to string
const stringMsg = message.toString();
// console.log("message", stringMsg);
Expand Down Expand Up @@ -56,13 +65,9 @@ wsServer.on('connection', socket => {
// unknown message type
console.error("unknown message type", rawPhxMessage);
}
}

// decode phx protocol
});
});


function onPhxJoin(socket: any, message: PhxJoinIncoming) {
function onPhxJoin(socket: WebSocket, message: PhxJoinIncoming) {
// console.log("phx_join", message);

// use url to route join request to component
Expand All @@ -77,7 +82,12 @@ function onPhxJoin(socket: any, message: PhxJoinIncoming) {
// update topicToPath
topicToPath[topic] = url.pathname;

const ctx = component.mount({}, {}, { id: message[PhxSocketProtocolNames.topic] });
const phxSocket: PhxSocket = {
id: topic,
connected: true, // websocket is connected
socket
}
const ctx = component.mount({}, {}, phxSocket);
const view = component.render(ctx);

// map array of dynamics to object with indiceies as keys
Expand Down Expand Up @@ -255,4 +265,38 @@ function onHeartbeat(socket: any, message: PhxHeartbeatIncoming) {
});
}

export function sendInternalMessage(socket: PhxSocket, component: LiveViewComponent<any>, event: any) {
console.log("internal message", event);
// check if component has event handler
if(!(component as any).handleInfo) {
console.warn("no info handler for component", component);
return;
}

// @ts-ignore
const ctx = component.handleInfo(event, socket);

const view = component.render(ctx);

// map array of dynamics to object with indiceies as keys
const dynamics = view.dynamics.reduce((acc: { [key: number]: string }, cur: string, index: number) => {
acc[index] = cur;
return acc;
}, {} as { [key: string]: string })

const reply: PhxDiffReply = [
null,
null,
socket.id,
"diff",
{ ...dynamics }
]
// console.log("sending phx_reply", reply);
socket.socket?.send(JSON.stringify(reply), { binary: false }, (err: any) => {
if (err) {
console.error("error", err)
}
});
}

export { wsServer };
7 changes: 7 additions & 0 deletions src/server/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function numberToCurrency(amount: number) {
var formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
});
return formatter.format(amount);
}

0 comments on commit 83b7f3e

Please sign in to comment.