@spoosh/react 0.11.0 → 0.13.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
@@ -1,6 +1,6 @@
1
1
  # @spoosh/react
2
2
 
3
- React hooks for Spoosh - `useRead`, `useWrite`, and `useInfiniteRead`.
3
+ React hooks for Spoosh - `useRead`, `useWrite`, `usePages`, and `useSSE`.
4
4
 
5
5
  **[Documentation](https://spoosh.dev/docs/react)** · **Requirements:** TypeScript >= 5.0, React >= 18.0
6
6
 
@@ -23,7 +23,7 @@ const spoosh = new Spoosh<ApiSchema, Error>("/api").use([
23
23
  cachePlugin({ staleTime: 5000 }),
24
24
  ]);
25
25
 
26
- export const { useRead, useWrite, useInfiniteRead } = create(spoosh);
26
+ export const { useRead, useWrite, usePages } = create(spoosh);
27
27
  ```
28
28
 
29
29
  ### useRead
@@ -98,7 +98,7 @@ await updateUser.trigger({
98
98
  });
99
99
  ```
100
100
 
101
- ### useInfiniteRead
101
+ ### usePages
102
102
 
103
103
  Bidirectional paginated data fetching with infinite scroll support.
104
104
 
@@ -106,7 +106,7 @@ Bidirectional paginated data fetching with infinite scroll support.
106
106
  function PostList() {
107
107
  const {
108
108
  data,
109
- allResponses,
109
+ pages,
110
110
  loading,
111
111
  canFetchNext,
112
112
  canFetchPrev,
@@ -114,26 +114,26 @@ function PostList() {
114
114
  fetchPrev,
115
115
  fetchingNext,
116
116
  fetchingPrev,
117
- } = useInfiniteRead(
117
+ } = usePages(
118
118
  (api) => api("posts").GET({ query: { page: 1 } }),
119
119
  {
120
120
  // Required: Check if next page exists
121
- canFetchNext: ({ response }) => response?.meta.hasMore ?? false,
121
+ canFetchNext: ({ lastPage }) => lastPage?.data?.meta.hasMore ?? false,
122
122
 
123
123
  // Required: Build request for next page
124
- nextPageRequest: ({ response, request }) => ({
125
- query: { ...request.query, page: (response?.meta.page ?? 0) + 1 },
124
+ nextPageRequest: ({ lastPage }) => ({
125
+ query: { page: (lastPage?.data?.meta.page ?? 0) + 1 },
126
126
  }),
127
127
 
128
- // Required: Merge all responses into items
129
- merger: (allResponses) => allResponses.flatMap((r) => r.items),
128
+ // Required: Merge all pages into items
129
+ merger: (pages) => pages.flatMap((p) => p.data?.items ?? []),
130
130
 
131
131
  // Optional: Check if previous page exists
132
- canFetchPrev: ({ response }) => (response?.meta.page ?? 1) > 1,
132
+ canFetchPrev: ({ firstPage }) => (firstPage?.data?.meta.page ?? 1) > 1,
133
133
 
134
134
  // Optional: Build request for previous page
135
- prevPageRequest: ({ response, request }) => ({
136
- query: { ...request.query, page: (response?.meta.page ?? 2) - 1 },
135
+ prevPageRequest: ({ firstPage }) => ({
136
+ query: { page: (firstPage?.data?.meta.page ?? 2) - 1 },
137
137
  }),
138
138
  }
139
139
  );
@@ -158,6 +158,61 @@ function PostList() {
158
158
  }
159
159
  ```
160
160
 
161
+ ### useSSE
162
+
163
+ Subscribe to real-time data streams using Server-Sent Events (SSE).
164
+
165
+ ```typescript
166
+ import { sse } from "@spoosh/transport-sse";
167
+
168
+ // Setup with SSE transport
169
+ const spoosh = new Spoosh<ApiSchema, Error>("/api").withTransports([sse()]);
170
+ export const { useSSE } = create(spoosh);
171
+
172
+ // Basic subscription
173
+ function Notifications() {
174
+ const { data, isConnected, loading } = useSSE(
175
+ (api) => api("notifications").GET({ query: { userId: "user-123" } })
176
+ );
177
+
178
+ if (loading) return <div>Connecting...</div>;
179
+
180
+ return (
181
+ <div>
182
+ <span>{isConnected ? "Connected" : "Disconnected"}</span>
183
+ {data?.message && <p>{data.message.text}</p>}
184
+ </div>
185
+ );
186
+ }
187
+
188
+ // Subscribe to specific events only
189
+ const { data } = useSSE(
190
+ (api) => api("notifications").GET({
191
+ query: { userId: "user-123" },
192
+ }),
193
+ { events: ["alert"] } // Only alert events
194
+ );
195
+
196
+ // AI streaming with accumulation
197
+ const { data, trigger } = useSSE(
198
+ (api) => api("chat").POST(),
199
+ {
200
+ events: ["chunk", "done"],
201
+ parse: "json-done",
202
+ accumulate: {
203
+ chunk: (prev, curr) => ({
204
+ ...curr,
205
+ chunk: (prev?.chunk || "") + curr.chunk,
206
+ }),
207
+ },
208
+ enabled: false,
209
+ }
210
+ );
211
+
212
+ // Start streaming on demand
213
+ await trigger({ body: { message: "Hello" } });
214
+ ```
215
+
161
216
  ## API Reference
162
217
 
163
218
  ### useRead(readFn, options?)
@@ -192,41 +247,88 @@ function PostList() {
192
247
  | `loading` | `boolean` | True while mutation is in progress |
193
248
  | `abort` | `() => void` | Abort current request |
194
249
 
195
- ### useInfiniteRead(readFn, options)
250
+ ### usePages(readFn, options)
196
251
 
197
- | Option | Type | Required | Description |
198
- | ----------------- | ---------------------------- | -------- | ------------------------------- |
199
- | `canFetchNext` | `(ctx) => boolean` | Yes | Check if next page exists |
200
- | `nextPageRequest` | `(ctx) => Partial<TRequest>` | Yes | Build request for next page |
201
- | `merger` | `(allResponses) => TItem[]` | Yes | Merge all responses into items |
202
- | `canFetchPrev` | `(ctx) => boolean` | No | Check if previous page exists |
203
- | `prevPageRequest` | `(ctx) => Partial<TRequest>` | No | Build request for previous page |
204
- | `enabled` | `boolean` | No | Whether to fetch automatically |
252
+ | Option | Type | Required | Description |
253
+ | ----------------- | ---------------------------- | -------- | ------------------------------------------------- |
254
+ | `merger` | `(pages) => TItem[]` | Yes | Merge all pages into items |
255
+ | `canFetchNext` | `(ctx) => boolean` | No | Check if next page exists. Default: `() => false` |
256
+ | `nextPageRequest` | `(ctx) => Partial<TRequest>` | No | Build request for next page |
257
+ | `canFetchPrev` | `(ctx) => boolean` | No | Check if previous page exists |
258
+ | `prevPageRequest` | `(ctx) => Partial<TRequest>` | No | Build request for previous page |
259
+ | `enabled` | `boolean` | No | Whether to fetch automatically |
205
260
 
206
261
  **Context object passed to callbacks:**
207
262
 
208
263
  ```typescript
209
- type Context<TData, TRequest> = {
210
- response: TData | undefined; // Latest response
211
- allResponses: TData[]; // All fetched responses
212
- request: TRequest; // Current request options
264
+ // For canFetchNext and nextPageRequest
265
+ type NextContext<TData, TRequest> = {
266
+ lastPage: InfinitePage<TData> | undefined;
267
+ pages: InfinitePage<TData>[];
268
+ request: TRequest;
269
+ };
270
+
271
+ // For canFetchPrev and prevPageRequest
272
+ type PrevContext<TData, TRequest> = {
273
+ firstPage: InfinitePage<TData> | undefined;
274
+ pages: InfinitePage<TData>[];
275
+ request: TRequest;
276
+ };
277
+
278
+ // Each page in the pages array
279
+ type InfinitePage<TData> = {
280
+ status: "pending" | "loading" | "success" | "error" | "stale";
281
+ data?: TData;
282
+ error?: TError;
283
+ meta?: TMeta;
284
+ input?: { query?; params?; body? };
213
285
  };
214
286
  ```
215
287
 
216
288
  **Returns:**
217
289
 
218
- | Property | Type | Description |
219
- | -------------- | ---------------------- | ------------------------------- |
220
- | `data` | `TItem[] \| undefined` | Merged items from all responses |
221
- | `allResponses` | `TData[] \| undefined` | Array of all raw responses |
222
- | `loading` | `boolean` | True during initial load |
223
- | `fetching` | `boolean` | True during any fetch |
224
- | `fetchingNext` | `boolean` | True while fetching next page |
225
- | `fetchingPrev` | `boolean` | True while fetching previous |
226
- | `canFetchNext` | `boolean` | Whether next page exists |
227
- | `canFetchPrev` | `boolean` | Whether previous page exists |
228
- | `fetchNext` | `() => Promise<void>` | Fetch the next page |
229
- | `fetchPrev` | `() => Promise<void>` | Fetch the previous page |
230
- | `refetch` | `() => Promise<void>` | Refetch all pages |
231
- | `abort` | `() => void` | Abort current request |
232
- | `error` | `TError \| undefined` | Error if request failed |
290
+ | Property | Type | Description |
291
+ | -------------- | ----------------------------- | ----------------------------------------------- |
292
+ | `data` | `TItem[] \| undefined` | Merged items from all pages |
293
+ | `pages` | `InfinitePage<TData>[]` | Array of all pages with status, data, and meta |
294
+ | `loading` | `boolean` | True during initial load |
295
+ | `fetching` | `boolean` | True during any fetch |
296
+ | `fetchingNext` | `boolean` | True while fetching next page |
297
+ | `fetchingPrev` | `boolean` | True while fetching previous |
298
+ | `canFetchNext` | `boolean` | Whether next page exists |
299
+ | `canFetchPrev` | `boolean` | Whether previous page exists |
300
+ | `fetchNext` | `() => Promise<void>` | Fetch the next page |
301
+ | `fetchPrev` | `() => Promise<void>` | Fetch the previous page |
302
+ | `trigger` | `(options?) => Promise<void>` | Trigger fetch with optional new request options |
303
+ | `abort` | `() => void` | Abort current request |
304
+ | `error` | `TError \| undefined` | Error if request failed |
305
+
306
+ ### useSSE(subFn, options?)
307
+
308
+ | Option | Type | Default | Description |
309
+ | ------------ | ------------------ | ----------- | --------------------------------- |
310
+ | `enabled` | `boolean` | `true` | Whether to connect automatically |
311
+ | `events` | `string[]` | all events | Subscribe to specific events only |
312
+ | `parse` | `ParseConfig` | `"auto"` | How to parse raw event data |
313
+ | `accumulate` | `AccumulateConfig` | `"replace"` | How to combine events over time |
314
+
315
+ **Returns:**
316
+
317
+ | Property | Type | Description |
318
+ | ------------- | ----------------------- | ------------------------------ |
319
+ | `data` | `TEvents \| undefined` | Accumulated event data |
320
+ | `error` | `TError \| undefined` | Error if connection failed |
321
+ | `loading` | `boolean` | True during initial connection |
322
+ | `isConnected` | `boolean` | True when connected to stream |
323
+ | `trigger` | `(options?) => Promise` | Reconnect with new options |
324
+ | `disconnect` | `() => void` | Disconnect from stream |
325
+ | `reset` | `() => void` | Reset accumulated data |
326
+
327
+ **Connection Options:**
328
+
329
+ | Option | Type | Description |
330
+ | ------------- | -------------------- | ------------------------------------------- |
331
+ | `headers` | `HeadersInit` | Request headers |
332
+ | `credentials` | `RequestCredentials` | Credentials mode |
333
+ | `maxRetries` | `number` | Max retry attempts (default: 3) |
334
+ | `retryDelay` | `number` | Delay between retries in ms (default: 1000) |