You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Move each read site to consult per-group state. Two shapes:
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.
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.
Background
PR #26 introduced
GroupStore.membershipFullyLoaded— a per-group sync.Map flag set when:IsMemberconsults 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.gostill gate on the globalg.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)HasRoledoes 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 callsUpdateMembersListfor 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:
For purely cache-driven reads (e.g.
GetMembers): if not inmembershipFullyLoaded, fall back to a per-group DB rebuild (latest 39002 + tail-of-log since its created_at). Cache the result.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
IsWriter = IsMember && HasRole(... \"writer\"). Adding a DB fallback means write-path latency includes a sub-ms DB query on cache miss. Acceptable.Test plan
TestGroupStore_IsMember_PartialWarmFallsBackToDB) for each converted function.Refs
membershipFullyLoaded.