mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
refactor(dm): rename UntilLimitPager to RelayLoadingCursors
Once the pager was split into the orchestrator (BackwardRelayPager) and the pure per-relay cursor state that lives on the model, "UntilLimitPager" no longer described the latter — it pages nothing, it just records how far each relay has loaded. Rename it (and its test) to RelayLoadingCursors. The geode wire-contract test keeps its name (UntilLimitPagingRelayTest): it pins the relay-side `until`+`limit` paging behaviour, not the class. Pure rename — no behaviour change. Design doc updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b37f1a6e56
commit
c512bd39c4
@@ -41,7 +41,7 @@ Each DM protocol — **NIP-17** gift wraps (kind 1059) and **NIP-04** legacy DMs
|
||||
when all settle, drives the boot spinner.
|
||||
2. **History** — everything *older* than the week floor, paged **backward by
|
||||
`until`+`limit`, per relay, on demand**. Backed by the **per-relay model**
|
||||
(`UntilLimitPager` + `PerRelayLoadTracker`), driven by on-screen markers.
|
||||
(`RelayLoadingCursors` + `PerRelayLoadTracker`), driven by on-screen markers.
|
||||
|
||||
The two are disjoint in time, so re-issuing a history page never re-streams the
|
||||
live tail, and consecutive history pages never re-stream each other.
|
||||
@@ -56,7 +56,7 @@ Accessed from the UI via `accountViewModel.dataSources()` as
|
||||
`.account.giftWrapsHistory`, `.chatroom.nip04History`,
|
||||
`.chatroomList.nip04History`.
|
||||
|
||||
### The history paging primitive: `UntilLimitPager`
|
||||
### The history paging primitive: `RelayLoadingCursors`
|
||||
|
||||
The time-window model can't tell "this relay is empty" from "this is a gap" — a
|
||||
`since`/`until` slice that returns nothing might just be a quiet stretch above
|
||||
@@ -76,7 +76,7 @@ Stop signals: an empty page marks the relay **`done`**. A relay returning fewer
|
||||
than `limit` is treated as its own cap, **not** exhaustion. A misbehaving relay
|
||||
that returns events but none older than already reached (echoing its newest
|
||||
events) is also treated as the bottom, so its marker can't re-request the same
|
||||
window forever. Tested in `UntilLimitPagerTest.kt`.
|
||||
window forever. Tested in `RelayLoadingCursorsTest.kt`.
|
||||
|
||||
### The two completion models (and where each lives)
|
||||
|
||||
@@ -92,7 +92,7 @@ the one-shot fixed-window backfill the live tail does.
|
||||
> in current use only the settle / idle / cap paths ever fire. The REQ-aware
|
||||
> machinery is dormant in production — see "Things to scrutinize".
|
||||
|
||||
**Per-relay model — `UntilLimitPager` + `PerRelayLoadTracker` (all history).**
|
||||
**Per-relay model — `RelayLoadingCursors` + `PerRelayLoadTracker` (all history).**
|
||||
Each relay advances to its next page the instant *it* EOSEs, independent of the
|
||||
others; the subscription layer diffs per relay, so re-issuing only re-REQs the
|
||||
relay whose cursor moved. `loading` starts **`false`** (a `true` start would
|
||||
@@ -231,7 +231,7 @@ pagination, but it lived here because unreachable relays were part of the same
|
||||
out of amethyst so desktop / CLI / any feed can reuse it; in the `jvmAndroid`
|
||||
source set (uses `java.util.concurrent`), visible to amethyst + desktop + quartz's
|
||||
`jvmAndroidTest` (geode in-process relay).
|
||||
- `UntilLimitPager.kt` — per-relay `until`+`limit` cursor. *(+ `UntilLimitPagerTest` in amethyst)*
|
||||
- `RelayLoadingCursors.kt` — per-relay `until`+`limit` cursor. *(+ `RelayLoadingCursorsTest` in amethyst)*
|
||||
- `PerRelayLoadTracker.kt` — per-relay in-flight tracker + silence watchdog.
|
||||
- `WindowLoadTracker.kt` — round/barrier completion tracker (live tail). *(+ silence test in amethyst)*
|
||||
- `RelayPagingProgress.kt` — `(reachedUntil, done, stalled)` per relay.
|
||||
@@ -291,7 +291,7 @@ strings, no app-theme / `java.time` deps.
|
||||
These sections describe earlier iterations, kept for context. The code has
|
||||
moved past all of them.
|
||||
|
||||
### v1 — time-slice history (superseded by `UntilLimitPager`)
|
||||
### v1 — time-slice history (superseded by `RelayLoadingCursors`)
|
||||
|
||||
History was first loaded in bounded `since`+`until` **time slices**
|
||||
(`TimeWindowPagination`, now deleted): `loadMore` fetched only the new band
|
||||
|
||||
+41
-41
@@ -20,90 +20,90 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.relayClient.eoseManagers
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.paging.UntilLimitPager
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.paging.RelayLoadingCursors
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class UntilLimitPagerTest {
|
||||
class RelayLoadingCursorsTest {
|
||||
private val relayA = RelayUrlNormalizer.normalizeOrNull("wss://a.relay")!!
|
||||
private val relayB = RelayUrlNormalizer.normalizeOrNull("wss://b.relay")!!
|
||||
private val start = 1_000L
|
||||
|
||||
@Test
|
||||
fun unarmedRelayIsNotRequestedAndSitsAtTheFloor() {
|
||||
val pager = UntilLimitPager()
|
||||
val cursors = RelayLoadingCursors()
|
||||
// never advanced, so it carries no REQ
|
||||
assertEquals(emptyList<Any>(), pager.armedRelays(listOf(relayA)))
|
||||
assertEquals(emptyList<Any>(), cursors.armedRelays(listOf(relayA)))
|
||||
// marker sits at the floor until it delivers
|
||||
assertEquals(start, pager.reachedUntilFor(relayA, start))
|
||||
assertEquals(start, cursors.reachedUntilFor(relayA, start))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun firstAdvanceRequestsTheFloorThenSubsequentPagesStepBelowReached() {
|
||||
val pager = UntilLimitPager()
|
||||
val cursors = RelayLoadingCursors()
|
||||
|
||||
assertTrue(pager.advance(relayA, start))
|
||||
assertEquals(start, pager.requestedUntilFor(relayA))
|
||||
assertTrue(cursors.advance(relayA, start))
|
||||
assertEquals(start, cursors.requestedUntilFor(relayA))
|
||||
|
||||
// page returns events; oldest seen = 800
|
||||
pager.onEvent(relayA, 900)
|
||||
pager.onEvent(relayA, 800)
|
||||
pager.onEose(relayA)
|
||||
assertEquals(800L, pager.reachedUntilFor(relayA, start))
|
||||
cursors.onEvent(relayA, 900)
|
||||
cursors.onEvent(relayA, 800)
|
||||
cursors.onEose(relayA)
|
||||
assertEquals(800L, cursors.reachedUntilFor(relayA, start))
|
||||
// EOSE does NOT move the requested cursor — the relay parks at the same filter
|
||||
assertEquals(start, pager.requestedUntilFor(relayA))
|
||||
assertEquals(start, cursors.requestedUntilFor(relayA))
|
||||
|
||||
// next advance steps to reached - 1
|
||||
assertTrue(pager.advance(relayA, start))
|
||||
assertEquals(799L, pager.requestedUntilFor(relayA))
|
||||
assertTrue(cursors.advance(relayA, start))
|
||||
assertEquals(799L, cursors.requestedUntilFor(relayA))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun emptyPageMarksRelayDoneAndBlocksFurtherAdvance() {
|
||||
val pager = UntilLimitPager()
|
||||
pager.advance(relayA, start)
|
||||
pager.onEose(relayA) // no events
|
||||
assertTrue(pager.isDone(relayA))
|
||||
assertFalse(pager.advance(relayA, start))
|
||||
assertEquals(emptyList<Any>(), pager.armedRelays(listOf(relayA)))
|
||||
val cursors = RelayLoadingCursors()
|
||||
cursors.advance(relayA, start)
|
||||
cursors.onEose(relayA) // no events
|
||||
assertTrue(cursors.isDone(relayA))
|
||||
assertFalse(cursors.advance(relayA, start))
|
||||
assertEquals(emptyList<Any>(), cursors.armedRelays(listOf(relayA)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aPageThatDoesNotStepOlderEndsTheRelayInsteadOfLooping() {
|
||||
val pager = UntilLimitPager()
|
||||
pager.advance(relayA, start)
|
||||
pager.onEvent(relayA, 800)
|
||||
pager.onEose(relayA)
|
||||
assertEquals(800L, pager.reachedUntilFor(relayA, start))
|
||||
val cursors = RelayLoadingCursors()
|
||||
cursors.advance(relayA, start)
|
||||
cursors.onEvent(relayA, 800)
|
||||
cursors.onEose(relayA)
|
||||
assertEquals(800L, cursors.reachedUntilFor(relayA, start))
|
||||
|
||||
// misbehaving relay: next page echoes an event no older than what we already reached
|
||||
pager.advance(relayA, start) // requested = 799
|
||||
pager.onEvent(relayA, 900) // newer than reached(800) — not strictly older
|
||||
pager.onEose(relayA)
|
||||
assertTrue("a non-advancing page should end the relay, not re-loop", pager.isDone(relayA))
|
||||
assertEquals(800L, pager.reachedUntilFor(relayA, start))
|
||||
cursors.advance(relayA, start) // requested = 799
|
||||
cursors.onEvent(relayA, 900) // newer than reached(800) — not strictly older
|
||||
cursors.onEose(relayA)
|
||||
assertTrue("a non-advancing page should end the relay, not re-loop", cursors.isDone(relayA))
|
||||
assertEquals(800L, cursors.reachedUntilFor(relayA, start))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relaysAreTrackedIndependently() {
|
||||
val pager = UntilLimitPager()
|
||||
pager.advance(relayA, start)
|
||||
pager.onEvent(relayA, 500)
|
||||
pager.onEose(relayA)
|
||||
val cursors = RelayLoadingCursors()
|
||||
cursors.advance(relayA, start)
|
||||
cursors.onEvent(relayA, 500)
|
||||
cursors.onEose(relayA)
|
||||
// B never advanced
|
||||
assertEquals(listOf(relayA), pager.armedRelays(listOf(relayA, relayB)))
|
||||
assertEquals(500L, pager.reachedUntilFor(relayA, start))
|
||||
assertEquals(start, pager.reachedUntilFor(relayB, start))
|
||||
assertEquals(listOf(relayA), cursors.armedRelays(listOf(relayA, relayB)))
|
||||
assertEquals(500L, cursors.reachedUntilFor(relayA, start))
|
||||
assertEquals(start, cursors.reachedUntilFor(relayB, start))
|
||||
// deepest reached across both = A's 500 (B counts as the floor)
|
||||
assertEquals(500L, pager.deepestReached(listOf(relayA, relayB), start))
|
||||
assertEquals(500L, cursors.deepestReached(listOf(relayA, relayB), start))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deepestReachedIsNullWhenNoRelays() {
|
||||
val pager = UntilLimitPager()
|
||||
assertEquals(null, pager.deepestReached(emptyList(), start))
|
||||
val cursors = RelayLoadingCursors()
|
||||
assertEquals(null, cursors.deepestReached(emptyList(), start))
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -30,7 +30,7 @@ import com.vitorpamplona.amethyst.commons.util.KmpLock
|
||||
import com.vitorpamplona.amethyst.commons.util.WeakReference
|
||||
import com.vitorpamplona.amethyst.commons.util.withLock
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.paging.UntilLimitPager
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.paging.RelayLoadingCursors
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -51,7 +51,7 @@ class Chatroom : NotesGatherer {
|
||||
// progress and the cursors share the lifetime of the cached messages. The conversation history
|
||||
// loader binds its (single-active) orchestrator to this. Lazy — most rooms in the rooms list are
|
||||
// never opened for history paging, so they never allocate it.
|
||||
val nip04History by lazy { UntilLimitPager() }
|
||||
val nip04History by lazy { RelayLoadingCursors() }
|
||||
|
||||
// Per-instance lock shared by previously @Synchronized methods.
|
||||
private val syncLock = KmpLock()
|
||||
|
||||
+3
-3
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.commons.model.privateChats
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.paging.UntilLimitPager
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.paging.RelayLoadingCursors
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.utils.cache.LargeCache
|
||||
@@ -39,8 +39,8 @@ class ChatroomList(
|
||||
// lifetime of the cached messages and are dropped when the cache prunes them. The account-level
|
||||
// history loaders bind their orchestrator to these. (Per-conversation NIP-04 cursors live on the
|
||||
// individual [Chatroom] instead.)
|
||||
val giftWrapHistory = UntilLimitPager()
|
||||
val nip04History = UntilLimitPager()
|
||||
val giftWrapHistory = RelayLoadingCursors()
|
||||
val nip04History = RelayLoadingCursors()
|
||||
|
||||
private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom = rooms.getOrCreate(key) { Chatroom() }
|
||||
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ import kotlin.concurrent.Volatile
|
||||
* thread-safe [LargeCache]. Keyed only by [NormalizedRelayUrl], which is `Comparable` and consistent
|
||||
* with `equals`, so the sorted cache identifies relays correctly.
|
||||
*/
|
||||
class UntilLimitPager {
|
||||
class RelayLoadingCursors {
|
||||
private class RelayCursor {
|
||||
// The `until` the REQ carries; null until the relay is first advanced. Moves only in advance().
|
||||
@Volatile var requestedUntil: Long? = null
|
||||
+5
-5
@@ -31,11 +31,11 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Reusable **per-relay backward pagination** engine: pages a set of relays back through history,
|
||||
* **one page at a time, per relay, on demand**, by `until`+`limit` ([UntilLimitPager]) — with each
|
||||
* **one page at a time, per relay, on demand**, by `until`+`limit` ([RelayLoadingCursors]) — with each
|
||||
* relay advancing independently the moment *it* settles, never paced by the slowest one.
|
||||
*
|
||||
* This is the **single-active orchestrator** around the paging state. The state itself (the per-relay
|
||||
* [UntilLimitPager] cursors) does NOT live here — it lives on the owning domain object (a `Chatroom`
|
||||
* [RelayLoadingCursors] cursors) does NOT live here — it lives on the owning domain object (a `Chatroom`
|
||||
* for a conversation, a `ChatroomList` for the account-level feeds), so its lifetime matches the cached
|
||||
* messages it describes. One orchestrator drives whichever scope is on screen; calling [bind] repoints
|
||||
* it at that scope's cursors. This is safe because history relays only ever arm while their on-screen
|
||||
@@ -44,7 +44,7 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
* What it owns (all transient, recomputed on each [bind]): the in-flight + silence tracking
|
||||
* ([PerRelayLoadTracker]), the stalled-relay set, and the display [StateFlow]s ([relayProgress],
|
||||
* [exhausted], [reachedBack], [relayCount], [stalledCount]). The persistent cursors and the pinned
|
||||
* history floor live on the bound [UntilLimitPager].
|
||||
* history floor live on the bound [RelayLoadingCursors].
|
||||
*
|
||||
* What it does NOT own (the caller supplies these — they are protocol- and framework-specific):
|
||||
* - **Building the actual REQ filters.** The caller reads [armedRelays] + [requestedUntilFor] and
|
||||
@@ -81,7 +81,7 @@ class BackwardRelayPager(
|
||||
// The active scope, set by [bind]: its persistent per-relay cursors (which live on the owning domain
|
||||
// object) and the lookup for the relay set it fans out to.
|
||||
@Volatile
|
||||
private var cursors: UntilLimitPager? = null
|
||||
private var cursors: RelayLoadingCursors? = null
|
||||
|
||||
@Volatile
|
||||
private var relaysFor: () -> Collection<NormalizedRelayUrl>? = { null }
|
||||
@@ -133,7 +133,7 @@ class BackwardRelayPager(
|
||||
* instead of restarting.
|
||||
*/
|
||||
fun bind(
|
||||
scopeCursors: UntilLimitPager,
|
||||
scopeCursors: RelayLoadingCursors,
|
||||
scope: CoroutineScope,
|
||||
relaysForScope: () -> Collection<NormalizedRelayUrl>?,
|
||||
) {
|
||||
|
||||
+5
-5
@@ -38,7 +38,7 @@ import kotlin.test.assertTrue
|
||||
* behaviour is covered separately against the in-process relay in `UntilLimitPagingRelayTest`.
|
||||
*
|
||||
* The pager is the **single-active orchestrator**: its per-relay cursors live on a separate
|
||||
* [UntilLimitPager] (in production, on a `Chatroom` / `ChatroomList`), bound in via [bind]. These tests
|
||||
* [RelayLoadingCursors] (in production, on a `Chatroom` / `ChatroomList`), bound in via [bind]. These tests
|
||||
* supply their own cursor object so they can rebind a previously-paged scope and assert what persists
|
||||
* (the cursors) versus what is transient and recomputed (the stalled set, the live flows).
|
||||
*/
|
||||
@@ -55,8 +55,8 @@ class BackwardRelayPagerTest {
|
||||
}
|
||||
|
||||
// A pager bound to a fresh scope of [relays]; returns both so tests can read the pinned cursor floor.
|
||||
private fun pagerOf(vararg relays: NormalizedRelayUrl): Pair<BackwardRelayPager, UntilLimitPager> {
|
||||
val cursors = UntilLimitPager()
|
||||
private fun pagerOf(vararg relays: NormalizedRelayUrl): Pair<BackwardRelayPager, RelayLoadingCursors> {
|
||||
val cursors = RelayLoadingCursors()
|
||||
val p = BackwardRelayPager("test")
|
||||
p.bind(cursors, scope) { relays.toList() }
|
||||
return p to cursors
|
||||
@@ -208,8 +208,8 @@ class BackwardRelayPagerTest {
|
||||
|
||||
@Test
|
||||
fun rebindingRepointsFlowsKeepingDoneCursorsButDroppingTransientStalls() {
|
||||
val cursorsA = UntilLimitPager()
|
||||
val cursorsB = UntilLimitPager()
|
||||
val cursorsA = RelayLoadingCursors()
|
||||
val cursorsB = RelayLoadingCursors()
|
||||
val p = BackwardRelayPager("test")
|
||||
|
||||
// Scope A: r1 bottoms out (done — a persistent cursor fact); r2 auth-walls (stalled — transient).
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Pins down the relay-side contract the whole [UntilLimitPager] / [BackwardRelayPager] design rests on,
|
||||
* Pins down the relay-side contract the whole [RelayLoadingCursors] / [BackwardRelayPager] design rests on,
|
||||
* against the in-process relay: a backward `until`+`limit` walk returns each event **exactly once**
|
||||
* (no re-download), in **newest-first** capped pages, and an **empty page + EOSE** is the gap-proof
|
||||
* stop. If a relay ever stopped honouring this (e.g. oldest-first, or ignoring `until`), these break —
|
||||
|
||||
Reference in New Issue
Block a user