From 89a4b76e0365f8d81689182d71eee434c7291dec Mon Sep 17 00:00:00 2001 From: Frederic Charette Date: Sat, 26 Jul 2025 21:09:15 -0400 Subject: [PATCH 01/11] updating toolchain, documentation, fixing WS and WebRTC --- .eslintignore | 6 - .eslintrc | 43 --- .github/workflows/master-status.yml | 2 +- .github/workflows/pr-validation.yml | 2 +- CHANGELOG.md | 11 +- LICENSE | 2 +- eslint.config.mjs | 20 ++ examples/binary_compression/README.md | 27 ++ examples/binary_compression/client.ts | 62 +++++ examples/binary_compression/server.ts | 79 ++++++ examples/browser_peer_to_peer/README.md | 38 +++ examples/browser_peer_to_peer/client.js | 2 +- examples/browser_peer_to_peer/server.js | 12 +- examples/chat_websocket/client.js | 23 -- examples/chat_websocket/server.js | 22 -- examples/compression/client.js | 23 -- examples/compression/server.js | 27 -- examples/distributed_pub_sub/README.md | 42 +++ examples/distributed_pub_sub/client.js | 56 +++- examples/distributed_pub_sub/server.js | 151 +++++++---- examples/typescript/client.ts | 19 -- examples/typescript/server.ts | 28 -- examples/typescript_websocket/README.md | 27 ++ examples/typescript_websocket/client.ts | 55 ++++ examples/typescript_websocket/server.ts | 72 +++++ package.json | 34 +-- packages/ipc/README.md | 7 +- packages/ipc/package.json | 2 +- packages/ipc/src/ipc.ts | 7 +- packages/ipc/types.d.ts | 16 +- packages/kalm/README.md | 36 ++- packages/kalm/package.json | 2 +- packages/kalm/src/components/client.ts | 17 +- packages/kalm/src/utils/logger.ts | 6 +- packages/kalm/types.d.ts | 314 +++++++++++----------- packages/tcp/README.md | 7 +- packages/tcp/package.json | 2 +- packages/tcp/src/tcp.ts | 5 +- packages/tcp/types.d.ts | 12 +- packages/udp/README.md | 10 +- packages/udp/package.json | 2 +- packages/udp/src/udp.ts | 15 +- packages/udp/types.d.ts | 24 +- packages/webrtc/README.md | 8 +- packages/webrtc/package.json | 2 +- packages/webrtc/src/webrtc.ts | 16 +- packages/webrtc/types.d.ts | 34 +-- packages/ws/README.md | 8 +- packages/ws/package.json | 2 +- packages/ws/src/ws.ts | 29 +- packages/ws/types.d.ts | 24 +- scripts/benchmarks/index.js | 15 +- scripts/benchmarks/transports/ipc.js | 9 +- scripts/benchmarks/transports/kalm.js | 13 +- scripts/benchmarks/transports/socketio.js | 9 +- scripts/benchmarks/transports/tcp.js | 9 +- scripts/benchmarks/transports/udp.js | 9 +- tests/integration/frame.spec.ts | 16 +- tests/integration/index.spec.ts | 48 ++-- 59 files changed, 988 insertions(+), 632 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc create mode 100644 eslint.config.mjs create mode 100644 examples/binary_compression/README.md create mode 100644 examples/binary_compression/client.ts create mode 100644 examples/binary_compression/server.ts create mode 100644 examples/browser_peer_to_peer/README.md delete mode 100644 examples/chat_websocket/client.js delete mode 100644 examples/chat_websocket/server.js delete mode 100644 examples/compression/client.js delete mode 100644 examples/compression/server.js create mode 100644 examples/distributed_pub_sub/README.md delete mode 100644 examples/typescript/client.ts delete mode 100644 examples/typescript/server.ts create mode 100644 examples/typescript_websocket/README.md create mode 100644 examples/typescript_websocket/client.ts create mode 100644 examples/typescript_websocket/server.ts diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index c6a8411..0000000 --- a/.eslintignore +++ /dev/null @@ -1,6 +0,0 @@ -*.js -examples -node_modules -scripts -*.d.ts -tests \ No newline at end of file diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 2c4ddcd..0000000 --- a/.eslintrc +++ /dev/null @@ -1,43 +0,0 @@ -{ - "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], - "parser": "@typescript-eslint/parser", - "plugins": [ "@typescript-eslint" ], - "settings": { - "import/resolver": { - "node": { - "extensions": [".js", ".ts"], - "moduleDirectory": ["node_modules", "packages/"] - } - } - }, - "rules": { - "import/extensions": 0, - "no-bitwise": 0, - "linebreak-style": 1, - "no-underscore-dangle": 0, - "object-curly-newline": 0, - "no-plusplus": 0, - "no-unused-expressions": 0, - "guard-for-in": 0, - "no-mixed-operators": 0, - "no-param-reassign": 0, - "arrow-parens": [2, "as-needed"], - "import/prefer-default-export": 0, - "no-continue": 0, - "no-unused-vars": 0, - "global-require": 0, - "max-len": [1, 180], - "@typescript-eslint/no-unused-vars": [ - "error", - { "vars": "all", "args": "after-used", "ignoreRestSiblings": false } - ], - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/ban-types": 0, - "@typescript-eslint/no-empty-function": 0 - }, - "env": { - "browser": true, - "jest": true, - "node": true - } -} diff --git a/.github/workflows/master-status.yml b/.github/workflows/master-status.yml index 2e90a39..30796f3 100644 --- a/.github/workflows/master-status.yml +++ b/.github/workflows/master-status.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [14.x, 16.x, 18.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v1 diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 669d480..6dd43ca 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [18.x, 20.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 22fca36..5bcad14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,18 @@ # Changelog -## [v7.0.0] - 2023-03-17 +## [v7.1.0] - 2025-07-26 commit [#](https://github.com/kalm/kalm.js/commits) +### Minor changes + +- Added support for the new native WS APIs in Node 22 and later +- Removed yarn from the toolchain. There's no reason to keep it now that NPM workspaces are more mature. + +## [v7.0.0] - 2023-03-17 + +commit [99a3ab9](https://github.com/kalm/kalm.js/commit/99a3ab9c495f6f50d7d4b4a0f478a213cc0ce484) + ### Major changes - Standardized parameter names and expected behavior diff --git a/LICENSE b/LICENSE index 24f7a38..37d14c9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2023 Frederic Charette +Copyright 2025 Frederic Charette Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..89aa4f5 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,20 @@ +import eslint from '@eslint/js'; +import { globalIgnores } from 'eslint/config'; +import tseslint from 'typescript-eslint'; +import jestConfig from 'eslint-plugin-jest'; +import spacing from '@stylistic/eslint-plugin'; + +export default tseslint.config( + eslint.configs.recommended, + tseslint.configs.recommended, + jestConfig.configs['flat/recommended'], + spacing.configs.recommended, + { + rules: { + '@stylistic/semi': [2, 'always'], + '@typescript-eslint/no-explicit-any': 0, + 'jest/no-done-callback': 0, + }, + }, + globalIgnores(['**/bin']), +); diff --git a/examples/binary_compression/README.md b/examples/binary_compression/README.md new file mode 100644 index 0000000..cff3fb1 --- /dev/null +++ b/examples/binary_compression/README.md @@ -0,0 +1,27 @@ +# Birary messages and compression example + +This example shows how to send brinary messages and how to add compression. + +# Requirements + +- The clients can run in either the browser or in Node.js. +- The server must run in a Node.js environment. +- NPM or other package manager to install `kalm`, `@kalm/ws` and the compression library of your choice. In this example, we are using `snappy`. + +# Testing + +Launch the server first: + +``` +node ./server.js +``` + +It should log that the server is ready to receive new connections. At this stage, launch any number of clients: + +``` +node ./client.js +``` + +The clients should connect to the server and send an "hello world!" message. + +In turn, the server will both respond to that client: "hello from the server!" and broadcast to all clients "A new client has connected!" diff --git a/examples/binary_compression/client.ts b/examples/binary_compression/client.ts new file mode 100644 index 0000000..98d980a --- /dev/null +++ b/examples/binary_compression/client.ts @@ -0,0 +1,62 @@ +import { connect, routines } from 'kalm'; +import ws from '@kalm/ws'; +import { compressSync, uncompress } from 'snappy'; + +/** + * Creates a kalm client that uses the Websocket transport. + * It will attempt to connect to a server located at the address '0.0.0.0' on port 3938. + * + * The realtime routine will emit messages as soon as possible. While this is ideal in a few scenarios, for instance when the client does not send a lot of events, + * it does not leverage the benefits of buffering and may cause slowdowns if many messages are sent rapidly. + * + * To ensure that the message sent is not coerced into JSON, we must pass the `json:false` parameter. + */ +const client = connect({ + transport: ws({}), + json: false, + port: 3938, + host: '0.0.0.0', + routine: routines.realtime(), +}); + +/** + * An example interface for messages sent between client and server + */ +type MyCustomPayload = { + message: string +}; + +/** + * To confirm that the client has succesfully connected to the server, listen to the 'connect' event. + */ +client.on('connect', () => { + /** + * Once a client has connected, we subscribe to messages sent on the "foo" channel. + */ + client.subscribe('foo', (body: MyCustomPayload, frame) => { + /** + * When we receive a message on the foo channel, we also receive information about the frame and context. + * We'll need to decompress the buffer to read it. + * + * body: + * frame: { + * client: , + * frame: { + * channel: "foo", + * id: 1, + * messageIndex: 1, + * payloadBytes: 22, + * payloadMessages: 1, + * } + * } + */ + uncompress(body, (decompressedMessage) => { + console.log('Server event', decompressedMessage, frame); + }); + }); + + /** + * To send messages from the client to the server, simply `write` to the desired channel. + */ + client.write('foo', compressSync(Buffer.from(JSON.stringify({ message: 'hello world!' })))); +}); diff --git a/examples/binary_compression/server.ts b/examples/binary_compression/server.ts new file mode 100644 index 0000000..e39e2a8 --- /dev/null +++ b/examples/binary_compression/server.ts @@ -0,0 +1,79 @@ +import { listen, routines } from 'kalm'; +import ws from '@kalm/ws'; +import { compressSync, uncompress } from 'snappy'; + +/** + * Creates a kalm server that uses the Websocket transport. + * It is bound to local IP 0.0.0.0 and listens on port 3938. + * + * The tick routine will emit messages to clients at a frequency no higher than 5hz, or no shorter than 20ms + * + * This is a common setup for relaying information to multiple connected clients that all send information rapidly. + * + * To ensure that the message sent is not coerced into JSON, we must pass the `json:false` parameter. + */ +const provider = listen({ + transport: ws(), + port: 3938, + json: false, + routine: routines.tick({ hz: 5 }), + host: '0.0.0.0', +}); + +/** + * An example interface for messages sent between client and server + */ +type MyCustomPayload = { + message: string +}; + +/** + * First, the server must listen for connection events + */ +provider.on('connection', (client) => { + /** + * Once a client has connected, we subscribe to messages sent on the "foo" channel. + */ + client.subscribe('foo', (body: MyCustomPayload, frame) => { + /** + * When we receive a message on the foo channel, we also receive information about the frame and context. + * We'll need to decompress the buffer to read it. + * + * body: + * frame: { + * client: , + * frame: { + * channel: "foo", + * id: 1, + * messageIndex: 1, + * payloadBytes: 12, + * payloadMessages: 1, + * } + * } + */ + uncompress(body, (decompressedMessage) => { + console.log('Client event', decompressedMessage, frame); + }); + }); + + /** + * To send messages from the server to the newly connected client, simply `write` to the desired channel. + */ + client.write('foo', compressSync(Buffer.from(JSON.stringify({ + message: 'hello from the server!', + } as MyCustomPayload)))); + + /** + * To send a message to all connected clients, for example to announce that a new client has connected, you may use the `broadcast` function. Again, it is important to specify which channel to use. + */ + provider.broadcast('foo', compressSync(Buffer.from(JSON.stringify({ + message: 'A new client has connected!', + } as MyCustomPayload)))); +}); + +/** + * The `ready` event lets you know that the server is ready to receive connections + */ +provider.on('ready', () => { + console.log('The server is now listening on port 3938'); +}); diff --git a/examples/browser_peer_to_peer/README.md b/examples/browser_peer_to_peer/README.md new file mode 100644 index 0000000..712946b --- /dev/null +++ b/examples/browser_peer_to_peer/README.md @@ -0,0 +1,38 @@ +# WebRTC Peer to Peer example + +This example shows how to create a P2P (Peer to Peer) network. + +# Requirements + +- The clients can run in either the browser or in Node.js. +- The server must run in a Node.js environment. +- NPM or other package manager to install `kalm`, `@kalm/tcp` and `@kalm/ws` + +# Testing + +Launch any number of servers first, using process arguments to specify the hosts and ports for this node and the seed node. + + + +``` +node ./server.js 0.0.0.0 3000 0.0.0.0 3000 +node ./server.js 0.0.0.0 3001 0.0.0.0 3000 +node ./server.js 0.0.0.0 3002 0.0.0.0 3000 +``` + +With this example, three servers are launched, listening on 3 different ports, but all connected to the seed host (port 3000). +The seed acts as an orchestrator and puts nodes in contact with each other. They all end up connected to one another. + +In parallel, servers also listen on `port + 10000` for clients to join. + +We will now launch clients connecting to any given server. + +``` +node ./client.js 0.0.0.0 13000 +node ./client.js 0.0.0.0 13001 +node ./client.js 0.0.0.0 13002 +``` + +The clients should connect to the server and send an "hello world!" message using the external channel. + +In turn, the servers will forward this message to the others via an internal channel before each broadcasts it back to its connected clients. diff --git a/examples/browser_peer_to_peer/client.js b/examples/browser_peer_to_peer/client.js index afb1079..45f1263 100644 --- a/examples/browser_peer_to_peer/client.js +++ b/examples/browser_peer_to_peer/client.js @@ -41,4 +41,4 @@ function createPeer(channel) { client.broadcast('/', 'new peer'); connection.subscribe('/', body => console.log(`Got peer message: "${body}"`)); }); -})(); \ No newline at end of file +})(); diff --git a/examples/browser_peer_to_peer/server.js b/examples/browser_peer_to_peer/server.js index b597163..5097f69 100644 --- a/examples/browser_peer_to_peer/server.js +++ b/examples/browser_peer_to_peer/server.js @@ -13,12 +13,12 @@ Server.on('connection', (client) => { client.subscribe('peering', (channel) => { client.subscribe(`${channel}.peering`, (body, frame) => { Server.connections - .filter((connection) => { - return (connection.label !== client.label && connection.getChannels().includes(`${channel}.peering`)); - }) - .forEach((connection) => { - connection.write(`${channel}.peering`, body); - }); + .filter((connection) => { + return (connection.label !== client.label && connection.getChannels().includes(`${channel}.peering`)); + }) + .forEach((connection) => { + connection.write(`${channel}.peering`, body); + }); }); }); }); diff --git a/examples/chat_websocket/client.js b/examples/chat_websocket/client.js deleted file mode 100644 index 9903069..0000000 --- a/examples/chat_websocket/client.js +++ /dev/null @@ -1,23 +0,0 @@ -const kalm = require('kalm'); -const ws = require('@kalm/ws'); - -const { randomBytes } = require('crypto'); - -const Client = kalm.connect({ - label: randomBytes(4).toString('hex'), - host: '0.0.0.0', - port: 8800, - transport: ws(), - routine: kalm.routines.realtime(), -}); - -Client.subscribe('r.evt', (body, frame) => console.log(`${body.name}: ${body.msg}`, frame)); -Client.subscribe('r.sys', (body, frame) => console.log(`[System]: ${body.msg}`, frame)); - -Client.on('connect', () => { - Client.write('c.evt', { name: Client.label, msg: 'Hey everyone!' }); -}); - -Client.on('disconnect', () => { - console.log('--Connection lost--'); -}); diff --git a/examples/chat_websocket/server.js b/examples/chat_websocket/server.js deleted file mode 100644 index 2a35a1a..0000000 --- a/examples/chat_websocket/server.js +++ /dev/null @@ -1,22 +0,0 @@ -const kalm = require('kalm'); -const ws = require('@kalm/ws'); - -const Server = kalm.listen({ - label: 'server', - port: 8800, - transport: ws(), - routine: kalm.routines.tick({ hz: 5 }), - host: '0.0.0.0', -}); - -Server.on('connection', (client) => { - client.subscribe('c.evt', (body, frame) => { - Server.broadcast('r.evt', body); - }); - - Server.broadcast('r.sys', { msg: 'user joined' }); -}); - -Server.on('deconnection', (client) => { - Server.broadcast('r.sys', { msg: 'user left' }); -}); diff --git a/examples/compression/client.js b/examples/compression/client.js deleted file mode 100644 index fd9a2f5..0000000 --- a/examples/compression/client.js +++ /dev/null @@ -1,23 +0,0 @@ -const kalm = require('kalm'); -const ws = require('@kalm/ws'); -const snappy = require('snappy'); - -const Client = kalm.connect({ - transport: ws(), - port: 3938, - json: false, - routine: kalm.routines.realtime(), -}); - -Client.subscribe('foo', (body, frame) => { - snappy.uncompress(body, (decompressedMessage) => { - console.log('Server event', decompressedMessage, frame); - }); -}); - -const payload = { - foo: 'bar', - message: 'hello from the client!', -}; - -Client.write('foo', snappy.compressSync(Buffer.from(JSON.stringify(payload)))); diff --git a/examples/compression/server.js b/examples/compression/server.js deleted file mode 100644 index 3a4b09c..0000000 --- a/examples/compression/server.js +++ /dev/null @@ -1,27 +0,0 @@ -const kalm = require('kalm'); -const ws = require('@kalm/ws'); -const snappy = require('snappy'); - -const provider = kalm.listen({ - label: 'internal', - transport: ws(), - port: 3000, - json: false, - routine: kalm.routines.realtime(), - host: '0.0.0.0', // Apply local ip -}); - -provider.on('connection', (client) => { - client.subscribe('foo', (body, frame) => { - snappy.uncompress(body, (decompressedMessage) => { - console.log('Client event', decompressedMessage, frame); - }); - }); - - const payload = { - foo: 'bar', - message: 'hello from the server!', - }; - - client.write('foo', snappy.compressSync(Buffer.from(JSON.stringify(payload)))); -}); diff --git a/examples/distributed_pub_sub/README.md b/examples/distributed_pub_sub/README.md new file mode 100644 index 0000000..d8127b3 --- /dev/null +++ b/examples/distributed_pub_sub/README.md @@ -0,0 +1,42 @@ +# Distributed Pub-Sub example + +This example shows how to create a network of multiple servers sharing their messages and relaying them to all thier clients. + +This is usefull if you have a very large set of connected clients that you wish to split between many servers. + +In this case, clients connect to this server mesh via websocket, yet servers communicate over straight TCP. + +# Requirements + +- The clients can run in either the browser or in Node.js. +- The server must run in a Node.js environment. +- NPM or other package manager to install `kalm`, `@kalm/tcp` and `@kalm/ws` + +# Testing + +Launch any number of servers first, using process arguments to specify the hosts and ports for this node and the seed node. + + + +``` +node ./server.js 0.0.0.0 3000 0.0.0.0 3000 +node ./server.js 0.0.0.0 3001 0.0.0.0 3000 +node ./server.js 0.0.0.0 3002 0.0.0.0 3000 +``` + +With this example, three servers are launched, listening on 3 different ports, but all connected to the seed host (port 3000). +The seed acts as an orchestrator and puts nodes in contact with each other. They all end up connected to one another. + +In parallel, servers also listen on `port + 10000` for clients to join. + +We will now launch clients connecting to any given server. + +``` +node ./client.js 0.0.0.0 13000 +node ./client.js 0.0.0.0 13001 +node ./client.js 0.0.0.0 13002 +``` + +The clients should connect to the server and send an "hello world!" message using the external channel. + +In turn, the servers will forward this message to the others via an internal channel before each broadcasts it back to its connected clients. diff --git a/examples/distributed_pub_sub/client.js b/examples/distributed_pub_sub/client.js index 4d68988..1469f4f 100644 --- a/examples/distributed_pub_sub/client.js +++ b/examples/distributed_pub_sub/client.js @@ -1,25 +1,53 @@ -const crypto = require('crypto'); +const { randomBytes } = require('node:crypto'); const kalm = require('kalm'); const ws = require('@kalm/ws'); -const clientId = crypto.randomBytes(4).toString('hex'); +/** + * Generates a unique Id for our client + */ +const clientId = randomBytes(4).toString('hex'); -const Client = kalm.connect({ +/** + * Creates a kalm client that uses the Websocket transport. + * It will attempt to connect to a server located at the address and port provided in the process arguments + */ +const client = kalm.connect({ label: clientId, transport: ws(), - port: 3938, + port: process.argv.at(-1), routine: kalm.routines.realtime(), + host: process.argv.at(-2), }); -Client.subscribe('r.evt', (body, frame) => { - console.log('Relayed event', body, frame); -}); +console.log('Creating client', clientId, '...'); + +/** + * To confirm that the client has succesfully connected to the server, listen to the 'connect' event. + */ +client.on('connect', () => { + console.log('Client', clientId, 'has joined the network.'); -// now send some events -setInterval(() => { - Client.write('c.evt', { - origin: clientId, - timestamp: Date.now(), - message: 'hello world!', + /** + * Subscribes to messages on a channel named r.evt (Response Event). + */ + client.subscribe('r.evt', (body, frame) => { + /** + * We'll ignore our own messages + */ + if (body.origin !== clientId) { + /** + * Whenever we receive a message, which may have come from a different server entirely, we will print it + */ + console.log('Relayed event', body); + } }); -}, 2000); + + // now send some events + setInterval(() => { + client.write('c.evt', { + origin: clientId, + timestamp: Date.now(), + message: 'hello world!', + }); + }, 2000); +}); diff --git a/examples/distributed_pub_sub/server.js b/examples/distributed_pub_sub/server.js index a9b9c42..b1184a9 100644 --- a/examples/distributed_pub_sub/server.js +++ b/examples/distributed_pub_sub/server.js @@ -2,60 +2,115 @@ const kalm = require('kalm'); const ws = require('@kalm/ws'); const tcp = require('@kalm/tcp'); -const seed = { host: '0.0.0.0', port: 3000 }; -const tickSeed = Date.now(); +/** + * Reads this node's config from process arguments + * A real production application might have this embedded in env variables + */ +const config = { host: process.argv.at(-4), port: process.argv.at(-3) }; +const seedConfig = { host: process.argv.at(-2), port: process.argv.at(-1) }; // Apply seed config -const seedHost = '0.0.0.0'; // Apply seed config +/** + * We'll register a timestamp here to help during gossip negociations + */ +const ts = Date.now(); -const providers = [ - kalm.listen({ - label: 'internal', +/** + * Creates one server to listen for internal gossip and a second, external one to receive messages from clients + */ +const internal = kalm.listen({ + label: 'internal', + transport: tcp(), + port: config.port, + routine: kalm.routines.realtime(), + host: config.host, +}); +const external = kalm.listen({ + label: 'external', + transport: ws(), + port: 10000 + Number(config.port), + routine: kalm.routines.tick({ hz: 120, seed: ts }), + host: '0.0.0.0', +}); + +const isSeed = (config.host === seedConfig.host && config.port === seedConfig.port); + +/** + * Lets us know when the node is ready + */ +internal.on('ready', () => console.log(`Node is ready ${config.host}:${config.port} (seed = ${isSeed})`)); +external.on('ready', () => console.log('Node is ready for client traffic on port', 10000 + Number(config.port))); + +/** + * Non-seed internal nodes will first connect with the seed node using a channel dedicated to this type of gossip. We'll name it "n.add" (for Node Added) + */ +if (!isSeed) { + console.log(`Connecting to seed node: ${seedConfig.host}:${seedConfig.port}`); + const seedNode = kalm.connect({ + ...seedConfig, transport: tcp(), - port: 3000, routine: kalm.routines.realtime(), - host: '0.0.0.0', // Apply local ip - }), - kalm.listen({ - label: 'external', - transport: ws(), - port: 3938, - routine: kalm.routines.tick({ hz: 120, seed: tickSeed }), - host: '0.0.0.0', // Apply local ip - }), -]; - -providers.forEach((provider) => { - const isIntern = provider.label === 'internal'; - const isSeed = (isIntern && seed.host === seedHost); - - if (!isSeed && isIntern) { - kalm.connect({}).write('n.add', { host: seedHost }); - } + }); + seedNode.write('n.add', { host: config.host, ts, port: config.port }); - provider.on('connection', (client) => { - if (isIntern) { - client.subscribe('n.add', (body, frame) => { - if (isSeed) { - provider.broadcast('n.add', body); - } else provider.connect(frame.remote); - }); - client.subscribe('n.evt', (body, frame) => { - providers.forEach((_provider) => { - if (_provider.label === 'external') { - _provider.broadcast('r.evt', body); - } - }); - }); - } else { - client.subscribe('c.evt', (body, frame) => { - providers.forEach((_provider) => { - if (_provider.label === 'internal') { - _provider.broadcast('n.evt', body); - } else { - _provider.broadcast('r.evt', body); - } - }); + /** + * We'll add the seed node to our internal server's connection list manually, this will allow us to broadcast once. + */ + internal.connections.push(seedNode); + + /** + * When a non-seed node receives this event, it will attempt to connect to it. + * We'll just check that we aren't connecting to ourselves + */ + seedNode.subscribe('n.add', (body) => { + if (body.port !== config.port || body.host !== config.host) { + console.log(`Connecting to new node: ${body.host}:${body.port}`); + const newNode = kalm.connect({ + ...body, + transport: tcp(), + routine: kalm.routines.realtime(), }); + + /** + * We'll add the new node to our internal server's connection list manually, this will allow us to broadcast once. + */ + internal.connections.push(newNode); } }); + + seedNode.subscribe('n.evt', (body) => { + external.broadcast('r.evt', body); + }); +} + +internal.on('connection', (client) => { + /** + * For internal nodes, we need to listen for any kind of gossip + */ + if (isSeed) { + client.subscribe('n.add', (body) => { + /** + * When a new node is added, the seed's role is to broadcast the event- since it's already connected to all other nodes, it should reach all server instances. + */ + console.log(`A new node is joining the cluster: ${body.host}:${body.port}`); + internal.broadcast('n.add', body); + }); + } + + /** + * Server nodes need to broadcast messages they received from gossiping. They listen to an internal channel we'll name "n.evt", for Node Events and broadcast to clients on a channel we'll name "r.evt" for Response Events. + */ + client.subscribe('n.evt', (body) => { + external.broadcast('r.evt', body); + }); +}); + +external.on('connection', (client) => { + console.log('A new client has connected to this node'); + /** + * External nodes will listen to client messages on a channel we'll name "c.evt" for Client Event and broadcast it to all nodes, internally and to all clients connected to this node. + */ + client.subscribe('c.evt', (body) => { + internal.broadcast('n.evt', body); + external.broadcast('r.evt', body); + }); }); diff --git a/examples/typescript/client.ts b/examples/typescript/client.ts deleted file mode 100644 index e88f018..0000000 --- a/examples/typescript/client.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {connect, routines} from 'kalm'; -import ws from '@kalm/ws'; - -const client = connect({ - transport: ws({}), - port: 3938, - routine: routines.realtime(), -}); - -type MyCustomPayload = { - foo: string - message: string -}; - -client.subscribe('r.evt', (body: MyCustomPayload, frame) => { - console.log('Server event', body, frame); -}); - -client.write('c.evt', 'hello world!'); diff --git a/examples/typescript/server.ts b/examples/typescript/server.ts deleted file mode 100644 index d1c72d3..0000000 --- a/examples/typescript/server.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {listen, routines} from 'kalm'; -import ws from '@kalm/ws'; - -const provider = listen({ - transport: ws(), - port: 3938, - routine: routines.tick({ hz: 5 }), - host: '0.0.0.0', -}); - -type MyCustomPayload = { - foo: string - message: string -}; - -provider.on('connection', (client) => { - client.subscribe('foo', (body: MyCustomPayload, frame) => { - console.log('Client event', body, frame); - }); - - const payload: MyCustomPayload = { - foo: 'bar', - message: 'hello from the server!', - }; - - client.write('foo', payload); -}); - diff --git a/examples/typescript_websocket/README.md b/examples/typescript_websocket/README.md new file mode 100644 index 0000000..613e3ec --- /dev/null +++ b/examples/typescript_websocket/README.md @@ -0,0 +1,27 @@ +# Typescript example + +This example shows how to create a websocket server and client in Typescript. + +# Requirements + +- The clients can run in either the browser or in Node.js, once transpiled. +- The server must run in a Node.js environment. +- NPM or other package manager to install `kalm` and `@kalm/ws` + +# Testing + +Launch the server first: + +``` +node ./server.ts +``` + +It should log that the server is ready to receive new connections. At this stage, launch any number of clients: + +``` +node ./client.ts +``` + +The clients should connect to the server and send an "hello world!" message. + +In turn, the server will both respond to that client: "hello from the server!" and broadcast to all clients "A new client has connected!" diff --git a/examples/typescript_websocket/client.ts b/examples/typescript_websocket/client.ts new file mode 100644 index 0000000..e34d1eb --- /dev/null +++ b/examples/typescript_websocket/client.ts @@ -0,0 +1,55 @@ +import { connect, routines } from 'kalm'; +import ws from '@kalm/ws'; + +/** + * Creates a kalm client that uses the Websocket transport. + * It will attempt to connect to a server located at the address '0.0.0.0' on port 3938. + * + * The realtime routine will emit messages as soon as possible. While this is ideal in a few scenarios, for instance when the client does not send a lot of events, + * it does not leverage the benefits of buffering and may cause slowdowns if many messages are sent rapidly. + */ +const client = connect({ + transport: ws({}), + port: 3938, + host: '0.0.0.0', + routine: routines.realtime(), +}); + +/** + * An example interface for messages sent between client and server + */ +type MyCustomPayload = { + message: string +}; + +/** + * To confirm that the client has succesfully connected to the server, listen to the 'connect' event. + */ +client.on('connect', () => { + /** + * Once a client has connected, we subscribe to messages sent on the "foo" channel. + */ + client.subscribe('foo', (body: MyCustomPayload, frame) => { + /** + * When we receive a message on the foo channel, we also receive information about the frame and context. + * + * body: { message: "hello from the server!" } + * frame: { + * client: , + * frame: { + * channel: "foo", + * id: 1, + * messageIndex: 1, + * payloadBytes: 22, + * payloadMessages: 1, + * } + * } + */ + console.log('Server event', body, frame); + }); + + /** + * To send messages from the client to the server, simply `write` to the desired channel. + */ + client.write('foo', { message: 'hello world!' }); +}); diff --git a/examples/typescript_websocket/server.ts b/examples/typescript_websocket/server.ts new file mode 100644 index 0000000..801e2be --- /dev/null +++ b/examples/typescript_websocket/server.ts @@ -0,0 +1,72 @@ +import { listen, routines } from 'kalm'; +import ws from '@kalm/ws'; + +/** + * Creates a kalm server that uses the Websocket transport. + * It is bound to local IP 0.0.0.0 and listens on port 3938. + * + * The tick routine will emit messages to clients at a frequency no higher than 5hz, or no shorter than 20ms + * + * This is a common setup for relaying information to multiple connected clients that all send information rapidly. + */ +const provider = listen({ + transport: ws(), + port: 3938, + routine: routines.tick({ hz: 5 }), + host: '0.0.0.0', +}); + +/** + * An example interface for messages sent between client and server + */ +type MyCustomPayload = { + message: string +}; + +/** + * First, the server must listen for connection events + */ +provider.on('connection', (client) => { + /** + * Once a client has connected, we subscribe to messages sent on the "foo" channel. + */ + client.subscribe('foo', (body: MyCustomPayload, frame) => { + /** + * When we receive a message on the foo channel, we also receive information about the frame and context. + * + * body: { message: "hello world!" } + * frame: { + * client: , + * frame: { + * channel: "foo", + * id: 1, + * messageIndex: 1, + * payloadBytes: 12, + * payloadMessages: 1, + * } + * } + */ + console.log('Client event', body, frame); + }); + + /** + * To send messages from the server to the newly connected client, simply `write` to the desired channel. + */ + client.write('foo', { + message: 'hello from the server!', + } as MyCustomPayload); + + /** + * To send a message to all connected clients, for example to announce that a new client has connected, you may use the `broadcast` function. Again, it is important to specify which channel to use. + */ + provider.broadcast('foo', { + message: 'A new client has connected!', + } as MyCustomPayload); +}); + +/** + * The `ready` event lets you know that the server is ready to receive connections + */ +provider.on('ready', () => { + console.log('The server is now listening on port 3938'); +}); diff --git a/package.json b/package.json index 6963d18..0dec463 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,16 @@ { "name": "kalm", "private": true, - "version": "7.0.0", + "version": "7.1.0", "description": "The socket optimizer", "main": "packages/kalm/bin/kalm.js", "scripts": { "lint": "eslint .", "lint:fix": "eslint . --fix", - "test": "yarn workspaces run test && yarn run test:integration", + "test": "npm run test --workspaces --if-present && npm run test:integration", "test:integration": "jest ./tests/integration --forceExit", - "build": "yarn workspaces run build", - "clean": "yarn workspaces run clean", + "build": "npm run build --workspaces --if-present", + "clean": "npm run clean --workspaces --if-present", "bench": "node ./scripts/benchmarks" }, "funding": { @@ -37,7 +37,7 @@ ], "husky": { "hooks": { - "pre-commit": "yarn lint" + "pre-commit": "npm run lint" } }, "jest": { @@ -52,16 +52,18 @@ } }, "devDependencies": { - "@types/jest": "^29.4.0", - "@types/node": "^18.15.0", - "@typescript-eslint/eslint-plugin": "^5.55.0", - "@typescript-eslint/parser": "^5.55.0", - "eslint": "^8.36.0", - "husky": "^8.0.0", - "jest": "^29.5.0", - "socket.io": "^4.6.0", - "socket.io-client": "^4.6.0", - "ts-jest": "^29.0.0", - "typescript": "^4.9.0" + "@stylistic/eslint-plugin": "^5.2.0", + "@types/jest": "^30.0.0", + "@types/node": "^24.1.0", + "eslint": "^9.32.0", + "eslint-plugin-jest": "^29.0.0", + "husky": "^9.1.0", + "jest": "^30.0.0", + "snappy": "^7.3.0", + "socket.io": "^4.8.0", + "socket.io-client": "^4.8.0", + "ts-jest": "^29.4.0", + "typescript": "^5.8.0", + "typescript-eslint": "^8.38.0" } } diff --git a/packages/ipc/README.md b/packages/ipc/README.md index ea19fd2..63a8c48 100644 --- a/packages/ipc/README.md +++ b/packages/ipc/README.md @@ -13,10 +13,13 @@ [![Kalm](https://img.shields.io/npm/v/kalm.svg)](https://www.npmjs.com/package/@kalm/ipc) [![Build Status](https://github.com/kalm/kalm.js/workflows/master-status/badge.svg)](https://github.com/kalm/kalm.js/actions?query=workflow%3A+master-status) -![Snyk Vulnerabilities for GitHub Repo](https://img.shields.io/snyk/vulnerabilities/github/kalm/kalm.js) [![Financial Contributors on Open Collective](https://opencollective.com/kalm/all/badge.svg?label=financial+contributors)](https://opencollective.com/kalm) [![Join the chat at https://gitter.im/KALM/home](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/KALM/?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +An IPC transport for the [Kalm](https://github.com/kalm/kalm.js) framework. + +- Send messages over file handles (IPC) to other processes with ease +- Supports Windows, Mac and Linux ## Installing @@ -39,4 +42,4 @@ If you think of something that you want, [open an issue](//github.com/kalm/kalm. ## License -[Apache 2.0](LICENSE) (c) 2022 Frederic Charette +[Apache 2.0](LICENSE) (c) 2025 Frederic Charette diff --git a/packages/ipc/package.json b/packages/ipc/package.json index 160bb9f..91f7d38 100644 --- a/packages/ipc/package.json +++ b/packages/ipc/package.json @@ -7,7 +7,7 @@ "build": "../../scripts/build.sh ipc", "clean": "../../scripts/cleanup.sh", "test": "jest ./tests", - "prepublish": "cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" + "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" }, "engines": { "node": ">=14" diff --git a/packages/ipc/src/ipc.ts b/packages/ipc/src/ipc.ts index 5f5b36c..82a2f59 100644 --- a/packages/ipc/src/ipc.ts +++ b/packages/ipc/src/ipc.ts @@ -15,7 +15,7 @@ interface IPCSocket extends net.Socket { type IPCConfig = { socketTimeout?: number path?: string -} +}; function ipc({ socketTimeout = 30000, path = '/tmp/app.socket-' }: IPCConfig = {}): KalmTransport { return function socket(params: ClientConfig, emitter: NodeJS.EventEmitter): Socket { @@ -44,13 +44,14 @@ function ipc({ socketTimeout = 30000, path = '/tmp/app.socket-' }: IPCConfig = { function connect(handle: IPCSocket): IPCSocket { const connection: net.Socket = handle || net.connect(`${path}${params.port}`); let buffer = ''; - connection.on('data', req => { + connection.on('data', (req) => { buffer += req.toString(); const chunks = buffer.split('\n\n'); if (buffer.substring(buffer.length - 2) !== '\n\n') { buffer = chunks[chunks.length - 1]; chunks.pop(); - } else { + } + else { buffer = ''; } diff --git a/packages/ipc/types.d.ts b/packages/ipc/types.d.ts index e919e84..07c502d 100644 --- a/packages/ipc/types.d.ts +++ b/packages/ipc/types.d.ts @@ -1,13 +1,13 @@ declare module '@kalm/ipc' { -interface IPCConfig { - /** The maximum idle time for the connection before it hangs up */ - socketTimeout?: number - /** The unique path for the IPC file descriptor (Unix only) */ - path?: string - } + interface IPCConfig { + /** The maximum idle time for the connection before it hangs up */ + socketTimeout?: number + /** The unique path for the IPC file descriptor (Unix only) */ + path?: string + } - /** + /** * Creates an IPC Transport */ - export default function ipc(config?: IPCConfig): (config?: IPCConfig) => any; + export default function ipc(config?: IPCConfig): (config?: IPCConfig) => any; } diff --git a/packages/kalm/README.md b/packages/kalm/README.md index a3080e5..319a62e 100644 --- a/packages/kalm/README.md +++ b/packages/kalm/README.md @@ -56,11 +56,14 @@ const server = kalm.listen({ }); server.on('connection', (client) => { - client.subscribe('my-channel', (body, frame) => { - // Handle messages here + client.subscribe('channel1', (body, frame) => { + // When receiving messages from this client on "channel1" + console.log(body) // + console.log(frame) // }); - server.broadcast('my-other-channel', 'some message'); + // Sends a message to all clients on "channel2" + server.broadcast('channel2', 'some message'); }); ``` @@ -78,11 +81,14 @@ const client = kalm.connect({ }); client.on('connect', () => { - client.subscribe('my-other-channel', (body, frame) => { - // Handle messages here + client.subscribe('channel1', (body, frame) => { + // When receiving messages from the server on "channel1" + console.log(body); // + console.log(frame); // }); - client.write('my-channel', 'hello world'); + // Sends a message to the server on "channel2" + client.write('channel2', 'hello world'); }); ``` @@ -119,16 +125,22 @@ Example: ## Events -Kalm offers events to track when packets are processed by routines or when a raw frame is received. +Kalm **servers** offers events to track when packets are processed by routines or when a raw frame is received. -| Event | Payload | Description | +| Server Event | Payload | Description | | --- | --- | --- | | `error` | Error | (server, client) Emits on errors. | | `ready` | void | (server) Indicates that the server is now actively listeneing for new connections | -| `connection` | [Client](./types.d.ts#L35) | (server) Indicates that a client has successfuly connected | -| `connect` | [Client](./types.d.ts#L35) | (client) Indicates that a client has successfuly connected | +| `connection` | [Client](./types.d.ts#L90) | (server) Indicates that a client has successfuly connected | + +Kalm **clients** offers events to track when packets are processed by routines or when a raw frame is received. + +| Client Event | Payload | Description | +| --- | --- | --- | +| `error` | Error | (server, client) Emits on errors. | +| `connect` | [Client](./types.d.ts#L90) | (client) Indicates that a client has successfuly connected | | `disconnect` | void | (client) Indicates that a client has disconnected | -| `frame` | [RawFrame](./types.d.ts#L111) | (client) Triggered when recieving a parsed full frame. | +| `frame` | [RawFrame](./types.d.ts#L189) | (client) Triggered when recieving a parsed full frame. | ## Testing @@ -165,4 +177,4 @@ Support this project with your organization. Your logo will show up here with a ## License -[Apache 2.0](LICENSE) (c) 2023 Frederic Charette +[Apache 2.0](LICENSE) (c) 2025 Frederic Charette diff --git a/packages/kalm/package.json b/packages/kalm/package.json index c569bbf..9de17ab 100644 --- a/packages/kalm/package.json +++ b/packages/kalm/package.json @@ -7,7 +7,7 @@ "build": "../../scripts/build.sh kalm", "clean": "../../scripts/cleanup.sh", "test": "jest ./tests", - "prepublish": "cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" + "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" }, "engines": { "node": ">=14" diff --git a/packages/kalm/src/components/client.ts b/packages/kalm/src/components/client.ts index c2ca5ce..401f8ce 100644 --- a/packages/kalm/src/components/client.ts +++ b/packages/kalm/src/components/client.ts @@ -8,13 +8,13 @@ export function Client(params: ClientConfig, emitter: EventEmitter, socket?: any name: string packets: any[] handlers: Function[] - } + }; - const channels: {[channel: string]: Channel } = {}; + const channels: { [channel: string]: Channel } = {}; const routine = params.routine(params, _wrap); const transport: Socket = params.transport(params, emitter); let instance = null; - + const remote: Remote = (params.isServer) ? transport.remote(socket) : { host: params.host, port: params.port }; const local: Remote = (params.isServer) ? { host: params.host, port: params.port } : null; @@ -39,7 +39,7 @@ export function Client(params: ClientConfig, emitter: EventEmitter, socket?: any return frame; }, { frameId, channels: {} })); - getChannels().forEach(channelName => { channels[channelName].packets.length = 0; }); + getChannels().forEach((channelName) => { channels[channelName].packets.length = 0; }); } function _handleConnect(): void { @@ -53,7 +53,7 @@ export function Client(params: ClientConfig, emitter: EventEmitter, socket?: any function _handleRequest(frame: RawFrame, payloadBytes: number): void { if (frame && frame.channels) { - Object.keys(frame.channels).forEach(channelName => { + Object.keys(frame.channels).forEach((channelName) => { frame.channels[channelName].forEach((packet, messageIndex) => { if (channelName in channels) { channels[channelName].handlers.forEach(handler => handler( @@ -93,8 +93,8 @@ export function Client(params: ClientConfig, emitter: EventEmitter, socket?: any if (connected > 1) setTimeout(() => transport.disconnect(socket), 0); } - function subscribe(channel: string, handler: (msg: any, frame: Frame) => void): void { - _resolveChannel(channel).handlers.push(handler); + function subscribe(channelName: string, handler: (msg: any, frame: Frame) => void): void { + _resolveChannel(channelName).handlers.push(handler); } function unsubscribe(channelName: string, handler?: (msg: any, frame: Frame) => void): void { @@ -102,7 +102,8 @@ export function Client(params: ClientConfig, emitter: EventEmitter, socket?: any if (handler) { const index = channels[channelName].handlers.indexOf(handler); if (index > -1) channels[channelName].handlers.splice(index, 1); - } else channels[channelName].handlers = []; + } + else channels[channelName].handlers = []; if (channels[channelName].handlers.length === 0 && channels[channelName].packets.length === 0) delete channels[channelName]; } diff --git a/packages/kalm/src/utils/logger.ts b/packages/kalm/src/utils/logger.ts index 30448e4..e6c7256 100644 --- a/packages/kalm/src/utils/logger.ts +++ b/packages/kalm/src/utils/logger.ts @@ -7,9 +7,9 @@ export function log(msg: string): void { if (enabled === null) { enabled = ( (typeof process === 'object' && process.env.NODE_DEBUG) - || (typeof window === 'object' && window.DEBUG) - || '' + || (typeof window === 'object' && window.DEBUG) + || '' ).indexOf('kalm') > -1; } - if (enabled === true) console.log(`${prefix}: ${msg}`); // eslint-disable-line no-console + if (enabled === true) console.log(`${prefix}: ${msg}`); } diff --git a/packages/kalm/types.d.ts b/packages/kalm/types.d.ts index 570c733..415ebbb 100644 --- a/packages/kalm/types.d.ts +++ b/packages/kalm/types.d.ts @@ -1,265 +1,265 @@ interface ClientConfig { - /** Optional name for the client */ - label?: string - /** The buffering strategy to use for sending messages */ - routine?: KalmRoutine - /** Wether messages are JSON objects or Buffers. (default: true) */ - json?: Boolean - /** The transport protocol to use. You'll need to install it seperatly ex: @kalm/tcp*/ - transport?: KalmTransport - /** The port to connect to */ - port?: number - /** The hostname or ip of the server to connect to */ - host?: string - /** Internal: Tells if the client has been created by the server */ - isServer?: boolean - /** Internal: The server object reference for server-created clients */ - server?: Partial + /** Optional name for the client */ + label?: string + /** The buffering strategy to use for sending messages */ + routine?: KalmRoutine + /** Wether messages are JSON objects or Buffers. (default: true) */ + json?: boolean + /** The transport protocol to use. You'll need to install it seperatly ex: @kalm/tcp */ + transport?: KalmTransport + /** The port to connect to */ + port?: number + /** The hostname or ip of the server to connect to */ + host?: string + /** Internal: Tells if the client has been created by the server */ + isServer?: boolean + /** Internal: The server object reference for server-created clients */ + server?: Partial } interface ServerConfig { - /** Optional name for the server */ - label?: string - /** The buffering strategy to use for sending messages*/ - routine?: KalmRoutine - /** Wether messages are JSON objects or Buffers. (default: true) */ - json?: Boolean - /** The transport protocol to use. You'll need to install it seperatly ex: @kalm/tcp*/ - transport?: KalmTransport - /** The port to listen on */ - port?: number - /** Optional the hostname or ip of the server */ - host?: string + /** Optional name for the server */ + label?: string + /** The buffering strategy to use for sending messages */ + routine?: KalmRoutine + /** Wether messages are JSON objects or Buffers. (default: true) */ + json?: boolean + /** The transport protocol to use. You'll need to install it seperatly ex: @kalm/tcp */ + transport?: KalmTransport + /** The port to listen on */ + port?: number + /** Optional the hostname or ip of the server */ + host?: string } type Remote = { - host: string - port: number -} + host: string + port: number +}; interface ServerEventMap { - 'ready': () => void - 'connection': (client: Client) => void - 'error': (error: Error) => void + ready: () => void + connection: (client: Client) => void + error: (error: Error) => void } interface ClientEventMap { - 'connect': (client: Client) => void - 'disconnect': () => void - 'frame': (frame: RawFrame) => void - 'error': (error: Error) => void + connect: (client: Client) => void + disconnect: () => void + frame: (frame: RawFrame) => void + error: (error: Error) => void } /** * A socket server instance. When a server receives a request from an initiating - * client, it creates a matching client instance on it's end, building it's connection pool. + * client, it creates a matching client instance on it's end, building it's connection pool. */ interface Server { - /** + /** * Sends a message to all active clients in the connection pool - * + * * @params channel The channel name for the message, any client with a matching subscription will receive the broadcast * @params message The message to be emitted to all active clients in the connection pool */ - broadcast: (channel: string, message: Serializable) => void - /** A unique label or name for the server (optional) */ - label: string - /** Kills the server and destroys all active clients and their connection */ - stop: () => void - /** The list of active clients */ - connections: Client[] + broadcast: (channel: string, message: Serializable) => void + /** A unique label or name for the server (optional) */ + label: string + /** Kills the server and destroys all active clients and their connection */ + stop: () => void + /** The list of active clients */ + connections: Client[] - /** + /** * Events emitted by the server: - * + * * 'ready': once the server is ready and accepting new connections - * + * * 'connection': when a client connects to the server - * + * * 'error': when an error occurs (non-fatal) */ - on(event: k, listener: ServerEventMap[k]): this; - once(event: k, listener: ServerEventMap[k] | Function): this; - removeListener(event: k, listener: ServerEventMap[k] | Function): this; - off(event: k, listener: ServerEventMap[k] | Function): this; + on(event: k, listener: ServerEventMap[k]): this + once(event: k, listener: ServerEventMap[k] | Function): this + removeListener(event: k, listener: ServerEventMap[k] | Function): this + off(event: k, listener: ServerEventMap[k] | Function): this } /** * A socket client instance. */ interface Client { - /** + /** * Writes a message to the remote client - * + * * @params channel The channel name for the message, given the remote client has a matching subscription * @params message The message to be emitted to the remote client */ - write: (channel: string, message: Serializable) => void - /** + write: (channel: string, message: Serializable) => void + /** * Kills the connection to the server and destroys the client */ - destroy: () => void - /** + destroy: () => void + /** * Begins listening for messages that are sent to a given channel - * + * * @param channel The channel name for the subscription * @param handler The function to invoke when a new message arrives */ - subscribe: (channel: string, handler: (body: any, frame: Frame) => any) => void - /** + subscribe: (channel: string, handler: (body: any, frame: Frame) => any) => void + /** * Stops listening for messages that are sent to a given channel - * + * * @param channel The channel name for the subscription to stop * @param handler Optionally, the function to stop invoking. If left empty, will clear all handlers for that subscription */ - unsubscribe: (channel: string, handler: (body: any, frame: Frame) => any) => void - /** + unsubscribe: (channel: string, handler: (body: any, frame: Frame) => any) => void + /** * Prints the coordinates of the local client */ - local: Remote - /** + local: Remote + /** * Prints the coordinates of the remote client */ - remote: Remote + remote: Remote - /** + /** * Events emitted by the client: - * + * * 'connect': once the client has connected to the server - * + * * 'disconnect': when the client disconnects from the server - * + * * 'frame': inspects a raw frame as it arrives - * + * * 'error': when an error occurs (non-fatal) */ - on(event: k, listener: ClientEventMap[k]): this; - once(event: k, listener: ClientEventMap[k] | Function): this; - removeListener(event: k, listener: ClientEventMap[k] | Function): this; - off(event: k, listener: ClientEventMap[k] | Function): this; + on(event: k, listener: ClientEventMap[k]): this + once(event: k, listener: ClientEventMap[k] | Function): this + removeListener(event: k, listener: ClientEventMap[k] | Function): this + off(event: k, listener: ClientEventMap[k] | Function): this } -type Serializable = Buffer | object | string | null +type Serializable = Buffer | object | string | null; interface KalmRoutine { - (params: any, routineEmitter: (frameId: number) => any): Queue + (params: any, routineEmitter: (frameId: number) => any): Queue } /** The message queue for a given subscription */ interface Queue { - add: (packet: any) => void - size: () => number - flush: () => void + add: (packet: any) => void + size: () => number + flush: () => void } interface KalmTransport { - (params: any, emitter: any): Socket + (params: any, emitter: any): Socket } type Peer = { - candidate?: { - candidate: string - sdpMLineIndex: number - sdpMid: string - } - type?: 'offer' | 'answer' - sdp?: string -} + candidate?: { + candidate: string + sdpMLineIndex: number + sdpMid: string + } + type?: 'offer' | 'answer' + sdp?: string +}; interface Socket { - /** The command for a server to start listening for messages */ - bind: () => void - /** Given a Client, prints the information of the remote party in the connection */ - remote: (handle: any) => Remote - /** Initiates the connection to a remote server */ - connect: (handle?: any) => any - /** The command to stop a server from accepting messages */ - stop: () => void - /** Given a Client, sends a message to a remote connection */ - send: (handle: any, message: RawFrame) => void - /** The command to disconnect a Client */ - disconnect: (handle: any) => void - /** Exclusive to WebRTC transport, allows the connection with a new Peer */ - negociate?: (params: { peer: Peer }) => Promise + /** The command for a server to start listening for messages */ + bind: () => void + /** Given a Client, prints the information of the remote party in the connection */ + remote: (handle: any) => Remote + /** Initiates the connection to a remote server */ + connect: (handle?: any) => any + /** The command to stop a server from accepting messages */ + stop: () => void + /** Given a Client, sends a message to a remote connection */ + send: (handle: any, message: RawFrame) => void + /** The command to disconnect a Client */ + disconnect: (handle: any) => void + /** Exclusive to WebRTC transport, allows the connection with a new Peer */ + negociate?: (params: { peer: Peer }) => Promise } /** * The raw format of data transferred between Kalm clients and servers. Can be inspected by listening for the `frame` event on a Client */ type RawFrame = { - /** The id of the frame, these are integers cycling from 0 to 0xffffffff */ - frameId: number - /** The list of channels and their received messages, still as Buffers of bytes */ - channels: { [channelName: string]: Buffer[] } -} + /** The id of the frame, these are integers cycling from 0 to 0xffffffff */ + frameId: number + /** The list of channels and their received messages, still as Buffers of bytes */ + channels: { [channelName: string]: Buffer[] } +}; /** * The contextual frame for a message received */ type Frame = { - /** A reference to the Client instance */ - client: Client - /** The name of the subscription channel */ - channel: string - /** The body of the message */ - frame: { - /** The id of the frame, these are integers cycling from 0 to 0xffffffff */ - id: number - /** The position of the message ion the frame */ - messageIndex: number - /** The number of bytes in the frame */ - payloadBytes: number - /** The number of messages in the frame */ - payloadMessages: number - } -} + /** A reference to the Client instance */ + client: Client + /** The name of the subscription channel */ + channel: string + /** The body of the message */ + frame: { + /** The id of the frame, these are integers cycling from 0 to 0xffffffff */ + id: number + /** The position of the message ion the frame */ + messageIndex: number + /** The number of bytes in the frame */ + payloadBytes: number + /** The number of messages in the frame */ + payloadMessages: number + } +}; type TickConfig = { - /** Interval in Hertz for emitting messages. The value can be translated as the number of emits per second. For example: `hz: 60` means 60 emits per second, or one emit every 16ms */ - hz: number - /** A timestamp to establish the first tick, can be used to time a group of servers to emit together, or in cascade as wanted */ - seed?: number -} + /** Interval in Hertz for emitting messages. The value can be translated as the number of emits per second. For example: `hz: 60` means 60 emits per second, or one emit every 16ms */ + hz: number + /** A timestamp to establish the first tick, can be used to time a group of servers to emit together, or in cascade as wanted */ + seed?: number +}; type DynamicConfig = { - /** Maximum interval between emits in milliseconds */ - maxInterval: number - /** Maximum number of messages in the queue */ - maxPackets?: number - /** Maximum number of total bytes in the queue across all messages */ - maxBytes?: number -} + /** Maximum interval between emits in milliseconds */ + maxInterval: number + /** Maximum number of messages in the queue */ + maxPackets?: number + /** Maximum number of total bytes in the queue across all messages */ + maxBytes?: number +}; type RealtimeConfig = { - /** Option to defer the execution of the emit until the next UV cycle. Can prevent blocking of the execution thread */ - deferred: boolean -} + /** Option to defer the execution of the emit until the next UV cycle. Can prevent blocking of the execution thread */ + deferred: boolean +}; declare module 'kalm' { - /** + /** * Starts a server instance that listens for incomming connections */ - export const listen: (config: ServerConfig) => Server; - /** + export const listen: (config: ServerConfig) => Server; + /** * Connects to a remote socket server */ - export const connect: (config: ClientConfig) => Client; - /** + export const connect: (config: ClientConfig) => Client; + /** * The list of buffering routines for packets */ - export const routines: { - /** + export const routines: { + /** * Emits buffered messages on a fixed time interval in Hertz. Can be synced with other servers with the `seed` property */ - tick: (config: TickConfig) => KalmRoutine - /** + tick: (config: TickConfig) => KalmRoutine + /** * Emits buffered messages when one of three conditions is met: * either the maximum time interval between emits, the maximum number of buffered messages or buffered bytes is reached. */ - dynamic: (config: DynamicConfig) => KalmRoutine - /** + dynamic: (config: DynamicConfig) => KalmRoutine + /** * Emits messages immediatly as they enter the queue, no buffering */ - realtime: (confg?: RealtimeConfig) => KalmRoutine - }; + realtime: (confg?: RealtimeConfig) => KalmRoutine + }; } diff --git a/packages/tcp/README.md b/packages/tcp/README.md index f1aa678..cf6cd8e 100644 --- a/packages/tcp/README.md +++ b/packages/tcp/README.md @@ -13,10 +13,13 @@ [![Kalm](https://img.shields.io/npm/v/kalm.svg)](https://www.npmjs.com/package/@kalm/tcp) [![Build Status](https://github.com/kalm/kalm.js/workflows/master-status/badge.svg)](https://github.com/kalm/kalm.js/actions?query=workflow%3A+master-status) -![Snyk Vulnerabilities for GitHub Repo](https://img.shields.io/snyk/vulnerabilities/github/kalm/kalm.js) [![Financial Contributors on Open Collective](https://opencollective.com/kalm/all/badge.svg?label=financial+contributors)](https://opencollective.com/kalm) [![Join the chat at https://gitter.im/KALM/home](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/KALM/?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +A TCP transport for the [Kalm](https://github.com/kalm/kalm.js) framework. + +- Send messages over TCP with ease + ## Installing `npm install @kalm/tcp` @@ -38,4 +41,4 @@ If you think of something that you want, [open an issue](//github.com/kalm/kalm. ## License -[Apache 2.0](LICENSE) (c) 2023 Frederic Charette +[Apache 2.0](LICENSE) (c) 2025 Frederic Charette diff --git a/packages/tcp/package.json b/packages/tcp/package.json index 861124c..11a7905 100644 --- a/packages/tcp/package.json +++ b/packages/tcp/package.json @@ -7,7 +7,7 @@ "build": "../../scripts/build.sh tcp", "clean": "../../scripts/cleanup.sh", "test": "jest ./tests", - "prepublish": "cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" + "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" }, "engines": { "node": ">=14" diff --git a/packages/tcp/src/tcp.ts b/packages/tcp/src/tcp.ts index 5a0705d..a416557 100644 --- a/packages/tcp/src/tcp.ts +++ b/packages/tcp/src/tcp.ts @@ -39,13 +39,14 @@ function tcp({ socketTimeout = 30000 }: TCPConfig = {}): KalmTransport { function connect(handle: TCPSocket): TCPSocket { const connection: net.Socket = handle || net.connect(params.port, params.host); let buffer = ''; - connection.on('data', req => { + connection.on('data', (req) => { buffer += req.toString(); const chunks = buffer.split('\n\n'); if (buffer.substring(buffer.length - 2) !== '\n\n') { buffer = chunks[chunks.length - 1]; chunks.pop(); - } else { + } + else { buffer = ''; } diff --git a/packages/tcp/types.d.ts b/packages/tcp/types.d.ts index 1cfaa87..95d1409 100644 --- a/packages/tcp/types.d.ts +++ b/packages/tcp/types.d.ts @@ -1,11 +1,11 @@ declare module '@kalm/tcp' { - interface TCPConfig { - /** The maximum idle time for the connection before it hangs up (default: 30000) */ - socketTimeout?: number - } + interface TCPConfig { + /** The maximum idle time for the connection before it hangs up (default: 30000) */ + socketTimeout?: number + } - /** + /** * Creates a TCP Transport */ - export default function tcp(config?: TCPConfig): (config?: TCPConfig) => any; + export default function tcp(config?: TCPConfig): (config?: TCPConfig) => any; } diff --git a/packages/udp/README.md b/packages/udp/README.md index f1eaecf..6c57772 100644 --- a/packages/udp/README.md +++ b/packages/udp/README.md @@ -13,10 +13,16 @@ [![Kalm](https://img.shields.io/npm/v/kalm.svg)](https://www.npmjs.com/package/@kalm/udp) [![Build Status](https://github.com/kalm/kalm.js/workflows/master-status/badge.svg)](https://github.com/kalm/kalm.js/actions?query=workflow%3A+master-status) -![Snyk Vulnerabilities for GitHub Repo](https://img.shields.io/snyk/vulnerabilities/github/kalm/kalm.js) [![Financial Contributors on Open Collective](https://opencollective.com/kalm/all/badge.svg?label=financial+contributors)](https://opencollective.com/kalm) [![Join the chat at https://gitter.im/KALM/home](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/KALM/?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +A UDP transport for the [Kalm](https://github.com/kalm/kalm.js) framework. + +- Use fire-and-forget type messaging with the conveinience of stateful interfaces +- Supports ipv4 and ipv6 addresses +- Adds timeouts for recycling instances + + ## Installing `npm install @kalm/udp` @@ -44,4 +50,4 @@ If you think of something that you want, [open an issue](//github.com/kalm/kalm. ## License -[Apache 2.0](LICENSE) (c) 2023 Frederic Charette +[Apache 2.0](LICENSE) (c) 2025 Frederic Charette diff --git a/packages/udp/package.json b/packages/udp/package.json index 9b3fb06..aee11b6 100644 --- a/packages/udp/package.json +++ b/packages/udp/package.json @@ -7,7 +7,7 @@ "build": "../../scripts/build.sh udp", "clean": "../../scripts/cleanup.sh", "test": "jest ./tests", - "prepublish": "cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" + "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" }, "funding": { "type": "Open Collective", diff --git a/packages/udp/src/udp.ts b/packages/udp/src/udp.ts index fd17226..c99b468 100644 --- a/packages/udp/src/udp.ts +++ b/packages/udp/src/udp.ts @@ -4,18 +4,17 @@ type UDPSocketHandle = { socket: dgram.Socket port: number host: string -} +}; type UDPConfig = { type?: dgram.SocketType localAddr?: string reuseAddr?: boolean socketTimeout?: number -} +}; function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = false, socketTimeout = 30000 }: UDPConfig = {}): KalmTransport { return function socket(params: ClientConfig, emitter: NodeJS.EventEmitter): Socket { - let listener: dgram.Socket; const clientCache = {}; @@ -36,7 +35,7 @@ function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = false, socketTi function remote(handle: UDPSocketHandle): Remote { return { host: handle?.host || null, - port: handle?.port || null + port: handle?.port || null, }; } @@ -65,9 +64,9 @@ function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = false, socketTi function connect(handle?: UDPSocketHandle): UDPSocketHandle { if (handle) return handle; const connection = dgram.createSocket(type as dgram.SocketType); - + connection.on('error', err => emitter.emit('error', err)); - connection.on('message', req => { + connection.on('message', (req) => { emitter.emit('connect', connection); emitter.emit('frame', JSON.parse(req.toString()), req.length); resetTimeout(res); @@ -92,7 +91,7 @@ function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = false, socketTi port: origin.port, socket: listener, }; - + const key = `${origin.address}.${origin.port}`; if (!clientCache[key]) { @@ -121,7 +120,7 @@ function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = false, socketTi listener.on('message', (data, origin) => resolveClient(origin, data)); listener.on('error', err => emitter.emit('error', err)); listener.bind(params.port, localAddr); - emitter.emit('ready'); + setTimeout(() => emitter.emit('ready'), 1); } if (emitter && typeof emitter.on === 'function') emitter.on('connection', addClient); diff --git a/packages/udp/types.d.ts b/packages/udp/types.d.ts index e127fac..ce9aaf9 100644 --- a/packages/udp/types.d.ts +++ b/packages/udp/types.d.ts @@ -1,17 +1,17 @@ declare module '@kalm/udp' { - interface UDPConfig { - /** The udp socket family (default: udp4) */ - type?: 'udp4' | 'udp6' - /** The ip address that shows up when calling `local()` (default: '0.0.0.0') */ - localAddr?: string - /** UDP reuse Address seting (default: false) */ - reuseAddr?: boolean - /** The maximum idle time for the connection before it hangs up (default: 30000) */ - socketTimeout?: number - } + interface UDPConfig { + /** The udp socket family (default: udp4) */ + type?: 'udp4' | 'udp6' + /** The ip address that shows up when calling `local()` (default: '0.0.0.0') */ + localAddr?: string + /** UDP reuse Address seting (default: false) */ + reuseAddr?: boolean + /** The maximum idle time for the connection before it hangs up (default: 30000) */ + socketTimeout?: number + } - /** + /** * Creates a UDP Transport */ - export default function udp(config?: UDPConfig): (config?: UDPConfig) => any; + export default function udp(config?: UDPConfig): (config?: UDPConfig) => any; } diff --git a/packages/webrtc/README.md b/packages/webrtc/README.md index 8d83938..08ccaf4 100644 --- a/packages/webrtc/README.md +++ b/packages/webrtc/README.md @@ -13,10 +13,14 @@ [![Kalm](https://img.shields.io/npm/v/kalm.svg)](https://www.npmjs.com/package/@kalm/webrtc) [![Build Status](https://github.com/kalm/kalm.js/workflows/master-status/badge.svg)](https://github.com/kalm/kalm.js/actions?query=workflow%3A+master-status) -![Snyk Vulnerabilities for GitHub Repo](https://img.shields.io/snyk/vulnerabilities/github/kalm/kalm.js) [![Financial Contributors on Open Collective](https://opencollective.com/kalm/all/badge.svg?label=financial+contributors)](https://opencollective.com/kalm) [![Join the chat at https://gitter.im/KALM/home](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/KALM/?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +A webrtc transport for the [Kalm](https://github.com/kalm/kalm.js) framework. + +- Uses the [simple-peer](https://github.com/feross/simple-peer) library +- Can be used in the browser or in a Node.js environment + ## Installing `npm install @kalm/webrtc` @@ -38,4 +42,4 @@ If you think of something that you want, [open an issue](//github.com/kalm/kalm. ## License -[Apache 2.0](LICENSE) (c) 2023 Frederic Charette +[Apache 2.0](LICENSE) (c) 2025 Frederic Charette diff --git a/packages/webrtc/package.json b/packages/webrtc/package.json index 2598d1b..e26ff81 100644 --- a/packages/webrtc/package.json +++ b/packages/webrtc/package.json @@ -7,7 +7,7 @@ "build": "../../scripts/build.sh webrtc", "clean": "../../scripts/cleanup.sh", "test": "jest ./tests", - "prepublish": "cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" + "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" }, "funding": { "type": "Open Collective", diff --git a/packages/webrtc/src/webrtc.ts b/packages/webrtc/src/webrtc.ts index 5da86ad..db174d8 100644 --- a/packages/webrtc/src/webrtc.ts +++ b/packages/webrtc/src/webrtc.ts @@ -5,7 +5,7 @@ if (!Peer.WEBRTC_SUPPORT && !(isNode && process.env.JEST_WORKER_ID)) throw new E type WebRTCConfig = { peers?: Peer[] -} +}; function webrtc(config: WebRTCConfig = {}): KalmTransport { return function socket(params: ClientConfig, emitter: NodeJS.EventEmitter): Socket { @@ -20,12 +20,13 @@ function webrtc(config: WebRTCConfig = {}): KalmTransport { } function negociate(event: any) { - return new Promise(resolve => { + return new Promise((resolve) => { if (!event.peer) throw new Error('No peer configuration provided in `connect`.'); if (event.peer.type === 'answer') { activeNode.signal(event.peer); - } else { - passiveNode.on('signal', signal => { + } + else { + passiveNode.on('signal', (signal) => { if (signal.type === 'answer') resolve(signal); }); @@ -37,8 +38,8 @@ function webrtc(config: WebRTCConfig = {}): KalmTransport { function bind(): void { activeNode = createNode(true); passiveNode = createNode(false); - activeNode.on('signal', signal => { - if (signal.type === 'offer') emitter.emit('ready', signal); + activeNode.on('signal', (signal) => { + if (signal.type === 'offer') setTimeout(() => emitter.emit('ready', signal), 1); }); if (config.peers) config.peers.forEach(peer => negociate({ peer })); @@ -57,7 +58,8 @@ function webrtc(config: WebRTCConfig = {}): KalmTransport { emitter.emit('connect', handle); handle.on('data', evt => emitter.emit('frame', JSON.parse(evt.data || evt), (evt.data || evt).length)); handle.on('close', () => emitter.emit('disconnect')); - } else { + } + else { throw new Error('Do not use `connect` for webrtc, use `Client.transport.negociate()` instead.'); } diff --git a/packages/webrtc/types.d.ts b/packages/webrtc/types.d.ts index 1de34e5..15019cf 100644 --- a/packages/webrtc/types.d.ts +++ b/packages/webrtc/types.d.ts @@ -1,25 +1,25 @@ declare module '@kalm/webrtc' { - interface WebRTCConfig { - /** The list of peers to connect with */ - peers?: Peer[] - } + interface WebRTCConfig { + /** The list of peers to connect with */ + peers?: Peer[] + } - type Peer = { - candidate?: { - candidate: string - sdpMLineIndex: number - sdpMid: string - } - type?: 'offer' | 'answer' - sdp?: string + type Peer = { + candidate?: { + candidate: string + sdpMLineIndex: number + sdpMid: string } + type?: 'offer' | 'answer' + sdp?: string + }; - interface Socket { - negociate: (params: { peer: Peer }) => Promise - } + interface Socket { + negociate: (params: { peer: Peer }) => Promise + } - /** + /** * Creates a WebRTC Transport */ - export default function ws(config?: WebRTCConfig): (config?: WebRTCConfig) => any; + export default function ws(config?: WebRTCConfig): (config?: WebRTCConfig) => any; } diff --git a/packages/ws/README.md b/packages/ws/README.md index 9a4d82e..a5c1a62 100644 --- a/packages/ws/README.md +++ b/packages/ws/README.md @@ -13,10 +13,14 @@ [![Kalm](https://img.shields.io/npm/v/kalm.svg)](https://www.npmjs.com/package/@kalm/ws) [![Build Status](https://github.com/kalm/kalm.js/workflows/master-status/badge.svg)](https://github.com/kalm/kalm.js/actions?query=workflow%3A+master-status) -![Snyk Vulnerabilities for GitHub Repo](https://img.shields.io/snyk/vulnerabilities/github/kalm/kalm.js) [![Financial Contributors on Open Collective](https://opencollective.com/kalm/all/badge.svg?label=financial+contributors)](https://opencollective.com/kalm) [![Join the chat at https://gitter.im/KALM/home](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/KALM/?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +A websocket transport for the [Kalm](https://github.com/kalm/kalm.js) framework. + +- Detects native Websocket APIs, or fallbacks to [ws](https://github.com/websockets/ws) +- Supports secure connections + ## Installing `npm install @kalm/ws` @@ -44,4 +48,4 @@ If you think of something that you want, [open an issue](//github.com/kalm/kalm. ## License -[Apache 2.0](LICENSE) (c) 2023 Frederic Charette +[Apache 2.0](LICENSE) (c) 2025 Frederic Charette diff --git a/packages/ws/package.json b/packages/ws/package.json index c0da693..0f3b6e3 100644 --- a/packages/ws/package.json +++ b/packages/ws/package.json @@ -7,7 +7,7 @@ "build": "../../scripts/build.sh ws", "clean": "../../scripts/cleanup.sh", "test": "jest ./tests", - "prepublish": "cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" + "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" }, "funding": { "type": "Open Collective", diff --git a/packages/ws/src/ws.ts b/packages/ws/src/ws.ts index 58d7f87..cee99e2 100644 --- a/packages/ws/src/ws.ts +++ b/packages/ws/src/ws.ts @@ -1,24 +1,24 @@ import events from 'events'; -import {createServer} from 'https'; +import { createServer } from 'https'; -const isBrowser = (typeof WebSocket !== 'undefined'); -const WS = isBrowser ? WebSocket : require('ws'); +const nativeAPIExists = (typeof WebSocket !== 'undefined'); +const WSClient = nativeAPIExists ? WebSocket : require('ws'); +const WSServer = require('ws').Server; type WSConfig = { cert?: string key?: string agent?: any socketTimeout?: number -} +}; type WSHandle = WebSocket & { - _queue: string[], + _queue: string[] _timer: ReturnType headers?: any connection?: any _socket?: any -} - +}; function ws({ cert, key, agent, socketTimeout = 30000 }: WSConfig = {}): KalmTransport { return function socket(params: ClientConfig, emitter: events.EventEmitter): Socket { @@ -27,13 +27,14 @@ function ws({ cert, key, agent, socketTimeout = 30000 }: WSConfig = {}): KalmTra function bind(): void { if (cert && key) { const server = createServer({ key, cert }, req => req.socket.end()); - listener = new WS.Server({ port: params.port, server }); - } else { - listener = new WS.Server({ port: params.port }); + listener = new WSServer({ port: params.port, server }); + } + else { + listener = new WSServer({ port: params.port }); } listener.on('connection', soc => emitter.emit('socket', soc)); listener.on('error', err => emitter.emit('error', err)); - emitter.emit('ready'); + setTimeout(() => emitter.emit('ready'), 1); } function send(handle: WSHandle & { _queue: string[] }, payload: RawFrame | string): void { @@ -48,12 +49,12 @@ function ws({ cert, key, agent, socketTimeout = 30000 }: WSConfig = {}): KalmTra function connect(handle?: WSHandle): WSHandle { const protocol: string = (!!cert && !!key) === true ? 'wss' : 'ws'; - const connection: WSHandle = handle || new WS(`${protocol}://${params.host}:${params.port}`, { ...(agent ? {agent} : {})}); + const connection: WSHandle = handle || new WSClient(`${protocol}://${params.host}:${params.port}`, { ...(agent ? { agent } : {}) }); connection.binaryType = 'arraybuffer'; - const evtType: string = isBrowser ? 'addEventListener' : 'on'; + const evtType: string = nativeAPIExists ? 'addEventListener' : 'on'; connection._queue = []; connection._timer = null; - connection[evtType]('message', evt => { + connection[evtType]('message', (evt) => { emitter.emit('frame', JSON.parse(evt.data || evt), (evt.data || evt).length); resetTimeout(connection); }); diff --git a/packages/ws/types.d.ts b/packages/ws/types.d.ts index 41bdeec..af24800 100644 --- a/packages/ws/types.d.ts +++ b/packages/ws/types.d.ts @@ -1,17 +1,17 @@ declare module '@kalm/ws' { - interface WSConfig { - /** The certificate file content for a secure socket connection, both this and `key` must be set */ - cert?: string - /** The key file content for a secure socket connection, both this and `cert` must be set */ - key?: string - /** A custom agent for the http connection, can be used to set proxies or other connection behaviours */ - agent?: any - /** The maximum idle time for the connection before it hangs up (default: 30000) */ - socketTimeout?: number - } + interface WSConfig { + /** The certificate file content for a secure socket connection, both this and `key` must be set */ + cert?: string + /** The key file content for a secure socket connection, both this and `cert` must be set */ + key?: string + /** A custom agent for the http connection, can be used to set proxies or other connection behaviours */ + agent?: any + /** The maximum idle time for the connection before it hangs up (default: 30000) */ + socketTimeout?: number + } - /** + /** * Creates a websocket Transport */ - export default function ws(config?: WSConfig): (config?: WSConfig) => any; + export default function ws(config?: WSConfig): (config?: WSConfig) => any; } diff --git a/scripts/benchmarks/index.js b/scripts/benchmarks/index.js index daa4ab0..5ee762e 100644 --- a/scripts/benchmarks/index.js +++ b/scripts/benchmarks/index.js @@ -2,7 +2,7 @@ * Kalm benchmarking */ -/* Requires ------------------------------------------------------------------*/ +/* Requires ------------------------------------------------------------------ */ const Kalm = require('./transports/kalm'); const TCP = require('./transports/tcp'); @@ -11,7 +11,7 @@ const UDP = require('./transports/udp'); const WS = require('./transports/socketio'); const settings = require('./settings'); -/* Local variables -----------------------------------------------------------*/ +/* Local variables ----------------------------------------------------------- */ const _maxCount = null; let _curr = 0; @@ -19,7 +19,7 @@ const Suite = { IPC, TCP, UDP, WS }; const tests = []; const results = {}; -/* Methods -------------------------------------------------------------------*/ +/* Methods ------------------------------------------------------------------- */ function _measure(transport, resolve) { _curr = 0; @@ -60,13 +60,12 @@ function _postResults() { process.exit(); } -/* Init ----------------------------------------------------------------------*/ - +/* Init ---------------------------------------------------------------------- */ // Roll port number settings.port = 3000 + Math.round(Math.random() * 1000); -const adpts = Object.keys(Suite).map((k) => ({ +const adpts = Object.keys(Suite).map(k => ({ transport: k, settings: { transport: k.toLowerCase() }, raw: Suite[k], @@ -98,9 +97,9 @@ adpts.forEach((i) => { tests.push(_postResults); -console.log(`Launching benchmarks for ${settings.testDuration/1000} second(s) -- MAKE SURE THAT YOU BUILD THE CODE FIRST --`) +console.log(`Launching benchmarks for ${settings.testDuration / 1000} second(s) -- MAKE SURE THAT YOU BUILD THE CODE FIRST --`); tests.reduce( - (c, n) => c.then((resolve) => new Promise(n).then(resolve, _errorHandler), _errorHandler), + (c, n) => c.then(resolve => new Promise(n).then(resolve, _errorHandler), _errorHandler), Promise.resolve(), ); diff --git a/scripts/benchmarks/transports/ipc.js b/scripts/benchmarks/transports/ipc.js index 88e101c..bb59b28 100644 --- a/scripts/benchmarks/transports/ipc.js +++ b/scripts/benchmarks/transports/ipc.js @@ -2,14 +2,13 @@ * KALM Benchmark */ - -/* Requires ------------------------------------------------------------------*/ +/* Requires ------------------------------------------------------------------ */ const net = require('net'); const settings = require('../settings'); -/* Local variables -----------------------------------------------------------*/ +/* Local variables ----------------------------------------------------------- */ let server; let client; @@ -17,7 +16,7 @@ let client; let count = 0; let handbreak = true; -/* Methods -------------------------------------------------------------------*/ +/* Methods ------------------------------------------------------------------- */ function _absorb(err) { console.log(err); /* eslint-disable-line */ @@ -62,7 +61,7 @@ function step(resolve) { resolve(); } -/* Exports -------------------------------------------------------------------*/ +/* Exports ------------------------------------------------------------------- */ module.exports = { setup, diff --git a/scripts/benchmarks/transports/kalm.js b/scripts/benchmarks/transports/kalm.js index 6af5055..dbced59 100644 --- a/scripts/benchmarks/transports/kalm.js +++ b/scripts/benchmarks/transports/kalm.js @@ -2,8 +2,7 @@ * KALM Benchmark */ - -/* Requires ------------------------------------------------------------------*/ +/* Requires ------------------------------------------------------------------ */ const settings = require('../settings'); const Kalm = require('../../../packages/kalm/bin/kalm'); @@ -15,7 +14,7 @@ const transports = { ws: require('../../../packages/ws/bin/ws'), }; -/* Local variables -----------------------------------------------------------*/ +/* Local variables ----------------------------------------------------------- */ let server; let client; @@ -24,7 +23,7 @@ let count = 0; let accDensity = 0; let handbreak = true; -/* Methods -------------------------------------------------------------------*/ +/* Methods ------------------------------------------------------------------- */ function setup(resolve) { server = Kalm.listen({ @@ -35,7 +34,7 @@ function setup(resolve) { }); server.on('connection', (c) => { - c.subscribe(settings.testChannel, (msg) => c.write(settings.testChannel, msg)); + c.subscribe(settings.testChannel, msg => c.write(settings.testChannel, msg)); }); server.on('error', (e) => { @@ -68,7 +67,7 @@ function step(resolve) { routine: Kalm.routines.realtime(), }); client.subscribe(settings.testChannel, (body, frame) => { - //console.log('got it', frame) + // console.log('got it', frame) count++; }); @@ -82,7 +81,7 @@ function step(resolve) { resolve(); } -/* Exports -------------------------------------------------------------------*/ +/* Exports ------------------------------------------------------------------- */ module.exports = { setup, diff --git a/scripts/benchmarks/transports/socketio.js b/scripts/benchmarks/transports/socketio.js index 5425c11..65dca1d 100644 --- a/scripts/benchmarks/transports/socketio.js +++ b/scripts/benchmarks/transports/socketio.js @@ -2,8 +2,7 @@ * KALM Benchmark */ - -/* Requires ------------------------------------------------------------------*/ +/* Requires ------------------------------------------------------------------ */ const io = require('socket.io'); const http = require('http'); @@ -11,7 +10,7 @@ const ioclient = require('socket.io-client'); const settings = require('../settings'); -/* Local variables -----------------------------------------------------------*/ +/* Local variables ----------------------------------------------------------- */ let server; let client; @@ -19,7 +18,7 @@ let client; let count = 0; let handbreak = true; -/* Methods -------------------------------------------------------------------*/ +/* Methods ------------------------------------------------------------------- */ function _absorb(err) { console.log(err); /* eslint-disable-line */ @@ -65,7 +64,7 @@ function step(resolve) { resolve(); } -/* Exports -------------------------------------------------------------------*/ +/* Exports ------------------------------------------------------------------- */ module.exports = { setup, diff --git a/scripts/benchmarks/transports/tcp.js b/scripts/benchmarks/transports/tcp.js index 15aca4e..2b0765d 100644 --- a/scripts/benchmarks/transports/tcp.js +++ b/scripts/benchmarks/transports/tcp.js @@ -2,13 +2,12 @@ * KALM Benchmark */ - -/* Requires ------------------------------------------------------------------*/ +/* Requires ------------------------------------------------------------------ */ const net = require('net'); const settings = require('../settings'); -/* Local variables -----------------------------------------------------------*/ +/* Local variables ----------------------------------------------------------- */ let server; let client; @@ -16,7 +15,7 @@ let client; let count = 0; let handbreak = true; -/* Methods -------------------------------------------------------------------*/ +/* Methods ------------------------------------------------------------------- */ function _absorb(err) { console.log(err); /* eslint-disable-line */ @@ -60,7 +59,7 @@ function step(resolve) { resolve(); } -/* Exports -------------------------------------------------------------------*/ +/* Exports ------------------------------------------------------------------- */ module.exports = { setup, diff --git a/scripts/benchmarks/transports/udp.js b/scripts/benchmarks/transports/udp.js index 3d4b290..1ff0e69 100644 --- a/scripts/benchmarks/transports/udp.js +++ b/scripts/benchmarks/transports/udp.js @@ -2,14 +2,13 @@ * KALM Benchmark */ - -/* Requires ------------------------------------------------------------------*/ +/* Requires ------------------------------------------------------------------ */ const dgram = require('dgram'); const settings = require('../settings'); -/* Local letiables -----------------------------------------------------------*/ +/* Local letiables ----------------------------------------------------------- */ let server; let client; @@ -17,7 +16,7 @@ let client; let count = 0; let handbreak = true; -/* Methods -------------------------------------------------------------------*/ +/* Methods ------------------------------------------------------------------- */ function _absorb(err) { console.log(err); /* eslint-disable-line */ @@ -60,7 +59,7 @@ function step(resolve) { resolve(); } -/* Exports -------------------------------------------------------------------*/ +/* Exports ------------------------------------------------------------------- */ module.exports = { setup, diff --git a/tests/integration/frame.spec.ts b/tests/integration/frame.spec.ts index aa82e42..1c349b6 100644 --- a/tests/integration/frame.spec.ts +++ b/tests/integration/frame.spec.ts @@ -1,14 +1,14 @@ -/* Requires ------------------------------------------------------------------*/ +/* Requires ------------------------------------------------------------------ */ import { connect, listen } from '../../packages/kalm/bin/kalm'; import ipc from '../../packages/ipc/bin/ipc'; -/* Suite --------------------------------------------------------------------*/ +/* Suite -------------------------------------------------------------------- */ describe('Frame', () => { let server; - /* --- Setup ---*/ + /* --- Setup --- */ // Create a server before each scenario beforeEach(() => { @@ -18,15 +18,15 @@ describe('Frame', () => { }); // Cleanup afterwards - afterEach(done => { + afterEach((done) => { server.stop(); server = null; setTimeout(() => done(), 100); }); - it('Should have a well structured frame reference', done => { + it('Should have a well structured frame reference', (done) => { const payload = { foo: 'bar' }; - server.on('connection', c => { + server.on('connection', (c) => { c.subscribe('test', (data, meta) => { expect(meta).toEqual({ client: c, @@ -41,12 +41,12 @@ describe('Frame', () => { done(); }); }); - server.on('error', e => { + server.on('error', (e) => { throw new Error(e); }); const client = connect({ transport: ipc() }); - client.on('error', e => { + client.on('error', (e) => { throw new Error(e); }); client.write('test', payload); diff --git a/tests/integration/index.spec.ts b/tests/integration/index.spec.ts index 4433d92..fbbd573 100644 --- a/tests/integration/index.spec.ts +++ b/tests/integration/index.spec.ts @@ -2,19 +2,19 @@ * Kalm integration test suite */ -/* Requires ------------------------------------------------------------------*/ +/* Requires ------------------------------------------------------------------ */ import { connect, listen } from '../../packages/kalm/bin/kalm'; -/* Suite --------------------------------------------------------------------*/ +/* Suite -------------------------------------------------------------------- */ describe('Integration tests', () => { - ['ipc', 'tcp', 'udp', 'ws'].forEach(transport => { + ['ipc', 'tcp', 'udp', 'ws'].forEach((transport) => { describe(`Testing ${transport} transport`, () => { let server; const soc = require(`../../packages/${transport}/bin/${transport}`)(); /* eslint-disable-line */ - /* --- Setup ---*/ + /* --- Setup --- */ // Create a server before each scenario beforeEach(() => { @@ -24,7 +24,7 @@ describe('Integration tests', () => { }); // Cleanup afterwards - afterEach(done => { + afterEach((done) => { server.stop(); server = null; setTimeout(() => done(), 100); @@ -32,62 +32,62 @@ describe('Integration tests', () => { /* --- Tests --- */ - it(`should work with ${transport}`, done => { + it(`should work with ${transport}`, (done) => { const payload = { foo: 'bar' }; - server.on('connection', c => { - c.subscribe('test', data => { + server.on('connection', (c) => { + c.subscribe('test', (data) => { expect(data).toEqual(payload); done(); }); }); - server.on('error', e => { + server.on('error', (e) => { throw new Error(e); }); const client = connect({ transport: soc }); - client.on('error', e => { + client.on('error', (e) => { throw new Error(e); }); client.write('test', payload); }); - it(`should handle foreign characters with ${transport}`, done => { + it(`should handle foreign characters with ${transport}`, (done) => { const payload = { foo: '한자' }; - server.on('connection', c => { - c.subscribe('test', data => { + server.on('connection', (c) => { + c.subscribe('test', (data) => { expect(data).toEqual(payload); done(); }); }); - server.on('error', e => { + server.on('error', (e) => { throw new Error(e); }); const client = connect({ transport: soc }); - client.on('error', e => { + client.on('error', (e) => { throw new Error(e); }); client.write('test', payload); }); - it(`should handle large payloads with ${transport}`, done => { + it(`should handle large payloads with ${transport}`, (done) => { const largePayload: { foo: string }[] = []; while (largePayload.length < 2048) { largePayload.push({ foo: 'bar' }); } - server.on('connection', c => { - c.subscribe('test.large', data => { + server.on('connection', (c) => { + c.subscribe('test.large', (data) => { expect(data).toEqual(largePayload); done(); }); }); - server.on('error', e => { + server.on('error', (e) => { throw new Error(e); }); const client = connect({ transport: soc }); - client.on('error', e => { + client.on('error', (e) => { if (transport === 'udp') { expect(e.message).toEqual('UDP Cannot send packets larger than 16384 bytes, tried to send 28715 bytes'); return done(); @@ -97,9 +97,9 @@ describe('Integration tests', () => { client.write('test.large', largePayload); }); - it('should not trigger for unsubscribed channels', done => { + it('should not trigger for unsubscribed channels', (done) => { const payload = { foo: 'bar' }; - server.on('connection', c => { + server.on('connection', (c) => { c.subscribe('test', () => { // Throw on purpose expect(false).toBe(true); @@ -108,12 +108,12 @@ describe('Integration tests', () => { c.unsubscribe('test'); }); - server.on('error', e => { + server.on('error', (e) => { throw new Error(e); }); const client = connect({ transport: soc }); - client.on('error', e => { + client.on('error', (e) => { throw new Error(e); }); setTimeout(() => client.write('test', payload), 100); From 5f723f0faf8a749b8fab5d6d8dfac6061b04c1a5 Mon Sep 17 00:00:00 2001 From: Frederic Charette Date: Tue, 29 Jul 2025 21:13:03 -0400 Subject: [PATCH 02/11] major overhaul --- .gitignore | 10 +- CHANGELOG.md | 13 +- eslint.config.mjs | 2 +- examples/browser_peer_to_peer/README.md | 33 +-- examples/browser_peer_to_peer/client.js | 44 ---- examples/browser_peer_to_peer/index.html | 232 ++++++++++++++++++++++ examples/browser_peer_to_peer/server.js | 55 +++-- examples/distributed_pub_sub/server.js | 23 ++- package.json | 21 +- packages/ipc/package.json | 8 +- packages/ipc/rollup.config.mjs | 17 ++ packages/ipc/src/ipc.ts | 11 +- packages/ipc/tests/unit/ipc.spec.ts | 4 +- packages/kalm/README.md | 1 - packages/kalm/package.json | 8 +- packages/kalm/rollup.config.mjs | 22 ++ packages/kalm/src/components/client.ts | 13 +- packages/kalm/src/components/server.ts | 9 +- packages/kalm/src/kalm.ts | 3 +- packages/kalm/src/utils/events.ts | 29 +++ packages/tcp/package.json | 8 +- packages/tcp/rollup.config.mjs | 17 ++ packages/tcp/src/tcp.ts | 12 +- packages/tcp/tests/unit/tcp.spec.ts | 4 +- packages/udp/package.json | 8 +- packages/udp/rollup.config.mjs | 17 ++ packages/udp/src/udp.ts | 9 +- packages/udp/tests/unit/udp.spec.ts | 4 +- packages/webrtc/README.md | 45 ----- packages/webrtc/package.json | 57 ------ packages/webrtc/src/webrtc.ts | 90 --------- packages/webrtc/tests/unit/webrtc.spec.ts | 41 ---- packages/webrtc/types.d.ts | 25 --- packages/ws/README.md | 2 - packages/ws/package.json | 13 +- packages/ws/rollup.config.mjs | 16 ++ packages/ws/src/ws.ts | 38 ++-- packages/ws/tests/unit/ws.spec.ts | 4 +- packages/ws/types.d.ts | 2 - scripts/benchmarks/transports/ipc.js | 10 +- scripts/benchmarks/transports/kalm.js | 16 +- scripts/benchmarks/transports/socketio.js | 10 +- scripts/benchmarks/transports/tcp.js | 10 +- scripts/benchmarks/transports/udp.js | 8 +- scripts/build.sh | 9 - scripts/cleanup.sh | 5 - tests/integration/frame.spec.ts | 10 +- tests/integration/import.spec.ts | 10 +- tests/integration/index.spec.ts | 28 +-- tsconfig.json | 3 +- packages/kalm/types.d.ts => types.d.ts | 7 +- 51 files changed, 599 insertions(+), 497 deletions(-) delete mode 100644 examples/browser_peer_to_peer/client.js create mode 100644 examples/browser_peer_to_peer/index.html create mode 100644 packages/ipc/rollup.config.mjs create mode 100644 packages/kalm/rollup.config.mjs create mode 100644 packages/kalm/src/utils/events.ts create mode 100644 packages/tcp/rollup.config.mjs create mode 100644 packages/udp/rollup.config.mjs delete mode 100644 packages/webrtc/README.md delete mode 100644 packages/webrtc/package.json delete mode 100644 packages/webrtc/src/webrtc.ts delete mode 100644 packages/webrtc/tests/unit/webrtc.spec.ts delete mode 100644 packages/webrtc/types.d.ts create mode 100644 packages/ws/rollup.config.mjs delete mode 100755 scripts/build.sh delete mode 100755 scripts/cleanup.sh rename packages/kalm/types.d.ts => types.d.ts (95%) diff --git a/.gitignore b/.gitignore index 00e8ee6..ec600a5 100644 --- a/.gitignore +++ b/.gitignore @@ -49,7 +49,13 @@ packages/ipc/bin packages/tcp/bin packages/udp/bin packages/ws/bin -packages/webrtc/bin + +# distribution code +packages/kalm/dist +packages/ipc/dist +packages/tcp/dist +packages/udp/dist +packages/ws/dist # publish-only files packages/kalm/LICENSE @@ -57,11 +63,9 @@ packages/ipc/LICENSE packages/tcp/LICENSE packages/udp/LICENSE packages/ws/LICENSE -packages/webrtc/LICENSE packages/kalm/CHANGELOG.md packages/ipc/CHANGELOG.md packages/tcp/CHANGELOG.md packages/udp/CHANGELOG.md packages/ws/CHANGELOG.md -packages/webrtc/CHANGELOG.md \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bcad14..98efce8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,24 @@ # Changelog -## [v7.1.0] - 2025-07-26 +## [v8.0.0] - 2025-07-26 commit [#](https://github.com/kalm/kalm.js/commits) +### Breaking changes + +- Updated bundling for greater compatibilty (exports *may* behave differently) +- Deprecated the WebRTC Transport (too convoluted to fit the Kalm model) +- Changed the signature of the `frame` event handler from `(frame: RawFrame, payloadBytes: number)` to `({ body: RawFrame, payloadBytes: number})` to ensure all event handlers only have one arguments. + ### Minor changes - Added support for the new native WS APIs in Node 22 and later - Removed yarn from the toolchain. There's no reason to keep it now that NPM workspaces are more mature. +- Deprecated the `agent` property for the WS Transport +- Migrated the underlying Node EventEmitter to the cross-platform EventTarget system. A translation layer should keep end-user code intact. +- Fixed server connections not getting cleaned up +- Removed empty channels from frame payloads, saving bandwidth + ## [v7.0.0] - 2023-03-17 diff --git a/eslint.config.mjs b/eslint.config.mjs index 89aa4f5..c9063a0 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -16,5 +16,5 @@ export default tseslint.config( 'jest/no-done-callback': 0, }, }, - globalIgnores(['**/bin']), + globalIgnores(['**/bin', '**/dist', '**/*.js']), ); diff --git a/examples/browser_peer_to_peer/README.md b/examples/browser_peer_to_peer/README.md index 712946b..5d4c5a2 100644 --- a/examples/browser_peer_to_peer/README.md +++ b/examples/browser_peer_to_peer/README.md @@ -6,33 +6,34 @@ This example shows how to create a P2P (Peer to Peer) network. - The clients can run in either the browser or in Node.js. - The server must run in a Node.js environment. -- NPM or other package manager to install `kalm`, `@kalm/tcp` and `@kalm/ws` +- NPM or other package manager to install `kalm`, `@kalm/tcp` and `@kalm/ws` +- A means to run a local web server to serve html pages from. Eg: python's SimpleHTTPServer # Testing -Launch any number of servers first, using process arguments to specify the hosts and ports for this node and the seed node. - - +First, let's start the WebSocket server, which enables peer discovery. ``` -node ./server.js 0.0.0.0 3000 0.0.0.0 3000 -node ./server.js 0.0.0.0 3001 0.0.0.0 3000 -node ./server.js 0.0.0.0 3002 0.0.0.0 3000 +node ./server.js ``` -With this example, three servers are launched, listening on 3 different ports, but all connected to the seed host (port 3000). -The seed acts as an orchestrator and puts nodes in contact with each other. They all end up connected to one another. +Clients will connect to this server to advertise to other peers. + +Next, we'll launch some clients. + +Serve the index.html file from an http server (otherwise you will run into CORS issues). At the root of this repo, run: -In parallel, servers also listen on `port + 10000` for clients to join. +``` +python -m SimpleHTTPServer +``` -We will now launch clients connecting to any given server. +OR, if using python 3: ``` -node ./client.js 0.0.0.0 13000 -node ./client.js 0.0.0.0 13001 -node ./client.js 0.0.0.0 13002 +python3 -m http.server ``` -The clients should connect to the server and send an "hello world!" message using the external channel. +Open as many browser tabs as desired and navigate to `http://localhost:8000/examples/browser_peer_to_peer/` + +Peers should appear in the list and give you the option to connect to them. -In turn, the servers will forward this message to the others via an internal channel before each broadcasts it back to its connected clients. diff --git a/examples/browser_peer_to_peer/client.js b/examples/browser_peer_to_peer/client.js deleted file mode 100644 index 45f1263..0000000 --- a/examples/browser_peer_to_peer/client.js +++ /dev/null @@ -1,44 +0,0 @@ -const roomPassword = 'some_random_string'; - -function createPeer(channel) { - return new Promise((resolve, reject) => { - let peerListener; - const peeringClient = kalm.connect({ - label: Math.random() * 1024, - host: '0.0.0.0', - port: 8800, - transport: ws(), - routine: kalm.routines.realtime(), - }); - - peeringClient.on('connect', () => { - peeringClient.write('peering', channel); - peerListener = kalm.listen({ transport: webrtc() }); - - peerListener.on('ready', (offer) => { - peeringClient.write(`${channel}.peering`, offer); - }); - - peeringClient.subscribe(`${channel}.peering`, (offer) => { - peerListener.transport.negociate({ peer: offer }) - .then((answer) => { - peeringClient.write(`${channel}.peering`, answer); - }); - }); - - resolve(peerListener); - }); - - peeringClient.on('error', err => reject(err)); - }); -} - -// Since we don't have top-level async yet -(async () => { - const client = await createPeer(roomPassword); - client.on('connection', (connection) => { - console.log('new connection', connection); - client.broadcast('/', 'new peer'); - connection.subscribe('/', body => console.log(`Got peer message: "${body}"`)); - }); -})(); diff --git a/examples/browser_peer_to_peer/index.html b/examples/browser_peer_to_peer/index.html new file mode 100644 index 0000000..c596735 --- /dev/null +++ b/examples/browser_peer_to_peer/index.html @@ -0,0 +1,232 @@ + + Kalm Peer-to-peer example + + + + + + + + + +

Kalm Peer-to-peer example

+ +
+

Login

+
+ + +
+
+ + + + + + \ No newline at end of file diff --git a/examples/browser_peer_to_peer/server.js b/examples/browser_peer_to_peer/server.js index 5097f69..eaf38c5 100644 --- a/examples/browser_peer_to_peer/server.js +++ b/examples/browser_peer_to_peer/server.js @@ -1,24 +1,53 @@ -const kalm = require('../kalm.js/packages/kalm'); -const ws = require('../kalm.js/packages/ws'); +const kalm = require('../../packages/kalm'); +const ws = require('../../packages/ws'); +/** + * Creates a kalm server that uses the Websocket transport. + * It is bound to local IP 0.0.0.0 and listens on port 9001. + * + * The realtime routine will emit messages to clients as soon as possible + * + * The purpose of this server is to advertise the existence of connected clients and facilitate the exchange of signal data for WebRTC connection + */ const Server = kalm.listen({ label: 'server', - port: 8800, + port: 9001, transport: ws(), routine: kalm.routines.realtime(), host: '0.0.0.0', }); Server.on('connection', (client) => { - client.subscribe('peering', (channel) => { - client.subscribe(`${channel}.peering`, (body, frame) => { - Server.connections - .filter((connection) => { - return (connection.label !== client.label && connection.getChannels().includes(`${channel}.peering`)); - }) - .forEach((connection) => { - connection.write(`${channel}.peering`, body); - }); - }); + + /** + * When we receive peering messages, we expect that a new client is advertising. + * We'll label it. The label property on Kalm Clients and Server can be anything and can also be defined in config. + * Note that labels do not travel over the wire and are only assigned to local instances. + */ + client.subscribe('peering', (adv) => { + client.label = adv; + Server.broadcast('peering', Server.connections.map(c => c.label)); + }); + + /** + * Candidate messages means that two nodes are in the process of exchanging information. + * We'll ensure that the right message get to the right node by filtering on the label property we set earlier. + */ + client.subscribe('candidate', (answer) => { + const match = Server.connections.find((c) => c.label.sdp === answer.target); + if (match) { + match.write('candidate', answer); + } }); + + /** + * When a client disconnects from the server, we'll also notify other connected clients + */ + client.on('disconnect', () => { + Server.broadcast('peering', Server.connections.map(c => c.label)); + }); +}); + +Server.on('ready', () => { + console.log('Server is ready and listening on port 9001'); }); diff --git a/examples/distributed_pub_sub/server.js b/examples/distributed_pub_sub/server.js index b1184a9..3978884 100644 --- a/examples/distributed_pub_sub/server.js +++ b/examples/distributed_pub_sub/server.js @@ -50,7 +50,6 @@ if (!isSeed) { transport: tcp(), routine: kalm.routines.realtime(), }); - seedNode.write('n.add', { host: config.host, ts, port: config.port }); /** * We'll add the seed node to our internal server's connection list manually, this will allow us to broadcast once. @@ -74,12 +73,30 @@ if (!isSeed) { * We'll add the new node to our internal server's connection list manually, this will allow us to broadcast once. */ internal.connections.push(newNode); + + newNode.subscribe('n.evt', (body) => { + external.broadcast('r.evt', body); + }); + + newNode.on('connect', () => { + console.log('Connected to new node'); + }); + + newNode.on('error', (err) => console.log('newNode error:', err)); } }); seedNode.subscribe('n.evt', (body) => { external.broadcast('r.evt', body); }); + + seedNode.on('connect', () => { + console.log('Connected to seed node'); + }); + + seedNode.on('error', (err) => console.log('seedNode error:', err)); + + seedNode.write('n.add', { host: config.host, ts, port: config.port }); } internal.on('connection', (client) => { @@ -114,3 +131,7 @@ external.on('connection', (client) => { external.broadcast('r.evt', body); }); }); + + +internal.on('error', (err) => console.log('internal error:', err)); +external.on('error', (err) => console.log('external error:', err)); \ No newline at end of file diff --git a/package.json b/package.json index 0dec463..88fa976 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "version": "7.1.0", "description": "The socket optimizer", - "main": "packages/kalm/bin/kalm.js", + "main": "packages/kalm/dist/kalm.js", "scripts": { "lint": "eslint .", "lint:fix": "eslint . --fix", @@ -45,13 +45,19 @@ "testEnvironment": "node", "transform": { "^.+\\.tsx?$": [ - "ts-jest", { - "diagnostics": false, - "isolatedModules": true - }] + "ts-jest", + { + "diagnostics": false, + "isolatedModules": true + } + ] } }, "devDependencies": { + "@rollup/plugin-commonjs": "^28.0.6", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-sucrase": "^5.0.2", + "@rollup/plugin-typescript": "^12.1.4", "@stylistic/eslint-plugin": "^5.2.0", "@types/jest": "^30.0.0", "@types/node": "^24.1.0", @@ -59,11 +65,14 @@ "eslint-plugin-jest": "^29.0.0", "husky": "^9.1.0", "jest": "^30.0.0", + "rollup": "^4.46.0", + "rollup-plugin-polyfill-node": "^0.13.0", "snappy": "^7.3.0", "socket.io": "^4.8.0", "socket.io-client": "^4.8.0", "ts-jest": "^29.4.0", "typescript": "^5.8.0", - "typescript-eslint": "^8.38.0" + "typescript-eslint": "^8.38.0", + "ws": "^8.18.3" } } diff --git a/packages/ipc/package.json b/packages/ipc/package.json index 91f7d38..298844d 100644 --- a/packages/ipc/package.json +++ b/packages/ipc/package.json @@ -2,10 +2,10 @@ "name": "@kalm/ipc", "version": "7.0.0", "description": "IPC transport for Kalm", - "main": "bin/ipc.js", + "main": "dist/ipc.js", "scripts": { - "build": "../../scripts/build.sh ipc", - "clean": "../../scripts/cleanup.sh", + "build": "npm run clean && rollup -c rollup.config.mjs", + "clean": "rm -rf ./dist/*", "test": "jest ./tests", "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" }, @@ -28,7 +28,7 @@ "nagle", "ipc" ], - "files": ["bin", "types.d.ts"], + "files": ["dist", "types.d.ts"], "author": "frederic charette ", "license": "Apache-2.0", "typings": "./types.d.ts", diff --git a/packages/ipc/rollup.config.mjs b/packages/ipc/rollup.config.mjs new file mode 100644 index 0000000..da14dd4 --- /dev/null +++ b/packages/ipc/rollup.config.mjs @@ -0,0 +1,17 @@ +import sucrase from '@rollup/plugin-sucrase'; + +export default { + input: 'src/ipc.ts', + plugins: [ + sucrase({ + include: ['src/**'], + transforms: ['typescript'] + }), + ], + external: ['net'], + output: { + file: 'dist/ipc.js', + name: 'ipc', + format: 'umd' + } +}; \ No newline at end of file diff --git a/packages/ipc/src/ipc.ts b/packages/ipc/src/ipc.ts index 82a2f59..1c6424b 100644 --- a/packages/ipc/src/ipc.ts +++ b/packages/ipc/src/ipc.ts @@ -1,4 +1,4 @@ -import net from 'net'; +import net from 'node:net'; interface IPCSocket extends net.Socket { server?: { @@ -17,7 +17,7 @@ type IPCConfig = { path?: string }; -function ipc({ socketTimeout = 30000, path = '/tmp/app.socket-' }: IPCConfig = {}): KalmTransport { +export default function ipc({ socketTimeout = 30000, path = '/tmp/app.socket-' }: IPCConfig = {}): KalmTransport { return function socket(params: ClientConfig, emitter: NodeJS.EventEmitter): Socket { let listener: net.Server; @@ -56,12 +56,12 @@ function ipc({ socketTimeout = 30000, path = '/tmp/app.socket-' }: IPCConfig = { } for (let i = 0; i < chunks.length; i++) { - if (chunks[i] !== '') emitter.emit('frame', JSON.parse(chunks[i]), req.length); + if (chunks[i] !== '') emitter.emit('frame', { body: JSON.parse(chunks[i]), payloadBytes: req.length }); } }); connection.on('error', err => emitter.emit('error', err)); connection.on('connect', () => emitter.emit('connect', connection)); - connection.on('close', () => emitter.emit('disconnect')); + connection.on('close', () => emitter.emit('disconnected')); connection.setTimeout(socketTimeout, () => disconnect(handle)); return connection; } @@ -84,6 +84,3 @@ function ipc({ socketTimeout = 30000, path = '/tmp/app.socket-' }: IPCConfig = { }; }; } - -// Ensures support for modules and requires -module.exports = ipc; diff --git a/packages/ipc/tests/unit/ipc.spec.ts b/packages/ipc/tests/unit/ipc.spec.ts index b543f77..4b43b9d 100644 --- a/packages/ipc/tests/unit/ipc.spec.ts +++ b/packages/ipc/tests/unit/ipc.spec.ts @@ -1,5 +1,5 @@ -import { EventEmitter } from 'events'; import * as ipc from '../../src/ipc'; +import { EventEmitter } from '../../../kalm/src/utils/events'; describe('IPC transport', () => { it('basic setup', () => { @@ -22,7 +22,7 @@ describe('IPC transport', () => { describe('when fetching remote', () => { it('should return null values', () => { - expect(socket.remote()).toEqual({ host: null, port: null }); + expect(socket.remote).toEqual({ host: null, port: null }); }); }); }); diff --git a/packages/kalm/README.md b/packages/kalm/README.md index 319a62e..51ba75b 100644 --- a/packages/kalm/README.md +++ b/packages/kalm/README.md @@ -108,7 +108,6 @@ To see working implementations, check out our [examples](https://github.com/kalm - [@kalm/ipc](https://www.npmjs.com/package/@kalm/ipc) - [@kalm/tcp](https://www.npmjs.com/package/@kalm/tcp) - [@kalm/udp](https://www.npmjs.com/package/@kalm/udp) - - [@kalm/webrtc](https://www.npmjs.com/package/@kalm/webrtc) - [@kalm/ws](https://www.npmjs.com/package/@kalm/ws) - Routines [[wiki]](https://github.com/kalm/kalm.js/wiki/Routines) - realtime diff --git a/packages/kalm/package.json b/packages/kalm/package.json index 9de17ab..85c1af6 100644 --- a/packages/kalm/package.json +++ b/packages/kalm/package.json @@ -2,10 +2,10 @@ "name": "kalm", "version": "7.0.0", "description": "The socket optimizer", - "main": "bin/kalm.js", + "main": "dist/kalm.js", "scripts": { - "build": "../../scripts/build.sh kalm", - "clean": "../../scripts/cleanup.sh", + "build": "npm run clean && rollup -c rollup.config.mjs", + "clean": "rm -rf ./dist/*", "test": "jest ./tests", "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" }, @@ -34,7 +34,7 @@ "peer" ], "files": [ - "bin", + "dist", "types.d.ts" ], "typings": "./types.d.ts", diff --git a/packages/kalm/rollup.config.mjs b/packages/kalm/rollup.config.mjs new file mode 100644 index 0000000..f1b4a32 --- /dev/null +++ b/packages/kalm/rollup.config.mjs @@ -0,0 +1,22 @@ +import resolve from '@rollup/plugin-node-resolve'; +import sucrase from '@rollup/plugin-sucrase'; + +export default { + input: 'src/kalm.ts', + plugins: [ + resolve({ + extensions: ['.ts'], + preferBuiltins: true, + browser: true, + }), + sucrase({ + include: ['src/**'], + transforms: ['typescript'] + }), + ], + output: { + file: 'dist/kalm.js', + name: 'kalm', + format: 'umd' + } +}; \ No newline at end of file diff --git a/packages/kalm/src/components/client.ts b/packages/kalm/src/components/client.ts index 401f8ce..a0b1e3a 100644 --- a/packages/kalm/src/components/client.ts +++ b/packages/kalm/src/components/client.ts @@ -1,4 +1,4 @@ -import { EventEmitter } from 'events'; +import { EventEmitter } from '../utils/events'; import { log } from '../utils/logger'; export function Client(params: ClientConfig, emitter: EventEmitter, socket?: any): Client { @@ -35,7 +35,7 @@ export function Client(params: ClientConfig, emitter: EventEmitter, socket?: any function _wrap(frameId: number): void { transport.send(socket, getChannels().reduce((frame, channelName) => { - frame.channels[channelName] = channels[channelName].packets; + if (channels[channelName].packets.length > 0) frame.channels[channelName] = channels[channelName].packets; return frame; }, { frameId, channels: {} })); @@ -51,7 +51,8 @@ export function Client(params: ClientConfig, emitter: EventEmitter, socket?: any log(`error ${err.message}`); } - function _handleRequest(frame: RawFrame, payloadBytes: number): void { + function _handleRequest(frameEvent: { body: RawFrame, payloadBytes: number }): void { + const frame = frameEvent.body; if (frame && frame.channels) { Object.keys(frame.channels).forEach((channelName) => { frame.channels[channelName].forEach((packet, messageIndex) => { @@ -64,7 +65,7 @@ export function Client(params: ClientConfig, emitter: EventEmitter, socket?: any channel: channelName, id: frame.frameId, messageIndex, - payloadBytes, + payloadBytes: frameEvent.payloadBytes, payloadMessages: frame.channels[channelName].length, }, }, @@ -78,6 +79,8 @@ export function Client(params: ClientConfig, emitter: EventEmitter, socket?: any function _handleDisconnect() { connected = 0; log(`lost connection to ${params.host}:${params.port}`); + // Gives a change for other internal listeners to run their business logic + setTimeout(() => emitter.emit('disconnect'), 0); } function write(channelName: string, message: Serializable): void { @@ -109,7 +112,7 @@ export function Client(params: ClientConfig, emitter: EventEmitter, socket?: any } emitter.on('connect', _handleConnect); - emitter.on('disconnect', _handleDisconnect); + emitter.on('disconnected', _handleDisconnect); emitter.on('error', _handleError); emitter.on('frame', _handleRequest); if (!socket) log(`connecting to ${params.host}:${params.port}`); diff --git a/packages/kalm/src/components/server.ts b/packages/kalm/src/components/server.ts index 19a15a9..ac88829 100644 --- a/packages/kalm/src/components/server.ts +++ b/packages/kalm/src/components/server.ts @@ -1,8 +1,8 @@ -import { EventEmitter } from 'events'; import { log } from '../utils/logger'; +import { EventEmitter } from '../utils/events'; import { Client } from './client'; -export function Server(params: ClientConfig, emitter: NodeJS.EventEmitter): Server { +export function Server(params: ClientConfig, emitter: EventEmitter): Server { const connections = []; const socket: Socket = params.transport(params, emitter); @@ -42,6 +42,11 @@ export function Server(params: ClientConfig, emitter: NodeJS.EventEmitter): Serv connections.push(client); emitter.emit('connection', client); log(`connection from ${origin.host}:${origin.port}`); + + client.on('disconnected', () => { + const clientIndex = connections.findIndex((o) => o == client); + if (clientIndex > -1) connections.splice(clientIndex, 1); + }); } emitter.on('socket', handleConnection); diff --git a/packages/kalm/src/kalm.ts b/packages/kalm/src/kalm.ts index f43dee7..3fbaeff 100644 --- a/packages/kalm/src/kalm.ts +++ b/packages/kalm/src/kalm.ts @@ -1,4 +1,3 @@ -import { EventEmitter } from 'events'; import { Client } from './components/client'; import { Server } from './components/server'; @@ -6,6 +5,8 @@ import { dynamic } from './routines/dynamic'; import { realtime } from './routines/realtime'; import { tick } from './routines/tick'; +import { EventEmitter } from './utils/events'; + const defaults: ServerConfig = { host: '0.0.0.0', json: true, diff --git a/packages/kalm/src/utils/events.ts b/packages/kalm/src/utils/events.ts new file mode 100644 index 0000000..c9a54b1 --- /dev/null +++ b/packages/kalm/src/utils/events.ts @@ -0,0 +1,29 @@ +function spread(handler, evt) { + return handler(evt.detail); +} + +export class EventEmitter extends EventTarget { + constructor() { + super(); + } + + on(eventName, handler) { + this.addEventListener(eventName, spread.bind(null, handler)); + } + + off(eventName, handler) { + this.removeEventListener(eventName, spread.bind(null, handler)); + } + + once(eventName, handler) { + this.addEventListener(eventName, spread.bind(null, handler), { once: true }); + } + + removeListener(eventName, handler) { + this.removeEventListener(eventName, spread.bind(null, handler)); + } + + emit(eventName, detail?) { + this.dispatchEvent(new CustomEvent(eventName, { detail })) + } +} diff --git a/packages/tcp/package.json b/packages/tcp/package.json index 11a7905..f399ed1 100644 --- a/packages/tcp/package.json +++ b/packages/tcp/package.json @@ -2,10 +2,10 @@ "name": "@kalm/tcp", "version": "7.0.0", "description": "TCP transport for Kalm", - "main": "bin/tcp.js", + "main": "dist/tcp.js", "scripts": { - "build": "../../scripts/build.sh tcp", - "clean": "../../scripts/cleanup.sh", + "build": "npm run clean && rollup -c rollup.config.mjs", + "clean": "rm -rf ./dist/*", "test": "jest ./tests", "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" }, @@ -28,7 +28,7 @@ "nagle", "tcp" ], - "files": ["bin", "types.d.ts"], + "files": ["dist", "types.d.ts"], "typings": "./types.d.ts", "author": "frederic charette ", "license": "Apache-2.0", diff --git a/packages/tcp/rollup.config.mjs b/packages/tcp/rollup.config.mjs new file mode 100644 index 0000000..937b531 --- /dev/null +++ b/packages/tcp/rollup.config.mjs @@ -0,0 +1,17 @@ +import sucrase from '@rollup/plugin-sucrase'; + +export default { + input: 'src/tcp.ts', + plugins: [ + sucrase({ + include: ['src/**'], + transforms: ['typescript'] + }), + ], + external: ['net'], + output: { + file: 'dist/tcp.js', + name: 'tcp', + format: 'umd' + } +}; \ No newline at end of file diff --git a/packages/tcp/src/tcp.ts b/packages/tcp/src/tcp.ts index a416557..531692c 100644 --- a/packages/tcp/src/tcp.ts +++ b/packages/tcp/src/tcp.ts @@ -1,5 +1,4 @@ import net from 'net'; -import events from 'events'; interface TCPSocket extends net.Socket { _peername: { @@ -12,8 +11,8 @@ interface TCPConfig { socketTimeout?: number } -function tcp({ socketTimeout = 30000 }: TCPConfig = {}): KalmTransport { - return function socket(params: ClientConfig, emitter: events.EventEmitter): Socket { +export default function tcp({ socketTimeout = 30000 }: TCPConfig = {}): KalmTransport { + return function socket(params: ClientConfig, emitter: NodeJS.EventEmitter): Socket { let listener: net.Server; function bind(): void { @@ -51,12 +50,12 @@ function tcp({ socketTimeout = 30000 }: TCPConfig = {}): KalmTransport { } for (let i = 0; i < chunks.length; i++) { - if (chunks[i] !== '') emitter.emit('frame', JSON.parse(chunks[i]), req.length); + if (chunks[i] !== '') emitter.emit('frame', { body: JSON.parse(chunks[i]), payloadBytes: req.length }); } }); connection.on('error', err => emitter.emit('error', err)); connection.on('connect', () => emitter.emit('connect', connection)); - connection.on('close', () => emitter.emit('disconnect')); + connection.on('close', () => emitter.emit('disconnected')); connection.setTimeout(socketTimeout, () => disconnect(handle)); return connection as TCPSocket; } @@ -79,6 +78,3 @@ function tcp({ socketTimeout = 30000 }: TCPConfig = {}): KalmTransport { }; }; } - -// Ensures support for modules and requires -module.exports = tcp; diff --git a/packages/tcp/tests/unit/tcp.spec.ts b/packages/tcp/tests/unit/tcp.spec.ts index d17e732..646010a 100644 --- a/packages/tcp/tests/unit/tcp.spec.ts +++ b/packages/tcp/tests/unit/tcp.spec.ts @@ -1,4 +1,4 @@ -import { EventEmitter } from 'events'; +import { EventEmitter } from '../../../kalm/src/utils/events'; import * as tcp from '../../src/tcp'; describe('TCP transport', () => { @@ -22,7 +22,7 @@ describe('TCP transport', () => { describe('when fetching remote', () => { it('should return null values', () => { - expect(socket.remote()).toEqual({ host: null, port: null }); + expect(socket.remote).toEqual({ host: null, port: null }); }); }); }); diff --git a/packages/udp/package.json b/packages/udp/package.json index aee11b6..8c0b874 100644 --- a/packages/udp/package.json +++ b/packages/udp/package.json @@ -2,10 +2,10 @@ "name": "@kalm/udp", "version": "7.0.0", "description": "UDP transport for Kalm", - "main": "bin/udp.js", + "main": "dist/udp.js", "scripts": { - "build": "../../scripts/build.sh udp", - "clean": "../../scripts/cleanup.sh", + "build": "npm run clean && rollup -c rollup.config.mjs", + "clean": "rm -rf ./dist/*", "test": "jest ./tests", "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" }, @@ -28,7 +28,7 @@ "nagle", "udp" ], - "files": ["bin", "types.d.ts"], + "files": ["dist", "types.d.ts"], "typings": "./types.d.ts", "author": "frederic charette ", "license": "Apache-2.0", diff --git a/packages/udp/rollup.config.mjs b/packages/udp/rollup.config.mjs new file mode 100644 index 0000000..5511327 --- /dev/null +++ b/packages/udp/rollup.config.mjs @@ -0,0 +1,17 @@ +import sucrase from '@rollup/plugin-sucrase'; + +export default { + input: 'src/udp.ts', + plugins: [ + sucrase({ + include: ['src/**'], + transforms: ['typescript'] + }), + ], + external: ['dgram'], + output: { + file: 'dist/udp.js', + name: 'udp', + format: 'umd' + } +}; \ No newline at end of file diff --git a/packages/udp/src/udp.ts b/packages/udp/src/udp.ts index c99b468..83e3ebe 100644 --- a/packages/udp/src/udp.ts +++ b/packages/udp/src/udp.ts @@ -13,7 +13,7 @@ type UDPConfig = { socketTimeout?: number }; -function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = false, socketTimeout = 30000 }: UDPConfig = {}): KalmTransport { +export default function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = false, socketTimeout = 30000 }: UDPConfig = {}): KalmTransport { return function socket(params: ClientConfig, emitter: NodeJS.EventEmitter): Socket { let listener: dgram.Socket; const clientCache = {}; @@ -58,7 +58,7 @@ function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = false, socketTi function disconnect(handle?: UDPSocketHandle): void { if (handle && handle.socket) handle.socket = null; - emitter.emit('disconnect'); + emitter.emit('disconnected'); } function connect(handle?: UDPSocketHandle): UDPSocketHandle { @@ -68,7 +68,7 @@ function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = false, socketTi connection.on('error', err => emitter.emit('error', err)); connection.on('message', (req) => { emitter.emit('connect', connection); - emitter.emit('frame', JSON.parse(req.toString()), req.length); + emitter.emit('frame', { body: JSON.parse(req.toString()), payloadBytes: req.length }); resetTimeout(res); }); connection.bind(null, localAddr); @@ -135,6 +135,3 @@ function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = false, socketTi }; }; } - -// Ensures support for modules and requires -module.exports = udp; diff --git a/packages/udp/tests/unit/udp.spec.ts b/packages/udp/tests/unit/udp.spec.ts index 8187a0f..1578df4 100644 --- a/packages/udp/tests/unit/udp.spec.ts +++ b/packages/udp/tests/unit/udp.spec.ts @@ -1,4 +1,4 @@ -import { EventEmitter } from 'events'; +import { EventEmitter } from '../../../kalm/src/utils/events'; import * as udp from '../../src/udp'; describe('UDP transport', () => { @@ -22,7 +22,7 @@ describe('UDP transport', () => { describe('when fetching remote', () => { it('should return null values', () => { - expect(socket.remote()).toEqual({ host: null, port: null }); + expect(socket.remote).toEqual({ host: null, port: null }); }); }); }); diff --git a/packages/webrtc/README.md b/packages/webrtc/README.md deleted file mode 100644 index 08ccaf4..0000000 --- a/packages/webrtc/README.md +++ /dev/null @@ -1,45 +0,0 @@ -

- - Kalm -
-
- Kalm -

-

- The Socket Optimizer -

-

-
- -[![Kalm](https://img.shields.io/npm/v/kalm.svg)](https://www.npmjs.com/package/@kalm/webrtc) -[![Build Status](https://github.com/kalm/kalm.js/workflows/master-status/badge.svg)](https://github.com/kalm/kalm.js/actions?query=workflow%3A+master-status) -[![Financial Contributors on Open Collective](https://opencollective.com/kalm/all/badge.svg?label=financial+contributors)](https://opencollective.com/kalm) -[![Join the chat at https://gitter.im/KALM/home](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/KALM/?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - -A webrtc transport for the [Kalm](https://github.com/kalm/kalm.js) framework. - -- Uses the [simple-peer](https://github.com/feross/simple-peer) library -- Can be used in the browser or in a Node.js environment - -## Installing - -`npm install @kalm/webrtc` - -## Options - -```typescript -{ - /** The peers to connect to */ - peers: [ { type: "offer", sdp: "..." } ], -} -``` - -For more info, refer to the [Kalm Homepage](https://github.com/kalm/kalm.js) - -## Contribute - -If you think of something that you want, [open an issue](//github.com/kalm/kalm.js/issues/new) or file a pull request, we'll be more than happy to take a look! - -## License - -[Apache 2.0](LICENSE) (c) 2025 Frederic Charette diff --git a/packages/webrtc/package.json b/packages/webrtc/package.json deleted file mode 100644 index e26ff81..0000000 --- a/packages/webrtc/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@kalm/webrtc", - "version": "7.0.0", - "description": "WebRTC transport for Kalm", - "main": "bin/webrtc.js", - "scripts": { - "build": "../../scripts/build.sh webrtc", - "clean": "../../scripts/cleanup.sh", - "test": "jest ./tests", - "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" - }, - "funding": { - "type": "Open Collective", - "url": "https://opencollective.com/kalm" - }, - "engines": { - "node": ">=14" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/kalm/kalm-js.git" - }, - "keywords": [ - "framework", - "network", - "realtime", - "socket", - "nagle", - "webrtc" - ], - "files": ["bin", "types.d.ts"], - "typings": "./types.d.ts", - "author": "frederic charette ", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/kalm/kalm-js/issues" - }, - "homepage": "https://kalm.js.org", - "contributors": [ - "frederic charette " - ], - "dependencies": { - "simple-peer": "^9.11.0" - }, - "jest": { - "preset": "ts-jest", - "testEnvironment": "node", - "transform": { - "^.+\\.tsx?$": [ - "ts-jest", { - "diagnostics": false, - "isolatedModules": true - }] - } - } -} - \ No newline at end of file diff --git a/packages/webrtc/src/webrtc.ts b/packages/webrtc/src/webrtc.ts deleted file mode 100644 index db174d8..0000000 --- a/packages/webrtc/src/webrtc.ts +++ /dev/null @@ -1,90 +0,0 @@ -import Peer from 'simple-peer'; - -const isNode = (typeof process !== 'undefined'); -if (!Peer.WEBRTC_SUPPORT && !(isNode && process.env.JEST_WORKER_ID)) throw new Error('Unsupported environement for WebRTC'); - -type WebRTCConfig = { - peers?: Peer[] -}; - -function webrtc(config: WebRTCConfig = {}): KalmTransport { - return function socket(params: ClientConfig, emitter: NodeJS.EventEmitter): Socket { - let activeNode; - let passiveNode; - - function createNode(isInitiator) { - const node = new Peer({ initiator: isInitiator, trickle: false }); - node.on('error', err => emitter.emit('error', err)); - node.on('connect', () => emitter.emit('socket', node)); - return node; - } - - function negociate(event: any) { - return new Promise((resolve) => { - if (!event.peer) throw new Error('No peer configuration provided in `connect`.'); - if (event.peer.type === 'answer') { - activeNode.signal(event.peer); - } - else { - passiveNode.on('signal', (signal) => { - if (signal.type === 'answer') resolve(signal); - }); - - passiveNode.signal(event.peer); - } - }); - } - - function bind(): void { - activeNode = createNode(true); - passiveNode = createNode(false); - activeNode.on('signal', (signal) => { - if (signal.type === 'offer') setTimeout(() => emitter.emit('ready', signal), 1); - }); - - if (config.peers) config.peers.forEach(peer => negociate({ peer })); - } - - function send(handle: any, payload: RawFrame): void { - handle.send(JSON.stringify(payload)); - } - - function stop(): void { - if (activeNode) activeNode.destroy(); - } - - function connect(handle: any): any { - if (handle) { - emitter.emit('connect', handle); - handle.on('data', evt => emitter.emit('frame', JSON.parse(evt.data || evt), (evt.data || evt).length)); - handle.on('close', () => emitter.emit('disconnect')); - } - else { - throw new Error('Do not use `connect` for webrtc, use `Client.transport.negociate()` instead.'); - } - - return handle; - } - - function remote(handle: any): Remote { - return handle || { host: null, port: null }; - } - - function disconnect(handle) { - if (handle) handle.destroy(); - } - - return { - bind, - connect, - disconnect, - remote, - send, - stop, - negociate, - }; - }; -} - -// Ensures support for modules and requires -module.exports = webrtc; diff --git a/packages/webrtc/tests/unit/webrtc.spec.ts b/packages/webrtc/tests/unit/webrtc.spec.ts deleted file mode 100644 index ec609e4..0000000 --- a/packages/webrtc/tests/unit/webrtc.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { EventEmitter } from 'events'; -import * as webrtc from '../../src/webrtc'; - -describe('webrtc transport', () => { - it('basic setup', () => { - expect(typeof webrtc.default).toBe('function'); - const transport = webrtc.default(); - expect(typeof transport).toBe('function'); - const socket = transport({}, new EventEmitter()); - - expect(socket).toHaveProperty('bind', expect.any(Function)); - expect(socket).toHaveProperty('connect', expect.any(Function)); - expect(socket).toHaveProperty('disconnect', expect.any(Function)); - expect(socket).toHaveProperty('remote', expect.any(Function)); - expect(socket).toHaveProperty('stop', expect.any(Function)); - expect(socket).toHaveProperty('send', expect.any(Function)); - expect(socket).toHaveProperty('negociate', expect.any(Function)); - }); - - describe('Given an empty handle reference and no configs', () => { - const transport = webrtc.default(); - const socket = transport({}, new EventEmitter()); - - describe('when fetching remote', () => { - it('should return null values', () => { - expect(socket.remote()).toEqual({ host: null, port: null }); - }); - }); - }); - - describe('Given a handle reference and no configs', () => { - const transport = webrtc.default(); - const socket = transport({}, new EventEmitter()); - - describe('when fetching remote', () => { - it('should return handle\'s values', () => { - expect(socket.remote({ host: '127.0.0.1', port: 3000 })).toEqual({ host: '127.0.0.1', port: 3000 }); - }); - }); - }); -}); diff --git a/packages/webrtc/types.d.ts b/packages/webrtc/types.d.ts deleted file mode 100644 index 15019cf..0000000 --- a/packages/webrtc/types.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -declare module '@kalm/webrtc' { - interface WebRTCConfig { - /** The list of peers to connect with */ - peers?: Peer[] - } - - type Peer = { - candidate?: { - candidate: string - sdpMLineIndex: number - sdpMid: string - } - type?: 'offer' | 'answer' - sdp?: string - }; - - interface Socket { - negociate: (params: { peer: Peer }) => Promise - } - - /** - * Creates a WebRTC Transport - */ - export default function ws(config?: WebRTCConfig): (config?: WebRTCConfig) => any; -} diff --git a/packages/ws/README.md b/packages/ws/README.md index a5c1a62..dc19be0 100644 --- a/packages/ws/README.md +++ b/packages/ws/README.md @@ -33,8 +33,6 @@ A websocket transport for the [Kalm](https://github.com/kalm/kalm.js) framework. cert?: string /** The key file content for a secure socket connection, both this and `cert` must be set */ key?: string - /** A custom agent for the http connection, can be used to set proxies or other connection behaviours */ - agent?: any /** The maximum idle time for the connection before it hangs up (default: 30000) */ socketTimeout: number } diff --git a/packages/ws/package.json b/packages/ws/package.json index 0f3b6e3..2cb4068 100644 --- a/packages/ws/package.json +++ b/packages/ws/package.json @@ -2,10 +2,10 @@ "name": "@kalm/ws", "version": "7.0.0", "description": "WebSocket transport for Kalm", - "main": "bin/ws.js", + "main": "dist/ws.js", "scripts": { - "build": "../../scripts/build.sh ws", - "clean": "../../scripts/cleanup.sh", + "build": "npm run clean && rollup -c rollup.config.mjs", + "clean": "rm -rf ./dist/*", "test": "jest ./tests", "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" }, @@ -28,7 +28,7 @@ "nagle", "websocket" ], - "files": ["bin", "types.d.ts"], + "files": ["dist", "types.d.ts"], "typings": "./types.d.ts", "author": "frederic charette ", "license": "Apache-2.0", @@ -40,12 +40,11 @@ "frederic charette " ], "dependencies": { - "ws": "^8.13.0" + "ws": "^8.18.0" }, "browser": { "http": false, - "https": false, - "events": false + "https": false }, "jest": { "preset": "ts-jest", diff --git a/packages/ws/rollup.config.mjs b/packages/ws/rollup.config.mjs new file mode 100644 index 0000000..2de218b --- /dev/null +++ b/packages/ws/rollup.config.mjs @@ -0,0 +1,16 @@ +import sucrase from '@rollup/plugin-sucrase'; + +export default (async () => ({ + input: 'src/ws.ts', + plugins: [ + sucrase({ + include: ['src/**'], + transforms: ['typescript'] + }), + ], + output: { + file: 'dist/ws.js', + name: 'ws', + format: 'umd', + } +}))(); \ No newline at end of file diff --git a/packages/ws/src/ws.ts b/packages/ws/src/ws.ts index cee99e2..0332b4d 100644 --- a/packages/ws/src/ws.ts +++ b/packages/ws/src/ws.ts @@ -1,36 +1,35 @@ -import events from 'events'; -import { createServer } from 'https'; - const nativeAPIExists = (typeof WebSocket !== 'undefined'); -const WSClient = nativeAPIExists ? WebSocket : require('ws'); -const WSServer = require('ws').Server; type WSConfig = { cert?: string key?: string - agent?: any socketTimeout?: number }; type WSHandle = WebSocket & { - _queue: string[] - _timer: ReturnType + _queue?: string[] + _timer?: ReturnType headers?: any connection?: any _socket?: any }; -function ws({ cert, key, agent, socketTimeout = 30000 }: WSConfig = {}): KalmTransport { - return function socket(params: ClientConfig, emitter: events.EventEmitter): Socket { +export default function ws({ cert, key, socketTimeout = 30000 }: WSConfig = {}): KalmTransport { + return function socket(params: ClientConfig, emitter: NodeJS.EventEmitter): Socket { let listener; - function bind(): void { + async function bind(): Promise { + if (typeof window !== 'undefined') throw new Error('Cannot create a websocket server from the browser'); + + const WSLib = await import('ws'); + if (cert && key) { - const server = createServer({ key, cert }, req => req.socket.end()); - listener = new WSServer({ port: params.port, server }); + const https = await import('https'); + const server = https.createServer({ key, cert }, req => req.socket.end()); + listener = new WSLib.WebSocketServer({ port: params.port, server }); } else { - listener = new WSServer({ port: params.port }); + listener = new WSLib.WebSocketServer({ port: params.port }); } listener.on('connection', soc => emitter.emit('socket', soc)); listener.on('error', err => emitter.emit('error', err)); @@ -49,17 +48,17 @@ function ws({ cert, key, agent, socketTimeout = 30000 }: WSConfig = {}): KalmTra function connect(handle?: WSHandle): WSHandle { const protocol: string = (!!cert && !!key) === true ? 'wss' : 'ws'; - const connection: WSHandle = handle || new WSClient(`${protocol}://${params.host}:${params.port}`, { ...(agent ? { agent } : {}) }); + const connection: WSHandle = handle || new WebSocket(`${protocol}://${params.host}:${params.port}`); connection.binaryType = 'arraybuffer'; const evtType: string = nativeAPIExists ? 'addEventListener' : 'on'; connection._queue = []; connection._timer = null; connection[evtType]('message', (evt) => { - emitter.emit('frame', JSON.parse(evt.data || evt), (evt.data || evt).length); + emitter.emit('frame', { body: JSON.parse(evt.data || evt), payloadBytes: (evt.data || evt).length }); resetTimeout(connection); }); connection[evtType]('error', err => emitter.emit('error', err)); - connection[evtType]('close', () => emitter.emit('disconnect')); + connection[evtType]('close', () => emitter.emit('disconnected')); connection[evtType]('open', () => { emitter.emit('connect', connection); connection._queue.forEach(payload => send(connection, payload)); @@ -91,7 +90,7 @@ function ws({ cert, key, agent, socketTimeout = 30000 }: WSConfig = {}): KalmTra function disconnect(handle) { if (handle) { clearTimeout(handle._timer); - handle.end(); + if (handle.end) handle.end(); handle.close(); } } @@ -106,6 +105,3 @@ function ws({ cert, key, agent, socketTimeout = 30000 }: WSConfig = {}): KalmTra }; }; } - -// Ensures support for modules and requires -module.exports = ws; diff --git a/packages/ws/tests/unit/ws.spec.ts b/packages/ws/tests/unit/ws.spec.ts index fdd70b4..3471fc7 100644 --- a/packages/ws/tests/unit/ws.spec.ts +++ b/packages/ws/tests/unit/ws.spec.ts @@ -1,4 +1,4 @@ -import { EventEmitter } from 'events'; +import { EventEmitter } from '../../../kalm/src/utils/events'; import * as ws from '../../src/ws'; describe('ws transport', () => { @@ -22,7 +22,7 @@ describe('ws transport', () => { describe('when fetching remote', () => { it('should return null values', () => { - expect(socket.remote()).toEqual({ host: null, port: null }); + expect(socket.remote).toEqual({ host: null, port: null }); }); }); }); diff --git a/packages/ws/types.d.ts b/packages/ws/types.d.ts index af24800..ab5aa90 100644 --- a/packages/ws/types.d.ts +++ b/packages/ws/types.d.ts @@ -4,8 +4,6 @@ declare module '@kalm/ws' { cert?: string /** The key file content for a secure socket connection, both this and `cert` must be set */ key?: string - /** A custom agent for the http connection, can be used to set proxies or other connection behaviours */ - agent?: any /** The maximum idle time for the connection before it hangs up (default: 30000) */ socketTimeout?: number } diff --git a/scripts/benchmarks/transports/ipc.js b/scripts/benchmarks/transports/ipc.js index bb59b28..3339968 100644 --- a/scripts/benchmarks/transports/ipc.js +++ b/scripts/benchmarks/transports/ipc.js @@ -25,11 +25,11 @@ function _absorb(err) { function setup(resolve) { server = net.createServer((socket) => { - socket.on('error', _absorb); - socket.on('data', () => socket.write(JSON.stringify(settings.testPayload))); + socket.addEventListener('error', _absorb); + socket.addEventListener('data', () => socket.write(JSON.stringify(settings.testPayload))); }); handbreak = false; - server.on('error', _absorb); + server.addEventListener('error', _absorb); server.listen(`/tmp/app.socket-${settings.port}`, resolve); } @@ -53,8 +53,8 @@ function step(resolve) { if (handbreak) return; if (!client) { client = net.connect(`/tmp/app.socket-${settings.port}`); - client.on('error', _absorb); - client.on('data', () => count++); + client.addEventListener('error', _absorb); + client.addEventListener('data', () => count++); } client.write(JSON.stringify(settings.testPayload)); diff --git a/scripts/benchmarks/transports/kalm.js b/scripts/benchmarks/transports/kalm.js index dbced59..be67c06 100644 --- a/scripts/benchmarks/transports/kalm.js +++ b/scripts/benchmarks/transports/kalm.js @@ -5,13 +5,13 @@ /* Requires ------------------------------------------------------------------ */ const settings = require('../settings'); -const Kalm = require('../../../packages/kalm/bin/kalm'); +const Kalm = require('../../../packages/kalm/dist/kalm'); const transports = { - ipc: require('../../../packages/ipc/bin/ipc'), - tcp: require('../../../packages/tcp/bin/tcp'), - udp: require('../../../packages/udp/bin/udp'), - ws: require('../../../packages/ws/bin/ws'), + ipc: require('../../../packages/ipc/dist/ipc'), + tcp: require('../../../packages/tcp/dist/tcp'), + udp: require('../../../packages/udp/dist/udp'), + ws: require('../../../packages/ws/dist/ws'), }; /* Local variables ----------------------------------------------------------- */ @@ -33,11 +33,11 @@ function setup(resolve) { routine: Kalm.routines[settings.routine[0]](settings.routine[1]), }); - server.on('connection', (c) => { + server.addEventListener('connection', (c) => { c.subscribe(settings.testChannel, msg => c.write(settings.testChannel, msg)); }); - server.on('error', (e) => { + server.addEventListener('error', (e) => { console.error('Server error:', e); }); @@ -71,7 +71,7 @@ function step(resolve) { count++; }); - client.on('error', (e) => { + client.addEventListener('error', (e) => { console.error('Client error:', e); }); } diff --git a/scripts/benchmarks/transports/socketio.js b/scripts/benchmarks/transports/socketio.js index 65dca1d..0b90cda 100644 --- a/scripts/benchmarks/transports/socketio.js +++ b/scripts/benchmarks/transports/socketio.js @@ -28,10 +28,10 @@ function _absorb(err) { function setup(resolve) { server = io(); handbreak = false; - server.on('connection', (socket) => { - socket.on('data', () => socket.emit('data', JSON.stringify(settings.testPayload))); + server.addEventListener('connection', (socket) => { + socket.addEventListener('data', () => socket.emit('data', JSON.stringify(settings.testPayload))); }); - server.on('error', _absorb); + server.addEventListener('error', _absorb); server.listen(http.createServer().listen(settings.port, '0.0.0.0')); setTimeout(resolve, 10); } @@ -56,8 +56,8 @@ function step(resolve) { if (handbreak) return; if (!client) { client = ioclient(`http://0.0.0.0:${settings.port}`); - client.on('error', _absorb); - client.on('data', () => count++); + client.addEventListener('error', _absorb); + client.addEventListener('data', () => count++); } client.emit('data', JSON.stringify(settings.testPayload)); diff --git a/scripts/benchmarks/transports/tcp.js b/scripts/benchmarks/transports/tcp.js index 2b0765d..d182ba4 100644 --- a/scripts/benchmarks/transports/tcp.js +++ b/scripts/benchmarks/transports/tcp.js @@ -23,11 +23,11 @@ function _absorb(err) { function setup(resolve) { server = net.createServer((socket) => { - socket.on('data', () => socket.write(JSON.stringify(settings.testPayload))); - socket.on('error', _absorb); + socket.addEventListener('data', () => socket.write(JSON.stringify(settings.testPayload))); + socket.addEventListener('error', _absorb); }); handbreak = false; - server.on('error', _absorb); + server.addEventListener('error', _absorb); server.listen(settings.port, resolve); } @@ -51,8 +51,8 @@ function step(resolve) { if (handbreak) return; if (!client) { client = net.connect(settings.port, '0.0.0.0'); - client.on('error', _absorb); - client.on('data', () => count++); + client.addEventListener('error', _absorb); + client.addEventListener('data', () => count++); } client.write(JSON.stringify(settings.testPayload)); diff --git a/scripts/benchmarks/transports/udp.js b/scripts/benchmarks/transports/udp.js index 1ff0e69..86599d5 100644 --- a/scripts/benchmarks/transports/udp.js +++ b/scripts/benchmarks/transports/udp.js @@ -24,11 +24,11 @@ function _absorb(err) { function setup(resolve) { server = dgram.createSocket('udp4'); - server.on('message', () => { + server.addEventListener('message', () => { server.send(Buffer.from(JSON.stringify(settings.testPayload)), 1111, '0.0.0.0'); }); handbreak = false; - server.on('error', _absorb); + server.addEventListener('error', _absorb); server.bind(settings.port, '0.0.0.0'); resolve(); } @@ -50,8 +50,8 @@ function step(resolve) { if (handbreak) return; if (!client) { client = dgram.createSocket('udp4'); - client.on('error', _absorb); - client.on('message', () => count++); + client.addEventListener('error', _absorb); + client.addEventListener('message', () => count++); client.bind(1111, '0.0.0.0'); } diff --git a/scripts/build.sh b/scripts/build.sh deleted file mode 100755 index 4de4644..0000000 --- a/scripts/build.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -echo "Building" $1 -../../scripts/cleanup.sh -cp ../../tsconfig.json ./tsconfig.json -../../node_modules/typescript/bin/tsc --outDir ./bin >/dev/null -echo "Build completed, cleaning up" -rm -rf ./tsconfig.json -rm -rf ./dist \ No newline at end of file diff --git a/scripts/cleanup.sh b/scripts/cleanup.sh deleted file mode 100755 index d60a5ec..0000000 --- a/scripts/cleanup.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -mkdir -p ./bin -rm -rf ./bin/* -rm -rf ./tsconfig.json \ No newline at end of file diff --git a/tests/integration/frame.spec.ts b/tests/integration/frame.spec.ts index 1c349b6..e06860a 100644 --- a/tests/integration/frame.spec.ts +++ b/tests/integration/frame.spec.ts @@ -1,7 +1,7 @@ /* Requires ------------------------------------------------------------------ */ -import { connect, listen } from '../../packages/kalm/bin/kalm'; -import ipc from '../../packages/ipc/bin/ipc'; +import { connect, listen } from '../../packages/kalm/dist/kalm'; +import ipc from '../../packages/ipc/dist/ipc'; /* Suite -------------------------------------------------------------------- */ @@ -26,7 +26,7 @@ describe('Frame', () => { it('Should have a well structured frame reference', (done) => { const payload = { foo: 'bar' }; - server.on('connection', (c) => { + server.addEventListener('connection', (c) => { c.subscribe('test', (data, meta) => { expect(meta).toEqual({ client: c, @@ -41,12 +41,12 @@ describe('Frame', () => { done(); }); }); - server.on('error', (e) => { + server.addEventListener('error', (e) => { throw new Error(e); }); const client = connect({ transport: ipc() }); - client.on('error', (e) => { + client.addEventListener('error', (e) => { throw new Error(e); }); client.write('test', payload); diff --git a/tests/integration/import.spec.ts b/tests/integration/import.spec.ts index 55b5d0a..372bae8 100644 --- a/tests/integration/import.spec.ts +++ b/tests/integration/import.spec.ts @@ -1,5 +1,5 @@ -import { connect, routines } from '../../packages/kalm/bin/kalm'; -import ipc from '../../packages/ipc/bin/ipc'; +import { connect, routines } from '../../packages/kalm/dist/kalm'; +import ipc from '../../packages/ipc/dist/ipc'; describe('Testing module loading patterns', () => { it('Should support import syntax', () => { @@ -9,14 +9,14 @@ describe('Testing module loading patterns', () => { }); it('Should support require syntax for kalm core', () => { - expect(typeof require('../../packages/kalm/bin/kalm').connect).toBe('function'); + expect(typeof require('../../packages/kalm/dist/kalm').connect).toBe('function'); }); it('Should support require syntax for kalm routines', () => { - expect(require('../../packages/kalm/bin/kalm').routines).toHaveProperty('realtime'); + expect(require('../../packages/kalm/dist/kalm').routines).toHaveProperty('realtime'); }); it('Should support require syntax for kalm transports', () => { - expect(typeof require('../../packages/ipc/bin/ipc')).toBe('function'); + expect(typeof require('../../packages/ipc/dist/ipc')).toBe('function'); }); }); diff --git a/tests/integration/index.spec.ts b/tests/integration/index.spec.ts index fbbd573..4ed246d 100644 --- a/tests/integration/index.spec.ts +++ b/tests/integration/index.spec.ts @@ -4,7 +4,7 @@ /* Requires ------------------------------------------------------------------ */ -import { connect, listen } from '../../packages/kalm/bin/kalm'; +import { connect, listen } from '../../packages/kalm/dist/kalm'; /* Suite -------------------------------------------------------------------- */ @@ -12,7 +12,7 @@ describe('Integration tests', () => { ['ipc', 'tcp', 'udp', 'ws'].forEach((transport) => { describe(`Testing ${transport} transport`, () => { let server; - const soc = require(`../../packages/${transport}/bin/${transport}`)(); /* eslint-disable-line */ + const soc = require(`../../packages/${transport}/dist/${transport}`)(); /* eslint-disable-line */ /* --- Setup --- */ @@ -34,18 +34,18 @@ describe('Integration tests', () => { it(`should work with ${transport}`, (done) => { const payload = { foo: 'bar' }; - server.on('connection', (c) => { + server.addEventListener('connection', (c) => { c.subscribe('test', (data) => { expect(data).toEqual(payload); done(); }); }); - server.on('error', (e) => { + server.addEventListener('error', (e) => { throw new Error(e); }); const client = connect({ transport: soc }); - client.on('error', (e) => { + client.addEventListener('error', (e) => { throw new Error(e); }); client.write('test', payload); @@ -53,18 +53,18 @@ describe('Integration tests', () => { it(`should handle foreign characters with ${transport}`, (done) => { const payload = { foo: '한자' }; - server.on('connection', (c) => { + server.addEventListener('connection', (c) => { c.subscribe('test', (data) => { expect(data).toEqual(payload); done(); }); }); - server.on('error', (e) => { + server.addEventListener('error', (e) => { throw new Error(e); }); const client = connect({ transport: soc }); - client.on('error', (e) => { + client.addEventListener('error', (e) => { throw new Error(e); }); client.write('test', payload); @@ -76,18 +76,18 @@ describe('Integration tests', () => { largePayload.push({ foo: 'bar' }); } - server.on('connection', (c) => { + server.addEventListener('connection', (c) => { c.subscribe('test.large', (data) => { expect(data).toEqual(largePayload); done(); }); }); - server.on('error', (e) => { + server.addEventListener('error', (e) => { throw new Error(e); }); const client = connect({ transport: soc }); - client.on('error', (e) => { + client.addEventListener('error', (e) => { if (transport === 'udp') { expect(e.message).toEqual('UDP Cannot send packets larger than 16384 bytes, tried to send 28715 bytes'); return done(); @@ -99,7 +99,7 @@ describe('Integration tests', () => { it('should not trigger for unsubscribed channels', (done) => { const payload = { foo: 'bar' }; - server.on('connection', (c) => { + server.addEventListener('connection', (c) => { c.subscribe('test', () => { // Throw on purpose expect(false).toBe(true); @@ -108,12 +108,12 @@ describe('Integration tests', () => { c.unsubscribe('test'); }); - server.on('error', (e) => { + server.addEventListener('error', (e) => { throw new Error(e); }); const client = connect({ transport: soc }); - client.on('error', (e) => { + client.addEventListener('error', (e) => { throw new Error(e); }); setTimeout(() => client.write('test', payload), 100); diff --git a/tsconfig.json b/tsconfig.json index b0ee6b0..d580c49 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "module": "CommonJS", + "module": "ESNext", + "moduleResolution": "node", "esModuleInterop": true, "allowSyntheticDefaultImports": true, "allowUmdGlobalAccess": true, diff --git a/packages/kalm/types.d.ts b/types.d.ts similarity index 95% rename from packages/kalm/types.d.ts rename to types.d.ts index 415ebbb..1f71082 100644 --- a/packages/kalm/types.d.ts +++ b/types.d.ts @@ -46,6 +46,7 @@ interface ServerEventMap { interface ClientEventMap { connect: (client: Client) => void disconnect: () => void + disconnected: () => void // Internal use only frame: (frame: RawFrame) => void error: (error: Error) => void } @@ -82,6 +83,8 @@ interface Server { once(event: k, listener: ServerEventMap[k] | Function): this removeListener(event: k, listener: ServerEventMap[k] | Function): this off(event: k, listener: ServerEventMap[k] | Function): this + addEventListener(event: k, listener: (evt?: Event) => void): this + removeEventListener(event: k, listener: (evt?: Event) => void): this } /** @@ -137,6 +140,8 @@ interface Client { once(event: k, listener: ClientEventMap[k] | Function): this removeListener(event: k, listener: ClientEventMap[k] | Function): this off(event: k, listener: ClientEventMap[k] | Function): this + addEventListener(event: k, listener: (evt?: Event) => void): this + removeEventListener(event: k, listener: (evt?: Event) => void): this } type Serializable = Buffer | object | string | null; @@ -179,8 +184,6 @@ interface Socket { send: (handle: any, message: RawFrame) => void /** The command to disconnect a Client */ disconnect: (handle: any) => void - /** Exclusive to WebRTC transport, allows the connection with a new Peer */ - negociate?: (params: { peer: Peer }) => Promise } /** From 37009c8084243fd7bd109dba0830f893665eb568 Mon Sep 17 00:00:00 2001 From: Frederic Charette Date: Wed, 30 Jul 2025 13:39:26 -0400 Subject: [PATCH 03/11] fixed linting, types, tests --- CHANGELOG.md | 3 +- eslint.config.mjs | 2 + examples/binary_compression/client.ts | 8 ++-- examples/binary_compression/server.ts | 8 ++-- examples/browser_peer_to_peer/README.md | 4 ++ examples/browser_peer_to_peer/index.html | 4 ++ examples/distributed_pub_sub/README.md | 6 +++ examples/distributed_pub_sub/client.js | 2 +- examples/typescript_websocket/client.ts | 6 +-- examples/typescript_websocket/server.ts | 6 +-- package.json | 10 ++-- packages/ipc/package.json | 5 +- packages/ipc/rollup.config.mjs | 28 +++++------ packages/ipc/tests/unit/ipc.spec.ts | 2 +- packages/kalm/README.md | 12 ++--- packages/kalm/package.json | 5 +- packages/kalm/rollup.config.mjs | 36 +++++++------- packages/kalm/src/components/client.ts | 10 ++-- packages/kalm/src/components/server.ts | 2 +- packages/kalm/src/utils/events.ts | 38 +++++++-------- .../kalm/tests/unit/components/client.spec.ts | 2 +- .../tests/unit/components/provider.spec.ts | 2 +- .../kalm/tests/unit/routines/dynamic.spec.ts | 2 +- .../kalm/tests/unit/routines/realtime.spec.ts | 2 +- .../kalm/tests/unit/routines/tick.spec.ts | 2 +- packages/kalm/tests/unit/utils/logger.spec.ts | 2 +- packages/kalm/tests/unit/utils/parser.spec.ts | 21 -------- packages/tcp/package.json | 5 +- packages/tcp/rollup.config.mjs | 28 +++++------ packages/tcp/src/tcp.ts | 2 +- packages/tcp/tests/unit/tcp.spec.ts | 2 +- packages/udp/package.json | 5 +- packages/udp/rollup.config.mjs | 28 +++++------ packages/udp/tests/unit/udp.spec.ts | 2 +- packages/ws/package.json | 5 +- packages/ws/rollup.config.mjs | 26 +++++----- packages/ws/src/ws.ts | 4 +- packages/ws/tests/unit/ws.spec.ts | 2 +- scripts/benchmarks/{index.js => index.ts} | 0 .../benchmarks/{settings.js => settings.ts} | 0 .../benchmarks/transports/{ipc.js => ipc.ts} | 10 ++-- .../transports/{kalm.js => kalm.ts} | 6 +-- .../transports/{socketio.js => socketio.ts} | 10 ++-- .../benchmarks/transports/{tcp.js => tcp.ts} | 10 ++-- .../benchmarks/transports/{udp.js => udp.ts} | 8 ++-- tests/integration/frame.spec.ts | 6 +-- tests/integration/import.spec.ts | 22 --------- tests/integration/index.spec.ts | 24 +++++----- types.d.ts | 48 ++++++++++--------- 49 files changed, 228 insertions(+), 255 deletions(-) delete mode 100644 packages/kalm/tests/unit/utils/parser.spec.ts rename scripts/benchmarks/{index.js => index.ts} (100%) rename scripts/benchmarks/{settings.js => settings.ts} (100%) rename scripts/benchmarks/transports/{ipc.js => ipc.ts} (81%) rename scripts/benchmarks/transports/{kalm.js => kalm.ts} (93%) rename scripts/benchmarks/transports/{socketio.js => socketio.ts} (81%) rename scripts/benchmarks/transports/{tcp.js => tcp.ts} (80%) rename scripts/benchmarks/transports/{udp.js => udp.ts} (87%) delete mode 100644 tests/integration/import.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 98efce8..72bda01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [v8.0.0] - 2025-07-26 +## [v8.0.0] - 2025-07-30 commit [#](https://github.com/kalm/kalm.js/commits) @@ -18,6 +18,7 @@ commit [#](https://github.com/kalm/kalm.js/commits) - Migrated the underlying Node EventEmitter to the cross-platform EventTarget system. A translation layer should keep end-user code intact. - Fixed server connections not getting cleaned up - Removed empty channels from frame payloads, saving bandwidth +- Changed the subscribe handler's second argument name from `frame` to `context`, to reduce confusion with its nested `frame` property. ## [v7.0.0] - 2023-03-17 diff --git a/eslint.config.mjs b/eslint.config.mjs index c9063a0..7db5522 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -13,7 +13,9 @@ export default tseslint.config( rules: { '@stylistic/semi': [2, 'always'], '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-require-imports': 1, 'jest/no-done-callback': 0, + 'jest/no-conditional-expect': 0, }, }, globalIgnores(['**/bin', '**/dist', '**/*.js']), diff --git a/examples/binary_compression/client.ts b/examples/binary_compression/client.ts index 98d980a..3de6df7 100644 --- a/examples/binary_compression/client.ts +++ b/examples/binary_compression/client.ts @@ -33,13 +33,13 @@ client.on('connect', () => { /** * Once a client has connected, we subscribe to messages sent on the "foo" channel. */ - client.subscribe('foo', (body: MyCustomPayload, frame) => { + client.subscribe('foo', (body: Buffer, context) => { /** * When we receive a message on the foo channel, we also receive information about the frame and context. * We'll need to decompress the buffer to read it. * * body: - * frame: { + * context: { * client: , * frame: { * channel: "foo", @@ -50,8 +50,8 @@ client.on('connect', () => { * } * } */ - uncompress(body, (decompressedMessage) => { - console.log('Server event', decompressedMessage, frame); + uncompress(body).then((decompressedMessage) => { + console.log('Server event', JSON.parse(decompressedMessage.toString()) as MyCustomPayload, context); }); }); diff --git a/examples/binary_compression/server.ts b/examples/binary_compression/server.ts index e39e2a8..75ec28f 100644 --- a/examples/binary_compression/server.ts +++ b/examples/binary_compression/server.ts @@ -34,13 +34,13 @@ provider.on('connection', (client) => { /** * Once a client has connected, we subscribe to messages sent on the "foo" channel. */ - client.subscribe('foo', (body: MyCustomPayload, frame) => { + client.subscribe('foo', (body: Buffer, context) => { /** * When we receive a message on the foo channel, we also receive information about the frame and context. * We'll need to decompress the buffer to read it. * * body: - * frame: { + * context: { * client: , * frame: { * channel: "foo", @@ -51,8 +51,8 @@ provider.on('connection', (client) => { * } * } */ - uncompress(body, (decompressedMessage) => { - console.log('Client event', decompressedMessage, frame); + uncompress(body).then((decompressedMessage) => { + console.log('Client event', JSON.parse(decompressedMessage.toString()) as MyCustomPayload, context); }); }); diff --git a/examples/browser_peer_to_peer/README.md b/examples/browser_peer_to_peer/README.md index 5d4c5a2..5105b46 100644 --- a/examples/browser_peer_to_peer/README.md +++ b/examples/browser_peer_to_peer/README.md @@ -37,3 +37,7 @@ Open as many browser tabs as desired and navigate to `http://localhost:8000/exam Peers should appear in the list and give you the option to connect to them. +# Diagram +

+ peer-to-peer diagram +

\ No newline at end of file diff --git a/examples/browser_peer_to_peer/index.html b/examples/browser_peer_to_peer/index.html index c596735..796f5f3 100644 --- a/examples/browser_peer_to_peer/index.html +++ b/examples/browser_peer_to_peer/index.html @@ -133,6 +133,10 @@ } function printPeers() { + if (peers.length === 0) { + return document.getElementById('clientList').innerHTML = `No peers available`; + } + // Pretty-print document.getElementById('clientList').innerHTML = ` ${peers.map((p, i) => `${p.name} ${p.status < 1 ? `` : ''}`).join('')} diff --git a/examples/distributed_pub_sub/README.md b/examples/distributed_pub_sub/README.md index d8127b3..8eabba7 100644 --- a/examples/distributed_pub_sub/README.md +++ b/examples/distributed_pub_sub/README.md @@ -40,3 +40,9 @@ node ./client.js 0.0.0.0 13002 The clients should connect to the server and send an "hello world!" message using the external channel. In turn, the servers will forward this message to the others via an internal channel before each broadcasts it back to its connected clients. + + +# Diagram +

+ peer-to-peer diagram +

\ No newline at end of file diff --git a/examples/distributed_pub_sub/client.js b/examples/distributed_pub_sub/client.js index 1469f4f..2aef6d2 100644 --- a/examples/distributed_pub_sub/client.js +++ b/examples/distributed_pub_sub/client.js @@ -30,7 +30,7 @@ client.on('connect', () => { /** * Subscribes to messages on a channel named r.evt (Response Event). */ - client.subscribe('r.evt', (body, frame) => { + client.subscribe('r.evt', (body) => { /** * We'll ignore our own messages */ diff --git a/examples/typescript_websocket/client.ts b/examples/typescript_websocket/client.ts index e34d1eb..d08f121 100644 --- a/examples/typescript_websocket/client.ts +++ b/examples/typescript_websocket/client.ts @@ -29,12 +29,12 @@ client.on('connect', () => { /** * Once a client has connected, we subscribe to messages sent on the "foo" channel. */ - client.subscribe('foo', (body: MyCustomPayload, frame) => { + client.subscribe('foo', (body: MyCustomPayload, context) => { /** * When we receive a message on the foo channel, we also receive information about the frame and context. * * body: { message: "hello from the server!" } - * frame: { + * context: { * client: , * frame: { * channel: "foo", @@ -45,7 +45,7 @@ client.on('connect', () => { * } * } */ - console.log('Server event', body, frame); + console.log('Server event', body, context); }); /** diff --git a/examples/typescript_websocket/server.ts b/examples/typescript_websocket/server.ts index 801e2be..e00a923 100644 --- a/examples/typescript_websocket/server.ts +++ b/examples/typescript_websocket/server.ts @@ -30,12 +30,12 @@ provider.on('connection', (client) => { /** * Once a client has connected, we subscribe to messages sent on the "foo" channel. */ - client.subscribe('foo', (body: MyCustomPayload, frame) => { + client.subscribe('foo', (body: MyCustomPayload, context) => { /** * When we receive a message on the foo channel, we also receive information about the frame and context. * * body: { message: "hello world!" } - * frame: { + * context: { * client: , * frame: { * channel: "foo", @@ -46,7 +46,7 @@ provider.on('connection', (client) => { * } * } */ - console.log('Client event', body, frame); + console.log('Client event', body, context); }); /** diff --git a/package.json b/package.json index 88fa976..44f19c0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "kalm", + "name": "kalm.js", "private": true, - "version": "7.1.0", + "version": "8.0.0", "description": "The socket optimizer", "main": "packages/kalm/dist/kalm.js", "scripts": { @@ -11,7 +11,7 @@ "test:integration": "jest ./tests/integration --forceExit", "build": "npm run build --workspaces --if-present", "clean": "npm run clean --workspaces --if-present", - "bench": "node ./scripts/benchmarks" + "bench": "ts-node --transpile-only ./scripts/benchmarks" }, "funding": { "type": "Open Collective", @@ -47,8 +47,7 @@ "^.+\\.tsx?$": [ "ts-jest", { - "diagnostics": false, - "isolatedModules": true + "diagnostics": false } ] } @@ -71,6 +70,7 @@ "socket.io": "^4.8.0", "socket.io-client": "^4.8.0", "ts-jest": "^29.4.0", + "ts-node": "10.9.2", "typescript": "^5.8.0", "typescript-eslint": "^8.38.0", "ws": "^8.18.3" diff --git a/packages/ipc/package.json b/packages/ipc/package.json index 298844d..64d0247 100644 --- a/packages/ipc/package.json +++ b/packages/ipc/package.json @@ -1,6 +1,6 @@ { "name": "@kalm/ipc", - "version": "7.0.0", + "version": "8.0.0", "description": "IPC transport for Kalm", "main": "dist/ipc.js", "scripts": { @@ -49,8 +49,7 @@ "transform": { "^.+\\.tsx?$": [ "ts-jest", { - "diagnostics": false, - "isolatedModules": true + "diagnostics": false }] } } diff --git a/packages/ipc/rollup.config.mjs b/packages/ipc/rollup.config.mjs index da14dd4..f1bc732 100644 --- a/packages/ipc/rollup.config.mjs +++ b/packages/ipc/rollup.config.mjs @@ -1,17 +1,17 @@ import sucrase from '@rollup/plugin-sucrase'; export default { - input: 'src/ipc.ts', - plugins: [ - sucrase({ - include: ['src/**'], - transforms: ['typescript'] - }), - ], - external: ['net'], - output: { - file: 'dist/ipc.js', - name: 'ipc', - format: 'umd' - } -}; \ No newline at end of file + input: 'src/ipc.ts', + plugins: [ + sucrase({ + include: ['src/**'], + transforms: ['typescript'], + }), + ], + external: ['net'], + output: { + file: 'dist/ipc.js', + name: 'ipc', + format: 'umd', + }, +}; diff --git a/packages/ipc/tests/unit/ipc.spec.ts b/packages/ipc/tests/unit/ipc.spec.ts index 4b43b9d..7a2aa12 100644 --- a/packages/ipc/tests/unit/ipc.spec.ts +++ b/packages/ipc/tests/unit/ipc.spec.ts @@ -22,7 +22,7 @@ describe('IPC transport', () => { describe('when fetching remote', () => { it('should return null values', () => { - expect(socket.remote).toEqual({ host: null, port: null }); + expect(socket.remote()).toEqual({ host: null, port: null }); }); }); }); diff --git a/packages/kalm/README.md b/packages/kalm/README.md index 51ba75b..bf09b60 100644 --- a/packages/kalm/README.md +++ b/packages/kalm/README.md @@ -21,12 +21,12 @@ - Flexible and extensible, create your own transports and buffering strategies - Can be used between servers or in the **browser** - Lower resource footprint and **better throughtput** than plain sockets -- **Zero dependencies** and can be bundled down to ~5kb! +- **Zero dependencies** and can be bundled down to ~6kb! ## Performance -perf +perf The performance gain comes from buffering packets before sending them- eventually sending batches instead of individual packages. The more traffic getting processed, the better the improvement. Many strategies are offered as routines. You can read more about the packet buffering algorithm [here](https://en.wikipedia.org/wiki/Nagle%27s_algorithm) @@ -56,10 +56,10 @@ const server = kalm.listen({ }); server.on('connection', (client) => { - client.subscribe('channel1', (body, frame) => { + client.subscribe('channel1', (body, context) => { // When receiving messages from this client on "channel1" console.log(body) // - console.log(frame) // + console.log(context) // }); // Sends a message to all clients on "channel2" @@ -81,10 +81,10 @@ const client = kalm.connect({ }); client.on('connect', () => { - client.subscribe('channel1', (body, frame) => { + client.subscribe('channel1', (body, context) => { // When receiving messages from the server on "channel1" console.log(body); // - console.log(frame); // + console.log(context); // }); // Sends a message to the server on "channel2" diff --git a/packages/kalm/package.json b/packages/kalm/package.json index 85c1af6..bd8e6ff 100644 --- a/packages/kalm/package.json +++ b/packages/kalm/package.json @@ -1,6 +1,6 @@ { "name": "kalm", - "version": "7.0.0", + "version": "8.0.0", "description": "The socket optimizer", "main": "dist/kalm.js", "scripts": { @@ -53,8 +53,7 @@ "transform": { "^.+\\.tsx?$": [ "ts-jest", { - "diagnostics": false, - "isolatedModules": true + "diagnostics": false }] } } diff --git a/packages/kalm/rollup.config.mjs b/packages/kalm/rollup.config.mjs index f1b4a32..566ef0c 100644 --- a/packages/kalm/rollup.config.mjs +++ b/packages/kalm/rollup.config.mjs @@ -2,21 +2,21 @@ import resolve from '@rollup/plugin-node-resolve'; import sucrase from '@rollup/plugin-sucrase'; export default { - input: 'src/kalm.ts', - plugins: [ - resolve({ - extensions: ['.ts'], - preferBuiltins: true, - browser: true, - }), - sucrase({ - include: ['src/**'], - transforms: ['typescript'] - }), - ], - output: { - file: 'dist/kalm.js', - name: 'kalm', - format: 'umd' - } -}; \ No newline at end of file + input: 'src/kalm.ts', + plugins: [ + resolve({ + extensions: ['.ts'], + preferBuiltins: true, + browser: true, + }), + sucrase({ + include: ['src/**'], + transforms: ['typescript'], + }), + ], + output: { + file: 'dist/kalm.js', + name: 'kalm', + format: 'umd', + }, +}; diff --git a/packages/kalm/src/components/client.ts b/packages/kalm/src/components/client.ts index a0b1e3a..8485900 100644 --- a/packages/kalm/src/components/client.ts +++ b/packages/kalm/src/components/client.ts @@ -7,7 +7,7 @@ export function Client(params: ClientConfig, emitter: EventEmitter, socket?: any type Channel = { name: string packets: any[] - handlers: Function[] + handlers: ((packet: any, context: Context) => void)[] }; const channels: { [channel: string]: Channel } = {}; @@ -39,7 +39,9 @@ export function Client(params: ClientConfig, emitter: EventEmitter, socket?: any return frame; }, { frameId, channels: {} })); - getChannels().forEach((channelName) => { channels[channelName].packets.length = 0; }); + for (const channelName in channels) { + channels[channelName].packets.length = 0; + } } function _handleConnect(): void { @@ -96,11 +98,11 @@ export function Client(params: ClientConfig, emitter: EventEmitter, socket?: any if (connected > 1) setTimeout(() => transport.disconnect(socket), 0); } - function subscribe(channelName: string, handler: (msg: any, frame: Frame) => void): void { + function subscribe(channelName: string, handler: (msg: any, context: Context) => void): void { _resolveChannel(channelName).handlers.push(handler); } - function unsubscribe(channelName: string, handler?: (msg: any, frame: Frame) => void): void { + function unsubscribe(channelName: string, handler?: (msg: any, context: Context) => void): void { if (!(channelName in channels)) return; if (handler) { const index = channels[channelName].handlers.indexOf(handler); diff --git a/packages/kalm/src/components/server.ts b/packages/kalm/src/components/server.ts index ac88829..576b078 100644 --- a/packages/kalm/src/components/server.ts +++ b/packages/kalm/src/components/server.ts @@ -44,7 +44,7 @@ export function Server(params: ClientConfig, emitter: EventEmitter): Server { log(`connection from ${origin.host}:${origin.port}`); client.on('disconnected', () => { - const clientIndex = connections.findIndex((o) => o == client); + const clientIndex = connections.findIndex(o => o == client); if (clientIndex > -1) connections.splice(clientIndex, 1); }); } diff --git a/packages/kalm/src/utils/events.ts b/packages/kalm/src/utils/events.ts index c9a54b1..008e8dc 100644 --- a/packages/kalm/src/utils/events.ts +++ b/packages/kalm/src/utils/events.ts @@ -1,29 +1,29 @@ function spread(handler, evt) { - return handler(evt.detail); + return handler(evt.detail); } export class EventEmitter extends EventTarget { - constructor() { - super(); - } + constructor() { + super(); + } - on(eventName, handler) { - this.addEventListener(eventName, spread.bind(null, handler)); - } + on(eventName, handler) { + this.addEventListener(eventName, spread.bind(null, handler)); + } - off(eventName, handler) { - this.removeEventListener(eventName, spread.bind(null, handler)); - } + off(eventName, handler) { + this.removeEventListener(eventName, spread.bind(null, handler)); + } - once(eventName, handler) { - this.addEventListener(eventName, spread.bind(null, handler), { once: true }); - } + once(eventName, handler) { + this.addEventListener(eventName, spread.bind(null, handler), { once: true }); + } - removeListener(eventName, handler) { - this.removeEventListener(eventName, spread.bind(null, handler)); - } + removeListener(eventName, handler) { + this.removeEventListener(eventName, spread.bind(null, handler)); + } - emit(eventName, detail?) { - this.dispatchEvent(new CustomEvent(eventName, { detail })) - } + emit(eventName, detail?) { + this.dispatchEvent(new CustomEvent(eventName, { detail })); + } } diff --git a/packages/kalm/tests/unit/components/client.spec.ts b/packages/kalm/tests/unit/components/client.spec.ts index 7e7b7a5..9571d1a 100644 --- a/packages/kalm/tests/unit/components/client.spec.ts +++ b/packages/kalm/tests/unit/components/client.spec.ts @@ -1,5 +1,5 @@ import { Client } from '../../../src/components/client'; describe('Client', () => { - it('TODO', () => { expect(Client).not.toBeUndefined(); }); + it('TODO', () => expect(Client).not.toBeUndefined()); }); diff --git a/packages/kalm/tests/unit/components/provider.spec.ts b/packages/kalm/tests/unit/components/provider.spec.ts index 660367b..cfd18a2 100644 --- a/packages/kalm/tests/unit/components/provider.spec.ts +++ b/packages/kalm/tests/unit/components/provider.spec.ts @@ -1,5 +1,5 @@ import { Server } from '../../../src/components/server'; describe('Provider', () => { - it('TODO', () => { expect(Server).not.toBeUndefined(); }); + it('TODO', () => expect(Server).not.toBeUndefined()); }); diff --git a/packages/kalm/tests/unit/routines/dynamic.spec.ts b/packages/kalm/tests/unit/routines/dynamic.spec.ts index 87d04ca..5774730 100644 --- a/packages/kalm/tests/unit/routines/dynamic.spec.ts +++ b/packages/kalm/tests/unit/routines/dynamic.spec.ts @@ -1,5 +1,5 @@ import { dynamic } from '../../../src/routines/dynamic'; describe('Dynamic routine', () => { - it('TODO', () => { expect(dynamic).not.toBeUndefined(); }); + it('TODO', () => expect(dynamic).not.toBeUndefined()); }); diff --git a/packages/kalm/tests/unit/routines/realtime.spec.ts b/packages/kalm/tests/unit/routines/realtime.spec.ts index 1d50d6b..ab6a8c1 100644 --- a/packages/kalm/tests/unit/routines/realtime.spec.ts +++ b/packages/kalm/tests/unit/routines/realtime.spec.ts @@ -1,5 +1,5 @@ import { realtime } from '../../../src/routines/realtime'; describe('Realtime routine', () => { - it('TODO', () => { expect(realtime).not.toBeUndefined(); }); + it('TODO', () => expect(realtime).not.toBeUndefined()); }); diff --git a/packages/kalm/tests/unit/routines/tick.spec.ts b/packages/kalm/tests/unit/routines/tick.spec.ts index 03dc147..db2d695 100644 --- a/packages/kalm/tests/unit/routines/tick.spec.ts +++ b/packages/kalm/tests/unit/routines/tick.spec.ts @@ -1,5 +1,5 @@ import { tick } from '../../../src/routines/tick'; describe('Tick routine', () => { - it('TODO', () => { expect(tick).not.toBeUndefined(); }); + it('TODO', () => expect(tick).not.toBeUndefined()); }); diff --git a/packages/kalm/tests/unit/utils/logger.spec.ts b/packages/kalm/tests/unit/utils/logger.spec.ts index f1db54f..2f74c0f 100644 --- a/packages/kalm/tests/unit/utils/logger.spec.ts +++ b/packages/kalm/tests/unit/utils/logger.spec.ts @@ -1,5 +1,5 @@ import { log } from '../../../src/utils/logger'; describe('Logger util', () => { - it('TODO', () => { expect(log).not.toBeUndefined(); }); + it('TODO', () => expect(log).not.toBeUndefined()); }); diff --git a/packages/kalm/tests/unit/utils/parser.spec.ts b/packages/kalm/tests/unit/utils/parser.spec.ts deleted file mode 100644 index 935680c..0000000 --- a/packages/kalm/tests/unit/utils/parser.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -describe('Parser util', () => { - describe('serialize', () => { - it('should serialize single message frames', () => { - - }); - - it('should serialize multi message frames', () => { - - }); - }); - - describe('deserialize', () => { - it('should deserialize single message frames', () => { - - }); - - it('should deserialize multi message frames', () => { - - }); - }); -}); diff --git a/packages/tcp/package.json b/packages/tcp/package.json index f399ed1..d20c756 100644 --- a/packages/tcp/package.json +++ b/packages/tcp/package.json @@ -1,6 +1,6 @@ { "name": "@kalm/tcp", - "version": "7.0.0", + "version": "8.0.0", "description": "TCP transport for Kalm", "main": "dist/tcp.js", "scripts": { @@ -48,8 +48,7 @@ "transform": { "^.+\\.tsx?$": [ "ts-jest", { - "diagnostics": false, - "isolatedModules": true + "diagnostics": false }] } } diff --git a/packages/tcp/rollup.config.mjs b/packages/tcp/rollup.config.mjs index 937b531..2749c2a 100644 --- a/packages/tcp/rollup.config.mjs +++ b/packages/tcp/rollup.config.mjs @@ -1,17 +1,17 @@ import sucrase from '@rollup/plugin-sucrase'; export default { - input: 'src/tcp.ts', - plugins: [ - sucrase({ - include: ['src/**'], - transforms: ['typescript'] - }), - ], - external: ['net'], - output: { - file: 'dist/tcp.js', - name: 'tcp', - format: 'umd' - } -}; \ No newline at end of file + input: 'src/tcp.ts', + plugins: [ + sucrase({ + include: ['src/**'], + transforms: ['typescript'], + }), + ], + external: ['net'], + output: { + file: 'dist/tcp.js', + name: 'tcp', + format: 'umd', + }, +}; diff --git a/packages/tcp/src/tcp.ts b/packages/tcp/src/tcp.ts index 531692c..dc669af 100644 --- a/packages/tcp/src/tcp.ts +++ b/packages/tcp/src/tcp.ts @@ -21,7 +21,7 @@ export default function tcp({ socketTimeout = 30000 }: TCPConfig = {}): KalmTran listener.listen(params.port, () => emitter.emit('ready')); } - function remote(handle: TCPSocket): Remote { + function remote(handle?: TCPSocket): Remote { return { host: handle?.remoteAddress || handle?._peername?.address || null, port: handle?.remotePort || handle?._peername?.port || null, diff --git a/packages/tcp/tests/unit/tcp.spec.ts b/packages/tcp/tests/unit/tcp.spec.ts index 646010a..36dbc33 100644 --- a/packages/tcp/tests/unit/tcp.spec.ts +++ b/packages/tcp/tests/unit/tcp.spec.ts @@ -22,7 +22,7 @@ describe('TCP transport', () => { describe('when fetching remote', () => { it('should return null values', () => { - expect(socket.remote).toEqual({ host: null, port: null }); + expect(socket.remote()).toEqual({ host: null, port: null }); }); }); }); diff --git a/packages/udp/package.json b/packages/udp/package.json index 8c0b874..89752ba 100644 --- a/packages/udp/package.json +++ b/packages/udp/package.json @@ -1,6 +1,6 @@ { "name": "@kalm/udp", - "version": "7.0.0", + "version": "8.0.0", "description": "UDP transport for Kalm", "main": "dist/udp.js", "scripts": { @@ -48,8 +48,7 @@ "transform": { "^.+\\.tsx?$": [ "ts-jest", { - "diagnostics": false, - "isolatedModules": true + "diagnostics": false }] } } diff --git a/packages/udp/rollup.config.mjs b/packages/udp/rollup.config.mjs index 5511327..ee0b181 100644 --- a/packages/udp/rollup.config.mjs +++ b/packages/udp/rollup.config.mjs @@ -1,17 +1,17 @@ import sucrase from '@rollup/plugin-sucrase'; export default { - input: 'src/udp.ts', - plugins: [ - sucrase({ - include: ['src/**'], - transforms: ['typescript'] - }), - ], - external: ['dgram'], - output: { - file: 'dist/udp.js', - name: 'udp', - format: 'umd' - } -}; \ No newline at end of file + input: 'src/udp.ts', + plugins: [ + sucrase({ + include: ['src/**'], + transforms: ['typescript'], + }), + ], + external: ['dgram'], + output: { + file: 'dist/udp.js', + name: 'udp', + format: 'umd', + }, +}; diff --git a/packages/udp/tests/unit/udp.spec.ts b/packages/udp/tests/unit/udp.spec.ts index 1578df4..9366200 100644 --- a/packages/udp/tests/unit/udp.spec.ts +++ b/packages/udp/tests/unit/udp.spec.ts @@ -22,7 +22,7 @@ describe('UDP transport', () => { describe('when fetching remote', () => { it('should return null values', () => { - expect(socket.remote).toEqual({ host: null, port: null }); + expect(socket.remote()).toEqual({ host: null, port: null }); }); }); }); diff --git a/packages/ws/package.json b/packages/ws/package.json index 2cb4068..50ecf3f 100644 --- a/packages/ws/package.json +++ b/packages/ws/package.json @@ -1,6 +1,6 @@ { "name": "@kalm/ws", - "version": "7.0.0", + "version": "8.0.0", "description": "WebSocket transport for Kalm", "main": "dist/ws.js", "scripts": { @@ -52,8 +52,7 @@ "transform": { "^.+\\.tsx?$": [ "ts-jest", { - "diagnostics": false, - "isolatedModules": true + "diagnostics": false }] } } diff --git a/packages/ws/rollup.config.mjs b/packages/ws/rollup.config.mjs index 2de218b..5b53d9e 100644 --- a/packages/ws/rollup.config.mjs +++ b/packages/ws/rollup.config.mjs @@ -1,16 +1,16 @@ import sucrase from '@rollup/plugin-sucrase'; export default (async () => ({ - input: 'src/ws.ts', - plugins: [ - sucrase({ - include: ['src/**'], - transforms: ['typescript'] - }), - ], - output: { - file: 'dist/ws.js', - name: 'ws', - format: 'umd', - } -}))(); \ No newline at end of file + input: 'src/ws.ts', + plugins: [ + sucrase({ + include: ['src/**'], + transforms: ['typescript'], + }), + ], + output: { + file: 'dist/ws.js', + name: 'ws', + format: 'umd', + }, +}))(); diff --git a/packages/ws/src/ws.ts b/packages/ws/src/ws.ts index 0332b4d..0c64e2c 100644 --- a/packages/ws/src/ws.ts +++ b/packages/ws/src/ws.ts @@ -36,7 +36,7 @@ export default function ws({ cert, key, socketTimeout = 30000 }: WSConfig = {}): setTimeout(() => emitter.emit('ready'), 1); } - function send(handle: WSHandle & { _queue: string[] }, payload: RawFrame | string): void { + function send(handle: WSHandle & { _queue?: string[] }, payload: RawFrame | string): void { if (handle && handle.readyState === 1) handle.send(typeof payload === 'string' ? payload : JSON.stringify(payload)); else handle._queue.push(JSON.stringify(payload)); resetTimeout(handle); @@ -77,7 +77,7 @@ export default function ws({ cert, key, socketTimeout = 30000 }: WSConfig = {}): } function remote(handle: WSHandle): Remote { - const h = handle && handle.headers || {}; + const h = (handle && handle.headers) || {}; const headerHost = h && h['x-forwarded-for'] && h['x-forwarded-for'].split(',')[0]; const remoteHost = handle?.connection?.remoteAddress; const socketHost = handle?._socket?.server?._connectionKey; diff --git a/packages/ws/tests/unit/ws.spec.ts b/packages/ws/tests/unit/ws.spec.ts index 3471fc7..d86333b 100644 --- a/packages/ws/tests/unit/ws.spec.ts +++ b/packages/ws/tests/unit/ws.spec.ts @@ -22,7 +22,7 @@ describe('ws transport', () => { describe('when fetching remote', () => { it('should return null values', () => { - expect(socket.remote).toEqual({ host: null, port: null }); + expect(socket.remote()).toEqual({ host: null, port: null }); }); }); }); diff --git a/scripts/benchmarks/index.js b/scripts/benchmarks/index.ts similarity index 100% rename from scripts/benchmarks/index.js rename to scripts/benchmarks/index.ts diff --git a/scripts/benchmarks/settings.js b/scripts/benchmarks/settings.ts similarity index 100% rename from scripts/benchmarks/settings.js rename to scripts/benchmarks/settings.ts diff --git a/scripts/benchmarks/transports/ipc.js b/scripts/benchmarks/transports/ipc.ts similarity index 81% rename from scripts/benchmarks/transports/ipc.js rename to scripts/benchmarks/transports/ipc.ts index 3339968..bb59b28 100644 --- a/scripts/benchmarks/transports/ipc.js +++ b/scripts/benchmarks/transports/ipc.ts @@ -25,11 +25,11 @@ function _absorb(err) { function setup(resolve) { server = net.createServer((socket) => { - socket.addEventListener('error', _absorb); - socket.addEventListener('data', () => socket.write(JSON.stringify(settings.testPayload))); + socket.on('error', _absorb); + socket.on('data', () => socket.write(JSON.stringify(settings.testPayload))); }); handbreak = false; - server.addEventListener('error', _absorb); + server.on('error', _absorb); server.listen(`/tmp/app.socket-${settings.port}`, resolve); } @@ -53,8 +53,8 @@ function step(resolve) { if (handbreak) return; if (!client) { client = net.connect(`/tmp/app.socket-${settings.port}`); - client.addEventListener('error', _absorb); - client.addEventListener('data', () => count++); + client.on('error', _absorb); + client.on('data', () => count++); } client.write(JSON.stringify(settings.testPayload)); diff --git a/scripts/benchmarks/transports/kalm.js b/scripts/benchmarks/transports/kalm.ts similarity index 93% rename from scripts/benchmarks/transports/kalm.js rename to scripts/benchmarks/transports/kalm.ts index be67c06..518a9f6 100644 --- a/scripts/benchmarks/transports/kalm.js +++ b/scripts/benchmarks/transports/kalm.ts @@ -33,11 +33,11 @@ function setup(resolve) { routine: Kalm.routines[settings.routine[0]](settings.routine[1]), }); - server.addEventListener('connection', (c) => { + server.on('connection', (c) => { c.subscribe(settings.testChannel, msg => c.write(settings.testChannel, msg)); }); - server.addEventListener('error', (e) => { + server.on('error', (e) => { console.error('Server error:', e); }); @@ -71,7 +71,7 @@ function step(resolve) { count++; }); - client.addEventListener('error', (e) => { + client.on('error', (e) => { console.error('Client error:', e); }); } diff --git a/scripts/benchmarks/transports/socketio.js b/scripts/benchmarks/transports/socketio.ts similarity index 81% rename from scripts/benchmarks/transports/socketio.js rename to scripts/benchmarks/transports/socketio.ts index 0b90cda..65dca1d 100644 --- a/scripts/benchmarks/transports/socketio.js +++ b/scripts/benchmarks/transports/socketio.ts @@ -28,10 +28,10 @@ function _absorb(err) { function setup(resolve) { server = io(); handbreak = false; - server.addEventListener('connection', (socket) => { - socket.addEventListener('data', () => socket.emit('data', JSON.stringify(settings.testPayload))); + server.on('connection', (socket) => { + socket.on('data', () => socket.emit('data', JSON.stringify(settings.testPayload))); }); - server.addEventListener('error', _absorb); + server.on('error', _absorb); server.listen(http.createServer().listen(settings.port, '0.0.0.0')); setTimeout(resolve, 10); } @@ -56,8 +56,8 @@ function step(resolve) { if (handbreak) return; if (!client) { client = ioclient(`http://0.0.0.0:${settings.port}`); - client.addEventListener('error', _absorb); - client.addEventListener('data', () => count++); + client.on('error', _absorb); + client.on('data', () => count++); } client.emit('data', JSON.stringify(settings.testPayload)); diff --git a/scripts/benchmarks/transports/tcp.js b/scripts/benchmarks/transports/tcp.ts similarity index 80% rename from scripts/benchmarks/transports/tcp.js rename to scripts/benchmarks/transports/tcp.ts index d182ba4..2b0765d 100644 --- a/scripts/benchmarks/transports/tcp.js +++ b/scripts/benchmarks/transports/tcp.ts @@ -23,11 +23,11 @@ function _absorb(err) { function setup(resolve) { server = net.createServer((socket) => { - socket.addEventListener('data', () => socket.write(JSON.stringify(settings.testPayload))); - socket.addEventListener('error', _absorb); + socket.on('data', () => socket.write(JSON.stringify(settings.testPayload))); + socket.on('error', _absorb); }); handbreak = false; - server.addEventListener('error', _absorb); + server.on('error', _absorb); server.listen(settings.port, resolve); } @@ -51,8 +51,8 @@ function step(resolve) { if (handbreak) return; if (!client) { client = net.connect(settings.port, '0.0.0.0'); - client.addEventListener('error', _absorb); - client.addEventListener('data', () => count++); + client.on('error', _absorb); + client.on('data', () => count++); } client.write(JSON.stringify(settings.testPayload)); diff --git a/scripts/benchmarks/transports/udp.js b/scripts/benchmarks/transports/udp.ts similarity index 87% rename from scripts/benchmarks/transports/udp.js rename to scripts/benchmarks/transports/udp.ts index 86599d5..1ff0e69 100644 --- a/scripts/benchmarks/transports/udp.js +++ b/scripts/benchmarks/transports/udp.ts @@ -24,11 +24,11 @@ function _absorb(err) { function setup(resolve) { server = dgram.createSocket('udp4'); - server.addEventListener('message', () => { + server.on('message', () => { server.send(Buffer.from(JSON.stringify(settings.testPayload)), 1111, '0.0.0.0'); }); handbreak = false; - server.addEventListener('error', _absorb); + server.on('error', _absorb); server.bind(settings.port, '0.0.0.0'); resolve(); } @@ -50,8 +50,8 @@ function step(resolve) { if (handbreak) return; if (!client) { client = dgram.createSocket('udp4'); - client.addEventListener('error', _absorb); - client.addEventListener('message', () => count++); + client.on('error', _absorb); + client.on('message', () => count++); client.bind(1111, '0.0.0.0'); } diff --git a/tests/integration/frame.spec.ts b/tests/integration/frame.spec.ts index e06860a..e6e892a 100644 --- a/tests/integration/frame.spec.ts +++ b/tests/integration/frame.spec.ts @@ -26,7 +26,7 @@ describe('Frame', () => { it('Should have a well structured frame reference', (done) => { const payload = { foo: 'bar' }; - server.addEventListener('connection', (c) => { + server.on('connection', (c) => { c.subscribe('test', (data, meta) => { expect(meta).toEqual({ client: c, @@ -41,12 +41,12 @@ describe('Frame', () => { done(); }); }); - server.addEventListener('error', (e) => { + server.on('error', (e) => { throw new Error(e); }); const client = connect({ transport: ipc() }); - client.addEventListener('error', (e) => { + client.on('error', (e) => { throw new Error(e); }); client.write('test', payload); diff --git a/tests/integration/import.spec.ts b/tests/integration/import.spec.ts deleted file mode 100644 index 372bae8..0000000 --- a/tests/integration/import.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { connect, routines } from '../../packages/kalm/dist/kalm'; -import ipc from '../../packages/ipc/dist/ipc'; - -describe('Testing module loading patterns', () => { - it('Should support import syntax', () => { - expect(typeof connect).toBe('function'); - expect(routines).toHaveProperty('realtime'); - expect(typeof ipc).toBe('function'); - }); - - it('Should support require syntax for kalm core', () => { - expect(typeof require('../../packages/kalm/dist/kalm').connect).toBe('function'); - }); - - it('Should support require syntax for kalm routines', () => { - expect(require('../../packages/kalm/dist/kalm').routines).toHaveProperty('realtime'); - }); - - it('Should support require syntax for kalm transports', () => { - expect(typeof require('../../packages/ipc/dist/ipc')).toBe('function'); - }); -}); diff --git a/tests/integration/index.spec.ts b/tests/integration/index.spec.ts index 4ed246d..bdfd6d3 100644 --- a/tests/integration/index.spec.ts +++ b/tests/integration/index.spec.ts @@ -34,18 +34,18 @@ describe('Integration tests', () => { it(`should work with ${transport}`, (done) => { const payload = { foo: 'bar' }; - server.addEventListener('connection', (c) => { + server.on('connection', (c) => { c.subscribe('test', (data) => { expect(data).toEqual(payload); done(); }); }); - server.addEventListener('error', (e) => { + server.on('error', (e) => { throw new Error(e); }); const client = connect({ transport: soc }); - client.addEventListener('error', (e) => { + client.on('error', (e) => { throw new Error(e); }); client.write('test', payload); @@ -53,18 +53,18 @@ describe('Integration tests', () => { it(`should handle foreign characters with ${transport}`, (done) => { const payload = { foo: '한자' }; - server.addEventListener('connection', (c) => { + server.on('connection', (c) => { c.subscribe('test', (data) => { expect(data).toEqual(payload); done(); }); }); - server.addEventListener('error', (e) => { + server.on('error', (e) => { throw new Error(e); }); const client = connect({ transport: soc }); - client.addEventListener('error', (e) => { + client.on('error', (e) => { throw new Error(e); }); client.write('test', payload); @@ -76,18 +76,18 @@ describe('Integration tests', () => { largePayload.push({ foo: 'bar' }); } - server.addEventListener('connection', (c) => { + server.on('connection', (c) => { c.subscribe('test.large', (data) => { expect(data).toEqual(largePayload); done(); }); }); - server.addEventListener('error', (e) => { + server.on('error', (e) => { throw new Error(e); }); const client = connect({ transport: soc }); - client.addEventListener('error', (e) => { + client.on('error', (e) => { if (transport === 'udp') { expect(e.message).toEqual('UDP Cannot send packets larger than 16384 bytes, tried to send 28715 bytes'); return done(); @@ -99,7 +99,7 @@ describe('Integration tests', () => { it('should not trigger for unsubscribed channels', (done) => { const payload = { foo: 'bar' }; - server.addEventListener('connection', (c) => { + server.on('connection', (c) => { c.subscribe('test', () => { // Throw on purpose expect(false).toBe(true); @@ -108,12 +108,12 @@ describe('Integration tests', () => { c.unsubscribe('test'); }); - server.addEventListener('error', (e) => { + server.on('error', (e) => { throw new Error(e); }); const client = connect({ transport: soc }); - client.addEventListener('error', (e) => { + client.on('error', (e) => { throw new Error(e); }); setTimeout(() => client.write('test', payload), 100); diff --git a/types.d.ts b/types.d.ts index 1f71082..dfdaa70 100644 --- a/types.d.ts +++ b/types.d.ts @@ -47,7 +47,7 @@ interface ClientEventMap { connect: (client: Client) => void disconnect: () => void disconnected: () => void // Internal use only - frame: (frame: RawFrame) => void + frame: (data: { body: RawFrame, payloadBytes: number }) => void error: (error: Error) => void } @@ -80,9 +80,9 @@ interface Server { * 'error': when an error occurs (non-fatal) */ on(event: k, listener: ServerEventMap[k]): this - once(event: k, listener: ServerEventMap[k] | Function): this - removeListener(event: k, listener: ServerEventMap[k] | Function): this - off(event: k, listener: ServerEventMap[k] | Function): this + once(event: k, listener: ServerEventMap[k]): this + removeListener(event: k, listener: ServerEventMap[k]): this + off(event: k, listener: ServerEventMap[k]): this addEventListener(event: k, listener: (evt?: Event) => void): this removeEventListener(event: k, listener: (evt?: Event) => void): this } @@ -108,14 +108,14 @@ interface Client { * @param channel The channel name for the subscription * @param handler The function to invoke when a new message arrives */ - subscribe: (channel: string, handler: (body: any, frame: Frame) => any) => void + subscribe: (channel: string, handler: (body: any, context: Context) => any) => void /** * Stops listening for messages that are sent to a given channel * * @param channel The channel name for the subscription to stop * @param handler Optionally, the function to stop invoking. If left empty, will clear all handlers for that subscription */ - unsubscribe: (channel: string, handler: (body: any, frame: Frame) => any) => void + unsubscribe: (channel: string, handler: (body: any, context: Context) => any) => void /** * Prints the coordinates of the local client */ @@ -137,9 +137,9 @@ interface Client { * 'error': when an error occurs (non-fatal) */ on(event: k, listener: ClientEventMap[k]): this - once(event: k, listener: ClientEventMap[k] | Function): this - removeListener(event: k, listener: ClientEventMap[k] | Function): this - off(event: k, listener: ClientEventMap[k] | Function): this + once(event: k, listener: ClientEventMap[k]): this + removeListener(event: k, listener: ClientEventMap[k]): this + off(event: k, listener: ClientEventMap[k]): this addEventListener(event: k, listener: (evt?: Event) => void): this removeEventListener(event: k, listener: (evt?: Event) => void): this } @@ -175,7 +175,7 @@ interface Socket { /** The command for a server to start listening for messages */ bind: () => void /** Given a Client, prints the information of the remote party in the connection */ - remote: (handle: any) => Remote + remote: (handle?: any) => Remote /** Initiates the connection to a remote server */ connect: (handle?: any) => any /** The command to stop a server from accepting messages */ @@ -197,24 +197,26 @@ type RawFrame = { }; /** - * The contextual frame for a message received + * The context for a message received */ -type Frame = { +type Context = { /** A reference to the Client instance */ client: Client + /** The body of the message */ + frame: Frame +}; + +type Frame = { /** The name of the subscription channel */ channel: string - /** The body of the message */ - frame: { - /** The id of the frame, these are integers cycling from 0 to 0xffffffff */ - id: number - /** The position of the message ion the frame */ - messageIndex: number - /** The number of bytes in the frame */ - payloadBytes: number - /** The number of messages in the frame */ - payloadMessages: number - } + /** The id of the frame, these are integers cycling from 0 to 0xffffffff */ + id: number + /** The position of the message ion the frame */ + messageIndex: number + /** The number of bytes in the frame */ + payloadBytes: number + /** The number of messages in the frame */ + payloadMessages: number }; type TickConfig = { From 14326eb10879692b66942f491c2e621144c938b9 Mon Sep 17 00:00:00 2001 From: Frederic Charette Date: Wed, 30 Jul 2025 20:53:24 -0400 Subject: [PATCH 04/11] fixed tests and udp events, removed node 18 support --- .github/workflows/master-status.yml | 2 +- .github/workflows/pr-validation.yml | 2 +- CHANGELOG.md | 2 + package.json | 2 +- packages/ipc/src/ipc.ts | 2 +- packages/tcp/src/tcp.ts | 2 +- packages/udp/src/udp.ts | 26 ++++++++---- packages/ws/src/ws.ts | 2 +- scripts/benchmarks/index.ts | 34 +++++---------- scripts/benchmarks/settings.ts | 2 +- scripts/benchmarks/transports/ipc.ts | 38 ++++------------- scripts/benchmarks/transports/kalm.ts | 52 +++++++---------------- scripts/benchmarks/transports/socketio.ts | 39 +++++------------ scripts/benchmarks/transports/tcp.ts | 38 +++++------------ scripts/benchmarks/transports/udp.ts | 39 +++++------------ tests/integration/index.spec.ts | 25 ++++++----- types.d.ts | 2 +- 17 files changed, 105 insertions(+), 204 deletions(-) diff --git a/.github/workflows/master-status.yml b/.github/workflows/master-status.yml index 30796f3..700c723 100644 --- a/.github/workflows/master-status.yml +++ b/.github/workflows/master-status.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [18.x, 20.x, 22.x] + node-version: [20.x, 22.x, 24.x] steps: - uses: actions/checkout@v1 diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 6dd43ca..8e08616 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [18.x, 20.x, 22.x] + node-version: [20.x, 22.x, 24.x] steps: - uses: actions/checkout@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 72bda01..e266b6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ commit [#](https://github.com/kalm/kalm.js/commits) - Fixed server connections not getting cleaned up - Removed empty channels from frame payloads, saving bandwidth - Changed the subscribe handler's second argument name from `frame` to `context`, to reduce confusion with its nested `frame` property. +- Fixed missing UDP client `connect` event. +- Removed the potentially misleading argument in the `connect` event since it only exposes the unbound socket. ## [v7.0.0] - 2023-03-17 diff --git a/package.json b/package.json index 44f19c0..405d5cf 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "lint": "eslint .", "lint:fix": "eslint . --fix", "test": "npm run test --workspaces --if-present && npm run test:integration", - "test:integration": "jest ./tests/integration --forceExit", + "test:integration": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest ./tests/integration --forceExit", "build": "npm run build --workspaces --if-present", "clean": "npm run clean --workspaces --if-present", "bench": "ts-node --transpile-only ./scripts/benchmarks" diff --git a/packages/ipc/src/ipc.ts b/packages/ipc/src/ipc.ts index 1c6424b..e6047aa 100644 --- a/packages/ipc/src/ipc.ts +++ b/packages/ipc/src/ipc.ts @@ -60,7 +60,7 @@ export default function ipc({ socketTimeout = 30000, path = '/tmp/app.socket-' } } }); connection.on('error', err => emitter.emit('error', err)); - connection.on('connect', () => emitter.emit('connect', connection)); + connection.on('connect', () => emitter.emit('connect')); connection.on('close', () => emitter.emit('disconnected')); connection.setTimeout(socketTimeout, () => disconnect(handle)); return connection; diff --git a/packages/tcp/src/tcp.ts b/packages/tcp/src/tcp.ts index dc669af..b2b352f 100644 --- a/packages/tcp/src/tcp.ts +++ b/packages/tcp/src/tcp.ts @@ -54,7 +54,7 @@ export default function tcp({ socketTimeout = 30000 }: TCPConfig = {}): KalmTran } }); connection.on('error', err => emitter.emit('error', err)); - connection.on('connect', () => emitter.emit('connect', connection)); + connection.on('connect', () => emitter.emit('connect')); connection.on('close', () => emitter.emit('disconnected')); connection.setTimeout(socketTimeout, () => disconnect(handle)); return connection as TCPSocket; diff --git a/packages/udp/src/udp.ts b/packages/udp/src/udp.ts index 83e3ebe..1a43b9c 100644 --- a/packages/udp/src/udp.ts +++ b/packages/udp/src/udp.ts @@ -18,18 +18,25 @@ export default function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = let listener: dgram.Socket; const clientCache = {}; + // Appends client instance and message queue function addClient(client: Client): void { const local: Remote = client.local; - const key = `${local.host}.${local.port}`; + const key = `${local.host}:${local.port}`; // Client connection - skip if (local.host === params.host && local.port === params.port) return; clientCache[key].client = client; - for (let i = 0; i < clientCache[key].data.length; i++) { - clientCache[key].client.emit('frame', JSON.parse(clientCache[key].data[i].toString()), clientCache[key].data[i].length); + setTimeout(() => emitFrames(key), 1); + } + + function emitFrames(key) { + if (clientCache[key].client) { + for (let i = 0; i < clientCache[key].data.length; i++) { + clientCache[key].client.emit('frame', { body: JSON.parse(clientCache[key].data[i].toString()), payloadBytes: clientCache[key].data[i].length }); + } + clientCache[key].data.length = 0; } - clientCache[key].data.length = 0; } function remote(handle: UDPSocketHandle): Remote { @@ -58,7 +65,7 @@ export default function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = function disconnect(handle?: UDPSocketHandle): void { if (handle && handle.socket) handle.socket = null; - emitter.emit('disconnected'); + setTimeout(() => emitter.emit('disconnected'), 1); } function connect(handle?: UDPSocketHandle): UDPSocketHandle { @@ -67,7 +74,6 @@ export default function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = connection.on('error', err => emitter.emit('error', err)); connection.on('message', (req) => { - emitter.emit('connect', connection); emitter.emit('frame', { body: JSON.parse(req.toString()), payloadBytes: req.length }); resetTimeout(res); }); @@ -80,6 +86,8 @@ export default function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = _timer: null, }; + setTimeout(() => emitter.emit('connect'), 1); + resetTimeout(res); return res; @@ -92,7 +100,7 @@ export default function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = socket: listener, }; - const key = `${origin.address}.${origin.port}`; + const key = `${origin.address}:${origin.port}`; if (!clientCache[key]) { clientCache[key] = { @@ -103,8 +111,8 @@ export default function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = } if (data) { - if (clientCache[key].client) clientCache[key].client.emit('frame', JSON.parse(data.toString())); - else clientCache[key].data.push(data); + clientCache[key].data.push(data); + setTimeout(() => emitFrames(key), 1); } } diff --git a/packages/ws/src/ws.ts b/packages/ws/src/ws.ts index 0c64e2c..37b04da 100644 --- a/packages/ws/src/ws.ts +++ b/packages/ws/src/ws.ts @@ -60,7 +60,7 @@ export default function ws({ cert, key, socketTimeout = 30000 }: WSConfig = {}): connection[evtType]('error', err => emitter.emit('error', err)); connection[evtType]('close', () => emitter.emit('disconnected')); connection[evtType]('open', () => { - emitter.emit('connect', connection); + emitter.emit('connect'); connection._queue.forEach(payload => send(connection, payload)); resetTimeout(connection); diff --git a/scripts/benchmarks/index.ts b/scripts/benchmarks/index.ts index 5ee762e..52c9ac4 100644 --- a/scripts/benchmarks/index.ts +++ b/scripts/benchmarks/index.ts @@ -1,26 +1,16 @@ -/** - * Kalm benchmarking - */ - -/* Requires ------------------------------------------------------------------ */ - -const Kalm = require('./transports/kalm'); -const TCP = require('./transports/tcp'); -const IPC = require('./transports/ipc'); -const UDP = require('./transports/udp'); -const WS = require('./transports/socketio'); -const settings = require('./settings'); - -/* Local variables ----------------------------------------------------------- */ +import * as kalm from './transports/kalm.ts'; +import * as ipc from './transports/ipc.ts'; +import * as tcp from './transports/tcp.ts'; +import * as udp from './transports/udp.ts'; +import * as ws from './transports/socketio.ts'; +import settings from './settings.ts'; const _maxCount = null; let _curr = 0; -const Suite = { IPC, TCP, UDP, WS }; -const tests = []; +const Suite = { ipc, tcp, udp, ws }; +const tests: any[] = []; const results = {}; -/* Methods ------------------------------------------------------------------- */ - function _measure(transport, resolve) { _curr = 0; transport.setup(() => { @@ -50,18 +40,16 @@ function _updateSettings(obj, resolve) { } function _errorHandler(err) { - console.error(err); /* eslint-disable-line */ + console.error(err); process.exit(1); } function _postResults() { - console.log(JSON.stringify(results)); /* eslint-disable-line */ + console.log(JSON.stringify(results)); // Do something with the info process.exit(); } -/* Init ---------------------------------------------------------------------- */ - // Roll port number settings.port = 3000 + Math.round(Math.random() * 1000); @@ -69,7 +57,7 @@ const adpts = Object.keys(Suite).map(k => ({ transport: k, settings: { transport: k.toLowerCase() }, raw: Suite[k], - kalm: Kalm, + kalm, })); adpts.forEach((i) => { diff --git a/scripts/benchmarks/settings.ts b/scripts/benchmarks/settings.ts index 2efd9fd..ecb0a9e 100644 --- a/scripts/benchmarks/settings.ts +++ b/scripts/benchmarks/settings.ts @@ -1,4 +1,4 @@ -module.exports = { +export default { transport: 'tcp', port: 3001, routine: ['dynamic', { maxInterval: 5 }], diff --git a/scripts/benchmarks/transports/ipc.ts b/scripts/benchmarks/transports/ipc.ts index bb59b28..6cadd00 100644 --- a/scripts/benchmarks/transports/ipc.ts +++ b/scripts/benchmarks/transports/ipc.ts @@ -1,14 +1,5 @@ -/** - * KALM Benchmark - */ - -/* Requires ------------------------------------------------------------------ */ - -const net = require('net'); - -const settings = require('../settings'); - -/* Local variables ----------------------------------------------------------- */ +import { createServer, connect } from 'net'; +import settings from '../settings.ts'; let server; let client; @@ -16,15 +7,13 @@ let client; let count = 0; let handbreak = true; -/* Methods ------------------------------------------------------------------- */ - function _absorb(err) { - console.log(err); /* eslint-disable-line */ + console.log(err); return true; } -function setup(resolve) { - server = net.createServer((socket) => { +export function setup(resolve) { + server = createServer((socket) => { socket.on('error', _absorb); socket.on('data', () => socket.write(JSON.stringify(settings.testPayload))); }); @@ -33,7 +22,7 @@ function setup(resolve) { server.listen(`/tmp/app.socket-${settings.port}`, resolve); } -function teardown(resolve) { +export function teardown(resolve) { if (client) client.destroy(); if (server) { server.close(() => { @@ -44,15 +33,15 @@ function teardown(resolve) { } } -function stop(resolve) { +export function stop(resolve) { handbreak = true; setTimeout(resolve, 0); } -function step(resolve) { +export function step(resolve) { if (handbreak) return; if (!client) { - client = net.connect(`/tmp/app.socket-${settings.port}`); + client = connect(`/tmp/app.socket-${settings.port}`); client.on('error', _absorb); client.on('data', () => count++); } @@ -60,12 +49,3 @@ function step(resolve) { client.write(JSON.stringify(settings.testPayload)); resolve(); } - -/* Exports ------------------------------------------------------------------- */ - -module.exports = { - setup, - teardown, - step, - stop, -}; diff --git a/scripts/benchmarks/transports/kalm.ts b/scripts/benchmarks/transports/kalm.ts index 518a9f6..4241dfc 100644 --- a/scripts/benchmarks/transports/kalm.ts +++ b/scripts/benchmarks/transports/kalm.ts @@ -1,36 +1,25 @@ -/** - * KALM Benchmark - */ +import settings from '../settings.ts'; +import kalm from '../../../packages/kalm/dist/kalm.js'; -/* Requires ------------------------------------------------------------------ */ +import ipc from '../../../packages/ipc/dist/ipc.js'; +import tcp from '../../../packages/tcp/dist/tcp.js'; +import udp from '../../../packages/udp/dist/udp.js'; +import ws from '../../../packages/ws/dist/ws.js'; -const settings = require('../settings'); -const Kalm = require('../../../packages/kalm/dist/kalm'); - -const transports = { - ipc: require('../../../packages/ipc/dist/ipc'), - tcp: require('../../../packages/tcp/dist/tcp'), - udp: require('../../../packages/udp/dist/udp'), - ws: require('../../../packages/ws/dist/ws'), -}; - -/* Local variables ----------------------------------------------------------- */ +const transports = { ipc, tcp, udp, ws }; let server; let client; let count = 0; -let accDensity = 0; let handbreak = true; -/* Methods ------------------------------------------------------------------- */ - -function setup(resolve) { - server = Kalm.listen({ +export function setup(resolve) { + server = kalm.listen({ port: settings.port, json: true, transport: transports[settings.transport](), - routine: Kalm.routines[settings.routine[0]](settings.routine[1]), + routine: kalm.routines[settings.routine[0]](settings.routine[1]), }); server.on('connection', (c) => { @@ -45,28 +34,28 @@ function setup(resolve) { setTimeout(resolve, 0); } -function teardown(resolve) { +export function teardown(resolve) { server.stop(); server = null; client = null; resolve(count); } -function stop(resolve) { +export function stop(resolve) { handbreak = true; setTimeout(resolve, 0); } -function step(resolve) { +export function step(resolve) { if (handbreak) return; if (!client) { - client = Kalm.connect({ + client = kalm.connect({ port: settings.port, json: true, transport: transports[settings.transport](), - routine: Kalm.routines.realtime(), + routine: kalm.routines.realtime(), }); - client.subscribe(settings.testChannel, (body, frame) => { + client.subscribe(settings.testChannel, () => { // console.log('got it', frame) count++; }); @@ -80,12 +69,3 @@ function step(resolve) { resolve(); } - -/* Exports ------------------------------------------------------------------- */ - -module.exports = { - setup, - teardown, - step, - stop, -}; diff --git a/scripts/benchmarks/transports/socketio.ts b/scripts/benchmarks/transports/socketio.ts index 65dca1d..56b7f09 100644 --- a/scripts/benchmarks/transports/socketio.ts +++ b/scripts/benchmarks/transports/socketio.ts @@ -1,16 +1,8 @@ -/** - * KALM Benchmark - */ +import * as io from 'socket.io'; +import http from 'http'; +import ioclient from 'socket.io-client'; -/* Requires ------------------------------------------------------------------ */ - -const io = require('socket.io'); -const http = require('http'); -const ioclient = require('socket.io-client'); - -const settings = require('../settings'); - -/* Local variables ----------------------------------------------------------- */ +import settings from '../settings.ts'; let server; let client; @@ -18,15 +10,13 @@ let client; let count = 0; let handbreak = true; -/* Methods ------------------------------------------------------------------- */ - function _absorb(err) { - console.log(err); /* eslint-disable-line */ + console.log(err); return true; } -function setup(resolve) { - server = io(); +export function setup(resolve) { + server = new io.Server(); handbreak = false; server.on('connection', (socket) => { socket.on('data', () => socket.emit('data', JSON.stringify(settings.testPayload))); @@ -36,7 +26,7 @@ function setup(resolve) { setTimeout(resolve, 10); } -function teardown(resolve) { +export function teardown(resolve) { if (client) client.close(); if (server) { server.close(() => { @@ -47,12 +37,12 @@ function teardown(resolve) { } } -function stop(resolve) { +export function stop(resolve) { handbreak = true; setTimeout(resolve, 0); } -function step(resolve) { +export function step(resolve) { if (handbreak) return; if (!client) { client = ioclient(`http://0.0.0.0:${settings.port}`); @@ -63,12 +53,3 @@ function step(resolve) { client.emit('data', JSON.stringify(settings.testPayload)); resolve(); } - -/* Exports ------------------------------------------------------------------- */ - -module.exports = { - setup, - teardown, - step, - stop, -}; diff --git a/scripts/benchmarks/transports/tcp.ts b/scripts/benchmarks/transports/tcp.ts index 2b0765d..4de2004 100644 --- a/scripts/benchmarks/transports/tcp.ts +++ b/scripts/benchmarks/transports/tcp.ts @@ -1,13 +1,5 @@ -/** - * KALM Benchmark - */ - -/* Requires ------------------------------------------------------------------ */ - -const net = require('net'); -const settings = require('../settings'); - -/* Local variables ----------------------------------------------------------- */ +import { createServer, connect } from 'net'; +import settings from '../settings.ts'; let server; let client; @@ -15,14 +7,13 @@ let client; let count = 0; let handbreak = true; -/* Methods ------------------------------------------------------------------- */ - function _absorb(err) { - console.log(err); /* eslint-disable-line */ + console.log(err); + return true; } -function setup(resolve) { - server = net.createServer((socket) => { +export function setup(resolve) { + server = createServer((socket) => { socket.on('data', () => socket.write(JSON.stringify(settings.testPayload))); socket.on('error', _absorb); }); @@ -31,7 +22,7 @@ function setup(resolve) { server.listen(settings.port, resolve); } -function teardown(resolve) { +export function teardown(resolve) { if (client) client.destroy(); if (server) { server.close(() => { @@ -42,15 +33,15 @@ function teardown(resolve) { } } -function stop(resolve) { +export function stop(resolve) { handbreak = true; setTimeout(resolve, 0); } -function step(resolve) { +export function step(resolve) { if (handbreak) return; if (!client) { - client = net.connect(settings.port, '0.0.0.0'); + client = connect(settings.port, '0.0.0.0'); client.on('error', _absorb); client.on('data', () => count++); } @@ -58,12 +49,3 @@ function step(resolve) { client.write(JSON.stringify(settings.testPayload)); resolve(); } - -/* Exports ------------------------------------------------------------------- */ - -module.exports = { - setup, - teardown, - step, - stop, -}; diff --git a/scripts/benchmarks/transports/udp.ts b/scripts/benchmarks/transports/udp.ts index 1ff0e69..d39cca3 100644 --- a/scripts/benchmarks/transports/udp.ts +++ b/scripts/benchmarks/transports/udp.ts @@ -1,14 +1,5 @@ -/** - * KALM Benchmark - */ - -/* Requires ------------------------------------------------------------------ */ - -const dgram = require('dgram'); - -const settings = require('../settings'); - -/* Local letiables ----------------------------------------------------------- */ +import { createSocket } from 'dgram'; +import settings from '../settings.ts'; let server; let client; @@ -16,14 +7,13 @@ let client; let count = 0; let handbreak = true; -/* Methods ------------------------------------------------------------------- */ - function _absorb(err) { - console.log(err); /* eslint-disable-line */ + console.log(err); + return true; } -function setup(resolve) { - server = dgram.createSocket('udp4'); +export function setup(resolve) { + server = createSocket('udp4'); server.on('message', () => { server.send(Buffer.from(JSON.stringify(settings.testPayload)), 1111, '0.0.0.0'); }); @@ -33,7 +23,7 @@ function setup(resolve) { resolve(); } -function teardown(resolve) { +export function teardown(resolve) { server.close(() => { server = null; client = null; @@ -41,15 +31,15 @@ function teardown(resolve) { }); } -function stop(resolve) { +export function stop(resolve) { handbreak = true; setTimeout(resolve, 0); } -function step(resolve) { +export function step(resolve) { if (handbreak) return; if (!client) { - client = dgram.createSocket('udp4'); + client = createSocket('udp4'); client.on('error', _absorb); client.on('message', () => count++); client.bind(1111, '0.0.0.0'); @@ -58,12 +48,3 @@ function step(resolve) { client.send(Buffer.from(JSON.stringify(settings.testPayload)), settings.port, '0.0.0.0'); resolve(); } - -/* Exports ------------------------------------------------------------------- */ - -module.exports = { - setup, - teardown, - step, - stop, -}; diff --git a/tests/integration/index.spec.ts b/tests/integration/index.spec.ts index bdfd6d3..59c4aad 100644 --- a/tests/integration/index.spec.ts +++ b/tests/integration/index.spec.ts @@ -1,18 +1,22 @@ -/** - * Kalm integration test suite - */ +import { connect, listen } from '../../packages/kalm/dist/kalm'; -/* Requires ------------------------------------------------------------------ */ +import ipc from '../../packages/ipc/dist/ipc.js'; +import tcp from '../../packages/tcp/dist/tcp.js'; +import udp from '../../packages/udp/dist/udp.js'; +import ws from '../../packages/ws/dist/ws.js'; -import { connect, listen } from '../../packages/kalm/dist/kalm'; +const transports = { ipc, tcp, udp, ws }; -/* Suite -------------------------------------------------------------------- */ +const largePayload: { foo: string }[] = []; +while (largePayload.length < 2048) { + largePayload.push({ foo: 'bar' }); +} describe('Integration tests', () => { ['ipc', 'tcp', 'udp', 'ws'].forEach((transport) => { describe(`Testing ${transport} transport`, () => { let server; - const soc = require(`../../packages/${transport}/dist/${transport}`)(); /* eslint-disable-line */ + const soc = transports[transport](); /* --- Setup --- */ @@ -51,7 +55,7 @@ describe('Integration tests', () => { client.write('test', payload); }); - it(`should handle foreign characters with ${transport}`, (done) => { + it(`should handle special characters with ${transport}`, (done) => { const payload = { foo: '한자' }; server.on('connection', (c) => { c.subscribe('test', (data) => { @@ -71,11 +75,6 @@ describe('Integration tests', () => { }); it(`should handle large payloads with ${transport}`, (done) => { - const largePayload: { foo: string }[] = []; - while (largePayload.length < 2048) { - largePayload.push({ foo: 'bar' }); - } - server.on('connection', (c) => { c.subscribe('test.large', (data) => { expect(data).toEqual(largePayload); diff --git a/types.d.ts b/types.d.ts index dfdaa70..1f1dd62 100644 --- a/types.d.ts +++ b/types.d.ts @@ -44,7 +44,7 @@ interface ServerEventMap { } interface ClientEventMap { - connect: (client: Client) => void + connect: () => void disconnect: () => void disconnected: () => void // Internal use only frame: (data: { body: RawFrame, payloadBytes: number }) => void From fa41ebdfdcd9aedd3629c59c2cb8817cf10003ab Mon Sep 17 00:00:00 2001 From: Frederic Charette Date: Wed, 30 Jul 2025 21:03:46 -0400 Subject: [PATCH 05/11] finalized publish details and bumped engines requirements --- .gitignore | 2 ++ CHANGELOG.md | 1 + packages/ipc/package.json | 4 ++-- packages/kalm/README.md | 2 +- packages/kalm/package.json | 4 ++-- packages/tcp/package.json | 4 ++-- packages/udp/package.json | 4 ++-- packages/ws/package.json | 4 ++-- 8 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index ec600a5..2ead6fe 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,5 @@ packages/ipc/CHANGELOG.md packages/tcp/CHANGELOG.md packages/udp/CHANGELOG.md packages/ws/CHANGELOG.md + +packages/kalm/types.d.ts \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e266b6f..d545c9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ commit [#](https://github.com/kalm/kalm.js/commits) - Changed the subscribe handler's second argument name from `frame` to `context`, to reduce confusion with its nested `frame` property. - Fixed missing UDP client `connect` event. - Removed the potentially misleading argument in the `connect` event since it only exposes the unbound socket. +- Bumped engines requirement to Node 20.x ## [v7.0.0] - 2023-03-17 diff --git a/packages/ipc/package.json b/packages/ipc/package.json index 64d0247..1717c9c 100644 --- a/packages/ipc/package.json +++ b/packages/ipc/package.json @@ -7,10 +7,10 @@ "build": "npm run clean && rollup -c rollup.config.mjs", "clean": "rm -rf ./dist/*", "test": "jest ./tests", - "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" + "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE" }, "engines": { - "node": ">=14" + "node": ">=20" }, "funding": { "type": "Open Collective", diff --git a/packages/kalm/README.md b/packages/kalm/README.md index bf09b60..49ba7d4 100644 --- a/packages/kalm/README.md +++ b/packages/kalm/README.md @@ -21,7 +21,7 @@ - Flexible and extensible, create your own transports and buffering strategies - Can be used between servers or in the **browser** - Lower resource footprint and **better throughtput** than plain sockets -- **Zero dependencies** and can be bundled down to ~6kb! +- **Zero dependencies** and can be bundled down to ~5kb! ## Performance diff --git a/packages/kalm/package.json b/packages/kalm/package.json index bd8e6ff..7d37d76 100644 --- a/packages/kalm/package.json +++ b/packages/kalm/package.json @@ -7,10 +7,10 @@ "build": "npm run clean && rollup -c rollup.config.mjs", "clean": "rm -rf ./dist/*", "test": "jest ./tests", - "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" + "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../types.d.ts ./types.d.ts" }, "engines": { - "node": ">=14" + "node": ">=20" }, "funding": { "type": "Open Collective", diff --git a/packages/tcp/package.json b/packages/tcp/package.json index d20c756..3d514cd 100644 --- a/packages/tcp/package.json +++ b/packages/tcp/package.json @@ -7,10 +7,10 @@ "build": "npm run clean && rollup -c rollup.config.mjs", "clean": "rm -rf ./dist/*", "test": "jest ./tests", - "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" + "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE" }, "engines": { - "node": ">=14" + "node": ">=20" }, "funding": { "type": "Open Collective", diff --git a/packages/udp/package.json b/packages/udp/package.json index 89752ba..f730d35 100644 --- a/packages/udp/package.json +++ b/packages/udp/package.json @@ -7,14 +7,14 @@ "build": "npm run clean && rollup -c rollup.config.mjs", "clean": "rm -rf ./dist/*", "test": "jest ./tests", - "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" + "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE" }, "funding": { "type": "Open Collective", "url": "https://opencollective.com/kalm" }, "engines": { - "node": ">=14" + "node": ">=20" }, "repository": { "type": "git", diff --git a/packages/ws/package.json b/packages/ws/package.json index 50ecf3f..a313fbb 100644 --- a/packages/ws/package.json +++ b/packages/ws/package.json @@ -7,14 +7,14 @@ "build": "npm run clean && rollup -c rollup.config.mjs", "clean": "rm -rf ./dist/*", "test": "jest ./tests", - "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md" + "prepublishOnly": "npm run build && cp ../../LICENSE ./LICENSE" }, "funding": { "type": "Open Collective", "url": "https://opencollective.com/kalm" }, "engines": { - "node": ">=14" + "node": ">=20" }, "repository": { "type": "git", From 7e824ef60880d836f98f49e3093d5995d01173b7 Mon Sep 17 00:00:00 2001 From: Frederic Charette Date: Wed, 30 Jul 2025 21:15:16 -0400 Subject: [PATCH 06/11] fixed typos, example links and removed badges from readme --- CHANGELOG.md | 2 +- examples/binary_compression/README.md | 4 ++-- examples/distributed_pub_sub/README.md | 4 ++-- packages/ipc/README.md | 5 ----- packages/kalm/README.md | 23 ++++++++--------------- packages/tcp/README.md | 5 ----- packages/udp/README.md | 5 ----- packages/ws/README.md | 5 ----- 8 files changed, 13 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d545c9d..f657eb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ commit [#](https://github.com/kalm/kalm.js/commits) ### Breaking changes -- Updated bundling for greater compatibilty (exports *may* behave differently) +- Updated bundling for greater compatibility (exports *may* behave differently) - Deprecated the WebRTC Transport (too convoluted to fit the Kalm model) - Changed the signature of the `frame` event handler from `(frame: RawFrame, payloadBytes: number)` to `({ body: RawFrame, payloadBytes: number})` to ensure all event handlers only have one arguments. diff --git a/examples/binary_compression/README.md b/examples/binary_compression/README.md index cff3fb1..99b40f2 100644 --- a/examples/binary_compression/README.md +++ b/examples/binary_compression/README.md @@ -1,6 +1,6 @@ -# Birary messages and compression example +# Binary messages and compression example -This example shows how to send brinary messages and how to add compression. +This example shows how to send binary messages and how to add compression. # Requirements diff --git a/examples/distributed_pub_sub/README.md b/examples/distributed_pub_sub/README.md index 8eabba7..471b51f 100644 --- a/examples/distributed_pub_sub/README.md +++ b/examples/distributed_pub_sub/README.md @@ -1,8 +1,8 @@ # Distributed Pub-Sub example -This example shows how to create a network of multiple servers sharing their messages and relaying them to all thier clients. +This example shows how to create a network of multiple servers sharing their messages and relaying them to all their clients. -This is usefull if you have a very large set of connected clients that you wish to split between many servers. +This is useful if you have a very large set of connected clients that you wish to split between many servers. In this case, clients connect to this server mesh via websocket, yet servers communicate over straight TCP. diff --git a/packages/ipc/README.md b/packages/ipc/README.md index 63a8c48..24b5c0a 100644 --- a/packages/ipc/README.md +++ b/packages/ipc/README.md @@ -11,11 +11,6 @@
-[![Kalm](https://img.shields.io/npm/v/kalm.svg)](https://www.npmjs.com/package/@kalm/ipc) -[![Build Status](https://github.com/kalm/kalm.js/workflows/master-status/badge.svg)](https://github.com/kalm/kalm.js/actions?query=workflow%3A+master-status) -[![Financial Contributors on Open Collective](https://opencollective.com/kalm/all/badge.svg?label=financial+contributors)](https://opencollective.com/kalm) -[![Join the chat at https://gitter.im/KALM/home](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/KALM/?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - An IPC transport for the [Kalm](https://github.com/kalm/kalm.js) framework. - Send messages over file handles (IPC) to other processes with ease diff --git a/packages/kalm/README.md b/packages/kalm/README.md index 49ba7d4..9a93700 100644 --- a/packages/kalm/README.md +++ b/packages/kalm/README.md @@ -11,16 +11,10 @@
-[![Kalm](https://img.shields.io/npm/v/kalm.svg)](https://www.npmjs.com/package/kalm) -[![Build Status](https://github.com/kalm/kalm.js/workflows/master-status/badge.svg)](https://github.com/kalm/kalm.js/actions?query=workflow%3A+master-status) -[![Financial Contributors on Open Collective](https://opencollective.com/kalm/all/badge.svg?label=financial+contributors)](https://opencollective.com/kalm) -[![Join the chat at https://gitter.im/KALM/home](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/KALM/?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ---- - - **Easy-to-use syntax** unified across protocols - Flexible and extensible, create your own transports and buffering strategies - Can be used between servers or in the **browser** -- Lower resource footprint and **better throughtput** than plain sockets +- Lower resource footprint and **better throughput** than plain sockets - **Zero dependencies** and can be bundled down to ~5kb! @@ -94,11 +88,10 @@ client.on('connect', () => { ``` To see working implementations, check out our [examples](https://github.com/kalm/kalm.js/tree/master/examples) folder. -- [Peer to peer](https://github.com/kalm/kalm.js/tree/master/examples/browser_peer_to_peer) -- [Chat via websockets](https://github.com/kalm/kalm.js/tree/master/examples/chat_websocket) +- [Peer to peer with WebRTC](https://github.com/kalm/kalm.js/tree/master/examples/browser_peer_to_peer) - [Distributed Pub-Sub](https://github.com/kalm/kalm.js/tree/master/examples/distributed_pub_sub) -- [Packet compressing](https://github.com/kalm/kalm.js/tree/master/examples/compression) -- [Typescript usage](https://github.com/kalm/kalm.js/tree/master/examples/typescript) +- [Binary packet compression](https://github.com/kalm/kalm.js/tree/master/examples/binary_compression) +- [Basic Typescript usage](https://github.com/kalm/kalm.js/tree/master/examples/typescript_websocket) ## Documentation @@ -129,17 +122,17 @@ Kalm **servers** offers events to track when packets are processed by routines o | Server Event | Payload | Description | | --- | --- | --- | | `error` | Error | (server, client) Emits on errors. | -| `ready` | void | (server) Indicates that the server is now actively listeneing for new connections | -| `connection` | [Client](./types.d.ts#L90) | (server) Indicates that a client has successfuly connected | +| `ready` | void | (server) Indicates that the server is now actively listening for new connections | +| `connection` | [Client](./types.d.ts#L90) | (server) Indicates that a client has successfully connected | Kalm **clients** offers events to track when packets are processed by routines or when a raw frame is received. | Client Event | Payload | Description | | --- | --- | --- | | `error` | Error | (server, client) Emits on errors. | -| `connect` | [Client](./types.d.ts#L90) | (client) Indicates that a client has successfuly connected | +| `connect` | [Client](./types.d.ts#L90) | (client) Indicates that a client has successfully connected | | `disconnect` | void | (client) Indicates that a client has disconnected | -| `frame` | [RawFrame](./types.d.ts#L189) | (client) Triggered when recieving a parsed full frame. | +| `frame` | [RawFrame](./types.d.ts#L189) | (client) Triggered when receiving a parsed full frame. | ## Testing diff --git a/packages/tcp/README.md b/packages/tcp/README.md index cf6cd8e..a140499 100644 --- a/packages/tcp/README.md +++ b/packages/tcp/README.md @@ -11,11 +11,6 @@
-[![Kalm](https://img.shields.io/npm/v/kalm.svg)](https://www.npmjs.com/package/@kalm/tcp) -[![Build Status](https://github.com/kalm/kalm.js/workflows/master-status/badge.svg)](https://github.com/kalm/kalm.js/actions?query=workflow%3A+master-status) -[![Financial Contributors on Open Collective](https://opencollective.com/kalm/all/badge.svg?label=financial+contributors)](https://opencollective.com/kalm) -[![Join the chat at https://gitter.im/KALM/home](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/KALM/?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - A TCP transport for the [Kalm](https://github.com/kalm/kalm.js) framework. - Send messages over TCP with ease diff --git a/packages/udp/README.md b/packages/udp/README.md index 6c57772..e4ac119 100644 --- a/packages/udp/README.md +++ b/packages/udp/README.md @@ -11,11 +11,6 @@
-[![Kalm](https://img.shields.io/npm/v/kalm.svg)](https://www.npmjs.com/package/@kalm/udp) -[![Build Status](https://github.com/kalm/kalm.js/workflows/master-status/badge.svg)](https://github.com/kalm/kalm.js/actions?query=workflow%3A+master-status) -[![Financial Contributors on Open Collective](https://opencollective.com/kalm/all/badge.svg?label=financial+contributors)](https://opencollective.com/kalm) -[![Join the chat at https://gitter.im/KALM/home](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/KALM/?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - A UDP transport for the [Kalm](https://github.com/kalm/kalm.js) framework. - Use fire-and-forget type messaging with the conveinience of stateful interfaces diff --git a/packages/ws/README.md b/packages/ws/README.md index dc19be0..565207f 100644 --- a/packages/ws/README.md +++ b/packages/ws/README.md @@ -11,11 +11,6 @@
-[![Kalm](https://img.shields.io/npm/v/kalm.svg)](https://www.npmjs.com/package/@kalm/ws) -[![Build Status](https://github.com/kalm/kalm.js/workflows/master-status/badge.svg)](https://github.com/kalm/kalm.js/actions?query=workflow%3A+master-status) -[![Financial Contributors on Open Collective](https://opencollective.com/kalm/all/badge.svg?label=financial+contributors)](https://opencollective.com/kalm) -[![Join the chat at https://gitter.im/KALM/home](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/KALM/?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - A websocket transport for the [Kalm](https://github.com/kalm/kalm.js) framework. - Detects native Websocket APIs, or fallbacks to [ws](https://github.com/websockets/ws) From 7950b29facea5be1f4456cb9f6925432241c90fb Mon Sep 17 00:00:00 2001 From: Frederic Charette Date: Wed, 30 Jul 2025 21:33:27 -0400 Subject: [PATCH 07/11] fixed ws for older node versions --- packages/ws/src/ws.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ws/src/ws.ts b/packages/ws/src/ws.ts index 37b04da..694fe00 100644 --- a/packages/ws/src/ws.ts +++ b/packages/ws/src/ws.ts @@ -1,3 +1,5 @@ +import { WebSocket as WSClient, WebSocketServer } from 'ws'; + const nativeAPIExists = (typeof WebSocket !== 'undefined'); type WSConfig = { @@ -21,15 +23,13 @@ export default function ws({ cert, key, socketTimeout = 30000 }: WSConfig = {}): async function bind(): Promise { if (typeof window !== 'undefined') throw new Error('Cannot create a websocket server from the browser'); - const WSLib = await import('ws'); - if (cert && key) { const https = await import('https'); const server = https.createServer({ key, cert }, req => req.socket.end()); - listener = new WSLib.WebSocketServer({ port: params.port, server }); + listener = new WebSocketServer({ port: params.port, server }); } else { - listener = new WSLib.WebSocketServer({ port: params.port }); + listener = new WebSocketServer({ port: params.port }); } listener.on('connection', soc => emitter.emit('socket', soc)); listener.on('error', err => emitter.emit('error', err)); @@ -48,7 +48,7 @@ export default function ws({ cert, key, socketTimeout = 30000 }: WSConfig = {}): function connect(handle?: WSHandle): WSHandle { const protocol: string = (!!cert && !!key) === true ? 'wss' : 'ws'; - const connection: WSHandle = handle || new WebSocket(`${protocol}://${params.host}:${params.port}`); + const connection: WSHandle = handle || new (nativeAPIExists ? WebSocket : WSClient)(`${protocol}://${params.host}:${params.port}`); connection.binaryType = 'arraybuffer'; const evtType: string = nativeAPIExists ? 'addEventListener' : 'on'; connection._queue = []; From e97abdca5a3747dcce1829869942becdde63f8a8 Mon Sep 17 00:00:00 2001 From: Frederic Charette Date: Wed, 30 Jul 2025 21:36:28 -0400 Subject: [PATCH 08/11] replaced yarn with npm in GHA --- .github/workflows/master-status.yml | 10 +++++----- .github/workflows/pr-validation.yml | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/master-status.yml b/.github/workflows/master-status.yml index 700c723..3000060 100644 --- a/.github/workflows/master-status.yml +++ b/.github/workflows/master-status.yml @@ -22,10 +22,10 @@ jobs: node-version: ${{ matrix.node-version }} - name: npm install, build, and test run: | - yarn - yarn run build - yarn run lint - yarn run test - yarn run bench + npm i + npm run build + npm run lint + npm run test + npm run bench env: CI: true diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 8e08616..69bb9a5 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -22,10 +22,10 @@ jobs: node-version: ${{ matrix.node-version }} - name: npm install, build, and test run: | - yarn - yarn run build - yarn run lint - yarn run test - yarn run bench + npm + npm run build + npm run lint + npm run test + npm run bench env: CI: true From f51ee0b8ea4feb07136ebaafbe5c2daa355847b1 Mon Sep 17 00:00:00 2001 From: Frederic Charette Date: Wed, 30 Jul 2025 21:38:36 -0400 Subject: [PATCH 09/11] fixed typo in GHA --- .github/workflows/pr-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 69bb9a5..a30af17 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -22,7 +22,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: npm install, build, and test run: | - npm + npm i npm run build npm run lint npm run test From 7e8ec42c2dfc2993d978e080995bb9558a0a7a33 Mon Sep 17 00:00:00 2001 From: Frederic Charette Date: Wed, 30 Jul 2025 21:51:32 -0400 Subject: [PATCH 10/11] replaced ts-node with tsx for benchmarks because esm --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 405d5cf..330b793 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "test:integration": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest ./tests/integration --forceExit", "build": "npm run build --workspaces --if-present", "clean": "npm run clean --workspaces --if-present", - "bench": "ts-node --transpile-only ./scripts/benchmarks" + "bench": "tsx ./scripts/benchmarks" }, "funding": { "type": "Open Collective", @@ -70,7 +70,7 @@ "socket.io": "^4.8.0", "socket.io-client": "^4.8.0", "ts-jest": "^29.4.0", - "ts-node": "10.9.2", + "tsx": "^4.20.3", "typescript": "^5.8.0", "typescript-eslint": "^8.38.0", "ws": "^8.18.3" From 5c65ebfa4f41b67d59dfad0d5114f5c6b26be88e Mon Sep 17 00:00:00 2001 From: Frederic Charette Date: Wed, 30 Jul 2025 22:00:15 -0400 Subject: [PATCH 11/11] wrapped udp send --- packages/udp/src/udp.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/udp/src/udp.ts b/packages/udp/src/udp.ts index 1a43b9c..3e4fabc 100644 --- a/packages/udp/src/udp.ts +++ b/packages/udp/src/udp.ts @@ -53,7 +53,13 @@ export default function udp({ type = 'udp4', localAddr = '0.0.0.0', reuseAddr = return; } if (handle && handle.socket) { - handle.socket.send(payloadBytes, handle.port, handle.host); + // We're leaving performance on the table, but sometimes the state of the drgam socket changes mid-flight... + try { + handle.socket.send(payloadBytes, handle.port, handle.host); + } + catch (e) { + emitter.emit('error', e); + } } resetTimeout(handle); }