Files
damus/damus/Nostr/NostrFilter.swift
T
Daniel D’Aquino 46185c55d1 Chunk home filters to avoid hitting max filter item limits
When a user is following several accounts, they may get a stale feed
caused by the subscription request being rejected by relays (due to max filter item limits).

This commit implements a fix that gets around the issue by
creating several chunked filters for the home feed event and contact
metadata subscriptions.

This is a short to medium-term practical fix, where we get around the
practical limitations imposed by most relays. In the future we should
work on longer-term solutions, which will likely require protocol improvements

Main Test
---------

Procedure:
1. Login with Elsat's npub (Or some account that follows about 2K people)
2. Check the home feed. There should be fresh notes.

REPRO:
Device: iPhone 15 simulator
iOS: 17.4
Damus: 1.9 (3) (0d9954290a)
Results:
- No fresh notes, most recent post is from several hours ago (Feed is stale)

FIX TEST:
Device: iPhone 15 simulator
iOS: 17.4
Damus: This commit
Results:
- Fresh notes appear, most recent post is from a few seconds ago.

Other testing:
--------------

- New automated test passing
- All other automated tests passing
- Tested scrolling down the feed on these conditions:
  - Device: iPhone 13 Mini
  - iOS: 17.4.1
  - Accounts:
    - One with about 160 contacts and 10 relays (Daniel D’Aquino)
    - One with about 1K+ contacts and 9 relays (Freedom Smuggler)
    - One with about 981 contacts and 6 relays (jb55)
    - Elsat's account (2K+ accounts and 8 relays)
  - Result: None of those were stale

Changelog-Fixed: Fix stale feed issue when follow list is too big
Closes: https://github.com/damus-io/damus/issues/2194
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
2024-05-13 10:56:15 -07:00

122 lines
4.1 KiB
Swift

//
// NostrFilter.swift
// damus
//
// Created by William Casarin on 2022-04-11.
//
import Foundation
struct NostrFilter: Codable, Equatable {
var ids: [NoteId]?
var kinds: [NostrKind]?
var referenced_ids: [NoteId]?
var pubkeys: [Pubkey]?
var since: UInt32?
var until: UInt32?
var limit: UInt32?
var authors: [Pubkey]?
var hashtag: [String]?
var parameter: [String]?
var quotes: [NoteId]?
private enum CodingKeys : String, CodingKey {
case ids
case kinds
case referenced_ids = "#e"
case pubkeys = "#p"
case hashtag = "#t"
case parameter = "#d"
case quotes = "#q"
case since
case until
case authors
case limit
}
init(ids: [NoteId]? = nil, kinds: [NostrKind]? = nil, referenced_ids: [NoteId]? = nil, pubkeys: [Pubkey]? = nil, since: UInt32? = nil, until: UInt32? = nil, limit: UInt32? = nil, authors: [Pubkey]? = nil, hashtag: [String]? = nil, quotes: [NoteId]? = nil) {
self.ids = ids
self.kinds = kinds
self.referenced_ids = referenced_ids
self.pubkeys = pubkeys
self.since = since
self.until = until
self.limit = limit
self.authors = authors
self.hashtag = hashtag
self.quotes = quotes
}
public static func copy(from: NostrFilter) -> NostrFilter {
NostrFilter(ids: from.ids, kinds: from.kinds, referenced_ids: from.referenced_ids, pubkeys: from.pubkeys, since: from.since, until: from.until, authors: from.authors, hashtag: from.hashtag)
}
public static func filter_hashtag(_ htags: [String]) -> NostrFilter {
NostrFilter(hashtag: htags.map { $0.lowercased() })
}
/// Splits the filter on a given filter path/axis into chunked filters
///
/// - Parameter path: The path where chunking should be done
/// - Parameter chunk_size: The maximum size of each chunk.
/// - Returns: An array of arrays, where each contained array is a chunk of the original array with up to `size` elements.
func chunked(on path: ChunkPath, into chunk_size: Int) -> [Self] {
let chunked_slices = self.get_slice(from: path).chunked(into: chunk_size)
var chunked_filters: [NostrFilter] = []
for chunked_slice in chunked_slices {
var chunked_filter = self
chunked_filter.apply_slice(chunked_slice)
chunked_filters.append(chunked_filter)
}
return chunked_filters
}
/// Gets a slice from a NostrFilter on a given path/axis
///
/// - Parameter path: The path where chunking should be done
/// - Parameter chunk_size: The maximum size of each chunk.
/// - Returns: An array of arrays, where each contained array is a chunk of the original array with up to `size` elements.
func get_slice(from path: ChunkPath) -> Slice {
switch path {
case .pubkeys:
return .pubkeys(self.pubkeys)
case .authors:
return .authors(self.authors)
}
}
/// Overrides one member/axis of a NostrFilter using a specific slice
/// - Parameter slice: The slice to be applied on this NostrFilter
mutating func apply_slice(_ slice: Slice) {
switch slice {
case .pubkeys(let pubkeys):
self.pubkeys = pubkeys
case .authors(let authors):
self.authors = authors
}
}
/// A path to one of the axes of a NostrFilter.
enum ChunkPath {
case pubkeys
case authors
// Other paths/axes not supported yet
}
/// Represents the value of a single axis of a NostrFilter
enum Slice {
case pubkeys([Pubkey]?)
case authors([Pubkey]?)
func chunked(into chunk_size: Int) -> [Slice] {
switch self {
case .pubkeys(let array):
return (array ?? []).chunked(into: chunk_size).map({ .pubkeys($0) })
case .authors(let array):
return (array ?? []).chunked(into: chunk_size).map({ .authors($0) })
}
}
}
}