diff --git a/examples/client/list/client-basic-uws.ts b/examples/client/list/client-basic-uws.ts new file mode 100644 index 0000000..d1dd034 --- /dev/null +++ b/examples/client/list/client-basic-uws.ts @@ -0,0 +1,28 @@ +import { $, h, onMount } from 'easyhard' +import { easyhardClient } from 'easyhard-client' +import { map, take } from 'rxjs/operators' +import { BasicActionsUWS } from '../../shared' + +const client = easyhardClient() + +function App() { + const count1 = client.call('getData').pipe( + take(5), + map(data => String(data.count)) + ) + const count2 = $(undefined).pipe( + client.pipe('getIP'), + map(data => String(data.ip)) + ) + + const el = h('div', {}, + h('div', {}, count1), + h('div', {}, count2) + ) + + onMount(el, () => client.connect(() => new WebSocket(`ws://${location.host}/uws/basic/`), { http: `http://${location.host}/uws/basic/` })) + + return el +} + +document.body.appendChild(App()) diff --git a/examples/package-lock.json b/examples/package-lock.json index 66ba72c..0302cfc 100644 --- a/examples/package-lock.json +++ b/examples/package-lock.json @@ -6418,6 +6418,10 @@ "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==", "dev": true }, + "uWebSockets.js": { + "version": "github:uNetworking/uWebSockets.js#4558ee00f9f1f686fffe1accbfc2e85b1af9c50f", + "from": "github:uNetworking/uWebSockets.js#v20.0.0" + }, "undefsafe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", diff --git a/examples/package.json b/examples/package.json index 0b4c86f..2964c2e 100644 --- a/examples/package.json +++ b/examples/package.json @@ -28,7 +28,8 @@ "module-alias": "^2.2.2", "rxjs": "^7.1.0", "todomvc-app-css": "^2.4.1", - "tsconfig-paths": "^3.9.0" + "tsconfig-paths": "^3.9.0", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.0.0" }, "devDependencies": { "babel-plugin-overload": "0.0.2", diff --git a/examples/server/index.ts b/examples/server/index.ts index fb3f5da..c4a10e3 100644 --- a/examples/server/index.ts +++ b/examples/server/index.ts @@ -10,6 +10,7 @@ import reconnect from './reconnect' import request from './request' import unstable from './unstable' import upload from './upload' +import './uws' const app = express() expressWs(app) diff --git a/examples/server/uws/basic.ts b/examples/server/uws/basic.ts new file mode 100644 index 0000000..9c09475 --- /dev/null +++ b/examples/server/uws/basic.ts @@ -0,0 +1,15 @@ +import { easyhardServer } from './easyhard-server'; +import { BasicActionsUWS } from '../../shared' +import { getInterval } from '../shared'; +import { map, mergeMap, take } from 'rxjs/operators'; +import { interval } from 'rxjs'; + +export default easyhardServer({ + getData: getInterval(), + getIP: mergeMap((payload) => { + return interval(500).pipe( + take(14), + map(count => ({ ip: String(count) + '|' + payload.$request.socket.ip })) + ) + }), +}) diff --git a/examples/server/uws/easyhard-server.ts b/examples/server/uws/easyhard-server.ts new file mode 100644 index 0000000..9fba3bb --- /dev/null +++ b/examples/server/uws/easyhard-server.ts @@ -0,0 +1,109 @@ +import { WebSocketState, WsConnection } from 'easyhard-bridge' +import { getUID } from 'easyhard-common' +import { attach, Attachment, BodyListeners, CookieSetters, Handlers, Http, ReqListeners } from 'easyhard-server' +import { WebSocketBehavior, WebSocket, HttpResponse, HttpRequest } from 'uWebSockets.js' +import { TextDecoder } from 'util' + +type HttpTunnel = (res: HttpResponse, req: HttpRequest) => void + +const decoder = new TextDecoder('utf-8') + +function arrayBufferToString(data: ArrayBuffer) { + return decoder.decode(new Uint8Array(data)) +} + +export function useHttp(): Http & { tunnel: HttpTunnel } { + const reqListeners: ReqListeners = new Map() + const bodyListeners: BodyListeners = new Map() + const cookieSetters: CookieSetters = new Map() + + function tunnel(res: HttpResponse, req: HttpRequest) { + } + + return { + bodyListeners, + reqListeners, + cookieSetters, + tunnel + } +} + +type Request = HttpRequest & { socket: { ip: string }} +type Props = { open: (ws: WebSocket, attachment: Attachment) => void } + +export function easyhardServer(actions: Handlers): { attachClient: (props: Props) => WebSocketBehavior, httpTunnel: HttpTunnel } { + const http = useHttp() + type Connection = WsConnection & { listeners: [string, (...args: any[]) => any][], emit: (event: string, payload: T) => void } + const connections = new Map() + + function attachClient(props: Props): WebSocketBehavior { + return { + upgrade(res, req, context) { + const id = getUID() + + res.upgrade( + { id, req: { + ...req, + socket: { ip: arrayBufferToString(res.getRemoteAddressAsText()) } + }}, + req.getHeader('sec-websocket-key'), + req.getHeader('sec-websocket-protocol'), + req.getHeader('sec-websocket-extensions'), + context + ) + }, + open: (ws: WebSocket) => { + const { id, req } = ws + const listeners: Connection['listeners'] = [] + const connection: Connection = { + listeners, + emit(event, payload) { + listeners + .filter(e => e[0] === event) + .forEach(e => e[1](payload)) + }, + addEventListener(event, handler) { + listeners.push([event, handler]) + }, + removeEventListener(event, handler) { + const listenersToRemove = [...listeners].filter(e => e[0] === event && e[1] === handler) + + listenersToRemove.forEach(item => { + const index = listeners.indexOf(item) + + if (index >= 0) listeners.splice(index, 1) + }) + }, + readyState: WebSocketState.OPEN, + send(data) { + ws.send(data) + } + } + const attachment = attach(actions, connection, req, http) + + connections.set(id, connection) + props.open(ws, attachment) + }, + message: (ws, message) => { + const id: string = ws.id + const connection = connections.get(id) + const data = arrayBufferToString(message) + + connection?.emit('message', { data }) + }, + close: (ws, code, message) => { + const id: string = ws.id + const connection = connections.get(id) + if (connection) connection.readyState = WebSocketState.CLOSED + + connection?.emit('close', { code, reson: message, wasClean: true }) + connections.delete(id) + } + } + } + + return { + attachClient, + httpTunnel: http.tunnel + } +} diff --git a/examples/server/uws/index.ts b/examples/server/uws/index.ts new file mode 100644 index 0000000..e7628b8 --- /dev/null +++ b/examples/server/uws/index.ts @@ -0,0 +1,17 @@ +import uWS from 'uWebSockets.js' +import basic from './basic' + +const app = uWS.App() + +app.ws('/uws/basic/', basic.attachClient({ open() {}})) +app.any('/uws/basic/', basic.httpTunnel) + +const port = 9001; + +app.listen(port, (token) => { + if (token) { + console.log('Listening to port ' + port); + } else { + console.log('Failed to listen to port ' + port); + } +}); diff --git a/examples/shared/index.ts b/examples/shared/index.ts index f690287..ceef0d7 100644 --- a/examples/shared/index.ts +++ b/examples/shared/index.ts @@ -14,6 +14,11 @@ export interface BasicActions { emptyResponse2: OperatorFunction<{ value: number }, void> } +export interface BasicActionsUWS { + getData: GetData + getIP: OperatorFunction +} + export interface CookieActions { sendCookie: OperatorFunction<{ value: Cookie }, { value: string | null, ok?: boolean }> setCookie: OperatorFunction diff --git a/examples/webpack.config.js b/examples/webpack.config.js index a4d9425..f8214ac 100644 --- a/examples/webpack.config.js +++ b/examples/webpack.config.js @@ -30,6 +30,10 @@ module.exports = { target: 'ws://localhost:3000', ws: true }, + '/uws': { + target: 'ws://localhost:9001', + ws: true + }, }, }, module: {