Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
2baa418
fix: run public pubky reads outside the paykit operation lock
Jasonvdb Sep 30, 2026
3678128
fix: list ring identities at once and reuse their profiles on adopt
Jasonvdb Sep 30, 2026
5cefe41
fix: show cached profile header and disk-cache pubky avatars
Jasonvdb Sep 30, 2026
139738d
fix: make pubky profile retries explicit and drop them for bulk loads
Jasonvdb Sep 30, 2026
a3b935c
fix: stop holding the paykit lock for reads and adopt republish
Jasonvdb Sep 30, 2026
012edee
fix: keep ring row lookups running and read the handoff after sign-in
Jasonvdb Sep 30, 2026
445d58c
fix: skip stale avatar cache writes and decide cached header in vm
Jasonvdb Sep 30, 2026
36d565c
test: add pubky profile journeys for cached header and ring rows
Jasonvdb Sep 30, 2026
2b719bc
Merge master into claude/flow-vibe-pubky-profile-load-lag
Jasonvdb Sep 30, 2026
951ad1f
fix: refresh the profile on open and tolerate avatar cache errors
Jasonvdb Sep 30, 2026
392b948
chore: add changelog fragment for pubky profile loading
Jasonvdb Sep 30, 2026
979e1fc
fix: drop stale profile cache writes that race a save or sign out
Jasonvdb Sep 30, 2026
1f4364f
fix: show which ring rows are still looking up their profile
Jasonvdb Oct 1, 2026
c5ef815
fix: wait for an in-flight identity publication before approval
Jasonvdb Oct 1, 2026
330579d
fix: reload the profile on open when the in-flight load fails
Jasonvdb Oct 1, 2026
1bab353
fix: read paykit receiver markers outside the operation lock
Jasonvdb Oct 1, 2026
1c4552e
fix: keep two pubky read slots free of bulk reads
Jasonvdb Oct 1, 2026
d37e07e
fix: import pending contacts without resolving them again
Jasonvdb Oct 1, 2026
32bfc1c
fix: keep a contact import running after leaving its screen
Jasonvdb Oct 1, 2026
9f9782b
fix: show saved contacts before their profiles resolve
Jasonvdb Oct 1, 2026
7525698
fix: wait for the pubky session retry before auth deep links
Jasonvdb Oct 1, 2026
cba86a4
test: add contact import journey and update the changelog
Jasonvdb Oct 1, 2026
7fa538f
test: wrap long pubky repo test lines
Jasonvdb Oct 1, 2026
200144c
fix: keep a saved contact's label when its follow cannot be resolved
Jasonvdb Oct 1, 2026
3a96905
fix: read a paid contact's receivers on the interactive lane
Jasonvdb Oct 1, 2026
641e6af
fix: load a label-only contact's profile before editing it
Jasonvdb Oct 1, 2026
248dca1
fix: leave the import overview once its import finishes elsewhere
Jasonvdb Oct 1, 2026
5683f45
refactor: simplify the contact profile refresh bookkeeping
Jasonvdb Oct 1, 2026
45ddfff
fix: look up import follows on the interactive read lane
Jasonvdb Oct 1, 2026
3942e68
test: match the contact import journey to its ios steps
Jasonvdb Oct 1, 2026
6d1f092
test: add the contacts list loading journey
Jasonvdb Oct 1, 2026
35f7e07
docs: list the pubky contact journeys as shared with ios
Jasonvdb Oct 1, 2026
d12b25a
Merge master into claude/flow-vibe-pubky-profile-load-lag
Jasonvdb Oct 1, 2026
43294eb
fix: reject public pubky reads that race a wallet wipe
Jasonvdb Oct 1, 2026
494db5f
docs: note how a wallet wipe applies to public pubky reads
Jasonvdb Oct 1, 2026
498ea61
docs: narrow the contact import journey to an import you leave
Jasonvdb Oct 1, 2026
a1be912
docs: note that leaving a contact import no longer stops it
Jasonvdb Oct 1, 2026
5973a4c
fix: keep a label-only contact pending until its profile loads
Jasonvdb Oct 1, 2026
54a7b9c
fix: save no contact edit whose profile never loaded
Jasonvdb Oct 1, 2026
2ca387e
docs: note that a contact edit waits for its profile to load
Jasonvdb Oct 1, 2026
c76bdb2
fix: save a contact edit or tag even when its profile lookups fail
Jasonvdb Oct 1, 2026
346b944
fix: let a screen's contact lookup take over its background lookup
Jasonvdb Oct 1, 2026
c49df2c
docs: describe a contact screen lookup taking over the background one
Jasonvdb Oct 1, 2026
7794d12
fix: keep a contact screen lookup running when its refresh is replaced
Jasonvdb Oct 1, 2026
86e731b
test: name the taken-over background lookup in flight, not queued
Jasonvdb Oct 1, 2026
a4f7b89
docs: say the contact edit form sets bio and links but not the avatar
Jasonvdb Oct 1, 2026
e167b52
test: merge the pubky image cache and cached profile cases into tables
Jasonvdb Oct 1, 2026
3f21982
test: merge near-duplicate pubky repo tests into case tables
Jasonvdb Oct 1, 2026
be1c522
test: merge paykit sdk lock wait and activation tests
Jasonvdb Oct 1, 2026
83a110f
test: merge near-duplicate profile and ring choice view model tests
Jasonvdb Oct 1, 2026
ac55e51
test: merge near-duplicate contact view model tests
Jasonvdb Oct 1, 2026
a3bffd0
test: require sign out's store reset to release the overtaken profile…
Jasonvdb Oct 1, 2026
28fe332
test: reset the repo mock and stop the view model between table cases
Jasonvdb Oct 1, 2026
97a8d06
test: name the case in any failure of a merged table test
Jasonvdb Oct 1, 2026
a8e1c52
fix: check the import owner under the paykit lock before saving
Jasonvdb Oct 1, 2026
74d43ef
docs: say each imported contact save rechecks the identity
Jasonvdb Oct 1, 2026
bbcffd6
fix: rethrow cancellation from the pubky image disk cache
Jasonvdb Oct 1, 2026
2a3f1c7
test: stub the saved session keychain read directly
Jasonvdb Oct 1, 2026
bccb045
refactor: log display profile failures in the repository
Jasonvdb Oct 1, 2026
a97b066
docs: match the ios journey names and steps for pubky profile
Jasonvdb Oct 1, 2026
7eb826a
Merge master into claude/flow-vibe-pubky-profile-load-lag
Jasonvdb Oct 2, 2026
c936f8c
test: check a failed import keeps the pending import
Jasonvdb Oct 2, 2026
489ff43
docs: describe the contact import after adopting #1395
Jasonvdb Oct 2, 2026
b001a3c
fix: give freed read slots to interactive reads before bulk reads
Jasonvdb Oct 2, 2026
3f02102
fix: keep the pending import when leaving a running contact import
Jasonvdb Oct 2, 2026
9aebeef
fix: show a failed contact import's error after leaving its screens
Jasonvdb Oct 2, 2026
a97597d
docs: describe leaving the contact import while it runs
Jasonvdb Oct 2, 2026
97bde5d
fix: keep a contact import quiet when the identity changes
Jasonvdb Oct 2, 2026
58d5c53
fix: snapshot saved contact keys before preparing private links
Jasonvdb Oct 2, 2026
11fe90b
fix: give each import preview lookup a deadline
Jasonvdb Oct 2, 2026
728d0b1
fix: skip contact lookups for profiles resolved in the last ten minutes
Jasonvdb Oct 2, 2026
994559c
fix: apply contact profile refresh results in batches
Jasonvdb Oct 2, 2026
4616414
fix: remember failed pubky avatar fetches
Jasonvdb Oct 2, 2026
b46f007
Merge remote-tracking branch 'origin/master' into claude/flow-vibe-pu…
Jasonvdb Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions app/src/main/java/to/bitkit/data/PubkyImageCacheEpoch.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package to.bitkit.data

import java.util.concurrent.atomic.AtomicLong
import javax.inject.Inject
import javax.inject.Singleton

/**
* Counts clears of the Pubky image disk cache. [PubkyImageFetcher] reads it before a network fetch and commits
* the fetched image only while it is unchanged, so a fetch in flight across a clear cannot re-populate the
* cleared directory, and [PubkyImageFailureCache] forgets every failed fetch once it changes. Advance it before
* clearing the directory.
*/
@Singleton
class PubkyImageCacheEpoch @Inject constructor() {
private val epoch = AtomicLong(0L)

fun current(): Long = epoch.get()

fun advance() {
epoch.incrementAndGet()
}
}
148 changes: 134 additions & 14 deletions app/src/main/java/to/bitkit/data/PubkyImageFetcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,171 @@ import coil3.ImageLoader
import coil3.Uri
import coil3.decode.DataSource
import coil3.decode.ImageSource
import coil3.disk.DiskCache
import coil3.fetch.FetchResult
import coil3.fetch.Fetcher
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import com.synonym.paykit.PaykitException
import okio.Buffer
import org.json.JSONObject
import to.bitkit.ext.nowMs
import to.bitkit.ext.runSuspendCatching
import to.bitkit.models.PubkyPublicKeyFormat
import to.bitkit.services.PubkyFileNotFoundError
import to.bitkit.services.PubkyService
import to.bitkit.utils.AppError
import to.bitkit.utils.Logger
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Clock
import kotlin.time.Duration.Companion.seconds

private const val TAG = "PubkyImageFetcher"
private const val PUBKY_SCHEME = "pubky://"

/** Maximum successful response body accepted for a displayed Pubky image. */
internal const val PUBKY_IMAGE_MAX_BYTES = 1_048_576uL

/** How long a Pubky image whose fetch failed for a reason that can pass, such as a network error, is not fetched. */
internal val PUBKY_IMAGE_TRANSIENT_FAILURE_TTL = 60.seconds

class PubkyImageFetcher(
private val uri: String,
private val options: Options,
private val pubkyService: PubkyService,
private val diskCache: DiskCache?,
private val cacheEpoch: PubkyImageCacheEpoch,
private val failures: PubkyImageFailureCache,
) : Fetcher {
private val diskCacheKey = options.diskCacheKey ?: uri

override suspend fun fetch(): FetchResult {
val data = pubkyService.fetchFile(uri, PUBKY_IMAGE_MAX_BYTES)
val blobData = resolveImageData(data)
val source = ImageSource(Buffer().apply { write(blobData) }, options.fileSystem)
readFromDiskCache()?.let { return it }
failures.recentFailure(uri)?.let { throw PubkyImageRecentlyFailedError(it) }

val epoch = cacheEpoch.current()
val data = runSuspendCatching { pubkyService.fetchFile(uri, PUBKY_IMAGE_MAX_BYTES) }
.onFailure { failures.remember(uri, it, epoch) }
.getOrThrow()
val image = resolveImageData(data, epoch)
if (image.isCacheable) writeToDiskCache(image.bytes, epoch)
val source = ImageSource(Buffer().apply { write(image.bytes) }, options.fileSystem)
return SourceFetchResult(source, null, dataSource = DataSource.NETWORK)
}

private suspend fun resolveImageData(data: ByteArray): ByteArray = runSuspendCatching {
val json = JSONObject(String(data))
val src = json.optString("src", "")
if (src.isNotEmpty() && src.startsWith(PUBKY_SCHEME)) {
Logger.debug("Found file descriptor, fetching blob from '$src'", context = TAG)
pubkyService.fetchFile(src, PUBKY_IMAGE_MAX_BYTES)
} else {
data
private suspend fun readFromDiskCache(): FetchResult? {
if (!options.diskCachePolicy.readEnabled) return null
val cache = diskCache ?: return null
val snapshot = runSuspendCatching { cache.openSnapshot(diskCacheKey) }
.onFailure { Logger.warn("Failed to read pubky image from disk cache", it, context = TAG) }
.getOrNull()
?: return null
val source = ImageSource(snapshot.data, cache.fileSystem, diskCacheKey, snapshot)
return SourceFetchResult(source, null, dataSource = DataSource.DISK)
}

private suspend fun writeToDiskCache(bytes: ByteArray, epoch: Long) {
if (!options.diskCachePolicy.writeEnabled) return
val cache = diskCache ?: return
val editor = runSuspendCatching { cache.openEditor(diskCacheKey) }
.onFailure { Logger.warn("Failed to open pubky image disk cache editor", it, context = TAG) }
.getOrNull()
?: return
runCatching {
cache.fileSystem.write(editor.data) { write(bytes) }
// The editor is open, so a clear after this check makes evictAll() zombie the entry and drop the commit.
if (cacheEpoch.current() == epoch) editor.commit() else editor.abort()
Comment thread
ovitrif marked this conversation as resolved.
}.onFailure {
runCatching { editor.abort() }
if (it is CancellationException) throw it
Logger.warn("Failed to cache pubky image", it, context = TAG)
}
}.getOrDefault(data)
}

private suspend fun resolveImageData(data: ByteArray, epoch: Long): PubkyImageData {
val descriptor = runSuspendCatching { JSONObject(String(data)) }.getOrNull()
?: return PubkyImageData(data, isCacheable = true)
val src = descriptor.optString("src", "")
if (!src.startsWith(PUBKY_SCHEME)) return PubkyImageData(data, isCacheable = false)

Logger.debug("Found file descriptor, fetching blob from '${PubkyPublicKeyFormat.redacted(src)}'", context = TAG)
return runSuspendCatching { pubkyService.fetchFile(src, PUBKY_IMAGE_MAX_BYTES) }
.map { PubkyImageData(it, isCacheable = true) }
.onFailure {
Logger.warn("Failed to fetch pubky image blob", it, context = TAG)
failures.remember(uri, it, epoch)
}
.getOrDefault(PubkyImageData(data, isCacheable = false))
}

class Factory(
private val pubkyService: PubkyService,
private val cacheEpoch: PubkyImageCacheEpoch,
clock: Clock,
) : Fetcher.Factory<Uri> {
private val failures = PubkyImageFailureCache(cacheEpoch, clock)

class Factory(private val pubkyService: PubkyService) : Fetcher.Factory<Uri> {
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
val uri = data.toString()
if (!uri.startsWith(PUBKY_SCHEME)) return null
return PubkyImageFetcher(uri, options, pubkyService)
return PubkyImageFetcher(uri, options, pubkyService, imageLoader.diskCache, cacheEpoch, failures)
}
}
}

/**
* Remembers failed Pubky image fetches by URI, so an image that cannot load is not fetched again each time it is
* shown. A missing file, a file over [PUBKY_IMAGE_MAX_BYTES] or another invalid response is remembered until the
* image cache is next cleared, and any other failure for [PUBKY_IMAGE_TRANSIENT_FAILURE_TTL]. A clear of the image
* cache, which advances [PubkyImageCacheEpoch] on sign-out and identity change, forgets every failure, and a failure
* of a fetch that a clear overtook is not remembered. For a file descriptor, a failed blob fetch counts as a failure
* of the descriptor URI.
*/
class PubkyImageFailureCache(
private val cacheEpoch: PubkyImageCacheEpoch,
private val clock: Clock,
) {
private val lock = Any()
private val failures = mutableMapOf<String, Failure>()
private var failuresEpoch = cacheEpoch.current()

fun recentFailure(uri: String): Throwable? = synchronized(lock) {
forgetIfCleared()
val failure = failures[uri] ?: return null
if (failure.expiresAtMillis != null && clock.nowMs() >= failure.expiresAtMillis) {
failures.remove(uri)
return null
}
failure.error
}

fun remember(uri: String, error: Throwable, epoch: Long) {
synchronized(lock) {
forgetIfCleared()
if (epoch != failuresEpoch) return
val expiresAtMillis = if (error.isPermanentImageFailure()) {
null
} else {
clock.nowMs() + PUBKY_IMAGE_TRANSIENT_FAILURE_TTL.inWholeMilliseconds
}
failures[uri] = Failure(error, expiresAtMillis)
}
}

private fun forgetIfCleared() {
val epoch = cacheEpoch.current()
if (epoch == failuresEpoch) return
failures.clear()
failuresEpoch = epoch
}

private fun Throwable.isPermanentImageFailure(): Boolean = generateSequence(this) { it.cause }.any {
it is PubkyFileNotFoundError || it is PaykitException.NotFound || it is PaykitException.Protocol
}

private class Failure(val error: Throwable, val expiresAtMillis: Long?)
}

class PubkyImageRecentlyFailedError(cause: Throwable) : AppError("Pubky image fetch failed recently", cause)

private class PubkyImageData(val bytes: ByteArray, val isCacheable: Boolean)
12 changes: 12 additions & 0 deletions app/src/main/java/to/bitkit/data/PubkyStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,19 @@ class PubkyStore @Inject constructor(
@Serializable
data class PubkyStoreData(
val ownerPublicKey: String? = null,
/** Public key whose profile [cachedName] and [cachedImageUri] were cached from. */
val cachedProfileOwner: String? = null,
val cachedName: String? = null,
val cachedImageUri: String? = null,
val contactProfileOverrides: Map<String, PubkyProfileData> = emptyMap(),
) {
fun cachedProfile(): PubkyCachedProfile? = cachedProfileOwner?.let {
PubkyCachedProfile(publicKey = it, name = cachedName.orEmpty(), imageUri = cachedImageUri)
}
}

data class PubkyCachedProfile(
val publicKey: String,
val name: String,
val imageUri: String?,
)
6 changes: 5 additions & 1 deletion app/src/main/java/to/bitkit/di/ImageModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import to.bitkit.data.PubkyImageCacheEpoch
import to.bitkit.data.PubkyImageFetcher
import to.bitkit.services.PubkyService
import javax.inject.Singleton
import kotlin.time.Clock

@Module
@InstallIn(SingletonComponent::class)
Expand All @@ -24,9 +26,11 @@ object ImageModule {
fun provideImageLoader(
@ApplicationContext context: Context,
pubkyService: PubkyService,
cacheEpoch: PubkyImageCacheEpoch,
clock: Clock,
): ImageLoader = ImageLoader.Builder(context)
.crossfade(true)
.components { add(PubkyImageFetcher.Factory(pubkyService)) }
.components { add(PubkyImageFetcher.Factory(pubkyService, cacheEpoch, clock)) }
.memoryCache {
MemoryCache.Builder()
.maxSizePercent(context, percent = 0.15)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import to.bitkit.models.PubkyPublicKeyFormat
import to.bitkit.models.satsToMsat
import to.bitkit.services.PaykitPaymentRequestProposalTerms
import to.bitkit.services.PaykitPaymentRequestRecurrenceTerms
import to.bitkit.services.PaykitReadLane
import to.bitkit.services.PaykitReceiverPaths
import to.bitkit.services.PaykitSdkService
import to.bitkit.utils.AppError
Expand Down Expand Up @@ -458,7 +459,7 @@ class PaykitPaymentRequestRepo @Inject constructor(
val context = targetContext(savedPublicKeys, expectedIdentity)
if (!force && context == cachedTargetContext) return@discovery
val previousTargets = _eligibleTargets.value.associateBy { it.publicKey }
val discovery = context?.let { eligibleTargets(it, previousTargets) }
val discovery = context?.let { eligibleTargets(it, previousTargets, PaykitReadLane.Bulk) }
?: PaykitPaymentRequestTargetDiscovery(emptyList(), isComplete = true)
operationMutex.withLock operation@{
if (!isCurrentState(generation, expectedIdentity)) return@operation
Expand Down Expand Up @@ -1085,13 +1086,14 @@ class PaykitPaymentRequestRepo @Inject constructor(
private suspend fun eligibleTargets(
context: PaykitPaymentRequestTargetContext,
previousTargets: Map<String, PaykitPaymentRequestTarget> = emptyMap(),
lane: PaykitReadLane = PaykitReadLane.Interactive,
): PaykitPaymentRequestTargetDiscovery {
var isComplete = true
val failedPublicKeys = mutableSetOf<String>()
val targets = context.savedPublicKeys.mapNotNull { publicKey ->
val linked = context.linkedReceiverPaths[publicKey] ?: return@mapNotNull null
val lookup = withTimeoutOrNull(TARGET_DISCOVERY_TIMEOUT) {
runSuspendCatching { paykitSdkService.paymentRequestReceiverPaths(publicKey) }
runSuspendCatching { paykitSdkService.paymentRequestReceiverPaths(publicKey, lane) }
}
if (lookup == null) {
isComplete = false
Expand Down
19 changes: 12 additions & 7 deletions app/src/main/java/to/bitkit/repositories/PrivatePaykitRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import to.bitkit.models.PubkyPublicKeyFormat
import to.bitkit.services.CoreService
import to.bitkit.services.PaykitPreparedPrivateContactPayment
import to.bitkit.services.PaykitPrivateContactPaymentResolution
import to.bitkit.services.PaykitReadLane
import to.bitkit.services.PaykitReceiverPaths
import to.bitkit.services.PaykitSdkService
import to.bitkit.services.PubkyService
Expand Down Expand Up @@ -213,7 +214,7 @@ class PrivatePaykitRepo @Inject constructor(
): Result<Unit> = withContext(serializedDispatcher) {
runSuspendCatching {
if (!canPublishPrivateEndpoints()) {
prepareRelevantPrivateLinksIfAvailable(knownSavedContactKeys, reason)
prepareRelevantPrivateLinksIfAvailable(knownSavedContactKeys.toList(), reason)
return@runSuspendCatching
}
publishLocalEndpoints(
Expand Down Expand Up @@ -638,6 +639,7 @@ class PrivatePaykitRepo @Inject constructor(
publishLocalEndpoints(
publicKeys = listOf(publicKey),
reason = "payment",
lane = PaykitReadLane.Interactive,
).onFailure {
Logger.warn(
"Failed to refresh private Paykit endpoints before payment for '${redacted(publicKey)}'",
Expand Down Expand Up @@ -788,6 +790,7 @@ class PrivatePaykitRepo @Inject constructor(
reason: String,
forceRefreshLightning: Boolean = false,
requireImmediatePublication: Boolean = false,
lane: PaykitReadLane = PaykitReadLane.Bulk,
): Result<Unit> = withContext(serializedDispatcher) {
runSuspendCatching {
val keys = publicKeys.mapNotNull { normalizedPublicKey(it) }.distinct()
Expand All @@ -804,6 +807,7 @@ class PrivatePaykitRepo @Inject constructor(
publicKeys = keys,
reason = reason,
forceRefreshLightning = forceRefreshLightning,
lane = lane,
)

if (preparation.updates.isEmpty()) {
Expand Down Expand Up @@ -837,6 +841,7 @@ class PrivatePaykitRepo @Inject constructor(
publicKeys: Collection<String>,
reason: String,
forceRefreshLightning: Boolean,
lane: PaykitReadLane,
): PrivatePublicationPreparation {
var firstError: Throwable? = null
var receiverPathSelectionError: Throwable? = null
Expand All @@ -847,7 +852,7 @@ class PrivatePaykitRepo @Inject constructor(
firstError = linkedReceiverPathsSnapshot.error

for (publicKey in publicKeys) {
val receiverPaths = runSuspendCatching { receiverPathsForSavedContact(publicKey) }
val receiverPaths = runSuspendCatching { receiverPathsForSavedContact(publicKey, lane) }
.onFailure {
firstError = firstError ?: it
Logger.warn(
Expand All @@ -856,7 +861,7 @@ class PrivatePaykitRepo @Inject constructor(
context = TAG,
)
}.getOrNull() ?: continue
val receiverPathSelection = paykitSdkService.privateReceiverPathSelection(publicKey, receiverPaths)
val receiverPathSelection = paykitSdkService.privateReceiverPathSelection(publicKey, receiverPaths, lane)
val linkableReceiverPaths = receiverPathSelection.linkableReceiverPaths
val publicationReceiverPaths = receiverPathSelection.publishableReceiverPaths
receiverPathSelection.error?.let {
Expand Down Expand Up @@ -901,15 +906,15 @@ class PrivatePaykitRepo @Inject constructor(

val retryKeys = mutableListOf<PrivateMessageDrainRetryKey>()
for (publicKey in publicKeys) {
val receiverPaths = runSuspendCatching { receiverPathsForSavedContact(publicKey) }
val receiverPaths = runSuspendCatching { receiverPathsForSavedContact(publicKey, PaykitReadLane.Bulk) }
.onFailure {
Logger.warn(
"Failed to read saved Paykit receivers for '${redacted(publicKey)}' during '$reason'",
it,
context = TAG,
)
}.getOrNull() ?: continue
val selection = paykitSdkService.privateReceiverPathSelection(publicKey, receiverPaths)
val selection = paykitSdkService.privateReceiverPathSelection(publicKey, receiverPaths, PaykitReadLane.Bulk)
selection.error?.let {
Logger.warn(
"Failed to inspect private Paykit receiver markers for '${redacted(publicKey)}' during '$reason'",
Expand Down Expand Up @@ -1490,12 +1495,12 @@ class PrivatePaykitRepo @Inject constructor(
)
}

private suspend fun receiverPathsForSavedContact(publicKey: String): List<String> {
private suspend fun receiverPathsForSavedContact(publicKey: String, lane: PaykitReadLane): List<String> {
val record = paykitSdkService.contactRecord(publicKey)
val savedPaths = supportedReceiverPaths(record?.receiverPaths.orEmpty())

return runSuspendCatching {
val discoveredPaths = pubkyService.discoverRelevantReceiverPaths(publicKey)
val discoveredPaths = pubkyService.discoverRelevantReceiverPaths(publicKey, lane)
val currentRecord = paykitSdkService.contactRecord(publicKey)
?: return@runSuspendCatching savedPaths
val currentSavedPaths = supportedReceiverPaths(currentRecord.receiverPaths)
Expand Down
Loading
Loading