@furystack/websocket-api 13.1.13 → 13.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/esm/websocket-api.d.ts +24 -1
- package/esm/websocket-api.d.ts.map +1 -1
- package/esm/websocket-api.js +25 -8
- package/esm/websocket-api.js.map +1 -1
- package/esm/websocket-api.spec.js +53 -1
- package/esm/websocket-api.spec.js.map +1 -1
- package/package.json +7 -7
- package/src/websocket-api.spec.ts +68 -1
- package/src/websocket-api.ts +39 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [13.2.0] - 2026-03-03
|
|
4
|
+
|
|
5
|
+
### ✨ Features
|
|
6
|
+
|
|
7
|
+
### Lifecycle and error events via `EventHub`
|
|
8
|
+
|
|
9
|
+
`WebSocketApi` now extends `EventHub` and emits structured events:
|
|
10
|
+
|
|
11
|
+
- `onError` — emitted when action execution fails (`canExecute`, `getInstance`, or `execute`), or when a WebSocket error occurs
|
|
12
|
+
- `onClientConnected` — emitted when a client connects, with `{ ws, message }`
|
|
13
|
+
- `onClientDisconnected` — emitted when a client disconnects, with `{ ws }`
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
webSocketApi.addListener('onError', ({ error, data, socket }) => {
|
|
17
|
+
logger.error('WebSocket action error', { error })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
webSocketApi.addListener('onClientConnected', ({ ws, message }) => {
|
|
21
|
+
logger.info('Client connected')
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 🐛 Bug Fixes
|
|
26
|
+
|
|
27
|
+
- Action execution now properly waits for async `execute()` to complete before calling `Symbol.dispose` on the action — the previous `using()` pattern disposed immediately while execution was still running
|
|
28
|
+
|
|
29
|
+
### ♻️ Refactoring
|
|
30
|
+
|
|
31
|
+
- Action execution now uses explicit try/catch and `Promise.resolve().then()` instead of `using()`, ensuring errors are properly caught and emitted rather than silently dropped
|
|
32
|
+
|
|
33
|
+
### ⬆️ Dependencies
|
|
34
|
+
|
|
35
|
+
- Updated `@furystack/rest-service` with improved error handling for malformed requests
|
|
36
|
+
|
|
37
|
+
## [13.1.14] - 2026-02-27
|
|
38
|
+
|
|
39
|
+
### ⬆️ Dependencies
|
|
40
|
+
|
|
41
|
+
- Updated `@furystack/rest-service` dependency
|
|
42
|
+
|
|
3
43
|
## [13.1.13] - 2026-02-26
|
|
4
44
|
|
|
5
45
|
### ⬆️ Dependencies
|
package/esm/websocket-api.d.ts
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
import type { Injector } from '@furystack/inject';
|
|
2
|
+
import { EventHub, type ListenerErrorPayload } from '@furystack/utils';
|
|
2
3
|
import { IncomingMessage } from 'http';
|
|
3
4
|
import type WebSocket from 'ws';
|
|
4
5
|
import type { Data } from 'ws';
|
|
5
6
|
import ws from 'ws';
|
|
7
|
+
/**
|
|
8
|
+
* Events emitted by the {@link WebSocketApi}
|
|
9
|
+
*/
|
|
10
|
+
export type WebSocketApiEvents = {
|
|
11
|
+
/** Emitted when an error occurs during action execution (canExecute, getInstance, or execute) */
|
|
12
|
+
onError: {
|
|
13
|
+
error: unknown;
|
|
14
|
+
data?: Data;
|
|
15
|
+
socket?: WebSocket;
|
|
16
|
+
};
|
|
17
|
+
/** Emitted when a client connects */
|
|
18
|
+
onClientConnected: {
|
|
19
|
+
ws: WebSocket;
|
|
20
|
+
message: IncomingMessage;
|
|
21
|
+
};
|
|
22
|
+
/** Emitted when a client disconnects */
|
|
23
|
+
onClientDisconnected: {
|
|
24
|
+
ws: WebSocket;
|
|
25
|
+
};
|
|
26
|
+
/** Emitted when an event listener throws or rejects */
|
|
27
|
+
onListenerError: ListenerErrorPayload;
|
|
28
|
+
};
|
|
6
29
|
/**
|
|
7
30
|
* A WebSocket API implementation for FuryStack
|
|
8
31
|
*/
|
|
9
|
-
export declare class WebSocketApi implements AsyncDisposable {
|
|
32
|
+
export declare class WebSocketApi extends EventHub<WebSocketApiEvents> implements AsyncDisposable {
|
|
10
33
|
readonly socket: import("ws").Server<typeof WebSocket, typeof IncomingMessage>;
|
|
11
34
|
private clients;
|
|
12
35
|
private readonly settings;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket-api.d.ts","sourceRoot":"","sources":["../src/websocket-api.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"websocket-api.d.ts","sourceRoot":"","sources":["../src/websocket-api.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAGjD,OAAO,EAAE,QAAQ,EAAE,KAAK,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,MAAM,CAAA;AAEtC,OAAO,KAAK,SAAS,MAAM,IAAI,CAAA;AAC/B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAA;AAC9B,OAAO,EAAuB,MAAM,IAAI,CAAA;AAIxC;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iGAAiG;IACjG,OAAO,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,IAAI,CAAC;QAAC,MAAM,CAAC,EAAE,SAAS,CAAA;KAAE,CAAA;IAC5D,qCAAqC;IACrC,iBAAiB,EAAE;QAAE,EAAE,EAAE,SAAS,CAAC;QAAC,OAAO,EAAE,eAAe,CAAA;KAAE,CAAA;IAC9D,wCAAwC;IACxC,oBAAoB,EAAE;QAAE,EAAE,EAAE,SAAS,CAAA;KAAE,CAAA;IACvC,uDAAuD;IACvD,eAAe,EAAE,oBAAoB,CAAA;CACtC,CAAA;AAED;;GAEG;AACH,qBACa,YAAa,SAAQ,QAAQ,CAAC,kBAAkB,CAAE,YAAW,eAAe;IACvF,SAAgB,MAAM,gEAA0C;IAEhE,OAAO,CAAC,OAAO,CAA0E;IAEzF,iBACyB,QAAQ,CAAsB;IAEvD,iBACyB,aAAa,CAAe;IAErD,iBAAyB,QAAQ,CAAU;IAE3C,OAAO,CAAC,aAAa,CAAQ;IAChB,IAAI;IAyDJ,CAAC,MAAM,CAAC,YAAY,CAAC;IAUrB,SAAS,CACpB,QAAQ,EAAE,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC;QAAC,EAAE,EAAE,EAAE,CAAC;QAAC,OAAO,EAAE,eAAe,CAAA;KAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBhG,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS;CAkB3F"}
|
package/esm/websocket-api.js
CHANGED
|
@@ -10,7 +10,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
10
10
|
import { AggregatedError, IdentityContext } from '@furystack/core';
|
|
11
11
|
import { Injectable, Injected } from '@furystack/inject';
|
|
12
12
|
import { HttpUserContext, ServerManager } from '@furystack/rest-service';
|
|
13
|
-
import {
|
|
13
|
+
import { EventHub } from '@furystack/utils';
|
|
14
14
|
import { IncomingMessage } from 'http';
|
|
15
15
|
import { URL } from 'url';
|
|
16
16
|
import ws, { WebSocketServer } from 'ws';
|
|
@@ -18,7 +18,7 @@ import { WebSocketApiSettings } from './websocket-api-settings.js';
|
|
|
18
18
|
/**
|
|
19
19
|
* A WebSocket API implementation for FuryStack
|
|
20
20
|
*/
|
|
21
|
-
let WebSocketApi = class WebSocketApi {
|
|
21
|
+
let WebSocketApi = class WebSocketApi extends EventHub {
|
|
22
22
|
socket = new WebSocketServer({ noServer: true });
|
|
23
23
|
clients = new Map();
|
|
24
24
|
isInitialized = false;
|
|
@@ -33,13 +33,18 @@ let WebSocketApi = class WebSocketApi {
|
|
|
33
33
|
isAuthenticated: () => httpUserContext.isAuthenticated(msg),
|
|
34
34
|
}, IdentityContext);
|
|
35
35
|
this.clients.set(websocket, { injector: connectionInjector, message: msg, ws: websocket });
|
|
36
|
+
this.emit('onClientConnected', { ws: websocket, message: msg });
|
|
36
37
|
websocket.on('message', (message) => {
|
|
37
38
|
this.execute(message, msg, connectionInjector, websocket);
|
|
38
39
|
});
|
|
40
|
+
websocket.on('error', (error) => {
|
|
41
|
+
this.emit('onError', { error, socket: websocket });
|
|
42
|
+
});
|
|
39
43
|
websocket.on('close', () => {
|
|
40
44
|
this.clients.delete(websocket);
|
|
45
|
+
this.emit('onClientDisconnected', { ws: websocket });
|
|
41
46
|
connectionInjector[Symbol.asyncDispose]().catch((err) => {
|
|
42
|
-
|
|
47
|
+
this.emit('onError', { error: err, socket: websocket });
|
|
43
48
|
});
|
|
44
49
|
});
|
|
45
50
|
});
|
|
@@ -72,6 +77,7 @@ let WebSocketApi = class WebSocketApi {
|
|
|
72
77
|
// Dispose all child injectors
|
|
73
78
|
await Promise.allSettled([...this.clients.values()].map((client) => client.injector[Symbol.asyncDispose]()));
|
|
74
79
|
this.clients.clear();
|
|
80
|
+
super[Symbol.dispose]();
|
|
75
81
|
}
|
|
76
82
|
async broadcast(callback) {
|
|
77
83
|
const errors = [];
|
|
@@ -90,11 +96,22 @@ let WebSocketApi = class WebSocketApi {
|
|
|
90
96
|
}
|
|
91
97
|
}
|
|
92
98
|
execute(data, request, injector, socket) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
try {
|
|
100
|
+
const Action = this.settings.actions.find((a) => a.canExecute({ data, request, socket }));
|
|
101
|
+
if (Action) {
|
|
102
|
+
const action = injector.getInstance(Action);
|
|
103
|
+
Promise.resolve()
|
|
104
|
+
.then(() => action.execute({ data, request, socket }))
|
|
105
|
+
.catch((error) => {
|
|
106
|
+
this.emit('onError', { error, data, socket });
|
|
107
|
+
})
|
|
108
|
+
.finally(() => {
|
|
109
|
+
action[Symbol.dispose]();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
this.emit('onError', { error, data, socket });
|
|
98
115
|
}
|
|
99
116
|
}
|
|
100
117
|
};
|
package/esm/websocket-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket-api.js","sourceRoot":"","sources":["../src/websocket-api.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,eAAe,EAAE,eAAe,EAAa,MAAM,iBAAiB,CAAA;AAE7E,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACxE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"websocket-api.js","sourceRoot":"","sources":["../src/websocket-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,7 +11,7 @@ import { getRepository } from '@furystack/repository';
|
|
|
11
11
|
import { DefaultSession } from '@furystack/rest-service';
|
|
12
12
|
import { PasswordCredential, PasswordResetToken, usePasswordPolicy } from '@furystack/security';
|
|
13
13
|
import { usingAsync } from '@furystack/utils';
|
|
14
|
-
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
15
15
|
import { WebSocket } from 'ws';
|
|
16
16
|
import { useWebsockets } from './helpers.js';
|
|
17
17
|
import { WebSocketApi } from './websocket-api.js';
|
|
@@ -69,6 +69,58 @@ describe('WebSocketApi', () => {
|
|
|
69
69
|
}));
|
|
70
70
|
});
|
|
71
71
|
});
|
|
72
|
+
it('Should emit onClientConnected and onClientDisconnected events', async () => {
|
|
73
|
+
const port = getPort();
|
|
74
|
+
await usingAsync(new Injector(), async (i) => {
|
|
75
|
+
setupStoresAndDataSets(i);
|
|
76
|
+
await useWebsockets(i, { path: '/ws-events', port });
|
|
77
|
+
const api = i.getInstance(WebSocketApi);
|
|
78
|
+
const connectedHandler = vi.fn();
|
|
79
|
+
const disconnectedHandler = vi.fn();
|
|
80
|
+
api.addListener('onClientConnected', connectedHandler);
|
|
81
|
+
api.addListener('onClientDisconnected', disconnectedHandler);
|
|
82
|
+
const client = new WebSocket(`ws://localhost:${port}/ws-events`);
|
|
83
|
+
await new Promise((resolve) => client.once('open', () => resolve()));
|
|
84
|
+
expect(connectedHandler).toHaveBeenCalled();
|
|
85
|
+
expect(connectedHandler).toHaveBeenCalledWith(expect.objectContaining({ ws: expect.any(Object), message: expect.any(Object) }));
|
|
86
|
+
client.close();
|
|
87
|
+
await new Promise((resolve) => client.once('close', () => resolve()));
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
89
|
+
expect(disconnectedHandler).toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
it('Should emit onError when action execution throws', async () => {
|
|
93
|
+
const port = getPort();
|
|
94
|
+
await usingAsync(new Injector(), async (i) => {
|
|
95
|
+
setupStoresAndDataSets(i);
|
|
96
|
+
let FailingAction = class FailingAction {
|
|
97
|
+
[Symbol.dispose]() {
|
|
98
|
+
/** */
|
|
99
|
+
}
|
|
100
|
+
static canExecute() {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
async execute() {
|
|
104
|
+
throw new Error('action failed');
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
FailingAction = __decorate([
|
|
108
|
+
Injectable()
|
|
109
|
+
], FailingAction);
|
|
110
|
+
await useWebsockets(i, { path: '/ws-error-test', port, actions: [FailingAction] });
|
|
111
|
+
const api = i.getInstance(WebSocketApi);
|
|
112
|
+
const errorHandler = vi.fn();
|
|
113
|
+
api.addListener('onError', errorHandler);
|
|
114
|
+
const client = new WebSocket(`ws://localhost:${port}/ws-error-test`);
|
|
115
|
+
await new Promise((resolve) => client.once('open', () => resolve()));
|
|
116
|
+
await new Promise((resolve, reject) => client.send('trigger', (err) => (err ? reject(err) : resolve())));
|
|
117
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
118
|
+
expect(errorHandler).toHaveBeenCalled();
|
|
119
|
+
expect(errorHandler).toHaveBeenCalledWith(expect.objectContaining({ error: expect.any(Error) }));
|
|
120
|
+
client.close();
|
|
121
|
+
await new Promise((resolve) => client.once('close', () => resolve()));
|
|
122
|
+
});
|
|
123
|
+
});
|
|
72
124
|
it('Should receive client messages', async () => {
|
|
73
125
|
const port = getPort();
|
|
74
126
|
await usingAsync(new Injector(), async (i) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket-api.spec.js","sourceRoot":"","sources":["../src/websocket-api.spec.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAA;AACxD,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAC/F,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"websocket-api.spec.js","sourceRoot":"","sources":["../src/websocket-api.spec.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@furystack/websocket-api",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.2.0",
|
|
4
4
|
"description": "WebSocket API implementation for FuryStack",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -37,15 +37,15 @@
|
|
|
37
37
|
},
|
|
38
38
|
"homepage": "https://github.com/furystack/furystack",
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@furystack/core": "^15.2.
|
|
41
|
-
"@furystack/inject": "^12.0.
|
|
42
|
-
"@furystack/rest-service": "^12.
|
|
43
|
-
"@furystack/utils": "^8.
|
|
40
|
+
"@furystack/core": "^15.2.3",
|
|
41
|
+
"@furystack/inject": "^12.0.31",
|
|
42
|
+
"@furystack/rest-service": "^12.2.0",
|
|
43
|
+
"@furystack/utils": "^8.2.0",
|
|
44
44
|
"ws": "^8.19.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@furystack/repository": "^10.1.
|
|
48
|
-
"@furystack/security": "^7.0.
|
|
47
|
+
"@furystack/repository": "^10.1.4",
|
|
48
|
+
"@furystack/security": "^7.0.2",
|
|
49
49
|
"@types/ws": "^8.18.1",
|
|
50
50
|
"typescript": "^5.9.3",
|
|
51
51
|
"vitest": "^4.0.18"
|
|
@@ -5,7 +5,7 @@ import { getRepository } from '@furystack/repository'
|
|
|
5
5
|
import { DefaultSession } from '@furystack/rest-service'
|
|
6
6
|
import { PasswordCredential, PasswordResetToken, usePasswordPolicy } from '@furystack/security'
|
|
7
7
|
import { usingAsync } from '@furystack/utils'
|
|
8
|
-
import { describe, expect, it } from 'vitest'
|
|
8
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
9
9
|
import { WebSocket, type Data } from 'ws'
|
|
10
10
|
import { useWebsockets } from './helpers.js'
|
|
11
11
|
import type { WebSocketAction } from './models/websocket-action.js'
|
|
@@ -83,6 +83,73 @@ describe('WebSocketApi', () => {
|
|
|
83
83
|
})
|
|
84
84
|
})
|
|
85
85
|
|
|
86
|
+
it('Should emit onClientConnected and onClientDisconnected events', async () => {
|
|
87
|
+
const port = getPort()
|
|
88
|
+
await usingAsync(new Injector(), async (i) => {
|
|
89
|
+
setupStoresAndDataSets(i)
|
|
90
|
+
await useWebsockets(i, { path: '/ws-events', port })
|
|
91
|
+
const api = i.getInstance(WebSocketApi)
|
|
92
|
+
|
|
93
|
+
const connectedHandler = vi.fn()
|
|
94
|
+
const disconnectedHandler = vi.fn()
|
|
95
|
+
api.addListener('onClientConnected', connectedHandler)
|
|
96
|
+
api.addListener('onClientDisconnected', disconnectedHandler)
|
|
97
|
+
|
|
98
|
+
const client = new WebSocket(`ws://localhost:${port}/ws-events`)
|
|
99
|
+
await new Promise<void>((resolve) => client.once('open', () => resolve()))
|
|
100
|
+
|
|
101
|
+
expect(connectedHandler).toHaveBeenCalled()
|
|
102
|
+
expect(connectedHandler).toHaveBeenCalledWith(
|
|
103
|
+
expect.objectContaining({ ws: expect.any(Object) as object, message: expect.any(Object) as object }),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
client.close()
|
|
107
|
+
await new Promise<void>((resolve) => client.once('close', () => resolve()))
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
109
|
+
|
|
110
|
+
expect(disconnectedHandler).toHaveBeenCalled()
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('Should emit onError when action execution throws', async () => {
|
|
115
|
+
const port = getPort()
|
|
116
|
+
await usingAsync(new Injector(), async (i) => {
|
|
117
|
+
setupStoresAndDataSets(i)
|
|
118
|
+
|
|
119
|
+
@Injectable()
|
|
120
|
+
class FailingAction implements WebSocketAction {
|
|
121
|
+
public [Symbol.dispose]() {
|
|
122
|
+
/** */
|
|
123
|
+
}
|
|
124
|
+
public static canExecute() {
|
|
125
|
+
return true
|
|
126
|
+
}
|
|
127
|
+
public async execute(): Promise<void> {
|
|
128
|
+
throw new Error('action failed')
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await useWebsockets(i, { path: '/ws-error-test', port, actions: [FailingAction] })
|
|
133
|
+
const api = i.getInstance(WebSocketApi)
|
|
134
|
+
|
|
135
|
+
const errorHandler = vi.fn()
|
|
136
|
+
api.addListener('onError', errorHandler)
|
|
137
|
+
|
|
138
|
+
const client = new WebSocket(`ws://localhost:${port}/ws-error-test`)
|
|
139
|
+
await new Promise<void>((resolve) => client.once('open', () => resolve()))
|
|
140
|
+
|
|
141
|
+
await new Promise<void>((resolve, reject) => client.send('trigger', (err) => (err ? reject(err) : resolve())))
|
|
142
|
+
|
|
143
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
144
|
+
|
|
145
|
+
expect(errorHandler).toHaveBeenCalled()
|
|
146
|
+
expect(errorHandler).toHaveBeenCalledWith(expect.objectContaining({ error: expect.any(Error) as Error }))
|
|
147
|
+
|
|
148
|
+
client.close()
|
|
149
|
+
await new Promise<void>((resolve) => client.once('close', () => resolve()))
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
86
153
|
it('Should receive client messages', async () => {
|
|
87
154
|
const port = getPort()
|
|
88
155
|
await usingAsync(new Injector(), async (i) => {
|
package/src/websocket-api.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { AggregatedError, IdentityContext, type User } from '@furystack/core'
|
|
|
2
2
|
import type { Injector } from '@furystack/inject'
|
|
3
3
|
import { Injectable, Injected } from '@furystack/inject'
|
|
4
4
|
import { HttpUserContext, ServerManager } from '@furystack/rest-service'
|
|
5
|
-
import {
|
|
5
|
+
import { EventHub, type ListenerErrorPayload } from '@furystack/utils'
|
|
6
6
|
import { IncomingMessage } from 'http'
|
|
7
7
|
import { URL } from 'url'
|
|
8
8
|
import type WebSocket from 'ws'
|
|
@@ -11,11 +11,25 @@ import ws, { WebSocketServer } from 'ws'
|
|
|
11
11
|
import type { WebSocketAction } from './models/websocket-action.js'
|
|
12
12
|
import { WebSocketApiSettings } from './websocket-api-settings.js'
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Events emitted by the {@link WebSocketApi}
|
|
16
|
+
*/
|
|
17
|
+
export type WebSocketApiEvents = {
|
|
18
|
+
/** Emitted when an error occurs during action execution (canExecute, getInstance, or execute) */
|
|
19
|
+
onError: { error: unknown; data?: Data; socket?: WebSocket }
|
|
20
|
+
/** Emitted when a client connects */
|
|
21
|
+
onClientConnected: { ws: WebSocket; message: IncomingMessage }
|
|
22
|
+
/** Emitted when a client disconnects */
|
|
23
|
+
onClientDisconnected: { ws: WebSocket }
|
|
24
|
+
/** Emitted when an event listener throws or rejects */
|
|
25
|
+
onListenerError: ListenerErrorPayload
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
/**
|
|
15
29
|
* A WebSocket API implementation for FuryStack
|
|
16
30
|
*/
|
|
17
31
|
@Injectable({ lifetime: 'scoped' })
|
|
18
|
-
export class WebSocketApi implements AsyncDisposable {
|
|
32
|
+
export class WebSocketApi extends EventHub<WebSocketApiEvents> implements AsyncDisposable {
|
|
19
33
|
public readonly socket = new WebSocketServer({ noServer: true })
|
|
20
34
|
|
|
21
35
|
private clients = new Map<ws, { injector: Injector; ws: ws; message: IncomingMessage }>()
|
|
@@ -45,14 +59,20 @@ export class WebSocketApi implements AsyncDisposable {
|
|
|
45
59
|
)
|
|
46
60
|
|
|
47
61
|
this.clients.set(websocket, { injector: connectionInjector, message: msg, ws: websocket })
|
|
62
|
+
this.emit('onClientConnected', { ws: websocket, message: msg })
|
|
48
63
|
websocket.on('message', (message) => {
|
|
49
64
|
this.execute(message, msg, connectionInjector, websocket)
|
|
50
65
|
})
|
|
51
66
|
|
|
67
|
+
websocket.on('error', (error) => {
|
|
68
|
+
this.emit('onError', { error, socket: websocket })
|
|
69
|
+
})
|
|
70
|
+
|
|
52
71
|
websocket.on('close', () => {
|
|
53
72
|
this.clients.delete(websocket)
|
|
73
|
+
this.emit('onClientDisconnected', { ws: websocket })
|
|
54
74
|
connectionInjector[Symbol.asyncDispose]().catch((err) => {
|
|
55
|
-
|
|
75
|
+
this.emit('onError', { error: err, socket: websocket })
|
|
56
76
|
})
|
|
57
77
|
})
|
|
58
78
|
})
|
|
@@ -87,6 +107,7 @@ export class WebSocketApi implements AsyncDisposable {
|
|
|
87
107
|
// Dispose all child injectors
|
|
88
108
|
await Promise.allSettled([...this.clients.values()].map((client) => client.injector[Symbol.asyncDispose]()))
|
|
89
109
|
this.clients.clear()
|
|
110
|
+
super[Symbol.dispose]()
|
|
90
111
|
}
|
|
91
112
|
|
|
92
113
|
public async broadcast(
|
|
@@ -110,11 +131,21 @@ export class WebSocketApi implements AsyncDisposable {
|
|
|
110
131
|
}
|
|
111
132
|
|
|
112
133
|
public execute(data: Data, request: IncomingMessage, injector: Injector, socket: WebSocket) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
134
|
+
try {
|
|
135
|
+
const Action = this.settings.actions.find((a) => a.canExecute({ data, request, socket }))
|
|
136
|
+
if (Action) {
|
|
137
|
+
const action = injector.getInstance<WebSocketAction>(Action)
|
|
138
|
+
Promise.resolve()
|
|
139
|
+
.then(() => action.execute({ data, request, socket }))
|
|
140
|
+
.catch((error: unknown) => {
|
|
141
|
+
this.emit('onError', { error, data, socket })
|
|
142
|
+
})
|
|
143
|
+
.finally(() => {
|
|
144
|
+
action[Symbol.dispose]()
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
this.emit('onError', { error, data, socket })
|
|
118
149
|
}
|
|
119
150
|
}
|
|
120
151
|
}
|