@gryt/core 0.29.0 → 0.30.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.
@@ -36,6 +36,10 @@ export interface ImportDeps {
36
36
  transfersAtOnce?: number;
37
37
  /** Send a big import's text as one sealed upload. Only for apps that read `import_bundle`. */
38
38
  bundles?: boolean;
39
+ /** Whether this device's history already holds an entry, so a second run skips it. */
40
+ alreadyHas?: (entryId: string) => Promise<boolean>;
41
+ /** The server's file size limit, so a file over it is never sealed and sent to be refused. */
42
+ maxFileBytes?: number | null;
39
43
  }
40
44
  export interface ImportProgress {
41
45
  messages: {
@@ -63,5 +67,5 @@ export interface ImportControl {
63
67
  /** For a 429's Retry-After header. Only the seconds form: anything else gets the default wait. */
64
68
  export declare function retryAfterMs(header: string | null): number;
65
69
  export declare const importId: (discordId: string) => string;
66
- /** Messages without files first, then every file downloaded to the store, then the messages that carry files. */
70
+ /** Text first, then 20 messages at a time: their files go straight through, then those messages go. */
67
71
  export declare function runDiscordImport(messages: DiscordPackageMessage[], deps: ImportDeps, control: ImportControl, report: (p: ImportProgress) => void): Promise<ImportProgress>;
@@ -24,12 +24,16 @@ function nameOf(url) {
24
24
  return "file";
25
25
  }
26
26
  }
27
- /** Messages without files first, then every file downloaded to the store, then the messages that carry files. */
27
+ /** Text first, then 20 messages at a time: their files go straight through, then those messages go. */
28
28
  export async function runDiscordImport(messages, deps, control, report) {
29
- const withFiles = messages.filter((m) => m.attachments.length > 0);
29
+ const fresh = [];
30
+ for (const m of messages)
31
+ if (!(await deps.alreadyHas?.(importId(m.discordId))))
32
+ fresh.push(m);
33
+ const withFiles = fresh.filter((m) => m.attachments.length > 0);
30
34
  const urls = [...new Set(withFiles.flatMap((m) => m.attachments))];
31
35
  const p = {
32
- messages: { done: 0, total: messages.length },
36
+ messages: { done: messages.length - fresh.length, total: messages.length },
33
37
  downloaded: { done: 0, total: urls.length },
34
38
  sent: { done: 0, total: urls.length },
35
39
  missing: 0,
@@ -80,7 +84,7 @@ export async function runDiscordImport(messages, deps, control, report) {
80
84
  await deps.sleep(SEND_GAP_MS);
81
85
  }
82
86
  };
83
- const textOnly = messages.filter((m) => m.attachments.length === 0).map((m) => ({ id: importId(m.discordId), sentAt: m.sentAt, text: m.text }));
87
+ const textOnly = fresh.filter((m) => m.attachments.length === 0).map((m) => ({ id: importId(m.discordId), sentAt: m.sentAt, text: m.text }));
84
88
  if (!(deps.bundles && textOnly.length > BUNDLE_ABOVE && !control.stopped() && (await sendBundle(textOnly))))
85
89
  await sendAll(textOnly);
86
90
  const missing = new Set();
@@ -98,74 +102,85 @@ export async function runDiscordImport(messages, deps, control, report) {
98
102
  }
99
103
  return null;
100
104
  };
101
- const eachAtOnce = async (run) => {
102
- let next = 0;
103
- await Promise.all(Array.from({ length: deps.transfersAtOnce ?? TRANSFERS_AT_ONCE }, async () => {
104
- while (!control.stopped()) {
105
- await hold();
106
- if (next >= urls.length)
107
- break;
108
- await run(urls[next++]);
109
- }
110
- }));
111
- };
112
- await eachAtOnce(async (url) => {
113
- if (!(await deps.store.has(url))) {
114
- const file = await downloadFile(url);
115
- if (file) {
105
+ const uploaded = new Map();
106
+ const tooLarge = new Set();
107
+ const finished = new Set();
108
+ /* The file is only kept if it can't go up now, so a stop or a failure never downloads it twice. */
109
+ const transfer = async (url) => {
110
+ if (finished.has(url))
111
+ return;
112
+ finished.add(url);
113
+ let file = await deps.store.take(url);
114
+ if (!file) {
115
+ file = await downloadFile(url);
116
+ if (file)
116
117
  p.bytes += file.bytes.length;
117
- await deps.store.put(url, file);
118
- }
119
- else {
118
+ else if (!control.stopped()) {
120
119
  missing.add(url);
121
120
  p.missing = missing.size;
122
121
  }
123
122
  }
123
+ if (control.stopped()) {
124
+ if (file)
125
+ await deps.store.put(url, file);
126
+ return;
127
+ }
124
128
  p.downloaded.done++;
125
129
  tick();
126
- });
127
- const uploaded = new Map();
128
- const tooLarge = new Set();
129
- await eachAtOnce(async (url) => {
130
- const stored = missing.has(url) ? null : await deps.store.take(url);
131
- if (stored) {
132
- const up = await uploadFile({ name: nameOf(url), ...stored });
133
- if (up && "tooLarge" in up) {
130
+ if (file) {
131
+ let up;
132
+ try {
133
+ up = deps.maxFileBytes && file.bytes.length > deps.maxFileBytes ? { tooLarge: true } : await uploadFile({ name: nameOf(url), ...file });
134
+ }
135
+ catch (e) {
136
+ await deps.store.put(url, file);
137
+ throw e;
138
+ }
139
+ if (!up) {
140
+ await deps.store.put(url, file);
141
+ return;
142
+ }
143
+ if ("tooLarge" in up) {
134
144
  tooLarge.add(url);
135
145
  p.tooLarge = tooLarge.size;
136
146
  }
137
- else if (up) {
147
+ else
138
148
  uploaded.set(url, up);
139
- }
140
- if (up)
141
- await deps.store.drop(url);
149
+ await deps.store.drop(url);
142
150
  }
143
151
  p.sent.done++;
144
152
  tick();
145
- });
146
- let pending = [];
147
- for (const m of withFiles) {
153
+ };
154
+ const atOnce = deps.transfersAtOnce ?? TRANSFERS_AT_ONCE;
155
+ for (let i = 0; i < withFiles.length && !control.stopped(); i += FILE_BATCH) {
156
+ const chunk = withFiles.slice(i, i + FILE_BATCH);
157
+ const todo = [...new Set(chunk.flatMap((m) => m.attachments))];
158
+ let next = 0;
159
+ await Promise.all(Array.from({ length: atOnce }, async () => {
160
+ while (!control.stopped()) {
161
+ await hold();
162
+ if (next >= todo.length)
163
+ break;
164
+ await transfer(todo[next++]);
165
+ }
166
+ }));
148
167
  if (control.stopped())
149
168
  break;
150
- const attachments = {};
151
- for (const url of m.attachments) {
152
- const up = uploaded.get(url);
153
- if (up)
154
- attachments[up.id] = up.key;
155
- }
156
- const big = m.attachments.filter((u) => tooLarge.has(u)).length;
157
- const lost = m.attachments.length - Object.keys(attachments).length - big;
158
- const lostNote = lost ? `${lost === 1 ? "A file" : `${lost} files`} from Discord couldn’t be downloaded.` : "";
159
- const bigNote = big ? `${big === 1 ? "A file" : `${big} files`} from Discord ${big === 1 ? "was" : "were"} too big for this server.` : "";
160
- const text = [m.text, lostNote, bigNote].filter(Boolean).join("\n");
161
- pending.push({ id: importId(m.discordId), sentAt: m.sentAt, text, ...(Object.keys(attachments).length ? { attachments } : {}) });
162
- if (pending.length >= FILE_BATCH) {
163
- await sendAll(pending);
164
- pending = [];
165
- }
169
+ await sendAll(chunk.map((m) => {
170
+ const attachments = {};
171
+ for (const url of m.attachments) {
172
+ const up = uploaded.get(url);
173
+ if (up)
174
+ attachments[up.id] = up.key;
175
+ }
176
+ const big = m.attachments.filter((u) => tooLarge.has(u)).length;
177
+ const lost = m.attachments.length - Object.keys(attachments).length - big;
178
+ const lostNote = lost ? `${lost === 1 ? "A file" : `${lost} files`} from Discord couldn’t be downloaded.` : "";
179
+ const bigNote = big ? `${big === 1 ? "A file" : `${big} files`} from Discord ${big === 1 ? "was" : "were"} too big for this server.` : "";
180
+ const text = [m.text, lostNote, bigNote].filter(Boolean).join("\n");
181
+ return { id: importId(m.discordId), sentAt: m.sentAt, text, ...(Object.keys(attachments).length ? { attachments } : {}) };
182
+ }));
166
183
  }
167
- if (pending.length)
168
- await sendAll(pending);
169
184
  tick();
170
185
  return p;
171
186
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gryt/core",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
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": {