Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2b82061
feat: enhance WebSocket dispatcher to include connection ID and impro…
AxiosLeo Apr 6, 2026
5e5d6a0
feat: add connection ID to WebSocketContext and implement methods for…
AxiosLeo Apr 6, 2026
7aca920
feat: add debug logging to WebSocket server for message broadcasting
AxiosLeo Apr 6, 2026
685bf1d
fix: update TypeScript definitions for send method to require WebSock…
AxiosLeo Apr 6, 2026
39bcd68
chore: update ECMAScript version in ESLint configuration from 2018 to…
AxiosLeo Apr 6, 2026
02b64a9
chore: update Node.js version requirements in package.json and CI wor…
AxiosLeo Apr 6, 2026
1703efc
feat: extend socket dispatcher to include connection ID in context fo…
AxiosLeo Apr 6, 2026
8300666
refactor: replace optional chaining with direct property access for a…
AxiosLeo Apr 6, 2026
0442187
feat: add application instance to KoaContext, SocketContext, and WebS…
AxiosLeo Apr 6, 2026
7090bf9
fix: ensure sendByConnectionId method checks for valid connection bef…
AxiosLeo Apr 6, 2026
90181cf
chore: lower Node.js version requirement to 16 in package.json and CI…
AxiosLeo Apr 6, 2026
63f59e9
feat: implement methods for sending, closing, and managing connection…
AxiosLeo Apr 6, 2026
b5097e1
feat: add ping configuration to SocketApplication and WebSocketApplic…
AxiosLeo Apr 7, 2026
1d64ee9
refactor: enhance SocketApplication and WebSocketApplication by addin…
AxiosLeo Apr 10, 2026
0cf61a8
docs: update project structure and add detailed documentation for Soc…
AxiosLeo Apr 10, 2026
e29063a
test: add comprehensive unit tests for Application, Controller, core …
AxiosLeo Apr 10, 2026
b47da55
refactor: update websocketOptions in WebSocketApplication to remove u…
AxiosLeo Apr 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .cursor/rules/project-structure.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ The main entry point is [index.js](mdc:index.js), which exports all framework co

## Application Types

1. **KoaApplication** - Standard HTTP web server
2. **SocketApplication** - Socket.io server
3. **WebSocketApplication** - Native WebSocket server
1. **KoaApplication** - Standard HTTP web server (extends Application)
2. **SocketApplication** - TCP socket server (extends Application)
3. **WebSocketApplication** - Native WebSocket server (extends SocketApplication)

All applications extend the base [Application](mdc:src/apps/app.js) class.
KoaApplication and SocketApplication extend the base [Application](mdc:src/apps/app.js) class.
WebSocketApplication extends [SocketApplication](mdc:src/apps/socket.js), inheriting connection management methods.
126 changes: 126 additions & 0 deletions .cursor/rules/socket-application.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
---
globs: *.js,*.ts
description: SocketApplication and WebSocketApplication usage patterns and class hierarchy
---

# Socket Application Patterns

## Class Hierarchy

```
Application (base)
└── SocketApplication (TCP socket, extends Application)
└── WebSocketApplication (WebSocket, extends SocketApplication)
```

`WebSocketApplication` extends [SocketApplication](mdc:src/apps/socket.js), not `Application` directly.
Connection management methods (`sendByConnectionId`, `closeByConnectionId`, `getConnection`, `ping`) are defined once in `SocketApplication` and inherited by `WebSocketApplication`.

## SocketApplication Setup

TCP socket server using `net.createServer`:

```javascript
const { SocketApplication, Router } = require("@axiosleo/koapp");

const router = new Router("/", { /* ... */ });

const app = new SocketApplication({
routers: [router],
port: 8081,
ping: {
open: true,
interval: 1000 * 10,
data: "this is a ping message",
},
});

app.start();
```

Message protocol: JSON payload terminated by `@@@@@@` delimiter.

```
{"path":"/test","method":"GET","query":{"test":123}}@@@@@@
```

## WebSocketApplication Setup

WebSocket server using the `ws` library:

```javascript
const { WebSocketApplication, Router } = require("@axiosleo/koapp");

const router = new Router("/", { /* ... */ });

const app = new WebSocketApplication({
routers: [router],
port: 8081,
ping: {
open: false,
interval: 1000 * 3,
data: "this is a ping message",
},
});

app.start();
```

Message protocol: plain JSON string (no delimiter).

```
{"path":"/test","method":"GET","query":{"test":123}}
```

## Ping Configuration

Both applications support automatic heartbeat via `pingConfig`:

| Field | Type | Default | Description |
|------------|---------|-----------------|-------------------------------|
| `open` | boolean | `false` | Enable periodic ping |
| `interval` | number | `300000` (5min) | Milliseconds between pings |
| `data` | any | `"this is a ping message"` | Payload sent with each ping |

## Connection Management (inherited from SocketApplication)

All methods below are available on both `SocketApplication` and `WebSocketApplication`:

- `app.send(connection, data, msg, code)` — Send to a connection object (overridden per subclass)
- `app.sendByConnectionId(id, data, msg, code)` — Send by connection ID
- `app.close(connection)` — Close a connection object (overridden per subclass)
- `app.closeByConnectionId(id)` — Close by connection ID
- `app.getConnection(id)` — Get connection object by ID, returns `null` if not found
- `app.ping(id)` — Send a ping to a specific connection
- `app.broadcast(data, msg, code, connections)` — Broadcast to all or specific connections (overridden per subclass)

Pass `connections = null` to `broadcast` to send to all active connections.

## Protocol Differences

| Aspect | SocketApplication | WebSocketApplication |
|------------------|-------------------------------|-------------------------------|
| Transport | TCP (`net`) | WebSocket (`ws`) |
| `send()` | `connection.write(data + '@@@@@@')` | `connection.send(data)` |
| `close()` | `connection.end()` | `connection.close()` |
| Response handler | Appends `@@@@@@` delimiter | No delimiter |

## Application Events

Both applications emit the following events:

- `starting` — When the application begins initialization
- `response` — After each response is sent (handled by internal response handler)
- `connection` — When a new client connects (emitted on `app.event`)
- `listen` — When the server starts listening (emitted on `app.event`)

## Context Properties

Inside route handlers, the context object includes:

- `context.app` — The application instance
- `context.socket` — The raw connection object
- `context.connection_id` — Unique ID for this connection
- `context.query` — Parsed query parameters
- `context.body` — Parsed request body
- `context.headers` — Request headers (WebSocket only)
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
operating-system: [macos-latest, ubuntu-latest]
node-version: [16, 18, 20]
node-version: [16, 18, 20, 22, 24]
name: Node.js ${{ matrix.node-version }} Test on ${{ matrix.operating-system }}

steps:
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default [...compat.extends("eslint:recommended"), {
beforeEach: true,
},

ecmaVersion: 2018,
ecmaVersion: 2020,
sourceType: "commonjs",

parserOptions: {
Expand Down
7 changes: 7 additions & 0 deletions examples/websocket.server.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const { debug } = require('@axiosleo/cli-tool');
const { WebSocketApplication } = require('../src/apps');
const root = require('./api.router');

Expand All @@ -11,4 +12,10 @@ const app = new WebSocketApplication({
}
});

setInterval(() => {
debug.log('send message');
const res = app.broadcast('Hello, world!', 'ok', 0, null);
debug.log('send message result:', res);
}, 1000);

app.start();
88 changes: 86 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,8 @@ interface KoaContext<
TBody = any,
TQuery = any,
> extends AppContext<TParams, TBody, TQuery> {
/** Application instance */
app: KoaApplication;
/** Route parameters */
params?: TParams;
/** Application configuration */
Expand Down Expand Up @@ -533,6 +535,10 @@ export interface SocketContext<
TBody = any,
TQuery = any,
> extends AppContext<TParams, TBody, TQuery> {
/** Application instance */
app: SocketApplication;
/** Connection ID */
connection_id: string;
/** Route parameters */
params?: TParams;
/** Application configuration */
Expand Down Expand Up @@ -580,6 +586,10 @@ export interface WebSocketContext<
TBody = any,
TQuery = any,
> extends AppContext<TParams, TBody, TQuery> {
/** Application instance */
app: WebSocketApplication;
/** Connection ID */
connection_id: string;
/** Route parameters */
params?: TParams;
/** Application configuration */
Expand Down Expand Up @@ -1236,7 +1246,7 @@ export declare abstract class Application extends EventEmitter {
/** Application identifier */
app_id: string;
/** Application configuration */
config: Configuration;
config: AppConfiguration;

constructor(config: AppConfiguration);

Expand Down Expand Up @@ -1297,10 +1307,19 @@ export declare class SocketClient {
close(): void;
}

export type PingConfig = { open: boolean; interval: number; data: any };

/**
* Socket-based application
*/
export declare class SocketApplication extends Application {
config: AppConfiguration & {
ping?: PingConfig;
};
pingConfig: PingConfig;
/** Active connections indexed by connection ID */
connections: Record<string, any>;

constructor(config: SocketAppConfiguration);

/**
Expand All @@ -1322,12 +1341,60 @@ export declare class SocketApplication extends Application {
code?: number,
connections?: Socket[],
): void;

/**
* Send data to a specific connection
* @param connection Connection to send to
* @param data Data to send
* @param msg Message
* @param code Status code
*/
send(connection: Socket, data?: any, msg?: string, code?: number): boolean;

/**
* Send data to a specific connection by connection ID
* @param connection_id Connection ID to send to
* @param data Data to send
* @param msg Message
* @param code Status code
*/
sendByConnectionId(
connection_id: string,
data?: any,
msg?: string,
code?: number,
): boolean;

/**
* Close a specific connection
* @param connection Connection to close
*/
close(connection: Socket): boolean;

/**
* Close a specific connection by connection ID
*/
closeByConnectionId(connection_id: string): boolean;

/**
* Get a specific connection by connection ID
* @param connection_id Connection ID to get
* @returns Connection or null if not found
*/
getConnection(connection_id: string): Socket | null;

/**
* Ping a specific connection
* @param connection_id Connection ID to ping
*/
ping(connection_id: string): boolean;
}

/**
* WebSocket-based application
*/
export declare class WebSocketApplication extends Application {
export declare class WebSocketApplication extends SocketApplication {
pingConfig: { open: boolean; interval: number; data: any };
constructor(config: WebSocketAppConfiguration);

/**
Expand All @@ -1349,6 +1416,23 @@ export declare class WebSocketApplication extends Application {
code?: number,
connections?: WebSocket[],
): void;

/**
* Send data to a specific connection
* @param connection Connection to send to
* @param data Data to send
* @param msg Message
* @param code Status code
*/
send(connection: WebSocket, data?: any, msg?: string, code?: number): boolean;

/**
* Close a specific connection
* @param connection Connection to close
*/
close(connection: WebSocket): boolean;

getConnection(connection_id: string): WebSocket | null;
}

// ========================================
Expand Down
Loading
Loading