rmapi-js 13.0.0 → 14.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/codes.d.ts +35 -0
- package/dist/codes.js +61 -0
- package/dist/index.d.ts +77 -2
- package/dist/index.js +160 -2
- package/dist/raw.d.ts +15 -1
- package/dist/raw.js +49 -33
- package/dist/rm5.d.ts +6 -30
- package/dist/rm5.js +3 -43
- package/dist/rm6.d.ts +7 -6
- package/dist/rm6.js +4 -3
- package/dist/rmapi-js.esm.min.js +85 -12
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -67,6 +67,20 @@ const [entry] = await api.listItems()
|
|
|
67
67
|
const buffer = await api.getEpub(entry)
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
To react to changes as they happen, `listen` yields a notification each time a
|
|
71
|
+
device finishes syncing. It names the device, not what changed, so use it as a
|
|
72
|
+
cue to re-read whatever you care about. Compare against `deviceId` to skip your
|
|
73
|
+
own syncs. reMarkable authorizes the socket's handshake with a header, and a
|
|
74
|
+
browser's `WebSocket` takes only a url, so this needs node or bun.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
for await (const { attributes } of api.listen()) {
|
|
78
|
+
if (attributes.sourceDeviceID !== api.deviceId) {
|
|
79
|
+
const fileEntries = await api.listItems(true);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
70
84
|
### Gotchas
|
|
71
85
|
|
|
72
86
|
By default, all calls try to do their best to verify that the input and output
|
package/dist/codes.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pen and color codes a stroke carries, shared by every `.rm` version.
|
|
3
|
+
*
|
|
4
|
+
* A stroke stores its pen and its color as small integers. Both sets grew with
|
|
5
|
+
* the firmware, so a code from a newer device can name the same pen or color
|
|
6
|
+
* as an older one. Reading a page checks the codes against these lists.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
/** a pen/tool name for a stroke */
|
|
12
|
+
export type RmBrush = "brush" | "pencil" | "ballpoint" | "marker" | "fineliner" | "highlighter" | "eraser" | "mechanicalPencil" | "eraseArea" | "calligraphy" | "shader";
|
|
13
|
+
/** a pen code that appears in a file */
|
|
14
|
+
export type RmBrushCode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 23;
|
|
15
|
+
export declare const rmBrushCode: z.ZodType<RmBrushCode>;
|
|
16
|
+
/**
|
|
17
|
+
* the reMarkable pens, by code
|
|
18
|
+
*
|
|
19
|
+
* The codes come in two firmware families, so two codes can name the same pen.
|
|
20
|
+
*/
|
|
21
|
+
export declare const rmBrushes: Readonly<Record<RmBrushCode, RmBrush>>;
|
|
22
|
+
/** a color name for a stroke */
|
|
23
|
+
export type RmColor = "black" | "gray" | "white" | "yellow" | "green" | "pink" | "blue" | "red" | "grayOverlap" | "highlight" | "cyan" | "magenta";
|
|
24
|
+
/** a color code that appears in a file */
|
|
25
|
+
export type RmColorCode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13;
|
|
26
|
+
export declare const rmColorCode: z.ZodType<RmColorCode>;
|
|
27
|
+
/**
|
|
28
|
+
* the reMarkable palette, by code
|
|
29
|
+
*
|
|
30
|
+
* The palette grew when colored annotations arrived and again for the Paper
|
|
31
|
+
* Pro, so two codes can name the same color. `"highlight"` is a marker rather
|
|
32
|
+
* than a color — the stroke's real color is its `colorRgba`. The shade a name
|
|
33
|
+
* renders as depends on the device.
|
|
34
|
+
*/
|
|
35
|
+
export declare const rmColors: Readonly<Record<RmColorCode, RmColor>>;
|
package/dist/codes.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pen and color codes a stroke carries, shared by every `.rm` version.
|
|
3
|
+
*
|
|
4
|
+
* A stroke stores its pen and its color as small integers. Both sets grew with
|
|
5
|
+
* the firmware, so a code from a newer device can name the same pen or color
|
|
6
|
+
* as an older one. Reading a page checks the codes against these lists.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
export const rmBrushCode = z.literal([0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 13, 14, 15, 16, 17, 18, 21, 23], { error: "unknown pen code" });
|
|
12
|
+
/**
|
|
13
|
+
* the reMarkable pens, by code
|
|
14
|
+
*
|
|
15
|
+
* The codes come in two firmware families, so two codes can name the same pen.
|
|
16
|
+
*/
|
|
17
|
+
export const rmBrushes = {
|
|
18
|
+
0: "brush",
|
|
19
|
+
12: "brush",
|
|
20
|
+
1: "pencil",
|
|
21
|
+
14: "pencil",
|
|
22
|
+
2: "ballpoint",
|
|
23
|
+
15: "ballpoint",
|
|
24
|
+
3: "marker",
|
|
25
|
+
16: "marker",
|
|
26
|
+
4: "fineliner",
|
|
27
|
+
17: "fineliner",
|
|
28
|
+
5: "highlighter",
|
|
29
|
+
18: "highlighter",
|
|
30
|
+
6: "eraser",
|
|
31
|
+
7: "mechanicalPencil",
|
|
32
|
+
13: "mechanicalPencil",
|
|
33
|
+
8: "eraseArea",
|
|
34
|
+
21: "calligraphy",
|
|
35
|
+
23: "shader",
|
|
36
|
+
};
|
|
37
|
+
export const rmColorCode = z.literal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], { error: "unknown color code" });
|
|
38
|
+
/**
|
|
39
|
+
* the reMarkable palette, by code
|
|
40
|
+
*
|
|
41
|
+
* The palette grew when colored annotations arrived and again for the Paper
|
|
42
|
+
* Pro, so two codes can name the same color. `"highlight"` is a marker rather
|
|
43
|
+
* than a color — the stroke's real color is its `colorRgba`. The shade a name
|
|
44
|
+
* renders as depends on the device.
|
|
45
|
+
*/
|
|
46
|
+
export const rmColors = {
|
|
47
|
+
0: "black",
|
|
48
|
+
1: "gray",
|
|
49
|
+
2: "white",
|
|
50
|
+
3: "yellow",
|
|
51
|
+
4: "green",
|
|
52
|
+
5: "pink",
|
|
53
|
+
6: "blue",
|
|
54
|
+
7: "red",
|
|
55
|
+
8: "grayOverlap",
|
|
56
|
+
9: "highlight",
|
|
57
|
+
10: "green",
|
|
58
|
+
11: "cyan",
|
|
59
|
+
12: "magenta",
|
|
60
|
+
13: "yellow",
|
|
61
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type BackgroundFilter, type CollectionContent, type Content, type DocumentContent, type Highlight, type ItemRef, type Metadata, type Orientation, type PageMetadata, RawRemarkable, type RmPage, type Tag, type TemplateContent, type TextAlignment, type ZoomMode } from "./raw.js";
|
|
2
|
+
export type { RmBrush, RmBrushCode, RmColor, RmColorCode, } from "./codes.js";
|
|
3
|
+
export { rmBrushes, rmColors } from "./codes.js";
|
|
2
4
|
export { type DeviceModel, type DeviceScreen, deviceScreens, } from "./devices.js";
|
|
3
5
|
export { HashNotFoundError, ValidationError } from "./error.js";
|
|
4
6
|
export type { BackgroundFilter, CollectionContent, Content, CPageNumberValue, CPagePage, CPageStringValue, CPages, CPageUUID, DocumentContent, DocumentMetadata, Entries, EntryType, FileType, Highlight, HighlightRect, ItemRef, KeyboardMetadata, LegacyCollectionContent, LegacyDocumentContent, Metadata, Orientation, PageLayer, PageMetadata, PageTag, PendingEntry, RawEntry, RawRemarkableApi, RmPage, SchemaVersion, Tag, TemplateContent, TextAlignment, UploadMimeType, ZoomMode, } from "./raw.js";
|
|
5
|
-
export type {
|
|
6
|
-
export { decodeBrush, rmColors } from "./rm5.js";
|
|
7
|
+
export type { RmLayer, RmLine, RmPageV5, RmPoint, RmVersion, } from "./rm5.js";
|
|
7
8
|
export type { AuthorIdsBlock, CrdtId, GlyphRange, LwwValue, MigrationInfoBlock, PageInfoBlock, Rectangle, RmBlock, RmScene, RmSceneItem, RmSceneLayer, RmV6Line, RmV6Point, RmV6Text, RmV6TextValue, RootTextBlock, SceneGlyphItemBlock, SceneGroupItemBlock, SceneInfoBlock, SceneItem, SceneLineItemBlock, SceneTextItemBlock, SceneTombstoneItemBlock, SceneTreeBlock, TreeNodeAnchor, TreeNodeBlock, UnknownBlock, } from "./rm6.js";
|
|
8
9
|
export { crdtKey } from "./rm6.js";
|
|
9
10
|
/** common properties shared by collections and documents */
|
|
@@ -184,6 +185,41 @@ export interface PutOptions {
|
|
|
184
185
|
*/
|
|
185
186
|
refresh?: boolean;
|
|
186
187
|
}
|
|
188
|
+
/** what a sync notification says happened */
|
|
189
|
+
export interface SyncEventAttributes {
|
|
190
|
+
/** the kind of event, always a sync completion here */
|
|
191
|
+
event: "SyncComplete";
|
|
192
|
+
/**
|
|
193
|
+
* the device that synced
|
|
194
|
+
*
|
|
195
|
+
* Tablets report their serial, api clients the id they registered with, so
|
|
196
|
+
* compare it against {@link RemarkableApi.deviceId | `deviceId`} to spot your
|
|
197
|
+
* own syncs.
|
|
198
|
+
*/
|
|
199
|
+
sourceDeviceID: string;
|
|
200
|
+
/** the account that synced, as an auth0 id */
|
|
201
|
+
auth0UserID: string;
|
|
202
|
+
/** the same value as `auth0UserID` in every event we've seen */
|
|
203
|
+
userID?: string;
|
|
204
|
+
/** the notification family, always "SyncEventNotification" so far */
|
|
205
|
+
eventType?: string;
|
|
206
|
+
/** the format version, always "1" so far */
|
|
207
|
+
eventVersion?: string;
|
|
208
|
+
/** the organization of a managed account, otherwise empty */
|
|
209
|
+
orgID?: string;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* a sync notification pushed over the notification socket
|
|
213
|
+
*
|
|
214
|
+
* Events carry a little more, but nothing worth depending on, so the rest is
|
|
215
|
+
* passed through untyped rather than validated.
|
|
216
|
+
*/
|
|
217
|
+
export interface SyncEvent {
|
|
218
|
+
/** what happened */
|
|
219
|
+
attributes: SyncEventAttributes;
|
|
220
|
+
/** the id of this notification */
|
|
221
|
+
messageid?: string;
|
|
222
|
+
}
|
|
187
223
|
/**
|
|
188
224
|
* the api for accessing remarkable functions
|
|
189
225
|
*
|
|
@@ -205,6 +241,13 @@ declare class Remarkable {
|
|
|
205
241
|
/** scoped access to the raw low-level api */
|
|
206
242
|
readonly raw: RawRemarkable;
|
|
207
243
|
constructor(sessionToken: string, rawHost: string, uploadHost: string, cache: Map<string, Uint8Array | null>, maxGenerationRetries: number, maxTransientRetries: number, maxCachedBytes: number);
|
|
244
|
+
/**
|
|
245
|
+
* the id this api is registered under
|
|
246
|
+
*
|
|
247
|
+
* This is the uuid passed to {@link register | `register`}, which reMarkable
|
|
248
|
+
* stamps on everything this client does. Tablets use their serial instead.
|
|
249
|
+
*/
|
|
250
|
+
get deviceId(): string;
|
|
208
251
|
/**
|
|
209
252
|
* list all items
|
|
210
253
|
*
|
|
@@ -673,6 +716,31 @@ declare class Remarkable {
|
|
|
673
716
|
* @returns references to the deleted entries, each with its new hash
|
|
674
717
|
*/
|
|
675
718
|
bulkDelete(refs: readonly ItemRef[], refresh?: boolean): Promise<ItemRef[]>;
|
|
719
|
+
/**
|
|
720
|
+
* listen for sync notifications
|
|
721
|
+
*
|
|
722
|
+
* reMarkable sends one every time a device finishes syncing. It names the
|
|
723
|
+
* device, not what changed, so use it as a cue to re-read.
|
|
724
|
+
*
|
|
725
|
+
* The socket is reopened when the server drops it, which happens every few
|
|
726
|
+
* minutes. Leaving the loop closes it. Session tokens expire after a few
|
|
727
|
+
* hours, and this throws once reconnecting with an expired one fails.
|
|
728
|
+
*
|
|
729
|
+
* reMarkable authorizes the handshake with a header, which node and bun can
|
|
730
|
+
* attach but a browser's `WebSocket` can't, so this is server side only.
|
|
731
|
+
*
|
|
732
|
+
* @example
|
|
733
|
+
* ```ts
|
|
734
|
+
* for await (const { attributes } of api.listen()) {
|
|
735
|
+
* if (attributes.sourceDeviceID !== api.deviceId) {
|
|
736
|
+
* const entries = await api.listItems(true);
|
|
737
|
+
* }
|
|
738
|
+
* }
|
|
739
|
+
* ```
|
|
740
|
+
*
|
|
741
|
+
* @returns the notifications, in the order they arrive
|
|
742
|
+
*/
|
|
743
|
+
listen(): AsyncGenerator<SyncEvent, void, undefined>;
|
|
676
744
|
/**
|
|
677
745
|
* get the current cache value as a string
|
|
678
746
|
*
|
|
@@ -719,6 +787,10 @@ export interface RemarkableSessionOptions {
|
|
|
719
787
|
/**
|
|
720
788
|
* the base url for making upload requests
|
|
721
789
|
*
|
|
790
|
+
* @deprecated uploads now go to the same server as everything else, so this
|
|
791
|
+
* will be removed and `rawHost` will cover both. Point `rawHost` at the
|
|
792
|
+
* backend you want instead.
|
|
793
|
+
*
|
|
722
794
|
* @defaultValue "https://internal.cloud.remarkable.com"
|
|
723
795
|
*/
|
|
724
796
|
uploadHost?: string;
|
|
@@ -792,6 +864,9 @@ export declare function auth(deviceToken: string, { authHost }?: AuthOptions): P
|
|
|
792
864
|
* If requests start failing, simply recreate the api instance with a freshly
|
|
793
865
|
* fetched session token.
|
|
794
866
|
*
|
|
867
|
+
* The device id is read out of the token, so this throws if it isn't one
|
|
868
|
+
* reMarkable minted.
|
|
869
|
+
*
|
|
795
870
|
* @param sessionToken - the session token used for authorization
|
|
796
871
|
* @returns an api instance
|
|
797
872
|
*/
|
package/dist/index.js
CHANGED
|
@@ -110,9 +110,9 @@ import { z } from "zod";
|
|
|
110
110
|
import { HashNotFoundError, ValidationError } from "./error.js";
|
|
111
111
|
import { LruCache } from "./lru.js";
|
|
112
112
|
import { BYTES_PREFIX, CACHE_VERSION, parseMetadata, RawRemarkable, TEXT_PREFIX, } from "./raw.js";
|
|
113
|
+
export { rmBrushes, rmColors } from "./codes.js";
|
|
113
114
|
export { deviceScreens, } from "./devices.js";
|
|
114
115
|
export { HashNotFoundError, ValidationError } from "./error.js";
|
|
115
|
-
export { decodeBrush, rmColors } from "./rm5.js";
|
|
116
116
|
export { crdtKey } from "./rm6.js";
|
|
117
117
|
const AUTH_HOST = "https://webapp-prod.cloud.remarkable.engineering";
|
|
118
118
|
const RAW_HOST = "https://eu.tectonic.remarkable.com";
|
|
@@ -237,6 +237,45 @@ export async function register(code, { deviceDesc = "browser-chrome", uuid = uui
|
|
|
237
237
|
return await resp.text();
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
|
+
const tokenClaims = z
|
|
241
|
+
.object({ "device-id": z.string() })
|
|
242
|
+
.loose();
|
|
243
|
+
function tokenDeviceId(token) {
|
|
244
|
+
const [, payload] = token.split(".", 2);
|
|
245
|
+
if (payload === undefined) {
|
|
246
|
+
throw new Error("token wasn't a jwt, so it had no device id");
|
|
247
|
+
}
|
|
248
|
+
const decoded = Uint8Array.fromBase64(payload, {
|
|
249
|
+
alphabet: "base64url",
|
|
250
|
+
lastChunkHandling: "loose",
|
|
251
|
+
});
|
|
252
|
+
const json = new TextDecoder().decode(decoded);
|
|
253
|
+
const parsed = tokenClaims.parse(JSON.parse(json));
|
|
254
|
+
return parsed["device-id"];
|
|
255
|
+
}
|
|
256
|
+
const notification = z
|
|
257
|
+
.object({
|
|
258
|
+
message: z
|
|
259
|
+
.object({ attributes: z.object({ event: z.string() }).loose() })
|
|
260
|
+
.loose(),
|
|
261
|
+
})
|
|
262
|
+
.loose();
|
|
263
|
+
const syncEvent = z
|
|
264
|
+
.object({
|
|
265
|
+
attributes: z
|
|
266
|
+
.object({
|
|
267
|
+
event: z.literal("SyncComplete"),
|
|
268
|
+
sourceDeviceID: z.string(),
|
|
269
|
+
auth0UserID: z.string(),
|
|
270
|
+
userID: z.string().optional(),
|
|
271
|
+
eventType: z.string().optional(),
|
|
272
|
+
eventVersion: z.string().optional(),
|
|
273
|
+
orgID: z.string().optional(),
|
|
274
|
+
})
|
|
275
|
+
.loose(),
|
|
276
|
+
messageid: z.string().optional(),
|
|
277
|
+
})
|
|
278
|
+
.loose();
|
|
240
279
|
/**
|
|
241
280
|
* the api for accessing remarkable functions
|
|
242
281
|
*
|
|
@@ -259,6 +298,7 @@ class Remarkable {
|
|
|
259
298
|
#cache;
|
|
260
299
|
/** scoped access to the raw low-level api */
|
|
261
300
|
raw;
|
|
301
|
+
#deviceId;
|
|
262
302
|
#maxGenerationRetries;
|
|
263
303
|
#maxTransientRetries;
|
|
264
304
|
#lastHashGen;
|
|
@@ -267,10 +307,24 @@ class Remarkable {
|
|
|
267
307
|
#rootMutex = new Mutex();
|
|
268
308
|
constructor(sessionToken, rawHost, uploadHost, cache, maxGenerationRetries, maxTransientRetries, maxCachedBytes) {
|
|
269
309
|
this.#sessionToken = sessionToken;
|
|
310
|
+
this.#deviceId = tokenDeviceId(sessionToken);
|
|
270
311
|
this.#cache = cache;
|
|
271
312
|
this.#maxGenerationRetries = maxGenerationRetries;
|
|
272
313
|
this.#maxTransientRetries = maxTransientRetries;
|
|
273
|
-
this.raw = new RawRemarkable((method, url, { body, headers } = {}) => this.#authedFetch(url, { method, body, headers }),
|
|
314
|
+
this.raw = new RawRemarkable((method, url, { body, headers } = {}) => this.#authedFetch(url, { method, body, headers }), (url) =>
|
|
315
|
+
// node and bun both take headers here, the dom types don't know about them
|
|
316
|
+
new WebSocket(url, {
|
|
317
|
+
headers: { Authorization: `Bearer ${this.#sessionToken}` },
|
|
318
|
+
}), cache, rawHost, uploadHost, maxCachedBytes);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* the id this api is registered under
|
|
322
|
+
*
|
|
323
|
+
* This is the uuid passed to {@link register | `register`}, which reMarkable
|
|
324
|
+
* stamps on everything this client does. Tablets use their serial instead.
|
|
325
|
+
*/
|
|
326
|
+
get deviceId() {
|
|
327
|
+
return this.#deviceId;
|
|
274
328
|
}
|
|
275
329
|
async #getRootHash(refresh = false) {
|
|
276
330
|
if (refresh || this.#lastHashGen === undefined) {
|
|
@@ -1577,6 +1631,107 @@ class Remarkable {
|
|
|
1577
1631
|
async bulkDelete(refs, refresh = false) {
|
|
1578
1632
|
return await this.bulkMove(refs, TRASH_ID, refresh);
|
|
1579
1633
|
}
|
|
1634
|
+
/**
|
|
1635
|
+
* listen for sync notifications
|
|
1636
|
+
*
|
|
1637
|
+
* reMarkable sends one every time a device finishes syncing. It names the
|
|
1638
|
+
* device, not what changed, so use it as a cue to re-read.
|
|
1639
|
+
*
|
|
1640
|
+
* The socket is reopened when the server drops it, which happens every few
|
|
1641
|
+
* minutes. Leaving the loop closes it. Session tokens expire after a few
|
|
1642
|
+
* hours, and this throws once reconnecting with an expired one fails.
|
|
1643
|
+
*
|
|
1644
|
+
* reMarkable authorizes the handshake with a header, which node and bun can
|
|
1645
|
+
* attach but a browser's `WebSocket` can't, so this is server side only.
|
|
1646
|
+
*
|
|
1647
|
+
* @example
|
|
1648
|
+
* ```ts
|
|
1649
|
+
* for await (const { attributes } of api.listen()) {
|
|
1650
|
+
* if (attributes.sourceDeviceID !== api.deviceId) {
|
|
1651
|
+
* const entries = await api.listItems(true);
|
|
1652
|
+
* }
|
|
1653
|
+
* }
|
|
1654
|
+
* ```
|
|
1655
|
+
*
|
|
1656
|
+
* @returns the notifications, in the order they arrive
|
|
1657
|
+
*/
|
|
1658
|
+
listen() {
|
|
1659
|
+
const stop = new AbortController();
|
|
1660
|
+
const events = this.#listen(stop.signal);
|
|
1661
|
+
const { return: finish } = events;
|
|
1662
|
+
// an async generator only acts on return once it reaches a yield, so
|
|
1663
|
+
// stopping has to unblock the loop and let it end on its own
|
|
1664
|
+
events.return = async (value) => {
|
|
1665
|
+
stop.abort();
|
|
1666
|
+
return await finish.call(events, value);
|
|
1667
|
+
};
|
|
1668
|
+
return events;
|
|
1669
|
+
}
|
|
1670
|
+
async *#listen(signal) {
|
|
1671
|
+
for (let failures = 0;;) {
|
|
1672
|
+
const socket = this.raw.notifications();
|
|
1673
|
+
const closeSocket = () => {
|
|
1674
|
+
socket.close();
|
|
1675
|
+
};
|
|
1676
|
+
signal.addEventListener("abort", closeSocket, { once: true });
|
|
1677
|
+
const queued = [];
|
|
1678
|
+
let arrived = () => { };
|
|
1679
|
+
// tells a failed handshake, likely an expired token, from the server's
|
|
1680
|
+
// routine drop of a healthy socket
|
|
1681
|
+
let opened = false;
|
|
1682
|
+
let done = false;
|
|
1683
|
+
socket.addEventListener("open", () => {
|
|
1684
|
+
opened = true;
|
|
1685
|
+
});
|
|
1686
|
+
socket.addEventListener("message", ({ data }) => {
|
|
1687
|
+
queued.push(data);
|
|
1688
|
+
arrived();
|
|
1689
|
+
});
|
|
1690
|
+
socket.addEventListener("error", () => {
|
|
1691
|
+
done = true;
|
|
1692
|
+
arrived();
|
|
1693
|
+
});
|
|
1694
|
+
socket.addEventListener("close", () => {
|
|
1695
|
+
done = true;
|
|
1696
|
+
arrived();
|
|
1697
|
+
});
|
|
1698
|
+
try {
|
|
1699
|
+
while (!done || queued.length) {
|
|
1700
|
+
const raw = queued.shift();
|
|
1701
|
+
if (raw === undefined) {
|
|
1702
|
+
await new Promise((res) => {
|
|
1703
|
+
arrived = res;
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
else {
|
|
1707
|
+
const { message } = notification.parse(JSON.parse(raw));
|
|
1708
|
+
// screenshare and passcode events share the socket, and their
|
|
1709
|
+
// attributes are shaped differently
|
|
1710
|
+
if (message.attributes.event === "SyncComplete") {
|
|
1711
|
+
yield syncEvent.parse(message);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
finally {
|
|
1717
|
+
signal.removeEventListener("abort", closeSocket);
|
|
1718
|
+
socket.close();
|
|
1719
|
+
}
|
|
1720
|
+
if (signal.aborted) {
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
else if (opened) {
|
|
1724
|
+
failures = 0;
|
|
1725
|
+
}
|
|
1726
|
+
else if (failures < this.#maxTransientRetries) {
|
|
1727
|
+
failures++;
|
|
1728
|
+
}
|
|
1729
|
+
else {
|
|
1730
|
+
throw new Error("couldn't open the reMarkable notification socket; the session token may have expired");
|
|
1731
|
+
}
|
|
1732
|
+
await sleep(backoffMs(failures, TRANSIENT_BASE_MS));
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1580
1735
|
/**
|
|
1581
1736
|
* get the current cache value as a string
|
|
1582
1737
|
*
|
|
@@ -1705,6 +1860,9 @@ export async function auth(deviceToken, { authHost = AUTH_HOST } = {}) {
|
|
|
1705
1860
|
* If requests start failing, simply recreate the api instance with a freshly
|
|
1706
1861
|
* fetched session token.
|
|
1707
1862
|
*
|
|
1863
|
+
* The device id is read out of the token, so this throws if it isn't one
|
|
1864
|
+
* reMarkable minted.
|
|
1865
|
+
*
|
|
1708
1866
|
* @param sessionToken - the session token used for authorization
|
|
1709
1867
|
* @returns an api instance
|
|
1710
1868
|
*/
|
package/dist/raw.d.ts
CHANGED
|
@@ -511,6 +511,7 @@ type AuthedFetch = (method: RequestMethod, url: string, init?: {
|
|
|
511
511
|
body?: string | Uint8Array;
|
|
512
512
|
headers?: Record<string, string>;
|
|
513
513
|
}) => Promise<Response>;
|
|
514
|
+
type AuthedSocket = (url: string) => WebSocket;
|
|
514
515
|
/**
|
|
515
516
|
* access to the low-level reMarkable api
|
|
516
517
|
*
|
|
@@ -576,7 +577,7 @@ type AuthedFetch = (method: RequestMethod, url: string, init?: {
|
|
|
576
577
|
*/
|
|
577
578
|
export declare class RawRemarkable {
|
|
578
579
|
#private;
|
|
579
|
-
constructor(authedFetch: AuthedFetch, cache: Map<string, Uint8Array | null>, rawHost: string, uploadHost: string, maxCachedBytes: number);
|
|
580
|
+
constructor(authedFetch: AuthedFetch, authedSocket: AuthedSocket, cache: Map<string, Uint8Array | null>, rawHost: string, uploadHost: string, maxCachedBytes: number);
|
|
580
581
|
/**
|
|
581
582
|
* gets the root hash and the current generation
|
|
582
583
|
*
|
|
@@ -778,6 +779,19 @@ export declare class RawRemarkable {
|
|
|
778
779
|
* @returns a simple entry with the id and hash of the uploaded file
|
|
779
780
|
*/
|
|
780
781
|
uploadFile(visibleName: string, bytes: Uint8Array, mime: UploadMimeType): Promise<ItemRef>;
|
|
782
|
+
/**
|
|
783
|
+
* open the websocket reMarkable pushes notifications to
|
|
784
|
+
*
|
|
785
|
+
* The server drops the socket every few minutes, so anything long lived has
|
|
786
|
+
* to reopen it. {@link RemarkableApi.listen | `listen`} does that, and parses
|
|
787
|
+
* the sync notifications out of what arrives.
|
|
788
|
+
*
|
|
789
|
+
* reMarkable authorizes the handshake with a header, which node and bun can
|
|
790
|
+
* attach but a browser's `WebSocket` can't, so this is server side only.
|
|
791
|
+
*
|
|
792
|
+
* @returns the connecting socket
|
|
793
|
+
*/
|
|
794
|
+
notifications(): WebSocket;
|
|
781
795
|
/**
|
|
782
796
|
* dump the current cache to a string to preserve between session
|
|
783
797
|
*
|