Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

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

未关闭
#2,058 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

@KyleAMathews 已经在做这个了。

开始于 2026年10月6日。

  • #2062 来自 @KyleAMathews —— 未关闭
  • #2064 来自 @KyleAMathews —— 未关闭

评估

难度
4/5
预计耗时
3-5 天
新手友好度
35/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
停滞
技术栈
typescript
领域
backend, testing

调研方向

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.

由索引模型根据 Issue 内容生成。

描述

  • 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.

主要语言
TypeScript
星标
3.9k
派生
268
平均合并
1 天 3 小时
30 天内合并 PR
193

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

TanStack/db 的其他 Issue

查看 TanStack/db 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。