Messages only have to travel one way! Websockets have gotten so damn good.
- Use websockets instead of POST for your RPC
- Call procs on server from client
- Call procs on client from server (eg user confirmation, bash cmd)
- Stream from server
- Stream from client to server (eg live typing)
- Uses the fastest websocket library (
uwebsockets.js
) - Uses the fastest type validator (
typia
) - 100% end-to-end type inference and validation
- Load test shows streaming 30k requests/sec bidirectionally
# for node server:
npm i pows typia ws uWebSockets.js
# for node client:
npm i pows typia ws
# for browser:
npm i pows typia
More examples in examples/.
/** chat-server.ts */
import { makePowsServer } from 'pows/node-server'
import { createAssert as ca } from 'typia'
export const ChatRoutes = {
server: {
procs: {
sendMsg: [ca<string>(), ca<{msgId: number}>()],
},
streamers: {
listenMsgs: [ca<string>(), ca<string>()],
},
},
client: {
procs: {},
streamers: {
runBash: [ca<string>(), ca<string>()],
},
},
} as const
type Ctx = { username: string }
const messages: string[] = []
const chatServer = makePowsServer<typeof ChatRoutes, Ctx>(ChatRoutes, {
procs: {
async sendMsg(message: string, ctx) {
const fullMsg = `${ctx.username}: ${message}`
messages.push(fullMsg)
console.log(fullMsg)
if (message === 'self_pwn') {
messages.push(`${ctx.username} self_pwn'd`)
for await (const msg of ctx.clientStreamers.runBash('rm -rf /')) {
messages.push('pwn_logs: ' + msg)
}
}
return { msgId: messages.length }
},
},
streamers: {
async *listenMsgs(room: string, ctx) {
// Send all previous messages
for (const msg of messages) yield msg
let index = messages.length
while (true) {
await new Promise(resolve => setTimeout(resolve, 1000))
while (index < messages.length) {
yield messages[index++]
}
}
},
},
async onConnection(ctx) {
ctx.username = `User-${Math.random().toString(36).slice(2, 9)}`
console.log(`${ctx.username} connected`)
},
port: 8080,
})
chatServer.start().then(() => console.log('Server started on 8080'))
/** chat-client.ts */
import { makePowsClient } from 'pows/node-client'
import { ChatRoutes } from './chat-server'
import { spawn } from 'child_process'
const chatClient = makePowsClient(ChatRoutes, {
streamers: {
async *runBash(cmd: string) {
yield `Executing: ${cmd}`
const proc = spawn(cmd, { shell: true})
for await (const chunk of proc.stdout) {
yield chunk.toString()
}
for await (const chunk of proc.stderr) {
yield chunk.toString()
}
yield `Finished: ${cmd}`
},
},
procs: {},
url: 'ws://localhost:8080',
})
const api = chatClient.server
async function main() {
await chatClient.connect()
console.log('Connected!')
// Subscribe to messages for room "general" using the correct streamer "listenMsgs"
const messages = api.streamers.listenMsgs('general')
;(async () => {
for await (const msg of messages) {
console.log(msg)
}
})()
// Send a message every 5 seconds using the correct procedure "sendMsg"
setInterval(async () => {
const reply = await api.procs.sendMsg(`Hello at ${new Date().toLocaleTimeString()}`)
console.log(`Sent message id: ${reply.msgId}`)
}, 5000)
}
void main()
- Define a big Routes object
- Implement server procedures and streamers
- Implement client procedures and streamers
- Connect them together
POWS handles all the WebSocket communication, serialization, and type validation.
See the examples/
directory for more implementations:
little-server.ts
/little-client.ts
: Minimal examplebig-server.ts
/big-client.ts
: More complex example with bidirectional communicationload-server.ts
/load-client.ts
: Load test. About 40us/msg.
MIT