@rlanz/socket 0.0.1-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.
Files changed (44) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +387 -0
  3. package/build/chunk-4UPUCRVG.js +1772 -0
  4. package/build/chunk-4UPUCRVG.js.map +1 -0
  5. package/build/chunk-GKAD2UOA.js +18 -0
  6. package/build/chunk-GKAD2UOA.js.map +1 -0
  7. package/build/chunk-HK7Z65DA.js +19 -0
  8. package/build/chunk-HK7Z65DA.js.map +1 -0
  9. package/build/chunk-XUDFDJME.js +42 -0
  10. package/build/chunk-XUDFDJME.js.map +1 -0
  11. package/build/index.d.ts +10 -0
  12. package/build/index.js +21 -0
  13. package/build/index.js.map +1 -0
  14. package/build/providers/socket_provider.d.ts +40 -0
  15. package/build/providers/socket_provider.js +132 -0
  16. package/build/providers/socket_provider.js.map +1 -0
  17. package/build/services/socket.d.ts +12 -0
  18. package/build/services/socket.js +10 -0
  19. package/build/services/socket.js.map +1 -0
  20. package/build/shared_types-Dw9AphfO.d.ts +21 -0
  21. package/build/socket_service-jxIaH5Rs.d.ts +144 -0
  22. package/build/src/client/index.d.ts +106 -0
  23. package/build/src/client/index.js +671 -0
  24. package/build/src/client/index.js.map +1 -0
  25. package/build/src/client/types.d.ts +101 -0
  26. package/build/src/client/types.js +1 -0
  27. package/build/src/client/types.js.map +1 -0
  28. package/build/src/decorators.d.ts +17 -0
  29. package/build/src/decorators.js +9 -0
  30. package/build/src/decorators.js.map +1 -0
  31. package/build/src/health_check.d.ts +18 -0
  32. package/build/src/health_check.js +7 -0
  33. package/build/src/health_check.js.map +1 -0
  34. package/build/src/otel.d.ts +31 -0
  35. package/build/src/otel.js +276 -0
  36. package/build/src/otel.js.map +1 -0
  37. package/build/src/types/tracing_channels.d.ts +40 -0
  38. package/build/src/types/tracing_channels.js +1 -0
  39. package/build/src/types/tracing_channels.js.map +1 -0
  40. package/build/src/types.d.ts +5 -0
  41. package/build/src/types.js +1 -0
  42. package/build/src/types.js.map +1 -0
  43. package/build/types-C0rDwbry.d.ts +393 -0
  44. package/package.json +105 -0
@@ -0,0 +1,5 @@
1
+ export { A as AuthenticatedSocket, C as ChannelEventMap, h as ChannelHandlerFn, a as ChannelHandlerMap, b as ChannelMatch, d as ChannelOptions, M as Middleware, i as MiddlewareClass, j as MiddlewareContext, k as MiddlewareFn, l as PresenceMember, R as RawSocket, m as SocketAuthenticationDetails, n as SocketAuthenticationHandler, o as SocketAuthenticationResult, e as SocketConfig, S as SocketEvents, g as SocketHealthSnapshot, p as SocketHttpContextFactory, f as SocketServiceStatus, q as SocketUpgradeContext } from '../types-C0rDwbry.js';
2
+ import 'ws';
3
+ import 'node:http';
4
+ export { C as ChannelAck, a as ChannelMessage } from '../shared_types-Dw9AphfO.js';
5
+ import '@boringnode/bus/types/main';
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,393 @@
1
+ import { WebSocket } from 'ws';
2
+ import { IncomingMessage } from 'node:http';
3
+ import { TransportFactory } from '@boringnode/bus/types/main';
4
+ import './shared_types-Dw9AphfO.js';
5
+
6
+ declare const PRESENCE_DATA_KEY = "__rlanzSocketPresence";
7
+ type PresenceSocketData = Record<string, unknown> & {
8
+ [PRESENCE_DATA_KEY]?: Record<string, PresenceUser>;
9
+ };
10
+ interface PresenceUser {
11
+ id: string;
12
+ name: string;
13
+ joinedAt: Date;
14
+ [key: string]: unknown;
15
+ }
16
+ interface PresenceData {
17
+ channel: string;
18
+ users: PresenceUser[];
19
+ count: number;
20
+ }
21
+ interface PresenceSocket {
22
+ id: string;
23
+ data: PresenceSocketData;
24
+ }
25
+ /**
26
+ * Tracks user presence in channels.
27
+ *
28
+ * channel -> socketId -> PresenceUser
29
+ */
30
+ declare class PresenceManager {
31
+ #private;
32
+ /**
33
+ * Sets the socket fetcher used to build distributed snapshots.
34
+ */
35
+ setSocketFetcher(fetchSockets: (channel: string) => Promise<PresenceSocket[]>): void;
36
+ /**
37
+ * Adds a user to a channel.
38
+ */
39
+ join(channel: string, socket: PresenceSocket, member: string | PresenceMember): void;
40
+ /**
41
+ * Removes a user from a channel.
42
+ */
43
+ leave(channel: string, socket: PresenceSocket): void;
44
+ /**
45
+ * Removes a user from every channel.
46
+ * Returns the list of channels the user left.
47
+ */
48
+ leaveAll(socket: PresenceSocket): string[];
49
+ /**
50
+ * Gets presence data for a channel.
51
+ */
52
+ snapshot(channel: string): Promise<PresenceData>;
53
+ /**
54
+ * Gets the number of users in a channel.
55
+ */
56
+ count(channel: string): Promise<number>;
57
+ /**
58
+ * Checks whether a socket is locally present in a channel.
59
+ */
60
+ hasLocal(channel: string, socketId: string): boolean;
61
+ /**
62
+ * Lists every local channel where a socket is present.
63
+ */
64
+ getLocalChannelsForSocket(socketId: string): string[];
65
+ /**
66
+ * Lists sockets that are locally present in a channel.
67
+ */
68
+ getLocalSockets(channel: string): PresenceSocket[];
69
+ }
70
+
71
+ interface ChannelBroadcaster {
72
+ to(channel: string): {
73
+ emit(event: string, data: unknown): void;
74
+ except(socketId: string): {
75
+ emit(event: string, data: unknown): void;
76
+ };
77
+ };
78
+ }
79
+ /**
80
+ * Abstract class used to define a channel.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * export default class ChatChannel extends BaseChannel {
85
+ * static pattern = 'chat/:roomId'
86
+ * static middlewares = [
87
+ * async (ctx, next) => {
88
+ * // Authorize access to the channel through middleware.
89
+ * // if (!Room.isMember(ctx.params.roomId, ctx.socket.user.id)) throw new Error('Forbidden')
90
+ * await next()
91
+ * },
92
+ * ]
93
+ * }
94
+ * ```
95
+ *
96
+ * @example With presence and decorated handlers
97
+ * ```ts
98
+ * import { onMessage } from '@rlanz/socket/decorators'
99
+ *
100
+ * export default class ChatChannel extends BaseChannel {
101
+ * static pattern = 'chat/:roomId'
102
+ * static options = { presence: true }
103
+ *
104
+ * static middlewares = [
105
+ * async (ctx, next) => {
106
+ * if (!ctx.socket.user.isVerified) throw new Error('Not verified')
107
+ * await next()
108
+ * }
109
+ * ]
110
+ *
111
+ * @onMessage('chat:send')
112
+ * async handleSend(socket: AuthenticatedSocket<User>, data: unknown) {
113
+ * // Handle chat:send event
114
+ * }
115
+ *
116
+ * @onMessage('chat:typing')
117
+ * async handleTyping(socket: AuthenticatedSocket<User>, data: unknown) {
118
+ * // Handle chat:typing event
119
+ * }
120
+ * }
121
+ * ```
122
+ */
123
+ declare class BaseChannel<User = unknown, Events extends ChannelEventMap = ChannelEventMap, HttpContext = unknown> {
124
+ /**
125
+ * URL-like channel pattern, for example: 'chat/:roomId'.
126
+ */
127
+ static pattern: string;
128
+ /**
129
+ * Channel options.
130
+ */
131
+ static options?: ChannelOptions;
132
+ /**
133
+ * Middlewares to run before subscription.
134
+ */
135
+ static middlewares?: Middleware<any, any>[];
136
+ /**
137
+ * Event handler map.
138
+ * Ex: { 'chat:send': 'handleSend' }
139
+ */
140
+ protected handlers: ChannelHandlerMap<User, Events, HttpContext>;
141
+ /**
142
+ * Injected socket service.
143
+ */
144
+ protected socket: ChannelBroadcaster;
145
+ /**
146
+ * Full channel name, for example: 'chat/general'.
147
+ */
148
+ protected channelName: string;
149
+ /**
150
+ * Parameters extracted from the pattern, for example: { roomId: 'general' }.
151
+ */
152
+ protected params: Record<string, string>;
153
+ /**
154
+ * Injected presence manager when presence is enabled.
155
+ */
156
+ protected presence: PresenceManager;
157
+ /**
158
+ * Injects dependencies.
159
+ */
160
+ $setContext(socket: ChannelBroadcaster, channelName: string, params: Record<string, string>, presence?: PresenceManager): void;
161
+ /**
162
+ * Emits to every channel member.
163
+ */
164
+ protected broadcast(event: string, data: unknown): void;
165
+ /**
166
+ * Emits to every channel member except one socket.
167
+ */
168
+ protected broadcastExcept(socketId: string, event: string, data: unknown): void;
169
+ /**
170
+ * Runs the middleware chain.
171
+ *
172
+ * middleware[0] -> middleware[1] -> ... -> subscribe
173
+ */
174
+ $runMiddlewares(ctx: MiddlewareContext<User, HttpContext>): Promise<void>;
175
+ /**
176
+ * Returns member information for presence.
177
+ * Implement when options.presence = true.
178
+ */
179
+ getPresenceInfo?(socket: AuthenticatedSocket<User, HttpContext>): PresenceMember;
180
+ /**
181
+ * Hook called when a socket joins the channel.
182
+ */
183
+ onJoin?(socket: AuthenticatedSocket<User, HttpContext>, ...params: string[]): Promise<void>;
184
+ /**
185
+ * Hook called when a socket leaves the channel.
186
+ */
187
+ onLeave?(socket: AuthenticatedSocket<User, HttpContext>, ...params: string[]): Promise<void>;
188
+ /**
189
+ * Hook called when a message is received on the channel.
190
+ * Used as fallback when no event handler matches.
191
+ */
192
+ onMessage?(socket: AuthenticatedSocket<User, HttpContext>, event: string, data: unknown): Promise<void>;
193
+ /**
194
+ * Hook called when a member joins a presence channel.
195
+ */
196
+ onMemberJoin?(socket: AuthenticatedSocket<User, HttpContext>, member: PresenceMember, ...params: string[]): Promise<void>;
197
+ /**
198
+ * Hook called when a member leaves a presence channel.
199
+ */
200
+ onMemberLeave?(socket: AuthenticatedSocket<User, HttpContext>, member: PresenceMember, ...params: string[]): Promise<void>;
201
+ /**
202
+ * Dispatches a message to the matching handler.
203
+ * Called by SocketService. Do not override.
204
+ */
205
+ $handleMessage(socket: AuthenticatedSocket<User, HttpContext>, event: string, data: unknown): Promise<unknown>;
206
+ /**
207
+ * Runs the generic join hook. ChannelSubscriptions owns subscription lifecycle.
208
+ */
209
+ $handleJoin(socket: AuthenticatedSocket<User, HttpContext>, ...params: string[]): Promise<void>;
210
+ /**
211
+ * Runs the generic leave hook. ChannelSubscriptions owns subscription lifecycle.
212
+ */
213
+ $handleLeave(socket: AuthenticatedSocket<User, HttpContext>, ...params: string[]): Promise<void>;
214
+ $getPresenceInfo(socket: AuthenticatedSocket<User, HttpContext>): PresenceMember;
215
+ }
216
+
217
+ interface SocketTransportConfig {
218
+ driver: TransportFactory;
219
+ channel?: string;
220
+ presenceTimeout?: number;
221
+ }
222
+
223
+ interface RawSocket<HttpContext = unknown> {
224
+ id: string;
225
+ data: Record<string, unknown>;
226
+ auth?: Record<string, unknown>;
227
+ connection: WebSocket;
228
+ request: IncomingMessage;
229
+ httpContext?: HttpContext;
230
+ }
231
+ /**
232
+ * Socket exposed to channels after the HTTP upgrade.
233
+ */
234
+ interface AuthenticatedSocket<User = unknown, HttpContext = unknown> {
235
+ /**
236
+ * Unique socket ID.
237
+ */
238
+ id: string;
239
+ /**
240
+ * Authenticated user resolved during the HTTP upgrade.
241
+ */
242
+ user: User | undefined;
243
+ /**
244
+ * Channels this socket is subscribed to.
245
+ */
246
+ channels: Set<string>;
247
+ /**
248
+ * Emits an event to this socket.
249
+ */
250
+ emit(event: string, data: unknown): void;
251
+ /**
252
+ * Joins the user target used by SocketService.toUser().
253
+ */
254
+ joinUserRoom(userId: string | number): Promise<void>;
255
+ /**
256
+ * Leaves the current SocketService.toUser() target.
257
+ */
258
+ leaveUserRoom(): Promise<void>;
259
+ /**
260
+ * Disconnects the socket.
261
+ */
262
+ disconnect(): void;
263
+ /**
264
+ * Raw WebSocket connection metadata.
265
+ */
266
+ raw: RawSocket<HttpContext>;
267
+ }
268
+ interface SocketUpgradeContext<HttpContext = unknown> {
269
+ request: IncomingMessage;
270
+ auth?: Record<string, unknown>;
271
+ httpContext?: HttpContext;
272
+ }
273
+ interface SocketAuthenticationDetails<User = unknown> {
274
+ user?: User;
275
+ auth?: Record<string, unknown>;
276
+ data?: Record<string, unknown>;
277
+ }
278
+ type SocketAuthenticationResult<User = unknown> = SocketAuthenticationDetails<User> | false | null;
279
+ type SocketAuthenticationHandler<User = unknown, HttpContext = unknown> = (ctx: SocketUpgradeContext<HttpContext>) => SocketAuthenticationResult<User> | Promise<SocketAuthenticationResult<User>>;
280
+ type SocketHttpContextFactory<HttpContext = unknown> = (request: IncomingMessage) => HttpContext | Promise<HttpContext>;
281
+ /**
282
+ * Result of matching a channel.
283
+ */
284
+ interface ChannelMatch {
285
+ channel: typeof BaseChannel;
286
+ pattern: string;
287
+ params: Record<string, string>;
288
+ }
289
+ /**
290
+ * Channel options.
291
+ */
292
+ interface ChannelOptions {
293
+ presence?: boolean;
294
+ }
295
+ /**
296
+ * Presence member information.
297
+ */
298
+ interface PresenceMember {
299
+ id: string;
300
+ name: string;
301
+ [key: string]: unknown;
302
+ }
303
+ /**
304
+ * Context passed to middlewares.
305
+ */
306
+ interface MiddlewareContext<User = unknown, HttpContext = unknown> {
307
+ socket: AuthenticatedSocket<User, HttpContext>;
308
+ channel: string;
309
+ params: Record<string, string>;
310
+ presenceData?: Record<string, unknown> | PresenceData;
311
+ setPresenceData(data: Record<string, unknown> | PresenceData): void;
312
+ }
313
+ /**
314
+ * Middleware function.
315
+ */
316
+ type MiddlewareFn<User = unknown, HttpContext = unknown> = (ctx: MiddlewareContext<User, HttpContext>, next: () => Promise<void>) => Promise<void>;
317
+ /**
318
+ * Middleware class.
319
+ */
320
+ interface MiddlewareClass<User = unknown, HttpContext = unknown> {
321
+ handle: MiddlewareFn<User, HttpContext>;
322
+ }
323
+ /**
324
+ * Middleware, either a function or a class.
325
+ */
326
+ type Middleware<User = unknown, HttpContext = unknown> = MiddlewareFn<User, HttpContext> | MiddlewareClass<User, HttpContext>;
327
+ /**
328
+ * Event handler map.
329
+ */
330
+ type ChannelEventMap = Record<string, unknown>;
331
+ type ChannelHandlerFn<User = unknown, Events extends ChannelEventMap = ChannelEventMap, HttpContext = unknown, K extends keyof Events = keyof Events> = (socket: AuthenticatedSocket<User, HttpContext>, data: Events[K]) => unknown | Promise<unknown>;
332
+ type ChannelHandlerMap<User = unknown, Events extends ChannelEventMap = ChannelEventMap, HttpContext = unknown> = {
333
+ [K in keyof Events]?: ChannelHandlerFn<User, Events, HttpContext, K>;
334
+ };
335
+ /**
336
+ * Package configuration.
337
+ */
338
+ interface SocketConfig<User = unknown, HttpContext = unknown> {
339
+ channels?: {
340
+ patterns?: string[];
341
+ };
342
+ websocket?: {
343
+ path?: string;
344
+ createContext?: SocketHttpContextFactory<HttpContext>;
345
+ authenticate?: SocketAuthenticationHandler<User, HttpContext>;
346
+ /**
347
+ * Sends a WebSocket ping every configured number of milliseconds.
348
+ *
349
+ * When only pingTimeout is configured, the service uses a default 25s interval.
350
+ */
351
+ pingInterval?: number;
352
+ /**
353
+ * Terminates connections that do not answer a ping within the configured milliseconds.
354
+ *
355
+ * When only pingInterval is configured, the service uses a default 5s timeout.
356
+ */
357
+ pingTimeout?: number;
358
+ };
359
+ /**
360
+ * Optional transport used to synchronize broadcasts between SocketService instances.
361
+ */
362
+ transport?: SocketTransportConfig | null;
363
+ }
364
+ type SocketServiceStatus = 'starting' | 'ready' | 'stopping' | 'stopped' | 'failed';
365
+ interface SocketHealthSnapshot {
366
+ status: SocketServiceStatus;
367
+ ready: boolean;
368
+ connections: number;
369
+ channels: number;
370
+ lastError?: string;
371
+ }
372
+ /**
373
+ * Events emitted by the service.
374
+ */
375
+ interface SocketEvents<User = unknown, HttpContext = unknown> {
376
+ connect: {
377
+ socket: AuthenticatedSocket<User, HttpContext>;
378
+ };
379
+ disconnect: {
380
+ socket: AuthenticatedSocket<User, HttpContext>;
381
+ reason: string;
382
+ };
383
+ subscribe: {
384
+ socket: AuthenticatedSocket<User, HttpContext>;
385
+ channel: string;
386
+ };
387
+ unsubscribe: {
388
+ socket: AuthenticatedSocket<User, HttpContext>;
389
+ channel: string;
390
+ };
391
+ }
392
+
393
+ export { type AuthenticatedSocket as A, BaseChannel as B, type ChannelEventMap as C, type Middleware as M, PresenceManager as P, type RawSocket as R, type SocketEvents as S, type ChannelHandlerMap as a, type ChannelMatch as b, type PresenceData as c, type ChannelOptions as d, type SocketConfig as e, type SocketServiceStatus as f, type SocketHealthSnapshot as g, type ChannelHandlerFn as h, type MiddlewareClass as i, type MiddlewareContext as j, type MiddlewareFn as k, type PresenceMember as l, type SocketAuthenticationDetails as m, type SocketAuthenticationHandler as n, type SocketAuthenticationResult as o, type SocketHttpContextFactory as p, type SocketUpgradeContext as q };
package/package.json ADDED
@@ -0,0 +1,105 @@
1
+ {
2
+ "name": "@rlanz/socket",
3
+ "version": "0.0.1-0",
4
+ "description": "WebSocket integration for AdonisJS",
5
+ "type": "module",
6
+ "main": "./build/index.js",
7
+ "types": "./build/index.d.ts",
8
+ "license": "MIT",
9
+ "engines": {
10
+ "node": ">=24.0.0"
11
+ },
12
+ "files": [
13
+ "build"
14
+ ],
15
+ "exports": {
16
+ ".": "./build/index.js",
17
+ "./provider": "./build/providers/socket_provider.js",
18
+ "./services/main": "./build/services/socket.js",
19
+ "./decorators": "./build/src/decorators.js",
20
+ "./health_check": "./build/src/health_check.js",
21
+ "./otel": "./build/src/otel.js",
22
+ "./testing": "./build/src/testing.js",
23
+ "./types": "./build/src/types.js",
24
+ "./types/tracing_channels": "./build/src/types/tracing_channels.js",
25
+ "./client": "./build/src/client/index.js",
26
+ "./client/types": "./build/src/client/types.js"
27
+ },
28
+ "scripts": {
29
+ "build": "npm run compile",
30
+ "clean": "del-cli build",
31
+ "compile": "npm run clean && tsup-node",
32
+ "lint": "oxlint",
33
+ "lint:fix": "oxlint --fix",
34
+ "format": "oxfmt --check .",
35
+ "format:fix": "oxfmt --write .",
36
+ "prepublishOnly": "npm run build",
37
+ "release": "release-it",
38
+ "test": "node --import=@poppinss/ts-exec --enable-source-maps bin/test.ts",
39
+ "typecheck": "tsc --noEmit",
40
+ "version": "npm run build"
41
+ },
42
+ "dependencies": {
43
+ "@boringnode/bus": "^0.9.0",
44
+ "@poppinss/matchit": "^3.2.0",
45
+ "emittery": "^1.2.0",
46
+ "ws": "^8.21.0"
47
+ },
48
+ "devDependencies": {
49
+ "@adonisjs/core": "^7.0.1",
50
+ "@adonisjs/tsconfig": "^2.0.0",
51
+ "@japa/assert": "^4.2.0",
52
+ "@japa/runner": "^5.3.0",
53
+ "@opentelemetry/api": "^1.9.1",
54
+ "@opentelemetry/context-async-hooks": "^2.7.1",
55
+ "@opentelemetry/core": "^2.7.1",
56
+ "@opentelemetry/instrumentation": "^0.218.0",
57
+ "@opentelemetry/sdk-metrics": "^2.7.1",
58
+ "@opentelemetry/sdk-trace-base": "^2.7.1",
59
+ "@poppinss/ts-exec": "^1.4.4",
60
+ "@types/ws": "^8.18.1",
61
+ "del-cli": "^7.0.0",
62
+ "oxfmt": "^0.38.0",
63
+ "oxlint": "^1.53.0",
64
+ "release-it": "^19.2.4",
65
+ "tsup": "^8.5.1",
66
+ "typescript": "^5.9.3"
67
+ },
68
+ "peerDependencies": {
69
+ "@adonisjs/core": "^7.0.1",
70
+ "@opentelemetry/api": "^1.9.0",
71
+ "@opentelemetry/core": "^1.30.0 || ^2.0.0",
72
+ "@opentelemetry/instrumentation": "^0.200.0"
73
+ },
74
+ "peerDependenciesMeta": {
75
+ "@opentelemetry/api": {
76
+ "optional": true
77
+ },
78
+ "@opentelemetry/core": {
79
+ "optional": true
80
+ },
81
+ "@opentelemetry/instrumentation": {
82
+ "optional": true
83
+ }
84
+ },
85
+ "keywords": [
86
+ "adonisjs",
87
+ "adonis",
88
+ "websocket",
89
+ "realtime"
90
+ ],
91
+ "homepage": "https://github.com/rlanz/socket#readme",
92
+ "repository": {
93
+ "type": "git",
94
+ "url": "git+https://github.com/rlanz/socket.git"
95
+ },
96
+ "bugs": {
97
+ "url": "https://github.com/rlanz/socket/issues"
98
+ },
99
+ "author": "Romain Lanz <romain.lanz@pm.me>",
100
+ "publishConfig": {
101
+ "access": "public",
102
+ "tag": "latest"
103
+ },
104
+ "packageManager": "yarn@4.11.0"
105
+ }