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
package/dist/crypto.js ADDED
@@ -0,0 +1,168 @@
1
+ import { Emitter } from './core/emitter';
2
+ export class E2E extends Emitter {
3
+ keys = new Map();
4
+ keyPair = null;
5
+ publicKeyRaw = null;
6
+ async init() {
7
+ this.keyPair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits']);
8
+ const raw = await crypto.subtle.exportKey('raw', this.keyPair.publicKey);
9
+ this.publicKeyRaw = new Uint8Array(raw).buffer;
10
+ }
11
+ getPublicKeyB64() {
12
+ if (!this.publicKeyRaw)
13
+ throw new Error('E2E not initialized');
14
+ return btoa(String.fromCharCode(...new Uint8Array(this.publicKeyRaw)));
15
+ }
16
+ getPublicKeyRaw() {
17
+ if (!this.publicKeyRaw)
18
+ throw new Error('E2E not initialized');
19
+ return this.publicKeyRaw;
20
+ }
21
+ async deriveKey(peerFingerprint, remotePublicKeyB64) {
22
+ if (!this.keyPair)
23
+ throw new Error('E2E not initialized');
24
+ const raw = Uint8Array.from(atob(remotePublicKeyB64), (c) => c.charCodeAt(0));
25
+ const remoteKey = await crypto.subtle.importKey('raw', raw, { name: 'ECDH', namedCurve: 'P-256' }, false, []);
26
+ const bits = await crypto.subtle.deriveBits({ name: 'ECDH', public: remoteKey }, this.keyPair.privateKey, 256);
27
+ const aesKey = await crypto.subtle.importKey('raw', bits, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
28
+ this.keys.set(peerFingerprint, aesKey);
29
+ this.emit('key_exchanged', peerFingerprint);
30
+ }
31
+ async encrypt(peerFingerprint, data) {
32
+ const key = this.keys.get(peerFingerprint);
33
+ if (!key)
34
+ throw new Error(`No key for ${peerFingerprint}`);
35
+ const iv = crypto.getRandomValues(new Uint8Array(12));
36
+ const encoded = new TextEncoder().encode(data);
37
+ const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
38
+ const combined = new Uint8Array(iv.length + encrypted.byteLength);
39
+ combined.set(iv);
40
+ combined.set(new Uint8Array(encrypted), iv.length);
41
+ return btoa(String.fromCharCode(...combined));
42
+ }
43
+ async decrypt(peerFingerprint, data) {
44
+ const key = this.keys.get(peerFingerprint);
45
+ if (!key)
46
+ throw new Error(`No key for ${peerFingerprint}`);
47
+ const combined = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
48
+ const iv = combined.slice(0, 12);
49
+ const ciphertext = combined.slice(12);
50
+ const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
51
+ return new TextDecoder().decode(decrypted);
52
+ }
53
+ hasKey(peerFingerprint) {
54
+ return this.keys.has(peerFingerprint);
55
+ }
56
+ removeKey(peerFingerprint) {
57
+ this.keys.delete(peerFingerprint);
58
+ }
59
+ destroy() {
60
+ this.keys.clear();
61
+ this.keyPair = null;
62
+ this.publicKeyRaw = null;
63
+ this.removeAllListeners();
64
+ }
65
+ }
66
+ export class GroupKeyManager {
67
+ e2e;
68
+ client;
69
+ constructor(client) {
70
+ this.client = client;
71
+ this.e2e = new E2E();
72
+ }
73
+ getE2E() {
74
+ return this.e2e;
75
+ }
76
+ async init() {
77
+ await this.e2e.init();
78
+ }
79
+ async exchangeWith(peer) {
80
+ if (peer.connectionState !== 'connected') {
81
+ await this.waitForConnection(peer);
82
+ }
83
+ const pubKey = this.e2e.getPublicKeyB64();
84
+ const identity = this.client.getIdentity();
85
+ let signature;
86
+ if (identity.getPrivateKey()) {
87
+ const sigRaw = await identity.sign(this.e2e.getPublicKeyRaw());
88
+ signature = btoa(String.fromCharCode(...new Uint8Array(sigRaw)));
89
+ }
90
+ peer.send({
91
+ _e2e_key_exchange: true,
92
+ publicKey: pubKey,
93
+ signature,
94
+ fingerprint: this.client.fingerprint,
95
+ }, 'data');
96
+ return new Promise((resolve, reject) => {
97
+ const timeout = setTimeout(() => {
98
+ off();
99
+ reject(new Error('Key exchange timeout'));
100
+ }, 10000);
101
+ const off = peer.on('data', async (data) => {
102
+ if (data?._e2e_key_exchange && data.publicKey) {
103
+ off();
104
+ clearTimeout(timeout);
105
+ try {
106
+ await this.e2e.deriveKey(peer.fingerprint, data.publicKey);
107
+ resolve();
108
+ }
109
+ catch (e) {
110
+ reject(e);
111
+ }
112
+ }
113
+ });
114
+ });
115
+ }
116
+ async handleIncomingKeyExchange(peer, data) {
117
+ if (data?._e2e_key_exchange && data.publicKey) {
118
+ await this.e2e.deriveKey(peer.fingerprint, data.publicKey);
119
+ if (peer.connectionState !== 'connected') {
120
+ await this.waitForConnection(peer);
121
+ }
122
+ const pubKey = this.e2e.getPublicKeyB64();
123
+ const identity = this.client.getIdentity();
124
+ let signature;
125
+ if (identity.getPrivateKey()) {
126
+ const sigRaw = await identity.sign(this.e2e.getPublicKeyRaw());
127
+ signature = btoa(String.fromCharCode(...new Uint8Array(sigRaw)));
128
+ }
129
+ peer.send({
130
+ _e2e_key_exchange: true,
131
+ publicKey: pubKey,
132
+ signature,
133
+ fingerprint: this.client.fingerprint,
134
+ }, 'data');
135
+ }
136
+ }
137
+ async encryptForPeer(fingerprint, data) {
138
+ const json = typeof data === 'string' ? data : JSON.stringify(data);
139
+ return this.e2e.encrypt(fingerprint, json);
140
+ }
141
+ async decryptFromPeer(fingerprint, data) {
142
+ const json = await this.e2e.decrypt(fingerprint, data);
143
+ try {
144
+ return JSON.parse(json);
145
+ }
146
+ catch {
147
+ return json;
148
+ }
149
+ }
150
+ waitForConnection(peer, timeout = 10000) {
151
+ if (peer.connectionState === 'connected')
152
+ return Promise.resolve();
153
+ return new Promise((resolve, reject) => {
154
+ const timer = setTimeout(() => {
155
+ off();
156
+ reject(new Error('Connection timeout for key exchange'));
157
+ }, timeout);
158
+ const off = peer.on('connected', () => {
159
+ off();
160
+ clearTimeout(timer);
161
+ resolve();
162
+ });
163
+ });
164
+ }
165
+ destroy() {
166
+ this.e2e.destroy();
167
+ }
168
+ }
@@ -0,0 +1,11 @@
1
+ export { PeerClient } from './core/client';
2
+ export { Peer } from './core/peer';
3
+ export { Identity } from './core/identity';
4
+ export { Transport } from './core/transport';
5
+ export { Emitter, setEmitterErrorHandler } from './core/emitter';
6
+ export { DirectRoom, GroupRoom } from './room';
7
+ export { DirectMedia, GroupMedia } from './media';
8
+ export { JSONTransfer, FileTransfer, ImageTransfer } from './transfer';
9
+ export { StateSync, CRDTSync } from './sync';
10
+ export { E2E, GroupKeyManager } from './crypto';
11
+ export * from './core/types';
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { PeerClient } from './core/client';
2
+ export { Peer } from './core/peer';
3
+ export { Identity } from './core/identity';
4
+ export { Transport } from './core/transport';
5
+ export { Emitter, setEmitterErrorHandler } from './core/emitter';
6
+ export { DirectRoom, GroupRoom } from './room';
7
+ export { DirectMedia, GroupMedia } from './media';
8
+ export { JSONTransfer, FileTransfer, ImageTransfer } from './transfer';
9
+ export { StateSync, CRDTSync } from './sync';
10
+ export { E2E, GroupKeyManager } from './crypto';
11
+ export * from './core/types';
@@ -0,0 +1,63 @@
1
+ import { Emitter } from './core/emitter';
2
+ import type { PeerClient } from './core/client';
3
+ import type { PeerInfo, MediaConfig } from './core/types';
4
+ type MediaEvent = 'local_stream' | 'remote_stream' | 'remote_stream_removed' | 'peer_joined' | 'peer_left' | 'muted' | 'unmuted' | 'error' | 'closed';
5
+ export declare class DirectMedia extends Emitter<MediaEvent> {
6
+ private client;
7
+ private roomId;
8
+ private localStream;
9
+ private remoteStream;
10
+ private remotePeer;
11
+ private remoteFingerprint;
12
+ private _closed;
13
+ private cleanups;
14
+ constructor(client: PeerClient, roomId: string);
15
+ start(config?: MediaConfig): Promise<MediaStream>;
16
+ createAndJoin(config?: MediaConfig): Promise<MediaStream>;
17
+ joinAndStart(config?: MediaConfig): Promise<{
18
+ stream: MediaStream;
19
+ peers: PeerInfo[];
20
+ }>;
21
+ private listen;
22
+ private connectTo;
23
+ muteAudio(): void;
24
+ unmuteAudio(): void;
25
+ muteVideo(): void;
26
+ unmuteVideo(): void;
27
+ isAudioMuted(): boolean;
28
+ isVideoMuted(): boolean;
29
+ getLocalStream(): MediaStream | null;
30
+ getRemoteStream(): MediaStream | null;
31
+ close(): void;
32
+ }
33
+ export declare class GroupMedia extends Emitter<MediaEvent> {
34
+ private client;
35
+ private roomId;
36
+ private localStream;
37
+ private remoteStreams;
38
+ private mediaPeers;
39
+ private _closed;
40
+ private cleanups;
41
+ constructor(client: PeerClient, roomId: string);
42
+ start(config?: MediaConfig): Promise<MediaStream>;
43
+ createAndJoin(config?: MediaConfig): Promise<MediaStream>;
44
+ joinAndStart(config?: MediaConfig): Promise<{
45
+ stream: MediaStream;
46
+ peers: PeerInfo[];
47
+ }>;
48
+ private listen;
49
+ private connectTo;
50
+ muteAudio(): void;
51
+ unmuteAudio(): void;
52
+ muteVideo(): void;
53
+ unmuteVideo(): void;
54
+ isAudioMuted(): boolean;
55
+ isVideoMuted(): boolean;
56
+ getLocalStream(): MediaStream | null;
57
+ getRemoteStreams(): Map<string, MediaStream>;
58
+ getRemoteStream(fingerprint: string): MediaStream | undefined;
59
+ getPeerCount(): number;
60
+ kick(fingerprint: string): void;
61
+ close(): void;
62
+ }
63
+ export {};
package/dist/media.js ADDED
@@ -0,0 +1,275 @@
1
+ import { Emitter } from './core/emitter';
2
+ import { LIMITS } from './core/types';
3
+ export class DirectMedia extends Emitter {
4
+ client;
5
+ roomId;
6
+ localStream = null;
7
+ remoteStream = null;
8
+ remotePeer = null;
9
+ remoteFingerprint = '';
10
+ _closed = false;
11
+ cleanups = [];
12
+ constructor(client, roomId) {
13
+ super();
14
+ this.client = client;
15
+ this.roomId = roomId;
16
+ }
17
+ async start(config = { audio: true, video: true }) {
18
+ this.localStream = await navigator.mediaDevices.getUserMedia({
19
+ audio: config.audio ?? true,
20
+ video: config.video ?? true,
21
+ });
22
+ this.emit('local_stream', this.localStream);
23
+ return this.localStream;
24
+ }
25
+ async createAndJoin(config) {
26
+ const stream = await this.start(config);
27
+ try {
28
+ await this.client.createRoom(this.roomId, { maxSize: 2 });
29
+ }
30
+ catch (e) {
31
+ this.emit('error', e);
32
+ }
33
+ this.listen();
34
+ return stream;
35
+ }
36
+ async joinAndStart(config) {
37
+ const stream = await this.start(config);
38
+ const peers = await this.client.joinRoom(this.roomId);
39
+ this.listen();
40
+ const remote = peers.find((p) => p.fingerprint !== this.client.fingerprint);
41
+ if (remote) {
42
+ this.connectTo(remote.fingerprint, remote.alias);
43
+ }
44
+ return { stream, peers };
45
+ }
46
+ listen() {
47
+ const offJoined = this.client.on('peer_joined', (info) => {
48
+ this.emit('peer_joined', info);
49
+ if (!this.remotePeer) {
50
+ this.connectTo(info.fingerprint, info.alias);
51
+ }
52
+ });
53
+ const offLeft = this.client.on('peer_left', (fp) => {
54
+ if (fp === this.remoteFingerprint) {
55
+ this.remotePeer = null;
56
+ this.remoteFingerprint = '';
57
+ this.remoteStream = null;
58
+ this.emit('remote_stream_removed', fp);
59
+ this.emit('peer_left', fp);
60
+ }
61
+ });
62
+ const offKicked = this.client.on('kicked', (payload) => {
63
+ if (payload?.room_id === this.roomId) {
64
+ this.close();
65
+ }
66
+ });
67
+ this.cleanups.push(offJoined, offLeft, offKicked);
68
+ }
69
+ connectTo(fingerprint, alias) {
70
+ this.remoteFingerprint = fingerprint;
71
+ const peer = this.client.createPeer(fingerprint, alias);
72
+ this.remotePeer = peer;
73
+ if (this.localStream) {
74
+ peer.addStream(this.localStream);
75
+ }
76
+ peer.on('stream', (stream) => {
77
+ this.remoteStream = stream;
78
+ this.emit('remote_stream', stream, fingerprint);
79
+ });
80
+ peer.on('disconnected', () => {
81
+ this.remoteStream = null;
82
+ this.emit('remote_stream_removed', fingerprint);
83
+ this.emit('peer_left', fingerprint);
84
+ });
85
+ peer.createOffer();
86
+ }
87
+ muteAudio() {
88
+ this.localStream?.getAudioTracks().forEach((t) => (t.enabled = false));
89
+ this.emit('muted', 'audio');
90
+ }
91
+ unmuteAudio() {
92
+ this.localStream?.getAudioTracks().forEach((t) => (t.enabled = true));
93
+ this.emit('unmuted', 'audio');
94
+ }
95
+ muteVideo() {
96
+ this.localStream?.getVideoTracks().forEach((t) => (t.enabled = false));
97
+ this.emit('muted', 'video');
98
+ }
99
+ unmuteVideo() {
100
+ this.localStream?.getVideoTracks().forEach((t) => (t.enabled = true));
101
+ this.emit('unmuted', 'video');
102
+ }
103
+ isAudioMuted() {
104
+ const track = this.localStream?.getAudioTracks()[0];
105
+ return track ? !track.enabled : true;
106
+ }
107
+ isVideoMuted() {
108
+ const track = this.localStream?.getVideoTracks()[0];
109
+ return track ? !track.enabled : true;
110
+ }
111
+ getLocalStream() {
112
+ return this.localStream;
113
+ }
114
+ getRemoteStream() {
115
+ return this.remoteStream;
116
+ }
117
+ close() {
118
+ if (this._closed)
119
+ return;
120
+ this._closed = true;
121
+ this.cleanups.forEach((fn) => fn());
122
+ this.cleanups = [];
123
+ this.localStream?.getTracks().forEach((t) => t.stop());
124
+ this.localStream = null;
125
+ this.remoteStream = null;
126
+ if (this.remotePeer) {
127
+ this.remotePeer.close();
128
+ this.remotePeer = null;
129
+ }
130
+ this.client.leave(this.roomId);
131
+ this.emit('closed');
132
+ this.removeAllListeners();
133
+ }
134
+ }
135
+ export class GroupMedia extends Emitter {
136
+ client;
137
+ roomId;
138
+ localStream = null;
139
+ remoteStreams = new Map();
140
+ mediaPeers = new Map();
141
+ _closed = false;
142
+ cleanups = [];
143
+ constructor(client, roomId) {
144
+ super();
145
+ this.client = client;
146
+ this.roomId = roomId;
147
+ }
148
+ async start(config = { audio: true, video: true }) {
149
+ this.localStream = await navigator.mediaDevices.getUserMedia({
150
+ audio: config.audio ?? true,
151
+ video: config.video ?? true,
152
+ });
153
+ this.emit('local_stream', this.localStream);
154
+ return this.localStream;
155
+ }
156
+ async createAndJoin(config) {
157
+ const stream = await this.start(config);
158
+ await this.client.createRoom(this.roomId, { maxSize: LIMITS.MAX_MEDIA_PEERS });
159
+ this.listen();
160
+ return stream;
161
+ }
162
+ async joinAndStart(config) {
163
+ const stream = await this.start(config);
164
+ const peers = await this.client.joinRoom(this.roomId);
165
+ this.listen();
166
+ const others = peers.filter((p) => p.fingerprint !== this.client.fingerprint);
167
+ for (const p of others) {
168
+ if (this.mediaPeers.size < LIMITS.MAX_MEDIA_PEERS - 1) {
169
+ this.connectTo(p.fingerprint, p.alias);
170
+ }
171
+ }
172
+ return { stream, peers };
173
+ }
174
+ listen() {
175
+ const offJoined = this.client.on('peer_joined', (info) => {
176
+ this.emit('peer_joined', info);
177
+ if (this.mediaPeers.size < LIMITS.MAX_MEDIA_PEERS - 1) {
178
+ this.connectTo(info.fingerprint, info.alias);
179
+ }
180
+ });
181
+ const offLeft = this.client.on('peer_left', (fp) => {
182
+ const peer = this.mediaPeers.get(fp);
183
+ if (peer) {
184
+ peer.close();
185
+ this.mediaPeers.delete(fp);
186
+ }
187
+ if (this.remoteStreams.has(fp)) {
188
+ this.remoteStreams.delete(fp);
189
+ this.emit('remote_stream_removed', fp);
190
+ }
191
+ this.emit('peer_left', fp);
192
+ });
193
+ const offKicked = this.client.on('kicked', (payload) => {
194
+ if (payload?.room_id === this.roomId) {
195
+ this.close();
196
+ }
197
+ });
198
+ this.cleanups.push(offJoined, offLeft, offKicked);
199
+ }
200
+ connectTo(fingerprint, alias) {
201
+ if (this.mediaPeers.has(fingerprint))
202
+ return;
203
+ const peer = this.client.createPeer(fingerprint, alias);
204
+ this.mediaPeers.set(fingerprint, peer);
205
+ if (this.localStream) {
206
+ peer.addStream(this.localStream);
207
+ }
208
+ peer.on('stream', (stream) => {
209
+ this.remoteStreams.set(fingerprint, stream);
210
+ this.emit('remote_stream', stream, fingerprint);
211
+ });
212
+ peer.on('disconnected', () => {
213
+ this.mediaPeers.delete(fingerprint);
214
+ if (this.remoteStreams.has(fingerprint)) {
215
+ this.remoteStreams.delete(fingerprint);
216
+ this.emit('remote_stream_removed', fingerprint);
217
+ }
218
+ });
219
+ peer.createOffer();
220
+ }
221
+ muteAudio() {
222
+ this.localStream?.getAudioTracks().forEach((t) => (t.enabled = false));
223
+ this.emit('muted', 'audio');
224
+ }
225
+ unmuteAudio() {
226
+ this.localStream?.getAudioTracks().forEach((t) => (t.enabled = true));
227
+ this.emit('unmuted', 'audio');
228
+ }
229
+ muteVideo() {
230
+ this.localStream?.getVideoTracks().forEach((t) => (t.enabled = false));
231
+ this.emit('muted', 'video');
232
+ }
233
+ unmuteVideo() {
234
+ this.localStream?.getVideoTracks().forEach((t) => (t.enabled = true));
235
+ this.emit('unmuted', 'video');
236
+ }
237
+ isAudioMuted() {
238
+ const track = this.localStream?.getAudioTracks()[0];
239
+ return track ? !track.enabled : true;
240
+ }
241
+ isVideoMuted() {
242
+ const track = this.localStream?.getVideoTracks()[0];
243
+ return track ? !track.enabled : true;
244
+ }
245
+ getLocalStream() {
246
+ return this.localStream;
247
+ }
248
+ getRemoteStreams() {
249
+ return new Map(this.remoteStreams);
250
+ }
251
+ getRemoteStream(fingerprint) {
252
+ return this.remoteStreams.get(fingerprint);
253
+ }
254
+ getPeerCount() {
255
+ return this.mediaPeers.size;
256
+ }
257
+ kick(fingerprint) {
258
+ this.client.kick(this.roomId, fingerprint);
259
+ }
260
+ close() {
261
+ if (this._closed)
262
+ return;
263
+ this._closed = true;
264
+ this.cleanups.forEach((fn) => fn());
265
+ this.cleanups = [];
266
+ this.localStream?.getTracks().forEach((t) => t.stop());
267
+ this.localStream = null;
268
+ this.mediaPeers.forEach((p) => p.close());
269
+ this.mediaPeers.clear();
270
+ this.remoteStreams.clear();
271
+ this.client.leave(this.roomId);
272
+ this.emit('closed');
273
+ this.removeAllListeners();
274
+ }
275
+ }
@@ -0,0 +1,6 @@
1
+ import { type AudioHTMLAttributes } from 'react';
2
+ interface AudioProps extends Omit<AudioHTMLAttributes<HTMLAudioElement>, 'ref'> {
3
+ stream: MediaStream | null;
4
+ }
5
+ export declare function Audio({ stream, ...props }: AudioProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ export function Audio({ stream, ...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("audio", { ref: ref, autoPlay: true, ...props });
18
+ }
@@ -0,0 +1,17 @@
1
+ import { type ReactNode } from 'react';
2
+ import { PeerClient } from '../core/client';
3
+ import type { ClientConfig } from '../core/types';
4
+ interface PeerContextValue {
5
+ client: PeerClient | null;
6
+ connected: boolean;
7
+ fingerprint: string;
8
+ alias: string;
9
+ error: Error | null;
10
+ }
11
+ export declare function usePeerContext(): PeerContextValue;
12
+ interface PeerProviderProps {
13
+ config: ClientConfig;
14
+ children: ReactNode;
15
+ }
16
+ export declare function PeerProvider({ config, children }: PeerProviderProps): import("react/jsx-runtime").JSX.Element;
17
+ export {};
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useState, useRef } from 'react';
3
+ import { PeerClient } from '../core/client';
4
+ const PeerContext = createContext({
5
+ client: null,
6
+ connected: false,
7
+ fingerprint: '',
8
+ alias: '',
9
+ error: null,
10
+ });
11
+ export function usePeerContext() {
12
+ return useContext(PeerContext);
13
+ }
14
+ export function PeerProvider({ config, children }) {
15
+ const [client, setClient] = useState(null);
16
+ const [connected, setConnected] = useState(false);
17
+ const [fingerprint, setFingerprint] = useState('');
18
+ const [alias, setAlias] = useState('');
19
+ const [error, setError] = useState(null);
20
+ const configRef = useRef(config);
21
+ configRef.current = config;
22
+ useEffect(() => {
23
+ const c = new PeerClient(configRef.current);
24
+ setClient(c);
25
+ const offRegistered = c.on('registered', (fp, a) => {
26
+ setFingerprint(fp);
27
+ setAlias(a);
28
+ });
29
+ const offDisconnected = c.on('disconnected', () => setConnected(false));
30
+ const offReconnected = c.on('reconnected', () => setConnected(true));
31
+ const offError = c.on('error', (e) => setError(e instanceof Error ? e : new Error(String(e))));
32
+ c.connect()
33
+ .then(() => setConnected(true))
34
+ .catch((e) => setError(e instanceof Error ? e : new Error(String(e))));
35
+ return () => {
36
+ offRegistered();
37
+ offDisconnected();
38
+ offReconnected();
39
+ offError();
40
+ c.disconnect();
41
+ setClient(null);
42
+ setConnected(false);
43
+ };
44
+ }, [config.url]);
45
+ return (_jsx(PeerContext.Provider, { value: { client, connected, fingerprint, alias, error }, children: children }));
46
+ }
@@ -0,0 +1,7 @@
1
+ interface PeerStatusProps {
2
+ state: string;
3
+ label?: string;
4
+ className?: string;
5
+ }
6
+ export declare function PeerStatus({ state, label, className }: PeerStatusProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const STATE_COLORS = {
3
+ connected: '#22c55e',
4
+ connecting: '#eab308',
5
+ disconnected: '#ef4444',
6
+ failed: '#ef4444',
7
+ closed: '#6b7280',
8
+ new: '#6b7280',
9
+ };
10
+ export function PeerStatus({ state, label, className }) {
11
+ const color = STATE_COLORS[state] ?? '#6b7280';
12
+ return (_jsxs("span", { className: className, "data-state": state, children: [_jsx("span", { "data-part": "dot", style: {
13
+ display: 'inline-block',
14
+ width: 8,
15
+ height: 8,
16
+ borderRadius: '50%',
17
+ backgroundColor: color,
18
+ marginRight: 6,
19
+ } }), _jsx("span", { "data-part": "label", children: label ?? state })] }));
20
+ }
@@ -0,0 +1,10 @@
1
+ import type { Transfer } from './useFileTransfer';
2
+ interface TransferProgressProps {
3
+ transfer: Transfer;
4
+ onAccept?: () => void;
5
+ onReject?: () => void;
6
+ onCancel?: () => void;
7
+ className?: string;
8
+ }
9
+ export declare function TransferProgress({ transfer, onAccept, onReject, onCancel, className }: TransferProgressProps): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ function formatBytes(bytes) {
3
+ if (bytes < 1024)
4
+ return `${bytes} B`;
5
+ if (bytes < 1024 * 1024)
6
+ return `${(bytes / 1024).toFixed(1)} KB`;
7
+ if (bytes < 1024 * 1024 * 1024)
8
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
9
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
10
+ }
11
+ function formatSpeed(bytesPerSecond) {
12
+ return `${formatBytes(bytesPerSecond)}/s`;
13
+ }
14
+ export function TransferProgress({ transfer, onAccept, onReject, onCancel, className }) {
15
+ const { filename, size, progress, bytesPerSecond, status, direction } = transfer;
16
+ return (_jsxs("div", { className: className, "data-status": status, "data-direction": direction, children: [_jsxs("div", { "data-part": "info", children: [_jsx("span", { "data-part": "filename", children: filename }), _jsx("span", { "data-part": "size", children: formatBytes(size) })] }), status === 'active' && (_jsxs("div", { "data-part": "progress", children: [_jsx("div", { "data-part": "bar", style: { width: `${progress}%` } }), _jsxs("span", { "data-part": "percent", children: [Math.round(progress), "%"] }), _jsx("span", { "data-part": "speed", children: formatSpeed(bytesPerSecond) })] })), status === 'pending' && direction === 'receive' && (_jsxs("div", { "data-part": "actions", children: [onAccept && _jsx("button", { onClick: onAccept, "data-action": "accept", children: "Accept" }), onReject && _jsx("button", { onClick: onReject, "data-action": "reject", children: "Reject" })] })), status === 'active' && onCancel && (_jsx("button", { onClick: onCancel, "data-action": "cancel", children: "Cancel" })), status === 'complete' && _jsx("span", { "data-part": "status", children: "Complete" }), status === 'cancelled' && _jsx("span", { "data-part": "status", children: "Cancelled" }), status === 'error' && _jsx("span", { "data-part": "status", children: "Error" })] }));
17
+ }