@dropgate/core 2.1.0 → 2.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -531,201 +531,251 @@ var DropgateClient = class {
531
531
  encrypt,
532
532
  filenameOverride,
533
533
  onProgress,
534
+ onCancel,
534
535
  signal,
535
536
  timeouts = {},
536
537
  retry = {}
537
538
  } = opts;
538
- const progress = (evt) => {
539
+ const internalController = signal ? null : new AbortController();
540
+ const effectiveSignal = signal || internalController?.signal;
541
+ let uploadState = "initializing";
542
+ let currentUploadId = null;
543
+ let currentBaseUrl = null;
544
+ const uploadPromise = (async () => {
539
545
  try {
540
- if (onProgress) onProgress(evt);
541
- } catch {
542
- }
543
- };
544
- if (!this.cryptoObj?.subtle) {
545
- throw new DropgateValidationError(
546
- "Web Crypto API not available (crypto.subtle)."
547
- );
548
- }
549
- const fileSizeBytes = file.size;
550
- progress({ phase: "server-info", text: "Checking server...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
551
- const compat = await this.checkCompatibility({
552
- host,
553
- port,
554
- secure,
555
- timeoutMs: timeouts.serverInfoMs ?? 5e3,
556
- signal
557
- });
558
- const { baseUrl, serverInfo } = compat;
559
- progress({ phase: "server-compat", text: compat.message, percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
560
- if (!compat.compatible) {
561
- throw new DropgateValidationError(compat.message);
562
- }
563
- const filename = filenameOverride ?? file.name ?? "file";
564
- if (!encrypt) {
565
- validatePlainFilename(filename);
566
- }
567
- this.validateUploadInputs({ file, lifetimeMs, encrypt, serverInfo });
568
- let cryptoKey = null;
569
- let keyB64 = null;
570
- let transmittedFilename = filename;
571
- if (encrypt) {
572
- progress({ phase: "crypto", text: "Generating encryption key...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
573
- try {
574
- cryptoKey = await generateAesGcmKey(this.cryptoObj);
575
- keyB64 = await exportKeyBase64(this.cryptoObj, cryptoKey);
576
- transmittedFilename = await encryptFilenameToBase64(
577
- this.cryptoObj,
578
- filename,
579
- cryptoKey
546
+ const progress = (evt) => {
547
+ try {
548
+ if (onProgress) onProgress(evt);
549
+ } catch {
550
+ }
551
+ };
552
+ if (!this.cryptoObj?.subtle) {
553
+ throw new DropgateValidationError(
554
+ "Web Crypto API not available (crypto.subtle)."
555
+ );
556
+ }
557
+ const fileSizeBytes = file.size;
558
+ progress({ phase: "server-info", text: "Checking server...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
559
+ const compat = await this.checkCompatibility({
560
+ host,
561
+ port,
562
+ secure,
563
+ timeoutMs: timeouts.serverInfoMs ?? 5e3,
564
+ signal: effectiveSignal
565
+ });
566
+ const { baseUrl, serverInfo } = compat;
567
+ progress({ phase: "server-compat", text: compat.message, percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
568
+ if (!compat.compatible) {
569
+ throw new DropgateValidationError(compat.message);
570
+ }
571
+ const filename = filenameOverride ?? file.name ?? "file";
572
+ if (!encrypt) {
573
+ validatePlainFilename(filename);
574
+ }
575
+ this.validateUploadInputs({ file, lifetimeMs, encrypt, serverInfo });
576
+ let cryptoKey = null;
577
+ let keyB64 = null;
578
+ let transmittedFilename = filename;
579
+ if (encrypt) {
580
+ progress({ phase: "crypto", text: "Generating encryption key...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
581
+ try {
582
+ cryptoKey = await generateAesGcmKey(this.cryptoObj);
583
+ keyB64 = await exportKeyBase64(this.cryptoObj, cryptoKey);
584
+ transmittedFilename = await encryptFilenameToBase64(
585
+ this.cryptoObj,
586
+ filename,
587
+ cryptoKey
588
+ );
589
+ } catch (err) {
590
+ throw new DropgateError("Failed to prepare encryption.", {
591
+ code: "CRYPTO_PREP_FAILED",
592
+ cause: err
593
+ });
594
+ }
595
+ }
596
+ const totalChunks = Math.ceil(file.size / this.chunkSize);
597
+ const totalUploadSize = estimateTotalUploadSizeBytes(
598
+ file.size,
599
+ totalChunks,
600
+ encrypt
580
601
  );
581
- } catch (err) {
582
- throw new DropgateError("Failed to prepare encryption.", {
583
- code: "CRYPTO_PREP_FAILED",
584
- cause: err
602
+ progress({ phase: "init", text: "Reserving server storage...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
603
+ const initPayload = {
604
+ filename: transmittedFilename,
605
+ lifetime: lifetimeMs,
606
+ isEncrypted: Boolean(encrypt),
607
+ totalSize: totalUploadSize,
608
+ totalChunks
609
+ };
610
+ const initRes = await fetchJson(this.fetchFn, `${baseUrl}/upload/init`, {
611
+ method: "POST",
612
+ timeoutMs: timeouts.initMs ?? 15e3,
613
+ signal: effectiveSignal,
614
+ headers: {
615
+ "Content-Type": "application/json",
616
+ Accept: "application/json"
617
+ },
618
+ body: JSON.stringify(initPayload)
585
619
  });
586
- }
587
- }
588
- const totalChunks = Math.ceil(file.size / this.chunkSize);
589
- const totalUploadSize = estimateTotalUploadSizeBytes(
590
- file.size,
591
- totalChunks,
592
- encrypt
593
- );
594
- progress({ phase: "init", text: "Reserving server storage...", percent: 0, processedBytes: 0, totalBytes: fileSizeBytes });
595
- const initPayload = {
596
- filename: transmittedFilename,
597
- lifetime: lifetimeMs,
598
- isEncrypted: Boolean(encrypt),
599
- totalSize: totalUploadSize,
600
- totalChunks
601
- };
602
- const initRes = await fetchJson(this.fetchFn, `${baseUrl}/upload/init`, {
603
- method: "POST",
604
- timeoutMs: timeouts.initMs ?? 15e3,
605
- signal,
606
- headers: {
607
- "Content-Type": "application/json",
608
- Accept: "application/json"
609
- },
610
- body: JSON.stringify(initPayload)
611
- });
612
- if (!initRes.res.ok) {
613
- const errorJson = initRes.json;
614
- const msg = errorJson?.error || `Server initialisation failed: ${initRes.res.status}`;
615
- throw new DropgateProtocolError(msg, {
616
- details: initRes.json || initRes.text
617
- });
618
- }
619
- const initJson = initRes.json;
620
- const uploadId = initJson?.uploadId;
621
- if (!uploadId || typeof uploadId !== "string") {
622
- throw new DropgateProtocolError(
623
- "Server did not return a valid uploadId."
624
- );
625
- }
626
- const retries = Number.isFinite(retry.retries) ? retry.retries : 5;
627
- const baseBackoffMs = Number.isFinite(retry.backoffMs) ? retry.backoffMs : 1e3;
628
- const maxBackoffMs = Number.isFinite(retry.maxBackoffMs) ? retry.maxBackoffMs : 3e4;
629
- for (let i = 0; i < totalChunks; i++) {
630
- if (signal?.aborted) {
631
- throw signal.reason || new DropgateAbortError();
632
- }
633
- const start = i * this.chunkSize;
634
- const end = Math.min(start + this.chunkSize, file.size);
635
- let chunkBlob = file.slice(start, end);
636
- const percentComplete = i / totalChunks * 100;
637
- const processedBytes = i * this.chunkSize;
638
- progress({
639
- phase: "chunk",
640
- text: `Uploading chunk ${i + 1} of ${totalChunks}...`,
641
- percent: percentComplete,
642
- processedBytes,
643
- totalBytes: fileSizeBytes,
644
- chunkIndex: i,
645
- totalChunks
646
- });
647
- const chunkBuffer = await chunkBlob.arrayBuffer();
648
- let uploadBlob;
649
- if (encrypt && cryptoKey) {
650
- uploadBlob = await encryptToBlob(this.cryptoObj, chunkBuffer, cryptoKey);
651
- } else {
652
- uploadBlob = new Blob([chunkBuffer]);
653
- }
654
- if (uploadBlob.size > DEFAULT_CHUNK_SIZE + 1024) {
655
- throw new DropgateValidationError(
656
- "Chunk too large (client-side). Check chunk size settings."
620
+ if (!initRes.res.ok) {
621
+ const errorJson = initRes.json;
622
+ const msg = errorJson?.error || `Server initialisation failed: ${initRes.res.status}`;
623
+ throw new DropgateProtocolError(msg, {
624
+ details: initRes.json || initRes.text
625
+ });
626
+ }
627
+ const initJson = initRes.json;
628
+ const uploadId = initJson?.uploadId;
629
+ if (!uploadId || typeof uploadId !== "string") {
630
+ throw new DropgateProtocolError(
631
+ "Server did not return a valid uploadId."
632
+ );
633
+ }
634
+ currentUploadId = uploadId;
635
+ currentBaseUrl = baseUrl;
636
+ uploadState = "uploading";
637
+ const retries = Number.isFinite(retry.retries) ? retry.retries : 5;
638
+ const baseBackoffMs = Number.isFinite(retry.backoffMs) ? retry.backoffMs : 1e3;
639
+ const maxBackoffMs = Number.isFinite(retry.maxBackoffMs) ? retry.maxBackoffMs : 3e4;
640
+ for (let i = 0; i < totalChunks; i++) {
641
+ if (effectiveSignal?.aborted) {
642
+ throw effectiveSignal.reason || new DropgateAbortError();
643
+ }
644
+ const start = i * this.chunkSize;
645
+ const end = Math.min(start + this.chunkSize, file.size);
646
+ let chunkBlob = file.slice(start, end);
647
+ const percentComplete = i / totalChunks * 100;
648
+ const processedBytes = i * this.chunkSize;
649
+ progress({
650
+ phase: "chunk",
651
+ text: `Uploading chunk ${i + 1} of ${totalChunks}...`,
652
+ percent: percentComplete,
653
+ processedBytes,
654
+ totalBytes: fileSizeBytes,
655
+ chunkIndex: i,
656
+ totalChunks
657
+ });
658
+ const chunkBuffer = await chunkBlob.arrayBuffer();
659
+ let uploadBlob;
660
+ if (encrypt && cryptoKey) {
661
+ uploadBlob = await encryptToBlob(this.cryptoObj, chunkBuffer, cryptoKey);
662
+ } else {
663
+ uploadBlob = new Blob([chunkBuffer]);
664
+ }
665
+ if (uploadBlob.size > DEFAULT_CHUNK_SIZE + 1024) {
666
+ throw new DropgateValidationError(
667
+ "Chunk too large (client-side). Check chunk size settings."
668
+ );
669
+ }
670
+ const toHash = await uploadBlob.arrayBuffer();
671
+ const hashHex = await sha256Hex(this.cryptoObj, toHash);
672
+ const headers = {
673
+ "Content-Type": "application/octet-stream",
674
+ "X-Upload-ID": uploadId,
675
+ "X-Chunk-Index": String(i),
676
+ "X-Chunk-Hash": hashHex
677
+ };
678
+ const chunkUrl = `${baseUrl}/upload/chunk`;
679
+ await this.attemptChunkUpload(
680
+ chunkUrl,
681
+ {
682
+ method: "POST",
683
+ headers,
684
+ body: uploadBlob
685
+ },
686
+ {
687
+ retries,
688
+ backoffMs: baseBackoffMs,
689
+ maxBackoffMs,
690
+ timeoutMs: timeouts.chunkMs ?? 6e4,
691
+ signal: effectiveSignal,
692
+ progress,
693
+ chunkIndex: i,
694
+ totalChunks,
695
+ chunkSize: this.chunkSize,
696
+ fileSizeBytes
697
+ }
698
+ );
699
+ }
700
+ progress({ phase: "complete", text: "Finalising upload...", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
701
+ uploadState = "completing";
702
+ const completeRes = await fetchJson(
703
+ this.fetchFn,
704
+ `${baseUrl}/upload/complete`,
705
+ {
706
+ method: "POST",
707
+ timeoutMs: timeouts.completeMs ?? 3e4,
708
+ signal: effectiveSignal,
709
+ headers: {
710
+ "Content-Type": "application/json",
711
+ Accept: "application/json"
712
+ },
713
+ body: JSON.stringify({ uploadId })
714
+ }
657
715
  );
716
+ if (!completeRes.res.ok) {
717
+ const errorJson = completeRes.json;
718
+ const msg = errorJson?.error || "Finalisation failed.";
719
+ throw new DropgateProtocolError(msg, {
720
+ details: completeRes.json || completeRes.text
721
+ });
722
+ }
723
+ const completeJson = completeRes.json;
724
+ const fileId = completeJson?.id;
725
+ if (!fileId || typeof fileId !== "string") {
726
+ throw new DropgateProtocolError(
727
+ "Server did not return a valid file id."
728
+ );
729
+ }
730
+ let downloadUrl = `${baseUrl}/${fileId}`;
731
+ if (encrypt && keyB64) {
732
+ downloadUrl += `#${keyB64}`;
733
+ }
734
+ progress({ phase: "done", text: "Upload successful!", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
735
+ uploadState = "completed";
736
+ return {
737
+ downloadUrl,
738
+ fileId,
739
+ uploadId,
740
+ baseUrl,
741
+ ...encrypt && keyB64 ? { keyB64 } : {}
742
+ };
743
+ } catch (err) {
744
+ if (err instanceof Error && (err.name === "AbortError" || err.message?.includes("abort"))) {
745
+ uploadState = "cancelled";
746
+ onCancel?.();
747
+ } else {
748
+ uploadState = "error";
749
+ }
750
+ throw err;
658
751
  }
659
- const toHash = await uploadBlob.arrayBuffer();
660
- const hashHex = await sha256Hex(this.cryptoObj, toHash);
661
- const headers = {
662
- "Content-Type": "application/octet-stream",
663
- "X-Upload-ID": uploadId,
664
- "X-Chunk-Index": String(i),
665
- "X-Chunk-Hash": hashHex
666
- };
667
- const chunkUrl = `${baseUrl}/upload/chunk`;
668
- await this.attemptChunkUpload(
669
- chunkUrl,
670
- {
752
+ })();
753
+ const callCancelEndpoint = async (uploadId, baseUrl) => {
754
+ try {
755
+ await fetchJson(this.fetchFn, `${baseUrl}/upload/cancel`, {
671
756
  method: "POST",
672
- headers,
673
- body: uploadBlob
674
- },
675
- {
676
- retries,
677
- backoffMs: baseBackoffMs,
678
- maxBackoffMs,
679
- timeoutMs: timeouts.chunkMs ?? 6e4,
680
- signal,
681
- progress,
682
- chunkIndex: i,
683
- totalChunks,
684
- chunkSize: this.chunkSize,
685
- fileSizeBytes
686
- }
687
- );
688
- }
689
- progress({ phase: "complete", text: "Finalising upload...", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
690
- const completeRes = await fetchJson(
691
- this.fetchFn,
692
- `${baseUrl}/upload/complete`,
693
- {
694
- method: "POST",
695
- timeoutMs: timeouts.completeMs ?? 3e4,
696
- signal,
697
- headers: {
698
- "Content-Type": "application/json",
699
- Accept: "application/json"
700
- },
701
- body: JSON.stringify({ uploadId })
757
+ timeoutMs: 5e3,
758
+ headers: {
759
+ "Content-Type": "application/json",
760
+ Accept: "application/json"
761
+ },
762
+ body: JSON.stringify({ uploadId })
763
+ });
764
+ } catch {
702
765
  }
703
- );
704
- if (!completeRes.res.ok) {
705
- const errorJson = completeRes.json;
706
- const msg = errorJson?.error || "Finalisation failed.";
707
- throw new DropgateProtocolError(msg, {
708
- details: completeRes.json || completeRes.text
709
- });
710
- }
711
- const completeJson = completeRes.json;
712
- const fileId = completeJson?.id;
713
- if (!fileId || typeof fileId !== "string") {
714
- throw new DropgateProtocolError(
715
- "Server did not return a valid file id."
716
- );
717
- }
718
- let downloadUrl = `${baseUrl}/${fileId}`;
719
- if (encrypt && keyB64) {
720
- downloadUrl += `#${keyB64}`;
721
- }
722
- progress({ phase: "done", text: "Upload successful!", percent: 100, processedBytes: fileSizeBytes, totalBytes: fileSizeBytes });
766
+ };
723
767
  return {
724
- downloadUrl,
725
- fileId,
726
- uploadId,
727
- baseUrl,
728
- ...encrypt && keyB64 ? { keyB64 } : {}
768
+ result: uploadPromise,
769
+ cancel: (reason) => {
770
+ if (uploadState === "completed" || uploadState === "cancelled") return;
771
+ uploadState = "cancelled";
772
+ if (currentUploadId && currentBaseUrl) {
773
+ callCancelEndpoint(currentUploadId, currentBaseUrl).catch(() => {
774
+ });
775
+ }
776
+ internalController?.abort(new DropgateAbortError(reason || "Upload cancelled by user."));
777
+ },
778
+ getStatus: () => uploadState
729
779
  };
730
780
  }
731
781
  /**
@@ -1170,7 +1220,8 @@ async function startP2PSend(opts) {
1170
1220
  onProgress,
1171
1221
  onComplete,
1172
1222
  onError,
1173
- onDisconnect
1223
+ onDisconnect,
1224
+ onCancel
1174
1225
  } = opts;
1175
1226
  if (!file) {
1176
1227
  throw new DropgateValidationError("File is missing.");
@@ -1216,7 +1267,7 @@ async function startP2PSend(opts) {
1216
1267
  onProgress?.({ processedBytes: safeReceived, totalBytes: safeTotal, percent });
1217
1268
  };
1218
1269
  const safeError = (err) => {
1219
- if (state === "closed" || state === "completed") return;
1270
+ if (state === "closed" || state === "completed" || state === "cancelled") return;
1220
1271
  state = "closed";
1221
1272
  onError?.(err);
1222
1273
  cleanup();
@@ -1255,11 +1306,21 @@ async function startP2PSend(opts) {
1255
1306
  window.addEventListener("beforeunload", handleUnload);
1256
1307
  }
1257
1308
  const stop = () => {
1258
- if (state === "closed") return;
1259
- state = "closed";
1309
+ if (state === "closed" || state === "cancelled") return;
1310
+ const wasActive = state === "transferring" || state === "finishing";
1311
+ state = "cancelled";
1312
+ try {
1313
+ if (activeConn && activeConn.open) {
1314
+ activeConn.send({ t: "cancelled", message: "Sender cancelled the transfer." });
1315
+ }
1316
+ } catch {
1317
+ }
1318
+ if (wasActive && onCancel) {
1319
+ onCancel({ cancelledBy: "sender" });
1320
+ }
1260
1321
  cleanup();
1261
1322
  };
1262
- const isStopped = () => state === "closed";
1323
+ const isStopped = () => state === "closed" || state === "cancelled";
1263
1324
  peer.on("connection", (conn) => {
1264
1325
  if (state === "closed") return;
1265
1326
  if (activeConn) {
@@ -1329,6 +1390,13 @@ async function startP2PSend(opts) {
1329
1390
  }
1330
1391
  if (msg.t === "error") {
1331
1392
  safeError(new DropgateNetworkError(msg.message || "Receiver reported an error."));
1393
+ return;
1394
+ }
1395
+ if (msg.t === "cancelled") {
1396
+ if (state === "cancelled" || state === "closed" || state === "completed") return;
1397
+ state = "cancelled";
1398
+ onCancel?.({ cancelledBy: "receiver", message: msg.message });
1399
+ cleanup();
1332
1400
  }
1333
1401
  });
1334
1402
  conn.on("open", async () => {
@@ -1366,6 +1434,7 @@ async function startP2PSend(opts) {
1366
1434
  if (isStopped()) return;
1367
1435
  const slice = file.slice(offset, offset + chunkSize);
1368
1436
  const buf = await slice.arrayBuffer();
1437
+ if (isStopped()) return;
1369
1438
  conn.send(buf);
1370
1439
  sentBytes += buf.byteLength;
1371
1440
  if (dc) {
@@ -1415,14 +1484,14 @@ async function startP2PSend(opts) {
1415
1484
  safeError(err);
1416
1485
  });
1417
1486
  conn.on("close", () => {
1418
- if (state === "closed" || state === "completed") {
1487
+ if (state === "closed" || state === "completed" || state === "cancelled") {
1419
1488
  cleanup();
1420
1489
  return;
1421
1490
  }
1422
1491
  if (state === "transferring" || state === "finishing") {
1423
- safeError(
1424
- new DropgateNetworkError("Receiver disconnected before transfer completed.")
1425
- );
1492
+ state = "cancelled";
1493
+ onCancel?.({ cancelledBy: "receiver" });
1494
+ cleanup();
1426
1495
  } else {
1427
1496
  activeConn = null;
1428
1497
  state = "listening";
@@ -1464,7 +1533,8 @@ async function startP2PReceive(opts) {
1464
1533
  onProgress,
1465
1534
  onComplete,
1466
1535
  onError,
1467
- onDisconnect
1536
+ onDisconnect,
1537
+ onCancel
1468
1538
  } = opts;
1469
1539
  if (!code) {
1470
1540
  throw new DropgateValidationError("No sharing code was provided.");
@@ -1521,7 +1591,7 @@ async function startP2PReceive(opts) {
1521
1591
  }
1522
1592
  };
1523
1593
  const safeError = (err) => {
1524
- if (state === "closed" || state === "completed") return;
1594
+ if (state === "closed" || state === "completed" || state === "cancelled") return;
1525
1595
  state = "closed";
1526
1596
  onError?.(err);
1527
1597
  cleanup();
@@ -1553,8 +1623,18 @@ async function startP2PReceive(opts) {
1553
1623
  window.addEventListener("beforeunload", handleUnload);
1554
1624
  }
1555
1625
  const stop = () => {
1556
- if (state === "closed") return;
1557
- state = "closed";
1626
+ if (state === "closed" || state === "cancelled") return;
1627
+ const wasActive = state === "transferring";
1628
+ state = "cancelled";
1629
+ try {
1630
+ if (activeConn && activeConn.open) {
1631
+ activeConn.send({ t: "cancelled", message: "Receiver cancelled the transfer." });
1632
+ }
1633
+ } catch {
1634
+ }
1635
+ if (wasActive && onCancel) {
1636
+ onCancel({ cancelledBy: "receiver" });
1637
+ }
1558
1638
  cleanup();
1559
1639
  };
1560
1640
  peer.on("error", (err) => {
@@ -1636,6 +1716,13 @@ async function startP2PReceive(opts) {
1636
1716
  if (msg.t === "error") {
1637
1717
  throw new DropgateNetworkError(msg.message || "Sender reported an error.");
1638
1718
  }
1719
+ if (msg.t === "cancelled") {
1720
+ if (state === "cancelled" || state === "closed" || state === "completed") return;
1721
+ state = "cancelled";
1722
+ onCancel?.({ cancelledBy: "sender", message: msg.message });
1723
+ cleanup();
1724
+ return;
1725
+ }
1639
1726
  return;
1640
1727
  }
1641
1728
  let bufPromise;
@@ -1681,12 +1768,14 @@ async function startP2PReceive(opts) {
1681
1768
  }
1682
1769
  });
1683
1770
  conn.on("close", () => {
1684
- if (state === "closed" || state === "completed") {
1771
+ if (state === "closed" || state === "completed" || state === "cancelled") {
1685
1772
  cleanup();
1686
1773
  return;
1687
1774
  }
1688
1775
  if (state === "transferring") {
1689
- safeError(new DropgateNetworkError("Sender disconnected during transfer."));
1776
+ state = "cancelled";
1777
+ onCancel?.({ cancelledBy: "sender" });
1778
+ cleanup();
1690
1779
  } else if (state === "negotiating") {
1691
1780
  state = "closed";
1692
1781
  cleanup();