@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 +49 -3
- package/dist/index.cjs +192 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +185 -18
- package/dist/index.mjs.map +1 -1
- package/dist/routesV3.client.d.ts +1 -1
- package/dist/routesV3.client.types.d.ts +76 -0
- package/dist/sockets/socket.client.context.provider.d.ts +14 -0
- package/dist/sockets/socket.client.core.d.ts +1 -1
- package/package.json +1 -1
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)
|
|
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
|
-
###
|
|
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
|
-
|
|
1250
|
+
built = buildInfiniteGetLeaf(
|
|
1233
1251
|
leaf,
|
|
1234
1252
|
rqOpts,
|
|
1235
1253
|
env
|
|
1236
1254
|
);
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
-
|
|
1246
|
-
leaf,
|
|
1247
|
-
|
|
1248
|
-
|
|
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
|
|
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,
|
|
2610
|
-
const [roomState, setRoomState] = (0,
|
|
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,
|
|
2614
|
-
const clientReadyRef = (0,
|
|
2615
|
-
const onReceiveEffectDebugRef = (0,
|
|
2616
|
-
const deriveRoomsEffectDebugRef = (0,
|
|
2617
|
-
const joinRoomsEffectDebugRef = (0,
|
|
2618
|
-
const applySocketEffectDebugRef = (0,
|
|
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,
|
|
2621
|
-
const joinMetaKey = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
2934
|
+
(0, import_react5.useEffect)(() => {
|
|
2770
2935
|
trackHookTrigger2({
|
|
2771
2936
|
ref: applySocketEffectDebugRef,
|
|
2772
2937
|
phase: "apply_socket_effect",
|