summaryrefslogtreecommitdiff
path: root/internal/cache/status.go
blob: f6fe45d99809b573681e4c3de1b141f1c70a7139 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package cache

import (
	"sync"

	"github.com/ReneKroon/ttlcache"
	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)

// statusCache is a wrapper around ttlcache.Cache to provide URL and URI lookups for gtsmodel.Status
type StatusCache struct {
	cache *ttlcache.Cache   // map of IDs -> cached statuses
	urls  map[string]string // map of status URLs -> IDs
	uris  map[string]string // map of status URIs -> IDs
	mutex sync.Mutex
}

// newStatusCache returns a new instantiated statusCache object
func NewStatusCache() *StatusCache {
	c := StatusCache{
		cache: ttlcache.NewCache(),
		urls:  make(map[string]string, 100),
		uris:  make(map[string]string, 100),
		mutex: sync.Mutex{},
	}

	// Set callback to purge lookup maps on expiration
	c.cache.SetExpirationCallback(func(key string, value interface{}) {
		status := value.(*gtsmodel.Status)

		c.mutex.Lock()
		delete(c.urls, status.URL)
		delete(c.uris, status.URI)
		c.mutex.Unlock()
	})

	return &c
}

// GetByID attempts to fetch a status from the cache by its ID, you will receive a copy for thread-safety
func (c *StatusCache) GetByID(id string) (*gtsmodel.Status, bool) {
	c.mutex.Lock()
	status, ok := c.getByID(id)
	c.mutex.Unlock()
	return status, ok
}

// GetByURL attempts to fetch a status from the cache by its URL, you will receive a copy for thread-safety
func (c *StatusCache) GetByURL(url string) (*gtsmodel.Status, bool) {
	// Perform safe ID lookup
	c.mutex.Lock()
	id, ok := c.urls[url]

	// Not found, unlock early
	if !ok {
		c.mutex.Unlock()
		return nil, false
	}

	// Attempt status lookup
	status, ok := c.getByID(id)
	c.mutex.Unlock()
	return status, ok
}

// GetByURI attempts to fetch a status from the cache by its URI, you will receive a copy for thread-safety
func (c *StatusCache) GetByURI(uri string) (*gtsmodel.Status, bool) {
	// Perform safe ID lookup
	c.mutex.Lock()
	id, ok := c.uris[uri]

	// Not found, unlock early
	if !ok {
		c.mutex.Unlock()
		return nil, false
	}

	// Attempt status lookup
	status, ok := c.getByID(id)
	c.mutex.Unlock()
	return status, ok
}

// getByID performs an unsafe (no mutex locks) lookup of status by ID, returning a copy of status in cache
func (c *StatusCache) getByID(id string) (*gtsmodel.Status, bool) {
	v, ok := c.cache.Get(id)
	if !ok {
		return nil, false
	}
	return copyStatus(v.(*gtsmodel.Status)), true
}

// Put places a status in the cache, ensuring that the object place is a copy for thread-safety
func (c *StatusCache) Put(status *gtsmodel.Status) {
	if status == nil || status.ID == "" {
		panic("invalid status")
	}

	c.mutex.Lock()
	c.cache.Set(status.ID, copyStatus(status))
	if status.URL != "" {
		c.urls[status.URL] = status.ID
	}
	if status.URI != "" {
		c.uris[status.URI] = status.ID
	}
	c.mutex.Unlock()
}

// copyStatus performs a surface-level copy of status, only keeping attached IDs intact, not the objects.
// due to all the data being copied being 99% primitive types or strings (which are immutable and passed by ptr)
// this should be a relatively cheap process
func copyStatus(status *gtsmodel.Status) *gtsmodel.Status {
	return &gtsmodel.Status{
		ID:                       status.ID,
		URI:                      status.URI,
		URL:                      status.URL,
		Content:                  status.Content,
		AttachmentIDs:            status.AttachmentIDs,
		Attachments:              nil,
		TagIDs:                   status.TagIDs,
		Tags:                     nil,
		MentionIDs:               status.MentionIDs,
		Mentions:                 nil,
		EmojiIDs:                 status.EmojiIDs,
		Emojis:                   nil,
		CreatedAt:                status.CreatedAt,
		UpdatedAt:                status.UpdatedAt,
		Local:                    status.Local,
		AccountID:                status.AccountID,
		Account:                  nil,
		AccountURI:               status.AccountURI,
		InReplyToID:              status.InReplyToID,
		InReplyTo:                nil,
		InReplyToURI:             status.InReplyToURI,
		InReplyToAccountID:       status.InReplyToAccountID,
		InReplyToAccount:         nil,
		BoostOfID:                status.BoostOfID,
		BoostOf:                  nil,
		BoostOfAccountID:         status.BoostOfAccountID,
		BoostOfAccount:           nil,
		ContentWarning:           status.ContentWarning,
		Visibility:               status.Visibility,
		Sensitive:                status.Sensitive,
		Language:                 status.Language,
		CreatedWithApplicationID: status.CreatedWithApplicationID,
		Federated:                status.Federated,
		Boostable:                status.Boostable,
		Replyable:                status.Replyable,
		Likeable:                 status.Likeable,
		ActivityStreamsType:      status.ActivityStreamsType,
		Text:                     status.Text,
		Pinned:                   status.Pinned,
	}
}