~gioverse/chat

ref: 84203ce5a460 chat/example/kitchen/row-tracker.go -rw-r--r-- 4.0 KiB
84203ce5Jack Mordaunt widget/material: maintain size for empty images 4 months ago
                                                                                
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
package main

import (
	"log"
	"math/rand"
	"sort"
	"sync"
	"time"

	"git.sr.ht/~gioverse/chat/example/kitchen/gen"
	"git.sr.ht/~gioverse/chat/example/kitchen/model"
	"git.sr.ht/~gioverse/chat/list"
	lorem "github.com/drhodes/golorem"
)

// RowTracker is a stand-in for an application's data access logic.
// It stores a set of chat messages and can load them on request.
// It simulates network latency during the load operations for
// realism.
type RowTracker struct {
	// SimulateLatency is the maximum latency in milliseconds to
	// simulate on loads.
	SimulateLatency int
	sync.Mutex
	Rows          []list.Element
	SerialToIndex map[list.Serial]int
	Users         *model.Users
	Local         *model.User
	Generator     *gen.Generator
	// MaxLoads specifies the number of elements a given load in either
	// direction can return.
	MaxLoads int
}

// NewExampleData constructs a RowTracker populated with the provided
// quantity of messages.
func NewExampleData(users *model.Users, local *model.User, g *gen.Generator, size int) *RowTracker {
	rt := &RowTracker{
		SerialToIndex: make(map[list.Serial]int),
		Generator:     g,
		Local:         local,
		Users:         users,
	}
	for i := 0; i < size; i++ {
		rt.Add(g.GenHistoricMessage(rt.Users.Random()))
	}
	return rt
}

// SendMessage adds the message to the data model.
// This is analogous to interacting with the backend api.
func (rt *RowTracker) Send(user, content string) model.Message {
	u, ok := rt.Users.Lookup(user)
	if !ok {
		return model.Message{}
	}
	msg := rt.Generator.GenNewMessage(u, content)
	rt.Add(msg)
	return msg
}

// Add a list element as a row of data to track.
func (rt *RowTracker) Add(r list.Element) {
	rt.Lock()
	rt.Rows = append(rt.Rows, r)
	rt.reindex()
	rt.Unlock()
}

// Latest returns the latest element, or nil.
func (r *RowTracker) Latest() list.Element {
	r.Lock()
	final := len(r.Rows) - 1
	// Unlock because index will lock again.
	r.Unlock()
	return r.Index(final)
}

// Index returns the element at the given index, or nil.
func (r *RowTracker) Index(ii int) list.Element {
	r.Lock()
	defer r.Unlock()
	if len(r.Rows) == 0 || len(r.Rows) < ii {
		return nil
	}
	if ii < 0 {
		return r.Rows[0]
	}
	return r.Rows[ii]
}

// NewRow generates a new row.
func (r *RowTracker) NewRow() list.Element {
	el := r.Generator.GenNewMessage(r.Users.Random(), lorem.Paragraph(1, 4))
	r.Add(el)
	return el
}

// Load simulates loading chat history from a database or API. It
// sleeps for a random number of milliseconds and then returns
// some messages.
func (r *RowTracker) Load(dir list.Direction, relativeTo list.Serial) (loaded []list.Element, more bool) {
	if r.SimulateLatency > 0 {
		duration := time.Millisecond * time.Duration(rand.Intn(r.SimulateLatency))
		log.Println("sleeping", duration)
		time.Sleep(duration)
	}
	r.Lock()
	defer r.Unlock()
	defer func() {
		// Ensure the slice we return is backed by different memory than the underlying
		// RowTracker's slice, to avoid data races when the RowTracker sorts its storage.
		loaded = dupSlice(loaded)
	}()
	numRows := len(r.Rows)
	if relativeTo == list.NoSerial {
		// If loading relative to nothing, likely the chat interface is empty.
		// We should load the most recent messages first in this case, regardless
		// of the direction parameter.
		return r.Rows[numRows-min(r.MaxLoads, numRows):], numRows > r.MaxLoads
	}
	idx := r.SerialToIndex[relativeTo]
	if dir == list.After {
		end := min(numRows, idx+r.MaxLoads)
		return r.Rows[idx+1 : end], end < len(r.Rows)-1
	}
	start := maximum(0, idx-r.MaxLoads)
	return r.Rows[start:idx], start > 0
}

// Delete removes the element with the provided serial from storage.
func (r *RowTracker) Delete(serial list.Serial) {
	r.Lock()
	defer r.Unlock()
	idx := r.SerialToIndex[serial]
	sliceRemove(&r.Rows, idx)
	r.reindex()
}

func (r *RowTracker) reindex() {
	sort.Slice(r.Rows, func(i, j int) bool {
		return rowLessThan(r.Rows[i], r.Rows[j])
	})
	r.SerialToIndex = make(map[list.Serial]int)
	for i, row := range r.Rows {
		r.SerialToIndex[row.Serial()] = i
	}
}