| 
									
										
										
										
											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" | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	"maps" | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	"slices" | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | 	"sync/atomic" | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	"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-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-02-03 17:00:33 +00:00
										 |  |  | 	Local            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 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Loaded is a temporary field that may be | 
					
						
							|  |  |  | 	// 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-01 13:51:17 +01:00
										 |  |  | // isNotLoaded is a small utility func that can fill | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | // the slices.DeleteFunc() signature requirements. | 
					
						
							| 
									
										
										
										
											2025-04-01 13:51:17 +01:00
										 |  |  | func (m *StatusMeta) isNotLoaded() bool { | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	return m.loaded == nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | // StatusTimelines ... | 
					
						
							|  |  |  | type StatusTimelines struct { | 
					
						
							|  |  |  | 	ptr atomic.Pointer[map[string]*StatusTimeline] // ronly except by CAS | 
					
						
							|  |  |  | 	cap int | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Init ... | 
					
						
							|  |  |  | func (t *StatusTimelines) Init(cap int) { t.cap = cap } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // MustGet ... | 
					
						
							|  |  |  | func (t *StatusTimelines) MustGet(key string) *StatusTimeline { | 
					
						
							|  |  |  | 	var tt *StatusTimeline | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for { | 
					
						
							|  |  |  | 		// Load current ptr. | 
					
						
							|  |  |  | 		cur := t.ptr.Load() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Get timeline map to work on. | 
					
						
							|  |  |  | 		var m map[string]*StatusTimeline | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if cur != nil { | 
					
						
							|  |  |  | 			// Look for existing | 
					
						
							|  |  |  | 			// timeline in cache. | 
					
						
							|  |  |  | 			tt = (*cur)[key] | 
					
						
							|  |  |  | 			if tt != nil { | 
					
						
							|  |  |  | 				return tt | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Get clone of current | 
					
						
							|  |  |  | 			// before modifications. | 
					
						
							|  |  |  | 			m = maps.Clone(*cur) | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			// Allocate new timeline map for below. | 
					
						
							|  |  |  | 			m = make(map[string]*StatusTimeline) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if tt == nil { | 
					
						
							|  |  |  | 			// Allocate new timeline. | 
					
						
							|  |  |  | 			tt = new(StatusTimeline) | 
					
						
							|  |  |  | 			tt.Init(t.cap) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Store timeline | 
					
						
							|  |  |  | 		// in new map. | 
					
						
							|  |  |  | 		m[key] = tt | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Attempt to update the map ptr. | 
					
						
							|  |  |  | 		if !t.ptr.CompareAndSwap(cur, &m) { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// We failed the | 
					
						
							|  |  |  | 			// CAS, reloop. | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Successfully inserted | 
					
						
							|  |  |  | 		// new timeline model. | 
					
						
							|  |  |  | 		return tt | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Delete ... | 
					
						
							|  |  |  | func (t *StatusTimelines) Delete(key string) { | 
					
						
							|  |  |  | 	for { | 
					
						
							|  |  |  | 		// Load current ptr. | 
					
						
							|  |  |  | 		cur := t.ptr.Load() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Check for empty map / not in map. | 
					
						
							|  |  |  | 		if cur == nil || (*cur)[key] == nil { | 
					
						
							|  |  |  | 			return | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Get clone of current | 
					
						
							|  |  |  | 		// before modifications. | 
					
						
							|  |  |  | 		m := maps.Clone(*cur) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Delete ID. | 
					
						
							|  |  |  | 		delete(m, key) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Attempt to update the map ptr. | 
					
						
							|  |  |  | 		if !t.ptr.CompareAndSwap(cur, &m) { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// We failed the | 
					
						
							|  |  |  | 			// CAS, reloop. | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Successfully | 
					
						
							|  |  |  | 		// deleted ID. | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // RemoveByStatusIDs ... | 
					
						
							|  |  |  | func (t *StatusTimelines) RemoveByStatusIDs(statusIDs ...string) { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		for _, tt := range *p { | 
					
						
							|  |  |  | 			tt.RemoveByStatusIDs(statusIDs...) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // RemoveByAccountIDs ... | 
					
						
							|  |  |  | func (t *StatusTimelines) RemoveByAccountIDs(accountIDs ...string) { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		for _, tt := range *p { | 
					
						
							|  |  |  | 			tt.RemoveByAccountIDs(accountIDs...) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // UnprepareByStatusIDs ... | 
					
						
							|  |  |  | func (t *StatusTimelines) UnprepareByStatusIDs(statusIDs ...string) { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		for _, tt := range *p { | 
					
						
							|  |  |  | 			tt.UnprepareByStatusIDs(statusIDs...) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // UnprepareByAccountIDs ... | 
					
						
							|  |  |  | func (t *StatusTimelines) UnprepareByAccountIDs(accountIDs ...string) { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		for _, tt := range *p { | 
					
						
							|  |  |  | 			tt.UnprepareByAccountIDs(accountIDs...) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-23 14:18:24 +00:00
										 |  |  | // Unprepare ... | 
					
						
							|  |  |  | func (t *StatusTimelines) Unprepare(key string) { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		if tt := (*p)[key]; tt != nil { | 
					
						
							|  |  |  | 			tt.UnprepareAll() | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-20 13:15:25 +00:00
										 |  |  | // UnprepareAll ... | 
					
						
							|  |  |  | func (t *StatusTimelines) UnprepareAll() { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		for _, tt := range *p { | 
					
						
							|  |  |  | 			tt.UnprepareAll() | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | // Trim ... | 
					
						
							|  |  |  | func (t *StatusTimelines) Trim(threshold float64) { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		for _, tt := range *p { | 
					
						
							|  |  |  | 			tt.Trim(threshold) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Clear ... | 
					
						
							|  |  |  | func (t *StatusTimelines) Clear(key string) { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		if tt := (*p)[key]; tt != nil { | 
					
						
							|  |  |  | 			tt.Clear() | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // ClearAll ... | 
					
						
							|  |  |  | func (t *StatusTimelines) ClearAll() { | 
					
						
							|  |  |  | 	if p := t.ptr.Load(); p != nil { | 
					
						
							|  |  |  | 		for _, tt := range *p { | 
					
						
							|  |  |  | 			tt.Clear() | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | // StatusTimeline ... | 
					
						
							|  |  |  | 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] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// 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-03-20 14:01:26 +00:00
										 |  |  | 	// last stores the last fetched direction | 
					
						
							|  |  |  | 	// of the timeline, which in turn determines | 
					
						
							|  |  |  | 	// where we will next trim from in keeping the | 
					
						
							|  |  |  | 	// timeline underneath configured 'max'. | 
					
						
							|  |  |  | 	// | 
					
						
							|  |  |  | 	// TODO: this could be more intelligent with | 
					
						
							|  |  |  | 	// a sliding average. a problem for future kim! | 
					
						
							| 
									
										
										
										
											2025-03-12 22:45:17 +00:00
										 |  |  | 	last atomic.Pointer[structr.Direction] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 16:48:20 +01:00
										 |  |  | 	// defines the 'maximum' count of | 
					
						
							|  |  |  | 	// entries in the timeline that we | 
					
						
							| 
									
										
										
										
											2025-04-01 14:32:36 +01:00
										 |  |  | 	// apply our Trim() call threshold | 
					
						
							|  |  |  | 	// to. the timeline itself does not | 
					
						
							|  |  |  | 	// limit items due to complexities | 
					
						
							|  |  |  | 	// it would introduce, so we apply | 
					
						
							|  |  |  | 	// a 'cut-off' at regular intervals. | 
					
						
							| 
									
										
										
										
											2025-03-12 22:45:17 +00:00
										 |  |  | 	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-03-31 16:48:20 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			// By setting multiple=false for BoostOfID, this will prevent | 
					
						
							|  |  |  | 			// timeline entries with matching BoostOfID will not be inserted | 
					
						
							|  |  |  | 			// after the first, which allows us to prevent repeated boosts | 
					
						
							|  |  |  | 			// of the same status from showing up within 'cap' entries. | 
					
						
							|  |  |  | 			{Fields: "BoostOfID", Multiple: false}, | 
					
						
							| 
									
										
										
										
											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-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
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Set max. | 
					
						
							|  |  |  | 	t.max = cap | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-01 14:10:53 +01:00
										 |  |  | // Load will load timeline statuses according to given | 
					
						
							|  |  |  | // page, using provided callbacks to load extra data when | 
					
						
							|  |  |  | // necessary, and perform fine-grained filtering loaded | 
					
						
							|  |  |  | // database models before eventual return to the user. The | 
					
						
							|  |  |  | // returned strings are the lo, hi ID paging values, used | 
					
						
							|  |  |  | // for generation of next, prev page links in the response. | 
					
						
							| 
									
										
										
										
											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
										 |  |  | 
 | 
					
						
							|  |  |  | 	// preFilter 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-02-13 12:34:45 +00:00
										 |  |  | 	preFilter func(each *gtsmodel.Status) (delete bool, err error), | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-01 14:10:53 +01:00
										 |  |  | 	// postFilter can be used to perform filtering of returned | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	// statuses AFTER insert into cache. i.e. this will not effect | 
					
						
							|  |  |  | 	// what actually gets stored in the timeline cache. | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	postFilter func(each *gtsmodel.Status) (delete bool, err error), | 
					
						
							| 
									
										
										
										
											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, | 
					
						
							|  |  |  | ) { | 
					
						
							|  |  |  | 	switch { | 
					
						
							|  |  |  | 	case page == nil: | 
					
						
							|  |  |  | 		panic("nil page") | 
					
						
							|  |  |  | 	case loadPage == nil: | 
					
						
							|  |  |  | 		panic("nil load page func") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-01 14:32:36 +01:00
										 |  |  | 	// TODO: there's quite a few opportunities for | 
					
						
							|  |  |  | 	// optimization here, with a lot of frequently | 
					
						
							|  |  |  | 	// used slices of the same types. depending on | 
					
						
							|  |  |  | 	// profiles it may be advantageous to pool some. | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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-03-12 22:45:17 +00:00
										 |  |  | 	// First we attempt to load status | 
					
						
							|  |  |  | 	// metadata entries from the timeline | 
					
						
							|  |  |  | 	// cache, up to given limit. | 
					
						
							|  |  |  | 	metas := t.cache.Select( | 
					
						
							| 
									
										
										
										
											2025-03-25 12:12:09 +00:00
										 |  |  | 		util.PtrIf(lo), | 
					
						
							|  |  |  | 		util.PtrIf(hi), | 
					
						
							| 
									
										
										
										
											2025-03-31 17:40:45 +01:00
										 |  |  | 		util.PtrIf(limit), | 
					
						
							| 
									
										
										
										
											2025-03-12 22:45:17 +00:00
										 |  |  | 		dir, | 
					
						
							|  |  |  | 	) | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	if len(metas) > 0 { | 
					
						
							| 
									
										
										
										
											2025-03-25 12:12:09 +00:00
										 |  |  | 		// We ALWAYS return and work on | 
					
						
							|  |  |  | 		// statuses in DESC order, but the | 
					
						
							|  |  |  | 		// timeline cache returns statuses | 
					
						
							|  |  |  | 		// in the *requested* order. | 
					
						
							|  |  |  | 		if dir == structr.Asc { | 
					
						
							|  |  |  | 			slices.Reverse(metas) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 		// 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-02-13 12:34:45 +00:00
										 |  |  | 		// Update paging values | 
					
						
							|  |  |  | 		// based on returned data. | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 		nextPageParams(nextPg, | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 			metas[len(metas)-1].ID, | 
					
						
							|  |  |  | 			metas[0].ID, | 
					
						
							| 
									
										
										
										
											2025-03-31 17:40:45 +01:00
										 |  |  | 			order, | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 		) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 		// Before any further loading, | 
					
						
							| 
									
										
										
										
											2025-03-31 15:39:15 +01:00
										 |  |  | 		// store current lo, hi values | 
					
						
							|  |  |  | 		// as possible lo, hi returns. | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 		lo = metas[len(metas)-1].ID | 
					
						
							|  |  |  | 		hi = metas[0].ID | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Drop all entries we failed to load statuses for. | 
					
						
							| 
									
										
										
										
											2025-04-01 13:51:17 +01:00
										 |  |  | 		metas = slices.DeleteFunc(metas, (*StatusMeta).isNotLoaded) | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 		// Perform post-filtering on cached status entries. | 
					
						
							|  |  |  | 		metas, err = doStatusPostFilter(metas, postFilter) | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, "", "", gtserror.Newf("error post-filtering statuses: %w", err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 	// Track all newly loaded status entries | 
					
						
							|  |  |  | 	// AFTER 'preFilter', but before 'postFilter', | 
					
						
							|  |  |  | 	// to later insert into timeline cache. | 
					
						
							|  |  |  | 	var justLoaded []*StatusMeta | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	// Check whether loaded enough from cache. | 
					
						
							| 
									
										
										
										
											2025-03-31 17:40:45 +01:00
										 |  |  | 	if need := limit - len(metas); need > 0 { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Perform a maximum of 5 | 
					
						
							|  |  |  | 		// load attempts fetching | 
					
						
							|  |  |  | 		// statuses to reach limit. | 
					
						
							|  |  |  | 		for i := 0; i < 5; i++ { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Load next timeline statuses. | 
					
						
							|  |  |  | 			statuses, err := loadPage(nextPg) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 				return nil, "", "", gtserror.Newf("error loading timeline: %w", err) | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// No more statuses from | 
					
						
							|  |  |  | 			// load function = at end. | 
					
						
							|  |  |  | 			if len(statuses) == 0 { | 
					
						
							|  |  |  | 				break | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 			// Update paging values | 
					
						
							|  |  |  | 			// based on returned data. | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 			nextPageParams(nextPg, | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 				statuses[len(statuses)-1].ID, | 
					
						
							|  |  |  | 				statuses[0].ID, | 
					
						
							| 
									
										
										
										
											2025-03-31 17:40:45 +01:00
										 |  |  | 				order, | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 			) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Perform any pre-filtering on newly loaded statuses. | 
					
						
							|  |  |  | 			statuses, err = doStatusPreFilter(statuses, preFilter) | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 			if err != nil { | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 				return nil, "", "", gtserror.Newf("error pre-filtering statuses: %w", err) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// After filtering no more | 
					
						
							|  |  |  | 			// statuses remain, retry. | 
					
						
							|  |  |  | 			if len(statuses) == 0 { | 
					
						
							|  |  |  | 				continue | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-01 14:04:32 +01:00
										 |  |  | 			// On each iteration, since statuses | 
					
						
							|  |  |  | 			// returned will always be in DESC order, | 
					
						
							|  |  |  | 			// iteratively update the lo paging value | 
					
						
							|  |  |  | 			// that we return for next / prev pages. | 
					
						
							| 
									
										
										
										
											2025-04-01 13:51:17 +01:00
										 |  |  | 			lo = statuses[len(statuses)-1].ID | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 			// Convert to our cache type, | 
					
						
							|  |  |  | 			// these will get inserted into | 
					
						
							|  |  |  | 			// the cache in prepare() below. | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 			uncached := toStatusMeta(statuses) | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 			// Before any filtering append to newly loaded. | 
					
						
							|  |  |  | 			justLoaded = append(justLoaded, uncached...) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Perform any post-filtering on loaded timeline entries. | 
					
						
							|  |  |  | 			filtered, err := doStatusPostFilter(uncached, postFilter) | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				return nil, "", "", gtserror.Newf("error post-filtering statuses: %w", err) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 			// Append newly filtered meta entries. | 
					
						
							|  |  |  | 			metas = append(metas, filtered...) | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			// Check if we reached | 
					
						
							|  |  |  | 			// requested page limit. | 
					
						
							| 
									
										
										
										
											2025-03-31 17:40:45 +01:00
										 |  |  | 			if len(metas) >= limit { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 				break | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 15:39:15 +01:00
										 |  |  | 	// Prepare frontend API models. | 
					
						
							| 
									
										
										
										
											2025-03-25 12:12:09 +00:00
										 |  |  | 	var apiStatuses []*apimodel.Status | 
					
						
							|  |  |  | 	if len(metas) > 0 { | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 		switch { | 
					
						
							| 
									
										
										
										
											2025-03-31 17:40:45 +01:00
										 |  |  | 		case len(metas) <= limit: | 
					
						
							| 
									
										
										
										
											2025-03-31 15:39:15 +01:00
										 |  |  | 			// We have under | 
					
						
							|  |  |  | 			// expected limit. | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 17:40:45 +01:00
										 |  |  | 		case order.Ascending(): | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 			// Ascending order was requested | 
					
						
							|  |  |  | 			// and we have more than limit, so | 
					
						
							|  |  |  | 			// trim extra metadata from end. | 
					
						
							| 
									
										
										
										
											2025-03-31 17:40:45 +01:00
										 |  |  | 			metas = metas[:limit] | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 15:39:15 +01:00
										 |  |  | 		default: /* i.e. descending */ | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 			// Descending order was requested | 
					
						
							|  |  |  | 			// and we have more than limit, so | 
					
						
							|  |  |  | 			// trim extra metadata from start. | 
					
						
							| 
									
										
										
										
											2025-03-31 17:40:45 +01:00
										 |  |  | 			metas = metas[len(metas)-limit:] | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-03-25 12:12:09 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Using meta and funcs, prepare frontend API models. | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 		apiStatuses = prepareStatuses(ctx, metas, prepareAPI) | 
					
						
							| 
									
										
										
										
											2025-04-01 13:51:17 +01:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-01 13:51:17 +01:00
										 |  |  | 	if len(justLoaded) > 0 { | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 		if hi == "" { | 
					
						
							| 
									
										
										
										
											2025-04-01 14:04:32 +01:00
										 |  |  | 			// Check whether a hi value was set | 
					
						
							|  |  |  | 			// from an initial load of cached entries, | 
					
						
							|  |  |  | 			// if not we set the returned hi paging | 
					
						
							|  |  |  | 			// value from first in loaded statuses. | 
					
						
							| 
									
										
										
										
											2025-04-01 13:51:17 +01:00
										 |  |  | 			hi = justLoaded[0].ID | 
					
						
							| 
									
										
										
										
											2025-03-25 12:12:09 +00:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Even if we don't return them, insert | 
					
						
							|  |  |  | 		// the excess (post-filtered) into cache. | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 		t.cache.Insert(justLoaded...) | 
					
						
							| 
									
										
										
										
											2025-03-25 12:12:09 +00:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	return apiStatuses, lo, hi, nil | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-20 13:15:25 +00:00
										 |  |  | // InsertOne allows you to insert a single status into the timeline, with optional prepared API model. | 
					
						
							|  |  |  | func (t *StatusTimeline) InsertOne(status *gtsmodel.Status, prepared *apimodel.Status) { | 
					
						
							|  |  |  | 	t.cache.Insert(&StatusMeta{ | 
					
						
							|  |  |  | 		ID:               status.ID, | 
					
						
							|  |  |  | 		AccountID:        status.AccountID, | 
					
						
							|  |  |  | 		BoostOfID:        status.BoostOfID, | 
					
						
							|  |  |  | 		BoostOfAccountID: status.BoostOfAccountID, | 
					
						
							|  |  |  | 		Local:            *status.Local, | 
					
						
							|  |  |  | 		loaded:           status, | 
					
						
							|  |  |  | 		prepared:         prepared, | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Insert allows you to bulk insert many statuses into the timeline. | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | func (t *StatusTimeline) Insert(statuses ...*gtsmodel.Status) { | 
					
						
							|  |  |  | 	t.cache.Insert(toStatusMeta(statuses)...) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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. | 
					
						
							|  |  |  | 	for meta := range t.cache.RangeKeys(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. | 
					
						
							|  |  |  | 	for meta := range t.cache.RangeKeys(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. | 
					
						
							|  |  |  | 	for meta := range t.cache.RangeKeys(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. | 
					
						
							|  |  |  | 	for meta := range t.cache.RangeKeys(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() { | 
					
						
							|  |  |  | 	for value := range t.cache.RangeUnsafe(structr.Asc) { | 
					
						
							|  |  |  | 		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 | 
					
						
							|  |  |  | // timeline's preconfigured maximum capacity. This will trim | 
					
						
							|  |  |  | // from top / bottom depending on which was recently accessed. | 
					
						
							| 
									
										
										
										
											2025-02-03 17:00:33 +00:00
										 |  |  | func (t *StatusTimeline) Trim(threshold float64) { | 
					
						
							| 
									
										
										
										
											2025-03-12 22:45:17 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 	// Default trim dir. | 
					
						
							| 
									
										
										
										
											2025-03-12 22:45:17 +00:00
										 |  |  | 	dir := structr.Asc | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 	// Calculate maximum allowed no. | 
					
						
							|  |  |  | 	// items as a percentage of max. | 
					
						
							| 
									
										
										
										
											2025-03-12 22:45:17 +00:00
										 |  |  | 	max := threshold * float64(t.max) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 	// Try load the last fetched | 
					
						
							|  |  |  | 	// timeline ordering, getting | 
					
						
							|  |  |  | 	// the inverse value for trimming. | 
					
						
							| 
									
										
										
										
											2025-03-12 22:45:17 +00:00
										 |  |  | 	if p := t.last.Load(); p != nil { | 
					
						
							|  |  |  | 		dir = !(*p) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 	// Trim timeline to 'max'. | 
					
						
							| 
									
										
										
										
											2025-03-12 22:45:17 +00:00
										 |  |  | 	t.cache.Trim(int(max), dir) | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-12 22:45:17 +00:00
										 |  |  | // Clear will remove all cached entries from underlying timeline. | 
					
						
							|  |  |  | func (t *StatusTimeline) Clear() { t.cache.Trim(0, structr.Desc) } | 
					
						
							| 
									
										
										
										
											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-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. | 
					
						
							|  |  |  | 	apiStatuses := make([]*apimodel.Status, 0, len(meta)) | 
					
						
							|  |  |  | 	for _, meta := range meta { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		if meta.loaded == nil { | 
					
						
							|  |  |  | 			// We failed loading this | 
					
						
							|  |  |  | 			// status, skip preparing. | 
					
						
							|  |  |  | 			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-03-24 21:34:36 +00:00
										 |  |  | 		if meta.prepared != nil { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 			// TODO: we won't need nil check when mutes | 
					
						
							|  |  |  | 			// / filters are moved to appropriate funcs. | 
					
						
							| 
									
										
										
										
											2025-03-24 21:34:36 +00:00
										 |  |  | 			// | 
					
						
							|  |  |  | 			// Add the prepared API model to return slice. | 
					
						
							|  |  |  | 			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{}. | 
					
						
							|  |  |  | func toStatusMeta(statuses []*gtsmodel.Status) []*StatusMeta { | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	return xslices.Gather(nil, statuses, func(s *gtsmodel.Status) *StatusMeta { | 
					
						
							|  |  |  | 		return &StatusMeta{ | 
					
						
							|  |  |  | 			ID:               s.ID, | 
					
						
							|  |  |  | 			AccountID:        s.AccountID, | 
					
						
							|  |  |  | 			BoostOfID:        s.BoostOfID, | 
					
						
							|  |  |  | 			BoostOfAccountID: s.BoostOfAccountID, | 
					
						
							|  |  |  | 			Local:            *s.Local, | 
					
						
							|  |  |  | 			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-03-20 13:15:25 +00:00
										 |  |  | // doStatusPreFilter performs given filter function on provided statuses, | 
					
						
							|  |  |  | // returning early if an error is returned. returns filtered statuses. | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | func doStatusPreFilter(statuses []*gtsmodel.Status, filter func(*gtsmodel.Status) (bool, error)) ([]*gtsmodel.Status, error) { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Check for provided | 
					
						
							|  |  |  | 	// filter function. | 
					
						
							|  |  |  | 	if filter == nil { | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		return statuses, nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Iterate through input statuses. | 
					
						
							|  |  |  | 	for i := 0; i < len(statuses); { | 
					
						
							|  |  |  | 		status := statuses[i] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Pass through filter func. | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 		ok, err := filter(status) | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if ok { | 
					
						
							|  |  |  | 			// Delete this status from input slice. | 
					
						
							|  |  |  | 			statuses = slices.Delete(statuses, i, i+1) | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Iter. | 
					
						
							|  |  |  | 		i++ | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return statuses, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-20 13:15:25 +00:00
										 |  |  | // doStatusPostFilter performs given filter function on provided status meta, | 
					
						
							|  |  |  | // expecting that embedded status is already loaded, returning filtered status | 
					
						
							|  |  |  | // meta, as well as those *filtered out*. returns early if error is returned. | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | func doStatusPostFilter(metas []*StatusMeta, filter func(*gtsmodel.Status) (bool, error)) ([]*StatusMeta, error) { | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Check for provided | 
					
						
							|  |  |  | 	// filter function. | 
					
						
							|  |  |  | 	if filter == nil { | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 		return metas, nil | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 	// Iterate through input metas. | 
					
						
							|  |  |  | 	for i := 0; i < len(metas); { | 
					
						
							|  |  |  | 		meta := metas[i] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Pass through filter func. | 
					
						
							|  |  |  | 		ok, err := filter(meta.loaded) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 			return nil, err | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if ok { | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 			// Delete meta entry from input slice. | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 			metas = slices.Delete(metas, i, i+1) | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Iter. | 
					
						
							|  |  |  | 		i++ | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-02-13 12:34:45 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 21:43:01 +00:00
										 |  |  | 	return metas, nil | 
					
						
							| 
									
										
										
										
											2024-12-30 17:12:55 +00:00
										 |  |  | } |