rmapi-js 12.0.2 → 13.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
@@ -1,3 +1,55 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
1
53
  /**
2
54
  * Create and interact with reMarkable cloud.
3
55
  *
@@ -36,11 +88,12 @@
36
88
  * The cloud api is essentially a collection of entries. Each entry has an id,
37
89
  * which is a uuid4 and a hash, which indicates it's current state, and changes
38
90
  * as the item mutates, where the id is constant. Most mutable operations take
39
- * the initial hash so that merge conflicts can be resolved. Each entry has a
40
- * number of properties, but a key property is the `parent`, which represents
41
- * its parent in the file structure. This will be another document id, or one of
42
- * two special ids, "" (the empty string) for the root directory, or "trash" for
43
- * the trash.
91
+ * both, as an {@link ItemRef | `ItemRef`}: the id says which item, and the hash
92
+ * says which state you meant to change, so a conflicting update fails rather
93
+ * than overwriting. Each entry has a number of properties, but a key property
94
+ * is the `parent`, which represents its parent in the file structure. This will
95
+ * be another document id, or one of two special ids, "" (the empty string) for
96
+ * the root directory, or "trash" for the trash.
44
97
  *
45
98
  * Detailed information about the low-level storage an apis can be found in
46
99
  * {@link RawRemarkableApi | `RawRemarkableApi`}.
@@ -56,11 +109,11 @@ import { v4 as uuid4 } from "uuid";
56
109
  import { z } from "zod";
57
110
  import { HashNotFoundError, ValidationError } from "./error.js";
58
111
  import { LruCache } from "./lru.js";
59
- import { parseMetadata, RawRemarkable, } from "./raw.js";
112
+ import { BYTES_PREFIX, CACHE_VERSION, parseMetadata, RawRemarkable, TEXT_PREFIX, } from "./raw.js";
60
113
  export { deviceScreens, } from "./devices.js";
61
114
  export { HashNotFoundError, ValidationError } from "./error.js";
62
115
  export { decodeBrush, rmColors } from "./rm5.js";
63
- export { crdtKey, END_MARKER, parseRmScene, ROOT_ID } from "./rm6.js";
116
+ export { crdtKey } from "./rm6.js";
64
117
  const AUTH_HOST = "https://webapp-prod.cloud.remarkable.engineering";
65
118
  const RAW_HOST = "https://eu.tectonic.remarkable.com";
66
119
  const UPLOAD_HOST = "https://internal.cloud.remarkable.com";
@@ -71,7 +124,6 @@ const TRASH_ID = "trash";
71
124
  /** the id of the root entry list */
72
125
  const ROOT_LIST = "root";
73
126
  /** the file name of the root entry index */
74
- const ROOT_SCHEMA = `${ROOT_LIST}.docSchema`;
75
127
  /** base backoff in milliseconds for transient request retries */
76
128
  const TRANSIENT_BASE_MS = 200;
77
129
  /** base backoff in milliseconds for generation-conflict retries */
@@ -90,27 +142,25 @@ function backoffMs(attempt, baseMs) {
90
142
  return Math.random() * capped;
91
143
  }
92
144
  /**
93
- * a mutex acquired by async iteration
145
+ * a mutex held for the scope of the acquiring block
94
146
  *
95
- * `for await (const _lock of mutex) { ... }` runs the body with the lock held
96
- * and releases it when the body exits — by return, throw, or break the way a
97
- * `using` block would. Waiters are served in acquisition order.
147
+ * `await using lock = await mutex.lock()` releases when the block exits, by
148
+ * return or by throw. Waiters are served in acquisition order.
98
149
  */
99
150
  class Mutex {
100
151
  #tail = Promise.resolve();
101
- async *[Symbol.asyncIterator]() {
152
+ async lock() {
102
153
  const previous = this.#tail;
103
154
  let release;
104
155
  this.#tail = new Promise((resolve) => {
105
156
  release = resolve;
106
157
  });
107
158
  await previous;
108
- try {
109
- yield;
110
- }
111
- finally {
112
- release();
113
- }
159
+ return {
160
+ async [Symbol.asyncDispose]() {
161
+ release();
162
+ },
163
+ };
114
164
  }
115
165
  }
116
166
  /** the ordered page ids of a document's content, from cPages or the legacy list */
@@ -215,12 +265,12 @@ class Remarkable {
215
265
  #schemaVersion;
216
266
  /** serializes root updates on this instance so they don't self-conflict */
217
267
  #rootMutex = new Mutex();
218
- constructor(sessionToken, rawHost, uploadHost, cache, maxGenerationRetries, maxTransientRetries) {
268
+ constructor(sessionToken, rawHost, uploadHost, cache, maxGenerationRetries, maxTransientRetries, maxCachedBytes) {
219
269
  this.#sessionToken = sessionToken;
220
270
  this.#cache = cache;
221
271
  this.#maxGenerationRetries = maxGenerationRetries;
222
272
  this.#maxTransientRetries = maxTransientRetries;
223
- this.raw = new RawRemarkable((method, url, { body, headers } = {}) => this.#authedFetch(url, { method, body, headers }), cache, rawHost, uploadHost);
273
+ this.raw = new RawRemarkable((method, url, { body, headers } = {}) => this.#authedFetch(url, { method, body, headers }), cache, rawHost, uploadHost, maxCachedBytes);
224
274
  }
225
275
  async #getRootHash(refresh = false) {
226
276
  if (refresh || this.#lastHashGen === undefined) {
@@ -257,10 +307,12 @@ class Remarkable {
257
307
  * before this so retries reuse the same (cached) blobs.
258
308
  */
259
309
  async #withRetry(op) {
260
- // hold the root lock across the whole read-merge-write so concurrent
261
- // mutators serialize instead of sharing a generation and forcing each
262
- // other into avoidable conflicts
263
- for await (const _lock of this.#rootMutex) {
310
+ const env_1 = { stack: [], error: void 0, hasError: false };
311
+ try {
312
+ // hold the root lock across the whole read-merge-write so concurrent
313
+ // mutators serialize instead of sharing a generation and forcing each
314
+ // other into avoidable conflicts
315
+ const _lock = __addDisposableResource(env_1, await this.#rootMutex.lock(), true);
264
316
  for (let attempt = 0;; attempt++) {
265
317
  try {
266
318
  return await op();
@@ -276,8 +328,15 @@ class Remarkable {
276
328
  }
277
329
  }
278
330
  }
279
- // the mutex yields exactly once, so the loop always returns or throws
280
- throw new Error("unreachable");
331
+ catch (e_1) {
332
+ env_1.error = e_1;
333
+ env_1.hasError = true;
334
+ }
335
+ finally {
336
+ const result_1 = __disposeResources(env_1);
337
+ if (result_1)
338
+ await result_1;
339
+ }
281
340
  }
282
341
  /**
283
342
  * splice an already-uploaded item entry into the root
@@ -290,13 +349,28 @@ class Remarkable {
290
349
  await this.#withRetry(async () => {
291
350
  const [rootHash, generation] = await this.#getRootHash();
292
351
  const { entries } = await this.raw.getEntries({
293
- id: ROOT_SCHEMA,
352
+ id: ROOT_LIST,
294
353
  hash: rootHash,
295
354
  });
296
355
  entries.push(entry);
297
- const [rootEntry, uploadRoot] = await this.raw.putEntries(ROOT_LIST, entries, 4);
298
- await uploadRoot;
299
- await this.#putRootHash(rootEntry.hash, generation);
356
+ let newRoot;
357
+ {
358
+ const env_2 = { stack: [], error: void 0, hasError: false };
359
+ try {
360
+ const rootEntry = __addDisposableResource(env_2, await this.raw.putEntries(ROOT_LIST, entries, 4), true);
361
+ newRoot = rootEntry.hash;
362
+ }
363
+ catch (e_2) {
364
+ env_2.error = e_2;
365
+ env_2.hasError = true;
366
+ }
367
+ finally {
368
+ const result_2 = __disposeResources(env_2);
369
+ if (result_2)
370
+ await result_2;
371
+ }
372
+ }
373
+ await this.#putRootHash(newRoot, generation);
300
374
  });
301
375
  }
302
376
  async #authedFetch(url, { body, method = "POST", headers = {}, }) {
@@ -345,11 +419,9 @@ class Remarkable {
345
419
  }
346
420
  }
347
421
  }
348
- async #convertEntry({ hash, id }) {
349
- const { entries } = await this.raw.getEntries({
350
- id: `${id}.docSchema`,
351
- hash,
352
- });
422
+ async #convertEntry(ref) {
423
+ const { id, hash } = ref;
424
+ const { entries } = await this.raw.getEntries(ref);
353
425
  const metaEnt = entries.find((ent) => ent.id.endsWith(".metadata"));
354
426
  const contentEnt = entries.find((ent) => ent.id.endsWith(".content"));
355
427
  if (metaEnt === undefined) {
@@ -423,17 +495,20 @@ class Remarkable {
423
495
  * @returns a list of all items with some metadata
424
496
  */
425
497
  async listItems(refresh = false) {
426
- const ids = await this.listIds(refresh);
498
+ const ids = await this.listRefs(refresh);
427
499
  return await Promise.all(ids.map((id) => this.#convertEntry(id)));
428
500
  }
429
501
  /**
430
- * similar to {@link listItems | `listItems`} but backed by the low level api
502
+ * list a reference to every item, backed by the low level api
503
+ *
504
+ * Unlike {@link listItems | `listItems`} this doesn't read each item's
505
+ * metadata, so it's cheaper but only gives you ids and hashes.
431
506
  *
432
507
  * @param refresh - if true, refresh the root hash before listing
433
508
  */
434
- async listIds(refresh = false) {
509
+ async listRefs(refresh = false) {
435
510
  const [hash] = await this.#getRootHash(refresh);
436
- const { entries } = await this.raw.getEntries({ id: ROOT_SCHEMA, hash });
511
+ const { entries } = await this.raw.getEntries({ id: ROOT_LIST, hash });
437
512
  return entries.map(({ id, hash }) => ({ id, hash }));
438
513
  }
439
514
  /**
@@ -444,14 +519,12 @@ class Remarkable {
444
519
  * the low-level api to get the raw text of the `.content` file in the
445
520
  * `RawEntry` for this hash.
446
521
  *
447
- * @param ref - a reference to the item (e.g. from `listItems` or `listIds`)
522
+ * @param ref - a reference to the item (e.g. from `listItems` or `listRefs`)
448
523
  * @returns the content
449
524
  */
450
- async getContent({ id, hash }) {
451
- const { entries } = await this.raw.getEntries({
452
- id: `${id}.docSchema`,
453
- hash,
454
- });
525
+ async getContent(ref) {
526
+ const { hash } = ref;
527
+ const { entries } = await this.raw.getEntries(ref);
455
528
  const cont = entries.find((e) => e.id.endsWith(".content"));
456
529
  if (cont === undefined) {
457
530
  throw new Error(`couldn't find contents for hash ${hash}`);
@@ -468,14 +541,12 @@ class Remarkable {
468
541
  * the low-level api to get the raw text of the `.metadata` file in the
469
542
  * `RawEntry` for this hash.
470
543
  *
471
- * @param ref - a reference to the item (e.g. from `listItems` or `listIds`)
544
+ * @param ref - a reference to the item (e.g. from `listItems` or `listRefs`)
472
545
  * @returns the metadata
473
546
  */
474
- async getMetadata({ id, hash }) {
475
- const { entries } = await this.raw.getEntries({
476
- id: `${id}.docSchema`,
477
- hash,
478
- });
547
+ async getMetadata(ref) {
548
+ const { hash } = ref;
549
+ const { entries } = await this.raw.getEntries(ref);
479
550
  const meta = entries.find((e) => e.id.endsWith(".metadata"));
480
551
  if (meta === undefined) {
481
552
  throw new Error(`couldn't find metadata for hash ${hash}`);
@@ -492,11 +563,9 @@ class Remarkable {
492
563
  * @param ref - a reference to the document (e.g. from `listItems`)
493
564
  * @returns the pdf bytes
494
565
  */
495
- async getPdf({ id, hash }) {
496
- const { entries } = await this.raw.getEntries({
497
- id: `${id}.docSchema`,
498
- hash,
499
- });
566
+ async getPdf(ref) {
567
+ const { hash } = ref;
568
+ const { entries } = await this.raw.getEntries(ref);
500
569
  const pdf = entries.find((e) => e.id.endsWith(".pdf"));
501
570
  if (pdf === undefined) {
502
571
  throw new Error(`couldn't find pdf for hash ${hash}`);
@@ -513,11 +582,9 @@ class Remarkable {
513
582
  * @param ref - a reference to the document (e.g. from `listItems`)
514
583
  * @returns the epub bytes
515
584
  */
516
- async getEpub({ id, hash }) {
517
- const { entries } = await this.raw.getEntries({
518
- id: `${id}.docSchema`,
519
- hash,
520
- });
585
+ async getEpub(ref) {
586
+ const { hash } = ref;
587
+ const { entries } = await this.raw.getEntries(ref);
521
588
  const epub = entries.find((e) => e.id.endsWith(".epub"));
522
589
  if (epub === undefined) {
523
590
  throw new Error(`couldn't find epub for hash ${hash}`);
@@ -526,6 +593,95 @@ class Remarkable {
526
593
  return await this.raw.getHash(epub);
527
594
  }
528
595
  }
596
+ #rmPageFile = {
597
+ name: (docId, pageId) => `${docId}/${pageId}.rm`,
598
+ read: (entry) => this.raw.getRm(entry),
599
+ write: (fileName, page) => this.raw.putRm(fileName, page),
600
+ };
601
+ #highlightPageFile = {
602
+ name: (docId, pageId) => `${docId}.highlights/${pageId}.json`,
603
+ read: (entry) => this.raw.getHighlights(entry),
604
+ write: (fileName, highlights) => this.raw.putHighlights(fileName, highlights),
605
+ };
606
+ #pageMetadataFile = {
607
+ name: (docId, pageId) => `${docId}/${pageId}-metadata.json`,
608
+ read: (entry) => this.raw.getPageMetadata(entry),
609
+ write: (fileName, meta) => this.raw.putPageMetadata(fileName, meta),
610
+ };
611
+ async #getPageFile(ref, pageId, file) {
612
+ const { id } = ref;
613
+ const { entries } = await this.raw.getEntries(ref);
614
+ const content = await this.getContent(ref);
615
+ if (!pageOrder(content).includes(pageId)) {
616
+ throw new Error(`document ${id} has no page ${pageId}`);
617
+ }
618
+ const entry = entries.find((ent) => ent.id === file.name(id, pageId));
619
+ if (entry === undefined) {
620
+ return undefined;
621
+ }
622
+ else {
623
+ return await file.read(entry);
624
+ }
625
+ }
626
+ async #getPageFiles(ref, file) {
627
+ const { id } = ref;
628
+ const { entries } = await this.raw.getEntries(ref);
629
+ const content = await this.getContent(ref);
630
+ const byName = new Map(entries.map((entry) => [entry.id, entry]));
631
+ const found = pageOrder(content)
632
+ .map((pageId) => [pageId, byName.get(file.name(id, pageId))])
633
+ .filter((pair) => pair[1] !== undefined);
634
+ const parsed = await Promise.all(found.map(([, entry]) => file.read(entry)));
635
+ return new Map(found.map(([pageId], index) => [pageId, parsed[index]]));
636
+ }
637
+ async #putPageFilesRaw(ref, pages, file, schemaVersion) {
638
+ const env_3 = { stack: [], error: void 0, hasError: false };
639
+ try {
640
+ const { id, hash } = ref;
641
+ const { entries } = await this.raw.getEntries(ref);
642
+ const contentEntry = entries.find((ent) => ent.id.endsWith(".content"));
643
+ if (contentEntry === undefined) {
644
+ throw new Error(`couldn't find contents for hash ${hash}`);
645
+ }
646
+ const content = await this.raw.getContent(contentEntry);
647
+ const order = new Set(pageOrder(content));
648
+ for (const pageId of pages.keys()) {
649
+ if (!order.has(pageId)) {
650
+ throw new Error(`document ${id} has no page ${pageId}`);
651
+ }
652
+ }
653
+ const uploads = __addDisposableResource(env_3, new AsyncDisposableStack(), true);
654
+ const written = await Promise.all([...pages].map(([pageId, value]) => file.write(file.name(id, pageId), value)));
655
+ for (const pageEntry of written) {
656
+ uploads.use(pageEntry);
657
+ const pageInd = entries.findIndex((ent) => ent.id === pageEntry.id);
658
+ if (pageInd === -1) {
659
+ entries.push(pageEntry);
660
+ }
661
+ else {
662
+ entries[pageInd] = pageEntry;
663
+ }
664
+ }
665
+ return await this.raw.putEntries(id, entries, schemaVersion);
666
+ }
667
+ catch (e_3) {
668
+ env_3.error = e_3;
669
+ env_3.hasError = true;
670
+ }
671
+ finally {
672
+ const result_3 = __disposeResources(env_3);
673
+ if (result_3)
674
+ await result_3;
675
+ }
676
+ }
677
+ async #putPageFiles(ref, pages, file, refresh) {
678
+ if (pages.size === 0) {
679
+ return ref;
680
+ }
681
+ else {
682
+ return await this.#editEntry(ref, refresh, (item, schemaVersion) => this.#putPageFilesRaw(item, pages, file, schemaVersion));
683
+ }
684
+ }
529
685
  /**
530
686
  * get a single page's parsed reMarkable lines (`.rm`) drawing
531
687
  *
@@ -537,22 +693,7 @@ class Remarkable {
537
693
  * @throws if `pageId` is not a page of the document
538
694
  */
539
695
  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);
546
- if (!pageOrder(content).includes(pageId)) {
547
- throw new Error(`document ${id} has no page ${pageId}`);
548
- }
549
- const entry = entries.find((ent) => ent.id === `${id}/${pageId}.rm`);
550
- if (entry === undefined) {
551
- return undefined;
552
- }
553
- else {
554
- return await this.raw.getRm(entry);
555
- }
696
+ return await this.#getPageFile(ref, pageId, this.#rmPageFile);
556
697
  }
557
698
  /**
558
699
  * get every drawn page of a document, parsed, keyed by page id
@@ -566,18 +707,243 @@ class Remarkable {
566
707
  * @returns the drawn pages, keyed by page id, in document order
567
708
  */
568
709
  async getRmPages(ref) {
569
- const { id, hash } = ref;
570
- const { entries } = await this.raw.getEntries({
571
- id: `${id}.docSchema`,
572
- hash,
710
+ return await this.#getPageFiles(ref, this.#rmPageFile);
711
+ }
712
+ /**
713
+ * write a single page's reMarkable lines (`.rm`) drawing
714
+ *
715
+ * @param ref - a reference to the document
716
+ * @param pageId - the id of the page, from the document's `.content` page list
717
+ * @param page - the drawing to write, replacing any already there
718
+ * @throws GenerationError if the generation doesn't match the current server generation
719
+ * @throws if `pageId` is not a page of the document
720
+ * @returns a reference to the updated document, with its new hash
721
+ */
722
+ async putRmPage(ref, pageId, page, refresh = false) {
723
+ return await this.putRmPages(ref, new Map([[pageId, page]]), refresh);
724
+ }
725
+ /**
726
+ * write several pages' reMarkable lines (`.rm`) drawings in one commit
727
+ *
728
+ * @param ref - a reference to the document
729
+ * @param pages - the drawings to write, keyed by page id, replacing any
730
+ * already on those pages and leaving every other page alone
731
+ * @throws GenerationError if the generation doesn't match the current server generation
732
+ * @throws if any key is not a page of the document
733
+ * @returns a reference to the updated document, with its new hash
734
+ */
735
+ async putRmPages(ref, pages, refresh = false) {
736
+ return await this.#putPageFiles(ref, pages, this.#rmPageFile, refresh);
737
+ }
738
+ /**
739
+ * get a single page's text highlights
740
+ *
741
+ * These are separate from the highlighter strokes drawn in a `.rm` scene.
742
+ *
743
+ * @param ref - a reference to the document
744
+ * @param pageId - the id of the page, from the document's `.content` page list
745
+ * @returns the page's highlights, or `undefined` if the page has none
746
+ * @throws if `pageId` is not a page of the document
747
+ */
748
+ async getHighlights(ref, pageId) {
749
+ return await this.#getPageFile(ref, pageId, this.#highlightPageFile);
750
+ }
751
+ /**
752
+ * get every highlighted page of a document, keyed by page id
753
+ *
754
+ * @param ref - a reference to the document
755
+ * @returns the highlights in page order, omitting pages with none
756
+ */
757
+ async getHighlightPages(ref) {
758
+ return await this.#getPageFiles(ref, this.#highlightPageFile);
759
+ }
760
+ /**
761
+ * write a single page's text highlights, replacing any already there
762
+ *
763
+ * @param ref - a reference to the document
764
+ * @param pageId - the id of the page, from the document's `.content` page list
765
+ * @param highlights - the highlights to write
766
+ * @throws GenerationError if the generation doesn't match the current server generation
767
+ * @throws if `pageId` is not a page of the document
768
+ * @returns a reference to the updated document, with its new hash
769
+ */
770
+ async putHighlights(ref, pageId, highlights, refresh = false) {
771
+ return await this.putHighlightPages(ref, new Map([[pageId, highlights]]), refresh);
772
+ }
773
+ /**
774
+ * write several pages' text highlights in one commit
775
+ *
776
+ * @param ref - a reference to the document
777
+ * @param pages - the highlights to write, keyed by page id, replacing any
778
+ * already on those pages and leaving every other page alone
779
+ * @throws GenerationError if the generation doesn't match the current server generation
780
+ * @throws if any key is not a page of the document
781
+ * @returns a reference to the updated document, with its new hash
782
+ */
783
+ async putHighlightPages(ref, pages, refresh = false) {
784
+ return await this.#putPageFiles(ref, pages, this.#highlightPageFile, refresh);
785
+ }
786
+ /**
787
+ * get a template attached to an item as a `.template` sidecar
788
+ *
789
+ * This is distinct from a {@link TemplateEntry | `TemplateEntry`} (whose
790
+ * template is its `.content`); collections and documents can carry a template
791
+ * this way.
792
+ *
793
+ * @param ref - a reference to the item
794
+ * @returns the template content, or `undefined` if the item has no `.template`
795
+ */
796
+ async getTemplate(ref) {
797
+ const { id } = ref;
798
+ const { entries } = await this.raw.getEntries(ref);
799
+ const entry = entries.find((e) => e.id === `${id}.template`);
800
+ if (entry === undefined) {
801
+ return undefined;
802
+ }
803
+ else {
804
+ return await this.raw.getTemplate(entry);
805
+ }
806
+ }
807
+ /**
808
+ * attach a template to an item as a `.template` sidecar
809
+ *
810
+ * @param ref - a reference to the item
811
+ * @param template - the template to attach, replacing any already there
812
+ * @throws GenerationError if the generation doesn't match the current server generation
813
+ * @returns a reference to the updated item, with its new hash
814
+ */
815
+ async putTemplate(ref, template, refresh = false) {
816
+ return await this.#editEntry(ref, refresh, async (item, schemaVersion) => {
817
+ const env_4 = { stack: [], error: void 0, hasError: false };
818
+ try {
819
+ const { id } = item;
820
+ const { entries } = await this.raw.getEntries(item);
821
+ const templateEntry = __addDisposableResource(env_4, await this.raw.putTemplate(`${id}.template`, template), true);
822
+ const ind = entries.findIndex((ent) => ent.id === templateEntry.id);
823
+ if (ind === -1) {
824
+ entries.push(templateEntry);
825
+ }
826
+ else {
827
+ entries[ind] = templateEntry;
828
+ }
829
+ return await this.raw.putEntries(id, entries, schemaVersion);
830
+ }
831
+ catch (e_4) {
832
+ env_4.error = e_4;
833
+ env_4.hasError = true;
834
+ }
835
+ finally {
836
+ const result_4 = __disposeResources(env_4);
837
+ if (result_4)
838
+ await result_4;
839
+ }
573
840
  });
574
- const content = await this.getContent(ref);
575
- const byName = new Map(entries.map((entry) => [entry.id, entry]));
576
- const drawn = pageOrder(content)
577
- .map((pageId) => [pageId, byName.get(`${id}/${pageId}.rm`)])
578
- .filter((pair) => pair[1] !== undefined);
579
- const parsed = await Promise.all(drawn.map(([, entry]) => this.raw.getRm(entry)));
580
- return new Map(drawn.map(([pageId], index) => [pageId, parsed[index]]));
841
+ }
842
+ /**
843
+ * get a document's per-page template names
844
+ *
845
+ * The `.pagedata` file lists one template name per page, in page order (an
846
+ * empty string for a page with no template).
847
+ *
848
+ * @param ref - a reference to the document
849
+ * @returns the per-page template names, or `undefined` if the document has
850
+ * no `.pagedata`
851
+ */
852
+ async getPagedata(ref) {
853
+ const { id } = ref;
854
+ const { entries } = await this.raw.getEntries(ref);
855
+ const entry = entries.find((e) => e.id === `${id}.pagedata`);
856
+ if (entry === undefined) {
857
+ return undefined;
858
+ }
859
+ else {
860
+ const lines = (await this.raw.getText(entry)).split("\n");
861
+ if (lines.at(-1) === "")
862
+ lines.pop();
863
+ return lines;
864
+ }
865
+ }
866
+ /**
867
+ * set a document's per-page template names
868
+ *
869
+ * @param ref - a reference to the document
870
+ * @param templates - one template name per page, in page order, an empty
871
+ * string for a page with no template
872
+ * @throws GenerationError if the generation doesn't match the current server generation
873
+ * @returns a reference to the updated document, with its new hash
874
+ */
875
+ async putPagedata(ref, templates, refresh = false) {
876
+ return await this.#editEntry(ref, refresh, async (item, schemaVersion) => {
877
+ const env_5 = { stack: [], error: void 0, hasError: false };
878
+ try {
879
+ const { id } = item;
880
+ const { entries } = await this.raw.getEntries(item);
881
+ const pagedataEntry = __addDisposableResource(env_5, await this.raw.putPagedata(`${id}.pagedata`, templates), true);
882
+ const ind = entries.findIndex((ent) => ent.id === pagedataEntry.id);
883
+ if (ind === -1) {
884
+ entries.push(pagedataEntry);
885
+ }
886
+ else {
887
+ entries[ind] = pagedataEntry;
888
+ }
889
+ return await this.raw.putEntries(id, entries, schemaVersion);
890
+ }
891
+ catch (e_5) {
892
+ env_5.error = e_5;
893
+ env_5.hasError = true;
894
+ }
895
+ finally {
896
+ const result_5 = __disposeResources(env_5);
897
+ if (result_5)
898
+ await result_5;
899
+ }
900
+ });
901
+ }
902
+ /**
903
+ * get a single page's layer metadata
904
+ *
905
+ * @param ref - a reference to the document
906
+ * @param pageId - the id of the page, from the document's `.content` page list
907
+ * @returns the page's layer metadata, or `undefined` if the page has none
908
+ * @throws if `pageId` is not a page of the document
909
+ */
910
+ async getPageMetadata(ref, pageId) {
911
+ return await this.#getPageFile(ref, pageId, this.#pageMetadataFile);
912
+ }
913
+ /**
914
+ * get every page's layer metadata, keyed by page id
915
+ *
916
+ * @param ref - a reference to the document
917
+ * @returns the layer metadata in page order, omitting pages with none
918
+ */
919
+ async getPageMetadataPages(ref) {
920
+ return await this.#getPageFiles(ref, this.#pageMetadataFile);
921
+ }
922
+ /**
923
+ * write a single page's layer metadata, replacing any already there
924
+ *
925
+ * @param ref - a reference to the document
926
+ * @param pageId - the id of the page, from the document's `.content` page list
927
+ * @param meta - the layer metadata to write
928
+ * @throws GenerationError if the generation doesn't match the current server generation
929
+ * @throws if `pageId` is not a page of the document
930
+ * @returns a reference to the updated document, with its new hash
931
+ */
932
+ async putPageMetadata(ref, pageId, meta, refresh = false) {
933
+ return await this.putPageMetadataPages(ref, new Map([[pageId, meta]]), refresh);
934
+ }
935
+ /**
936
+ * write several pages' layer metadata in one commit
937
+ *
938
+ * @param ref - a reference to the document
939
+ * @param pages - the layer metadata to write, keyed by page id, replacing any
940
+ * already on those pages and leaving every other page alone
941
+ * @throws GenerationError if the generation doesn't match the current server generation
942
+ * @throws if any key is not a page of the document
943
+ * @returns a reference to the updated document, with its new hash
944
+ */
945
+ async putPageMetadataPages(ref, pages, refresh = false) {
946
+ return await this.#putPageFiles(ref, pages, this.#pageMetadataFile, refresh);
581
947
  }
582
948
  /**
583
949
  * get a document's entire contents as a zip archive
@@ -591,11 +957,8 @@ class Remarkable {
591
957
  *
592
958
  * @param ref - a reference to the document (e.g. from `listItems`)
593
959
  */
594
- async getDocumentArchive({ id, hash }) {
595
- const { entries } = await this.raw.getEntries({
596
- id: `${id}.docSchema`,
597
- hash,
598
- });
960
+ async getDocumentArchive(ref) {
961
+ const { entries } = await this.raw.getEntries(ref);
599
962
  const zip = new JSZip();
600
963
  for (const entry of entries) {
601
964
  // TODO if this is .metadata we might want to assert type === "DocumentType"
@@ -610,16 +973,16 @@ class Remarkable {
610
973
  * as a blob, and commits a new document into the root.
611
974
  *
612
975
  * @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.
976
+ * This is an experimental feature. A fresh document id is generated, so
977
+ * re-uploading to the same account doesn't collide with the original. Like
978
+ * the other low-level puts, this may throw a
979
+ * {@link GenerationError | `GenerationError`} if the generation is stale,
980
+ * requiring a retry.
618
981
  *
619
982
  * @param buffer - the archive bytes, as returned by `getDocumentArchive`
620
- * @param options - overrides for parent, visible name, and id
983
+ * @param options - overrides for parent and visible name
621
984
  */
622
- async putDocumentArchive(buffer, { refresh = false, parent, visibleName, id: keepId, } = {}) {
985
+ async putDocumentArchive(buffer, { refresh = false, parent, visibleName } = {}) {
623
986
  if (parent !== undefined && parent && !idReg.test(parent)) {
624
987
  throw new ValidationError(parent, idReg, "parent must be a valid document id");
625
988
  }
@@ -633,7 +996,7 @@ class Remarkable {
633
996
  if (oldId.includes("/")) {
634
997
  throw new Error(`unexpected nested .metadata path '${metaPath}'`);
635
998
  }
636
- const newId = keepId ?? uuid4();
999
+ const newId = uuid4();
637
1000
  // rewrite the old document id prefix on every archived file to the new id,
638
1001
  // patching the .metadata as we pass it (parent/name/lastModified). the
639
1002
  // blobs don't depend on the generation, so upload the rewritten files and
@@ -659,9 +1022,27 @@ class Remarkable {
659
1022
  }
660
1023
  return this.raw.putFile(newPath, bytes);
661
1024
  }));
662
- const fileEntries = fileUploads.map(([entry]) => entry);
663
- const [docEntry, uploadDoc] = await this.raw.putEntries(newId, fileEntries, schemaVersion);
664
- await Promise.all([...fileUploads.map(([, upload]) => upload), uploadDoc]);
1025
+ let docEntry;
1026
+ {
1027
+ const env_6 = { stack: [], error: void 0, hasError: false };
1028
+ try {
1029
+ const uploads = __addDisposableResource(env_6, new AsyncDisposableStack(), true);
1030
+ for (const entry of fileUploads) {
1031
+ uploads.use(entry);
1032
+ }
1033
+ const indexEntry = __addDisposableResource(env_6, await this.raw.putEntries(newId, fileUploads, schemaVersion), true);
1034
+ docEntry = indexEntry;
1035
+ }
1036
+ catch (e_6) {
1037
+ env_6.error = e_6;
1038
+ env_6.hasError = true;
1039
+ }
1040
+ finally {
1041
+ const result_6 = __disposeResources(env_6);
1042
+ if (result_6)
1043
+ await result_6;
1044
+ }
1045
+ }
665
1046
  await this.#commit(docEntry);
666
1047
  return { id: newId, hash: docEntry.hash };
667
1048
  }
@@ -715,24 +1096,35 @@ class Remarkable {
715
1096
  // themselves don't depend on the generation, so upload them once and let
716
1097
  // #commit retry only the root merge
717
1098
  const [, , schemaVersion] = await this.#getRootHash(refresh);
718
- const [[contentEntry, uploadContent], [metadataEntry, uploadMetadata], [pagedataEntry, uploadPagedata], [fileEntry, uploadFile],] = await Promise.all([
719
- this.raw.putContent(`${id}.content`, content),
720
- this.raw.putMetadata(`${id}.metadata`, metadata),
721
- this.raw.putText(`${id}.pagedata`, "\n"),
722
- this.raw.putFile(`${id}.${fileType}`, buffer),
723
- ]);
724
- const [collectionEntry, uploadCollection] = await this.raw.putEntries(id, [contentEntry, metadataEntry, pagedataEntry, fileEntry], schemaVersion);
725
1099
  // TODO we could return a full entry here, but we should probably decide
726
1100
  // what that should be, e.g. we could return more fields than the standard
727
1101
  // entry. Same for putFolder
728
1102
  // TODO we should also decide if the api should take hashes or ids...
729
- await Promise.all([
730
- uploadContent,
731
- uploadMetadata,
732
- uploadPagedata,
733
- uploadFile,
734
- uploadCollection,
735
- ]);
1103
+ let collectionEntry;
1104
+ {
1105
+ const env_7 = { stack: [], error: void 0, hasError: false };
1106
+ try {
1107
+ const contentReq = this.raw.putContent(`${id}.content`, content);
1108
+ const metadataReq = this.raw.putMetadata(`${id}.metadata`, metadata);
1109
+ const pagedataReq = this.raw.putFile(`${id}.pagedata`, new TextEncoder().encode("\n"));
1110
+ const fileReq = this.raw.putFile(`${id}.${fileType}`, buffer);
1111
+ const contentEntry = __addDisposableResource(env_7, await contentReq, true);
1112
+ const metadataEntry = __addDisposableResource(env_7, await metadataReq, true);
1113
+ const pagedataEntry = __addDisposableResource(env_7, await pagedataReq, true);
1114
+ const fileEntry = __addDisposableResource(env_7, await fileReq, true);
1115
+ const indexEntry = __addDisposableResource(env_7, await this.raw.putEntries(id, [contentEntry, metadataEntry, pagedataEntry, fileEntry], schemaVersion), true);
1116
+ collectionEntry = indexEntry;
1117
+ }
1118
+ catch (e_7) {
1119
+ env_7.error = e_7;
1120
+ env_7.hasError = true;
1121
+ }
1122
+ finally {
1123
+ const result_7 = __disposeResources(env_7);
1124
+ if (result_7)
1125
+ await result_7;
1126
+ }
1127
+ }
736
1128
  await this.#commit(collectionEntry);
737
1129
  return { id, hash: collectionEntry.hash };
738
1130
  }
@@ -818,12 +1210,27 @@ class Remarkable {
818
1210
  // the blobs don't depend on the generation, so upload them once and let
819
1211
  // #commit retry only the root merge
820
1212
  const [, , schemaVersion] = await this.#getRootHash(refresh);
821
- const [[contentEntry, uploadContent], [metadataEntry, uploadMetadata]] = await Promise.all([
822
- this.raw.putContent(`${id}.content`, content),
823
- this.raw.putMetadata(`${id}.metadata`, metadata),
824
- ]);
825
- const [collectionEntry, uploadCollection] = await this.raw.putEntries(id, [contentEntry, metadataEntry], schemaVersion);
826
- await Promise.all([uploadContent, uploadMetadata, uploadCollection]);
1213
+ let collectionEntry;
1214
+ {
1215
+ const env_8 = { stack: [], error: void 0, hasError: false };
1216
+ try {
1217
+ const contentReq = this.raw.putContent(`${id}.content`, content);
1218
+ const metadataReq = this.raw.putMetadata(`${id}.metadata`, metadata);
1219
+ const contentEntry = __addDisposableResource(env_8, await contentReq, true);
1220
+ const metadataEntry = __addDisposableResource(env_8, await metadataReq, true);
1221
+ const indexEntry = __addDisposableResource(env_8, await this.raw.putEntries(id, [contentEntry, metadataEntry], schemaVersion), true);
1222
+ collectionEntry = indexEntry;
1223
+ }
1224
+ catch (e_8) {
1225
+ env_8.error = e_8;
1226
+ env_8.hasError = true;
1227
+ }
1228
+ finally {
1229
+ const result_8 = __disposeResources(env_8);
1230
+ if (result_8)
1231
+ await result_8;
1232
+ }
1233
+ }
827
1234
  await this.#commit(collectionEntry);
828
1235
  return { id, hash: collectionEntry.hash };
829
1236
  }
@@ -866,49 +1273,87 @@ class Remarkable {
866
1273
  return await this.raw.uploadFile(visibleName, new Uint8Array(0), "folder");
867
1274
  }
868
1275
  /** edit just a content entry */
869
- async #editContentRaw(id, hash, update, schemaVersion) {
870
- const { entries } = await this.raw.getEntries({
871
- id: `${id}.docSchema`,
872
- hash,
873
- });
874
- const contInd = entries.findIndex((ent) => ent.id.endsWith(".content"));
875
- const contEntry = entries[contInd];
876
- if (contEntry === undefined) {
877
- throw new Error("internal error: couldn't find content in entry hash");
1276
+ async #editContentRaw(ref, update, schemaVersion) {
1277
+ const env_9 = { stack: [], error: void 0, hasError: false };
1278
+ try {
1279
+ const { id } = ref;
1280
+ const { entries } = await this.raw.getEntries(ref);
1281
+ const contInd = entries.findIndex((ent) => ent.id.endsWith(".content"));
1282
+ const contEntry = entries[contInd];
1283
+ if (contEntry === undefined) {
1284
+ throw new Error("internal error: couldn't find content in entry hash");
1285
+ }
1286
+ const cont = await this.raw.getContent(contEntry);
1287
+ Object.assign(cont, update);
1288
+ const newContEntry = __addDisposableResource(env_9, await this.raw.putContent(contEntry.id, cont), true);
1289
+ entries[contInd] = newContEntry;
1290
+ return await this.raw.putEntries(id, entries, schemaVersion);
1291
+ }
1292
+ catch (e_9) {
1293
+ env_9.error = e_9;
1294
+ env_9.hasError = true;
1295
+ }
1296
+ finally {
1297
+ const result_9 = __disposeResources(env_9);
1298
+ if (result_9)
1299
+ await result_9;
878
1300
  }
879
- const cont = await this.raw.getContent(contEntry);
880
- Object.assign(cont, update);
881
- const [newContEntry, uploadCont] = await this.raw.putContent(contEntry.id, cont);
882
- entries[contInd] = newContEntry;
883
- const [result, uploadEntries] = await this.raw.putEntries(id, entries, schemaVersion);
884
- const upload = Promise.all([uploadCont, uploadEntries]);
885
- return [result, upload];
886
1301
  }
887
- /** fully sync a content edit */
888
- async #editContent(hash, update, expectedType, refresh) {
1302
+ /**
1303
+ * rewrite one item's files and splice the result into the root
1304
+ *
1305
+ * `edit` receives a ref to the item and the schema version, and returns its
1306
+ * new entry plus a promise for the uploads. Everything generation-dependent
1307
+ * lives here, so writers only describe the file change.
1308
+ */
1309
+ async #editEntry(ref, refresh, edit) {
889
1310
  return await this.#withRetry(async () => {
890
1311
  const [rootHash, generation, schemaVersion] = await this.#getRootHash(refresh);
891
1312
  const { entries } = await this.raw.getEntries({
892
- id: ROOT_SCHEMA,
1313
+ id: ROOT_LIST,
893
1314
  hash: rootHash,
894
1315
  });
895
- const hashInd = entries.findIndex((ent) => ent.hash === hash);
1316
+ const hashInd = entries.findIndex((ent) => ent.id === ref.id && ent.hash === ref.hash);
896
1317
  const hashEnt = entries[hashInd];
897
1318
  if (hashEnt === undefined) {
898
- throw new HashNotFoundError(hash);
1319
+ throw new HashNotFoundError(ref.hash);
899
1320
  }
900
- const [[newEnt, uploadEnt], meta] = await Promise.all([
901
- this.#editContentRaw(hashEnt.id, hash, update, schemaVersion),
902
- this.getMetadata(hashEnt),
1321
+ let newRoot;
1322
+ let newHash;
1323
+ {
1324
+ const env_10 = { stack: [], error: void 0, hasError: false };
1325
+ try {
1326
+ const newEnt = __addDisposableResource(env_10, await edit({ id: hashEnt.id, hash: ref.hash }, schemaVersion), true);
1327
+ entries[hashInd] = newEnt;
1328
+ const rootEntry = __addDisposableResource(env_10, await this.raw.putEntries(ROOT_LIST, entries, 4), true);
1329
+ newRoot = rootEntry.hash;
1330
+ newHash = newEnt.hash;
1331
+ }
1332
+ catch (e_10) {
1333
+ env_10.error = e_10;
1334
+ env_10.hasError = true;
1335
+ }
1336
+ finally {
1337
+ const result_10 = __disposeResources(env_10);
1338
+ if (result_10)
1339
+ await result_10;
1340
+ }
1341
+ }
1342
+ await this.#putRootHash(newRoot, generation);
1343
+ return { id: hashEnt.id, hash: newHash };
1344
+ });
1345
+ }
1346
+ /** fully sync a content edit */
1347
+ async #editContent(ref, update, expectedType, refresh) {
1348
+ return await this.#editEntry(ref, refresh, async (item, schemaVersion) => {
1349
+ const [newEnt, meta] = await Promise.all([
1350
+ this.#editContentRaw(item, update, schemaVersion),
1351
+ this.getMetadata(item),
903
1352
  ]);
904
1353
  if (meta.type !== expectedType) {
905
- throw new Error(`expected type ${expectedType} but got ${meta.type} for hash ${hash}`);
1354
+ throw new Error(`expected type ${expectedType} but got ${meta.type} for hash ${item.hash}`);
906
1355
  }
907
- entries[hashInd] = newEnt;
908
- const [rootEntry, uploadRoot] = await this.raw.putEntries(ROOT_LIST, entries, 4);
909
- await Promise.all([uploadEnt, uploadRoot]);
910
- await this.#putRootHash(rootEntry.hash, generation);
911
- return { id: hashEnt.id, hash: newEnt.hash };
1356
+ return newEnt;
912
1357
  });
913
1358
  }
914
1359
  /**
@@ -924,7 +1369,7 @@ class Remarkable {
924
1369
  * @returns a reference to the updated entry, with its new hash
925
1370
  */
926
1371
  async updateDocument(ref, content, refresh = false) {
927
- return await this.#editContent(ref.hash, content, "DocumentType", refresh);
1372
+ return await this.#editContent(ref, content, "DocumentType", refresh);
928
1373
  }
929
1374
  /**
930
1375
  * update content metadata for a collection
@@ -939,7 +1384,7 @@ class Remarkable {
939
1384
  * @returns a reference to the updated entry, with its new hash
940
1385
  */
941
1386
  async updateCollection(ref, content, refresh = false) {
942
- return await this.#editContent(ref.hash, content, "CollectionType", refresh);
1387
+ return await this.#editContent(ref, content, "CollectionType", refresh);
943
1388
  }
944
1389
  /**
945
1390
  * update content metadata for a template
@@ -954,47 +1399,38 @@ class Remarkable {
954
1399
  * @returns a reference to the updated entry, with its new hash
955
1400
  */
956
1401
  async updateTemplate(ref, content, refresh = false) {
957
- return await this.#editContent(ref.hash, content, "TemplateType", refresh);
1402
+ return await this.#editContent(ref, content, "TemplateType", refresh);
958
1403
  }
959
- async #editMetaRaw(id, hash, update, schemaVersion) {
960
- const { entries } = await this.raw.getEntries({
961
- id: `${id}.docSchema`,
962
- hash,
963
- });
964
- const metaInd = entries.findIndex((ent) => ent.id.endsWith(".metadata"));
965
- const metaEntry = entries[metaInd];
966
- if (metaEntry === undefined) {
967
- throw new Error("internal error: couldn't find metadata in entry hash");
968
- }
969
- const meta = await this.raw.getMetadata(metaEntry);
970
- Object.assign(meta, update);
971
- meta.version = (meta.version ?? 0) + 1;
972
- meta.metadatamodified = true;
973
- const [newMetaEntry, uploadMeta] = await this.raw.putMetadata(metaEntry.id, meta);
974
- entries[metaInd] = newMetaEntry;
975
- const [result, uploadEntries] = await this.raw.putEntries(id, entries, schemaVersion);
976
- const upload = Promise.all([uploadMeta, uploadEntries]);
977
- return [result, upload];
978
- }
979
- async #editMeta(hash, update, refresh = false) {
980
- return await this.#withRetry(async () => {
981
- const [rootHash, generation, schemaVersion] = await this.#getRootHash(refresh);
982
- const { entries } = await this.raw.getEntries({
983
- id: ROOT_SCHEMA,
984
- hash: rootHash,
985
- });
986
- const hashInd = entries.findIndex((ent) => ent.hash === hash);
987
- const hashEnt = entries[hashInd];
988
- if (hashEnt === undefined) {
989
- throw new HashNotFoundError(hash);
1404
+ async #editMetaRaw(ref, update, schemaVersion) {
1405
+ const env_11 = { stack: [], error: void 0, hasError: false };
1406
+ try {
1407
+ const { id } = ref;
1408
+ const { entries } = await this.raw.getEntries(ref);
1409
+ const metaInd = entries.findIndex((ent) => ent.id.endsWith(".metadata"));
1410
+ const metaEntry = entries[metaInd];
1411
+ if (metaEntry === undefined) {
1412
+ throw new Error("internal error: couldn't find metadata in entry hash");
990
1413
  }
991
- const [newEnt, uploadEnt] = await this.#editMetaRaw(hashEnt.id, hash, update, schemaVersion);
992
- entries[hashInd] = newEnt;
993
- const [rootEntry, uploadRoot] = await this.raw.putEntries(ROOT_LIST, entries, 4);
994
- await Promise.all([uploadEnt, uploadRoot]);
995
- await this.#putRootHash(rootEntry.hash, generation);
996
- return { id: hashEnt.id, hash: newEnt.hash };
997
- });
1414
+ const meta = await this.raw.getMetadata(metaEntry);
1415
+ Object.assign(meta, update);
1416
+ meta.version = (meta.version ?? 0) + 1;
1417
+ meta.metadatamodified = true;
1418
+ const newMetaEntry = __addDisposableResource(env_11, await this.raw.putMetadata(metaEntry.id, meta), true);
1419
+ entries[metaInd] = newMetaEntry;
1420
+ return await this.raw.putEntries(id, entries, schemaVersion);
1421
+ }
1422
+ catch (e_11) {
1423
+ env_11.error = e_11;
1424
+ env_11.hasError = true;
1425
+ }
1426
+ finally {
1427
+ const result_11 = __disposeResources(env_11);
1428
+ if (result_11)
1429
+ await result_11;
1430
+ }
1431
+ }
1432
+ async #editMeta(ref, update, refresh = false) {
1433
+ return await this.#editEntry(ref, refresh, (item, schemaVersion) => this.#editMetaRaw(item, update, schemaVersion));
998
1434
  }
999
1435
  /**
1000
1436
  * move an entry
@@ -1012,7 +1448,7 @@ class Remarkable {
1012
1448
  if (!idReg.test(parent)) {
1013
1449
  throw new ValidationError(parent, idReg, "parent must be a valid document id");
1014
1450
  }
1015
- return await this.#editMeta(ref.hash, { parent }, refresh);
1451
+ return await this.#editMeta(ref, { parent }, refresh);
1016
1452
  }
1017
1453
  /**
1018
1454
  * delete an entry
@@ -1039,7 +1475,7 @@ class Remarkable {
1039
1475
  * @returns a reference to the renamed entry, with its new hash
1040
1476
  */
1041
1477
  async rename(ref, visibleName, refresh = false) {
1042
- return await this.#editMeta(ref.hash, { visibleName }, refresh);
1478
+ return await this.#editMeta(ref, { visibleName }, refresh);
1043
1479
  }
1044
1480
  /**
1045
1481
  * star or unstar an entry
@@ -1053,7 +1489,7 @@ class Remarkable {
1053
1489
  * @returns a reference to the updated entry, with its new hash
1054
1490
  */
1055
1491
  async star(ref, starred, refresh = false) {
1056
- return await this.#editMeta(ref.hash, { pinned: starred }, refresh);
1492
+ return await this.#editMeta(ref, { pinned: starred }, refresh);
1057
1493
  }
1058
1494
  /**
1059
1495
  * move many entries
@@ -1074,27 +1510,56 @@ class Remarkable {
1074
1510
  return await this.#withRetry(async () => {
1075
1511
  const [rootHash, generation, schemaVersion] = await this.#getRootHash(refresh);
1076
1512
  const { entries } = await this.raw.getEntries({
1077
- id: ROOT_SCHEMA,
1513
+ id: ROOT_LIST,
1078
1514
  hash: rootHash,
1079
1515
  });
1080
- const hashSet = new Set(refs.map((ref) => ref.hash));
1516
+ const wanted = new Set(refs.map((ref) => `${ref.id}\0${ref.hash}`));
1517
+ const found = new Set();
1081
1518
  const toUpdate = [];
1082
1519
  const newEntries = [];
1083
1520
  for (const entry of entries) {
1084
- const part = hashSet.has(entry.hash) ? toUpdate : newEntries;
1085
- part.push(entry);
1521
+ const key = `${entry.id}\0${entry.hash}`;
1522
+ if (wanted.has(key)) {
1523
+ toUpdate.push(entry);
1524
+ found.add(key);
1525
+ }
1526
+ else {
1527
+ newEntries.push(entry);
1528
+ }
1529
+ }
1530
+ for (const ref of refs) {
1531
+ if (!found.has(`${ref.id}\0${ref.hash}`)) {
1532
+ throw new HashNotFoundError(ref.hash);
1533
+ }
1086
1534
  }
1087
- const resolved = await Promise.all(toUpdate.map(({ id, hash }) => this.#editMetaRaw(id, hash, { parent }, schemaVersion)));
1088
- const uploads = [];
1535
+ const resolved = await Promise.all(toUpdate.map((entry) => this.#editMetaRaw(entry, { parent }, schemaVersion)));
1089
1536
  const result = [];
1090
- for (const [i, [newEnt, upload]] of resolved.entries()) {
1537
+ for (const [i, newEnt] of resolved.entries()) {
1091
1538
  newEntries.push(newEnt);
1092
- uploads.push(upload);
1093
1539
  result.push({ id: toUpdate[i].id, hash: newEnt.hash });
1094
1540
  }
1095
- const [rootEntry, uploadRoot] = await this.raw.putEntries(ROOT_LIST, newEntries, 4);
1096
- await Promise.all([Promise.all(uploads), uploadRoot]);
1097
- await this.#putRootHash(rootEntry.hash, generation);
1541
+ let newRoot;
1542
+ {
1543
+ const env_12 = { stack: [], error: void 0, hasError: false };
1544
+ try {
1545
+ const docs = __addDisposableResource(env_12, new AsyncDisposableStack(), true);
1546
+ for (const entry of resolved) {
1547
+ docs.use(entry);
1548
+ }
1549
+ const rootEntry = __addDisposableResource(env_12, await this.raw.putEntries(ROOT_LIST, newEntries, 4), true);
1550
+ newRoot = rootEntry.hash;
1551
+ }
1552
+ catch (e_12) {
1553
+ env_12.error = e_12;
1554
+ env_12.hasError = true;
1555
+ }
1556
+ finally {
1557
+ const result_12 = __disposeResources(env_12);
1558
+ if (result_12)
1559
+ await result_12;
1560
+ }
1561
+ }
1562
+ await this.#putRootHash(newRoot, generation);
1098
1563
  return result;
1099
1564
  });
1100
1565
  }
@@ -1145,15 +1610,15 @@ class Remarkable {
1145
1610
  // should only go one step) to track all hashes encountered
1146
1611
  // NOTE that we could increase the cache in this process, or it's possible
1147
1612
  // for other calls to increase the cache with misc values.
1148
- const base = await this.raw.getEntries({ id: ROOT_SCHEMA, hash: rootHash });
1613
+ const base = await this.raw.getEntries({ id: ROOT_LIST, hash: rootHash });
1149
1614
  let entries = [base.entries];
1150
1615
  let nextEntries = [];
1151
1616
  while (entries.length) {
1152
1617
  for (const entryList of entries) {
1153
- for (const { hash, subfiles, id } of entryList) {
1154
- toDelete.delete(hash);
1155
- if (subfiles > 0) {
1156
- nextEntries.push(this.raw.getEntries({ id: `${id}.docSchema`, hash }));
1618
+ for (const entry of entryList) {
1619
+ toDelete.delete(entry.hash);
1620
+ if (entry.subfiles > 0) {
1621
+ nextEntries.push(this.raw.getEntries(entry));
1157
1622
  }
1158
1623
  }
1159
1624
  }
@@ -1175,7 +1640,46 @@ class Remarkable {
1175
1640
  this.raw.clearCache();
1176
1641
  }
1177
1642
  }
1178
- const cached = z.record(z.string(), z.string().nullable());
1643
+ /** the default {@link RemarkableSessionOptions.maxCachedBytes} */
1644
+ const MAX_CACHED_BYTES = 1024 * 1024;
1645
+ const cacheEntries = z.record(z.string(), z.string().nullable());
1646
+ /**
1647
+ * a dumped cache, either the tagged envelope or the original bare mapping
1648
+ *
1649
+ * The original format had no version, so its absence marks untagged text.
1650
+ */
1651
+ const cacheDump = z
1652
+ .object({
1653
+ version: z.literal(CACHE_VERSION),
1654
+ entries: cacheEntries,
1655
+ })
1656
+ .or(cacheEntries.transform((entries) => ({ version: undefined, entries })));
1657
+ /** decode a dumped cache into the byte map the api holds */
1658
+ function decodeCache(dumped) {
1659
+ const parsed = cacheDump.safeParse(dumped);
1660
+ if (!parsed.success) {
1661
+ throw new Error(`cache was neither a version ${CACHE_VERSION} dump nor the original mapping of hashes to text. Either construct the api without a cache, or fix its format.`);
1662
+ }
1663
+ const { version, entries } = parsed.data;
1664
+ const enc = new TextEncoder();
1665
+ return Object.entries(entries).map(([hash, value]) => {
1666
+ if (value === null) {
1667
+ return [hash, null];
1668
+ }
1669
+ else if (version === undefined) {
1670
+ return [hash, enc.encode(value)];
1671
+ }
1672
+ else if (value.startsWith(BYTES_PREFIX)) {
1673
+ return [hash, Uint8Array.fromBase64(value.slice(1))];
1674
+ }
1675
+ else if (value.startsWith(TEXT_PREFIX)) {
1676
+ return [hash, enc.encode(value.slice(1))];
1677
+ }
1678
+ else {
1679
+ throw new Error(`cache entry ${hash} wasn't tagged '${TEXT_PREFIX}' or '${BYTES_PREFIX}'. Either construct the api without a cache, or fix its format.`);
1680
+ }
1681
+ });
1682
+ }
1179
1683
  /**
1180
1684
  * Exchange a device token for a session token.
1181
1685
  *
@@ -1204,17 +1708,12 @@ export async function auth(deviceToken, { authHost = AUTH_HOST } = {}) {
1204
1708
  * @param sessionToken - the session token used for authorization
1205
1709
  * @returns an api instance
1206
1710
  */
1207
- export function session(sessionToken, { rawHost = RAW_HOST, uploadHost = UPLOAD_HOST, cache, maxCacheSize = Infinity, maxGenerationRetries = 10, maxTransientRetries = 3, } = {}) {
1208
- const initCache = JSON.parse(cache ?? "{}");
1209
- const parsedCache = cached.safeParse(initCache);
1210
- if (parsedCache.success) {
1211
- const entries = Object.entries(parsedCache.data);
1212
- const cacheMap = maxCacheSize === Infinity
1213
- ? new Map(entries)
1214
- : new LruCache(maxCacheSize, entries);
1215
- return new Remarkable(sessionToken, rawHost, uploadHost, cacheMap, maxGenerationRetries, maxTransientRetries);
1216
- }
1217
- 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.");
1711
+ export function session(sessionToken, { rawHost = RAW_HOST, uploadHost = UPLOAD_HOST, cache, maxCacheSize = Infinity, maxCachedBytes = MAX_CACHED_BYTES, maxGenerationRetries = 10, maxTransientRetries = 3, } = {}) {
1712
+ const entries = decodeCache(JSON.parse(cache ?? "{}"));
1713
+ const cacheMap = maxCacheSize === Infinity
1714
+ ? new Map(entries)
1715
+ : new LruCache(maxCacheSize, entries);
1716
+ return new Remarkable(sessionToken, rawHost, uploadHost, cacheMap, maxGenerationRetries, maxTransientRetries, maxCachedBytes);
1218
1717
  }
1219
1718
  /**
1220
1719
  * create an instance of the api
@@ -1227,14 +1726,9 @@ export function session(sessionToken, { rawHost = RAW_HOST, uploadHost = UPLOAD_
1227
1726
  * @returns an api instance
1228
1727
  */
1229
1728
  export async function remarkable(deviceToken, options = {}) {
1230
- const { authHost, rawHost, uploadHost, cache, maxCacheSize, maxGenerationRetries, maxTransientRetries, } = options ?? {};
1729
+ // forward everything but the auth option, so a new session option can't be
1730
+ // dropped here by omission
1731
+ const { authHost, ...sessionOptions } = options ?? {};
1231
1732
  const sessionToken = await auth(deviceToken, { authHost });
1232
- return session(sessionToken, {
1233
- rawHost,
1234
- uploadHost,
1235
- cache,
1236
- maxCacheSize,
1237
- maxGenerationRetries,
1238
- maxTransientRetries,
1239
- });
1733
+ return session(sessionToken, sessionOptions);
1240
1734
  }