| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | // GoToSocial | 
					
						
							|  |  |  | // Copyright (C) GoToSocial Authors admin@gotosocial.org | 
					
						
							|  |  |  | // SPDX-License-Identifier: AGPL-3.0-or-later | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // This program is free software: you can redistribute it and/or modify | 
					
						
							|  |  |  | // it under the terms of the GNU Affero General Public License as published by | 
					
						
							|  |  |  | // the Free Software Foundation, either version 3 of the License, or | 
					
						
							|  |  |  | // (at your option) any later version. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // This program is distributed in the hope that it will be useful, | 
					
						
							|  |  |  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | 
					
						
							|  |  |  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
					
						
							|  |  |  | // GNU Affero General Public License for more details. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // You should have received a copy of the GNU Affero General Public License | 
					
						
							|  |  |  | // along with this program.  If not, see <http://www.gnu.org/licenses/>. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | package timeline | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"slices" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"codeberg.org/gruf/go-structr" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/log" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/paging" | 
					
						
							| 
									
										
										
										
											2025-03-12 22:45:17 +00:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/util/xslices" | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 18:14:26 +01:00
										 |  |  | // repeatBoostDepth determines the minimum count | 
					
						
							|  |  |  | // of statuses after which repeat boosts, or boosts | 
					
						
							|  |  |  | // of the original, may appear. This is may not end | 
					
						
							|  |  |  | // up *exact*, as small races between insert and the | 
					
						
							|  |  |  | // repeatBoost calculation may allow 1 or so extra | 
					
						
							|  |  |  | // to sneak in ahead of time. but it mostly works! | 
					
						
							| 
									
										
										
										
											2025-04-08 14:31:58 +01:00
										 |  |  | const repeatBoostDepth = 40 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | // StatusMeta contains minimum viable metadata | 
					
						
							|  |  |  | // about a Status in order to cache a timeline. | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | type StatusMeta struct { | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	ID               string | 
					
						
							|  |  |  | 	AccountID        string | 
					
						
							|  |  |  | 	BoostOfID        string | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	BoostOfAccountID string | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 	// is an internal flag that may be set on | 
					
						
							|  |  |  | 	// a StatusMeta object that will prevent | 
					
						
							|  |  |  | 	// preparation of its apimodel.Status, due | 
					
						
							|  |  |  | 	// to it being a recently repeated boost. | 
					
						
							|  |  |  | 	repeatBoost bool | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	// prepared contains prepared frontend API | 
					
						
							|  |  |  | 	// model for the referenced status. This may | 
					
						
							|  |  |  | 	// or may-not be nil depending on whether the | 
					
						
							|  |  |  | 	// status has been "unprepared" since the last | 
					
						
							|  |  |  | 	// call to "prepare" the frontend model. | 
					
						
							|  |  |  | 	prepared *apimodel.Status | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 	// loaded is a temporary field that may be | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	// set for a newly loaded timeline status | 
					
						
							|  |  |  | 	// so that statuses don't need to be loaded | 
					
						
							|  |  |  | 	// from the database twice in succession. | 
					
						
							|  |  |  | 	// | 
					
						
							|  |  |  | 	// i.e. this will only be set if the status | 
					
						
							|  |  |  | 	// was newly inserted into the timeline cache. | 
					
						
							|  |  |  | 	// for existing cache items this will be nil. | 
					
						
							|  |  |  | 	loaded *gtsmodel.Status | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | // StatusTimeline provides a concurrency-safe sliding-window | 
					
						
							|  |  |  | // cache of the freshest statuses in a timeline. Internally, | 
					
						
							|  |  |  | // only StatusMeta{} objects themselves are stored, loading | 
					
						
							|  |  |  | // the actual statuses when necessary, but caching prepared | 
					
						
							|  |  |  | // frontend API models where possible. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // Notes on design: | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // Previously, and initially when designing this newer type, | 
					
						
							|  |  |  | // we had status timeline caches that would dynamically fill | 
					
						
							|  |  |  | // themselves with statuses on call to Load() with statuses | 
					
						
							|  |  |  | // at *any* location in the timeline, while simultaneously | 
					
						
							|  |  |  | // accepting new input of statuses from the background workers. | 
					
						
							|  |  |  | // This unfortunately can lead to situations where posts need | 
					
						
							|  |  |  | // to be fetched from the database, but the cache isn't aware | 
					
						
							|  |  |  | // they exist and instead returns an incomplete selection. | 
					
						
							|  |  |  | // This problem is best outlined by the follow simple example: | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // "what if my timeline cache contains posts 0-to-6 and 8-to-12, | 
					
						
							|  |  |  | // and i make a request for posts between 4-and-10 with no limit, | 
					
						
							|  |  |  | // how is it to know that it's missing post 7?" | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // The solution is to unfortunately remove a lot of the caching | 
					
						
							|  |  |  | // of "older areas" of the timeline, and instead just have it | 
					
						
							|  |  |  | // be a sliding window of the freshest posts of that timeline. | 
					
						
							|  |  |  | // It gets preloaded initially on start / first-call, and kept | 
					
						
							|  |  |  | // up-to-date with new posts by streamed inserts from background | 
					
						
							|  |  |  | // workers. Any requests for posts outside this we know therefore | 
					
						
							|  |  |  | // must hit the database, (which we then *don't* cache). | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | type StatusTimeline struct { | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-01 14:32:36 +01:00
										 |  |  | 	// underlying timeline cache of *StatusMeta{}, | 
					
						
							|  |  |  | 	// primary-keyed by ID, with extra indices below. | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	cache structr.Timeline[*StatusMeta, string] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | 	// preloader synchronizes preload | 
					
						
							|  |  |  | 	// state of the timeline cache. | 
					
						
							|  |  |  | 	preloader preloader | 
					
						
							| 
									
										
										
										
											2025-04-08 16:28:33 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	// fast-access cache indices. | 
					
						
							|  |  |  | 	idx_ID               *structr.Index //nolint:revive | 
					
						
							|  |  |  | 	idx_AccountID        *structr.Index //nolint:revive | 
					
						
							|  |  |  | 	idx_BoostOfID        *structr.Index //nolint:revive | 
					
						
							|  |  |  | 	idx_BoostOfAccountID *structr.Index //nolint:revive | 
					
						
							| 
									
										
										
										
											2025-03-12 22:45:17 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 	// cutoff and maximum item lengths. | 
					
						
							|  |  |  | 	// the timeline is trimmed back to | 
					
						
							|  |  |  | 	// cutoff on each call to Trim(), | 
					
						
							|  |  |  | 	// and maximum len triggers a Trim(). | 
					
						
							| 
									
										
										
										
											2025-03-20 14:01:26 +00:00
										 |  |  | 	// | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 	// the timeline itself does not | 
					
						
							| 
									
										
										
										
											2025-04-01 14:32:36 +01:00
										 |  |  | 	// limit items due to complexities | 
					
						
							|  |  |  | 	// it would introduce, so we apply | 
					
						
							|  |  |  | 	// a 'cut-off' at regular intervals. | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 	cut, max int | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 16:48:20 +01:00
										 |  |  | // Init will initialize the timeline for usage, | 
					
						
							|  |  |  | // by preparing internal indices etc. This also | 
					
						
							|  |  |  | // sets the given max capacity for Trim() operations. | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | func (t *StatusTimeline) Init(cap int) { | 
					
						
							|  |  |  | 	t.cache.Init(structr.TimelineConfig[*StatusMeta, string]{ | 
					
						
							| 
									
										
										
										
											2025-03-31 16:48:20 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Timeline item primary key field. | 
					
						
							| 
									
										
										
										
											2025-03-28 13:51:31 +00:00
										 |  |  | 		PKey: structr.IndexConfig{Fields: "ID"}, | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 16:48:20 +01:00
										 |  |  | 		// Additional indexed fields. | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		Indices: []structr.IndexConfig{ | 
					
						
							|  |  |  | 			{Fields: "AccountID", Multiple: true}, | 
					
						
							|  |  |  | 			{Fields: "BoostOfAccountID", Multiple: true}, | 
					
						
							| 
									
										
										
										
											2025-04-03 17:17:39 +01:00
										 |  |  | 			{Fields: "BoostOfID", Multiple: true}, | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 16:48:20 +01:00
										 |  |  | 		// Timeline item copy function. | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		Copy: func(s *StatusMeta) *StatusMeta { | 
					
						
							|  |  |  | 			var prepared *apimodel.Status | 
					
						
							|  |  |  | 			if s.prepared != nil { | 
					
						
							|  |  |  | 				prepared = new(apimodel.Status) | 
					
						
							|  |  |  | 				*prepared = *s.prepared | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			return &StatusMeta{ | 
					
						
							|  |  |  | 				ID:               s.ID, | 
					
						
							|  |  |  | 				AccountID:        s.AccountID, | 
					
						
							|  |  |  | 				BoostOfID:        s.BoostOfID, | 
					
						
							|  |  |  | 				BoostOfAccountID: s.BoostOfAccountID, | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 				repeatBoost:      s.repeatBoost, | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 				loaded:           nil, // NEVER stored | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 				prepared:         prepared, | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 15:39:15 +01:00
										 |  |  | 	// Get fast index lookup ptrs. | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	t.idx_ID = t.cache.Index("ID") | 
					
						
							|  |  |  | 	t.idx_AccountID = t.cache.Index("AccountID") | 
					
						
							|  |  |  | 	t.idx_BoostOfID = t.cache.Index("BoostOfID") | 
					
						
							|  |  |  | 	t.idx_BoostOfAccountID = t.cache.Index("BoostOfAccountID") | 
					
						
							| 
									
										
										
										
											2025-03-31 15:41:15 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 	// Set maximum capacity and | 
					
						
							|  |  |  | 	// cutoff threshold we trim to. | 
					
						
							|  |  |  | 	t.cut = int(0.60 * float64(cap)) | 
					
						
							| 
									
										
										
										
											2025-03-31 15:41:15 +01:00
										 |  |  | 	t.max = cap | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | // Preload will fill with StatusTimeline{} cache with | 
					
						
							|  |  |  | // the latest sliding window of status metadata for the | 
					
						
							|  |  |  | // timeline type returned by database 'loadPage' function. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // This function is concurrency-safe and repeated calls to | 
					
						
							|  |  |  | // it when already preloaded will be no-ops. To trigger a | 
					
						
							|  |  |  | // preload as being required, call .Clear(). | 
					
						
							|  |  |  | func (t *StatusTimeline) Preload( | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// loadPage should load the timeline of given page for cache hydration. | 
					
						
							|  |  |  | 	loadPage func(page *paging.Page) (statuses []*gtsmodel.Status, err error), | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// filter can be used to perform filtering of returned | 
					
						
							|  |  |  | 	// statuses BEFORE insert into cache. i.e. this will effect | 
					
						
							|  |  |  | 	// what actually gets stored in the timeline cache. | 
					
						
							| 
									
										
										
										
											2025-04-08 22:04:43 +01:00
										 |  |  | 	filter func(each *gtsmodel.Status) (delete bool), | 
					
						
							| 
									
										
										
										
											2025-04-08 16:28:33 +01:00
										 |  |  | ) ( | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | 	n int, | 
					
						
							| 
									
										
										
										
											2025-04-08 16:28:33 +01:00
										 |  |  | 	err error, | 
					
						
							|  |  |  | ) { | 
					
						
							| 
									
										
										
										
											2025-04-08 18:14:26 +01:00
										 |  |  | 	t.preloader.CheckPreload(func() { | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | 		n, err = t.preload(loadPage, filter) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return | 
					
						
							| 
									
										
										
										
											2025-04-08 16:28:33 +01:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | 		// Mark preloaded. | 
					
						
							| 
									
										
										
										
											2025-04-08 18:14:26 +01:00
										 |  |  | 		t.preloader.Done() | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | 	}) | 
					
						
							|  |  |  | 	return | 
					
						
							| 
									
										
										
										
											2025-04-08 16:28:33 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | // preload contains the core logic of | 
					
						
							|  |  |  | // Preload(), without t.preloader checks. | 
					
						
							|  |  |  | func (t *StatusTimeline) preload( | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// loadPage should load the timeline of given page for cache hydration. | 
					
						
							|  |  |  | 	loadPage func(page *paging.Page) (statuses []*gtsmodel.Status, err error), | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// filter can be used to perform filtering of returned | 
					
						
							|  |  |  | 	// statuses BEFORE insert into cache. i.e. this will effect | 
					
						
							|  |  |  | 	// what actually gets stored in the timeline cache. | 
					
						
							| 
									
										
										
										
											2025-04-08 22:04:43 +01:00
										 |  |  | 	filter func(each *gtsmodel.Status) (delete bool), | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | ) (int, error) { | 
					
						
							|  |  |  | 	if loadPage == nil { | 
					
						
							|  |  |  | 		panic("nil load page func") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 16:28:33 +01:00
										 |  |  | 	// Clear timeline | 
					
						
							|  |  |  | 	// before preload. | 
					
						
							|  |  |  | 	t.cache.Clear() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | 	// Our starting, page at the top | 
					
						
							|  |  |  | 	// of the possible timeline. | 
					
						
							|  |  |  | 	page := new(paging.Page) | 
					
						
							|  |  |  | 	order := paging.OrderDescending | 
					
						
							|  |  |  | 	page.Max.Order = order | 
					
						
							|  |  |  | 	page.Max.Value = plus24hULID() | 
					
						
							|  |  |  | 	page.Min.Order = order | 
					
						
							|  |  |  | 	page.Min.Value = "" | 
					
						
							|  |  |  | 	page.Limit = 100 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Prepare a slice for gathering status meta. | 
					
						
							|  |  |  | 	metas := make([]*StatusMeta, 0, page.Limit) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var n int | 
					
						
							|  |  |  | 	for n < t.cut { | 
					
						
							|  |  |  | 		// Load page of timeline statuses. | 
					
						
							|  |  |  | 		statuses, err := loadPage(page) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return n, gtserror.Newf("error loading statuses: %w", err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// No more statuses from | 
					
						
							|  |  |  | 		// load function = at end. | 
					
						
							|  |  |  | 		if len(statuses) == 0 { | 
					
						
							|  |  |  | 			break | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Update our next page cursor from statuses. | 
					
						
							|  |  |  | 		page.Max.Value = statuses[len(statuses)-1].ID | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Perform any filtering on newly loaded statuses. | 
					
						
							| 
									
										
										
										
											2025-04-08 22:04:43 +01:00
										 |  |  | 		statuses = doStatusFilter(statuses, filter) | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// After filtering no more | 
					
						
							|  |  |  | 		// statuses remain, retry. | 
					
						
							|  |  |  | 		if len(statuses) == 0 { | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Convert statuses to meta and insert. | 
					
						
							|  |  |  | 		metas = toStatusMeta(metas[:0], statuses) | 
					
						
							|  |  |  | 		n = t.cache.Insert(metas...) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 14:31:58 +01:00
										 |  |  | 	// This is a potentially 100-1000s size map, | 
					
						
							|  |  |  | 	// but still easily manageable memory-wise. | 
					
						
							|  |  |  | 	recentBoosts := make(map[string]int, t.cut) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 15:19:36 +01:00
										 |  |  | 	// Iterate timeline ascending (i.e. oldest -> newest), marking | 
					
						
							|  |  |  | 	// entry IDs and marking down if boosts have been seen recently. | 
					
						
							| 
									
										
										
										
											2025-04-08 14:31:58 +01:00
										 |  |  | 	for idx, value := range t.cache.RangeUnsafe(structr.Asc) { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Store current ID in map. | 
					
						
							|  |  |  | 		recentBoosts[value.ID] = idx | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// If it's a boost, check if the original, | 
					
						
							|  |  |  | 		// or a boost of it has been seen recently. | 
					
						
							|  |  |  | 		if id := value.BoostOfID; id != "" { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Check if seen recently. | 
					
						
							| 
									
										
										
										
											2025-04-08 21:50:07 +01:00
										 |  |  | 			last, ok := recentBoosts[id] | 
					
						
							|  |  |  | 			repeat := ok && (idx-last) < 40 | 
					
						
							|  |  |  | 			value.repeatBoost = repeat | 
					
						
							| 
									
										
										
										
											2025-04-08 14:31:58 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			// Update last-seen idx. | 
					
						
							|  |  |  | 			recentBoosts[id] = idx | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | 	return n, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | // Load will load given page of timeline statuses. First it | 
					
						
							|  |  |  | // will prioritize fetching statuses from the sliding window | 
					
						
							|  |  |  | // that is the timeline cache of latest statuses, else it will | 
					
						
							|  |  |  | // fall back to loading from the database using callback funcs. | 
					
						
							|  |  |  | // The returned string values are the low / high status ID | 
					
						
							|  |  |  | // paging values, used in calculating next / prev page links. | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | func (t *StatusTimeline) Load( | 
					
						
							|  |  |  | 	ctx context.Context, | 
					
						
							|  |  |  | 	page *paging.Page, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// loadPage should load the timeline of given page for cache hydration. | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	loadPage func(page *paging.Page) (statuses []*gtsmodel.Status, err error), | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	// loadIDs should load status models with given IDs, this is used | 
					
						
							|  |  |  | 	// to load status models of already cached entries in the timeline. | 
					
						
							|  |  |  | 	loadIDs func(ids []string) (statuses []*gtsmodel.Status, err error), | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 22:04:43 +01:00
										 |  |  | 	// filter performs filtering of returned statuses. | 
					
						
							|  |  |  | 	filter func(each *gtsmodel.Status) (delete bool), | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// prepareAPI should prepare internal status model to frontend API model. | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	prepareAPI func(status *gtsmodel.Status) (apiStatus *apimodel.Status, err error), | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | ) ( | 
					
						
							|  |  |  | 	[]*apimodel.Status, | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	string, // lo | 
					
						
							|  |  |  | 	string, // hi | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	error, | 
					
						
							|  |  |  | ) { | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | 	var err error | 
					
						
							| 
									
										
										
										
											2025-04-08 16:28:33 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	// Get paging details. | 
					
						
							| 
									
										
										
										
											2025-03-25 12:12:09 +00:00
										 |  |  | 	lo := page.Min.Value | 
					
						
							|  |  |  | 	hi := page.Max.Value | 
					
						
							| 
									
										
										
										
											2025-03-31 17:40:45 +01:00
										 |  |  | 	limit := page.Limit | 
					
						
							|  |  |  | 	order := page.Order() | 
					
						
							|  |  |  | 	dir := toDirection(order) | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 	// Use a copy of current page so | 
					
						
							|  |  |  | 	// we can repeatedly update it. | 
					
						
							|  |  |  | 	nextPg := new(paging.Page) | 
					
						
							|  |  |  | 	*nextPg = *page | 
					
						
							|  |  |  | 	nextPg.Min.Value = lo | 
					
						
							|  |  |  | 	nextPg.Max.Value = hi | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | 	// Interstitial meta objects. | 
					
						
							|  |  |  | 	var metas []*StatusMeta | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | 	// Returned frontend API statuses. | 
					
						
							| 
									
										
										
										
											2025-04-08 15:19:36 +01:00
										 |  |  | 	var apiStatuses []*apimodel.Status | 
					
						
							| 
									
										
										
										
											2025-03-25 12:12:09 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | 	// TODO: we can remove this nil | 
					
						
							|  |  |  | 	// check when we've updated all | 
					
						
							|  |  |  | 	// our timeline endpoints to have | 
					
						
							|  |  |  | 	// streamed timeline caches. | 
					
						
							|  |  |  | 	if t != nil { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Ensure timeline has been preloaded. | 
					
						
							|  |  |  | 		_, err = t.Preload(loadPage, filter) | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 		if err != nil { | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | 			return nil, "", "", err | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | 		// First we attempt to load status | 
					
						
							|  |  |  | 		// metadata entries from the timeline | 
					
						
							|  |  |  | 		// cache, up to given limit. | 
					
						
							|  |  |  | 		metas = t.cache.Select( | 
					
						
							|  |  |  | 			util.PtrIf(lo), | 
					
						
							|  |  |  | 			util.PtrIf(hi), | 
					
						
							|  |  |  | 			util.PtrIf(limit), | 
					
						
							|  |  |  | 			dir, | 
					
						
							|  |  |  | 		) | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | 		if len(metas) > 0 { | 
					
						
							|  |  |  | 			// Before we can do any filtering, we need | 
					
						
							|  |  |  | 			// to load status models for cached entries. | 
					
						
							|  |  |  | 			err = loadStatuses(metas, loadIDs) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				return nil, "", "", gtserror.Newf("error loading statuses: %w", err) | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2025-04-08 15:19:36 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 18:46:04 +01:00
										 |  |  | 			// Set returned lo, hi values. | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | 			lo = metas[len(metas)-1].ID | 
					
						
							|  |  |  | 			hi = metas[0].ID | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Allocate slice of expected required API models. | 
					
						
							|  |  |  | 			apiStatuses = make([]*apimodel.Status, 0, len(metas)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Prepare frontend API models for | 
					
						
							|  |  |  | 			// the cached statuses. For now this | 
					
						
							|  |  |  | 			// also does its own extra filtering. | 
					
						
							|  |  |  | 			apiStatuses = prepareStatuses(ctx, | 
					
						
							|  |  |  | 				metas, | 
					
						
							|  |  |  | 				prepareAPI, | 
					
						
							|  |  |  | 				apiStatuses, | 
					
						
							|  |  |  | 				limit, | 
					
						
							|  |  |  | 			) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 15:19:36 +01:00
										 |  |  | 	// If no cached timeline statuses | 
					
						
							|  |  |  | 	// were found for page, we need to | 
					
						
							|  |  |  | 	// call through to the database. | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | 	if len(apiStatuses) == 0 { | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 15:19:36 +01:00
										 |  |  | 		// Pass through to main timeline db load function. | 
					
						
							|  |  |  | 		apiStatuses, lo, hi, err = loadStatusTimeline(ctx, | 
					
						
							|  |  |  | 			nextPg, | 
					
						
							|  |  |  | 			metas, | 
					
						
							|  |  |  | 			apiStatuses, | 
					
						
							|  |  |  | 			loadPage, | 
					
						
							|  |  |  | 			filter, | 
					
						
							|  |  |  | 			prepareAPI, | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, "", "", err | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 	if order.Ascending() { | 
					
						
							|  |  |  | 		// The caller always expects the statuses | 
					
						
							|  |  |  | 		// to be returned in DESC order, but we | 
					
						
							|  |  |  | 		// build the status slice in paging order. | 
					
						
							|  |  |  | 		// If paging ASC, we need to reverse the | 
					
						
							|  |  |  | 		// returned statuses and paging values. | 
					
						
							|  |  |  | 		slices.Reverse(apiStatuses) | 
					
						
							|  |  |  | 		lo, hi = hi, lo | 
					
						
							| 
									
										
										
										
											2025-04-01 13:51:17 +01:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	return apiStatuses, lo, hi, nil | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | // loadStatusTimeline encapsulates the logic of iteratively | 
					
						
							|  |  |  | // attempting to load a status timeline page from the database, | 
					
						
							|  |  |  | // that is in the form of given callback functions. these will | 
					
						
							|  |  |  | // then be prepared to frontend API models for return. | 
					
						
							| 
									
										
										
										
											2025-04-08 15:19:36 +01:00
										 |  |  | // | 
					
						
							| 
									
										
										
										
											2025-04-08 18:03:32 +01:00
										 |  |  | // in time it may make sense to move this logic | 
					
						
							|  |  |  | // into the StatusTimeline{}.Load() function. | 
					
						
							| 
									
										
										
										
											2025-04-08 15:19:36 +01:00
										 |  |  | func loadStatusTimeline( | 
					
						
							|  |  |  | 	ctx context.Context, | 
					
						
							|  |  |  | 	nextPg *paging.Page, | 
					
						
							|  |  |  | 	metas []*StatusMeta, | 
					
						
							|  |  |  | 	apiStatuses []*apimodel.Status, | 
					
						
							|  |  |  | 	loadPage func(page *paging.Page) (statuses []*gtsmodel.Status, err error), | 
					
						
							| 
									
										
										
										
											2025-04-08 22:04:43 +01:00
										 |  |  | 	filter func(each *gtsmodel.Status) (delete bool), | 
					
						
							| 
									
										
										
										
											2025-04-08 15:19:36 +01:00
										 |  |  | 	prepareAPI func(status *gtsmodel.Status) (apiStatus *apimodel.Status, err error), | 
					
						
							|  |  |  | ) ( | 
					
						
							|  |  |  | 	[]*apimodel.Status, | 
					
						
							|  |  |  | 	string, // lo | 
					
						
							|  |  |  | 	string, // hi | 
					
						
							|  |  |  | 	error, | 
					
						
							|  |  |  | ) { | 
					
						
							|  |  |  | 	if loadPage == nil { | 
					
						
							|  |  |  | 		panic("nil load page func") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Lowest and highest ID | 
					
						
							|  |  |  | 	// vals of loaded statuses. | 
					
						
							|  |  |  | 	var lo, hi string | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 15:19:36 +01:00
										 |  |  | 	// Extract paging params. | 
					
						
							|  |  |  | 	order := nextPg.Order() | 
					
						
							|  |  |  | 	limit := nextPg.Limit | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | 	// Load a little more than | 
					
						
							|  |  |  | 	// limit to reduce db calls. | 
					
						
							|  |  |  | 	nextPg.Limit += 10 | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 15:19:36 +01:00
										 |  |  | 	// Ensure we have a slice of meta objects to | 
					
						
							|  |  |  | 	// use in later preparation of the API models. | 
					
						
							|  |  |  | 	metas = xslices.GrowJust(metas[:0], nextPg.Limit) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Ensure we have a slice of required frontend API models. | 
					
						
							|  |  |  | 	apiStatuses = xslices.GrowJust(apiStatuses[:0], nextPg.Limit) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | 	// Perform maximum of 5 load | 
					
						
							|  |  |  | 	// attempts fetching statuses. | 
					
						
							|  |  |  | 	for i := 0; i < 5; i++ { | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | 		// Load next timeline statuses. | 
					
						
							|  |  |  | 		statuses, err := loadPage(nextPg) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, "", "", gtserror.Newf("error loading timeline: %w", err) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | 		// No more statuses from | 
					
						
							|  |  |  | 		// load function = at end. | 
					
						
							|  |  |  | 		if len(statuses) == 0 { | 
					
						
							|  |  |  | 			break | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | 		if hi == "" { | 
					
						
							|  |  |  | 			// Set hi returned paging | 
					
						
							|  |  |  | 			// value if not already set. | 
					
						
							|  |  |  | 			hi = statuses[0].ID | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | 		// Update nextPg cursor parameter for next database query. | 
					
						
							|  |  |  | 		nextPageParams(nextPg, statuses[len(statuses)-1].ID, order) | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | 		// Perform any filtering on newly loaded statuses. | 
					
						
							| 
									
										
										
										
											2025-04-08 22:04:43 +01:00
										 |  |  | 		statuses = doStatusFilter(statuses, filter) | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | 		// After filtering no more | 
					
						
							|  |  |  | 		// statuses remain, retry. | 
					
						
							|  |  |  | 		if len(statuses) == 0 { | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 18:46:04 +01:00
										 |  |  | 		// Convert to our interstitial meta type. | 
					
						
							| 
									
										
										
										
											2025-04-08 15:19:36 +01:00
										 |  |  | 		metas = toStatusMeta(metas[:0], statuses) | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | 		// Prepare frontend API models for | 
					
						
							|  |  |  | 		// the loaded statuses. For now this | 
					
						
							|  |  |  | 		// also does its own extra filtering. | 
					
						
							|  |  |  | 		apiStatuses = prepareStatuses(ctx, | 
					
						
							|  |  |  | 			metas, | 
					
						
							|  |  |  | 			prepareAPI, | 
					
						
							|  |  |  | 			apiStatuses, | 
					
						
							|  |  |  | 			limit, | 
					
						
							|  |  |  | 		) | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | 		// If we have anything, return | 
					
						
							|  |  |  | 		// here. Even if below limit. | 
					
						
							|  |  |  | 		if len(apiStatuses) > 0 { | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | 			// Set returned lo status paging value. | 
					
						
							|  |  |  | 			lo = apiStatuses[len(apiStatuses)-1].ID | 
					
						
							|  |  |  | 			break | 
					
						
							| 
									
										
										
										
											2025-04-03 13:51:47 +01:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return apiStatuses, lo, hi, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 15:19:36 +01:00
										 |  |  | // InsertOne allows you to insert a single status into the timeline, with optional prepared API model. | 
					
						
							|  |  |  | // The return value indicates whether status should be skipped from streams, e.g. if already boosted recently. | 
					
						
							|  |  |  | func (t *StatusTimeline) InsertOne(status *gtsmodel.Status, prepared *apimodel.Status) (skip bool) { | 
					
						
							| 
									
										
										
										
											2025-04-08 18:14:26 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// If timeline no preloaded, i.e. | 
					
						
							|  |  |  | 	// no-one using it, don't insert. | 
					
						
							|  |  |  | 	if !t.preloader.Check() { | 
					
						
							|  |  |  | 		return false | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 	if status.BoostOfID != "" { | 
					
						
							| 
									
										
										
										
											2025-04-08 18:39:08 +01:00
										 |  |  | 		// Check through top $repeatBoostDepth number of items. | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 		for i, value := range t.cache.RangeUnsafe(structr.Desc) { | 
					
						
							|  |  |  | 			if i >= repeatBoostDepth { | 
					
						
							|  |  |  | 				break | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// If inserted status has already been boosted, or original was posted | 
					
						
							|  |  |  | 			// within last $repeatBoostDepth, we indicate it as a repeated boost. | 
					
						
							|  |  |  | 			if value.ID == status.BoostOfID || value.BoostOfID == status.BoostOfID { | 
					
						
							| 
									
										
										
										
											2025-04-08 15:19:36 +01:00
										 |  |  | 				skip = true | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 				break | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 18:39:08 +01:00
										 |  |  | 	// Insert new timeline status. | 
					
						
							|  |  |  | 	t.cache.Insert(&StatusMeta{ | 
					
						
							| 
									
										
										
										
											2025-03-20 13:15:25 +00:00
										 |  |  | 		ID:               status.ID, | 
					
						
							|  |  |  | 		AccountID:        status.AccountID, | 
					
						
							|  |  |  | 		BoostOfID:        status.BoostOfID, | 
					
						
							|  |  |  | 		BoostOfAccountID: status.BoostOfAccountID, | 
					
						
							| 
									
										
										
										
											2025-04-08 15:19:36 +01:00
										 |  |  | 		repeatBoost:      skip, | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 		loaded:           nil, | 
					
						
							| 
									
										
										
										
											2025-03-20 13:15:25 +00:00
										 |  |  | 		prepared:         prepared, | 
					
						
							| 
									
										
										
										
											2025-04-08 18:39:08 +01:00
										 |  |  | 	}) | 
					
						
							| 
									
										
										
										
											2025-04-08 11:58:38 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	return | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | // RemoveByStatusID removes all cached timeline entries pertaining to | 
					
						
							|  |  |  | // status ID, including those that may be a boost of the given status. | 
					
						
							|  |  |  | func (t *StatusTimeline) RemoveByStatusIDs(statusIDs ...string) { | 
					
						
							|  |  |  | 	keys := make([]structr.Key, len(statusIDs)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	// Nil check indices outside loops. | 
					
						
							|  |  |  | 	if t.idx_ID == nil || | 
					
						
							|  |  |  | 		t.idx_BoostOfID == nil { | 
					
						
							|  |  |  | 		panic("indices are nil") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	// Convert statusIDs to index keys. | 
					
						
							|  |  |  | 	for i, id := range statusIDs { | 
					
						
							|  |  |  | 		keys[i] = t.idx_ID.Key(id) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Invalidate all cached entries with IDs. | 
					
						
							|  |  |  | 	t.cache.Invalidate(t.idx_ID, keys...) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Convert statusIDs to index keys. | 
					
						
							|  |  |  | 	for i, id := range statusIDs { | 
					
						
							|  |  |  | 		keys[i] = t.idx_BoostOfID.Key(id) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Invalidate all cached entries as boost of IDs. | 
					
						
							|  |  |  | 	t.cache.Invalidate(t.idx_BoostOfID, keys...) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // RemoveByAccountID removes all cached timeline entries authored by | 
					
						
							|  |  |  | // account ID, including those that may be boosted by account ID. | 
					
						
							|  |  |  | func (t *StatusTimeline) RemoveByAccountIDs(accountIDs ...string) { | 
					
						
							|  |  |  | 	keys := make([]structr.Key, len(accountIDs)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	// Nil check indices outside loops. | 
					
						
							|  |  |  | 	if t.idx_AccountID == nil || | 
					
						
							|  |  |  | 		t.idx_BoostOfAccountID == nil { | 
					
						
							|  |  |  | 		panic("indices are nil") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	// Convert accountIDs to index keys. | 
					
						
							|  |  |  | 	for i, id := range accountIDs { | 
					
						
							|  |  |  | 		keys[i] = t.idx_AccountID.Key(id) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Invalidate all cached entries as by IDs. | 
					
						
							|  |  |  | 	t.cache.Invalidate(t.idx_AccountID, keys...) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Convert accountIDs to index keys. | 
					
						
							|  |  |  | 	for i, id := range accountIDs { | 
					
						
							|  |  |  | 		keys[i] = t.idx_BoostOfAccountID.Key(id) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Invalidate all cached entries as boosted by IDs. | 
					
						
							|  |  |  | 	t.cache.Invalidate(t.idx_BoostOfAccountID, keys...) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // UnprepareByStatusIDs removes cached frontend API models for all cached | 
					
						
							|  |  |  | // timeline entries pertaining to status ID, including boosts of given status. | 
					
						
							|  |  |  | func (t *StatusTimeline) UnprepareByStatusIDs(statusIDs ...string) { | 
					
						
							|  |  |  | 	keys := make([]structr.Key, len(statusIDs)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	// Nil check indices outside loops. | 
					
						
							|  |  |  | 	if t.idx_ID == nil || | 
					
						
							|  |  |  | 		t.idx_BoostOfID == nil { | 
					
						
							|  |  |  | 		panic("indices are nil") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	// Convert statusIDs to index keys. | 
					
						
							|  |  |  | 	for i, id := range statusIDs { | 
					
						
							|  |  |  | 		keys[i] = t.idx_ID.Key(id) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	// Unprepare all statuses stored under StatusMeta.ID. | 
					
						
							| 
									
										
										
										
											2025-04-07 13:49:49 +01:00
										 |  |  | 	for meta := range t.cache.RangeKeysUnsafe(t.idx_ID, keys...) { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		meta.prepared = nil | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Convert statusIDs to index keys. | 
					
						
							|  |  |  | 	for i, id := range statusIDs { | 
					
						
							|  |  |  | 		keys[i] = t.idx_BoostOfID.Key(id) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	// Unprepare all statuses stored under StatusMeta.BoostOfID. | 
					
						
							| 
									
										
										
										
											2025-04-07 13:49:49 +01:00
										 |  |  | 	for meta := range t.cache.RangeKeysUnsafe(t.idx_BoostOfID, keys...) { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		meta.prepared = nil | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // UnprepareByAccountIDs removes cached frontend API models for all cached | 
					
						
							|  |  |  | // timeline entries authored by account ID, including boosts by account ID. | 
					
						
							|  |  |  | func (t *StatusTimeline) UnprepareByAccountIDs(accountIDs ...string) { | 
					
						
							|  |  |  | 	keys := make([]structr.Key, len(accountIDs)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	// Nil check indices outside loops. | 
					
						
							|  |  |  | 	if t.idx_AccountID == nil || | 
					
						
							|  |  |  | 		t.idx_BoostOfAccountID == nil { | 
					
						
							|  |  |  | 		panic("indices are nil") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	// Convert accountIDs to index keys. | 
					
						
							|  |  |  | 	for i, id := range accountIDs { | 
					
						
							|  |  |  | 		keys[i] = t.idx_AccountID.Key(id) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	// Unprepare all statuses stored under StatusMeta.AccountID. | 
					
						
							| 
									
										
										
										
											2025-04-07 13:49:49 +01:00
										 |  |  | 	for meta := range t.cache.RangeKeysUnsafe(t.idx_AccountID, keys...) { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		meta.prepared = nil | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Convert accountIDs to index keys. | 
					
						
							|  |  |  | 	for i, id := range accountIDs { | 
					
						
							|  |  |  | 		keys[i] = t.idx_BoostOfAccountID.Key(id) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	// Unprepare all statuses stored under StatusMeta.BoostOfAccountID. | 
					
						
							| 
									
										
										
										
											2025-04-07 13:49:49 +01:00
										 |  |  | 	for meta := range t.cache.RangeKeysUnsafe(t.idx_BoostOfAccountID, keys...) { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		meta.prepared = nil | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-20 13:15:25 +00:00
										 |  |  | // UnprepareAll removes cached frontend API | 
					
						
							|  |  |  | // models for all cached timeline entries. | 
					
						
							|  |  |  | func (t *StatusTimeline) UnprepareAll() { | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 	for _, value := range t.cache.RangeUnsafe(structr.Asc) { | 
					
						
							| 
									
										
										
										
											2025-03-20 13:15:25 +00:00
										 |  |  | 		value.prepared = nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-01 14:10:53 +01:00
										 |  |  | // Trim will ensure that receiving timeline is less than or | 
					
						
							|  |  |  | // equal in length to the given threshold percentage of the | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | // timeline's preconfigured maximum capacity. This will always | 
					
						
							|  |  |  | // trim from the bottom-up to prioritize streamed inserts. | 
					
						
							|  |  |  | func (t *StatusTimeline) Trim() { t.cache.Trim(t.cut, structr.Asc) } | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 16:28:33 +01:00
										 |  |  | // Clear will mark the entire timeline as requiring preload, | 
					
						
							|  |  |  | // which will trigger a clear and reload of the entire thing. | 
					
						
							| 
									
										
										
										
											2025-04-08 18:14:26 +01:00
										 |  |  | func (t *StatusTimeline) Clear() { t.preloader.Clear() } | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | // prepareStatuses takes a slice of cached (or, freshly loaded!) StatusMeta{} | 
					
						
							|  |  |  | // models, and use given function to return prepared frontend API models. | 
					
						
							|  |  |  | func prepareStatuses( | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	ctx context.Context, | 
					
						
							|  |  |  | 	meta []*StatusMeta, | 
					
						
							|  |  |  | 	prepareAPI func(*gtsmodel.Status) (*apimodel.Status, error), | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 	apiStatuses []*apimodel.Status, | 
					
						
							|  |  |  | 	limit int, | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | ) []*apimodel.Status { | 
					
						
							| 
									
										
										
										
											2025-03-26 20:43:41 +00:00
										 |  |  | 	switch { //nolint:gocritic | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	case prepareAPI == nil: | 
					
						
							|  |  |  | 		panic("nil prepare fn") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-24 21:34:36 +00:00
										 |  |  | 	// Iterate the given StatusMeta objects for pre-prepared | 
					
						
							|  |  |  | 	// frontend models, otherwise attempting to prepare them. | 
					
						
							|  |  |  | 	for _, meta := range meta { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | 		// Check if we have prepared enough | 
					
						
							|  |  |  | 		// API statuses for caller to return. | 
					
						
							|  |  |  | 		if len(apiStatuses) >= limit { | 
					
						
							|  |  |  | 			break | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		if meta.loaded == nil { | 
					
						
							|  |  |  | 			// We failed loading this | 
					
						
							|  |  |  | 			// status, skip preparing. | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 17:46:17 +01:00
										 |  |  | 		if meta.repeatBoost { | 
					
						
							|  |  |  | 			// This is a repeat boost in | 
					
						
							|  |  |  | 			// short timespan, skip it. | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-24 21:34:36 +00:00
										 |  |  | 		if meta.prepared == nil { | 
					
						
							|  |  |  | 			var err error | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Prepare the provided status to frontend. | 
					
						
							|  |  |  | 			meta.prepared, err = prepareAPI(meta.loaded) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				log.Errorf(ctx, "error preparing status %s: %v", meta.loaded.URI, err) | 
					
						
							|  |  |  | 				continue | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-07 13:49:49 +01:00
										 |  |  | 		// Append to return slice. | 
					
						
							| 
									
										
										
										
											2025-03-24 21:34:36 +00:00
										 |  |  | 		if meta.prepared != nil { | 
					
						
							|  |  |  | 			apiStatuses = append(apiStatuses, meta.prepared) | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 	return apiStatuses | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-23 14:18:24 +00:00
										 |  |  | // loadStatuses loads statuses using provided callback | 
					
						
							|  |  |  | // for the statuses in meta slice that aren't loaded. | 
					
						
							|  |  |  | // the amount very much depends on whether meta objects | 
					
						
							|  |  |  | // are yet-to-be-cached (i.e. newly loaded, with status), | 
					
						
							|  |  |  | // or are from the timeline cache (unloaded status). | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | func loadStatuses( | 
					
						
							|  |  |  | 	metas []*StatusMeta, | 
					
						
							|  |  |  | 	loadIDs func([]string) ([]*gtsmodel.Status, error), | 
					
						
							|  |  |  | ) error { | 
					
						
							| 
									
										
										
										
											2025-03-23 14:18:24 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-20 13:15:25 +00:00
										 |  |  | 	// Determine which of our passed status | 
					
						
							|  |  |  | 	// meta objects still need statuses loading. | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	toLoadIDs := make([]string, len(metas)) | 
					
						
							|  |  |  | 	loadedMap := make(map[string]*StatusMeta, len(metas)) | 
					
						
							|  |  |  | 	for i, meta := range metas { | 
					
						
							|  |  |  | 		if meta.loaded == nil { | 
					
						
							|  |  |  | 			toLoadIDs[i] = meta.ID | 
					
						
							|  |  |  | 			loadedMap[meta.ID] = meta | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Load statuses with given IDs. | 
					
						
							|  |  |  | 	loaded, err := loadIDs(toLoadIDs) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return gtserror.Newf("error loading statuses: %w", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Update returned StatusMeta objects | 
					
						
							|  |  |  | 	// with newly loaded statuses by IDs. | 
					
						
							|  |  |  | 	for i := range loaded { | 
					
						
							|  |  |  | 		status := loaded[i] | 
					
						
							|  |  |  | 		meta := loadedMap[status.ID] | 
					
						
							|  |  |  | 		meta.loaded = status | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | // toStatusMeta converts a slice of database model statuses | 
					
						
							|  |  |  | // into our cache wrapper type, a slice of []StatusMeta{}. | 
					
						
							| 
									
										
										
										
											2025-04-08 14:16:08 +01:00
										 |  |  | func toStatusMeta(in []*StatusMeta, statuses []*gtsmodel.Status) []*StatusMeta { | 
					
						
							|  |  |  | 	return xslices.Gather(in, statuses, func(s *gtsmodel.Status) *StatusMeta { | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 		return &StatusMeta{ | 
					
						
							|  |  |  | 			ID:               s.ID, | 
					
						
							|  |  |  | 			AccountID:        s.AccountID, | 
					
						
							|  |  |  | 			BoostOfID:        s.BoostOfID, | 
					
						
							|  |  |  | 			BoostOfAccountID: s.BoostOfAccountID, | 
					
						
							|  |  |  | 			loaded:           s, | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 			prepared:         nil, | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	}) | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-02 17:25:33 +01:00
										 |  |  | // doStatusFilter performs given filter function on provided statuses, | 
					
						
							| 
									
										
										
										
											2025-04-08 22:04:43 +01:00
										 |  |  | func doStatusFilter(statuses []*gtsmodel.Status, filter func(*gtsmodel.Status) bool) []*gtsmodel.Status { | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Check for provided | 
					
						
							|  |  |  | 	// filter function. | 
					
						
							|  |  |  | 	if filter == nil { | 
					
						
							| 
									
										
										
										
											2025-04-08 22:04:43 +01:00
										 |  |  | 		return statuses | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-08 22:04:43 +01:00
										 |  |  | 	// Filter the provided input statuses. | 
					
						
							|  |  |  | 	return slices.DeleteFunc(statuses, filter) | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } |