graphprotocol / graphprotocol/indexer
sequentialTimerReduce never publishes updated values, leaving deployment status stuck at its initial state
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 262
- Forks
- 148
- PR merge metrics
- No merged PRs in 30d
Description
Summary
In v0.25.10, sequentialTimerReduce continues executing its reducer but never publishes the resulting values. Consumers of the returned Eventual remain at its initial value.
This affects deployment-status monitoring: an initial synced: false can remain visible indefinitely, making the agent select a remote fallback even when the local deployment is healthy and synced.
Affected code
packages/indexer-common/src/sequential-timer.ts, lines 61–70:
let acc: U = initial
let previousT: T | undefined
let latestT: T | undefined
function outputReduce(value: U) {
previousT = latestT
acc = value
if (!equal(latestT, previousT)) {
output.push(value)
}
}
latestT is never assigned. Both comparison operands remain undefined, so output.push(value) is never reached. Updating acc advances the reducer's private state, but does not update the published Eventual.
Minimal reproduction
Run against the compiled indexer-common package (adjust the module path as needed):
const { sequentialTimerReduce } = require('./dist/sequential-timer.js')
let calls = 0
const status = sequentialTimerReduce(
{ logger: { warn() {} }, milliseconds: 20 },
async () => {
calls++
return { synced: true, health: 'healthy' }
},
{ synced: false, health: 'healthy' },
)
setTimeout(async () => {
console.log({ reducerCalls: calls, observedStatus: await status.value() })
process.exit(0)
}, 100)
Observed with the installed v0.25.10 module:
{
"reducerCalls": 5,
"observedStatus": { "synced": false, "health": "healthy" }
}
The exact number of timer calls is timing-dependent; the defect is that the published status remains unchanged despite every reducer result having synced: true.
Consequences
subgraph-client.ts initializes deployment monitoring with synced: false and selects the local client only when:
const healthy = status.synced && status.health === 'healthy'
Consequently, the client can remain on the configured remote endpoint indefinitely. If that endpoint is stale, the separate freshness checker can repeatedly reject it even though a usable local deployment exists. Restarting does not correct the logic: it recreates the same initial state.
Other callers of this shared utility should also be audited for dependence on published updates. This report does not establish the operational impact of every caller.
Suggested correction
Compare the previous accumulated value with the incoming value:
function outputReduce(value: U) {
const changed = !equal(acc, value)
acc = value
if (changed) {
output.push(value)
}
}
Remove the unused comparison variables/generic if appropriate. The exact equality/publication semantics should be checked against the utility's intended contract, including whether reducers may mutate their accumulator in place.
Suggested regression coverage
- A changed first result is published instead of retaining the initial value.
- Subsequent changed results reach subscribers.
- Unchanged results follow the intended deduplication behavior.
- Both synchronous and asynchronous reducers work.
- A local deployment transitioning to healthy/synced becomes eligible for selection instead of remaining on the remote fallback.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/indexer-common/src/sequential-timer.ts and inspect the reducer and Eventual publication contract, then review its use in packages/indexer-common/src/subgraph-client.ts. Reproduce the issue against the compiled package and check the intended equality and mutation semantics. Done means changed synchronous and asynchronous results publish, unchanged results follow the contract, and local healthy/synced deployment status can be selected.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100