Skip to content

Commit

Permalink
make a client-server example, not force use of IDs
Browse files Browse the repository at this point in the history
  • Loading branch information
v1rtl committed Jun 4, 2023
1 parent 0d69e98 commit 0f553c2
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 76 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

# rpc

[![nest badge][nest-badge]](https://nest.land/package/rpc) [![][docs-badge]][docs] [![][code-quality-img]][code-quality]
[![nest badge][nest-badge]](https://nest.land/package/rpc)
[![][docs-badge]][docs] [![][code-quality-img]][code-quality]

</div>

JSONRPC server router for Deno using native WebSocket, based on [jsonrpc](https://github.com/Vehmloewff/jsonrpc).
JSONRPC server router for Deno using native WebSocket, based on
[jsonrpc](https://github.com/Vehmloewff/jsonrpc).

## Features

Expand Down
73 changes: 37 additions & 36 deletions server.ts → app.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { parseRequest, send } from './request.ts'
import type { Message, RPCOptions } from './types.ts'
import type { JsonRpcResponse, RPCOptions } from './types.ts'
import { lazyJSONParse, paramsEncoder } from './utils.ts'

export class App {
httpConn?: Deno.HttpConn
listener?: Deno.Listener
options: RPCOptions
socks: Map<string, WebSocket>
methods: Map<string, (params: any[], clientId: string) => Promise<any>>
methods: Map<
string,
(params: unknown[], clientId: string) => unknown | Promise<unknown>
>
emitters: Map<
string,
(params: any[], emit: (data: any) => void, clientId: string) => void
(params: unknown[], emit: (data: unknown) => void, clientId: string) => void
>
#timeout: number
constructor(options: RPCOptions = { path: '/' }) {
Expand Down Expand Up @@ -56,8 +59,8 @@ export class App {
socket.onmessage = ({ data }) => {
if (typeof data === 'string') {
this.#handleRPCMethod(clientId as string, data)
} else if (data instanceof Uint8Array) {
console.warn('Warn: an invalid jsonrpc message was sent. Skipping.')
} else {
console.warn('Warn: an invalid jsonrpc message was sent. Skipping.')
}
}

Expand Down Expand Up @@ -86,7 +89,13 @@ export class App {
method: string,
handler: (params: T, clientId: string) => unknown | Promise<unknown>,
) {
this.methods.set(method, handler as any)
this.methods.set(
method,
handler as (
params: unknown,
clientId: string,
) => unknown | Promise<unknown>,
)
}

/**
Expand All @@ -102,7 +111,6 @@ export class App {
`Warn: recieved a request from and undefined connection`,
)
}

const requests = parseRequest(data)
if (requests === 'parse-error') {
return send(sock, {
Expand All @@ -111,7 +119,7 @@ export class App {
})
}

const responses: Message[] = []
const responses: JsonRpcResponse[] = []

const promises = requests.map(async (request) => {
if (request === 'invalid') {
Expand All @@ -125,35 +133,30 @@ export class App {
const handler = this.methods.get(request.method)

if (!handler) {
if (request.id !== undefined) {
return responses.push({
error: { code: -32601, message: 'Method not found' },
id: request.id,
})
} else return
return responses.push({
error: { code: -32601, message: 'Method not found' },
id: request.id!,
})
}
const result = await handler(request.params, client)

if (request.id !== undefined) responses.push({ id: request.id, result })
responses.push({ id: request.id!, result })
} else {
// It's an emitter
const handler = this.emitters.get(request.method)

if (!handler) {
if (request.id !== undefined) {
return responses.push({
error: { code: -32601, message: 'Emitter not found' },
id: request.id,
})
} else return
return responses.push({
error: { code: -32601, message: 'Emitter not found' },
id: request.id!,
})
}

// Because emitters can return a value at any time, we are going to have to send messages on their schedule.
// This may break batches, but I don't think that is a big deal
handler(
request.params,
(data) => {
send(sock, { result: data, id: request.id })
send(sock, { result: data, id: request.id || null })
},
client,
)
Expand All @@ -170,20 +173,18 @@ export class App {
* @param options `Deno.listen` options
* @param cb Callback that triggers after HTTP server is started
*/
async listen(options: Deno.ListenOptions, cb?: () => void) {
async listen(options: Deno.ListenOptions, cb?: (addr: Deno.NetAddr) => void) {
const listener = Deno.listen(options)

const httpConn = Deno.serveHttp(await listener.accept())

this.httpConn = httpConn
this.listener = listener

cb?.()

const e = await httpConn.nextRequest()

if (e) {
e.respondWith(this.handle(e.request))
cb?.(listener.addr as Deno.NetAddr)

for await (const conn of listener) {
const requests = Deno.serveHttp(conn)
for await (const { request, respondWith } of requests) {
const response = await this.handle(request)
if (response) {
respondWith(response)
}
}
}
}
/**
Expand Down
10 changes: 5 additions & 5 deletions deno.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"fmt": {
"useTabs": false,
"lineWidth": 80,
"indentWidth": 2,
"singleQuote": true,
"semiColons": false
"useTabs": false,
"lineWidth": 80,
"indentWidth": 2,
"singleQuote": true,
"semiColons": false
}
}
9 changes: 0 additions & 9 deletions example.ts

This file was deleted.

16 changes: 16 additions & 0 deletions example/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { lazyJSONParse } from '../utils.ts'

const socket = new WebSocket('ws://localhost:8080')

socket.onopen = () => {
if (socket.readyState === socket.OPEN) {
socket.send(
JSON.stringify({ method: 'hello', params: ['world'], jsonrpc: '2.0' }),
)
}
}

socket.onmessage = (ev) => {
console.log(lazyJSONParse(ev.data))
socket.close()
}
11 changes: 11 additions & 0 deletions example/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { App } from '../mod.ts'

const app = new App()

app.method<[string]>('hello', (params) => {
return `Hello ${params[0]}`
})

await app.listen({ port: 8080, hostname: '0.0.0.0' }, (addr) => {
console.log(`Listening on ${addr.port}`)
})
2 changes: 1 addition & 1 deletion mod.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './server.ts'
export * from './app.ts'
export * from './request.ts'
export type { ErrorResponse, JsonRpcRequest, RPCOptions } from './types.ts'
12 changes: 5 additions & 7 deletions request.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { JsonRpcRequest, Message } from './types.ts'
import type { JsonRpcRequest, JsonRpcResponse } from './types.ts'
import { makeArray } from './utils.ts'

export function send(
socket: WebSocket,
message: Message | Message[],
message: JsonRpcResponse | JsonRpcResponse[],
): void {
const messages = makeArray<Message>(message)
const messages = makeArray<JsonRpcResponse>(message)
messages.forEach((message) => {
message.jsonrpc = '2.0'
if (messages.length === 1) socket.send(JSON.stringify(message))
Expand All @@ -19,14 +19,12 @@ export function parseRequest(
try {
const arr = makeArray(JSON.parse(json))
const res: (JsonRpcRequest | 'invalid')[] = []

for (const obj of arr) {
if (
typeof obj !== 'object' || !obj || obj.jsonrpc !== '2.0' ||
typeof obj.method !== 'string'
) {
res.push('invalid')
} else res.push(obj)
) res.push('invalid')
else res.push(obj)
}

if (!res.length) return ['invalid']
Expand Down
33 changes: 18 additions & 15 deletions types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,24 @@ export interface JsonRpcRequest<T extends unknown[] = unknown[]> {
params: T
}

export type JsonRpcError = {
name?: string
code: number
message: string
}

export type JsonRpcResponse<T extends unknown = unknown> =
& ({
result: T | null
} | { error: JsonRpcError | null })
& { jsonrpc?: '2.0'; id: string | null }

export type ClientAdded = <T extends unknown[] = unknown[]>(
params: T,
socket: WebSocket,
) => Promise<{ error: ErrorResponse } | string | null>
) => Promise<{ error: JsonRpcError } | string | null>

export interface RPCOptions {
export type RPCOptions = Partial<{
/**
* Creates an ID for a specific client.
*
Expand All @@ -19,11 +31,11 @@ export interface RPCOptions {
*
* If `null` is returned, or if this function is not specified, the `clientId` will be set to a uuid
*/
clientAdded?: ClientAdded
clientAdded: ClientAdded
/**
* Called when a socket is closed.
*/
clientRemoved?(clientId: string): Promise<void> | void
clientRemoved(clientId: string): Promise<void> | void
/**
* The path to listen for connections at.
* If '*' is specified, all incoming ws requests will be used
Expand All @@ -33,14 +45,5 @@ export interface RPCOptions {
/**
* Timeout
*/
timeout?: number
}

export interface ErrorResponse<T extends unknown[] = unknown[]> {
code: number
message: string
data?: T
}


export type Message = { jsonrpc?: string } & Record<string, unknown>
timeout: number
}>
2 changes: 1 addition & 1 deletion utils_test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { lazyJSONParse, pathsAreEqual } from './utils.ts'
import { assertEquals } from 'https://deno.land/std@0.181.0/testing/asserts.ts'
import { assertEquals } from 'https://deno.land/std@0.190.0/testing/asserts.ts'

Deno.test('lazyJSONParse', async (it) => {
await it.step('should parse JSON like JSON.parse', () => {
Expand Down

0 comments on commit 0f553c2

Please sign in to comment.