@gravito/ripple 1.0.0-alpha.2

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 ADDED
@@ -0,0 +1,190 @@
1
+ # @gravito/ripple
2
+
3
+ > 🌊 Bun-native WebSocket broadcasting for Gravito. Channel-based real-time communication.
4
+
5
+ ## Features
6
+
7
+ - **Bun Native WebSocket** - Zero external dependencies, maximum performance
8
+ - **Channel-based Broadcasting** - Public, Private, and Presence channels
9
+ - **Laravel Echo-style API** - Familiar patterns for Laravel developers
10
+ - **Sentinel Integration** - Built-in authentication for private channels
11
+ - **Horizontal Scaling** - Redis driver support (coming soon)
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ bun add @gravito/ripple
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ### Server Setup
22
+
23
+ ```typescript
24
+ import { PlanetCore } from 'gravito-core'
25
+ import { OrbitRipple, RippleServer } from '@gravito/ripple'
26
+
27
+ const core = new PlanetCore()
28
+
29
+ // Install Ripple WebSocket module
30
+ core.install(new OrbitRipple({
31
+ path: '/ws',
32
+ authorizer: async (channel, userId, socketId) => {
33
+ // Return true for authorized, false for denied
34
+ // For presence channels, return { id: userId, info: { name: '...' } }
35
+ if (channel.startsWith('private-orders.')) {
36
+ return userId !== undefined
37
+ }
38
+ return true
39
+ }
40
+ }))
41
+
42
+ // Get the Ripple module
43
+ const ripple = core.container.make<OrbitRipple>('ripple')
44
+
45
+ // Start server with WebSocket support
46
+ Bun.serve({
47
+ port: 3000,
48
+ fetch: (req, server) => {
49
+ // Let Ripple handle WebSocket upgrades
50
+ if (ripple.getServer().upgrade(req, server)) return
51
+
52
+ // Regular HTTP handling
53
+ return core.adapter.fetch(req, server)
54
+ },
55
+ websocket: ripple.getHandler()
56
+ })
57
+ ```
58
+
59
+ ### Broadcasting Events
60
+
61
+ ```typescript
62
+ import { broadcast, PrivateChannel, BroadcastEvent } from '@gravito/ripple'
63
+
64
+ // Define a broadcast event
65
+ class OrderShipped extends BroadcastEvent {
66
+ constructor(public order: { id: number; userId: number }) {
67
+ super()
68
+ }
69
+
70
+ broadcastOn() {
71
+ return new PrivateChannel(`orders.${this.order.userId}`)
72
+ }
73
+
74
+ broadcastAs() {
75
+ return 'OrderShipped' // Event name
76
+ }
77
+ }
78
+
79
+ // Broadcast from anywhere in your app
80
+ broadcast(new OrderShipped({ id: 123, userId: 456 }))
81
+ ```
82
+
83
+ ### Fluent API
84
+
85
+ ```typescript
86
+ import { Broadcaster } from '@gravito/ripple'
87
+
88
+ // Broadcast to a public channel
89
+ Broadcaster.to('news')
90
+ .emit('ArticlePublished', { title: 'Hello World' })
91
+
92
+ // Broadcast to a private channel
93
+ Broadcaster.toPrivate('orders.123')
94
+ .emit('OrderUpdated', { status: 'shipped' })
95
+
96
+ // Broadcast to a presence channel
97
+ Broadcaster.toPresence('chat.lobby')
98
+ .emit('NewMessage', { message: 'Hi!' })
99
+ ```
100
+
101
+ ## Channel Types
102
+
103
+ ### Public Channel
104
+
105
+ No authentication required. Anyone can subscribe.
106
+
107
+ ```typescript
108
+ import { PublicChannel } from '@gravito/ripple'
109
+
110
+ const channel = new PublicChannel('news')
111
+ // fullName: 'news'
112
+ ```
113
+
114
+ ### Private Channel
115
+
116
+ Requires authentication. Only authorized users can subscribe.
117
+
118
+ ```typescript
119
+ import { PrivateChannel } from '@gravito/ripple'
120
+
121
+ const channel = new PrivateChannel('orders.123')
122
+ // fullName: 'private-orders.123'
123
+ ```
124
+
125
+ ### Presence Channel
126
+
127
+ Requires authentication. Tracks online users in the channel.
128
+
129
+ ```typescript
130
+ import { PresenceChannel } from '@gravito/ripple'
131
+
132
+ const channel = new PresenceChannel('chat.lobby')
133
+ // fullName: 'presence-chat.lobby'
134
+ ```
135
+
136
+ ## Client SDK
137
+
138
+ For frontend integration, use `@gravito/ripple-client` (coming soon):
139
+
140
+ ```typescript
141
+ import { createRippleClient } from '@gravito/ripple-client'
142
+
143
+ const ripple = createRippleClient({
144
+ host: 'ws://localhost:3000/ws',
145
+ authEndpoint: '/broadcasting/auth',
146
+ })
147
+
148
+ // Subscribe to public channel
149
+ ripple.channel('news')
150
+ .listen('ArticlePublished', (event) => {
151
+ console.log('New article:', event.title)
152
+ })
153
+
154
+ // Subscribe to private channel
155
+ ripple.private(`orders.${userId}`)
156
+ .listen('OrderShipped', (event) => {
157
+ toast.success('Your order has shipped!')
158
+ })
159
+
160
+ // Join presence channel
161
+ ripple.join(`chat.${roomId}`)
162
+ .here((users) => console.log('Online:', users))
163
+ .joining((user) => console.log(`${user.name} joined`))
164
+ .leaving((user) => console.log(`${user.name} left`))
165
+ ```
166
+
167
+ ## Configuration
168
+
169
+ ```typescript
170
+ interface RippleConfig {
171
+ /** WebSocket endpoint path (default: '/ws') */
172
+ path?: string
173
+
174
+ /** Authentication endpoint for private/presence channels */
175
+ authEndpoint?: string
176
+
177
+ /** Driver to use ('local' | 'redis') */
178
+ driver?: 'local' | 'redis'
179
+
180
+ /** Channel authorizer function */
181
+ authorizer?: ChannelAuthorizer
182
+
183
+ /** Ping interval in milliseconds (default: 30000) */
184
+ pingInterval?: number
185
+ }
186
+ ```
187
+
188
+ ## License
189
+
190
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ module.exports = require("./index.mjs");
package/dist/index.js ADDED
@@ -0,0 +1,520 @@
1
+ // @bun
2
+ // src/channels/Channel.ts
3
+ var CHANNEL_PREFIXES = {
4
+ private: "private-",
5
+ presence: "presence-"
6
+ };
7
+
8
+ class BaseChannel {
9
+ name;
10
+ constructor(name) {
11
+ this.name = name;
12
+ }
13
+ static parse(fullName) {
14
+ if (fullName.startsWith(CHANNEL_PREFIXES.presence)) {
15
+ return {
16
+ type: "presence",
17
+ name: fullName.slice(CHANNEL_PREFIXES.presence.length)
18
+ };
19
+ }
20
+ if (fullName.startsWith(CHANNEL_PREFIXES.private)) {
21
+ return {
22
+ type: "private",
23
+ name: fullName.slice(CHANNEL_PREFIXES.private.length)
24
+ };
25
+ }
26
+ return { type: "public", name: fullName };
27
+ }
28
+ static requiresAuth(fullName) {
29
+ return fullName.startsWith(CHANNEL_PREFIXES.private) || fullName.startsWith(CHANNEL_PREFIXES.presence);
30
+ }
31
+ }
32
+
33
+ class PublicChannel extends BaseChannel {
34
+ type = "public";
35
+ get fullName() {
36
+ return this.name;
37
+ }
38
+ }
39
+
40
+ class PrivateChannel extends BaseChannel {
41
+ type = "private";
42
+ get fullName() {
43
+ return `${CHANNEL_PREFIXES.private}${this.name}`;
44
+ }
45
+ }
46
+
47
+ class PresenceChannel extends BaseChannel {
48
+ type = "presence";
49
+ get fullName() {
50
+ return `${CHANNEL_PREFIXES.presence}${this.name}`;
51
+ }
52
+ }
53
+ function createChannel(fullName) {
54
+ const { type, name } = BaseChannel.parse(fullName);
55
+ switch (type) {
56
+ case "presence":
57
+ return new PresenceChannel(name);
58
+ case "private":
59
+ return new PrivateChannel(name);
60
+ default:
61
+ return new PublicChannel(name);
62
+ }
63
+ }
64
+ var requiresAuth = BaseChannel.requiresAuth;
65
+ // src/channels/ChannelManager.ts
66
+ class ChannelManager {
67
+ subscriptions = new Map;
68
+ clients = new Map;
69
+ presenceMembers = new Map;
70
+ addClient(ws) {
71
+ this.clients.set(ws.data.id, ws);
72
+ }
73
+ removeClient(clientId) {
74
+ const ws = this.clients.get(clientId);
75
+ if (!ws)
76
+ return [];
77
+ const leftChannels = [];
78
+ for (const channel of ws.data.channels) {
79
+ this.unsubscribe(clientId, channel);
80
+ leftChannels.push(channel);
81
+ }
82
+ this.clients.delete(clientId);
83
+ return leftChannels;
84
+ }
85
+ getClient(clientId) {
86
+ return this.clients.get(clientId);
87
+ }
88
+ getAllClients() {
89
+ return Array.from(this.clients.values());
90
+ }
91
+ subscribe(clientId, channel, userInfo) {
92
+ const ws = this.clients.get(clientId);
93
+ if (!ws)
94
+ return false;
95
+ if (!this.subscriptions.has(channel)) {
96
+ this.subscriptions.set(channel, new Set);
97
+ }
98
+ this.subscriptions.get(channel).add(clientId);
99
+ ws.data.channels.add(channel);
100
+ if (channel.startsWith(CHANNEL_PREFIXES.presence) && userInfo) {
101
+ this.addPresenceMember(channel, userInfo);
102
+ ws.data.userId = userInfo.id;
103
+ ws.data.userInfo = userInfo.info;
104
+ }
105
+ return true;
106
+ }
107
+ unsubscribe(clientId, channel) {
108
+ const ws = this.clients.get(clientId);
109
+ if (!ws)
110
+ return false;
111
+ const channelSubs = this.subscriptions.get(channel);
112
+ if (channelSubs) {
113
+ channelSubs.delete(clientId);
114
+ if (channelSubs.size === 0) {
115
+ this.subscriptions.delete(channel);
116
+ }
117
+ }
118
+ ws.data.channels.delete(channel);
119
+ if (channel.startsWith(CHANNEL_PREFIXES.presence) && ws.data.userId) {
120
+ this.removePresenceMember(channel, ws.data.userId);
121
+ }
122
+ return true;
123
+ }
124
+ getSubscribers(channel) {
125
+ const clientIds = this.subscriptions.get(channel);
126
+ if (!clientIds)
127
+ return [];
128
+ return Array.from(clientIds).map((id) => this.clients.get(id)).filter((ws) => ws !== undefined);
129
+ }
130
+ isSubscribed(clientId, channel) {
131
+ const channelSubs = this.subscriptions.get(channel);
132
+ return channelSubs?.has(clientId) ?? false;
133
+ }
134
+ addPresenceMember(channel, userInfo) {
135
+ if (!this.presenceMembers.has(channel)) {
136
+ this.presenceMembers.set(channel, new Map);
137
+ }
138
+ this.presenceMembers.get(channel).set(userInfo.id, userInfo);
139
+ }
140
+ removePresenceMember(channel, userId) {
141
+ const members = this.presenceMembers.get(channel);
142
+ if (members) {
143
+ members.delete(userId);
144
+ if (members.size === 0) {
145
+ this.presenceMembers.delete(channel);
146
+ }
147
+ }
148
+ }
149
+ getPresenceMembers(channel) {
150
+ const members = this.presenceMembers.get(channel);
151
+ return members ? Array.from(members.values()) : [];
152
+ }
153
+ getMemberCount(channel) {
154
+ return this.subscriptions.get(channel)?.size ?? 0;
155
+ }
156
+ getStats() {
157
+ return {
158
+ totalClients: this.clients.size,
159
+ totalChannels: this.subscriptions.size,
160
+ channels: Array.from(this.subscriptions.entries()).map(([name, subs]) => ({
161
+ name,
162
+ subscribers: subs.size
163
+ }))
164
+ };
165
+ }
166
+ }
167
+ // src/drivers/LocalDriver.ts
168
+ class LocalDriver {
169
+ name = "local";
170
+ listeners = new Map;
171
+ async publish(channel, event, data) {
172
+ const callbacks = this.listeners.get(channel);
173
+ if (callbacks) {
174
+ for (const callback of callbacks) {
175
+ callback(event, data);
176
+ }
177
+ }
178
+ }
179
+ async subscribe(channel, callback) {
180
+ if (!this.listeners.has(channel)) {
181
+ this.listeners.set(channel, new Set);
182
+ }
183
+ this.listeners.get(channel).add(callback);
184
+ }
185
+ async unsubscribe(channel) {
186
+ this.listeners.delete(channel);
187
+ }
188
+ async init() {}
189
+ async shutdown() {
190
+ this.listeners.clear();
191
+ }
192
+ }
193
+ // src/events/BroadcastEvent.ts
194
+ class BroadcastEvent {
195
+ broadcastAs() {
196
+ return this.constructor.name;
197
+ }
198
+ broadcastExcept() {
199
+ return [];
200
+ }
201
+ broadcastWith() {
202
+ const data = {};
203
+ for (const key of Object.keys(this)) {
204
+ data[key] = this[key];
205
+ }
206
+ return data;
207
+ }
208
+ }
209
+ // src/events/Broadcaster.ts
210
+ var globalRippleServer = null;
211
+ function setRippleServer(server) {
212
+ globalRippleServer = server;
213
+ }
214
+ function getRippleServer() {
215
+ return globalRippleServer;
216
+ }
217
+ function broadcast(event) {
218
+ if (!globalRippleServer) {
219
+ console.warn("[Ripple] No server configured. Event not broadcast.");
220
+ return;
221
+ }
222
+ const channels = event.broadcastOn();
223
+ const eventName = event.broadcastAs();
224
+ const data = event.broadcastWith();
225
+ const except = event.broadcastExcept();
226
+ const channelList = Array.isArray(channels) ? channels : [channels];
227
+ for (const channel of channelList) {
228
+ globalRippleServer.broadcast(channel.fullName, eventName, data);
229
+ }
230
+ }
231
+
232
+ class Broadcaster {
233
+ _channel;
234
+ _except = [];
235
+ constructor(channel) {
236
+ this._channel = channel;
237
+ }
238
+ static to(channel) {
239
+ return new Broadcaster(channel);
240
+ }
241
+ static toPrivate(channel) {
242
+ return new Broadcaster(`private-${channel}`);
243
+ }
244
+ static toPresence(channel) {
245
+ return new Broadcaster(`presence-${channel}`);
246
+ }
247
+ except(socketIds) {
248
+ const ids = Array.isArray(socketIds) ? socketIds : [socketIds];
249
+ this._except.push(...ids);
250
+ return this;
251
+ }
252
+ emit(event, data) {
253
+ if (!globalRippleServer) {
254
+ console.warn("[Ripple] No server configured. Event not broadcast.");
255
+ return;
256
+ }
257
+ globalRippleServer.broadcast(this._channel, event, data);
258
+ }
259
+ }
260
+ // src/RippleServer.ts
261
+ class RippleServer {
262
+ channels;
263
+ driver;
264
+ authorizer;
265
+ pingInterval;
266
+ config;
267
+ constructor(config = {}) {
268
+ this.config = {
269
+ path: "/ws",
270
+ authEndpoint: "/broadcasting/auth",
271
+ pingInterval: 30000,
272
+ ...config
273
+ };
274
+ this.channels = new ChannelManager;
275
+ this.driver = config.driver === "redis" ? new LocalDriver : new LocalDriver;
276
+ this.authorizer = config.authorizer;
277
+ }
278
+ upgrade(req, server) {
279
+ const url = new URL(req.url);
280
+ if (url.pathname !== this.config.path) {
281
+ return false;
282
+ }
283
+ const success = server.upgrade(req, {
284
+ data: {
285
+ id: crypto.randomUUID(),
286
+ channels: new Set
287
+ }
288
+ });
289
+ return success;
290
+ }
291
+ getHandler() {
292
+ return {
293
+ open: (ws) => this.handleOpen(ws),
294
+ message: (ws, message) => this.handleMessage(ws, message),
295
+ close: (ws, code, reason) => this.handleClose(ws, code, reason),
296
+ drain: (ws) => this.handleDrain(ws)
297
+ };
298
+ }
299
+ handleOpen(ws) {
300
+ this.channels.addClient(ws);
301
+ this.send(ws, {
302
+ type: "connected",
303
+ socketId: ws.data.id
304
+ });
305
+ }
306
+ async handleMessage(ws, message) {
307
+ try {
308
+ const data = JSON.parse(message.toString());
309
+ switch (data.type) {
310
+ case "subscribe":
311
+ await this.handleSubscribe(ws, data.channel, data.auth);
312
+ break;
313
+ case "unsubscribe":
314
+ this.handleUnsubscribe(ws, data.channel);
315
+ break;
316
+ case "whisper":
317
+ this.handleWhisper(ws, data.channel, data.event, data.data);
318
+ break;
319
+ case "ping":
320
+ this.send(ws, { type: "pong" });
321
+ break;
322
+ }
323
+ } catch (error) {
324
+ this.send(ws, {
325
+ type: "error",
326
+ message: error instanceof Error ? error.message : "Invalid message"
327
+ });
328
+ }
329
+ }
330
+ handleClose(ws, _code, _reason) {
331
+ const leftChannels = this.channels.removeClient(ws.data.id);
332
+ for (const channel of leftChannels) {
333
+ if (channel.startsWith("presence-") && ws.data.userId) {
334
+ this.broadcastToChannel(channel, "presence", {
335
+ event: "leave",
336
+ data: {
337
+ id: ws.data.userId,
338
+ info: ws.data.userInfo
339
+ }
340
+ });
341
+ }
342
+ }
343
+ }
344
+ handleDrain(_ws) {}
345
+ async handleSubscribe(ws, channel, _auth) {
346
+ if (requiresAuth(channel)) {
347
+ if (!this.authorizer) {
348
+ this.send(ws, {
349
+ type: "error",
350
+ message: "No authorizer configured for private channels",
351
+ channel
352
+ });
353
+ return;
354
+ }
355
+ const result = await this.authorizer(channel, ws.data.userId, ws.data.id);
356
+ if (result === false) {
357
+ this.send(ws, {
358
+ type: "error",
359
+ message: "Unauthorized",
360
+ channel
361
+ });
362
+ return;
363
+ }
364
+ if (typeof result === "object" && "id" in result) {
365
+ this.channels.subscribe(ws.data.id, channel, result);
366
+ this.broadcastToChannel(channel, "presence", {
367
+ event: "join",
368
+ data: result
369
+ }, ws.data.id);
370
+ this.send(ws, {
371
+ type: "presence",
372
+ channel,
373
+ event: "members",
374
+ data: this.channels.getPresenceMembers(channel)
375
+ });
376
+ } else {
377
+ this.channels.subscribe(ws.data.id, channel);
378
+ }
379
+ } else {
380
+ this.channels.subscribe(ws.data.id, channel);
381
+ }
382
+ this.send(ws, { type: "subscribed", channel });
383
+ }
384
+ handleUnsubscribe(ws, channel) {
385
+ if (channel.startsWith("presence-") && ws.data.userId) {
386
+ this.broadcastToChannel(channel, "presence", {
387
+ event: "leave",
388
+ data: {
389
+ id: ws.data.userId,
390
+ info: ws.data.userInfo
391
+ }
392
+ }, ws.data.id);
393
+ }
394
+ this.channels.unsubscribe(ws.data.id, channel);
395
+ this.send(ws, { type: "unsubscribed", channel });
396
+ }
397
+ handleWhisper(ws, channel, event, data) {
398
+ if (!this.channels.isSubscribed(ws.data.id, channel)) {
399
+ this.send(ws, {
400
+ type: "error",
401
+ message: "Not subscribed to channel",
402
+ channel
403
+ });
404
+ return;
405
+ }
406
+ this.broadcastToChannel(channel, event, data, ws.data.id);
407
+ }
408
+ broadcast(channel, event, data) {
409
+ this.broadcastToChannel(channel, event, data);
410
+ }
411
+ broadcastToClients(clientIds, event, data) {
412
+ for (const clientId of clientIds) {
413
+ const ws = this.channels.getClient(clientId);
414
+ if (ws) {
415
+ this.send(ws, {
416
+ type: "event",
417
+ channel: "",
418
+ event,
419
+ data
420
+ });
421
+ }
422
+ }
423
+ }
424
+ broadcastToChannel(channel, event, data, excludeClientId) {
425
+ const subscribers = this.channels.getSubscribers(channel);
426
+ for (const ws of subscribers) {
427
+ if (excludeClientId && ws.data.id === excludeClientId) {
428
+ continue;
429
+ }
430
+ if (event === "presence") {
431
+ this.send(ws, {
432
+ type: "presence",
433
+ channel,
434
+ event: data.event,
435
+ data: data.data
436
+ });
437
+ } else {
438
+ this.send(ws, {
439
+ type: "event",
440
+ channel,
441
+ event,
442
+ data
443
+ });
444
+ }
445
+ }
446
+ }
447
+ send(ws, message) {
448
+ try {
449
+ ws.send(JSON.stringify(message));
450
+ } catch {}
451
+ }
452
+ getStats() {
453
+ return this.channels.getStats();
454
+ }
455
+ async init() {
456
+ await this.driver.init?.();
457
+ if (this.config.pingInterval > 0) {
458
+ this.pingInterval = setInterval(() => {
459
+ for (const ws of this.channels.getAllClients()) {
460
+ this.send(ws, { type: "pong" });
461
+ }
462
+ }, this.config.pingInterval);
463
+ }
464
+ }
465
+ async shutdown() {
466
+ if (this.pingInterval) {
467
+ clearInterval(this.pingInterval);
468
+ }
469
+ await this.driver.shutdown?.();
470
+ }
471
+ }
472
+
473
+ // src/OrbitRipple.ts
474
+ class OrbitRipple {
475
+ server;
476
+ config;
477
+ constructor(config = {}) {
478
+ this.config = config;
479
+ this.server = new RippleServer(config);
480
+ }
481
+ install(core) {
482
+ core.logger.info("\uD83C\uDF0A Orbit Ripple installed");
483
+ setRippleServer(this.server);
484
+ core.adapter.use("*", async (ctx, next) => {
485
+ ctx.set("ripple", this.server);
486
+ await next();
487
+ });
488
+ this.server.init().then(() => {
489
+ core.logger.info(`\uD83C\uDF0A Ripple WebSocket ready at ${this.config.path || "/ws"}`);
490
+ });
491
+ core.hooks.addAction("shutdown", async () => {
492
+ await this.server.shutdown();
493
+ });
494
+ }
495
+ getServer() {
496
+ return this.server;
497
+ }
498
+ getHandler() {
499
+ return this.server.getHandler();
500
+ }
501
+ }
502
+ export {
503
+ setRippleServer,
504
+ requiresAuth,
505
+ getRippleServer,
506
+ createChannel,
507
+ broadcast,
508
+ RippleServer,
509
+ PublicChannel,
510
+ PrivateChannel,
511
+ PresenceChannel,
512
+ OrbitRipple,
513
+ LocalDriver,
514
+ ChannelManager,
515
+ CHANNEL_PREFIXES,
516
+ Broadcaster,
517
+ BroadcastEvent
518
+ };
519
+
520
+ //# debugId=9A29471B4A59755064756E2164756E21
@@ -0,0 +1,16 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/channels/Channel.ts", "../src/channels/ChannelManager.ts", "../src/drivers/LocalDriver.ts", "../src/events/BroadcastEvent.ts", "../src/events/Broadcaster.ts", "../src/RippleServer.ts", "../src/OrbitRipple.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * @fileoverview Channel implementations for @gravito/ripple\n * @module @gravito/ripple/channels\n */\n\nimport type { Channel, ChannelType } from '../types'\n\n// ─────────────────────────────────────────────────────────────\n// Channel Prefixes\n// ─────────────────────────────────────────────────────────────\n\nexport const CHANNEL_PREFIXES = {\n private: 'private-',\n presence: 'presence-',\n} as const\n\n// ─────────────────────────────────────────────────────────────\n// Base Channel Class\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Abstract base class for channels\n */\nabstract class BaseChannel implements Channel {\n abstract readonly type: ChannelType\n\n constructor(public readonly name: string) {}\n\n abstract get fullName(): string\n\n /**\n * Parse a full channel name to extract type and base name\n */\n static parse(fullName: string): { type: ChannelType; name: string } {\n if (fullName.startsWith(CHANNEL_PREFIXES.presence)) {\n return {\n type: 'presence',\n name: fullName.slice(CHANNEL_PREFIXES.presence.length),\n }\n }\n if (fullName.startsWith(CHANNEL_PREFIXES.private)) {\n return {\n type: 'private',\n name: fullName.slice(CHANNEL_PREFIXES.private.length),\n }\n }\n return { type: 'public', name: fullName }\n }\n\n /**\n * Check if a channel name requires authentication\n */\n static requiresAuth(fullName: string): boolean {\n return (\n fullName.startsWith(CHANNEL_PREFIXES.private) ||\n fullName.startsWith(CHANNEL_PREFIXES.presence)\n )\n }\n}\n\n// ─────────────────────────────────────────────────────────────\n// Public Channel\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Public channel - no authentication required\n *\n * @example\n * ```typescript\n * const channel = new PublicChannel('news')\n * // fullName: 'news'\n * ```\n */\nexport class PublicChannel extends BaseChannel {\n readonly type = 'public' as const\n\n get fullName(): string {\n return this.name\n }\n}\n\n// ─────────────────────────────────────────────────────────────\n// Private Channel\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Private channel - requires authentication\n *\n * @example\n * ```typescript\n * const channel = new PrivateChannel('orders.123')\n * // fullName: 'private-orders.123'\n * ```\n */\nexport class PrivateChannel extends BaseChannel {\n readonly type = 'private' as const\n\n get fullName(): string {\n return `${CHANNEL_PREFIXES.private}${this.name}`\n }\n}\n\n// ─────────────────────────────────────────────────────────────\n// Presence Channel\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Presence channel - requires authentication, tracks online users\n *\n * @example\n * ```typescript\n * const channel = new PresenceChannel('chat.lobby')\n * // fullName: 'presence-chat.lobby'\n * ```\n */\nexport class PresenceChannel extends BaseChannel {\n readonly type = 'presence' as const\n\n get fullName(): string {\n return `${CHANNEL_PREFIXES.presence}${this.name}`\n }\n}\n\n// ─────────────────────────────────────────────────────────────\n// Factory Functions\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Create a channel from a full name\n */\nexport function createChannel(fullName: string): Channel {\n const { type, name } = BaseChannel.parse(fullName)\n\n switch (type) {\n case 'presence':\n return new PresenceChannel(name)\n case 'private':\n return new PrivateChannel(name)\n default:\n return new PublicChannel(name)\n }\n}\n\n/**\n * Check if channel requires authentication\n */\nexport const requiresAuth = BaseChannel.requiresAuth\n",
6
+ "/**\n * @fileoverview Channel Manager for @gravito/ripple\n *\n * Manages channel subscriptions and member tracking.\n *\n * @module @gravito/ripple/channels\n */\n\nimport type { ClientData, PresenceUserInfo, RippleWebSocket } from '../types'\nimport { CHANNEL_PREFIXES } from './Channel'\n\n/**\n * Manages all channel subscriptions and presence tracking\n */\nexport class ChannelManager {\n /** Map of channel name -> Set of client IDs */\n private subscriptions = new Map<string, Set<string>>()\n\n /** Map of client ID -> WebSocket */\n private clients = new Map<string, RippleWebSocket>()\n\n /** Map of presence channel -> Map of user ID -> user info */\n private presenceMembers = new Map<string, Map<string | number, PresenceUserInfo>>()\n\n // ─────────────────────────────────────────────────────────────\n // Client Management\n // ─────────────────────────────────────────────────────────────\n\n /**\n * Register a new client connection\n */\n addClient(ws: RippleWebSocket): void {\n this.clients.set(ws.data.id, ws)\n }\n\n /**\n * Remove a client and all its subscriptions\n */\n removeClient(clientId: string): string[] {\n const ws = this.clients.get(clientId)\n if (!ws) return []\n\n const leftChannels: string[] = []\n\n // Unsubscribe from all channels\n for (const channel of ws.data.channels) {\n this.unsubscribe(clientId, channel)\n leftChannels.push(channel)\n }\n\n this.clients.delete(clientId)\n return leftChannels\n }\n\n /**\n * Get a client by ID\n */\n getClient(clientId: string): RippleWebSocket | undefined {\n return this.clients.get(clientId)\n }\n\n /**\n * Get all connected clients\n */\n getAllClients(): RippleWebSocket[] {\n return Array.from(this.clients.values())\n }\n\n // ─────────────────────────────────────────────────────────────\n // Subscription Management\n // ─────────────────────────────────────────────────────────────\n\n /**\n * Subscribe a client to a channel\n */\n subscribe(clientId: string, channel: string, userInfo?: PresenceUserInfo): boolean {\n const ws = this.clients.get(clientId)\n if (!ws) return false\n\n // Add to channel subscriptions\n if (!this.subscriptions.has(channel)) {\n this.subscriptions.set(channel, new Set())\n }\n this.subscriptions.get(channel)!.add(clientId)\n\n // Track in client's channel set\n ws.data.channels.add(channel)\n\n // Handle presence channel\n if (channel.startsWith(CHANNEL_PREFIXES.presence) && userInfo) {\n this.addPresenceMember(channel, userInfo)\n ws.data.userId = userInfo.id\n ws.data.userInfo = userInfo.info\n }\n\n return true\n }\n\n /**\n * Unsubscribe a client from a channel\n */\n unsubscribe(clientId: string, channel: string): boolean {\n const ws = this.clients.get(clientId)\n if (!ws) return false\n\n // Remove from channel subscriptions\n const channelSubs = this.subscriptions.get(channel)\n if (channelSubs) {\n channelSubs.delete(clientId)\n if (channelSubs.size === 0) {\n this.subscriptions.delete(channel)\n }\n }\n\n // Remove from client's channel set\n ws.data.channels.delete(channel)\n\n // Handle presence channel\n if (channel.startsWith(CHANNEL_PREFIXES.presence) && ws.data.userId) {\n this.removePresenceMember(channel, ws.data.userId)\n }\n\n return true\n }\n\n /**\n * Get all subscribers of a channel\n */\n getSubscribers(channel: string): RippleWebSocket[] {\n const clientIds = this.subscriptions.get(channel)\n if (!clientIds) return []\n\n return Array.from(clientIds)\n .map((id) => this.clients.get(id))\n .filter((ws): ws is RippleWebSocket => ws !== undefined)\n }\n\n /**\n * Check if a client is subscribed to a channel\n */\n isSubscribed(clientId: string, channel: string): boolean {\n const channelSubs = this.subscriptions.get(channel)\n return channelSubs?.has(clientId) ?? false\n }\n\n // ─────────────────────────────────────────────────────────────\n // Presence Management\n // ─────────────────────────────────────────────────────────────\n\n /**\n * Add a member to a presence channel\n */\n private addPresenceMember(channel: string, userInfo: PresenceUserInfo): void {\n if (!this.presenceMembers.has(channel)) {\n this.presenceMembers.set(channel, new Map())\n }\n this.presenceMembers.get(channel)!.set(userInfo.id, userInfo)\n }\n\n /**\n * Remove a member from a presence channel\n */\n private removePresenceMember(channel: string, userId: string | number): void {\n const members = this.presenceMembers.get(channel)\n if (members) {\n members.delete(userId)\n if (members.size === 0) {\n this.presenceMembers.delete(channel)\n }\n }\n }\n\n /**\n * Get all members of a presence channel\n */\n getPresenceMembers(channel: string): PresenceUserInfo[] {\n const members = this.presenceMembers.get(channel)\n return members ? Array.from(members.values()) : []\n }\n\n /**\n * Get member count for a channel\n */\n getMemberCount(channel: string): number {\n return this.subscriptions.get(channel)?.size ?? 0\n }\n\n // ─────────────────────────────────────────────────────────────\n // Statistics\n // ─────────────────────────────────────────────────────────────\n\n /**\n * Get channel statistics\n */\n getStats(): {\n totalClients: number\n totalChannels: number\n channels: { name: string; subscribers: number }[]\n } {\n return {\n totalClients: this.clients.size,\n totalChannels: this.subscriptions.size,\n channels: Array.from(this.subscriptions.entries()).map(([name, subs]) => ({\n name,\n subscribers: subs.size,\n })),\n }\n }\n}\n",
7
+ "/**\n * @fileoverview Local (in-memory) driver for @gravito/ripple\n *\n * Suitable for single-instance deployments. For horizontal scaling,\n * use the Redis driver.\n *\n * @module @gravito/ripple/drivers\n */\n\nimport type { RippleDriver } from '../types'\n\n/**\n * In-memory driver for single-instance deployments\n *\n * This driver keeps all state in memory and is suitable for:\n * - Development\n * - Single-server deployments\n * - Serverless functions (with caveats)\n *\n * For multi-server deployments, use RedisDriver instead.\n */\nexport class LocalDriver implements RippleDriver {\n readonly name = 'local'\n\n /** Event callbacks per channel */\n private listeners = new Map<string, Set<(event: string, data: unknown) => void>>()\n\n async publish(channel: string, event: string, data: unknown): Promise<void> {\n const callbacks = this.listeners.get(channel)\n if (callbacks) {\n for (const callback of callbacks) {\n callback(event, data)\n }\n }\n }\n\n async subscribe(\n channel: string,\n callback: (event: string, data: unknown) => void\n ): Promise<void> {\n if (!this.listeners.has(channel)) {\n this.listeners.set(channel, new Set())\n }\n this.listeners.get(channel)!.add(callback)\n }\n\n async unsubscribe(channel: string): Promise<void> {\n this.listeners.delete(channel)\n }\n\n async init(): Promise<void> {\n // No-op for local driver\n }\n\n async shutdown(): Promise<void> {\n this.listeners.clear()\n }\n}\n",
8
+ "/**\n * @fileoverview Broadcast Event base class\n *\n * Events that can be broadcast to WebSocket channels.\n *\n * @module @gravito/ripple/events\n */\n\nimport type { Channel } from '../types'\n\n/**\n * Abstract base class for broadcast events\n *\n * @example\n * ```typescript\n * class OrderShipped extends BroadcastEvent {\n * constructor(public order: Order) {\n * super()\n * }\n *\n * broadcastOn() {\n * return new PrivateChannel(`orders.${this.order.userId}`)\n * }\n *\n * broadcastAs() {\n * return 'OrderShipped'\n * }\n * }\n *\n * // Broadcast\n * broadcast(new OrderShipped(order))\n * ```\n */\nexport abstract class BroadcastEvent {\n /**\n * The channels to broadcast this event on\n */\n abstract broadcastOn(): Channel | Channel[]\n\n /**\n * The event name to use when broadcasting\n * Defaults to the class name\n */\n broadcastAs(): string {\n return this.constructor.name\n }\n\n /**\n * Socket IDs to exclude from the broadcast\n */\n broadcastExcept(): string[] {\n return []\n }\n\n /**\n * Get the event data payload\n * Override to customize the broadcast payload\n */\n broadcastWith(): Record<string, unknown> {\n // By default, return all public properties\n const data: Record<string, unknown> = {}\n for (const key of Object.keys(this)) {\n data[key] = (this as Record<string, unknown>)[key]\n }\n return data\n }\n}\n",
9
+ "/**\n * @fileoverview Broadcaster for sending events to channels\n *\n * @module @gravito/ripple/events\n */\n\nimport type { RippleServer } from '../RippleServer'\nimport type { BroadcastEvent } from './BroadcastEvent'\n\n/**\n * Global Ripple server instance holder\n */\nlet globalRippleServer: RippleServer | null = null\n\n/**\n * Set the global Ripple server instance\n */\nexport function setRippleServer(server: RippleServer): void {\n globalRippleServer = server\n}\n\n/**\n * Get the global Ripple server instance\n */\nexport function getRippleServer(): RippleServer | null {\n return globalRippleServer\n}\n\n/**\n * Broadcast an event to its channels\n *\n * @example\n * ```typescript\n * class OrderShipped extends BroadcastEvent {\n * constructor(public order: Order) { super() }\n * broadcastOn() { return new PrivateChannel(`orders.${this.order.userId}`) }\n * }\n *\n * broadcast(new OrderShipped(order))\n * ```\n */\nexport function broadcast(event: BroadcastEvent): void {\n if (!globalRippleServer) {\n console.warn('[Ripple] No server configured. Event not broadcast.')\n return\n }\n\n const channels = event.broadcastOn()\n const eventName = event.broadcastAs()\n const data = event.broadcastWith()\n const except = event.broadcastExcept()\n\n const channelList = Array.isArray(channels) ? channels : [channels]\n\n for (const channel of channelList) {\n // For each subscriber in the channel, excluding specified sockets\n globalRippleServer.broadcast(channel.fullName, eventName, data)\n }\n}\n\n/**\n * Fluent Broadcaster API for more control\n *\n * @example\n * ```typescript\n * Broadcaster.to('orders.123')\n * .emit('OrderUpdated', { status: 'shipped' })\n *\n * Broadcaster.toPrivate('orders.123')\n * .except(socketId)\n * .emit('OrderUpdated', { status: 'shipped' })\n * ```\n */\nexport class Broadcaster {\n private _channel: string\n private _except: string[] = []\n\n private constructor(channel: string) {\n this._channel = channel\n }\n\n /**\n * Target a public channel\n */\n static to(channel: string): Broadcaster {\n return new Broadcaster(channel)\n }\n\n /**\n * Target a private channel\n */\n static toPrivate(channel: string): Broadcaster {\n return new Broadcaster(`private-${channel}`)\n }\n\n /**\n * Target a presence channel\n */\n static toPresence(channel: string): Broadcaster {\n return new Broadcaster(`presence-${channel}`)\n }\n\n /**\n * Exclude specific socket IDs from broadcast\n */\n except(socketIds: string | string[]): this {\n const ids = Array.isArray(socketIds) ? socketIds : [socketIds]\n this._except.push(...ids)\n return this\n }\n\n /**\n * Emit an event to the channel\n */\n emit(event: string, data: unknown): void {\n if (!globalRippleServer) {\n console.warn('[Ripple] No server configured. Event not broadcast.')\n return\n }\n\n globalRippleServer.broadcast(this._channel, event, data)\n }\n}\n",
10
+ "/**\n * @fileoverview Ripple WebSocket Server\n *\n * Core WebSocket server implementation using Bun's native WebSocket API.\n *\n * @module @gravito/ripple\n */\n\nimport type { Server } from 'bun'\nimport { ChannelManager, requiresAuth } from './channels'\nimport { LocalDriver } from './drivers'\nimport type {\n ChannelAuthorizer,\n ClientData,\n ClientMessage,\n RippleConfig,\n RippleDriver,\n RippleWebSocket,\n ServerMessage,\n WebSocketHandlerConfig,\n} from './types'\n\n/**\n * Ripple WebSocket Server\n *\n * Provides channel-based real-time communication using Bun's native WebSocket.\n *\n * @example\n * ```typescript\n * const ripple = new RippleServer({\n * path: '/ws',\n * authorizer: async (channel, userId) => {\n * // Custom authorization logic\n * return true\n * }\n * })\n *\n * Bun.serve({\n * fetch: (req, server) => {\n * if (ripple.upgrade(req, server)) return\n * return new Response('Not found', { status: 404 })\n * },\n * websocket: ripple.getHandler()\n * })\n * ```\n */\nexport class RippleServer {\n private channels: ChannelManager\n private driver: RippleDriver\n private authorizer?: ChannelAuthorizer\n private pingInterval?: Timer\n\n readonly config: Required<Pick<RippleConfig, 'path' | 'authEndpoint' | 'pingInterval'>> &\n RippleConfig\n\n constructor(config: RippleConfig = {}) {\n this.config = {\n path: '/ws',\n authEndpoint: '/broadcasting/auth',\n pingInterval: 30000,\n ...config,\n }\n\n this.channels = new ChannelManager()\n this.driver = config.driver === 'redis' ? new LocalDriver() : new LocalDriver() // TODO: RedisDriver\n this.authorizer = config.authorizer\n }\n\n // ─────────────────────────────────────────────────────────────\n // Bun.serve Integration\n // ─────────────────────────────────────────────────────────────\n\n /**\n * Attempt to upgrade an HTTP request to WebSocket\n *\n * @returns true if upgraded, false otherwise\n */\n upgrade(req: Request, server: Server<ClientData>): boolean {\n const url = new URL(req.url)\n\n if (url.pathname !== this.config.path) {\n return false\n }\n\n const success = server.upgrade(req, {\n data: {\n id: crypto.randomUUID(),\n channels: new Set<string>(),\n } satisfies ClientData,\n })\n\n return success\n }\n\n /**\n * Get WebSocket handler configuration for Bun.serve\n */\n getHandler(): WebSocketHandlerConfig {\n return {\n open: (ws) => this.handleOpen(ws),\n message: (ws, message) => this.handleMessage(ws, message),\n close: (ws, code, reason) => this.handleClose(ws, code, reason),\n drain: (ws) => this.handleDrain(ws),\n }\n }\n\n // ─────────────────────────────────────────────────────────────\n // WebSocket Event Handlers\n // ─────────────────────────────────────────────────────────────\n\n private handleOpen(ws: RippleWebSocket): void {\n this.channels.addClient(ws)\n\n // Send connection confirmation with socket ID\n this.send(ws, {\n type: 'connected',\n socketId: ws.data.id,\n })\n }\n\n private async handleMessage(ws: RippleWebSocket, message: string | Buffer): Promise<void> {\n try {\n const data: ClientMessage = JSON.parse(message.toString())\n\n switch (data.type) {\n case 'subscribe':\n await this.handleSubscribe(ws, data.channel, data.auth)\n break\n\n case 'unsubscribe':\n this.handleUnsubscribe(ws, data.channel)\n break\n\n case 'whisper':\n this.handleWhisper(ws, data.channel, data.event, data.data)\n break\n\n case 'ping':\n this.send(ws, { type: 'pong' })\n break\n }\n } catch (error) {\n this.send(ws, {\n type: 'error',\n message: error instanceof Error ? error.message : 'Invalid message',\n })\n }\n }\n\n private handleClose(ws: RippleWebSocket, _code: number, _reason: string): void {\n const leftChannels = this.channels.removeClient(ws.data.id)\n\n // Notify presence channels about user leaving\n for (const channel of leftChannels) {\n if (channel.startsWith('presence-') && ws.data.userId) {\n this.broadcastToChannel(channel, 'presence', {\n event: 'leave',\n data: {\n id: ws.data.userId,\n info: ws.data.userInfo,\n },\n })\n }\n }\n }\n\n private handleDrain(_ws: RippleWebSocket): void {\n // Called when backpressure is relieved\n // Currently no-op, but useful for flow control\n }\n\n // ─────────────────────────────────────────────────────────────\n // Subscription Handlers\n // ─────────────────────────────────────────────────────────────\n\n private async handleSubscribe(\n ws: RippleWebSocket,\n channel: string,\n _auth?: { socketId: string; signature: string }\n ): Promise<void> {\n // Check if channel requires authentication\n if (requiresAuth(channel)) {\n if (!this.authorizer) {\n this.send(ws, {\n type: 'error',\n message: 'No authorizer configured for private channels',\n channel,\n })\n return\n }\n\n const result = await this.authorizer(channel, ws.data.userId, ws.data.id)\n\n if (result === false) {\n this.send(ws, {\n type: 'error',\n message: 'Unauthorized',\n channel,\n })\n return\n }\n\n // For presence channels, result contains user info\n if (typeof result === 'object' && 'id' in result) {\n this.channels.subscribe(ws.data.id, channel, result)\n\n // Notify other members about join\n this.broadcastToChannel(\n channel,\n 'presence',\n {\n event: 'join',\n data: result,\n },\n ws.data.id\n )\n\n // Send current members to new subscriber\n this.send(ws, {\n type: 'presence',\n channel,\n event: 'members',\n data: this.channels.getPresenceMembers(channel),\n })\n } else {\n this.channels.subscribe(ws.data.id, channel)\n }\n } else {\n this.channels.subscribe(ws.data.id, channel)\n }\n\n this.send(ws, { type: 'subscribed', channel })\n }\n\n private handleUnsubscribe(ws: RippleWebSocket, channel: string): void {\n // Notify presence channel before leaving\n if (channel.startsWith('presence-') && ws.data.userId) {\n this.broadcastToChannel(\n channel,\n 'presence',\n {\n event: 'leave',\n data: {\n id: ws.data.userId,\n info: ws.data.userInfo,\n },\n },\n ws.data.id\n )\n }\n\n this.channels.unsubscribe(ws.data.id, channel)\n this.send(ws, { type: 'unsubscribed', channel })\n }\n\n private handleWhisper(ws: RippleWebSocket, channel: string, event: string, data: unknown): void {\n // Whispers are client-to-client messages, excluding sender\n if (!this.channels.isSubscribed(ws.data.id, channel)) {\n this.send(ws, {\n type: 'error',\n message: 'Not subscribed to channel',\n channel,\n })\n return\n }\n\n this.broadcastToChannel(channel, event, data, ws.data.id)\n }\n\n // ─────────────────────────────────────────────────────────────\n // Broadcasting\n // ─────────────────────────────────────────────────────────────\n\n /**\n * Broadcast an event to a channel\n */\n broadcast(channel: string, event: string, data: unknown): void {\n this.broadcastToChannel(channel, event, data)\n }\n\n /**\n * Broadcast to specific client IDs\n */\n broadcastToClients(clientIds: string[], event: string, data: unknown): void {\n for (const clientId of clientIds) {\n const ws = this.channels.getClient(clientId)\n if (ws) {\n this.send(ws, {\n type: 'event',\n channel: '',\n event,\n data,\n })\n }\n }\n }\n\n private broadcastToChannel(\n channel: string,\n event: string,\n data: unknown,\n excludeClientId?: string\n ): void {\n const subscribers = this.channels.getSubscribers(channel)\n\n for (const ws of subscribers) {\n if (excludeClientId && ws.data.id === excludeClientId) {\n continue\n }\n\n if (event === 'presence') {\n this.send(ws, {\n type: 'presence',\n channel,\n event: (data as { event: 'join' | 'leave' | 'members' }).event,\n data: (data as { data: unknown }).data,\n })\n } else {\n this.send(ws, {\n type: 'event',\n channel,\n event,\n data,\n })\n }\n }\n }\n\n // ─────────────────────────────────────────────────────────────\n // Utilities\n // ─────────────────────────────────────────────────────────────\n\n private send(ws: RippleWebSocket, message: ServerMessage): void {\n try {\n ws.send(JSON.stringify(message))\n } catch {\n // Connection might be closed\n }\n }\n\n /**\n * Get server statistics\n */\n getStats() {\n return this.channels.getStats()\n }\n\n /**\n * Initialize the server\n */\n async init(): Promise<void> {\n await this.driver.init?.()\n\n // Start ping interval\n if (this.config.pingInterval > 0) {\n this.pingInterval = setInterval(() => {\n for (const ws of this.channels.getAllClients()) {\n this.send(ws, { type: 'pong' })\n }\n }, this.config.pingInterval)\n }\n }\n\n /**\n * Shutdown the server\n */\n async shutdown(): Promise<void> {\n if (this.pingInterval) {\n clearInterval(this.pingInterval)\n }\n await this.driver.shutdown?.()\n }\n}\n",
11
+ "/**\n * @fileoverview OrbitRipple - Gravito module wrapper for Ripple WebSocket\n *\n * Integrates RippleServer with Gravito's PlanetCore.\n *\n * @module @gravito/ripple\n */\n\nimport { setRippleServer } from './events/Broadcaster'\nimport { RippleServer } from './RippleServer'\nimport type { RippleConfig } from './types'\n\n/**\n * PlanetCore interface for type safety without importing\n */\ninterface PlanetCore {\n logger: { info: (msg: string) => void }\n adapter: {\n use: (path: string, handler: (ctx: any, next: () => Promise<void>) => Promise<void>) => void\n }\n hooks: {\n addAction: (hook: string, callback: (args: unknown) => Promise<void>) => void\n }\n}\n\n/**\n * OrbitRipple - Gravito module for real-time WebSocket communication\n *\n * @example\n * ```typescript\n * import { OrbitRipple } from '@gravito/ripple'\n *\n * const core = new PlanetCore()\n *\n * core.install(new OrbitRipple({\n * path: '/ws',\n * authorizer: async (channel, userId, socketId) => {\n * // Return true for authorized, false for denied\n * // For presence channels, return { id: userId, info: { name: '...' } }\n * return true\n * }\n * }))\n *\n * // The WebSocket is automatically integrated with Bun.serve\n * core.boot()\n * ```\n */\nexport class OrbitRipple {\n private server: RippleServer\n private config: RippleConfig\n\n constructor(config: RippleConfig = {}) {\n this.config = config\n this.server = new RippleServer(config)\n }\n\n /**\n * Install the module into PlanetCore\n */\n install(core: PlanetCore): void {\n core.logger.info('🌊 Orbit Ripple installed')\n\n // Store reference globally for broadcast() function\n setRippleServer(this.server)\n\n // Expose Ripple server via context variable\n core.adapter.use('*', async (ctx, next) => {\n ctx.set('ripple' as any, this.server)\n await next()\n })\n\n // Initialize server immediately\n this.server.init().then(() => {\n core.logger.info(`🌊 Ripple WebSocket ready at ${this.config.path || '/ws'}`)\n })\n\n // Register shutdown hook\n core.hooks.addAction('shutdown', async () => {\n await this.server.shutdown()\n })\n }\n\n /**\n * Get the underlying RippleServer instance\n */\n getServer(): RippleServer {\n return this.server\n }\n\n /**\n * Get WebSocket handler for Bun.serve integration\n *\n * @example\n * ```typescript\n * const ripple = new OrbitRipple()\n *\n * Bun.serve({\n * fetch: (req, server) => {\n * // Let Ripple handle WebSocket upgrades\n * if (ripple.getServer().upgrade(req, server)) return\n *\n * // Regular HTTP handling\n * return core.adapter.fetch(req, server)\n * },\n * websocket: ripple.getHandler()\n * })\n * ```\n */\n getHandler() {\n return this.server.getHandler()\n }\n}\n"
12
+ ],
13
+ "mappings": "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",
14
+ "debugId": "9A29471B4A59755064756E2164756E21",
15
+ "names": []
16
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@gravito/ripple",
3
+ "version": "1.0.0-alpha.2",
4
+ "description": "Bun-native WebSocket broadcasting for Gravito. Channel-based real-time communication.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "bun run build.ts",
22
+ "test": "bun test",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "keywords": [
26
+ "gravito",
27
+ "websocket",
28
+ "bun",
29
+ "realtime",
30
+ "broadcasting",
31
+ "channels"
32
+ ],
33
+ "author": "Carl Lee <carllee0520@gmail.com>",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/gravito-framework/gravito.git",
38
+ "directory": "packages/ripple"
39
+ },
40
+ "homepage": "https://github.com/gravito-framework/gravito#readme",
41
+ "peerDependencies": {
42
+ "gravito-core": "1.0.0-beta.2"
43
+ },
44
+ "devDependencies": {
45
+ "bun-types": "latest",
46
+ "typescript": "^5.9.3"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }