@dropgate/core 2.1.0 → 2.2.0-beta.2

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.
@@ -208,7 +208,8 @@ async function startP2PSend(opts) {
208
208
  onProgress,
209
209
  onComplete,
210
210
  onError,
211
- onDisconnect
211
+ onDisconnect,
212
+ onCancel
212
213
  } = opts;
213
214
  if (!file) {
214
215
  throw new DropgateValidationError("File is missing.");
@@ -254,7 +255,7 @@ async function startP2PSend(opts) {
254
255
  onProgress?.({ processedBytes: safeReceived, totalBytes: safeTotal, percent });
255
256
  };
256
257
  const safeError = (err) => {
257
- if (state === "closed" || state === "completed") return;
258
+ if (state === "closed" || state === "completed" || state === "cancelled") return;
258
259
  state = "closed";
259
260
  onError?.(err);
260
261
  cleanup();
@@ -293,11 +294,21 @@ async function startP2PSend(opts) {
293
294
  window.addEventListener("beforeunload", handleUnload);
294
295
  }
295
296
  const stop = () => {
296
- if (state === "closed") return;
297
- state = "closed";
297
+ if (state === "closed" || state === "cancelled") return;
298
+ const wasActive = state === "transferring" || state === "finishing";
299
+ state = "cancelled";
300
+ try {
301
+ if (activeConn && activeConn.open) {
302
+ activeConn.send({ t: "cancelled", message: "Sender cancelled the transfer." });
303
+ }
304
+ } catch {
305
+ }
306
+ if (wasActive && onCancel) {
307
+ onCancel({ cancelledBy: "sender" });
308
+ }
298
309
  cleanup();
299
310
  };
300
- const isStopped = () => state === "closed";
311
+ const isStopped = () => state === "closed" || state === "cancelled";
301
312
  peer.on("connection", (conn) => {
302
313
  if (state === "closed") return;
303
314
  if (activeConn) {
@@ -367,6 +378,13 @@ async function startP2PSend(opts) {
367
378
  }
368
379
  if (msg.t === "error") {
369
380
  safeError(new DropgateNetworkError(msg.message || "Receiver reported an error."));
381
+ return;
382
+ }
383
+ if (msg.t === "cancelled") {
384
+ if (state === "cancelled" || state === "closed" || state === "completed") return;
385
+ state = "cancelled";
386
+ onCancel?.({ cancelledBy: "receiver", message: msg.message });
387
+ cleanup();
370
388
  }
371
389
  });
372
390
  conn.on("open", async () => {
@@ -404,6 +422,7 @@ async function startP2PSend(opts) {
404
422
  if (isStopped()) return;
405
423
  const slice = file.slice(offset, offset + chunkSize);
406
424
  const buf = await slice.arrayBuffer();
425
+ if (isStopped()) return;
407
426
  conn.send(buf);
408
427
  sentBytes += buf.byteLength;
409
428
  if (dc) {
@@ -453,14 +472,14 @@ async function startP2PSend(opts) {
453
472
  safeError(err);
454
473
  });
455
474
  conn.on("close", () => {
456
- if (state === "closed" || state === "completed") {
475
+ if (state === "closed" || state === "completed" || state === "cancelled") {
457
476
  cleanup();
458
477
  return;
459
478
  }
460
479
  if (state === "transferring" || state === "finishing") {
461
- safeError(
462
- new DropgateNetworkError("Receiver disconnected before transfer completed.")
463
- );
480
+ state = "cancelled";
481
+ onCancel?.({ cancelledBy: "receiver" });
482
+ cleanup();
464
483
  } else {
465
484
  activeConn = null;
466
485
  state = "listening";
@@ -502,7 +521,8 @@ async function startP2PReceive(opts) {
502
521
  onProgress,
503
522
  onComplete,
504
523
  onError,
505
- onDisconnect
524
+ onDisconnect,
525
+ onCancel
506
526
  } = opts;
507
527
  if (!code) {
508
528
  throw new DropgateValidationError("No sharing code was provided.");
@@ -559,7 +579,7 @@ async function startP2PReceive(opts) {
559
579
  }
560
580
  };
561
581
  const safeError = (err) => {
562
- if (state === "closed" || state === "completed") return;
582
+ if (state === "closed" || state === "completed" || state === "cancelled") return;
563
583
  state = "closed";
564
584
  onError?.(err);
565
585
  cleanup();
@@ -591,8 +611,18 @@ async function startP2PReceive(opts) {
591
611
  window.addEventListener("beforeunload", handleUnload);
592
612
  }
593
613
  const stop = () => {
594
- if (state === "closed") return;
595
- state = "closed";
614
+ if (state === "closed" || state === "cancelled") return;
615
+ const wasActive = state === "transferring";
616
+ state = "cancelled";
617
+ try {
618
+ if (activeConn && activeConn.open) {
619
+ activeConn.send({ t: "cancelled", message: "Receiver cancelled the transfer." });
620
+ }
621
+ } catch {
622
+ }
623
+ if (wasActive && onCancel) {
624
+ onCancel({ cancelledBy: "receiver" });
625
+ }
596
626
  cleanup();
597
627
  };
598
628
  peer.on("error", (err) => {
@@ -674,6 +704,13 @@ async function startP2PReceive(opts) {
674
704
  if (msg.t === "error") {
675
705
  throw new DropgateNetworkError(msg.message || "Sender reported an error.");
676
706
  }
707
+ if (msg.t === "cancelled") {
708
+ if (state === "cancelled" || state === "closed" || state === "completed") return;
709
+ state = "cancelled";
710
+ onCancel?.({ cancelledBy: "sender", message: msg.message });
711
+ cleanup();
712
+ return;
713
+ }
677
714
  return;
678
715
  }
679
716
  let bufPromise;
@@ -719,12 +756,14 @@ async function startP2PReceive(opts) {
719
756
  }
720
757
  });
721
758
  conn.on("close", () => {
722
- if (state === "closed" || state === "completed") {
759
+ if (state === "closed" || state === "completed" || state === "cancelled") {
723
760
  cleanup();
724
761
  return;
725
762
  }
726
763
  if (state === "transferring") {
727
- safeError(new DropgateNetworkError("Sender disconnected during transfer."));
764
+ state = "cancelled";
765
+ onCancel?.({ cancelledBy: "sender" });
766
+ cleanup();
728
767
  } else if (state === "negotiating") {
729
768
  state = "closed";
730
769
  cleanup();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/p2p/index.ts","../../src/errors.ts","../../src/utils/network.ts","../../src/adapters/defaults.ts","../../src/p2p/utils.ts","../../src/p2p/helpers.ts","../../src/p2p/send.ts","../../src/p2p/receive.ts"],"sourcesContent":["// P2P module exports\r\nexport { startP2PSend } from './send.js';\r\nexport { startP2PReceive } from './receive.js';\r\nexport { generateP2PCode, isP2PCodeLike, isLocalhostHostname, isSecureContextForP2P } from './utils.js';\r\nexport { buildPeerOptions, createPeerWithRetries, resolvePeerConfig } from './helpers.js';\r\n\r\n// Re-export types\r\nexport type {\r\n // State machine types\r\n P2PSendState,\r\n P2PReceiveState,\r\n // PeerJS types\r\n PeerConstructor,\r\n PeerInstance,\r\n PeerInstanceEvents,\r\n PeerOptions,\r\n DataConnection,\r\n DataConnectionEvents,\r\n // P2P config\r\n P2PServerConfig,\r\n // P2P event types\r\n P2PStatusEvent,\r\n P2PSendProgressEvent,\r\n P2PReceiveProgressEvent,\r\n P2PMetadataEvent,\r\n P2PReceiveCompleteEvent,\r\n // P2P options and sessions\r\n P2PSendOptions,\r\n P2PSendSession,\r\n P2PReceiveOptions,\r\n P2PReceiveSession,\r\n} from './types.js';\r\n","export interface DropgateErrorOptions {\r\n code?: string;\r\n details?: unknown;\r\n cause?: unknown;\r\n}\r\n\r\n/**\r\n * Base error class for all Dropgate errors\r\n */\r\nexport class DropgateError extends Error {\r\n readonly code: string;\r\n readonly details?: unknown;\r\n\r\n constructor(message: string, opts: DropgateErrorOptions = {}) {\r\n super(message);\r\n this.name = this.constructor.name;\r\n this.code = opts.code || 'DROPGATE_ERROR';\r\n this.details = opts.details;\r\n if (opts.cause !== undefined) {\r\n // Use Object.defineProperty for cause to maintain compatibility\r\n Object.defineProperty(this, 'cause', {\r\n value: opts.cause,\r\n writable: false,\r\n enumerable: false,\r\n configurable: true,\r\n });\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Validation error for invalid inputs\r\n */\r\nexport class DropgateValidationError extends DropgateError {\r\n constructor(message: string, opts: DropgateErrorOptions = {}) {\r\n super(message, { ...opts, code: opts.code || 'VALIDATION_ERROR' });\r\n }\r\n}\r\n\r\n/**\r\n * Network error for connection issues\r\n */\r\nexport class DropgateNetworkError extends DropgateError {\r\n constructor(message: string, opts: DropgateErrorOptions = {}) {\r\n super(message, { ...opts, code: opts.code || 'NETWORK_ERROR' });\r\n }\r\n}\r\n\r\n/**\r\n * Protocol error for server communication issues\r\n */\r\nexport class DropgateProtocolError extends DropgateError {\r\n constructor(message: string, opts: DropgateErrorOptions = {}) {\r\n super(message, { ...opts, code: opts.code || 'PROTOCOL_ERROR' });\r\n }\r\n}\r\n\r\n/**\r\n * Abort error - replacement for DOMException with AbortError name\r\n * Used when operations are cancelled\r\n */\r\nexport class DropgateAbortError extends DropgateError {\r\n constructor(message = 'Operation aborted') {\r\n super(message, { code: 'ABORT_ERROR' });\r\n this.name = 'AbortError';\r\n }\r\n}\r\n\r\n/**\r\n * Timeout error - replacement for DOMException with TimeoutError name\r\n * Used when operations exceed their time limit\r\n */\r\nexport class DropgateTimeoutError extends DropgateError {\r\n constructor(message = 'Request timed out') {\r\n super(message, { code: 'TIMEOUT_ERROR' });\r\n this.name = 'TimeoutError';\r\n }\r\n}\r\n","import { DropgateAbortError, DropgateTimeoutError, DropgateValidationError } from '../errors.js';\r\nimport type { FetchFn, ServerTarget } from '../types.js';\r\n\r\n/**\r\n * Parse a server URL string into host, port, and secure components.\r\n * If no protocol is specified, defaults to HTTPS.\r\n */\r\nexport function parseServerUrl(urlStr: string): ServerTarget {\r\n let normalized = urlStr.trim();\r\n if (!normalized.startsWith('http://') && !normalized.startsWith('https://')) {\r\n normalized = 'https://' + normalized;\r\n }\r\n const url = new URL(normalized);\r\n return {\r\n host: url.hostname,\r\n port: url.port ? Number(url.port) : undefined,\r\n secure: url.protocol === 'https:',\r\n };\r\n}\r\n\r\n/**\r\n * Build a base URL from host, port, and secure options.\r\n */\r\nexport function buildBaseUrl(opts: ServerTarget): string {\r\n const { host, port, secure } = opts;\r\n\r\n if (!host || typeof host !== 'string') {\r\n throw new DropgateValidationError('Server host is required.');\r\n }\r\n\r\n const protocol = secure === false ? 'http' : 'https';\r\n const portSuffix = port ? `:${port}` : '';\r\n\r\n return `${protocol}://${host}${portSuffix}`;\r\n}\r\n\r\n/**\r\n * Sleep for a specified duration, with optional abort signal support.\r\n */\r\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n if (signal?.aborted) {\r\n return reject(signal.reason || new DropgateAbortError());\r\n }\r\n\r\n const t = setTimeout(resolve, ms);\r\n\r\n if (signal) {\r\n signal.addEventListener(\r\n 'abort',\r\n () => {\r\n clearTimeout(t);\r\n reject(signal.reason || new DropgateAbortError());\r\n },\r\n { once: true }\r\n );\r\n }\r\n });\r\n}\r\n\r\nexport interface AbortSignalWithCleanup {\r\n signal: AbortSignal;\r\n cleanup: () => void;\r\n}\r\n\r\n/**\r\n * Create an AbortSignal that combines a parent signal with a timeout.\r\n */\r\nexport function makeAbortSignal(\r\n parentSignal?: AbortSignal | null,\r\n timeoutMs?: number\r\n): AbortSignalWithCleanup {\r\n const controller = new AbortController();\r\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\r\n\r\n const abort = (reason?: unknown): void => {\r\n if (!controller.signal.aborted) {\r\n controller.abort(reason);\r\n }\r\n };\r\n\r\n if (parentSignal) {\r\n if (parentSignal.aborted) {\r\n abort(parentSignal.reason);\r\n } else {\r\n parentSignal.addEventListener('abort', () => abort(parentSignal.reason), {\r\n once: true,\r\n });\r\n }\r\n }\r\n\r\n if (Number.isFinite(timeoutMs) && timeoutMs! > 0) {\r\n timeoutId = setTimeout(() => {\r\n abort(new DropgateTimeoutError());\r\n }, timeoutMs);\r\n }\r\n\r\n return {\r\n signal: controller.signal,\r\n cleanup: () => {\r\n if (timeoutId) clearTimeout(timeoutId);\r\n },\r\n };\r\n}\r\n\r\nexport interface FetchJsonResult {\r\n res: Response;\r\n json: unknown;\r\n text: string;\r\n}\r\n\r\nexport interface FetchJsonOptions extends Omit<RequestInit, 'signal'> {\r\n timeoutMs?: number;\r\n signal?: AbortSignal;\r\n}\r\n\r\n/**\r\n * Fetch JSON from a URL with timeout and error handling.\r\n */\r\nexport async function fetchJson(\r\n fetchFn: FetchFn,\r\n url: string,\r\n opts: FetchJsonOptions = {}\r\n): Promise<FetchJsonResult> {\r\n const { timeoutMs, signal, ...rest } = opts;\r\n const { signal: s, cleanup } = makeAbortSignal(signal, timeoutMs);\r\n\r\n try {\r\n const res = await fetchFn(url, { ...rest, signal: s });\r\n const text = await res.text();\r\n\r\n let json: unknown = null;\r\n try {\r\n json = text ? JSON.parse(text) : null;\r\n } catch {\r\n // Ignore parse errors - json will remain null\r\n }\r\n\r\n return { res, json, text };\r\n } finally {\r\n cleanup();\r\n }\r\n}\r\n","import type { Base64Adapter, CryptoAdapter, FetchFn } from '../types.js';\r\n\r\n/**\r\n * Get the default Base64 adapter for the current environment.\r\n * Automatically detects Node.js Buffer vs browser btoa/atob.\r\n */\r\nexport function getDefaultBase64(): Base64Adapter {\r\n // Check for Node.js Buffer (works in Node.js and some bundlers)\r\n if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {\r\n return {\r\n encode(bytes: Uint8Array): string {\r\n return Buffer.from(bytes).toString('base64');\r\n },\r\n decode(b64: string): Uint8Array {\r\n return new Uint8Array(Buffer.from(b64, 'base64'));\r\n },\r\n };\r\n }\r\n\r\n // Browser fallback using btoa/atob\r\n if (typeof btoa === 'function' && typeof atob === 'function') {\r\n return {\r\n encode(bytes: Uint8Array): string {\r\n let binary = '';\r\n for (let i = 0; i < bytes.length; i++) {\r\n binary += String.fromCharCode(bytes[i]);\r\n }\r\n return btoa(binary);\r\n },\r\n decode(b64: string): Uint8Array {\r\n const binary = atob(b64);\r\n const out = new Uint8Array(binary.length);\r\n for (let i = 0; i < binary.length; i++) {\r\n out[i] = binary.charCodeAt(i);\r\n }\r\n return out;\r\n },\r\n };\r\n }\r\n\r\n throw new Error(\r\n 'No Base64 implementation available. Provide a Base64Adapter via options.'\r\n );\r\n}\r\n\r\n/**\r\n * Get the default crypto object for the current environment.\r\n * Returns globalThis.crypto if available.\r\n */\r\nexport function getDefaultCrypto(): CryptoAdapter | undefined {\r\n return globalThis.crypto as CryptoAdapter | undefined;\r\n}\r\n\r\n/**\r\n * Get the default fetch function for the current environment.\r\n * Returns globalThis.fetch if available.\r\n */\r\nexport function getDefaultFetch(): FetchFn | undefined {\r\n return globalThis.fetch?.bind(globalThis) as FetchFn | undefined;\r\n}\r\n","import type { CryptoAdapter } from '../types.js';\r\nimport { getDefaultCrypto } from '../adapters/defaults.js';\r\n\r\n/**\r\n * Check if a hostname is localhost\r\n */\r\nexport function isLocalhostHostname(hostname: string): boolean {\r\n const host = String(hostname || '').toLowerCase();\r\n return host === 'localhost' || host === '127.0.0.1' || host === '::1';\r\n}\r\n\r\n/**\r\n * Check if the current context allows P2P (HTTPS or localhost)\r\n */\r\nexport function isSecureContextForP2P(\r\n hostname?: string,\r\n isSecureContext?: boolean\r\n): boolean {\r\n return Boolean(isSecureContext) || isLocalhostHostname(hostname || '');\r\n}\r\n\r\n/**\r\n * Generate a P2P sharing code using cryptographically secure random.\r\n * Format: XXXX-0000 (4 letters + 4 digits)\r\n */\r\nexport function generateP2PCode(cryptoObj?: CryptoAdapter): string {\r\n const crypto = cryptoObj || getDefaultCrypto();\r\n const letters = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // Excluded I and O to avoid confusion\r\n\r\n if (crypto) {\r\n const randomBytes = new Uint8Array(8);\r\n crypto.getRandomValues(randomBytes);\r\n\r\n let letterPart = '';\r\n for (let i = 0; i < 4; i++) {\r\n letterPart += letters[randomBytes[i] % letters.length];\r\n }\r\n\r\n let numberPart = '';\r\n for (let i = 4; i < 8; i++) {\r\n numberPart += (randomBytes[i] % 10).toString();\r\n }\r\n\r\n return `${letterPart}-${numberPart}`;\r\n }\r\n\r\n // Fallback to Math.random (less secure, but works everywhere)\r\n let a = '';\r\n for (let i = 0; i < 4; i++) {\r\n a += letters[Math.floor(Math.random() * letters.length)];\r\n }\r\n let b = '';\r\n for (let i = 0; i < 4; i++) {\r\n b += Math.floor(Math.random() * 10);\r\n }\r\n return `${a}-${b}`;\r\n}\r\n\r\n/**\r\n * Check if a string looks like a P2P sharing code\r\n */\r\nexport function isP2PCodeLike(code: string): boolean {\r\n return /^[A-Z]{4}-\\d{4}$/.test(String(code || '').trim());\r\n}\r\n","import { DropgateNetworkError } from '../errors.js';\r\nimport type { P2PCapabilities } from '../types.js';\r\nimport type { PeerInstance, PeerOptions, P2PServerConfig } from './types.js';\r\n\r\n/**\r\n * Resolve P2P server configuration from user options and server capabilities.\r\n * User-provided values take precedence over server capabilities.\r\n */\r\nexport function resolvePeerConfig(\r\n userConfig: P2PServerConfig,\r\n serverCaps?: P2PCapabilities\r\n): { path: string; iceServers: RTCIceServer[] } {\r\n return {\r\n path: userConfig.peerjsPath ?? serverCaps?.peerjsPath ?? '/peerjs',\r\n iceServers: userConfig.iceServers ?? serverCaps?.iceServers ?? [],\r\n };\r\n}\r\n\r\n/**\r\n * Build PeerJS connection options from P2P server configuration.\r\n */\r\nexport function buildPeerOptions(config: P2PServerConfig = {}): PeerOptions {\r\n const { host, port, peerjsPath = '/peerjs', secure = false, iceServers = [] } = config;\r\n\r\n const peerOpts: PeerOptions = {\r\n host,\r\n path: peerjsPath,\r\n secure,\r\n config: { iceServers },\r\n debug: 0,\r\n };\r\n\r\n if (port) {\r\n peerOpts.port = port;\r\n }\r\n\r\n return peerOpts;\r\n}\r\n\r\nexport interface CreatePeerWithRetriesOptions {\r\n code?: string | null;\r\n codeGenerator: () => string;\r\n maxAttempts: number;\r\n buildPeer: (id: string) => PeerInstance;\r\n onCode?: (code: string, attempt: number) => void;\r\n}\r\n\r\n/**\r\n * Create a peer with retries if the code is already taken\r\n */\r\nexport async function createPeerWithRetries(\r\n opts: CreatePeerWithRetriesOptions\r\n): Promise<{ peer: PeerInstance; code: string }> {\r\n const { code, codeGenerator, maxAttempts, buildPeer, onCode } = opts;\r\n\r\n let nextCode = code || codeGenerator();\r\n let peer: PeerInstance | null = null;\r\n let lastError: Error | null = null;\r\n\r\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\r\n onCode?.(nextCode, attempt);\r\n\r\n try {\r\n peer = await new Promise<PeerInstance>((resolve, reject) => {\r\n const instance = buildPeer(nextCode);\r\n instance.on('open', () => resolve(instance));\r\n instance.on('error', (err: Error) => {\r\n try {\r\n instance.destroy();\r\n } catch {\r\n // Ignore destroy errors\r\n }\r\n reject(err);\r\n });\r\n });\r\n\r\n return { peer, code: nextCode };\r\n } catch (err) {\r\n lastError = err as Error;\r\n nextCode = codeGenerator();\r\n }\r\n }\r\n\r\n throw lastError || new DropgateNetworkError('Could not establish PeerJS connection.');\r\n}\r\n","import { DropgateValidationError, DropgateNetworkError } from '../errors.js';\r\nimport { sleep } from '../utils/network.js';\r\nimport type { P2PSendOptions, P2PSendSession, P2PSendState, DataConnection } from './types.js';\r\nimport { generateP2PCode } from './utils.js';\r\nimport { buildPeerOptions, createPeerWithRetries, resolvePeerConfig } from './helpers.js';\r\n\r\n/**\r\n * Generate a unique session ID for transfer tracking.\r\n * Uses crypto.randomUUID if available, falls back to timestamp + random.\r\n */\r\nfunction generateSessionId(): string {\r\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\r\n return crypto.randomUUID();\r\n }\r\n // Fallback for environments without crypto.randomUUID\r\n return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;\r\n}\r\n\r\n/**\r\n * Start a direct transfer (P2P) sender session.\r\n *\r\n * IMPORTANT: Consumer must provide the PeerJS Peer constructor.\r\n * This removes DOM coupling (no script injection).\r\n *\r\n * Example:\r\n * ```js\r\n * import Peer from 'peerjs';\r\n * import { startP2PSend } from '@dropgate/core/p2p';\r\n *\r\n * const session = await startP2PSend({\r\n * file: myFile,\r\n * Peer,\r\n * host: 'dropgate.link',\r\n * secure: true,\r\n * onCode: (code) => console.log('Share this code:', code),\r\n * onProgress: (evt) => console.log(`${evt.percent}% sent`),\r\n * onComplete: () => console.log('Done!'),\r\n * });\r\n * ```\r\n */\r\nexport async function startP2PSend(opts: P2PSendOptions): Promise<P2PSendSession> {\r\n const {\r\n file,\r\n Peer,\r\n serverInfo,\r\n host,\r\n port,\r\n peerjsPath,\r\n secure = false,\r\n iceServers,\r\n codeGenerator,\r\n cryptoObj,\r\n maxAttempts = 4,\r\n chunkSize = 256 * 1024,\r\n endAckTimeoutMs = 15000,\r\n bufferHighWaterMark = 8 * 1024 * 1024,\r\n bufferLowWaterMark = 2 * 1024 * 1024,\r\n heartbeatIntervalMs = 5000,\r\n onCode,\r\n onStatus,\r\n onProgress,\r\n onComplete,\r\n onError,\r\n onDisconnect,\r\n } = opts;\r\n\r\n // Validate required options\r\n if (!file) {\r\n throw new DropgateValidationError('File is missing.');\r\n }\r\n\r\n if (!Peer) {\r\n throw new DropgateValidationError(\r\n 'PeerJS Peer constructor is required. Install peerjs and pass it as the Peer option.'\r\n );\r\n }\r\n\r\n // Check P2P capabilities if serverInfo is provided\r\n const p2pCaps = serverInfo?.capabilities?.p2p;\r\n if (serverInfo && !p2pCaps?.enabled) {\r\n throw new DropgateValidationError('Direct transfer is disabled on this server.');\r\n }\r\n\r\n // Resolve config from user options and server capabilities\r\n const { path: finalPath, iceServers: finalIceServers } = resolvePeerConfig(\r\n { peerjsPath, iceServers },\r\n p2pCaps\r\n );\r\n\r\n // Build peer options\r\n const peerOpts = buildPeerOptions({\r\n host,\r\n port,\r\n peerjsPath: finalPath,\r\n secure,\r\n iceServers: finalIceServers,\r\n });\r\n\r\n // Create the code generator\r\n const finalCodeGenerator = codeGenerator || (() => generateP2PCode(cryptoObj));\r\n\r\n // Create peer with retries\r\n const buildPeer = (id: string) => new Peer(id, peerOpts);\r\n const { peer, code } = await createPeerWithRetries({\r\n code: null,\r\n codeGenerator: finalCodeGenerator,\r\n maxAttempts,\r\n buildPeer,\r\n onCode,\r\n });\r\n\r\n // Generate unique session ID for this transfer\r\n const sessionId = generateSessionId();\r\n\r\n // State machine - replaces boolean flags to prevent race conditions\r\n let state: P2PSendState = 'listening';\r\n let activeConn: DataConnection | null = null;\r\n let sentBytes = 0;\r\n let heartbeatTimer: ReturnType<typeof setInterval> | null = null;\r\n\r\n const reportProgress = (data: { received: number; total: number }): void => {\r\n const safeTotal =\r\n Number.isFinite(data.total) && data.total > 0 ? data.total : file.size;\r\n const safeReceived = Math.min(Number(data.received) || 0, safeTotal || 0);\r\n const percent = safeTotal ? (safeReceived / safeTotal) * 100 : 0;\r\n onProgress?.({ processedBytes: safeReceived, totalBytes: safeTotal, percent });\r\n };\r\n\r\n // Safe error handler - prevents calling onError after completion\r\n const safeError = (err: Error): void => {\r\n if (state === 'closed' || state === 'completed') return;\r\n state = 'closed';\r\n onError?.(err);\r\n cleanup();\r\n };\r\n\r\n // Safe complete handler - only fires from finishing state\r\n const safeComplete = (): void => {\r\n if (state !== 'finishing') return;\r\n state = 'completed';\r\n onComplete?.();\r\n cleanup();\r\n };\r\n\r\n // Cleanup all resources\r\n const cleanup = (): void => {\r\n // Clear heartbeat timer\r\n if (heartbeatTimer) {\r\n clearInterval(heartbeatTimer);\r\n heartbeatTimer = null;\r\n }\r\n\r\n // Remove beforeunload listener if in browser\r\n if (typeof window !== 'undefined') {\r\n window.removeEventListener('beforeunload', handleUnload);\r\n }\r\n\r\n try {\r\n activeConn?.close();\r\n } catch {\r\n // Ignore close errors\r\n }\r\n try {\r\n peer.destroy();\r\n } catch {\r\n // Ignore destroy errors\r\n }\r\n };\r\n\r\n // Handle browser tab close/refresh\r\n const handleUnload = (): void => {\r\n try {\r\n activeConn?.send({ t: 'error', message: 'Sender closed the connection.' });\r\n } catch {\r\n // Best effort\r\n }\r\n stop();\r\n };\r\n\r\n // Add beforeunload listener if in browser\r\n if (typeof window !== 'undefined') {\r\n window.addEventListener('beforeunload', handleUnload);\r\n }\r\n\r\n const stop = (): void => {\r\n if (state === 'closed') return;\r\n state = 'closed';\r\n cleanup();\r\n };\r\n\r\n // Helper to check if session is stopped - bypasses TypeScript narrowing\r\n // which doesn't understand state can change asynchronously\r\n const isStopped = (): boolean => state === 'closed';\r\n\r\n peer.on('connection', (conn: DataConnection) => {\r\n if (state === 'closed') return;\r\n\r\n // Connection replacement logic - allow new connections if old one is dead\r\n if (activeConn) {\r\n // Check if existing connection is actually still open\r\n // @ts-expect-error - open property may exist on PeerJS connections\r\n const isOldConnOpen = activeConn.open !== false;\r\n\r\n if (isOldConnOpen && state === 'transferring') {\r\n // Actively transferring, reject new connection\r\n try {\r\n conn.send({ t: 'error', message: 'Transfer already in progress.' });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n try {\r\n conn.close();\r\n } catch {\r\n // Ignore close errors\r\n }\r\n return;\r\n } else if (!isOldConnOpen) {\r\n // Old connection is dead, clean it up and accept new one\r\n try {\r\n activeConn.close();\r\n } catch {\r\n // Ignore\r\n }\r\n activeConn = null;\r\n // Reset state to allow new transfer\r\n state = 'listening';\r\n sentBytes = 0;\r\n } else {\r\n // Connection exists but not transferring (maybe in negotiating state)\r\n // Reject to avoid confusion\r\n try {\r\n conn.send({ t: 'error', message: 'Another receiver is already connected.' });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n try {\r\n conn.close();\r\n } catch {\r\n // Ignore close errors\r\n }\r\n return;\r\n }\r\n }\r\n\r\n activeConn = conn;\r\n state = 'negotiating';\r\n onStatus?.({ phase: 'waiting', message: 'Connected. Waiting for receiver to accept...' });\r\n\r\n let readyResolve: (() => void) | null = null;\r\n let ackResolve: ((data: unknown) => void) | null = null;\r\n\r\n const readyPromise = new Promise<void>((resolve) => {\r\n readyResolve = resolve;\r\n });\r\n\r\n const ackPromise = new Promise<unknown>((resolve) => {\r\n ackResolve = resolve;\r\n });\r\n\r\n conn.on('data', (data: unknown) => {\r\n if (\r\n !data ||\r\n typeof data !== 'object' ||\r\n data instanceof ArrayBuffer ||\r\n ArrayBuffer.isView(data)\r\n ) {\r\n return;\r\n }\r\n\r\n const msg = data as { t?: string; received?: number; total?: number; phase?: string; message?: string };\r\n if (!msg.t) return;\r\n\r\n if (msg.t === 'ready') {\r\n onStatus?.({ phase: 'transferring', message: 'Receiver accepted. Starting transfer...' });\r\n readyResolve?.();\r\n return;\r\n }\r\n\r\n if (msg.t === 'progress') {\r\n reportProgress({ received: msg.received || 0, total: msg.total || 0 });\r\n return;\r\n }\r\n\r\n if (msg.t === 'ack' && msg.phase === 'end') {\r\n ackResolve?.(msg);\r\n return;\r\n }\r\n\r\n if (msg.t === 'pong') {\r\n // Heartbeat response received, connection is alive\r\n return;\r\n }\r\n\r\n if (msg.t === 'error') {\r\n safeError(new DropgateNetworkError(msg.message || 'Receiver reported an error.'));\r\n }\r\n });\r\n\r\n conn.on('open', async () => {\r\n try {\r\n if (isStopped()) return;\r\n\r\n // Send metadata with sessionId\r\n conn.send({\r\n t: 'meta',\r\n sessionId,\r\n name: file.name,\r\n size: file.size,\r\n mime: file.type || 'application/octet-stream',\r\n });\r\n\r\n const total = file.size;\r\n const dc = conn._dc;\r\n\r\n if (dc && Number.isFinite(bufferLowWaterMark)) {\r\n try {\r\n dc.bufferedAmountLowThreshold = bufferLowWaterMark;\r\n } catch {\r\n // Ignore threshold setting errors\r\n }\r\n }\r\n\r\n // Wait for ready signal\r\n await readyPromise;\r\n if (isStopped()) return;\r\n\r\n // Start heartbeat for long transfers\r\n if (heartbeatIntervalMs > 0) {\r\n heartbeatTimer = setInterval(() => {\r\n if (state === 'transferring' || state === 'finishing') {\r\n try {\r\n conn.send({ t: 'ping' });\r\n } catch {\r\n // Ignore ping errors\r\n }\r\n }\r\n }, heartbeatIntervalMs);\r\n }\r\n\r\n state = 'transferring';\r\n\r\n // Send file in chunks\r\n for (let offset = 0; offset < total; offset += chunkSize) {\r\n if (isStopped()) return;\r\n\r\n const slice = file.slice(offset, offset + chunkSize);\r\n const buf = await slice.arrayBuffer();\r\n conn.send(buf);\r\n sentBytes += buf.byteLength;\r\n\r\n // Flow control\r\n if (dc) {\r\n while (dc.bufferedAmount > bufferHighWaterMark) {\r\n await new Promise<void>((resolve) => {\r\n const fallback = setTimeout(resolve, 60);\r\n try {\r\n dc.addEventListener(\r\n 'bufferedamountlow',\r\n () => {\r\n clearTimeout(fallback);\r\n resolve();\r\n },\r\n { once: true }\r\n );\r\n } catch {\r\n // Fallback only\r\n }\r\n });\r\n }\r\n }\r\n }\r\n\r\n if (isStopped()) return;\r\n\r\n state = 'finishing';\r\n conn.send({ t: 'end' });\r\n\r\n // Wait for acknowledgment\r\n const ackTimeoutMs = Number.isFinite(endAckTimeoutMs)\r\n ? Math.max(endAckTimeoutMs, Math.ceil(file.size / (1024 * 1024)) * 1000)\r\n : null;\r\n\r\n const ackResult = await Promise.race([\r\n ackPromise,\r\n sleep(ackTimeoutMs || 15000).catch(() => null),\r\n ]);\r\n\r\n if (isStopped()) return;\r\n\r\n if (!ackResult || typeof ackResult !== 'object') {\r\n throw new DropgateNetworkError('Receiver did not confirm completion.');\r\n }\r\n\r\n const ackData = ackResult as { total?: number; received?: number };\r\n const ackTotal = Number(ackData.total) || file.size;\r\n const ackReceived = Number(ackData.received) || 0;\r\n\r\n if (ackTotal && ackReceived < ackTotal) {\r\n throw new DropgateNetworkError('Receiver reported an incomplete transfer.');\r\n }\r\n\r\n reportProgress({ received: ackReceived || ackTotal, total: ackTotal });\r\n safeComplete();\r\n } catch (err) {\r\n safeError(err as Error);\r\n }\r\n });\r\n\r\n conn.on('error', (err: Error) => {\r\n safeError(err);\r\n });\r\n\r\n conn.on('close', () => {\r\n if (state === 'closed' || state === 'completed') {\r\n // Clean shutdown, ensure full cleanup\r\n cleanup();\r\n return;\r\n }\r\n\r\n if (state === 'transferring' || state === 'finishing') {\r\n // Disconnected during transfer\r\n safeError(\r\n new DropgateNetworkError('Receiver disconnected before transfer completed.')\r\n );\r\n } else {\r\n // Disconnected before transfer started (during waiting/negotiating phase)\r\n // Reset state to allow reconnection\r\n activeConn = null;\r\n state = 'listening';\r\n sentBytes = 0;\r\n onDisconnect?.();\r\n }\r\n });\r\n });\r\n\r\n return {\r\n peer,\r\n code,\r\n sessionId,\r\n stop,\r\n getStatus: () => state,\r\n getBytesSent: () => sentBytes,\r\n getConnectedPeerId: () => {\r\n if (!activeConn) return null;\r\n // @ts-expect-error - peer property exists on PeerJS DataConnection\r\n return activeConn.peer || null;\r\n },\r\n };\r\n}\r\n","import { DropgateValidationError, DropgateNetworkError } from '../errors.js';\r\nimport type { P2PReceiveOptions, P2PReceiveSession, P2PReceiveState, DataConnection } from './types.js';\r\nimport { isP2PCodeLike } from './utils.js';\r\nimport { buildPeerOptions, resolvePeerConfig } from './helpers.js';\r\n\r\n/**\r\n * Start a direct transfer (P2P) receiver session.\r\n *\r\n * IMPORTANT: Consumer must provide the PeerJS Peer constructor and handle file writing.\r\n * This removes DOM coupling (no streamSaver).\r\n *\r\n * Example:\r\n * ```js\r\n * import Peer from 'peerjs';\r\n * import { startP2PReceive } from '@dropgate/core/p2p';\r\n *\r\n * let writer;\r\n * const session = await startP2PReceive({\r\n * code: 'ABCD-1234',\r\n * Peer,\r\n * host: 'dropgate.link',\r\n * secure: true,\r\n * onMeta: ({ name, total }) => {\r\n * // Consumer creates file writer\r\n * writer = createWriteStream(name);\r\n * },\r\n * onData: async (chunk) => {\r\n * // Consumer writes data\r\n * await writer.write(chunk);\r\n * },\r\n * onComplete: () => {\r\n * writer.close();\r\n * console.log('Done!');\r\n * },\r\n * });\r\n * ```\r\n */\r\nexport async function startP2PReceive(opts: P2PReceiveOptions): Promise<P2PReceiveSession> {\r\n const {\r\n code,\r\n Peer,\r\n serverInfo,\r\n host,\r\n port,\r\n peerjsPath,\r\n secure = false,\r\n iceServers,\r\n autoReady = true,\r\n watchdogTimeoutMs = 15000,\r\n onStatus,\r\n onMeta,\r\n onData,\r\n onProgress,\r\n onComplete,\r\n onError,\r\n onDisconnect,\r\n } = opts;\r\n\r\n // Validate required options\r\n if (!code) {\r\n throw new DropgateValidationError('No sharing code was provided.');\r\n }\r\n\r\n if (!Peer) {\r\n throw new DropgateValidationError(\r\n 'PeerJS Peer constructor is required. Install peerjs and pass it as the Peer option.'\r\n );\r\n }\r\n\r\n // Check P2P capabilities if serverInfo is provided\r\n const p2pCaps = serverInfo?.capabilities?.p2p;\r\n if (serverInfo && !p2pCaps?.enabled) {\r\n throw new DropgateValidationError('Direct transfer is disabled on this server.');\r\n }\r\n\r\n // Validate and normalize code\r\n const normalizedCode = String(code).trim().replace(/\\s+/g, '').toUpperCase();\r\n if (!isP2PCodeLike(normalizedCode)) {\r\n throw new DropgateValidationError('Invalid direct transfer code.');\r\n }\r\n\r\n // Resolve config from user options and server capabilities\r\n const { path: finalPath, iceServers: finalIceServers } = resolvePeerConfig(\r\n { peerjsPath, iceServers },\r\n p2pCaps\r\n );\r\n\r\n // Build peer options\r\n const peerOpts = buildPeerOptions({\r\n host,\r\n port,\r\n peerjsPath: finalPath,\r\n secure,\r\n iceServers: finalIceServers,\r\n });\r\n\r\n // Create peer (receiver doesn't need a specific ID)\r\n const peer = new Peer(undefined, peerOpts);\r\n\r\n // State machine - replaces boolean flags to prevent race conditions\r\n let state: P2PReceiveState = 'initializing';\r\n let total = 0;\r\n let received = 0;\r\n let currentSessionId: string | null = null;\r\n let lastProgressSentAt = 0;\r\n const progressIntervalMs = 120;\r\n let writeQueue = Promise.resolve();\r\n let watchdogTimer: ReturnType<typeof setTimeout> | null = null;\r\n let activeConn: DataConnection | null = null;\r\n\r\n // Watchdog - detects dead connections during transfer\r\n const resetWatchdog = (): void => {\r\n if (watchdogTimeoutMs <= 0) return;\r\n\r\n if (watchdogTimer) {\r\n clearTimeout(watchdogTimer);\r\n }\r\n\r\n watchdogTimer = setTimeout(() => {\r\n if (state === 'transferring') {\r\n safeError(new DropgateNetworkError('Connection timed out (no data received).'));\r\n }\r\n }, watchdogTimeoutMs);\r\n };\r\n\r\n const clearWatchdog = (): void => {\r\n if (watchdogTimer) {\r\n clearTimeout(watchdogTimer);\r\n watchdogTimer = null;\r\n }\r\n };\r\n\r\n // Safe error handler - prevents calling onError after completion\r\n const safeError = (err: Error): void => {\r\n if (state === 'closed' || state === 'completed') return;\r\n state = 'closed';\r\n onError?.(err);\r\n cleanup();\r\n };\r\n\r\n // Safe complete handler - only fires from transferring state\r\n const safeComplete = (completeData: { received: number; total: number }): void => {\r\n if (state !== 'transferring') return;\r\n state = 'completed';\r\n onComplete?.(completeData);\r\n cleanup();\r\n };\r\n\r\n // Cleanup all resources\r\n const cleanup = (): void => {\r\n clearWatchdog();\r\n\r\n // Remove beforeunload listener if in browser\r\n if (typeof window !== 'undefined') {\r\n window.removeEventListener('beforeunload', handleUnload);\r\n }\r\n\r\n try {\r\n peer.destroy();\r\n } catch {\r\n // Ignore destroy errors\r\n }\r\n };\r\n\r\n // Handle browser tab close/refresh\r\n const handleUnload = (): void => {\r\n try {\r\n activeConn?.send({ t: 'error', message: 'Receiver closed the connection.' });\r\n } catch {\r\n // Best effort\r\n }\r\n stop();\r\n };\r\n\r\n // Add beforeunload listener if in browser\r\n if (typeof window !== 'undefined') {\r\n window.addEventListener('beforeunload', handleUnload);\r\n }\r\n\r\n const stop = (): void => {\r\n if (state === 'closed') return;\r\n state = 'closed';\r\n cleanup();\r\n };\r\n\r\n peer.on('error', (err: Error) => {\r\n safeError(err);\r\n });\r\n\r\n peer.on('open', () => {\r\n state = 'connecting';\r\n const conn = peer.connect(normalizedCode, { reliable: true });\r\n activeConn = conn;\r\n\r\n conn.on('open', () => {\r\n state = 'negotiating';\r\n onStatus?.({ phase: 'connected', message: 'Waiting for file details...' });\r\n });\r\n\r\n conn.on('data', async (data: unknown) => {\r\n try {\r\n // Reset watchdog on any data received\r\n resetWatchdog();\r\n\r\n // Handle control messages\r\n if (\r\n data &&\r\n typeof data === 'object' &&\r\n !(data instanceof ArrayBuffer) &&\r\n !ArrayBuffer.isView(data)\r\n ) {\r\n const msg = data as {\r\n t?: string;\r\n sessionId?: string;\r\n name?: string;\r\n size?: number;\r\n message?: string;\r\n };\r\n\r\n if (msg.t === 'meta') {\r\n // Session ID validation - reject if we're busy with a different session\r\n if (currentSessionId && msg.sessionId && msg.sessionId !== currentSessionId) {\r\n try {\r\n conn.send({ t: 'error', message: 'Busy with another session.' });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n return;\r\n }\r\n\r\n // Store the session ID for this transfer\r\n if (msg.sessionId) {\r\n currentSessionId = msg.sessionId;\r\n }\r\n\r\n const name = String(msg.name || 'file');\r\n total = Number(msg.size) || 0;\r\n received = 0;\r\n writeQueue = Promise.resolve();\r\n\r\n // Function to send ready signal - called automatically if autoReady is true,\r\n // or passed to onMeta callback for manual invocation if autoReady is false\r\n const sendReady = (): void => {\r\n state = 'transferring';\r\n // Start watchdog once we're ready to receive data\r\n resetWatchdog();\r\n try {\r\n conn.send({ t: 'ready' });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n };\r\n\r\n if (autoReady) {\r\n onMeta?.({ name, total });\r\n onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });\r\n sendReady();\r\n } else {\r\n // Pass sendReady function to callback so consumer can trigger transfer start\r\n onMeta?.({ name, total, sendReady });\r\n onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });\r\n }\r\n return;\r\n }\r\n\r\n if (msg.t === 'ping') {\r\n // Respond to heartbeat - keeps watchdog alive and confirms we're active\r\n try {\r\n conn.send({ t: 'pong' });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n return;\r\n }\r\n\r\n if (msg.t === 'end') {\r\n clearWatchdog();\r\n await writeQueue;\r\n\r\n if (total && received < total) {\r\n const err = new DropgateNetworkError(\r\n 'Transfer ended before the full file was received.'\r\n );\r\n try {\r\n conn.send({ t: 'error', message: err.message });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n throw err;\r\n }\r\n\r\n try {\r\n conn.send({ t: 'ack', phase: 'end', received, total });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n\r\n safeComplete({ received, total });\r\n return;\r\n }\r\n\r\n if (msg.t === 'error') {\r\n throw new DropgateNetworkError(msg.message || 'Sender reported an error.');\r\n }\r\n\r\n return;\r\n }\r\n\r\n // Handle binary data\r\n let bufPromise: Promise<Uint8Array>;\r\n\r\n if (data instanceof ArrayBuffer) {\r\n bufPromise = Promise.resolve(new Uint8Array(data));\r\n } else if (ArrayBuffer.isView(data)) {\r\n bufPromise = Promise.resolve(\r\n new Uint8Array(data.buffer, data.byteOffset, data.byteLength)\r\n );\r\n } else if (typeof Blob !== 'undefined' && data instanceof Blob) {\r\n bufPromise = data.arrayBuffer().then((buffer) => new Uint8Array(buffer));\r\n } else {\r\n return;\r\n }\r\n\r\n writeQueue = writeQueue\r\n .then(async () => {\r\n const buf = await bufPromise;\r\n\r\n // Call consumer's onData handler\r\n if (onData) {\r\n await onData(buf);\r\n }\r\n\r\n received += buf.byteLength;\r\n const percent = total ? Math.min(100, (received / total) * 100) : 0;\r\n onProgress?.({ processedBytes: received, totalBytes: total, percent });\r\n\r\n const now = Date.now();\r\n if (received === total || now - lastProgressSentAt >= progressIntervalMs) {\r\n lastProgressSentAt = now;\r\n try {\r\n conn.send({ t: 'progress', received, total });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n }\r\n })\r\n .catch((err) => {\r\n try {\r\n conn.send({\r\n t: 'error',\r\n message: (err as Error)?.message || 'Receiver write failed.',\r\n });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n safeError(err as Error);\r\n });\r\n } catch (err) {\r\n safeError(err as Error);\r\n }\r\n });\r\n\r\n conn.on('close', () => {\r\n if (state === 'closed' || state === 'completed') {\r\n // Clean shutdown, ensure full cleanup\r\n cleanup();\r\n return;\r\n }\r\n\r\n // Sender disconnected before transfer completed\r\n if (state === 'transferring') {\r\n // We were mid-transfer\r\n safeError(new DropgateNetworkError('Sender disconnected during transfer.'));\r\n } else if (state === 'negotiating') {\r\n // We had metadata but transfer hadn't started\r\n state = 'closed';\r\n cleanup();\r\n onDisconnect?.();\r\n } else {\r\n // Disconnected before we even got file metadata\r\n safeError(new DropgateNetworkError('Sender disconnected before file details were received.'));\r\n }\r\n });\r\n });\r\n\r\n return {\r\n peer,\r\n stop,\r\n getStatus: () => state,\r\n getBytesReceived: () => received,\r\n getTotalBytes: () => total,\r\n getSessionId: () => currentSessionId,\r\n };\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACSO,IAAM,gBAAN,cAA4B,MAAM;AAAA,EAIvC,YAAY,SAAiB,OAA6B,CAAC,GAAG;AAC5D,UAAM,OAAO;AACb,SAAK,OAAO,KAAK,YAAY;AAC7B,SAAK,OAAO,KAAK,QAAQ;AACzB,SAAK,UAAU,KAAK;AACpB,QAAI,KAAK,UAAU,QAAW;AAE5B,aAAO,eAAe,MAAM,SAAS;AAAA,QACnC,OAAO,KAAK;AAAA,QACZ,UAAU;AAAA,QACV,YAAY;AAAA,QACZ,cAAc;AAAA,MAChB,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAKO,IAAM,0BAAN,cAAsC,cAAc;AAAA,EACzD,YAAY,SAAiB,OAA6B,CAAC,GAAG;AAC5D,UAAM,SAAS,EAAE,GAAG,MAAM,MAAM,KAAK,QAAQ,mBAAmB,CAAC;AAAA,EACnE;AACF;AAKO,IAAM,uBAAN,cAAmC,cAAc;AAAA,EACtD,YAAY,SAAiB,OAA6B,CAAC,GAAG;AAC5D,UAAM,SAAS,EAAE,GAAG,MAAM,MAAM,KAAK,QAAQ,gBAAgB,CAAC;AAAA,EAChE;AACF;AAeO,IAAM,qBAAN,cAAiC,cAAc;AAAA,EACpD,YAAY,UAAU,qBAAqB;AACzC,UAAM,SAAS,EAAE,MAAM,cAAc,CAAC;AACtC,SAAK,OAAO;AAAA,EACd;AACF;;;AC3BO,SAAS,MAAM,IAAY,QAAqC;AACrE,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,QAAQ,SAAS;AACnB,aAAO,OAAO,OAAO,UAAU,IAAI,mBAAmB,CAAC;AAAA,IACzD;AAEA,UAAM,IAAI,WAAW,SAAS,EAAE;AAEhC,QAAI,QAAQ;AACV,aAAO;AAAA,QACL;AAAA,QACA,MAAM;AACJ,uBAAa,CAAC;AACd,iBAAO,OAAO,UAAU,IAAI,mBAAmB,CAAC;AAAA,QAClD;AAAA,QACA,EAAE,MAAM,KAAK;AAAA,MACf;AAAA,IACF;AAAA,EACF,CAAC;AACH;;;ACTO,SAAS,mBAA8C;AAC5D,SAAO,WAAW;AACpB;;;AC7CO,SAAS,oBAAoB,UAA2B;AAC7D,QAAM,OAAO,OAAO,YAAY,EAAE,EAAE,YAAY;AAChD,SAAO,SAAS,eAAe,SAAS,eAAe,SAAS;AAClE;AAKO,SAAS,sBACd,UACA,iBACS;AACT,SAAO,QAAQ,eAAe,KAAK,oBAAoB,YAAY,EAAE;AACvE;AAMO,SAAS,gBAAgB,WAAmC;AACjE,QAAMA,UAAS,aAAa,iBAAiB;AAC7C,QAAM,UAAU;AAEhB,MAAIA,SAAQ;AACV,UAAM,cAAc,IAAI,WAAW,CAAC;AACpC,IAAAA,QAAO,gBAAgB,WAAW;AAElC,QAAI,aAAa;AACjB,aAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,oBAAc,QAAQ,YAAY,CAAC,IAAI,QAAQ,MAAM;AAAA,IACvD;AAEA,QAAI,aAAa;AACjB,aAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,qBAAe,YAAY,CAAC,IAAI,IAAI,SAAS;AAAA,IAC/C;AAEA,WAAO,GAAG,UAAU,IAAI,UAAU;AAAA,EACpC;AAGA,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,SAAK,QAAQ,KAAK,MAAM,KAAK,OAAO,IAAI,QAAQ,MAAM,CAAC;AAAA,EACzD;AACA,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,SAAK,KAAK,MAAM,KAAK,OAAO,IAAI,EAAE;AAAA,EACpC;AACA,SAAO,GAAG,CAAC,IAAI,CAAC;AAClB;AAKO,SAAS,cAAc,MAAuB;AACnD,SAAO,mBAAmB,KAAK,OAAO,QAAQ,EAAE,EAAE,KAAK,CAAC;AAC1D;;;ACvDO,SAAS,kBACd,YACA,YAC8C;AAC9C,SAAO;AAAA,IACL,MAAM,WAAW,cAAc,YAAY,cAAc;AAAA,IACzD,YAAY,WAAW,cAAc,YAAY,cAAc,CAAC;AAAA,EAClE;AACF;AAKO,SAAS,iBAAiB,SAA0B,CAAC,GAAgB;AAC1E,QAAM,EAAE,MAAM,MAAM,aAAa,WAAW,SAAS,OAAO,aAAa,CAAC,EAAE,IAAI;AAEhF,QAAM,WAAwB;AAAA,IAC5B;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA,QAAQ,EAAE,WAAW;AAAA,IACrB,OAAO;AAAA,EACT;AAEA,MAAI,MAAM;AACR,aAAS,OAAO;AAAA,EAClB;AAEA,SAAO;AACT;AAaA,eAAsB,sBACpB,MAC+C;AAC/C,QAAM,EAAE,MAAM,eAAe,aAAa,WAAW,OAAO,IAAI;AAEhE,MAAI,WAAW,QAAQ,cAAc;AACrC,MAAI,OAA4B;AAChC,MAAI,YAA0B;AAE9B,WAAS,UAAU,GAAG,UAAU,aAAa,WAAW;AACtD,aAAS,UAAU,OAAO;AAE1B,QAAI;AACF,aAAO,MAAM,IAAI,QAAsB,CAAC,SAAS,WAAW;AAC1D,cAAM,WAAW,UAAU,QAAQ;AACnC,iBAAS,GAAG,QAAQ,MAAM,QAAQ,QAAQ,CAAC;AAC3C,iBAAS,GAAG,SAAS,CAAC,QAAe;AACnC,cAAI;AACF,qBAAS,QAAQ;AAAA,UACnB,QAAQ;AAAA,UAER;AACA,iBAAO,GAAG;AAAA,QACZ,CAAC;AAAA,MACH,CAAC;AAED,aAAO,EAAE,MAAM,MAAM,SAAS;AAAA,IAChC,SAAS,KAAK;AACZ,kBAAY;AACZ,iBAAW,cAAc;AAAA,IAC3B;AAAA,EACF;AAEA,QAAM,aAAa,IAAI,qBAAqB,wCAAwC;AACtF;;;AC1EA,SAAS,oBAA4B;AACnC,MAAI,OAAO,WAAW,eAAe,OAAO,YAAY;AACtD,WAAO,OAAO,WAAW;AAAA,EAC3B;AAEA,SAAO,GAAG,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,UAAU,GAAG,EAAE,CAAC;AACrE;AAwBA,eAAsB,aAAa,MAA+C;AAChF,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd,YAAY,MAAM;AAAA,IAClB,kBAAkB;AAAA,IAClB,sBAAsB,IAAI,OAAO;AAAA,IACjC,qBAAqB,IAAI,OAAO;AAAA,IAChC,sBAAsB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAGJ,MAAI,CAAC,MAAM;AACT,UAAM,IAAI,wBAAwB,kBAAkB;AAAA,EACtD;AAEA,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAGA,QAAM,UAAU,YAAY,cAAc;AAC1C,MAAI,cAAc,CAAC,SAAS,SAAS;AACnC,UAAM,IAAI,wBAAwB,6CAA6C;AAAA,EACjF;AAGA,QAAM,EAAE,MAAM,WAAW,YAAY,gBAAgB,IAAI;AAAA,IACvD,EAAE,YAAY,WAAW;AAAA,IACzB;AAAA,EACF;AAGA,QAAM,WAAW,iBAAiB;AAAA,IAChC;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ;AAAA,IACA,YAAY;AAAA,EACd,CAAC;AAGD,QAAM,qBAAqB,kBAAkB,MAAM,gBAAgB,SAAS;AAG5E,QAAM,YAAY,CAAC,OAAe,IAAI,KAAK,IAAI,QAAQ;AACvD,QAAM,EAAE,MAAM,KAAK,IAAI,MAAM,sBAAsB;AAAA,IACjD,MAAM;AAAA,IACN,eAAe;AAAA,IACf;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAGD,QAAM,YAAY,kBAAkB;AAGpC,MAAI,QAAsB;AAC1B,MAAI,aAAoC;AACxC,MAAI,YAAY;AAChB,MAAI,iBAAwD;AAE5D,QAAM,iBAAiB,CAAC,SAAoD;AAC1E,UAAM,YACJ,OAAO,SAAS,KAAK,KAAK,KAAK,KAAK,QAAQ,IAAI,KAAK,QAAQ,KAAK;AACpE,UAAM,eAAe,KAAK,IAAI,OAAO,KAAK,QAAQ,KAAK,GAAG,aAAa,CAAC;AACxE,UAAM,UAAU,YAAa,eAAe,YAAa,MAAM;AAC/D,iBAAa,EAAE,gBAAgB,cAAc,YAAY,WAAW,QAAQ,CAAC;AAAA,EAC/E;AAGA,QAAM,YAAY,CAAC,QAAqB;AACtC,QAAI,UAAU,YAAY,UAAU,YAAa;AACjD,YAAQ;AACR,cAAU,GAAG;AACb,YAAQ;AAAA,EACV;AAGA,QAAM,eAAe,MAAY;AAC/B,QAAI,UAAU,YAAa;AAC3B,YAAQ;AACR,iBAAa;AACb,YAAQ;AAAA,EACV;AAGA,QAAM,UAAU,MAAY;AAE1B,QAAI,gBAAgB;AAClB,oBAAc,cAAc;AAC5B,uBAAiB;AAAA,IACnB;AAGA,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,oBAAoB,gBAAgB,YAAY;AAAA,IACzD;AAEA,QAAI;AACF,kBAAY,MAAM;AAAA,IACpB,QAAQ;AAAA,IAER;AACA,QAAI;AACF,WAAK,QAAQ;AAAA,IACf,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,eAAe,MAAY;AAC/B,QAAI;AACF,kBAAY,KAAK,EAAE,GAAG,SAAS,SAAS,gCAAgC,CAAC;AAAA,IAC3E,QAAQ;AAAA,IAER;AACA,SAAK;AAAA,EACP;AAGA,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO,iBAAiB,gBAAgB,YAAY;AAAA,EACtD;AAEA,QAAM,OAAO,MAAY;AACvB,QAAI,UAAU,SAAU;AACxB,YAAQ;AACR,YAAQ;AAAA,EACV;AAIA,QAAM,YAAY,MAAe,UAAU;AAE3C,OAAK,GAAG,cAAc,CAAC,SAAyB;AAC9C,QAAI,UAAU,SAAU;AAGxB,QAAI,YAAY;AAGd,YAAM,gBAAgB,WAAW,SAAS;AAE1C,UAAI,iBAAiB,UAAU,gBAAgB;AAE7C,YAAI;AACF,eAAK,KAAK,EAAE,GAAG,SAAS,SAAS,gCAAgC,CAAC;AAAA,QACpE,QAAQ;AAAA,QAER;AACA,YAAI;AACF,eAAK,MAAM;AAAA,QACb,QAAQ;AAAA,QAER;AACA;AAAA,MACF,WAAW,CAAC,eAAe;AAEzB,YAAI;AACF,qBAAW,MAAM;AAAA,QACnB,QAAQ;AAAA,QAER;AACA,qBAAa;AAEb,gBAAQ;AACR,oBAAY;AAAA,MACd,OAAO;AAGL,YAAI;AACF,eAAK,KAAK,EAAE,GAAG,SAAS,SAAS,yCAAyC,CAAC;AAAA,QAC7E,QAAQ;AAAA,QAER;AACA,YAAI;AACF,eAAK,MAAM;AAAA,QACb,QAAQ;AAAA,QAER;AACA;AAAA,MACF;AAAA,IACF;AAEA,iBAAa;AACb,YAAQ;AACR,eAAW,EAAE,OAAO,WAAW,SAAS,+CAA+C,CAAC;AAExF,QAAI,eAAoC;AACxC,QAAI,aAA+C;AAEnD,UAAM,eAAe,IAAI,QAAc,CAAC,YAAY;AAClD,qBAAe;AAAA,IACjB,CAAC;AAED,UAAM,aAAa,IAAI,QAAiB,CAAC,YAAY;AACnD,mBAAa;AAAA,IACf,CAAC;AAED,SAAK,GAAG,QAAQ,CAAC,SAAkB;AACjC,UACE,CAAC,QACD,OAAO,SAAS,YAChB,gBAAgB,eAChB,YAAY,OAAO,IAAI,GACvB;AACA;AAAA,MACF;AAEA,YAAM,MAAM;AACZ,UAAI,CAAC,IAAI,EAAG;AAEZ,UAAI,IAAI,MAAM,SAAS;AACrB,mBAAW,EAAE,OAAO,gBAAgB,SAAS,0CAA0C,CAAC;AACxF,uBAAe;AACf;AAAA,MACF;AAEA,UAAI,IAAI,MAAM,YAAY;AACxB,uBAAe,EAAE,UAAU,IAAI,YAAY,GAAG,OAAO,IAAI,SAAS,EAAE,CAAC;AACrE;AAAA,MACF;AAEA,UAAI,IAAI,MAAM,SAAS,IAAI,UAAU,OAAO;AAC1C,qBAAa,GAAG;AAChB;AAAA,MACF;AAEA,UAAI,IAAI,MAAM,QAAQ;AAEpB;AAAA,MACF;AAEA,UAAI,IAAI,MAAM,SAAS;AACrB,kBAAU,IAAI,qBAAqB,IAAI,WAAW,6BAA6B,CAAC;AAAA,MAClF;AAAA,IACF,CAAC;AAED,SAAK,GAAG,QAAQ,YAAY;AAC1B,UAAI;AACF,YAAI,UAAU,EAAG;AAGjB,aAAK,KAAK;AAAA,UACR,GAAG;AAAA,UACH;AAAA,UACA,MAAM,KAAK;AAAA,UACX,MAAM,KAAK;AAAA,UACX,MAAM,KAAK,QAAQ;AAAA,QACrB,CAAC;AAED,cAAM,QAAQ,KAAK;AACnB,cAAM,KAAK,KAAK;AAEhB,YAAI,MAAM,OAAO,SAAS,kBAAkB,GAAG;AAC7C,cAAI;AACF,eAAG,6BAA6B;AAAA,UAClC,QAAQ;AAAA,UAER;AAAA,QACF;AAGA,cAAM;AACN,YAAI,UAAU,EAAG;AAGjB,YAAI,sBAAsB,GAAG;AAC3B,2BAAiB,YAAY,MAAM;AACjC,gBAAI,UAAU,kBAAkB,UAAU,aAAa;AACrD,kBAAI;AACF,qBAAK,KAAK,EAAE,GAAG,OAAO,CAAC;AAAA,cACzB,QAAQ;AAAA,cAER;AAAA,YACF;AAAA,UACF,GAAG,mBAAmB;AAAA,QACxB;AAEA,gBAAQ;AAGR,iBAAS,SAAS,GAAG,SAAS,OAAO,UAAU,WAAW;AACxD,cAAI,UAAU,EAAG;AAEjB,gBAAM,QAAQ,KAAK,MAAM,QAAQ,SAAS,SAAS;AACnD,gBAAM,MAAM,MAAM,MAAM,YAAY;AACpC,eAAK,KAAK,GAAG;AACb,uBAAa,IAAI;AAGjB,cAAI,IAAI;AACN,mBAAO,GAAG,iBAAiB,qBAAqB;AAC9C,oBAAM,IAAI,QAAc,CAAC,YAAY;AACnC,sBAAM,WAAW,WAAW,SAAS,EAAE;AACvC,oBAAI;AACF,qBAAG;AAAA,oBACD;AAAA,oBACA,MAAM;AACJ,mCAAa,QAAQ;AACrB,8BAAQ;AAAA,oBACV;AAAA,oBACA,EAAE,MAAM,KAAK;AAAA,kBACf;AAAA,gBACF,QAAQ;AAAA,gBAER;AAAA,cACF,CAAC;AAAA,YACH;AAAA,UACF;AAAA,QACF;AAEA,YAAI,UAAU,EAAG;AAEjB,gBAAQ;AACR,aAAK,KAAK,EAAE,GAAG,MAAM,CAAC;AAGtB,cAAM,eAAe,OAAO,SAAS,eAAe,IAChD,KAAK,IAAI,iBAAiB,KAAK,KAAK,KAAK,QAAQ,OAAO,KAAK,IAAI,GAAI,IACrE;AAEJ,cAAM,YAAY,MAAM,QAAQ,KAAK;AAAA,UACnC;AAAA,UACA,MAAM,gBAAgB,IAAK,EAAE,MAAM,MAAM,IAAI;AAAA,QAC/C,CAAC;AAED,YAAI,UAAU,EAAG;AAEjB,YAAI,CAAC,aAAa,OAAO,cAAc,UAAU;AAC/C,gBAAM,IAAI,qBAAqB,sCAAsC;AAAA,QACvE;AAEA,cAAM,UAAU;AAChB,cAAM,WAAW,OAAO,QAAQ,KAAK,KAAK,KAAK;AAC/C,cAAM,cAAc,OAAO,QAAQ,QAAQ,KAAK;AAEhD,YAAI,YAAY,cAAc,UAAU;AACtC,gBAAM,IAAI,qBAAqB,2CAA2C;AAAA,QAC5E;AAEA,uBAAe,EAAE,UAAU,eAAe,UAAU,OAAO,SAAS,CAAC;AACrE,qBAAa;AAAA,MACf,SAAS,KAAK;AACZ,kBAAU,GAAY;AAAA,MACxB;AAAA,IACF,CAAC;AAED,SAAK,GAAG,SAAS,CAAC,QAAe;AAC/B,gBAAU,GAAG;AAAA,IACf,CAAC;AAED,SAAK,GAAG,SAAS,MAAM;AACrB,UAAI,UAAU,YAAY,UAAU,aAAa;AAE/C,gBAAQ;AACR;AAAA,MACF;AAEA,UAAI,UAAU,kBAAkB,UAAU,aAAa;AAErD;AAAA,UACE,IAAI,qBAAqB,kDAAkD;AAAA,QAC7E;AAAA,MACF,OAAO;AAGL,qBAAa;AACb,gBAAQ;AACR,oBAAY;AACZ,uBAAe;AAAA,MACjB;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,MAAM;AAAA,IACjB,cAAc,MAAM;AAAA,IACpB,oBAAoB,MAAM;AACxB,UAAI,CAAC,WAAY,QAAO;AAExB,aAAO,WAAW,QAAQ;AAAA,IAC5B;AAAA,EACF;AACF;;;AC3ZA,eAAsB,gBAAgB,MAAqD;AACzF,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA,YAAY;AAAA,IACZ,oBAAoB;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAGJ,MAAI,CAAC,MAAM;AACT,UAAM,IAAI,wBAAwB,+BAA+B;AAAA,EACnE;AAEA,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAGA,QAAM,UAAU,YAAY,cAAc;AAC1C,MAAI,cAAc,CAAC,SAAS,SAAS;AACnC,UAAM,IAAI,wBAAwB,6CAA6C;AAAA,EACjF;AAGA,QAAM,iBAAiB,OAAO,IAAI,EAAE,KAAK,EAAE,QAAQ,QAAQ,EAAE,EAAE,YAAY;AAC3E,MAAI,CAAC,cAAc,cAAc,GAAG;AAClC,UAAM,IAAI,wBAAwB,+BAA+B;AAAA,EACnE;AAGA,QAAM,EAAE,MAAM,WAAW,YAAY,gBAAgB,IAAI;AAAA,IACvD,EAAE,YAAY,WAAW;AAAA,IACzB;AAAA,EACF;AAGA,QAAM,WAAW,iBAAiB;AAAA,IAChC;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ;AAAA,IACA,YAAY;AAAA,EACd,CAAC;AAGD,QAAM,OAAO,IAAI,KAAK,QAAW,QAAQ;AAGzC,MAAI,QAAyB;AAC7B,MAAI,QAAQ;AACZ,MAAI,WAAW;AACf,MAAI,mBAAkC;AACtC,MAAI,qBAAqB;AACzB,QAAM,qBAAqB;AAC3B,MAAI,aAAa,QAAQ,QAAQ;AACjC,MAAI,gBAAsD;AAC1D,MAAI,aAAoC;AAGxC,QAAM,gBAAgB,MAAY;AAChC,QAAI,qBAAqB,EAAG;AAE5B,QAAI,eAAe;AACjB,mBAAa,aAAa;AAAA,IAC5B;AAEA,oBAAgB,WAAW,MAAM;AAC/B,UAAI,UAAU,gBAAgB;AAC5B,kBAAU,IAAI,qBAAqB,0CAA0C,CAAC;AAAA,MAChF;AAAA,IACF,GAAG,iBAAiB;AAAA,EACtB;AAEA,QAAM,gBAAgB,MAAY;AAChC,QAAI,eAAe;AACjB,mBAAa,aAAa;AAC1B,sBAAgB;AAAA,IAClB;AAAA,EACF;AAGA,QAAM,YAAY,CAAC,QAAqB;AACtC,QAAI,UAAU,YAAY,UAAU,YAAa;AACjD,YAAQ;AACR,cAAU,GAAG;AACb,YAAQ;AAAA,EACV;AAGA,QAAM,eAAe,CAAC,iBAA4D;AAChF,QAAI,UAAU,eAAgB;AAC9B,YAAQ;AACR,iBAAa,YAAY;AACzB,YAAQ;AAAA,EACV;AAGA,QAAM,UAAU,MAAY;AAC1B,kBAAc;AAGd,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,oBAAoB,gBAAgB,YAAY;AAAA,IACzD;AAEA,QAAI;AACF,WAAK,QAAQ;AAAA,IACf,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,eAAe,MAAY;AAC/B,QAAI;AACF,kBAAY,KAAK,EAAE,GAAG,SAAS,SAAS,kCAAkC,CAAC;AAAA,IAC7E,QAAQ;AAAA,IAER;AACA,SAAK;AAAA,EACP;AAGA,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO,iBAAiB,gBAAgB,YAAY;AAAA,EACtD;AAEA,QAAM,OAAO,MAAY;AACvB,QAAI,UAAU,SAAU;AACxB,YAAQ;AACR,YAAQ;AAAA,EACV;AAEA,OAAK,GAAG,SAAS,CAAC,QAAe;AAC/B,cAAU,GAAG;AAAA,EACf,CAAC;AAED,OAAK,GAAG,QAAQ,MAAM;AACpB,YAAQ;AACR,UAAM,OAAO,KAAK,QAAQ,gBAAgB,EAAE,UAAU,KAAK,CAAC;AAC5D,iBAAa;AAEb,SAAK,GAAG,QAAQ,MAAM;AACpB,cAAQ;AACR,iBAAW,EAAE,OAAO,aAAa,SAAS,8BAA8B,CAAC;AAAA,IAC3E,CAAC;AAED,SAAK,GAAG,QAAQ,OAAO,SAAkB;AACvC,UAAI;AAEF,sBAAc;AAGd,YACE,QACA,OAAO,SAAS,YAChB,EAAE,gBAAgB,gBAClB,CAAC,YAAY,OAAO,IAAI,GACxB;AACA,gBAAM,MAAM;AAQZ,cAAI,IAAI,MAAM,QAAQ;AAEpB,gBAAI,oBAAoB,IAAI,aAAa,IAAI,cAAc,kBAAkB;AAC3E,kBAAI;AACF,qBAAK,KAAK,EAAE,GAAG,SAAS,SAAS,6BAA6B,CAAC;AAAA,cACjE,QAAQ;AAAA,cAER;AACA;AAAA,YACF;AAGA,gBAAI,IAAI,WAAW;AACjB,iCAAmB,IAAI;AAAA,YACzB;AAEA,kBAAM,OAAO,OAAO,IAAI,QAAQ,MAAM;AACtC,oBAAQ,OAAO,IAAI,IAAI,KAAK;AAC5B,uBAAW;AACX,yBAAa,QAAQ,QAAQ;AAI7B,kBAAM,YAAY,MAAY;AAC5B,sBAAQ;AAER,4BAAc;AACd,kBAAI;AACF,qBAAK,KAAK,EAAE,GAAG,QAAQ,CAAC;AAAA,cAC1B,QAAQ;AAAA,cAER;AAAA,YACF;AAEA,gBAAI,WAAW;AACb,uBAAS,EAAE,MAAM,MAAM,CAAC;AACxB,2BAAa,EAAE,gBAAgB,UAAU,YAAY,OAAO,SAAS,EAAE,CAAC;AACxE,wBAAU;AAAA,YACZ,OAAO;AAEL,uBAAS,EAAE,MAAM,OAAO,UAAU,CAAC;AACnC,2BAAa,EAAE,gBAAgB,UAAU,YAAY,OAAO,SAAS,EAAE,CAAC;AAAA,YAC1E;AACA;AAAA,UACF;AAEA,cAAI,IAAI,MAAM,QAAQ;AAEpB,gBAAI;AACF,mBAAK,KAAK,EAAE,GAAG,OAAO,CAAC;AAAA,YACzB,QAAQ;AAAA,YAER;AACA;AAAA,UACF;AAEA,cAAI,IAAI,MAAM,OAAO;AACnB,0BAAc;AACd,kBAAM;AAEN,gBAAI,SAAS,WAAW,OAAO;AAC7B,oBAAM,MAAM,IAAI;AAAA,gBACd;AAAA,cACF;AACA,kBAAI;AACF,qBAAK,KAAK,EAAE,GAAG,SAAS,SAAS,IAAI,QAAQ,CAAC;AAAA,cAChD,QAAQ;AAAA,cAER;AACA,oBAAM;AAAA,YACR;AAEA,gBAAI;AACF,mBAAK,KAAK,EAAE,GAAG,OAAO,OAAO,OAAO,UAAU,MAAM,CAAC;AAAA,YACvD,QAAQ;AAAA,YAER;AAEA,yBAAa,EAAE,UAAU,MAAM,CAAC;AAChC;AAAA,UACF;AAEA,cAAI,IAAI,MAAM,SAAS;AACrB,kBAAM,IAAI,qBAAqB,IAAI,WAAW,2BAA2B;AAAA,UAC3E;AAEA;AAAA,QACF;AAGA,YAAI;AAEJ,YAAI,gBAAgB,aAAa;AAC/B,uBAAa,QAAQ,QAAQ,IAAI,WAAW,IAAI,CAAC;AAAA,QACnD,WAAW,YAAY,OAAO,IAAI,GAAG;AACnC,uBAAa,QAAQ;AAAA,YACnB,IAAI,WAAW,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU;AAAA,UAC9D;AAAA,QACF,WAAW,OAAO,SAAS,eAAe,gBAAgB,MAAM;AAC9D,uBAAa,KAAK,YAAY,EAAE,KAAK,CAAC,WAAW,IAAI,WAAW,MAAM,CAAC;AAAA,QACzE,OAAO;AACL;AAAA,QACF;AAEA,qBAAa,WACV,KAAK,YAAY;AAChB,gBAAM,MAAM,MAAM;AAGlB,cAAI,QAAQ;AACV,kBAAM,OAAO,GAAG;AAAA,UAClB;AAEA,sBAAY,IAAI;AAChB,gBAAM,UAAU,QAAQ,KAAK,IAAI,KAAM,WAAW,QAAS,GAAG,IAAI;AAClE,uBAAa,EAAE,gBAAgB,UAAU,YAAY,OAAO,QAAQ,CAAC;AAErE,gBAAM,MAAM,KAAK,IAAI;AACrB,cAAI,aAAa,SAAS,MAAM,sBAAsB,oBAAoB;AACxE,iCAAqB;AACrB,gBAAI;AACF,mBAAK,KAAK,EAAE,GAAG,YAAY,UAAU,MAAM,CAAC;AAAA,YAC9C,QAAQ;AAAA,YAER;AAAA,UACF;AAAA,QACF,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,cAAI;AACF,iBAAK,KAAK;AAAA,cACR,GAAG;AAAA,cACH,SAAU,KAAe,WAAW;AAAA,YACtC,CAAC;AAAA,UACH,QAAQ;AAAA,UAER;AACA,oBAAU,GAAY;AAAA,QACxB,CAAC;AAAA,MACL,SAAS,KAAK;AACZ,kBAAU,GAAY;AAAA,MACxB;AAAA,IACF,CAAC;AAED,SAAK,GAAG,SAAS,MAAM;AACrB,UAAI,UAAU,YAAY,UAAU,aAAa;AAE/C,gBAAQ;AACR;AAAA,MACF;AAGA,UAAI,UAAU,gBAAgB;AAE5B,kBAAU,IAAI,qBAAqB,sCAAsC,CAAC;AAAA,MAC5E,WAAW,UAAU,eAAe;AAElC,gBAAQ;AACR,gBAAQ;AACR,uBAAe;AAAA,MACjB,OAAO;AAEL,kBAAU,IAAI,qBAAqB,wDAAwD,CAAC;AAAA,MAC9F;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,WAAW,MAAM;AAAA,IACjB,kBAAkB,MAAM;AAAA,IACxB,eAAe,MAAM;AAAA,IACrB,cAAc,MAAM;AAAA,EACtB;AACF;","names":["crypto"]}
1
+ {"version":3,"sources":["../../src/p2p/index.ts","../../src/errors.ts","../../src/utils/network.ts","../../src/adapters/defaults.ts","../../src/p2p/utils.ts","../../src/p2p/helpers.ts","../../src/p2p/send.ts","../../src/p2p/receive.ts"],"sourcesContent":["// P2P module exports\r\nexport { startP2PSend } from './send.js';\r\nexport { startP2PReceive } from './receive.js';\r\nexport { generateP2PCode, isP2PCodeLike, isLocalhostHostname, isSecureContextForP2P } from './utils.js';\r\nexport { buildPeerOptions, createPeerWithRetries, resolvePeerConfig } from './helpers.js';\r\n\r\n// Re-export types\r\nexport type {\r\n // State machine types\r\n P2PSendState,\r\n P2PReceiveState,\r\n // PeerJS types\r\n PeerConstructor,\r\n PeerInstance,\r\n PeerInstanceEvents,\r\n PeerOptions,\r\n DataConnection,\r\n DataConnectionEvents,\r\n // P2P config\r\n P2PServerConfig,\r\n // P2P event types\r\n P2PStatusEvent,\r\n P2PSendProgressEvent,\r\n P2PReceiveProgressEvent,\r\n P2PMetadataEvent,\r\n P2PReceiveCompleteEvent,\r\n // P2P options and sessions\r\n P2PSendOptions,\r\n P2PSendSession,\r\n P2PReceiveOptions,\r\n P2PReceiveSession,\r\n} from './types.js';\r\n","export interface DropgateErrorOptions {\r\n code?: string;\r\n details?: unknown;\r\n cause?: unknown;\r\n}\r\n\r\n/**\r\n * Base error class for all Dropgate errors\r\n */\r\nexport class DropgateError extends Error {\r\n readonly code: string;\r\n readonly details?: unknown;\r\n\r\n constructor(message: string, opts: DropgateErrorOptions = {}) {\r\n super(message);\r\n this.name = this.constructor.name;\r\n this.code = opts.code || 'DROPGATE_ERROR';\r\n this.details = opts.details;\r\n if (opts.cause !== undefined) {\r\n // Use Object.defineProperty for cause to maintain compatibility\r\n Object.defineProperty(this, 'cause', {\r\n value: opts.cause,\r\n writable: false,\r\n enumerable: false,\r\n configurable: true,\r\n });\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Validation error for invalid inputs\r\n */\r\nexport class DropgateValidationError extends DropgateError {\r\n constructor(message: string, opts: DropgateErrorOptions = {}) {\r\n super(message, { ...opts, code: opts.code || 'VALIDATION_ERROR' });\r\n }\r\n}\r\n\r\n/**\r\n * Network error for connection issues\r\n */\r\nexport class DropgateNetworkError extends DropgateError {\r\n constructor(message: string, opts: DropgateErrorOptions = {}) {\r\n super(message, { ...opts, code: opts.code || 'NETWORK_ERROR' });\r\n }\r\n}\r\n\r\n/**\r\n * Protocol error for server communication issues\r\n */\r\nexport class DropgateProtocolError extends DropgateError {\r\n constructor(message: string, opts: DropgateErrorOptions = {}) {\r\n super(message, { ...opts, code: opts.code || 'PROTOCOL_ERROR' });\r\n }\r\n}\r\n\r\n/**\r\n * Abort error - replacement for DOMException with AbortError name\r\n * Used when operations are cancelled\r\n */\r\nexport class DropgateAbortError extends DropgateError {\r\n constructor(message = 'Operation aborted') {\r\n super(message, { code: 'ABORT_ERROR' });\r\n this.name = 'AbortError';\r\n }\r\n}\r\n\r\n/**\r\n * Timeout error - replacement for DOMException with TimeoutError name\r\n * Used when operations exceed their time limit\r\n */\r\nexport class DropgateTimeoutError extends DropgateError {\r\n constructor(message = 'Request timed out') {\r\n super(message, { code: 'TIMEOUT_ERROR' });\r\n this.name = 'TimeoutError';\r\n }\r\n}\r\n","import { DropgateAbortError, DropgateTimeoutError, DropgateValidationError } from '../errors.js';\r\nimport type { FetchFn, ServerTarget } from '../types.js';\r\n\r\n/**\r\n * Parse a server URL string into host, port, and secure components.\r\n * If no protocol is specified, defaults to HTTPS.\r\n */\r\nexport function parseServerUrl(urlStr: string): ServerTarget {\r\n let normalized = urlStr.trim();\r\n if (!normalized.startsWith('http://') && !normalized.startsWith('https://')) {\r\n normalized = 'https://' + normalized;\r\n }\r\n const url = new URL(normalized);\r\n return {\r\n host: url.hostname,\r\n port: url.port ? Number(url.port) : undefined,\r\n secure: url.protocol === 'https:',\r\n };\r\n}\r\n\r\n/**\r\n * Build a base URL from host, port, and secure options.\r\n */\r\nexport function buildBaseUrl(opts: ServerTarget): string {\r\n const { host, port, secure } = opts;\r\n\r\n if (!host || typeof host !== 'string') {\r\n throw new DropgateValidationError('Server host is required.');\r\n }\r\n\r\n const protocol = secure === false ? 'http' : 'https';\r\n const portSuffix = port ? `:${port}` : '';\r\n\r\n return `${protocol}://${host}${portSuffix}`;\r\n}\r\n\r\n/**\r\n * Sleep for a specified duration, with optional abort signal support.\r\n */\r\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n if (signal?.aborted) {\r\n return reject(signal.reason || new DropgateAbortError());\r\n }\r\n\r\n const t = setTimeout(resolve, ms);\r\n\r\n if (signal) {\r\n signal.addEventListener(\r\n 'abort',\r\n () => {\r\n clearTimeout(t);\r\n reject(signal.reason || new DropgateAbortError());\r\n },\r\n { once: true }\r\n );\r\n }\r\n });\r\n}\r\n\r\nexport interface AbortSignalWithCleanup {\r\n signal: AbortSignal;\r\n cleanup: () => void;\r\n}\r\n\r\n/**\r\n * Create an AbortSignal that combines a parent signal with a timeout.\r\n */\r\nexport function makeAbortSignal(\r\n parentSignal?: AbortSignal | null,\r\n timeoutMs?: number\r\n): AbortSignalWithCleanup {\r\n const controller = new AbortController();\r\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\r\n\r\n const abort = (reason?: unknown): void => {\r\n if (!controller.signal.aborted) {\r\n controller.abort(reason);\r\n }\r\n };\r\n\r\n if (parentSignal) {\r\n if (parentSignal.aborted) {\r\n abort(parentSignal.reason);\r\n } else {\r\n parentSignal.addEventListener('abort', () => abort(parentSignal.reason), {\r\n once: true,\r\n });\r\n }\r\n }\r\n\r\n if (Number.isFinite(timeoutMs) && timeoutMs! > 0) {\r\n timeoutId = setTimeout(() => {\r\n abort(new DropgateTimeoutError());\r\n }, timeoutMs);\r\n }\r\n\r\n return {\r\n signal: controller.signal,\r\n cleanup: () => {\r\n if (timeoutId) clearTimeout(timeoutId);\r\n },\r\n };\r\n}\r\n\r\nexport interface FetchJsonResult {\r\n res: Response;\r\n json: unknown;\r\n text: string;\r\n}\r\n\r\nexport interface FetchJsonOptions extends Omit<RequestInit, 'signal'> {\r\n timeoutMs?: number;\r\n signal?: AbortSignal;\r\n}\r\n\r\n/**\r\n * Fetch JSON from a URL with timeout and error handling.\r\n */\r\nexport async function fetchJson(\r\n fetchFn: FetchFn,\r\n url: string,\r\n opts: FetchJsonOptions = {}\r\n): Promise<FetchJsonResult> {\r\n const { timeoutMs, signal, ...rest } = opts;\r\n const { signal: s, cleanup } = makeAbortSignal(signal, timeoutMs);\r\n\r\n try {\r\n const res = await fetchFn(url, { ...rest, signal: s });\r\n const text = await res.text();\r\n\r\n let json: unknown = null;\r\n try {\r\n json = text ? JSON.parse(text) : null;\r\n } catch {\r\n // Ignore parse errors - json will remain null\r\n }\r\n\r\n return { res, json, text };\r\n } finally {\r\n cleanup();\r\n }\r\n}\r\n","import type { Base64Adapter, CryptoAdapter, FetchFn } from '../types.js';\r\n\r\n/**\r\n * Get the default Base64 adapter for the current environment.\r\n * Automatically detects Node.js Buffer vs browser btoa/atob.\r\n */\r\nexport function getDefaultBase64(): Base64Adapter {\r\n // Check for Node.js Buffer (works in Node.js and some bundlers)\r\n if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {\r\n return {\r\n encode(bytes: Uint8Array): string {\r\n return Buffer.from(bytes).toString('base64');\r\n },\r\n decode(b64: string): Uint8Array {\r\n return new Uint8Array(Buffer.from(b64, 'base64'));\r\n },\r\n };\r\n }\r\n\r\n // Browser fallback using btoa/atob\r\n if (typeof btoa === 'function' && typeof atob === 'function') {\r\n return {\r\n encode(bytes: Uint8Array): string {\r\n let binary = '';\r\n for (let i = 0; i < bytes.length; i++) {\r\n binary += String.fromCharCode(bytes[i]);\r\n }\r\n return btoa(binary);\r\n },\r\n decode(b64: string): Uint8Array {\r\n const binary = atob(b64);\r\n const out = new Uint8Array(binary.length);\r\n for (let i = 0; i < binary.length; i++) {\r\n out[i] = binary.charCodeAt(i);\r\n }\r\n return out;\r\n },\r\n };\r\n }\r\n\r\n throw new Error(\r\n 'No Base64 implementation available. Provide a Base64Adapter via options.'\r\n );\r\n}\r\n\r\n/**\r\n * Get the default crypto object for the current environment.\r\n * Returns globalThis.crypto if available.\r\n */\r\nexport function getDefaultCrypto(): CryptoAdapter | undefined {\r\n return globalThis.crypto as CryptoAdapter | undefined;\r\n}\r\n\r\n/**\r\n * Get the default fetch function for the current environment.\r\n * Returns globalThis.fetch if available.\r\n */\r\nexport function getDefaultFetch(): FetchFn | undefined {\r\n return globalThis.fetch?.bind(globalThis) as FetchFn | undefined;\r\n}\r\n","import type { CryptoAdapter } from '../types.js';\r\nimport { getDefaultCrypto } from '../adapters/defaults.js';\r\n\r\n/**\r\n * Check if a hostname is localhost\r\n */\r\nexport function isLocalhostHostname(hostname: string): boolean {\r\n const host = String(hostname || '').toLowerCase();\r\n return host === 'localhost' || host === '127.0.0.1' || host === '::1';\r\n}\r\n\r\n/**\r\n * Check if the current context allows P2P (HTTPS or localhost)\r\n */\r\nexport function isSecureContextForP2P(\r\n hostname?: string,\r\n isSecureContext?: boolean\r\n): boolean {\r\n return Boolean(isSecureContext) || isLocalhostHostname(hostname || '');\r\n}\r\n\r\n/**\r\n * Generate a P2P sharing code using cryptographically secure random.\r\n * Format: XXXX-0000 (4 letters + 4 digits)\r\n */\r\nexport function generateP2PCode(cryptoObj?: CryptoAdapter): string {\r\n const crypto = cryptoObj || getDefaultCrypto();\r\n const letters = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // Excluded I and O to avoid confusion\r\n\r\n if (crypto) {\r\n const randomBytes = new Uint8Array(8);\r\n crypto.getRandomValues(randomBytes);\r\n\r\n let letterPart = '';\r\n for (let i = 0; i < 4; i++) {\r\n letterPart += letters[randomBytes[i] % letters.length];\r\n }\r\n\r\n let numberPart = '';\r\n for (let i = 4; i < 8; i++) {\r\n numberPart += (randomBytes[i] % 10).toString();\r\n }\r\n\r\n return `${letterPart}-${numberPart}`;\r\n }\r\n\r\n // Fallback to Math.random (less secure, but works everywhere)\r\n let a = '';\r\n for (let i = 0; i < 4; i++) {\r\n a += letters[Math.floor(Math.random() * letters.length)];\r\n }\r\n let b = '';\r\n for (let i = 0; i < 4; i++) {\r\n b += Math.floor(Math.random() * 10);\r\n }\r\n return `${a}-${b}`;\r\n}\r\n\r\n/**\r\n * Check if a string looks like a P2P sharing code\r\n */\r\nexport function isP2PCodeLike(code: string): boolean {\r\n return /^[A-Z]{4}-\\d{4}$/.test(String(code || '').trim());\r\n}\r\n","import { DropgateNetworkError } from '../errors.js';\r\nimport type { P2PCapabilities } from '../types.js';\r\nimport type { PeerInstance, PeerOptions, P2PServerConfig } from './types.js';\r\n\r\n/**\r\n * Resolve P2P server configuration from user options and server capabilities.\r\n * User-provided values take precedence over server capabilities.\r\n */\r\nexport function resolvePeerConfig(\r\n userConfig: P2PServerConfig,\r\n serverCaps?: P2PCapabilities\r\n): { path: string; iceServers: RTCIceServer[] } {\r\n return {\r\n path: userConfig.peerjsPath ?? serverCaps?.peerjsPath ?? '/peerjs',\r\n iceServers: userConfig.iceServers ?? serverCaps?.iceServers ?? [],\r\n };\r\n}\r\n\r\n/**\r\n * Build PeerJS connection options from P2P server configuration.\r\n */\r\nexport function buildPeerOptions(config: P2PServerConfig = {}): PeerOptions {\r\n const { host, port, peerjsPath = '/peerjs', secure = false, iceServers = [] } = config;\r\n\r\n const peerOpts: PeerOptions = {\r\n host,\r\n path: peerjsPath,\r\n secure,\r\n config: { iceServers },\r\n debug: 0,\r\n };\r\n\r\n if (port) {\r\n peerOpts.port = port;\r\n }\r\n\r\n return peerOpts;\r\n}\r\n\r\nexport interface CreatePeerWithRetriesOptions {\r\n code?: string | null;\r\n codeGenerator: () => string;\r\n maxAttempts: number;\r\n buildPeer: (id: string) => PeerInstance;\r\n onCode?: (code: string, attempt: number) => void;\r\n}\r\n\r\n/**\r\n * Create a peer with retries if the code is already taken\r\n */\r\nexport async function createPeerWithRetries(\r\n opts: CreatePeerWithRetriesOptions\r\n): Promise<{ peer: PeerInstance; code: string }> {\r\n const { code, codeGenerator, maxAttempts, buildPeer, onCode } = opts;\r\n\r\n let nextCode = code || codeGenerator();\r\n let peer: PeerInstance | null = null;\r\n let lastError: Error | null = null;\r\n\r\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\r\n onCode?.(nextCode, attempt);\r\n\r\n try {\r\n peer = await new Promise<PeerInstance>((resolve, reject) => {\r\n const instance = buildPeer(nextCode);\r\n instance.on('open', () => resolve(instance));\r\n instance.on('error', (err: Error) => {\r\n try {\r\n instance.destroy();\r\n } catch {\r\n // Ignore destroy errors\r\n }\r\n reject(err);\r\n });\r\n });\r\n\r\n return { peer, code: nextCode };\r\n } catch (err) {\r\n lastError = err as Error;\r\n nextCode = codeGenerator();\r\n }\r\n }\r\n\r\n throw lastError || new DropgateNetworkError('Could not establish PeerJS connection.');\r\n}\r\n","import { DropgateValidationError, DropgateNetworkError } from '../errors.js';\r\nimport { sleep } from '../utils/network.js';\r\nimport type { P2PSendOptions, P2PSendSession, P2PSendState, DataConnection } from './types.js';\r\nimport { generateP2PCode } from './utils.js';\r\nimport { buildPeerOptions, createPeerWithRetries, resolvePeerConfig } from './helpers.js';\r\n\r\n/**\r\n * Generate a unique session ID for transfer tracking.\r\n * Uses crypto.randomUUID if available, falls back to timestamp + random.\r\n */\r\nfunction generateSessionId(): string {\r\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\r\n return crypto.randomUUID();\r\n }\r\n // Fallback for environments without crypto.randomUUID\r\n return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;\r\n}\r\n\r\n/**\r\n * Start a direct transfer (P2P) sender session.\r\n *\r\n * IMPORTANT: Consumer must provide the PeerJS Peer constructor.\r\n * This removes DOM coupling (no script injection).\r\n *\r\n * Example:\r\n * ```js\r\n * import Peer from 'peerjs';\r\n * import { startP2PSend } from '@dropgate/core/p2p';\r\n *\r\n * const session = await startP2PSend({\r\n * file: myFile,\r\n * Peer,\r\n * host: 'dropgate.link',\r\n * secure: true,\r\n * onCode: (code) => console.log('Share this code:', code),\r\n * onProgress: (evt) => console.log(`${evt.percent}% sent`),\r\n * onComplete: () => console.log('Done!'),\r\n * });\r\n * ```\r\n */\r\nexport async function startP2PSend(opts: P2PSendOptions): Promise<P2PSendSession> {\r\n const {\r\n file,\r\n Peer,\r\n serverInfo,\r\n host,\r\n port,\r\n peerjsPath,\r\n secure = false,\r\n iceServers,\r\n codeGenerator,\r\n cryptoObj,\r\n maxAttempts = 4,\r\n chunkSize = 256 * 1024,\r\n endAckTimeoutMs = 15000,\r\n bufferHighWaterMark = 8 * 1024 * 1024,\r\n bufferLowWaterMark = 2 * 1024 * 1024,\r\n heartbeatIntervalMs = 5000,\r\n onCode,\r\n onStatus,\r\n onProgress,\r\n onComplete,\r\n onError,\r\n onDisconnect,\r\n onCancel,\r\n } = opts;\r\n\r\n // Validate required options\r\n if (!file) {\r\n throw new DropgateValidationError('File is missing.');\r\n }\r\n\r\n if (!Peer) {\r\n throw new DropgateValidationError(\r\n 'PeerJS Peer constructor is required. Install peerjs and pass it as the Peer option.'\r\n );\r\n }\r\n\r\n // Check P2P capabilities if serverInfo is provided\r\n const p2pCaps = serverInfo?.capabilities?.p2p;\r\n if (serverInfo && !p2pCaps?.enabled) {\r\n throw new DropgateValidationError('Direct transfer is disabled on this server.');\r\n }\r\n\r\n // Resolve config from user options and server capabilities\r\n const { path: finalPath, iceServers: finalIceServers } = resolvePeerConfig(\r\n { peerjsPath, iceServers },\r\n p2pCaps\r\n );\r\n\r\n // Build peer options\r\n const peerOpts = buildPeerOptions({\r\n host,\r\n port,\r\n peerjsPath: finalPath,\r\n secure,\r\n iceServers: finalIceServers,\r\n });\r\n\r\n // Create the code generator\r\n const finalCodeGenerator = codeGenerator || (() => generateP2PCode(cryptoObj));\r\n\r\n // Create peer with retries\r\n const buildPeer = (id: string) => new Peer(id, peerOpts);\r\n const { peer, code } = await createPeerWithRetries({\r\n code: null,\r\n codeGenerator: finalCodeGenerator,\r\n maxAttempts,\r\n buildPeer,\r\n onCode,\r\n });\r\n\r\n // Generate unique session ID for this transfer\r\n const sessionId = generateSessionId();\r\n\r\n // State machine - replaces boolean flags to prevent race conditions\r\n let state: P2PSendState = 'listening';\r\n let activeConn: DataConnection | null = null;\r\n let sentBytes = 0;\r\n let heartbeatTimer: ReturnType<typeof setInterval> | null = null;\r\n\r\n const reportProgress = (data: { received: number; total: number }): void => {\r\n const safeTotal =\r\n Number.isFinite(data.total) && data.total > 0 ? data.total : file.size;\r\n const safeReceived = Math.min(Number(data.received) || 0, safeTotal || 0);\r\n const percent = safeTotal ? (safeReceived / safeTotal) * 100 : 0;\r\n onProgress?.({ processedBytes: safeReceived, totalBytes: safeTotal, percent });\r\n };\r\n\r\n // Safe error handler - prevents calling onError after completion or cancellation\r\n const safeError = (err: Error): void => {\r\n if (state === 'closed' || state === 'completed' || state === 'cancelled') return;\r\n state = 'closed';\r\n onError?.(err);\r\n cleanup();\r\n };\r\n\r\n // Safe complete handler - only fires from finishing state\r\n const safeComplete = (): void => {\r\n if (state !== 'finishing') return;\r\n state = 'completed';\r\n onComplete?.();\r\n cleanup();\r\n };\r\n\r\n // Cleanup all resources\r\n const cleanup = (): void => {\r\n // Clear heartbeat timer\r\n if (heartbeatTimer) {\r\n clearInterval(heartbeatTimer);\r\n heartbeatTimer = null;\r\n }\r\n\r\n // Remove beforeunload listener if in browser\r\n if (typeof window !== 'undefined') {\r\n window.removeEventListener('beforeunload', handleUnload);\r\n }\r\n\r\n try {\r\n activeConn?.close();\r\n } catch {\r\n // Ignore close errors\r\n }\r\n try {\r\n peer.destroy();\r\n } catch {\r\n // Ignore destroy errors\r\n }\r\n };\r\n\r\n // Handle browser tab close/refresh\r\n const handleUnload = (): void => {\r\n try {\r\n activeConn?.send({ t: 'error', message: 'Sender closed the connection.' });\r\n } catch {\r\n // Best effort\r\n }\r\n stop();\r\n };\r\n\r\n // Add beforeunload listener if in browser\r\n if (typeof window !== 'undefined') {\r\n window.addEventListener('beforeunload', handleUnload);\r\n }\r\n\r\n const stop = (): void => {\r\n if (state === 'closed' || state === 'cancelled') return;\r\n\r\n const wasActive = state === 'transferring' || state === 'finishing';\r\n state = 'cancelled';\r\n\r\n // Notify peer before cleanup\r\n try {\r\n // @ts-expect-error - open property may exist on PeerJS connections\r\n if (activeConn && activeConn.open) {\r\n activeConn.send({ t: 'cancelled', message: 'Sender cancelled the transfer.' });\r\n }\r\n } catch {\r\n // Best effort\r\n }\r\n\r\n if (wasActive && onCancel) {\r\n onCancel({ cancelledBy: 'sender' });\r\n }\r\n\r\n cleanup();\r\n };\r\n\r\n // Helper to check if session is stopped - bypasses TypeScript narrowing\r\n // which doesn't understand state can change asynchronously\r\n const isStopped = (): boolean => state === 'closed' || state === 'cancelled';\r\n\r\n peer.on('connection', (conn: DataConnection) => {\r\n if (state === 'closed') return;\r\n\r\n // Connection replacement logic - allow new connections if old one is dead\r\n if (activeConn) {\r\n // Check if existing connection is actually still open\r\n // @ts-expect-error - open property may exist on PeerJS connections\r\n const isOldConnOpen = activeConn.open !== false;\r\n\r\n if (isOldConnOpen && state === 'transferring') {\r\n // Actively transferring, reject new connection\r\n try {\r\n conn.send({ t: 'error', message: 'Transfer already in progress.' });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n try {\r\n conn.close();\r\n } catch {\r\n // Ignore close errors\r\n }\r\n return;\r\n } else if (!isOldConnOpen) {\r\n // Old connection is dead, clean it up and accept new one\r\n try {\r\n activeConn.close();\r\n } catch {\r\n // Ignore\r\n }\r\n activeConn = null;\r\n // Reset state to allow new transfer\r\n state = 'listening';\r\n sentBytes = 0;\r\n } else {\r\n // Connection exists but not transferring (maybe in negotiating state)\r\n // Reject to avoid confusion\r\n try {\r\n conn.send({ t: 'error', message: 'Another receiver is already connected.' });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n try {\r\n conn.close();\r\n } catch {\r\n // Ignore close errors\r\n }\r\n return;\r\n }\r\n }\r\n\r\n activeConn = conn;\r\n state = 'negotiating';\r\n onStatus?.({ phase: 'waiting', message: 'Connected. Waiting for receiver to accept...' });\r\n\r\n let readyResolve: (() => void) | null = null;\r\n let ackResolve: ((data: unknown) => void) | null = null;\r\n\r\n const readyPromise = new Promise<void>((resolve) => {\r\n readyResolve = resolve;\r\n });\r\n\r\n const ackPromise = new Promise<unknown>((resolve) => {\r\n ackResolve = resolve;\r\n });\r\n\r\n conn.on('data', (data: unknown) => {\r\n if (\r\n !data ||\r\n typeof data !== 'object' ||\r\n data instanceof ArrayBuffer ||\r\n ArrayBuffer.isView(data)\r\n ) {\r\n return;\r\n }\r\n\r\n const msg = data as { t?: string; received?: number; total?: number; phase?: string; message?: string };\r\n if (!msg.t) return;\r\n\r\n if (msg.t === 'ready') {\r\n onStatus?.({ phase: 'transferring', message: 'Receiver accepted. Starting transfer...' });\r\n readyResolve?.();\r\n return;\r\n }\r\n\r\n if (msg.t === 'progress') {\r\n reportProgress({ received: msg.received || 0, total: msg.total || 0 });\r\n return;\r\n }\r\n\r\n if (msg.t === 'ack' && msg.phase === 'end') {\r\n ackResolve?.(msg);\r\n return;\r\n }\r\n\r\n if (msg.t === 'pong') {\r\n // Heartbeat response received, connection is alive\r\n return;\r\n }\r\n\r\n if (msg.t === 'error') {\r\n safeError(new DropgateNetworkError(msg.message || 'Receiver reported an error.'));\r\n return;\r\n }\r\n\r\n if (msg.t === 'cancelled') {\r\n if (state === 'cancelled' || state === 'closed' || state === 'completed') return;\r\n state = 'cancelled';\r\n onCancel?.({ cancelledBy: 'receiver', message: msg.message });\r\n cleanup();\r\n }\r\n });\r\n\r\n conn.on('open', async () => {\r\n try {\r\n if (isStopped()) return;\r\n\r\n // Send metadata with sessionId\r\n conn.send({\r\n t: 'meta',\r\n sessionId,\r\n name: file.name,\r\n size: file.size,\r\n mime: file.type || 'application/octet-stream',\r\n });\r\n\r\n const total = file.size;\r\n const dc = conn._dc;\r\n\r\n if (dc && Number.isFinite(bufferLowWaterMark)) {\r\n try {\r\n dc.bufferedAmountLowThreshold = bufferLowWaterMark;\r\n } catch {\r\n // Ignore threshold setting errors\r\n }\r\n }\r\n\r\n // Wait for ready signal\r\n await readyPromise;\r\n if (isStopped()) return;\r\n\r\n // Start heartbeat for long transfers\r\n if (heartbeatIntervalMs > 0) {\r\n heartbeatTimer = setInterval(() => {\r\n if (state === 'transferring' || state === 'finishing') {\r\n try {\r\n conn.send({ t: 'ping' });\r\n } catch {\r\n // Ignore ping errors\r\n }\r\n }\r\n }, heartbeatIntervalMs);\r\n }\r\n\r\n state = 'transferring';\r\n\r\n // Send file in chunks\r\n for (let offset = 0; offset < total; offset += chunkSize) {\r\n if (isStopped()) return;\r\n\r\n const slice = file.slice(offset, offset + chunkSize);\r\n const buf = await slice.arrayBuffer();\r\n if (isStopped()) return;\r\n conn.send(buf);\r\n sentBytes += buf.byteLength;\r\n\r\n // Flow control\r\n if (dc) {\r\n while (dc.bufferedAmount > bufferHighWaterMark) {\r\n await new Promise<void>((resolve) => {\r\n const fallback = setTimeout(resolve, 60);\r\n try {\r\n dc.addEventListener(\r\n 'bufferedamountlow',\r\n () => {\r\n clearTimeout(fallback);\r\n resolve();\r\n },\r\n { once: true }\r\n );\r\n } catch {\r\n // Fallback only\r\n }\r\n });\r\n }\r\n }\r\n }\r\n\r\n if (isStopped()) return;\r\n\r\n state = 'finishing';\r\n conn.send({ t: 'end' });\r\n\r\n // Wait for acknowledgment\r\n const ackTimeoutMs = Number.isFinite(endAckTimeoutMs)\r\n ? Math.max(endAckTimeoutMs, Math.ceil(file.size / (1024 * 1024)) * 1000)\r\n : null;\r\n\r\n const ackResult = await Promise.race([\r\n ackPromise,\r\n sleep(ackTimeoutMs || 15000).catch(() => null),\r\n ]);\r\n\r\n if (isStopped()) return;\r\n\r\n if (!ackResult || typeof ackResult !== 'object') {\r\n throw new DropgateNetworkError('Receiver did not confirm completion.');\r\n }\r\n\r\n const ackData = ackResult as { total?: number; received?: number };\r\n const ackTotal = Number(ackData.total) || file.size;\r\n const ackReceived = Number(ackData.received) || 0;\r\n\r\n if (ackTotal && ackReceived < ackTotal) {\r\n throw new DropgateNetworkError('Receiver reported an incomplete transfer.');\r\n }\r\n\r\n reportProgress({ received: ackReceived || ackTotal, total: ackTotal });\r\n safeComplete();\r\n } catch (err) {\r\n safeError(err as Error);\r\n }\r\n });\r\n\r\n conn.on('error', (err: Error) => {\r\n safeError(err);\r\n });\r\n\r\n conn.on('close', () => {\r\n if (state === 'closed' || state === 'completed' || state === 'cancelled') {\r\n // Clean shutdown or already cancelled, ensure full cleanup\r\n cleanup();\r\n return;\r\n }\r\n\r\n if (state === 'transferring' || state === 'finishing') {\r\n // Connection closed during active transfer — the receiver either cancelled\r\n // or disconnected. Treat as a receiver-initiated cancellation so the UI\r\n // can reset cleanly instead of showing a raw error.\r\n state = 'cancelled';\r\n onCancel?.({ cancelledBy: 'receiver' });\r\n cleanup();\r\n } else {\r\n // Disconnected before transfer started (during waiting/negotiating phase)\r\n // Reset state to allow reconnection\r\n activeConn = null;\r\n state = 'listening';\r\n sentBytes = 0;\r\n onDisconnect?.();\r\n }\r\n });\r\n });\r\n\r\n return {\r\n peer,\r\n code,\r\n sessionId,\r\n stop,\r\n getStatus: () => state,\r\n getBytesSent: () => sentBytes,\r\n getConnectedPeerId: () => {\r\n if (!activeConn) return null;\r\n // @ts-expect-error - peer property exists on PeerJS DataConnection\r\n return activeConn.peer || null;\r\n },\r\n };\r\n}\r\n","import { DropgateValidationError, DropgateNetworkError } from '../errors.js';\r\nimport type { P2PReceiveOptions, P2PReceiveSession, P2PReceiveState, DataConnection } from './types.js';\r\nimport { isP2PCodeLike } from './utils.js';\r\nimport { buildPeerOptions, resolvePeerConfig } from './helpers.js';\r\n\r\n/**\r\n * Start a direct transfer (P2P) receiver session.\r\n *\r\n * IMPORTANT: Consumer must provide the PeerJS Peer constructor and handle file writing.\r\n * This removes DOM coupling (no streamSaver).\r\n *\r\n * Example:\r\n * ```js\r\n * import Peer from 'peerjs';\r\n * import { startP2PReceive } from '@dropgate/core/p2p';\r\n *\r\n * let writer;\r\n * const session = await startP2PReceive({\r\n * code: 'ABCD-1234',\r\n * Peer,\r\n * host: 'dropgate.link',\r\n * secure: true,\r\n * onMeta: ({ name, total }) => {\r\n * // Consumer creates file writer\r\n * writer = createWriteStream(name);\r\n * },\r\n * onData: async (chunk) => {\r\n * // Consumer writes data\r\n * await writer.write(chunk);\r\n * },\r\n * onComplete: () => {\r\n * writer.close();\r\n * console.log('Done!');\r\n * },\r\n * });\r\n * ```\r\n */\r\nexport async function startP2PReceive(opts: P2PReceiveOptions): Promise<P2PReceiveSession> {\r\n const {\r\n code,\r\n Peer,\r\n serverInfo,\r\n host,\r\n port,\r\n peerjsPath,\r\n secure = false,\r\n iceServers,\r\n autoReady = true,\r\n watchdogTimeoutMs = 15000,\r\n onStatus,\r\n onMeta,\r\n onData,\r\n onProgress,\r\n onComplete,\r\n onError,\r\n onDisconnect,\r\n onCancel,\r\n } = opts;\r\n\r\n // Validate required options\r\n if (!code) {\r\n throw new DropgateValidationError('No sharing code was provided.');\r\n }\r\n\r\n if (!Peer) {\r\n throw new DropgateValidationError(\r\n 'PeerJS Peer constructor is required. Install peerjs and pass it as the Peer option.'\r\n );\r\n }\r\n\r\n // Check P2P capabilities if serverInfo is provided\r\n const p2pCaps = serverInfo?.capabilities?.p2p;\r\n if (serverInfo && !p2pCaps?.enabled) {\r\n throw new DropgateValidationError('Direct transfer is disabled on this server.');\r\n }\r\n\r\n // Validate and normalize code\r\n const normalizedCode = String(code).trim().replace(/\\s+/g, '').toUpperCase();\r\n if (!isP2PCodeLike(normalizedCode)) {\r\n throw new DropgateValidationError('Invalid direct transfer code.');\r\n }\r\n\r\n // Resolve config from user options and server capabilities\r\n const { path: finalPath, iceServers: finalIceServers } = resolvePeerConfig(\r\n { peerjsPath, iceServers },\r\n p2pCaps\r\n );\r\n\r\n // Build peer options\r\n const peerOpts = buildPeerOptions({\r\n host,\r\n port,\r\n peerjsPath: finalPath,\r\n secure,\r\n iceServers: finalIceServers,\r\n });\r\n\r\n // Create peer (receiver doesn't need a specific ID)\r\n const peer = new Peer(undefined, peerOpts);\r\n\r\n // State machine - replaces boolean flags to prevent race conditions\r\n let state: P2PReceiveState = 'initializing';\r\n let total = 0;\r\n let received = 0;\r\n let currentSessionId: string | null = null;\r\n let lastProgressSentAt = 0;\r\n const progressIntervalMs = 120;\r\n let writeQueue = Promise.resolve();\r\n let watchdogTimer: ReturnType<typeof setTimeout> | null = null;\r\n let activeConn: DataConnection | null = null;\r\n\r\n // Watchdog - detects dead connections during transfer\r\n const resetWatchdog = (): void => {\r\n if (watchdogTimeoutMs <= 0) return;\r\n\r\n if (watchdogTimer) {\r\n clearTimeout(watchdogTimer);\r\n }\r\n\r\n watchdogTimer = setTimeout(() => {\r\n if (state === 'transferring') {\r\n safeError(new DropgateNetworkError('Connection timed out (no data received).'));\r\n }\r\n }, watchdogTimeoutMs);\r\n };\r\n\r\n const clearWatchdog = (): void => {\r\n if (watchdogTimer) {\r\n clearTimeout(watchdogTimer);\r\n watchdogTimer = null;\r\n }\r\n };\r\n\r\n // Safe error handler - prevents calling onError after completion or cancellation\r\n const safeError = (err: Error): void => {\r\n if (state === 'closed' || state === 'completed' || state === 'cancelled') return;\r\n state = 'closed';\r\n onError?.(err);\r\n cleanup();\r\n };\r\n\r\n // Safe complete handler - only fires from transferring state\r\n const safeComplete = (completeData: { received: number; total: number }): void => {\r\n if (state !== 'transferring') return;\r\n state = 'completed';\r\n onComplete?.(completeData);\r\n cleanup();\r\n };\r\n\r\n // Cleanup all resources\r\n const cleanup = (): void => {\r\n clearWatchdog();\r\n\r\n // Remove beforeunload listener if in browser\r\n if (typeof window !== 'undefined') {\r\n window.removeEventListener('beforeunload', handleUnload);\r\n }\r\n\r\n try {\r\n peer.destroy();\r\n } catch {\r\n // Ignore destroy errors\r\n }\r\n };\r\n\r\n // Handle browser tab close/refresh\r\n const handleUnload = (): void => {\r\n try {\r\n activeConn?.send({ t: 'error', message: 'Receiver closed the connection.' });\r\n } catch {\r\n // Best effort\r\n }\r\n stop();\r\n };\r\n\r\n // Add beforeunload listener if in browser\r\n if (typeof window !== 'undefined') {\r\n window.addEventListener('beforeunload', handleUnload);\r\n }\r\n\r\n const stop = (): void => {\r\n if (state === 'closed' || state === 'cancelled') return;\r\n\r\n const wasActive = state === 'transferring';\r\n state = 'cancelled';\r\n\r\n // Notify peer before cleanup\r\n try {\r\n // @ts-expect-error - open property may exist on PeerJS connections\r\n if (activeConn && activeConn.open) {\r\n activeConn.send({ t: 'cancelled', message: 'Receiver cancelled the transfer.' });\r\n }\r\n } catch {\r\n // Best effort\r\n }\r\n\r\n if (wasActive && onCancel) {\r\n onCancel({ cancelledBy: 'receiver' });\r\n }\r\n\r\n cleanup();\r\n };\r\n\r\n peer.on('error', (err: Error) => {\r\n safeError(err);\r\n });\r\n\r\n peer.on('open', () => {\r\n state = 'connecting';\r\n const conn = peer.connect(normalizedCode, { reliable: true });\r\n activeConn = conn;\r\n\r\n conn.on('open', () => {\r\n state = 'negotiating';\r\n onStatus?.({ phase: 'connected', message: 'Waiting for file details...' });\r\n });\r\n\r\n conn.on('data', async (data: unknown) => {\r\n try {\r\n // Reset watchdog on any data received\r\n resetWatchdog();\r\n\r\n // Handle control messages\r\n if (\r\n data &&\r\n typeof data === 'object' &&\r\n !(data instanceof ArrayBuffer) &&\r\n !ArrayBuffer.isView(data)\r\n ) {\r\n const msg = data as {\r\n t?: string;\r\n sessionId?: string;\r\n name?: string;\r\n size?: number;\r\n message?: string;\r\n };\r\n\r\n if (msg.t === 'meta') {\r\n // Session ID validation - reject if we're busy with a different session\r\n if (currentSessionId && msg.sessionId && msg.sessionId !== currentSessionId) {\r\n try {\r\n conn.send({ t: 'error', message: 'Busy with another session.' });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n return;\r\n }\r\n\r\n // Store the session ID for this transfer\r\n if (msg.sessionId) {\r\n currentSessionId = msg.sessionId;\r\n }\r\n\r\n const name = String(msg.name || 'file');\r\n total = Number(msg.size) || 0;\r\n received = 0;\r\n writeQueue = Promise.resolve();\r\n\r\n // Function to send ready signal - called automatically if autoReady is true,\r\n // or passed to onMeta callback for manual invocation if autoReady is false\r\n const sendReady = (): void => {\r\n state = 'transferring';\r\n // Start watchdog once we're ready to receive data\r\n resetWatchdog();\r\n try {\r\n conn.send({ t: 'ready' });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n };\r\n\r\n if (autoReady) {\r\n onMeta?.({ name, total });\r\n onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });\r\n sendReady();\r\n } else {\r\n // Pass sendReady function to callback so consumer can trigger transfer start\r\n onMeta?.({ name, total, sendReady });\r\n onProgress?.({ processedBytes: received, totalBytes: total, percent: 0 });\r\n }\r\n return;\r\n }\r\n\r\n if (msg.t === 'ping') {\r\n // Respond to heartbeat - keeps watchdog alive and confirms we're active\r\n try {\r\n conn.send({ t: 'pong' });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n return;\r\n }\r\n\r\n if (msg.t === 'end') {\r\n clearWatchdog();\r\n await writeQueue;\r\n\r\n if (total && received < total) {\r\n const err = new DropgateNetworkError(\r\n 'Transfer ended before the full file was received.'\r\n );\r\n try {\r\n conn.send({ t: 'error', message: err.message });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n throw err;\r\n }\r\n\r\n try {\r\n conn.send({ t: 'ack', phase: 'end', received, total });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n\r\n safeComplete({ received, total });\r\n return;\r\n }\r\n\r\n if (msg.t === 'error') {\r\n throw new DropgateNetworkError(msg.message || 'Sender reported an error.');\r\n }\r\n\r\n if (msg.t === 'cancelled') {\r\n if (state === 'cancelled' || state === 'closed' || state === 'completed') return;\r\n state = 'cancelled';\r\n onCancel?.({ cancelledBy: 'sender', message: msg.message });\r\n cleanup();\r\n return;\r\n }\r\n\r\n return;\r\n }\r\n\r\n // Handle binary data\r\n let bufPromise: Promise<Uint8Array>;\r\n\r\n if (data instanceof ArrayBuffer) {\r\n bufPromise = Promise.resolve(new Uint8Array(data));\r\n } else if (ArrayBuffer.isView(data)) {\r\n bufPromise = Promise.resolve(\r\n new Uint8Array(data.buffer, data.byteOffset, data.byteLength)\r\n );\r\n } else if (typeof Blob !== 'undefined' && data instanceof Blob) {\r\n bufPromise = data.arrayBuffer().then((buffer) => new Uint8Array(buffer));\r\n } else {\r\n return;\r\n }\r\n\r\n writeQueue = writeQueue\r\n .then(async () => {\r\n const buf = await bufPromise;\r\n\r\n // Call consumer's onData handler\r\n if (onData) {\r\n await onData(buf);\r\n }\r\n\r\n received += buf.byteLength;\r\n const percent = total ? Math.min(100, (received / total) * 100) : 0;\r\n onProgress?.({ processedBytes: received, totalBytes: total, percent });\r\n\r\n const now = Date.now();\r\n if (received === total || now - lastProgressSentAt >= progressIntervalMs) {\r\n lastProgressSentAt = now;\r\n try {\r\n conn.send({ t: 'progress', received, total });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n }\r\n })\r\n .catch((err) => {\r\n try {\r\n conn.send({\r\n t: 'error',\r\n message: (err as Error)?.message || 'Receiver write failed.',\r\n });\r\n } catch {\r\n // Ignore send errors\r\n }\r\n safeError(err as Error);\r\n });\r\n } catch (err) {\r\n safeError(err as Error);\r\n }\r\n });\r\n\r\n conn.on('close', () => {\r\n if (state === 'closed' || state === 'completed' || state === 'cancelled') {\r\n // Clean shutdown or already cancelled, ensure full cleanup\r\n cleanup();\r\n return;\r\n }\r\n\r\n // Sender disconnected or cancelled before transfer completed\r\n if (state === 'transferring') {\r\n // Connection closed during active transfer — the sender either cancelled\r\n // or disconnected. Treat as a sender-initiated cancellation so the UI\r\n // can show a clean message instead of a raw error.\r\n state = 'cancelled';\r\n onCancel?.({ cancelledBy: 'sender' });\r\n cleanup();\r\n } else if (state === 'negotiating') {\r\n // We had metadata but transfer hadn't started\r\n state = 'closed';\r\n cleanup();\r\n onDisconnect?.();\r\n } else {\r\n // Disconnected before we even got file metadata\r\n safeError(new DropgateNetworkError('Sender disconnected before file details were received.'));\r\n }\r\n });\r\n });\r\n\r\n return {\r\n peer,\r\n stop,\r\n getStatus: () => state,\r\n getBytesReceived: () => received,\r\n getTotalBytes: () => total,\r\n getSessionId: () => currentSessionId,\r\n };\r\n}\r\n"],"mappings":"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","names":["crypto"]}
@@ -94,11 +94,11 @@ interface FileSource {
94
94
  * Finite state machine states for P2P send sessions.
95
95
  * Prevents race conditions and ensures callbacks fire in correct order.
96
96
  */
97
- type P2PSendState = 'initializing' | 'listening' | 'negotiating' | 'transferring' | 'finishing' | 'completed' | 'closed';
97
+ type P2PSendState = 'initializing' | 'listening' | 'negotiating' | 'transferring' | 'finishing' | 'completed' | 'cancelled' | 'closed';
98
98
  /**
99
99
  * Finite state machine states for P2P receive sessions.
100
100
  */
101
- type P2PReceiveState = 'initializing' | 'connecting' | 'negotiating' | 'transferring' | 'completed' | 'closed';
101
+ type P2PReceiveState = 'initializing' | 'connecting' | 'negotiating' | 'transferring' | 'completed' | 'cancelled' | 'closed';
102
102
  /**
103
103
  * PeerJS Peer constructor interface.
104
104
  * Consumer must provide this constructor to P2P functions.
@@ -208,6 +208,13 @@ interface P2PReceiveCompleteEvent {
208
208
  received: number;
209
209
  total: number;
210
210
  }
211
+ /** Cancellation event for P2P operations. */
212
+ interface P2PCancellationEvent {
213
+ /** Who cancelled the transfer ('sender' or 'receiver'). */
214
+ cancelledBy: 'sender' | 'receiver';
215
+ /** Optional cancellation message. */
216
+ message?: string;
217
+ }
211
218
  /**
212
219
  * Options for starting a P2P send session.
213
220
  */
@@ -246,6 +253,8 @@ interface P2PSendOptions extends P2PServerConfig {
246
253
  onError?: (err: Error) => void;
247
254
  /** Callback when receiver disconnects. */
248
255
  onDisconnect?: () => void;
256
+ /** Callback when transfer is cancelled by either party. */
257
+ onCancel?: (evt: P2PCancellationEvent) => void;
249
258
  }
250
259
  /**
251
260
  * Return value from startP2PSend containing session control.
@@ -306,6 +315,8 @@ interface P2PReceiveOptions extends P2PServerConfig {
306
315
  onError?: (err: Error) => void;
307
316
  /** Callback when sender disconnects. */
308
317
  onDisconnect?: () => void;
318
+ /** Callback when transfer is cancelled by either party. */
319
+ onCancel?: (evt: P2PCancellationEvent) => void;
309
320
  }
310
321
  /**
311
322
  * Return value from startP2PReceive containing session control.
@@ -94,11 +94,11 @@ interface FileSource {
94
94
  * Finite state machine states for P2P send sessions.
95
95
  * Prevents race conditions and ensures callbacks fire in correct order.
96
96
  */
97
- type P2PSendState = 'initializing' | 'listening' | 'negotiating' | 'transferring' | 'finishing' | 'completed' | 'closed';
97
+ type P2PSendState = 'initializing' | 'listening' | 'negotiating' | 'transferring' | 'finishing' | 'completed' | 'cancelled' | 'closed';
98
98
  /**
99
99
  * Finite state machine states for P2P receive sessions.
100
100
  */
101
- type P2PReceiveState = 'initializing' | 'connecting' | 'negotiating' | 'transferring' | 'completed' | 'closed';
101
+ type P2PReceiveState = 'initializing' | 'connecting' | 'negotiating' | 'transferring' | 'completed' | 'cancelled' | 'closed';
102
102
  /**
103
103
  * PeerJS Peer constructor interface.
104
104
  * Consumer must provide this constructor to P2P functions.
@@ -208,6 +208,13 @@ interface P2PReceiveCompleteEvent {
208
208
  received: number;
209
209
  total: number;
210
210
  }
211
+ /** Cancellation event for P2P operations. */
212
+ interface P2PCancellationEvent {
213
+ /** Who cancelled the transfer ('sender' or 'receiver'). */
214
+ cancelledBy: 'sender' | 'receiver';
215
+ /** Optional cancellation message. */
216
+ message?: string;
217
+ }
211
218
  /**
212
219
  * Options for starting a P2P send session.
213
220
  */
@@ -246,6 +253,8 @@ interface P2PSendOptions extends P2PServerConfig {
246
253
  onError?: (err: Error) => void;
247
254
  /** Callback when receiver disconnects. */
248
255
  onDisconnect?: () => void;
256
+ /** Callback when transfer is cancelled by either party. */
257
+ onCancel?: (evt: P2PCancellationEvent) => void;
249
258
  }
250
259
  /**
251
260
  * Return value from startP2PSend containing session control.
@@ -306,6 +315,8 @@ interface P2PReceiveOptions extends P2PServerConfig {
306
315
  onError?: (err: Error) => void;
307
316
  /** Callback when sender disconnects. */
308
317
  onDisconnect?: () => void;
318
+ /** Callback when transfer is cancelled by either party. */
319
+ onCancel?: (evt: P2PCancellationEvent) => void;
309
320
  }
310
321
  /**
311
322
  * Return value from startP2PReceive containing session control.