wstx
v2.1.6
Published
Simple RPC-like websocket communications for Node.JS and the browser
Downloads
7
Readme
WsTx
Simple RPC-like websocket communications for Node.JS and the browser.
Getting started
Shared code:
/**
* First, define the router config in a shared-code
* location that both client and server can access
*/
import { defineRouterConfig, action } from "wstx"
export const wsConfig = defineRouterConfig({
// create 'actions' - they can be nested!
todos: {
create: action<(text: string) => TodoItem>(),
getAll: action<() => TodoItem[]>(),
update: action<(todo: TodoItem) => TodoItem>(),
delete: action<(id: number) => TodoItem>(),
},
})
Server code:
/**
* On the server, we use 'createServerRouter' to define
* an action handler schema that matches our config
*/
import { displayError, createServerRouter } from "wstx"
const router = createServerRouter<typeof wsConfig>({
todos: {
create: (text) => {
const todo = {
id: Date.now(),
text,
}
todos.push(todo)
return todo
},
getAll: () => todos,
update: (todo) => {
const match = todos.find((t) => t.id === todo.id)
if (!match) return displayError("Item not found")
match.text = todo.text
return match
},
delete: (id) => {
const idx = todos.findIndex((t) => t.id === id)
if (idx === -1) return displayError("Item not found")
const [todo] = todos.splice(idx, 1)
return todo
},
},
})
/**
* For this example, we'll set up a simple WebSocket
* Server from the well-known 'ws' package. As sockets
* connect and disconnect we will add them to the router.
*/
new WebSocketServer({ port: 6969 })
.on("connection", (ws) => {
ws.on("error", console.error)
router.addSocket(ws)
ws.on("close", () => router.removeSocket(ws))
})
.on("listening", () => {
console.log("wss listening")
})
Client code:
/**
* Finally, we can now initialise a clientRouter that
* talks to our server and calls our actions.
*/
import { createClientRouter } from "wstx"
const wsClient = createClientRouter(
wsConfig,
new WebSocket("ws://localhost:6969")
)
async function loadTodos() {
// actions return a Tuple of [Error, null] | [null, Value]
const [error, result] = await wsClient.todos.getAll()
if (error) return console.log(error)
return result
}
Using context
/**
* First, let's revisit our config and set up our context:
*/
import { defineRouterConfig, action, context } from "wstx"
export const wsConfig = defineRouterConfig({
// define per-socket context
$context: context<{ user: User | null }>({ user: null }),
todos: {
//...
},
})
/**
* Now, when we add a new socket to the router, we'll also
* need to initialize it's context:
*/
new WebSocketServer({ port: 6969 })
.on("connection", (ws) => {
ws.on("error", console.error)
router.addSocket(ws, { user: null })
ws.on("close", () => router.removeSocket(ws))
})
.on("listening", () => {
console.log("wss listening")
})
/**
* We can now use the router's 'ctx'
* method which returns the context for the socket
* that we're currently responding to.
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* This should be called before performing any
* asynchronous calls to ensure that you get the
* correct socket context.
* The behind-the-scenes implementation looks
* something like this - notice we don't wait for
* anything:
.......
currentSocket = socket
const res = handler(...args) // this is your function
currentSocket = null
if (res instanceof Promise) {
res.then(...)
} else {
...
}
.......
*/
const router = createServerRouter<typeof wsConfig>({
//...
login: async ({ username, password }) => {
const [ctx, setCtx] = router.ctx()
if (ctx.user) return displayError("Already logged in")
const user = await authService.authenticate(username, password)
if (!user) return displayError("Incorrect credentials")
setCtx({ user })
return user
},
//...
})
Using middlewares (on the server)
/**
* you can define a group of middlewares to run before
* your handler on an action-by-action basis
*/
const router = createServerRouter<typeof wsConfig>({
todos: {
create: {
$middlewares: [
(ctx) => {
if (!ctx.user) return displayError("Unauthenticated")
},
],
handler: (text) => {
const todo = {
id: Date.now(),
text,
}
todos.push(todo)
return todo
},
},
//...
},
})
/**
* With the 'Middleware' type, you can define
* type-safe, reusable middlewares.
*/
import { type Middleware } from "wstx"
const auth: Middleware<typeof wsConfig> = (ctx) => {
if (!ctx.user) return displayError("Unauthenticated")
}
/**
* You can also create middlewares
* that apply to a group of actions by using
* 'withMiddlewares':
*/
import { withMiddlewares } from "wstx"
const router = createServerRouter<typeof wsConfig>({
todos: withMiddlewares<typeof wsConfig, typeof wsConfig.$routes.todos>(
[auth],
{
create: (text) => {
const todo = {
id: Date.now(),
text,
}
todos.push(todo)
return todo
},
//...
}
),
})