Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

Debounce and throttle paced mutations allow concurrent persistence despite the documented single-flight contract

Abierto
#2,058 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

@KyleAMathews ya está trabajando en esto.

Desde el 6/10/2026.

  • #2062 de @KyleAMathews — abierto
  • #2064 de @KyleAMathews — abierto

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
Área
backend, testing

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:

  1. Create a paced manager with { wait: 10, leading: true, trailing: true }.
  2. Mutate at time 0 and hold the successful mutationFn promise unresolved.
  3. Mutate again at time 1.
  4. Advance to time 21 without resolving the first write.
  5. 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-lite 0.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

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de TanStack/db

Todos los issues de TanStack/db

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.