rmapi-js 7.0.0 → 8.0.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/README.md CHANGED
@@ -73,6 +73,7 @@ this project.
73
73
 
74
74
  - [✉️ Send Via](https://sendvia.me/) [[github](https://github.com/PaulKinlan/send-to-remarkable)] - upload to reMarkable via email
75
75
  - [ⓡ rePub](https://chromewebstore.google.com/detail/repub/blkjpagbjaekkpojgcgdapmikoaolpbl) [[github](https://github.com/hafaio/repub)] - web clipper for reMarkable that supports images and customization
76
+ - [reMarkable Digest](https://digest.ferrucc.io) - create and receive a daily digest on your reMarkable
76
77
 
77
78
  ## Contributing
78
79
 
package/dist/index.d.ts CHANGED
@@ -146,6 +146,12 @@ export declare class ValidationError extends Error {
146
146
  readonly regex: RegExp;
147
147
  constructor(field: string, regex: RegExp, message: string);
148
148
  }
149
+ /** an error that results while supplying a hash not found in the entries of the root hash */
150
+ export declare class HashNotFoundError extends Error {
151
+ /** the hash that couldn't be found */
152
+ readonly hash: string;
153
+ constructor(hash: string);
154
+ }
149
155
  /** options for registering with the api */
150
156
  export interface RegisterOptions {
151
157
  /**
@@ -671,9 +677,14 @@ export interface RemarkableApi {
671
677
  * await api.listItems();
672
678
  * ```
673
679
  *
680
+ * @remarks
681
+ * This is now backed by the low level api, and you may notice some
682
+ * performance degradation if not taking advantage of the cache.
683
+ *
684
+ * @param refresh - if true, refresh the root hash before listing
674
685
  * @returns a list of all items with some metadata
675
686
  */
676
- listItems(): Promise<Entry[]>;
687
+ listItems(refresh?: boolean): Promise<Entry[]>;
677
688
  /**
678
689
  * similar to {@link listItems | `listItems`} but backed by the low level api
679
690
  *
@@ -778,7 +789,7 @@ export interface RemarkableApi {
778
789
  */
779
790
  putEpub(visibleName: string, buffer: Uint8Array, opts?: PutOptions): Promise<SimpleEntry>;
780
791
  /** create a folder */
781
- createFolder(visibleName: string, opts?: UploadOptions): Promise<SimpleEntry>;
792
+ createFolder(visibleName: string, opts?: UploadOptions, refresh?: boolean): Promise<SimpleEntry>;
782
793
  /**
783
794
  * upload an epub
784
795
  *
@@ -787,6 +798,9 @@ export interface RemarkableApi {
787
798
  * await api.uploadEpub("My EPub", ...);
788
799
  * ```
789
800
  *
801
+ * @remarks
802
+ * this is now simply a less powerful version of {@link putEpub | `putEpub`}.
803
+ *
790
804
  * @param visibleName - the name to show for the uploaded epub
791
805
  * @param buffer - the epub contents
792
806
  */
@@ -799,6 +813,9 @@ export interface RemarkableApi {
799
813
  * await api.uploadPdf("My PDF", ...);
800
814
  * ```
801
815
  *
816
+ * @remarks
817
+ * this is now simply a less powerful version of {@link putPdf | `putPdf`}.
818
+ *
802
819
  * @param visibleName - the name to show for the uploaded epub
803
820
  * @param buffer - the epub contents
804
821
  */
@@ -814,7 +831,7 @@ export interface RemarkableApi {
814
831
  * @param hash - the hash of the file to move
815
832
  * @param parent - the id of the directory to move the entry to, "" (root) and "trash" are special parents
816
833
  */
817
- move(hash: string, parent: string): Promise<HashEntry>;
834
+ move(hash: string, parent: string, refresh?: boolean): Promise<HashEntry>;
818
835
  /**
819
836
  * delete an entry
820
837
  *
@@ -824,7 +841,7 @@ export interface RemarkableApi {
824
841
  * ```
825
842
  * @param hash - the hash of the entry to delete
826
843
  */
827
- delete(hash: string): Promise<HashEntry>;
844
+ delete(hash: string, refresh?: boolean): Promise<HashEntry>;
828
845
  /**
829
846
  * rename an entry
830
847
  *
@@ -835,7 +852,7 @@ export interface RemarkableApi {
835
852
  * @param hash - the hash of the entry to rename
836
853
  * @param visibleName - the new name to assign
837
854
  */
838
- rename(hash: string, visibleName: string): Promise<HashEntry>;
855
+ rename(hash: string, visibleName: string, refresh?: boolean): Promise<HashEntry>;
839
856
  /**
840
857
  * move many entries
841
858
  *
@@ -847,7 +864,7 @@ export interface RemarkableApi {
847
864
  * @param hashes - an array of entry hashes to move
848
865
  * @param parent - the directory id to move the entries to, "" (root) and "trash" are special ids
849
866
  */
850
- bulkMove(hashes: readonly string[], parent: string): Promise<HashesEntry>;
867
+ bulkMove(hashes: readonly string[], parent: string, refresh?: boolean): Promise<HashesEntry>;
851
868
  /**
852
869
  * delete many entries
853
870
  *
@@ -858,7 +875,7 @@ export interface RemarkableApi {
858
875
  *
859
876
  * @param hashes - the hashes of the entries to delete
860
877
  */
861
- bulkDelete(hashes: readonly string[]): Promise<HashesEntry>;
878
+ bulkDelete(hashes: readonly string[], refresh?: boolean): Promise<HashesEntry>;
862
879
  /**
863
880
  * get the current cache value as a string
864
881
  *
@@ -938,4 +955,4 @@ export interface RemarkableOptions {
938
955
  * registered. Create one with {@link register}.
939
956
  * @returns an api instance
940
957
  */
941
- export declare function remarkable(deviceToken: string, { authHost, syncHost, rawHost, cache, maxCacheSize, }?: RemarkableOptions): Promise<RemarkableApi>;
958
+ export declare function remarkable(deviceToken: string, { authHost, rawHost, cache, maxCacheSize, }?: RemarkableOptions): Promise<RemarkableApi>;
package/dist/index.js CHANGED
@@ -29,7 +29,6 @@
29
29
  *
30
30
  * const api = await remarkable(...);
31
31
  * const entry = await api.putEpub("document name", epubBuffer);
32
- * await api.create(entry);
33
32
  * ```
34
33
  *
35
34
  * @remarks
@@ -55,12 +54,11 @@
55
54
  import { fromByteArray } from "base64-js";
56
55
  import CRC32C from "crc-32/crc32c";
57
56
  import JSZip from "jszip";
58
- import { boolean, discriminator, elements, enumeration, float64, int32, nullable, properties, string, timestamp, uint32, uint8, values, } from "jtd-ts";
57
+ import { boolean, elements, enumeration, float64, int32, nullable, properties, string, timestamp, uint32, uint8, values, } from "jtd-ts";
59
58
  import { v4 as uuid4 } from "uuid";
60
59
  import { LruCache } from "./lru";
61
60
  const AUTH_HOST = "https://webapp-prod.cloud.remarkable.engineering";
62
61
  const RAW_HOST = "https://eu.tectonic.remarkable.com";
63
- const SYNC_HOST = "https://web.eu.tectonic.remarkable.com";
64
62
  const idReg = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}||trash)$/;
65
63
  const hashReg = /^[0-9a-f]{64}$/;
66
64
  const tag = properties({
@@ -72,37 +70,6 @@ const pageTag = properties({
72
70
  pageId: string(),
73
71
  timestamp: float64(),
74
72
  }, undefined, true);
75
- const commonProperties = {
76
- id: string(),
77
- hash: string(),
78
- visibleName: string(),
79
- lastModified: string(),
80
- pinned: boolean(),
81
- };
82
- const commonOptionalProperties = {
83
- parent: string(),
84
- tags: elements(properties({
85
- name: string(),
86
- timestamp: float64(),
87
- }, undefined, true)),
88
- };
89
- const entry = discriminator("type", {
90
- CollectionType: properties(commonProperties, commonOptionalProperties, true),
91
- DocumentType: properties({
92
- ...commonProperties,
93
- lastOpened: string(),
94
- fileType: enumeration("epub", "pdf", "notebook"),
95
- }, commonOptionalProperties, true),
96
- });
97
- const entries = elements(entry);
98
- const uploadEntry = properties({
99
- docID: string(),
100
- hash: string(),
101
- }, undefined, true);
102
- const hashEntry = properties({ hash: string() }, undefined, true);
103
- const hashesEntry = properties({
104
- hashes: values(string()),
105
- }, undefined, true);
106
73
  /** An error that gets thrown when the backend while trying to update
107
74
  *
108
75
  * IF you encounter this error, you likely just need to try th request again. If
@@ -141,6 +108,15 @@ export class ValidationError extends Error {
141
108
  this.regex = regex;
142
109
  }
143
110
  }
111
+ /** an error that results while supplying a hash not found in the entries of the root hash */
112
+ export class HashNotFoundError extends Error {
113
+ /** the hash that couldn't be found */
114
+ hash;
115
+ constructor(hash) {
116
+ super(`'${hash}' not found in the root hash`);
117
+ this.hash = hash;
118
+ }
119
+ }
144
120
  /**
145
121
  * register a device and get the token needed to access the api
146
122
  *
@@ -569,14 +545,12 @@ class RawRemarkable {
569
545
  /** the implementation of that api */
570
546
  class Remarkable {
571
547
  #userToken;
572
- #syncHost;
573
548
  /** the same cache that underlies the raw api, allowing us to modify it */
574
549
  #cache;
575
550
  raw;
576
551
  #lastHashGen;
577
- constructor(userToken, syncHost, rawHost, cache) {
552
+ constructor(userToken, rawHost, cache) {
578
553
  this.#userToken = userToken;
579
- this.#syncHost = syncHost;
580
554
  this.#cache = cache;
581
555
  this.raw = new RawRemarkable((method, url, { body, headers } = {}) => this.#authedFetch(url, { method, body, headers }), cache, rawHost);
582
556
  }
@@ -620,38 +594,51 @@ class Remarkable {
620
594
  return resp;
621
595
  }
622
596
  }
623
- /** a generic request to the new files api
624
- *
625
- * @param meta - remarkable metadata to set, often json formatted or empty
626
- * @param method - the http method to use
627
- * @param contentType - the http content type to set
628
- * @param body - body content, often raw bytes or json
629
- * @param hash - the hash of a specific file to target
630
- */
631
- async #fileRequest({ meta = "", method = "GET",
632
- // eslint-disable-next-line spellcheck/spell-checker
633
- contentType = "text/plain;charset=UTF-8", body, hash, } = {}) {
634
- const encoder = new TextEncoder();
635
- const encMeta = encoder.encode(meta);
636
- const suffix = hash === undefined ? "" : `/${hash}`;
637
- const resp = await this.#authedFetch(`${this.#syncHost}/doc/v2/files${suffix}`, {
638
- body,
639
- method,
640
- headers: {
641
- "content-type": contentType,
642
- "rm-meta": fromByteArray(encMeta),
643
- "rm-source": "WebLibrary",
644
- },
645
- });
646
- const raw = await resp.text();
647
- return JSON.parse(raw);
597
+ async #convertEntry({ hash, id }) {
598
+ const entries = await this.raw.getEntries(hash);
599
+ const metaEnt = entries.find((ent) => ent.id.endsWith(".metadata"));
600
+ const contentEnt = entries.find((ent) => ent.id.endsWith(".content"));
601
+ if (metaEnt === undefined) {
602
+ throw new Error(`couldn't find metadata for hash ${hash}`);
603
+ }
604
+ else if (contentEnt === undefined) {
605
+ throw new Error(`couldn't find content for hash ${hash}`);
606
+ }
607
+ const [{ visibleName, lastModified, pinned, parent, lastOpened }, content] = await Promise.all([
608
+ this.raw.getMetadata(metaEnt.hash),
609
+ this.raw.getContent(contentEnt.hash),
610
+ ]);
611
+ if (content.fileType === undefined) {
612
+ return {
613
+ id,
614
+ hash,
615
+ visibleName,
616
+ lastModified,
617
+ pinned,
618
+ parent,
619
+ tags: content.tags,
620
+ type: "CollectionType",
621
+ };
622
+ }
623
+ else {
624
+ return {
625
+ id,
626
+ hash,
627
+ visibleName,
628
+ lastModified,
629
+ pinned,
630
+ parent,
631
+ tags: content.tags,
632
+ lastOpened: lastOpened ?? "",
633
+ fileType: content.fileType,
634
+ type: "DocumentType",
635
+ };
636
+ }
648
637
  }
649
638
  /** list all items */
650
- async listItems() {
651
- const res = await this.#fileRequest();
652
- if (!entries.guardAssert(res))
653
- throw Error("invalid entries");
654
- return res;
639
+ async listItems(refresh = false) {
640
+ const ids = await this.listIds(refresh);
641
+ return await Promise.all(ids.map((id) => this.#convertEntry(id)));
655
642
  }
656
643
  async listIds(refresh = false) {
657
644
  const [hash] = await this.#getRootHash(refresh);
@@ -713,45 +700,46 @@ class Remarkable {
713
700
  }
714
701
  const id = uuid4();
715
702
  const now = new Date();
716
- const enc = new TextEncoder();
703
+ const metadata = {
704
+ parent,
705
+ pinned,
706
+ lastModified: (+now).toFixed(),
707
+ createdTime: (+now).toFixed(),
708
+ type: "DocumentType",
709
+ visibleName,
710
+ lastOpened: "0",
711
+ lastOpenedPage: 0,
712
+ };
713
+ const content = {
714
+ coverPageNumber,
715
+ documentMetadata: { authors, title, publicationDate, publisher },
716
+ extraMetadata,
717
+ lineHeight,
718
+ margins,
719
+ orientation,
720
+ fileType,
721
+ formatVersion: 1,
722
+ tags: tags?.map((name) => ({ name, timestamp: +now })) ?? [],
723
+ fontName,
724
+ textAlignment,
725
+ textScale,
726
+ zoomMode,
727
+ viewBackgroundFilter,
728
+ // NOTE for some reason we need to "fake" the number of pages at 1, and
729
+ // create "valid" output for that
730
+ originalPageCount: 1,
731
+ pageCount: 1,
732
+ pageTags: [],
733
+ pages: [uuid4()],
734
+ redirectionPageMap: [0],
735
+ sizeInBytes: buffer.length.toFixed(),
736
+ };
717
737
  // upload raw files, and get root hash
718
738
  const [[contentEntry, uploadContent], [metadataEntry, uploadMetadata], [pagedataEntry, uploadPagedata], [fileEntry, uploadFile], [rootHash, generation],] = await Promise.all([
719
- this.raw.putContent(`${id}.content`, {
720
- coverPageNumber,
721
- documentMetadata: { authors, title, publicationDate, publisher },
722
- extraMetadata,
723
- lineHeight,
724
- margins,
725
- orientation,
726
- fileType,
727
- formatVersion: 1,
728
- tags: tags?.map((name) => ({ name, timestamp: +now })) ?? [],
729
- fontName,
730
- textAlignment,
731
- textScale,
732
- zoomMode,
733
- viewBackgroundFilter,
734
- // NOTE for some reason we need to "fake" the number of pages at 1, and
735
- // create "valid" output for that
736
- originalPageCount: 1,
737
- pageCount: 1,
738
- pageTags: [],
739
- pages: [uuid4()],
740
- redirectionPageMap: [0],
741
- sizeInBytes: buffer.length.toFixed(),
742
- }),
743
- this.raw.putMetadata(`${id}.metadata`, {
744
- parent,
745
- pinned,
746
- lastModified: (+now).toFixed(),
747
- createdTime: (+now).toFixed(),
748
- type: "DocumentType",
749
- visibleName,
750
- lastOpened: "0",
751
- lastOpenedPage: 0,
752
- }),
739
+ this.raw.putContent(`${id}.content`, content),
740
+ this.raw.putMetadata(`${id}.metadata`, metadata),
753
741
  // eslint-disable-next-line spellcheck/spell-checker
754
- this.raw.putFile(`${id}.pagedata`, enc.encode("\n")),
742
+ this.raw.putText(`${id}.pagedata`, "\n"),
755
743
  this.raw.putFile(`${id}.${fileType}`, buffer),
756
744
  this.#getRootHash(refresh),
757
745
  ]);
@@ -777,6 +765,10 @@ class Remarkable {
777
765
  uploadCollection,
778
766
  uploadRoot,
779
767
  ]);
768
+ // TODO we could return a full entry here, but we should probably decide
769
+ // what that should be, e.g. we could return more fields than the standard
770
+ // entry. Same for putFolder
771
+ // TODO we should also decide if the api should take hashes or ids...
780
772
  await this.#putRootHash(rootEntry.hash, generation);
781
773
  return { id, hash: collectionEntry.hash };
782
774
  }
@@ -786,96 +778,134 @@ class Remarkable {
786
778
  async putEpub(visibleName, buffer, opts = {}) {
787
779
  return await this.#putFile(visibleName, "epub", buffer, opts);
788
780
  }
789
- /** upload a file */
790
- async #uploadFile(parent, visibleName, buffer, contentType) {
791
- if (!idReg.test(parent)) {
781
+ /** create a folder */
782
+ async createFolder(visibleName, { parent = "" } = {}, refresh = false) {
783
+ if (parent && !idReg.test(parent)) {
792
784
  throw new ValidationError(parent, idReg, "parent must be a valid document id");
793
785
  }
794
- const res = await this.#fileRequest({
795
- meta: JSON.stringify({ parent, file_name: visibleName }),
796
- method: "POST",
797
- contentType,
798
- body: buffer,
799
- });
800
- this.#lastHashGen = undefined; // clear the hash gen since this will change it
801
- if (!uploadEntry.guardAssert(res))
802
- throw Error("invalid upload entry");
803
- const { hash, docID: id } = res;
804
- return { hash, id };
805
- }
806
- /** create a folder */
807
- async createFolder(visibleName, { parent = "" } = {}) {
808
- return await this.#uploadFile(parent, visibleName, new Uint8Array(0), "folder");
786
+ const id = uuid4();
787
+ const now = new Date();
788
+ const content = {
789
+ tags: [],
790
+ };
791
+ const metadata = {
792
+ lastModified: (+now).toFixed(),
793
+ createdTime: (+now).toFixed(),
794
+ parent,
795
+ pinned: false,
796
+ type: "CollectionType",
797
+ visibleName,
798
+ };
799
+ // upload folder contents
800
+ const [[contentEntry, uploadContent], [metadataEntry, uploadMetadata], [rootHash, generation],] = await Promise.all([
801
+ this.raw.putContent(`${id}.content`, content),
802
+ this.raw.putMetadata(`${id}.metadata`, metadata),
803
+ this.#getRootHash(refresh),
804
+ ]);
805
+ // now fetch root entries and upload this file entry
806
+ const [[collectionEntry, uploadCollection], rootEntries] = await Promise.all([
807
+ this.raw.putEntries(id, [contentEntry, metadataEntry]),
808
+ this.raw.getEntries(rootHash),
809
+ ]);
810
+ // now upload a new root entry
811
+ rootEntries.push(collectionEntry);
812
+ const [rootEntry, uploadRoot] = await this.raw.putEntries("root", rootEntries);
813
+ // before updating the root hash, first upload everything
814
+ await Promise.all([
815
+ uploadContent,
816
+ uploadMetadata,
817
+ uploadCollection,
818
+ uploadRoot,
819
+ ]);
820
+ // put root hash and return
821
+ await this.#putRootHash(rootEntry.hash, generation);
822
+ return { id, hash: collectionEntry.hash };
809
823
  }
810
824
  /** upload an epub */
811
- async uploadEpub(visibleName, buffer, { parent = "" } = {}) {
812
- return await this.#uploadFile(parent, visibleName, buffer, "application/epub+zip");
825
+ async uploadEpub(visibleName, buffer, opts = {}) {
826
+ return await this.putEpub(visibleName, buffer, opts);
813
827
  }
814
828
  /** upload a pdf */
815
- async uploadPdf(visibleName, buffer, { parent = "" } = {}) {
816
- return await this.#uploadFile(parent, visibleName, buffer, "application/pdf");
829
+ async uploadPdf(visibleName, buffer, opts = {}) {
830
+ return await this.putPdf(visibleName, buffer, opts);
817
831
  }
818
- async #modify(hash, properties) {
819
- if (!hashReg.test(hash)) {
820
- throw new ValidationError(hash, hashReg, "hash to modify was not a valid hash");
821
- }
822
- // this does not allow setting pinned, although I don't know why
823
- const res = await this.#fileRequest({
824
- hash,
825
- body: JSON.stringify(properties),
826
- method: "PATCH",
827
- });
828
- this.#lastHashGen = undefined; // clear the hash gen since this will change it
829
- if (!hashEntry.guardAssert(res))
830
- throw Error("invalid hash entry");
831
- return res;
832
+ async #editMetaRaw(id, hash, update) {
833
+ const entries = await this.raw.getEntries(hash);
834
+ const metaInd = entries.findIndex((ent) => ent.id.endsWith(".metadata"));
835
+ const metaEntry = entries[metaInd];
836
+ if (metaEntry === undefined) {
837
+ throw new Error("internal error: couldn't find metadata in entry hash");
838
+ }
839
+ const meta = await this.raw.getMetadata(metaEntry.hash);
840
+ Object.assign(meta, update);
841
+ const [newMetaEntry, uploadMeta] = await this.raw.putMetadata(metaEntry.id, meta);
842
+ entries[metaInd] = newMetaEntry;
843
+ const [result, uploadentries] = await this.raw.putEntries(id, entries);
844
+ const upload = Promise.all([uploadMeta, uploadentries]).then(() => { });
845
+ return [result, upload];
846
+ }
847
+ async #editMeta(hash, update, refresh = false) {
848
+ const [rootHash, generation] = await this.#getRootHash(refresh);
849
+ const entries = await this.raw.getEntries(rootHash);
850
+ const hashInd = entries.findIndex((ent) => ent.hash === hash);
851
+ const hashEnt = entries[hashInd];
852
+ if (hashEnt === undefined) {
853
+ throw new HashNotFoundError(hash);
854
+ }
855
+ const [newEnt, uploadEnt] = await this.#editMetaRaw(hashEnt.id, hash, update);
856
+ entries[hashInd] = newEnt;
857
+ const [rootEntry, uploadRoot] = await this.raw.putEntries("root", entries);
858
+ await Promise.all([uploadEnt, uploadRoot]);
859
+ await this.#putRootHash(rootEntry.hash, generation);
860
+ return { hash: newEnt.hash };
832
861
  }
833
862
  /** move an entry */
834
- async move(hash, parent) {
863
+ async move(hash, parent, refresh = false) {
835
864
  if (!idReg.test(parent)) {
836
865
  throw new ValidationError(parent, idReg, "parent must be a valid document id");
837
866
  }
838
- return await this.#modify(hash, { parent });
867
+ return await this.#editMeta(hash, { parent }, refresh);
839
868
  }
840
869
  /** delete an entry */
841
- async delete(hash) {
842
- return await this.move(hash, "trash");
870
+ async delete(hash, refresh = false) {
871
+ return await this.move(hash, "trash", refresh);
843
872
  }
844
873
  /** rename an entry */
845
- async rename(hash, visibleName) {
846
- return await this.#modify(hash, { file_name: visibleName });
847
- }
848
- /** bulk modify hashes */
849
- async #bulkModify(hashes, properties) {
850
- const invalidHashes = hashes.filter((hash) => !hashReg.test(hash));
851
- if (invalidHashes.length) {
852
- throw new ValidationError(hashes.join(", "), hashReg, "hashes to modify were not a valid hashes");
853
- }
854
- // this does not allow setting pinned, although I don't know why
855
- const res = await this.#fileRequest({
856
- body: JSON.stringify({
857
- updates: properties,
858
- hashes,
859
- }),
860
- method: "PATCH",
861
- });
862
- this.#lastHashGen = undefined; // clear the hash gen since this will change it
863
- if (!hashesEntry.guardAssert(res))
864
- throw Error("invalid hashes entry");
865
- return res;
874
+ async rename(hash, visibleName, refresh = false) {
875
+ return await this.#editMeta(hash, { visibleName }, refresh);
866
876
  }
867
877
  /** move many hashes */
868
- async bulkMove(hashes, parent) {
878
+ async bulkMove(hashes, parent, refresh = false) {
869
879
  if (!idReg.test(parent)) {
870
880
  throw new ValidationError(parent, idReg, "parent must be a valid document id");
871
881
  }
872
- return await this.#bulkModify(hashes, { parent });
882
+ const [rootHash, generation] = await this.#getRootHash(refresh);
883
+ const entries = await this.raw.getEntries(rootHash);
884
+ const hashSet = new Set(hashes);
885
+ const toUpdate = [];
886
+ const newEntries = [];
887
+ for (const entry of entries) {
888
+ const part = hashSet.has(entry.hash) ? toUpdate : newEntries;
889
+ part.push(entry);
890
+ }
891
+ const resolved = await Promise.all(toUpdate.map(({ id, hash }) => this.#editMetaRaw(id, hash, { parent })));
892
+ const uploads = [];
893
+ const result = {};
894
+ for (const [i, [newEnt, upload]] of resolved.entries()) {
895
+ newEntries.push(newEnt);
896
+ uploads.push(upload);
897
+ result[toUpdate[i].hash] = newEnt.hash;
898
+ }
899
+ const [rootEntry, uploadRoot] = await this.raw.putEntries("root", newEntries);
900
+ uploads.push(uploadRoot);
901
+ await Promise.all(uploads);
902
+ await this.#putRootHash(rootEntry.hash, generation);
903
+ return { hashes: result };
873
904
  }
874
905
  /** delete many hashes */
875
- async bulkDelete(hashes) {
876
- return await this.bulkMove(hashes, "trash");
906
+ async bulkDelete(hashes, refresh = false) {
907
+ return await this.bulkMove(hashes, "trash", refresh);
877
908
  }
878
- // TODO ostensibly we could implement a bulk rename but idk why
879
909
  /** dump the raw cache */
880
910
  dumpCache() {
881
911
  return this.raw.dumpCache();
@@ -922,7 +952,7 @@ const cached = values(nullable(string()));
922
952
  * registered. Create one with {@link register}.
923
953
  * @returns an api instance
924
954
  */
925
- export async function remarkable(deviceToken, { authHost = AUTH_HOST, syncHost = SYNC_HOST, rawHost = RAW_HOST, cache, maxCacheSize = Infinity, } = {}) {
955
+ export async function remarkable(deviceToken, { authHost = AUTH_HOST, rawHost = RAW_HOST, cache, maxCacheSize = Infinity, } = {}) {
926
956
  const resp = await fetch(`${authHost}/token/json/2/user/new`, {
927
957
  method: "POST",
928
958
  headers: {
@@ -939,7 +969,7 @@ export async function remarkable(deviceToken, { authHost = AUTH_HOST, syncHost =
939
969
  const cache = maxCacheSize === Infinity
940
970
  ? new Map(entries)
941
971
  : new LruCache(maxCacheSize, entries);
942
- return new Remarkable(userToken, syncHost, rawHost, cache);
972
+ return new Remarkable(userToken, rawHost, cache);
943
973
  }
944
974
  else {
945
975
  throw new Error("cache was not a valid cache (json string mapping); your cache must be corrupted somehow. Either initialize remarkable without a cache, or fix its format.");