@convex-dev/agent 0.1.8-alpha.2 → 0.1.8
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/commonjs/component/threads.d.ts.map +1 -1
- package/dist/commonjs/component/threads.js +5 -4
- package/dist/commonjs/component/threads.js.map +1 -1
- package/dist/commonjs/component/users.d.ts +9 -0
- package/dist/commonjs/component/users.d.ts.map +1 -1
- package/dist/commonjs/component/users.js +92 -21
- package/dist/commonjs/component/users.js.map +1 -1
- package/dist/commonjs.tsbuildinfo +1 -1
- package/dist/esm/component/threads.d.ts.map +1 -1
- package/dist/esm/component/threads.js +5 -4
- package/dist/esm/component/threads.js.map +1 -1
- package/dist/esm/component/users.d.ts +9 -0
- package/dist/esm/component/users.d.ts.map +1 -1
- package/dist/esm/component/users.js +92 -21
- package/dist/esm/component/users.js.map +1 -1
- package/dist/esm.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/client/setup.test.ts +1 -2
- package/src/component/threads.test.ts +560 -0
- package/src/component/threads.ts +9 -4
- package/src/component/users.test.ts +478 -0
- package/src/component/users.ts +100 -20
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import { convexTest } from "convex-test";
|
|
4
|
+
import { describe, expect, test, vi } from "vitest";
|
|
5
|
+
import { api, internal } from "./_generated/api.js";
|
|
6
|
+
import type { Id } from "./_generated/dataModel.js";
|
|
7
|
+
import schema from "./schema.js";
|
|
8
|
+
import { modules } from "./setup.test.js";
|
|
9
|
+
|
|
10
|
+
describe("users", () => {
|
|
11
|
+
test("listUsersWithThreads returns users who have threads", async () => {
|
|
12
|
+
const t = convexTest(schema, modules);
|
|
13
|
+
|
|
14
|
+
// Create two users with threads and one without
|
|
15
|
+
const thread1 = await t.mutation(api.threads.createThread, {
|
|
16
|
+
userId: "user1",
|
|
17
|
+
title: "Test thread 1",
|
|
18
|
+
});
|
|
19
|
+
const thread2 = await t.mutation(api.threads.createThread, {
|
|
20
|
+
userId: "user2",
|
|
21
|
+
title: "Test thread 2",
|
|
22
|
+
});
|
|
23
|
+
const thread3 = await t.mutation(api.threads.createThread, {
|
|
24
|
+
userId: "user1", // Same user, different thread
|
|
25
|
+
title: "Test thread 3",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const result = await t.query(api.users.listUsersWithThreads, {
|
|
29
|
+
paginationOpts: { cursor: null, numItems: 10 },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(result.page).toHaveLength(2);
|
|
33
|
+
expect(result.page).toContain("user1");
|
|
34
|
+
expect(result.page).toContain("user2");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("listUsersWithThreads pagination works", async () => {
|
|
38
|
+
const t = convexTest(schema, modules);
|
|
39
|
+
|
|
40
|
+
// Create multiple users with threads
|
|
41
|
+
for (let i = 0; i < 5; i++) {
|
|
42
|
+
await t.mutation(api.threads.createThread, {
|
|
43
|
+
userId: `user${i}`,
|
|
44
|
+
title: `Test thread ${i}`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const firstPage = await t.query(api.users.listUsersWithThreads, {
|
|
49
|
+
paginationOpts: { cursor: null, numItems: 2 },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(firstPage.page).toHaveLength(2);
|
|
53
|
+
expect(firstPage.isDone).toBe(false);
|
|
54
|
+
|
|
55
|
+
const secondPage = await t.query(api.users.listUsersWithThreads, {
|
|
56
|
+
paginationOpts: { cursor: firstPage.continueCursor, numItems: 2 },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(secondPage.page).toHaveLength(2);
|
|
60
|
+
// Should not have duplicate users
|
|
61
|
+
console.log(firstPage.page, secondPage.page);
|
|
62
|
+
expect(
|
|
63
|
+
firstPage.page.every((user) => !secondPage.page.includes(user))
|
|
64
|
+
).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("deleteAllForUserId sync deletes all threads and messages for a user", async () => {
|
|
68
|
+
const t = convexTest(schema, modules);
|
|
69
|
+
|
|
70
|
+
// Create a user with multiple threads and messages
|
|
71
|
+
const thread1 = await t.mutation(api.threads.createThread, {
|
|
72
|
+
userId: "testUser",
|
|
73
|
+
title: "Thread 1",
|
|
74
|
+
});
|
|
75
|
+
const thread2 = await t.mutation(api.threads.createThread, {
|
|
76
|
+
userId: "testUser",
|
|
77
|
+
title: "Thread 2",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Add messages to both threads
|
|
81
|
+
await t.mutation(api.messages.addMessages, {
|
|
82
|
+
threadId: thread1._id as Id<"threads">,
|
|
83
|
+
messages: [
|
|
84
|
+
{ message: { role: "user" as const, content: "Hello thread 1" } },
|
|
85
|
+
{
|
|
86
|
+
message: { role: "assistant" as const, content: "Response thread 1" },
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await t.mutation(api.messages.addMessages, {
|
|
92
|
+
threadId: thread2._id as Id<"threads">,
|
|
93
|
+
messages: [
|
|
94
|
+
{ message: { role: "user" as const, content: "Hello thread 2" } },
|
|
95
|
+
{
|
|
96
|
+
message: { role: "assistant" as const, content: "Response thread 2" },
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Verify data exists before deletion
|
|
102
|
+
const beforeThreads = await t.query(api.threads.listThreadsByUserId, {
|
|
103
|
+
userId: "testUser",
|
|
104
|
+
});
|
|
105
|
+
expect(beforeThreads.page).toHaveLength(2);
|
|
106
|
+
|
|
107
|
+
const beforeMessages1 = await t.query(api.messages.listMessagesByThreadId, {
|
|
108
|
+
threadId: thread1._id as Id<"threads">,
|
|
109
|
+
order: "desc",
|
|
110
|
+
paginationOpts: { cursor: null, numItems: 10 },
|
|
111
|
+
});
|
|
112
|
+
const beforeMessages2 = await t.query(api.messages.listMessagesByThreadId, {
|
|
113
|
+
threadId: thread2._id as Id<"threads">,
|
|
114
|
+
order: "desc",
|
|
115
|
+
paginationOpts: { cursor: null, numItems: 10 },
|
|
116
|
+
});
|
|
117
|
+
expect(beforeMessages1.page).toHaveLength(2);
|
|
118
|
+
expect(beforeMessages2.page).toHaveLength(2);
|
|
119
|
+
|
|
120
|
+
// Delete all data for the user
|
|
121
|
+
await t.action(api.users.deleteAllForUserId, { userId: "testUser" });
|
|
122
|
+
|
|
123
|
+
// Verify all threads are deleted
|
|
124
|
+
const afterThreads = await t.query(api.threads.listThreadsByUserId, {
|
|
125
|
+
userId: "testUser",
|
|
126
|
+
});
|
|
127
|
+
expect(afterThreads.page).toHaveLength(0);
|
|
128
|
+
|
|
129
|
+
// Verify threads are actually deleted from DB
|
|
130
|
+
const thread1After = await t.query(api.threads.getThread, {
|
|
131
|
+
threadId: thread1._id as Id<"threads">,
|
|
132
|
+
});
|
|
133
|
+
const thread2After = await t.query(api.threads.getThread, {
|
|
134
|
+
threadId: thread2._id as Id<"threads">,
|
|
135
|
+
});
|
|
136
|
+
expect(thread1After).toBeNull();
|
|
137
|
+
expect(thread2After).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("deleteAllForUserIdAsync deletes data asynchronously", async () => {
|
|
141
|
+
// Enable fake timers for scheduled function testing
|
|
142
|
+
vi.useFakeTimers();
|
|
143
|
+
|
|
144
|
+
const t = convexTest(schema, modules);
|
|
145
|
+
|
|
146
|
+
// Create a user with a thread and messages
|
|
147
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
148
|
+
userId: "asyncUser",
|
|
149
|
+
title: "Async Thread",
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await t.mutation(api.messages.addMessages, {
|
|
153
|
+
threadId: thread._id as Id<"threads">,
|
|
154
|
+
messages: [
|
|
155
|
+
{ message: { role: "user" as const, content: "Hello async" } },
|
|
156
|
+
{ message: { role: "assistant" as const, content: "Response async" } },
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Start async deletion
|
|
161
|
+
const result = await t.mutation(api.users.deleteAllForUserIdAsync, {
|
|
162
|
+
userId: "asyncUser",
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// If there's more work to do, advance timers and wait for scheduled functions
|
|
166
|
+
if (!result) {
|
|
167
|
+
// Run all pending timers to trigger scheduled functions
|
|
168
|
+
vi.runAllTimers();
|
|
169
|
+
|
|
170
|
+
// Wait for all scheduled functions to complete
|
|
171
|
+
await t.finishInProgressScheduledFunctions();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Verify deletion completed
|
|
175
|
+
const afterThreads = await t.query(api.threads.listThreadsByUserId, {
|
|
176
|
+
userId: "asyncUser",
|
|
177
|
+
});
|
|
178
|
+
expect(afterThreads.page).toHaveLength(0);
|
|
179
|
+
|
|
180
|
+
// Reset to normal timers
|
|
181
|
+
vi.useRealTimers();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("deletePageForUserId handles messages phase correctly", async () => {
|
|
185
|
+
const t = convexTest(schema, modules);
|
|
186
|
+
|
|
187
|
+
// Create a thread with many messages to test pagination
|
|
188
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
189
|
+
userId: "paginationUser",
|
|
190
|
+
title: "Pagination Thread",
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Add multiple messages to force pagination
|
|
194
|
+
const messages = [];
|
|
195
|
+
for (let i = 0; i < 150; i++) {
|
|
196
|
+
messages.push({
|
|
197
|
+
message: { role: "user" as const, content: `Message ${i}` },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
await t.mutation(api.messages.addMessages, {
|
|
201
|
+
threadId: thread._id as Id<"threads">,
|
|
202
|
+
messages,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Test first page deletion
|
|
206
|
+
const result1 = await t.mutation(internal.users._deletePageForUserId, {
|
|
207
|
+
userId: "paginationUser",
|
|
208
|
+
messagesCursor: null,
|
|
209
|
+
threadsCursor: null,
|
|
210
|
+
threadInProgress: null,
|
|
211
|
+
streamsInProgress: false,
|
|
212
|
+
streamOrder: undefined,
|
|
213
|
+
deltaCursor: undefined,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(result1.isDone).toBe(false);
|
|
217
|
+
expect(result1.threadInProgress).toBe(thread._id);
|
|
218
|
+
expect(result1.streamsInProgress).toBe(false);
|
|
219
|
+
expect(result1.messagesCursor).toBeTruthy();
|
|
220
|
+
|
|
221
|
+
// Continue until messages are done
|
|
222
|
+
let currentResult = result1;
|
|
223
|
+
let iterations = 0;
|
|
224
|
+
while (!currentResult.streamsInProgress && iterations < 10) {
|
|
225
|
+
currentResult = await t.mutation(internal.users._deletePageForUserId, {
|
|
226
|
+
userId: "paginationUser",
|
|
227
|
+
messagesCursor: currentResult.messagesCursor,
|
|
228
|
+
threadsCursor: currentResult.threadsCursor,
|
|
229
|
+
threadInProgress: currentResult.threadInProgress,
|
|
230
|
+
streamsInProgress: currentResult.streamsInProgress,
|
|
231
|
+
streamOrder: currentResult.streamOrder,
|
|
232
|
+
deltaCursor: currentResult.deltaCursor,
|
|
233
|
+
});
|
|
234
|
+
iterations++;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Should now be in streams phase
|
|
238
|
+
expect(currentResult.streamsInProgress).toBe(true);
|
|
239
|
+
expect(currentResult.messagesCursor).toBeNull();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("deletePageForUserId handles streams phase correctly", async () => {
|
|
243
|
+
const t = convexTest(schema, modules);
|
|
244
|
+
|
|
245
|
+
// Create a thread with messages (no streams for this test)
|
|
246
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
247
|
+
userId: "streamsUser",
|
|
248
|
+
title: "Streams Thread",
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await t.mutation(api.messages.addMessages, {
|
|
252
|
+
threadId: thread._id as Id<"threads">,
|
|
253
|
+
messages: [
|
|
254
|
+
{ message: { role: "user" as const, content: "Test message" } },
|
|
255
|
+
],
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Test starting directly in streams phase
|
|
259
|
+
const result = await t.mutation(internal.users._deletePageForUserId, {
|
|
260
|
+
userId: "streamsUser",
|
|
261
|
+
messagesCursor: null,
|
|
262
|
+
threadsCursor: null,
|
|
263
|
+
threadInProgress: thread._id as Id<"threads">,
|
|
264
|
+
streamsInProgress: true,
|
|
265
|
+
streamOrder: undefined,
|
|
266
|
+
deltaCursor: undefined,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(result.isDone).toBe(false);
|
|
270
|
+
expect(result.streamsInProgress).toBe(true);
|
|
271
|
+
expect(result.threadInProgress).toBe(thread._id);
|
|
272
|
+
|
|
273
|
+
const result2 = await t.mutation(internal.users._deletePageForUserId, {
|
|
274
|
+
userId: "streamsUser",
|
|
275
|
+
messagesCursor: result.messagesCursor,
|
|
276
|
+
threadsCursor: result.threadsCursor,
|
|
277
|
+
threadInProgress: result.threadInProgress,
|
|
278
|
+
streamsInProgress: result.streamsInProgress,
|
|
279
|
+
streamOrder: result.streamOrder,
|
|
280
|
+
deltaCursor: result.deltaCursor,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
expect(result2.isDone).toBe(false);
|
|
284
|
+
expect(result2.threadInProgress).toBeNull();
|
|
285
|
+
expect(result2.streamsInProgress).toBe(false);
|
|
286
|
+
expect(result2.messagesCursor).toBeNull();
|
|
287
|
+
expect(result2.streamOrder).toBeUndefined();
|
|
288
|
+
expect(result2.deltaCursor).toBeUndefined();
|
|
289
|
+
|
|
290
|
+
// Thread should be deleted
|
|
291
|
+
const threadAfter = await t.query(api.threads.getThread, {
|
|
292
|
+
threadId: thread._id as Id<"threads">,
|
|
293
|
+
});
|
|
294
|
+
expect(threadAfter).toBeNull();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("deletePageForUserId handles multiple threads correctly", async () => {
|
|
298
|
+
const t = convexTest(schema, modules);
|
|
299
|
+
|
|
300
|
+
// Create multiple threads for the same user
|
|
301
|
+
const thread1 = await t.mutation(api.threads.createThread, {
|
|
302
|
+
userId: "multiUser",
|
|
303
|
+
title: "Thread 1",
|
|
304
|
+
});
|
|
305
|
+
const thread2 = await t.mutation(api.threads.createThread, {
|
|
306
|
+
userId: "multiUser",
|
|
307
|
+
title: "Thread 2",
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Add a message to each
|
|
311
|
+
await t.mutation(api.messages.addMessages, {
|
|
312
|
+
threadId: thread1._id as Id<"threads">,
|
|
313
|
+
messages: [{ message: { role: "user" as const, content: "Message 1" } }],
|
|
314
|
+
});
|
|
315
|
+
await t.mutation(api.messages.addMessages, {
|
|
316
|
+
threadId: thread2._id as Id<"threads">,
|
|
317
|
+
messages: [{ message: { role: "user" as const, content: "Message 2" } }],
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Process first thread completely
|
|
321
|
+
let currentResult = await t.mutation(internal.users._deletePageForUserId, {
|
|
322
|
+
userId: "multiUser",
|
|
323
|
+
messagesCursor: null,
|
|
324
|
+
threadsCursor: null,
|
|
325
|
+
threadInProgress: null,
|
|
326
|
+
streamsInProgress: false,
|
|
327
|
+
streamOrder: undefined,
|
|
328
|
+
deltaCursor: undefined,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Continue until first thread is done
|
|
332
|
+
while (currentResult.threadInProgress !== null) {
|
|
333
|
+
currentResult = await t.mutation(internal.users._deletePageForUserId, {
|
|
334
|
+
userId: "multiUser",
|
|
335
|
+
messagesCursor: currentResult.messagesCursor,
|
|
336
|
+
threadsCursor: currentResult.threadsCursor,
|
|
337
|
+
threadInProgress: currentResult.threadInProgress,
|
|
338
|
+
streamsInProgress: currentResult.streamsInProgress,
|
|
339
|
+
streamOrder: currentResult.streamOrder,
|
|
340
|
+
deltaCursor: currentResult.deltaCursor,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Should move to second thread
|
|
345
|
+
currentResult = await t.mutation(internal.users._deletePageForUserId, {
|
|
346
|
+
userId: "multiUser",
|
|
347
|
+
messagesCursor: currentResult.messagesCursor,
|
|
348
|
+
threadsCursor: currentResult.threadsCursor,
|
|
349
|
+
threadInProgress: currentResult.threadInProgress,
|
|
350
|
+
streamsInProgress: currentResult.streamsInProgress,
|
|
351
|
+
streamOrder: currentResult.streamOrder,
|
|
352
|
+
deltaCursor: currentResult.deltaCursor,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(currentResult.threadInProgress).toBeTruthy();
|
|
356
|
+
expect(currentResult.isDone).toBe(false);
|
|
357
|
+
|
|
358
|
+
// Complete second thread
|
|
359
|
+
while (!currentResult.isDone) {
|
|
360
|
+
currentResult = await t.mutation(internal.users._deletePageForUserId, {
|
|
361
|
+
userId: "multiUser",
|
|
362
|
+
messagesCursor: currentResult.messagesCursor,
|
|
363
|
+
threadsCursor: currentResult.threadsCursor,
|
|
364
|
+
threadInProgress: currentResult.threadInProgress,
|
|
365
|
+
streamsInProgress: currentResult.streamsInProgress,
|
|
366
|
+
streamOrder: currentResult.streamOrder,
|
|
367
|
+
deltaCursor: currentResult.deltaCursor,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// All threads should be deleted
|
|
372
|
+
const threadsAfter = await t.query(api.threads.listThreadsByUserId, {
|
|
373
|
+
userId: "multiUser",
|
|
374
|
+
});
|
|
375
|
+
expect(threadsAfter.page).toHaveLength(0);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("deleteAllForUserIdAsync with multiple scheduled iterations", async () => {
|
|
379
|
+
// Enable fake timers for scheduled function testing
|
|
380
|
+
vi.useFakeTimers();
|
|
381
|
+
|
|
382
|
+
const t = convexTest(schema, modules);
|
|
383
|
+
|
|
384
|
+
// Create a user with multiple threads and many messages to force multiple scheduling iterations
|
|
385
|
+
const thread1 = await t.mutation(api.threads.createThread, {
|
|
386
|
+
userId: "multiIterUser",
|
|
387
|
+
title: "Multi Iter Thread 1",
|
|
388
|
+
});
|
|
389
|
+
const thread2 = await t.mutation(api.threads.createThread, {
|
|
390
|
+
userId: "multiIterUser",
|
|
391
|
+
title: "Multi Iter Thread 2",
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Add many messages to force pagination
|
|
395
|
+
const messages = [];
|
|
396
|
+
for (let i = 0; i < 150; i++) {
|
|
397
|
+
messages.push({
|
|
398
|
+
message: { role: "user" as const, content: `Message ${i}` },
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
await t.mutation(api.messages.addMessages, {
|
|
402
|
+
threadId: thread1._id as Id<"threads">,
|
|
403
|
+
messages,
|
|
404
|
+
});
|
|
405
|
+
await t.mutation(api.messages.addMessages, {
|
|
406
|
+
threadId: thread2._id as Id<"threads">,
|
|
407
|
+
messages,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Start async deletion
|
|
411
|
+
const result = await t.mutation(api.users.deleteAllForUserIdAsync, {
|
|
412
|
+
userId: "multiIterUser",
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Should return false since there's a lot of work to do
|
|
416
|
+
expect(result).toBe(false);
|
|
417
|
+
|
|
418
|
+
// Use finishAllScheduledFunctions to handle recursive scheduling
|
|
419
|
+
await t.finishAllScheduledFunctions(vi.runAllTimers);
|
|
420
|
+
|
|
421
|
+
// Verify all data is deleted
|
|
422
|
+
const afterThreads = await t.query(api.threads.listThreadsByUserId, {
|
|
423
|
+
userId: "multiIterUser",
|
|
424
|
+
});
|
|
425
|
+
expect(afterThreads.page).toHaveLength(0);
|
|
426
|
+
|
|
427
|
+
// Reset to normal timers
|
|
428
|
+
vi.useRealTimers();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("getThreadUserId returns correct userId", async () => {
|
|
432
|
+
const t = convexTest(schema, modules);
|
|
433
|
+
|
|
434
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
435
|
+
userId: "testUserId",
|
|
436
|
+
title: "Test Thread",
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const userId = await t.query(internal.users.getThreadUserId, {
|
|
440
|
+
threadId: thread._id as Id<"threads">,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
expect(userId).toBe("testUserId");
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("getThreadUserId returns null for non-existent thread", async () => {
|
|
447
|
+
const t = convexTest(schema, modules);
|
|
448
|
+
|
|
449
|
+
// Create a thread, then delete it to get a valid but non-existent ID
|
|
450
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
451
|
+
userId: "tempUser",
|
|
452
|
+
title: "Temp Thread",
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Delete the thread so it no longer exists
|
|
456
|
+
await t.action(api.threads.deleteAllForThreadIdSync, {
|
|
457
|
+
threadId: thread._id as Id<"threads">,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Now test with the deleted thread's ID
|
|
461
|
+
const userId = await t.query(internal.users.getThreadUserId, {
|
|
462
|
+
threadId: thread._id as Id<"threads">,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
expect(userId).toBeNull();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("deleteAllForUserId handles user with no threads", async () => {
|
|
469
|
+
const t = convexTest(schema, modules);
|
|
470
|
+
|
|
471
|
+
// Try to delete for a user that doesn't exist
|
|
472
|
+
await expect(
|
|
473
|
+
t.action(api.users.deleteAllForUserId, { userId: "nonexistentUser" })
|
|
474
|
+
).resolves.not.toThrow();
|
|
475
|
+
|
|
476
|
+
// Should complete successfully without errors
|
|
477
|
+
});
|
|
478
|
+
});
|
package/src/component/users.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
query,
|
|
16
16
|
} from "./_generated/server.js";
|
|
17
17
|
import { deleteMessage } from "./messages.js";
|
|
18
|
+
import { deleteStreamsPageForThreadId } from "./streams.js";
|
|
18
19
|
import { schema, v } from "./schema.js";
|
|
19
20
|
|
|
20
21
|
// Note: it only searches for users with threads
|
|
@@ -43,15 +44,30 @@ export const deleteAllForUserId = action({
|
|
|
43
44
|
let threadsCursor = null;
|
|
44
45
|
let threadInProgress = null;
|
|
45
46
|
let messagesCursor = null;
|
|
47
|
+
let streamsInProgress = false;
|
|
48
|
+
let streamOrder = undefined;
|
|
49
|
+
let deltaCursor = undefined;
|
|
46
50
|
let isDone = false;
|
|
47
51
|
while (!isDone) {
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
const result: DeleteAllReturns = await ctx.runMutation(
|
|
53
|
+
internal.users._deletePageForUserId,
|
|
54
|
+
{
|
|
50
55
|
userId: args.userId,
|
|
51
56
|
messagesCursor,
|
|
52
57
|
threadInProgress,
|
|
53
58
|
threadsCursor,
|
|
54
|
-
|
|
59
|
+
streamsInProgress,
|
|
60
|
+
streamOrder,
|
|
61
|
+
deltaCursor,
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
messagesCursor = result.messagesCursor;
|
|
65
|
+
threadInProgress = result.threadInProgress;
|
|
66
|
+
threadsCursor = result.threadsCursor;
|
|
67
|
+
streamsInProgress = result.streamsInProgress ?? false;
|
|
68
|
+
streamOrder = result.streamOrder;
|
|
69
|
+
deltaCursor = result.deltaCursor;
|
|
70
|
+
isDone = result.isDone;
|
|
55
71
|
}
|
|
56
72
|
},
|
|
57
73
|
returns: v.null(),
|
|
@@ -67,6 +83,9 @@ export const deleteAllForUserIdAsync = mutation({
|
|
|
67
83
|
messagesCursor: null,
|
|
68
84
|
threadsCursor: null,
|
|
69
85
|
threadInProgress: null,
|
|
86
|
+
streamsInProgress: false,
|
|
87
|
+
streamOrder: undefined,
|
|
88
|
+
deltaCursor: undefined,
|
|
70
89
|
});
|
|
71
90
|
return isDone;
|
|
72
91
|
},
|
|
@@ -78,12 +97,18 @@ const deleteAllArgs = {
|
|
|
78
97
|
messagesCursor: nullable(v.string()),
|
|
79
98
|
threadsCursor: nullable(v.string()),
|
|
80
99
|
threadInProgress: nullable(v.id("threads")),
|
|
100
|
+
streamsInProgress: v.optional(v.boolean()),
|
|
101
|
+
streamOrder: v.optional(v.number()),
|
|
102
|
+
deltaCursor: v.optional(v.string()),
|
|
81
103
|
};
|
|
82
104
|
type DeleteAllArgs = ObjectType<typeof deleteAllArgs>;
|
|
83
105
|
const deleteAllReturns = {
|
|
84
106
|
threadsCursor: v.string(),
|
|
85
107
|
threadInProgress: nullable(v.id("threads")),
|
|
86
108
|
messagesCursor: nullable(v.string()),
|
|
109
|
+
streamsInProgress: v.optional(v.boolean()),
|
|
110
|
+
streamOrder: v.optional(v.number()),
|
|
111
|
+
deltaCursor: v.optional(v.string()),
|
|
87
112
|
isDone: v.boolean(),
|
|
88
113
|
};
|
|
89
114
|
type DeleteAllReturns = ObjectType<typeof deleteAllReturns>;
|
|
@@ -102,7 +127,12 @@ async function deleteAllForUserIdAsyncHandler(
|
|
|
102
127
|
if (!result.isDone) {
|
|
103
128
|
await ctx.scheduler.runAfter(0, internal.users._deleteAllForUserIdAsync, {
|
|
104
129
|
userId: args.userId,
|
|
105
|
-
|
|
130
|
+
messagesCursor: result.messagesCursor,
|
|
131
|
+
threadsCursor: result.threadsCursor,
|
|
132
|
+
threadInProgress: result.threadInProgress,
|
|
133
|
+
streamsInProgress: result.streamsInProgress ?? false,
|
|
134
|
+
streamOrder: result.streamOrder,
|
|
135
|
+
deltaCursor: result.deltaCursor,
|
|
106
136
|
});
|
|
107
137
|
}
|
|
108
138
|
return result.isDone;
|
|
@@ -120,6 +150,11 @@ async function deletePageForUserId(
|
|
|
120
150
|
let threadInProgress: Id<"threads"> | null = args.threadInProgress;
|
|
121
151
|
let threadsCursor: string | null = args.threadsCursor;
|
|
122
152
|
let messagesCursor: string | null = args.messagesCursor;
|
|
153
|
+
let streamsInProgress: boolean = args.streamsInProgress ?? false;
|
|
154
|
+
let streamOrder: number | undefined = args.streamOrder;
|
|
155
|
+
let deltaCursor: string | undefined = args.deltaCursor;
|
|
156
|
+
|
|
157
|
+
// Phase 1: Get a thread to work on if we don't have one
|
|
123
158
|
if (!threadsCursor || !threadInProgress) {
|
|
124
159
|
const threads = await paginator(ctx.db, schema)
|
|
125
160
|
.query("threads")
|
|
@@ -133,40 +168,85 @@ async function deletePageForUserId(
|
|
|
133
168
|
if (threads.page.length > 0) {
|
|
134
169
|
threadInProgress = threads.page[0]._id;
|
|
135
170
|
messagesCursor = null;
|
|
171
|
+
streamsInProgress = false;
|
|
172
|
+
streamOrder = undefined;
|
|
173
|
+
deltaCursor = undefined;
|
|
136
174
|
} else {
|
|
137
175
|
return {
|
|
138
176
|
isDone: true,
|
|
139
177
|
threadsCursor,
|
|
140
178
|
threadInProgress,
|
|
141
179
|
messagesCursor,
|
|
180
|
+
streamsInProgress,
|
|
181
|
+
streamOrder,
|
|
182
|
+
deltaCursor,
|
|
142
183
|
};
|
|
143
184
|
}
|
|
144
185
|
}
|
|
145
|
-
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
186
|
+
|
|
187
|
+
// Phase 2: Delete messages for the current thread
|
|
188
|
+
if (!streamsInProgress) {
|
|
189
|
+
const messages = await paginator(ctx.db, schema)
|
|
190
|
+
.query("messages")
|
|
191
|
+
.withIndex("threadId_status_tool_order_stepOrder", (q) =>
|
|
192
|
+
q.eq("threadId", threadInProgress!)
|
|
193
|
+
)
|
|
194
|
+
.order("desc")
|
|
195
|
+
.paginate({
|
|
196
|
+
numItems: 100,
|
|
197
|
+
cursor: args.messagesCursor,
|
|
198
|
+
});
|
|
199
|
+
await Promise.all(messages.page.map((m) => deleteMessage(ctx, m)));
|
|
200
|
+
|
|
201
|
+
if (messages.isDone) {
|
|
202
|
+
// Messages are done, move to streams deletion phase
|
|
203
|
+
streamsInProgress = true;
|
|
204
|
+
messagesCursor = null;
|
|
205
|
+
streamOrder = undefined;
|
|
206
|
+
deltaCursor = undefined;
|
|
207
|
+
} else {
|
|
208
|
+
messagesCursor = messages.continueCursor;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
messagesCursor,
|
|
213
|
+
threadsCursor,
|
|
214
|
+
threadInProgress,
|
|
215
|
+
streamsInProgress,
|
|
216
|
+
streamOrder,
|
|
217
|
+
deltaCursor,
|
|
218
|
+
isDone: false,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Phase 3: Delete streams for the current thread
|
|
223
|
+
const streamResult = await deleteStreamsPageForThreadId(ctx, {
|
|
224
|
+
threadId: threadInProgress!,
|
|
225
|
+
streamOrder,
|
|
226
|
+
deltaCursor,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (streamResult.isDone) {
|
|
230
|
+
// Streams are done, delete the thread and reset for next thread
|
|
160
231
|
await ctx.db.delete(threadInProgress);
|
|
161
232
|
threadInProgress = null;
|
|
162
233
|
messagesCursor = null;
|
|
234
|
+
streamsInProgress = false;
|
|
235
|
+
streamOrder = undefined;
|
|
236
|
+
deltaCursor = undefined;
|
|
163
237
|
} else {
|
|
164
|
-
|
|
238
|
+
// Continue with streams deletion
|
|
239
|
+
streamOrder = streamResult.streamOrder;
|
|
240
|
+
deltaCursor = streamResult.deltaCursor;
|
|
165
241
|
}
|
|
242
|
+
|
|
166
243
|
return {
|
|
167
244
|
messagesCursor,
|
|
168
245
|
threadsCursor,
|
|
169
246
|
threadInProgress,
|
|
247
|
+
streamsInProgress,
|
|
248
|
+
streamOrder,
|
|
249
|
+
deltaCursor,
|
|
170
250
|
isDone: false,
|
|
171
251
|
};
|
|
172
252
|
}
|