@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.
- package/README.md +179 -6
- package/README.zh-TW.md +104 -2
- package/dist/core/src/Application.d.ts +215 -0
- package/dist/core/src/ConfigManager.d.ts +26 -0
- package/dist/core/src/Container.d.ts +78 -0
- package/dist/core/src/ErrorHandler.d.ts +63 -0
- package/dist/core/src/Event.d.ts +5 -0
- package/dist/core/src/EventManager.d.ts +123 -0
- package/dist/core/src/GlobalErrorHandlers.d.ts +47 -0
- package/dist/core/src/GravitoServer.d.ts +28 -0
- package/dist/core/src/HookManager.d.ts +84 -0
- package/dist/core/src/Listener.d.ts +4 -0
- package/dist/core/src/Logger.d.ts +20 -0
- package/dist/core/src/PlanetCore.d.ts +289 -0
- package/dist/core/src/Route.d.ts +36 -0
- package/dist/core/src/Router.d.ts +288 -0
- package/dist/core/src/ServiceProvider.d.ts +156 -0
- package/dist/core/src/adapters/GravitoEngineAdapter.d.ts +26 -0
- package/dist/core/src/adapters/PhotonAdapter.d.ts +170 -0
- package/dist/core/src/adapters/bun/BunContext.d.ts +45 -0
- package/dist/core/src/adapters/bun/BunNativeAdapter.d.ts +30 -0
- package/dist/core/src/adapters/bun/BunRequest.d.ts +31 -0
- package/dist/core/src/adapters/bun/RadixNode.d.ts +19 -0
- package/dist/core/src/adapters/bun/RadixRouter.d.ts +31 -0
- package/dist/core/src/adapters/bun/types.d.ts +20 -0
- package/dist/core/src/adapters/photon-types.d.ts +73 -0
- package/dist/core/src/adapters/types.d.ts +208 -0
- package/dist/core/src/engine/AOTRouter.d.ts +134 -0
- package/dist/core/src/engine/FastContext.d.ts +98 -0
- package/dist/core/src/engine/Gravito.d.ts +137 -0
- package/dist/core/src/engine/MinimalContext.d.ts +77 -0
- package/dist/core/src/engine/analyzer.d.ts +27 -0
- package/dist/core/src/engine/constants.d.ts +23 -0
- package/dist/core/src/engine/index.d.ts +26 -0
- package/dist/core/src/engine/path.d.ts +26 -0
- package/dist/core/src/engine/pool.d.ts +83 -0
- package/dist/core/src/engine/types.d.ts +138 -0
- package/dist/core/src/exceptions/AuthenticationException.d.ts +8 -0
- package/dist/core/src/exceptions/AuthorizationException.d.ts +8 -0
- package/dist/core/src/exceptions/GravitoException.d.ts +23 -0
- package/dist/core/src/exceptions/HttpException.d.ts +9 -0
- package/dist/core/src/exceptions/ModelNotFoundException.d.ts +10 -0
- package/dist/core/src/exceptions/ValidationException.d.ts +22 -0
- package/dist/core/src/exceptions/index.d.ts +6 -0
- package/dist/core/src/helpers/Arr.d.ts +19 -0
- package/dist/core/src/helpers/Str.d.ts +23 -0
- package/dist/core/src/helpers/data.d.ts +25 -0
- package/dist/core/src/helpers/errors.d.ts +34 -0
- package/dist/core/src/helpers/response.d.ts +41 -0
- package/dist/core/src/helpers.d.ts +338 -0
- package/dist/core/src/http/CookieJar.d.ts +51 -0
- package/dist/core/src/http/cookie.d.ts +29 -0
- package/dist/core/src/http/middleware/BodySizeLimit.d.ts +16 -0
- package/dist/core/src/http/middleware/Cors.d.ts +24 -0
- package/dist/core/src/http/middleware/Csrf.d.ts +23 -0
- package/dist/core/src/http/middleware/HeaderTokenGate.d.ts +28 -0
- package/dist/core/src/http/middleware/SecurityHeaders.d.ts +29 -0
- package/dist/core/src/http/middleware/ThrottleRequests.d.ts +18 -0
- package/dist/core/src/http/types.d.ts +334 -0
- package/dist/core/src/index.d.ts +67 -0
- package/dist/core/src/runtime.d.ts +119 -0
- package/dist/core/src/security/Encrypter.d.ts +33 -0
- package/dist/core/src/security/Hasher.d.ts +29 -0
- package/dist/core/src/testing/HttpTester.d.ts +39 -0
- package/dist/core/src/testing/TestResponse.d.ts +78 -0
- package/dist/core/src/testing/index.d.ts +2 -0
- package/dist/core/src/types/events.d.ts +94 -0
- package/dist/index.js +10206 -37
- package/dist/index.js.map +69 -10
- package/dist/photon/src/index.d.ts +70 -0
- package/dist/photon/src/middleware/binary.d.ts +31 -0
- package/dist/photon/src/middleware/htmx.d.ts +39 -0
- package/dist/ripple/src/OrbitRipple.d.ts +64 -0
- package/dist/ripple/src/RippleServer.d.ts +518 -0
- package/dist/{channels → ripple/src/channels}/Channel.d.ts +6 -1
- package/dist/ripple/src/channels/ChannelManager.d.ts +173 -0
- package/dist/{channels → ripple/src/channels}/index.d.ts +0 -1
- package/dist/ripple/src/drivers/LocalDriver.d.ts +61 -0
- package/dist/ripple/src/drivers/RedisDriver.d.ts +141 -0
- package/dist/ripple/src/drivers/index.d.ts +2 -0
- package/dist/ripple/src/errors/RippleError.d.ts +48 -0
- package/dist/ripple/src/errors/index.d.ts +1 -0
- package/dist/ripple/src/events/BroadcastEvent.d.ts +123 -0
- package/dist/ripple/src/events/BroadcastManager.d.ts +100 -0
- package/dist/ripple/src/events/Broadcaster.d.ts +264 -0
- package/dist/{events → ripple/src/events}/index.d.ts +1 -1
- package/dist/ripple/src/health/HealthChecker.d.ts +93 -0
- package/dist/ripple/src/health/index.d.ts +1 -0
- package/dist/ripple/src/index.d.ts +60 -0
- package/dist/ripple/src/logging/Logger.d.ts +99 -0
- package/dist/ripple/src/logging/index.d.ts +1 -0
- package/dist/ripple/src/tracking/ConnectionTracker.d.ts +116 -0
- package/dist/ripple/src/tracking/index.d.ts +1 -0
- package/dist/ripple/src/types.d.ts +753 -0
- package/dist/ripple/src/utils/MessageSerializer.d.ts +44 -0
- package/dist/ripple/src/utils/index.d.ts +1 -0
- package/package.json +14 -5
- package/dist/OrbitRipple.d.ts +0 -80
- package/dist/OrbitRipple.d.ts.map +0 -1
- package/dist/RippleServer.d.ts +0 -126
- package/dist/RippleServer.d.ts.map +0 -1
- package/dist/channels/Channel.d.ts.map +0 -1
- package/dist/channels/ChannelManager.d.ts +0 -79
- package/dist/channels/ChannelManager.d.ts.map +0 -1
- package/dist/channels/index.d.ts.map +0 -1
- package/dist/drivers/LocalDriver.d.ts +0 -30
- package/dist/drivers/LocalDriver.d.ts.map +0 -1
- package/dist/drivers/index.d.ts +0 -2
- package/dist/drivers/index.d.ts.map +0 -1
- package/dist/events/BroadcastEvent.d.ts +0 -52
- package/dist/events/BroadcastEvent.d.ts.map +0 -1
- package/dist/events/Broadcaster.d.ts +0 -68
- package/dist/events/Broadcaster.d.ts.map +0 -1
- package/dist/events/index.d.ts.map +0 -1
- package/dist/index.d.ts +0 -38
- package/dist/index.d.ts.map +0 -1
- package/dist/types.d.ts +0 -163
- 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
|
+
}
|