@convex-dev/agent 0.1.8-alpha.2 → 0.1.9-alpha.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.
Files changed (40) hide show
  1. package/README.md +3 -0
  2. package/dist/commonjs/component/_generated/api.d.ts +18 -0
  3. package/dist/commonjs/component/messages.d.ts.map +1 -1
  4. package/dist/commonjs/component/messages.js +1 -0
  5. package/dist/commonjs/component/messages.js.map +1 -1
  6. package/dist/commonjs/component/streams.d.ts.map +1 -1
  7. package/dist/commonjs/component/streams.js +0 -1
  8. package/dist/commonjs/component/streams.js.map +1 -1
  9. package/dist/commonjs/component/threads.d.ts.map +1 -1
  10. package/dist/commonjs/component/threads.js +6 -7
  11. package/dist/commonjs/component/threads.js.map +1 -1
  12. package/dist/commonjs/component/users.d.ts +9 -0
  13. package/dist/commonjs/component/users.d.ts.map +1 -1
  14. package/dist/commonjs/component/users.js +92 -21
  15. package/dist/commonjs/component/users.js.map +1 -1
  16. package/dist/commonjs.tsbuildinfo +1 -1
  17. package/dist/esm/component/_generated/api.d.ts +18 -0
  18. package/dist/esm/component/messages.d.ts.map +1 -1
  19. package/dist/esm/component/messages.js +1 -0
  20. package/dist/esm/component/messages.js.map +1 -1
  21. package/dist/esm/component/streams.d.ts.map +1 -1
  22. package/dist/esm/component/streams.js +0 -1
  23. package/dist/esm/component/streams.js.map +1 -1
  24. package/dist/esm/component/threads.d.ts.map +1 -1
  25. package/dist/esm/component/threads.js +6 -7
  26. package/dist/esm/component/threads.js.map +1 -1
  27. package/dist/esm/component/users.d.ts +9 -0
  28. package/dist/esm/component/users.d.ts.map +1 -1
  29. package/dist/esm/component/users.js +92 -21
  30. package/dist/esm/component/users.js.map +1 -1
  31. package/dist/esm.tsbuildinfo +1 -1
  32. package/package.json +3 -3
  33. package/src/client/setup.test.ts +1 -2
  34. package/src/component/_generated/api.d.ts +18 -0
  35. package/src/component/messages.ts +1 -0
  36. package/src/component/streams.ts +1 -3
  37. package/src/component/threads.test.ts +566 -0
  38. package/src/component/threads.ts +10 -6
  39. package/src/component/users.test.ts +478 -0
  40. package/src/component/users.ts +100 -20
@@ -0,0 +1,566 @@
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("threads", () => {
11
+ test("createThread creates a thread with correct data", async () => {
12
+ const t = convexTest(schema, modules);
13
+
14
+ const thread = await t.mutation(api.threads.createThread, {
15
+ userId: "testUser",
16
+ title: "Test Thread",
17
+ summary: "A test thread",
18
+ });
19
+
20
+ expect(thread.userId).toBe("testUser");
21
+ expect(thread.title).toBe("Test Thread");
22
+ expect(thread.summary).toBe("A test thread");
23
+ expect(thread.status).toBe("active");
24
+ expect(thread._id).toBeTruthy();
25
+ expect(thread._creationTime).toBeTruthy();
26
+ });
27
+
28
+ test("getThread returns thread data", async () => {
29
+ const t = convexTest(schema, modules);
30
+
31
+ const created = await t.mutation(api.threads.createThread, {
32
+ userId: "getUser",
33
+ title: "Get Thread",
34
+ });
35
+
36
+ const fetched = await t.query(api.threads.getThread, {
37
+ threadId: created._id as Id<"threads">,
38
+ });
39
+
40
+ expect(fetched).not.toBeNull();
41
+ expect(fetched!._id).toBe(created._id);
42
+ expect(fetched!.userId).toBe("getUser");
43
+ expect(fetched!.title).toBe("Get Thread");
44
+ });
45
+
46
+ test("getThread returns null for non-existent thread", async () => {
47
+ const t = convexTest(schema, modules);
48
+
49
+ // Create a thread, then delete it to get a valid but non-existent ID
50
+ const thread = await t.mutation(api.threads.createThread, {
51
+ userId: "tempUser",
52
+ title: "Temp Thread",
53
+ });
54
+
55
+ // Delete the thread so it no longer exists
56
+ await t.action(api.threads.deleteAllForThreadIdSync, {
57
+ threadId: thread._id as Id<"threads">,
58
+ });
59
+
60
+ // Now test with the deleted thread's ID
61
+ const result = await t.query(api.threads.getThread, {
62
+ threadId: thread._id as Id<"threads">,
63
+ });
64
+
65
+ expect(result).toBeNull();
66
+ });
67
+
68
+ test("listThreadsByUserId returns threads for user", async () => {
69
+ const t = convexTest(schema, modules);
70
+
71
+ // Create threads for different users
72
+ const thread1 = await t.mutation(api.threads.createThread, {
73
+ userId: "listUser",
74
+ title: "Thread 1",
75
+ });
76
+ const thread2 = await t.mutation(api.threads.createThread, {
77
+ userId: "listUser",
78
+ title: "Thread 2",
79
+ });
80
+ const thread3 = await t.mutation(api.threads.createThread, {
81
+ userId: "otherUser",
82
+ title: "Other Thread",
83
+ });
84
+
85
+ const result = await t.query(api.threads.listThreadsByUserId, {
86
+ userId: "listUser",
87
+ });
88
+
89
+ expect(result.page).toHaveLength(2);
90
+ expect(result.page.map((t) => t._id)).toContain(thread1._id);
91
+ expect(result.page.map((t) => t._id)).toContain(thread2._id);
92
+ expect(result.page.map((t) => t._id)).not.toContain(thread3._id);
93
+ });
94
+
95
+ test("listThreadsByUserId pagination works", async () => {
96
+ const t = convexTest(schema, modules);
97
+
98
+ // Create multiple threads
99
+ const threads = [];
100
+ for (let i = 0; i < 5; i++) {
101
+ const thread = await t.mutation(api.threads.createThread, {
102
+ userId: "paginationUser",
103
+ title: `Thread ${i}`,
104
+ });
105
+ threads.push(thread);
106
+ }
107
+
108
+ const firstPage = await t.query(api.threads.listThreadsByUserId, {
109
+ userId: "paginationUser",
110
+ paginationOpts: { cursor: null, numItems: 2 },
111
+ });
112
+
113
+ expect(firstPage.page).toHaveLength(2);
114
+ expect(firstPage.isDone).toBe(false);
115
+
116
+ const secondPage = await t.query(api.threads.listThreadsByUserId, {
117
+ userId: "paginationUser",
118
+ paginationOpts: { cursor: firstPage.continueCursor, numItems: 2 },
119
+ });
120
+
121
+ expect(secondPage.page).toHaveLength(2);
122
+ // Should not have duplicate threads
123
+ const firstPageIds = firstPage.page.map((t) => t._id);
124
+ const secondPageIds = secondPage.page.map((t) => t._id);
125
+ expect(firstPageIds.every((id) => !secondPageIds.includes(id))).toBe(true);
126
+ });
127
+
128
+ test("listThreadsByUserId ordering works", async () => {
129
+ const t = convexTest(schema, modules);
130
+
131
+ // Create threads with slight delays to ensure different creation times
132
+ const thread1 = await t.mutation(api.threads.createThread, {
133
+ userId: "orderUser",
134
+ title: "First Thread",
135
+ });
136
+
137
+ // Small delay to ensure different creation times
138
+ await new Promise((resolve) => setTimeout(resolve, 1));
139
+
140
+ const thread2 = await t.mutation(api.threads.createThread, {
141
+ userId: "orderUser",
142
+ title: "Second Thread",
143
+ });
144
+
145
+ // Test descending order (default)
146
+ const descResult = await t.query(api.threads.listThreadsByUserId, {
147
+ userId: "orderUser",
148
+ order: "desc",
149
+ });
150
+
151
+ expect(descResult.page[0]._id).toBe(thread2._id);
152
+ expect(descResult.page[1]._id).toBe(thread1._id);
153
+
154
+ // Test ascending order
155
+ const ascResult = await t.query(api.threads.listThreadsByUserId, {
156
+ userId: "orderUser",
157
+ order: "asc",
158
+ });
159
+
160
+ expect(ascResult.page[0]._id).toBe(thread1._id);
161
+ expect(ascResult.page[1]._id).toBe(thread2._id);
162
+ });
163
+
164
+ test("updateThread updates thread fields", async () => {
165
+ const t = convexTest(schema, modules);
166
+
167
+ const thread = await t.mutation(api.threads.createThread, {
168
+ userId: "updateUser",
169
+ title: "Original Title",
170
+ summary: "Original Summary",
171
+ });
172
+
173
+ const updated = await t.mutation(api.threads.updateThread, {
174
+ threadId: thread._id as Id<"threads">,
175
+ patch: {
176
+ title: "Updated Title",
177
+ summary: "Updated Summary",
178
+ status: "archived",
179
+ },
180
+ });
181
+
182
+ expect(updated.title).toBe("Updated Title");
183
+ expect(updated.summary).toBe("Updated Summary");
184
+ expect(updated.status).toBe("archived");
185
+ expect(updated._id).toBe(thread._id);
186
+ });
187
+
188
+ test("updateThread with partial patch works", async () => {
189
+ const t = convexTest(schema, modules);
190
+
191
+ const thread = await t.mutation(api.threads.createThread, {
192
+ userId: "partialUser",
193
+ title: "Original Title",
194
+ summary: "Original Summary",
195
+ });
196
+
197
+ const updated = await t.mutation(api.threads.updateThread, {
198
+ threadId: thread._id as Id<"threads">,
199
+ patch: {
200
+ title: "New Title Only",
201
+ },
202
+ });
203
+
204
+ expect(updated.title).toBe("New Title Only");
205
+ expect(updated.summary).toBe("Original Summary"); // Unchanged
206
+ expect(updated.status).toBe("active"); // Unchanged
207
+ });
208
+
209
+ test("updateThread throws error for non-existent thread", async () => {
210
+ const t = convexTest(schema, modules);
211
+
212
+ // Create a thread, then delete it to get a valid but non-existent ID
213
+ const thread = await t.mutation(api.threads.createThread, {
214
+ userId: "tempUser",
215
+ title: "Temp Thread",
216
+ });
217
+
218
+ // Delete the thread so it no longer exists
219
+ await t.action(api.threads.deleteAllForThreadIdSync, {
220
+ threadId: thread._id as Id<"threads">,
221
+ });
222
+
223
+ // Now test updating the deleted thread
224
+ await expect(
225
+ t.mutation(api.threads.updateThread, {
226
+ threadId: thread._id as Id<"threads">,
227
+ patch: { title: "New Title" },
228
+ })
229
+ ).rejects.toThrow();
230
+ });
231
+
232
+ test("deleteAllForThreadIdSync deletes thread and all related data", async () => {
233
+ const t = convexTest(schema, modules);
234
+
235
+ // Create a thread with messages
236
+ const thread = await t.mutation(api.threads.createThread, {
237
+ userId: "deleteUser",
238
+ title: "Thread to Delete",
239
+ });
240
+
241
+ // Add messages to the thread
242
+ await t.mutation(api.messages.addMessages, {
243
+ threadId: thread._id as Id<"threads">,
244
+ messages: [
245
+ { message: { role: "user" as const, content: "Hello" } },
246
+ { message: { role: "assistant" as const, content: "Hi there" } },
247
+ { message: { role: "user" as const, content: "How are you?" } },
248
+ ],
249
+ });
250
+
251
+ // Verify data exists before deletion
252
+ const beforeMessages = await t.query(api.messages.listMessagesByThreadId, {
253
+ threadId: thread._id as Id<"threads">,
254
+ order: "desc",
255
+ paginationOpts: { cursor: null, numItems: 10 },
256
+ });
257
+ expect(beforeMessages.page).toHaveLength(3);
258
+
259
+ const beforeThread = await t.query(api.threads.getThread, {
260
+ threadId: thread._id as Id<"threads">,
261
+ });
262
+ expect(beforeThread).not.toBeNull();
263
+
264
+ // Delete the thread synchronously
265
+ await t.action(api.threads.deleteAllForThreadIdSync, {
266
+ threadId: thread._id as Id<"threads">,
267
+ });
268
+
269
+ // Verify thread is deleted
270
+ const afterThread = await t.query(api.threads.getThread, {
271
+ threadId: thread._id as Id<"threads">,
272
+ });
273
+ expect(afterThread).toBeNull();
274
+
275
+ // Verify messages are deleted (this will return empty page since thread is gone)
276
+ const afterMessages = await t.query(api.messages.listMessagesByThreadId, {
277
+ threadId: thread._id as Id<"threads">,
278
+ order: "desc",
279
+ paginationOpts: { cursor: null, numItems: 10 },
280
+ });
281
+ expect(afterMessages.page).toHaveLength(0);
282
+ });
283
+
284
+ test("deleteAllForThreadIdAsync deletes thread asynchronously", async () => {
285
+ // Enable fake timers for scheduled function testing
286
+ vi.useFakeTimers();
287
+
288
+ const t = convexTest(schema, modules);
289
+
290
+ // Create a thread with messages
291
+ const thread = await t.mutation(api.threads.createThread, {
292
+ userId: "asyncDeleteUser",
293
+ title: "Async Thread to Delete",
294
+ });
295
+
296
+ await t.mutation(api.messages.addMessages, {
297
+ threadId: thread._id as Id<"threads">,
298
+ messages: Array.from({ length: 100 }, (_, i) => ({
299
+ message: { role: "user" as const, content: `Message ${i}` },
300
+ })),
301
+ });
302
+
303
+ // Start async deletion
304
+ const result = await t.mutation(api.threads.deleteAllForThreadIdAsync, {
305
+ threadId: thread._id as Id<"threads">,
306
+ });
307
+
308
+ // If there's more work to do, advance timers and wait for scheduled functions
309
+ if (!result.isDone) {
310
+ // Run all pending timers to trigger scheduled functions
311
+ vi.runAllTimers();
312
+
313
+ // Wait for all scheduled functions to complete
314
+ await t.finishInProgressScheduledFunctions();
315
+ }
316
+
317
+ // Verify thread is deleted
318
+ const afterThread = await t.query(api.threads.getThread, {
319
+ threadId: thread._id as Id<"threads">,
320
+ });
321
+ expect(afterThread).toBeNull();
322
+
323
+ // Verify streams are deleted
324
+ const afterStreams = await t.query(api.streams.list, {
325
+ threadId: thread._id as Id<"threads">,
326
+ });
327
+ expect(afterStreams).toHaveLength(0);
328
+
329
+ // Reset to normal timers
330
+ vi.useRealTimers();
331
+ });
332
+
333
+ test("deletePageForThreadId handles pagination correctly", async () => {
334
+ const t = convexTest(schema, modules);
335
+
336
+ // Create a thread with many messages to test pagination
337
+ const thread = await t.mutation(api.threads.createThread, {
338
+ userId: "paginationDeleteUser",
339
+ title: "Pagination Delete Thread",
340
+ });
341
+
342
+ // Add many messages to force pagination
343
+ const messages = [];
344
+ for (let i = 0; i < 150; i++) {
345
+ messages.push({
346
+ message: { role: "user" as const, content: `Message ${i}` },
347
+ });
348
+ }
349
+ await t.mutation(api.messages.addMessages, {
350
+ threadId: thread._id as Id<"threads">,
351
+ messages,
352
+ });
353
+
354
+ // Delete first page
355
+ const result1 = await t.mutation(internal.threads._deletePageForThreadId, {
356
+ threadId: thread._id as Id<"threads">,
357
+ });
358
+
359
+ expect(result1.isDone).toBe(false);
360
+ expect(result1.cursor).toBeTruthy();
361
+
362
+ // Continue deleting pages until done
363
+ let currentResult = result1;
364
+ let iterations = 0;
365
+ while (!currentResult.isDone && iterations < 10) {
366
+ currentResult = await t.mutation(
367
+ internal.threads._deletePageForThreadId,
368
+ {
369
+ threadId: thread._id as Id<"threads">,
370
+ cursor: currentResult.cursor,
371
+ }
372
+ );
373
+ iterations++;
374
+ }
375
+
376
+ // Thread should be deleted when done
377
+ expect(currentResult.isDone).toBe(true);
378
+ const afterThread = await t.query(api.threads.getThread, {
379
+ threadId: thread._id as Id<"threads">,
380
+ });
381
+ expect(afterThread).toBeNull();
382
+ });
383
+
384
+ test("deletePageForThreadId with custom limit works", async () => {
385
+ const t = convexTest(schema, modules);
386
+
387
+ // Create a thread with messages
388
+ const thread = await t.mutation(api.threads.createThread, {
389
+ userId: "limitUser",
390
+ title: "Limit Test Thread",
391
+ });
392
+
393
+ // Add messages
394
+ const messages = [];
395
+ for (let i = 0; i < 50; i++) {
396
+ messages.push({
397
+ message: { role: "user" as const, content: `Message ${i}` },
398
+ });
399
+ }
400
+ await t.mutation(api.messages.addMessages, {
401
+ threadId: thread._id as Id<"threads">,
402
+ messages,
403
+ });
404
+
405
+ // Delete with custom limit
406
+ const result = await t.mutation(internal.threads._deletePageForThreadId, {
407
+ threadId: thread._id as Id<"threads">,
408
+ limit: 25,
409
+ });
410
+
411
+ // Should still have messages left with this limit
412
+ expect(result.isDone).toBe(false);
413
+ expect(result.cursor).toBeTruthy();
414
+ });
415
+
416
+ test("deleteAllForThreadIdAsync with multiple scheduled iterations", async () => {
417
+ // Enable fake timers for scheduled function testing
418
+ vi.useFakeTimers();
419
+
420
+ const t = convexTest(schema, modules);
421
+
422
+ // Create a thread with many messages to force multiple scheduling iterations
423
+ const thread = await t.mutation(api.threads.createThread, {
424
+ userId: "multiIterDeleteUser",
425
+ title: "Multi Iter Delete Thread",
426
+ });
427
+
428
+ // Add many messages to force pagination and multiple scheduled iterations
429
+ const messages = [];
430
+ for (let i = 0; i < 300; i++) {
431
+ messages.push({
432
+ message: { role: "user" as const, content: `Message ${i}` },
433
+ });
434
+ }
435
+ await t.mutation(api.messages.addMessages, {
436
+ threadId: thread._id as Id<"threads">,
437
+ messages,
438
+ });
439
+
440
+ // Start async deletion
441
+ const result = await t.mutation(api.threads.deleteAllForThreadIdAsync, {
442
+ threadId: thread._id as Id<"threads">,
443
+ });
444
+
445
+ // Should not be done immediately with this many messages
446
+ expect(result.isDone).toBe(false);
447
+
448
+ // Use finishAllScheduledFunctions to handle recursive scheduling
449
+ await t.finishAllScheduledFunctions(vi.runAllTimers);
450
+
451
+ // Verify thread is deleted
452
+ const afterThread = await t.query(api.threads.getThread, {
453
+ threadId: thread._id as Id<"threads">,
454
+ });
455
+ expect(afterThread).toBeNull();
456
+
457
+ // Reset to normal timers
458
+ vi.useRealTimers();
459
+ });
460
+
461
+ test("deleteAllForThreadIdSync handles thread with no messages", async () => {
462
+ const t = convexTest(schema, modules);
463
+
464
+ // Create a thread with no messages
465
+ const thread = await t.mutation(api.threads.createThread, {
466
+ userId: "emptyUser",
467
+ title: "Empty Thread",
468
+ });
469
+
470
+ // Delete should work without errors
471
+ await expect(
472
+ t.action(api.threads.deleteAllForThreadIdSync, {
473
+ threadId: thread._id as Id<"threads">,
474
+ })
475
+ ).resolves.not.toThrow();
476
+
477
+ // Thread should be deleted
478
+ const afterThread = await t.query(api.threads.getThread, {
479
+ threadId: thread._id as Id<"threads">,
480
+ });
481
+ expect(afterThread).toBeNull();
482
+ });
483
+
484
+ test("deleteAllForThreadIdSync handles non-existent thread", async () => {
485
+ const t = convexTest(schema, modules);
486
+
487
+ // Create a thread, then delete it to get a valid but non-existent ID
488
+ const thread = await t.mutation(api.threads.createThread, {
489
+ userId: "tempUser",
490
+ title: "Temp Thread",
491
+ });
492
+
493
+ // Delete the thread so it no longer exists
494
+ await t.action(api.threads.deleteAllForThreadIdSync, {
495
+ threadId: thread._id as Id<"threads">,
496
+ });
497
+
498
+ // Try to delete the already-deleted thread - should not throw
499
+ await expect(
500
+ t.action(api.threads.deleteAllForThreadIdSync, {
501
+ threadId: thread._id as Id<"threads">,
502
+ })
503
+ ).resolves.not.toThrow();
504
+ });
505
+
506
+ test("thread creation sets status to active by default", async () => {
507
+ const t = convexTest(schema, modules);
508
+
509
+ const thread = await t.mutation(api.threads.createThread, {
510
+ userId: "statusUser",
511
+ title: "Status Test Thread",
512
+ });
513
+
514
+ expect(thread.status).toBe("active");
515
+ });
516
+
517
+ test("multiple threads for same user work correctly", async () => {
518
+ const t = convexTest(schema, modules);
519
+
520
+ const threads = [];
521
+ for (let i = 0; i < 3; i++) {
522
+ const thread = await t.mutation(api.threads.createThread, {
523
+ userId: "multiThreadUser",
524
+ title: `Thread ${i + 1}`,
525
+ });
526
+ threads.push(thread);
527
+ }
528
+
529
+ const result = await t.query(api.threads.listThreadsByUserId, {
530
+ userId: "multiThreadUser",
531
+ });
532
+
533
+ expect(result.page).toHaveLength(3);
534
+ expect(result.page.map((t) => t.title)).toContain("Thread 1");
535
+ expect(result.page.map((t) => t.title)).toContain("Thread 2");
536
+ expect(result.page.map((t) => t.title)).toContain("Thread 3");
537
+ });
538
+
539
+ test("threads with different users are isolated", async () => {
540
+ const t = convexTest(schema, modules);
541
+
542
+ const thread1 = await t.mutation(api.threads.createThread, {
543
+ userId: "user1",
544
+ title: "User 1 Thread",
545
+ });
546
+
547
+ const thread2 = await t.mutation(api.threads.createThread, {
548
+ userId: "user2",
549
+ title: "User 2 Thread",
550
+ });
551
+
552
+ const user1Threads = await t.query(api.threads.listThreadsByUserId, {
553
+ userId: "user1",
554
+ });
555
+
556
+ const user2Threads = await t.query(api.threads.listThreadsByUserId, {
557
+ userId: "user2",
558
+ });
559
+
560
+ expect(user1Threads.page).toHaveLength(1);
561
+ expect(user1Threads.page[0]._id).toBe(thread1._id);
562
+
563
+ expect(user2Threads.page).toHaveLength(1);
564
+ expect(user2Threads.page[0]._id).toBe(thread2._id);
565
+ });
566
+ });
@@ -157,12 +157,13 @@ export const deleteAllForThreadIdAsync = mutation({
157
157
  threadId: args.threadId,
158
158
  cursor: result.cursor,
159
159
  });
160
- } else {
161
- // Kick off the streams deletion
162
- await ctx.scheduler.runAfter(0, api.threads.deleteAllForThreadIdSync, {
163
- threadId: args.threadId,
164
- });
165
160
  }
161
+ // Kick off the streams deletion
162
+ await ctx.scheduler.runAfter(
163
+ 0,
164
+ api.streams.deleteAllStreamsForThreadIdSync,
165
+ { threadId: args.threadId }
166
+ );
166
167
  return result;
167
168
  },
168
169
  returns: deleteThreadReturns,
@@ -183,7 +184,10 @@ async function deletePageForThreadIdHandler(
183
184
  });
184
185
  await Promise.all(messages.page.map((m) => deleteMessage(ctx, m)));
185
186
  if (messages.isDone) {
186
- await ctx.db.delete(args.threadId);
187
+ const thread = await ctx.db.get(args.threadId);
188
+ if (thread) {
189
+ await ctx.db.delete(args.threadId);
190
+ }
187
191
  }
188
192
  return {
189
193
  cursor: messages.continueCursor,