|  | 
| 1 | 1 | <script lang="ts"> | 
| 2 |  | -import { useQuery, useConvexClient } from '$lib/client.svelte.js'; | 
| 3 |  | -import type { Doc } from '../../convex/_generated/dataModel.js'; | 
| 4 |  | -import { api } from '../../convex/_generated/api.js'; | 
|  | 2 | +import { useQuery, useConvexClient } from '$lib/client.svelte.js'; | 
|  | 3 | +import type { Doc } from '../../convex/_generated/dataModel.js'; | 
|  | 4 | +import { api } from '../../convex/_generated/api.js'; | 
| 5 | 5 | 
 | 
| 6 |  | -const convex = useConvexClient(); | 
| 7 |  | -const serverNumbers = useQuery(api.numbers.get, {}); | 
|  | 6 | +const convex = useConvexClient(); | 
|  | 7 | +const serverNumbers = useQuery(api.numbers.get, {}); | 
| 8 | 8 | 
 | 
| 9 |  | -let numbers = $state(serverNumbers.isLoading ? {} : { a: serverNumbers.a, b: serverNumbers.b, c: serverNumbers.c }); | 
| 10 |  | -let pendingMutations = $state(0); | 
| 11 |  | -let lastMutationPromise: Promise<any> | null = $state(null); | 
| 12 |  | -let hasUnsentChanges = $state(false); // Track if we have changes waiting in debounce | 
|  | 9 | +let numbers = $state(null); | 
|  | 10 | +// Have some changes not yet been sent? | 
|  | 11 | +let hasUnsentChanges = $state(false); | 
|  | 12 | +// Does delivered server state not yet reflect all local changes? | 
|  | 13 | +let hasUnsavedChanges = $state(false); | 
|  | 14 | +let mutationInFlight = $state(false); | 
| 13 | 15 | 
 | 
| 14 |  | -// Stay in sync with server data only when no mutations are pending and there are now changes waiting to be sent | 
| 15 |  | -$effect(() => { | 
| 16 |  | - if (!serverNumbers.isLoading && serverNumbers.data &&  | 
| 17 |  | -pendingMutations === 0 && !hasUnsentChanges) { | 
| 18 |  | -console.log('Received data from server:', { | 
| 19 |  | - a: serverNumbers.data.a, | 
| 20 |  | - b: serverNumbers.data.b, | 
| 21 |  | - c: serverNumbers.data.c, | 
| 22 |  | -}); | 
| 23 |  | -numbers.a = serverNumbers.data.a; | 
| 24 |  | -numbers.b = serverNumbers.data.b; | 
| 25 |  | -numbers.c = serverNumbers.data.c; | 
| 26 |  | - } | 
| 27 |  | -}); | 
|  | 16 | +// Initialize local state when server data first arrives | 
|  | 17 | +$effect(() => { | 
|  | 18 | + if (!serverNumbers.isLoading && serverNumbers.data && !numbers) { | 
|  | 19 | + numbers = { ...serverNumbers.data }; | 
|  | 20 | + } | 
|  | 21 | +}); | 
| 28 | 22 | 
 | 
| 29 |  | -// Queue updates and track pending mutations | 
| 30 |  | -async function queueMutation() { | 
| 31 |  | - if (serverNumbers.isLoading) return; | 
|  | 23 | +// Update local state with server data | 
|  | 24 | +$effect(() => { | 
|  | 25 | + if (!hasUnsavedChanges && !serverNumbers.isLoading && serverNumbers.data) { | 
|  | 26 | + numbers = { ...serverNumbers.data }; | 
|  | 27 | + } | 
|  | 28 | +}); | 
| 32 | 29 | 
 | 
| 33 |  | - pendingMutations++; | 
| 34 |  | - hasUnsentChanges = false; | 
|  | 30 | +async function publishChanges() { | 
|  | 31 | + hasUnsentChanges = true; | 
|  | 32 | + hasUnsavedChanges = true; | 
|  | 33 | + if (!numbers || mutationInFlight) return; | 
| 35 | 34 | 
 | 
| 36 |  | - console.log('Updating server with', numbers, pendingMutations, 'mutations pending'); | 
| 37 |  | - const currentMutation = convex.mutation(api.numbers.update, { | 
| 38 |  | -a: numbers.a, | 
| 39 |  | -b: numbers.b, | 
| 40 |  | -c: numbers.c | 
| 41 |  | - }); | 
|  | 35 | + hasUnsentChanges = false; | 
|  | 36 | + mutationInFlight = true | 
|  | 37 | + await convex.mutation(api.numbers.update, numbers); | 
|  | 38 | + mutationInFlight = false | 
| 42 | 39 | 
 | 
| 43 |  | - lastMutationPromise = currentMutation; | 
|  | 40 | + if (hasUnsentChanges) { | 
|  | 41 | +publishChanges(); | 
|  | 42 | + } else { | 
|  | 43 | +hasUnsavedChanges = false; | 
|  | 44 | + } | 
|  | 45 | +} | 
| 44 | 46 | 
 | 
| 45 |  | - try { | 
| 46 |  | -await currentMutation; | 
| 47 |  | -console.log('saved to server'); | 
| 48 |  | - } finally { | 
| 49 |  | -pendingMutations--; | 
| 50 |  | -
 | 
| 51 |  | -// If this was the last mutation in the queue, | 
| 52 |  | -// explicitly sync with server state | 
| 53 |  | -if (pendingMutations === 0 && !hasUnsentChanges &&  | 
| 54 |  | - serverNumbers.data && currentMutation === lastMutationPromise) { | 
| 55 |  | - console.log('finished persisting state to server, back to following useQuery'); | 
| 56 |  | - numbers.a = serverNumbers.data.a; | 
| 57 |  | - numbers.b = serverNumbers.data.b; | 
| 58 |  | - numbers.c = serverNumbers.data.c; | 
| 59 |  | -} | 
| 60 |  | - } | 
| 61 |  | -} | 
|  | 47 | +function handleNumericInput(prop, e) { | 
|  | 48 | + numbers[prop] = e.currentTarget.valueAsNumber | 
|  | 49 | + publishChanges(); | 
|  | 50 | +}; | 
| 62 | 51 | 
 | 
| 63 |  | -// Track changes immediately but debounce the actual mutation | 
| 64 |  | -let updateTimeout: number | undefined; | 
| 65 |  | -$effect(() => { | 
| 66 |  | - if (serverNumbers.isLoading) return; | 
| 67 |  | -
 | 
| 68 |  | - // reference values so this is reactive on them | 
| 69 |  | - const currentValues = { | 
| 70 |  | -a: numbers.a, | 
| 71 |  | -b: numbers.b, | 
| 72 |  | -c: numbers.c | 
| 73 |  | - }; | 
| 74 |  | - hasUnsentChanges = true; | 
| 75 |  | -  | 
| 76 |  | - clearTimeout(updateTimeout); | 
| 77 |  | - updateTimeout = setTimeout(queueMutation, 500) as unknown as number; | 
| 78 |  | -
 | 
| 79 |  | - return () => clearTimeout(updateTimeout); | 
| 80 |  | -}); | 
| 81 | 52 | </script> | 
| 82 | 53 | 
 | 
| 83 | 54 | <div class="numbers"> | 
| 84 |  | - {#if serverNumbers.isLoading} | 
|  | 55 | + {#if serverNumbers.isLoading || !numbers} | 
| 85 | 56 |  <div> | 
| 86 | 57 |  <p>Loading values...</p> | 
| 87 | 58 |  </div> | 
|  | 
| 91 | 62 |  <input  | 
| 92 | 63 |  id="a" | 
| 93 | 64 |  type="number" | 
| 94 |  | - bind:value={numbers.a} | 
|  | 65 | +oninput={(e) => handleNumericInput('a', e)} | 
|  | 66 | +value={numbers.a} | 
| 95 | 67 |  /> | 
| 96 | 68 |  </div> | 
| 97 | 69 | 
 | 
|  | 
| 100 | 72 |  <input  | 
| 101 | 73 |  id="b" | 
| 102 | 74 |  type="number" | 
| 103 |  | - bind:value={numbers.b} | 
|  | 75 | +oninput={(e) => handleNumericInput('b', e)} | 
|  | 76 | +value={numbers.b} | 
| 104 | 77 |  /> | 
| 105 | 78 |  </div> | 
| 106 | 79 | 
 | 
|  | 
| 109 | 82 |  <input  | 
| 110 | 83 |  id="c" | 
| 111 | 84 |  type="number" | 
| 112 |  | - bind:value={numbers.c} | 
|  | 85 | +oninput={(e) => handleNumericInput('c', e)} | 
|  | 86 | +value={numbers.c} | 
| 113 | 87 |  /> | 
| 114 | 88 |  </div> | 
| 115 | 89 | 
 | 
|  | 
0 commit comments