@emeryld/rrroutes-client 2.6.6 → 2.6.7

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,7 +1,7 @@
1
1
  <!--
2
2
  Summary:
3
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).
4
+ - Added new sections for router helpers, batched branches (`buildBranch`), infinite/feeds, FormData uploads, and socket utilities (SocketClient, provider hooks, socketed routes).
5
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
6
  -->
7
7
 
@@ -220,7 +220,53 @@ const listUsers = buildRoute('listUsers') // builds from routes.listUsers
220
220
  const updateUser = buildRoute('updateUser', {}, { name: 'profile' }) // debug name filtering
221
221
  ```
222
222
 
223
- ### 8) File uploads (FormData)
223
+ ### 8) Batch multiple built endpoints (`buildBranch`)
224
+
225
+ `buildBranch` lets you batch already-built endpoints behind one batch path.
226
+
227
+ ```ts
228
+ const getUser = client.build(registry.byKey['GET /v1/users/:userId'])
229
+ const updateUser = client.build(registry.byKey['PATCH /v1/users/:userId'])
230
+
231
+ const userBatch = client.buildBranch(
232
+ { getUser, updateUser },
233
+ { path: '/v1/batch', method: 'post' }, // method defaults to POST
234
+ )
235
+
236
+ const result = await userBatch.fetch({
237
+ getUser: {
238
+ args: { params: { userId: 'u_1' } },
239
+ },
240
+ updateUser: {
241
+ args: { params: { userId: 'u_1' } },
242
+ body: { name: 'Emery' }, // required for mutation leaves
243
+ },
244
+ })
245
+
246
+ // result keys map back to your aliases:
247
+ // { getUser: ..., updateUser: ... }
248
+ ```
249
+
250
+ The request body sent to `/v1/batch` is keyed by URL-encoded leaf keys:
251
+
252
+ ```ts
253
+ {
254
+ [encodeURIComponent('GET /v1/users/:userId')]: { params: { userId: 'u_1' } },
255
+ [encodeURIComponent('PATCH /v1/users/:userId')]: {
256
+ params: { userId: 'u_1' },
257
+ body: { name: 'Emery' },
258
+ },
259
+ }
260
+ ```
261
+
262
+ `buildBranch` also provides:
263
+
264
+ - `useEndpoint(input, options?)` for a single React Query hook over the batched response.
265
+ - `getQueryKeys(input?)` to derive per-alias keys.
266
+ - `invalidate(input?)` to invalidate each leaf alias exactly.
267
+ - `setData(input)` to write cache per alias.
268
+
269
+ ### 9) File uploads (FormData)
224
270
 
225
271
  If a leaf has `bodyFiles` set in its contract, the client automatically converts the body to `FormData`.
226
272
  For each declared file field name, pass files using `file${name}` in the input body.
@@ -251,7 +297,7 @@ await uploadAvatar.fetch(
251
297
  )
252
298
  ```
253
299
 
254
- ### 9) Debug logging
300
+ ### 10) Debug logging
255
301
 
256
302
  ```ts
257
303
  const client = createRouteClient({
package/dist/index.cjs CHANGED
@@ -44,6 +44,11 @@ __export(index_exports, {
44
44
  });
45
45
  module.exports = __toCommonJS(index_exports);
46
46
 
47
+ // src/routesV3.client.ts
48
+ var import_rrroutes_contract5 = require("@emeryld/rrroutes-contract");
49
+ var import_react_query4 = require("@tanstack/react-query");
50
+ var import_react4 = require("react");
51
+
47
52
  // src/routesV3.client.fetch.ts
48
53
  var HttpError = class extends Error {
49
54
  constructor({
@@ -1131,6 +1136,7 @@ function buildMutationLeaf(leaf, rqOpts, env) {
1131
1136
  }
1132
1137
 
1133
1138
  // src/routesV3.client.ts
1139
+ var BUILT_LEAF_META = "__rrroutesLeaf";
1134
1140
  var defaultDebugLogger = (event) => {
1135
1141
  if (typeof console === "undefined") return;
1136
1142
  const fn = console.debug ?? console.log;
@@ -1197,6 +1203,7 @@ function createRouteClient(opts) {
1197
1203
  const fetcher = opts.fetcher ?? defaultFetcher;
1198
1204
  const baseUrl = opts.baseUrl;
1199
1205
  const environment = opts.environment ?? void 0;
1206
+ const validateResponses = opts.validateResponses ?? true;
1200
1207
  const { emit: emitDebug, mode: debugMode } = createDebugEmitter(
1201
1208
  opts.debug,
1202
1209
  environment
@@ -1211,13 +1218,23 @@ function createRouteClient(opts) {
1211
1218
  await queryClient.invalidateQueries({ queryKey, exact });
1212
1219
  emitDebug({ type: "invalidate", key: queryKey, exact });
1213
1220
  }
1221
+ const toArgsTuple2 = (args) => typeof args === "undefined" ? [] : [args];
1222
+ const getBuiltLeaf = (built) => {
1223
+ const leaf = built[BUILT_LEAF_META];
1224
+ if (!leaf) {
1225
+ throw new Error(
1226
+ "buildBranch(...) expects endpoints created with this route client via client.build(...)."
1227
+ );
1228
+ }
1229
+ return leaf;
1230
+ };
1231
+ const encodeLeafKey = (leaf) => encodeURIComponent(`${leaf.method.toUpperCase()} ${leaf.path}`);
1214
1232
  function buildInternal(leaf, rqOpts, meta) {
1215
1233
  const leafLabel = `${leaf.method.toUpperCase()} ${String(leaf.path)}`;
1216
1234
  const debugName = meta?.name;
1217
1235
  const emit = (event) => emitDebug(event, debugName);
1218
1236
  const isGet = leaf.method === "get";
1219
1237
  const isFeed = !!leaf.cfg.feed;
1220
- const validateResponses = opts.validateResponses ?? true;
1221
1238
  const env = {
1222
1239
  baseUrl,
1223
1240
  validateResponses,
@@ -1228,25 +1245,33 @@ function createRouteClient(opts) {
1228
1245
  isVerboseDebug,
1229
1246
  leafLabel
1230
1247
  };
1248
+ let built;
1231
1249
  if (isGet && isFeed) {
1232
- return buildInfiniteGetLeaf(
1250
+ built = buildInfiniteGetLeaf(
1233
1251
  leaf,
1234
1252
  rqOpts,
1235
1253
  env
1236
1254
  );
1237
- }
1238
- if (isGet) {
1239
- return buildGetLeaf(
1255
+ } else if (isGet) {
1256
+ built = buildGetLeaf(
1257
+ leaf,
1258
+ rqOpts,
1259
+ env
1260
+ );
1261
+ } else {
1262
+ built = buildMutationLeaf(
1240
1263
  leaf,
1241
1264
  rqOpts,
1242
1265
  env
1243
1266
  );
1244
1267
  }
1245
- return buildMutationLeaf(
1246
- leaf,
1247
- rqOpts,
1248
- env
1249
- );
1268
+ Object.defineProperty(built, BUILT_LEAF_META, {
1269
+ value: leaf,
1270
+ enumerable: false,
1271
+ configurable: false,
1272
+ writable: false
1273
+ });
1274
+ return built;
1250
1275
  }
1251
1276
  const fetchRaw = async (input) => {
1252
1277
  const { path, method, query, body, params } = input;
@@ -1315,11 +1340,151 @@ function createRouteClient(opts) {
1315
1340
  throw error;
1316
1341
  }
1317
1342
  };
1343
+ const buildBranchInternal = (leaves, options) => {
1344
+ const batchMethod = options.method ?? "POST";
1345
+ const defaultHeaders = options.headers;
1346
+ const getQueryKeys = (input) => {
1347
+ const out = {};
1348
+ const argsByLeaf = input ?? {};
1349
+ for (const [alias, built] of Object.entries(leaves)) {
1350
+ const args = argsByLeaf[alias];
1351
+ out[alias] = built.getQueryKeys(...toArgsTuple2(args));
1352
+ }
1353
+ return out;
1354
+ };
1355
+ const invalidateBranch = async (input) => {
1356
+ const argsByLeaf = input ?? {};
1357
+ await Promise.all(
1358
+ Object.entries(leaves).map(([alias, built]) => {
1359
+ const args = argsByLeaf[alias];
1360
+ return built.invalidate(...toArgsTuple2(args));
1361
+ })
1362
+ );
1363
+ };
1364
+ const setDataBranch = (input) => {
1365
+ const out = {};
1366
+ for (const [alias, def] of Object.entries(input)) {
1367
+ if (!def) continue;
1368
+ const built = leaves[alias];
1369
+ if (!built) continue;
1370
+ out[alias] = built.setData(
1371
+ def.updater,
1372
+ ...toArgsTuple2(def.args)
1373
+ );
1374
+ }
1375
+ return out;
1376
+ };
1377
+ const fetchBranch = async (input) => {
1378
+ const payload = {};
1379
+ const keyByAlias = /* @__PURE__ */ new Map();
1380
+ for (const [aliasRaw, built] of Object.entries(leaves)) {
1381
+ const alias = aliasRaw;
1382
+ const leaf = getBuiltLeaf(built);
1383
+ const encodedLeaf = encodeLeafKey(leaf);
1384
+ keyByAlias.set(alias, encodedLeaf);
1385
+ const branchInput = input[alias];
1386
+ const args = branchInput?.args;
1387
+ const body = branchInput?.body;
1388
+ payload[encodedLeaf] = {
1389
+ ...args?.params !== void 0 ? { params: args.params } : {},
1390
+ ...args?.query !== void 0 ? { query: args.query } : {},
1391
+ ...body !== void 0 ? { body } : {}
1392
+ };
1393
+ }
1394
+ const batchResponse = await fetchRaw({
1395
+ path: options.path,
1396
+ method: batchMethod,
1397
+ body: payload,
1398
+ headers: defaultHeaders
1399
+ });
1400
+ const rawData = batchResponse.data;
1401
+ if (!rawData || typeof rawData !== "object" || Array.isArray(rawData)) {
1402
+ throw new Error(
1403
+ "Batch response must be a plain object keyed by encoded route keys."
1404
+ );
1405
+ }
1406
+ const mapped = {};
1407
+ for (const [aliasRaw, built] of Object.entries(leaves)) {
1408
+ const alias = aliasRaw;
1409
+ const encodedLeaf = keyByAlias.get(alias);
1410
+ if (!encodedLeaf) {
1411
+ throw new Error(
1412
+ `Internal batch error: missing encoded key for alias "${String(alias)}".`
1413
+ );
1414
+ }
1415
+ if (!(encodedLeaf in rawData)) {
1416
+ throw new Error(`Batch response missing key "${encodedLeaf}".`);
1417
+ }
1418
+ const leaf = getBuiltLeaf(built);
1419
+ const rawLeafData = rawData[encodedLeaf];
1420
+ const parsedLeafData = validateResponses && leaf.cfg.outputSchema ? (0, import_rrroutes_contract5.lowProfileParse)(leaf.cfg.outputSchema, rawLeafData) : rawLeafData;
1421
+ if (validateResponses && !leaf.cfg.outputSchema) {
1422
+ throw new Error(
1423
+ `No output schema defined for leaf ${leaf.method.toUpperCase()} ${leaf.path}, cannot validate batch response.`
1424
+ );
1425
+ }
1426
+ ;
1427
+ mapped[aliasRaw] = parsedLeafData;
1428
+ }
1429
+ return mapped;
1430
+ };
1431
+ const useEndpoint = (input, rqOpts) => {
1432
+ const queryKeys = getQueryKeys(input);
1433
+ const branchQueryKey = [
1434
+ "batch",
1435
+ String(batchMethod).toLowerCase(),
1436
+ options.path,
1437
+ queryKeys
1438
+ ];
1439
+ const { onReceive, ...useQueryOptions } = rqOpts ?? {};
1440
+ const listenersRef = (0, import_react4.useRef)(
1441
+ /* @__PURE__ */ new Set()
1442
+ );
1443
+ const notifyOnReceive = (0, import_react4.useCallback)(
1444
+ (data) => {
1445
+ onReceive?.(data);
1446
+ listenersRef.current.forEach((listener) => listener(data));
1447
+ },
1448
+ [onReceive]
1449
+ );
1450
+ const registerOnReceive = (0, import_react4.useCallback)(
1451
+ (listener) => {
1452
+ listenersRef.current.add(listener);
1453
+ return () => {
1454
+ listenersRef.current.delete(listener);
1455
+ };
1456
+ },
1457
+ []
1458
+ );
1459
+ const queryResult = (0, import_react_query4.useQuery)(
1460
+ {
1461
+ ...useQueryOptions,
1462
+ queryKey: branchQueryKey,
1463
+ placeholderData: useQueryOptions.placeholderData ?? import_react_query4.keepPreviousData,
1464
+ queryFn: async () => {
1465
+ const result = await fetchBranch(input);
1466
+ notifyOnReceive(result);
1467
+ return result;
1468
+ }
1469
+ },
1470
+ queryClient
1471
+ );
1472
+ return { ...queryResult, onReceive: registerOnReceive };
1473
+ };
1474
+ return {
1475
+ fetch: fetchBranch,
1476
+ useEndpoint,
1477
+ getQueryKeys,
1478
+ invalidate: invalidateBranch,
1479
+ setData: setDataBranch
1480
+ };
1481
+ };
1318
1482
  return {
1319
1483
  queryClient,
1320
1484
  invalidate,
1321
1485
  fetch: fetchRaw,
1322
- build: buildInternal
1486
+ build: buildInternal,
1487
+ buildBranch: buildBranchInternal
1323
1488
  };
1324
1489
  }
1325
1490
  function buildRouter(routeClient, routes) {
@@ -2438,7 +2603,7 @@ function SocketProvider(props) {
2438
2603
  }
2439
2604
 
2440
2605
  // src/sockets/socketedRoute/socket.client.helper.route.ts
2441
- var import_react4 = require("react");
2606
+ var import_react5 = require("react");
2442
2607
 
2443
2608
  // src/sockets/socketedRoute/socket.client.helper.debug.ts
2444
2609
  var objectReferenceIds = /* @__PURE__ */ new WeakMap();
@@ -2606,23 +2771,23 @@ function buildSocketedRoute(options) {
2606
2771
  const endpointResult = useInnerEndpoint(
2607
2772
  ...useArgs
2608
2773
  );
2609
- const argsKey = (0, import_react4.useMemo)(() => safeJsonKey(useArgs[0] ?? null), [useArgs]);
2610
- const [roomState, setRoomState] = (0, import_react4.useState)(
2774
+ const argsKey = (0, import_react5.useMemo)(() => safeJsonKey(useArgs[0] ?? null), [useArgs]);
2775
+ const [roomState, setRoomState] = (0, import_react5.useState)(
2611
2776
  () => roomsFromData(endpointResult.data, toRooms)
2612
2777
  );
2613
- const renderCountRef = (0, import_react4.useRef)(0);
2614
- const clientReadyRef = (0, import_react4.useRef)(null);
2615
- const onReceiveEffectDebugRef = (0, import_react4.useRef)(null);
2616
- const deriveRoomsEffectDebugRef = (0, import_react4.useRef)(null);
2617
- const joinRoomsEffectDebugRef = (0, import_react4.useRef)(null);
2618
- const applySocketEffectDebugRef = (0, import_react4.useRef)(null);
2778
+ const renderCountRef = (0, import_react5.useRef)(0);
2779
+ const clientReadyRef = (0, import_react5.useRef)(null);
2780
+ const onReceiveEffectDebugRef = (0, import_react5.useRef)(null);
2781
+ const deriveRoomsEffectDebugRef = (0, import_react5.useRef)(null);
2782
+ const joinRoomsEffectDebugRef = (0, import_react5.useRef)(null);
2783
+ const applySocketEffectDebugRef = (0, import_react5.useRef)(null);
2619
2784
  renderCountRef.current += 1;
2620
- const roomsKey = (0, import_react4.useMemo)(() => roomState.rooms.join("|"), [roomState.rooms]);
2621
- const joinMetaKey = (0, import_react4.useMemo)(
2785
+ const roomsKey = (0, import_react5.useMemo)(() => roomState.rooms.join("|"), [roomState.rooms]);
2786
+ const joinMetaKey = (0, import_react5.useMemo)(
2622
2787
  () => safeJsonKey(roomState.joinMeta ?? null),
2623
2788
  [roomState.joinMeta]
2624
2789
  );
2625
- const leaveMetaKey = (0, import_react4.useMemo)(
2790
+ const leaveMetaKey = (0, import_react5.useMemo)(
2626
2791
  () => safeJsonKey(roomState.leaveMeta ?? null),
2627
2792
  [roomState.leaveMeta]
2628
2793
  );
@@ -2645,7 +2810,7 @@ function buildSocketedRoute(options) {
2645
2810
  joinMetaKey,
2646
2811
  leaveMetaKey
2647
2812
  });
2648
- (0, import_react4.useEffect)(() => {
2813
+ (0, import_react5.useEffect)(() => {
2649
2814
  trackHookTrigger2({
2650
2815
  ref: onReceiveEffectDebugRef,
2651
2816
  phase: "endpoint_on_receive_effect",
@@ -2664,7 +2829,7 @@ function buildSocketedRoute(options) {
2664
2829
  });
2665
2830
  return unsubscribe;
2666
2831
  }, [endpointResult, toRooms, debug]);
2667
- (0, import_react4.useEffect)(() => {
2832
+ (0, import_react5.useEffect)(() => {
2668
2833
  trackHookTrigger2({
2669
2834
  ref: deriveRoomsEffectDebugRef,
2670
2835
  phase: "derive_rooms_effect",
@@ -2680,7 +2845,7 @@ function buildSocketedRoute(options) {
2680
2845
  );
2681
2846
  setRoomState((prev) => roomStateEqual(prev, next) ? prev : next);
2682
2847
  }, [endpointResult.data, toRooms, debug]);
2683
- (0, import_react4.useEffect)(() => {
2848
+ (0, import_react5.useEffect)(() => {
2684
2849
  trackHookTrigger2({
2685
2850
  ref: joinRoomsEffectDebugRef,
2686
2851
  phase: "join_rooms_effect",
@@ -2766,7 +2931,7 @@ function buildSocketedRoute(options) {
2766
2931
  });
2767
2932
  };
2768
2933
  }, [client, roomsKey, joinMetaKey, leaveMetaKey, debug]);
2769
- (0, import_react4.useEffect)(() => {
2934
+ (0, import_react5.useEffect)(() => {
2770
2935
  trackHookTrigger2({
2771
2936
  ref: applySocketEffectDebugRef,
2772
2937
  phase: "apply_socket_effect",