aboutsummaryrefslogtreecommitdiff
path: root/internal/caching/cache_typing.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/caching/cache_typing.go')
-rw-r--r--internal/caching/cache_typing.go184
1 files changed, 184 insertions, 0 deletions
diff --git a/internal/caching/cache_typing.go b/internal/caching/cache_typing.go
new file mode 100644
index 00000000..bd6a5fc1
--- /dev/null
+++ b/internal/caching/cache_typing.go
@@ -0,0 +1,184 @@
+// Copyright 2017 Vector Creations Ltd
+// Copyright 2017-2018 New Vector Ltd
+// Copyright 2019-2020 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package caching
+
+import (
+ "sync"
+ "time"
+)
+
+const defaultTypingTimeout = 10 * time.Second
+
+// userSet is a map of user IDs to a timer, timer fires at expiry.
+type userSet map[string]*time.Timer
+
+// TimeoutCallbackFn is a function called right after the removal of a user
+// from the typing user list due to timeout.
+// latestSyncPosition is the typing sync position after the removal.
+type TimeoutCallbackFn func(userID, roomID string, latestSyncPosition int64)
+
+type roomData struct {
+ syncPosition int64
+ userSet userSet
+}
+
+// EDUCache maintains a list of users typing in each room.
+type EDUCache struct {
+ sync.RWMutex
+ latestSyncPosition int64
+ data map[string]*roomData
+ timeoutCallback TimeoutCallbackFn
+}
+
+// Create a roomData with its sync position set to the latest sync position.
+// Must only be called after locking the cache.
+func (t *EDUCache) newRoomData() *roomData {
+ return &roomData{
+ syncPosition: t.latestSyncPosition,
+ userSet: make(userSet),
+ }
+}
+
+// NewTypingCache returns a new EDUCache initialised for use.
+func NewTypingCache() *EDUCache {
+ return &EDUCache{data: make(map[string]*roomData)}
+}
+
+// SetTimeoutCallback sets a callback function that is called right after
+// a user is removed from the typing user list due to timeout.
+func (t *EDUCache) SetTimeoutCallback(fn TimeoutCallbackFn) {
+ t.timeoutCallback = fn
+}
+
+// GetTypingUsers returns the list of users typing in a room.
+func (t *EDUCache) GetTypingUsers(roomID string) []string {
+ users, _ := t.GetTypingUsersIfUpdatedAfter(roomID, 0)
+ // 0 should work above because the first position used will be 1.
+ return users
+}
+
+// GetTypingUsersIfUpdatedAfter returns all users typing in this room with
+// updated == true if the typing sync position of the room is after the given
+// position. Otherwise, returns an empty slice with updated == false.
+func (t *EDUCache) GetTypingUsersIfUpdatedAfter(
+ roomID string, position int64,
+) (users []string, updated bool) {
+ t.RLock()
+ defer t.RUnlock()
+
+ roomData, ok := t.data[roomID]
+ if ok && roomData.syncPosition > position {
+ updated = true
+ userSet := roomData.userSet
+ users = make([]string, 0, len(userSet))
+ for userID := range userSet {
+ users = append(users, userID)
+ }
+ }
+
+ return
+}
+
+// AddTypingUser sets an user as typing in a room.
+// expire is the time when the user typing should time out.
+// if expire is nil, defaultTypingTimeout is assumed.
+// Returns the latest sync position for typing after update.
+func (t *EDUCache) AddTypingUser(
+ userID, roomID string, expire *time.Time,
+) int64 {
+ expireTime := getExpireTime(expire)
+ if until := time.Until(expireTime); until > 0 {
+ timer := time.AfterFunc(until, func() {
+ latestSyncPosition := t.RemoveUser(userID, roomID)
+ if t.timeoutCallback != nil {
+ t.timeoutCallback(userID, roomID, latestSyncPosition)
+ }
+ })
+ return t.addUser(userID, roomID, timer)
+ }
+ return t.GetLatestSyncPosition()
+}
+
+// addUser with mutex lock & replace the previous timer.
+// Returns the latest typing sync position after update.
+func (t *EDUCache) addUser(
+ userID, roomID string, expiryTimer *time.Timer,
+) int64 {
+ t.Lock()
+ defer t.Unlock()
+
+ t.latestSyncPosition++
+
+ if t.data[roomID] == nil {
+ t.data[roomID] = t.newRoomData()
+ } else {
+ t.data[roomID].syncPosition = t.latestSyncPosition
+ }
+
+ // Stop the timer to cancel the call to timeoutCallback
+ if timer, ok := t.data[roomID].userSet[userID]; ok {
+ // It may happen that at this stage the timer fires, but we now have a lock on
+ // it. Hence the execution of timeoutCallback will happen after we unlock. So
+ // we may lose a typing state, though this is highly unlikely. This can be
+ // mitigated by keeping another time.Time in the map and checking against it
+ // before removing, but its occurrence is so infrequent it does not seem
+ // worthwhile.
+ timer.Stop()
+ }
+
+ t.data[roomID].userSet[userID] = expiryTimer
+
+ return t.latestSyncPosition
+}
+
+// RemoveUser with mutex lock & stop the timer.
+// Returns the latest sync position for typing after update.
+func (t *EDUCache) RemoveUser(userID, roomID string) int64 {
+ t.Lock()
+ defer t.Unlock()
+
+ roomData, ok := t.data[roomID]
+ if !ok {
+ return t.latestSyncPosition
+ }
+
+ timer, ok := roomData.userSet[userID]
+ if !ok {
+ return t.latestSyncPosition
+ }
+
+ timer.Stop()
+ delete(roomData.userSet, userID)
+
+ t.latestSyncPosition++
+ t.data[roomID].syncPosition = t.latestSyncPosition
+
+ return t.latestSyncPosition
+}
+
+func (t *EDUCache) GetLatestSyncPosition() int64 {
+ t.Lock()
+ defer t.Unlock()
+ return t.latestSyncPosition
+}
+
+func getExpireTime(expire *time.Time) time.Time {
+ if expire != nil {
+ return *expire
+ }
+ return time.Now().Add(defaultTypingTimeout)
+}