rmapi-js 11.2.0 → 12.0.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.js CHANGED
@@ -19,7 +19,7 @@
19
19
  * // list all items (documents and collections)
20
20
  * const [first, ...rest] = await api.listItems();
21
21
  * // rename first item
22
- * const entry = await api.rename(first.hash, "new name");
22
+ * const entry = await api.rename(first, "new name");
23
23
  * ```
24
24
  *
25
25
  * @example
@@ -187,11 +187,27 @@ export async function register(code, { deviceDesc = "browser-chrome", uuid = uui
187
187
  return await resp.text();
188
188
  }
189
189
  }
190
- /** the implementation of that api */
190
+ /**
191
+ * the api for accessing remarkable functions
192
+ *
193
+ * There are roughly two types of functions.
194
+ * - high-level api functions that provide simple access with a single round
195
+ * trip based on the web api
196
+ * - low-level wrapped functions that take more round trips, but provide more
197
+ * control and may be faster since they can be cached.
198
+ *
199
+ * Most of these functions validate the return values so that typescript is
200
+ * accurate. However, sometimes those return values are more strict than the
201
+ * "true" underlying types. If this happens, please [submit a an
202
+ * issue](https://github.com/erikbrinkman/rmapi-js/issues). In the mean time,
203
+ * you should be able to use the low level api to work around any restrictive
204
+ * validation.
205
+ */
191
206
  class Remarkable {
192
207
  #sessionToken;
193
208
  /** the same cache that underlies the raw api, allowing us to modify it */
194
209
  #cache;
210
+ /** scoped access to the raw low-level api */
195
211
  raw;
196
212
  #maxGenerationRetries;
197
213
  #maxTransientRetries;
@@ -273,7 +289,10 @@ class Remarkable {
273
289
  async #commit(entry) {
274
290
  await this.#withRetry(async () => {
275
291
  const [rootHash, generation] = await this.#getRootHash();
276
- const { entries } = await this.raw.getEntries(ROOT_SCHEMA, rootHash);
292
+ const { entries } = await this.raw.getEntries({
293
+ id: ROOT_SCHEMA,
294
+ hash: rootHash,
295
+ });
277
296
  entries.push(entry);
278
297
  const [rootEntry, uploadRoot] = await this.raw.putEntries(ROOT_LIST, entries, 4);
279
298
  await uploadRoot;
@@ -327,18 +346,21 @@ class Remarkable {
327
346
  }
328
347
  }
329
348
  async #convertEntry({ hash, id }) {
330
- const { entries } = await this.raw.getEntries(`${id}.docSchema`, hash);
349
+ const { entries } = await this.raw.getEntries({
350
+ id: `${id}.docSchema`,
351
+ hash,
352
+ });
331
353
  const metaEnt = entries.find((ent) => ent.id.endsWith(".metadata"));
332
354
  const contentEnt = entries.find((ent) => ent.id.endsWith(".content"));
333
355
  if (metaEnt === undefined) {
334
356
  throw new Error(`couldn't find metadata for hash ${hash}`);
335
357
  }
336
358
  const [{ visibleName, lastModified, pinned, parent, lastOpened, createdTime, new: isNew, source, }, content,] = await Promise.all([
337
- this.raw.getMetadata(metaEnt.id, metaEnt.hash),
359
+ this.raw.getMetadata(metaEnt),
338
360
  // collections don't always have content, since content only lists tags
339
361
  contentEnt === undefined
340
362
  ? Promise.resolve({ fileType: undefined, tags: undefined })
341
- : this.raw.getContent(contentEnt.id, contentEnt.hash),
363
+ : this.raw.getContent(contentEnt),
342
364
  ]);
343
365
  if ("templateVersion" in content) {
344
366
  return {
@@ -381,59 +403,146 @@ class Remarkable {
381
403
  };
382
404
  }
383
405
  }
384
- /** list all items */
406
+ /**
407
+ * list all items
408
+ *
409
+ * Items include both collections and documents. Documents that are in folders
410
+ * will have their parent set to something other than "" or "trash", but
411
+ * everything will be returned by this function.
412
+ *
413
+ * @example
414
+ * ```ts
415
+ * await api.listItems();
416
+ * ```
417
+ *
418
+ * @remarks
419
+ * This is now backed by the low level api, and you may notice some
420
+ * performance degradation if not taking advantage of the cache.
421
+ *
422
+ * @param refresh - if true, refresh the root hash before listing
423
+ * @returns a list of all items with some metadata
424
+ */
385
425
  async listItems(refresh = false) {
386
426
  const ids = await this.listIds(refresh);
387
427
  return await Promise.all(ids.map((id) => this.#convertEntry(id)));
388
428
  }
429
+ /**
430
+ * similar to {@link listItems | `listItems`} but backed by the low level api
431
+ *
432
+ * @param refresh - if true, refresh the root hash before listing
433
+ */
389
434
  async listIds(refresh = false) {
390
435
  const [hash] = await this.#getRootHash(refresh);
391
- const { entries } = await this.raw.getEntries(ROOT_SCHEMA, hash);
436
+ const { entries } = await this.raw.getEntries({ id: ROOT_SCHEMA, hash });
392
437
  return entries.map(({ id, hash }) => ({ id, hash }));
393
438
  }
394
- async getContent(id, hash) {
395
- const { entries } = await this.raw.getEntries(`${id}.docSchema`, hash);
439
+ /**
440
+ * get the content metadata for an item
441
+ *
442
+ * @remarks
443
+ * If this fails validation and you still want to get the content, you can use
444
+ * the low-level api to get the raw text of the `.content` file in the
445
+ * `RawEntry` for this hash.
446
+ *
447
+ * @param ref - a reference to the item (e.g. from `listItems` or `listIds`)
448
+ * @returns the content
449
+ */
450
+ async getContent({ id, hash }) {
451
+ const { entries } = await this.raw.getEntries({
452
+ id: `${id}.docSchema`,
453
+ hash,
454
+ });
396
455
  const cont = entries.find((e) => e.id.endsWith(".content"));
397
456
  if (cont === undefined) {
398
457
  throw new Error(`couldn't find contents for hash ${hash}`);
399
458
  }
400
459
  else {
401
- return await this.raw.getContent(cont.id, cont.hash);
460
+ return await this.raw.getContent(cont);
402
461
  }
403
462
  }
404
- async getMetadata(id, hash) {
405
- const { entries } = await this.raw.getEntries(`${id}.docSchema`, hash);
463
+ /**
464
+ * get the metadata for an item
465
+ *
466
+ * @remarks
467
+ * If this fails validation and you still want to get the content, you can use
468
+ * the low-level api to get the raw text of the `.metadata` file in the
469
+ * `RawEntry` for this hash.
470
+ *
471
+ * @param ref - a reference to the item (e.g. from `listItems` or `listIds`)
472
+ * @returns the metadata
473
+ */
474
+ async getMetadata({ id, hash }) {
475
+ const { entries } = await this.raw.getEntries({
476
+ id: `${id}.docSchema`,
477
+ hash,
478
+ });
406
479
  const meta = entries.find((e) => e.id.endsWith(".metadata"));
407
480
  if (meta === undefined) {
408
481
  throw new Error(`couldn't find metadata for hash ${hash}`);
409
482
  }
410
483
  else {
411
- return await this.raw.getMetadata(meta.id, meta.hash);
484
+ return await this.raw.getMetadata(meta);
412
485
  }
413
486
  }
414
- async getPdf(id, hash) {
415
- const { entries } = await this.raw.getEntries(`${id}.docSchema`, hash);
487
+ /**
488
+ * get the pdf associated with a document
489
+ *
490
+ * This returns the raw input pdf, not the rendered pdf with any markup.
491
+ *
492
+ * @param ref - a reference to the document (e.g. from `listItems`)
493
+ * @returns the pdf bytes
494
+ */
495
+ async getPdf({ id, hash }) {
496
+ const { entries } = await this.raw.getEntries({
497
+ id: `${id}.docSchema`,
498
+ hash,
499
+ });
416
500
  const pdf = entries.find((e) => e.id.endsWith(".pdf"));
417
501
  if (pdf === undefined) {
418
502
  throw new Error(`couldn't find pdf for hash ${hash}`);
419
503
  }
420
504
  else {
421
- return await this.raw.getHash(pdf.id, pdf.hash);
505
+ return await this.raw.getHash(pdf);
422
506
  }
423
507
  }
424
- async getEpub(id, hash) {
425
- const { entries } = await this.raw.getEntries(`${id}.docSchema`, hash);
508
+ /**
509
+ * get the epub associated with a document
510
+ *
511
+ * This returns the raw input epub if a document was created from an epub.
512
+ *
513
+ * @param ref - a reference to the document (e.g. from `listItems`)
514
+ * @returns the epub bytes
515
+ */
516
+ async getEpub({ id, hash }) {
517
+ const { entries } = await this.raw.getEntries({
518
+ id: `${id}.docSchema`,
519
+ hash,
520
+ });
426
521
  const epub = entries.find((e) => e.id.endsWith(".epub"));
427
522
  if (epub === undefined) {
428
523
  throw new Error(`couldn't find epub for hash ${hash}`);
429
524
  }
430
525
  else {
431
- return await this.raw.getHash(epub.id, epub.hash);
526
+ return await this.raw.getHash(epub);
432
527
  }
433
528
  }
434
- async getRmPage(id, hash, pageId) {
435
- const { entries } = await this.raw.getEntries(`${id}.docSchema`, hash);
436
- const content = await this.getContent(id, hash);
529
+ /**
530
+ * get a single page's parsed reMarkable lines (`.rm`) drawing
531
+ *
532
+ * @param ref - a reference to the document (e.g. from `listItems`)
533
+ * @param pageId - the id of the page, from the document's `.content` page
534
+ * list (see {@link getRmPages | `getRmPages`} for every page)
535
+ * @returns the parsed page, or `undefined` if the page exists but has no
536
+ * `.rm` drawing (a page you haven't drawn on has no `.rm` file)
537
+ * @throws if `pageId` is not a page of the document
538
+ */
539
+ async getRmPage(ref, pageId) {
540
+ const { id, hash } = ref;
541
+ const { entries } = await this.raw.getEntries({
542
+ id: `${id}.docSchema`,
543
+ hash,
544
+ });
545
+ const content = await this.getContent(ref);
437
546
  if (!pageOrder(content).includes(pageId)) {
438
547
  throw new Error(`document ${id} has no page ${pageId}`);
439
548
  }
@@ -442,28 +551,74 @@ class Remarkable {
442
551
  return undefined;
443
552
  }
444
553
  else {
445
- return await this.raw.getRm(entry.id, entry.hash);
554
+ return await this.raw.getRm(entry);
446
555
  }
447
556
  }
448
- async getRmPages(id, hash) {
449
- const { entries } = await this.raw.getEntries(`${id}.docSchema`, hash);
450
- const content = await this.getContent(id, hash);
557
+ /**
558
+ * get every drawn page of a document, parsed, keyed by page id
559
+ *
560
+ * Returns a map from page id to its parsed {@link RmPage | `RmPage`},
561
+ * iterating in the page order given by the document's `.content`. Pages with
562
+ * no drawing (and soft-deleted pages) are omitted. Version 3, 5, and 6 pages
563
+ * are all supported.
564
+ *
565
+ * @param ref - a reference to the document (e.g. from `listItems`)
566
+ * @returns the drawn pages, keyed by page id, in document order
567
+ */
568
+ async getRmPages(ref) {
569
+ const { id, hash } = ref;
570
+ const { entries } = await this.raw.getEntries({
571
+ id: `${id}.docSchema`,
572
+ hash,
573
+ });
574
+ const content = await this.getContent(ref);
451
575
  const byName = new Map(entries.map((entry) => [entry.id, entry]));
452
576
  const drawn = pageOrder(content)
453
577
  .map((pageId) => [pageId, byName.get(`${id}/${pageId}.rm`)])
454
578
  .filter((pair) => pair[1] !== undefined);
455
- const parsed = await Promise.all(drawn.map(([, entry]) => this.raw.getRm(entry.id, entry.hash)));
579
+ const parsed = await Promise.all(drawn.map(([, entry]) => this.raw.getRm(entry)));
456
580
  return new Map(drawn.map(([pageId], index) => [pageId, parsed[index]]));
457
581
  }
458
- async getDocumentArchive(id, hash) {
459
- const { entries } = await this.raw.getEntries(`${id}.docSchema`, hash);
582
+ /**
583
+ * get a document's entire contents as a zip archive
584
+ *
585
+ * This gets every file associated with a document and puts them into a zip
586
+ * archive.
587
+ *
588
+ * @remarks
589
+ * This is an experimental feature. The resulting archive round-trips back
590
+ * through {@link putDocumentArchive | `putDocumentArchive`}.
591
+ *
592
+ * @param ref - a reference to the document (e.g. from `listItems`)
593
+ */
594
+ async getDocumentArchive({ id, hash }) {
595
+ const { entries } = await this.raw.getEntries({
596
+ id: `${id}.docSchema`,
597
+ hash,
598
+ });
460
599
  const zip = new JSZip();
461
600
  for (const entry of entries) {
462
601
  // TODO if this is .metadata we might want to assert type === "DocumentType"
463
- zip.file(entry.id, this.raw.getHash(entry.id, entry.hash));
602
+ zip.file(entry.id, this.raw.getHash(entry));
464
603
  }
465
604
  return zip.generateAsync({ type: "uint8array" });
466
605
  }
606
+ /**
607
+ * upload a document archive produced by {@link getDocumentArchive | `getDocumentArchive`}
608
+ *
609
+ * This explodes the zip archive back into its constituent files, uploads each
610
+ * as a blob, and commits a new document into the root.
611
+ *
612
+ * @remarks
613
+ * This is an experimental feature. By default a fresh document id is generated
614
+ * so re-uploading to the same account doesn't collide with the original; pass
615
+ * {@link PutDocumentOptions.id | `id`} to keep the original id. Like the other
616
+ * low-level puts, this may throw a {@link GenerationError | `GenerationError`}
617
+ * if the generation is stale, requiring a retry.
618
+ *
619
+ * @param buffer - the archive bytes, as returned by `getDocumentArchive`
620
+ * @param options - overrides for parent, visible name, and id
621
+ */
467
622
  async putDocumentArchive(buffer, { refresh = false, parent, visibleName, id: keepId, } = {}) {
468
623
  if (parent !== undefined && parent && !idReg.test(parent)) {
469
624
  throw new ValidationError(parent, idReg, "parent must be a valid document id");
@@ -510,14 +665,6 @@ class Remarkable {
510
665
  await this.#commit(docEntry);
511
666
  return { id: newId, hash: docEntry.hash };
512
667
  }
513
- /** @deprecated renamed; use {@link getDocumentArchive | `getDocumentArchive`} */
514
- async getDocument(id, hash) {
515
- return await this.getDocumentArchive(id, hash);
516
- }
517
- /** @deprecated renamed; use {@link putDocumentArchive | `putDocumentArchive`} */
518
- async putDocument(buffer, options) {
519
- return await this.putDocumentArchive(buffer, options);
520
- }
521
668
  async #putFile(visibleName, fileType, buffer, { refresh, parent = ROOT_ID, pinned = false, zoomMode = "bestFit", viewBackgroundFilter, textScale = 1, textAlignment = "justify", fontName = "", coverPageNumber = -1, authors, title, publicationDate, publisher, extraMetadata = {}, lineHeight = -1, margins = 125, orientation = "portrait", tags, customZoomScale, customZoomCenterX, customZoomCenterY, customZoomPageWidth, customZoomPageHeight, customZoomOrientation, }) {
522
669
  if (parent && !idReg.test(parent)) {
523
670
  throw new ValidationError(parent, idReg, "parent must be a valid document id");
@@ -589,9 +736,64 @@ class Remarkable {
589
736
  await this.#commit(collectionEntry);
590
737
  return { id, hash: collectionEntry.hash };
591
738
  }
739
+ /**
740
+ * use the low-level api to add a pdf document
741
+ *
742
+ * Since this uses the low-level api, it provides more options than
743
+ * {@link uploadPdf | `uploadPdf`}, but is a little more finicky. Notably, it
744
+ * may throw a {@link GenerationError | `GenerationError`} if the generation
745
+ * doesn't match the current server generation, requiring you to retry until
746
+ * it works.
747
+ *
748
+ * @remarks
749
+ * When `zoomMode` is `"customFit"` the `customZoom*` fields describe the view,
750
+ * all in the source page's device pixels: `customZoomPageWidth` and
751
+ * `customZoomPageHeight` are the page dimensions scaled by the device dpi
752
+ * (`pagePt * dpi / 72`, see {@link deviceScreens | `deviceScreens`}), and the
753
+ * centers are in those pixels.
754
+ *
755
+ * The view always has the device's aspect ratio — you control its height and
756
+ * position, not its shape. `customZoomScale = screenHeight / viewHeight` in
757
+ * device pixels (`screenHeight` fixed per model, see {@link deviceScreens |
758
+ * `deviceScreens`}), normalized to 1:1 native pixels: at `1` the view is
759
+ * screen-tall, showing `screenHeight / customZoomPageHeight` of the page.
760
+ *
761
+ * `customZoomCenterX` offsets the center of the view horizontally from the
762
+ * page center, and `customZoomCenterY` is the absolute distance of the center
763
+ * down from the top of the page; the view's width follows from its height and
764
+ * the device aspect ratio.
765
+ *
766
+ * The fields are a single document-wide setting, but `customZoomCenterY` is
767
+ * applied against each page's own rendered height. On a page rendered taller
768
+ * than `customZoomPageHeight` that distance is a smaller fraction of the page,
769
+ * so the view sits higher and cuts off the bottom; on a shorter page it sits
770
+ * lower and cuts off the top. `customZoomScale` (a ratio) and
771
+ * `customZoomCenterX` (an offset from center) do not shift with page size.
772
+ *
773
+ * @param visibleName - the name to display on the reMarkable
774
+ * @param buffer - the raw pdf
775
+ * @param opts - put options
776
+ * @throws GenerationError if the generation doesn't match the current server generation
777
+ * @returns the entry for the newly inserted document
778
+ */
592
779
  async putPdf(visibleName, buffer, opts = {}) {
593
780
  return await this.#putFile(visibleName, "pdf", buffer, opts);
594
781
  }
782
+ /**
783
+ * use the low-level api to add an epub document
784
+ *
785
+ * Since this uses the low-level api, it provides more options than
786
+ * {@link uploadEpub | `uploadEpub`}, but is a little more finicky. Notably, it
787
+ * may throw a {@link GenerationError | `GenerationError`} if the generation
788
+ * doesn't match the current server generation, requiring you to retry until
789
+ * it works.
790
+ *
791
+ * @param visibleName - the name to display on the reMarkable
792
+ * @param buffer - the raw epub
793
+ * @param opts - put options
794
+ * @throws GenerationError if the generation doesn't match the current server generation
795
+ * @returns the entry for the newly inserted document
796
+ */
595
797
  async putEpub(visibleName, buffer, opts = {}) {
596
798
  return await this.#putFile(visibleName, "epub", buffer, opts);
597
799
  }
@@ -625,27 +827,56 @@ class Remarkable {
625
827
  await this.#commit(collectionEntry);
626
828
  return { id, hash: collectionEntry.hash };
627
829
  }
628
- /** upload an epub */
830
+ /**
831
+ * upload an epub
832
+ *
833
+ * @example
834
+ * ```ts
835
+ * await api.uploadEpub("My EPub", ...);
836
+ * ```
837
+ *
838
+ * @remarks
839
+ * this uses a simpler api that works even with schema version 4.
840
+ *
841
+ * @param visibleName - the name to show for the uploaded epub
842
+ * @param buffer - the epub contents
843
+ */
629
844
  async uploadEpub(visibleName, buffer) {
630
845
  return await this.raw.uploadFile(visibleName, buffer, "application/epub+zip");
631
846
  }
632
- /** upload a pdf */
847
+ /**
848
+ * upload a pdf
849
+ *
850
+ * @example
851
+ * ```ts
852
+ * await api.uploadPdf("My PDF", ...);
853
+ * ```
854
+ *
855
+ * @remarks
856
+ * this uses a simpler api that works even with schema version 4.
857
+ *
858
+ * @param visibleName - the name to show for the uploaded epub
859
+ * @param buffer - the epub contents
860
+ */
633
861
  async uploadPdf(visibleName, buffer) {
634
862
  return await this.raw.uploadFile(visibleName, buffer, "application/pdf");
635
863
  }
636
- /** upload a folder */
864
+ /** create a folder using the simple api */
637
865
  async uploadFolder(visibleName) {
638
866
  return await this.raw.uploadFile(visibleName, new Uint8Array(0), "folder");
639
867
  }
640
868
  /** edit just a content entry */
641
869
  async #editContentRaw(id, hash, update, schemaVersion) {
642
- const { entries } = await this.raw.getEntries(`${id}.docSchema`, hash);
870
+ const { entries } = await this.raw.getEntries({
871
+ id: `${id}.docSchema`,
872
+ hash,
873
+ });
643
874
  const contInd = entries.findIndex((ent) => ent.id.endsWith(".content"));
644
875
  const contEntry = entries[contInd];
645
876
  if (contEntry === undefined) {
646
877
  throw new Error("internal error: couldn't find content in entry hash");
647
878
  }
648
- const cont = await this.raw.getContent(contEntry.id, contEntry.hash);
879
+ const cont = await this.raw.getContent(contEntry);
649
880
  Object.assign(cont, update);
650
881
  const [newContEntry, uploadCont] = await this.raw.putContent(contEntry.id, cont);
651
882
  entries[contInd] = newContEntry;
@@ -657,7 +888,10 @@ class Remarkable {
657
888
  async #editContent(hash, update, expectedType, refresh) {
658
889
  return await this.#withRetry(async () => {
659
890
  const [rootHash, generation, schemaVersion] = await this.#getRootHash(refresh);
660
- const { entries } = await this.raw.getEntries(ROOT_SCHEMA, rootHash);
891
+ const { entries } = await this.raw.getEntries({
892
+ id: ROOT_SCHEMA,
893
+ hash: rootHash,
894
+ });
661
895
  const hashInd = entries.findIndex((ent) => ent.hash === hash);
662
896
  const hashEnt = entries[hashInd];
663
897
  if (hashEnt === undefined) {
@@ -665,7 +899,7 @@ class Remarkable {
665
899
  }
666
900
  const [[newEnt, uploadEnt], meta] = await Promise.all([
667
901
  this.#editContentRaw(hashEnt.id, hash, update, schemaVersion),
668
- this.getMetadata(hashEnt.id, hash),
902
+ this.getMetadata(hashEnt),
669
903
  ]);
670
904
  if (meta.type !== expectedType) {
671
905
  throw new Error(`expected type ${expectedType} but got ${meta.type} for hash ${hash}`);
@@ -674,29 +908,65 @@ class Remarkable {
674
908
  const [rootEntry, uploadRoot] = await this.raw.putEntries(ROOT_LIST, entries, 4);
675
909
  await Promise.all([uploadEnt, uploadRoot]);
676
910
  await this.#putRootHash(rootEntry.hash, generation);
677
- return { hash: newEnt.hash };
911
+ return { id: hashEnt.id, hash: newEnt.hash };
678
912
  });
679
913
  }
680
- /** update document content */
681
- async updateDocument(hash, content, refresh = false) {
682
- return await this.#editContent(hash, content, "DocumentType", refresh);
914
+ /**
915
+ * update content metadata for a document
916
+ *
917
+ * @example
918
+ * ```ts
919
+ * const next = await api.updateDocument(doc, { textAlignment: "left" });
920
+ * ```
921
+ *
922
+ * @param ref - a reference to the file to update
923
+ * @param content - the fields of content to update
924
+ * @returns a reference to the updated entry, with its new hash
925
+ */
926
+ async updateDocument(ref, content, refresh = false) {
927
+ return await this.#editContent(ref.hash, content, "DocumentType", refresh);
683
928
  }
684
- /** update collection content */
685
- async updateCollection(hash, content, refresh = false) {
686
- return await this.#editContent(hash, content, "CollectionType", refresh);
929
+ /**
930
+ * update content metadata for a collection
931
+ *
932
+ * @example
933
+ * ```ts
934
+ * const next = await api.updateCollection(dir, { textAlignment: "left" });
935
+ * ```
936
+ *
937
+ * @param ref - a reference to the collection to update
938
+ * @param content - the fields of content to update
939
+ * @returns a reference to the updated entry, with its new hash
940
+ */
941
+ async updateCollection(ref, content, refresh = false) {
942
+ return await this.#editContent(ref.hash, content, "CollectionType", refresh);
687
943
  }
688
- /** update template content */
689
- async updateTemplate(hash, content, refresh = false) {
690
- return await this.#editContent(hash, content, "TemplateType", refresh);
944
+ /**
945
+ * update content metadata for a template
946
+ *
947
+ * @example
948
+ * ```ts
949
+ * const next = await api.updateTemplate(tmpl, { textAlignment: "left" });
950
+ * ```
951
+ *
952
+ * @param ref - a reference to the template to update
953
+ * @param content - the fields of content to update
954
+ * @returns a reference to the updated entry, with its new hash
955
+ */
956
+ async updateTemplate(ref, content, refresh = false) {
957
+ return await this.#editContent(ref.hash, content, "TemplateType", refresh);
691
958
  }
692
959
  async #editMetaRaw(id, hash, update, schemaVersion) {
693
- const { entries } = await this.raw.getEntries(`${id}.docSchema`, hash);
960
+ const { entries } = await this.raw.getEntries({
961
+ id: `${id}.docSchema`,
962
+ hash,
963
+ });
694
964
  const metaInd = entries.findIndex((ent) => ent.id.endsWith(".metadata"));
695
965
  const metaEntry = entries[metaInd];
696
966
  if (metaEntry === undefined) {
697
967
  throw new Error("internal error: couldn't find metadata in entry hash");
698
968
  }
699
- const meta = await this.raw.getMetadata(metaEntry.id, metaEntry.hash);
969
+ const meta = await this.raw.getMetadata(metaEntry);
700
970
  Object.assign(meta, update);
701
971
  meta.version = (meta.version ?? 0) + 1;
702
972
  meta.metadatamodified = true;
@@ -709,7 +979,10 @@ class Remarkable {
709
979
  async #editMeta(hash, update, refresh = false) {
710
980
  return await this.#withRetry(async () => {
711
981
  const [rootHash, generation, schemaVersion] = await this.#getRootHash(refresh);
712
- const { entries } = await this.raw.getEntries(ROOT_SCHEMA, rootHash);
982
+ const { entries } = await this.raw.getEntries({
983
+ id: ROOT_SCHEMA,
984
+ hash: rootHash,
985
+ });
713
986
  const hashInd = entries.findIndex((ent) => ent.hash === hash);
714
987
  const hashEnt = entries[hashInd];
715
988
  if (hashEnt === undefined) {
@@ -720,41 +993,91 @@ class Remarkable {
720
993
  const [rootEntry, uploadRoot] = await this.raw.putEntries(ROOT_LIST, entries, 4);
721
994
  await Promise.all([uploadEnt, uploadRoot]);
722
995
  await this.#putRootHash(rootEntry.hash, generation);
723
- return { hash: newEnt.hash };
996
+ return { id: hashEnt.id, hash: newEnt.hash };
724
997
  });
725
998
  }
726
- /** move an entry */
727
- async move(hash, parent, refresh = false) {
999
+ /**
1000
+ * move an entry
1001
+ *
1002
+ * @example
1003
+ * ```ts
1004
+ * const next = await api.move(doc, dir.id);
1005
+ * ```
1006
+ *
1007
+ * @param ref - a reference to the entry to move
1008
+ * @param parent - the id of the directory to move the entry to, "" (root) and "trash" are special parents
1009
+ * @returns a reference to the moved entry, with its new hash
1010
+ */
1011
+ async move(ref, parent, refresh = false) {
728
1012
  if (!idReg.test(parent)) {
729
1013
  throw new ValidationError(parent, idReg, "parent must be a valid document id");
730
1014
  }
731
- return await this.#editMeta(hash, { parent }, refresh);
1015
+ return await this.#editMeta(ref.hash, { parent }, refresh);
732
1016
  }
733
- /** delete an entry */
734
- async delete(hash, refresh = false) {
735
- return await this.move(hash, TRASH_ID, refresh);
736
- }
737
- /** rename an entry */
738
- async rename(hash, visibleName, refresh = false) {
739
- return await this.#editMeta(hash, { visibleName }, refresh);
1017
+ /**
1018
+ * delete an entry
1019
+ *
1020
+ * @example
1021
+ * ```ts
1022
+ * await api.delete(file);
1023
+ * ```
1024
+ * @param ref - a reference to the entry to delete
1025
+ * @returns a reference to the deleted entry, with its new hash
1026
+ */
1027
+ async delete(ref, refresh = false) {
1028
+ return await this.move(ref, TRASH_ID, refresh);
740
1029
  }
741
- /** star or unstar an entry */
742
- async star(hash, starred, refresh = false) {
743
- return await this.#editMeta(hash, { pinned: starred }, refresh);
1030
+ /**
1031
+ * rename an entry
1032
+ *
1033
+ * @example
1034
+ * ```ts
1035
+ * const next = await api.rename(file, "new name");
1036
+ * ```
1037
+ * @param ref - a reference to the entry to rename
1038
+ * @param visibleName - the new name to assign
1039
+ * @returns a reference to the renamed entry, with its new hash
1040
+ */
1041
+ async rename(ref, visibleName, refresh = false) {
1042
+ return await this.#editMeta(ref.hash, { visibleName }, refresh);
744
1043
  }
745
- /** @deprecated misspelling; use {@link star | `star`} */
746
- async stared(hash, stared, refresh = false) {
747
- return await this.star(hash, stared, refresh);
1044
+ /**
1045
+ * star or unstar an entry
1046
+ *
1047
+ * @example
1048
+ * ```ts
1049
+ * const next = await api.star(file, true);
1050
+ * ```
1051
+ * @param ref - a reference to the entry to star
1052
+ * @param starred - whether the entry should be starred or not
1053
+ * @returns a reference to the updated entry, with its new hash
1054
+ */
1055
+ async star(ref, starred, refresh = false) {
1056
+ return await this.#editMeta(ref.hash, { pinned: starred }, refresh);
748
1057
  }
749
- /** move many hashes */
750
- async bulkMove(hashes, parent, refresh = false) {
1058
+ /**
1059
+ * move many entries
1060
+ *
1061
+ * @example
1062
+ * ```ts
1063
+ * const next = await api.bulkMove([file], dir.id);
1064
+ * ```
1065
+ *
1066
+ * @param refs - references to the entries to move
1067
+ * @param parent - the directory id to move the entries to, "" (root) and "trash" are special ids
1068
+ * @returns references to the moved entries, each with its new hash
1069
+ */
1070
+ async bulkMove(refs, parent, refresh = false) {
751
1071
  if (!idReg.test(parent)) {
752
1072
  throw new ValidationError(parent, idReg, "parent must be a valid document id");
753
1073
  }
754
1074
  return await this.#withRetry(async () => {
755
1075
  const [rootHash, generation, schemaVersion] = await this.#getRootHash(refresh);
756
- const { entries } = await this.raw.getEntries(ROOT_SCHEMA, rootHash);
757
- const hashSet = new Set(hashes);
1076
+ const { entries } = await this.raw.getEntries({
1077
+ id: ROOT_SCHEMA,
1078
+ hash: rootHash,
1079
+ });
1080
+ const hashSet = new Set(refs.map((ref) => ref.hash));
758
1081
  const toUpdate = [];
759
1082
  const newEntries = [];
760
1083
  for (const entry of entries) {
@@ -763,26 +1086,56 @@ class Remarkable {
763
1086
  }
764
1087
  const resolved = await Promise.all(toUpdate.map(({ id, hash }) => this.#editMetaRaw(id, hash, { parent }, schemaVersion)));
765
1088
  const uploads = [];
766
- const result = {};
1089
+ const result = [];
767
1090
  for (const [i, [newEnt, upload]] of resolved.entries()) {
768
1091
  newEntries.push(newEnt);
769
1092
  uploads.push(upload);
770
- result[toUpdate[i].hash] = newEnt.hash;
1093
+ result.push({ id: toUpdate[i].id, hash: newEnt.hash });
771
1094
  }
772
1095
  const [rootEntry, uploadRoot] = await this.raw.putEntries(ROOT_LIST, newEntries, 4);
773
1096
  await Promise.all([Promise.all(uploads), uploadRoot]);
774
1097
  await this.#putRootHash(rootEntry.hash, generation);
775
- return { hashes: result };
1098
+ return result;
776
1099
  });
777
1100
  }
778
- /** delete many hashes */
779
- async bulkDelete(hashes, refresh = false) {
780
- return await this.bulkMove(hashes, TRASH_ID, refresh);
1101
+ /**
1102
+ * delete many entries
1103
+ *
1104
+ * @example
1105
+ * ```ts
1106
+ * await api.bulkDelete([file]);
1107
+ * ```
1108
+ *
1109
+ * @param refs - references to the entries to delete
1110
+ * @returns references to the deleted entries, each with its new hash
1111
+ */
1112
+ async bulkDelete(refs, refresh = false) {
1113
+ return await this.bulkMove(refs, TRASH_ID, refresh);
781
1114
  }
782
- /** dump the raw cache */
1115
+ /**
1116
+ * get the current cache value as a string
1117
+ *
1118
+ * You can use this to warm start a new instance of
1119
+ * {@link remarkable | `remarkable`} with any previously cached results.
1120
+ */
783
1121
  dumpCache() {
784
1122
  return this.raw.dumpCache();
785
1123
  }
1124
+ /**
1125
+ * prune the cache so that it contains only reachable hashes
1126
+ *
1127
+ * The cache is append only, so it can grow without bound, even as hashes
1128
+ * become unreachable. In the future, this may have better cache management to
1129
+ * track this in real time, but for now, you can call this method, to keep it
1130
+ * from growing continuously.
1131
+ *
1132
+ * @remarks
1133
+ * This won't necessarily reduce the cache size. In order to see if
1134
+ * hashes are reachable we first have to search through all existing entry
1135
+ * lists.
1136
+ *
1137
+ * @param refresh - whether to refresh the root hash before pruning
1138
+ */
786
1139
  async pruneCache(refresh) {
787
1140
  const [rootHash] = await this.#getRootHash(refresh);
788
1141
  // start by assuming every cached hash is unreachable, then keep the ones we reach
@@ -792,7 +1145,7 @@ class Remarkable {
792
1145
  // should only go one step) to track all hashes encountered
793
1146
  // NOTE that we could increase the cache in this process, or it's possible
794
1147
  // for other calls to increase the cache with misc values.
795
- const base = await this.raw.getEntries(ROOT_SCHEMA, rootHash);
1148
+ const base = await this.raw.getEntries({ id: ROOT_SCHEMA, hash: rootHash });
796
1149
  let entries = [base.entries];
797
1150
  let nextEntries = [];
798
1151
  while (entries.length) {
@@ -800,7 +1153,7 @@ class Remarkable {
800
1153
  for (const { hash, subfiles, id } of entryList) {
801
1154
  toDelete.delete(hash);
802
1155
  if (subfiles > 0) {
803
- nextEntries.push(this.raw.getEntries(`${id}.docSchema`, hash));
1156
+ nextEntries.push(this.raw.getEntries({ id: `${id}.docSchema`, hash }));
804
1157
  }
805
1158
  }
806
1159
  }
@@ -812,6 +1165,12 @@ class Remarkable {
812
1165
  this.#cache.delete(key);
813
1166
  }
814
1167
  }
1168
+ /**
1169
+ * completely delete the cache
1170
+ *
1171
+ * If the cache is causing memory issues, you can clear it, but this will hurt
1172
+ * performance.
1173
+ */
815
1174
  clearCache() {
816
1175
  this.raw.clearCache();
817
1176
  }