@marktiderman/genesis-chat 0.1.1 → 0.2.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 +304 -13
- package/dist/{chunk-SAY24E6V.js → chunk-5D5A7I4Q.js} +166 -45
- package/dist/chunk-5D5A7I4Q.js.map +1 -0
- package/dist/{chunk-JQXZUEEL.js → chunk-7S6HYNWW.js} +382 -8
- package/dist/chunk-7S6HYNWW.js.map +1 -0
- package/dist/chunk-BJCE5O7V.js +70 -0
- package/dist/chunk-BJCE5O7V.js.map +1 -0
- package/dist/chunk-PZTOY2LY.js +576 -0
- package/dist/chunk-PZTOY2LY.js.map +1 -0
- package/dist/chunk-VEPZROQB.js +187 -0
- package/dist/chunk-VEPZROQB.js.map +1 -0
- package/dist/{client-BtdALFCx.d.ts → client-DMa2j7kz.d.ts} +152 -11
- package/dist/index.d.ts +78 -163
- package/dist/index.js +3 -2
- package/dist/native.d.ts +1910 -1
- package/dist/native.js +4900 -0
- package/dist/native.js.map +1 -1
- package/dist/outbox-B0y3phE0.d.ts +151 -0
- package/dist/push.d.ts +206 -0
- package/dist/push.js +3 -0
- package/dist/push.js.map +1 -0
- package/dist/react.d.ts +149 -7
- package/dist/react.js +3 -3
- package/dist/server.d.ts +510 -21
- package/dist/server.js +863 -21
- package/dist/server.js.map +1 -1
- package/dist/thread-DWXHKPdw.d.ts +57 -0
- package/dist/web.d.ts +313 -10
- package/dist/web.js +1731 -60
- package/dist/web.js.map +1 -1
- package/migrations/0002_genesis_chat_coaching.sql +934 -0
- package/migrations/tests/0002_genesis_chat_coaching.test.sql +265 -0
- package/package.json +74 -6
- package/src/__tests__/coaching-core.test.ts +332 -0
- package/src/__tests__/coaching-react.test.tsx +248 -0
- package/src/__tests__/coaching-web.test.tsx +449 -0
- package/src/__tests__/core.test.ts +91 -0
- package/src/__tests__/outbox.test.ts +122 -0
- package/src/__tests__/realtime.test.ts +70 -0
- package/src/__tests__/review-fixes-core.test.ts +193 -0
- package/src/__tests__/review-fixes-web.test.tsx +402 -0
- package/src/__tests__/server-effects.test.ts +158 -0
- package/src/__tests__/server-fakes.ts +90 -0
- package/src/__tests__/server-integrations.test.ts +247 -0
- package/src/__tests__/server-providers.test.ts +246 -0
- package/src/__tests__/server-runner.test.ts +277 -0
- package/src/__tests__/server-safety.test.ts +290 -0
- package/src/__tests__/web-markdown.test.tsx +88 -0
- package/src/__tests__/web.test.tsx +333 -0
- package/src/core/availability.ts +223 -0
- package/src/core/client.ts +757 -0
- package/src/core/extras.ts +106 -0
- package/src/core/media.ts +144 -0
- package/src/core/outbox.ts +429 -0
- package/src/core/thread.ts +171 -0
- package/src/core/types.ts +311 -0
- package/src/index.ts +13 -0
- package/src/native/__tests__/auto-retry.test.tsx +128 -0
- package/src/native/__tests__/composer-mic-lock.test.tsx +115 -0
- package/src/native/__tests__/composer-state.test.ts +188 -0
- package/src/native/__tests__/composer.test.tsx +133 -0
- package/src/native/__tests__/emoji.test.tsx +238 -0
- package/src/native/__tests__/inbox-screen.test.tsx +48 -0
- package/src/native/__tests__/message-bubble.test.tsx +127 -0
- package/src/native/__tests__/organise-logic.test.ts +164 -0
- package/src/native/__tests__/organise-screens.test.tsx +344 -0
- package/src/native/__tests__/photo-viewer.test.tsx +226 -0
- package/src/native/__tests__/push-payload.test.ts +66 -0
- package/src/native/__tests__/push.test.ts +209 -0
- package/src/native/__tests__/recorder-machine.test.ts +96 -0
- package/src/native/__tests__/rows-actions.test.ts +76 -0
- package/src/native/__tests__/thread-layout.test.ts +41 -0
- package/src/native/__tests__/thread-screen.test.tsx +116 -0
- package/src/native/__tests__/tus.test.ts +154 -0
- package/src/native/__tests__/use-voice-recorder.test.tsx +316 -0
- package/src/native/__tests__/video-media.test.tsx +421 -0
- package/src/native/__tests__/video-recorder-state.test.ts +87 -0
- package/src/native/__tests__/video-upload.test.ts +227 -0
- package/src/native/access-error.tsx +31 -0
- package/src/native/actions.ts +42 -0
- package/src/native/attachment-cards.tsx +116 -0
- package/src/native/auto-retry.ts +69 -0
- package/src/native/availability.tsx +376 -0
- package/src/native/broadcast-sheet.tsx +193 -0
- package/src/native/composer-state.ts +302 -0
- package/src/native/composer.tsx +453 -0
- package/src/native/context.tsx +70 -0
- package/src/native/copy.ts +296 -0
- package/src/native/emoji-data.ts +21 -0
- package/src/native/emoji-picker.tsx +229 -0
- package/src/native/emoji.ts +118 -0
- package/src/native/icon-button.tsx +40 -0
- package/src/native/inbox-screen.tsx +116 -0
- package/src/native/index.ts +184 -0
- package/src/native/link-preview.tsx +104 -0
- package/src/native/media-colors.ts +15 -0
- package/src/native/media.tsx +187 -0
- package/src/native/message-actions-sheet.tsx +121 -0
- package/src/native/message-bubble.tsx +374 -0
- package/src/native/moderation.tsx +42 -0
- package/src/native/modules.ts +275 -0
- package/src/native/organise-copy.ts +195 -0
- package/src/native/photo-viewer.tsx +531 -0
- package/src/native/push.ts +375 -0
- package/src/native/recorder-machine.ts +176 -0
- package/src/native/rows.ts +81 -0
- package/src/native/schedule-sheet.tsx +314 -0
- package/src/native/search-screen.tsx +168 -0
- package/src/native/stack-options.ts +36 -0
- package/src/native/start-sheet.tsx +87 -0
- package/src/native/thread-screen.tsx +620 -0
- package/src/native/topics.tsx +179 -0
- package/src/native/tus.ts +282 -0
- package/src/native/use-composer.ts +71 -0
- package/src/native/use-jump.ts +91 -0
- package/src/native/use-voice-recorder.ts +234 -0
- package/src/native/video-player.tsx +370 -0
- package/src/native/video-recorder-state.ts +140 -0
- package/src/native/video-recorder.tsx +271 -0
- package/src/native/video-upload-tray.tsx +77 -0
- package/src/native/video-upload.ts +432 -0
- package/src/native/voice-player.tsx +283 -0
- package/src/react/index.tsx +810 -0
- package/src/server/assistant.ts +235 -0
- package/src/server/crypto.ts +86 -0
- package/src/server/expo.ts +119 -0
- package/src/server/handler.ts +32 -0
- package/src/server/index.ts +57 -0
- package/src/server/moderation.ts +155 -0
- package/src/server/mux.ts +290 -0
- package/src/server/notifications.ts +130 -0
- package/src/server/runner.ts +468 -0
- package/src/server/transcribers.ts +111 -0
- package/src/server/types.ts +171 -0
- package/src/server/unfurl.ts +480 -0
- package/src/server/upload.ts +158 -0
- package/src/web/attachments.tsx +379 -0
- package/src/web/availability.tsx +299 -0
- package/src/web/broadcast.tsx +166 -0
- package/src/web/bubble.tsx +563 -0
- package/src/web/cards.tsx +136 -0
- package/src/web/composer.tsx +571 -0
- package/src/web/emoji-picker.tsx +84 -0
- package/src/web/error-boundary.tsx +33 -0
- package/src/web/format.ts +55 -0
- package/src/web/inbox.tsx +195 -0
- package/src/web/index.ts +75 -0
- package/src/web/markdown.tsx +245 -0
- package/src/web/memory-client.ts +706 -0
- package/src/web/panel.tsx +135 -0
- package/src/web/schedule.tsx +192 -0
- package/src/web/search.tsx +129 -0
- package/src/web/thread-context.ts +64 -0
- package/src/web/thread.tsx +802 -0
- package/src/web/topics.tsx +160 -0
- package/src/web/use-voice-recorder.ts +264 -0
- package/src/web/video.tsx +522 -0
- package/src/web/voice-note.tsx +197 -0
- package/dist/chunk-27UTR5FW.js +0 -322
- package/dist/chunk-27UTR5FW.js.map +0 -1
- package/dist/chunk-JQXZUEEL.js.map +0 -1
- package/dist/chunk-SAY24E6V.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomId, attachmentKindFor, ChatError } from './chunk-
|
|
1
|
+
import { randomId, attachmentKindFor, ChatError } from './chunk-7S6HYNWW.js';
|
|
2
2
|
|
|
3
3
|
// src/core/thread.ts
|
|
4
4
|
function mergeThread(server, pending) {
|
|
@@ -128,18 +128,42 @@ var memoryOutboxStore = () => {
|
|
|
128
128
|
let saved = [];
|
|
129
129
|
return { load: async () => saved, save: async (q) => void (saved = q) };
|
|
130
130
|
};
|
|
131
|
+
function releaseObjectUrl(url) {
|
|
132
|
+
if (!url.startsWith("blob:") || typeof URL === "undefined" || typeof URL.revokeObjectURL !== "function") return;
|
|
133
|
+
try {
|
|
134
|
+
URL.revokeObjectURL(url);
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
}
|
|
131
138
|
function createOutbox(opts) {
|
|
132
139
|
const store = opts.store ?? memoryOutboxStore();
|
|
133
140
|
let queue = [];
|
|
134
|
-
let
|
|
135
|
-
const running = /* @__PURE__ */ new
|
|
141
|
+
let generation = 0;
|
|
142
|
+
const running = /* @__PURE__ */ new Map();
|
|
143
|
+
const aborts = /* @__PURE__ */ new Map();
|
|
136
144
|
const uploaded = /* @__PURE__ */ new Map();
|
|
145
|
+
const release = opts.releasePreview ?? releaseObjectUrl;
|
|
146
|
+
const releasePreviews = (q) => {
|
|
147
|
+
for (const url of /* @__PURE__ */ new Set([...Object.values(q.previews ?? {}), ...q.files.map((f) => f.previewUrl)])) {
|
|
148
|
+
if (typeof url === "string" && url) release(url);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
137
151
|
const newId = opts.newId ?? randomId;
|
|
138
|
-
const saveable = (q) =>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
const saveable = (q) => {
|
|
153
|
+
const slots = q.pendingFiles?.length ? q.pendingFiles : q.files.map(describe);
|
|
154
|
+
const here = [...q.files];
|
|
155
|
+
const files = [];
|
|
156
|
+
const pendingFiles = slots.map((slot) => {
|
|
157
|
+
if (slot.lost) return slot;
|
|
158
|
+
const f = here.shift();
|
|
159
|
+
if (f && typeof f.data === "string") {
|
|
160
|
+
files.push(f);
|
|
161
|
+
return { ...slot, lost: false };
|
|
162
|
+
}
|
|
163
|
+
return { ...slot, lost: true };
|
|
164
|
+
});
|
|
165
|
+
return { ...q, files, pendingFiles };
|
|
166
|
+
};
|
|
143
167
|
const emit = () => {
|
|
144
168
|
opts.onChange?.([...queue]);
|
|
145
169
|
void store.save(queue.map(saveable));
|
|
@@ -155,41 +179,59 @@ function createOutbox(opts) {
|
|
|
155
179
|
}
|
|
156
180
|
}
|
|
157
181
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
182
|
+
let loading = null;
|
|
183
|
+
function ensureLoaded() {
|
|
184
|
+
loading ??= (async () => {
|
|
185
|
+
const gen = generation;
|
|
186
|
+
let saved = [];
|
|
187
|
+
try {
|
|
188
|
+
saved = await store.load() ?? [];
|
|
189
|
+
} catch {
|
|
190
|
+
saved = [];
|
|
191
|
+
}
|
|
192
|
+
if (gen !== generation) return;
|
|
193
|
+
const live = new Set(queue.map((q) => q.clientId));
|
|
194
|
+
const restored = saved.filter((q) => !live.has(q.clientId)).map((q) => {
|
|
195
|
+
const lost = (q.pendingFiles ?? []).filter((f) => f.lost).length;
|
|
196
|
+
return {
|
|
197
|
+
...q,
|
|
198
|
+
status: q.status === "sending" || lost > 0 ? "failed" : q.status,
|
|
199
|
+
error: lost > 0 ? "Attach the file again to send" : q.error ?? "Not sent",
|
|
200
|
+
files: q.files ?? [],
|
|
201
|
+
attempts: q.attempts ?? 0,
|
|
202
|
+
lostFiles: lost
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
if (restored.length) {
|
|
206
|
+
queue = [...restored, ...queue];
|
|
207
|
+
opts.onChange?.([...queue]);
|
|
208
|
+
}
|
|
209
|
+
})();
|
|
210
|
+
return loading;
|
|
174
211
|
}
|
|
175
|
-
async function uploadFile(conversationId, file) {
|
|
176
|
-
if (opts.upload) return opts.upload(conversationId, file);
|
|
212
|
+
async function uploadFile(conversationId, file, signal) {
|
|
213
|
+
if (opts.upload) return opts.upload(conversationId, file, { signal });
|
|
177
214
|
const data = typeof file.data === "string" ? await (opts.readFile ?? notReadable)(file.data) : file.data;
|
|
178
215
|
return opts.client.upload(conversationId, opts.userId, data, { name: file.name, contentType: file.contentType });
|
|
179
216
|
}
|
|
180
217
|
async function run(conversationId) {
|
|
181
|
-
|
|
182
|
-
running.
|
|
218
|
+
const gen = generation;
|
|
219
|
+
if (running.get(conversationId) === gen) return;
|
|
220
|
+
running.set(conversationId, gen);
|
|
221
|
+
const stale = (q) => gen !== generation || !queue.includes(q);
|
|
183
222
|
try {
|
|
184
223
|
for (; ; ) {
|
|
185
|
-
|
|
186
|
-
|
|
224
|
+
if (gen !== generation) return;
|
|
225
|
+
const next = queue.find((q) => q.input.conversationId === conversationId);
|
|
226
|
+
if (!next || next.status !== "sending") break;
|
|
187
227
|
if ((next.lostFiles ?? 0) > 0) {
|
|
188
228
|
next.status = "failed";
|
|
189
229
|
next.error = "Attach the file again to send";
|
|
190
230
|
emit();
|
|
191
231
|
break;
|
|
192
232
|
}
|
|
233
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
234
|
+
if (controller) aborts.set(next.clientId, controller);
|
|
193
235
|
try {
|
|
194
236
|
const attachments = [...next.input.attachments ?? []];
|
|
195
237
|
for (let i = 0; i < next.files.length; i++) {
|
|
@@ -197,7 +239,8 @@ function createOutbox(opts) {
|
|
|
197
239
|
const key = `${next.clientId}:${i}`;
|
|
198
240
|
let path = uploaded.get(key);
|
|
199
241
|
if (!path) {
|
|
200
|
-
path = await uploadFile(conversationId, file);
|
|
242
|
+
path = await uploadFile(conversationId, file, controller?.signal);
|
|
243
|
+
if (stale(next)) return;
|
|
201
244
|
uploaded.set(key, path);
|
|
202
245
|
}
|
|
203
246
|
attachments.push({
|
|
@@ -208,28 +251,37 @@ function createOutbox(opts) {
|
|
|
208
251
|
mime: file.contentType
|
|
209
252
|
});
|
|
210
253
|
}
|
|
254
|
+
if (stale(next)) return;
|
|
211
255
|
const message = await opts.client.send({ ...next.input, attachments });
|
|
256
|
+
if (gen !== generation) return;
|
|
212
257
|
queue = queue.filter((q) => q.clientId !== next.clientId);
|
|
213
258
|
for (let i = 0; i < next.files.length; i++) uploaded.delete(`${next.clientId}:${i}`);
|
|
214
259
|
emit();
|
|
215
260
|
opts.onSent?.(message, next.clientId);
|
|
261
|
+
releasePreviews(next);
|
|
216
262
|
} catch (e) {
|
|
263
|
+
if (stale(next)) return;
|
|
217
264
|
const err = e instanceof ChatError ? e : new ChatError(e instanceof Error ? e.message : String(e), void 0, "send");
|
|
218
265
|
next.attempts += 1;
|
|
219
266
|
next.status = "failed";
|
|
267
|
+
next.permanent = err.permanent;
|
|
220
268
|
next.error = err.forbidden ? "You can't post here" : err.transient ? "Not sent. Check your connection." : err.message;
|
|
221
269
|
emit();
|
|
222
270
|
break;
|
|
271
|
+
} finally {
|
|
272
|
+
if (controller && aborts.get(next.clientId) === controller) aborts.delete(next.clientId);
|
|
223
273
|
}
|
|
224
274
|
}
|
|
225
275
|
} finally {
|
|
226
|
-
running.delete(conversationId);
|
|
276
|
+
if (running.get(conversationId) === gen) running.delete(conversationId);
|
|
227
277
|
}
|
|
228
278
|
}
|
|
279
|
+
const canAutoRetry = (x) => !x.permanent && !(x.lostFiles ?? 0) && x.files.every((f) => typeof f.data !== "string" || Boolean(opts.readFile || opts.upload));
|
|
229
280
|
return {
|
|
230
281
|
/** Queues a message; it shows at once and sends in order. Returns its clientId. */
|
|
231
282
|
async send(input, files = []) {
|
|
232
283
|
await ensureLoaded();
|
|
284
|
+
const gen = generation;
|
|
233
285
|
const clientId = input.clientId ?? newId();
|
|
234
286
|
const previews = {};
|
|
235
287
|
files.forEach((f, i) => {
|
|
@@ -249,6 +301,7 @@ function createOutbox(opts) {
|
|
|
249
301
|
queue.push(queued);
|
|
250
302
|
opts.onChange?.([...queue]);
|
|
251
303
|
await persist(queued);
|
|
304
|
+
if (gen !== generation || !queue.includes(queued)) return clientId;
|
|
252
305
|
emit();
|
|
253
306
|
void run(input.conversationId);
|
|
254
307
|
return clientId;
|
|
@@ -257,39 +310,107 @@ function createOutbox(opts) {
|
|
|
257
310
|
await ensureLoaded();
|
|
258
311
|
const q = queue.find((x) => x.clientId === clientId);
|
|
259
312
|
if (!q) return;
|
|
260
|
-
for (const x of queue)
|
|
313
|
+
for (const x of queue) {
|
|
314
|
+
if (x.input.conversationId !== q.input.conversationId || x.status !== "failed") continue;
|
|
315
|
+
if ((x.lostFiles ?? 0) > 0) break;
|
|
316
|
+
x.status = "sending";
|
|
317
|
+
x.permanent = false;
|
|
318
|
+
}
|
|
261
319
|
emit();
|
|
262
320
|
void run(q.input.conversationId);
|
|
263
321
|
},
|
|
264
|
-
/**
|
|
322
|
+
/**
|
|
323
|
+
* Gives a message the files it lost on a relaunch and sends it. `files` fill the lost
|
|
324
|
+
* slots in order (`PendingMessage.files[i].lost`); the files that were kept stay. Pass
|
|
325
|
+
* one per lost slot; with fewer, the message keeps waiting for the rest.
|
|
326
|
+
*/
|
|
265
327
|
async reattach(clientId, files) {
|
|
266
328
|
await ensureLoaded();
|
|
267
329
|
const q = queue.find((x) => x.clientId === clientId);
|
|
268
330
|
if (!q) return;
|
|
269
|
-
q.
|
|
270
|
-
|
|
271
|
-
|
|
331
|
+
const slots = q.pendingFiles ?? [];
|
|
332
|
+
const kept = [...q.files];
|
|
333
|
+
const given = [...files];
|
|
334
|
+
const merged = [];
|
|
335
|
+
const described = [];
|
|
336
|
+
for (const slot of slots) {
|
|
337
|
+
if (slot.lost) {
|
|
338
|
+
const f = given.shift();
|
|
339
|
+
if (f) {
|
|
340
|
+
merged.push(f);
|
|
341
|
+
described.push(describe(f));
|
|
342
|
+
} else {
|
|
343
|
+
described.push(slot);
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
const f = kept.shift();
|
|
347
|
+
if (f) {
|
|
348
|
+
merged.push(f);
|
|
349
|
+
described.push(slot);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (slots.length === 0) {
|
|
354
|
+
merged.push(...given);
|
|
355
|
+
described.push(...given.map(describe));
|
|
356
|
+
}
|
|
357
|
+
q.files = merged;
|
|
358
|
+
q.pendingFiles = described;
|
|
359
|
+
q.lostFiles = described.filter((d) => d.lost).length;
|
|
360
|
+
if (q.lostFiles > 0) {
|
|
361
|
+
emit();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const gen = generation;
|
|
272
365
|
await persist(q);
|
|
366
|
+
if (gen !== generation || !queue.includes(q)) return;
|
|
273
367
|
q.status = "sending";
|
|
274
368
|
emit();
|
|
275
369
|
void run(q.input.conversationId);
|
|
276
370
|
},
|
|
277
|
-
/**
|
|
371
|
+
/**
|
|
372
|
+
* Retries failed messages (after the connection comes back), in order per conversation,
|
|
373
|
+
* stopping at the first that can't go on its own: one waiting for a lost file, or one the
|
|
374
|
+
* server refused (`permanent`, retried only by hand). The ones after it keep waiting.
|
|
375
|
+
*/
|
|
278
376
|
async retryAll() {
|
|
279
377
|
await ensureLoaded();
|
|
280
378
|
const conversations = /* @__PURE__ */ new Set();
|
|
379
|
+
const held = /* @__PURE__ */ new Set();
|
|
281
380
|
for (const x of queue) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
381
|
+
const c = x.input.conversationId;
|
|
382
|
+
if (held.has(c) || x.status !== "failed") continue;
|
|
383
|
+
if (!canAutoRetry(x)) {
|
|
384
|
+
held.add(c);
|
|
385
|
+
continue;
|
|
285
386
|
}
|
|
387
|
+
x.status = "sending";
|
|
388
|
+
conversations.add(c);
|
|
286
389
|
}
|
|
287
390
|
emit();
|
|
288
391
|
for (const c of conversations) void run(c);
|
|
289
392
|
},
|
|
393
|
+
/** Forgets every queued message, here and in the store (call it on sign-out). */
|
|
394
|
+
async clear() {
|
|
395
|
+
generation += 1;
|
|
396
|
+
for (const c of aborts.values()) c.abort();
|
|
397
|
+
aborts.clear();
|
|
398
|
+
running.clear();
|
|
399
|
+
const gone = queue;
|
|
400
|
+
queue = [];
|
|
401
|
+
uploaded.clear();
|
|
402
|
+
gone.forEach(releasePreviews);
|
|
403
|
+
opts.onChange?.([]);
|
|
404
|
+
await ensureLoaded();
|
|
405
|
+
await store.save([]);
|
|
406
|
+
},
|
|
290
407
|
async discard(clientId) {
|
|
291
408
|
await ensureLoaded();
|
|
409
|
+
const gone = queue.find((q) => q.clientId === clientId);
|
|
292
410
|
queue = queue.filter((q) => q.clientId !== clientId);
|
|
411
|
+
aborts.get(clientId)?.abort();
|
|
412
|
+
aborts.delete(clientId);
|
|
413
|
+
if (gone) releasePreviews(gone);
|
|
293
414
|
emit();
|
|
294
415
|
},
|
|
295
416
|
pending(conversationId) {
|
|
@@ -301,9 +422,9 @@ function createOutbox(opts) {
|
|
|
301
422
|
};
|
|
302
423
|
}
|
|
303
424
|
async function notReadable() {
|
|
304
|
-
throw new ChatError("This file can't be read on this device", "
|
|
425
|
+
throw new ChatError("This file can't be read on this device", "GSFILE", "upload");
|
|
305
426
|
}
|
|
306
427
|
|
|
307
|
-
export { activityLine, createOutbox, currentActivity, dayLabel, firstUnreadId, itemSender, itemTime, memoryOutboxStore, mergeThread, parseMentions, previewText, sameDay, startsDay, startsGroup, summarizeReactions, throttle, upsertMessages };
|
|
308
|
-
//# sourceMappingURL=chunk-
|
|
309
|
-
//# sourceMappingURL=chunk-
|
|
428
|
+
export { activityLine, createOutbox, currentActivity, dayLabel, firstUnreadId, itemSender, itemTime, memoryOutboxStore, mergeThread, parseMentions, previewText, releaseObjectUrl, sameDay, startsDay, startsGroup, summarizeReactions, throttle, upsertMessages };
|
|
429
|
+
//# sourceMappingURL=chunk-5D5A7I4Q.js.map
|
|
430
|
+
//# sourceMappingURL=chunk-5D5A7I4Q.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/thread.ts","../src/core/outbox.ts"],"names":[],"mappings":"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","file":"chunk-5D5A7I4Q.js","sourcesContent":["/**\n * @feat:chat\n * Pure thread logic, the same on every platform: merging the server's messages\n * with ones still on their way, grouping a run of messages from one sender,\n * day dividers, the unread line, reactions by emoji, and the typing line.\n */\nimport type { ChatActivity, ChatMessage, PendingMessage } from \"./types\";\n\nexport type ThreadItem =\n | { type: \"message\"; message: ChatMessage; pending?: undefined }\n | { type: \"pending\"; pending: PendingMessage; message?: undefined };\n\n/**\n * The server's messages (by seq), then what is still sending, oldest first. A\n * pending message the server already has (same clientId) is dropped, so a send\n * that landed never shows twice.\n */\nexport function mergeThread(server: ChatMessage[], pending: PendingMessage[]): ThreadItem[] {\n const byId = new Map<string, ChatMessage>();\n for (const m of server) byId.set(m.id, m);\n const sorted = [...byId.values()].sort((a, b) => a.seq - b.seq);\n const landed = new Set(sorted.map((m) => m.clientId));\n const waiting = pending\n .filter((p) => !landed.has(p.clientId))\n .sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n return [\n ...sorted.map((message) => ({ type: \"message\" as const, message })),\n ...waiting.map((p) => ({ type: \"pending\" as const, pending: p })),\n ];\n}\n\n/** Adds or replaces messages in a page, keeping it sorted and free of duplicates. */\nexport function upsertMessages(existing: ChatMessage[], incoming: ChatMessage[]): ChatMessage[] {\n const byId = new Map(existing.map((m) => [m.id, m]));\n for (const m of incoming) byId.set(m.id, m);\n return [...byId.values()].sort((a, b) => a.seq - b.seq);\n}\n\nexport function itemSender(item: ThreadItem): string | null {\n return item.type === \"message\" ? item.message.senderId : item.pending.senderId;\n}\n\nexport function itemTime(item: ThreadItem): string {\n return item.type === \"message\" ? item.message.createdAt : item.pending.createdAt;\n}\n\n/** Runs of one sender within five minutes share a header (name, avatar). */\nexport function startsGroup(items: ThreadItem[], index: number, gapMs = 5 * 60_000): boolean {\n if (index === 0) return true;\n const prev = items[index - 1]!;\n const cur = items[index]!;\n if (cur.type === \"message\" && cur.message.kind === \"system\") return true;\n if (prev.type === \"message\" && prev.message.kind === \"system\") return true;\n if (itemSender(prev) !== itemSender(cur)) return true;\n return Date.parse(itemTime(cur)) - Date.parse(itemTime(prev)) > gapMs || !sameDay(itemTime(prev), itemTime(cur));\n}\n\nexport function sameDay(a: string, b: string): boolean {\n const x = new Date(a);\n const y = new Date(b);\n return x.getFullYear() === y.getFullYear() && x.getMonth() === y.getMonth() && x.getDate() === y.getDate();\n}\n\n/** A day divider goes before this item. */\nexport function startsDay(items: ThreadItem[], index: number): boolean {\n return index === 0 || !sameDay(itemTime(items[index - 1]!), itemTime(items[index]!));\n}\n\n/** \"Today\", \"Yesterday\", \"Monday\", or \"12 Mar 2026\". */\nexport function dayLabel(iso: string, now = new Date(), locale?: string): string {\n const d = new Date(iso);\n const startOf = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();\n const days = Math.round((startOf(now) - startOf(d)) / 86_400_000);\n if (days === 0) return \"Today\";\n if (days === 1) return \"Yesterday\";\n if (days > 1 && days < 7) return d.toLocaleDateString(locale, { weekday: \"long\" });\n return d.toLocaleDateString(locale, { day: \"numeric\", month: \"short\", year: d.getFullYear() === now.getFullYear() ? undefined : \"numeric\" });\n}\n\n/** The first message after what you had read, from someone else: where \"New\" goes. */\nexport function firstUnreadId(messages: ChatMessage[], lastReadSeq: number, me: string): string | null {\n return messages.find((m) => m.seq > lastReadSeq && m.senderId !== me && !m.deletedAt)?.id ?? null;\n}\n\nexport interface ReactionSummary {\n emoji: string;\n count: number;\n mine: boolean;\n userIds: string[];\n}\n\n/** Reactions by emoji, most first, then in first-used order. */\nexport function summarizeReactions(message: ChatMessage, me: string): ReactionSummary[] {\n const order: string[] = [];\n const by = new Map<string, string[]>();\n for (const r of message.reactions) {\n if (!by.has(r.emoji)) {\n by.set(r.emoji, []);\n order.push(r.emoji);\n }\n by.get(r.emoji)!.push(r.userId);\n }\n return order\n .map((emoji) => ({ emoji, count: by.get(emoji)!.length, mine: by.get(emoji)!.includes(me), userIds: by.get(emoji)! }))\n .sort((a, b) => b.count - a.count || order.indexOf(a.emoji) - order.indexOf(b.emoji));\n}\n\n/** Who is doing what now: activity older than six seconds has stopped. */\nexport function currentActivity(activity: ChatActivity[], now = Date.now(), staleMs = 6000): ChatActivity[] {\n const latest = new Map<string, ChatActivity>();\n for (const a of activity) {\n if (now - a.at > staleMs) continue;\n const prev = latest.get(a.userId);\n if (!prev || prev.at < a.at) latest.set(a.userId, a);\n }\n return [...latest.values()];\n}\n\n/** \"Sam is typing…\", \"Sam and Lee are typing…\", \"Sam is recording a voice message…\". */\nexport function activityLine(activity: ChatActivity[], nameOf: (userId: string) => string): string | null {\n if (activity.length === 0) return null;\n const recording = activity.find((a) => a.activity !== \"typing\");\n if (recording) {\n return `${nameOf(recording.userId)} is recording ${recording.activity === \"recording-video\" ? \"a video\" : \"a voice message\"}…`;\n }\n const names = activity.map((a) => nameOf(a.userId));\n if (names.length === 1) return `${names[0]} is typing…`;\n if (names.length === 2) return `${names[0]} and ${names[1]} are typing…`;\n return \"Several people are typing…\";\n}\n\n/** Sends activity at most once every `everyMs`. */\nexport function throttle(fn: () => void, everyMs = 3000): () => void {\n let last = 0;\n return () => {\n const now = Date.now();\n if (now - last >= everyMs) {\n last = now;\n fn();\n }\n };\n}\n\n/** The ids @mentioned in markdown written as `@[Name](user:<uuid>)`. */\nexport function parseMentions(body: string): string[] {\n const ids = new Set<string>();\n for (const m of body.matchAll(/@\\[[^\\]]{1,80}\\]\\(user:([0-9a-f-]{36})\\)/gi)) ids.add(m[1]!.toLowerCase());\n return [...ids];\n}\n\n/** A one-line preview: text, or what was attached. */\nexport function previewText(body: string, attachment: string | null | undefined, deleted = false): string {\n if (deleted) return \"Message deleted\";\n const text = body.replace(/@\\[([^\\]]+)\\]\\(user:[0-9a-f-]{36}\\)/gi, \"@$1\").replace(/[*_`#>~]/g, \"\").trim();\n if (text) return text.length > 140 ? `${text.slice(0, 139)}…` : text;\n switch (attachment) {\n case \"image\":\n return \"Photo\";\n case \"voice\":\n return \"Voice message\";\n case \"video\":\n return \"Video\";\n case \"file\":\n return \"File\";\n case null:\n case undefined:\n return \"\";\n default:\n return \"Attachment\";\n }\n}\n","/**\n * @feat:chat\n * The send queue: a message shows at once as pending, its files upload, then it\n * is sent with the clientId made when it was written, so a retry (after a lost\n * connection, a closed app) never posts twice. Messages to one conversation go\n * in the order they were written: the first that can't go (a lost file, a refusal)\n * holds back the ones after it. A refusal (not allowed, bad data) is `permanent`\n * and only sent again by hand. `clear()` (sign-out) stops sends in flight: none\n * posts, or saves anything, afterwards. Storage is the platform's (memory by default;\n * localStorage on web, MMKV or SQLite on native), behind OutboxStore.\n */\nimport type { ChatClient } from \"./client\";\nimport { ChatError } from \"./client\";\nimport { attachmentKindFor, randomId } from \"./media\";\nimport type { AttachmentInput, ChatMessage, PendingFile, PendingMessage, SendInput } from \"./types\";\n\n/** A file still on the device, uploaded when its message is sent. */\nexport interface LocalFile {\n /** The bytes, or (native) a file URI the platform reads in `readFile`. */\n data: Blob | ArrayBuffer | string;\n name: string;\n contentType: string;\n /** A preview to show while it uploads (an object URL, a file URI). The outbox owns it from here: see `releasePreview`. */\n previewUrl?: string;\n /** Known up front: a voice note's duration and waveform, a photo's size. */\n meta?: Partial<Omit<AttachmentInput, \"path\" | \"kind\">> & { kind?: AttachmentInput[\"kind\"] };\n}\n\nexport interface QueuedMessage extends Omit<PendingMessage, \"files\"> {\n files: LocalFile[];\n /** What each file is, kept with the saved copy (PendingMessage.files). */\n pendingFiles?: PendingFile[];\n /** Files lost on a relaunch (bytes that weren't persisted). */\n lostFiles?: number;\n attempts: number;\n}\n\nfunction describe(f: LocalFile): PendingFile {\n return { name: f.name, contentType: f.contentType, kind: f.meta?.kind ?? attachmentKindFor(f.contentType), meta: f.meta };\n}\n\nexport interface OutboxStore {\n load(): Promise<QueuedMessage[]>;\n save(queue: QueuedMessage[]): Promise<void>;\n}\n\nexport const memoryOutboxStore = (): OutboxStore => {\n let saved: QueuedMessage[] = [];\n return { load: async () => saved, save: async (q) => void (saved = q) };\n};\n\nexport interface OutboxOptions {\n client: ChatClient;\n userId: string;\n store?: OutboxStore;\n /** Reads a native file URI into bytes for upload. */\n readFile?: (uri: string) => Promise<Blob | ArrayBuffer>;\n /** Upload hook, e.g. resumable uploads on native. Defaults to client.upload. `signal` aborts when the message is discarded or the outbox cleared. */\n upload?: (conversationId: string, file: LocalFile, opts?: { signal?: AbortSignal }) => Promise<string>;\n /** Called with every change, for the UI. */\n onChange?: (queue: QueuedMessage[]) => void;\n /** Called when a message lands. */\n onSent?: (message: ChatMessage, clientId: string) => void;\n /** Makes client ids. Default: a random UUID (works where crypto.randomUUID is missing). */\n newId?: () => string;\n /**\n * Keeps a file held as bytes (a pasted image, a recording) between launches: write it\n * somewhere and return a URI `readFile` can read (native: the app's cache directory).\n * Without it, such a file isn't saved, and after a relaunch its message asks for it again.\n */\n persistFile?: (file: LocalFile) => Promise<string>;\n /**\n * Lets go of a file's `previewUrl` once its message has landed, been discarded or cleared.\n * Default: revokes `blob:` object URLs (the web's), and leaves everything else alone.\n */\n releasePreview?: (url: string) => void;\n}\n\n/** Revokes a `blob:` object URL; anything else (a native file URI) is left alone. */\nexport function releaseObjectUrl(url: string) {\n if (!url.startsWith(\"blob:\") || typeof URL === \"undefined\" || typeof URL.revokeObjectURL !== \"function\") return;\n try {\n URL.revokeObjectURL(url);\n } catch {\n // Already gone, or a runtime without object URLs.\n }\n}\n\n\nexport function createOutbox(opts: OutboxOptions) {\n const store = opts.store ?? memoryOutboxStore();\n let queue: QueuedMessage[] = [];\n // Bumped by clear(): work begun under an older generation stops at its next await.\n let generation = 0;\n const running = new Map<string, number>(); // conversationId -> the generation running it\n const aborts = new Map<string, AbortController>(); // clientId -> its uploads' controller\n const uploaded = new Map<string, string>(); // clientId:index -> path\n\n const release = opts.releasePreview ?? releaseObjectUrl;\n const releasePreviews = (q: QueuedMessage) => {\n for (const url of new Set([...Object.values(q.previews ?? {}), ...q.files.map((f) => f.previewUrl)])) {\n if (typeof url === \"string\" && url) release(url);\n }\n };\n\n const newId = opts.newId ?? randomId;\n\n // Saved copies: a file held as bytes is kept only as a URI (persistFile) or marked lost.\n // `files` holds the files still here, in slot order; `pendingFiles` holds every slot.\n const saveable = (q: QueuedMessage): QueuedMessage => {\n const slots = q.pendingFiles?.length ? q.pendingFiles : q.files.map(describe);\n const here = [...q.files];\n const files: LocalFile[] = [];\n const pendingFiles = slots.map((slot) => {\n if (slot.lost) return slot;\n const f = here.shift();\n if (f && typeof f.data === \"string\") {\n files.push(f);\n return { ...slot, lost: false };\n }\n return { ...slot, lost: true };\n });\n return { ...q, files, pendingFiles };\n };\n\n const emit = () => {\n opts.onChange?.([...queue]);\n void store.save(queue.map(saveable));\n };\n\n /** Swaps a byte file for a saved URI, so it survives a relaunch. */\n async function persist(q: QueuedMessage) {\n if (!opts.persistFile) return;\n for (let i = 0; i < q.files.length; i++) {\n const f = q.files[i]!;\n if (typeof f.data === \"string\") continue;\n try {\n q.files[i] = { ...f, data: await opts.persistFile(f) };\n } catch {\n // Kept in memory only; a relaunch asks for it again.\n }\n }\n }\n\n /**\n * One load for the outbox's life, which every method awaits. What was saved is merged into\n * the live queue (a send made while the load was in flight stays), deduped by clientId.\n */\n let loading: Promise<void> | null = null;\n function ensureLoaded(): Promise<void> {\n loading ??= (async () => {\n const gen = generation;\n let saved: QueuedMessage[] = [];\n try {\n saved = (await store.load()) ?? [];\n } catch {\n saved = [];\n }\n // Cleared (signed out) while it loaded: what was saved belonged to that session.\n if (gen !== generation) return;\n // A file held as bytes that wasn't persisted doesn't survive a relaunch: its message waits,\n // failed, for the file to be attached again (never sent without it) or discarded.\n const live = new Set(queue.map((q) => q.clientId));\n const restored: QueuedMessage[] = saved\n .filter((q) => !live.has(q.clientId))\n .map((q) => {\n const lost = (q.pendingFiles ?? []).filter((f) => f.lost).length;\n return {\n ...q,\n status: q.status === \"sending\" || lost > 0 ? \"failed\" : q.status,\n error: lost > 0 ? \"Attach the file again to send\" : (q.error ?? \"Not sent\"),\n files: q.files ?? [],\n attempts: q.attempts ?? 0,\n lostFiles: lost,\n };\n });\n if (restored.length) {\n queue = [...restored, ...queue];\n opts.onChange?.([...queue]);\n }\n })();\n return loading;\n }\n\n async function uploadFile(conversationId: string, file: LocalFile, signal?: AbortSignal): Promise<string> {\n if (opts.upload) return opts.upload(conversationId, file, { signal });\n const data = typeof file.data === \"string\" ? await (opts.readFile ?? notReadable)(file.data) : file.data;\n return opts.client.upload(conversationId, opts.userId, data, { name: file.name, contentType: file.contentType });\n }\n\n async function run(conversationId: string) {\n const gen = generation;\n if (running.get(conversationId) === gen) return;\n running.set(conversationId, gen);\n // Cleared (signed out) or discarded while we waited: stop, sending and saving nothing.\n const stale = (q: QueuedMessage) => gen !== generation || !queue.includes(q);\n try {\n for (;;) {\n if (gen !== generation) return;\n // In the order written: the first message still here decides. One that failed (a lost\n // file, a refusal, no connection) holds back the ones after it until it is retried.\n const next = queue.find((q) => q.input.conversationId === conversationId);\n if (!next || next.status !== \"sending\") break;\n if ((next.lostFiles ?? 0) > 0) {\n next.status = \"failed\";\n next.error = \"Attach the file again to send\";\n emit();\n break;\n }\n const controller = typeof AbortController !== \"undefined\" ? new AbortController() : null;\n if (controller) aborts.set(next.clientId, controller);\n try {\n const attachments: AttachmentInput[] = [...(next.input.attachments ?? [])];\n for (let i = 0; i < next.files.length; i++) {\n const file = next.files[i]!;\n const key = `${next.clientId}:${i}`;\n let path = uploaded.get(key);\n if (!path) {\n path = await uploadFile(conversationId, file, controller?.signal);\n if (stale(next)) return;\n uploaded.set(key, path);\n }\n attachments.push({\n ...file.meta,\n kind: file.meta?.kind ?? attachmentKindFor(file.contentType),\n path,\n name: file.meta?.name ?? file.name,\n mime: file.contentType,\n });\n }\n if (stale(next)) return;\n const message = await opts.client.send({ ...next.input, attachments });\n // Posted, but the person signed out meanwhile: the next session must not hear of it.\n if (gen !== generation) return;\n queue = queue.filter((q) => q.clientId !== next.clientId);\n for (let i = 0; i < next.files.length; i++) uploaded.delete(`${next.clientId}:${i}`);\n emit();\n opts.onSent?.(message, next.clientId);\n // The bubble now draws the stored copy: the local preview can go.\n releasePreviews(next);\n } catch (e) {\n if (stale(next)) return;\n const err = e instanceof ChatError ? e : new ChatError(e instanceof Error ? e.message : String(e), undefined, \"send\");\n next.attempts += 1;\n next.status = \"failed\";\n next.permanent = err.permanent;\n next.error = err.forbidden ? \"You can't post here\" : err.transient ? \"Not sent. Check your connection.\" : err.message;\n emit();\n // Later messages to this conversation wait behind a failed one, so order holds.\n break;\n } finally {\n if (controller && aborts.get(next.clientId) === controller) aborts.delete(next.clientId);\n }\n }\n } finally {\n if (running.get(conversationId) === gen) running.delete(conversationId);\n }\n }\n\n /** Can this failed message go without the person's help (retryAll, coming back online)? */\n const canAutoRetry = (x: QueuedMessage) =>\n !x.permanent && !(x.lostFiles ?? 0) && x.files.every((f) => typeof f.data !== \"string\" || Boolean(opts.readFile || opts.upload));\n\n return {\n /** Queues a message; it shows at once and sends in order. Returns its clientId. */\n async send(input: Omit<SendInput, \"clientId\"> & { clientId?: string }, files: LocalFile[] = []): Promise<string> {\n await ensureLoaded();\n const gen = generation;\n const clientId = input.clientId ?? newId();\n const previews: Record<number, string> = {};\n files.forEach((f, i) => {\n if (f.previewUrl) previews[i] = f.previewUrl;\n });\n const queued: QueuedMessage = {\n clientId,\n input: { ...input, clientId },\n senderId: opts.userId,\n createdAt: new Date().toISOString(),\n status: \"sending\",\n previews,\n files: [...files],\n pendingFiles: files.map(describe),\n attempts: 0,\n };\n queue.push(queued);\n opts.onChange?.([...queue]);\n await persist(queued);\n // Cleared or discarded while its files were saved: it goes nowhere.\n if (gen !== generation || !queue.includes(queued)) return clientId;\n emit();\n void run(input.conversationId);\n return clientId;\n },\n\n async retry(clientId: string) {\n await ensureLoaded();\n const q = queue.find((x) => x.clientId === clientId);\n if (!q) return;\n // Retrying one retries everything failed in that conversation, in order, refusals too\n // (a person asked), up to one still waiting for a file.\n for (const x of queue) {\n if (x.input.conversationId !== q.input.conversationId || x.status !== \"failed\") continue;\n if ((x.lostFiles ?? 0) > 0) break;\n x.status = \"sending\";\n x.permanent = false;\n }\n emit();\n void run(q.input.conversationId);\n },\n\n /**\n * Gives a message the files it lost on a relaunch and sends it. `files` fill the lost\n * slots in order (`PendingMessage.files[i].lost`); the files that were kept stay. Pass\n * one per lost slot; with fewer, the message keeps waiting for the rest.\n */\n async reattach(clientId: string, files: LocalFile[]) {\n await ensureLoaded();\n const q = queue.find((x) => x.clientId === clientId);\n if (!q) return;\n const slots = q.pendingFiles ?? [];\n const kept = [...q.files];\n const given = [...files];\n const merged: LocalFile[] = [];\n const described: PendingFile[] = [];\n for (const slot of slots) {\n if (slot.lost) {\n const f = given.shift();\n if (f) {\n merged.push(f);\n described.push(describe(f));\n } else {\n described.push(slot);\n }\n } else {\n const f = kept.shift();\n if (f) {\n merged.push(f);\n described.push(slot);\n }\n }\n }\n // A message saved before slots were recorded: the given files are all of them.\n if (slots.length === 0) {\n merged.push(...given);\n described.push(...given.map(describe));\n }\n q.files = merged;\n q.pendingFiles = described;\n q.lostFiles = described.filter((d) => d.lost).length;\n if (q.lostFiles > 0) {\n emit();\n return;\n }\n const gen = generation;\n await persist(q);\n if (gen !== generation || !queue.includes(q)) return;\n q.status = \"sending\";\n emit();\n void run(q.input.conversationId);\n },\n\n /**\n * Retries failed messages (after the connection comes back), in order per conversation,\n * stopping at the first that can't go on its own: one waiting for a lost file, or one the\n * server refused (`permanent`, retried only by hand). The ones after it keep waiting.\n */\n async retryAll() {\n await ensureLoaded();\n const conversations = new Set<string>();\n const held = new Set<string>();\n for (const x of queue) {\n const c = x.input.conversationId;\n if (held.has(c) || x.status !== \"failed\") continue;\n if (!canAutoRetry(x)) {\n held.add(c);\n continue;\n }\n x.status = \"sending\";\n conversations.add(c);\n }\n emit();\n for (const c of conversations) void run(c);\n },\n\n /** Forgets every queued message, here and in the store (call it on sign-out). */\n async clear() {\n // At once, before any await: a send in flight stops at its next step and posts nothing.\n generation += 1;\n for (const c of aborts.values()) c.abort();\n aborts.clear();\n running.clear();\n const gone = queue;\n queue = [];\n uploaded.clear();\n gone.forEach(releasePreviews);\n opts.onChange?.([]);\n await ensureLoaded();\n await store.save([]);\n },\n\n async discard(clientId: string) {\n await ensureLoaded();\n const gone = queue.find((q) => q.clientId === clientId);\n queue = queue.filter((q) => q.clientId !== clientId);\n aborts.get(clientId)?.abort();\n aborts.delete(clientId);\n if (gone) releasePreviews(gone);\n emit();\n },\n\n pending(conversationId?: string): PendingMessage[] {\n return queue\n .filter((q) => !conversationId || q.input.conversationId === conversationId)\n .map((q) => ({ ...q, files: q.pendingFiles }));\n },\n\n /** A new client id, made the way this outbox makes them. */\n newId,\n\n load: ensureLoaded,\n };\n}\n\nexport type Outbox = ReturnType<typeof createOutbox>;\n\nasync function notReadable(): Promise<never> {\n // This device's set-up (no readFile), not a refusal: a relaunch that can read it sends it.\n throw new ChatError(\"This file can't be read on this device\", \"GSFILE\", \"upload\");\n}\n"]}
|