Skip to content

fix(app): keep the Office worker queue moving and bound What's New - #62000

Merged
Hona merged 1 commit into
devfrom
office-lows-one
Nov 5, 2026
Merged

Hona merged 1 commit into
devfrom
office-lows-one

Conversation

@Hona

@Hona Hona commented Nov 5, 2026

Copy link
Copy Markdown
Member

Moves two low-severity lifecycles in gui-plugins into explicit state machines: the Office worker RPC queue (microsoft-office/worker-rpc-machine.ts) and post-update What's New (updater/whats-new-machine.ts).

Bugs

Office worker RPC (worker-rpc.ts)

  • Queue wedge: a handler reply that self.postMessage cannot clone (a detached buffer or an uncloneable value) threw inside .then(onFulfilled). That skipped running = false, so the caller never settled and every later call on the worker hung.
  • Stale current: an unknown method, or a finished call, stayed in state.current across the next macrotask yield.
  • Abort and close looked the same: a caller's own abort rejected with WorkerClosedError, the same class as a dead worker.
  • Leaks and dead cancels:
    • On close or crash, pending calls rejected without detaching their abort listeners, so a later abort posted { cancel } to a terminated worker.
    • A synchronous postMessage throw left the call in pending forever.
    • office-worker.ts added duplicate error/messageerror listeners and aborted its controller before it closed the client.

What's New (renderer.tsx, whats-new.tsx, beta-dialog.tsx)

  • Hang, then clobber: the changelog fetch had no timeout and the chunk import() calls had no .catch. A stalled request could resolve minutes later and open with { replace: true }, which closed every open dialog.
  • Disable ignored: turning Release notes off in Settings while the changelog loaded still opened the dialog.
  • Channel drift: stable waited 500 ms in a timers set before it marked the release seen, but beta marked it at once. The host's whenMounted gate already covers what the timer did.

Machines

// worker-rpc-machine.ts
type WorkerRpcClientState =
  | { status: "live"; pending: ReadonlySet<number> }
  | { status: "closed"; failure: { kind: "closed" | "crashed"; message: string } }

type WorkerRpcServerState =
  | { status: "idle"; attempt: number }
  | { status: "yielding"; attempt: number; queue: readonly RpcRequest[] }
  | { status: "running"; attempt: number; request: RpcRequest; cancelled: boolean; queue: readonly RpcRequest[] }

// whats-new-machine.ts
type WhatsNewState =
  | { status: "loading"; version: string; commit: string | undefined; previousVersion: string }
  | { status: "done" }
stateDiagram-v2
  direction LR
  state "worker queue" as S {
    idle --> yielding: request (schedule-turn)
    yielding --> running: turn.ready [attempt]
    yielding --> idle: cancel empties queue
    running --> running: cancel (abort-handler) / keyed replace in queue
    running --> yielding: handler settles, queue non-empty
    running --> idle: handler settles, queue empty
  }
Loading
What's New: loading + event Effects Release seen?
loaded with content abort-load, mark-seen, open yes
loaded empty, disabled abort-load, mark-seen yes
load.failed, load.timed-out (15 s), disposed abort-load no, so the next start retries
any event in done none
  • Always advances: both drivers apply events one at a time through a queue that shifts after each apply. Each effect runs isolated in Promise.try(...).catch(...). A failed post-request becomes call.post-failed, and a failed ok reply becomes reply.failed, which closes its bitmaps and posts an error reply so the caller settles. A finished call always leaves running in the same transition.
  • Stale results: turn.ready and handler.* carry the attempt, and a stale one only closes its bitmaps.

Deleted

  • ClientState { next, failure }, ServerState { running, current }, and the mutable queue and pending maps (the driver keeps only the callback map and the controllers).
  • The duplicate error/messageerror listeners in office-worker.ts.
  • showWhatsNew / showBetaNotes, the timers set, the 500 ms delay, and the scattered ctx.signal.aborted checks. These are now split into loadWhatsNewHighlights / openWhatsNew and readBetaNotes / openBetaNotes.

Deviations from the design

  • What's New union: collapsed to loading | done. shown, settled, failed and disposed showed nothing different. Channel results merged into one loaded event with a WhatsNewContent union.
  • Server issued field: dropped, because attempt alone fences timers. No cancel-turn effect either: a stale turn.ready is a no-op.
  • AbortedError subclasses WorkerClosedError: current callers in spreadsheet.tsx and presentation.tsx keep ignoring aborts without edits. Those files belong to batch L38, so this PR leaves them alone.

Product calls

  • What's New now stacks: What's New and What's new in beta open above an open dialog instead of replacing it ({ replace: true } removed).
  • Load timeout: 15 s, after which What's New gives up for this start and stays unseen.
  • Mark seen at open: stable marks the release seen when it opens, as beta already did (no 500 ms gap).

Off the hot path: no benchmark.

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none" stroke="currentColor">
  <style>
    .q{animation:q 3s linear infinite}
    .b{animation:b 3s linear infinite}
    @keyframes q{0%,20%{transform:translateX(0)}40%,100%{transform:translateX(-36px)}}
    @keyframes b{0%,45%{opacity:0}55%,90%{opacity:.9}100%{opacity:0}}
    @media (prefers-reduced-motion:reduce){.q,.b{animation:none}.b{opacity:.9}}
  </style>
  <rect x="20" y="24" width="28" height="24" rx="3" opacity=".9"/>
  <path d="M28 36h12" opacity=".6"/>
  <g class="q">
    <rect x="64" y="24" width="28" height="24" rx="3" opacity=".5"/>
    <rect x="100" y="24" width="28" height="24" rx="3" opacity=".5"/>
    <rect x="136" y="24" width="28" height="24" rx="3" opacity=".5"/>
  </g>
  <rect x="164" y="16" width="60" height="40" rx="4" opacity=".35"/>
  <rect class="b" x="176" y="10" width="60" height="40" rx="4" opacity=".9"/>
</svg>

Copilot AI balanced review requested due to automatic review settings November 5, 2026 06:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change looks sound. I reproduced the Office worker queue bug on the base by running the new worker-rpc.test.ts case there: a reply that cannot be cloned throws DataCloneError out of the handler's .then, which takes down the worker in Bun and would hang the queue in a browser. With this PR the caller rejects, the bitmaps are closed and the next call runs. All worker-rpc* and whats-new-machine tests pass (29), and tsc -b for gui-plugins is clean. What's New needs the desktop app, so I checked it only through the machine tests and by reading the code. The Settings watcher only dispatches disabled when release notes are turned off, and the timeout and dispose paths both abort the changelog fetch.

Public API: unchanged. AbortedError and the onCrash option are internal to gui-plugins, and AbortedError extends WorkerClosedError, so existing instanceof checks keep working.

No blocking concerns.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new push rebases and squashes the same change onto newer dev. The patch is unchanged apart from one line of context in renderer.tsx that comes from dev. I re-ran the worker-rpc* and whats-new-machine tests (29 pass), and tsc -b for gui-plugins is clean.

Public API: unchanged.

No new concerns.

@Hona
Hona merged commit b6a5a10 into dev Nov 5, 2026
20 checks passed
@Hona
Hona deleted the office-lows-one branch November 5, 2026 07:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants