@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 +62 -44
- package/dist/index.cjs +77 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +77 -29
- package/dist/index.mjs.map +1 -1
- package/dist/routesV3.client.fetch.d.ts +7 -3
- package/dist/routesV3.client.types.d.ts +1 -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
|
@@ -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(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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 =
|
|
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.
|
|
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:
|
|
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")
|
|
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")
|
|
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")
|
|
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 {
|
|
865
|
-
|
|
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, {
|
|
901
|
+
dbg(providerDebugRef.current, {
|
|
902
|
+
type: "resolve",
|
|
903
|
+
phase: "cancelled"
|
|
904
|
+
});
|
|
888
905
|
return;
|
|
889
906
|
}
|
|
890
907
|
if (!s) {
|
|
891
|
-
dbg(providerDebugRef.current, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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(
|
|
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)
|
|
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 {
|
|
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)
|
|
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)
|
|
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(
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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?.());
|