@gravito/ripple 3.0.0 → 3.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 (118) hide show
  1. package/README.md +179 -6
  2. package/README.zh-TW.md +104 -2
  3. package/dist/core/src/Application.d.ts +215 -0
  4. package/dist/core/src/ConfigManager.d.ts +26 -0
  5. package/dist/core/src/Container.d.ts +78 -0
  6. package/dist/core/src/ErrorHandler.d.ts +63 -0
  7. package/dist/core/src/Event.d.ts +5 -0
  8. package/dist/core/src/EventManager.d.ts +123 -0
  9. package/dist/core/src/GlobalErrorHandlers.d.ts +47 -0
  10. package/dist/core/src/GravitoServer.d.ts +28 -0
  11. package/dist/core/src/HookManager.d.ts +84 -0
  12. package/dist/core/src/Listener.d.ts +4 -0
  13. package/dist/core/src/Logger.d.ts +20 -0
  14. package/dist/core/src/PlanetCore.d.ts +289 -0
  15. package/dist/core/src/Route.d.ts +36 -0
  16. package/dist/core/src/Router.d.ts +288 -0
  17. package/dist/core/src/ServiceProvider.d.ts +156 -0
  18. package/dist/core/src/adapters/GravitoEngineAdapter.d.ts +26 -0
  19. package/dist/core/src/adapters/PhotonAdapter.d.ts +170 -0
  20. package/dist/core/src/adapters/bun/BunContext.d.ts +45 -0
  21. package/dist/core/src/adapters/bun/BunNativeAdapter.d.ts +30 -0
  22. package/dist/core/src/adapters/bun/BunRequest.d.ts +31 -0
  23. package/dist/core/src/adapters/bun/RadixNode.d.ts +19 -0
  24. package/dist/core/src/adapters/bun/RadixRouter.d.ts +31 -0
  25. package/dist/core/src/adapters/bun/types.d.ts +20 -0
  26. package/dist/core/src/adapters/photon-types.d.ts +73 -0
  27. package/dist/core/src/adapters/types.d.ts +208 -0
  28. package/dist/core/src/engine/AOTRouter.d.ts +134 -0
  29. package/dist/core/src/engine/FastContext.d.ts +98 -0
  30. package/dist/core/src/engine/Gravito.d.ts +137 -0
  31. package/dist/core/src/engine/MinimalContext.d.ts +77 -0
  32. package/dist/core/src/engine/analyzer.d.ts +27 -0
  33. package/dist/core/src/engine/constants.d.ts +23 -0
  34. package/dist/core/src/engine/index.d.ts +26 -0
  35. package/dist/core/src/engine/path.d.ts +26 -0
  36. package/dist/core/src/engine/pool.d.ts +83 -0
  37. package/dist/core/src/engine/types.d.ts +138 -0
  38. package/dist/core/src/exceptions/AuthenticationException.d.ts +8 -0
  39. package/dist/core/src/exceptions/AuthorizationException.d.ts +8 -0
  40. package/dist/core/src/exceptions/GravitoException.d.ts +23 -0
  41. package/dist/core/src/exceptions/HttpException.d.ts +9 -0
  42. package/dist/core/src/exceptions/ModelNotFoundException.d.ts +10 -0
  43. package/dist/core/src/exceptions/ValidationException.d.ts +22 -0
  44. package/dist/core/src/exceptions/index.d.ts +6 -0
  45. package/dist/core/src/helpers/Arr.d.ts +19 -0
  46. package/dist/core/src/helpers/Str.d.ts +23 -0
  47. package/dist/core/src/helpers/data.d.ts +25 -0
  48. package/dist/core/src/helpers/errors.d.ts +34 -0
  49. package/dist/core/src/helpers/response.d.ts +41 -0
  50. package/dist/core/src/helpers.d.ts +338 -0
  51. package/dist/core/src/http/CookieJar.d.ts +51 -0
  52. package/dist/core/src/http/cookie.d.ts +29 -0
  53. package/dist/core/src/http/middleware/BodySizeLimit.d.ts +16 -0
  54. package/dist/core/src/http/middleware/Cors.d.ts +24 -0
  55. package/dist/core/src/http/middleware/Csrf.d.ts +23 -0
  56. package/dist/core/src/http/middleware/HeaderTokenGate.d.ts +28 -0
  57. package/dist/core/src/http/middleware/SecurityHeaders.d.ts +29 -0
  58. package/dist/core/src/http/middleware/ThrottleRequests.d.ts +18 -0
  59. package/dist/core/src/http/types.d.ts +334 -0
  60. package/dist/core/src/index.d.ts +67 -0
  61. package/dist/core/src/runtime.d.ts +119 -0
  62. package/dist/core/src/security/Encrypter.d.ts +33 -0
  63. package/dist/core/src/security/Hasher.d.ts +29 -0
  64. package/dist/core/src/testing/HttpTester.d.ts +39 -0
  65. package/dist/core/src/testing/TestResponse.d.ts +78 -0
  66. package/dist/core/src/testing/index.d.ts +2 -0
  67. package/dist/core/src/types/events.d.ts +94 -0
  68. package/dist/index.js +10206 -37
  69. package/dist/index.js.map +69 -10
  70. package/dist/photon/src/index.d.ts +70 -0
  71. package/dist/photon/src/middleware/binary.d.ts +31 -0
  72. package/dist/photon/src/middleware/htmx.d.ts +39 -0
  73. package/dist/ripple/src/OrbitRipple.d.ts +64 -0
  74. package/dist/ripple/src/RippleServer.d.ts +518 -0
  75. package/dist/{channels → ripple/src/channels}/Channel.d.ts +6 -1
  76. package/dist/ripple/src/channels/ChannelManager.d.ts +173 -0
  77. package/dist/{channels → ripple/src/channels}/index.d.ts +0 -1
  78. package/dist/ripple/src/drivers/LocalDriver.d.ts +61 -0
  79. package/dist/ripple/src/drivers/RedisDriver.d.ts +141 -0
  80. package/dist/ripple/src/drivers/index.d.ts +2 -0
  81. package/dist/ripple/src/errors/RippleError.d.ts +48 -0
  82. package/dist/ripple/src/errors/index.d.ts +1 -0
  83. package/dist/ripple/src/events/BroadcastEvent.d.ts +123 -0
  84. package/dist/ripple/src/events/BroadcastManager.d.ts +100 -0
  85. package/dist/ripple/src/events/Broadcaster.d.ts +264 -0
  86. package/dist/{events → ripple/src/events}/index.d.ts +1 -1
  87. package/dist/ripple/src/health/HealthChecker.d.ts +93 -0
  88. package/dist/ripple/src/health/index.d.ts +1 -0
  89. package/dist/ripple/src/index.d.ts +60 -0
  90. package/dist/ripple/src/logging/Logger.d.ts +99 -0
  91. package/dist/ripple/src/logging/index.d.ts +1 -0
  92. package/dist/ripple/src/tracking/ConnectionTracker.d.ts +116 -0
  93. package/dist/ripple/src/tracking/index.d.ts +1 -0
  94. package/dist/ripple/src/types.d.ts +753 -0
  95. package/dist/ripple/src/utils/MessageSerializer.d.ts +44 -0
  96. package/dist/ripple/src/utils/index.d.ts +1 -0
  97. package/package.json +14 -5
  98. package/dist/OrbitRipple.d.ts +0 -80
  99. package/dist/OrbitRipple.d.ts.map +0 -1
  100. package/dist/RippleServer.d.ts +0 -126
  101. package/dist/RippleServer.d.ts.map +0 -1
  102. package/dist/channels/Channel.d.ts.map +0 -1
  103. package/dist/channels/ChannelManager.d.ts +0 -79
  104. package/dist/channels/ChannelManager.d.ts.map +0 -1
  105. package/dist/channels/index.d.ts.map +0 -1
  106. package/dist/drivers/LocalDriver.d.ts +0 -30
  107. package/dist/drivers/LocalDriver.d.ts.map +0 -1
  108. package/dist/drivers/index.d.ts +0 -2
  109. package/dist/drivers/index.d.ts.map +0 -1
  110. package/dist/events/BroadcastEvent.d.ts +0 -52
  111. package/dist/events/BroadcastEvent.d.ts.map +0 -1
  112. package/dist/events/Broadcaster.d.ts +0 -68
  113. package/dist/events/Broadcaster.d.ts.map +0 -1
  114. package/dist/events/index.d.ts.map +0 -1
  115. package/dist/index.d.ts +0 -38
  116. package/dist/index.d.ts.map +0 -1
  117. package/dist/types.d.ts +0 -163
  118. package/dist/types.d.ts.map +0 -1
@@ -0,0 +1,753 @@
1
+ /**
2
+ * @fileoverview Core types for @gravito/ripple WebSocket module
3
+ * @module @gravito/ripple
4
+ */
5
+ import type { Server, ServerWebSocket } from 'bun';
6
+ /**
7
+ * Data attached to each WebSocket connection.
8
+ *
9
+ * Contains client identification, authentication state, channel subscriptions,
10
+ * and presence information. This data is accessed via `ws.data` on any RippleWebSocket.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * // Access client data in event handlers
15
+ * const handleMessage = (ws: RippleWebSocket, message: string) => {
16
+ * console.log('Client ID:', ws.data.id)
17
+ * console.log('User ID:', ws.data.userId)
18
+ * console.log('Channels:', Array.from(ws.data.channels))
19
+ * }
20
+ * ```
21
+ */
22
+ export interface ClientData {
23
+ /** Unique client identifier */
24
+ id: string;
25
+ /** User ID if authenticated */
26
+ userId?: string | number;
27
+ /** Channels this client has joined */
28
+ channels: Set<string>;
29
+ /** Additional user info for presence channels */
30
+ userInfo?: Record<string, unknown>;
31
+ }
32
+ /**
33
+ * Supported WebSocket channel types.
34
+ *
35
+ * - `public`: Open channels accessible to all clients without authentication
36
+ * - `private`: Channels requiring authorization via the authorizer callback
37
+ * - `presence`: Private channels that track online users and their metadata
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * // Channel type determines the authorization flow
42
+ * const publicChannel: ChannelType = 'public' // No auth needed
43
+ * const privateChannel: ChannelType = 'private' // Boolean auth result
44
+ * const presenceChannel: ChannelType = 'presence' // PresenceUserInfo required
45
+ * ```
46
+ *
47
+ * @public
48
+ * @since 3.0.0
49
+ */
50
+ export type ChannelType = 'public' | 'private' | 'presence';
51
+ /**
52
+ * Base channel interface representing a WebSocket channel.
53
+ *
54
+ * Channels are the core abstraction for organizing WebSocket communications.
55
+ * Each channel has a type (public/private/presence) and manages subscriptions.
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * import { PublicChannel, PrivateChannel } from '@gravito/ripple'
60
+ *
61
+ * // Create different channel types
62
+ * const news = new PublicChannel('news')
63
+ * console.log(news.fullName) // 'news'
64
+ *
65
+ * const orders = new PrivateChannel('orders.123')
66
+ * console.log(orders.fullName) // 'private-orders.123'
67
+ * ```
68
+ */
69
+ export interface Channel {
70
+ /** Channel name (without prefix) */
71
+ readonly name: string;
72
+ /** Channel type */
73
+ readonly type: ChannelType;
74
+ /** Full channel name with prefix */
75
+ readonly fullName: string;
76
+ }
77
+ /**
78
+ * User information for presence channels.
79
+ *
80
+ * Defines the shape of user data returned by the authorizer for presence channels.
81
+ * This data is shared with all members of the presence channel.
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * // Return presence user info from authorizer
86
+ * const authorizer: ChannelAuthorizer = async (channel, userId, socketId) => {
87
+ * if (channel.startsWith('presence-chat.')) {
88
+ * const user = await db.users.findById(userId)
89
+ * return {
90
+ * id: user.id,
91
+ * info: {
92
+ * name: user.name,
93
+ * avatar: user.avatarUrl,
94
+ * status: 'online'
95
+ * }
96
+ * }
97
+ * }
98
+ * return true
99
+ * }
100
+ * ```
101
+ */
102
+ export interface PresenceUserInfo {
103
+ /** Unique user identifier */
104
+ id: string | number;
105
+ /** Additional user metadata shared with channel members */
106
+ info: Record<string, unknown>;
107
+ }
108
+ /**
109
+ * Authorization callback for channel subscriptions.
110
+ *
111
+ * Determines whether a client can subscribe to a given channel. The return value
112
+ * depends on the channel type:
113
+ *
114
+ * - **Public channels**: Always return `true` or omit authorization
115
+ * - **Private channels**: Return `boolean` or `Promise<boolean>`
116
+ * - **Presence channels**: Return `PresenceUserInfo` or `Promise<PresenceUserInfo | false>`
117
+ *
118
+ * @param channelName - Full channel name including prefix (e.g., 'private-orders.123')
119
+ * @param userId - User ID from authenticated session (undefined for unauthenticated)
120
+ * @param socketId - Unique WebSocket connection ID
121
+ * @returns Authorization result:
122
+ * - `true` = authorized
123
+ * - `false` = denied
124
+ * - `PresenceUserInfo` = authorized with user data (presence channels only)
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * // Example 1: Public channel (no authorization)
129
+ * const publicAuthorizer: ChannelAuthorizer = (channel, userId, socketId) => {
130
+ * return true // Allow all
131
+ * }
132
+ *
133
+ * // Example 2: Private channel with simple auth
134
+ * const privateAuthorizer: ChannelAuthorizer = (channel, userId, socketId) => {
135
+ * if (channel === 'private-admin') {
136
+ * return userId === 'admin' // Only admin user
137
+ * }
138
+ * return userId !== undefined // Require authentication
139
+ * }
140
+ *
141
+ * // Example 3: Presence channel with async database lookup
142
+ * const presenceAuthorizer: ChannelAuthorizer = async (channel, userId, socketId) => {
143
+ * if (channel.startsWith('presence-chat.')) {
144
+ * if (!userId) return false
145
+ *
146
+ * const user = await db.users.findById(userId)
147
+ * if (!user) return false
148
+ *
149
+ * return {
150
+ * id: user.id,
151
+ * info: {
152
+ * name: user.name,
153
+ * avatar: user.avatarUrl,
154
+ * role: user.role
155
+ * }
156
+ * }
157
+ * }
158
+ *
159
+ * // Default: require authentication
160
+ * return userId !== undefined
161
+ * }
162
+ *
163
+ * // Example 4: Resource ownership check
164
+ * const ownershipAuthorizer: ChannelAuthorizer = async (channel, userId, socketId) => {
165
+ * const match = channel.match(/^private-orders\.(\d+)$/)
166
+ * if (match) {
167
+ * const orderId = match[1]
168
+ * const order = await db.orders.findById(orderId)
169
+ * return order.userId === userId // Only order owner
170
+ * }
171
+ * return false
172
+ * }
173
+ * ```
174
+ */
175
+ export type ChannelAuthorizer = (channelName: string, userId: string | number | undefined, socketId: string) => boolean | Promise<boolean> | PresenceUserInfo | Promise<PresenceUserInfo | false>;
176
+ /**
177
+ * Interface for broadcast event classes.
178
+ *
179
+ * Implement this interface to create type-safe broadcast events that can be
180
+ * dispatched using the `broadcast()` function or `BroadcastManager`.
181
+ *
182
+ * @see {@link BroadcastEvent} - Abstract base class implementation
183
+ *
184
+ * @example
185
+ * ```typescript
186
+ * import { BroadcastEventInterface, Channel, PrivateChannel } from '@gravito/ripple'
187
+ *
188
+ * class OrderShipped implements BroadcastEventInterface {
189
+ * constructor(public order: { id: number; userId: number }) {}
190
+ *
191
+ * broadcastOn(): Channel {
192
+ * return new PrivateChannel(`orders.${this.order.userId}`)
193
+ * }
194
+ *
195
+ * broadcastAs(): string {
196
+ * return 'OrderShipped'
197
+ * }
198
+ *
199
+ * broadcastExcept(): string[] {
200
+ * return [] // Don't exclude anyone
201
+ * }
202
+ * }
203
+ * ```
204
+ */
205
+ export interface BroadcastEventInterface {
206
+ /** Channels to broadcast to */
207
+ broadcastOn(): Channel | Channel[];
208
+ /** Event name (defaults to class name) */
209
+ broadcastAs?(): string;
210
+ /** Exclude specific socket IDs */
211
+ broadcastExcept?(): string[];
212
+ }
213
+ /**
214
+ * Messages sent from client to server over WebSocket.
215
+ *
216
+ * This discriminated union defines the protocol for client-to-server communication.
217
+ * All messages must have a `type` field for proper routing.
218
+ *
219
+ * @example
220
+ * ```typescript
221
+ * // Subscribe to a channel
222
+ * const subscribeMsg: ClientMessage = {
223
+ * type: 'subscribe',
224
+ * channel: 'private-orders.123',
225
+ * auth: { socketId: 'abc', signature: 'xyz' }
226
+ * }
227
+ *
228
+ * // Unsubscribe from a channel
229
+ * const unsubscribeMsg: ClientMessage = {
230
+ * type: 'unsubscribe',
231
+ * channel: 'news'
232
+ * }
233
+ *
234
+ * // Send a whisper (client-to-client event)
235
+ * const whisperMsg: ClientMessage = {
236
+ * type: 'whisper',
237
+ * channel: 'presence-chat.lobby',
238
+ * event: 'typing',
239
+ * data: { userId: 123, isTyping: true }
240
+ * }
241
+ *
242
+ * // Ping for connection health check
243
+ * const pingMsg: ClientMessage = { type: 'ping' }
244
+ * ```
245
+ */
246
+ export type ClientMessage = {
247
+ type: 'subscribe';
248
+ channel: string;
249
+ auth?: {
250
+ socketId: string;
251
+ signature: string;
252
+ };
253
+ } | {
254
+ type: 'unsubscribe';
255
+ channel: string;
256
+ } | {
257
+ type: 'whisper';
258
+ channel: string;
259
+ event: string;
260
+ data: unknown;
261
+ } | {
262
+ type: 'ping';
263
+ };
264
+ /**
265
+ * Error codes for Ripple WebSocket protocol.
266
+ *
267
+ * Used in `ErrorServerMessage` to communicate specific failure reasons to clients.
268
+ *
269
+ * @example
270
+ * ```typescript
271
+ * // Send authorization error to client
272
+ * const error: ErrorServerMessage = {
273
+ * type: 'error',
274
+ * code: 'UNAUTHORIZED',
275
+ * message: 'You are not authorized to join this channel',
276
+ * channel: 'private-orders.123'
277
+ * }
278
+ * ```
279
+ */
280
+ export type RippleErrorCode = 'UNAUTHORIZED' | 'NOT_SUBSCRIBED' | 'INVALID_FORMAT' | 'DRIVER_NOT_INITIALIZED' | 'REDIS_NOT_INSTALLED' | 'REDIS_CONNECTION_FAILED';
281
+ /**
282
+ * Error message sent from server to client.
283
+ *
284
+ * Contains a typed error code and human-readable message.
285
+ *
286
+ * @deprecated Use the `ServerMessage` discriminated union instead
287
+ */
288
+ export interface ErrorServerMessage {
289
+ type: 'error';
290
+ code: RippleErrorCode;
291
+ message: string;
292
+ channel?: string;
293
+ }
294
+ /**
295
+ * Messages sent from server to client over WebSocket.
296
+ *
297
+ * This discriminated union defines the protocol for server-to-client communication.
298
+ * Clients should handle messages based on the `type` field.
299
+ *
300
+ * @example
301
+ * ```typescript
302
+ * // Handle incoming server messages
303
+ * ws.onmessage = (event) => {
304
+ * const message: ServerMessage = JSON.parse(event.data)
305
+ *
306
+ * switch (message.type) {
307
+ * case 'connected':
308
+ * console.log('Connected with socket ID:', message.socketId)
309
+ * break
310
+ *
311
+ * case 'subscribed':
312
+ * console.log('Subscribed to:', message.channel)
313
+ * break
314
+ *
315
+ * case 'event':
316
+ * console.log(`Event "${message.event}" on ${message.channel}:`, message.data)
317
+ * break
318
+ *
319
+ * case 'presence':
320
+ * if (message.event === 'join') {
321
+ * console.log('User joined:', message.data)
322
+ * }
323
+ * break
324
+ *
325
+ * case 'error':
326
+ * console.error('Error:', message.message, message.code)
327
+ * break
328
+ *
329
+ * case 'pong':
330
+ * // Heartbeat response
331
+ * break
332
+ * }
333
+ * }
334
+ * ```
335
+ */
336
+ export type ServerMessage = {
337
+ type: 'subscribed';
338
+ channel: string;
339
+ } | {
340
+ type: 'unsubscribed';
341
+ channel: string;
342
+ } | {
343
+ type: 'error';
344
+ message: string;
345
+ channel?: string;
346
+ code?: RippleErrorCode;
347
+ } | {
348
+ type: 'event';
349
+ channel: string;
350
+ event: string;
351
+ data: unknown;
352
+ } | {
353
+ type: 'presence';
354
+ channel: string;
355
+ event: 'join' | 'leave' | 'members';
356
+ data: unknown;
357
+ } | {
358
+ type: 'pong';
359
+ } | {
360
+ type: 'connected';
361
+ socketId: string;
362
+ };
363
+ /**
364
+ * Driver health status information.
365
+ *
366
+ * Returned by `RippleDriver.getStatus()` to report the current state of the driver.
367
+ *
368
+ * @example
369
+ * ```typescript
370
+ * const driver = new RedisDriver(config)
371
+ * const status = driver.getStatus()
372
+ *
373
+ * console.log(status.name) // 'redis'
374
+ * console.log(status.initialized) // true
375
+ * console.log(status.connected) // true
376
+ * console.log(status.lastError) // undefined (or error message if failed)
377
+ * ```
378
+ */
379
+ export interface DriverStatus {
380
+ /** Driver name (e.g., 'local', 'redis') */
381
+ name: string;
382
+ /** Whether the driver has been initialized */
383
+ initialized: boolean;
384
+ /** Whether the driver is currently connected */
385
+ connected: boolean;
386
+ /** Last error message if connection failed */
387
+ lastError?: string;
388
+ }
389
+ /**
390
+ * Server message type constants.
391
+ *
392
+ * Use these constants instead of string literals for type safety.
393
+ *
394
+ * @example
395
+ * ```typescript
396
+ * import { SERVER_MESSAGE_TYPES } from '@gravito/ripple'
397
+ *
398
+ * // Type-safe message creation
399
+ * const message = {
400
+ * type: SERVER_MESSAGE_TYPES.SUBSCRIBED,
401
+ * channel: 'news'
402
+ * }
403
+ * ```
404
+ */
405
+ export declare const SERVER_MESSAGE_TYPES: {
406
+ readonly SUBSCRIBED: "subscribed";
407
+ readonly UNSUBSCRIBED: "unsubscribed";
408
+ readonly ERROR: "error";
409
+ readonly EVENT: "event";
410
+ readonly PRESENCE: "presence";
411
+ readonly PONG: "pong";
412
+ readonly CONNECTED: "connected";
413
+ };
414
+ /**
415
+ * Client message type constants.
416
+ *
417
+ * Use these constants instead of string literals for type safety.
418
+ *
419
+ * @example
420
+ * ```typescript
421
+ * import { CLIENT_MESSAGE_TYPES } from '@gravito/ripple'
422
+ *
423
+ * // Type-safe message creation
424
+ * const message = {
425
+ * type: CLIENT_MESSAGE_TYPES.SUBSCRIBE,
426
+ * channel: 'private-orders.123'
427
+ * }
428
+ * ```
429
+ */
430
+ export declare const CLIENT_MESSAGE_TYPES: {
431
+ readonly SUBSCRIBE: "subscribe";
432
+ readonly UNSUBSCRIBE: "unsubscribe";
433
+ readonly WHISPER: "whisper";
434
+ readonly PING: "ping";
435
+ };
436
+ /**
437
+ * Interface for implementing custom Ripple drivers.
438
+ *
439
+ * Drivers handle message distribution across server instances. The `local` driver
440
+ * keeps messages in-memory (single server), while the `redis` driver enables
441
+ * horizontal scaling across multiple servers.
442
+ *
443
+ * Implement this interface to create custom drivers (e.g., NATS, RabbitMQ, Kafka).
444
+ *
445
+ * @example
446
+ * ```typescript
447
+ * // Example: Custom NATS driver implementation
448
+ * import { RippleDriver, DriverStatus } from '@gravito/ripple'
449
+ * import { connect, NatsConnection, Subscription } from 'nats'
450
+ *
451
+ * export class NatsDriver implements RippleDriver {
452
+ * readonly name = 'nats'
453
+ * private connection?: NatsConnection
454
+ * private subscriptions = new Map<string, Subscription>()
455
+ * private status: DriverStatus = {
456
+ * name: 'nats',
457
+ * initialized: false,
458
+ * connected: false
459
+ * }
460
+ *
461
+ * async init(): Promise<void> {
462
+ * try {
463
+ * this.connection = await connect({ servers: 'nats://localhost:4222' })
464
+ * this.status.initialized = true
465
+ * this.status.connected = true
466
+ * } catch (error) {
467
+ * this.status.lastError = error.message
468
+ * throw error
469
+ * }
470
+ * }
471
+ *
472
+ * async publish(channel: string, event: string, data: unknown): Promise<void> {
473
+ * const payload = JSON.stringify({ event, data })
474
+ * await this.connection?.publish(`ripple.${channel}`, payload)
475
+ * }
476
+ *
477
+ * async subscribe(channel: string, callback: (event: string, data: unknown) => void): Promise<void> {
478
+ * const sub = this.connection?.subscribe(`ripple.${channel}`)
479
+ * this.subscriptions.set(channel, sub!)
480
+ *
481
+ * for await (const msg of sub!) {
482
+ * const { event, data } = JSON.parse(msg.data.toString())
483
+ * callback(event, data)
484
+ * }
485
+ * }
486
+ *
487
+ * async unsubscribe(channel: string): Promise<void> {
488
+ * const sub = this.subscriptions.get(channel)
489
+ * if (sub) {
490
+ * await sub.unsubscribe()
491
+ * this.subscriptions.delete(channel)
492
+ * }
493
+ * }
494
+ *
495
+ * async shutdown(): Promise<void> {
496
+ * await this.connection?.close()
497
+ * this.status.connected = false
498
+ * }
499
+ *
500
+ * getStatus(): DriverStatus {
501
+ * return this.status
502
+ * }
503
+ * }
504
+ * ```
505
+ */
506
+ export interface RippleDriver {
507
+ /** Driver name (e.g., 'local', 'redis', 'nats') */
508
+ readonly name: string;
509
+ /**
510
+ * Publish a message to a channel.
511
+ *
512
+ * @param channel - Channel name to publish to
513
+ * @param event - Event name
514
+ * @param data - Event payload
515
+ */
516
+ publish(channel: string, event: string, data: unknown): Promise<void>;
517
+ /**
518
+ * Subscribe to a channel for incoming messages (optional).
519
+ *
520
+ * For multi-server setups, implement this to receive messages from other servers.
521
+ * The local driver doesn't need this since messages are in-memory.
522
+ *
523
+ * @param channel - Channel name to subscribe to
524
+ * @param callback - Called when a message is received
525
+ */
526
+ subscribe?(channel: string, callback: (event: string, data: unknown) => void): Promise<void>;
527
+ /**
528
+ * Unsubscribe from a channel (optional).
529
+ *
530
+ * @param channel - Channel name to unsubscribe from
531
+ */
532
+ unsubscribe?(channel: string): Promise<void>;
533
+ /**
534
+ * Initialize the driver (optional).
535
+ *
536
+ * Called when RippleServer starts. Use this to establish connections,
537
+ * initialize resources, etc.
538
+ */
539
+ init?(): Promise<void>;
540
+ /**
541
+ * Shutdown the driver (optional).
542
+ *
543
+ * Called when RippleServer shuts down. Clean up connections and resources here.
544
+ */
545
+ shutdown?(): Promise<void>;
546
+ /**
547
+ * Get current driver status (optional).
548
+ *
549
+ * @returns Current driver status
550
+ */
551
+ getStatus?(): DriverStatus;
552
+ }
553
+ /**
554
+ * Ripple server configuration.
555
+ *
556
+ * Configures WebSocket behavior, authentication, drivers, and observability.
557
+ *
558
+ * @example
559
+ * ```typescript
560
+ * // Example 1: Basic setup with local driver (single server)
561
+ * const config: RippleConfig = {
562
+ * path: '/ws',
563
+ * authorizer: async (channel, userId, socketId) => {
564
+ * if (channel.startsWith('private-')) {
565
+ * return userId !== undefined
566
+ * }
567
+ * return true
568
+ * }
569
+ * }
570
+ *
571
+ * // Example 2: Production setup with Redis driver (multi-server)
572
+ * const config: RippleConfig = {
573
+ * path: '/realtime',
574
+ * driver: 'redis',
575
+ * redis: {
576
+ * host: process.env.REDIS_HOST || 'localhost',
577
+ * port: 6379,
578
+ * password: process.env.REDIS_PASSWORD,
579
+ * db: 0
580
+ * },
581
+ * authorizer: async (channel, userId, socketId) => {
582
+ * if (channel.startsWith('presence-')) {
583
+ * if (!userId) return false
584
+ * const user = await db.users.findById(userId)
585
+ * return {
586
+ * id: user.id,
587
+ * info: { name: user.name, avatar: user.avatarUrl }
588
+ * }
589
+ * }
590
+ * return userId !== undefined
591
+ * },
592
+ * pingInterval: 30000,
593
+ * logLevel: 'info'
594
+ * }
595
+ *
596
+ * // Example 3: Custom logger and health check
597
+ * import { RippleLogger } from '@gravito/ripple'
598
+ *
599
+ * const customLogger: RippleLogger = {
600
+ * debug: (message, context) => console.debug(message, context),
601
+ * info: (message, context) => console.info(message, context),
602
+ * warn: (message, context) => console.warn(message, context),
603
+ * error: (message, context) => console.error(message, context)
604
+ * }
605
+ *
606
+ * const config: RippleConfig = {
607
+ * logger: customLogger,
608
+ * logLevel: 'debug',
609
+ * healthCheck: {
610
+ * enabled: true,
611
+ * path: '/health'
612
+ * }
613
+ * }
614
+ *
615
+ * // Example 4: Connection tracking
616
+ * import { ConnectionTracker } from '@gravito/ripple'
617
+ *
618
+ * const tracker = new ConnectionTracker()
619
+ * const config: RippleConfig = {
620
+ * connectionTracker: tracker
621
+ * }
622
+ *
623
+ * // Later: query connection stats
624
+ * const stats = tracker.getStats()
625
+ * console.log('Total connections:', stats.totalConnections)
626
+ * console.log('Active connections:', stats.activeConnections)
627
+ * ```
628
+ */
629
+ export interface RippleConfig {
630
+ /** WebSocket endpoint path (default: '/ws') */
631
+ path?: string;
632
+ /** Authentication endpoint for private/presence channels */
633
+ authEndpoint?: string;
634
+ /** Driver to use ('local' | 'redis') */
635
+ driver?: 'local' | 'redis';
636
+ /** Redis configuration (if using redis driver) */
637
+ redis?: {
638
+ host?: string;
639
+ port?: number;
640
+ password?: string;
641
+ db?: number;
642
+ };
643
+ /** Channel authorizer function */
644
+ authorizer?: ChannelAuthorizer;
645
+ /** Ping interval in milliseconds (default: 30000) */
646
+ pingInterval?: number;
647
+ /** Custom logger */
648
+ logger?: import('./logging/Logger').RippleLogger;
649
+ /** Log level */
650
+ logLevel?: import('./logging/Logger').LogLevel;
651
+ /** Connection tracker */
652
+ connectionTracker?: import('./tracking/ConnectionTracker').ConnectionTracker;
653
+ /** Health check configuration */
654
+ healthCheck?: {
655
+ enabled: boolean;
656
+ path?: string;
657
+ };
658
+ }
659
+ /**
660
+ * Strongly-typed Bun ServerWebSocket for Ripple.
661
+ *
662
+ * This type adds `ClientData` to Bun's native `ServerWebSocket`, providing
663
+ * type-safe access to client ID, user ID, and channel subscriptions.
664
+ *
665
+ * @example
666
+ * ```typescript
667
+ * import { RippleWebSocket } from '@gravito/ripple'
668
+ *
669
+ * const handleMessage = (ws: RippleWebSocket, message: string) => {
670
+ * // Type-safe access to client data
671
+ * console.log('Client ID:', ws.data.id)
672
+ * console.log('User ID:', ws.data.userId)
673
+ * console.log('Channels:', Array.from(ws.data.channels))
674
+ *
675
+ * // Standard Bun WebSocket methods
676
+ * ws.send('Message received')
677
+ * ws.close()
678
+ * }
679
+ * ```
680
+ *
681
+ * @public
682
+ * @since 3.0.0
683
+ */
684
+ export type RippleWebSocket = ServerWebSocket<ClientData>;
685
+ /**
686
+ * Strongly-typed Bun Server for Ripple.
687
+ *
688
+ * This type ensures the Bun server is configured with `ClientData` for WebSocket handling.
689
+ *
690
+ * @example
691
+ * ```typescript
692
+ * import { RippleBunServer } from '@gravito/ripple'
693
+ *
694
+ * let server: RippleBunServer
695
+ *
696
+ * server = Bun.serve({
697
+ * port: 3000,
698
+ * fetch: (req, server) => {
699
+ * if (ripple.upgrade(req, server)) return
700
+ * return new Response('Not found', { status: 404 })
701
+ * },
702
+ * websocket: ripple.getHandler()
703
+ * })
704
+ *
705
+ * // Later: graceful shutdown
706
+ * server.stop()
707
+ * ```
708
+ *
709
+ * @public
710
+ * @since 3.0.0
711
+ */
712
+ export type RippleBunServer = Server<ClientData>;
713
+ /**
714
+ * WebSocket handler configuration for Bun.serve.
715
+ *
716
+ * Defines the WebSocket lifecycle event handlers that Bun expects.
717
+ * RippleServer implements this interface internally.
718
+ *
719
+ * @example
720
+ * ```typescript
721
+ * import { WebSocketHandlerConfig, RippleWebSocket } from '@gravito/ripple'
722
+ *
723
+ * // Custom WebSocket handler (usually handled by RippleServer)
724
+ * const handler: WebSocketHandlerConfig = {
725
+ * open: (ws: RippleWebSocket) => {
726
+ * console.log('Client connected:', ws.data.id)
727
+ * },
728
+ *
729
+ * message: (ws: RippleWebSocket, message: string | Buffer) => {
730
+ * console.log('Received:', message)
731
+ * },
732
+ *
733
+ * close: (ws: RippleWebSocket, code: number, reason: string) => {
734
+ * console.log('Client disconnected:', code, reason)
735
+ * },
736
+ *
737
+ * drain: (ws: RippleWebSocket) => {
738
+ * console.log('Backpressure drained')
739
+ * }
740
+ * }
741
+ *
742
+ * // Pass to Bun.serve
743
+ * Bun.serve({
744
+ * websocket: handler
745
+ * })
746
+ * ```
747
+ */
748
+ export interface WebSocketHandlerConfig {
749
+ open: (ws: RippleWebSocket) => void | Promise<void>;
750
+ message: (ws: RippleWebSocket, message: string | Buffer) => void | Promise<void>;
751
+ close: (ws: RippleWebSocket, code: number, reason: string) => void | Promise<void>;
752
+ drain?: (ws: RippleWebSocket) => void;
753
+ }