Skip to content

Convert remaining cachesWarmed-gated GroupStore reads to per-group membershipFullyLoaded #28

Description

@MastaP

Background

PR #26 introduced GroupStore.membershipFullyLoaded — a per-group sync.Map flag set when:

  • WarmCaches successfully applies a kind-39002 snapshot for that group, or
  • OnEventSaved for kind-9007 (new group creation) explicitly marks it before the first AddMember.

IsMember consults this per-group flag and falls back to a DB query for groups that aren't loaded. That closed the false "you are not a member" rejection class on writes — the user-visible regression that motivated #25.

What's still on the global flag

These read paths in zooid/groups.go still gate on the global g.cachesWarmed:

  • GetMetadata (groups.go:343)
  • GetMembers (groups.go:640)
  • GetMemberCount (groups.go:679)
  • IsPrivateGroup (groups.go:843)
  • GetGroupCreator (groups.go:858)
  • IsWriteRestricted (groups.go:882)
  • HasRole does not gate at all — reads roleCache directly, returns false on cache miss.

If a partial WarmCaches ever sets cachesWarmed=true (today my heuristic only catches the 0-snapshots catastrophic case, not partial reads), these would return wrong answers for unloaded groups:

  • GetMembers([]) for an unloaded group → if anything calls UpdateMembersList for that group, it'd publish an empty 39002. PR groups: warm membership/role caches from kind-39001/39002 snapshots #26 added an early-return guard for exactly this case in UpdateMembersList, so the worst symptom is contained — but the underlying inconsistency is real.
  • HasRole(... \"writer\") = false → write-restricted groups reject legitimate writers.
  • GetMemberCount = 0 → metadata refresh writes wrong member_count.

Proposed change

Move each read site to consult per-group state. Two shapes:

  1. For purely cache-driven reads (e.g. GetMembers): if not in membershipFullyLoaded, fall back to a per-group DB rebuild (latest 39002 + tail-of-log since its created_at). Cache the result.

  2. For read paths where the right answer is "I don't know yet, ask the DB" (e.g. HasRole): same DB rebuild, scoped to the role question.

Both shapes need a small helper that does the rebuild atomically and marks fullyLoaded on success. The WarmCaches code already does roughly this; extract and reuse.

For the metadata-cache and creator-cache paths, similar but probably driven by a separate metadataFullyLoaded marker (tracking groups whose 39000/9007 reads succeeded). These caches have their own failure modes that are independent of membership.

Risks / out of scope

  • Changing the on-disk format of these caches isn't required.
  • HasRole's downstream use is IsWriter = IsMember && HasRole(... \"writer\"). Adding a DB fallback means write-path latency includes a sub-ms DB query on cache miss. Acceptable.
  • For NIP-29 groups with hot member churn this could amplify DB query rate transiently after a partial WarmCaches restart. Mitigated by the same pattern: cache the rebuild result.

Test plan

  • Mirror the IsMember partial-warm test (TestGroupStore_IsMember_PartialWarmFallsBackToDB) for each converted function.
  • Verify TestGroupMembershipCache_GetMembers + similar still pass after conversion.

Refs

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions