filepizza-client 2.0.1 → 2.1.1

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.
@@ -1,210 +1,161 @@
1
1
  // src/filepizza-uploader.ts
2
2
  import Peer, { DataConnection } from 'peerjs'
3
3
  import { EventEmitter } from './event-emitter'
4
- import { FileInfo, ProgressInfo, ConnectionInfo, ConnectionStatus, MessageType } from './types';
4
+ import {
5
+ FileInfo,
6
+ ProgressInfo,
7
+ ConnectionInfo,
8
+ ConnectionStatus,
9
+ MessageType,
10
+ PeerJSSignalingServer,
11
+ } from './types'
12
+ import { createPeer } from './peerjs'
13
+
14
+ type ChannelInfo = {
15
+ longSlug: string
16
+ shortSlug: string
17
+ secret?: string
18
+ }
19
+
20
+ type UploaderOptions = {
21
+ filePizzaServerUrl?: string
22
+ password?: string
23
+ sharedSlug?: string
24
+ peerJSSignalingServer?: PeerJSSignalingServer
25
+ discoverPeerJSSignalingServer?: boolean
26
+ }
5
27
 
6
28
  /**
7
- * FilePizza Uploader - connects to the FilePizza server and uploads files
29
+ * FilePizza Uploader - connects to the FilePizza server and uploads files.
8
30
  */
9
31
  export class FilePizzaUploader extends EventEmitter {
10
- private peer?: Peer;
11
- private connections: Map<string, any> = new Map();
12
- private connectionInfoMap = new Map<string, any>();
13
- private files: File[] = [];
14
- private password?: string;
15
- private filePizzaServerUrl: string;
16
- private channelInfo?: { longSlug: string; shortSlug: string; secret?: string };
17
- private sharedSlug?: string;
18
- private iceServers?: RTCIceServer[];
19
- private renewalTimer?: NodeJS.Timeout;
20
-
21
- /**
22
- * Create a new FilePizza uploader
23
- * @param options Configuration options
24
- */
25
- constructor(options: {
26
- filePizzaServerUrl?: string;
27
- password?: string;
28
- sharedSlug?: string;
29
- } = {}) {
30
- super();
31
- this.filePizzaServerUrl = options.filePizzaServerUrl || 'http://localhost:8081';
32
- this.password = options.password;
33
- this.sharedSlug = options.sharedSlug;
32
+ private peer?: Peer
33
+ private connections: Map<string, any> = new Map()
34
+ private connectionInfoMap = new Map<string, any>()
35
+ private files: File[] = []
36
+ private password?: string
37
+ private filePizzaServerUrl: string
38
+ private channelInfo?: ChannelInfo
39
+ private sharedSlug?: string
40
+ private renewalTimer?: NodeJS.Timeout
41
+ private peerJSSignalingServer?: PeerJSSignalingServer
42
+ private discoverPeerJSSignalingServer: boolean
43
+
44
+ constructor(options: UploaderOptions = {}) {
45
+ super()
46
+ this.filePizzaServerUrl = options.filePizzaServerUrl || 'http://localhost:8081'
47
+ this.password = options.password
48
+ this.sharedSlug = options.sharedSlug
49
+ this.peerJSSignalingServer = options.peerJSSignalingServer
50
+ this.discoverPeerJSSignalingServer =
51
+ options.discoverPeerJSSignalingServer ?? true
34
52
  }
35
53
 
36
- /**
37
- * Initialize the uploader
38
- */
39
54
  async initialize(): Promise<void> {
40
55
  if (this.peer) {
41
- return;
56
+ return
42
57
  }
43
58
 
44
- // Get ICE servers
45
- await this.getIceServers();
59
+ this.peer = await createPeer({
60
+ filePizzaServerUrl: this.filePizzaServerUrl,
61
+ peerJSSignalingServer: this.peerJSSignalingServer,
62
+ discoverPeerJSSignalingServer: this.discoverPeerJSSignalingServer,
63
+ })
46
64
 
47
- // Initialize PeerJS
48
- this.peer = new Peer({
49
- config: {
50
- iceServers: this.iceServers || [{ urls: 'stun:stun.l.google.com:19302' }],
51
- },
52
- debug: 2,
53
- });
54
-
55
- // Wait for peer to be ready
56
- if (!this.peer.id) {
57
- await new Promise<void>((resolve) => {
58
- const onOpen = () => {
59
- this.peer?.off('open', onOpen);
60
- resolve();
61
- };
62
- this.peer?.on('open', onOpen);
63
- });
64
- }
65
+ this.peer.on('connection', this.handleConnection.bind(this))
65
66
 
66
- // Set up connection handling
67
- this.peer.on('connection', this.handleConnection.bind(this));
68
-
69
- // Create channel
70
- if (this.peer.id) {
71
- await this.createChannel(this.peer.id, this.sharedSlug || undefined);
72
- this.startChannelRenewal();
73
- }
67
+ await this.createChannel(this.peer.id, this.sharedSlug || undefined)
68
+ this.startChannelRenewal()
74
69
  }
75
70
 
76
71
  setPassword(password: string): void {
77
72
  this.password = password
78
73
  }
79
74
 
80
- /**
81
- * Set files to be shared
82
- */
83
75
  setFiles(files: File[]): void {
84
- this.files = Array.from(files);
76
+ this.files = Array.from(files)
85
77
 
86
- // Update file info for existing connections
87
78
  if (this.files.length > 0) {
88
- for (const [_, connection] of this.connections.entries()) {
79
+ for (const [, connection] of this.connections.entries()) {
89
80
  if (connection.status === ConnectionStatus.Ready) {
90
81
  connection.dataConnection.send({
91
82
  type: MessageType.Info,
92
83
  files: this.getFileInfo(),
93
- });
84
+ })
94
85
  }
95
86
  }
96
87
  }
97
88
  }
98
89
 
99
- /**
100
- * Get shareable links for the current channel
101
- */
102
90
  getShareableLinks(): { long: string; short: string } | null {
103
91
  if (!this.channelInfo) {
104
- return null;
92
+ return null
105
93
  }
106
94
 
107
95
  return {
108
96
  long: `${this.filePizzaServerUrl}/download/${this.channelInfo.longSlug}`,
109
97
  short: `${this.filePizzaServerUrl}/download/${this.channelInfo.shortSlug}`,
110
- };
98
+ }
111
99
  }
112
100
 
113
- /**
114
- * Stop sharing and clean up
115
- */
116
101
  async stop(): Promise<void> {
117
- // Stop channel renewal
118
102
  if (this.renewalTimer) {
119
- clearTimeout(this.renewalTimer);
120
- this.renewalTimer = undefined;
103
+ clearTimeout(this.renewalTimer)
104
+ this.renewalTimer = undefined
121
105
  }
122
106
 
123
- // Destroy channel if we have one
124
107
  if (this.channelInfo) {
125
108
  try {
126
- await this.destroyChannel(this.channelInfo.shortSlug);
109
+ await this.destroyChannel(this.channelInfo.shortSlug)
127
110
  } catch (error) {
128
- console.error('Error destroying channel:', error);
111
+ console.error('Error destroying channel:', error)
129
112
  }
130
113
  }
131
114
 
132
- // Close all connections
133
- for (const [_, connection] of this.connections.entries()) {
115
+ for (const [, connection] of this.connections.entries()) {
134
116
  if (connection.dataConnection.open) {
135
- connection.dataConnection.close();
117
+ connection.dataConnection.close()
136
118
  }
137
119
  }
138
120
 
139
- // Clear connections
140
- this.connections.clear();
121
+ this.connections.clear()
141
122
 
142
- // Destroy peer
143
123
  if (this.peer) {
144
- this.peer.destroy();
145
- this.peer = undefined;
124
+ this.peer.destroy()
125
+ this.peer = undefined
146
126
  }
147
127
 
148
- // Reset state
149
- this.channelInfo = undefined;
128
+ this.channelInfo = undefined
150
129
  }
151
130
 
152
- /**
153
- * Get ICE servers from the FilePizza server
154
- */
155
- private async getIceServers(): Promise<RTCIceServer[]> {
156
- try {
157
- const response = await fetch(`${this.filePizzaServerUrl}/api/ice`, {
158
- method: 'POST',
159
- });
160
-
161
- if (!response.ok) {
162
- throw new Error(`Failed to get ICE servers: ${response.status}`);
163
- }
164
-
165
- const data = await response.json();
166
- this.iceServers = data.iceServers;
167
- return data.iceServers;
168
- } catch (error) {
169
- console.error('Error getting ICE servers:', error);
170
- return [{ urls: 'stun:stun.l.google.com:19302' }];
131
+ private async createChannel(
132
+ uploaderPeerID: string,
133
+ sharedSlug?: string,
134
+ ): Promise<void> {
135
+ const payload: { uploaderPeerID: string; sharedSlug?: string } = {
136
+ uploaderPeerID,
171
137
  }
172
- }
173
-
174
- /**
175
- * Create a new channel on the FilePizza server
176
- */
177
- private async createChannel(uploaderPeerID: string, sharedSlug?: string): Promise<void> {
178
- try {
179
- const payload: { uploaderPeerID: string; sharedSlug?: string } = { uploaderPeerID };
180
138
 
181
- if (sharedSlug) {
182
- payload.sharedSlug = sharedSlug;
183
- }
184
-
185
- const response = await fetch(`${this.filePizzaServerUrl}/api/create`, {
186
- method: 'POST',
187
- headers: { 'Content-Type': 'application/json' },
188
- body: JSON.stringify(payload),
189
- });
139
+ if (sharedSlug) {
140
+ payload.sharedSlug = sharedSlug
141
+ }
190
142
 
191
- if (!response.ok) {
192
- throw new Error(`Failed to create channel: ${response.status}`);
193
- }
143
+ const response = await fetch(`${this.filePizzaServerUrl}/api/create`, {
144
+ method: 'POST',
145
+ headers: { 'Content-Type': 'application/json' },
146
+ body: JSON.stringify(payload),
147
+ })
194
148
 
195
- this.channelInfo = await response.json();
196
- } catch (error) {
197
- console.error('Error creating channel:', error);
198
- throw error;
149
+ if (!response.ok) {
150
+ throw new Error(`Failed to create channel: ${response.status}`)
199
151
  }
152
+
153
+ this.channelInfo = await response.json()
200
154
  }
201
155
 
202
- /**
203
- * Renew the channel to keep it alive
204
- */
205
156
  private async renewChannel(): Promise<void> {
206
157
  if (!this.channelInfo || !this.channelInfo.secret) {
207
- return;
158
+ return
208
159
  }
209
160
 
210
161
  try {
@@ -215,63 +166,51 @@ export class FilePizzaUploader extends EventEmitter {
215
166
  slug: this.channelInfo.shortSlug,
216
167
  secret: this.channelInfo.secret,
217
168
  }),
218
- });
169
+ })
219
170
 
220
171
  if (!response.ok) {
221
- throw new Error(`Failed to renew channel: ${response.status}`);
172
+ throw new Error(`Failed to renew channel: ${response.status}`)
222
173
  }
223
174
  } catch (error) {
224
- console.error('Error renewing channel:', error);
175
+ console.error('Error renewing channel:', error)
225
176
  }
226
177
  }
227
178
 
228
- /**
229
- * Destroy a channel
230
- */
231
179
  private async destroyChannel(slug: string): Promise<void> {
232
180
  try {
233
181
  const response = await fetch(`${this.filePizzaServerUrl}/api/destroy`, {
234
182
  method: 'POST',
235
183
  headers: { 'Content-Type': 'application/json' },
236
184
  body: JSON.stringify({ slug }),
237
- });
185
+ })
238
186
 
239
187
  if (!response.ok) {
240
- throw new Error(`Failed to destroy channel: ${response.status}`);
188
+ throw new Error(`Failed to destroy channel: ${response.status}`)
241
189
  }
242
190
  } catch (error) {
243
- console.error('Error destroying channel:', error);
191
+ console.error('Error destroying channel:', error)
244
192
  }
245
193
  }
246
194
 
247
- /**
248
- * Start channel renewal
249
- */
250
195
  private startChannelRenewal(): void {
251
196
  if (!this.channelInfo || !this.channelInfo.secret) {
252
- return;
197
+ return
253
198
  }
254
199
 
255
- // Renew every 30 minutes
256
- const renewalInterval = 30 * 60 * 1000;
257
-
258
- this.renewalTimer = setInterval(() => {
259
- this.renewChannel();
260
- }, renewalInterval);
200
+ this.renewalTimer = setInterval(
201
+ () => {
202
+ this.renewChannel()
203
+ },
204
+ 30 * 60 * 1000,
205
+ )
261
206
  }
262
207
 
263
- /**
264
- * Handle new connection
265
- */
266
208
  private handleConnection(conn: DataConnection): void {
267
- // Ignore connections for reporting (handled separately)
268
209
  if (conn.metadata?.type === 'report') {
269
- this.emit('report', conn.peer);
270
- return;
210
+ this.emit('report', conn.peer)
211
+ return
271
212
  }
272
213
 
273
- console.log(`[FilePizzaUploader] New connection from ${conn.peer}`);
274
-
275
214
  const connectionContext = {
276
215
  status: ConnectionStatus.Pending,
277
216
  dataConnection: conn,
@@ -281,105 +220,81 @@ export class FilePizzaUploader extends EventEmitter {
281
220
  bytesTransferred: 0,
282
221
  totalBytes: this.getTotalBytes(),
283
222
  currentFileProgress: 0,
284
- };
223
+ acknowledgedBytes: 0,
224
+ }
285
225
 
286
- this.connections.set(conn.peer, connectionContext);
226
+ this.connections.set(conn.peer, connectionContext)
287
227
 
288
- // Set up event handlers
289
- conn.on('data', (data) => this.handleData(conn, data));
290
- conn.on('close', () => this.handleClose(conn));
291
- conn.on('error', (error) => this.handleError(conn, error));
228
+ conn.on('data', (data) => this.handleData(conn, data))
229
+ conn.on('close', () => this.handleClose(conn))
230
+ conn.on('error', (error) => this.handleError(conn, error))
292
231
 
293
- // Emit connection event
294
- this.emit('connection', this.getConnectionInfo(conn.peer));
232
+ this.emit('connection', this.getConnectionInfo(conn.peer))
295
233
  }
296
234
 
297
- /**
298
- * Handle data messages from connection
299
- */
300
235
  private handleData(conn: DataConnection, data: unknown): void {
301
- const context = this.connections.get(conn.peer);
302
- if (!context) return;
236
+ const context = this.connections.get(conn.peer)
237
+ if (!context) return
303
238
 
304
239
  try {
305
- // WebRTC messages follow a specific format with a type field
306
- const message = data as any;
240
+ const message = data as any
307
241
 
308
242
  switch (message.type) {
309
243
  case MessageType.RequestInfo:
310
- this.handleRequestInfo(conn, context, message);
311
- break;
244
+ this.handleRequestInfo(conn, context, message)
245
+ break
312
246
 
313
247
  case MessageType.UsePassword:
314
- this.handleUsePassword(conn, context, message);
315
- break;
248
+ this.handleUsePassword(conn, context, message)
249
+ break
316
250
 
317
251
  case MessageType.Start:
318
- this.handleStart(conn, context, message);
319
- break;
252
+ this.handleStart(conn, context, message)
253
+ break
320
254
 
321
255
  case MessageType.Pause:
322
- this.handlePause(conn, context);
323
- break;
256
+ this.handlePause(conn, context)
257
+ break
324
258
 
325
- case MessageType.Resume:
326
- this.handleResume(conn, context, message);
327
- break;
259
+ case MessageType.ChunkAck:
260
+ this.handleChunkAck(conn, context, message)
261
+ break
328
262
 
329
263
  case MessageType.Done:
330
- this.handleDone(conn, context);
331
- break;
264
+ this.handleDone(conn, context)
265
+ break
332
266
  }
333
267
  } catch (error) {
334
- console.error('[FilePizzaUploader] Error handling message:', error);
268
+ console.error('[FilePizzaUploader] Error handling message:', error)
335
269
  conn.send({
336
270
  type: MessageType.Error,
337
271
  error: 'Failed to process message',
338
- });
272
+ })
339
273
  }
340
274
  }
341
275
 
342
- /**
343
- * Handle connection close
344
- */
345
276
  private handleClose(conn: DataConnection): void {
346
- const context = this.connections.get(conn.peer);
347
- if (!context) return;
277
+ const context = this.connections.get(conn.peer)
278
+ if (!context) return
348
279
 
349
- // Update connection status
350
- context.status = ConnectionStatus.Closed;
351
-
352
- // Emit connection closed event
353
- this.emit('disconnection', conn.peer);
354
-
355
- // Remove connection
356
- this.connections.delete(conn.peer);
280
+ context.status = ConnectionStatus.Closed
281
+ this.emit('disconnection', conn.peer)
282
+ this.connections.delete(conn.peer)
357
283
  }
358
284
 
359
- /**
360
- * Handle connection error
361
- */
362
285
  private handleError(conn: DataConnection, error: Error): void {
363
- const context = this.connections.get(conn.peer);
364
- if (!context) return;
365
-
366
- // Update connection status
367
- context.status = ConnectionStatus.Error;
286
+ const context = this.connections.get(conn.peer)
287
+ if (!context) return
368
288
 
369
- // Emit error event
370
- this.emit('error', { connectionId: conn.peer, error });
289
+ context.status = ConnectionStatus.Error
290
+ this.emit('error', { connectionId: conn.peer, error })
371
291
 
372
- // Close connection
373
292
  if (conn.open) {
374
- conn.close();
293
+ conn.close()
375
294
  }
376
295
  }
377
296
 
378
- /**
379
- * Handle RequestInfo message
380
- */
381
297
  private handleRequestInfo(conn: DataConnection, context: any, message: any): void {
382
- // Store browser info in connection metadata
383
298
  this.connectionInfoMap.set(conn.connectionId, {
384
299
  browserName: message.browserName,
385
300
  browserVersion: message.browserVersion,
@@ -387,273 +302,204 @@ export class FilePizzaUploader extends EventEmitter {
387
302
  osVersion: message.osVersion,
388
303
  mobileVendor: message.mobileVendor,
389
304
  mobileModel: message.mobileModel,
390
- });
305
+ })
306
+
307
+ const connectionInfo = {
308
+ browserName: message.browserName,
309
+ browserVersion: message.browserVersion,
310
+ osName: message.osName,
311
+ osVersion: message.osVersion,
312
+ mobileVendor: message.mobileVendor,
313
+ mobileModel: message.mobileModel,
314
+ }
391
315
 
392
- // Check if password is required
393
316
  if (this.password) {
394
317
  conn.send({
395
318
  type: MessageType.PasswordRequired,
396
- });
319
+ })
397
320
 
398
- context.status = ConnectionStatus.Authenticating;
399
- } else {
400
- // Send file info
401
- conn.send({
402
- type: MessageType.Info,
403
- files: context.filesInfo,
404
- });
321
+ context.status = ConnectionStatus.Authenticating
322
+ Object.assign(context, connectionInfo)
405
323
 
406
- context.status = ConnectionStatus.Ready;
324
+ this.emit('connectionUpdate', this.getConnectionInfo(conn.peer))
325
+ return
407
326
  }
408
327
 
409
- // Emit connection update
410
- this.emit('connectionUpdate', this.getConnectionInfo(conn.peer));
328
+ conn.send({
329
+ type: MessageType.Info,
330
+ files: context.filesInfo,
331
+ })
332
+
333
+ context.status = ConnectionStatus.Ready
334
+ Object.assign(context, connectionInfo)
335
+
336
+ this.emit('connectionUpdate', this.getConnectionInfo(conn.peer))
411
337
  }
412
338
 
413
- /**
414
- * Handle UsePassword message
415
- */
416
339
  private handleUsePassword(conn: DataConnection, context: any, message: any): void {
417
- // Check password
418
340
  if (message.password === this.password) {
419
- // Password correct, send file info
420
341
  conn.send({
421
342
  type: MessageType.Info,
422
343
  files: context.filesInfo,
423
- });
344
+ })
424
345
 
425
- context.status = ConnectionStatus.Ready;
346
+ context.status = ConnectionStatus.Ready
426
347
  } else {
427
- // Password incorrect
428
348
  conn.send({
429
349
  type: MessageType.PasswordRequired,
430
- errorMessage: 'Incorrect password',
431
- });
350
+ errorMessage: 'Invalid password',
351
+ })
432
352
 
433
- context.status = ConnectionStatus.InvalidPassword;
353
+ context.status = ConnectionStatus.InvalidPassword
434
354
  }
435
355
 
436
- // Emit connection update
437
- this.emit('connectionUpdate', this.getConnectionInfo(conn.peer));
356
+ this.emit('connectionUpdate', this.getConnectionInfo(conn.peer))
438
357
  }
439
358
 
440
- /**
441
- * Handle Start message
442
- */
443
359
  private handleStart(conn: DataConnection, context: any, message: any): void {
444
- // Find the requested file
445
- const fileName = message.fileName;
446
- const offset = message.offset;
360
+ const fileName = message.fileName
361
+ const offset = message.offset
447
362
 
448
- const file = this.findFile(fileName);
449
- if (!file) {
363
+ const file = this.findFile(fileName)
364
+ if (!file || offset > file.size) {
450
365
  conn.send({
451
366
  type: MessageType.Error,
452
- error: `File not found: ${fileName}`,
453
- });
454
- return;
367
+ error: `Invalid file or offset: ${fileName}`,
368
+ })
369
+ return
455
370
  }
456
371
 
457
- // Update connection status
458
- context.status = ConnectionStatus.Uploading;
459
- context.uploadingFileName = fileName;
460
- context.uploadingOffset = offset;
461
-
462
- // Emit status update
463
- this.emit('connectionUpdate', this.getConnectionInfo(conn.peer));
372
+ context.status = ConnectionStatus.Uploading
373
+ context.uploadingFileName = fileName
374
+ context.uploadingOffset = offset
375
+ context.acknowledgedBytes = 0
376
+ context.currentFileProgress = 0
464
377
 
465
- // Begin sending file chunks
466
- this.sendFileChunks(conn, context, file, offset);
378
+ this.emit('connectionUpdate', this.getConnectionInfo(conn.peer))
379
+ this.sendFileChunks(conn, context, file, offset)
467
380
  }
468
381
 
469
- /**
470
- * Handle Pause message
471
- */
472
382
  private handlePause(conn: DataConnection, context: any): void {
473
- context.status = ConnectionStatus.Paused;
474
- this.emit('connectionUpdate', this.getConnectionInfo(conn.peer));
383
+ context.status = ConnectionStatus.Paused
384
+ this.emit('connectionUpdate', this.getConnectionInfo(conn.peer))
475
385
  }
476
386
 
477
- /**
478
- * Handle Resume message
479
- */
480
- private handleResume(conn: DataConnection, context: any, message: any): void {
481
- const fileName = message.fileName;
482
- const offset = message.offset;
387
+ private handleChunkAck(conn: DataConnection, context: any, message: any): void {
388
+ context.acknowledgedBytes =
389
+ (context.acknowledgedBytes || 0) + message.bytesReceived
483
390
 
484
- const file = this.findFile(fileName);
485
- if (!file) {
486
- conn.send({
487
- type: MessageType.Error,
488
- error: `File not found: ${fileName}`,
489
- });
490
- return;
391
+ const file = this.findFile(message.fileName)
392
+ if (file) {
393
+ context.currentFileProgress = context.acknowledgedBytes / file.size
491
394
  }
492
395
 
493
- context.status = ConnectionStatus.Uploading;
494
- context.uploadingFileName = fileName;
495
- context.uploadingOffset = offset;
496
-
497
- this.emit('connectionUpdate', this.getConnectionInfo(conn.peer));
498
-
499
- this.sendFileChunks(conn, context, file, offset);
396
+ this.emit('progress', this.getProgressInfo(conn.peer))
500
397
  }
501
398
 
502
- /**
503
- * Handle Done message
504
- */
505
399
  private handleDone(conn: DataConnection, context: any): void {
506
- context.status = ConnectionStatus.Done;
507
- this.emit('connectionUpdate', this.getConnectionInfo(conn.peer));
508
- conn.close();
400
+ context.status = ConnectionStatus.Done
401
+ this.emit('connectionUpdate', this.getConnectionInfo(conn.peer))
402
+ conn.close()
509
403
  }
510
404
 
511
- /**
512
- * Send file chunks to the downloader
513
- */
514
405
  private sendFileChunks(
515
406
  conn: DataConnection,
516
407
  context: any,
517
408
  file: File,
518
- startOffset: number
409
+ startOffset: number,
519
410
  ): void {
520
- let offset = startOffset;
521
- const CHUNK_SIZE = 256 * 1024; // 256 KB
411
+ let offset = startOffset
412
+ const chunkSize = 256 * 1024
522
413
 
523
414
  const sendNextChunk = () => {
524
- // Check if connection is still open and in uploading state
525
415
  if (!conn.open || context.status !== ConnectionStatus.Uploading) {
526
- return;
416
+ return
527
417
  }
528
418
 
529
- const end = Math.min(file.size, offset + CHUNK_SIZE);
530
- const chunkSize = end - offset;
531
- const final = end >= file.size;
419
+ const end = Math.min(file.size, offset + chunkSize)
420
+ const final = end >= file.size
421
+ const sentBytes = end - offset
532
422
 
533
- // Create chunk
534
- const chunk = file.slice(offset, end);
535
-
536
- // Send chunk
537
423
  conn.send({
538
424
  type: MessageType.Chunk,
539
425
  fileName: file.name,
540
426
  offset,
541
- bytes: chunk,
427
+ bytes: file.slice(offset, end),
542
428
  final,
543
- });
544
-
545
- // Update progress
546
- offset = end;
547
- context.uploadingOffset = offset;
548
- context.currentFileProgress = offset / file.size;
549
- context.bytesTransferred += chunkSize;
429
+ })
550
430
 
551
- // Emit progress update
552
- this.emit('progress', this.getProgressInfo(conn.peer));
431
+ offset = end
432
+ context.uploadingOffset = offset
433
+ context.bytesTransferred += sentBytes
553
434
 
554
- // If this was the final chunk
555
435
  if (final) {
556
436
  if (context.fileIndex < context.totalFiles - 1) {
557
- // Move to next file
558
- context.fileIndex++;
559
- context.currentFileProgress = 0;
560
- context.status = ConnectionStatus.Ready;
561
-
562
- // Emit update
563
- this.emit('connectionUpdate', this.getConnectionInfo(conn.peer));
437
+ context.fileIndex += 1
438
+ context.currentFileProgress = 0
439
+ context.status = ConnectionStatus.Ready
564
440
  } else {
565
- // All files completed
566
- context.fileIndex = context.totalFiles;
567
- context.currentFileProgress = 1;
568
- context.status = ConnectionStatus.Done;
569
-
570
- // Emit update
571
- this.emit('connectionUpdate', this.getConnectionInfo(conn.peer));
441
+ context.fileIndex = context.totalFiles
442
+ context.currentFileProgress = 1
443
+ context.status = ConnectionStatus.Ready
572
444
  }
445
+
446
+ this.emit('connectionUpdate', this.getConnectionInfo(conn.peer))
573
447
  } else {
574
- // Schedule next chunk
575
- setTimeout(sendNextChunk, 0);
448
+ setTimeout(sendNextChunk, 0)
576
449
  }
577
- };
450
+ }
578
451
 
579
- // Start sending chunks
580
- sendNextChunk();
452
+ sendNextChunk()
581
453
  }
582
454
 
583
- /**
584
- * Find a file by name
585
- */
586
455
  private findFile(fileName: string): File | undefined {
587
- return this.files.find(file => file.name === fileName);
456
+ return this.files.find((file) => file.name === fileName)
588
457
  }
589
458
 
590
- /**
591
- * Get file info for all files
592
- */
593
459
  private getFileInfo(): FileInfo[] {
594
- return this.files.map(file => ({
460
+ return this.files.map((file) => ({
595
461
  fileName: file.name,
596
462
  size: file.size,
597
463
  type: file.type,
598
- }));
464
+ }))
599
465
  }
600
466
 
601
- /**
602
- * Get connection info for a specific connection
603
- */
604
467
  private getConnectionInfo(peerId: string): ConnectionInfo {
605
- const context = this.connections.get(peerId);
468
+ const context = this.connections.get(peerId)
606
469
  if (!context) {
607
- throw new Error(`Connection not found: ${peerId}`);
470
+ throw new Error(`Connection not found: ${peerId}`)
608
471
  }
609
472
 
610
473
  return {
611
474
  id: peerId,
612
475
  status: context.status,
613
- browserName: context.dataConnection.metadata?.browserName,
614
- browserVersion: context.dataConnection.metadata?.browserVersion,
615
- osName: context.dataConnection.metadata?.osName,
616
- osVersion: context.dataConnection.metadata?.osVersion,
617
- mobileVendor: context.dataConnection.metadata?.mobileVendor,
618
- mobileModel: context.dataConnection.metadata?.mobileModel,
619
- };
476
+ browserName: context.browserName,
477
+ browserVersion: context.browserVersion,
478
+ osName: context.osName,
479
+ osVersion: context.osVersion,
480
+ mobileVendor: context.mobileVendor,
481
+ mobileModel: context.mobileModel,
482
+ }
620
483
  }
621
484
 
622
- /**
623
- * Get connection info for all connections
624
- */
625
485
  public getConnectionInfoAll(): ConnectionInfo[] {
626
- const connectionInfos: ConnectionInfo[] = [];
627
-
628
- for (const [peerId, context] of this.connections.entries()) {
629
- const connectionInfo = this.getConnectionInfo(peerId);
630
- connectionInfos.push(connectionInfo);
631
- }
632
-
633
- return connectionInfos;
486
+ return Array.from(this.connections.keys()).map((peerId) =>
487
+ this.getConnectionInfo(peerId),
488
+ )
634
489
  }
635
490
 
636
- /**
637
- * Remove connection info
638
- */
639
491
  public removeConnectionInfo(connectionId: string): void {
640
- this.connectionInfoMap.delete(connectionId);
492
+ this.connectionInfoMap.delete(connectionId)
641
493
  }
642
494
 
643
- /**
644
- * Get total bytes for all files
645
- */
646
495
  private getTotalBytes(): number {
647
- return this.files.reduce((total, file) => total + file.size, 0);
496
+ return this.files.reduce((total, file) => total + file.size, 0)
648
497
  }
649
498
 
650
- /**
651
- * Get progress info for a specific connection
652
- */
653
499
  private getProgressInfo(peerId: string): ProgressInfo {
654
- const context = this.connections.get(peerId);
500
+ const context = this.connections.get(peerId)
655
501
  if (!context) {
656
- throw new Error(`Connection not found: ${peerId}`);
502
+ throw new Error(`Connection not found: ${peerId}`)
657
503
  }
658
504
 
659
505
  return {
@@ -661,9 +507,9 @@ export class FilePizzaUploader extends EventEmitter {
661
507
  fileName: context.uploadingFileName || '',
662
508
  totalFiles: context.totalFiles,
663
509
  currentFileProgress: context.currentFileProgress,
664
- overallProgress: context.bytesTransferred / context.totalBytes,
510
+ overallProgress: context.bytesTransferred / (context.totalBytes || 1),
665
511
  bytesTransferred: context.bytesTransferred,
666
512
  totalBytes: context.totalBytes,
667
- };
513
+ }
668
514
  }
669
- }
515
+ }