peer-client 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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1086 -0
  3. package/dist/core/client.d.ts +41 -0
  4. package/dist/core/client.js +361 -0
  5. package/dist/core/emitter.d.ts +11 -0
  6. package/dist/core/emitter.js +46 -0
  7. package/dist/core/identity.d.ts +15 -0
  8. package/dist/core/identity.js +54 -0
  9. package/dist/core/index.d.ts +6 -0
  10. package/dist/core/index.js +6 -0
  11. package/dist/core/peer.d.ts +29 -0
  12. package/dist/core/peer.js +234 -0
  13. package/dist/core/transport.d.ts +35 -0
  14. package/dist/core/transport.js +174 -0
  15. package/dist/core/types.d.ts +173 -0
  16. package/dist/core/types.js +28 -0
  17. package/dist/crypto.d.ts +32 -0
  18. package/dist/crypto.js +168 -0
  19. package/dist/index.d.ts +11 -0
  20. package/dist/index.js +11 -0
  21. package/dist/media.d.ts +63 -0
  22. package/dist/media.js +275 -0
  23. package/dist/react/Audio.d.ts +6 -0
  24. package/dist/react/Audio.js +18 -0
  25. package/dist/react/PeerProvider.d.ts +17 -0
  26. package/dist/react/PeerProvider.js +46 -0
  27. package/dist/react/PeerStatus.d.ts +7 -0
  28. package/dist/react/PeerStatus.js +20 -0
  29. package/dist/react/TransferProgress.d.ts +10 -0
  30. package/dist/react/TransferProgress.js +17 -0
  31. package/dist/react/Video.d.ts +7 -0
  32. package/dist/react/Video.js +18 -0
  33. package/dist/react/index.d.ts +19 -0
  34. package/dist/react/index.js +18 -0
  35. package/dist/react/useBroadcast.d.ts +12 -0
  36. package/dist/react/useBroadcast.js +21 -0
  37. package/dist/react/useCRDT.d.ts +8 -0
  38. package/dist/react/useCRDT.js +37 -0
  39. package/dist/react/useE2E.d.ts +11 -0
  40. package/dist/react/useE2E.js +62 -0
  41. package/dist/react/useFileTransfer.d.ts +24 -0
  42. package/dist/react/useFileTransfer.js +133 -0
  43. package/dist/react/useIdentity.d.ts +9 -0
  44. package/dist/react/useIdentity.js +63 -0
  45. package/dist/react/useMatch.d.ts +11 -0
  46. package/dist/react/useMatch.js +33 -0
  47. package/dist/react/useMedia.d.ts +13 -0
  48. package/dist/react/useMedia.js +89 -0
  49. package/dist/react/useNamespace.d.ts +7 -0
  50. package/dist/react/useNamespace.js +38 -0
  51. package/dist/react/usePeer.d.ts +6 -0
  52. package/dist/react/usePeer.js +49 -0
  53. package/dist/react/usePeerClient.d.ts +7 -0
  54. package/dist/react/usePeerClient.js +5 -0
  55. package/dist/react/useRelay.d.ts +11 -0
  56. package/dist/react/useRelay.js +19 -0
  57. package/dist/react/useRoom.d.ts +17 -0
  58. package/dist/react/useRoom.js +67 -0
  59. package/dist/react/useSync.d.ts +8 -0
  60. package/dist/react/useSync.js +45 -0
  61. package/dist/room.d.ts +44 -0
  62. package/dist/room.js +246 -0
  63. package/dist/sync.d.ts +45 -0
  64. package/dist/sync.js +333 -0
  65. package/dist/transfer.d.ts +49 -0
  66. package/dist/transfer.js +454 -0
  67. package/package.json +76 -0
@@ -0,0 +1,7 @@
1
+ import { type VideoHTMLAttributes } from 'react';
2
+ interface VideoProps extends Omit<VideoHTMLAttributes<HTMLVideoElement>, 'ref'> {
3
+ stream: MediaStream | null;
4
+ muted?: boolean;
5
+ }
6
+ export declare function Video({ stream, muted, ...props }: VideoProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ export function Video({ stream, muted = false, ...props }) {
4
+ const ref = useRef(null);
5
+ useEffect(() => {
6
+ if (!ref.current)
7
+ return;
8
+ ref.current.srcObject = stream;
9
+ if (stream) {
10
+ ref.current.play().catch(() => { });
11
+ }
12
+ return () => {
13
+ if (ref.current)
14
+ ref.current.srcObject = null;
15
+ };
16
+ }, [stream]);
17
+ return _jsx("video", { ref: ref, autoPlay: true, playsInline: true, muted: muted, ...props });
18
+ }
@@ -0,0 +1,19 @@
1
+ export { PeerProvider, usePeerContext } from './PeerProvider';
2
+ export { usePeerClient } from './usePeerClient';
3
+ export { usePeer } from './usePeer';
4
+ export { useRoom } from './useRoom';
5
+ export { useMedia } from './useMedia';
6
+ export { useFileTransfer } from './useFileTransfer';
7
+ export type { Transfer } from './useFileTransfer';
8
+ export { useSync } from './useSync';
9
+ export { useE2E } from './useE2E';
10
+ export { useNamespace } from './useNamespace';
11
+ export { useRelay } from './useRelay';
12
+ export { useBroadcast } from './useBroadcast';
13
+ export { useMatch } from './useMatch';
14
+ export { useIdentity } from './useIdentity';
15
+ export { useCRDT } from './useCRDT';
16
+ export { Video } from './Video';
17
+ export { Audio } from './Audio';
18
+ export { TransferProgress } from './TransferProgress';
19
+ export { PeerStatus } from './PeerStatus';
@@ -0,0 +1,18 @@
1
+ export { PeerProvider, usePeerContext } from './PeerProvider';
2
+ export { usePeerClient } from './usePeerClient';
3
+ export { usePeer } from './usePeer';
4
+ export { useRoom } from './useRoom';
5
+ export { useMedia } from './useMedia';
6
+ export { useFileTransfer } from './useFileTransfer';
7
+ export { useSync } from './useSync';
8
+ export { useE2E } from './useE2E';
9
+ export { useNamespace } from './useNamespace';
10
+ export { useRelay } from './useRelay';
11
+ export { useBroadcast } from './useBroadcast';
12
+ export { useMatch } from './useMatch';
13
+ export { useIdentity } from './useIdentity';
14
+ export { useCRDT } from './useCRDT';
15
+ export { Video } from './Video';
16
+ export { Audio } from './Audio';
17
+ export { TransferProgress } from './TransferProgress';
18
+ export { PeerStatus } from './PeerStatus';
@@ -0,0 +1,12 @@
1
+ interface BroadcastMessage {
2
+ from: string;
3
+ namespace: string;
4
+ payload: any;
5
+ ts: number;
6
+ }
7
+ export declare function useBroadcast(namespace: string): {
8
+ messages: BroadcastMessage[];
9
+ send: (payload: any) => void;
10
+ clear: () => void;
11
+ };
12
+ export {};
@@ -0,0 +1,21 @@
1
+ import { useEffect, useState, useCallback } from 'react';
2
+ import { usePeerContext } from './PeerProvider';
3
+ export function useBroadcast(namespace) {
4
+ const { client } = usePeerContext();
5
+ const [messages, setMessages] = useState([]);
6
+ useEffect(() => {
7
+ if (!client || !namespace)
8
+ return;
9
+ const off = client.on('broadcast', (from, ns, payload) => {
10
+ if (ns === namespace) {
11
+ setMessages((prev) => [...prev, { from, namespace: ns, payload, ts: Date.now() }]);
12
+ }
13
+ });
14
+ return () => { off(); };
15
+ }, [client, namespace]);
16
+ const send = useCallback((payload) => {
17
+ client?.broadcast(namespace, payload);
18
+ }, [client, namespace]);
19
+ const clear = useCallback(() => setMessages([]), []);
20
+ return { messages, send, clear };
21
+ }
@@ -0,0 +1,8 @@
1
+ export declare function useCRDT(roomId: string, Y: any): {
2
+ ready: boolean;
3
+ getDoc: () => any;
4
+ getMap: (name: string) => any;
5
+ getText: (name: string) => any;
6
+ getArray: (name: string) => any;
7
+ error: Error | null;
8
+ };
@@ -0,0 +1,37 @@
1
+ import { useEffect, useState, useCallback, useRef } from 'react';
2
+ import { usePeerContext } from './PeerProvider';
3
+ import { CRDTSync } from '../sync';
4
+ export function useCRDT(roomId, Y) {
5
+ const { client } = usePeerContext();
6
+ const [ready, setReady] = useState(false);
7
+ const [error, setError] = useState(null);
8
+ const crdtRef = useRef(null);
9
+ const yRef = useRef(Y);
10
+ yRef.current = Y;
11
+ useEffect(() => {
12
+ if (!client || !roomId || !yRef.current)
13
+ return;
14
+ const crdt = new CRDTSync(client, roomId, yRef.current);
15
+ crdtRef.current = crdt;
16
+ crdt.start();
17
+ setReady(true);
18
+ return () => {
19
+ crdt.destroy();
20
+ crdtRef.current = null;
21
+ setReady(false);
22
+ };
23
+ }, [client, roomId]);
24
+ const getDoc = useCallback(() => {
25
+ return crdtRef.current?.getDoc() ?? null;
26
+ }, []);
27
+ const getMap = useCallback((name) => {
28
+ return crdtRef.current?.getMap(name) ?? null;
29
+ }, []);
30
+ const getText = useCallback((name) => {
31
+ return crdtRef.current?.getText(name) ?? null;
32
+ }, []);
33
+ const getArray = useCallback((name) => {
34
+ return crdtRef.current?.getArray(name) ?? null;
35
+ }, []);
36
+ return { ready, getDoc, getMap, getText, getArray, error };
37
+ }
@@ -0,0 +1,11 @@
1
+ import type { Peer } from '../core/peer';
2
+ export declare function useE2E(): {
3
+ ready: boolean;
4
+ exchangedPeers: Set<string>;
5
+ exchange: (peer: Peer) => Promise<void>;
6
+ handleIncoming: (peer: Peer, data: any) => Promise<void>;
7
+ encrypt: (fingerprint: string, data: any) => Promise<string>;
8
+ decrypt: (fingerprint: string, data: string) => Promise<any>;
9
+ hasKey: (fingerprint: string) => boolean;
10
+ error: Error | null;
11
+ };
@@ -0,0 +1,62 @@
1
+ import { useEffect, useState, useCallback, useRef } from 'react';
2
+ import { usePeerContext } from './PeerProvider';
3
+ import { GroupKeyManager } from '../crypto';
4
+ export function useE2E() {
5
+ const { client } = usePeerContext();
6
+ const [ready, setReady] = useState(false);
7
+ const [exchangedPeers, setExchangedPeers] = useState(new Set());
8
+ const [error, setError] = useState(null);
9
+ const kmRef = useRef(null);
10
+ useEffect(() => {
11
+ if (!client)
12
+ return;
13
+ const km = new GroupKeyManager(client);
14
+ kmRef.current = km;
15
+ km.init()
16
+ .then(() => setReady(true))
17
+ .catch((e) => setError(e instanceof Error ? e : new Error(String(e))));
18
+ return () => {
19
+ km.destroy();
20
+ kmRef.current = null;
21
+ setReady(false);
22
+ setExchangedPeers(new Set());
23
+ };
24
+ }, [client]);
25
+ const exchange = useCallback(async (peer) => {
26
+ if (!kmRef.current)
27
+ throw new Error('E2E not initialized');
28
+ await kmRef.current.exchangeWith(peer);
29
+ setExchangedPeers((prev) => new Set(prev).add(peer.fingerprint));
30
+ }, []);
31
+ const handleIncoming = useCallback(async (peer, data) => {
32
+ if (!kmRef.current)
33
+ return;
34
+ await kmRef.current.handleIncomingKeyExchange(peer, data);
35
+ if (kmRef.current.getE2E().hasKey(peer.fingerprint)) {
36
+ setExchangedPeers((prev) => new Set(prev).add(peer.fingerprint));
37
+ }
38
+ }, []);
39
+ const encrypt = useCallback(async (fingerprint, data) => {
40
+ if (!kmRef.current)
41
+ throw new Error('E2E not initialized');
42
+ return kmRef.current.encryptForPeer(fingerprint, data);
43
+ }, []);
44
+ const decrypt = useCallback(async (fingerprint, data) => {
45
+ if (!kmRef.current)
46
+ throw new Error('E2E not initialized');
47
+ return kmRef.current.decryptFromPeer(fingerprint, data);
48
+ }, []);
49
+ const hasKey = useCallback((fingerprint) => {
50
+ return kmRef.current?.getE2E().hasKey(fingerprint) ?? false;
51
+ }, []);
52
+ return {
53
+ ready,
54
+ exchangedPeers,
55
+ exchange,
56
+ handleIncoming,
57
+ encrypt,
58
+ decrypt,
59
+ hasKey,
60
+ error,
61
+ };
62
+ }
@@ -0,0 +1,24 @@
1
+ import type { Peer } from '../core/peer';
2
+ import type { FileMetadata } from '../core/types';
3
+ interface Transfer {
4
+ id: string;
5
+ filename: string;
6
+ size: number;
7
+ direction: 'send' | 'receive';
8
+ from?: string;
9
+ progress: number;
10
+ bytesPerSecond: number;
11
+ status: 'pending' | 'active' | 'complete' | 'cancelled' | 'error';
12
+ blob?: Blob;
13
+ meta?: FileMetadata;
14
+ }
15
+ export type { Transfer };
16
+ export declare function useFileTransfer(): {
17
+ transfers: Map<string, Transfer>;
18
+ send: (peer: Peer, file: File | Blob, filename?: string) => Promise<string>;
19
+ accept: (id: string) => void;
20
+ reject: (id: string) => void;
21
+ cancel: (id: string) => void;
22
+ listenToPeer: (peer: Peer) => () => void;
23
+ clearCompleted: () => void;
24
+ };
@@ -0,0 +1,133 @@
1
+ import { useEffect, useState, useCallback, useRef } from 'react';
2
+ import { usePeerContext } from './PeerProvider';
3
+ import { FileTransfer } from '../transfer';
4
+ export function useFileTransfer() {
5
+ const { client } = usePeerContext();
6
+ const [transfers, setTransfers] = useState(new Map());
7
+ const ftRef = useRef(null);
8
+ const cleanupRefs = useRef([]);
9
+ useEffect(() => {
10
+ if (!client)
11
+ return;
12
+ ftRef.current = new FileTransfer(client);
13
+ const ft = ftRef.current;
14
+ ft.on('progress', (p) => {
15
+ setTransfers((prev) => {
16
+ const next = new Map(prev);
17
+ const t = next.get(p.id);
18
+ if (t) {
19
+ next.set(p.id, { ...t, progress: p.percentage, bytesPerSecond: p.bytesPerSecond ?? 0, status: 'active' });
20
+ }
21
+ return next;
22
+ });
23
+ });
24
+ ft.on('incoming', (meta, from) => {
25
+ setTransfers((prev) => {
26
+ const next = new Map(prev);
27
+ next.set(meta.id, {
28
+ id: meta.id,
29
+ filename: meta.filename,
30
+ size: meta.size,
31
+ direction: 'receive',
32
+ from,
33
+ progress: 0,
34
+ bytesPerSecond: 0,
35
+ status: 'pending',
36
+ meta,
37
+ });
38
+ return next;
39
+ });
40
+ });
41
+ ft.on('complete', (id, blob) => {
42
+ setTransfers((prev) => {
43
+ const next = new Map(prev);
44
+ const t = next.get(id);
45
+ if (t) {
46
+ next.set(id, { ...t, progress: 100, status: 'complete', blob: blob instanceof Blob ? blob : undefined });
47
+ }
48
+ return next;
49
+ });
50
+ });
51
+ ft.on('cancelled', (id) => {
52
+ setTransfers((prev) => {
53
+ const next = new Map(prev);
54
+ const t = next.get(id);
55
+ if (t)
56
+ next.set(id, { ...t, status: 'cancelled' });
57
+ return next;
58
+ });
59
+ });
60
+ ft.on('error', (err) => {
61
+ const id = err?.id;
62
+ if (id) {
63
+ setTransfers((prev) => {
64
+ const next = new Map(prev);
65
+ const t = next.get(id);
66
+ if (t)
67
+ next.set(id, { ...t, status: 'error' });
68
+ return next;
69
+ });
70
+ }
71
+ });
72
+ return () => {
73
+ cleanupRefs.current.forEach((fn) => fn());
74
+ cleanupRefs.current = [];
75
+ ft.destroy();
76
+ ftRef.current = null;
77
+ };
78
+ }, [client]);
79
+ const send = useCallback(async (peer, file, filename) => {
80
+ if (!ftRef.current)
81
+ throw new Error('FileTransfer not initialized');
82
+ const name = filename ?? (file instanceof File ? file.name : 'file');
83
+ const id = crypto.randomUUID();
84
+ setTransfers((prev) => {
85
+ const next = new Map(prev);
86
+ next.set(id, {
87
+ id,
88
+ filename: name,
89
+ size: file.size,
90
+ direction: 'send',
91
+ progress: 0,
92
+ bytesPerSecond: 0,
93
+ status: 'active',
94
+ });
95
+ return next;
96
+ });
97
+ await ftRef.current.send(peer, file, name);
98
+ return id;
99
+ }, []);
100
+ const accept = useCallback((id) => {
101
+ ftRef.current?.accept(id);
102
+ }, []);
103
+ const reject = useCallback((id) => {
104
+ ftRef.current?.reject(id);
105
+ setTransfers((prev) => {
106
+ const next = new Map(prev);
107
+ next.delete(id);
108
+ return next;
109
+ });
110
+ }, []);
111
+ const cancel = useCallback((id) => {
112
+ ftRef.current?.cancel(id);
113
+ }, []);
114
+ const listenToPeer = useCallback((peer) => {
115
+ if (!ftRef.current)
116
+ return () => { };
117
+ const off = ftRef.current.handleIncoming(peer);
118
+ cleanupRefs.current.push(off);
119
+ return off;
120
+ }, []);
121
+ const clearCompleted = useCallback(() => {
122
+ setTransfers((prev) => {
123
+ const next = new Map(prev);
124
+ for (const [id, t] of next) {
125
+ if (t.status === 'complete' || t.status === 'cancelled' || t.status === 'error') {
126
+ next.delete(id);
127
+ }
128
+ }
129
+ return next;
130
+ });
131
+ }, []);
132
+ return { transfers, send, accept, reject, cancel, listenToPeer, clearCompleted };
133
+ }
@@ -0,0 +1,9 @@
1
+ import type { IdentityKeys } from '../core/types';
2
+ export declare function useIdentity(persistKey?: string): {
3
+ ready: boolean;
4
+ fingerprint: string;
5
+ exportKeys: () => Promise<IdentityKeys | null>;
6
+ regenerate: () => Promise<void>;
7
+ clear: () => void;
8
+ error: Error | null;
9
+ };
@@ -0,0 +1,63 @@
1
+ import { useEffect, useState, useCallback, useRef } from 'react';
2
+ import { Identity } from '../core/identity';
3
+ const STORAGE_KEY = 'peerlib_identity';
4
+ export function useIdentity(persistKey = STORAGE_KEY) {
5
+ const [ready, setReady] = useState(false);
6
+ const [fingerprint, setFingerprint] = useState('');
7
+ const [error, setError] = useState(null);
8
+ const idRef = useRef(null);
9
+ useEffect(() => {
10
+ const id = new Identity();
11
+ idRef.current = id;
12
+ const init = async () => {
13
+ try {
14
+ const stored = typeof localStorage !== 'undefined' ? localStorage.getItem(persistKey) : null;
15
+ if (stored) {
16
+ await id.restore(JSON.parse(stored));
17
+ }
18
+ else {
19
+ await id.generate();
20
+ const exported = await id.export();
21
+ if (typeof localStorage !== 'undefined') {
22
+ localStorage.setItem(persistKey, JSON.stringify(exported));
23
+ }
24
+ }
25
+ setFingerprint(id.fingerprint);
26
+ setReady(true);
27
+ }
28
+ catch (e) {
29
+ setError(e instanceof Error ? e : new Error(String(e)));
30
+ }
31
+ };
32
+ init();
33
+ return () => { idRef.current = null; };
34
+ }, [persistKey]);
35
+ const exportKeys = useCallback(async () => {
36
+ if (!idRef.current)
37
+ return null;
38
+ return idRef.current.export();
39
+ }, []);
40
+ const regenerate = useCallback(async () => {
41
+ if (!idRef.current)
42
+ return;
43
+ try {
44
+ await idRef.current.generate();
45
+ const exported = await idRef.current.export();
46
+ if (typeof localStorage !== 'undefined') {
47
+ localStorage.setItem(persistKey, JSON.stringify(exported));
48
+ }
49
+ setFingerprint(idRef.current.fingerprint);
50
+ }
51
+ catch (e) {
52
+ setError(e instanceof Error ? e : new Error(String(e)));
53
+ }
54
+ }, [persistKey]);
55
+ const clear = useCallback(() => {
56
+ if (typeof localStorage !== 'undefined') {
57
+ localStorage.removeItem(persistKey);
58
+ }
59
+ setFingerprint('');
60
+ setReady(false);
61
+ }, [persistKey]);
62
+ return { ready, fingerprint, exportKeys, regenerate, clear, error };
63
+ }
@@ -0,0 +1,11 @@
1
+ import type { PeerInfo, MatchResult } from '../core/types';
2
+ type MatchStatus = 'idle' | 'matching' | 'matched' | 'error';
3
+ export declare function useMatch(): {
4
+ match: (namespace: string, meta?: Record<string, any>, count?: number) => Promise<MatchResult | undefined>;
5
+ status: MatchStatus;
6
+ peers: PeerInfo[];
7
+ sessionId: string;
8
+ reset: () => void;
9
+ error: Error | null;
10
+ };
11
+ export {};
@@ -0,0 +1,33 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { usePeerContext } from './PeerProvider';
3
+ export function useMatch() {
4
+ const { client } = usePeerContext();
5
+ const [status, setStatus] = useState('idle');
6
+ const [peers, setPeers] = useState([]);
7
+ const [sessionId, setSessionId] = useState('');
8
+ const [error, setError] = useState(null);
9
+ const match = useCallback(async (namespace, meta, count) => {
10
+ if (!client)
11
+ return;
12
+ setStatus('matching');
13
+ setError(null);
14
+ try {
15
+ const result = await client.match(namespace, meta, count);
16
+ setPeers(result.peers);
17
+ setSessionId(result.session_id);
18
+ setStatus('matched');
19
+ return result;
20
+ }
21
+ catch (e) {
22
+ setError(e instanceof Error ? e : new Error(String(e)));
23
+ setStatus('error');
24
+ }
25
+ }, [client]);
26
+ const reset = useCallback(() => {
27
+ setStatus('idle');
28
+ setPeers([]);
29
+ setSessionId('');
30
+ setError(null);
31
+ }, []);
32
+ return { match, status, peers, sessionId, reset, error };
33
+ }
@@ -0,0 +1,13 @@
1
+ export declare function useMedia(roomId: string, type?: 'direct' | 'group', create?: boolean, audio?: boolean, video?: boolean): {
2
+ localStream: MediaStream | null;
3
+ remoteStreams: Map<string, MediaStream>;
4
+ audioMuted: boolean;
5
+ videoMuted: boolean;
6
+ muteAudio: () => void;
7
+ unmuteAudio: () => void;
8
+ muteVideo: () => void;
9
+ unmuteVideo: () => void;
10
+ toggleAudio: () => void;
11
+ toggleVideo: () => void;
12
+ error: Error | null;
13
+ };
@@ -0,0 +1,89 @@
1
+ import { useEffect, useState, useCallback, useRef } from 'react';
2
+ import { usePeerContext } from './PeerProvider';
3
+ import { DirectMedia, GroupMedia } from '../media';
4
+ export function useMedia(roomId, type = 'direct', create = false, audio = true, video = true) {
5
+ const { client } = usePeerContext();
6
+ const [localStream, setLocalStream] = useState(null);
7
+ const [remoteStreams, setRemoteStreams] = useState(new Map());
8
+ const [audioMuted, setAudioMuted] = useState(false);
9
+ const [videoMuted, setVideoMuted] = useState(false);
10
+ const [error, setError] = useState(null);
11
+ const mediaRef = useRef(null);
12
+ useEffect(() => {
13
+ if (!client || !roomId)
14
+ return;
15
+ const media = type === 'group'
16
+ ? new GroupMedia(client, roomId)
17
+ : new DirectMedia(client, roomId);
18
+ mediaRef.current = media;
19
+ media.on('local_stream', (stream) => setLocalStream(stream));
20
+ media.on('remote_stream', (stream, fp) => {
21
+ setRemoteStreams((prev) => new Map(prev).set(fp, stream));
22
+ });
23
+ media.on('remote_stream_removed', (fp) => {
24
+ setRemoteStreams((prev) => {
25
+ const next = new Map(prev);
26
+ next.delete(fp);
27
+ return next;
28
+ });
29
+ });
30
+ media.on('error', (e) => {
31
+ setError(e instanceof Error ? e : new Error(String(e)));
32
+ });
33
+ const init = async () => {
34
+ try {
35
+ if (create) {
36
+ await media.createAndJoin({ audio, video });
37
+ }
38
+ else {
39
+ await media.joinAndStart({ audio, video });
40
+ }
41
+ }
42
+ catch (e) {
43
+ setError(e instanceof Error ? e : new Error(String(e)));
44
+ }
45
+ };
46
+ init();
47
+ return () => {
48
+ media.close();
49
+ mediaRef.current = null;
50
+ setLocalStream(null);
51
+ setRemoteStreams(new Map());
52
+ };
53
+ }, [client, roomId, type, create, audio, video]);
54
+ const muteAudio = useCallback(() => {
55
+ mediaRef.current?.muteAudio();
56
+ setAudioMuted(true);
57
+ }, []);
58
+ const unmuteAudio = useCallback(() => {
59
+ mediaRef.current?.unmuteAudio();
60
+ setAudioMuted(false);
61
+ }, []);
62
+ const muteVideo = useCallback(() => {
63
+ mediaRef.current?.muteVideo();
64
+ setVideoMuted(true);
65
+ }, []);
66
+ const unmuteVideo = useCallback(() => {
67
+ mediaRef.current?.unmuteVideo();
68
+ setVideoMuted(false);
69
+ }, []);
70
+ const toggleAudio = useCallback(() => {
71
+ audioMuted ? unmuteAudio() : muteAudio();
72
+ }, [audioMuted, muteAudio, unmuteAudio]);
73
+ const toggleVideo = useCallback(() => {
74
+ videoMuted ? unmuteVideo() : muteVideo();
75
+ }, [videoMuted, muteVideo, unmuteVideo]);
76
+ return {
77
+ localStream,
78
+ remoteStreams,
79
+ audioMuted,
80
+ videoMuted,
81
+ muteAudio,
82
+ unmuteAudio,
83
+ muteVideo,
84
+ unmuteVideo,
85
+ toggleAudio,
86
+ toggleVideo,
87
+ error,
88
+ };
89
+ }
@@ -0,0 +1,7 @@
1
+ import type { PeerInfo } from '../core/types';
2
+ export declare function useNamespace(namespace: string): {
3
+ peers: PeerInfo[];
4
+ joined: boolean;
5
+ discover: (limit?: number) => Promise<PeerInfo[]>;
6
+ error: Error | null;
7
+ };
@@ -0,0 +1,38 @@
1
+ import { useEffect, useState, useCallback } from 'react';
2
+ import { usePeerContext } from './PeerProvider';
3
+ export function useNamespace(namespace) {
4
+ const { client } = usePeerContext();
5
+ const [peers, setPeers] = useState([]);
6
+ const [joined, setJoined] = useState(false);
7
+ const [error, setError] = useState(null);
8
+ useEffect(() => {
9
+ if (!client || !namespace)
10
+ return;
11
+ const offJoined = client.on('peer_joined', (info) => {
12
+ setPeers((prev) => [...prev.filter((p) => p.fingerprint !== info.fingerprint), info]);
13
+ });
14
+ const offLeft = client.on('peer_left', (fp) => {
15
+ setPeers((prev) => prev.filter((p) => p.fingerprint !== fp));
16
+ });
17
+ client.join(namespace)
18
+ .then((list) => {
19
+ setPeers(list.filter((p) => p.fingerprint !== client.fingerprint));
20
+ setJoined(true);
21
+ })
22
+ .catch((e) => setError(e instanceof Error ? e : new Error(String(e))));
23
+ return () => {
24
+ offJoined();
25
+ offLeft();
26
+ client.leave(namespace);
27
+ setJoined(false);
28
+ setPeers([]);
29
+ };
30
+ }, [client, namespace]);
31
+ const discover = useCallback(async (limit) => {
32
+ if (!client)
33
+ return [];
34
+ const result = await client.discover(namespace, limit);
35
+ return result;
36
+ }, [client, namespace]);
37
+ return { peers, joined, discover, error };
38
+ }
@@ -0,0 +1,6 @@
1
+ import type { Peer } from '../core/peer';
2
+ export declare function usePeer(fingerprint: string): {
3
+ peer: Peer | null;
4
+ connectionState: string;
5
+ send: (data: any, channel?: string) => void;
6
+ };