From d9d9f0c195e16f245e3aa333b64782cbe4724d44 Mon Sep 17 00:00:00 2001 From: Donnie Flood Date: Wed, 26 Jan 2022 12:43:17 -0700 Subject: [PATCH] use socket for sending internal messages to handleInfo --- src/examples/autocomplete/component.ts | 7 +- src/examples/live-search/component.ts | 8 +-- src/examples/sales_dashboard_liveview.ts | 3 +- src/server/live_view_server.ts | 3 +- src/server/socket/component_manager.ts | 82 +++++++++++++----------- src/server/socket/message_router.ts | 69 ++++++++++---------- src/server/types.ts | 15 ++--- 7 files changed, 96 insertions(+), 91 deletions(-) diff --git a/src/examples/autocomplete/component.ts b/src/examples/autocomplete/component.ts index 8f933b7d..fa43ade3 100644 --- a/src/examples/autocomplete/component.ts +++ b/src/examples/autocomplete/component.ts @@ -1,6 +1,5 @@ import html from "../../server/templates"; import { BaseLiveViewComponent, LiveViewExternalEventListener, LiveViewInternalEventListener, LiveViewSocket } from "../../server/types"; -import { sendInternalMessage } from "../../server/socket/message_router"; import { WebSocket } from "ws"; import { searchByCity, searchByZip, Store } from "../live-search/data"; import { suggest } from "./data"; @@ -116,14 +115,14 @@ export class AutocompleteLiveViewComponent extends BaseLiveViewComponent) { - console.log("event:", event, params, socket); + // console.log("event:", event, params, socket); if (event === "zip-search") { // @ts-ignore TODO better params types for different events const { zip } = params; // wait a second to send the message setTimeout(() => { - sendInternalMessage(socket, this, { type: "run_zip_search", zip }); + socket.sendInternal({ type: "run_zip_search", zip }); }, 1000); return { zip, city: "", stores: [], matches: [], loading: true }; } @@ -140,7 +139,7 @@ export class AutocompleteLiveViewComponent extends BaseLiveViewComponent { - sendInternalMessage(socket, this, { type: "run_city_search", city }); + socket.sendInternal({ type: "run_city_search", city }); }, 1000); return { zip: "", city, stores: [], matches: [], loading: true }; } diff --git a/src/examples/live-search/component.ts b/src/examples/live-search/component.ts index 22259d21..ad9bd9fd 100644 --- a/src/examples/live-search/component.ts +++ b/src/examples/live-search/component.ts @@ -1,7 +1,5 @@ import html from "../../server/templates"; import { BaseLiveViewComponent, LiveViewExternalEventListener, LiveViewInternalEventListener, LiveViewSocket } from "../../server/types"; -import { sendInternalMessage } from "../../server/socket/message_router"; -import { WebSocket } from "ws"; import { searchByZip, Store } from "./data"; @@ -90,18 +88,18 @@ export class SearchLiveViewComponent extends BaseLiveViewComponent) { - console.log("event:", event, params, socket); + // console.log("event:", event, params, socket); const { zip } = params; // wait a second to send the message setTimeout(() => { - sendInternalMessage(socket, this, { type: "run_zip_search", zip }); + socket.sendInternal({ type: "run_zip_search", zip }); }, 1000); return { zip, stores: [], loading: true }; } handleInfo(event: { type: "run_zip_search", zip: string }, socket: LiveViewSocket) { - // console.log("run_zip_search:", event, socket); + console.log("run_zip_search:", event); const { zip } = event; const stores = searchByZip(zip); return { diff --git a/src/examples/sales_dashboard_liveview.ts b/src/examples/sales_dashboard_liveview.ts index 17993df6..30063ad2 100644 --- a/src/examples/sales_dashboard_liveview.ts +++ b/src/examples/sales_dashboard_liveview.ts @@ -1,7 +1,6 @@ import html from "../server/templates"; import { BaseLiveViewComponent, LiveViewExternalEventListener, LiveViewInternalEventListener, LiveViewSocket } from "../server/types"; import { numberToCurrency } from "./utils"; -import { sendInternalMessage } from "../server/socket/message_router"; // generate a random number between min and max const random = (min: number, max: number): () => number => { @@ -28,7 +27,7 @@ export class SalesDashboardLiveViewComponent extends BaseLiveViewComponent { - sendInternalMessage(socket, this, "tick"); + socket.sendInternal("tick"); }, 1000); } return { diff --git a/src/server/live_view_server.ts b/src/server/live_view_server.ts index f93b6e5f..597d46d3 100644 --- a/src/server/live_view_server.ts +++ b/src/server/live_view_server.ts @@ -108,7 +108,8 @@ export class LiveViewServer { const liveViewSocket: LiveViewSocket = { id: liveViewId, connected: false, // ws socket not connected on http request - context: {} + context: {}, + sendInternal: () => { }, } // look up component for route diff --git a/src/server/socket/component_manager.ts b/src/server/socket/component_manager.ts index 16645fc8..ccd46eb1 100644 --- a/src/server/socket/component_manager.ts +++ b/src/server/socket/component_manager.ts @@ -1,13 +1,12 @@ import { WebSocket } from "ws"; import { LiveViewComponent, LiveViewSocket } from ".."; -import { newHeartbeatReply, newPhxReply, PhxClickPayload, PhxFormPayload, PhxHeartbeatIncoming, PhxIncomingMessage, PhxJoinIncoming, PhxJoinPayload, PhxLivePatchIncoming, PhxOutgoingMessage } from "./types"; +import { newHeartbeatReply, newPhxReply, PhxClickPayload, PhxDiffReply, PhxFormPayload, PhxHeartbeatIncoming, PhxIncomingMessage, PhxJoinIncoming, PhxJoinPayload, PhxLivePatchIncoming, PhxOutgoingMessage } from "./types"; import jwt from 'jsonwebtoken'; export class LiveViewComponentManager { private context: unknown; private component: LiveViewComponent; - private topic: string; private signingSecret: string; constructor(component: LiveViewComponent, signingSecret: string) { @@ -31,19 +30,13 @@ export class LiveViewComponentManager { // console.log("session is", session); const session = {} - const liveViewSocket: LiveViewSocket = { - id: topic, - connected: true, // websocket is connected - ws, // the websocket - context: this.context - } + const liveViewSocket = this.buildLiveViewSocket(ws, topic); // pass in phx_join payload params, session, and socket this.context = this.component.mount(payloadParams, session, liveViewSocket); - const ctx = this.component.handleParams(urlParams, urlString, liveViewSocket); - // merge contexts - if (typeof this.context === 'object' && typeof ctx === 'object' && Object.keys(ctx).length > 0) { - this.context = { ...this.context, ...ctx }; - } + + // update socket with new context + liveViewSocket.context = this.context; + this.context = this.component.handleParams(urlParams, urlString, liveViewSocket); const view = this.component.render(this.context); @@ -78,18 +71,8 @@ export class LiveViewComponentManager { } if (isEventHandler(this.component)) { - const phxSocket: LiveViewSocket = { - id: topic, - connected: true, // websocket is connected - ws, // the websocket - context: this.context - } // @ts-ignore - already checked if handleEvent is defined - const ctx = this.component.handleEvent(event, value, phxSocket); - // merge contexts - if (typeof this.context === 'object' && typeof ctx === 'object' && Object.keys(ctx).length > 0) { - this.context = { ...this.context, ...ctx }; - } + this.context = this.component.handleEvent(event, value, this.buildLiveViewSocket(ws, topic)); const view = this.component.render(this.context); @@ -121,17 +104,7 @@ export class LiveViewComponentManager { // @ts-ignore - URLSearchParams has an entries method but not typed const params = Object.fromEntries(url.searchParams); - const phxSocket: LiveViewSocket = { - id: topic, - connected: true, // websocket is connected - ws, // the websocket - context: this.context - } - - const ctx = this.component.handleParams(params, urlString, phxSocket); - if (typeof this.context === 'object' && typeof ctx === 'object' && Object.keys(ctx).length > 0) { - this.context = { ...this.context, ...ctx }; - } + this.context = this.component.handleParams(params, urlString, this.buildLiveViewSocket(ws, topic)); const view = this.component.render(this.context); @@ -147,8 +120,41 @@ export class LiveViewComponentManager { this.sendPhxReply(ws, newPhxReply(message, replyPayload)); } + private sendInternal(ws: WebSocket, event: any, topic: string): void { + // console.log("sendInternal", event); - sendPhxReply(ws: WebSocket, reply: PhxOutgoingMessage) { + if (isInfoHandler(this.component)) { + // @ts-ignore - already checked if handleInfo is defined + this.context = this.component.handleInfo(event, this.buildLiveViewSocket(ws, topic)); + + const view = this.component.render(this.context); + + const reply: PhxDiffReply = [ + null, // no join reference + null, // no message reference + topic, + "diff", + view.partsTree(false) as any + ] + + this.sendPhxReply(ws, reply); + } + else { + console.error("received internal event but no handleInfo in component", this.component); + } + } + + private buildLiveViewSocket(ws: WebSocket, topic: string): LiveViewSocket { + return { + id: topic, + connected: true, // websocket is connected + ws, // the websocket + context: this.context, + sendInternal: (event) => this.sendInternal(ws, event, topic), + } + } + + private sendPhxReply(ws: WebSocket, reply: PhxOutgoingMessage) { ws.send(JSON.stringify(reply), { binary: false }, (err: any) => { if (err) { console.error("error", err); @@ -158,6 +164,10 @@ export class LiveViewComponentManager { } +function isInfoHandler(component: LiveViewComponent) { + return "handleInfo" in component; +} + function isEventHandler(component: LiveViewComponent) { return "handleEvent" in component; } \ No newline at end of file diff --git a/src/server/socket/message_router.ts b/src/server/socket/message_router.ts index f028cc7d..156234d7 100644 --- a/src/server/socket/message_router.ts +++ b/src/server/socket/message_router.ts @@ -18,6 +18,7 @@ export function onMessage(ws: WebSocket, message: WebSocket.RawData, router: Liv if (typeof rawPhxMessage === 'object' && Array.isArray(rawPhxMessage) && rawPhxMessage.length === 5) { const [joinRef, messageRef, topic, event, payload] = rawPhxMessage; + let componentManager: LiveViewComponentManager | undefined; switch (event) { case "phx_join": // assume componentManager is not defined since join creates a new component manager @@ -25,7 +26,7 @@ export function onMessage(ws: WebSocket, message: WebSocket.RawData, router: Liv break; case "heartbeat": // heartbeat comes in as a "phoenix" topic so lookup via connectionId - let componentManager = heartbeatRouter[connectionId]; + componentManager = heartbeatRouter[connectionId]; if (componentManager) { componentManager.onHeartbeat(ws, rawPhxMessage as PhxHeartbeatIncoming); } else { @@ -79,45 +80,45 @@ export function onPhxJoin(ws: WebSocket, message: PhxJoinIncoming, router: LiveV } -export function sendInternalMessage(socket: LiveViewSocket, component: LiveViewComponent, event: any, payload?: any) { +// export function sendInternalMessage(socket: LiveViewSocket, component: LiveViewComponent, event: any, payload?: any) { - // check if component has event handler - if (!(component as any).handleInfo) { - console.warn("no info handler for component", component); - return; - } +// // 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); +// // @ts-ignore +// const ctx = component.handleInfo(event, socket); - const view = component.render(ctx); +// const view = component.render(ctx); - const reply: PhxDiffReply = [ - null, // no join reference - null, // no message reference - socket.id, - "diff", - view.partsTree(false) as any - ] +// const reply: PhxDiffReply = [ +// null, // no join reference +// null, // no message reference +// socket.id, +// "diff", +// view.partsTree(false) as any +// ] - sendPhxReply(socket.ws!, reply); -} +// sendPhxReply(socket.ws!, reply); +// } -function sendPhxReply(ws: WebSocket, reply: PhxOutgoingMessage) { - ws.send(JSON.stringify(reply), { binary: false }, (err: any) => { - if (err) { - console.error("error", err); - } - }); -} +// function sendPhxReply(ws: WebSocket, reply: PhxOutgoingMessage) { +// ws.send(JSON.stringify(reply), { binary: false }, (err: any) => { +// if (err) { +// console.error("error", err); +// } +// }); +// } -function printHtml(rendered: RenderedNode) { - const statics = rendered.s; - let html = statics[0]; - for (let i = 1; i < statics.length; i++) { - html += rendered[i - 1] + statics[i]; - } - console.log("html:\n", html); -} \ No newline at end of file +// function printHtml(rendered: RenderedNode) { +// const statics = rendered.s; +// let html = statics[0]; +// for (let i = 1; i < statics.length; i++) { +// html += rendered[i - 1] + statics[i]; +// } +// console.log("html:\n", html); +// } \ No newline at end of file diff --git a/src/server/types.ts b/src/server/types.ts index aa8e342a..ae17f468 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -6,12 +6,9 @@ export interface LiveViewSocket { connected: boolean; // true for websocket, false for http request context: T; ws?: WebSocket; + sendInternal: (event: unknown) => void; } -// export interface LiveViewContext { -// data: T; -// } - export interface LiveViewTemplate extends HtmlSafeString { } @@ -28,18 +25,18 @@ export interface LiveViewComponent { mount(params: LiveViewMountParams, session: LiveViewSessionParams, socket: LiveViewSocket): T; render(context: T): LiveViewTemplate; - handleParams(params: P, url: string, socket: LiveViewSocket): Partial; + handleParams(params: P, url: string, socket: LiveViewSocket): T; } // TODO: support event returing Partial? export interface LiveViewExternalEventListener { - handleEvent(event: Lowercase, params: P, socket: LiveViewSocket): Partial; + handleEvent(event: Lowercase, params: P, socket: LiveViewSocket): T; } // TODO: support event returing Partial? export interface LiveViewInternalEventListener { - handleInfo(event: E, socket: LiveViewSocket): Partial; + handleInfo(event: E, socket: LiveViewSocket): T; } export interface LiveViewRouter { @@ -51,8 +48,8 @@ export abstract class BaseLiveViewComponent implements LiveViewComponent): T; abstract render(context: T): LiveViewTemplate; - handleParams(params: P, url: string, socket: LiveViewSocket): Partial { - return {}; + handleParams(params: P, url: string, socket: LiveViewSocket): T { + return socket.context; } }