rmapi-js 8.1.1 → 8.3.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/error.d.ts +14 -0
- package/dist/error.js +21 -0
- package/dist/index.d.ts +50 -526
- package/dist/index.js +65 -450
- package/dist/raw.d.ts +558 -0
- package/dist/raw.js +427 -0
- package/dist/rmapi-js.esm.min.js +12 -12
- package/package.json +12 -12
package/dist/index.js
CHANGED
|
@@ -51,25 +51,20 @@
|
|
|
51
51
|
*
|
|
52
52
|
* @packageDocumentation
|
|
53
53
|
*/
|
|
54
|
-
import { fromByteArray } from "base64-js";
|
|
55
|
-
import CRC32C from "crc-32/crc32c";
|
|
56
54
|
import JSZip from "jszip";
|
|
57
|
-
import {
|
|
55
|
+
import { nullable, string, values } from "jtd-ts";
|
|
58
56
|
import { v4 as uuid4 } from "uuid";
|
|
57
|
+
import { HashNotFoundError, ValidationError } from "./error";
|
|
59
58
|
import { LruCache } from "./lru";
|
|
59
|
+
import { RawRemarkable, } from "./raw";
|
|
60
|
+
export { HashNotFoundError, ValidationError } from "./error";
|
|
60
61
|
const AUTH_HOST = "https://webapp-prod.cloud.remarkable.engineering";
|
|
61
62
|
const RAW_HOST = "https://eu.tectonic.remarkable.com";
|
|
63
|
+
// ------------ //
|
|
64
|
+
// Request Info //
|
|
65
|
+
// ------------ //
|
|
66
|
+
// The section has all the types that are stored in the remarkable cloud.
|
|
62
67
|
const idReg = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}||trash)$/;
|
|
63
|
-
const hashReg = /^[0-9a-f]{64}$/;
|
|
64
|
-
const tag = properties({
|
|
65
|
-
name: string(),
|
|
66
|
-
timestamp: float64(),
|
|
67
|
-
}, undefined, true);
|
|
68
|
-
const pageTag = properties({
|
|
69
|
-
name: string(),
|
|
70
|
-
pageId: string(),
|
|
71
|
-
timestamp: float64(),
|
|
72
|
-
}, undefined, true);
|
|
73
68
|
/** An error that gets thrown when the backend while trying to update
|
|
74
69
|
*
|
|
75
70
|
* IF you encounter this error, you likely just need to try th request again. If
|
|
@@ -96,27 +91,6 @@ export class ResponseError extends Error {
|
|
|
96
91
|
this.statusText = statusText;
|
|
97
92
|
}
|
|
98
93
|
}
|
|
99
|
-
/** an error that results from a failed request */
|
|
100
|
-
export class ValidationError extends Error {
|
|
101
|
-
/** the response status number */
|
|
102
|
-
field;
|
|
103
|
-
/** the response status text */
|
|
104
|
-
regex;
|
|
105
|
-
constructor(field, regex, message) {
|
|
106
|
-
super(message);
|
|
107
|
-
this.field = field;
|
|
108
|
-
this.regex = regex;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
/** an error that results while supplying a hash not found in the entries of the root hash */
|
|
112
|
-
export class HashNotFoundError extends Error {
|
|
113
|
-
/** the hash that couldn't be found */
|
|
114
|
-
hash;
|
|
115
|
-
constructor(hash) {
|
|
116
|
-
super(`'${hash}' not found in the root hash`);
|
|
117
|
-
this.hash = hash;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
94
|
/**
|
|
121
95
|
* register a device and get the token needed to access the api
|
|
122
96
|
*
|
|
@@ -149,418 +123,6 @@ export async function register(code, { deviceDesc = "browser-chrome", uuid = uui
|
|
|
149
123
|
return await resp.text();
|
|
150
124
|
}
|
|
151
125
|
}
|
|
152
|
-
const documentMetadata = properties(undefined, {
|
|
153
|
-
authors: elements(string()),
|
|
154
|
-
title: string(),
|
|
155
|
-
publicationDate: string(),
|
|
156
|
-
publisher: string(),
|
|
157
|
-
}, true);
|
|
158
|
-
const cPagePage = properties({
|
|
159
|
-
id: string(),
|
|
160
|
-
idx: properties({
|
|
161
|
-
timestamp: string(),
|
|
162
|
-
value: string(),
|
|
163
|
-
}, undefined, true),
|
|
164
|
-
}, {
|
|
165
|
-
template: properties({
|
|
166
|
-
timestamp: string(),
|
|
167
|
-
value: string(),
|
|
168
|
-
}, undefined, true),
|
|
169
|
-
redir: properties({
|
|
170
|
-
timestamp: string(),
|
|
171
|
-
value: int32(),
|
|
172
|
-
}, undefined, true),
|
|
173
|
-
scrollTime: properties({
|
|
174
|
-
timestamp: string(),
|
|
175
|
-
value: timestamp(),
|
|
176
|
-
}, undefined, true),
|
|
177
|
-
verticalScroll: properties({
|
|
178
|
-
timestamp: string(),
|
|
179
|
-
value: float64(),
|
|
180
|
-
}, undefined, true),
|
|
181
|
-
deleted: properties({
|
|
182
|
-
timestamp: string(),
|
|
183
|
-
value: int32(),
|
|
184
|
-
}, undefined, true),
|
|
185
|
-
}, true);
|
|
186
|
-
const cPages = properties({
|
|
187
|
-
lastOpened: properties({
|
|
188
|
-
timestamp: string(),
|
|
189
|
-
value: string(),
|
|
190
|
-
}, undefined, true),
|
|
191
|
-
original: properties({
|
|
192
|
-
timestamp: string(),
|
|
193
|
-
value: int32(),
|
|
194
|
-
}, undefined, true),
|
|
195
|
-
pages: elements(cPagePage),
|
|
196
|
-
uuids: elements(properties({
|
|
197
|
-
first: string(),
|
|
198
|
-
second: uint32(),
|
|
199
|
-
}, undefined, true)),
|
|
200
|
-
}, undefined, true);
|
|
201
|
-
const collectionContent = properties(undefined, {
|
|
202
|
-
tags: elements(tag),
|
|
203
|
-
});
|
|
204
|
-
const templateContent = properties({
|
|
205
|
-
name: string(),
|
|
206
|
-
author: string(),
|
|
207
|
-
iconData: string(),
|
|
208
|
-
categories: elements(string()),
|
|
209
|
-
labels: elements(string()),
|
|
210
|
-
orientation: enumeration("portrait", "landscape"),
|
|
211
|
-
templateVersion: string(),
|
|
212
|
-
formatVersion: uint8(),
|
|
213
|
-
supportedScreens: elements(enumeration("rm2", "rmPP")),
|
|
214
|
-
constants: elements(values(int32())),
|
|
215
|
-
items: elements(empty()),
|
|
216
|
-
});
|
|
217
|
-
const documentContent = properties({
|
|
218
|
-
coverPageNumber: int32(),
|
|
219
|
-
documentMetadata,
|
|
220
|
-
extraMetadata: values(string()),
|
|
221
|
-
fileType: enumeration("epub", "notebook", "pdf"),
|
|
222
|
-
fontName: string(),
|
|
223
|
-
formatVersion: uint8(),
|
|
224
|
-
lineHeight: int32(),
|
|
225
|
-
orientation: enumeration("portrait", "landscape"),
|
|
226
|
-
pageCount: uint32(),
|
|
227
|
-
sizeInBytes: string(),
|
|
228
|
-
textAlignment: enumeration("justify", "left"),
|
|
229
|
-
textScale: float64(),
|
|
230
|
-
}, {
|
|
231
|
-
cPages,
|
|
232
|
-
customZoomCenterX: float64(),
|
|
233
|
-
customZoomCenterY: float64(),
|
|
234
|
-
customZoomOrientation: enumeration("portrait", "landscape"),
|
|
235
|
-
customZoomPageHeight: float64(),
|
|
236
|
-
customZoomPageWidth: float64(),
|
|
237
|
-
customZoomScale: float64(),
|
|
238
|
-
dummyDocument: boolean(),
|
|
239
|
-
keyboardMetadata: properties({
|
|
240
|
-
count: uint32(),
|
|
241
|
-
timestamp: float64(),
|
|
242
|
-
}, undefined, true),
|
|
243
|
-
lastOpenedPage: uint32(),
|
|
244
|
-
margins: uint32(),
|
|
245
|
-
originalPageCount: int32(),
|
|
246
|
-
pages: elements(string()),
|
|
247
|
-
pageTags: elements(pageTag),
|
|
248
|
-
redirectionPageMap: elements(int32()),
|
|
249
|
-
tags: elements(tag),
|
|
250
|
-
transform: properties({
|
|
251
|
-
m11: float64(),
|
|
252
|
-
m12: float64(),
|
|
253
|
-
m13: float64(),
|
|
254
|
-
m21: float64(),
|
|
255
|
-
m22: float64(),
|
|
256
|
-
m23: float64(),
|
|
257
|
-
m31: float64(),
|
|
258
|
-
m32: float64(),
|
|
259
|
-
m33: float64(),
|
|
260
|
-
}, undefined, true),
|
|
261
|
-
// eslint-disable-next-line spellcheck/spell-checker
|
|
262
|
-
viewBackgroundFilter: enumeration("off", "fullpage"),
|
|
263
|
-
zoomMode: enumeration("bestFit", "customFit", "fitToHeight", "fitToWidth"),
|
|
264
|
-
}, true);
|
|
265
|
-
const metadata = properties({
|
|
266
|
-
lastModified: string(),
|
|
267
|
-
parent: string(),
|
|
268
|
-
pinned: boolean(),
|
|
269
|
-
type: enumeration("DocumentType", "CollectionType", "TemplateType"),
|
|
270
|
-
visibleName: string(),
|
|
271
|
-
}, {
|
|
272
|
-
lastOpened: string(),
|
|
273
|
-
lastOpenedPage: uint32(),
|
|
274
|
-
createdTime: string(),
|
|
275
|
-
deleted: boolean(),
|
|
276
|
-
metadatamodified: boolean(),
|
|
277
|
-
modified: boolean(),
|
|
278
|
-
synced: boolean(),
|
|
279
|
-
version: uint8(),
|
|
280
|
-
}, true);
|
|
281
|
-
const updatedRootHash = properties({
|
|
282
|
-
hash: string(),
|
|
283
|
-
generation: float64(),
|
|
284
|
-
}, undefined, true);
|
|
285
|
-
const rootHash = properties({
|
|
286
|
-
hash: string(),
|
|
287
|
-
generation: float64(),
|
|
288
|
-
schemaVersion: uint8(),
|
|
289
|
-
}, undefined, true);
|
|
290
|
-
async function digest(buff) {
|
|
291
|
-
const digest = await crypto.subtle.digest("SHA-256", buff);
|
|
292
|
-
return [...new Uint8Array(digest)]
|
|
293
|
-
.map((x) => x.toString(16).padStart(2, "0"))
|
|
294
|
-
.join("");
|
|
295
|
-
}
|
|
296
|
-
class RawRemarkable {
|
|
297
|
-
#authedFetch;
|
|
298
|
-
#rawHost;
|
|
299
|
-
/**
|
|
300
|
-
* a cache of all hashes we know exist
|
|
301
|
-
*
|
|
302
|
-
* The backend is a readonly file system of hashes to content. After a hash has
|
|
303
|
-
* been read or written successfully, we know it exists, and potentially it's
|
|
304
|
-
* contents. We don't want to cache large binary files, but we can cache the
|
|
305
|
-
* small text based metadata files. For binary files we write null, so we know
|
|
306
|
-
* not to write a a cached value again, but we'll still need to read it.
|
|
307
|
-
*/
|
|
308
|
-
#cache;
|
|
309
|
-
constructor(authedFetch, cache, rawHost) {
|
|
310
|
-
this.#authedFetch = authedFetch;
|
|
311
|
-
this.#cache = cache;
|
|
312
|
-
this.#rawHost = rawHost;
|
|
313
|
-
}
|
|
314
|
-
/** make an authorized request to remarkable */
|
|
315
|
-
async getRootHash() {
|
|
316
|
-
const res = await this.#authedFetch("GET", `${this.#rawHost}/sync/v4/root`);
|
|
317
|
-
const raw = await res.text();
|
|
318
|
-
const loaded = JSON.parse(raw);
|
|
319
|
-
if (!rootHash.guardAssert(loaded))
|
|
320
|
-
throw Error("invalid root hash");
|
|
321
|
-
const { hash, generation, schemaVersion } = loaded;
|
|
322
|
-
if (schemaVersion !== 3) {
|
|
323
|
-
throw new Error(`schema version ${schemaVersion} not supported`);
|
|
324
|
-
}
|
|
325
|
-
else if (!Number.isSafeInteger(generation)) {
|
|
326
|
-
throw new Error(`generation ${generation} was not a safe integer; please file a bug report`);
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
return [hash, generation];
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
async #getHash(hash) {
|
|
333
|
-
if (!hashReg.test(hash)) {
|
|
334
|
-
throw new ValidationError(hash, hashReg, "hash was not a valid hash");
|
|
335
|
-
}
|
|
336
|
-
const resp = await this.#authedFetch("GET", `${this.#rawHost}/sync/v3/files/${hash}`);
|
|
337
|
-
// TODO switch to `.bytes()`.
|
|
338
|
-
const raw = await resp.arrayBuffer();
|
|
339
|
-
return new Uint8Array(raw);
|
|
340
|
-
}
|
|
341
|
-
async getHash(hash) {
|
|
342
|
-
const cached = this.#cache.get(hash);
|
|
343
|
-
if (cached != null) {
|
|
344
|
-
const enc = new TextEncoder();
|
|
345
|
-
return enc.encode(cached);
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
const res = await this.#getHash(hash);
|
|
349
|
-
// mark that we know hash exists
|
|
350
|
-
const cacheVal = this.#cache.get(hash);
|
|
351
|
-
if (cacheVal === undefined) {
|
|
352
|
-
this.#cache.set(hash, null);
|
|
353
|
-
}
|
|
354
|
-
return res;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
async getText(hash) {
|
|
358
|
-
const cached = this.#cache.get(hash);
|
|
359
|
-
if (cached != null) {
|
|
360
|
-
return cached;
|
|
361
|
-
}
|
|
362
|
-
else {
|
|
363
|
-
// NOTE two simultaneous requests will fetch twice
|
|
364
|
-
const raw = await this.#getHash(hash);
|
|
365
|
-
const dec = new TextDecoder();
|
|
366
|
-
const res = dec.decode(raw);
|
|
367
|
-
this.#cache.set(hash, res);
|
|
368
|
-
return res;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
async getEntries(hash) {
|
|
372
|
-
const rawFile = await this.getText(hash);
|
|
373
|
-
const [version, ...rest] = rawFile.slice(0, -1).split("\n");
|
|
374
|
-
if (version != "3") {
|
|
375
|
-
throw new Error(`schema version ${version} not supported`);
|
|
376
|
-
}
|
|
377
|
-
else {
|
|
378
|
-
return rest.map((line) => {
|
|
379
|
-
const [hash, type, id, subfiles, size] = line.split(":");
|
|
380
|
-
if (hash === undefined ||
|
|
381
|
-
type === undefined ||
|
|
382
|
-
id === undefined ||
|
|
383
|
-
subfiles === undefined ||
|
|
384
|
-
size === undefined) {
|
|
385
|
-
throw new Error(`line '${line}' was not formatted correctly`);
|
|
386
|
-
}
|
|
387
|
-
else if (type === "80000000") {
|
|
388
|
-
return {
|
|
389
|
-
hash,
|
|
390
|
-
type: 80000000,
|
|
391
|
-
id,
|
|
392
|
-
subfiles: parseInt(subfiles),
|
|
393
|
-
size: parseInt(size),
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
else if (type === "0" && subfiles === "0") {
|
|
397
|
-
return {
|
|
398
|
-
hash,
|
|
399
|
-
type: 0,
|
|
400
|
-
id,
|
|
401
|
-
subfiles: 0,
|
|
402
|
-
size: parseInt(size),
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
else {
|
|
406
|
-
throw new Error(`line '${line}' was not formatted correctly`);
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
async getContent(hash) {
|
|
412
|
-
const raw = await this.getText(hash);
|
|
413
|
-
const loaded = JSON.parse(raw);
|
|
414
|
-
// jtd can't verify non-discriminated unions, in this case, we have fileType
|
|
415
|
-
// defined or not. As a result, we try each, and concatenate the errors at the end
|
|
416
|
-
const errors = [];
|
|
417
|
-
for (const [name, valid] of [
|
|
418
|
-
["collection", collectionContent],
|
|
419
|
-
["template", templateContent],
|
|
420
|
-
["document", documentContent],
|
|
421
|
-
]) {
|
|
422
|
-
try {
|
|
423
|
-
if (valid.guardAssert(loaded))
|
|
424
|
-
return loaded;
|
|
425
|
-
}
|
|
426
|
-
catch (ex) {
|
|
427
|
-
const msg = ex instanceof Error ? ex.message : "unknown error type";
|
|
428
|
-
errors.push(`Couldn't validate as ${name} because:\n${msg}`);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
const joined = errors.join("\n\nor\n\n");
|
|
432
|
-
throw new Error(`invalid content: ${joined}`);
|
|
433
|
-
}
|
|
434
|
-
async getMetadata(hash) {
|
|
435
|
-
const raw = await this.getText(hash);
|
|
436
|
-
const loaded = JSON.parse(raw);
|
|
437
|
-
if (!metadata.guardAssert(loaded))
|
|
438
|
-
throw Error("invalid metadata");
|
|
439
|
-
return loaded;
|
|
440
|
-
}
|
|
441
|
-
async putRootHash(hash, generation, broadcast = true) {
|
|
442
|
-
if (!Number.isSafeInteger(generation)) {
|
|
443
|
-
throw new Error(`generation ${generation} was not a safe integer`);
|
|
444
|
-
}
|
|
445
|
-
else if (!hashReg.test(hash)) {
|
|
446
|
-
throw new ValidationError(hash, hashReg, "rootHash was not a valid hash");
|
|
447
|
-
}
|
|
448
|
-
const body = JSON.stringify({
|
|
449
|
-
hash,
|
|
450
|
-
generation,
|
|
451
|
-
broadcast,
|
|
452
|
-
});
|
|
453
|
-
const resp = await this.#authedFetch("PUT", `${this.#rawHost}/sync/v3/root`, { body });
|
|
454
|
-
const raw = await resp.text();
|
|
455
|
-
const loaded = JSON.parse(raw);
|
|
456
|
-
if (!updatedRootHash.guardAssert(loaded))
|
|
457
|
-
throw Error("invalid root hash");
|
|
458
|
-
const { hash: newHash, generation: newGen } = loaded;
|
|
459
|
-
if (Number.isSafeInteger(newGen)) {
|
|
460
|
-
return [newHash, newGen];
|
|
461
|
-
}
|
|
462
|
-
else {
|
|
463
|
-
throw new Error(`new generation ${newGen} was not a safe integer; please file a bug report`);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
async #putFile(hash, fileName, bytes) {
|
|
467
|
-
// if the hash is already in the cache, writing is pointless
|
|
468
|
-
if (!this.#cache.has(hash)) {
|
|
469
|
-
const crc = CRC32C.buf(bytes, 0);
|
|
470
|
-
const buff = new ArrayBuffer(4);
|
|
471
|
-
new DataView(buff).setInt32(0, crc, false);
|
|
472
|
-
const crcHash = fromByteArray(new Uint8Array(buff));
|
|
473
|
-
await this.#authedFetch("PUT", `${this.#rawHost}/sync/v3/files/${hash}`, {
|
|
474
|
-
body: bytes,
|
|
475
|
-
headers: {
|
|
476
|
-
"rm-filename": fileName,
|
|
477
|
-
// eslint-disable-next-line spellcheck/spell-checker
|
|
478
|
-
"x-goog-hash": `crc32c=${crcHash}`,
|
|
479
|
-
},
|
|
480
|
-
});
|
|
481
|
-
// mark that we know this hash exists
|
|
482
|
-
const cacheVal = this.#cache.get(hash);
|
|
483
|
-
if (cacheVal === undefined) {
|
|
484
|
-
this.#cache.set(hash, null);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
async putFile(id, bytes) {
|
|
489
|
-
const hash = await digest(bytes);
|
|
490
|
-
const res = {
|
|
491
|
-
id,
|
|
492
|
-
hash,
|
|
493
|
-
type: 0,
|
|
494
|
-
subfiles: 0,
|
|
495
|
-
size: bytes.length,
|
|
496
|
-
};
|
|
497
|
-
return [res, this.#putFile(hash, id, bytes)];
|
|
498
|
-
}
|
|
499
|
-
async putText(id, text) {
|
|
500
|
-
const enc = new TextEncoder();
|
|
501
|
-
const bytes = enc.encode(text);
|
|
502
|
-
const [ent, upload] = await this.putFile(id, bytes);
|
|
503
|
-
return [
|
|
504
|
-
ent,
|
|
505
|
-
upload.then(() => {
|
|
506
|
-
// on success, write to cache
|
|
507
|
-
this.#cache.set(ent.hash, text);
|
|
508
|
-
}),
|
|
509
|
-
];
|
|
510
|
-
}
|
|
511
|
-
async putContent(id, content) {
|
|
512
|
-
if (!id.endsWith(".content")) {
|
|
513
|
-
throw new Error(`id ${id} did not end with '.content'`);
|
|
514
|
-
}
|
|
515
|
-
else {
|
|
516
|
-
return await this.putText(id, JSON.stringify(content));
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
async putMetadata(id, metadata) {
|
|
520
|
-
if (!id.endsWith(".metadata")) {
|
|
521
|
-
throw new Error(`id ${id} did not end with '.metadata'`);
|
|
522
|
-
}
|
|
523
|
-
else {
|
|
524
|
-
return await this.putText(id, JSON.stringify(metadata));
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
async putEntries(id, entries) {
|
|
528
|
-
// NOTE collections have a special hash function, the hash of their
|
|
529
|
-
// contents, so this needs to be different
|
|
530
|
-
entries.sort((a, b) => a.id.localeCompare(b.id));
|
|
531
|
-
const hashBuff = new Uint8Array(entries.length * 32);
|
|
532
|
-
for (const [start, { hash }] of entries.entries()) {
|
|
533
|
-
for (const [i, byte] of (hash.match(/../g) ?? []).entries()) {
|
|
534
|
-
hashBuff[start * 32 + i] = parseInt(byte, 16);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
const hash = await digest(hashBuff);
|
|
538
|
-
const size = entries.reduce((acc, ent) => acc + ent.size, 0);
|
|
539
|
-
const records = ["3\n"];
|
|
540
|
-
for (const { hash, type, id, subfiles, size } of entries) {
|
|
541
|
-
records.push(`${hash}:${type}:${id}:${subfiles}:${size}\n`);
|
|
542
|
-
}
|
|
543
|
-
const res = {
|
|
544
|
-
id,
|
|
545
|
-
hash,
|
|
546
|
-
type: 80000000,
|
|
547
|
-
subfiles: entries.length,
|
|
548
|
-
size,
|
|
549
|
-
};
|
|
550
|
-
const enc = new TextEncoder();
|
|
551
|
-
return [
|
|
552
|
-
res,
|
|
553
|
-
// NOTE when monitoring requests, this had the extension .docSchema appended, but I'm not entirely sure why
|
|
554
|
-
this.#putFile(hash, `${id}.docSchema`, enc.encode(records.join(""))),
|
|
555
|
-
];
|
|
556
|
-
}
|
|
557
|
-
dumpCache() {
|
|
558
|
-
return JSON.stringify(Object.fromEntries(this.#cache));
|
|
559
|
-
}
|
|
560
|
-
clearCache() {
|
|
561
|
-
this.#cache.clear();
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
126
|
/** the implementation of that api */
|
|
565
127
|
class Remarkable {
|
|
566
128
|
#userToken;
|
|
@@ -861,6 +423,56 @@ class Remarkable {
|
|
|
861
423
|
async uploadPdf(visibleName, buffer, opts = {}) {
|
|
862
424
|
return await this.putPdf(visibleName, buffer, opts);
|
|
863
425
|
}
|
|
426
|
+
/** edit just a content entry */
|
|
427
|
+
async #editContentRaw(id, hash, update) {
|
|
428
|
+
const entries = await this.raw.getEntries(hash);
|
|
429
|
+
const contInd = entries.findIndex((ent) => ent.id.endsWith(".content"));
|
|
430
|
+
const contEntry = entries[contInd];
|
|
431
|
+
if (contEntry === undefined) {
|
|
432
|
+
throw new Error("internal error: couldn't find content in entry hash");
|
|
433
|
+
}
|
|
434
|
+
const cont = await this.raw.getContent(contEntry.hash);
|
|
435
|
+
Object.assign(cont, update);
|
|
436
|
+
const [newContEntry, uploadCont] = await this.raw.putContent(contEntry.id, cont);
|
|
437
|
+
entries[contInd] = newContEntry;
|
|
438
|
+
const [result, uploadEntries] = await this.raw.putEntries(id, entries);
|
|
439
|
+
const upload = Promise.all([uploadCont, uploadEntries]);
|
|
440
|
+
return [result, upload];
|
|
441
|
+
}
|
|
442
|
+
/** fully sync a content edit */
|
|
443
|
+
async #editContent(hash, update, expectedType, refresh) {
|
|
444
|
+
const [rootHash, generation] = await this.#getRootHash(refresh);
|
|
445
|
+
const entries = await this.raw.getEntries(rootHash);
|
|
446
|
+
const hashInd = entries.findIndex((ent) => ent.hash === hash);
|
|
447
|
+
const hashEnt = entries[hashInd];
|
|
448
|
+
if (hashEnt === undefined) {
|
|
449
|
+
throw new HashNotFoundError(hash);
|
|
450
|
+
}
|
|
451
|
+
const [[newEnt, uploadEnt], meta] = await Promise.all([
|
|
452
|
+
this.#editContentRaw(hashEnt.id, hash, update),
|
|
453
|
+
this.getMetadata(hash),
|
|
454
|
+
]);
|
|
455
|
+
if (meta.type !== expectedType) {
|
|
456
|
+
throw new Error(`expected type ${expectedType} but got ${meta.type} for hash ${hash}`);
|
|
457
|
+
}
|
|
458
|
+
entries[hashInd] = newEnt;
|
|
459
|
+
const [rootEntry, uploadRoot] = await this.raw.putEntries("root", entries);
|
|
460
|
+
await Promise.all([uploadEnt, uploadRoot]);
|
|
461
|
+
await this.#putRootHash(rootEntry.hash, generation);
|
|
462
|
+
return { hash: newEnt.hash };
|
|
463
|
+
}
|
|
464
|
+
/** update document content */
|
|
465
|
+
async updateDocument(hash, content, refresh = false) {
|
|
466
|
+
return await this.#editContent(hash, content, "DocumentType", refresh);
|
|
467
|
+
}
|
|
468
|
+
/** update collection content */
|
|
469
|
+
async updateCollection(hash, content, refresh = false) {
|
|
470
|
+
return await this.#editContent(hash, content, "CollectionType", refresh);
|
|
471
|
+
}
|
|
472
|
+
/** update template content */
|
|
473
|
+
async updateTemplate(hash, content, refresh = false) {
|
|
474
|
+
return await this.#editContent(hash, content, "TemplateType", refresh);
|
|
475
|
+
}
|
|
864
476
|
async #editMetaRaw(id, hash, update) {
|
|
865
477
|
const entries = await this.raw.getEntries(hash);
|
|
866
478
|
const metaInd = entries.findIndex((ent) => ent.id.endsWith(".metadata"));
|
|
@@ -872,8 +484,8 @@ class Remarkable {
|
|
|
872
484
|
Object.assign(meta, update);
|
|
873
485
|
const [newMetaEntry, uploadMeta] = await this.raw.putMetadata(metaEntry.id, meta);
|
|
874
486
|
entries[metaInd] = newMetaEntry;
|
|
875
|
-
const [result,
|
|
876
|
-
const upload = Promise.all([uploadMeta,
|
|
487
|
+
const [result, uploadEntries] = await this.raw.putEntries(id, entries);
|
|
488
|
+
const upload = Promise.all([uploadMeta, uploadEntries]);
|
|
877
489
|
return [result, upload];
|
|
878
490
|
}
|
|
879
491
|
async #editMeta(hash, update, refresh = false) {
|
|
@@ -906,6 +518,10 @@ class Remarkable {
|
|
|
906
518
|
async rename(hash, visibleName, refresh = false) {
|
|
907
519
|
return await this.#editMeta(hash, { visibleName }, refresh);
|
|
908
520
|
}
|
|
521
|
+
/** stared */
|
|
522
|
+
async stared(hash, stared, refresh = false) {
|
|
523
|
+
return await this.#editMeta(hash, { pinned: stared }, refresh);
|
|
524
|
+
}
|
|
909
525
|
/** move many hashes */
|
|
910
526
|
async bulkMove(hashes, parent, refresh = false) {
|
|
911
527
|
if (!idReg.test(parent)) {
|
|
@@ -929,8 +545,7 @@ class Remarkable {
|
|
|
929
545
|
result[toUpdate[i].hash] = newEnt.hash;
|
|
930
546
|
}
|
|
931
547
|
const [rootEntry, uploadRoot] = await this.raw.putEntries("root", newEntries);
|
|
932
|
-
|
|
933
|
-
await Promise.all(uploads);
|
|
548
|
+
await Promise.all([Promise.all(uploads), uploadRoot]);
|
|
934
549
|
await this.#putRootHash(rootEntry.hash, generation);
|
|
935
550
|
return { hashes: result };
|
|
936
551
|
}
|