use-next-sse 0.0.0 → 0.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 +13 -13
- package/dist/client/sse-manager.d.ts +10 -0
- package/dist/client/sse-manager.js +51 -0
- package/dist/client/use-sse.d.ts +26 -7
- package/dist/client/use-sse.js +40 -54
- package/dist/index.d.ts +0 -1
- package/dist/server/sse-server.d.ts +25 -18
- package/dist/server/sse-server.js +56 -52
- package/package.json +13 -3
package/README.md
CHANGED
|
@@ -4,9 +4,9 @@ use-next-sse is a lightweight and easy-to-use React hook library for implementin
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
8
|
npm install use-next-sse
|
|
9
|
-
|
|
9
|
+
```
|
|
10
10
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
@@ -14,45 +14,45 @@ npm install use-next-sse
|
|
|
14
14
|
|
|
15
15
|
Create a new file `app/api/sse/route.ts` with the following content:
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
```typescript
|
|
18
18
|
import { createSSEHandler } from 'use-next-sse';
|
|
19
|
-
|
|
20
|
-
export const GET = createSSEHandler(async (
|
|
19
|
+
export const dynamic = 'force-dynamic';
|
|
20
|
+
export const GET = createSSEHandler(async (send, close) => {
|
|
21
21
|
let count = 0;
|
|
22
22
|
const interval = setInterval(() => {
|
|
23
|
-
|
|
23
|
+
send({ count: count++ }, 'counter');
|
|
24
24
|
if (count > 10) {
|
|
25
25
|
clearInterval(interval);
|
|
26
|
-
|
|
26
|
+
close();
|
|
27
27
|
}
|
|
28
28
|
}, 1000);
|
|
29
29
|
});
|
|
30
|
-
|
|
30
|
+
```
|
|
31
31
|
|
|
32
32
|
### Client-Side (React Component)
|
|
33
33
|
|
|
34
34
|
Create a new file `app/components/Counter.tsx` with the following content:
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
```typescript
|
|
37
37
|
'use client'
|
|
38
38
|
|
|
39
39
|
import { useSSE } from 'use-next-sse';
|
|
40
40
|
|
|
41
41
|
export default function Counter() {
|
|
42
|
-
const { data, error } = useSSE('/api/sse', 'counter');
|
|
42
|
+
const { data, error } = useSSE({url: '/api/sse', eventName: 'counter'});
|
|
43
43
|
|
|
44
44
|
if (error) return <div>Error: {error.message}</div>;
|
|
45
45
|
if (!data) return <div>Loading...</div>;
|
|
46
46
|
|
|
47
47
|
return <div>Count: {data.count}</div>;
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
```
|
|
50
50
|
|
|
51
51
|
### Usage in a Page
|
|
52
52
|
|
|
53
53
|
Use the `Counter` component in a page, for example in `app/page.tsx`:
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
```typescript
|
|
56
56
|
import Counter from './components/Counter';
|
|
57
57
|
|
|
58
58
|
export default function Home() {
|
|
@@ -63,6 +63,6 @@ export default function Home() {
|
|
|
63
63
|
</main>
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
```
|
|
67
67
|
|
|
68
68
|
This example demonstrates a simple counter that updates every second using Server-Sent Events. The server sends updates for 10 seconds before closing the connection.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type Listener = (event: MessageEvent) => void;
|
|
2
|
+
declare class SSEManager {
|
|
3
|
+
private connections;
|
|
4
|
+
getConnection(url: string): EventSource;
|
|
5
|
+
releaseConnection(url: string): void;
|
|
6
|
+
addEventListener(url: string, eventName: string, listener: Listener): void;
|
|
7
|
+
removeEventListener(url: string, eventName: string, listener: Listener): void;
|
|
8
|
+
}
|
|
9
|
+
export declare const sseManager: SSEManager;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
class SSEManager {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.connections = new Map();
|
|
4
|
+
}
|
|
5
|
+
getConnection(url) {
|
|
6
|
+
let connection = this.connections.get(url);
|
|
7
|
+
if (!connection) {
|
|
8
|
+
const source = new EventSource(url);
|
|
9
|
+
connection = { source, refCount: 0, listeners: new Map() };
|
|
10
|
+
this.connections.set(url, connection);
|
|
11
|
+
}
|
|
12
|
+
connection.refCount++;
|
|
13
|
+
return connection.source;
|
|
14
|
+
}
|
|
15
|
+
releaseConnection(url) {
|
|
16
|
+
const connection = this.connections.get(url);
|
|
17
|
+
if (connection) {
|
|
18
|
+
connection.refCount--;
|
|
19
|
+
if (connection.refCount === 0) {
|
|
20
|
+
connection.source.close();
|
|
21
|
+
this.connections.delete(url);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
addEventListener(url, eventName, listener) {
|
|
26
|
+
const connection = this.connections.get(url);
|
|
27
|
+
if (connection) {
|
|
28
|
+
if (!connection.listeners.has(eventName)) {
|
|
29
|
+
connection.listeners.set(eventName, new Set());
|
|
30
|
+
connection.source.addEventListener(eventName, (event) => {
|
|
31
|
+
const listeners = connection.listeners.get(eventName);
|
|
32
|
+
listeners === null || listeners === void 0 ? void 0 : listeners.forEach(listener => listener(event));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
connection.listeners.get(eventName).add(listener);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
removeEventListener(url, eventName, listener) {
|
|
39
|
+
const connection = this.connections.get(url);
|
|
40
|
+
if (connection) {
|
|
41
|
+
const listeners = connection.listeners.get(eventName);
|
|
42
|
+
if (listeners) {
|
|
43
|
+
listeners.delete(listener);
|
|
44
|
+
if (listeners.size === 0) {
|
|
45
|
+
connection.listeners.delete(eventName);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export const sseManager = new SSEManager();
|
package/dist/client/use-sse.d.ts
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
export interface SSEOptions {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
reconnectInterval?: number;
|
|
5
|
-
maxReconnectAttempts?: number;
|
|
2
|
+
url: string;
|
|
3
|
+
eventName?: string;
|
|
6
4
|
}
|
|
7
|
-
|
|
8
|
-
data:
|
|
5
|
+
interface SSEResult<T> {
|
|
6
|
+
data: T | null;
|
|
9
7
|
error: Error | null;
|
|
8
|
+
lastEventId: string | null;
|
|
10
9
|
close: () => void;
|
|
11
|
-
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Hook to manage Server-Sent Events (SSE) connections.
|
|
13
|
+
*
|
|
14
|
+
* @template T - The type of the data expected from the SSE.
|
|
15
|
+
* @param {SSEOptions} options - The options for the SSE connection.
|
|
16
|
+
* @param {string} options.url - The URL to connect to for SSE.
|
|
17
|
+
* @param {string} [options.eventName='message'] - The name of the event to listen for.
|
|
18
|
+
* @returns {SSEResult<T>} The result of the SSE connection, including data, error, last event ID, and a close function.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const { data, error, lastEventId, close } = useSSE<{ message: string }>({ url: 'https://example.com/sse' });
|
|
22
|
+
*
|
|
23
|
+
* useEffect(() => {
|
|
24
|
+
* if (data) {
|
|
25
|
+
* console.log(data.message);
|
|
26
|
+
* }
|
|
27
|
+
* }, [data]);
|
|
28
|
+
*/
|
|
29
|
+
export declare function useSSE<T = any>({ url, eventName }: SSEOptions): SSEResult<T>;
|
|
30
|
+
export {};
|
package/dist/client/use-sse.js
CHANGED
|
@@ -1,68 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { sseManager } from './sse-manager';
|
|
4
|
+
/**
|
|
5
|
+
* Hook to manage Server-Sent Events (SSE) connections.
|
|
6
|
+
*
|
|
7
|
+
* @template T - The type of the data expected from the SSE.
|
|
8
|
+
* @param {SSEOptions} options - The options for the SSE connection.
|
|
9
|
+
* @param {string} options.url - The URL to connect to for SSE.
|
|
10
|
+
* @param {string} [options.eventName='message'] - The name of the event to listen for.
|
|
11
|
+
* @returns {SSEResult<T>} The result of the SSE connection, including data, error, last event ID, and a close function.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const { data, error, lastEventId, close } = useSSE<{ message: string }>({ url: 'https://example.com/sse' });
|
|
15
|
+
*
|
|
16
|
+
* useEffect(() => {
|
|
17
|
+
* if (data) {
|
|
18
|
+
* console.log(data.message);
|
|
19
|
+
* }
|
|
20
|
+
* }, [data]);
|
|
21
|
+
*/
|
|
22
|
+
export function useSSE({ url, eventName = 'message' }) {
|
|
3
23
|
const [data, setData] = useState(null);
|
|
4
24
|
const [error, setError] = useState(null);
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
25
|
+
const [lastEventId, setLastEventId] = useState(null);
|
|
26
|
+
const close = useCallback(() => {
|
|
27
|
+
sseManager.releaseConnection(url);
|
|
28
|
+
}, [url]);
|
|
9
29
|
useEffect(() => {
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
var _a, _b;
|
|
30
|
+
const source = sseManager.getConnection(url);
|
|
31
|
+
const handleMessage = (event) => {
|
|
13
32
|
try {
|
|
14
33
|
const parsedData = JSON.parse(event.data);
|
|
15
34
|
setData(parsedData);
|
|
16
|
-
(
|
|
17
|
-
|
|
35
|
+
setLastEventId(event.lastEventId);
|
|
36
|
+
setError(null);
|
|
18
37
|
}
|
|
19
38
|
catch (err) {
|
|
20
|
-
|
|
21
|
-
setError(parseError);
|
|
22
|
-
(_b = options.onError) === null || _b === void 0 ? void 0 : _b.call(options, parseError);
|
|
39
|
+
setError(new Error('Failed to parse event data'));
|
|
23
40
|
}
|
|
24
41
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
eventSource.addEventListener(eventName, messageHandler);
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
eventSource.onmessage = messageHandler;
|
|
32
|
-
}
|
|
33
|
-
eventSource.onerror = (event) => {
|
|
34
|
-
var _a;
|
|
35
|
-
const sseError = new Error('SSE connection error');
|
|
36
|
-
setError(sseError);
|
|
37
|
-
(_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, sseError);
|
|
38
|
-
eventSource.close();
|
|
39
|
-
if (reconnectAttemptsRef.current < maxReconnectAttempts) {
|
|
40
|
-
reconnectAttemptsRef.current++;
|
|
41
|
-
console.log(`Reconnecting... Attempt ${reconnectAttemptsRef.current} of ${maxReconnectAttempts}`);
|
|
42
|
-
timeoutId = window.setTimeout(connect, reconnectInterval);
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
console.error('Max reconnect attempts reached. Giving up.');
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
eventSourceRef.current = eventSource;
|
|
42
|
+
sseManager.addEventListener(url, eventName, handleMessage);
|
|
43
|
+
const handleError = (event) => {
|
|
44
|
+
setError(new Error('EventSource failed'));
|
|
49
45
|
};
|
|
50
|
-
|
|
46
|
+
source.addEventListener('error', handleError);
|
|
51
47
|
return () => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
eventSourceRef.current.close();
|
|
57
|
-
}
|
|
58
|
-
window.clearTimeout(timeoutId);
|
|
48
|
+
sseManager.removeEventListener(url, eventName, handleMessage);
|
|
49
|
+
source.removeEventListener('error', handleError);
|
|
50
|
+
sseManager.releaseConnection(url);
|
|
59
51
|
};
|
|
60
|
-
}, [url, eventName
|
|
61
|
-
|
|
62
|
-
if (eventSourceRef.current) {
|
|
63
|
-
eventSourceRef.current.close();
|
|
64
|
-
eventSourceRef.current = null;
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
return { data, error, close };
|
|
52
|
+
}, [url, eventName]);
|
|
53
|
+
return { data, error, lastEventId, close };
|
|
68
54
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
type SendFunction = (data: any, eventName?: string) => void;
|
|
3
|
+
type SSECallback = (send: SendFunction, close: () => void) => void | Promise<void> | (() => void);
|
|
4
|
+
/**
|
|
5
|
+
* Creates a Server-Sent Events (SSE) handler for Next.js.
|
|
6
|
+
*
|
|
7
|
+
* @param callback - A function that takes two arguments:
|
|
8
|
+
* - `send`: A function to send data to the client. It accepts two parameters:
|
|
9
|
+
* - `data`: The data to send to the client.
|
|
10
|
+
* - `eventName` (optional): The name of the event.
|
|
11
|
+
* - `close`: A function to close the SSE connection.
|
|
12
|
+
* The callback can return a cleanup function that will be called when the connection is closed.
|
|
13
|
+
*
|
|
14
|
+
* @returns A function that handles the SSE request. This function takes a `NextRequest` object as an argument.
|
|
15
|
+
*
|
|
16
|
+
* The returned function creates a `ReadableStream` to handle the SSE connection. It sets up the `send` and `close` functions,
|
|
17
|
+
* and listens for the `abort` event on the request signal to close the connection.
|
|
18
|
+
*
|
|
19
|
+
* The response is returned with the appropriate headers for SSE:
|
|
20
|
+
* - `Content-Type`: `text/event-stream`
|
|
21
|
+
* - `Cache-Control`: `no-cache, no-transform`
|
|
22
|
+
* - `Connection`: `keep-alive`
|
|
23
|
+
*/
|
|
24
|
+
export declare function createSSEHandler(callback: SSECallback): (request: NextRequest) => Promise<Response>;
|
|
25
|
+
export {};
|
|
@@ -1,62 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
this.controller.close();
|
|
29
|
-
}
|
|
30
|
-
getResponse() {
|
|
1
|
+
/**
|
|
2
|
+
* Creates a Server-Sent Events (SSE) handler for Next.js.
|
|
3
|
+
*
|
|
4
|
+
* @param callback - A function that takes two arguments:
|
|
5
|
+
* - `send`: A function to send data to the client. It accepts two parameters:
|
|
6
|
+
* - `data`: The data to send to the client.
|
|
7
|
+
* - `eventName` (optional): The name of the event.
|
|
8
|
+
* - `close`: A function to close the SSE connection.
|
|
9
|
+
* The callback can return a cleanup function that will be called when the connection is closed.
|
|
10
|
+
*
|
|
11
|
+
* @returns A function that handles the SSE request. This function takes a `NextRequest` object as an argument.
|
|
12
|
+
*
|
|
13
|
+
* The returned function creates a `ReadableStream` to handle the SSE connection. It sets up the `send` and `close` functions,
|
|
14
|
+
* and listens for the `abort` event on the request signal to close the connection.
|
|
15
|
+
*
|
|
16
|
+
* The response is returned with the appropriate headers for SSE:
|
|
17
|
+
* - `Content-Type`: `text/event-stream`
|
|
18
|
+
* - `Cache-Control`: `no-cache, no-transform`
|
|
19
|
+
* - `Connection`: `keep-alive`
|
|
20
|
+
*/
|
|
21
|
+
export function createSSEHandler(callback) {
|
|
22
|
+
return async function (request) {
|
|
23
|
+
const encoder = new TextEncoder();
|
|
24
|
+
let isClosed = false;
|
|
25
|
+
let cleanup;
|
|
26
|
+
let messageId = 0;
|
|
31
27
|
const stream = new ReadableStream({
|
|
32
|
-
start
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
start(controller) {
|
|
29
|
+
const send = (data, eventName) => {
|
|
30
|
+
if (!isClosed) {
|
|
31
|
+
let message = `id: ${messageId}\n`;
|
|
32
|
+
if (eventName) {
|
|
33
|
+
message += `event: ${eventName}\n`;
|
|
34
|
+
}
|
|
35
|
+
message += `data: ${JSON.stringify(data)}\n\n`;
|
|
36
|
+
controller.enqueue(encoder.encode(message));
|
|
37
|
+
messageId++;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function close() {
|
|
41
|
+
if (!isClosed) {
|
|
42
|
+
isClosed = true;
|
|
43
|
+
controller.close();
|
|
44
|
+
if (typeof cleanup === 'function') {
|
|
45
|
+
cleanup();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
37
48
|
}
|
|
38
|
-
|
|
49
|
+
const result = callback(send, close);
|
|
50
|
+
if (typeof result === 'function') {
|
|
51
|
+
cleanup = result;
|
|
52
|
+
}
|
|
53
|
+
request.signal.addEventListener('abort', () => {
|
|
54
|
+
close();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
39
57
|
});
|
|
40
|
-
return new
|
|
58
|
+
return new Response(stream, {
|
|
41
59
|
headers: {
|
|
42
60
|
'Content-Type': 'text/event-stream',
|
|
43
|
-
'Cache-Control': 'no-cache',
|
|
61
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
44
62
|
'Connection': 'keep-alive',
|
|
45
|
-
'X-Accel-Buffering': 'no',
|
|
46
|
-
'retry': this.config.reconnectInterval.toString(),
|
|
47
63
|
},
|
|
48
64
|
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
export function createSSEHandler(handler, config) {
|
|
52
|
-
return async () => {
|
|
53
|
-
const sse = new SSEConnection(config);
|
|
54
|
-
const response = sse.getResponse();
|
|
55
|
-
await sse.initialize();
|
|
56
|
-
handler(sse).catch((error) => {
|
|
57
|
-
console.error('SSE Handler Error:', error);
|
|
58
|
-
sse.close();
|
|
59
|
-
});
|
|
60
|
-
return response;
|
|
61
65
|
};
|
|
62
66
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "use-next-sse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "A lightweight Server-Sent Events (SSE) library for Next.js",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
|
+
"watch": "tsc -w",
|
|
9
10
|
"test": "jest",
|
|
10
11
|
"prepublishOnly": "npm run build"
|
|
11
12
|
},
|
|
@@ -33,7 +34,8 @@
|
|
|
33
34
|
"@types/node": "^14.0.0",
|
|
34
35
|
"@types/react": "^18.0.0",
|
|
35
36
|
"jest": "^29.5.0",
|
|
36
|
-
"
|
|
37
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
38
|
+
"prettier": "^3.3",
|
|
37
39
|
"react": "^18.2.0",
|
|
38
40
|
"react-dom": "^18.2.0",
|
|
39
41
|
"ts-jest": "^29.1.0",
|
|
@@ -44,5 +46,13 @@
|
|
|
44
46
|
"reconnectInterval": 1000,
|
|
45
47
|
"maxReconnectAttempts": 5
|
|
46
48
|
}
|
|
47
|
-
}
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+ssh://git@github.com/alexanderkasten/use-next-sse.git"
|
|
53
|
+
},
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/alexanderkasten/use-next-sse/issues"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/alexanderkasten/use-next-sse#readme"
|
|
48
58
|
}
|