rmapi-js 14.1.0 → 14.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -667,6 +667,28 @@ declare class Remarkable {
667
667
  * @returns a reference to the deleted entry, with its new hash
668
668
  */
669
669
  delete(ref: ItemRef, refresh?: boolean): Promise<ItemRef>;
670
+ /**
671
+ * permanently delete an entry
672
+ *
673
+ * Unlike {@link delete | `delete`}, which moves an entry to the trash where
674
+ * the device can still restore it, this drops the entry from the account
675
+ * outright. Its files stay in the cloud, but nothing points at them anymore
676
+ * and nothing brings the entry back.
677
+ *
678
+ * Only the entry named goes: purging a folder leaves everything inside it
679
+ * pointing at a parent that's no longer there. Those entries stay in the
680
+ * account and {@link listItems | `listItems`} still returns them, but no
681
+ * folder holds them, so nothing browsing the tree will find them. Purge the
682
+ * contents first, or use {@link purgeTrash | `purgeTrash`}, which takes the
683
+ * whole tree.
684
+ *
685
+ * @example
686
+ * ```ts
687
+ * await api.purge(file);
688
+ * ```
689
+ * @param ref - a reference to the entry to purge
690
+ */
691
+ purge(ref: ItemRef, refresh?: boolean): Promise<void>;
670
692
  /**
671
693
  * rename an entry
672
694
  *
@@ -716,6 +738,41 @@ declare class Remarkable {
716
738
  * @returns references to the deleted entries, each with its new hash
717
739
  */
718
740
  bulkDelete(refs: readonly ItemRef[], refresh?: boolean): Promise<ItemRef[]>;
741
+ /**
742
+ * permanently delete many entries
743
+ *
744
+ * The bulk form of {@link purge | `purge`}, done in a single root write.
745
+ *
746
+ * @example
747
+ * ```ts
748
+ * await api.bulkPurge([file]);
749
+ * ```
750
+ *
751
+ * @param refs - references to the entries to purge
752
+ */
753
+ bulkPurge(refs: readonly ItemRef[], refresh?: boolean): Promise<void>;
754
+ /**
755
+ * permanently delete everything in the trash
756
+ *
757
+ * Trashing a folder doesn't touch what's inside it — those entries keep
758
+ * naming the folder as their parent, which is what lets the device restore
759
+ * them together — so the trash holds the whole tree hanging off it, not just
760
+ * the entries whose parent is "trash". This purges all of it in one root
761
+ * write.
762
+ *
763
+ * @example
764
+ * ```ts
765
+ * await api.purgeTrash();
766
+ * ```
767
+ *
768
+ * @remarks
769
+ * Finding that tree means reading every item's metadata, so this costs about
770
+ * as much as {@link listItems | `listItems`}.
771
+ *
772
+ * @param refresh - if true, refresh the root hash before purging
773
+ * @returns references to the entries that were purged
774
+ */
775
+ purgeTrash(refresh?: boolean): Promise<ItemRef[]>;
719
776
  /**
720
777
  * listen for sync notifications
721
778
  *
package/dist/index.js CHANGED
@@ -407,26 +407,35 @@ class Remarkable {
407
407
  hash: rootHash,
408
408
  });
409
409
  entries.push(entry);
410
- let newRoot;
411
- {
412
- const env_2 = { stack: [], error: void 0, hasError: false };
413
- try {
414
- const rootEntry = __addDisposableResource(env_2, await this.raw.putEntries(ROOT_LIST, entries, 4), true);
415
- newRoot = rootEntry.hash;
416
- }
417
- catch (e_2) {
418
- env_2.error = e_2;
419
- env_2.hasError = true;
420
- }
421
- finally {
422
- const result_2 = __disposeResources(env_2);
423
- if (result_2)
424
- await result_2;
425
- }
426
- }
427
- await this.#putRootHash(newRoot, generation);
410
+ await this.#writeRoot(entries, generation);
428
411
  });
429
412
  }
413
+ /**
414
+ * upload a new root index and point the account at it
415
+ *
416
+ * Every blob the index names must already be uploaded, since the root hash
417
+ * lands as soon as the index does.
418
+ */
419
+ async #writeRoot(entries, generation) {
420
+ let newRoot;
421
+ {
422
+ const env_2 = { stack: [], error: void 0, hasError: false };
423
+ try {
424
+ const rootEntry = __addDisposableResource(env_2, await this.raw.putEntries(ROOT_LIST, entries, 4), true);
425
+ newRoot = rootEntry.hash;
426
+ }
427
+ catch (e_2) {
428
+ env_2.error = e_2;
429
+ env_2.hasError = true;
430
+ }
431
+ finally {
432
+ const result_2 = __disposeResources(env_2);
433
+ if (result_2)
434
+ await result_2;
435
+ }
436
+ }
437
+ await this.#putRootHash(newRoot, generation);
438
+ }
430
439
  async #authedFetch(url, { body, method = "POST", headers = {}, }) {
431
440
  // the root PUT is a compare-and-set; retrying a lost-but-applied response
432
441
  // would resurface as a false generation conflict and be double-applied by
@@ -1517,6 +1526,30 @@ class Remarkable {
1517
1526
  async delete(ref, refresh = false) {
1518
1527
  return await this.move(ref, TRASH_ID, refresh);
1519
1528
  }
1529
+ /**
1530
+ * permanently delete an entry
1531
+ *
1532
+ * Unlike {@link delete | `delete`}, which moves an entry to the trash where
1533
+ * the device can still restore it, this drops the entry from the account
1534
+ * outright. Its files stay in the cloud, but nothing points at them anymore
1535
+ * and nothing brings the entry back.
1536
+ *
1537
+ * Only the entry named goes: purging a folder leaves everything inside it
1538
+ * pointing at a parent that's no longer there. Those entries stay in the
1539
+ * account and {@link listItems | `listItems`} still returns them, but no
1540
+ * folder holds them, so nothing browsing the tree will find them. Purge the
1541
+ * contents first, or use {@link purgeTrash | `purgeTrash`}, which takes the
1542
+ * whole tree.
1543
+ *
1544
+ * @example
1545
+ * ```ts
1546
+ * await api.purge(file);
1547
+ * ```
1548
+ * @param ref - a reference to the entry to purge
1549
+ */
1550
+ async purge(ref, refresh = false) {
1551
+ await this.bulkPurge([ref], refresh);
1552
+ }
1520
1553
  /**
1521
1554
  * rename an entry
1522
1555
  *
@@ -1631,6 +1664,118 @@ class Remarkable {
1631
1664
  async bulkDelete(refs, refresh = false) {
1632
1665
  return await this.bulkMove(refs, TRASH_ID, refresh);
1633
1666
  }
1667
+ /**
1668
+ * permanently delete many entries
1669
+ *
1670
+ * The bulk form of {@link purge | `purge`}, done in a single root write.
1671
+ *
1672
+ * @example
1673
+ * ```ts
1674
+ * await api.bulkPurge([file]);
1675
+ * ```
1676
+ *
1677
+ * @param refs - references to the entries to purge
1678
+ */
1679
+ async bulkPurge(refs, refresh = false) {
1680
+ if (!refs.length) {
1681
+ return;
1682
+ }
1683
+ await this.#withRetry(async () => {
1684
+ const [rootHash, generation] = await this.#getRootHash(refresh);
1685
+ const { entries } = await this.raw.getEntries({
1686
+ id: ROOT_LIST,
1687
+ hash: rootHash,
1688
+ });
1689
+ const wanted = new Set(refs.map((ref) => `${ref.id}\0${ref.hash}`));
1690
+ const found = new Set();
1691
+ const newEntries = [];
1692
+ for (const entry of entries) {
1693
+ const key = `${entry.id}\0${entry.hash}`;
1694
+ if (wanted.has(key)) {
1695
+ found.add(key);
1696
+ }
1697
+ else {
1698
+ newEntries.push(entry);
1699
+ }
1700
+ }
1701
+ for (const ref of refs) {
1702
+ if (!found.has(`${ref.id}\0${ref.hash}`)) {
1703
+ throw new HashNotFoundError(ref.hash);
1704
+ }
1705
+ }
1706
+ await this.#writeRoot(newEntries, generation);
1707
+ });
1708
+ }
1709
+ /**
1710
+ * permanently delete everything in the trash
1711
+ *
1712
+ * Trashing a folder doesn't touch what's inside it — those entries keep
1713
+ * naming the folder as their parent, which is what lets the device restore
1714
+ * them together — so the trash holds the whole tree hanging off it, not just
1715
+ * the entries whose parent is "trash". This purges all of it in one root
1716
+ * write.
1717
+ *
1718
+ * @example
1719
+ * ```ts
1720
+ * await api.purgeTrash();
1721
+ * ```
1722
+ *
1723
+ * @remarks
1724
+ * Finding that tree means reading every item's metadata, so this costs about
1725
+ * as much as {@link listItems | `listItems`}.
1726
+ *
1727
+ * @param refresh - if true, refresh the root hash before purging
1728
+ * @returns references to the entries that were purged
1729
+ */
1730
+ async purgeTrash(refresh = false) {
1731
+ return await this.#withRetry(async () => {
1732
+ const [rootHash, generation] = await this.#getRootHash(refresh);
1733
+ const { entries } = await this.raw.getEntries({
1734
+ id: ROOT_LIST,
1735
+ hash: rootHash,
1736
+ });
1737
+ const parents = await Promise.all(entries.map(async (entry) => (await this.getMetadata(entry)).parent));
1738
+ const children = new Map();
1739
+ for (const [ind, entry] of entries.entries()) {
1740
+ const siblings = children.get(parents[ind]);
1741
+ if (siblings === undefined) {
1742
+ children.set(parents[ind], [entry]);
1743
+ }
1744
+ else {
1745
+ siblings.push(entry);
1746
+ }
1747
+ }
1748
+ const trashed = new Set();
1749
+ let frontier = children.get(TRASH_ID) ?? [];
1750
+ while (frontier.length) {
1751
+ const next = [];
1752
+ for (const { id } of frontier) {
1753
+ // skipping what we've already seen also ends a parent cycle, which
1754
+ // the account shouldn't have but which would loop here forever
1755
+ if (!trashed.has(id)) {
1756
+ trashed.add(id);
1757
+ next.push(...(children.get(id) ?? []));
1758
+ }
1759
+ }
1760
+ frontier = next;
1761
+ }
1762
+ if (!trashed.size) {
1763
+ return [];
1764
+ }
1765
+ const purged = [];
1766
+ const newEntries = [];
1767
+ for (const entry of entries) {
1768
+ if (trashed.has(entry.id)) {
1769
+ purged.push({ id: entry.id, hash: entry.hash });
1770
+ }
1771
+ else {
1772
+ newEntries.push(entry);
1773
+ }
1774
+ }
1775
+ await this.#writeRoot(newEntries, generation);
1776
+ return purged;
1777
+ });
1778
+ }
1634
1779
  /**
1635
1780
  * listen for sync notifications
1636
1781
  *