From cb50c29e2628f1fa22762975ca8f14e2640c0920 Mon Sep 17 00:00:00 2001 From: Frederic Charette Date: Thu, 16 Mar 2023 20:35:01 -0400 Subject: [PATCH] updated READMEs, added description in ts definitions, fixed bugs in tick --- CHANGELOG.md | 35 +++++- LICENSE | 2 +- SECURITY.md | 4 +- packages/kalm/README.md | 4 +- packages/kalm/src/routines/dynamic.ts | 12 +- packages/kalm/src/routines/realtime.ts | 4 +- packages/kalm/src/routines/tick.ts | 16 ++- packages/kalm/types.d.ts | 154 +++++++++++++++++++++++-- packages/tcp/README.md | 5 +- packages/tcp/types.d.ts | 2 +- packages/udp/README.md | 17 +-- packages/udp/types.d.ts | 6 +- packages/webrtc/README.md | 5 +- packages/ws/README.md | 15 ++- packages/ws/types.d.ts | 2 +- scripts/benchmarks/settings.js | 2 +- 16 files changed, 237 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f608daf..22fca36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,41 @@ # Changelog +## [v7.0.0] - 2023-03-17 + +commit [#](https://github.com/kalm/kalm.js/commits) + +### Major changes + +- Standardized parameter names and expected behavior + - Removed `secure` WS option, instead checking if `cert` and `key` are set + - Routines.dynamic option `hz` is now `maxInterval` and is measured in milliseconds + - Renamed `provider` internally to `server` for easier understanding + - Removed previously deprecated UDP `connectTimeout` option +- Added UDP idle timeout behavior +- Added WS idle timeout behavior +- Added WS Agent option for proxying +- frameId counter now goes up to 0xffffffff before cycling instead of 0xffff + +### Bug fixes + +- Fixed an issue in Routines.tick where all queues shared the same frameId counter +- Routines.tick option `seed` now correctly sets the `frameId` and starts the counter to match the expected pace +- Fixed references to Node modules in TS definitions + +## [v6.1.0] - 2022-09-21 + +commit [a0e88e3](https://github.com/kalm/kalm.js/commit/a0e88e310d98646b53fbcc56f6efeea4db5e87d8) + +### Major changes + +- Removed SYN/ACK UDP handshake, which removes the socket timeout behaviour for that transport +- Added error event for UDP packet over the safe limit (16384 bytes), previous behaviour was to crash silently +- Routines are no longer event emitters, but have a size function + + ## [v6.0.0] - 2021-04-26 -commit: [#](https://github.com/kalm/kalm.js/commits) +commit: [47b810d](https://github.com/kalm/kalm.js/commit/47b810d5ab212686c3194d53e781e1728bd735f9) ### Breaking changes diff --git a/LICENSE b/LICENSE index 88a9ccc..24f7a38 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2022 Frederic Charette +Copyright 2023 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/SECURITY.md b/SECURITY.md index 610e8e4..f3d8372 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,9 +4,9 @@ | Version | Supported | | ------- | ------------------ | +| 7.x | :white_check_mark: | | 6.x | :white_check_mark: | -| 5.x | :white_check_mark: | -| <= 4.x | :x: | +| <= 5.x | :x: | ## Reporting a Vulnerability diff --git a/packages/kalm/README.md b/packages/kalm/README.md index 9243a56..a8c2005 100644 --- a/packages/kalm/README.md +++ b/packages/kalm/README.md @@ -19,7 +19,7 @@ --- - **Easy-to-use syntax** unified across protocols -- Flexible and extensible, load your own transports and routines +- 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! @@ -166,4 +166,4 @@ Support this project with your organization. Your logo will show up here with a ## License -[Apache 2.0](LICENSE) (c) 2022 Frederic Charette +[Apache 2.0](LICENSE) (c) 2023 Frederic Charette diff --git a/packages/kalm/src/routines/dynamic.ts b/packages/kalm/src/routines/dynamic.ts index b8cf1e1..df6524e 100644 --- a/packages/kalm/src/routines/dynamic.ts +++ b/packages/kalm/src/routines/dynamic.ts @@ -1,10 +1,10 @@ export function dynamic({ - hz, + maxInterval, maxPackets = Infinity, maxBytes = Infinity, -}: { hz: number, maxPackets?: number, maxBytes?: number }): KalmRoutine { - if (hz <= 0 || hz > 1000) { - throw new Error(`Unable to set Hertz value of ${hz}. Must be between 0.1e13 and 1000`); +}: { maxInterval: number, maxPackets?: number, maxBytes?: number }): KalmRoutine { + if (maxInterval < 1) { + throw new Error(`Unable to set millisecond value of ${maxInterval}. Must be above or equal to 1`); } return function queue(params: any, routineEmitter: (frameId: number) => any): Queue { @@ -17,7 +17,7 @@ export function dynamic({ clearTimeout(timer); timer = null; routineEmitter(frameId); - if (++frameId > 0xffff) frameId = 0; + if (++frameId > 0xffffffff) frameId = 0; numPackets = 0; totalBytes = 0; } @@ -48,7 +48,7 @@ export function dynamic({ } if (timer === null) { - timer = setTimeout(_step, Math.round(1000 / hz)); + timer = setTimeout(_step, maxInterval); } _add(packet); diff --git a/packages/kalm/src/routines/realtime.ts b/packages/kalm/src/routines/realtime.ts index aeacf45..5b3b1ee 100644 --- a/packages/kalm/src/routines/realtime.ts +++ b/packages/kalm/src/routines/realtime.ts @@ -1,5 +1,5 @@ export function realtime(): KalmRoutine { - return function queue(params: { deferred: boolean }, routineEmitter: (frameId: number) => any): Queue { + return function queue(params: { deferred: boolean } = { deferred: false }, routineEmitter: (frameId: number) => any): Queue { let frameId = 0; function add(): void { @@ -13,7 +13,7 @@ export function realtime(): KalmRoutine { function _step() { routineEmitter(frameId); - if (++frameId > 0xffff) frameId = 0; + if (++frameId > 0xffffffff) frameId = 0; } function flush(): void {} diff --git a/packages/kalm/src/routines/tick.ts b/packages/kalm/src/routines/tick.ts index b2e2bdc..f8def54 100644 --- a/packages/kalm/src/routines/tick.ts +++ b/packages/kalm/src/routines/tick.ts @@ -3,21 +3,27 @@ export function tick({ hz, seed = 0 }: { hz: number, seed?: number }): KalmRouti throw new Error(`Unable to set Hertz value of ${hz}. Must be between 0.1e13 and 1000`); } - let frameId: number = seed || 0; - return function queue(params: object, routineEmitter: (frameId: number) => any): Queue { - let timer: ReturnType = null; + const adjustedTime = seed ? Date.now() - seed : 0; + let frameId = Math.floor(adjustedTime / (1000 / hz)) % 0xffffffff; + + let timer: ReturnType = setTimeout(_init, adjustedTime % Math.round(1000 / hz)); let numPackets = 0; function _step(): void { - frameId++; routineEmitter(frameId); numPackets = 0; + if (++frameId > 0xffffffff) frameId = 0; + } + + function _init(): void { + clearTimeout(timer); + timer = setInterval(_step, Math.round(1000 / hz)); + _step(); } function add(): void { numPackets++; - if (timer === null) timer = setInterval(_step, 1000 / hz); } return { add, flush: _step, size: () => numPackets }; diff --git a/packages/kalm/types.d.ts b/packages/kalm/types.d.ts index d4ab8e5..570c733 100644 --- a/packages/kalm/types.d.ts +++ b/packages/kalm/types.d.ts @@ -1,20 +1,34 @@ 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 } 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 } @@ -24,38 +38,101 @@ type Remote = { } interface ServerEventMap { - 'ready': () => void, - 'connection': (client: Client) => void, + 'ready': () => void + 'connection': (client: Client) => void 'error': (error: Error) => void } interface ClientEventMap { - 'connect': (client: Client) => void, + 'connect': (client: Client) => void 'disconnect': () => void - 'frame': (frame: RawFrame) => 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. + */ 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[] + /** + * 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; } +/** + * 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 + /** + * Kills the connection to the server and destroys the client + */ 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 + /** + * 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 + /** + * Prints the coordinates of the local client + */ local: Remote + /** + * Prints the coordinates of the remote client + */ 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; @@ -68,6 +145,7 @@ interface KalmRoutine { (params: any, routineEmitter: (frameId: number) => any): Queue } +/** The message queue for a given subscription */ interface Queue { add: (packet: any) => void size: () => number @@ -89,37 +167,99 @@ type Peer = { } 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 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 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 } } +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 +} + +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 +} + +type RealtimeConfig = { + /** 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; + /** + * Connects to a remote socket server + */ export const connect: (config: ClientConfig) => Client; + /** + * The list of buffering routines for packets + */ export const routines: { - tick: (config: { hz: number, seed?: number }) => KalmRoutine - dynamic: (config: { hz: number, maxPackets?: number, maxBytes?: number }) => KalmRoutine - realtime: () => KalmRoutine + /** + * Emits buffered messages on a fixed time interval in Hertz. Can be synced with other servers with the `seed` property + */ + 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 + /** + * Emits messages immediatly as they enter the queue, no buffering + */ + realtime: (confg?: RealtimeConfig) => KalmRoutine }; } diff --git a/packages/tcp/README.md b/packages/tcp/README.md index 4f299d6..f1aa678 100644 --- a/packages/tcp/README.md +++ b/packages/tcp/README.md @@ -25,7 +25,8 @@ ``` { - socketTimeout: 30000 + /** The maximum idle time for the connection before it hangs up (default: 30000) */ + socketTimeout?: number } ``` @@ -37,4 +38,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) 2023 Frederic Charette diff --git a/packages/tcp/types.d.ts b/packages/tcp/types.d.ts index 7b1e2a7..1cfaa87 100644 --- a/packages/tcp/types.d.ts +++ b/packages/tcp/types.d.ts @@ -1,6 +1,6 @@ declare module '@kalm/tcp' { interface TCPConfig { - /** The maximum idle time for the connection before it hangs up */ + /** The maximum idle time for the connection before it hangs up (default: 30000) */ socketTimeout?: number } diff --git a/packages/udp/README.md b/packages/udp/README.md index 20d32c7..f1eaecf 100644 --- a/packages/udp/README.md +++ b/packages/udp/README.md @@ -23,13 +23,16 @@ ## Options -``` +```typescript { - type: 'udp4', - localAddr: '0.0.0.0', - reuseAddr: true, - socketTimeout: 30000, - connectTimeout: 1000, + /** 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 } ``` @@ -41,4 +44,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) 2023 Frederic Charette diff --git a/packages/udp/types.d.ts b/packages/udp/types.d.ts index e6821f8..e127fac 100644 --- a/packages/udp/types.d.ts +++ b/packages/udp/types.d.ts @@ -2,11 +2,11 @@ declare module '@kalm/udp' { interface UDPConfig { /** The udp socket family (default: udp4) */ type?: 'udp4' | 'udp6' - /** The ip address that shows up when calling `local()` */ + /** The ip address that shows up when calling `local()` (default: '0.0.0.0') */ localAddr?: string - /** UDP reuse Address seting */ + /** UDP reuse Address seting (default: false) */ reuseAddr?: boolean - /** The maximum idle time for the connection before it hangs up */ + /** The maximum idle time for the connection before it hangs up (default: 30000) */ socketTimeout?: number } diff --git a/packages/webrtc/README.md b/packages/webrtc/README.md index 762eb51..8d83938 100644 --- a/packages/webrtc/README.md +++ b/packages/webrtc/README.md @@ -23,8 +23,9 @@ ## Options -``` +```typescript { + /** The peers to connect to */ peers: [ { type: "offer", sdp: "..." } ], } ``` @@ -37,4 +38,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) 2023 Frederic Charette diff --git a/packages/ws/README.md b/packages/ws/README.md index f1ece37..9a4d82e 100644 --- a/packages/ws/README.md +++ b/packages/ws/README.md @@ -23,11 +23,16 @@ ## Options -``` +```typescript { - cert: null, - key: null, - secure: false + /** 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 } ``` @@ -39,4 +44,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) 2023 Frederic Charette diff --git a/packages/ws/types.d.ts b/packages/ws/types.d.ts index 028f467..41bdeec 100644 --- a/packages/ws/types.d.ts +++ b/packages/ws/types.d.ts @@ -6,7 +6,7 @@ declare module '@kalm/ws' { 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 */ + /** The maximum idle time for the connection before it hangs up (default: 30000) */ socketTimeout?: number } diff --git a/scripts/benchmarks/settings.js b/scripts/benchmarks/settings.js index 9866ea7..2efd9fd 100644 --- a/scripts/benchmarks/settings.js +++ b/scripts/benchmarks/settings.js @@ -1,7 +1,7 @@ module.exports = { transport: 'tcp', port: 3001, - routine: ['dynamic', { hz: 200 }], + routine: ['dynamic', { maxInterval: 5 }], testDuration: 1000 * 10, testPayload: { foo: 'bar' }, testChannel: 'test',