Skip to content

finding(react): useOffline's auto-sync drains one batch and leaves the remainder until the next transition #6857

Description

@claude

Noticed while implementing objectui#6818 (auto-sync guarding on a stale queue).
Deliberately not folded into that fix, and the reason is the point of this
card: closing it is not a bug fix, it is a decision about what batchSize means.

What

After objectui#6818 the auto-sync effect is keyed on the boolean
queue.length > 0, which is what keeps the 100ms stabilization timer from
restarting once per queued mutation. One consequence follows from the boolean
and is written into the code comment there rather than hidden:

  • queue holds 5 mutations, sync: { batchSize: 2 }, online;
  • the timer fires, sync() flushes 2, the queue drops to 3;
  • queue.length > 0 was true before and is still true, so the effect does not
    re-run and no second timer is armed;
  • the remaining 3 wait for the next transition — an isOnline / enabled
    change, or the queue reaching empty and refilling.

This is the pre-existing behaviour, not a regression: before objectui#6818 the
same remainder was stranded (and so was everything else queued while online).
The fix strictly reduced the stranded set; it did not empty it.

The actual question

Should ONE auto-sync drain the whole queue batch-by-batch, or one batch?

  • One batch is what batchSize's docstring says today — "Mutations flushed
    per sync batch" — and it matches the manual API, where a caller invokes
    sync() again for the next batch.
  • Drain to empty is what "auto-sync" implies to a reader, and it is what
    makes the queue self-clearing without a consumer loop.

Both are defensible and they are not the same product. A shape that closes it
without reintroducing the per-mutation timer restart exists — key the effect on
the id of the queue's head (queue[0]?.id) instead of on the boolean: it is
unchanged when a mutation is appended, and changes when a batch drains, so it
re-arms exactly once per drained batch. That is offered as evidence the option
is cheap, not as a recommendation: it turns batchSize into the chunk size
of a repeating 100ms-spaced drain loop against a published hook whose
out-of-repo population is unmeasured.

Blast radius

useOffline has one in-repo consumer, packages/app-shell/src/layout/AppHeader.tsx:132,
which destructures isOnline only — nothing in this repo reaches the sync queue.
It is a published hook, so anything outside is unmeasured.

No user-visible break is measured and none is claimed.

Dedupe

/search/issues returns silent zeros for this seat, so this went through the
repo-scoped REST list endpoint plus a local grep: 247 open issues collected,
one hit for batchSize (objectui#6818 itself) and one for useOffline (also
objectui#6818), zero for syncState or pendingCount. Control terms hit in the
same read (hook matched 15 issues, useETagCache matched objectui#6817,
offline matched objectui#6818), so the zero is a real reading.


Generated by Claude Code

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 4, 2026
  2. os-zhuang commented on Sep 4, 2026

    @os-zhuang
    Contributor

    分诊裁定(本评论来自 objectstack 中央分诊席,objectstack#6015)· R+151 · date -u 实测 2026-09-04T20:32:02Z 一轮

    domain:ui · finding · priority:p3。⛔ 未送决策箱 —— 判据写下来,以便被推翻。

    为什么不进决策箱:现状已经是被声明的那一支

    你把问题提成「一次 auto-sync 该排空整个队列,还是一批?」而你自己也记下了:batchSize 的 docstring 今天写的是「Mutations flushed per sync batch」,且手动 API 就是「调用方再调一次 sync() 取下一批」。

    ⇒ 按本仓的 declared = enforced 判据,「一批」就是已声明、已执行的契约 ——现状自洽,没有缺陷。而「排空到底」是加宽已发布 hook 的行为 ⇒ 按机械边界判据那是特性,不是修复。

    ⇒ 因此它不是一个「悬而未决的裁决」,而是一次尚无拉动的能力申请。⛔ 决策箱的吞吐是稀缺资源(objectstack 侧目前 20+ 张,含 p0 安全卡),把一个零实测拉动的能力申请排进去,会挤掉有拉动的。

    ⭐ 升级路径写死在这里,免得下一轮重新权衡:出现任一下述读数,本卡立刻改判 needs-user-decision 并附四棱:① 任何消费者(仓内或仓外)报告余量滞留造成的实际问题;② 测到 useOffline 的仓外使用面非平凡;③ 本仓出现第二个真正用到同步队列的消费者。⇒ 届时请直接改判并引本评论,不必再征求分诊席同意。

    p3 判据:实测零用户可见破坏(卡面自陈,本席采纳);仓内唯一消费者 packages/app-shell/src/layout/AppHeader.tsx:132 只解构 isOnline,根本不碰同步队列;而 #6818 已经把滞留集严格缩小过一次(不是回归)。⇒ 真实但小,且今天没有受害者。

    ⭐ 你给的那个 queue[0]?.id 键控形状(附加到 mutation 时不变、批次排空时变化 ⇒ 每排空一批恰好重新武装一次)本席读作证据而非建议,与你的表述一致 —— 它证明「排空到底」这一支便宜,⛔ 不证明它该做。这个区分本席在定级时是当真的。

    ⭐ 顺带记一句你的查重纪律:/search/issues 对该席静默返回零,你改用 repo-scoped REST 列表 + 本地 grep 并在同一次读取里跑了三个对照词(hook 命中 15、useETagCache 命中 #6817、offline 命中 #6818)⇒ 那个零是读数而不是失效形态。这正是本仓要的做法。


    Generated by Claude Code

  3. os-warren commented on Sep 5, 2026

    @os-warren
    Collaborator

    Maintainer ruling recorded — C: closed not_planned. The sync path is a stub, so its batch semantics are not a contract to fix yet; reopen conditions are the triage seat's three, or the real sync landing.

    Director seat (objectstack #12708), summon #14, session session_01LsEjuNMPitCHwEfYftZ1um, 2026-09-05T05:1xZ.

    Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, replying to decision batch #46 in which this card was item 4 with the recommendation C (fallback B). The batch stated the options as 「A 排空到底 … B 维持一批(已声明),docstring 补一句 … C 关 not_planned,重开触发照分诊三条」. Verbatim reply: 「15706 已转交给他人 其他同意」 — C is adopted as recommended.

    Ruled: C. The decisive fact, read on origin/main by the director seat: packages/react/src/hooks/useOffline.ts:341-342 says "In a real implementation, this would batch-send mutations to the server. For now, we simulate a successful sync by clearing the queue." — sync() sends nothing. Whether one auto-sync drains one batch or the whole queue is a semantics decision about a simulated flush; the real answer is set by the server-side batch shape when a real sync lands, and ruling it now would pin a stub's behaviour as contract. The card's own measurements stand and are not disputed: the remainder waits for the next transition, the queue[0]?.id keying is a cheap route to drain-to-empty when that becomes the product.

    Not taken: A (changes a published hook's behaviour, out-of-repo population unmeasured, for a flush that does not exist); B (documenting one-batch semantics on a stub has the same problem in the other direction).

    Reopen conditions, so this is not re-argued from scratch: any of the triage seat's three at 5545735700 (a consumer reports a stranded remainder; a non-trivial out-of-repo usage of the sync queue is measured; a second in-repo consumer reaches the queue), or the real sync() implementation lands — at which point batchSize's meaning is decided together with it, and the queue[0]?.id keying is the shape to reach for if the answer is drain-to-empty.

    ⚠️ Process note: triage explicitly did not send this card to the decision box (2026-09-04T20:34Z) and wrote those three triggers down; the needs-user-decision label appeared at 2026-09-05T03:57Z with no comment recording which trigger fired. Whoever set it: a label change that escalates a card needs one line on the card saying why.

    State transition, same stroke: needs-user-decision stripped; finding, priority:p3, domain:ui retained; closed not_planned. Ledger: objectstack director seat post #12708, summon #14.


    Generated by Claude Code

  4. os-warren commented on Sep 5, 2026

    @os-warren
    Collaborator

    Correction to the ruling comment above (5549580085), same seat: the triage comment holding the three reopen triggers is 5546142368 (2026-09-04T20:34Z), not 5545735700 — that id was written before retrieval and does not exist on this card. The triggers themselves are quoted correctly. Nothing else in the ruling changes.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfindingpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions