Repository navigation
fix: resolve tx.onceSealed() hanging indefinitely - #2793
Conversation
|
@zenabot27 is attempting to deploy a commit to the Flow Team on Vercel. A member of the Team first needs to authorize it. |
🦋 Changeset detectedLatest commit: e5011ff The changes in this PR will be included in the next version bump. This PR includes changesets to release 7 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
3aee7bb to
2d5a56b
Compare
|
Hi @lmcmz, I cannot reproduce this bug. Here is a branch, and summary from tests that I had done by Claude showing my findings: |
|
Hi @jribbink, thanks for testing! The reason you can't reproduce it is that your test uses the default transport (no The bug is in the legacy polling fallback, which only triggers when import { send as httpSend } from "@onflow/transport-http"
fcl.config()
.put("accessNode.api", "https://rest-testnet.onflow.org")
.put("sdk.transport", httpSend) // ← triggers the broken fallback pathWith this config:
Your test never hits this path because without To reproduce, just add |
|
Also worth noting — The gRPC transport README still shows this exact pattern: import { send as transportGRPC } from "@onflow/transport-grpc"
config({ "sdk.transport": transportGRPC })And the SDK changelog uses A quick GitHub search shows many repos still use this configuration: So this is a real regression affecting existing users who haven't migrated to the new |
2d5a56b to
a3ccfdb
Compare
…rence bug The legacy polling actor's SUBSCRIBE handler sends ctx.all() (its internal KVS object) by reference, and the POLL handler mutates it via ctx.merge(). When the new createObservable wrapper (introduced in onflow#2258) stores this reference as `value` and later compares with isDiff(), the deepEqual check short-circuits on `a === b` (same object reference) and always returns false — so status updates are never delivered to subscribers. Fix: spread the objects before sending/broadcasting so consumers get their own copy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
a3ccfdb to
394e1ef
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Thanks for clarifying and identifying this bug @lmcmz. I made some edits to your PR to simplify the fix & address the root cause (the actor was returning a shared mutable reference). |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
876b93f to
60c8cb7
Compare
There was a problem hiding this comment.
Pull request overview
Fixes a regression where fcl.tx(txId).onceSealed() / subscribe() can hang indefinitely when the transaction status stream falls back to legacy polling (legacy sdk.transport / send configuration). The change prevents the polling actor from leaking mutable references that break downstream diffing, ensuring subscribers continue to receive status updates.
Changes:
- Emit cloned transaction status objects from legacy polling actor handlers (SUBSCRIBE, SNAPSHOT, POLL) to avoid leaking the actor’s mutable KVS reference.
- Add a regression test ensuring the initial subscribe value is not mutated by later polls (covers the onceSealed hang scenario).
- Add a changeset for a
@onflow/fcl-corepatch release.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/fcl-core/src/transaction/legacy-polling.js | Clones emitted status objects to prevent reference-mutation from breaking update delivery. |
| packages/fcl-core/src/transaction/legacy-polling.test.js | Adds regression coverage for mutation of the initial subscribe value across polls. |
| .changeset/fix-legacy-polling-hang.md | Declares a patch release note for the legacy polling hang fix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "@onflow/fcl-core": patch | ||
| --- | ||
|
|
||
| Fix `fcl.tx().onceSealed()` hanging when using legacy `sdk.transport` polling fallback |
There was a problem hiding this comment.
The release note says fcl.tx().onceSealed(), but the API requires a transaction id argument (and the issue/PR text consistently references fcl.tx(txId).onceSealed()). Consider updating the note to avoid confusion for readers.
| Fix `fcl.tx().onceSealed()` hanging when using legacy `sdk.transport` polling fallback | |
| Fix `fcl.tx(txId).onceSealed()` hanging when using legacy `sdk.transport` polling fallback |
| if (!isSealed(tx)) poll() | ||
| if (isDiff(prevTx, tx)) ctx.broadcast(UPDATED, tx) | ||
| if (isDiff(prevTx, tx)) ctx.broadcast(UPDATED, {...tx}) | ||
| ctx.merge(tx) |
There was a problem hiding this comment.
These spreads only shallow-clone the transaction status. Nested fields (notably events) are still shared by reference with the internal actor state because ctx.merge(tx) stores tx.events directly. If the intent is to fully prevent consumers from accidentally mutating actor state (or to ensure diffing can’t be affected by nested shared references), consider deep-cloning before emitting (or at least cloning known nested fields like events).
Verifies subscribers receive progressive status updates through sealed, preventing the isDiff short-circuit caused by leaked mutable references. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
60c8cb7 to
e5011ff
Compare
Summary
Fix
fcl.tx(txId).onceSealed()hanging indefinitely when using legacysdk.transport(send function) configuration.Root Cause
The legacy polling actor in
legacy-polling.jsleaks mutable references to its internal KVS viactx.all(). WhencreateObservable(introduced in #2258) stores this reference asvalueand later compares it withisDiff(), thedeepEqualcheck short-circuits ona === b(same object reference) becausectx.merge(tx)has already mutated the KVS in-place between broadcast and subscriber processing. Every status update after the first hasisDiff = false, so subscribers are never notified.Fix
Clone objects before sending them to subscribers — spread
ctx.all()andtxin the SUBSCRIBE, SNAPSHOT, and POLL handlers so consumers get their own copies.Test plan
onceSealed()andsubscribe()work withsdk.transport: httpSendon both testnet and mainnetCloses #2792