peerserver-client-react 1.0.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 ADDED
@@ -0,0 +1,75 @@
1
+ # peerserver-client-react
2
+
3
+ React hooks for [peerserver-client](https://www.npmjs.com/package/peerserver-client).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install peerserver-client peerserver-client-react
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ ```tsx
14
+ import { PeerProvider } from 'peerserver-client-react';
15
+
16
+ function App() {
17
+ return (
18
+ <PeerProvider config={{ url: 'wss://your-server.com/ws', alias: 'myuser' }}>
19
+ <Chat />
20
+ </PeerProvider>
21
+ );
22
+ }
23
+ ```
24
+
25
+ ## Hooks
26
+
27
+ ### `usePeerClient()`
28
+
29
+ ```tsx
30
+ const { connected, fingerprint, alias, error, reconnecting, broadcast, relay } = usePeerClient();
31
+ ```
32
+
33
+ ### `useNamespace(namespace, appType?, version?)`
34
+
35
+ ```tsx
36
+ const { peers, joined, discover } = useNamespace('chat', 'chat', '1.0');
37
+ // peers: PeerInfo[] — auto-updates on join/leave
38
+ ```
39
+
40
+ ### `usePeer(fingerprint)`
41
+
42
+ ```tsx
43
+ const { connected, messages, streams, send, addStream, close } = usePeer(targetFingerprint);
44
+ send({ msg: 'hello' });
45
+ ```
46
+
47
+ ### `useMatch(namespace)`
48
+
49
+ ```tsx
50
+ const { match, result, matching, connectToMatch } = useMatch('tictactoe');
51
+ await match({ skill: 'beginner' });
52
+ const peer = connectToMatch(); // connects to first matched peer
53
+ ```
54
+
55
+ ### `useBroadcast(namespace)`
56
+
57
+ ```tsx
58
+ const { messages, send } = useBroadcast('whiteboard');
59
+ send({ tool: 'pen', x: 100, y: 200 });
60
+ ```
61
+
62
+ ### `useRelay()`
63
+
64
+ ```tsx
65
+ const { messages, send } = useRelay();
66
+ send(targetFingerprint, { text: 'hello' });
67
+ ```
68
+
69
+ ### `useMediaStream()`
70
+
71
+ ```tsx
72
+ const { localStream, start, stop, attachToPeer, toggleAudio, toggleVideo } = useMediaStream();
73
+ await start({ video: true, audio: true });
74
+ attachToPeer(peer);
75
+ ```
@@ -0,0 +1,99 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import * as peerserver_client from 'peerserver-client';
4
+ import { ClientConfig, PeerClient, PeerInfo, DataChannelConfig, Peer, MatchResult } from 'peerserver-client';
5
+
6
+ interface PeerContextValue {
7
+ client: PeerClient | null;
8
+ connected: boolean;
9
+ fingerprint: string;
10
+ alias: string;
11
+ error: Error | null;
12
+ reconnecting: boolean;
13
+ }
14
+ declare function PeerProvider({ config, children, autoConnect, }: {
15
+ config: ClientConfig;
16
+ children: ReactNode;
17
+ autoConnect?: boolean;
18
+ }): react_jsx_runtime.JSX.Element;
19
+ declare function usePeerContext(): PeerContextValue;
20
+
21
+ declare function usePeerClient(): {
22
+ client: peerserver_client.PeerClient | null;
23
+ connected: boolean;
24
+ fingerprint: string;
25
+ alias: string;
26
+ error: Error | null;
27
+ reconnecting: boolean;
28
+ relay: (to: string, payload: any) => void | undefined;
29
+ broadcast: (namespace: string, data: any) => void | undefined;
30
+ updateMetadata: (meta: Record<string, any>) => void | undefined;
31
+ disconnect: () => void | undefined;
32
+ };
33
+
34
+ declare function useNamespace(namespace: string, appType?: string, version?: string): {
35
+ peers: PeerInfo[];
36
+ joined: boolean;
37
+ discover: (limit?: number) => Promise<PeerInfo[]>;
38
+ };
39
+
40
+ declare function usePeer(fingerprint: string | null, options?: {
41
+ alias?: string;
42
+ autoConnect?: boolean;
43
+ channelConfig?: DataChannelConfig;
44
+ }): {
45
+ peer: Peer | null;
46
+ connectionState: string;
47
+ connected: boolean;
48
+ messages: any[];
49
+ streams: MediaStream[];
50
+ send: (data: any, channel?: string) => void;
51
+ addStream: (stream: MediaStream) => void;
52
+ removeStream: (stream: MediaStream) => void;
53
+ close: () => void;
54
+ clearMessages: () => void;
55
+ };
56
+
57
+ declare function useMatch(namespace: string): {
58
+ match: (criteria?: Record<string, any>) => Promise<MatchResult | undefined>;
59
+ result: MatchResult | null;
60
+ matching: boolean;
61
+ error: Error | null;
62
+ connectToMatch: (index?: number) => peerserver_client.Peer | null;
63
+ };
64
+
65
+ interface BroadcastMessage {
66
+ from: string;
67
+ namespace: string;
68
+ data: any;
69
+ ts: number;
70
+ }
71
+ declare function useBroadcast(namespace: string, maxHistory?: number): {
72
+ messages: BroadcastMessage[];
73
+ send: (data: any) => void;
74
+ clear: () => void;
75
+ };
76
+
77
+ interface RelayMessage {
78
+ from: string;
79
+ data: any;
80
+ ts: number;
81
+ }
82
+ declare function useRelay(maxHistory?: number): {
83
+ messages: RelayMessage[];
84
+ send: (to: string, data: any) => void;
85
+ clear: () => void;
86
+ };
87
+
88
+ declare function useMediaStream(): {
89
+ localStream: MediaStream | null;
90
+ error: Error | null;
91
+ start: (constraints?: MediaStreamConstraints) => Promise<MediaStream | null>;
92
+ stop: () => void;
93
+ attachToPeer: (peer: Peer) => void;
94
+ detachFromPeer: (peer: Peer) => void;
95
+ toggleAudio: (enabled?: boolean) => void;
96
+ toggleVideo: (enabled?: boolean) => void;
97
+ };
98
+
99
+ export { PeerProvider, useBroadcast, useMatch, useMediaStream, useNamespace, usePeer, usePeerClient, usePeerContext, useRelay };
@@ -0,0 +1,99 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import * as peerserver_client from 'peerserver-client';
4
+ import { ClientConfig, PeerClient, PeerInfo, DataChannelConfig, Peer, MatchResult } from 'peerserver-client';
5
+
6
+ interface PeerContextValue {
7
+ client: PeerClient | null;
8
+ connected: boolean;
9
+ fingerprint: string;
10
+ alias: string;
11
+ error: Error | null;
12
+ reconnecting: boolean;
13
+ }
14
+ declare function PeerProvider({ config, children, autoConnect, }: {
15
+ config: ClientConfig;
16
+ children: ReactNode;
17
+ autoConnect?: boolean;
18
+ }): react_jsx_runtime.JSX.Element;
19
+ declare function usePeerContext(): PeerContextValue;
20
+
21
+ declare function usePeerClient(): {
22
+ client: peerserver_client.PeerClient | null;
23
+ connected: boolean;
24
+ fingerprint: string;
25
+ alias: string;
26
+ error: Error | null;
27
+ reconnecting: boolean;
28
+ relay: (to: string, payload: any) => void | undefined;
29
+ broadcast: (namespace: string, data: any) => void | undefined;
30
+ updateMetadata: (meta: Record<string, any>) => void | undefined;
31
+ disconnect: () => void | undefined;
32
+ };
33
+
34
+ declare function useNamespace(namespace: string, appType?: string, version?: string): {
35
+ peers: PeerInfo[];
36
+ joined: boolean;
37
+ discover: (limit?: number) => Promise<PeerInfo[]>;
38
+ };
39
+
40
+ declare function usePeer(fingerprint: string | null, options?: {
41
+ alias?: string;
42
+ autoConnect?: boolean;
43
+ channelConfig?: DataChannelConfig;
44
+ }): {
45
+ peer: Peer | null;
46
+ connectionState: string;
47
+ connected: boolean;
48
+ messages: any[];
49
+ streams: MediaStream[];
50
+ send: (data: any, channel?: string) => void;
51
+ addStream: (stream: MediaStream) => void;
52
+ removeStream: (stream: MediaStream) => void;
53
+ close: () => void;
54
+ clearMessages: () => void;
55
+ };
56
+
57
+ declare function useMatch(namespace: string): {
58
+ match: (criteria?: Record<string, any>) => Promise<MatchResult | undefined>;
59
+ result: MatchResult | null;
60
+ matching: boolean;
61
+ error: Error | null;
62
+ connectToMatch: (index?: number) => peerserver_client.Peer | null;
63
+ };
64
+
65
+ interface BroadcastMessage {
66
+ from: string;
67
+ namespace: string;
68
+ data: any;
69
+ ts: number;
70
+ }
71
+ declare function useBroadcast(namespace: string, maxHistory?: number): {
72
+ messages: BroadcastMessage[];
73
+ send: (data: any) => void;
74
+ clear: () => void;
75
+ };
76
+
77
+ interface RelayMessage {
78
+ from: string;
79
+ data: any;
80
+ ts: number;
81
+ }
82
+ declare function useRelay(maxHistory?: number): {
83
+ messages: RelayMessage[];
84
+ send: (to: string, data: any) => void;
85
+ clear: () => void;
86
+ };
87
+
88
+ declare function useMediaStream(): {
89
+ localStream: MediaStream | null;
90
+ error: Error | null;
91
+ start: (constraints?: MediaStreamConstraints) => Promise<MediaStream | null>;
92
+ stop: () => void;
93
+ attachToPeer: (peer: Peer) => void;
94
+ detachFromPeer: (peer: Peer) => void;
95
+ toggleAudio: (enabled?: boolean) => void;
96
+ toggleVideo: (enabled?: boolean) => void;
97
+ };
98
+
99
+ export { PeerProvider, useBroadcast, useMatch, useMediaStream, useNamespace, usePeer, usePeerClient, usePeerContext, useRelay };
package/dist/index.js ADDED
@@ -0,0 +1,398 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ PeerProvider: () => PeerProvider,
24
+ useBroadcast: () => useBroadcast,
25
+ useMatch: () => useMatch,
26
+ useMediaStream: () => useMediaStream,
27
+ useNamespace: () => useNamespace,
28
+ usePeer: () => usePeer,
29
+ usePeerClient: () => usePeerClient,
30
+ usePeerContext: () => usePeerContext,
31
+ useRelay: () => useRelay
32
+ });
33
+ module.exports = __toCommonJS(index_exports);
34
+
35
+ // src/provider.tsx
36
+ var import_react = require("react");
37
+ var import_peerserver_client = require("peerserver-client");
38
+ var import_jsx_runtime = require("react/jsx-runtime");
39
+ var PeerContext = (0, import_react.createContext)({
40
+ client: null,
41
+ connected: false,
42
+ fingerprint: "",
43
+ alias: "",
44
+ error: null,
45
+ reconnecting: false
46
+ });
47
+ function PeerProvider({
48
+ config,
49
+ children,
50
+ autoConnect = true
51
+ }) {
52
+ const clientRef = (0, import_react.useRef)(null);
53
+ const [connected, setConnected] = (0, import_react.useState)(false);
54
+ const [fingerprint, setFingerprint] = (0, import_react.useState)("");
55
+ const [alias, setAlias] = (0, import_react.useState)("");
56
+ const [error, setError] = (0, import_react.useState)(null);
57
+ const [reconnecting, setReconnecting] = (0, import_react.useState)(false);
58
+ (0, import_react.useEffect)(() => {
59
+ const client = new import_peerserver_client.PeerClient(config);
60
+ clientRef.current = client;
61
+ client.on("registered", (fp, a) => {
62
+ setFingerprint(fp);
63
+ setAlias(a);
64
+ setConnected(true);
65
+ setReconnecting(false);
66
+ setError(null);
67
+ });
68
+ client.on("disconnected", () => {
69
+ setConnected(false);
70
+ });
71
+ client.on("reconnecting", () => {
72
+ setReconnecting(true);
73
+ });
74
+ client.on("reconnected", () => {
75
+ setReconnecting(false);
76
+ setConnected(true);
77
+ });
78
+ client.on("error", (err) => {
79
+ setError(err instanceof Error ? err : new Error(String(err)));
80
+ });
81
+ if (autoConnect) {
82
+ client.connect().catch((e) => {
83
+ setError(e instanceof Error ? e : new Error(String(e)));
84
+ });
85
+ }
86
+ return () => {
87
+ client.disconnect();
88
+ clientRef.current = null;
89
+ };
90
+ }, [config.url]);
91
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
92
+ PeerContext.Provider,
93
+ {
94
+ value: {
95
+ client: clientRef.current,
96
+ connected,
97
+ fingerprint,
98
+ alias,
99
+ error,
100
+ reconnecting
101
+ },
102
+ children
103
+ }
104
+ );
105
+ }
106
+ function usePeerContext() {
107
+ return (0, import_react.useContext)(PeerContext);
108
+ }
109
+
110
+ // src/use-peer-client.ts
111
+ function usePeerClient() {
112
+ const ctx = usePeerContext();
113
+ return {
114
+ client: ctx.client,
115
+ connected: ctx.connected,
116
+ fingerprint: ctx.fingerprint,
117
+ alias: ctx.alias,
118
+ error: ctx.error,
119
+ reconnecting: ctx.reconnecting,
120
+ relay: (to, payload) => ctx.client?.relay(to, payload),
121
+ broadcast: (namespace, data) => ctx.client?.broadcast(namespace, data),
122
+ updateMetadata: (meta) => ctx.client?.updateMetadata(meta),
123
+ disconnect: () => ctx.client?.disconnect()
124
+ };
125
+ }
126
+
127
+ // src/use-namespace.ts
128
+ var import_react2 = require("react");
129
+ function useNamespace(namespace, appType, version) {
130
+ const { client, connected } = usePeerContext();
131
+ const [peers, setPeers] = (0, import_react2.useState)([]);
132
+ const [joined, setJoined] = (0, import_react2.useState)(false);
133
+ (0, import_react2.useEffect)(() => {
134
+ if (!client || !connected || !namespace) return;
135
+ client.join(namespace, appType, version).then((list) => {
136
+ setPeers(list);
137
+ setJoined(true);
138
+ });
139
+ const offJoined = client.on("peer_joined", (info) => {
140
+ setPeers((prev) => {
141
+ if (prev.some((p) => p.fingerprint === info.fingerprint)) return prev;
142
+ return [...prev, info];
143
+ });
144
+ });
145
+ const offLeft = client.on("peer_left", (fp) => {
146
+ setPeers((prev) => prev.filter((p) => p.fingerprint !== fp));
147
+ });
148
+ return () => {
149
+ offJoined();
150
+ offLeft();
151
+ client.leave(namespace);
152
+ setJoined(false);
153
+ setPeers([]);
154
+ };
155
+ }, [client, connected, namespace]);
156
+ const discover = (0, import_react2.useCallback)(
157
+ (limit) => client?.discover(namespace, limit) ?? Promise.resolve([]),
158
+ [client, namespace]
159
+ );
160
+ return { peers, joined, discover };
161
+ }
162
+
163
+ // src/use-peer.ts
164
+ var import_react3 = require("react");
165
+ function usePeer(fingerprint, options) {
166
+ const { client } = usePeerContext();
167
+ const [peer, setPeer] = (0, import_react3.useState)(null);
168
+ const [connectionState, setConnectionState] = (0, import_react3.useState)("new");
169
+ const [messages, setMessages] = (0, import_react3.useState)([]);
170
+ const [streams, setStreams] = (0, import_react3.useState)([]);
171
+ const maxMessages = (0, import_react3.useRef)(100);
172
+ (0, import_react3.useEffect)(() => {
173
+ if (!client || !fingerprint) return;
174
+ const autoConnect = options?.autoConnect ?? true;
175
+ let p;
176
+ const existing = client.getPeer(fingerprint);
177
+ if (existing) {
178
+ p = existing;
179
+ } else if (autoConnect) {
180
+ p = client.connectToPeer(fingerprint, options?.alias ?? "", options?.channelConfig);
181
+ } else {
182
+ return;
183
+ }
184
+ setPeer(p);
185
+ setConnectionState(p.connectionState);
186
+ const offConnected = p.on("connected", () => setConnectionState("connected"));
187
+ const offDisconnected = p.on("disconnected", (state) => setConnectionState(state));
188
+ const offData = p.on("data", (data, channel) => {
189
+ setMessages((prev) => {
190
+ const next = [...prev, { data, channel, ts: Date.now() }];
191
+ return next.length > maxMessages.current ? next.slice(-maxMessages.current) : next;
192
+ });
193
+ });
194
+ const offStream = p.on("stream", (stream) => {
195
+ setStreams((prev) => [...prev, stream]);
196
+ });
197
+ return () => {
198
+ offConnected();
199
+ offDisconnected();
200
+ offData();
201
+ offStream();
202
+ };
203
+ }, [client, fingerprint]);
204
+ const send = (0, import_react3.useCallback)(
205
+ (data, channel) => {
206
+ peer?.send(data, channel);
207
+ },
208
+ [peer]
209
+ );
210
+ const addStream = (0, import_react3.useCallback)(
211
+ (stream) => {
212
+ peer?.addStream(stream);
213
+ },
214
+ [peer]
215
+ );
216
+ const removeStream = (0, import_react3.useCallback)(
217
+ (stream) => {
218
+ peer?.removeStream(stream);
219
+ },
220
+ [peer]
221
+ );
222
+ const close = (0, import_react3.useCallback)(() => {
223
+ if (fingerprint) client?.closePeer(fingerprint);
224
+ setPeer(null);
225
+ setConnectionState("closed");
226
+ setMessages([]);
227
+ setStreams([]);
228
+ }, [client, fingerprint]);
229
+ const clearMessages = (0, import_react3.useCallback)(() => setMessages([]), []);
230
+ return {
231
+ peer,
232
+ connectionState,
233
+ connected: connectionState === "connected",
234
+ messages,
235
+ streams,
236
+ send,
237
+ addStream,
238
+ removeStream,
239
+ close,
240
+ clearMessages
241
+ };
242
+ }
243
+
244
+ // src/use-match.ts
245
+ var import_react4 = require("react");
246
+ function useMatch(namespace) {
247
+ const { client } = usePeerContext();
248
+ const [result, setResult] = (0, import_react4.useState)(null);
249
+ const [matching, setMatching] = (0, import_react4.useState)(false);
250
+ const [error, setError] = (0, import_react4.useState)(null);
251
+ const match = (0, import_react4.useCallback)(
252
+ async (criteria) => {
253
+ if (!client) return;
254
+ setMatching(true);
255
+ setError(null);
256
+ try {
257
+ const res = await client.match(namespace, criteria);
258
+ setResult(res);
259
+ return res;
260
+ } catch (e) {
261
+ setError(e instanceof Error ? e : new Error(String(e)));
262
+ } finally {
263
+ setMatching(false);
264
+ }
265
+ },
266
+ [client, namespace]
267
+ );
268
+ const connectToMatch = (0, import_react4.useCallback)(
269
+ (index = 0) => {
270
+ if (!client || !result) return null;
271
+ const target = result.peers[index];
272
+ if (!target) return null;
273
+ return client.connectToPeer(target.fingerprint, target.alias);
274
+ },
275
+ [client, result]
276
+ );
277
+ return { match, result, matching, error, connectToMatch };
278
+ }
279
+
280
+ // src/use-broadcast.ts
281
+ var import_react5 = require("react");
282
+ function useBroadcast(namespace, maxHistory = 100) {
283
+ const { client } = usePeerContext();
284
+ const [messages, setMessages] = (0, import_react5.useState)([]);
285
+ const maxRef = (0, import_react5.useRef)(maxHistory);
286
+ maxRef.current = maxHistory;
287
+ (0, import_react5.useEffect)(() => {
288
+ if (!client) return;
289
+ const off = client.on("broadcast", (from, ns, data) => {
290
+ if (ns !== namespace) return;
291
+ setMessages((prev) => {
292
+ const next = [...prev, { from, namespace: ns, data, ts: Date.now() }];
293
+ return next.length > maxRef.current ? next.slice(-maxRef.current) : next;
294
+ });
295
+ });
296
+ return off;
297
+ }, [client, namespace]);
298
+ const send = (0, import_react5.useCallback)(
299
+ (data) => {
300
+ client?.broadcast(namespace, data);
301
+ },
302
+ [client, namespace]
303
+ );
304
+ const clear = (0, import_react5.useCallback)(() => setMessages([]), []);
305
+ return { messages, send, clear };
306
+ }
307
+
308
+ // src/use-relay.ts
309
+ var import_react6 = require("react");
310
+ function useRelay(maxHistory = 100) {
311
+ const { client } = usePeerContext();
312
+ const [messages, setMessages] = (0, import_react6.useState)([]);
313
+ const maxRef = (0, import_react6.useRef)(maxHistory);
314
+ maxRef.current = maxHistory;
315
+ (0, import_react6.useEffect)(() => {
316
+ if (!client) return;
317
+ const off = client.on("relay", (from, data) => {
318
+ setMessages((prev) => {
319
+ const next = [...prev, { from, data, ts: Date.now() }];
320
+ return next.length > maxRef.current ? next.slice(-maxRef.current) : next;
321
+ });
322
+ });
323
+ return off;
324
+ }, [client]);
325
+ const send = (0, import_react6.useCallback)(
326
+ (to, data) => {
327
+ client?.relay(to, data);
328
+ },
329
+ [client]
330
+ );
331
+ const clear = (0, import_react6.useCallback)(() => setMessages([]), []);
332
+ return { messages, send, clear };
333
+ }
334
+
335
+ // src/use-media-stream.ts
336
+ var import_react7 = require("react");
337
+ function useMediaStream() {
338
+ const [localStream, setLocalStream] = (0, import_react7.useState)(null);
339
+ const [error, setError] = (0, import_react7.useState)(null);
340
+ const streamRef = (0, import_react7.useRef)(null);
341
+ const start = (0, import_react7.useCallback)(async (constraints) => {
342
+ try {
343
+ const stream = await navigator.mediaDevices.getUserMedia(
344
+ constraints ?? { audio: true, video: true }
345
+ );
346
+ streamRef.current = stream;
347
+ setLocalStream(stream);
348
+ setError(null);
349
+ return stream;
350
+ } catch (e) {
351
+ setError(e instanceof Error ? e : new Error(String(e)));
352
+ return null;
353
+ }
354
+ }, []);
355
+ const stop = (0, import_react7.useCallback)(() => {
356
+ streamRef.current?.getTracks().forEach((t) => t.stop());
357
+ streamRef.current = null;
358
+ setLocalStream(null);
359
+ }, []);
360
+ const attachToPeer = (0, import_react7.useCallback)((peer) => {
361
+ if (streamRef.current) peer.addStream(streamRef.current);
362
+ }, []);
363
+ const detachFromPeer = (0, import_react7.useCallback)((peer) => {
364
+ if (streamRef.current) peer.removeStream(streamRef.current);
365
+ }, []);
366
+ const toggleAudio = (0, import_react7.useCallback)((enabled) => {
367
+ streamRef.current?.getAudioTracks().forEach((t) => {
368
+ t.enabled = enabled ?? !t.enabled;
369
+ });
370
+ }, []);
371
+ const toggleVideo = (0, import_react7.useCallback)((enabled) => {
372
+ streamRef.current?.getVideoTracks().forEach((t) => {
373
+ t.enabled = enabled ?? !t.enabled;
374
+ });
375
+ }, []);
376
+ return {
377
+ localStream,
378
+ error,
379
+ start,
380
+ stop,
381
+ attachToPeer,
382
+ detachFromPeer,
383
+ toggleAudio,
384
+ toggleVideo
385
+ };
386
+ }
387
+ // Annotate the CommonJS export names for ESM import in node:
388
+ 0 && (module.exports = {
389
+ PeerProvider,
390
+ useBroadcast,
391
+ useMatch,
392
+ useMediaStream,
393
+ useNamespace,
394
+ usePeer,
395
+ usePeerClient,
396
+ usePeerContext,
397
+ useRelay
398
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,363 @@
1
+ // src/provider.tsx
2
+ import { createContext, useContext, useEffect, useRef, useState } from "react";
3
+ import { PeerClient } from "peerserver-client";
4
+ import { jsx } from "react/jsx-runtime";
5
+ var PeerContext = createContext({
6
+ client: null,
7
+ connected: false,
8
+ fingerprint: "",
9
+ alias: "",
10
+ error: null,
11
+ reconnecting: false
12
+ });
13
+ function PeerProvider({
14
+ config,
15
+ children,
16
+ autoConnect = true
17
+ }) {
18
+ const clientRef = useRef(null);
19
+ const [connected, setConnected] = useState(false);
20
+ const [fingerprint, setFingerprint] = useState("");
21
+ const [alias, setAlias] = useState("");
22
+ const [error, setError] = useState(null);
23
+ const [reconnecting, setReconnecting] = useState(false);
24
+ useEffect(() => {
25
+ const client = new PeerClient(config);
26
+ clientRef.current = client;
27
+ client.on("registered", (fp, a) => {
28
+ setFingerprint(fp);
29
+ setAlias(a);
30
+ setConnected(true);
31
+ setReconnecting(false);
32
+ setError(null);
33
+ });
34
+ client.on("disconnected", () => {
35
+ setConnected(false);
36
+ });
37
+ client.on("reconnecting", () => {
38
+ setReconnecting(true);
39
+ });
40
+ client.on("reconnected", () => {
41
+ setReconnecting(false);
42
+ setConnected(true);
43
+ });
44
+ client.on("error", (err) => {
45
+ setError(err instanceof Error ? err : new Error(String(err)));
46
+ });
47
+ if (autoConnect) {
48
+ client.connect().catch((e) => {
49
+ setError(e instanceof Error ? e : new Error(String(e)));
50
+ });
51
+ }
52
+ return () => {
53
+ client.disconnect();
54
+ clientRef.current = null;
55
+ };
56
+ }, [config.url]);
57
+ return /* @__PURE__ */ jsx(
58
+ PeerContext.Provider,
59
+ {
60
+ value: {
61
+ client: clientRef.current,
62
+ connected,
63
+ fingerprint,
64
+ alias,
65
+ error,
66
+ reconnecting
67
+ },
68
+ children
69
+ }
70
+ );
71
+ }
72
+ function usePeerContext() {
73
+ return useContext(PeerContext);
74
+ }
75
+
76
+ // src/use-peer-client.ts
77
+ function usePeerClient() {
78
+ const ctx = usePeerContext();
79
+ return {
80
+ client: ctx.client,
81
+ connected: ctx.connected,
82
+ fingerprint: ctx.fingerprint,
83
+ alias: ctx.alias,
84
+ error: ctx.error,
85
+ reconnecting: ctx.reconnecting,
86
+ relay: (to, payload) => ctx.client?.relay(to, payload),
87
+ broadcast: (namespace, data) => ctx.client?.broadcast(namespace, data),
88
+ updateMetadata: (meta) => ctx.client?.updateMetadata(meta),
89
+ disconnect: () => ctx.client?.disconnect()
90
+ };
91
+ }
92
+
93
+ // src/use-namespace.ts
94
+ import { useEffect as useEffect2, useState as useState2, useCallback } from "react";
95
+ function useNamespace(namespace, appType, version) {
96
+ const { client, connected } = usePeerContext();
97
+ const [peers, setPeers] = useState2([]);
98
+ const [joined, setJoined] = useState2(false);
99
+ useEffect2(() => {
100
+ if (!client || !connected || !namespace) return;
101
+ client.join(namespace, appType, version).then((list) => {
102
+ setPeers(list);
103
+ setJoined(true);
104
+ });
105
+ const offJoined = client.on("peer_joined", (info) => {
106
+ setPeers((prev) => {
107
+ if (prev.some((p) => p.fingerprint === info.fingerprint)) return prev;
108
+ return [...prev, info];
109
+ });
110
+ });
111
+ const offLeft = client.on("peer_left", (fp) => {
112
+ setPeers((prev) => prev.filter((p) => p.fingerprint !== fp));
113
+ });
114
+ return () => {
115
+ offJoined();
116
+ offLeft();
117
+ client.leave(namespace);
118
+ setJoined(false);
119
+ setPeers([]);
120
+ };
121
+ }, [client, connected, namespace]);
122
+ const discover = useCallback(
123
+ (limit) => client?.discover(namespace, limit) ?? Promise.resolve([]),
124
+ [client, namespace]
125
+ );
126
+ return { peers, joined, discover };
127
+ }
128
+
129
+ // src/use-peer.ts
130
+ import { useEffect as useEffect3, useState as useState3, useCallback as useCallback2, useRef as useRef2 } from "react";
131
+ function usePeer(fingerprint, options) {
132
+ const { client } = usePeerContext();
133
+ const [peer, setPeer] = useState3(null);
134
+ const [connectionState, setConnectionState] = useState3("new");
135
+ const [messages, setMessages] = useState3([]);
136
+ const [streams, setStreams] = useState3([]);
137
+ const maxMessages = useRef2(100);
138
+ useEffect3(() => {
139
+ if (!client || !fingerprint) return;
140
+ const autoConnect = options?.autoConnect ?? true;
141
+ let p;
142
+ const existing = client.getPeer(fingerprint);
143
+ if (existing) {
144
+ p = existing;
145
+ } else if (autoConnect) {
146
+ p = client.connectToPeer(fingerprint, options?.alias ?? "", options?.channelConfig);
147
+ } else {
148
+ return;
149
+ }
150
+ setPeer(p);
151
+ setConnectionState(p.connectionState);
152
+ const offConnected = p.on("connected", () => setConnectionState("connected"));
153
+ const offDisconnected = p.on("disconnected", (state) => setConnectionState(state));
154
+ const offData = p.on("data", (data, channel) => {
155
+ setMessages((prev) => {
156
+ const next = [...prev, { data, channel, ts: Date.now() }];
157
+ return next.length > maxMessages.current ? next.slice(-maxMessages.current) : next;
158
+ });
159
+ });
160
+ const offStream = p.on("stream", (stream) => {
161
+ setStreams((prev) => [...prev, stream]);
162
+ });
163
+ return () => {
164
+ offConnected();
165
+ offDisconnected();
166
+ offData();
167
+ offStream();
168
+ };
169
+ }, [client, fingerprint]);
170
+ const send = useCallback2(
171
+ (data, channel) => {
172
+ peer?.send(data, channel);
173
+ },
174
+ [peer]
175
+ );
176
+ const addStream = useCallback2(
177
+ (stream) => {
178
+ peer?.addStream(stream);
179
+ },
180
+ [peer]
181
+ );
182
+ const removeStream = useCallback2(
183
+ (stream) => {
184
+ peer?.removeStream(stream);
185
+ },
186
+ [peer]
187
+ );
188
+ const close = useCallback2(() => {
189
+ if (fingerprint) client?.closePeer(fingerprint);
190
+ setPeer(null);
191
+ setConnectionState("closed");
192
+ setMessages([]);
193
+ setStreams([]);
194
+ }, [client, fingerprint]);
195
+ const clearMessages = useCallback2(() => setMessages([]), []);
196
+ return {
197
+ peer,
198
+ connectionState,
199
+ connected: connectionState === "connected",
200
+ messages,
201
+ streams,
202
+ send,
203
+ addStream,
204
+ removeStream,
205
+ close,
206
+ clearMessages
207
+ };
208
+ }
209
+
210
+ // src/use-match.ts
211
+ import { useState as useState4, useCallback as useCallback3 } from "react";
212
+ function useMatch(namespace) {
213
+ const { client } = usePeerContext();
214
+ const [result, setResult] = useState4(null);
215
+ const [matching, setMatching] = useState4(false);
216
+ const [error, setError] = useState4(null);
217
+ const match = useCallback3(
218
+ async (criteria) => {
219
+ if (!client) return;
220
+ setMatching(true);
221
+ setError(null);
222
+ try {
223
+ const res = await client.match(namespace, criteria);
224
+ setResult(res);
225
+ return res;
226
+ } catch (e) {
227
+ setError(e instanceof Error ? e : new Error(String(e)));
228
+ } finally {
229
+ setMatching(false);
230
+ }
231
+ },
232
+ [client, namespace]
233
+ );
234
+ const connectToMatch = useCallback3(
235
+ (index = 0) => {
236
+ if (!client || !result) return null;
237
+ const target = result.peers[index];
238
+ if (!target) return null;
239
+ return client.connectToPeer(target.fingerprint, target.alias);
240
+ },
241
+ [client, result]
242
+ );
243
+ return { match, result, matching, error, connectToMatch };
244
+ }
245
+
246
+ // src/use-broadcast.ts
247
+ import { useEffect as useEffect4, useState as useState5, useCallback as useCallback4, useRef as useRef3 } from "react";
248
+ function useBroadcast(namespace, maxHistory = 100) {
249
+ const { client } = usePeerContext();
250
+ const [messages, setMessages] = useState5([]);
251
+ const maxRef = useRef3(maxHistory);
252
+ maxRef.current = maxHistory;
253
+ useEffect4(() => {
254
+ if (!client) return;
255
+ const off = client.on("broadcast", (from, ns, data) => {
256
+ if (ns !== namespace) return;
257
+ setMessages((prev) => {
258
+ const next = [...prev, { from, namespace: ns, data, ts: Date.now() }];
259
+ return next.length > maxRef.current ? next.slice(-maxRef.current) : next;
260
+ });
261
+ });
262
+ return off;
263
+ }, [client, namespace]);
264
+ const send = useCallback4(
265
+ (data) => {
266
+ client?.broadcast(namespace, data);
267
+ },
268
+ [client, namespace]
269
+ );
270
+ const clear = useCallback4(() => setMessages([]), []);
271
+ return { messages, send, clear };
272
+ }
273
+
274
+ // src/use-relay.ts
275
+ import { useEffect as useEffect5, useState as useState6, useCallback as useCallback5, useRef as useRef4 } from "react";
276
+ function useRelay(maxHistory = 100) {
277
+ const { client } = usePeerContext();
278
+ const [messages, setMessages] = useState6([]);
279
+ const maxRef = useRef4(maxHistory);
280
+ maxRef.current = maxHistory;
281
+ useEffect5(() => {
282
+ if (!client) return;
283
+ const off = client.on("relay", (from, data) => {
284
+ setMessages((prev) => {
285
+ const next = [...prev, { from, data, ts: Date.now() }];
286
+ return next.length > maxRef.current ? next.slice(-maxRef.current) : next;
287
+ });
288
+ });
289
+ return off;
290
+ }, [client]);
291
+ const send = useCallback5(
292
+ (to, data) => {
293
+ client?.relay(to, data);
294
+ },
295
+ [client]
296
+ );
297
+ const clear = useCallback5(() => setMessages([]), []);
298
+ return { messages, send, clear };
299
+ }
300
+
301
+ // src/use-media-stream.ts
302
+ import { useState as useState7, useCallback as useCallback6, useRef as useRef5 } from "react";
303
+ function useMediaStream() {
304
+ const [localStream, setLocalStream] = useState7(null);
305
+ const [error, setError] = useState7(null);
306
+ const streamRef = useRef5(null);
307
+ const start = useCallback6(async (constraints) => {
308
+ try {
309
+ const stream = await navigator.mediaDevices.getUserMedia(
310
+ constraints ?? { audio: true, video: true }
311
+ );
312
+ streamRef.current = stream;
313
+ setLocalStream(stream);
314
+ setError(null);
315
+ return stream;
316
+ } catch (e) {
317
+ setError(e instanceof Error ? e : new Error(String(e)));
318
+ return null;
319
+ }
320
+ }, []);
321
+ const stop = useCallback6(() => {
322
+ streamRef.current?.getTracks().forEach((t) => t.stop());
323
+ streamRef.current = null;
324
+ setLocalStream(null);
325
+ }, []);
326
+ const attachToPeer = useCallback6((peer) => {
327
+ if (streamRef.current) peer.addStream(streamRef.current);
328
+ }, []);
329
+ const detachFromPeer = useCallback6((peer) => {
330
+ if (streamRef.current) peer.removeStream(streamRef.current);
331
+ }, []);
332
+ const toggleAudio = useCallback6((enabled) => {
333
+ streamRef.current?.getAudioTracks().forEach((t) => {
334
+ t.enabled = enabled ?? !t.enabled;
335
+ });
336
+ }, []);
337
+ const toggleVideo = useCallback6((enabled) => {
338
+ streamRef.current?.getVideoTracks().forEach((t) => {
339
+ t.enabled = enabled ?? !t.enabled;
340
+ });
341
+ }, []);
342
+ return {
343
+ localStream,
344
+ error,
345
+ start,
346
+ stop,
347
+ attachToPeer,
348
+ detachFromPeer,
349
+ toggleAudio,
350
+ toggleVideo
351
+ };
352
+ }
353
+ export {
354
+ PeerProvider,
355
+ useBroadcast,
356
+ useMatch,
357
+ useMediaStream,
358
+ useNamespace,
359
+ usePeer,
360
+ usePeerClient,
361
+ usePeerContext,
362
+ useRelay
363
+ };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "peerserver-client-react",
3
+ "version": "1.0.0",
4
+ "description": "React hooks for peerserver-client",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.mjs",
15
+ "require": "./dist/index.js"
16
+ }
17
+ },
18
+ "peerDependencies": {
19
+ "react": ">=17.0.0",
20
+ "peerserver-client": ">=1.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/react": "^18.2.0",
24
+ "react": "^18.2.0",
25
+ "tsup": "^8.0.0",
26
+ "typescript": "^5.4.0"
27
+ },
28
+ "keywords": [
29
+ "webrtc",
30
+ "react",
31
+ "hooks",
32
+ "p2p",
33
+ "peerserver"
34
+ ],
35
+ "license": "MIT",
36
+ "scripts": {
37
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean --external react --external peerserver-client"
38
+ }
39
+ }