@emeryld/rrroutes-client 2.2.9 → 2.2.11
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 +405 -32
- package/dist/index.cjs +5 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +5 -5
- package/dist/index.mjs.map +1 -1
- package/dist/sockets/socketedRoute/socket.client.helper.d.ts +2 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Summary:
|
|
3
|
+
- Added comprehensive usage for `createRouteClient`, built endpoints (GET/feeds/mutations), cache helpers, invalidation, debug modes, custom fetchers, and React Query integration.
|
|
4
|
+
- Added new sections for router helpers, infinite/feeds, FormData uploads, and socket utilities (SocketClient, provider hooks, socketed routes).
|
|
5
|
+
- Removed the sparse README and outdated publishing script focus; future additions should cover server-side Socket.IO envelope expectations and testing recipes for hooks.
|
|
6
|
+
-->
|
|
7
|
+
|
|
1
8
|
# @emeryld/rrroutes-client
|
|
2
9
|
|
|
3
|
-
React Query helpers that sit on top of
|
|
10
|
+
Typed React Query + Socket.IO helpers that sit on top of RRRoutes contracts. Build endpoints directly from finalized leaves, get strongly-typed hooks/fetchers, ready-to-use cache keys, debug logging, and optional socket utilities (client + React provider + socketed routes).
|
|
4
11
|
|
|
5
12
|
## Installation
|
|
6
13
|
|
|
@@ -10,63 +17,429 @@ pnpm add @emeryld/rrroutes-client @tanstack/react-query
|
|
|
10
17
|
npm install @emeryld/rrroutes-client @tanstack/react-query
|
|
11
18
|
```
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
`@emeryld/rrroutes-contract` and `zod` come along as dependencies; you supply React Query.
|
|
14
21
|
|
|
15
|
-
##
|
|
22
|
+
## Quick start (typed GET with React Query)
|
|
16
23
|
|
|
17
24
|
```ts
|
|
18
|
-
import { createRouteClient } from '@emeryld/rrroutes-client';
|
|
19
25
|
import { QueryClient } from '@tanstack/react-query';
|
|
20
|
-
import {
|
|
26
|
+
import { createRouteClient } from '@emeryld/rrroutes-client';
|
|
27
|
+
import { registry } from '../routes'; // from @emeryld/rrroutes-contract + finalize(...)
|
|
21
28
|
|
|
22
29
|
const routeClient = createRouteClient({
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
baseUrl: '/api', // prepended to all paths
|
|
31
|
+
queryClient: new QueryClient(), // shared React Query instance
|
|
25
32
|
});
|
|
26
33
|
|
|
27
|
-
const listUsers = routeClient.build(registry.byKey['GET /v1/users']
|
|
34
|
+
const listUsers = routeClient.build(registry.byKey['GET /v1/users'], {
|
|
35
|
+
staleTime: 60_000,
|
|
36
|
+
onReceive: (data) => console.log('fresh users', data),
|
|
37
|
+
});
|
|
28
38
|
|
|
29
|
-
function Users() {
|
|
39
|
+
export function Users() {
|
|
30
40
|
const { data, isLoading } = listUsers.useEndpoint({ query: { search: 'emery' } });
|
|
31
41
|
if (isLoading) return <p>Loading…</p>;
|
|
32
|
-
return <pre>{JSON.stringify(data)}</pre>;
|
|
42
|
+
return <pre>{JSON.stringify(data, null, 2)}</pre>;
|
|
33
43
|
}
|
|
34
44
|
```
|
|
35
45
|
|
|
36
|
-
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
- `createRouteClient` wires your `QueryClient`, base URL, optional custom fetcher, and debug settings.
|
|
49
|
+
- `build(leaf, options?, meta?)` returns a helper that exposes:
|
|
50
|
+
- `useEndpoint(args?)` — React hook for GET/feeds/mutations (typed params/query/body/output).
|
|
51
|
+
- `fetch(...)` — direct fetcher (no cache). Mutations require the body as the last argument.
|
|
52
|
+
- `getQueryKeys(...)` — deterministic cache key used by React Query + invalidation.
|
|
53
|
+
- `invalidate(...)` — invalidate this exact endpoint instance.
|
|
54
|
+
- `setData(updater, args?)` — mutate cache (infinite-aware).
|
|
55
|
+
- For feed endpoints (`cfg.feed === true`), cursors are handled automatically; cache keys omit the cursor so pages merge correctly.
|
|
37
56
|
|
|
38
|
-
|
|
57
|
+
---
|
|
39
58
|
|
|
40
|
-
|
|
59
|
+
## Detailed usage
|
|
60
|
+
|
|
61
|
+
### 1) Configure the client
|
|
41
62
|
|
|
42
63
|
```ts
|
|
43
|
-
|
|
64
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
65
|
+
import { createRouteClient, defaultFetcher } from '@emeryld/rrroutes-client';
|
|
66
|
+
|
|
67
|
+
const queryClient = new QueryClient();
|
|
68
|
+
|
|
69
|
+
const client = createRouteClient({
|
|
70
|
+
baseUrl: 'https://api.example.com',
|
|
71
|
+
queryClient,
|
|
72
|
+
fetcher: async (req) => {
|
|
73
|
+
// Attach auth headers, reuse defaultFetcher for JSON parsing + error handling
|
|
74
|
+
return defaultFetcher({
|
|
75
|
+
...req,
|
|
76
|
+
headers: { ...req.headers, Authorization: `Bearer ${getToken()}` },
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
cursorParam: 'cursor', // optional; default is "cursor"
|
|
80
|
+
getNextCursor: (page) => page?.links?.next ?? page?.nextCursor, // optional override
|
|
81
|
+
environment: process.env.NODE_ENV, // disables debug when "production"
|
|
82
|
+
debug: {
|
|
83
|
+
fetch: true,
|
|
84
|
+
invalidate: true,
|
|
85
|
+
verbose: true, // include params/query/output in debug events
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 2) Build endpoints from your registry
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { registry } from '../routes';
|
|
94
|
+
|
|
95
|
+
// Plain GET
|
|
96
|
+
const getUser = client.build(registry.byKey['GET /v1/users/:userId'], {
|
|
97
|
+
staleTime: 30_000,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Infinite/feed GET (cfg.feed === true)
|
|
101
|
+
const listFeed = client.build(registry.byKey['GET /v1/posts'], {
|
|
102
|
+
getNextPageParam: (last) => last.nextCursor, // React Query option override
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Mutation
|
|
106
|
+
const updateUser = client.build(registry.byKey['PATCH /v1/users/:userId'], {
|
|
107
|
+
onSuccess: () => client.invalidate(['get', 'v1', 'users']), // prefix invalidate
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 3) Use GET hooks (with params/query/body)
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
type User = Awaited<ReturnType<typeof getUser.fetch>>; // fully typed output
|
|
115
|
+
|
|
116
|
+
function Profile({ userId }: { userId: string }) {
|
|
117
|
+
const result = getUser.useEndpoint({ params: { userId } });
|
|
118
|
+
if (result.isLoading) return <p>Loading…</p>;
|
|
119
|
+
if (result.error) return <p>Failed: {String(result.error)}</p>;
|
|
120
|
+
|
|
121
|
+
// Register a listener for push-based updates (e.g., sockets) against this hook
|
|
122
|
+
result.onReceive((freshUser) => {
|
|
123
|
+
console.log('pushed update', freshUser);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return <div>{result.data.name}</div>;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- For GET leaves that define a `bodySchema`, pass the body after the args tuple:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const auditStatus = client.build(registry.byKey['GET /v1/audit']);
|
|
134
|
+
await auditStatus.fetch({}, { includeExternal: true }); // body matches the leaf's bodySchema
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 4) Use infinite feeds
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
function PostFeed() {
|
|
141
|
+
const feed = listFeed.useEndpoint({ query: { cursor: undefined, limit: 20 } });
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<>
|
|
145
|
+
{(feed.data?.pages ?? []).map((page) =>
|
|
146
|
+
page.items.map((post) => <article key={post.id}>{post.title}</article>),
|
|
147
|
+
)}
|
|
148
|
+
<button
|
|
149
|
+
disabled={!feed.hasNextPage || feed.isFetchingNextPage}
|
|
150
|
+
onClick={() => feed.fetchNextPage()}
|
|
151
|
+
>
|
|
152
|
+
Load more
|
|
153
|
+
</button>
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
- Cursor params are stripped from cache keys automatically so pages share the same base key.
|
|
160
|
+
|
|
161
|
+
### 5) Use mutations (with optimistic cache helpers)
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
async function rename(userId: string, name: string) {
|
|
165
|
+
// Direct fetch (server action / non-React usage)
|
|
166
|
+
await updateUser.fetch({ params: { userId } }, { name });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function RenameForm({ userId }: { userId: string }) {
|
|
170
|
+
const mutation = updateUser.useEndpoint({ params: { userId } });
|
|
171
|
+
|
|
172
|
+
async function submit(e: React.FormEvent) {
|
|
173
|
+
e.preventDefault();
|
|
174
|
+
const name = new FormData(e.currentTarget).get('name') as string;
|
|
175
|
+
|
|
176
|
+
// Optimistically update cache for both the detail and list
|
|
177
|
+
updateUser.setData((prev) => (prev ? { ...prev, name } : prev), { params: { userId } });
|
|
178
|
+
client.invalidate(['get', 'v1', 'users']);
|
|
179
|
+
|
|
180
|
+
await mutation.mutateAsync({ name });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<form onSubmit={submit}>
|
|
185
|
+
<input name="name" defaultValue="" />
|
|
186
|
+
<button disabled={mutation.isLoading}>Save</button>
|
|
187
|
+
{mutation.error && <p>Error: {String(mutation.error)}</p>}
|
|
188
|
+
</form>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 6) Cache keys, invalidation, and manual cache writes
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
const keys = getUser.getQueryKeys({ params: { userId: 'u_1' } }); // ['get','v1','users','u_1', {}]
|
|
197
|
+
await getUser.invalidate({ params: { userId: 'u_1' } }); // invalidate exact detail
|
|
198
|
+
await client.invalidate(['get', 'v1', 'users']); // invalidate any users endpoints
|
|
199
|
+
|
|
200
|
+
getUser.setData((prev) => (prev ? { ...prev, status: 'online' } : prev), {
|
|
201
|
+
params: { userId: 'u_1' },
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`setData` respects feeds (updates `InfiniteData` shape when `cfg.feed === true`).
|
|
206
|
+
|
|
207
|
+
### 7) Router helper (build by name instead of leaf)
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { buildRouter } from '@emeryld/rrroutes-client';
|
|
211
|
+
import { registry } from '../routes';
|
|
212
|
+
|
|
213
|
+
const routes = {
|
|
214
|
+
listUsers: registry.byKey['GET /v1/users'],
|
|
215
|
+
updateUser: registry.byKey['PATCH /v1/users/:userId'],
|
|
216
|
+
} as const;
|
|
217
|
+
|
|
218
|
+
const buildRoute = buildRouter(client, routes);
|
|
219
|
+
|
|
220
|
+
const listUsers = buildRoute('listUsers'); // builds from routes.listUsers
|
|
221
|
+
const updateUser = buildRoute('updateUser', {}, { name: 'profile' }); // debug name filtering
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 8) File uploads (FormData)
|
|
225
|
+
|
|
226
|
+
If a leaf has `bodyFiles` set in its contract, the client automatically converts the body to `FormData`:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const uploadAvatar = client.build(registry.byKey['PUT /v1/users/:userId/avatar']);
|
|
230
|
+
|
|
231
|
+
await uploadAvatar.fetch(
|
|
232
|
+
{ params: { userId: 'u_1' } },
|
|
233
|
+
{ avatar: new File([blob], 'avatar.png', { type: 'image/png' }) },
|
|
234
|
+
);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### 9) Debug logging
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
const client = createRouteClient({
|
|
44
241
|
baseUrl: '/api',
|
|
45
|
-
queryClient
|
|
46
|
-
debug:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
242
|
+
queryClient,
|
|
243
|
+
debug: {
|
|
244
|
+
build: true,
|
|
245
|
+
fetch: true,
|
|
246
|
+
invalidate: true,
|
|
247
|
+
setData: true,
|
|
248
|
+
useEndpoint: true,
|
|
249
|
+
verbose: true,
|
|
250
|
+
// Limit to specific endpoints by name (third arg to build)
|
|
251
|
+
only: ['profile', 'feed'],
|
|
252
|
+
logger: (e) => console.info('[rrroutes-client]', e),
|
|
253
|
+
},
|
|
52
254
|
});
|
|
255
|
+
|
|
256
|
+
const profile = client.build(registry.byKey['GET /v1/me'], {}, { name: 'profile' });
|
|
53
257
|
```
|
|
54
258
|
|
|
55
|
-
|
|
259
|
+
Set `environment: 'production'` to silence all debug output regardless of the `debug` option.
|
|
56
260
|
|
|
57
|
-
|
|
261
|
+
---
|
|
58
262
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
263
|
+
## Socket utilities (optional)
|
|
264
|
+
|
|
265
|
+
The package also ships a typed Socket.IO client, React provider hooks, and a helper to merge socket events into React Query caches.
|
|
266
|
+
|
|
267
|
+
### Define events + config
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import { z } from 'zod';
|
|
271
|
+
import { defineSocketEvents } from '@emeryld/rrroutes-contract';
|
|
272
|
+
|
|
273
|
+
const { events, config } = defineSocketEvents(
|
|
274
|
+
{
|
|
275
|
+
joinMetaMessage: z.object({ source: z.string().optional() }),
|
|
276
|
+
leaveMetaMessage: z.object({ source: z.string().optional() }),
|
|
277
|
+
pingPayload: z.object({ clientEcho: z.object({ sentAt: z.string() }).optional() }),
|
|
278
|
+
pongPayload: z.object({
|
|
279
|
+
clientEcho: z.object({ sentAt: z.string() }).optional(),
|
|
280
|
+
sinceMs: z.number().optional(),
|
|
281
|
+
}),
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
'chat:message': { message: z.object({ roomId: z.string(), text: z.string(), userId: z.string() }) },
|
|
285
|
+
},
|
|
286
|
+
);
|
|
63
287
|
```
|
|
64
288
|
|
|
65
|
-
|
|
289
|
+
### Vanilla SocketClient
|
|
66
290
|
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
|
|
291
|
+
```ts
|
|
292
|
+
import { io } from 'socket.io-client';
|
|
293
|
+
import { SocketClient } from '@emeryld/rrroutes-client';
|
|
294
|
+
import { events, config } from './socketContract';
|
|
295
|
+
|
|
296
|
+
const socket = io('https://socket.example.com', { transports: ['websocket'] });
|
|
297
|
+
|
|
298
|
+
const client = new SocketClient(events, {
|
|
299
|
+
socket,
|
|
300
|
+
config,
|
|
301
|
+
sys: {
|
|
302
|
+
'sys:ping': async () => ({
|
|
303
|
+
clientEcho: { sentAt: new Date().toISOString() },
|
|
304
|
+
}),
|
|
305
|
+
'sys:pong': async ({ payload }) => {
|
|
306
|
+
console.log('pong latency', payload.sinceMs);
|
|
307
|
+
},
|
|
308
|
+
'sys:room_join': async ({ rooms }) => {
|
|
309
|
+
console.log('joining rooms', rooms);
|
|
310
|
+
return true; // allow join
|
|
311
|
+
},
|
|
312
|
+
'sys:room_leave': async ({ rooms }) => {
|
|
313
|
+
console.log('leaving rooms', rooms);
|
|
314
|
+
return true;
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
heartbeat: { intervalMs: 15_000, timeoutMs: 7_500 },
|
|
318
|
+
debug: { receive: true, emit: true, verbose: true, logger: console.log },
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
client.on('chat:message', (payload, { ctx }) => {
|
|
322
|
+
console.log('socket message', payload.text, 'latency', ctx.latencyMs);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
void client.emit('chat:message', { roomId: 'general', text: 'hi', userId: 'u_1' });
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Key methods: `emit`, `on`, `joinRooms` / `leaveRooms`, `startHeartbeat` / `stopHeartbeat`, `connect` / `disconnect`, `stats()`, `destroy()`.
|
|
329
|
+
|
|
330
|
+
### React provider + hooks
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import { buildSocketProvider } from '@emeryld/rrroutes-client';
|
|
334
|
+
import { io } from 'socket.io-client';
|
|
335
|
+
import { events, config } from './socketContract';
|
|
336
|
+
|
|
337
|
+
const { SocketProvider, useSocketClient, useSocketConnection } = buildSocketProvider({
|
|
338
|
+
events,
|
|
339
|
+
options: {
|
|
340
|
+
config,
|
|
341
|
+
sys: {
|
|
342
|
+
'sys:ping': async () => ({ clientEcho: { sentAt: new Date().toISOString() } }),
|
|
343
|
+
'sys:pong': async () => {},
|
|
344
|
+
'sys:room_join': async () => true,
|
|
345
|
+
'sys:room_leave': async () => true,
|
|
346
|
+
},
|
|
347
|
+
heartbeat: { intervalMs: 10_000 },
|
|
348
|
+
debug: { receive: true, hook: true, logger: console.log },
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
function App({ children }: { children: React.ReactNode }) {
|
|
353
|
+
return (
|
|
354
|
+
<SocketProvider
|
|
355
|
+
getSocket={() => io('https://socket.example.com')}
|
|
356
|
+
destroyLeaveMeta={{ source: 'app:unmount' }}
|
|
357
|
+
fallback={<p>Connecting…</p>}
|
|
358
|
+
>
|
|
359
|
+
{children}
|
|
360
|
+
</SocketProvider>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function RoomMessages({ roomId }: { roomId: string }) {
|
|
365
|
+
const client = useSocketClient<typeof events, typeof config>();
|
|
366
|
+
|
|
367
|
+
useSocketConnection({
|
|
368
|
+
event: 'chat:message',
|
|
369
|
+
rooms: roomId,
|
|
370
|
+
joinMeta: { source: 'room-hydration' },
|
|
371
|
+
leaveMeta: { source: 'room-hydration' },
|
|
372
|
+
onMessage: (payload) => console.log('message for room', payload.text),
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<button onClick={() => client.emit('chat:message', { roomId, text: 'ping', userId: 'me' })}>
|
|
377
|
+
Send
|
|
378
|
+
</button>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Socket + React Query: `buildSocketedRoute`
|
|
384
|
+
|
|
385
|
+
Automatically join rooms based on fetched data and patch the cache when socket messages arrive.
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
import { buildSocketedRoute } from '@emeryld/rrroutes-client';
|
|
389
|
+
import { useSocketClient } from './socketProvider';
|
|
390
|
+
|
|
391
|
+
const listRooms = client.build(registry.byKey['GET /v1/rooms'], { staleTime: 120_000 });
|
|
392
|
+
|
|
393
|
+
const useSocketedRooms = buildSocketedRoute({
|
|
394
|
+
built: listRooms,
|
|
395
|
+
event: 'chat:message',
|
|
396
|
+
toRooms: (page) => page.items.map((r) => r.id), // derive rooms from data (feeds supported)
|
|
397
|
+
joinMeta: { source: 'rooms:list' },
|
|
398
|
+
leaveMeta: { source: 'rooms:list' },
|
|
399
|
+
useSocketClient,
|
|
400
|
+
applyMessage: (prev, payload) => {
|
|
401
|
+
if (!prev) return prev;
|
|
402
|
+
// Example: bump unread count in cache
|
|
403
|
+
const apply = (items: any[]) =>
|
|
404
|
+
items.map((room) =>
|
|
405
|
+
room.id === payload.roomId ? { ...room, unread: (room.unread ?? 0) + 1 } : room,
|
|
406
|
+
);
|
|
407
|
+
return 'pages' in prev
|
|
408
|
+
? { ...prev, pages: prev.pages.map((p) => ({ ...p, items: apply(p.items) })) }
|
|
409
|
+
: { ...prev, items: apply(prev.items) };
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
function RoomList() {
|
|
414
|
+
const { data, rooms } = useSocketedRooms();
|
|
415
|
+
return (
|
|
416
|
+
<>
|
|
417
|
+
<p>Subscribed rooms: {rooms.join(', ')}</p>
|
|
418
|
+
<ul>{data?.items.map((r) => <li key={r.id}>{r.name}</li>)}</ul>
|
|
419
|
+
</>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
70
422
|
```
|
|
71
423
|
|
|
72
|
-
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Edge cases & notes
|
|
427
|
+
|
|
428
|
+
- Mutation `fetch` requires a body argument; GET `fetch` only requires a body when the leaf defines one.
|
|
429
|
+
- Path and query params are validated with the contract schemas before fetch; missing params throw synchronously.
|
|
430
|
+
- Query objects that contain arrays/objects are JSON-stringified in the URL query string.
|
|
431
|
+
- Feed cache keys omit the cursor so `invalidate(['get','v1','posts'])` clears all pages.
|
|
432
|
+
- `setData` runs your updater against the current cache value; return `undefined` to leave the cache untouched.
|
|
433
|
+
- When `environment` is `'production'`, debug logs are disabled even if `debug` is set.
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Scripts (monorepo)
|
|
438
|
+
|
|
439
|
+
Run from the repo root:
|
|
440
|
+
|
|
441
|
+
```sh
|
|
442
|
+
pnpm --filter @emeryld/rrroutes-client build
|
|
443
|
+
pnpm --filter @emeryld/rrroutes-client typecheck
|
|
444
|
+
pnpm --filter @emeryld/rrroutes-client test
|
|
445
|
+
```
|
package/dist/index.cjs
CHANGED
|
@@ -746,9 +746,9 @@ function roomsFromData(data, toRooms) {
|
|
|
746
746
|
return Array.from(merge);
|
|
747
747
|
}
|
|
748
748
|
function buildSocketedRoute(options) {
|
|
749
|
-
const { built, event, toRooms, applyMessage, joinMeta, leaveMeta,
|
|
749
|
+
const { built, event, toRooms, applyMessage, joinMeta, leaveMeta, useSocketClient: useSocketClient2 } = options;
|
|
750
750
|
return (...useArgs) => {
|
|
751
|
-
const client =
|
|
751
|
+
const client = useSocketClient2();
|
|
752
752
|
const endpointResult = built.useEndpoint(...useArgs);
|
|
753
753
|
const argsKey = (0, import_react2.useMemo)(() => JSON.stringify(useArgs[0] ?? null), [useArgs]);
|
|
754
754
|
const [rooms, setRooms] = (0, import_react2.useState)(
|
|
@@ -771,7 +771,7 @@ function buildSocketedRoute(options) {
|
|
|
771
771
|
setRooms(roomsFromData(endpointResult.data, toRooms));
|
|
772
772
|
}, [endpointResult.data, toRooms]);
|
|
773
773
|
(0, import_react2.useEffect)(() => {
|
|
774
|
-
if (
|
|
774
|
+
if (rooms.length === 0) return;
|
|
775
775
|
let active = true;
|
|
776
776
|
(async () => {
|
|
777
777
|
try {
|
|
@@ -780,12 +780,12 @@ function buildSocketedRoute(options) {
|
|
|
780
780
|
}
|
|
781
781
|
})();
|
|
782
782
|
return () => {
|
|
783
|
-
if (!active ||
|
|
783
|
+
if (!active || rooms.length === 0) return;
|
|
784
784
|
active = false;
|
|
785
785
|
void client.leaveRooms(rooms, leaveMeta).catch(() => {
|
|
786
786
|
});
|
|
787
787
|
};
|
|
788
|
-
}, [client, joinMeta, leaveMeta,
|
|
788
|
+
}, [client, joinMeta, leaveMeta, roomsKey]);
|
|
789
789
|
(0, import_react2.useEffect)(() => {
|
|
790
790
|
const unsubscribe = client.on(event, (payload, meta) => {
|
|
791
791
|
built.setData((prev) => {
|