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 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
- \`\`\`bash
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
- \`\`\`typescript
17
+ ```typescript
18
18
  import { createSSEHandler } from 'use-next-sse';
19
-
20
- export const GET = createSSEHandler(async (sse) => {
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
- sse.send({ count: count++ }, 'counter');
23
+ send({ count: count++ }, 'counter');
24
24
  if (count > 10) {
25
25
  clearInterval(interval);
26
- sse.close();
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
- \`\`\`typescript
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
- \`\`\`typescript
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();
@@ -1,11 +1,30 @@
1
1
  export interface SSEOptions {
2
- onMessage?: (event: MessageEvent) => void;
3
- onError?: (error: Error) => void;
4
- reconnectInterval?: number;
5
- maxReconnectAttempts?: number;
2
+ url: string;
3
+ eventName?: string;
6
4
  }
7
- export declare function useSSE(url: string, eventName?: string, options?: SSEOptions): {
8
- data: any;
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 {};
@@ -1,68 +1,54 @@
1
- import { useState, useEffect, useRef } from 'react';
2
- export function useSSE(url, eventName, options = {}) {
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 eventSourceRef = useRef(null);
6
- const reconnectAttemptsRef = useRef(0);
7
- const reconnectInterval = options.reconnectInterval || 1000;
8
- const maxReconnectAttempts = options.maxReconnectAttempts || 5;
25
+ const [lastEventId, setLastEventId] = useState(null);
26
+ const close = useCallback(() => {
27
+ sseManager.releaseConnection(url);
28
+ }, [url]);
9
29
  useEffect(() => {
10
- let timeoutId;
11
- const messageHandler = (event) => {
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
- (_a = options.onMessage) === null || _a === void 0 ? void 0 : _a.call(options, event);
17
- reconnectAttemptsRef.current = 0;
35
+ setLastEventId(event.lastEventId);
36
+ setError(null);
18
37
  }
19
38
  catch (err) {
20
- const parseError = new Error(`Failed to parse SSE data: ${err.message}`);
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
- const connect = () => {
26
- const eventSource = new EventSource(url);
27
- if (eventName) {
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
- connect();
46
+ source.addEventListener('error', handleError);
51
47
  return () => {
52
- if (eventSourceRef.current) {
53
- if (eventName) {
54
- eventSourceRef.current.removeEventListener(eventName, messageHandler);
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, options, reconnectInterval, maxReconnectAttempts]);
61
- const close = () => {
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,4 +1,3 @@
1
1
  export { createSSEHandler } from './server/sse-server';
2
- export type { SSEConnection, SSEConfig } from './server/sse-server';
3
2
  export { useSSE } from './client/use-sse';
4
3
  export type { SSEOptions } from './client/use-sse';
@@ -1,18 +1,25 @@
1
- import { NextResponse } from 'next/server';
2
- export type SSEConfig = {
3
- reconnectInterval?: number;
4
- maxReconnectAttempts?: number;
5
- };
6
- export declare class SSEConnection {
7
- private encoder;
8
- private controller;
9
- private initializePromise;
10
- private resolveInitialize;
11
- private config;
12
- constructor(config?: SSEConfig);
13
- initialize(): Promise<void>;
14
- send(data: any, event?: string): void;
15
- close(): void;
16
- getResponse(): NextResponse<unknown>;
17
- }
18
- export declare function createSSEHandler(handler: (sse: SSEConnection) => Promise<void>, config?: SSEConfig): () => Promise<NextResponse<unknown>>;
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
- import { NextResponse } from 'next/server';
2
- export class SSEConnection {
3
- constructor(config = {}) {
4
- this.resolveInitialize = null;
5
- this.encoder = new TextEncoder();
6
- this.initializePromise = new Promise((resolve) => {
7
- this.resolveInitialize = resolve;
8
- });
9
- this.config = {
10
- reconnectInterval: config.reconnectInterval || 1000,
11
- maxReconnectAttempts: config.maxReconnectAttempts || 5,
12
- };
13
- }
14
- async initialize() {
15
- await this.initializePromise;
16
- }
17
- send(data, event) {
18
- if (!this.controller) {
19
- throw new Error('SSEConnection not initialized. Call initialize() first.');
20
- }
21
- const message = `data: ${JSON.stringify(data)}\n${event ? `event: ${event}\n` : ''}id: ${Date.now()}\n\n`;
22
- this.controller.enqueue(this.encoder.encode(message));
23
- }
24
- close() {
25
- if (!this.controller) {
26
- throw new Error('SSEConnection not initialized. Call initialize() first.');
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: (controller) => {
33
- this.controller = controller;
34
- if (this.resolveInitialize) {
35
- this.resolveInitialize();
36
- this.resolveInitialize = null;
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 NextResponse(stream, {
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.0.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
- "prettier": "^3.4.2",
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
  }