diff --git a/node/package.json b/node/package.json index bea5812..47b9a15 100644 --- a/node/package.json +++ b/node/package.json @@ -19,13 +19,12 @@ "lint": "eslint --ext .js,.ts --max-warnings 0 src/" }, "dependencies": { - "@mojotech/json-type-validation": "^3.1.0", "better-sqlite3": "^6.0.1", "express": "^4.17.1", "node": "^13.10.1", "qanban-types": "0.0.1", + "qured-client": "0.0.1", "uuid": "^7.0.2", - "ws": "^7.2.3", "yargs": "^15.3.1" }, "devDependencies": { @@ -36,7 +35,6 @@ "@types/node-fetch": "^2.5.5", "@types/uuid": "^7.0.0", "@types/wait-on": "^4.0.0", - "@types/ws": "^7.2.2", "@types/yargs": "^15.0.4", "@typescript-eslint/eslint-plugin": "^2.23.0", "@typescript-eslint/parser": "^2.23.0", diff --git a/node/src/index.ts b/node/src/index.ts index b7701bf..de1b6e1 100644 --- a/node/src/index.ts +++ b/node/src/index.ts @@ -1,13 +1,12 @@ -import * as jtv from '@mojotech/json-type-validation'; import sqlite3 from 'better-sqlite3'; import express from 'express'; -import WebSocket from 'ws'; import { Command, commandDecoder, Contract, contractDecoder, ContractState, Id, idDecoder, Message, messageDecoder, Party, partyDecoder, UpdateMessage } from 'qanban-types'; import { v4 as uuidV4 } from 'uuid'; import yargs from 'yargs'; import os from 'os'; import path from 'path'; import fs from 'fs'; +import QuredClient, { Message as QuredMessage } from 'qured-client'; type Ledger = Readonly<{ list(): Id[]; @@ -17,11 +16,6 @@ type Ledger = Readonly<{ update(id: Id, contract: Contract): void; }> -const sendMessage = (socket: WebSocket, message: unknown) => { - console.log('outgoing message:', message); - socket.send(JSON.stringify(message)); -} - function stakeholders(contract: Contract): Set { const stakeholders = new Set(); stakeholders.add(contract.proposer); @@ -133,19 +127,15 @@ function updateLedger(ledger: Ledger, persist: boolean, sender: Party, message: } } -function handleMessage(ledger: Ledger, rawMessage: unknown) { +function handleMessage(ledger: Ledger, message: QuredMessage) { try { - const { sender, payload: message } = jtv.object({ - sender: partyDecoder(), - payload: messageDecoder(), - }).runWithException(rawMessage); - updateLedger(ledger, true, sender, message); + updateLedger(ledger, true, message.sender, message.payload); } catch (error) { - console.error('failed to handle message', rawMessage, error); + console.error('failed to handle message', message, error); } } -function handleCommand(ledger: Ledger, socket: WebSocket, participant: Party, command: Command) { +function handleCommand(ledger: Ledger, client: QuredClient, participant: Party, command: Command) { let id: Id; let message: Message; if (command.type === "propose") { @@ -155,7 +145,7 @@ function handleCommand(ledger: Ledger, socket: WebSocket, participant: Party, co message = { ...command }; } const contract = updateLedger(ledger, false, participant, message); - sendMessage(socket, { + client.send({ sender: participant, receivers: [...stakeholders(contract)], payload: message @@ -260,21 +250,19 @@ if (args.clean && fs.existsSync(database)) { const ledger = Ledger(database); -const socket = new WebSocket(`ws://${routerHost}`); +const client = new QuredClient({ + router: `ws://${routerHost}`, + login: participant, + partyDecoder: partyDecoder(), + payloadDecoder: messageDecoder(), +}); -socket.on('open', () => { - console.log('connected to router'); - socket.on('ping', () => socket.pong()); - socket.on('message', rawMessage => { - const json = JSON.parse(rawMessage.toString()); - console.log('incoming message:', json); - handleMessage(ledger, json); - }); - socket.on('close', () => { - process.exit(1); - }); - sendMessage(socket, { login: participant }); +client.on("open", () => console.log(`connected to router at ${routerHost}`)); +client.on("message", message => handleMessage(ledger, message)); +client.on("error", error => { + console.error(error instanceof Error ? error.toString() : JSON.stringify(error)); }); +client.on("close", () => process.exit(1)); const app = express(); app.use(express.static('ui/build')); @@ -297,7 +285,7 @@ app.post('/api/command', (req, res) => { try { console.log('incoming command:', req.body); const command = commandDecoder().runWithException(req.body); - handleCommand(ledger, socket, participant, command); + handleCommand(ledger, client, participant, command); res.status(200).send({ success: true }); } catch (error) { res.status(500).send({ diff --git a/qured-client/src/index.ts b/qured-client/src/index.ts index f9002a6..b71314b 100644 --- a/qured-client/src/index.ts +++ b/qured-client/src/index.ts @@ -2,46 +2,52 @@ import * as jtv from '@mojotech/json-type-validation'; import Emittery from 'emittery'; import WebSocket from 'ws'; -export type Config = { - router: string; - login: string; - payloadDecoder?: jtv.Decoder; -} +export type Config = { + readonly router: string; + readonly login: Party; + readonly partyDecoder?: jtv.Decoder; + readonly payloadDecoder?: jtv.Decoder; +} & (string extends Party ? {} : { readonly partyDecoder: jtv.Decoder }) +// NOTE(MH): The purpose of the intersection with the conditional type is to +// make `partyDecoder` non-optional when `Party` is _not_ `string`. -export type Message = { - sender: string; - receivers: string[]; +export type Message = { + sender: Party; + receivers: Party[]; payload: Payload; } -export type Events = { - message: Message; +export type Events = { + message: Message; error: unknown; close: number; } export type EmptyEvents = "open" -export default class QuredClient extends Emittery.Typed, EmptyEvents> { +export default class QuredClient extends Emittery.Typed, EmptyEvents> { private socket: WebSocket; - private messageDecoder: jtv.Decoder>; + private login: Party; + private messageDecoder: jtv.Decoder>; - constructor(config: Config) { + constructor(config: Config) { super(); const socket = new WebSocket(config.router); this.socket = socket; + this.login = config.login; + const partyDecoder = config.partyDecoder ?? jtv.string().map(party => party as Party); this.messageDecoder = jtv.object({ - sender: jtv.string(), - receivers: jtv.array(jtv.string()), + sender: partyDecoder, + receivers: jtv.array(partyDecoder), payload: config.payloadDecoder ?? jtv.anyJson(), }); socket.on("open", () => { socket.on("ping", () => socket.pong()); socket.on("message", rawMessage => { try { - console.log(`incoming message: ${rawMessage}`); - const message: Message = - this.messageDecoder.runWithException(JSON.parse(rawMessage.toString())); + const json = JSON.parse(rawMessage.toString()); + console.log('incoming message:', json); + const message: Message = this.messageDecoder.runWithException(json); // eslint-disable-next-line @typescript-eslint/no-floating-promises this.emit("message", message); } catch (error) { @@ -63,10 +69,13 @@ export default class QuredClient extends Emittery.Typed }); } - send(message: Message): void { - const rawMessage = JSON.stringify(message); - console.log(`outgoing message: ${rawMessage}`); - this.socket.send(rawMessage); + send(message: Message): void { + if (message.sender !== this.login) { + throw Error(`sender '${message.sender}' of message does not match login '${this.login}'`); + } + const json = this.messageDecoder.runWithException(message); + console.log('outgoing message:', json); + this.socket.send(JSON.stringify(json)); } close(): void {