@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.
@@ -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
+ });
@@ -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
- ({ messagesCursor, threadInProgress, threadsCursor, isDone } =
49
- await ctx.runMutation(internal.users._deletePageForUserId, {
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
- ...result,
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
- // TODO: make a stream of thread queries and delete those in pages
146
- // then get rid of the "userId" index.
147
- const messages = await paginator(ctx.db, schema)
148
- .query("messages")
149
- .withIndex("threadId_status_tool_order_stepOrder", (q) =>
150
- q.eq("threadId", threadInProgress!)
151
- )
152
- .order("desc")
153
- .paginate({
154
- numItems: 100,
155
- cursor: args.messagesCursor,
156
- });
157
- await Promise.all(messages.page.map((m) => deleteMessage(ctx, m)));
158
- if (messages.isDone) {
159
- // TODO: delete streams - maybe use workpool?
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
- messagesCursor = messages.continueCursor;
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
  }