Debounce and throttle paced mutations allow concurrent persistence despite the documented single-flight contract
Los mantenedores suelen responder en 1 día
@KyleAMathews ya está trabajando en esto.
Desde el 6/10/2026.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 35/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Estancado
- Stack tecnológico
- typescript
Línea de trabajo
Read packages/db/src/paced-mutations.ts and the debounce and throttle strategies in packages/db/src/strategies, then run the supplied reproducer with the package's Vitest runner. Extend packages/db/tests/paced-mutations-oracle.test.ts with held-write histories covering the admission cases and release behavior described in the issue. Done means the documented one-persisting-transaction limit holds and admitted transactions settle with optimistic state retained; two open linked pull requests indicate work is already underway.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
- I've validated the bug against the latest version of DB packages
Reproduced against source commit 5688e232eff34eeac555f2586a34672bad25865b (@tanstack/db package version 0.11.3). The affected paced-mutations.ts, debounceStrategy.ts, and throttleStrategy.ts blobs match the current default branch as checked on 2026-10-06. This is source-level validation; the latest published npm packages were not tested.
Describe the bug
Debounce and throttle can start a second persistence call while the first is still pending, contradicting the documented limit of one persisting transaction at a time.
The Paced Mutations guide states:
Debounce/Throttle: Only one pending transaction (collecting mutations) and one persisting transaction (writing to backend) at a time.
For auto-save callers relying on that promise, writes can overlap. This reproduction establishes overlapping requests; it does not simulate server-side reordering or claim a demonstrated stale server value.
To Reproduce
For either debounceStrategy or throttleStrategy:
- Create a paced manager with
{ wait: 10, leading: true, trailing: true }. - Mutate at time 0 and hold the successful
mutationFnpromise unresolved. - Mutate again at time 1.
- Advance to time 21 without resolving the first write.
- Both returned transactions are
persisting.
No rejected persistence, invalid input, same-key interaction, or cleanup race is required.
Save this test as packages/db/tests/paced-persistence-concurrency.test.ts and run it with the package's Vitest runner:
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { createCollection } from '../src/collection'
import { createPacedMutations } from '../src/paced-mutations'
import {
debounceStrategy,
throttleStrategy,
} from '../src/strategies'
import { mockSyncCollectionOptionsNoInitialState } from './utils'
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(1000000)
})
afterEach(() => vi.useRealTimers())
async function ready() {
const collection = createCollection(
mockSyncCollectionOptionsNoInitialState<{ id: number }>({
id: 'review-witness',
getKey: (row) => row.id,
}),
)
const preload = collection.preload()
collection.utils.begin()
collection.utils.commit()
collection.utils.markReady()
await preload
return collection
}
for (const [name, factory] of [
['debounce', debounceStrategy],
['throttle', throttleStrategy],
] as const) {
it(`${name}: public guide permits at most one persisting transaction`, async () => {
const collection = await ready()
const strategy = factory({ wait: 10, leading: true, trailing: true })
const starts: number[] = []
let release!: () => void
const hold = new Promise<void>((resolve) => (release = resolve))
const mutate = createPacedMutations<number, { id: number }>({
strategy,
onMutate: (id) => {
collection.insert({ id })
},
mutationFn: () => {
starts.push(Date.now() - 1000000)
return hold
},
})
const first = mutate(1)
await vi.advanceTimersByTimeAsync(1)
const second = mutate(2)
await vi.advanceTimersByTimeAsync(20)
try {
console.log(
JSON.stringify({
strategy: name,
starts,
states: [first.state, second.state],
}),
)
expect(
[first, second].filter((tx) => tx.state === 'persisting').length,
'docs/guides/mutations.md:1099 one persisting transaction at a time',
).toBeLessThanOrEqual(1)
} finally {
release()
await Promise.all([first.isPersisted.promise, second.isPersisted.promise])
strategy.cleanup()
await collection.cleanup()
}
})
}
Expected behavior
At most one transaction per paced manager is persisting at a time, as promised by the guide. Timer eligibility and actual persistence invocation need distinct treatment while an earlier write remains in flight. The later admitted work must still settle after release.
Observed behavior
| Strategy | Persistence callback starts (ms) | Transaction states at time 21 |
|---|---|---|
| Debounce | [0, 11] |
['persisting', 'persisting'] |
| Throttle | [0, 10] |
['persisting', 'persisting'] |
Both tests reach and fail the concurrency assertion with expected 2 to be less than or equal to 1. Their cleanup releases both writes and awaits successful settlement.
Why the existing oracle misses it
All 42 tests in paced-mutations-oracle.test.ts pass. The ordinary timing driver immediately fulfills writes. Its held debounce/throttle witnesses cover a dropped second call; they do not let an admitted second write reach its timer edge while the first remains in flight. Final successful settlement does not reveal the overlap.
Extend that owner with successful hold/release histories for admitted debounce and throttle calls. Observe concurrent persistence before the next edge, at the edge while the first write is held, and after release. Include leading and non-leading initial admission, receipt settlement, and retained optimistic state. This reproduction covers the two leading-enabled cases; it does not establish closure of the wider class.
A change allowing concurrent persistence would be an explicit product-contract decision with implications for callers. Simply weakening these expectations to match current output would leave the documented promise unresolved.
Environment and validation
- macOS, Node 24.19.0, Vitest 3.2.4,
@tanstack/pacer-lite0.2.1. - Real DB source entry points and fake timers in a Node test host; coverage and typechecking disabled for this bounded component experiment.
- The 42 existing tests passed; the two concurrency witnesses failed at their intended assertions.
- Production and existing oracle files were not modified.
Related background: #35 discusses serialized strategy support. #1060 concerns optimistic value regression; this report isolates simultaneous persistence calls and does not require Electric.
- Lenguaje dominante
- TypeScript
- Estrellas
- 3.9k
- Forks
- 268
- Merge medio
- 1 d 3 h
- PR fusionados (30 d)
- 193
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de TanStack/db
-
Dificultad 3/5 1-2 días Aptitud para principiantes 72/100
Los mantenedores suelen responder en 1 día
-
Dificultad 3/5 1-2 días Aptitud para principiantes 68/100
Los mantenedores suelen responder en 1 día
-
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
TanStack/db#1972 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 45/100
Los mantenedores suelen responder en 1 día
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 45/100
Los mantenedores suelen responder en 1 día
Todos los issues de TanStack/db
Issues similares
-
enhancement
Dificultad 2/5 1-3 horas Aptitud para principiantes 74/100
Los mantenedores suelen responder en 1 día
-
[Bug] remember() with special characters in namespace hangs until timeout instead of returning 400Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
MystenLabs/MemWal#1133 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
bug user-priority/P2
Dificultad 1/5 Menos de una hora Aptitud para principiantes 92/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 Menos de una hora Aptitud para principiantes 88/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
Effect-TS/effect#8881 · 1 comentario ·
Los mantenedores suelen responder en 1 día