@laravel/stream-react 0.2.0 → 0.3.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
@@ -7,7 +7,7 @@
7
7
  <a href="https://www.npmjs.com/package/@laravel/stream-react"><img src="https://img.shields.io/npm/l/@laravel/stream-react" alt="License"></a>
8
8
  </p>
9
9
 
10
- Easily consume [Server-Sent Events (SSE)](https://laravel.com/docs/responses#event-streams) in your React application.
10
+ Easily consume streams in your React application.
11
11
 
12
12
  ## Installation
13
13
 
@@ -15,17 +15,148 @@ Easily consume [Server-Sent Events (SSE)](https://laravel.com/docs/responses#eve
15
15
  npm install @laravel/stream-react
16
16
  ```
17
17
 
18
- ## Usage
18
+ ## Streaming Responses
19
19
 
20
- Provide your stream URL and the hook will automatically update the `message` with the concatenated response as messages are returned from your server:
20
+ > [!IMPORTANT]
21
+ > The `useStream` hook is currently in Beta, the API is subject to change prior to the v1.0.0 release. All notable changes will be documented in the [changelog](./../../CHANGELOG.md).
22
+
23
+ The `useStream` hook allows you to seamlessly consume [streamed responses](https://laravel.com/docs/responses#streamed-responses) in your React application.
24
+
25
+ Provide your stream URL and the hook will automatically update `data` with the concatenated response as data is returned from your server:
26
+
27
+ ```tsx
28
+ import { useStream } from "@laravel/stream-react";
29
+
30
+ function App() {
31
+ const { data, isFetching, isStreaming, send } = useStream("chat");
32
+
33
+ const sendMessage = () => {
34
+ send({
35
+ message: `Current timestamp: ${Date.now()}`,
36
+ });
37
+ };
38
+
39
+ return (
40
+ <div>
41
+ <div>{data}</div>
42
+ {isFetching && <div>Connecting...</div>}
43
+ {isStreaming && <div>Generating...</div>}
44
+ <button onClick={sendMessage}>Send Message</button>
45
+ </div>
46
+ );
47
+ }
48
+ ```
49
+
50
+ When sending data back to the stream, the active connection to the stream is canceled before sending the new data. All requests are sent as JSON `POST` requests.
51
+
52
+ The second argument given to `useStream` is an options object that you may use to customize the stream consumption behavior. The default values for this object are shown below:
53
+
54
+ ```tsx
55
+ import { useStream } from "@laravel/stream-react";
56
+
57
+ function App() {
58
+ const { data } = useStream("chat", {
59
+ id: undefined,
60
+ initialInput: undefined,
61
+ headers: undefined,
62
+ csrfToken: undefined,
63
+ onResponse: (response: Response) => void,
64
+ onData: (data: string) => void,
65
+ onCancel: () => void,
66
+ onFinish: () => void,
67
+ onError: (error: Error) => void,
68
+ });
69
+
70
+ return <div>{data}</div>;
71
+ }
72
+ ```
73
+
74
+ `onResponse` is triggered after a successful initial response from the stream and the raw [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) is passed to the callback.
75
+
76
+ `onData` is called as each chunk is received, the current chunk is passed to the callback.
77
+
78
+ `onFinish` is called when a stream has finished and when an error is thrown during the fetch/read cycle.
79
+
80
+ By default, a request is not made the to stream on initialization. You may pass an initial payload to the stream by using the `initialInput` option:
81
+
82
+ ```tsx
83
+ import { useStream } from "@laravel/stream-react";
84
+
85
+ function App() {
86
+ const { data } = useStream("chat", {
87
+ initialInput: {
88
+ message: "Introduce yourself.",
89
+ },
90
+ });
91
+
92
+ return <div>{data}</div>;
93
+ }
94
+ ```
95
+
96
+ To cancel a stream manually, you may use the `cancel` method returned from the hook:
97
+
98
+ ```tsx
99
+ import { useStream } from "@laravel/stream-react";
100
+
101
+ function App() {
102
+ const { data, cancel } = useStream("chat");
103
+
104
+ return (
105
+ <div>
106
+ <div>{data}</div>
107
+ <button onClick={cancel}>Cancel</button>
108
+ </div>
109
+ );
110
+ }
111
+ ```
112
+
113
+ Each time the `useStream` hook is used, a random `id` is generated to identify the stream. This is sent back to the server with each request in the `X-STREAM-ID` header.
114
+
115
+ When consuming the same stream from multiple components, you can read and write to the stream by providing your own `id`:
116
+
117
+ ```tsx
118
+ // App.tsx
119
+ import { useStream } from "@laravel/stream-react";
120
+
121
+ function App() {
122
+ const { data, id } = useStream("chat");
123
+
124
+ return (
125
+ <div>
126
+ <div>{data}</div>
127
+ <StreamStatus id={id} />
128
+ </div>
129
+ );
130
+ }
131
+
132
+ // StreamStatus.tsx
133
+ import { useStream } from "@laravel/stream-react";
134
+
135
+ function StreamStatus({ id }) {
136
+ const { isFetching, isStreaming } = useStream("chat", { id });
137
+
138
+ return (
139
+ <div>
140
+ {isFetching && <div>Connecting...</div>}
141
+ {isStreaming && <div>Generating...</div>}
142
+ </div>
143
+ );
144
+ }
145
+ ```
146
+
147
+ ## Event Streams (SSE)
148
+
149
+ The `useEventStream` hook allows you to seamlessly consume [Server-Sent Events (SSE)](https://laravel.com/docs/responses#event-streams) in your React application.
150
+
151
+ Provide your stream URL and the hook will automatically update `message` with the concatenated response as messages are returned from your server:
21
152
 
22
153
  ```tsx
23
154
  import { useEventStream } from "@laravel/stream-react";
24
155
 
25
156
  function App() {
26
- const { message } = useEventStream("/stream");
157
+ const { message } = useEventStream("/stream");
27
158
 
28
- return <div>{message}</div>;
159
+ return <div>{message}</div>;
29
160
  }
30
161
  ```
31
162
 
@@ -35,15 +166,36 @@ You also have access to the array of message parts:
35
166
  import { useEventStream } from "@laravel/stream-react";
36
167
 
37
168
  function App() {
38
- const { messageParts } = useEventStream("/stream");
39
-
40
- return (
41
- <ul>
42
- {messageParts.forEach((message) => (
43
- <li>{message}</li>
44
- ))}
45
- </ul>
46
- );
169
+ const { messageParts } = useEventStream("/stream");
170
+
171
+ return (
172
+ <ul>
173
+ {messageParts.forEach((message) => (
174
+ <li>{message}</li>
175
+ ))}
176
+ </ul>
177
+ );
178
+ }
179
+ ```
180
+
181
+ If you'd like to listen to multiple events:
182
+
183
+ ```tsx
184
+ import { useEventStream } from "@laravel/stream-react";
185
+
186
+ function App() {
187
+ useEventStream("/stream", {
188
+ eventName: ["update", "create"],
189
+ onMessage: (event) => {
190
+ if (event.type === "update") {
191
+ // Handle update
192
+ } else {
193
+ // Handle create
194
+ }
195
+ },
196
+ });
197
+
198
+ return null;
47
199
  }
48
200
  ```
49
201
 
@@ -53,22 +205,23 @@ The second parameter is an options object where all properties are optional (def
53
205
  import { useEventStream } from "@laravel/stream-react";
54
206
 
55
207
  function App() {
56
- const { message } = useEventStream("/stream", {
57
- event: "update",
58
- onMessage: (message) => {
59
- //
60
- },
61
- onError: (error) => {
62
- //
63
- },
64
- onComplete: () => {
65
- //
66
- },
67
- endSignal: "</stream>",
68
- glue: " ",
69
- });
70
-
71
- return <div>{message}</div>;
208
+ const { message } = useEventStream("/stream", {
209
+ event: "update",
210
+ onMessage: (message) => {
211
+ //
212
+ },
213
+ onError: (error) => {
214
+ //
215
+ },
216
+ onComplete: () => {
217
+ //
218
+ },
219
+ endSignal: "</stream>",
220
+ glue: " ",
221
+ replace: false,
222
+ });
223
+
224
+ return <div>{message}</div>;
72
225
  }
73
226
  ```
74
227
 
@@ -79,15 +232,15 @@ import { useEventStream } from "@laravel/stream-react";
79
232
  import { useEffect } from "react";
80
233
 
81
234
  function App() {
82
- const { message, close } = useEventStream("/stream");
235
+ const { message, close } = useEventStream("/stream");
83
236
 
84
- useEffect(() => {
85
- setTimeout(() => {
86
- close();
87
- }, 3000);
88
- }, []);
237
+ useEffect(() => {
238
+ setTimeout(() => {
239
+ close();
240
+ }, 3000);
241
+ }, []);
89
242
 
90
- return <div>{message}</div>;
243
+ return <div>{message}</div>;
91
244
  }
92
245
  ```
93
246
 
@@ -98,15 +251,15 @@ import { useEventStream } from "@laravel/stream-react";
98
251
  import { useEffect } from "react";
99
252
 
100
253
  function App() {
101
- const { message, clearMessage } = useEventStream("/stream");
254
+ const { message, clearMessage } = useEventStream("/stream");
102
255
 
103
- useEffect(() => {
104
- setTimeout(() => {
105
- clearMessage();
106
- }, 3000);
107
- }, []);
256
+ useEffect(() => {
257
+ setTimeout(() => {
258
+ clearMessage();
259
+ }, 3000);
260
+ }, []);
108
261
 
109
- return <div>{message}</div>;
262
+ return <div>{message}</div>;
110
263
  }
111
264
  ```
112
265
 
package/dist/index.d.ts CHANGED
@@ -1,19 +1,32 @@
1
- declare type Options = {
2
- eventName?: string;
1
+ declare type EventStreamOptions = {
2
+ eventName?: string | string[];
3
3
  endSignal?: string;
4
4
  glue?: string;
5
+ replace?: boolean;
5
6
  onMessage?: (event: MessageEvent) => void;
6
7
  onComplete?: () => void;
7
8
  onError?: (error: Event) => void;
8
9
  };
9
10
 
10
- declare type StreamResult = {
11
+ declare type EventStreamResult = {
11
12
  message: string;
12
13
  messageParts: string[];
13
14
  close: (resetMessage?: boolean) => void;
14
15
  clearMessage: () => void;
15
16
  };
16
17
 
18
+ declare type StreamOptions = {
19
+ id?: string;
20
+ initialInput?: Record<string, any>;
21
+ headers?: Record<string, string>;
22
+ csrfToken?: string;
23
+ onResponse?: (response: Response) => void;
24
+ onData?: (data: string) => void;
25
+ onCancel?: () => void;
26
+ onFinish?: () => void;
27
+ onError?: (error: Error) => void;
28
+ };
29
+
17
30
  /**
18
31
  * Hook for handling server-sent event (SSE) streams
19
32
  *
@@ -24,6 +37,15 @@ declare type StreamResult = {
24
37
  *
25
38
  * @returns StreamResult object containing the accumulated response, close, and reset functions
26
39
  */
27
- export declare const useEventStream: (url: string, { eventName, endSignal, glue, onMessage, onComplete, onError, }?: Options) => StreamResult;
40
+ export declare const useEventStream: (url: string, { eventName, endSignal, glue, replace, onMessage, onComplete, onError, }?: EventStreamOptions) => EventStreamResult;
41
+
42
+ export declare const useStream: (url: string, options?: StreamOptions) => {
43
+ data: string;
44
+ isFetching: boolean;
45
+ isStreaming: boolean;
46
+ id: string;
47
+ send: (body: Record<string, any>) => void;
48
+ cancel: () => void;
49
+ };
28
50
 
29
51
  export { }
package/dist/index.es.js CHANGED
@@ -1,38 +1,169 @@
1
- import { useRef as p, useState as L, useCallback as c, useEffect as C } from "react";
2
- const l = "data: ", j = (d, {
3
- eventName: t = "update",
4
- endSignal: i = "</stream>",
5
- glue: f = " ",
6
- onMessage: P = () => null,
7
- onComplete: R = () => null,
8
- onError: S = () => null
1
+ import { useRef as b, useMemo as x, useState as y, useCallback as d, useEffect as C } from "react";
2
+ const I = "data: ", J = (r, {
3
+ eventName: e = "update",
4
+ endSignal: c = "</stream>",
5
+ glue: o = " ",
6
+ replace: k = !1,
7
+ onMessage: v = () => null,
8
+ onComplete: D = () => null,
9
+ onError: w = () => null
9
10
  } = {}) => {
10
- const r = p(null), s = p([]), [b, g] = L(""), [x, E] = L([]), n = c(() => {
11
- s.current = [], g(""), E([]);
12
- }, []), u = c(
13
- (e) => {
14
- if ([i, `${l}${i}`].includes(e.data)) {
15
- a(), R();
11
+ const l = b(null), f = b([]), m = x(
12
+ () => Array.isArray(e) ? e : [e],
13
+ Array.isArray(e) ? e : [e]
14
+ ), [i, g] = y(""), [A, R] = y([]), h = d(() => {
15
+ f.current = [], g(""), R([]);
16
+ }, []), n = d(
17
+ (t) => {
18
+ if ([c, `${I}${c}`].includes(t.data)) {
19
+ a(), D();
16
20
  return;
17
21
  }
18
- s.current.push(
19
- e.data.startsWith(l) ? e.data.substring(l.length) : e.data
20
- ), g(s.current.join(f)), E(s.current), P(e);
22
+ k && h(), f.current.push(
23
+ t.data.startsWith(I) ? t.data.substring(I.length) : t.data
24
+ ), g(f.current.join(o)), R(f.current), v(t);
21
25
  },
22
- [t, f]
23
- ), o = c((e) => {
24
- S(e), a();
25
- }, []), a = c((e = !1) => {
26
- var m, h, M;
27
- (m = r.current) == null || m.removeEventListener(t, u), (h = r.current) == null || h.removeEventListener("error", o), (M = r.current) == null || M.close(), r.current = null, e && n();
26
+ [m, o]
27
+ ), s = d((t) => {
28
+ w(t), a();
29
+ }, []), a = d((t = !1) => {
30
+ var u, S;
31
+ m.forEach((L) => {
32
+ var E;
33
+ (E = l.current) == null || E.removeEventListener(L, n);
34
+ }), (u = l.current) == null || u.removeEventListener("error", s), (S = l.current) == null || S.close(), l.current = null, t && h();
28
35
  }, []);
29
- return C(() => (n(), r.current = new EventSource(d), r.current.addEventListener(t, u), r.current.addEventListener("error", o), a), [d, t, u, o, n]), {
30
- message: b,
31
- messageParts: x,
36
+ return C(() => (h(), l.current = new EventSource(r), m.forEach((t) => {
37
+ var u;
38
+ (u = l.current) == null || u.addEventListener(t, n);
39
+ }), l.current.addEventListener("error", s), a), [r, m, n, s, h]), {
40
+ message: i,
41
+ messageParts: A,
32
42
  close: a,
33
- clearMessage: n
43
+ clearMessage: h
44
+ };
45
+ }, O = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
46
+ let j = (r = 21) => {
47
+ let e = "", c = crypto.getRandomValues(new Uint8Array(r |= 0));
48
+ for (; r--; )
49
+ e += O[c[r] & 63];
50
+ return e;
51
+ };
52
+ const T = /* @__PURE__ */ new Map(), F = /* @__PURE__ */ new Map(), M = (r) => {
53
+ const e = T.get(r);
54
+ return e || (T.set(r, {
55
+ controller: new AbortController(),
56
+ data: "",
57
+ isFetching: !1,
58
+ isStreaming: !1
59
+ }), T.get(r));
60
+ }, P = (r) => (F.has(r) || F.set(r, []), F.get(r)), q = (r, e) => (P(r).push(e), () => {
61
+ F.set(
62
+ r,
63
+ P(r).filter((c) => c !== e)
64
+ );
65
+ }), K = (r, e = {}) => {
66
+ const c = b(e.id ?? j()), o = b(M(c.current)), k = b(
67
+ (() => {
68
+ var a;
69
+ const n = {
70
+ "Content-Type": "application/json",
71
+ "X-STREAM-ID": c.current
72
+ }, s = e.csrfToken ?? ((a = document.querySelector('meta[name="csrf-token"]')) == null ? void 0 : a.getAttribute("content"));
73
+ return s && (n["X-CSRF-TOKEN"] = s), n;
74
+ })()
75
+ ), [v, D] = y(o.current.data), [w, l] = y(o.current.isFetching), [f, m] = y(o.current.isStreaming), i = d((n) => {
76
+ var a;
77
+ T.set(c.current, {
78
+ ...M(c.current),
79
+ ...n
80
+ });
81
+ const s = M(c.current);
82
+ (a = F.get(c.current)) == null || a.forEach((t) => t(s));
83
+ }, []), g = d(() => {
84
+ var n;
85
+ o.current.controller.abort(), (w || f) && ((n = e.onCancel) == null || n.call(e)), i({
86
+ isFetching: !1,
87
+ isStreaming: !1
88
+ });
89
+ }, [w, f]), A = d(
90
+ (n = {}) => {
91
+ const s = new AbortController();
92
+ i({
93
+ isFetching: !0,
94
+ controller: s
95
+ }), fetch(r, {
96
+ method: "POST",
97
+ signal: s.signal,
98
+ headers: {
99
+ ...k.current,
100
+ ...e.headers ?? {}
101
+ },
102
+ body: JSON.stringify(n)
103
+ }).then(async (a) => {
104
+ var t;
105
+ if (!a.ok) {
106
+ const u = await a.text();
107
+ throw new Error(u);
108
+ }
109
+ if (!a.body)
110
+ throw new Error(
111
+ "ReadableStream not yet supported in this browser."
112
+ );
113
+ return (t = e.onResponse) == null || t.call(e, a), i({
114
+ isFetching: !1,
115
+ isStreaming: !0
116
+ }), h(a.body.getReader());
117
+ }).catch((a) => {
118
+ var t, u;
119
+ i({
120
+ isFetching: !1,
121
+ isStreaming: !1
122
+ }), (t = e.onError) == null || t.call(e, a), (u = e.onFinish) == null || u.call(e);
123
+ });
124
+ },
125
+ [r]
126
+ ), R = d((n) => {
127
+ g(), A(n), i({
128
+ data: ""
129
+ });
130
+ }, []), h = d(
131
+ (n, s = "") => n.read().then(({ done: a, value: t }) => {
132
+ var L, E;
133
+ const u = new TextDecoder("utf-8").decode(t), S = s + u;
134
+ return (L = e.onData) == null || L.call(e, u), a ? (i({
135
+ data: S,
136
+ isStreaming: !1
137
+ }), (E = e.onFinish) == null || E.call(e), "") : (i({
138
+ data: S
139
+ }), h(n, S));
140
+ }),
141
+ []
142
+ );
143
+ return C(() => {
144
+ const n = q(
145
+ c.current,
146
+ (s) => {
147
+ o.current = M(c.current), l(s.isFetching), m(s.isStreaming), D(s.data);
148
+ }
149
+ );
150
+ return () => {
151
+ n();
152
+ };
153
+ }, []), C(() => (window.addEventListener("beforeunload", g), () => {
154
+ window.removeEventListener("beforeunload", g);
155
+ }), [g]), C(() => {
156
+ e.initialInput && A(e.initialInput);
157
+ }, []), {
158
+ data: v,
159
+ isFetching: w,
160
+ isStreaming: f,
161
+ id: c.current,
162
+ send: R,
163
+ cancel: g
34
164
  };
35
165
  };
36
166
  export {
37
- j as useEventStream
167
+ J as useEventStream,
168
+ K as useStream
38
169
  };
package/dist/index.umd.js CHANGED
@@ -1 +1 @@
1
- (function(s,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("react")):typeof define=="function"&&define.amd?define(["exports","react"],e):(s=typeof globalThis<"u"?globalThis:s||self,e(s.LaravelStreamReact={},s.React))})(this,function(s,e){"use strict";const c="data: ",b=(d,{eventName:n="update",endSignal:f="</stream>",glue:g=" ",onMessage:M=()=>null,onComplete:R=()=>null,onError:C=()=>null}={})=>{const r=e.useRef(null),u=e.useRef([]),[L,m]=e.useState(""),[P,p]=e.useState([]),a=e.useCallback(()=>{u.current=[],m(""),p([])},[]),l=e.useCallback(t=>{if([f,`${c}${f}`].includes(t.data)){o(),R();return}u.current.push(t.data.startsWith(c)?t.data.substring(c.length):t.data),m(u.current.join(g)),p(u.current),M(t)},[n,g]),i=e.useCallback(t=>{C(t),o()},[]),o=e.useCallback((t=!1)=>{var E,h,S;(E=r.current)==null||E.removeEventListener(n,l),(h=r.current)==null||h.removeEventListener("error",i),(S=r.current)==null||S.close(),r.current=null,t&&a()},[]);return e.useEffect(()=>(a(),r.current=new EventSource(d),r.current.addEventListener(n,l),r.current.addEventListener("error",i),o),[d,n,l,i,a]),{message:L,messageParts:P,close:o,clearMessage:a}};s.useEventStream=b,Object.defineProperty(s,Symbol.toStringTag,{value:"Module"})});
1
+ (function(d,t){typeof exports=="object"&&typeof module<"u"?t(exports,require("react")):typeof define=="function"&&define.amd?define(["exports","react"],t):(d=typeof globalThis<"u"?globalThis:d||self,t(d.LaravelStreamReact={},d.React))})(this,function(d,t){"use strict";const L="data: ",D=(r,{eventName:e="update",endSignal:c="</stream>",glue:o=" ",replace:M=!1,onMessage:T=()=>null,onComplete:v=()=>null,onError:w=()=>null}={})=>{const i=t.useRef(null),g=t.useRef([]),S=t.useMemo(()=>Array.isArray(e)?e:[e],Array.isArray(e)?e:[e]),[f,h]=t.useState(""),[k,F]=t.useState([]),m=t.useCallback(()=>{g.current=[],h(""),F([])},[]),s=t.useCallback(n=>{if([c,`${L}${c}`].includes(n.data)){a(),v();return}M&&m(),g.current.push(n.data.startsWith(L)?n.data.substring(L.length):n.data),h(g.current.join(o)),F(g.current),T(n)},[S,o]),u=t.useCallback(n=>{w(n),a()},[]),a=t.useCallback((n=!1)=>{var l,b;S.forEach(A=>{var y;(y=i.current)==null||y.removeEventListener(A,s)}),(l=i.current)==null||l.removeEventListener("error",u),(b=i.current)==null||b.close(),i.current=null,n&&m()},[]);return t.useEffect(()=>(m(),i.current=new EventSource(r),S.forEach(n=>{var l;(l=i.current)==null||l.addEventListener(n,s)}),i.current.addEventListener("error",u),a),[r,S,s,u,m]),{message:f,messageParts:k,close:a,clearMessage:m}},I="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";let j=(r=21)=>{let e="",c=crypto.getRandomValues(new Uint8Array(r|=0));for(;r--;)e+=I[c[r]&63];return e};const R=new Map,E=new Map,C=r=>{const e=R.get(r);return e||(R.set(r,{controller:new AbortController,data:"",isFetching:!1,isStreaming:!1}),R.get(r))},P=r=>(E.has(r)||E.set(r,[]),E.get(r)),x=(r,e)=>(P(r).push(e),()=>{E.set(r,P(r).filter(c=>c!==e))}),O=(r,e={})=>{const c=t.useRef(e.id??j()),o=t.useRef(C(c.current)),M=t.useRef((()=>{var a;const s={"Content-Type":"application/json","X-STREAM-ID":c.current},u=e.csrfToken??((a=document.querySelector('meta[name="csrf-token"]'))==null?void 0:a.getAttribute("content"));return u&&(s["X-CSRF-TOKEN"]=u),s})()),[T,v]=t.useState(o.current.data),[w,i]=t.useState(o.current.isFetching),[g,S]=t.useState(o.current.isStreaming),f=t.useCallback(s=>{var a;R.set(c.current,{...C(c.current),...s});const u=C(c.current);(a=E.get(c.current))==null||a.forEach(n=>n(u))},[]),h=t.useCallback(()=>{var s;o.current.controller.abort(),(w||g)&&((s=e.onCancel)==null||s.call(e)),f({isFetching:!1,isStreaming:!1})},[w,g]),k=t.useCallback((s={})=>{const u=new AbortController;f({isFetching:!0,controller:u}),fetch(r,{method:"POST",signal:u.signal,headers:{...M.current,...e.headers??{}},body:JSON.stringify(s)}).then(async a=>{var n;if(!a.ok){const l=await a.text();throw new Error(l)}if(!a.body)throw new Error("ReadableStream not yet supported in this browser.");return(n=e.onResponse)==null||n.call(e,a),f({isFetching:!1,isStreaming:!0}),m(a.body.getReader())}).catch(a=>{var n,l;f({isFetching:!1,isStreaming:!1}),(n=e.onError)==null||n.call(e,a),(l=e.onFinish)==null||l.call(e)})},[r]),F=t.useCallback(s=>{h(),k(s),f({data:""})},[]),m=t.useCallback((s,u="")=>s.read().then(({done:a,value:n})=>{var A,y;const l=new TextDecoder("utf-8").decode(n),b=u+l;return(A=e.onData)==null||A.call(e,l),a?(f({data:b,isStreaming:!1}),(y=e.onFinish)==null||y.call(e),""):(f({data:b}),m(s,b))}),[]);return t.useEffect(()=>{const s=x(c.current,u=>{o.current=C(c.current),i(u.isFetching),S(u.isStreaming),v(u.data)});return()=>{s()}},[]),t.useEffect(()=>(window.addEventListener("beforeunload",h),()=>{window.removeEventListener("beforeunload",h)}),[h]),t.useEffect(()=>{e.initialInput&&k(e.initialInput)},[]),{data:T,isFetching:w,isStreaming:g,id:c.current,send:F,cancel:h}};d.useEventStream=D,d.useStream=O,Object.defineProperty(d,Symbol.toStringTag,{value:"Module"})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laravel/stream-react",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Laravel streaming hooks for React",
5
5
  "keywords": [
6
6
  "laravel",
@@ -47,10 +47,11 @@
47
47
  "@vitejs/plugin-vue": "^5.0.0",
48
48
  "eslint": "^9.0.0",
49
49
  "jsdom": "^26.0.0",
50
+ "msw": "^2.8.2",
50
51
  "prettier": "^3.5.3",
51
52
  "typescript": "^5.3.0",
52
- "vite-plugin-dts": "^4.5.3",
53
53
  "vite": "^5.1.0",
54
+ "vite-plugin-dts": "^4.5.3",
54
55
  "vitest": "^3.1.1"
55
56
  },
56
57
  "peerDependencies": {