@emeryld/rrroutes-client 2.2.11 → 2.2.13

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
@@ -392,21 +392,24 @@ const listRooms = client.build(registry.byKey['GET /v1/rooms'], { staleTime: 120
392
392
 
393
393
  const useSocketedRooms = buildSocketedRoute({
394
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' },
395
+ toRooms: (page) => ({
396
+ rooms: page.items.map((r) => r.id), // derive rooms from data (feeds supported)
397
+ joinMeta: { source: 'rooms:list' },
398
+ leaveMeta: { source: 'rooms:list' },
399
+ }),
399
400
  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) };
401
+ applyMessage: {
402
+ 'chat:message': (prev, payload) => {
403
+ if (!prev) return prev;
404
+ // Example: bump unread count in cache
405
+ const apply = (items: any[]) =>
406
+ items.map((room) =>
407
+ room.id === payload.roomId ? { ...room, unread: (room.unread ?? 0) + 1 } : room,
408
+ );
409
+ return 'pages' in prev
410
+ ? { ...prev, pages: prev.pages.map((p) => ({ ...p, items: apply(p.items) })) }
411
+ : { ...prev, items: apply(prev.items) };
412
+ },
410
413
  },
411
414
  });
412
415
 
package/dist/index.cjs CHANGED
@@ -71,8 +71,17 @@ var defaultFetcher = async (req) => {
71
71
  // src/routesV3.client.index.ts
72
72
  var import_react = require("react");
73
73
  var import_react_query = require("@tanstack/react-query");
74
+ var import_zod = require("zod");
74
75
  var import_rrroutes_contract = require("@emeryld/rrroutes-contract");
75
76
  var toUpper = (m) => m.toUpperCase();
77
+ var defaultFeedQuerySchema = import_zod.z.object({
78
+ cursor: import_zod.z.string().optional(),
79
+ limit: import_zod.z.coerce.number().min(1).max(100).default(20)
80
+ });
81
+ var defaultFeedOutputSchema = import_zod.z.object({
82
+ items: import_zod.z.array(import_zod.z.unknown()),
83
+ nextCursor: import_zod.z.string().optional()
84
+ });
76
85
  function zParse(value, schema) {
77
86
  return schema ? schema.parse(value) : value;
78
87
  }
@@ -174,6 +183,37 @@ function extractArgs(args) {
174
183
  function toArgsTuple(args) {
175
184
  return typeof args === "undefined" ? [] : [args];
176
185
  }
186
+ function augmentFeedQuerySchema(schema) {
187
+ if (!schema) return defaultFeedQuerySchema;
188
+ if (schema instanceof import_zod.z.ZodObject) {
189
+ const shape = schema.shape ? schema.shape : schema._def?.shape?.();
190
+ return schema.extend({
191
+ ...shape ?? {},
192
+ cursor: defaultFeedQuerySchema.shape.cursor,
193
+ limit: defaultFeedQuerySchema.shape.limit
194
+ });
195
+ }
196
+ return import_zod.z.intersection(schema, defaultFeedQuerySchema);
197
+ }
198
+ function augmentFeedOutputSchema(schema) {
199
+ if (!schema) return defaultFeedOutputSchema;
200
+ if (schema instanceof import_zod.z.ZodObject) {
201
+ const shape = schema.shape ? schema.shape : schema._def?.shape?.();
202
+ const hasItems = Boolean(shape?.items);
203
+ if (hasItems) return schema.extend({ nextCursor: import_zod.z.string().optional() });
204
+ return import_zod.z.object({
205
+ items: import_zod.z.array(schema),
206
+ nextCursor: import_zod.z.string().optional()
207
+ });
208
+ }
209
+ if (schema instanceof import_zod.z.ZodArray) {
210
+ return import_zod.z.object({
211
+ items: schema,
212
+ nextCursor: import_zod.z.string().optional()
213
+ });
214
+ }
215
+ return defaultFeedOutputSchema;
216
+ }
177
217
  function buildUrl(leaf, baseUrl, params, query) {
178
218
  const normalizedParams = zParse(params, leaf.cfg.paramsSchema);
179
219
  const normalizedQuery = zParse(query, leaf.cfg.querySchema);
@@ -202,8 +242,13 @@ function createRouteClient(opts) {
202
242
  function buildInternal(leaf, rqOpts, meta) {
203
243
  const isGet = leaf.method === "get";
204
244
  const isFeed = !!leaf.cfg.feed;
245
+ const leafCfg = isFeed ? {
246
+ ...leaf.cfg,
247
+ querySchema: augmentFeedQuerySchema(leaf.cfg.querySchema),
248
+ outputSchema: augmentFeedOutputSchema(leaf.cfg.outputSchema)
249
+ } : leaf.cfg;
205
250
  const method = toUpper(leaf.method);
206
- const expectsArgs = Boolean(leaf.cfg.paramsSchema || leaf.cfg.querySchema);
251
+ const expectsArgs = Boolean(leafCfg.paramsSchema || leafCfg.querySchema);
207
252
  const leafLabel = `${leaf.method.toUpperCase()} ${String(leaf.path)}`;
208
253
  const debugName = meta?.name;
209
254
  const emit = (event) => emitDebug(event, debugName);
@@ -247,13 +292,18 @@ function createRouteClient(opts) {
247
292
  const a = extractArgs(tuple);
248
293
  const params = a?.params;
249
294
  const query = options?.queryOverride ?? a?.query;
250
- const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);
295
+ const { url, normalizedQuery, normalizedParams } = buildUrl(
296
+ { ...leaf, cfg: leafCfg },
297
+ baseUrl,
298
+ params,
299
+ query
300
+ );
251
301
  let payload;
252
- const acceptsBody = Boolean(leaf.cfg.bodySchema);
302
+ const acceptsBody = Boolean(leafCfg.bodySchema);
253
303
  const requiresBody = options?.requireBody ?? (!isGet && acceptsBody);
254
304
  if (typeof options?.body !== "undefined") {
255
- const normalizedBody = zParse(options.body, leaf.cfg.bodySchema);
256
- const isMultipart = Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0;
305
+ const normalizedBody = zParse(options.body, leafCfg.bodySchema);
306
+ const isMultipart = Array.isArray(leafCfg.bodyFiles) && leafCfg.bodyFiles.length > 0;
257
307
  payload = isMultipart ? toFormData(normalizedBody) : normalizedBody;
258
308
  } else if (requiresBody) {
259
309
  throw new Error("Body is required when invoking a mutation fetch.");
@@ -277,7 +327,7 @@ function createRouteClient(opts) {
277
327
  const out = await fetcher(
278
328
  payload === void 0 ? { url, method } : { url, method, body: payload }
279
329
  );
280
- const parsed = zParse(out, leaf.cfg.outputSchema);
330
+ const parsed = zParse(out, leafCfg.outputSchema);
281
331
  emit(
282
332
  decorateDebugEvent(
283
333
  {
@@ -313,7 +363,7 @@ function createRouteClient(opts) {
313
363
  }
314
364
  };
315
365
  const fetchGet = (...tupleWithBody) => {
316
- const acceptsBody = Boolean(leaf.cfg.bodySchema);
366
+ const acceptsBody = Boolean(leafCfg.bodySchema);
317
367
  const tupleLength = tupleWithBody.length;
318
368
  const maybeBodyIndex = expectsArgs ? 1 : 0;
319
369
  const hasBodyCandidate = acceptsBody && tupleLength > maybeBodyIndex;
@@ -346,7 +396,12 @@ function createRouteClient(opts) {
346
396
  },
347
397
  []
348
398
  );
349
- const { normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);
399
+ const { normalizedQuery, normalizedParams } = buildUrl(
400
+ { ...leaf, cfg: leafCfg },
401
+ baseUrl,
402
+ params,
403
+ query
404
+ );
350
405
  const queryResult = (0, import_react_query.useInfiniteQuery)({
351
406
  ...buildOptions,
352
407
  queryKey: getQueryKeys(...tuple),
@@ -400,7 +455,7 @@ function createRouteClient(opts) {
400
455
  },
401
456
  []
402
457
  );
403
- buildUrl(leaf, baseUrl, params, query);
458
+ buildUrl({ ...leaf, cfg: leafCfg }, baseUrl, params, query);
404
459
  const queryResult = (0, import_react_query.useQuery)({
405
460
  ...buildOptions,
406
461
  queryKey: getQueryKeys(...tuple),
@@ -488,9 +543,9 @@ function toFormData(body) {
488
543
  }
489
544
 
490
545
  // src/sockets/socket.client.sys.ts
491
- var import_zod = require("zod");
492
- var roomValueSchema = import_zod.z.union([import_zod.z.array(import_zod.z.string()), import_zod.z.string()]);
493
- var buildRoomPayloadSchema = (metaSchema) => import_zod.z.object({
546
+ var import_zod2 = require("zod");
547
+ var roomValueSchema = import_zod2.z.union([import_zod2.z.array(import_zod2.z.string()), import_zod2.z.string()]);
548
+ var buildRoomPayloadSchema = (metaSchema) => import_zod2.z.object({
494
549
  rooms: roomValueSchema,
495
550
  meta: metaSchema
496
551
  });
@@ -731,72 +786,84 @@ function normalizeRooms(rooms) {
731
786
  }
732
787
  return normalized;
733
788
  }
789
+ function mergeRoomState(prev, toRoomsResult) {
790
+ const merged = new Set(prev.rooms);
791
+ for (const r of normalizeRooms(toRoomsResult.rooms)) merged.add(r);
792
+ return {
793
+ rooms: Array.from(merged),
794
+ joinMeta: toRoomsResult.joinMeta ?? prev.joinMeta,
795
+ leaveMeta: toRoomsResult.leaveMeta ?? prev.leaveMeta
796
+ };
797
+ }
734
798
  function roomsFromData(data, toRooms) {
735
- if (data == null) return [];
736
- const merge = /* @__PURE__ */ new Set();
799
+ if (data == null) return { rooms: [] };
800
+ let state = { rooms: [] };
737
801
  const add = (input) => {
738
- for (const r of normalizeRooms(input)) merge.add(r);
802
+ state = mergeRoomState(state, toRooms(input));
739
803
  };
740
804
  const maybePages = data?.pages;
741
805
  if (Array.isArray(maybePages)) {
742
- for (const page of maybePages) add(toRooms(page));
743
- return Array.from(merge);
806
+ for (const page of maybePages) add(page);
807
+ return state;
744
808
  }
745
- add(toRooms(data));
746
- return Array.from(merge);
809
+ add(data);
810
+ return state;
747
811
  }
748
812
  function buildSocketedRoute(options) {
749
- const { built, event, toRooms, applyMessage, joinMeta, leaveMeta, useSocketClient: useSocketClient2 } = options;
813
+ const { built, toRooms, applyMessage, useSocketClient: useSocketClient2 } = options;
750
814
  return (...useArgs) => {
751
815
  const client = useSocketClient2();
752
816
  const endpointResult = built.useEndpoint(...useArgs);
753
817
  const argsKey = (0, import_react2.useMemo)(() => JSON.stringify(useArgs[0] ?? null), [useArgs]);
754
- const [rooms, setRooms] = (0, import_react2.useState)(
818
+ const [roomState, setRoomState] = (0, import_react2.useState)(
755
819
  () => roomsFromData(endpointResult.data, toRooms)
756
820
  );
757
- const roomsKey = (0, import_react2.useMemo)(() => rooms.join("|"), [rooms]);
821
+ const roomsKey = (0, import_react2.useMemo)(() => roomState.rooms.join("|"), [roomState.rooms]);
822
+ const joinMetaKey = (0, import_react2.useMemo)(() => JSON.stringify(roomState.joinMeta ?? null), [roomState.joinMeta]);
823
+ const leaveMetaKey = (0, import_react2.useMemo)(() => JSON.stringify(roomState.leaveMeta ?? null), [roomState.leaveMeta]);
758
824
  (0, import_react2.useEffect)(() => {
759
825
  const unsubscribe = endpointResult.onReceive((data) => {
760
- setRooms((prev) => {
761
- const next = normalizeRooms(toRooms(data));
762
- if (next.length === 0) return prev;
763
- const merged = new Set(prev);
764
- next.forEach((r) => merged.add(r));
765
- return Array.from(merged);
766
- });
826
+ setRoomState((prev) => mergeRoomState(prev, toRooms(data)));
767
827
  });
768
828
  return unsubscribe;
769
829
  }, [endpointResult, toRooms]);
770
830
  (0, import_react2.useEffect)(() => {
771
- setRooms(roomsFromData(endpointResult.data, toRooms));
831
+ setRoomState(roomsFromData(endpointResult.data, toRooms));
772
832
  }, [endpointResult.data, toRooms]);
773
833
  (0, import_react2.useEffect)(() => {
774
- if (rooms.length === 0) return;
834
+ if (roomState.rooms.length === 0) return;
835
+ const { joinMeta, leaveMeta } = roomState;
836
+ if (!joinMeta || !leaveMeta) return;
775
837
  let active = true;
776
838
  (async () => {
777
839
  try {
778
- await client.joinRooms(rooms, joinMeta);
840
+ await client.joinRooms(roomState.rooms, joinMeta);
779
841
  } catch {
780
842
  }
781
843
  })();
782
844
  return () => {
783
- if (!active || rooms.length === 0) return;
845
+ if (!active || roomState.rooms.length === 0) return;
784
846
  active = false;
785
- void client.leaveRooms(rooms, leaveMeta).catch(() => {
847
+ void client.leaveRooms(roomState.rooms, leaveMeta).catch(() => {
786
848
  });
787
849
  };
788
- }, [client, joinMeta, leaveMeta, roomsKey]);
850
+ }, [client, roomsKey, roomState.joinMeta, roomState.leaveMeta, joinMetaKey, leaveMetaKey]);
789
851
  (0, import_react2.useEffect)(() => {
790
- const unsubscribe = client.on(event, (payload, meta) => {
791
- built.setData((prev) => {
792
- const next = applyMessage(prev, payload, meta);
793
- setRooms(roomsFromData(next, toRooms));
794
- return next;
795
- }, ...useArgs);
796
- });
797
- return unsubscribe;
798
- }, [client, event, applyMessage, built, argsKey]);
799
- return { ...endpointResult, rooms };
852
+ const entries = Object.entries(applyMessage).filter(
853
+ ([_event, fn]) => typeof fn === "function"
854
+ );
855
+ const unsubscribes = entries.map(
856
+ ([ev, fn]) => client.on(ev, (payload, meta) => {
857
+ built.setData((prev) => {
858
+ const next = fn(prev, payload, meta);
859
+ setRoomState(roomsFromData(next, toRooms));
860
+ return next;
861
+ }, ...useArgs);
862
+ })
863
+ );
864
+ return () => unsubscribes.forEach((u) => u?.());
865
+ }, [client, applyMessage, built, argsKey, toRooms]);
866
+ return { ...endpointResult, rooms: roomState.rooms };
800
867
  };
801
868
  }
802
869