1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-09-19 16:40:29 +00:00
arikawa/state/store/store.go
diamondburned efde3f4ea6
state, handler: Refactor state storage and sync handlers
This commit refactors a lot of packages.

It refactors the handler package, removing the Synchronous field and
replacing it the AddSyncHandler API, which allows each handler to
control whether or not it should be ran synchronously independent of
other handlers. This is useful for libraries that need to guarantee the
incoming order of events.

It also refactors the store interfaces to accept more interfaces. This
is to make the API more consistent as well as reducing potential useless
copies. The public-facing state API should still be the same, so this
change will mostly concern users with their own store implementations.

Several miscellaneous functions (such as a few in package gateway) were
modified to be more suitable to other packages, but those functions
should rarely ever be used, anyway.

Several tests are also fixed within this commit, namely fixing state's
intents bug.
2021-11-03 15:16:02 -07:00

390 lines
12 KiB
Go

// Package store contains interfaces of the state's storage and its
// implementations.
//
// Getter Methods
//
// All getter methods will be wrapped by the State. If the State can't find
// anything in the storage, it will call the API itself and automatically add
// what's missing into the storage.
//
// Methods that return with a slice should pay attention to race conditions that
// would mutate the underlying slice (and as a result the returned slice as
// well). The best way to avoid this is to copy the whole slice, like
// defaultstore implementations do.
//
// Getter methods should not care about returning slices in order, unless
// explicitly stated against.
//
// ErrNotFound Rules
//
// If a getter method cannot find something, it should return ErrNotFound.
// Callers including State may check if the error is ErrNotFound to do something
// else. For example, if Guilds currently stores nothing, then it should return
// an empty slice and a nil error.
//
// In some cases, there may not be a way to know whether or not the store is
// unpopulated or is actually empty. In that case, implementations can return
// ErrNotFound when either happens. This will make State refetch from the API,
// so it is not ideal.
//
// Remove Methods
//
// Remove methods should return a nil error if the item it wants to delete is
// not found. This helps save some additional work in some cases.
package store
import (
"errors"
"fmt"
"github.com/diamondburned/arikawa/v3/discord"
)
// ErrNotFound is an error that a store can use to return when something isn't
// in the storage. There is no strict restrictions on what uses this (the
// default one does, though), so be advised.
var ErrNotFound = errors.New("item not found in store")
// Cabinet combines all store interfaces into one but allows swapping individual
// stores out for another. Since the struct only consists of interfaces, it can
// be copied around.
type Cabinet struct {
MeStore
ChannelStore
EmojiStore
GuildStore
MemberStore
MessageStore
PresenceStore
RoleStore
VoiceStateStore
}
// Reset resets everything inside the container.
func (sc *Cabinet) Reset() error {
errors := []error{
sc.MeStore.Reset(),
sc.ChannelStore.Reset(),
sc.EmojiStore.Reset(),
sc.GuildStore.Reset(),
sc.MemberStore.Reset(),
sc.MessageStore.Reset(),
sc.PresenceStore.Reset(),
sc.RoleStore.Reset(),
sc.VoiceStateStore.Reset(),
}
nonNils := errors[:0]
for _, err := range errors {
if err != nil {
nonNils = append(nonNils, err)
}
}
if len(nonNils) > 0 {
return ResetErrors(nonNils)
}
return nil
}
// ResetErrors represents the multiple errors when StoreContainer is being
// resetted. A ResetErrors value must have at least 1 error.
type ResetErrors []error
// Error formats ResetErrors, showing the number of errors and the last error.
func (errs ResetErrors) Error() string {
return fmt.Sprintf(
"encountered %d reset errors (last: %v)",
len(errs), errs[len(errs)-1],
)
}
// Unwrap returns the last error in the list.
func (errs ResetErrors) Unwrap() error {
return errs[len(errs)-1]
}
// append adds the error only if it is not nil.
func (errs *ResetErrors) append(err error) {
if err != nil {
*errs = append(*errs, err)
}
}
// Noop is the value for a NoopStore.
var Noop = NoopStore{}
// NoopStore is a no-op implementation of all store interfaces. Its getters will
// always return ErrNotFound, and its setters will never return an error.
type NoopStore = noop
// NoopCabinet is a store cabinet with all store methods set to the Noop
// implementations.
var NoopCabinet = &Cabinet{
MeStore: Noop,
ChannelStore: Noop,
EmojiStore: Noop,
GuildStore: Noop,
MemberStore: Noop,
MessageStore: Noop,
PresenceStore: Noop,
RoleStore: Noop,
VoiceStateStore: Noop,
}
// noop is the Noop type that implements methods.
type noop struct{}
// Resetter is an interface to reset the store on every Ready event.
type Resetter interface {
// Reset resets the store to a new valid instance.
Reset() error
}
type CoreStorer interface {
Resetter
Lock()
Unlock()
}
var _ Resetter = (*noop)(nil)
func (noop) Reset() error { return nil }
// MeStore is the store interface for the current user.
type MeStore interface {
Resetter
Me() (*discord.User, error)
MyselfSet(me discord.User, update bool) error
}
func (noop) Me() (*discord.User, error) { return nil, ErrNotFound }
func (noop) MyselfSet(discord.User, bool) error { return nil }
// ChannelStore is the store interface for all channels.
type ChannelStore interface {
Resetter
// Channel searches for both DM and guild channels.
Channel(discord.ChannelID) (*discord.Channel, error)
// CreatePrivateChannel searches for private channels by the recipient ID.
// It has the same API as *api.Client does.
CreatePrivateChannel(recipient discord.UserID) (*discord.Channel, error)
// Channels returns only channels from a guild.
Channels(discord.GuildID) ([]discord.Channel, error)
// PrivateChannels returns all private channels from the state.
PrivateChannels() ([]discord.Channel, error)
// Both ChannelSet and ChannelRemove should switch on Type to know if it's a
// private channel or not.
ChannelSet(c *discord.Channel, update bool) error
ChannelRemove(*discord.Channel) error
}
var _ ChannelStore = (*noop)(nil)
func (noop) Channel(discord.ChannelID) (*discord.Channel, error) {
return nil, ErrNotFound
}
func (noop) CreatePrivateChannel(discord.UserID) (*discord.Channel, error) {
return nil, ErrNotFound
}
func (noop) Channels(discord.GuildID) ([]discord.Channel, error) {
return nil, ErrNotFound
}
func (noop) PrivateChannels() ([]discord.Channel, error) {
return nil, ErrNotFound
}
func (noop) ChannelSet(*discord.Channel, bool) error {
return nil
}
func (noop) ChannelRemove(*discord.Channel) error {
return nil
}
// EmojiStore is the store interface for all emojis.
type EmojiStore interface {
Resetter
Emoji(discord.GuildID, discord.EmojiID) (*discord.Emoji, error)
Emojis(discord.GuildID) ([]discord.Emoji, error)
// EmojiSet should delete all old emojis before setting new ones. The given
// emojis slice will be a complete list of all emojis.
EmojiSet(guildID discord.GuildID, emojis []discord.Emoji, update bool) error
}
var _ EmojiStore = (*noop)(nil)
func (noop) Emoji(discord.GuildID, discord.EmojiID) (*discord.Emoji, error) {
return nil, ErrNotFound
}
func (noop) Emojis(discord.GuildID) ([]discord.Emoji, error) {
return nil, ErrNotFound
}
func (noop) EmojiSet(discord.GuildID, []discord.Emoji, bool) error {
return nil
}
// GuildStore is the store interface for all guilds.
type GuildStore interface {
Resetter
Guild(discord.GuildID) (*discord.Guild, error)
Guilds() ([]discord.Guild, error)
GuildSet(g *discord.Guild, update bool) error
GuildRemove(id discord.GuildID) error
}
var _ GuildStore = (*noop)(nil)
func (noop) Guild(discord.GuildID) (*discord.Guild, error) { return nil, ErrNotFound }
func (noop) Guilds() ([]discord.Guild, error) { return nil, ErrNotFound }
func (noop) GuildSet(*discord.Guild, bool) error { return nil }
func (noop) GuildRemove(discord.GuildID) error { return nil }
// MemberStore is the store interface for all members.
type MemberStore interface {
Resetter
Member(discord.GuildID, discord.UserID) (*discord.Member, error)
Members(discord.GuildID) ([]discord.Member, error)
MemberSet(guildID discord.GuildID, m *discord.Member, update bool) error
MemberRemove(discord.GuildID, discord.UserID) error
}
var _ MemberStore = (*noop)(nil)
func (noop) Member(discord.GuildID, discord.UserID) (*discord.Member, error) {
return nil, ErrNotFound
}
func (noop) Members(discord.GuildID) ([]discord.Member, error) {
return nil, ErrNotFound
}
func (noop) MemberSet(discord.GuildID, *discord.Member, bool) error {
return nil
}
func (noop) MemberRemove(discord.GuildID, discord.UserID) error {
return nil
}
// MessageStore is the store interface for all messages.
type MessageStore interface {
Resetter
// MaxMessages returns the maximum number of messages. It is used to know if
// the state cache is filled or not for one channel
MaxMessages() int
Message(discord.ChannelID, discord.MessageID) (*discord.Message, error)
// Messages should return messages ordered from latest to earliest.
Messages(discord.ChannelID) ([]discord.Message, error)
// MessageSet either updates or adds a new message.
//
// A new message can be added, by setting update to false. Depending on
// timestamp of the message, it will either be prepended or appended.
//
// If update is set to true, MessageSet will check if a message with the
// id of the passed message is stored, and update it if so. Otherwise, if
// there is no such message, it will be discarded.
MessageSet(m *discord.Message, update bool) error
MessageRemove(discord.ChannelID, discord.MessageID) error
}
var _ MessageStore = (*noop)(nil)
func (noop) MaxMessages() int {
return 0
}
func (noop) Message(discord.ChannelID, discord.MessageID) (*discord.Message, error) {
return nil, ErrNotFound
}
func (noop) Messages(discord.ChannelID) ([]discord.Message, error) {
return nil, ErrNotFound
}
func (noop) MessageSet(*discord.Message, bool) error {
return nil
}
func (noop) MessageRemove(discord.ChannelID, discord.MessageID) error {
return nil
}
// PresenceStore is the store interface for all user presences. Presences don't get
// fetched from the API; they will only be updated through the Gateway.
type PresenceStore interface {
Resetter
Presence(discord.GuildID, discord.UserID) (*discord.Presence, error)
Presences(discord.GuildID) ([]discord.Presence, error)
PresenceSet(guildID discord.GuildID, p *discord.Presence, update bool) error
PresenceRemove(discord.GuildID, discord.UserID) error
}
var _ PresenceStore = (*noop)(nil)
func (noop) Presence(discord.GuildID, discord.UserID) (*discord.Presence, error) {
return nil, ErrNotFound
}
func (noop) Presences(discord.GuildID) ([]discord.Presence, error) {
return nil, ErrNotFound
}
func (noop) PresenceSet(discord.GuildID, *discord.Presence, bool) error {
return nil
}
func (noop) PresenceRemove(discord.GuildID, discord.UserID) error {
return nil
}
// RoleStore is the store interface for all member roles.
type RoleStore interface {
Resetter
Role(discord.GuildID, discord.RoleID) (*discord.Role, error)
Roles(discord.GuildID) ([]discord.Role, error)
RoleSet(guildID discord.GuildID, r *discord.Role, update bool) error
RoleRemove(discord.GuildID, discord.RoleID) error
}
var _ RoleStore = (*noop)(nil)
func (noop) Role(discord.GuildID, discord.RoleID) (*discord.Role, error) { return nil, ErrNotFound }
func (noop) Roles(discord.GuildID) ([]discord.Role, error) { return nil, ErrNotFound }
func (noop) RoleSet(discord.GuildID, *discord.Role, bool) error { return nil }
func (noop) RoleRemove(discord.GuildID, discord.RoleID) error { return nil }
// VoiceStateStore is the store interface for all voice states.
type VoiceStateStore interface {
Resetter
VoiceState(discord.GuildID, discord.UserID) (*discord.VoiceState, error)
VoiceStates(discord.GuildID) ([]discord.VoiceState, error)
VoiceStateSet(guildID discord.GuildID, s *discord.VoiceState, update bool) error
VoiceStateRemove(discord.GuildID, discord.UserID) error
}
var _ VoiceStateStore = (*noop)(nil)
func (noop) VoiceState(discord.GuildID, discord.UserID) (*discord.VoiceState, error) {
return nil, ErrNotFound
}
func (noop) VoiceStates(discord.GuildID) ([]discord.VoiceState, error) {
return nil, ErrNotFound
}
func (noop) VoiceStateSet(discord.GuildID, *discord.VoiceState, bool) error {
return nil
}
func (noop) VoiceStateRemove(discord.GuildID, discord.UserID) error {
return nil
}