Skip to content

Commit

Permalink
use socket for sending internal messages to handleInfo
Browse files Browse the repository at this point in the history
floodfx committed Jan 26, 2022

Verified

This commit was signed with the committer’s verified signature.
rm3l Armel Soro
1 parent 63e234d commit d9d9f0c
Showing 7 changed files with 96 additions and 91 deletions.
7 changes: 3 additions & 4 deletions src/examples/autocomplete/component.ts
Original file line number Diff line number Diff line change
@@ -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<Autocom
};

handleEvent(event: "zip-search" | "suggest-city", params: { zip: string } | { city: string }, socket: LiveViewSocket<AutocompleteContext>) {
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<Autocom
const { city } = params;
// wait a second to send the message
setTimeout(() => {
sendInternalMessage(socket, this, { type: "run_city_search", city });
socket.sendInternal({ type: "run_city_search", city });
}, 1000);
return { zip: "", city, stores: [], matches: [], loading: true };
}
8 changes: 3 additions & 5 deletions src/examples/live-search/component.ts
Original file line number Diff line number Diff line change
@@ -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<SearchContext
};

handleEvent(event: "zip-search", params: { zip: string }, socket: LiveViewSocket<SearchContext>) {
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<SearchContext>) {
// console.log("run_zip_search:", event, socket);
console.log("run_zip_search:", event);
const { zip } = event;
const stores = searchByZip(zip);
return {
3 changes: 1 addition & 2 deletions src/examples/sales_dashboard_liveview.ts
Original file line number Diff line number Diff line change
@@ -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<Sales
if (socket.connected) {
// TODO clean up interval on unmount
const intervalId = setInterval(() => {
sendInternalMessage(socket, this, "tick");
socket.sendInternal("tick");
}, 1000);
}
return {
3 changes: 2 additions & 1 deletion src/server/live_view_server.ts
Original file line number Diff line number Diff line change
@@ -108,7 +108,8 @@ export class LiveViewServer {
const liveViewSocket: LiveViewSocket<unknown> = {
id: liveViewId,
connected: false, // ws socket not connected on http request
context: {}
context: {},
sendInternal: () => { },
}

// look up component for route
82 changes: 46 additions & 36 deletions src/server/socket/component_manager.ts
Original file line number Diff line number Diff line change
@@ -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<unknown, unknown>;
private topic: string;
private signingSecret: string;

constructor(component: LiveViewComponent<unknown, unknown>, signingSecret: string) {
@@ -31,19 +30,13 @@ export class LiveViewComponentManager {
// console.log("session is", session);
const session = {}

const liveViewSocket: LiveViewSocket<unknown> = {
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<unknown> = {
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<unknown> = {
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<any>) {
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<unknown> {
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<any>) {
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<unknown, unknown>) {
return "handleInfo" in component;
}

function isEventHandler(component: LiveViewComponent<unknown, unknown>) {
return "handleEvent" in component;
}
69 changes: 35 additions & 34 deletions src/server/socket/message_router.ts
Original file line number Diff line number Diff line change
@@ -18,14 +18,15 @@ 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
onPhxJoin(ws, rawPhxMessage as PhxJoinIncoming, router, signingSecret, connectionId);
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<unknown>, component: LiveViewComponent<any, any>, event: any, payload?: any) {
// export function sendInternalMessage(socket: LiveViewSocket<unknown>, component: LiveViewComponent<any, any>, 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<any>) {
ws.send(JSON.stringify(reply), { binary: false }, (err: any) => {
if (err) {
console.error("error", err);
}
});
}
// function sendPhxReply(ws: WebSocket, reply: PhxOutgoingMessage<any>) {
// 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);
}
// 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);
// }
15 changes: 6 additions & 9 deletions src/server/types.ts
Original file line number Diff line number Diff line change
@@ -6,12 +6,9 @@ export interface LiveViewSocket<T> {
connected: boolean; // true for websocket, false for http request
context: T;
ws?: WebSocket;
sendInternal: (event: unknown) => void;
}

// export interface LiveViewContext<T> {
// data: T;
// }

export interface LiveViewTemplate extends HtmlSafeString {
}

@@ -28,18 +25,18 @@ export interface LiveViewComponent<T, P> {

mount(params: LiveViewMountParams, session: LiveViewSessionParams, socket: LiveViewSocket<T>): T;
render(context: T): LiveViewTemplate;
handleParams(params: P, url: string, socket: LiveViewSocket<T>): Partial<T>;
handleParams(params: P, url: string, socket: LiveViewSocket<T>): T;

}

// TODO: support event returing Partial<T>?
export interface LiveViewExternalEventListener<T, E extends string, P> {
handleEvent(event: Lowercase<E>, params: P, socket: LiveViewSocket<T>): Partial<T>;
handleEvent(event: Lowercase<E>, params: P, socket: LiveViewSocket<T>): T;
}

// TODO: support event returing Partial<T>?
export interface LiveViewInternalEventListener<T, E> {
handleInfo(event: E, socket: LiveViewSocket<T>): Partial<T>;
handleInfo(event: E, socket: LiveViewSocket<T>): T;
}

export interface LiveViewRouter {
@@ -51,8 +48,8 @@ export abstract class BaseLiveViewComponent<T, P> implements LiveViewComponent<T
abstract mount(params: any, session: any, socket: LiveViewSocket<T>): T;
abstract render(context: T): LiveViewTemplate;

handleParams(params: P, url: string, socket: LiveViewSocket<T>): Partial<T> {
return {};
handleParams(params: P, url: string, socket: LiveViewSocket<T>): T {
return socket.context;
}

}

0 comments on commit d9d9f0c

Please sign in to comment.