@flightdev/realtime 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 Flight Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,410 @@
1
+ # @flight-framework/realtime
2
+
3
+ Real-time communication for Flight Framework. Unified API for WebSocket and Server-Sent Events (SSE).
4
+
5
+ ## Table of Contents
6
+
7
+ - [Features](#features)
8
+ - [Installation](#installation)
9
+ - [Quick Start](#quick-start)
10
+ - [Adapters](#adapters)
11
+ - [Channels](#channels)
12
+ - [Presence](#presence)
13
+ - [React Integration](#react-integration)
14
+ - [Vue Integration](#vue-integration)
15
+ - [Server-Side Events](#server-side-events)
16
+ - [API Reference](#api-reference)
17
+ - [License](#license)
18
+
19
+ ---
20
+
21
+ ## Features
22
+
23
+ - Multiple transport adapters (WebSocket, SSE)
24
+ - Pub/sub channels with room support
25
+ - Presence tracking (who's online)
26
+ - Automatic reconnection with backoff
27
+ - Message history and replay
28
+ - Binary data support
29
+ - React and Vue hooks
30
+ - Edge-compatible SSE adapter
31
+
32
+ ---
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ npm install @flight-framework/realtime
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Quick Start
43
+
44
+ ### Client
45
+
46
+ ```typescript
47
+ import { createRealtime } from '@flight-framework/realtime';
48
+ import { websocket } from '@flight-framework/realtime/websocket';
49
+
50
+ const realtime = createRealtime(websocket({
51
+ url: 'wss://api.example.com/ws',
52
+ }));
53
+
54
+ // Connect
55
+ await realtime.connect();
56
+
57
+ // Subscribe to a channel
58
+ const chat = realtime.channel('chat:general');
59
+
60
+ chat.on('message', (data) => {
61
+ console.log('Received:', data);
62
+ });
63
+
64
+ // Send a message
65
+ chat.send('message', { text: 'Hello everyone!' });
66
+ ```
67
+
68
+ ### Server
69
+
70
+ ```typescript
71
+ import { createServer } from '@flight-framework/realtime/server';
72
+
73
+ const server = createServer({ port: 3001 });
74
+
75
+ server.on('connection', (socket) => {
76
+ console.log('Client connected:', socket.id);
77
+
78
+ socket.on('message', (data) => {
79
+ // Broadcast to all clients in the channel
80
+ server.to('chat:general').emit('message', data);
81
+ });
82
+ });
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Adapters
88
+
89
+ ### WebSocket (Full Duplex)
90
+
91
+ Bidirectional communication for real-time apps.
92
+
93
+ ```typescript
94
+ import { websocket } from '@flight-framework/realtime/websocket';
95
+
96
+ const adapter = websocket({
97
+ url: 'wss://api.example.com/ws',
98
+ protocols: ['v1'],
99
+ reconnect: true,
100
+ reconnectDelay: 1000,
101
+ maxReconnectDelay: 30000,
102
+ pingInterval: 25000,
103
+ });
104
+ ```
105
+
106
+ ### SSE (Server-Sent Events)
107
+
108
+ One-way server-to-client, edge-compatible.
109
+
110
+ ```typescript
111
+ import { sse } from '@flight-framework/realtime/sse';
112
+
113
+ const adapter = sse({
114
+ url: '/api/events',
115
+ withCredentials: true,
116
+ retryDelay: 3000,
117
+ });
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Channels
123
+
124
+ ### Subscribe to Channel
125
+
126
+ ```typescript
127
+ const channel = realtime.channel('notifications:user_123');
128
+
129
+ channel.on('new', (notification) => {
130
+ showNotification(notification);
131
+ });
132
+
133
+ channel.on('read', (id) => {
134
+ markAsRead(id);
135
+ });
136
+ ```
137
+
138
+ ### Send to Channel
139
+
140
+ ```typescript
141
+ channel.send('typing', { userId: 'user_123' });
142
+ ```
143
+
144
+ ### Leave Channel
145
+
146
+ ```typescript
147
+ channel.leave();
148
+ ```
149
+
150
+ ### Channel Events
151
+
152
+ ```typescript
153
+ channel.on('join', () => console.log('Joined channel'));
154
+ channel.on('leave', () => console.log('Left channel'));
155
+ channel.on('error', (err) => console.error('Channel error:', err));
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Presence
161
+
162
+ Track who's online in a channel:
163
+
164
+ ```typescript
165
+ const channel = realtime.channel('room:lobby');
166
+
167
+ // Track current user
168
+ channel.presence.enter({
169
+ userId: 'user_123',
170
+ name: 'John',
171
+ status: 'online',
172
+ });
173
+
174
+ // Get current members
175
+ const members = channel.presence.get();
176
+ console.log('Online:', members);
177
+
178
+ // Listen for changes
179
+ channel.presence.on('join', (member) => {
180
+ console.log(`${member.name} joined`);
181
+ });
182
+
183
+ channel.presence.on('leave', (member) => {
184
+ console.log(`${member.name} left`);
185
+ });
186
+
187
+ channel.presence.on('update', (member) => {
188
+ console.log(`${member.name} updated status`);
189
+ });
190
+
191
+ // Update presence
192
+ channel.presence.update({ status: 'away' });
193
+
194
+ // Leave
195
+ channel.presence.leave();
196
+ ```
197
+
198
+ ---
199
+
200
+ ## React Integration
201
+
202
+ ### RealtimeProvider
203
+
204
+ ```tsx
205
+ import { RealtimeProvider } from '@flight-framework/realtime/react';
206
+
207
+ function App() {
208
+ return (
209
+ <RealtimeProvider client={realtime}>
210
+ <ChatRoom />
211
+ </RealtimeProvider>
212
+ );
213
+ }
214
+ ```
215
+
216
+ ### useRealtime Hook
217
+
218
+ ```tsx
219
+ import { useRealtime } from '@flight-framework/realtime/react';
220
+
221
+ function ConnectionStatus() {
222
+ const { state, connect, disconnect } = useRealtime();
223
+
224
+ return (
225
+ <div>
226
+ Status: {state}
227
+ {state === 'disconnected' && (
228
+ <button onClick={connect}>Reconnect</button>
229
+ )}
230
+ </div>
231
+ );
232
+ }
233
+ ```
234
+
235
+ ### useChannel Hook
236
+
237
+ ```tsx
238
+ import { useChannel } from '@flight-framework/realtime/react';
239
+
240
+ function ChatRoom({ roomId }) {
241
+ const [messages, setMessages] = useState([]);
242
+
243
+ const { send } = useChannel(`chat:${roomId}`, {
244
+ onMessage: (data) => {
245
+ setMessages(prev => [...prev, data]);
246
+ },
247
+ });
248
+
249
+ const handleSend = (text) => {
250
+ send('message', { text, timestamp: Date.now() });
251
+ };
252
+
253
+ return (
254
+ <div>
255
+ {messages.map(msg => (
256
+ <div key={msg.timestamp}>{msg.text}</div>
257
+ ))}
258
+ <MessageInput onSend={handleSend} />
259
+ </div>
260
+ );
261
+ }
262
+ ```
263
+
264
+ ### usePresence Hook
265
+
266
+ ```tsx
267
+ import { usePresence } from '@flight-framework/realtime/react';
268
+
269
+ function OnlineUsers({ roomId }) {
270
+ const { members, enter, leave } = usePresence(`room:${roomId}`);
271
+
272
+ useEffect(() => {
273
+ enter({ name: currentUser.name });
274
+ return () => leave();
275
+ }, []);
276
+
277
+ return (
278
+ <ul>
279
+ {members.map(member => (
280
+ <li key={member.id}>{member.name}</li>
281
+ ))}
282
+ </ul>
283
+ );
284
+ }
285
+ ```
286
+
287
+ ---
288
+
289
+ ## Vue Integration
290
+
291
+ ### Composables
292
+
293
+ ```vue
294
+ <script setup>
295
+ import { useRealtime, useChannel, usePresence } from '@flight-framework/realtime/vue';
296
+
297
+ const { state } = useRealtime();
298
+ const { messages, send } = useChannel('chat:general');
299
+ const { members, enter } = usePresence('room:lobby');
300
+
301
+ onMounted(() => {
302
+ enter({ name: user.name });
303
+ });
304
+
305
+ const sendMessage = (text) => {
306
+ send('message', { text });
307
+ };
308
+ </script>
309
+
310
+ <template>
311
+ <div>
312
+ <span>Status: {{ state }}</span>
313
+ <div v-for="msg in messages" :key="msg.id">
314
+ {{ msg.text }}
315
+ </div>
316
+ <input @keyup.enter="sendMessage($event.target.value)" />
317
+ </div>
318
+ </template>
319
+ ```
320
+
321
+ ---
322
+
323
+ ## Server-Side Events
324
+
325
+ ### SSE Handler
326
+
327
+ ```typescript
328
+ // src/routes/api/events.get.ts
329
+ import { createSSEResponse } from '@flight-framework/realtime/sse';
330
+
331
+ export async function GET(request: Request) {
332
+ const userId = getUserId(request);
333
+
334
+ return createSSEResponse(request, (send, close) => {
335
+ // Subscribe to events
336
+ const unsubscribe = eventBus.subscribe(userId, (event) => {
337
+ send(event.type, event.data);
338
+ });
339
+
340
+ // Cleanup on disconnect
341
+ return () => {
342
+ unsubscribe();
343
+ };
344
+ });
345
+ }
346
+ ```
347
+
348
+ ### SSE Client
349
+
350
+ ```typescript
351
+ const events = new EventSource('/api/events');
352
+
353
+ events.addEventListener('notification', (e) => {
354
+ const data = JSON.parse(e.data);
355
+ showNotification(data);
356
+ });
357
+
358
+ events.addEventListener('message', (e) => {
359
+ const data = JSON.parse(e.data);
360
+ addMessage(data);
361
+ });
362
+ ```
363
+
364
+ ---
365
+
366
+ ## API Reference
367
+
368
+ ### createRealtime Options
369
+
370
+ | Option | Type | Default | Description |
371
+ |--------|------|---------|-------------|
372
+ | `reconnect` | `boolean` | `true` | Auto-reconnect |
373
+ | `reconnectDelay` | `number` | `1000` | Initial delay (ms) |
374
+ | `maxReconnectDelay` | `number` | `30000` | Max delay (ms) |
375
+ | `timeout` | `number` | `20000` | Connection timeout |
376
+
377
+ ### Realtime Methods
378
+
379
+ | Method | Description |
380
+ |--------|-------------|
381
+ | `connect()` | Connect to server |
382
+ | `disconnect()` | Disconnect |
383
+ | `channel(name)` | Get or create channel |
384
+ | `on(event, handler)` | Listen to events |
385
+ | `off(event, handler)` | Remove listener |
386
+
387
+ ### Channel Methods
388
+
389
+ | Method | Description |
390
+ |--------|-------------|
391
+ | `send(event, data)` | Send message |
392
+ | `on(event, handler)` | Listen to event |
393
+ | `off(event, handler)` | Remove listener |
394
+ | `leave()` | Leave channel |
395
+ | `presence` | Presence API |
396
+
397
+ ### Connection States
398
+
399
+ | State | Description |
400
+ |-------|-------------|
401
+ | `connecting` | Establishing connection |
402
+ | `connected` | Connected and ready |
403
+ | `disconnected` | Not connected |
404
+ | `reconnecting` | Attempting reconnect |
405
+
406
+ ---
407
+
408
+ ## License
409
+
410
+ MIT
@@ -0,0 +1,59 @@
1
+ import { RealtimeAdapterFactory } from '../index.js';
2
+
3
+ /**
4
+ * SSE (Server-Sent Events) Adapter for @flightdev/realtime
5
+ *
6
+ * Edge-compatible transport using SSE for server-to-client
7
+ * and fetch POST for client-to-server communication.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { createRealtime } from '@flightdev/realtime';
12
+ * import { sse } from '@flightdev/realtime/sse';
13
+ *
14
+ * const realtime = createRealtime(sse({
15
+ * url: '/api/realtime',
16
+ * }));
17
+ * ```
18
+ */
19
+
20
+ interface SSEConfig {
21
+ /** URL for SSE endpoint */
22
+ url: string;
23
+ /** URL for sending messages (defaults to same as url) */
24
+ sendUrl?: string;
25
+ /** Retry delay on disconnect */
26
+ retryDelay?: number;
27
+ /** Custom headers */
28
+ headers?: Record<string, string>;
29
+ }
30
+ /**
31
+ * Create an SSE adapter
32
+ *
33
+ * SSE provides server-to-client streaming, with fetch POST for
34
+ * client-to-server messages. Works on Edge runtimes.
35
+ */
36
+ declare const sse: RealtimeAdapterFactory<SSEConfig>;
37
+
38
+ /**
39
+ * Create an SSE response for server-side streaming
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * // In your API route
44
+ * export async function GET(request: Request) {
45
+ * return createSSEResponse(request, (send) => {
46
+ * // Subscribe to updates
47
+ * const unsubscribe = mySource.subscribe((data) => {
48
+ * send('message', data);
49
+ * });
50
+ *
51
+ * // Cleanup on close
52
+ * return () => unsubscribe();
53
+ * });
54
+ * }
55
+ * ```
56
+ */
57
+ declare function createSSEResponse(_request: Request, handler: (send: (event: string, data: unknown) => void) => (() => void) | void): Response;
58
+
59
+ export { type SSEConfig, createSSEResponse, sse as default, sse };
@@ -0,0 +1,188 @@
1
+ import { createMessage, parseMessage } from '../chunk-D6OMTDYP.js';
2
+
3
+ // src/adapters/sse.ts
4
+ var SSEChannel = class {
5
+ name;
6
+ subscribers = /* @__PURE__ */ new Map();
7
+ allSubscribers = /* @__PURE__ */ new Set();
8
+ sendFn;
9
+ constructor(name, sendFn) {
10
+ this.name = name;
11
+ this.sendFn = sendFn;
12
+ }
13
+ subscribe(callback) {
14
+ this.allSubscribers.add(callback);
15
+ return () => this.allSubscribers.delete(callback);
16
+ }
17
+ on(event, callback) {
18
+ if (!this.subscribers.has(event)) {
19
+ this.subscribers.set(event, /* @__PURE__ */ new Set());
20
+ }
21
+ this.subscribers.get(event).add(callback);
22
+ return () => this.subscribers.get(event)?.delete(callback);
23
+ }
24
+ broadcast(data, event = "message") {
25
+ this.sendFn(this.name, event, data);
26
+ }
27
+ send(data, event = "message") {
28
+ this.broadcast(data, event);
29
+ }
30
+ leave() {
31
+ this.allSubscribers.clear();
32
+ this.subscribers.clear();
33
+ }
34
+ _handleMessage(message) {
35
+ this.allSubscribers.forEach((cb) => cb(message));
36
+ const eventSubs = this.subscribers.get(message.event);
37
+ eventSubs?.forEach((cb) => cb(message));
38
+ }
39
+ };
40
+ var sse = (config) => {
41
+ if (!config?.url) {
42
+ throw new Error("@flightdev/realtime: SSE requires a url configuration");
43
+ }
44
+ const {
45
+ url,
46
+ sendUrl = url,
47
+ retryDelay = 3e3,
48
+ headers = {}
49
+ } = config;
50
+ let eventSource = null;
51
+ let state = "disconnected";
52
+ const channels = /* @__PURE__ */ new Map();
53
+ const stateCallbacks = /* @__PURE__ */ new Set();
54
+ const errorCallbacks = /* @__PURE__ */ new Set();
55
+ const subscribedChannels = /* @__PURE__ */ new Set();
56
+ function setState(newState) {
57
+ state = newState;
58
+ stateCallbacks.forEach((cb) => cb(state));
59
+ }
60
+ async function send(channel, event, data) {
61
+ const message = createMessage(channel, event, data);
62
+ try {
63
+ const response = await fetch(sendUrl, {
64
+ method: "POST",
65
+ headers: {
66
+ "Content-Type": "application/json",
67
+ ...headers
68
+ },
69
+ body: JSON.stringify(message)
70
+ });
71
+ if (!response.ok) {
72
+ throw new Error(`Failed to send message: ${response.status}`);
73
+ }
74
+ } catch (error) {
75
+ errorCallbacks.forEach((cb) => cb(error));
76
+ }
77
+ }
78
+ function handleMessage(event) {
79
+ const message = parseMessage(event.data);
80
+ if (!message) return;
81
+ const channel = channels.get(message.channel);
82
+ if (channel) {
83
+ channel._handleMessage(message);
84
+ }
85
+ }
86
+ const adapter = {
87
+ name: "sse",
88
+ get state() {
89
+ return state;
90
+ },
91
+ async connect() {
92
+ if (state === "connected" || state === "connecting") {
93
+ return;
94
+ }
95
+ setState("connecting");
96
+ return new Promise((resolve, reject) => {
97
+ const sseUrl = new URL(url, globalThis.location?.origin ?? "http://localhost");
98
+ subscribedChannels.forEach((ch) => {
99
+ sseUrl.searchParams.append("channel", ch);
100
+ });
101
+ eventSource = new EventSource(sseUrl.toString());
102
+ eventSource.onopen = () => {
103
+ setState("connected");
104
+ resolve();
105
+ };
106
+ eventSource.onerror = (_event) => {
107
+ if (state === "connecting") {
108
+ setState("disconnected");
109
+ reject(new Error("SSE connection failed"));
110
+ } else {
111
+ setState("reconnecting");
112
+ setTimeout(() => {
113
+ adapter.connect().catch(() => {
114
+ });
115
+ }, retryDelay);
116
+ }
117
+ errorCallbacks.forEach((cb) => cb(new Error("SSE error")));
118
+ };
119
+ eventSource.onmessage = handleMessage;
120
+ eventSource.addEventListener("realtime", (e) => {
121
+ handleMessage(e);
122
+ });
123
+ });
124
+ },
125
+ disconnect() {
126
+ eventSource?.close();
127
+ eventSource = null;
128
+ setState("disconnected");
129
+ },
130
+ channel(name, _options) {
131
+ if (!channels.has(name)) {
132
+ channels.set(name, new SSEChannel(name, send));
133
+ subscribedChannels.add(name);
134
+ if (state === "connected") {
135
+ adapter.disconnect();
136
+ adapter.connect().catch(() => {
137
+ });
138
+ }
139
+ }
140
+ return channels.get(name);
141
+ },
142
+ onStateChange(callback) {
143
+ stateCallbacks.add(callback);
144
+ return () => stateCallbacks.delete(callback);
145
+ },
146
+ onError(callback) {
147
+ errorCallbacks.add(callback);
148
+ return () => errorCallbacks.delete(callback);
149
+ },
150
+ send(channel, event, data) {
151
+ send(channel, event, data).catch(() => {
152
+ });
153
+ }
154
+ };
155
+ return adapter;
156
+ };
157
+ var sse_default = sse;
158
+ function createSSEResponse(_request, handler) {
159
+ const encoder = new TextEncoder();
160
+ let cleanup;
161
+ const stream = new ReadableStream({
162
+ start(controller) {
163
+ function send(event, data) {
164
+ const message = `event: ${event}
165
+ data: ${JSON.stringify(data)}
166
+
167
+ `;
168
+ controller.enqueue(encoder.encode(message));
169
+ }
170
+ cleanup = handler(send);
171
+ send("connected", { timestamp: Date.now() });
172
+ },
173
+ cancel() {
174
+ cleanup?.();
175
+ }
176
+ });
177
+ return new Response(stream, {
178
+ headers: {
179
+ "Content-Type": "text/event-stream",
180
+ "Cache-Control": "no-cache",
181
+ "Connection": "keep-alive"
182
+ }
183
+ });
184
+ }
185
+
186
+ export { createSSEResponse, sse_default as default, sse };
187
+ //# sourceMappingURL=sse.js.map
188
+ //# sourceMappingURL=sse.js.map