Files
8b682aa6f6 fix(coinbaseprime): avoid refetching payments on every account fetch cycle (#707)
* fix(coinbaseprime): avoid refetching payments on every account fetch

TASK_FETCH_PAYMENTS was a child of TASK_FETCH_ACCOUNTS, so payments
were re-fetched from scratch on every account sync cycle. Moving it
to a top-level sibling task makes it run independently on its own
schedule instead of being triggered by every account fetch.

Backport of fix from PR #657 (fd62842).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(coinbaseprime): fix cursor reset causing full refetch on every cycle

Without advanceCursor, when Coinbase returns an empty next_cursor at
end-of-pagination the state was reset to an empty cursor, causing the
next run to refetch all transactions from the beginning.

- Extract paymentsState into shared incrementalState in state.go
- Introduce advanceCursor: keep the previous cursor when next_cursor is
  empty so the pagination marker is never wiped

Backport of fix from PR #657 (fd62842).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* test(coinbaseprime): add unit tests for advanceCursor

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

---------

Co-authored-by: Claude <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
2026-05-04 10:15:55 +02:00

32 lines
1.3 KiB
Go

package coinbaseprime
// incrementalState persists the opaque Coinbase Prime pagination cursor
// between fetch cycles. On each call the plugin sends Cursor as the
// `cursor` query param; Coinbase returns records strictly after it.
//
// First-run state is the zero value (empty Cursor) — the client omits the
// query param and Coinbase returns the oldest records first under the
// pinned `sort_direction=ASC`.
//
// The cursor is treated as opaque: no assumption about its format or
// lifetime is made. It is persisted verbatim and updated only when
// Coinbase returns a non-empty `next_cursor`. Coinbase returns an empty
// next_cursor at end-of-pagination; we keep the previous cursor in that
// case so the marker does not reset to the start of history. The cost is
// re-fetching the last page each cycle — the framework dedupes emitted
// records, so this is cheap and safe.
type incrementalState struct {
Cursor string `json:"cursor"`
}
// advanceCursor returns the cursor value to persist after a page
// response. If Coinbase returned a non-empty next_cursor, use it;
// otherwise keep the previous cursor so end-of-pagination does not wipe
// the marker.
func advanceCursor(oldCursor, nextCursor string) string {
if nextCursor != "" {
return nextCursor
}
return oldCursor
}