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/README.md +3 -2
- package/dist/index.d.ts +72 -100
- package/dist/index.js +448 -89
- package/dist/raw.d.ts +47 -66
- package/dist/raw.js +216 -12
- package/dist/rmapi-js.esm.min.js +8 -8
- package/package.json +1 -1
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
|
|
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
|
-
/**
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
395
|
-
|
|
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
|
|
460
|
+
return await this.raw.getContent(cont);
|
|
402
461
|
}
|
|
403
462
|
}
|
|
404
|
-
|
|
405
|
-
|
|
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
|
|
484
|
+
return await this.raw.getMetadata(meta);
|
|
412
485
|
}
|
|
413
486
|
}
|
|
414
|
-
|
|
415
|
-
|
|
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
|
|
505
|
+
return await this.raw.getHash(pdf);
|
|
422
506
|
}
|
|
423
507
|
}
|
|
424
|
-
|
|
425
|
-
|
|
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
|
|
526
|
+
return await this.raw.getHash(epub);
|
|
432
527
|
}
|
|
433
528
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
554
|
+
return await this.raw.getRm(entry);
|
|
446
555
|
}
|
|
447
556
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
|
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
|
-
|
|
459
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
/**
|
|
681
|
-
|
|
682
|
-
|
|
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
|
-
/**
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
/**
|
|
689
|
-
|
|
690
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
/**
|
|
727
|
-
|
|
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
|
-
/**
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
/**
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
/**
|
|
746
|
-
|
|
747
|
-
|
|
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
|
-
/**
|
|
750
|
-
|
|
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(
|
|
757
|
-
|
|
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
|
|
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
|
|
1098
|
+
return result;
|
|
776
1099
|
});
|
|
777
1100
|
}
|
|
778
|
-
/**
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
/**
|
|
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
|
}
|