|
1 | 1 | import { describe, expect, expectTypeOf, it, vi } from "vitest" |
2 | 2 | import { createCollection, createOptimisticAction } from "../src" |
3 | | -import { createLiveQueryCollection, eq } from "../src/query" |
4 | 3 | import type { |
5 | 4 | MutationFnParams, |
6 | 5 | Transaction, |
@@ -208,357 +207,6 @@ describe(`createOptimisticAction`, () => { |
208 | 207 | expectTypeOf(userAction).returns.toEqualTypeOf<Transaction>() |
209 | 208 | }) |
210 | 209 |
|
211 | | - // Test with syncMode "on-demand" |
212 | | - it(`should call mutationFn when using syncMode on-demand`, async () => { |
213 | | - // This test reproduces the bug where mutationFn is not called |
214 | | - // when the collection is configured with syncMode: "on-demand" |
215 | | - // Bug report: https://discord.com/channels/... |
216 | | - // - onMutate runs but mutationFn never runs |
217 | | - // - works with eager mode but not on-demand |
218 | | - |
219 | | - const onMutateMock = vi.fn() |
220 | | - const mutationFnMock = vi.fn().mockResolvedValue({ success: true }) |
221 | | - |
222 | | - // Create a collection with syncMode: "on-demand" |
223 | | - // This requires a loadSubset handler |
224 | | - const collection = createCollection<{ id: string; text: string }>({ |
225 | | - id: `on-demand-collection`, |
226 | | - getKey: (item) => item.id, |
227 | | - syncMode: `on-demand`, |
228 | | - sync: { |
229 | | - sync: ({ markReady }) => { |
230 | | - // For on-demand mode, we mark ready immediately but don't load data |
231 | | - // Data is loaded on-demand via loadSubset |
232 | | - markReady() |
233 | | - |
234 | | - return { |
235 | | - loadSubset: () => { |
236 | | - // No-op for testing - just return true to indicate sync |
237 | | - return true |
238 | | - }, |
239 | | - } |
240 | | - }, |
241 | | - }, |
242 | | - }) |
243 | | - |
244 | | - // Create an optimistic action |
245 | | - const addTodo = createOptimisticAction<string>({ |
246 | | - onMutate: (text) => { |
247 | | - collection.insert({ id: `1`, text }) |
248 | | - onMutateMock(text) |
249 | | - }, |
250 | | - mutationFn: async (text, params) => { |
251 | | - return Promise.resolve(mutationFnMock(text, params)) |
252 | | - }, |
253 | | - }) |
254 | | - |
255 | | - // Execute the optimistic action |
256 | | - const transaction = addTodo(`Test Todo`) |
257 | | - |
258 | | - // Verify onMutate was called |
259 | | - expect(onMutateMock).toHaveBeenCalledWith(`Test Todo`) |
260 | | - |
261 | | - // Verify the optimistic update was applied to the collection |
262 | | - expect(collection.get(`1`)).toEqual({ id: `1`, text: `Test Todo` }) |
263 | | - |
264 | | - // Wait for the mutation to complete |
265 | | - await transaction.isPersisted.promise |
266 | | - |
267 | | - // BUG: mutationFn should be called but it's not! |
268 | | - expect(mutationFnMock).toHaveBeenCalledTimes(1) |
269 | | - }) |
270 | | - |
271 | | - // Test with syncMode "on-demand" where collection has NOT started sync yet |
272 | | - it(`should call mutationFn when collection is not started (idle)`, async () => { |
273 | | - // This test checks if mutationFn is called when the collection hasn't started sync |
274 | | - const onMutateMock = vi.fn() |
275 | | - const mutationFnMock = vi.fn().mockResolvedValue({ success: true }) |
276 | | - |
277 | | - // Create a collection that doesn't start sync automatically |
278 | | - const collection = createCollection<{ id: string; text: string }>({ |
279 | | - id: `idle-collection`, |
280 | | - getKey: (item) => item.id, |
281 | | - startSync: false, |
282 | | - sync: { |
283 | | - sync: () => { |
284 | | - // No-op sync for testing |
285 | | - }, |
286 | | - }, |
287 | | - }) |
288 | | - |
289 | | - // Create an optimistic action |
290 | | - const addTodo = createOptimisticAction<string>({ |
291 | | - onMutate: (text) => { |
292 | | - collection.insert({ id: `1`, text }) |
293 | | - onMutateMock(text) |
294 | | - }, |
295 | | - mutationFn: async (text, params) => { |
296 | | - return Promise.resolve(mutationFnMock(text, params)) |
297 | | - }, |
298 | | - }) |
299 | | - |
300 | | - // Execute the optimistic action (collection is in idle state) |
301 | | - const transaction = addTodo(`Test Todo`) |
302 | | - |
303 | | - // Verify onMutate was called |
304 | | - expect(onMutateMock).toHaveBeenCalledWith(`Test Todo`) |
305 | | - |
306 | | - // Verify the optimistic update was applied |
307 | | - expect(collection.get(`1`)).toEqual({ id: `1`, text: `Test Todo` }) |
308 | | - |
309 | | - // Wait for the mutation to complete |
310 | | - await transaction.isPersisted.promise |
311 | | - |
312 | | - // mutationFn should be called |
313 | | - expect(mutationFnMock).toHaveBeenCalledTimes(1) |
314 | | - }) |
315 | | - |
316 | | - // Test with syncMode "on-demand" where sync is in loading state (not ready yet) |
317 | | - it(`should call mutationFn when collection is loading (not ready)`, async () => { |
318 | | - // This test checks if mutationFn is called when the collection is still loading |
319 | | - const onMutateMock = vi.fn() |
320 | | - const mutationFnMock = vi.fn().mockResolvedValue({ success: true }) |
321 | | - |
322 | | - // Create a collection that starts sync but doesn't call markReady |
323 | | - const collection = createCollection<{ id: string; text: string }>({ |
324 | | - id: `loading-collection`, |
325 | | - getKey: (item) => item.id, |
326 | | - startSync: true, |
327 | | - sync: { |
328 | | - sync: () => { |
329 | | - // Intentionally don't call markReady - collection stays in "loading" state |
330 | | - return () => {} |
331 | | - }, |
332 | | - }, |
333 | | - }) |
334 | | - |
335 | | - // Create an optimistic action |
336 | | - const addTodo = createOptimisticAction<string>({ |
337 | | - onMutate: (text) => { |
338 | | - collection.insert({ id: `1`, text }) |
339 | | - onMutateMock(text) |
340 | | - }, |
341 | | - mutationFn: async (text, params) => { |
342 | | - return Promise.resolve(mutationFnMock(text, params)) |
343 | | - }, |
344 | | - }) |
345 | | - |
346 | | - // Execute the optimistic action (collection is in loading state) |
347 | | - const transaction = addTodo(`Test Todo`) |
348 | | - |
349 | | - // Verify onMutate was called |
350 | | - expect(onMutateMock).toHaveBeenCalledWith(`Test Todo`) |
351 | | - |
352 | | - // Verify the optimistic update was applied |
353 | | - expect(collection.get(`1`)).toEqual({ id: `1`, text: `Test Todo` }) |
354 | | - |
355 | | - // Wait for the mutation to complete |
356 | | - await transaction.isPersisted.promise |
357 | | - |
358 | | - // mutationFn should be called |
359 | | - expect(mutationFnMock).toHaveBeenCalledTimes(1) |
360 | | - }) |
361 | | - |
362 | | - // Test with on-demand collection and a live query with filters - the reported scenario |
363 | | - it(`should call mutationFn with on-demand collection and live query filter`, async () => { |
364 | | - // This test attempts to reproduce the exact bug scenario: |
365 | | - // - Base collection with syncMode: "on-demand" |
366 | | - // - Live query collection with filters on top |
367 | | - // - createOptimisticAction used to mutate the base collection |
368 | | - // - Bug: onMutate runs but mutationFn never runs |
369 | | - |
370 | | - const onMutateMock = vi.fn() |
371 | | - const mutationFnMock = vi.fn().mockResolvedValue({ success: true }) |
372 | | - |
373 | | - // Create a base collection with syncMode: "on-demand" |
374 | | - type Todo = { id: string; text: string; status: string } |
375 | | - const baseCollection = createCollection<Todo>({ |
376 | | - id: `on-demand-base-collection`, |
377 | | - getKey: (item) => item.id, |
378 | | - syncMode: `on-demand`, |
379 | | - startSync: true, |
380 | | - sync: { |
381 | | - sync: ({ markReady, begin, write, commit }) => { |
382 | | - // Simulate on-demand mode: mark ready immediately, load data on demand |
383 | | - markReady() |
384 | | - |
385 | | - // Pre-populate with some data |
386 | | - begin() |
387 | | - write({ |
388 | | - type: `insert`, |
389 | | - value: { id: `1`, text: `Existing todo`, status: `active` }, |
390 | | - }) |
391 | | - commit() |
392 | | - |
393 | | - return { |
394 | | - loadSubset: () => { |
395 | | - return true |
396 | | - }, |
397 | | - } |
398 | | - }, |
399 | | - }, |
400 | | - }) |
401 | | - |
402 | | - // Create a live query collection with a filter on status |
403 | | - const activeTodos = createLiveQueryCollection({ |
404 | | - id: `active-todos-query`, |
405 | | - startSync: true, |
406 | | - query: (q) => |
407 | | - q |
408 | | - .from({ todo: baseCollection }) |
409 | | - .where(({ todo }) => eq(todo.status, `active`)) |
410 | | - .select(({ todo }) => ({ todo })), |
411 | | - }) |
412 | | - |
413 | | - // Wait for the live query to be ready |
414 | | - await activeTodos.preload() |
415 | | - |
416 | | - // Verify initial state |
417 | | - expect([...activeTodos.values()].length).toBe(1) |
418 | | - expect([...activeTodos.values()][0]?.todo.text).toBe(`Existing todo`) |
419 | | - |
420 | | - // Create an optimistic action to INSERT a new todo |
421 | | - const addTodo = createOptimisticAction<{ text: string }>({ |
422 | | - onMutate: (input) => { |
423 | | - baseCollection.insert({ id: `2`, text: input.text, status: `active` }) |
424 | | - onMutateMock(input) |
425 | | - }, |
426 | | - mutationFn: async (input, params) => { |
427 | | - return Promise.resolve(mutationFnMock(input, params)) |
428 | | - }, |
429 | | - }) |
430 | | - |
431 | | - // Execute the optimistic action |
432 | | - const transaction = addTodo({ text: `New todo` }) |
433 | | - |
434 | | - // Verify onMutate was called |
435 | | - expect(onMutateMock).toHaveBeenCalledWith({ text: `New todo` }) |
436 | | - |
437 | | - // Verify the optimistic update was applied to the base collection |
438 | | - expect(baseCollection.get(`2`)).toEqual({ |
439 | | - id: `2`, |
440 | | - text: `New todo`, |
441 | | - status: `active`, |
442 | | - }) |
443 | | - |
444 | | - // Wait for the mutation to complete |
445 | | - await transaction.isPersisted.promise |
446 | | - |
447 | | - // BUG: mutationFn should be called! |
448 | | - expect(mutationFnMock).toHaveBeenCalledTimes(1) |
449 | | - }) |
450 | | - |
451 | | - // Test UPDATE scenario which might have different behavior |
452 | | - it(`should call mutationFn when UPDATE is performed on on-demand collection`, async () => { |
453 | | - const onMutateMock = vi.fn() |
454 | | - const mutationFnMock = vi.fn().mockResolvedValue({ success: true }) |
455 | | - |
456 | | - type Todo = { id: string; text: string; status: string } |
457 | | - const collection = createCollection<Todo>({ |
458 | | - id: `on-demand-update-collection`, |
459 | | - getKey: (item) => item.id, |
460 | | - syncMode: `on-demand`, |
461 | | - startSync: true, |
462 | | - sync: { |
463 | | - sync: ({ markReady, begin, write, commit }) => { |
464 | | - markReady() |
465 | | - |
466 | | - // Pre-populate with data |
467 | | - begin() |
468 | | - write({ |
469 | | - type: `insert`, |
470 | | - value: { id: `1`, text: `Original text`, status: `active` }, |
471 | | - }) |
472 | | - commit() |
473 | | - |
474 | | - return { |
475 | | - loadSubset: () => true, |
476 | | - } |
477 | | - }, |
478 | | - }, |
479 | | - }) |
480 | | - |
481 | | - // Create an optimistic action to UPDATE an existing todo |
482 | | - const updateTodo = createOptimisticAction<{ id: string; text: string }>({ |
483 | | - onMutate: (input) => { |
484 | | - collection.update(input.id, (draft) => { |
485 | | - draft.text = input.text |
486 | | - }) |
487 | | - onMutateMock(input) |
488 | | - }, |
489 | | - mutationFn: async (input, params) => { |
490 | | - return Promise.resolve(mutationFnMock(input, params)) |
491 | | - }, |
492 | | - }) |
493 | | - |
494 | | - // Execute the optimistic action |
495 | | - const transaction = updateTodo({ id: `1`, text: `Updated text` }) |
496 | | - |
497 | | - // Verify onMutate was called |
498 | | - expect(onMutateMock).toHaveBeenCalledWith({ id: `1`, text: `Updated text` }) |
499 | | - |
500 | | - // Verify the optimistic update was applied |
501 | | - expect(collection.get(`1`)?.text).toBe(`Updated text`) |
502 | | - |
503 | | - // Wait for the mutation to complete |
504 | | - await transaction.isPersisted.promise |
505 | | - |
506 | | - // mutationFn should be called |
507 | | - expect(mutationFnMock).toHaveBeenCalledTimes(1) |
508 | | - }) |
509 | | - |
510 | | - // Debug test: verify mutations array is populated correctly |
511 | | - it(`should have mutations in the transaction after onMutate completes`, async () => { |
512 | | - const onMutateMock = vi.fn() |
513 | | - const mutationFnMock = vi.fn().mockResolvedValue({ success: true }) |
514 | | - |
515 | | - // Create an on-demand collection with live query filter |
516 | | - type Todo = { id: string; text: string; status: string } |
517 | | - const baseCollection = createCollection<Todo>({ |
518 | | - id: `debug-collection`, |
519 | | - getKey: (item) => item.id, |
520 | | - syncMode: `on-demand`, |
521 | | - startSync: true, |
522 | | - sync: { |
523 | | - sync: ({ markReady }) => { |
524 | | - markReady() |
525 | | - return { |
526 | | - loadSubset: () => true, |
527 | | - } |
528 | | - }, |
529 | | - }, |
530 | | - }) |
531 | | - |
532 | | - // Track mutations count at mutationFn call time |
533 | | - let mutationsAtMutationFn: number | undefined |
534 | | - |
535 | | - const addTodo = createOptimisticAction<{ text: string }>({ |
536 | | - onMutate: (input) => { |
537 | | - baseCollection.insert({ id: `1`, text: input.text, status: `active` }) |
538 | | - onMutateMock(input) |
539 | | - }, |
540 | | - mutationFn: async (input, params) => { |
541 | | - // Record the number of mutations at this point |
542 | | - mutationsAtMutationFn = params.transaction.mutations.length |
543 | | - return Promise.resolve(mutationFnMock(input, params)) |
544 | | - }, |
545 | | - }) |
546 | | - |
547 | | - const transaction = addTodo({ text: `Test` }) |
548 | | - |
549 | | - // Verify onMutate was called |
550 | | - expect(onMutateMock).toHaveBeenCalled() |
551 | | - |
552 | | - // Wait for the transaction to complete |
553 | | - await transaction.isPersisted.promise |
554 | | - |
555 | | - // Verify mutationFn was called |
556 | | - expect(mutationFnMock).toHaveBeenCalledTimes(1) |
557 | | - |
558 | | - // Verify there was at least one mutation |
559 | | - expect(mutationsAtMutationFn).toBeGreaterThan(0) |
560 | | - }) |
561 | | - |
562 | 210 | // Test error handling |
563 | 211 | it(`should handle errors in mutationFn correctly`, async () => { |
564 | 212 | // Setup a mock collection |
|
0 commit comments