@blokjs/trigger-websocket 0.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 +22 -0
- package/dist/WebSocketTrigger.d.ts +264 -0
- package/dist/WebSocketTrigger.d.ts.map +1 -0
- package/dist/WebSocketTrigger.js +626 -0
- package/dist/WebSocketTrigger.js.map +1 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +117 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
- package/src/WebSocketTrigger.test.ts +490 -0
- package/src/WebSocketTrigger.ts +869 -0
- package/src/WebSocketTriggerMonitoring.test.ts +371 -0
- package/src/index.ts +127 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Trigger Monitoring Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests that the WebSocket trigger properly integrates with the
|
|
5
|
+
* monitoring infrastructure from TriggerBase:
|
|
6
|
+
* - Health checks for WebSocket server dependencies
|
|
7
|
+
* - Rate limiting per-client message throughput
|
|
8
|
+
* - Circuit breaker for downstream workflow failures
|
|
9
|
+
* - Metrics collection (connections, messages, latency)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { HelperResponse } from "@blokjs/helper";
|
|
13
|
+
import type { BlokService } from "@blokjs/runner";
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
15
|
+
import { type WebSocketEvent, WebSocketTrigger } from "./WebSocketTrigger";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Concrete test trigger exposing monitoring methods from TriggerBase
|
|
19
|
+
*/
|
|
20
|
+
class MonitoredWebSocketTrigger extends WebSocketTrigger {
|
|
21
|
+
protected override nodes = {} as Record<string, BlokService<unknown>>;
|
|
22
|
+
protected override workflows = {} as Record<string, HelperResponse>;
|
|
23
|
+
|
|
24
|
+
public getClientsMap() {
|
|
25
|
+
return this.clients;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public getRoomsMap() {
|
|
29
|
+
return this.rooms;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createMockSocket() {
|
|
34
|
+
return {
|
|
35
|
+
send: vi.fn(),
|
|
36
|
+
close: vi.fn(),
|
|
37
|
+
ping: vi.fn(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("WebSocket Trigger - Monitoring Integration", () => {
|
|
42
|
+
let trigger: MonitoredWebSocketTrigger;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
trigger = new MonitoredWebSocketTrigger();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(async () => {
|
|
49
|
+
trigger.destroyMonitoring();
|
|
50
|
+
await trigger.stop();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("Health Checks", () => {
|
|
54
|
+
it("should report healthy when no dependencies registered", async () => {
|
|
55
|
+
const health = await trigger.getHealth();
|
|
56
|
+
expect(health.status).toBe("healthy");
|
|
57
|
+
expect(health.uptime).toBeGreaterThanOrEqual(0);
|
|
58
|
+
expect(Object.keys(health.checks)).toHaveLength(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should support custom WebSocket dependency checks", async () => {
|
|
62
|
+
trigger.registerHealthDependency("ws-server", async () => ({
|
|
63
|
+
status: "healthy",
|
|
64
|
+
message: "WebSocket server running on port 8080",
|
|
65
|
+
lastChecked: Date.now(),
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
const health = await trigger.getHealth();
|
|
69
|
+
expect(health.status).toBe("healthy");
|
|
70
|
+
expect(health.checks["ws-server"].status).toBe("healthy");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should report degraded when WebSocket server has high latency", async () => {
|
|
74
|
+
trigger.registerHealthDependency("ws-server", async () => ({
|
|
75
|
+
status: "degraded",
|
|
76
|
+
message: "High connection latency detected",
|
|
77
|
+
lastChecked: Date.now(),
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
const health = await trigger.getHealth();
|
|
81
|
+
expect(health.status).toBe("degraded");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should pass liveness check even with dependency failures", async () => {
|
|
85
|
+
trigger.registerHealthDependency("external-api", async () => {
|
|
86
|
+
throw new Error("API unreachable");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const liveness = trigger.getLiveness();
|
|
90
|
+
expect(liveness.status).toBe("ok");
|
|
91
|
+
|
|
92
|
+
const readiness = await trigger.getReadiness();
|
|
93
|
+
expect(readiness.ready).toBe(false);
|
|
94
|
+
expect(readiness.status).toBe("unhealthy");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should track connection count as a health indicator", async () => {
|
|
98
|
+
const socket = createMockSocket();
|
|
99
|
+
|
|
100
|
+
// Connect 3 clients
|
|
101
|
+
const client1 = await trigger.handleConnection(socket, {});
|
|
102
|
+
const client2 = await trigger.handleConnection(socket, {});
|
|
103
|
+
const client3 = await trigger.handleConnection(socket, {});
|
|
104
|
+
|
|
105
|
+
expect(client1).not.toBeNull();
|
|
106
|
+
expect(client2).not.toBeNull();
|
|
107
|
+
expect(client3).not.toBeNull();
|
|
108
|
+
|
|
109
|
+
const stats = trigger.getStats();
|
|
110
|
+
expect(stats.activeConnections).toBe(3);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("Rate Limiting", () => {
|
|
115
|
+
it("should rate limit messages per client when enabled", async () => {
|
|
116
|
+
trigger.enableRateLimiting({
|
|
117
|
+
maxTokens: 5,
|
|
118
|
+
refillRate: 2,
|
|
119
|
+
keyStrategy: "client",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// First 5 messages should be allowed
|
|
123
|
+
for (let i = 0; i < 5; i++) {
|
|
124
|
+
const result = trigger.checkRateLimit("client-1");
|
|
125
|
+
expect(result.allowed).toBe(true);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 6th message should be rejected
|
|
129
|
+
const rejected = trigger.checkRateLimit("client-1");
|
|
130
|
+
expect(rejected.allowed).toBe(false);
|
|
131
|
+
expect(rejected.retryAfterMs).toBeGreaterThan(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should isolate rate limits between different clients", () => {
|
|
135
|
+
trigger.enableRateLimiting({
|
|
136
|
+
maxTokens: 3,
|
|
137
|
+
refillRate: 1,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Use up all tokens for client-1
|
|
141
|
+
trigger.checkRateLimit("client-1");
|
|
142
|
+
trigger.checkRateLimit("client-1");
|
|
143
|
+
trigger.checkRateLimit("client-1");
|
|
144
|
+
|
|
145
|
+
// client-1 is rate limited
|
|
146
|
+
expect(trigger.checkRateLimit("client-1").allowed).toBe(false);
|
|
147
|
+
|
|
148
|
+
// client-2 should still have tokens
|
|
149
|
+
expect(trigger.checkRateLimit("client-2").allowed).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should allow unlimited traffic when rate limiting is disabled", () => {
|
|
153
|
+
// Rate limiting not enabled
|
|
154
|
+
for (let i = 0; i < 100; i++) {
|
|
155
|
+
const result = trigger.checkRateLimit(`client-${i}`);
|
|
156
|
+
expect(result.allowed).toBe(true);
|
|
157
|
+
expect(result.remaining).toBe(Number.MAX_SAFE_INTEGER);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("Circuit Breaker", () => {
|
|
163
|
+
it("should be null when not enabled", () => {
|
|
164
|
+
// Access through trigger metrics instead of exposing internal state
|
|
165
|
+
// The circuit breaker is protected, so we test its behavior
|
|
166
|
+
const metrics = trigger.getTriggerMetrics();
|
|
167
|
+
expect(metrics.triggerType).toBe("MonitoredWebSocketTrigger");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should support enabling circuit breaker for workflow execution", () => {
|
|
171
|
+
trigger.enableCircuitBreaker({
|
|
172
|
+
failureThreshold: 5,
|
|
173
|
+
resetTimeoutMs: 30000,
|
|
174
|
+
halfOpenMaxAttempts: 2,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Circuit breaker is now active - verify through health check
|
|
178
|
+
const liveness = trigger.getLiveness();
|
|
179
|
+
expect(liveness.status).toBe("ok");
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("Trigger Metrics", () => {
|
|
184
|
+
it("should collect trigger-level metrics", () => {
|
|
185
|
+
const metrics = trigger.getTriggerMetrics();
|
|
186
|
+
expect(metrics.triggerType).toBe("MonitoredWebSocketTrigger");
|
|
187
|
+
expect(metrics.throughput.totalRequests).toBe(0);
|
|
188
|
+
expect(metrics.latency.count).toBe(0);
|
|
189
|
+
expect(metrics.errors.total).toBe(0);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should track connection counts through metrics", async () => {
|
|
193
|
+
const socket = createMockSocket();
|
|
194
|
+
|
|
195
|
+
await trigger.handleConnection(socket, {});
|
|
196
|
+
await trigger.handleConnection(socket, {});
|
|
197
|
+
|
|
198
|
+
const stats = trigger.getStats();
|
|
199
|
+
expect(stats.activeConnections).toBe(2);
|
|
200
|
+
expect(stats.totalMessages).toBe(0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should track message counts", async () => {
|
|
204
|
+
const socket = createMockSocket();
|
|
205
|
+
const client = await trigger.handleConnection(socket, {});
|
|
206
|
+
expect(client).not.toBeNull();
|
|
207
|
+
|
|
208
|
+
// Handle a text message (won't execute workflow since none configured)
|
|
209
|
+
await trigger.handleMessage(client!.id, '{"event":"test","data":"hello"}', false);
|
|
210
|
+
await trigger.handleMessage(client!.id, '{"event":"test","data":"world"}', false);
|
|
211
|
+
|
|
212
|
+
const stats = trigger.getStats();
|
|
213
|
+
expect(stats.totalMessages).toBe(2);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("Connection Lifecycle with Monitoring", () => {
|
|
218
|
+
it("should track full connection lifecycle", async () => {
|
|
219
|
+
const socket = createMockSocket();
|
|
220
|
+
|
|
221
|
+
// Connect
|
|
222
|
+
const client = await trigger.handleConnection(socket, {});
|
|
223
|
+
expect(client).not.toBeNull();
|
|
224
|
+
expect(trigger.getStats().activeConnections).toBe(1);
|
|
225
|
+
|
|
226
|
+
// Join room
|
|
227
|
+
await trigger.joinRoom(client!.id, "lobby");
|
|
228
|
+
expect(trigger.getStats().roomCount).toBe(1);
|
|
229
|
+
|
|
230
|
+
// Send message
|
|
231
|
+
await trigger.handleMessage(client!.id, '{"event":"chat","data":"hi"}', false);
|
|
232
|
+
expect(trigger.getStats().totalMessages).toBe(1);
|
|
233
|
+
|
|
234
|
+
// Leave room
|
|
235
|
+
await trigger.leaveRoom(client!.id, "lobby");
|
|
236
|
+
expect(trigger.getStats().roomCount).toBe(0);
|
|
237
|
+
|
|
238
|
+
// Disconnect
|
|
239
|
+
await trigger.handleClose(client!.id, 1000, "Normal closure");
|
|
240
|
+
expect(trigger.getStats().activeConnections).toBe(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should clean up monitoring on stop", async () => {
|
|
244
|
+
const socket = createMockSocket();
|
|
245
|
+
|
|
246
|
+
// Connect clients
|
|
247
|
+
await trigger.handleConnection(socket, {});
|
|
248
|
+
await trigger.handleConnection(socket, {});
|
|
249
|
+
await trigger.handleConnection(socket, {});
|
|
250
|
+
|
|
251
|
+
expect(trigger.getStats().activeConnections).toBe(3);
|
|
252
|
+
|
|
253
|
+
// Stop trigger
|
|
254
|
+
await trigger.stop();
|
|
255
|
+
|
|
256
|
+
expect(trigger.getStats().activeConnections).toBe(0);
|
|
257
|
+
expect(trigger.getClientsMap().size).toBe(0);
|
|
258
|
+
expect(trigger.getRoomsMap().size).toBe(0);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("Multi-Client Broadcasting with Monitoring", () => {
|
|
263
|
+
it("should broadcast to room and track metrics", async () => {
|
|
264
|
+
const socket1 = createMockSocket();
|
|
265
|
+
const socket2 = createMockSocket();
|
|
266
|
+
const socket3 = createMockSocket();
|
|
267
|
+
|
|
268
|
+
const client1 = await trigger.handleConnection(socket1, {});
|
|
269
|
+
const client2 = await trigger.handleConnection(socket2, {});
|
|
270
|
+
const client3 = await trigger.handleConnection(socket3, {});
|
|
271
|
+
|
|
272
|
+
// Join room
|
|
273
|
+
await trigger.joinRoom(client1!.id, "chat");
|
|
274
|
+
await trigger.joinRoom(client2!.id, "chat");
|
|
275
|
+
// client3 does NOT join the room
|
|
276
|
+
|
|
277
|
+
// Broadcast to room (excluding sender)
|
|
278
|
+
const sent = trigger.broadcastToRoom("chat", "msg", { text: "hello" }, client1!.id);
|
|
279
|
+
expect(sent).toBe(1); // Only client2 receives
|
|
280
|
+
|
|
281
|
+
// Verify socket2 received the message
|
|
282
|
+
expect(socket2.send).toHaveBeenCalledTimes(1);
|
|
283
|
+
const sentData = JSON.parse(socket2.send.mock.calls[0][0]);
|
|
284
|
+
expect(sentData.event).toBe("msg");
|
|
285
|
+
expect(sentData.data.text).toBe("hello");
|
|
286
|
+
|
|
287
|
+
// socket1 (sender, excluded) and socket3 (not in room) should not receive
|
|
288
|
+
expect(socket1.send).not.toHaveBeenCalled();
|
|
289
|
+
expect(socket3.send).not.toHaveBeenCalled();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("should broadcast to all clients", async () => {
|
|
293
|
+
const sockets = Array.from({ length: 5 }, () => createMockSocket());
|
|
294
|
+
const clients = await Promise.all(sockets.map((s) => trigger.handleConnection(s, {})));
|
|
295
|
+
|
|
296
|
+
const sent = trigger.broadcastToAll("notification", { message: "Server restart" });
|
|
297
|
+
expect(sent).toBe(5);
|
|
298
|
+
|
|
299
|
+
for (const socket of sockets) {
|
|
300
|
+
expect(socket.send).toHaveBeenCalledTimes(1);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe("Authentication with Monitoring", () => {
|
|
306
|
+
it("should reject unauthenticated connections", async () => {
|
|
307
|
+
trigger.setAuthHandler(async (_req, headers) => {
|
|
308
|
+
if (headers.authorization === "Bearer valid-token") {
|
|
309
|
+
return { authenticated: true, clientId: "auth-user-1" };
|
|
310
|
+
}
|
|
311
|
+
return { authenticated: false, error: "Invalid token" };
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const socket = createMockSocket();
|
|
315
|
+
|
|
316
|
+
// Without valid auth
|
|
317
|
+
const result = await trigger.handleConnection(socket, {}, {});
|
|
318
|
+
expect(result).toBeNull();
|
|
319
|
+
expect(socket.close).toHaveBeenCalledWith(4001, "Invalid token");
|
|
320
|
+
|
|
321
|
+
// With valid auth
|
|
322
|
+
const socket2 = createMockSocket();
|
|
323
|
+
const authedClient = await trigger.handleConnection(
|
|
324
|
+
socket2,
|
|
325
|
+
{},
|
|
326
|
+
{
|
|
327
|
+
authorization: "Bearer valid-token",
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
expect(authedClient).not.toBeNull();
|
|
331
|
+
expect(authedClient!.id).toBe("auth-user-1");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("should track authenticated connections in metrics", async () => {
|
|
335
|
+
trigger.setAuthHandler(async () => ({
|
|
336
|
+
authenticated: true,
|
|
337
|
+
clientId: "user-1",
|
|
338
|
+
metadata: { role: "admin" },
|
|
339
|
+
}));
|
|
340
|
+
|
|
341
|
+
const socket = createMockSocket();
|
|
342
|
+
const client = await trigger.handleConnection(socket, {}, {});
|
|
343
|
+
|
|
344
|
+
expect(client).not.toBeNull();
|
|
345
|
+
expect(client!.metadata.role).toBe("admin");
|
|
346
|
+
expect(trigger.getStats().activeConnections).toBe(1);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe("Max Connections Enforcement", () => {
|
|
351
|
+
it("should reject connections when at capacity", async () => {
|
|
352
|
+
// Override max clients to a small number for testing
|
|
353
|
+
(trigger as unknown as { maxClients: number }).maxClients = 3;
|
|
354
|
+
|
|
355
|
+
const sockets = Array.from({ length: 4 }, () => createMockSocket());
|
|
356
|
+
|
|
357
|
+
// First 3 should succeed
|
|
358
|
+
for (let i = 0; i < 3; i++) {
|
|
359
|
+
const client = await trigger.handleConnection(sockets[i], {});
|
|
360
|
+
expect(client).not.toBeNull();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// 4th should be rejected
|
|
364
|
+
const rejected = await trigger.handleConnection(sockets[3], {});
|
|
365
|
+
expect(rejected).toBeNull();
|
|
366
|
+
expect(sockets[3].close).toHaveBeenCalledWith(1013, "Server at capacity");
|
|
367
|
+
|
|
368
|
+
expect(trigger.getStats().activeConnections).toBe(3);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @blokjs/trigger-websocket
|
|
3
|
+
*
|
|
4
|
+
* WebSocket trigger for Blok workflows.
|
|
5
|
+
* Handle real-time bidirectional communication.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Connection management (connect, disconnect, reconnect)
|
|
9
|
+
* - Room/channel support for broadcasting
|
|
10
|
+
* - Message routing to workflows
|
|
11
|
+
* - Heartbeat/ping-pong for connection health
|
|
12
|
+
* - Authentication middleware
|
|
13
|
+
* - Binary message support
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { WebSocketTrigger } from "@blokjs/trigger-websocket";
|
|
18
|
+
* import { WebSocketServer } from "ws";
|
|
19
|
+
*
|
|
20
|
+
* class MyWebSocketTrigger extends WebSocketTrigger {
|
|
21
|
+
* protected nodes = myNodes;
|
|
22
|
+
* protected workflows = myWorkflows;
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* const trigger = new MyWebSocketTrigger();
|
|
26
|
+
* await trigger.listen();
|
|
27
|
+
*
|
|
28
|
+
* // Create WebSocket server
|
|
29
|
+
* const wss = new WebSocketServer({ port: 8080 });
|
|
30
|
+
*
|
|
31
|
+
* wss.on("connection", async (ws, req) => {
|
|
32
|
+
* const headers = req.headers as Record<string, string>;
|
|
33
|
+
* const client = await trigger.handleConnection(
|
|
34
|
+
* {
|
|
35
|
+
* send: (data) => ws.send(data),
|
|
36
|
+
* close: (code, reason) => ws.close(code, reason),
|
|
37
|
+
* ping: () => ws.ping(),
|
|
38
|
+
* },
|
|
39
|
+
* req,
|
|
40
|
+
* headers
|
|
41
|
+
* );
|
|
42
|
+
*
|
|
43
|
+
* if (!client) return;
|
|
44
|
+
*
|
|
45
|
+
* ws.on("message", async (data, isBinary) => {
|
|
46
|
+
* await trigger.handleMessage(client.id, data, isBinary);
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* ws.on("close", (code, reason) => {
|
|
50
|
+
* trigger.handleClose(client.id, code, reason.toString());
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* ws.on("error", (error) => {
|
|
54
|
+
* trigger.handleError(client.id, error);
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* ws.on("ping", () => trigger.handlePing(client.id));
|
|
58
|
+
* ws.on("pong", () => trigger.handlePong(client.id));
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* Workflow Definition:
|
|
63
|
+
* ```typescript
|
|
64
|
+
* Workflow({ name: "chat-message", version: "1.0.0" })
|
|
65
|
+
* .addTrigger("websocket", {
|
|
66
|
+
* events: ["message", "chat.*"],
|
|
67
|
+
* rooms: ["general", "support"],
|
|
68
|
+
* })
|
|
69
|
+
* .addStep({ ... });
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* Authentication:
|
|
73
|
+
* ```typescript
|
|
74
|
+
* trigger.setAuthHandler(async (request, headers) => {
|
|
75
|
+
* const token = headers["authorization"]?.replace("Bearer ", "");
|
|
76
|
+
* if (!token) {
|
|
77
|
+
* return { authenticated: false, error: "No token provided" };
|
|
78
|
+
* }
|
|
79
|
+
*
|
|
80
|
+
* const user = await verifyToken(token);
|
|
81
|
+
* if (!user) {
|
|
82
|
+
* return { authenticated: false, error: "Invalid token" };
|
|
83
|
+
* }
|
|
84
|
+
*
|
|
85
|
+
* return {
|
|
86
|
+
* authenticated: true,
|
|
87
|
+
* clientId: user.id,
|
|
88
|
+
* metadata: { userId: user.id, role: user.role },
|
|
89
|
+
* };
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* Room Management:
|
|
94
|
+
* ```typescript
|
|
95
|
+
* // Join a room
|
|
96
|
+
* await trigger.joinRoom(clientId, "room-name");
|
|
97
|
+
*
|
|
98
|
+
* // Leave a room
|
|
99
|
+
* await trigger.leaveRoom(clientId, "room-name");
|
|
100
|
+
*
|
|
101
|
+
* // Broadcast to room
|
|
102
|
+
* trigger.broadcastToRoom("room-name", "event", { message: "Hello!" });
|
|
103
|
+
*
|
|
104
|
+
* // Send to specific client
|
|
105
|
+
* trigger.sendToClient(clientId, "event", { message: "Private message" });
|
|
106
|
+
*
|
|
107
|
+
* // Broadcast to all
|
|
108
|
+
* trigger.broadcastToAll("event", { message: "System message" });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
// Core exports
|
|
113
|
+
export {
|
|
114
|
+
WebSocketTrigger,
|
|
115
|
+
type WebSocketMessage,
|
|
116
|
+
type WebSocketMessageType,
|
|
117
|
+
type WebSocketState,
|
|
118
|
+
type WebSocketClient,
|
|
119
|
+
type WebSocketRoom,
|
|
120
|
+
type WebSocketEventType,
|
|
121
|
+
type WebSocketEvent,
|
|
122
|
+
type AuthResult,
|
|
123
|
+
type AuthHandler,
|
|
124
|
+
} from "./WebSocketTrigger";
|
|
125
|
+
|
|
126
|
+
// Re-export types from helper for convenience
|
|
127
|
+
export type { WebSocketTriggerOpts } from "@blokjs/helper";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ts-node": {
|
|
3
|
+
"transpileOnly": true
|
|
4
|
+
},
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"target": "ES2022",
|
|
7
|
+
"module": "es2022",
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"outDir": "./dist",
|
|
13
|
+
"rootDir": "./src",
|
|
14
|
+
"strict": true,
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"forceConsistentCasingInFileNames": true,
|
|
18
|
+
"resolveJsonModule": true
|
|
19
|
+
},
|
|
20
|
+
"include": ["src/**/*"],
|
|
21
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
22
|
+
}
|