@gryt/core 0.28.0 → 0.29.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/import/discordImportJob.d.ts +2 -0
- package/dist/import/discordImportJob.js +32 -17
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mls/content.d.ts +10 -0
- package/dist/mls/content.js +20 -2
- package/package.json +1 -1
|
@@ -34,6 +34,8 @@ export interface ImportDeps {
|
|
|
34
34
|
newId: () => string;
|
|
35
35
|
/** Downloads and uploads running together. A phone holding each file in memory wants fewer. */
|
|
36
36
|
transfersAtOnce?: number;
|
|
37
|
+
/** Send a big import's text as one sealed upload. Only for apps that read `import_bundle`. */
|
|
38
|
+
bundles?: boolean;
|
|
37
39
|
}
|
|
38
40
|
export interface ImportProgress {
|
|
39
41
|
messages: {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { importBatches } from "../mls/content.js";
|
|
1
|
+
import { encodeImportBundle, importBatches } from "../mls/content.js";
|
|
2
2
|
const TRANSFERS_AT_ONCE = 16;
|
|
3
3
|
/** `mls:send` allows about one send every two seconds sustained (RL_SEND in the server). */
|
|
4
4
|
const SEND_GAP_MS = 2_000;
|
|
5
5
|
const FILE_BATCH = 20;
|
|
6
|
+
/* A batch holds up to 200, and one send is the point where a bundle starts saving time. */
|
|
7
|
+
const BUNDLE_ABOVE = 200;
|
|
6
8
|
const DOWNLOAD_RETRIES = 5;
|
|
7
9
|
/* A server allows 30 uploads a minute with a ban after, so a big import waits many times over. */
|
|
8
10
|
const UPLOAD_RETRIES = 200;
|
|
@@ -41,6 +43,32 @@ export async function runDiscordImport(messages, deps, control, report) {
|
|
|
41
43
|
while (control.paused() && !control.stopped())
|
|
42
44
|
await deps.sleep(500);
|
|
43
45
|
};
|
|
46
|
+
let uploadClearAt = 0;
|
|
47
|
+
const uploadFile = async (file) => {
|
|
48
|
+
for (let attempt = 0; attempt <= UPLOAD_RETRIES && !control.stopped(); attempt++) {
|
|
49
|
+
const wait = uploadClearAt - Date.now();
|
|
50
|
+
if (wait > 0)
|
|
51
|
+
await deps.sleep(wait);
|
|
52
|
+
const got = await deps.upload(file);
|
|
53
|
+
if (!("retryAfterMs" in got))
|
|
54
|
+
return got;
|
|
55
|
+
uploadClearAt = Math.max(uploadClearAt, Date.now() + Math.min(got.retryAfterMs, MAX_RATE_LIMIT_WAIT_MS));
|
|
56
|
+
}
|
|
57
|
+
if (control.stopped())
|
|
58
|
+
return null;
|
|
59
|
+
throw new Error("The server kept refusing uploads.");
|
|
60
|
+
};
|
|
61
|
+
/* False when the bundle can't go up, so the batches carry it. */
|
|
62
|
+
const sendBundle = async (entries) => {
|
|
63
|
+
await hold();
|
|
64
|
+
const up = await uploadFile({ name: "discord-import.json", type: "application/json", bytes: encodeImportBundle(entries) });
|
|
65
|
+
if (!up || "tooLarge" in up)
|
|
66
|
+
return false;
|
|
67
|
+
await deps.send({ type: "import_bundle", id: deps.newId(), source: "discord", attachments: { [up.id]: up.key } });
|
|
68
|
+
p.messages.done += entries.length;
|
|
69
|
+
tick();
|
|
70
|
+
return true;
|
|
71
|
+
};
|
|
44
72
|
const sendAll = async (entries) => {
|
|
45
73
|
for (const batch of importBatches(entries, deps.newId)) {
|
|
46
74
|
await hold();
|
|
@@ -52,7 +80,9 @@ export async function runDiscordImport(messages, deps, control, report) {
|
|
|
52
80
|
await deps.sleep(SEND_GAP_MS);
|
|
53
81
|
}
|
|
54
82
|
};
|
|
55
|
-
|
|
83
|
+
const textOnly = messages.filter((m) => m.attachments.length === 0).map((m) => ({ id: importId(m.discordId), sentAt: m.sentAt, text: m.text }));
|
|
84
|
+
if (!(deps.bundles && textOnly.length > BUNDLE_ABOVE && !control.stopped() && (await sendBundle(textOnly))))
|
|
85
|
+
await sendAll(textOnly);
|
|
56
86
|
const missing = new Set();
|
|
57
87
|
let clearAt = 0;
|
|
58
88
|
const downloadFile = async (url) => {
|
|
@@ -96,21 +126,6 @@ export async function runDiscordImport(messages, deps, control, report) {
|
|
|
96
126
|
});
|
|
97
127
|
const uploaded = new Map();
|
|
98
128
|
const tooLarge = new Set();
|
|
99
|
-
let uploadClearAt = 0;
|
|
100
|
-
const uploadFile = async (file) => {
|
|
101
|
-
for (let attempt = 0; attempt <= UPLOAD_RETRIES && !control.stopped(); attempt++) {
|
|
102
|
-
const wait = uploadClearAt - Date.now();
|
|
103
|
-
if (wait > 0)
|
|
104
|
-
await deps.sleep(wait);
|
|
105
|
-
const got = await deps.upload(file);
|
|
106
|
-
if (!("retryAfterMs" in got))
|
|
107
|
-
return got;
|
|
108
|
-
uploadClearAt = Math.max(uploadClearAt, Date.now() + Math.min(got.retryAfterMs, MAX_RATE_LIMIT_WAIT_MS));
|
|
109
|
-
}
|
|
110
|
-
if (control.stopped())
|
|
111
|
-
return null;
|
|
112
|
-
throw new Error("The server kept refusing uploads.");
|
|
113
|
-
};
|
|
114
129
|
await eachAtOnce(async (url) => {
|
|
115
130
|
const stored = missing.has(url) ? null : await deps.store.take(url);
|
|
116
131
|
if (stored) {
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { agreementAt, createTermsGate, GUIDELINES_URL, TERMS_STORAGE_KEY, TERMS_
|
|
|
13
13
|
export { createMlsDmDriver, MlsDriverError, type MlsDriverErrorCode } from "./mls/dmDriver.js";
|
|
14
14
|
export { mlsPinsFromPeerPins } from "./mls/pins.js";
|
|
15
15
|
export { reencryptedId, reencryptMine, type ReencryptCandidate, type ReencryptDeps } from "./mls/reencrypt.js";
|
|
16
|
-
export { decodeMlsDmContent, encodeMlsDmContent, acceptImport, historyBatches, importBatches, MAX_HISTORY_BATCH_BYTES, MLS_DM_CONTENT_VERSION, type MlsDmContent, type MlsHistoryEntry, type MlsHistoryReaction, type MlsImportEntry, readMlsDmContent, } from "./mls/content.js";
|
|
16
|
+
export { decodeMlsDmContent, encodeImportBundle, readImportBundle, encodeMlsDmContent, acceptImport, historyBatches, importBatches, MAX_HISTORY_BATCH_BYTES, MLS_DM_CONTENT_VERSION, type MlsDmContent, type MlsHistoryEntry, type MlsHistoryReaction, type MlsImportEntry, readMlsDmContent, } from "./mls/content.js";
|
|
17
17
|
export { applyMlsReaction, mlsReactionAction, type MlsReaction } from "./mls/reactions.js";
|
|
18
18
|
export { type DiscordPackageDm, type DiscordPackageMessage, isDiscordPackage, linksExpireAt, listDiscordDms, type PackageFiles, readDiscordDm, } from "./import/discordPackage.js";
|
|
19
19
|
export { inflateRaw, openZipBytes } from "./import/unzip.js";
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export { agreementAt, createTermsGate, GUIDELINES_URL, TERMS_STORAGE_KEY, TERMS_
|
|
|
15
15
|
export { createMlsDmDriver, MlsDriverError } from "./mls/dmDriver.js";
|
|
16
16
|
export { mlsPinsFromPeerPins } from "./mls/pins.js";
|
|
17
17
|
export { reencryptedId, reencryptMine } from "./mls/reencrypt.js";
|
|
18
|
-
export { decodeMlsDmContent, encodeMlsDmContent, acceptImport, historyBatches, importBatches, MAX_HISTORY_BATCH_BYTES, MLS_DM_CONTENT_VERSION, readMlsDmContent, } from "./mls/content.js";
|
|
18
|
+
export { decodeMlsDmContent, encodeImportBundle, readImportBundle, encodeMlsDmContent, acceptImport, historyBatches, importBatches, MAX_HISTORY_BATCH_BYTES, MLS_DM_CONTENT_VERSION, readMlsDmContent, } from "./mls/content.js";
|
|
19
19
|
export { applyMlsReaction, mlsReactionAction } from "./mls/reactions.js";
|
|
20
20
|
export { isDiscordPackage, linksExpireAt, listDiscordDms, readDiscordDm, } from "./import/discordPackage.js";
|
|
21
21
|
export { inflateRaw, openZipBytes } from "./import/unzip.js";
|
package/dist/mls/content.d.ts
CHANGED
|
@@ -30,6 +30,13 @@ export type MlsDmContent = {
|
|
|
30
30
|
id: string;
|
|
31
31
|
source: "discord";
|
|
32
32
|
entries: MlsImportEntry[];
|
|
33
|
+
} | {
|
|
34
|
+
/** An import too big for messages: its entries sit in one sealed upload, read with `readImportBundle`. */
|
|
35
|
+
type: "import_bundle";
|
|
36
|
+
id: string;
|
|
37
|
+
source: "discord";
|
|
38
|
+
/** Upload id to its key. Exactly one. */
|
|
39
|
+
attachments: Record<string, SealedAttachmentKey>;
|
|
33
40
|
} | {
|
|
34
41
|
type: "history";
|
|
35
42
|
/** For the batch itself, so a repeat is the same message. */
|
|
@@ -82,6 +89,9 @@ export declare function acceptImport(content: Extract<MlsDmContent, {
|
|
|
82
89
|
senderId: string;
|
|
83
90
|
at: number;
|
|
84
91
|
}): MlsHistoryEntry[];
|
|
92
|
+
export declare function encodeImportBundle(entries: MlsImportEntry[]): Uint8Array;
|
|
93
|
+
/** A bundle's entries, or null for junk. A peer wrote it, so each entry is checked like a message's. */
|
|
94
|
+
export declare function readImportBundle(bytes: Uint8Array): MlsImportEntry[] | null;
|
|
85
95
|
/** Null for anything this version can't read: a newer version, a new type, or junk. */
|
|
86
96
|
export declare function decodeMlsDmContent(bytes: Uint8Array): MlsDmContent | null;
|
|
87
97
|
/**
|
package/dist/mls/content.js
CHANGED
|
@@ -84,7 +84,10 @@ function readAttachments(v) {
|
|
|
84
84
|
/** The same bytes the phone writes: `v` first, then the content's fields in this order. */
|
|
85
85
|
export function encodeMlsDmContent(content) {
|
|
86
86
|
let body;
|
|
87
|
-
if (content.type === "
|
|
87
|
+
if (content.type === "import_bundle") {
|
|
88
|
+
body = { type: content.type, id: content.id, source: content.source, attachments: content.attachments };
|
|
89
|
+
}
|
|
90
|
+
else if (content.type === "history") {
|
|
88
91
|
body = { type: content.type, id: content.id, entries: content.entries.map(historyBody) };
|
|
89
92
|
}
|
|
90
93
|
else if (content.type === "import") {
|
|
@@ -213,6 +216,17 @@ function readImportEntry(v) {
|
|
|
213
216
|
}
|
|
214
217
|
return out;
|
|
215
218
|
}
|
|
219
|
+
export function encodeImportBundle(entries) {
|
|
220
|
+
return new TextEncoder().encode(JSON.stringify({ v: MLS_DM_CONTENT_VERSION, entries: entries.map(importBody) }));
|
|
221
|
+
}
|
|
222
|
+
/** A bundle's entries, or null for junk. A peer wrote it, so each entry is checked like a message's. */
|
|
223
|
+
export function readImportBundle(bytes) {
|
|
224
|
+
const raw = parse(bytes);
|
|
225
|
+
if (!isObject(raw) || raw.v !== MLS_DM_CONTENT_VERSION || !Array.isArray(raw.entries))
|
|
226
|
+
return null;
|
|
227
|
+
const entries = raw.entries.map(readImportEntry);
|
|
228
|
+
return entries.every((e) => e !== null) ? entries : null;
|
|
229
|
+
}
|
|
216
230
|
/** Null for anything this version can't read: a newer version, a new type, or junk. */
|
|
217
231
|
export function decodeMlsDmContent(bytes) {
|
|
218
232
|
return fromRaw(parse(bytes));
|
|
@@ -225,7 +239,7 @@ export function readMlsDmContent(bytes) {
|
|
|
225
239
|
const raw = parse(bytes);
|
|
226
240
|
return fromRaw(raw) ?? (isNewer(raw) ? "newer" : null);
|
|
227
241
|
}
|
|
228
|
-
const KNOWN_TYPES = new Set(["message", "edit", "delete", "reaction", "history", "import"]);
|
|
242
|
+
const KNOWN_TYPES = new Set(["message", "edit", "delete", "reaction", "history", "import", "import_bundle"]);
|
|
229
243
|
const TYPE_NAME = /^[a-z][a-z0-9_]{0,31}$/;
|
|
230
244
|
/** A later version, or a type this one doesn't know that names its message the way ours do. */
|
|
231
245
|
function isNewer(raw) {
|
|
@@ -259,6 +273,10 @@ function fromRaw(raw) {
|
|
|
259
273
|
const entries = raw.entries.map(readImportEntry);
|
|
260
274
|
return entries.every((e) => e !== null) ? { type, id, source: raw.source, entries } : null;
|
|
261
275
|
}
|
|
276
|
+
if (type === "import_bundle") {
|
|
277
|
+
const attachments = raw.source === "discord" ? readAttachments(raw.attachments) : null;
|
|
278
|
+
return attachments && Object.keys(attachments).length === 1 ? { type, id, source: "discord", attachments } : null;
|
|
279
|
+
}
|
|
262
280
|
if (type === "message" && isText(text)) {
|
|
263
281
|
const out = { type, id, text };
|
|
264
282
|
if (raw.replyTo !== undefined) {
|
package/package.json
CHANGED