@gryt/core 0.31.0 → 0.32.1

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.
@@ -41,6 +41,14 @@ export interface ImportDeps {
41
41
  /** The server's file size limit, so a file over it is never sealed and sent to be refused. */
42
42
  maxFileBytes?: number | null;
43
43
  }
44
+ /** A file that didn't come over, for the receipt. */
45
+ export interface ImportLeftOut {
46
+ url: string;
47
+ name: string;
48
+ /** Bytes, when it was downloaded. */
49
+ size: number | null;
50
+ reason: "too_large" | "not_downloaded";
51
+ }
44
52
  export interface ImportProgress {
45
53
  messages: {
46
54
  done: number;
@@ -58,6 +66,7 @@ export interface ImportProgress {
58
66
  /** Files this server won't take, each left out with a note on its message. */
59
67
  tooLarge: number;
60
68
  bytes: number;
69
+ leftOut: ImportLeftOut[];
61
70
  startedAt: number;
62
71
  }
63
72
  export interface ImportControl {
@@ -67,5 +76,10 @@ export interface ImportControl {
67
76
  /** For a 429's Retry-After header. Only the seconds form: anything else gets the default wait. */
68
77
  export declare function retryAfterMs(header: string | null): number;
69
78
  export declare const importId: (discordId: string) => string;
79
+ /**
80
+ * Another go at files left out: new entries beside the originals, holding only those files.
81
+ * A first run's message is already in history, and a repeat of its id is skipped there.
82
+ */
83
+ export declare function retryLeftOut(messages: DiscordPackageMessage[], urls: ReadonlySet<string>): DiscordPackageMessage[];
70
84
  /** Text first, then 20 messages at a time: their files go straight through, then those messages go. */
71
85
  export declare function runDiscordImport(messages: DiscordPackageMessage[], deps: ImportDeps, control: ImportControl, report: (p: ImportProgress) => void): Promise<ImportProgress>;
@@ -24,6 +24,16 @@ function nameOf(url) {
24
24
  return "file";
25
25
  }
26
26
  }
27
+ /**
28
+ * Another go at files left out: new entries beside the originals, holding only those files.
29
+ * A first run's message is already in history, and a repeat of its id is skipped there.
30
+ */
31
+ export function retryLeftOut(messages, urls) {
32
+ return messages.flatMap((m) => {
33
+ const attachments = m.attachments.filter((u) => urls.has(u));
34
+ return attachments.length ? [{ ...m, discordId: `${m.discordId}-files`, text: "", attachments }] : [];
35
+ });
36
+ }
27
37
  /** Text first, then 20 messages at a time: their files go straight through, then those messages go. */
28
38
  export async function runDiscordImport(messages, deps, control, report) {
29
39
  const fresh = [];
@@ -39,9 +49,10 @@ export async function runDiscordImport(messages, deps, control, report) {
39
49
  missing: 0,
40
50
  tooLarge: 0,
41
51
  bytes: 0,
52
+ leftOut: [],
42
53
  startedAt: Date.now(),
43
54
  };
44
- const tick = () => report({ ...p, messages: { ...p.messages }, downloaded: { ...p.downloaded }, sent: { ...p.sent } });
55
+ const tick = () => report({ ...p, leftOut: [...p.leftOut], messages: { ...p.messages }, downloaded: { ...p.downloaded }, sent: { ...p.sent } });
45
56
  tick();
46
57
  const hold = async () => {
47
58
  while (control.paused() && !control.stopped())
@@ -118,6 +129,7 @@ export async function runDiscordImport(messages, deps, control, report) {
118
129
  else if (!control.stopped()) {
119
130
  missing.add(url);
120
131
  p.missing = missing.size;
132
+ p.leftOut.push({ url, name: nameOf(url), size: null, reason: "not_downloaded" });
121
133
  }
122
134
  }
123
135
  if (control.stopped()) {
@@ -143,6 +155,7 @@ export async function runDiscordImport(messages, deps, control, report) {
143
155
  if ("tooLarge" in up) {
144
156
  tooLarge.add(url);
145
157
  p.tooLarge = tooLarge.size;
158
+ p.leftOut.push({ url, name: nameOf(url), size: file.bytes.length, reason: "too_large" });
146
159
  }
147
160
  else
148
161
  uploaded.set(url, up);
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@ export { decodeMlsDmContent, encodeImportBundle, readImportBundle, encodeMlsDmCo
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";
20
- export { type ImportControl, type ImportDeps, type ImportFile, type ImportFileStore, importId, type ImportProgress, retryAfterMs, runDiscordImport, } from "./import/discordImportJob.js";
20
+ export { type ImportControl, type ImportDeps, type ImportFile, type ImportFileStore, importId, type ImportProgress, retryAfterMs, retryLeftOut, runDiscordImport, type ImportLeftOut, } from "./import/discordImportJob.js";
21
21
  export type { DmSealingMode, MlsAddOwnDeviceOptions, MlsClaimedKeyPackage, MlsDecryptedMessage, MlsDeviceRecord, MlsDeviceRef, MlsDmDriver, MlsDmDriverOptions, MlsDmEvents, MlsGroupPosition, MlsGroupRecord, MlsGroupView, MlsKeyPackageRecord, MlsListedDevice, MlsLogEntry, MlsDmDiagnostics, MlsOwnDevice, MlsOwnDeviceAdd, MlsPins, MlsRefusal, MlsReply, MlsServerCapability, MlsStateStore, MlsTransport, MlsWelcomeDelivery, } from "./mls/interfaces.js";
22
22
  export { createApproverPairing, type ApproverOptions, type ApproverPairing, type ApproverState, } from "./pairing/approver.js";
23
23
  export { createNewDevicePairing, PAIRING_APPROVAL_MS, type NewDeviceOptions, type NewDevicePairing, type NewDeviceState, } from "./pairing/newDevice.js";
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ export { decodeMlsDmContent, encodeImportBundle, readImportBundle, encodeMlsDmCo
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";
22
- export { importId, retryAfterMs, runDiscordImport, } from "./import/discordImportJob.js";
22
+ export { importId, retryAfterMs, retryLeftOut, runDiscordImport, } from "./import/discordImportJob.js";
23
23
  export { createApproverPairing, } from "./pairing/approver.js";
24
24
  export { createNewDevicePairing, PAIRING_APPROVAL_MS, } from "./pairing/newDevice.js";
25
25
  export { createPairingRelay, PairingRelayError } from "./pairing/relayClient.js";
@@ -2,6 +2,7 @@ import { addMlsMembers, asIdentityScope, base64UrlDecode, createMlsGroup, decode
2
2
  /** Devices per person per server. Must match the server's MLS_MAX_DEVICES, or extra devices are left out quietly. */
3
3
  const MLS_MAX_DEVICES_PER_PERSON = 100;
4
4
  const COMMIT_ATTEMPTS = 5;
5
+ const OWN_DEVICE_ADDS_AT_ONCE = 4;
5
6
  /** socket.io closes the connection on a packet with more than 10 binary parts (GRYT-1522). */
6
7
  const MAX_BINARY_PARTS = 10;
7
8
  const LOG_PAGE = MAX_BINARY_PARTS;
@@ -1072,21 +1073,29 @@ export function createMlsDmDriver(options) {
1072
1073
  const ids = (await store.listGroups()).map((r) => r.conversationId);
1073
1074
  const first = (options.order ?? []).filter((id) => ids.includes(id));
1074
1075
  const ordered = [...new Set([...first, ...ids])];
1075
- const results = [];
1076
- for (const conversationId of ordered) {
1077
- let result;
1078
- try {
1079
- result = await run(conversationId, () => addOwnDeviceTo(conversationId, deviceId, ownPersonKey));
1080
- }
1081
- catch (e) {
1082
- if (e instanceof MlsDriverError && e.code === "not_own_device")
1083
- throw e;
1084
- const groupId = (await store.loadGroup(conversationId))?.groupId ?? "";
1085
- result = { conversationId, groupId, outcome: "failed", add: null, error: e instanceof MlsDriverError ? e.code : "error" };
1076
+ const results = new Array(ordered.length);
1077
+ let done = 0;
1078
+ let next = 0;
1079
+ // Each group is its own commit, so a few go at once; `order` still decides which start first.
1080
+ const worker = async () => {
1081
+ while (next < ordered.length) {
1082
+ const i = next++;
1083
+ const conversationId = ordered[i];
1084
+ let result;
1085
+ try {
1086
+ result = await run(conversationId, () => addOwnDeviceTo(conversationId, deviceId, ownPersonKey));
1087
+ }
1088
+ catch (e) {
1089
+ if (e instanceof MlsDriverError && e.code === "not_own_device")
1090
+ throw e;
1091
+ const groupId = (await store.loadGroup(conversationId))?.groupId ?? "";
1092
+ result = { conversationId, groupId, outcome: "failed", add: null, error: e instanceof MlsDriverError ? e.code : "error" };
1093
+ }
1094
+ results[i] = result;
1095
+ options.onProgress?.({ done: ++done, total: ordered.length, result });
1086
1096
  }
1087
- results.push(result);
1088
- options.onProgress?.({ done: results.length, total: ordered.length, result });
1089
- }
1097
+ };
1098
+ await Promise.all(Array.from({ length: Math.min(OWN_DEVICE_ADDS_AT_ONCE, ordered.length) }, worker));
1090
1099
  return results;
1091
1100
  }
1092
1101
  return {
@@ -142,16 +142,30 @@ export function createApproverPairing(options) {
142
142
  }
143
143
  async function addEverywhere(ready) {
144
144
  const added = {};
145
- for (const { host, deviceId } of ready) {
145
+ // Servers go at once, so progress is the sum over them.
146
+ const counts = new Map();
147
+ const report = (host) => {
148
+ let done = 0;
149
+ let total = 0;
150
+ for (const c of counts.values()) {
151
+ done += c.done;
152
+ total += c.total;
153
+ }
154
+ set({ phase: "adding", device: confirmed().device, host, done, total });
155
+ };
156
+ await Promise.all(ready.map(async ({ host, deviceId }) => {
146
157
  const driver = devices(host);
147
158
  if (!driver)
148
- continue;
159
+ return;
149
160
  for (let tries = 0;; tries++) {
150
161
  try {
151
162
  added[host] = await driver.addOwnDevice(deviceId, {
152
- onProgress: ({ done, total }) => set({ phase: "adding", device: confirmed().device, host, done, total }),
163
+ onProgress: ({ done, total }) => {
164
+ counts.set(host, { done, total });
165
+ report(host);
166
+ },
153
167
  });
154
- break;
168
+ return;
155
169
  }
156
170
  catch (e) {
157
171
  // The server lists N's device once its KeyPackages are in, which can trail N's "ready".
@@ -160,7 +174,7 @@ export function createApproverPairing(options) {
160
174
  await clock.sleep(2000, abort.signal);
161
175
  }
162
176
  }
163
- }
177
+ }));
164
178
  return added;
165
179
  }
166
180
  /** Each group's cursor on every server in the envelope: the snapshot covers up to there. */
@@ -234,7 +234,14 @@ export function createNewDevicePairing(options) {
234
234
  throw new Error("Not waiting to join.");
235
235
  const { from } = state;
236
236
  const body = sealJson(current.session, { t: "ready", devices: devices.map(({ host, deviceId }) => ({ host, deviceId })) });
237
- await relay.post(current.id, current.token, { type: "sealed", body });
237
+ try {
238
+ await relay.post(current.id, current.token, { type: "sealed", body });
239
+ }
240
+ catch (e) {
241
+ // A closed before N got here: say so, rather than sit on "joining" with nothing listening.
242
+ await end(reasonFor(e));
243
+ throw e;
244
+ }
238
245
  set({ phase: "linked", from });
239
246
  void linked(from);
240
247
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gryt/core",
3
- "version": "0.31.0",
3
+ "version": "0.32.1",
4
4
  "description": "Gryt's shared application logic: one implementation of the things the desktop and mobile apps both do.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {