@emeryld/rrroutes-client 2.2.19 → 2.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 +62 -44
- package/dist/index.cjs +68 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +68 -23
- package/dist/index.mjs.map +1 -1
- package/dist/routesV3.client.fetch.d.ts +3 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -61,10 +61,10 @@ export function Users() {
|
|
|
61
61
|
### 1) Configure the client
|
|
62
62
|
|
|
63
63
|
```ts
|
|
64
|
-
import { QueryClient } from '@tanstack/react-query'
|
|
65
|
-
import { createRouteClient, defaultFetcher } from '@emeryld/rrroutes-client'
|
|
64
|
+
import { QueryClient } from '@tanstack/react-query'
|
|
65
|
+
import { createRouteClient, defaultFetcher } from '@emeryld/rrroutes-client'
|
|
66
66
|
|
|
67
|
-
const queryClient = new QueryClient()
|
|
67
|
+
const queryClient = new QueryClient()
|
|
68
68
|
|
|
69
69
|
const client = createRouteClient({
|
|
70
70
|
baseUrl: 'https://api.example.com',
|
|
@@ -74,7 +74,7 @@ const client = createRouteClient({
|
|
|
74
74
|
return defaultFetcher({
|
|
75
75
|
...req,
|
|
76
76
|
headers: { ...req.headers, Authorization: `Bearer ${getToken()}` },
|
|
77
|
-
})
|
|
77
|
+
})
|
|
78
78
|
},
|
|
79
79
|
cursorParam: 'cursor', // optional; default is "cursor"
|
|
80
80
|
getNextCursor: (page) => page?.links?.next ?? page?.nextCursor, // optional override
|
|
@@ -84,28 +84,28 @@ const client = createRouteClient({
|
|
|
84
84
|
invalidate: true,
|
|
85
85
|
verbose: true, // include params/query/output in debug events
|
|
86
86
|
},
|
|
87
|
-
})
|
|
87
|
+
})
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
### 2) Build endpoints from your registry
|
|
91
91
|
|
|
92
92
|
```ts
|
|
93
|
-
import { registry } from '../routes'
|
|
93
|
+
import { registry } from '../routes'
|
|
94
94
|
|
|
95
95
|
// Plain GET
|
|
96
96
|
const getUser = client.build(registry.byKey['GET /v1/users/:userId'], {
|
|
97
97
|
staleTime: 30_000,
|
|
98
|
-
})
|
|
98
|
+
})
|
|
99
99
|
|
|
100
100
|
// Infinite/feed GET (cfg.feed === true)
|
|
101
101
|
const listFeed = client.build(registry.byKey['GET /v1/posts'], {
|
|
102
102
|
getNextPageParam: (last) => last.nextCursor, // React Query option override
|
|
103
|
-
})
|
|
103
|
+
})
|
|
104
104
|
|
|
105
105
|
// Mutation
|
|
106
106
|
const updateUser = client.build(registry.byKey['PATCH /v1/users/:userId'], {
|
|
107
107
|
onSuccess: () => client.invalidate(['get', 'v1', 'users']), // prefix invalidate
|
|
108
|
-
})
|
|
108
|
+
})
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
### 3) Use GET hooks (with params/query/body)
|
|
@@ -130,8 +130,8 @@ function Profile({ userId }: { userId: string }) {
|
|
|
130
130
|
- For GET leaves that define a `bodySchema`, pass the body after the args tuple:
|
|
131
131
|
|
|
132
132
|
```ts
|
|
133
|
-
const auditStatus = client.build(registry.byKey['GET /v1/audit'])
|
|
134
|
-
await auditStatus.fetch({}, { includeExternal: true })
|
|
133
|
+
const auditStatus = client.build(registry.byKey['GET /v1/audit'])
|
|
134
|
+
await auditStatus.fetch({}, { includeExternal: true }) // body matches the leaf's bodySchema
|
|
135
135
|
```
|
|
136
136
|
|
|
137
137
|
### 4) Use infinite feeds
|
|
@@ -193,13 +193,13 @@ export function RenameForm({ userId }: { userId: string }) {
|
|
|
193
193
|
### 6) Cache keys, invalidation, and manual cache writes
|
|
194
194
|
|
|
195
195
|
```ts
|
|
196
|
-
const keys = getUser.getQueryKeys({ params: { userId: 'u_1' } })
|
|
197
|
-
await getUser.invalidate({ params: { userId: 'u_1' } })
|
|
198
|
-
await client.invalidate(['get', 'v1', 'users'])
|
|
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
199
|
|
|
200
200
|
getUser.setData((prev) => (prev ? { ...prev, status: 'online' } : prev), {
|
|
201
201
|
params: { userId: 'u_1' },
|
|
202
|
-
})
|
|
202
|
+
})
|
|
203
203
|
```
|
|
204
204
|
|
|
205
205
|
`setData` respects feeds (updates `InfiniteData` shape when `cfg.feed === true`).
|
|
@@ -207,18 +207,18 @@ getUser.setData((prev) => (prev ? { ...prev, status: 'online' } : prev), {
|
|
|
207
207
|
### 7) Router helper (build by name instead of leaf)
|
|
208
208
|
|
|
209
209
|
```ts
|
|
210
|
-
import { buildRouter } from '@emeryld/rrroutes-client'
|
|
211
|
-
import { registry } from '../routes'
|
|
210
|
+
import { buildRouter } from '@emeryld/rrroutes-client'
|
|
211
|
+
import { registry } from '../routes'
|
|
212
212
|
|
|
213
213
|
const routes = {
|
|
214
214
|
listUsers: registry.byKey['GET /v1/users'],
|
|
215
215
|
updateUser: registry.byKey['PATCH /v1/users/:userId'],
|
|
216
|
-
} as const
|
|
216
|
+
} as const
|
|
217
217
|
|
|
218
|
-
const buildRoute = buildRouter(client, routes)
|
|
218
|
+
const buildRoute = buildRouter(client, routes)
|
|
219
219
|
|
|
220
|
-
const listUsers = buildRoute('listUsers')
|
|
221
|
-
const updateUser = buildRoute('updateUser', {}, { name: 'profile' })
|
|
220
|
+
const listUsers = buildRoute('listUsers') // builds from routes.listUsers
|
|
221
|
+
const updateUser = buildRoute('updateUser', {}, { name: 'profile' }) // debug name filtering
|
|
222
222
|
```
|
|
223
223
|
|
|
224
224
|
### 8) File uploads (FormData)
|
|
@@ -226,12 +226,14 @@ const updateUser = buildRoute('updateUser', {}, { name: 'profile' }); // debug n
|
|
|
226
226
|
If a leaf has `bodyFiles` set in its contract, the client automatically converts the body to `FormData`:
|
|
227
227
|
|
|
228
228
|
```ts
|
|
229
|
-
const uploadAvatar = client.build(
|
|
229
|
+
const uploadAvatar = client.build(
|
|
230
|
+
registry.byKey['PUT /v1/users/:userId/avatar'],
|
|
231
|
+
)
|
|
230
232
|
|
|
231
233
|
await uploadAvatar.fetch(
|
|
232
234
|
{ params: { userId: 'u_1' } },
|
|
233
235
|
{ avatar: new File([blob], 'avatar.png', { type: 'image/png' }) },
|
|
234
|
-
)
|
|
236
|
+
)
|
|
235
237
|
```
|
|
236
238
|
|
|
237
239
|
### 9) Debug logging
|
|
@@ -251,9 +253,13 @@ const client = createRouteClient({
|
|
|
251
253
|
only: ['profile', 'feed'],
|
|
252
254
|
logger: (e) => console.info('[rrroutes-client]', e),
|
|
253
255
|
},
|
|
254
|
-
})
|
|
256
|
+
})
|
|
255
257
|
|
|
256
|
-
const profile = client.build(
|
|
258
|
+
const profile = client.build(
|
|
259
|
+
registry.byKey['GET /v1/me'],
|
|
260
|
+
{},
|
|
261
|
+
{ name: 'profile' },
|
|
262
|
+
)
|
|
257
263
|
```
|
|
258
264
|
|
|
259
265
|
Set `environment: 'production'` to silence all debug output regardless of the `debug` option.
|
|
@@ -267,33 +273,41 @@ The package also ships a typed Socket.IO client, React provider hooks, and a hel
|
|
|
267
273
|
### Define events + config
|
|
268
274
|
|
|
269
275
|
```ts
|
|
270
|
-
import { z } from 'zod'
|
|
271
|
-
import { defineSocketEvents } from '@emeryld/rrroutes-contract'
|
|
276
|
+
import { z } from 'zod'
|
|
277
|
+
import { defineSocketEvents } from '@emeryld/rrroutes-contract'
|
|
272
278
|
|
|
273
279
|
const { events, config } = defineSocketEvents(
|
|
274
280
|
{
|
|
275
281
|
joinMetaMessage: z.object({ source: z.string().optional() }),
|
|
276
282
|
leaveMetaMessage: z.object({ source: z.string().optional() }),
|
|
277
|
-
pingPayload: z.object({
|
|
283
|
+
pingPayload: z.object({
|
|
284
|
+
clientEcho: z.object({ sentAt: z.string() }).optional(),
|
|
285
|
+
}),
|
|
278
286
|
pongPayload: z.object({
|
|
279
287
|
clientEcho: z.object({ sentAt: z.string() }).optional(),
|
|
280
288
|
sinceMs: z.number().optional(),
|
|
281
289
|
}),
|
|
282
290
|
},
|
|
283
291
|
{
|
|
284
|
-
'chat:message': {
|
|
292
|
+
'chat:message': {
|
|
293
|
+
message: z.object({
|
|
294
|
+
roomId: z.string(),
|
|
295
|
+
text: z.string(),
|
|
296
|
+
userId: z.string(),
|
|
297
|
+
}),
|
|
298
|
+
},
|
|
285
299
|
},
|
|
286
|
-
)
|
|
300
|
+
)
|
|
287
301
|
```
|
|
288
302
|
|
|
289
303
|
### Vanilla SocketClient
|
|
290
304
|
|
|
291
305
|
```ts
|
|
292
|
-
import { io } from 'socket.io-client'
|
|
293
|
-
import { SocketClient } from '@emeryld/rrroutes-client'
|
|
294
|
-
import { events, config } from './socketContract'
|
|
306
|
+
import { io } from 'socket.io-client'
|
|
307
|
+
import { SocketClient } from '@emeryld/rrroutes-client'
|
|
308
|
+
import { events, config } from './socketContract'
|
|
295
309
|
|
|
296
|
-
const socket = io('https://socket.example.com', { transports: ['websocket'] })
|
|
310
|
+
const socket = io('https://socket.example.com', { transports: ['websocket'] })
|
|
297
311
|
|
|
298
312
|
const client = new SocketClient(events, {
|
|
299
313
|
socket,
|
|
@@ -303,26 +317,30 @@ const client = new SocketClient(events, {
|
|
|
303
317
|
clientEcho: { sentAt: new Date().toISOString() },
|
|
304
318
|
}),
|
|
305
319
|
'sys:pong': async ({ payload }) => {
|
|
306
|
-
console.log('pong latency', payload.sinceMs)
|
|
320
|
+
console.log('pong latency', payload.sinceMs)
|
|
307
321
|
},
|
|
308
322
|
'sys:room_join': async ({ rooms }) => {
|
|
309
|
-
console.log('joining rooms', rooms)
|
|
310
|
-
return true
|
|
323
|
+
console.log('joining rooms', rooms)
|
|
324
|
+
return true // allow join
|
|
311
325
|
},
|
|
312
326
|
'sys:room_leave': async ({ rooms }) => {
|
|
313
|
-
console.log('leaving rooms', rooms)
|
|
314
|
-
return true
|
|
327
|
+
console.log('leaving rooms', rooms)
|
|
328
|
+
return true
|
|
315
329
|
},
|
|
316
330
|
},
|
|
317
331
|
heartbeat: { intervalMs: 15_000, timeoutMs: 7_500 },
|
|
318
332
|
debug: { receive: true, emit: true, verbose: true, logger: console.log },
|
|
319
|
-
})
|
|
333
|
+
})
|
|
320
334
|
|
|
321
335
|
client.on('chat:message', (payload, { ctx }) => {
|
|
322
|
-
console.log('socket message', payload.text, 'latency', ctx.latencyMs)
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
void client.emit('chat:message', {
|
|
336
|
+
console.log('socket message', payload.text, 'latency', ctx.latencyMs)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
void client.emit('chat:message', {
|
|
340
|
+
roomId: 'general',
|
|
341
|
+
text: 'hi',
|
|
342
|
+
userId: 'u_1',
|
|
343
|
+
})
|
|
326
344
|
```
|
|
327
345
|
|
|
328
346
|
Key methods: `emit`, `on`, `joinRooms` / `leaveRooms`, `startHeartbeat` / `stopHeartbeat`, `connect` / `disconnect`, `stats()`, `destroy()`.
|
package/dist/index.cjs
CHANGED
|
@@ -58,7 +58,6 @@ var HttpError = class extends Error {
|
|
|
58
58
|
this.url = url;
|
|
59
59
|
this.body = { message };
|
|
60
60
|
this.headers = res.headers ? Object.fromEntries(res.headers.entries()) : {};
|
|
61
|
-
console.log("Created HttpError:", this);
|
|
62
61
|
}
|
|
63
62
|
};
|
|
64
63
|
var defaultFetcher = async (req) => {
|
|
@@ -780,7 +779,8 @@ function safeDescribeHookValue(value) {
|
|
|
780
779
|
return value;
|
|
781
780
|
}
|
|
782
781
|
if (valueType === "bigint" || valueType === "symbol") return String(value);
|
|
783
|
-
if (valueType === "function")
|
|
782
|
+
if (valueType === "function")
|
|
783
|
+
return `[function ${value.name || "anonymous"}]`;
|
|
784
784
|
if (Array.isArray(value)) return `[array length=${value.length}]`;
|
|
785
785
|
if (isProbablySocket(value)) {
|
|
786
786
|
return describeSocketLike(value);
|
|
@@ -792,7 +792,8 @@ function safeDescribeHookValue(value) {
|
|
|
792
792
|
return `[${ctorName} keys=${keyPreview}${suffix}]`;
|
|
793
793
|
}
|
|
794
794
|
function summarizeEvents(events) {
|
|
795
|
-
if (!events || typeof events !== "object")
|
|
795
|
+
if (!events || typeof events !== "object")
|
|
796
|
+
return safeDescribeHookValue(events);
|
|
796
797
|
const keys = Object.keys(events);
|
|
797
798
|
if (!keys.length) return "[events empty]";
|
|
798
799
|
const preview = keys.slice(0, 4).join(",");
|
|
@@ -800,7 +801,8 @@ function summarizeEvents(events) {
|
|
|
800
801
|
return `[events count=${keys.length} keys=${preview}${suffix}]`;
|
|
801
802
|
}
|
|
802
803
|
function summarizeBaseOptions(options) {
|
|
803
|
-
if (!options || typeof options !== "object")
|
|
804
|
+
if (!options || typeof options !== "object")
|
|
805
|
+
return safeDescribeHookValue(options);
|
|
804
806
|
const obj = options;
|
|
805
807
|
const keys = Object.keys(obj);
|
|
806
808
|
if (!keys.length) return "[baseOptions empty]";
|
|
@@ -864,8 +866,17 @@ function buildSocketProvider(args) {
|
|
|
864
866
|
};
|
|
865
867
|
}
|
|
866
868
|
function SocketProvider(props) {
|
|
867
|
-
const {
|
|
868
|
-
|
|
869
|
+
const {
|
|
870
|
+
events,
|
|
871
|
+
baseOptions,
|
|
872
|
+
children,
|
|
873
|
+
fallback,
|
|
874
|
+
providerDebug,
|
|
875
|
+
destroyLeaveMeta
|
|
876
|
+
} = props;
|
|
877
|
+
const [resolvedSocket, setResolvedSocket] = React.useState(
|
|
878
|
+
null
|
|
879
|
+
);
|
|
869
880
|
const socket = "socket" in props ? props.socket ?? null : resolvedSocket;
|
|
870
881
|
const providerDebugRef = React.useRef();
|
|
871
882
|
providerDebugRef.current = providerDebug;
|
|
@@ -887,18 +898,28 @@ function SocketProvider(props) {
|
|
|
887
898
|
if (!resolvedSocket) {
|
|
888
899
|
Promise.resolve(props.getSocket()).then((s) => {
|
|
889
900
|
if (cancelled) {
|
|
890
|
-
dbg(providerDebugRef.current, {
|
|
901
|
+
dbg(providerDebugRef.current, {
|
|
902
|
+
type: "resolve",
|
|
903
|
+
phase: "cancelled"
|
|
904
|
+
});
|
|
891
905
|
return;
|
|
892
906
|
}
|
|
893
907
|
if (!s) {
|
|
894
|
-
dbg(providerDebugRef.current, {
|
|
908
|
+
dbg(providerDebugRef.current, {
|
|
909
|
+
type: "resolve",
|
|
910
|
+
phase: "socketMissing"
|
|
911
|
+
});
|
|
895
912
|
return;
|
|
896
913
|
}
|
|
897
914
|
setResolvedSocket(s);
|
|
898
915
|
dbg(providerDebugRef.current, { type: "resolve", phase: "ok" });
|
|
899
916
|
}).catch((err) => {
|
|
900
917
|
if (cancelled) return;
|
|
901
|
-
dbg(providerDebugRef.current, {
|
|
918
|
+
dbg(providerDebugRef.current, {
|
|
919
|
+
type: "resolve",
|
|
920
|
+
phase: "error",
|
|
921
|
+
err: String(err)
|
|
922
|
+
});
|
|
902
923
|
});
|
|
903
924
|
}
|
|
904
925
|
return () => {
|
|
@@ -917,11 +938,19 @@ function SocketProvider(props) {
|
|
|
917
938
|
});
|
|
918
939
|
const client = React.useMemo(() => {
|
|
919
940
|
if (!socket) {
|
|
920
|
-
dbg(providerDebugRef.current, {
|
|
941
|
+
dbg(providerDebugRef.current, {
|
|
942
|
+
type: "client",
|
|
943
|
+
phase: "init",
|
|
944
|
+
missing: true
|
|
945
|
+
});
|
|
921
946
|
return null;
|
|
922
947
|
}
|
|
923
948
|
const c = new SocketClient(events, { ...baseOptions, socket });
|
|
924
|
-
dbg(providerDebugRef.current, {
|
|
949
|
+
dbg(providerDebugRef.current, {
|
|
950
|
+
type: "client",
|
|
951
|
+
phase: "init",
|
|
952
|
+
missing: false
|
|
953
|
+
});
|
|
925
954
|
return c;
|
|
926
955
|
}, [events, baseOptions, socket]);
|
|
927
956
|
const destroyLeaveMetaRef = React.useRef(destroyLeaveMeta);
|
|
@@ -935,7 +964,10 @@ function SocketProvider(props) {
|
|
|
935
964
|
providerDebug: providerDebugRef.current,
|
|
936
965
|
snapshot: {
|
|
937
966
|
hasClient: !!client,
|
|
938
|
-
destroyLeaveMeta: summarizeMeta(
|
|
967
|
+
destroyLeaveMeta: summarizeMeta(
|
|
968
|
+
destroyLeaveMetaRef.current,
|
|
969
|
+
"destroyLeaveMeta"
|
|
970
|
+
)
|
|
939
971
|
}
|
|
940
972
|
});
|
|
941
973
|
return () => {
|
|
@@ -950,22 +982,32 @@ function SocketProvider(props) {
|
|
|
950
982
|
}
|
|
951
983
|
function useSocketClient() {
|
|
952
984
|
const ctx = React.useContext(SocketCtx);
|
|
953
|
-
if (!ctx)
|
|
985
|
+
if (!ctx)
|
|
986
|
+
throw new Error("SocketClient not found. Wrap with <SocketProvider>.");
|
|
954
987
|
return ctx;
|
|
955
988
|
}
|
|
956
989
|
function useSocketConnection(args) {
|
|
957
|
-
const {
|
|
990
|
+
const {
|
|
991
|
+
event,
|
|
992
|
+
rooms,
|
|
993
|
+
onMessage,
|
|
994
|
+
onCleanup,
|
|
995
|
+
autoJoin = true,
|
|
996
|
+
autoLeave = true
|
|
997
|
+
} = args;
|
|
958
998
|
const client = useSocketClient();
|
|
959
999
|
const normalizedRooms = React.useMemo(
|
|
960
1000
|
() => rooms == null ? [] : Array.isArray(rooms) ? rooms : [rooms],
|
|
961
1001
|
[rooms]
|
|
962
1002
|
);
|
|
963
1003
|
React.useEffect(() => {
|
|
964
|
-
if (autoJoin && normalizedRooms.length > 0)
|
|
1004
|
+
if (autoJoin && normalizedRooms.length > 0)
|
|
1005
|
+
client.joinRooms(normalizedRooms, args.joinMeta);
|
|
965
1006
|
const unsubscribe = client.on(event, onMessage);
|
|
966
1007
|
return () => {
|
|
967
1008
|
unsubscribe();
|
|
968
|
-
if (autoLeave && normalizedRooms.length > 0)
|
|
1009
|
+
if (autoLeave && normalizedRooms.length > 0)
|
|
1010
|
+
client.leaveRooms(normalizedRooms, args.leaveMeta);
|
|
969
1011
|
if (onCleanup) onCleanup();
|
|
970
1012
|
};
|
|
971
1013
|
}, [client, event, onMessage, autoJoin, autoLeave, ...normalizedRooms]);
|
|
@@ -1087,13 +1129,16 @@ function buildSocketedRoute(options) {
|
|
|
1087
1129
|
);
|
|
1088
1130
|
const unsubscribes = entries.map(
|
|
1089
1131
|
([ev, fn]) => client.on(ev, (payload, meta) => {
|
|
1090
|
-
built.setData(
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1132
|
+
built.setData(
|
|
1133
|
+
(prev) => {
|
|
1134
|
+
const next = fn(prev, payload, meta);
|
|
1135
|
+
setRoomState(
|
|
1136
|
+
roomsFromData(next, toRooms)
|
|
1137
|
+
);
|
|
1138
|
+
return next;
|
|
1139
|
+
},
|
|
1140
|
+
...useArgs
|
|
1141
|
+
);
|
|
1097
1142
|
})
|
|
1098
1143
|
);
|
|
1099
1144
|
return () => unsubscribes.forEach((u) => u?.());
|