We have a VisibilityCache
, keyed by VisibilityType
and requesting account and item IDs, and storing a CachedVisibility
. The type can be one of these:
VisibilityTypeAccount
: item is account, account is visible to requesterVisibilityTypeStatus
: item is status, status is visible at all to requester (partially depends onVisibilityTypeAccount
)VisibilityTypeHome
: item is status, status is suitable for requester's home timeline and lists (partially depends onVisibilityTypeStatus
)VisibilityTypePublic
: item is status, status is suitable for requester's public timelines (partially depends onVisibilityTypeStatus
)
CachedVisibility
stores all the lookup keys and a boolean for visibility, which takes into account blocks, suspensions, locked accounts, a status's visibility level, etc. These are populated or fetched through cache methods like StatusHomeTimelineable
and StatusPublicTimelineable
.
We have caches for filters, filter keywords, and filter statuses, keyed by ID and by owning account ID. Filters apply to statuses in one of five FilterContext
s:
FilterContextHome
: home timeline and listsFilterContextNotifications
: notifications timelineFilterContextPublic
: public timelinesFilterContextThread
: thread context (ancestors and descendants of a status)FilterContextAccount
: account timeline (looking at an account's statuses)
A filter can show, hide, or warn for a given status. Statuses that result in FilterActionWarn
are still returned to the client, but accompanied by a list of matching filters, at which point the client gets the final decision on whether to show it or not. Unlike blocks or timeline visibility rules, filters can expire.
We don't currently cache filtering decisions at all: filters are applied every time statuses from a given filter context are requested.
My current draft for user muting closely follows how filters are implemented. User mutes always apply to FilterContextHome
, FilterContextPublic
, and FilterContextThread
. They can optionally apply to FilterContextNotifications
. (They don't apply to FilterContextAccount
; it's assumed that if you're looking at an account on purpose, you actually want to see its statuses.) User mutes can expire, but there is no equivalent of a warn filter.
The draft doesn't cache muting decisions. But I got to thinking about what it'd look like if we did…
The goal here is to spend less time checking to see if a status is filtered or muted.
We'd extend VisibilityCache
to cover all filter contexts (and thus all mute contexts), to store an expiration time, and to support warn filters. VisibilityType
gets three new cases for statuses, corresponding to the rest of the filter contexts:
VisibilityTypeNotifications
VisibilityTypeThread
VisibilityTypeAccount
CachedVisibility.Value
, currently a show/hide bool, changes type to a new CachedVisibilityDecision
enum with three values:
CachedVisibilityDecisionShow
CachedVisibilityDecisionWarn
CachedVisibilityDecisionHide
The type change would force us to update code paths that couldn't handle the warn case before; for cases where warn doesn't make sense, we should determine visibility with .Value == CachedVisibilityDecisionShow
.
We'd also add some new fields to CachedVisibility
:
ExpiresAt
is the minimum expiration time of all filters or user mutes involved in a cached visibility decision. If it's zero, the decision never expires based on time. If it's nonzero, the decision is not valid after that time, and if a cache method gets an expiredCachedVisibility
, it should evict it from the cache.FilterIDs
is only used whenValue
isCachedVisibilityDecisionWarn
, and contains a list of filter IDs that will be retrieved when the status is serialized, and sent along with the status.
If the Type
is VisibilityTypeAccount
or VisibilityTypeStatus
, they don't apply and aren't used.
I'm assuming that changing filters and mutes is much less common than checking a status's visibility.
- Whenever a filter is updated or removed, visibility decisions and filter IDs may change for any status, so we should invalidate all
CachedVisibility
entries where the requester is the account that owns the filter and theVisibilityType
corresponds to one of the filter's contexts.- It's not necessary to invalidate when a filter is added, since a new filter has no keywords or statuses yet, and thus can't match anything.
- Filter keywords can match any status, so whenever a filter keyword is added, updated, or removed, we should invalidate all
CachedVisibility
entries where the requester is the account that owns the filter and theVisibilityType
corresponds to one of the filter's contexts. - Filter statuses can only match one status, so whenever a filter status is added or removed (they currently have no state that can be updated), we should invalidate all
CachedVisibility
entries where the requester is the account that owns the filter, the item is the status, and theVisibilityType
corresponds to one of the filter's contexts. - Whenever a user mute is added, updated, or removed, visibility decisions may change for any status, so we should invalidate all
CachedVisibility
entries where the requester is the account that owns the mute and theVisibilityType
corresponds to one of the mute's contexts.
Entries for VisibilityTypeHome
and VisibilityTypePublic
use info previously cached in VisibilityTypeStatus
when calculating their own visibility, and VisibilityTypeStatus
likewise depends on VisibilityTypeAccount
, but invalidating an entry in VisibilityTypeAccount
or VisibilityTypeStatus
(for example, when a block is added or removed) doesn't seem to have any mechanism for invalidating downstream caches, so outdated visibility info could persist indefinitely.
We might be able to fix this by adding an additional OwnerID
key to VisibilityCache
that lets us find entries for statuses by the account that owns them. This would let us invalidate all visibility cache entries for an account when it's blocked or unblocked, suspended or unsuspended, or muted or unmuted, and thus improve correctness at the cost of however much memory it takes to have another key.