@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 +21 -0
- package/README.md +410 -0
- package/dist/adapters/sse.d.ts +59 -0
- package/dist/adapters/sse.js +188 -0
- package/dist/adapters/sse.js.map +1 -0
- package/dist/adapters/websocket.d.ts +40 -0
- package/dist/adapters/websocket.js +161 -0
- package/dist/adapters/websocket.js.map +1 -0
- package/dist/chunk-D6OMTDYP.js +85 -0
- package/dist/chunk-D6OMTDYP.js.map +1 -0
- package/dist/frameworks/react.d.ts +81 -0
- package/dist/frameworks/react.js +93 -0
- package/dist/frameworks/react.js.map +1 -0
- package/dist/frameworks/vue.d.ts +29 -0
- package/dist/frameworks/vue.js +55 -0
- package/dist/frameworks/vue.js.map +1 -0
- package/dist/index.d.ts +168 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
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
|