@pylonsync/react 0.3.189 → 0.3.192
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/package.json +3 -3
- package/src/hooks.ts +34 -39
- package/src/index.ts +83 -95
- package/src/useRoom.ts +42 -58
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.3.
|
|
6
|
+
"version": "0.3.192",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "src/index.ts",
|
|
9
9
|
"types": "src/index.ts",
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"check": "tsc -p tsconfig.json --noEmit"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@pylonsync/sdk": "0.3.
|
|
16
|
-
"@pylonsync/sync": "0.3.
|
|
15
|
+
"@pylonsync/sdk": "0.3.192",
|
|
16
|
+
"@pylonsync/sync": "0.3.192"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"react": ">=19.0.0"
|
package/src/hooks.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { SyncEngine, generateId, type Row } from "@pylonsync/sync";
|
|
3
|
+
import { SyncEngine, generateId, pylonFetch, type Row } from "@pylonsync/sync";
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
5
5
|
import { callFn, getBaseUrl, getReactStorage, storageKey } from "./index";
|
|
6
6
|
|
|
@@ -905,20 +905,17 @@ export function useAggregate<Row = Record<string, unknown>>(
|
|
|
905
905
|
setLoading(true);
|
|
906
906
|
setError(null);
|
|
907
907
|
try {
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
908
|
+
const json = await pylonFetch<{ rows?: Row[] }>(
|
|
909
|
+
{
|
|
910
|
+
baseUrl: getBaseUrl(),
|
|
911
|
+
getToken: () =>
|
|
912
|
+
(getReactStorage().get(storageKey("token")) ?? undefined) as
|
|
913
|
+
| string
|
|
914
|
+
| undefined,
|
|
915
915
|
},
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
if (!res.ok) {
|
|
920
|
-
throw new Error(json.error?.message || `HTTP ${res.status}`);
|
|
921
|
-
}
|
|
916
|
+
`/api/aggregate/${entity}`,
|
|
917
|
+
{ method: "POST", body: specKey },
|
|
918
|
+
);
|
|
922
919
|
setData(json.rows ?? []);
|
|
923
920
|
} catch (e) {
|
|
924
921
|
setError(e instanceof Error ? e : new Error(String(e)));
|
|
@@ -1051,36 +1048,34 @@ export function useSearch<T = Row>(
|
|
|
1051
1048
|
setLoading(true);
|
|
1052
1049
|
setError(null);
|
|
1053
1050
|
try {
|
|
1054
|
-
const
|
|
1055
|
-
const token = getReactStorage().get(storageKey("token"));
|
|
1056
|
-
const body = JSON.stringify({
|
|
1057
|
-
query: spec.query ?? "",
|
|
1058
|
-
filters: spec.filters ?? {},
|
|
1059
|
-
facets: spec.facets ?? [],
|
|
1060
|
-
sort: spec.sort,
|
|
1061
|
-
page: spec.page ?? 0,
|
|
1062
|
-
page_size: spec.pageSize ?? 20,
|
|
1063
|
-
});
|
|
1064
|
-
const res = await fetch(`${baseUrl}/api/search/${entity}`, {
|
|
1065
|
-
method: "POST",
|
|
1066
|
-
headers: {
|
|
1067
|
-
"Content-Type": "application/json",
|
|
1068
|
-
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
1069
|
-
},
|
|
1070
|
-
body,
|
|
1071
|
-
signal: controller.signal,
|
|
1072
|
-
});
|
|
1073
|
-
const json = (await res.json()) as {
|
|
1051
|
+
const json = await pylonFetch<{
|
|
1074
1052
|
hits?: T[];
|
|
1075
1053
|
facetCounts?: Record<string, Record<string, number>>;
|
|
1076
1054
|
total?: number;
|
|
1077
1055
|
tookMs?: number;
|
|
1078
|
-
|
|
1079
|
-
|
|
1056
|
+
}>(
|
|
1057
|
+
{
|
|
1058
|
+
baseUrl: getBaseUrl(),
|
|
1059
|
+
getToken: () =>
|
|
1060
|
+
(getReactStorage().get(storageKey("token")) ?? undefined) as
|
|
1061
|
+
| string
|
|
1062
|
+
| undefined,
|
|
1063
|
+
},
|
|
1064
|
+
`/api/search/${entity}`,
|
|
1065
|
+
{
|
|
1066
|
+
method: "POST",
|
|
1067
|
+
json: {
|
|
1068
|
+
query: spec.query ?? "",
|
|
1069
|
+
filters: spec.filters ?? {},
|
|
1070
|
+
facets: spec.facets ?? [],
|
|
1071
|
+
sort: spec.sort,
|
|
1072
|
+
page: spec.page ?? 0,
|
|
1073
|
+
page_size: spec.pageSize ?? 20,
|
|
1074
|
+
},
|
|
1075
|
+
signal: controller.signal,
|
|
1076
|
+
},
|
|
1077
|
+
);
|
|
1080
1078
|
if (myId !== requestIdRef.current) return; // stale — newer in flight
|
|
1081
|
-
if (!res.ok) {
|
|
1082
|
-
throw new Error(json.error?.message ?? `HTTP ${res.status}`);
|
|
1083
|
-
}
|
|
1084
1079
|
setHits(json.hits ?? []);
|
|
1085
1080
|
setFacetCounts(json.facetCounts ?? {});
|
|
1086
1081
|
setTotal(json.total ?? 0);
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export { defineRoute } from "@pylonsync/sdk";
|
|
2
2
|
export type { RouteMode, AppManifest } from "@pylonsync/sdk";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
defaultStorage,
|
|
6
|
+
pylonFetch,
|
|
7
|
+
pylonFetchRaw,
|
|
8
|
+
type Storage as PylonStorage,
|
|
9
|
+
} from "@pylonsync/sync";
|
|
5
10
|
|
|
6
11
|
// React hooks — high-level ergonomic shape
|
|
7
12
|
export {
|
|
@@ -192,30 +197,30 @@ function assertBaseUrlSafeForEnv(): void {
|
|
|
192
197
|
}
|
|
193
198
|
}
|
|
194
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Build the transport config for the React free helpers — base URL,
|
|
202
|
+
* token getter, and cookie credentials are all centralized in
|
|
203
|
+
* `pylonFetch`. Cookie-auth apps work because the transport always
|
|
204
|
+
* sets `credentials: "include"`; bearer-auth apps work because
|
|
205
|
+
* `getToken` returns the cached session token.
|
|
206
|
+
*/
|
|
207
|
+
function transportConfig(): import("@pylonsync/sync").TransportConfig {
|
|
208
|
+
return {
|
|
209
|
+
baseUrl: _baseUrl,
|
|
210
|
+
getToken: () => currentAuthToken() ?? undefined,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
195
214
|
async function apiRequest(
|
|
196
215
|
method: string,
|
|
197
216
|
path: string,
|
|
198
|
-
body?: unknown
|
|
217
|
+
body?: unknown,
|
|
199
218
|
): Promise<unknown> {
|
|
200
219
|
assertBaseUrlSafeForEnv();
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const headers: Record<string, string> = {};
|
|
205
|
-
if (body) headers["Content-Type"] = "application/json";
|
|
206
|
-
const token = currentAuthToken();
|
|
207
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
208
|
-
const res = await fetch(`${_baseUrl}${path}`, {
|
|
209
|
-
method,
|
|
210
|
-
headers,
|
|
211
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
220
|
+
return pylonFetch(transportConfig(), path, {
|
|
221
|
+
method: method as "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
|
|
222
|
+
json: body,
|
|
212
223
|
});
|
|
213
|
-
if (!res.ok) {
|
|
214
|
-
const err = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
215
|
-
const errorObj = err?.error as Record<string, unknown> | undefined;
|
|
216
|
-
throw new Error((errorObj?.message as string) ?? `HTTP ${res.status}`);
|
|
217
|
-
}
|
|
218
|
-
return res.json();
|
|
219
224
|
}
|
|
220
225
|
|
|
221
226
|
// ---------------------------------------------------------------------------
|
|
@@ -278,12 +283,10 @@ export async function createSession(
|
|
|
278
283
|
export async function getAuthContext(
|
|
279
284
|
token?: string
|
|
280
285
|
): Promise<{ user_id: string | null }> {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const res = await fetch(`${_baseUrl}/api/auth/me`, { headers });
|
|
286
|
-
return res.json() as Promise<{ user_id: string | null }>;
|
|
286
|
+
return pylonFetch<{ user_id: string | null }>(
|
|
287
|
+
{ baseUrl: _baseUrl, token },
|
|
288
|
+
"/api/auth/me",
|
|
289
|
+
);
|
|
287
290
|
}
|
|
288
291
|
|
|
289
292
|
/**
|
|
@@ -297,16 +300,15 @@ export async function getAuthContext(
|
|
|
297
300
|
export async function refreshSession(
|
|
298
301
|
token: string
|
|
299
302
|
): Promise<{ token: string; user_id: string; expires_at: number } | null> {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}>;
|
|
303
|
+
try {
|
|
304
|
+
return await pylonFetch<{
|
|
305
|
+
token: string;
|
|
306
|
+
user_id: string;
|
|
307
|
+
expires_at: number;
|
|
308
|
+
}>({ baseUrl: _baseUrl, token }, "/api/auth/refresh", { method: "POST" });
|
|
309
|
+
} catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
310
312
|
}
|
|
311
313
|
|
|
312
314
|
/**
|
|
@@ -405,20 +407,14 @@ export async function callFn<T = unknown>(
|
|
|
405
407
|
args: Record<string, unknown> = {},
|
|
406
408
|
options: { token?: string } = {}
|
|
407
409
|
): Promise<T> {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const json = (await res.json()) as unknown;
|
|
417
|
-
if (!res.ok) {
|
|
418
|
-
const err = (json as { error?: { code: string; message: string } }).error;
|
|
419
|
-
throw new Error(err?.message || `HTTP ${res.status}`);
|
|
420
|
-
}
|
|
421
|
-
return json as T;
|
|
410
|
+
return pylonFetch<T>(
|
|
411
|
+
{
|
|
412
|
+
baseUrl: _baseUrl,
|
|
413
|
+
getToken: () => options.token ?? currentAuthToken() ?? undefined,
|
|
414
|
+
},
|
|
415
|
+
`/api/fn/${name}`,
|
|
416
|
+
{ method: "POST", json: args },
|
|
417
|
+
);
|
|
422
418
|
}
|
|
423
419
|
|
|
424
420
|
/**
|
|
@@ -436,17 +432,21 @@ export async function* streamFn(
|
|
|
436
432
|
args: Record<string, unknown> = {},
|
|
437
433
|
options: { token?: string } = {}
|
|
438
434
|
): AsyncGenerator<string, unknown, unknown> {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
435
|
+
// Streaming response — use pylonFetchRaw so we can read .body
|
|
436
|
+
// ourselves. URL + auth + credentials are centralized in the
|
|
437
|
+
// transport.
|
|
438
|
+
const res = await pylonFetchRaw(
|
|
439
|
+
{
|
|
440
|
+
baseUrl: _baseUrl,
|
|
441
|
+
getToken: () => options.token ?? currentAuthToken() ?? undefined,
|
|
442
|
+
},
|
|
443
|
+
`/api/fn/${name}`,
|
|
444
|
+
{
|
|
445
|
+
method: "POST",
|
|
446
|
+
json: args,
|
|
447
|
+
accept: "text/event-stream",
|
|
448
|
+
},
|
|
449
|
+
);
|
|
450
450
|
if (!res.ok || !res.body) {
|
|
451
451
|
throw new Error(`Stream failed: HTTP ${res.status}`);
|
|
452
452
|
}
|
|
@@ -559,26 +559,21 @@ export async function uploadFile(
|
|
|
559
559
|
filename ??= "upload";
|
|
560
560
|
contentType ??= "application/octet-stream";
|
|
561
561
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
};
|
|
578
|
-
throw new Error(err.error?.message || `Upload failed: HTTP ${res.status}`);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
return (await res.json()) as UploadedFile;
|
|
562
|
+
return pylonFetch<UploadedFile>(
|
|
563
|
+
{
|
|
564
|
+
baseUrl: _baseUrl,
|
|
565
|
+
getToken: () => options.token ?? currentAuthToken() ?? undefined,
|
|
566
|
+
},
|
|
567
|
+
"/api/files/upload",
|
|
568
|
+
{
|
|
569
|
+
method: "POST",
|
|
570
|
+
body,
|
|
571
|
+
headers: {
|
|
572
|
+
"Content-Type": contentType,
|
|
573
|
+
"X-Filename": filename,
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
);
|
|
582
577
|
}
|
|
583
578
|
|
|
584
579
|
/**
|
|
@@ -597,19 +592,12 @@ export async function uploadFileMultipart(
|
|
|
597
592
|
}
|
|
598
593
|
form.append("file", file);
|
|
599
594
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
body: form,
|
|
607
|
-
|
|
608
|
-
if (!res.ok) {
|
|
609
|
-
const err = (await res.json().catch(() => ({}))) as {
|
|
610
|
-
error?: { code: string; message: string };
|
|
611
|
-
};
|
|
612
|
-
throw new Error(err.error?.message || `Upload failed: HTTP ${res.status}`);
|
|
613
|
-
}
|
|
614
|
-
return (await res.json()) as UploadedFile;
|
|
595
|
+
return pylonFetch<UploadedFile>(
|
|
596
|
+
{
|
|
597
|
+
baseUrl: _baseUrl,
|
|
598
|
+
getToken: () => options.token ?? currentAuthToken() ?? undefined,
|
|
599
|
+
},
|
|
600
|
+
"/api/files/upload",
|
|
601
|
+
{ method: "POST", body: form },
|
|
602
|
+
);
|
|
615
603
|
}
|
package/src/useRoom.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
+
import { pylonFetch } from '@pylonsync/sync';
|
|
4
5
|
import { getBaseUrl, getReactStorage, storageKey } from './index';
|
|
5
6
|
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
@@ -96,12 +97,13 @@ export function useRoom(
|
|
|
96
97
|
const presenceRef = useRef(initialPresence);
|
|
97
98
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
98
99
|
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
// Transport config — shared across every room API call. The
|
|
101
|
+
// `pylonFetch` helper owns auth + credentials + JSON handling so
|
|
102
|
+
// every site here drops the duplicate header builder.
|
|
103
|
+
const transport = useCallback(
|
|
104
|
+
() => ({ baseUrl, token }),
|
|
105
|
+
[baseUrl, token],
|
|
106
|
+
);
|
|
105
107
|
|
|
106
108
|
// ------- lifecycle: join / heartbeat / leave -------
|
|
107
109
|
//
|
|
@@ -118,34 +120,25 @@ export function useRoom(
|
|
|
118
120
|
|
|
119
121
|
const join = async () => {
|
|
120
122
|
try {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
user_id: userId,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
});
|
|
130
|
-
const body = await res.json();
|
|
123
|
+
const body = await pylonFetch<{ snapshot?: { peers?: RoomPeer[] } }>(
|
|
124
|
+
transport(),
|
|
125
|
+
'/api/rooms/join',
|
|
126
|
+
{
|
|
127
|
+
method: 'POST',
|
|
128
|
+
json: { room: roomId, user_id: userId, data: presenceRef.current },
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
131
|
if (!mounted) return;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
(body.snapshot.peers as RoomPeer[]).filter(
|
|
140
|
-
(p) => p.user_id !== userId,
|
|
141
|
-
),
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
} else {
|
|
145
|
-
setError(body.error?.message || 'Failed to join room');
|
|
132
|
+
joined = true;
|
|
133
|
+
setIsConnected(true);
|
|
134
|
+
setError(null);
|
|
135
|
+
if (body.snapshot?.peers) {
|
|
136
|
+
setPeers(
|
|
137
|
+
body.snapshot.peers.filter((p) => p.user_id !== userId),
|
|
138
|
+
);
|
|
146
139
|
}
|
|
147
140
|
} catch (e: any) {
|
|
148
|
-
if (mounted) setError(e
|
|
141
|
+
if (mounted) setError(e?.message ?? 'Failed to join room');
|
|
149
142
|
}
|
|
150
143
|
};
|
|
151
144
|
|
|
@@ -155,19 +148,14 @@ export function useRoom(
|
|
|
155
148
|
intervalRef.current = setInterval(async () => {
|
|
156
149
|
if (!mounted) return;
|
|
157
150
|
try {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
{
|
|
151
|
+
const body = await pylonFetch<{ members?: RoomPeer[] }>(
|
|
152
|
+
transport(),
|
|
153
|
+
`/api/rooms/${encodeURIComponent(roomId)}`,
|
|
161
154
|
);
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
((body.members ?? []) as RoomPeer[]).filter(
|
|
167
|
-
(p) => p.user_id !== userId,
|
|
168
|
-
),
|
|
169
|
-
);
|
|
170
|
-
}
|
|
155
|
+
if (mounted) {
|
|
156
|
+
setPeers(
|
|
157
|
+
(body.members ?? []).filter((p) => p.user_id !== userId),
|
|
158
|
+
);
|
|
171
159
|
}
|
|
172
160
|
} catch {
|
|
173
161
|
// Swallow -- next heartbeat will retry.
|
|
@@ -183,10 +171,9 @@ export function useRoom(
|
|
|
183
171
|
// not in this room" errors. Server leave is also idempotent so
|
|
184
172
|
// a stray duplicate would 200 anyway, but we save the round trip.
|
|
185
173
|
if (joined) {
|
|
186
|
-
|
|
174
|
+
pylonFetch(transport(), '/api/rooms/leave', {
|
|
187
175
|
method: 'POST',
|
|
188
|
-
|
|
189
|
-
body: JSON.stringify({ room: roomId, user_id: userId }),
|
|
176
|
+
json: { room: roomId, user_id: userId },
|
|
190
177
|
}).catch(() => {});
|
|
191
178
|
}
|
|
192
179
|
};
|
|
@@ -199,35 +186,32 @@ export function useRoom(
|
|
|
199
186
|
const setPresence = useCallback(
|
|
200
187
|
(data: Record<string, any>) => {
|
|
201
188
|
presenceRef.current = data;
|
|
202
|
-
|
|
189
|
+
pylonFetch(transport(), '/api/rooms/presence', {
|
|
203
190
|
method: 'POST',
|
|
204
|
-
|
|
205
|
-
body: JSON.stringify({ room: roomId, user_id: userId, data }),
|
|
191
|
+
json: { room: roomId, user_id: userId, data },
|
|
206
192
|
}).catch(() => {});
|
|
207
193
|
},
|
|
208
|
-
[roomId, userId,
|
|
194
|
+
[roomId, userId, transport],
|
|
209
195
|
);
|
|
210
196
|
|
|
211
197
|
const broadcast = useCallback(
|
|
212
198
|
(topic: string, data: any) => {
|
|
213
|
-
|
|
199
|
+
pylonFetch(transport(), '/api/rooms/broadcast', {
|
|
214
200
|
method: 'POST',
|
|
215
|
-
|
|
216
|
-
body: JSON.stringify({ room: roomId, user_id: userId, topic, data }),
|
|
201
|
+
json: { room: roomId, user_id: userId, topic, data },
|
|
217
202
|
}).catch(() => {});
|
|
218
203
|
},
|
|
219
|
-
[roomId, userId,
|
|
204
|
+
[roomId, userId, transport],
|
|
220
205
|
);
|
|
221
206
|
|
|
222
207
|
const leave = useCallback(() => {
|
|
223
|
-
|
|
208
|
+
pylonFetch(transport(), '/api/rooms/leave', {
|
|
224
209
|
method: 'POST',
|
|
225
|
-
|
|
226
|
-
body: JSON.stringify({ room: roomId, user_id: userId }),
|
|
210
|
+
json: { room: roomId, user_id: userId },
|
|
227
211
|
}).catch(() => {});
|
|
228
212
|
setIsConnected(false);
|
|
229
213
|
setPeers([]);
|
|
230
|
-
}, [roomId, userId,
|
|
214
|
+
}, [roomId, userId, transport]);
|
|
231
215
|
|
|
232
216
|
return { peers, isConnected, setPresence, broadcast, leave, error };
|
|
233
217
|
}
|