rmapi-js 2.2.0 → 3.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/CHANGELOG.md +51 -0
- package/bundle/rmapi-js.cjs.min.js +12 -4
- package/bundle/rmapi-js.esm.min.js +12 -4
- package/bundle/rmapi-js.iife.min.js +12 -4
- package/dist/index.d.ts +114 -41
- package/dist/index.js +173 -73
- package/package.json +17 -7
package/dist/index.js
CHANGED
|
@@ -15,9 +15,8 @@
|
|
|
15
15
|
* const token = await register(code)
|
|
16
16
|
* // persist token
|
|
17
17
|
* const api = await remarkable(token);
|
|
18
|
-
* const
|
|
19
|
-
* const
|
|
20
|
-
* for (const entry of fileEntries) {
|
|
18
|
+
* const rootEntries = await api.getEntries();
|
|
19
|
+
* for (const entry of rootEntries) {
|
|
21
20
|
* const children = await api.getEntries(entry.hash);
|
|
22
21
|
* for (const { hash, documentId } of children) {
|
|
23
22
|
* if (documentId.endsWith(".metadata")) {
|
|
@@ -42,6 +41,7 @@
|
|
|
42
41
|
* @packageDocumentation
|
|
43
42
|
*/
|
|
44
43
|
import { fromByteArray } from "base64-js";
|
|
44
|
+
import stringify from "json-stable-stringify";
|
|
45
45
|
import { v4 as uuid4 } from "uuid";
|
|
46
46
|
import { concatBuffers, fromHex, toHex } from "./utils";
|
|
47
47
|
import { validate } from "./validate";
|
|
@@ -295,15 +295,22 @@ export function parseEntry(line) {
|
|
|
295
295
|
class Remarkable {
|
|
296
296
|
#userToken;
|
|
297
297
|
#fetch;
|
|
298
|
-
#cache;
|
|
299
298
|
#subtle;
|
|
300
299
|
#syncHost;
|
|
301
|
-
|
|
300
|
+
// caches
|
|
301
|
+
#cacheLimitBytes;
|
|
302
|
+
#cache = new Map();
|
|
303
|
+
#rootCache = null;
|
|
304
|
+
constructor(userToken, fetch, subtle, syncHost, cacheLimitBytes, initCache) {
|
|
302
305
|
this.#userToken = userToken;
|
|
303
306
|
this.#fetch = fetch;
|
|
304
|
-
this.#cache = cache;
|
|
305
307
|
this.#subtle = subtle;
|
|
306
308
|
this.#syncHost = syncHost;
|
|
309
|
+
this.#cacheLimitBytes = cacheLimitBytes;
|
|
310
|
+
// set cache
|
|
311
|
+
for (const [hash, val] of initCache) {
|
|
312
|
+
this.#cache.set(hash, Promise.resolve(val));
|
|
313
|
+
}
|
|
307
314
|
}
|
|
308
315
|
/** make an authorized request to remarkable */
|
|
309
316
|
async #authedFetch(url, { body, method = "POST", headers = {}, }) {
|
|
@@ -348,7 +355,7 @@ class Remarkable {
|
|
|
348
355
|
async #getUrl(relativePath, gen, rootHash) {
|
|
349
356
|
const key = gen === undefined ? "downloads" : "uploads";
|
|
350
357
|
// NOTE this is done manually to serialize the bigints appropriately
|
|
351
|
-
const body = rootHash
|
|
358
|
+
const body = rootHash && gen !== null && gen !== undefined
|
|
352
359
|
? `{ "http_method": "PUT", "relative_path": "${relativePath}", "root_schema": "${rootHash}", "generation": ${gen} }`
|
|
353
360
|
: JSON.stringify({ http_method: "GET", relative_path: relativePath });
|
|
354
361
|
const resp = await this.#authedFetch(`${this.#syncHost}/sync/v2/signed-urls/${key}`, { body });
|
|
@@ -360,14 +367,38 @@ class Remarkable {
|
|
|
360
367
|
/**
|
|
361
368
|
* get the root hash and the current generation
|
|
362
369
|
*/
|
|
363
|
-
async getRootHash() {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
370
|
+
async getRootHash({ cache = true } = {}) {
|
|
371
|
+
if (cache) {
|
|
372
|
+
while (this.#rootCache) {
|
|
373
|
+
try {
|
|
374
|
+
const [hash, gen] = await this.#rootCache;
|
|
375
|
+
return [hash, gen];
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// noop
|
|
379
|
+
}
|
|
380
|
+
}
|
|
369
381
|
}
|
|
370
|
-
|
|
382
|
+
const prom = (async () => {
|
|
383
|
+
try {
|
|
384
|
+
const signed = await this.#getUrl("root");
|
|
385
|
+
const resp = await this.#signedFetch(signed);
|
|
386
|
+
const generation = resp.headers.get(GENERATION_HEADER);
|
|
387
|
+
if (!generation) {
|
|
388
|
+
throw new Error("no generation header in root hash");
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
return [await resp.text(), BigInt(generation)];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch (ex) {
|
|
395
|
+
this.#rootCache = null;
|
|
396
|
+
throw ex;
|
|
397
|
+
}
|
|
398
|
+
})();
|
|
399
|
+
this.#rootCache = prom;
|
|
400
|
+
const [hash, gen] = await prom;
|
|
401
|
+
return [hash, gen];
|
|
371
402
|
}
|
|
372
403
|
/**
|
|
373
404
|
* write the root hash, incrementing from the current generation
|
|
@@ -382,55 +413,92 @@ class Remarkable {
|
|
|
382
413
|
}
|
|
383
414
|
catch (ex) {
|
|
384
415
|
if (ex instanceof ResponseError && ex.status === 412) {
|
|
416
|
+
this.#rootCache = null;
|
|
385
417
|
throw new GenerationError();
|
|
386
418
|
}
|
|
387
419
|
else {
|
|
388
420
|
throw ex;
|
|
389
421
|
}
|
|
390
422
|
}
|
|
391
|
-
const
|
|
392
|
-
if (!
|
|
423
|
+
const genStr = resp.headers.get(GENERATION_HEADER);
|
|
424
|
+
if (!genStr) {
|
|
393
425
|
throw new Error("no generation header in root hash");
|
|
394
426
|
}
|
|
395
|
-
|
|
427
|
+
const gen = BigInt(genStr);
|
|
428
|
+
this.#rootCache = Promise.resolve([hash, gen]);
|
|
429
|
+
return gen;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* get content associated with hash
|
|
433
|
+
*/
|
|
434
|
+
async #getHash(hash) {
|
|
435
|
+
let cached = this.#cache.get(hash);
|
|
436
|
+
while (cached) {
|
|
437
|
+
try {
|
|
438
|
+
const val = await cached;
|
|
439
|
+
if (val) {
|
|
440
|
+
return val;
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
cached = undefined; // break
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
// try again if promise rejected
|
|
448
|
+
cached = this.#cache.get(hash);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const prom = (async () => {
|
|
452
|
+
const signed = await this.#getUrl(hash);
|
|
453
|
+
const resp = await this.#signedFetch(signed);
|
|
454
|
+
return await resp.arrayBuffer();
|
|
455
|
+
})();
|
|
456
|
+
// set cache with appropriate promise that cleans up on rejection
|
|
457
|
+
this.#cache.set(hash, prom.then((buff) => (buff.byteLength < this.#cacheLimitBytes ? buff : null), (ex) => {
|
|
458
|
+
this.#cache.delete(hash);
|
|
459
|
+
throw ex;
|
|
460
|
+
}));
|
|
461
|
+
return await prom;
|
|
396
462
|
}
|
|
397
463
|
/**
|
|
398
464
|
* get text content associated with hash
|
|
399
465
|
*/
|
|
400
466
|
async getBuffer(hash) {
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
return
|
|
467
|
+
const buff = await this.#getHash(hash);
|
|
468
|
+
// copy if it is long enough to be cached to preven mutations
|
|
469
|
+
return buff.byteLength < this.#cacheLimitBytes ? buff.slice(0) : buff;
|
|
404
470
|
}
|
|
405
471
|
/**
|
|
406
472
|
* get text content associated with hash
|
|
407
473
|
*/
|
|
408
474
|
async getText(hash) {
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
475
|
+
const buff = await this.#getHash(hash);
|
|
476
|
+
const decoder = new TextDecoder();
|
|
477
|
+
return decoder.decode(buff);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* get json content associated with hash
|
|
481
|
+
*/
|
|
482
|
+
async getJson(hash) {
|
|
483
|
+
const str = await this.getText(hash);
|
|
484
|
+
return JSON.parse(str);
|
|
420
485
|
}
|
|
421
486
|
/**
|
|
422
487
|
* get metadata from hash
|
|
423
488
|
*/
|
|
424
489
|
async getMetadata(hash) {
|
|
425
|
-
const raw = await this.
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
return parsed;
|
|
490
|
+
const raw = await this.getJson(hash);
|
|
491
|
+
validate(metadataSchema, raw);
|
|
492
|
+
return raw;
|
|
429
493
|
}
|
|
430
494
|
/**
|
|
431
495
|
* get entries from a collection hash
|
|
432
496
|
*/
|
|
433
497
|
async getEntries(hash) {
|
|
498
|
+
if (hash === undefined) {
|
|
499
|
+
const [newHash] = await this.getRootHash({ cache: true });
|
|
500
|
+
hash = newHash;
|
|
501
|
+
}
|
|
434
502
|
const raw = await this.getText(hash);
|
|
435
503
|
// slice for trailing new line
|
|
436
504
|
const [schema, ...lines] = raw.slice(0, -1).split("\n");
|
|
@@ -441,8 +509,31 @@ class Remarkable {
|
|
|
441
509
|
}
|
|
442
510
|
/** upload data to hash */
|
|
443
511
|
async #putHash(hash, body) {
|
|
444
|
-
|
|
445
|
-
|
|
512
|
+
// try cached version
|
|
513
|
+
const cached = this.#cache.get(hash);
|
|
514
|
+
if (cached) {
|
|
515
|
+
try {
|
|
516
|
+
await cached;
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
// noop
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// if missing or rejected then put for real
|
|
524
|
+
const prom = (async () => {
|
|
525
|
+
try {
|
|
526
|
+
const signed = await this.#getUrl(hash, null);
|
|
527
|
+
await this.#signedFetch(signed, body);
|
|
528
|
+
return body.byteLength < this.#cacheLimitBytes ? body : null;
|
|
529
|
+
}
|
|
530
|
+
catch (ex) {
|
|
531
|
+
this.#cache.delete(hash);
|
|
532
|
+
throw ex;
|
|
533
|
+
}
|
|
534
|
+
})();
|
|
535
|
+
this.#cache.set(hash, prom);
|
|
536
|
+
await prom;
|
|
446
537
|
}
|
|
447
538
|
/** put a reference to a set of entries into the cloud */
|
|
448
539
|
async putEntries(documentId, entries) {
|
|
@@ -456,7 +547,6 @@ class Remarkable {
|
|
|
456
547
|
const contents = `${SCHEMA_VERSION}\n${entryContents}`;
|
|
457
548
|
const buffer = enc.encode(contents);
|
|
458
549
|
await this.#putHash(hash, buffer);
|
|
459
|
-
await this.#cache?.set(hash, contents);
|
|
460
550
|
return {
|
|
461
551
|
hash,
|
|
462
552
|
type: "80000000",
|
|
@@ -475,19 +565,21 @@ class Remarkable {
|
|
|
475
565
|
type: "0",
|
|
476
566
|
documentId,
|
|
477
567
|
subfiles: 0,
|
|
478
|
-
size: BigInt(buffer.
|
|
568
|
+
size: BigInt(buffer.byteLength),
|
|
479
569
|
};
|
|
480
570
|
}
|
|
481
|
-
/** put
|
|
571
|
+
/** put text in the cloud */
|
|
482
572
|
async putText(documentId, contents) {
|
|
483
573
|
const enc = new TextEncoder();
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
574
|
+
return await this.putBuffer(documentId, enc.encode(contents));
|
|
575
|
+
}
|
|
576
|
+
/** put json in the cloud */
|
|
577
|
+
async putJson(documentId, contents) {
|
|
578
|
+
return await this.putText(documentId, stringify(contents));
|
|
487
579
|
}
|
|
488
580
|
/** put metadata into the cloud */
|
|
489
581
|
async putMetadata(documentId, metadata) {
|
|
490
|
-
return await this.
|
|
582
|
+
return await this.putJson(`${documentId}.metadata`, metadata);
|
|
491
583
|
}
|
|
492
584
|
/** put a new collection (folder) */
|
|
493
585
|
async putCollection(visibleName, parent = "") {
|
|
@@ -593,32 +685,32 @@ class Remarkable {
|
|
|
593
685
|
});
|
|
594
686
|
}
|
|
595
687
|
/** try to sync, always succeed */
|
|
596
|
-
async #
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
688
|
+
async #tryPutRootEntries(gen, entries, sync) {
|
|
689
|
+
const { hash } = await this.putEntries("", entries);
|
|
690
|
+
const nextGen = await this.putRootHash(hash, gen);
|
|
691
|
+
if (sync) {
|
|
692
|
+
try {
|
|
693
|
+
await this.syncComplete(nextGen);
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
600
699
|
}
|
|
601
|
-
|
|
700
|
+
else {
|
|
602
701
|
return false;
|
|
603
702
|
}
|
|
604
703
|
}
|
|
605
704
|
/** high level api to create an entry */
|
|
606
|
-
async create(entry, sync = true) {
|
|
607
|
-
const [root, gen] = await this.getRootHash();
|
|
705
|
+
async create(entry, { cache = true, sync = true } = {}) {
|
|
706
|
+
const [root, gen] = await this.getRootHash({ cache });
|
|
608
707
|
const rootEntries = await this.getEntries(root);
|
|
609
708
|
rootEntries.push(entry);
|
|
610
|
-
|
|
611
|
-
const nextGen = await this.putRootHash(hash, gen);
|
|
612
|
-
if (sync) {
|
|
613
|
-
return await this.#trySync(nextGen);
|
|
614
|
-
}
|
|
615
|
-
else {
|
|
616
|
-
return false;
|
|
617
|
-
}
|
|
709
|
+
return await this.#tryPutRootEntries(gen, rootEntries, sync);
|
|
618
710
|
}
|
|
619
711
|
/** high level api to move a document / collection */
|
|
620
|
-
async move(documentId, dest, sync = true) {
|
|
621
|
-
const [root, gen] = await this.getRootHash();
|
|
712
|
+
async move(documentId, dest, { cache = true, sync = true } = {}) {
|
|
713
|
+
const [root, gen] = await this.getRootHash({ cache });
|
|
622
714
|
const rootEntries = await this.getEntries(root);
|
|
623
715
|
// check if destination is a collection
|
|
624
716
|
if (!dest || dest === "trash") {
|
|
@@ -667,16 +759,7 @@ class Remarkable {
|
|
|
667
759
|
// update root entries
|
|
668
760
|
const newEntry = await this.putEntries(documentId, docEnts);
|
|
669
761
|
rootEntries.push(newEntry);
|
|
670
|
-
|
|
671
|
-
const { hash } = await this.putEntries("", rootEntries);
|
|
672
|
-
const nextGen = await this.putRootHash(hash, gen);
|
|
673
|
-
// sync
|
|
674
|
-
if (sync) {
|
|
675
|
-
return await this.#trySync(nextGen);
|
|
676
|
-
}
|
|
677
|
-
else {
|
|
678
|
-
return false;
|
|
679
|
-
}
|
|
762
|
+
return await this.#tryPutRootEntries(gen, rootEntries, sync);
|
|
680
763
|
}
|
|
681
764
|
/** get entries and metadata for all files */
|
|
682
765
|
async getEntriesMetadata() {
|
|
@@ -720,6 +803,20 @@ class Remarkable {
|
|
|
720
803
|
// TODO why doesn't this work
|
|
721
804
|
return await this.#uploadFile(visibleName, buffer, "application/pdf");
|
|
722
805
|
}
|
|
806
|
+
async getCache() {
|
|
807
|
+
const promises = [];
|
|
808
|
+
for (const [hash, prom] of this.#cache) {
|
|
809
|
+
promises.push(prom.then((val) => [hash, val], () => [hash, null]));
|
|
810
|
+
}
|
|
811
|
+
const entries = await Promise.all(promises);
|
|
812
|
+
const cache = new Map();
|
|
813
|
+
for (const [hash, val] of entries) {
|
|
814
|
+
if (val) {
|
|
815
|
+
cache.set(hash, val);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return cache;
|
|
819
|
+
}
|
|
723
820
|
}
|
|
724
821
|
/**
|
|
725
822
|
* create an instance of the api
|
|
@@ -731,7 +828,10 @@ class Remarkable {
|
|
|
731
828
|
* registered. Create one with {@link register}.
|
|
732
829
|
* @returns an api instance
|
|
733
830
|
*/
|
|
734
|
-
export async function remarkable(deviceToken, { fetch = globalThis.fetch,
|
|
831
|
+
export async function remarkable(deviceToken, { fetch = globalThis.fetch, subtle = globalThis.crypto?.subtle, authHost = AUTH_HOST, syncHost = SYNC_HOST, cacheLimitBytes = 1048576, initCache = [], } = {}) {
|
|
832
|
+
if (!subtle) {
|
|
833
|
+
throw new Error("subtle was missing, try specifying it manually");
|
|
834
|
+
}
|
|
735
835
|
const resp = await fetch(`${authHost}/token/json/2/user/new`, {
|
|
736
836
|
method: "POST",
|
|
737
837
|
headers: {
|
|
@@ -742,5 +842,5 @@ export async function remarkable(deviceToken, { fetch = globalThis.fetch, cache,
|
|
|
742
842
|
throw new Error(`couldn't fetch auth token: ${resp.statusText}`);
|
|
743
843
|
}
|
|
744
844
|
const userToken = await resp.text();
|
|
745
|
-
return new Remarkable(userToken, fetch,
|
|
845
|
+
return new Remarkable(userToken, fetch, subtle, syncHost, cacheLimitBytes, initCache);
|
|
746
846
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rmapi-js",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "JavaScript implementation of the reMarkable 1.5 api",
|
|
5
5
|
"repository": "git@github.com:erikbrinkman/rmapi-js.git",
|
|
6
6
|
"author": "Erik Brinkman <erik.brinkman@gmail.com>",
|
|
@@ -21,14 +21,17 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"doc": "typedoc",
|
|
23
23
|
"fmt": "prettier --cache --write 'src/*.ts' '*.json' bundle.mjs",
|
|
24
|
-
"lint": "
|
|
24
|
+
"lint:es": "eslint --cache 'src/*.ts'",
|
|
25
|
+
"lint:doc": "typedoc --emit none",
|
|
26
|
+
"lint": "tsc && yarn lint:es && yarn lint:doc",
|
|
25
27
|
"test": "jest --coverage",
|
|
26
|
-
"build": "tsc -p tsconfig.build.json &&
|
|
28
|
+
"build": "tsc -p tsconfig.build.json && node bundle.mjs",
|
|
27
29
|
"prepack": "yarn lint && yarn test && yarn build"
|
|
28
30
|
},
|
|
29
31
|
"dependencies": {
|
|
30
32
|
"ajv": "^8.11.2",
|
|
31
33
|
"base64-js": "^1.5.1",
|
|
34
|
+
"json-stable-stringify": "^1.0.2",
|
|
32
35
|
"jtd": "^0.1.1",
|
|
33
36
|
"uuid": "^9.0.0"
|
|
34
37
|
},
|
|
@@ -38,7 +41,8 @@
|
|
|
38
41
|
"@babel/preset-typescript": "^7.18.6",
|
|
39
42
|
"@types/babel__core": "^7.1.20",
|
|
40
43
|
"@types/babel__preset-env": "^7.9.2",
|
|
41
|
-
"@types/jest": "^29.2.
|
|
44
|
+
"@types/jest": "^29.2.5",
|
|
45
|
+
"@types/json-stable-stringify": "^1.0.34",
|
|
42
46
|
"@types/node": "^18.11.18",
|
|
43
47
|
"@types/uuid": "^9.0.0",
|
|
44
48
|
"@typescript-eslint/eslint-plugin": "^5.47.1",
|
|
@@ -46,10 +50,10 @@
|
|
|
46
50
|
"@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.15",
|
|
47
51
|
"babel-jest": "^29.3.1",
|
|
48
52
|
"chalk": "^5.2.0",
|
|
49
|
-
"esbuild": "^0.16.
|
|
50
|
-
"eslint": "^8.
|
|
53
|
+
"esbuild": "^0.16.12",
|
|
54
|
+
"eslint": "^8.31.0",
|
|
51
55
|
"eslint-config-prettier": "^8.5.0",
|
|
52
|
-
"eslint-plugin-jest": "^27.
|
|
56
|
+
"eslint-plugin-jest": "^27.2.0",
|
|
53
57
|
"eslint-plugin-spellcheck": "^0.0.20",
|
|
54
58
|
"eslint-plugin-tsdoc": "^0.2.17",
|
|
55
59
|
"jest": "^29.3.1",
|
|
@@ -88,9 +92,15 @@
|
|
|
88
92
|
"extends": [
|
|
89
93
|
"eslint:recommended",
|
|
90
94
|
"plugin:@typescript-eslint/recommended",
|
|
95
|
+
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
|
91
96
|
"plugin:jest/recommended",
|
|
92
97
|
"prettier"
|
|
93
98
|
],
|
|
99
|
+
"parserOptions": {
|
|
100
|
+
"project": [
|
|
101
|
+
"./tsconfig.json"
|
|
102
|
+
]
|
|
103
|
+
},
|
|
94
104
|
"env": {
|
|
95
105
|
"node": true
|
|
96
106
|
},
|