@emeryld/rrroutes-client 2.2.18 → 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 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 }); // body matches the leaf's bodySchema
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' } }); // ['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
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'); // builds from routes.listUsers
221
- const updateUser = buildRoute('updateUser', {}, { name: 'profile' }); // debug name filtering
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(registry.byKey['PUT /v1/users/:userId/avatar']);
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(registry.byKey['GET /v1/me'], {}, { name: 'profile' });
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({ clientEcho: z.object({ sentAt: z.string() }).optional() }),
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': { message: z.object({ roomId: z.string(), text: z.string(), userId: z.string() }) },
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; // allow join
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', { roomId: 'general', text: 'hi', userId: 'u_1' });
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
@@ -46,15 +46,17 @@ module.exports = __toCommonJS(index_exports);
46
46
 
47
47
  // src/routesV3.client.fetch.ts
48
48
  var HttpError = class extends Error {
49
- constructor(args) {
50
- const { res, url, body } = args;
51
- super(`[${res.status}] ${res.statusText}${body ? ` - ${body}` : ""}`);
52
- this.message = `${this.message}`;
49
+ constructor({
50
+ res,
51
+ url,
52
+ body: { message }
53
+ }) {
54
+ super(`[${res.status}] ${res.statusText}${message ? ` - ${message}` : ""}`);
53
55
  this.name = "HttpError";
54
56
  this.status = res.status;
55
57
  this.statusText = res.statusText;
56
58
  this.url = url;
57
- this.body = body;
59
+ this.body = { message };
58
60
  this.headers = res.headers ? Object.fromEntries(res.headers.entries()) : {};
59
61
  }
60
62
  };
@@ -70,7 +72,7 @@ var defaultFetcher = async (req) => {
70
72
  headers,
71
73
  body: isFormData ? req.body : req.body == null ? void 0 : JSON.stringify(req.body)
72
74
  });
73
- const text = await res.text();
75
+ const text = await res.json();
74
76
  if (!res.ok) {
75
77
  throw new HttpError({
76
78
  res,
@@ -80,7 +82,7 @@ var defaultFetcher = async (req) => {
80
82
  }
81
83
  try {
82
84
  return {
83
- data: JSON.parse(text),
85
+ data: text,
84
86
  headers: res.headers ? Object.fromEntries(res.headers.entries()) : {},
85
87
  status: res.status
86
88
  };
@@ -777,7 +779,8 @@ function safeDescribeHookValue(value) {
777
779
  return value;
778
780
  }
779
781
  if (valueType === "bigint" || valueType === "symbol") return String(value);
780
- if (valueType === "function") return `[function ${value.name || "anonymous"}]`;
782
+ if (valueType === "function")
783
+ return `[function ${value.name || "anonymous"}]`;
781
784
  if (Array.isArray(value)) return `[array length=${value.length}]`;
782
785
  if (isProbablySocket(value)) {
783
786
  return describeSocketLike(value);
@@ -789,7 +792,8 @@ function safeDescribeHookValue(value) {
789
792
  return `[${ctorName} keys=${keyPreview}${suffix}]`;
790
793
  }
791
794
  function summarizeEvents(events) {
792
- if (!events || typeof events !== "object") return safeDescribeHookValue(events);
795
+ if (!events || typeof events !== "object")
796
+ return safeDescribeHookValue(events);
793
797
  const keys = Object.keys(events);
794
798
  if (!keys.length) return "[events empty]";
795
799
  const preview = keys.slice(0, 4).join(",");
@@ -797,7 +801,8 @@ function summarizeEvents(events) {
797
801
  return `[events count=${keys.length} keys=${preview}${suffix}]`;
798
802
  }
799
803
  function summarizeBaseOptions(options) {
800
- if (!options || typeof options !== "object") return safeDescribeHookValue(options);
804
+ if (!options || typeof options !== "object")
805
+ return safeDescribeHookValue(options);
801
806
  const obj = options;
802
807
  const keys = Object.keys(obj);
803
808
  if (!keys.length) return "[baseOptions empty]";
@@ -861,8 +866,17 @@ function buildSocketProvider(args) {
861
866
  };
862
867
  }
863
868
  function SocketProvider(props) {
864
- const { events, baseOptions, children, fallback, providerDebug, destroyLeaveMeta } = props;
865
- const [resolvedSocket, setResolvedSocket] = React.useState(null);
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
+ );
866
880
  const socket = "socket" in props ? props.socket ?? null : resolvedSocket;
867
881
  const providerDebugRef = React.useRef();
868
882
  providerDebugRef.current = providerDebug;
@@ -884,18 +898,28 @@ function SocketProvider(props) {
884
898
  if (!resolvedSocket) {
885
899
  Promise.resolve(props.getSocket()).then((s) => {
886
900
  if (cancelled) {
887
- dbg(providerDebugRef.current, { type: "resolve", phase: "cancelled" });
901
+ dbg(providerDebugRef.current, {
902
+ type: "resolve",
903
+ phase: "cancelled"
904
+ });
888
905
  return;
889
906
  }
890
907
  if (!s) {
891
- dbg(providerDebugRef.current, { type: "resolve", phase: "socketMissing" });
908
+ dbg(providerDebugRef.current, {
909
+ type: "resolve",
910
+ phase: "socketMissing"
911
+ });
892
912
  return;
893
913
  }
894
914
  setResolvedSocket(s);
895
915
  dbg(providerDebugRef.current, { type: "resolve", phase: "ok" });
896
916
  }).catch((err) => {
897
917
  if (cancelled) return;
898
- dbg(providerDebugRef.current, { type: "resolve", phase: "error", err: String(err) });
918
+ dbg(providerDebugRef.current, {
919
+ type: "resolve",
920
+ phase: "error",
921
+ err: String(err)
922
+ });
899
923
  });
900
924
  }
901
925
  return () => {
@@ -914,11 +938,19 @@ function SocketProvider(props) {
914
938
  });
915
939
  const client = React.useMemo(() => {
916
940
  if (!socket) {
917
- dbg(providerDebugRef.current, { type: "client", phase: "init", missing: true });
941
+ dbg(providerDebugRef.current, {
942
+ type: "client",
943
+ phase: "init",
944
+ missing: true
945
+ });
918
946
  return null;
919
947
  }
920
948
  const c = new SocketClient(events, { ...baseOptions, socket });
921
- dbg(providerDebugRef.current, { type: "client", phase: "init", missing: false });
949
+ dbg(providerDebugRef.current, {
950
+ type: "client",
951
+ phase: "init",
952
+ missing: false
953
+ });
922
954
  return c;
923
955
  }, [events, baseOptions, socket]);
924
956
  const destroyLeaveMetaRef = React.useRef(destroyLeaveMeta);
@@ -932,7 +964,10 @@ function SocketProvider(props) {
932
964
  providerDebug: providerDebugRef.current,
933
965
  snapshot: {
934
966
  hasClient: !!client,
935
- destroyLeaveMeta: summarizeMeta(destroyLeaveMetaRef.current, "destroyLeaveMeta")
967
+ destroyLeaveMeta: summarizeMeta(
968
+ destroyLeaveMetaRef.current,
969
+ "destroyLeaveMeta"
970
+ )
936
971
  }
937
972
  });
938
973
  return () => {
@@ -947,22 +982,32 @@ function SocketProvider(props) {
947
982
  }
948
983
  function useSocketClient() {
949
984
  const ctx = React.useContext(SocketCtx);
950
- if (!ctx) throw new Error("SocketClient not found. Wrap with <SocketProvider>.");
985
+ if (!ctx)
986
+ throw new Error("SocketClient not found. Wrap with <SocketProvider>.");
951
987
  return ctx;
952
988
  }
953
989
  function useSocketConnection(args) {
954
- const { event, rooms, onMessage, onCleanup, autoJoin = true, autoLeave = true } = args;
990
+ const {
991
+ event,
992
+ rooms,
993
+ onMessage,
994
+ onCleanup,
995
+ autoJoin = true,
996
+ autoLeave = true
997
+ } = args;
955
998
  const client = useSocketClient();
956
999
  const normalizedRooms = React.useMemo(
957
1000
  () => rooms == null ? [] : Array.isArray(rooms) ? rooms : [rooms],
958
1001
  [rooms]
959
1002
  );
960
1003
  React.useEffect(() => {
961
- if (autoJoin && normalizedRooms.length > 0) client.joinRooms(normalizedRooms, args.joinMeta);
1004
+ if (autoJoin && normalizedRooms.length > 0)
1005
+ client.joinRooms(normalizedRooms, args.joinMeta);
962
1006
  const unsubscribe = client.on(event, onMessage);
963
1007
  return () => {
964
1008
  unsubscribe();
965
- if (autoLeave && normalizedRooms.length > 0) client.leaveRooms(normalizedRooms, args.leaveMeta);
1009
+ if (autoLeave && normalizedRooms.length > 0)
1010
+ client.leaveRooms(normalizedRooms, args.leaveMeta);
966
1011
  if (onCleanup) onCleanup();
967
1012
  };
968
1013
  }, [client, event, onMessage, autoJoin, autoLeave, ...normalizedRooms]);
@@ -1084,13 +1129,16 @@ function buildSocketedRoute(options) {
1084
1129
  );
1085
1130
  const unsubscribes = entries.map(
1086
1131
  ([ev, fn]) => client.on(ev, (payload, meta) => {
1087
- built.setData((prev) => {
1088
- const next = fn(prev, payload, meta);
1089
- setRoomState(
1090
- roomsFromData(next, toRooms)
1091
- );
1092
- return next;
1093
- }, ...useArgs);
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
+ );
1094
1142
  })
1095
1143
  );
1096
1144
  return () => unsubscribes.forEach((u) => u?.());