data

package
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 4, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RealmPool

func RealmPool[P Pooler](r *Realm, key string, create func() P) P

RealmPool retrieves or creates a typed pool within a realm. The type parameter P must implement Pooler (satisfied by *Pool[T], *MutatingPool[T], and *KeyedPool[K, T]).

Each key maps to exactly one concrete type — callers must be consistent. The Hub's typed accessors enforce this.

Types

type AccountData

type AccountData[T any] struct {
	Account AccountInfo
	Data    T
	Err     error
}

AccountData holds the typed outcome of a fan-out query against one account. Replaces the type-erased AccountResult for new pool-based code.

func FanOut

func FanOut[T any](ctx context.Context, ms *MultiStore,
	fn func(acct AccountInfo, client *basecamp.AccountClient) (T, error),
) []AccountData[T]

FanOut runs fn against all discovered accounts concurrently, limiting parallelism to maxConcurrent. Results are returned in account order. Unlike MultiStore.FanOut, the result is fully typed — no type assertions.

type AccountInfo

type AccountInfo struct {
	ID   string
	Name string
}

AccountInfo represents a discovered Basecamp account.

type AccountResult

type AccountResult struct {
	Account AccountInfo
	Data    any
	Err     error
}

AccountResult holds the outcome of a single fan-out query against one account.

type ActivityEntryInfo

type ActivityEntryInfo struct {
	ID          int64
	Title       string
	Type        string // "Todo", "Message", "Document", etc.
	Creator     string
	Account     string
	AccountID   string
	Project     string
	ProjectID   int64
	UpdatedAt   string // formatted time
	UpdatedAtTS int64  // unix timestamp for sorting
}

ActivityEntryInfo represents a recording from any account for the activity feed.

type AssignmentInfo

type AssignmentInfo struct {
	ID        int64
	Content   string
	DueOn     string
	Completed bool
	Account   string
	AccountID string
	Project   string
	ProjectID int64
	Todolist  string
	Overdue   bool
}

AssignmentInfo represents a todo assigned to the current user.

type BoostEmbed

type BoostEmbed struct {
	BoostsSummary BoostSummary
}

BoostEmbed can be embedded in recording info types to add boost support.

func (BoostEmbed) GetBoosts

func (be BoostEmbed) GetBoosts() BoostSummary

GetBoosts returns the embedded boost summary.

func (*BoostEmbed) SetBoosts

func (be *BoostEmbed) SetBoosts(summary BoostSummary)

SetBoosts sets the boost summary on the embedded field.

type BoostInfo

type BoostInfo struct {
	ID        int64
	Content   string // emoji or short text
	Booster   string // name of person who boosted
	BoosterID int64
	CreatedAt string // formatted time
}

BoostInfo is a lightweight representation of a boost (emoji reaction).

type BoostPreview

type BoostPreview struct {
	Content   string // the boost content (emoji or text)
	BoosterID int64  // for avatar-based display
}

BoostPreview is a single boost preview for list display. Shows either emoji content or avatar indicator depending on context.

type BoostSummary

type BoostSummary struct {
	Count   int            // total boost count
	Preview []BoostPreview // compact preview for list display
}

BoostSummary holds boost metadata for display on list items. Includes both a count and a preview of who's boosting.

func (BoostSummary) GetBoosts

func (b BoostSummary) GetBoosts() BoostSummary

GetBoosts returns an empty boost summary - recording types should embed BoostEmbed and override this if they have boosts.

type CampfireLineInfo

type CampfireLineInfo struct {
	ID          int64
	Body        string // HTML content
	Creator     string
	CreatedAt   string    // formatted time
	CreatedAtTS time.Time // raw timestamp for grouping
	BoostEmbed
}

CampfireLineInfo is a lightweight representation of a campfire line.

type CampfireLinesResult

type CampfireLinesResult struct {
	Lines      []CampfireLineInfo
	TotalCount int
}

CampfireLinesResult holds the lines plus pagination metadata from a campfire fetch. This compound type is the Pool's data value so that views can access TotalCount for pagination without a side-channel.

type CardColumnInfo

type CardColumnInfo struct {
	ID         int64
	Title      string
	Color      string
	Type       string // "Kanban::Triage", "Kanban::Column", "Kanban::DoneColumn", "Kanban::NotNowColumn"
	CardsCount int    // from column metadata (available without fetching cards)
	Deferred   bool   // true when cards were not fetched (Done/NotNow columns)
	Cards      []CardInfo
}

CardColumnInfo represents a kanban column with its cards.

type CardCreateMutation

type CardCreateMutation struct {
	Title     string
	ColumnID  int64 // identity-based column target (not index)
	ProjectID int64
	Client    *basecamp.AccountClient
	// contains filtered or unexported fields
}

CardCreateMutation optimistically prepends a new card to a kanban column. Implements Mutation[[]CardColumnInfo] for use with MutatingPool.

The createdID field uses atomic.Int64 because ApplyRemotely runs in the mutation's tea.Cmd goroutine while a concurrent background fetch can trigger reconcile → IsReflectedIn under the pool lock in a different goroutine. Pointer receiver required so the atomic field is shared.

func (*CardCreateMutation) ApplyLocally

func (m *CardCreateMutation) ApplyLocally(columns []CardColumnInfo) []CardColumnInfo

ApplyLocally prepends a placeholder card to the target column (by ID). If the column is not found (e.g. reorder during flight), no-op.

func (*CardCreateMutation) ApplyRemotely

func (m *CardCreateMutation) ApplyRemotely(ctx context.Context) error

ApplyRemotely calls the SDK to create the card.

func (*CardCreateMutation) IsReflectedIn

func (m *CardCreateMutation) IsReflectedIn(columns []CardColumnInfo) bool

IsReflectedIn returns true when the created card appears in the target column. Returns false if ApplyRemotely hasn't completed yet (createdID == 0).

type CardInfo

type CardInfo struct {
	ID            int64
	Title         string
	Assignees     []string
	DueOn         string
	Position      int
	Completed     bool
	StepsTotal    int
	StepsDone     int
	CommentsCount int
	BoostEmbed    // embedded boost support
}

CardInfo represents a single card.

type CardMoveMutation

type CardMoveMutation struct {
	CardID         int64
	SourceColIdx   int   // index of the source column
	TargetColIdx   int   // index of the target column
	TargetColumnID int64 // API ID of target column
	Client         *basecamp.AccountClient
	ProjectID      int64
}

CardMoveMutation moves a card from one column to another. Implements Mutation[[]CardColumnInfo] for use with MutatingPool.

func (CardMoveMutation) ApplyLocally

func (m CardMoveMutation) ApplyLocally(columns []CardColumnInfo) []CardColumnInfo

ApplyLocally moves the card between columns in the local data.

func (CardMoveMutation) ApplyRemotely

func (m CardMoveMutation) ApplyRemotely(ctx context.Context) error

ApplyRemotely calls the SDK to move the card.

func (CardMoveMutation) IsReflectedIn

func (m CardMoveMutation) IsReflectedIn(columns []CardColumnInfo) bool

IsReflectedIn returns true when the card appears in the target column in the remote data. For deferred target columns (Done/NotNow) whose Cards slice is always empty, we check that the card is absent from the source column instead.

type CheckinAnswerInfo

type CheckinAnswerInfo struct {
	ID            int64
	Creator       string
	CreatedAt     time.Time
	Content       string // HTML
	GroupOn       string // YYYY-MM-DD or empty
	CommentsCount int
}

CheckinAnswerInfo is a lightweight representation of a check-in answer.

type CheckinQuestionInfo

type CheckinQuestionInfo struct {
	ID           int64
	Title        string
	Paused       bool
	AnswersCount int
	Frequency    string
}

CheckinQuestionInfo is a lightweight representation of a check-in question.

type DockToolInfo

type DockToolInfo struct {
	ID      int64
	Name    string // "todoset", "chat", "message_board", etc.
	Title   string
	Enabled bool
}

DockToolInfo represents an enabled tool on a project's dock.

type DocsFilesItemInfo

type DocsFilesItemInfo struct {
	ID           int64
	Title        string
	Type         string // "Folder", "Document", "Upload"
	CreatedAt    string
	Creator      string
	VaultsCount  int // sub-folders (Type=="Folder" only)
	DocsCount    int // documents (Type=="Folder" only)
	UploadsCount int // uploads (Type=="Folder" only)
}

DocsFilesItemInfo is a lightweight representation of a vault item.

type FetchFunc

type FetchFunc[T any] func(ctx context.Context) (T, error)

FetchFunc retrieves data for a pool.

type ForwardInfo

type ForwardInfo struct {
	ID      int64
	Subject string
	From    string
}

ForwardInfo is a lightweight representation of an email forward.

type HeyEntryInfo

type HeyEntryInfo struct {
	ID        int64
	Title     string
	Excerpt   string
	Creator   string
	Project   string
	CreatedAt string
	IsRead    bool
}

HeyEntryInfo is a lightweight representation of an inbox entry.

type Hub

type Hub struct {
	// contains filtered or unexported fields
}

Hub is the central data coordinator providing typed, realm-scoped pool access.

Hub manages three realm tiers:

  • Global: app lifetime (identity, account list)
  • Account: active account session (projects, people)
  • Project: active project context (schedule, campfire, messages, etc.)

Typed pool accessors return realm-scoped pools whose lifecycle is automatic: project pools are torn down on LeaveProject/EnsureProject(different), account pools on SwitchAccount, global pools on Shutdown.

func NewHub

func NewHub(multi *MultiStore, poller *Poller) *Hub

NewHub creates a Hub with a global realm and the given dependencies.

func (*Hub) Account

func (h *Hub) Account() *Realm

Account returns the active account realm, or nil.

func (*Hub) AccountContext

func (h *Hub) AccountContext() context.Context

AccountContext returns the account realm's context, or the global context as fallback. Views should pass this to pool Fetch calls for account-scoped data so that SwitchAccount cancels in-flight fetches.

func (*Hub) Assignments

func (h *Hub) Assignments() *Pool[[]AssignmentInfo]

Assignments returns a global-scope pool of cross-account todo assignments.

func (*Hub) Boosts

func (h *Hub) Boosts(projectID, recordingID int64) *Pool[BoostSummary]

Boosts returns a project-scoped pool of boosts for a recording. The pool stores BoostSummary (count + preview) for list item display.

func (*Hub) CampfireLines

func (h *Hub) CampfireLines(projectID, campfireID int64) *Pool[CampfireLinesResult]

CampfireLines returns a project-scoped pool of campfire lines with polling config. The pool stores CampfireLinesResult (lines + TotalCount) for pagination support. Pagination (fetchOlderLines) and writes (sendLine) remain view-owned.

func (*Hub) Cards

func (h *Hub) Cards(projectID, tableID int64) *MutatingPool[[]CardColumnInfo]

Cards returns a project-scoped MutatingPool of card columns with their cards. The MutatingPool supports optimistic card moves via CardMoveMutation. Done and Not Now columns are deferred: metadata only, no card fetching.

func (*Hub) CheckinAnswers

func (h *Hub) CheckinAnswers(projectID, questionID int64) *Pool[[]CheckinAnswerInfo]

CheckinAnswers returns a project-scoped pool of answers for a specific check-in question.

func (*Hub) Checkins

func (h *Hub) Checkins(projectID, questionnaireID int64) *Pool[[]CheckinQuestionInfo]

Checkins returns a project-scoped pool of check-in questions.

func (*Hub) ClearCardAssignees

func (h *Hub) ClearCardAssignees(ctx context.Context, accountID string, projectID, cardID int64) error

ClearCardAssignees clears all assignees on a card. Uses a raw Put to bypass the SDK's omitempty on AssigneeIDs which prevents sending empty slices.

func (*Hub) ClearCardDueOn

func (h *Hub) ClearCardDueOn(ctx context.Context, accountID string, projectID, cardID int64) error

ClearCardDueOn clears the due date on a card. Uses a raw Put to bypass the SDK's omitempty on DueOn which prevents sending empty strings.

func (*Hub) ClearTodoAssignees

func (h *Hub) ClearTodoAssignees(ctx context.Context, accountID string, projectID, todoID int64) error

ClearTodoAssignees clears all assignees on a todo. Uses a raw Put to bypass the SDK's omitempty on AssigneeIDs which prevents sending empty slices.

func (*Hub) ClearTodoDueOn

func (h *Hub) ClearTodoDueOn(ctx context.Context, accountID string, projectID, todoID int64) error

ClearTodoDueOn clears the due date on a todo. Uses a raw Put to bypass the SDK's omitempty on DueOn which prevents sending empty strings.

func (*Hub) CompleteTodo

func (h *Hub) CompleteTodo(ctx context.Context, accountID string, projectID, todoID int64) error

CompleteTodo marks a todo as completed. Uses explicit accountID for cross-account mutations from aggregate views (Assignments, Hey).

func (*Hub) CompletedTodos

func (h *Hub) CompletedTodos(projectID, todolistID int64) *Pool[[]TodoInfo]

CompletedTodos returns a project-scoped Pool of completed todos for a specific todolist. Unlike Todos(), this is a plain Pool (not MutatingPool) since un-completing uses invalidate+refetch rather than optimistic mutation.

func (*Hub) CreateBoost

func (h *Hub) CreateBoost(ctx context.Context, accountID string, projectID, recordingID int64, content string) (BoostInfo, error)

CreateBoost creates a new boost on a recording. accountID selects which account's client to use; empty means the Hub's current account. Returns the created BoostInfo or an error.

func (*Hub) CreateCheckinAnswer

func (h *Hub) CreateCheckinAnswer(ctx context.Context, accountID string, projectID, questionID int64, content string) error

CreateCheckinAnswer posts a new answer to a check-in question.

func (*Hub) CreateDocument

func (h *Hub) CreateDocument(ctx context.Context, accountID string, projectID, vaultID int64, title string) error

CreateDocument creates a new document in a vault.

func (*Hub) CreateScheduleEntry

func (h *Hub) CreateScheduleEntry(ctx context.Context, accountID string, projectID, scheduleID int64, req *basecamp.CreateScheduleEntryRequest) error

CreateScheduleEntry creates a new schedule entry.

func (*Hub) CreateTodolist

func (h *Hub) CreateTodolist(ctx context.Context, accountID string, projectID, todosetID int64, name string) error

CreateTodolist creates a new todolist in a todoset.

func (*Hub) CreateVault

func (h *Hub) CreateVault(ctx context.Context, accountID string, projectID, parentVaultID int64, title string) error

CreateVault creates a new sub-folder in a vault.

func (*Hub) DeleteBoost

func (h *Hub) DeleteBoost(ctx context.Context, projectID, boostID int64) error

DeleteBoost deletes a boost by ID.

func (*Hub) DocsFiles

func (h *Hub) DocsFiles(projectID, vaultID int64) *Pool[[]DocsFilesItemInfo]

DocsFiles returns a project-scoped pool of vault items (folders, documents, uploads).

func (*Hub) EnsureAccount

func (h *Hub) EnsureAccount(accountID string) *Realm

EnsureAccount returns the account realm, creating one if needed. If called with a different accountID than the current realm, the old realm is torn down (along with any project realm) and a fresh one created.

func (*Hub) EnsureProject

func (h *Hub) EnsureProject(projectID int64) *Realm

EnsureProject returns the project realm, creating one if needed. If called with a different projectID than the current realm, the old realm is torn down and a fresh one created.

func (*Hub) Forwards

func (h *Hub) Forwards(projectID, inboxID int64) *Pool[[]ForwardInfo]

Forwards returns a project-scoped pool of email forwards.

func (*Hub) Global

func (h *Hub) Global() *Realm

Global returns the app-lifetime realm.

func (*Hub) HeyActivity

func (h *Hub) HeyActivity() *Pool[[]ActivityEntryInfo]

HeyActivity returns a global-scope pool of cross-account activity entries. The pool fans out Recordings.List across all accounts, caches for 30s (fresh) / 5m (stale), and polls at 30s/2m intervals.

func (*Hub) LeaveProject

func (h *Hub) LeaveProject()

LeaveProject tears down the project realm.

func (*Hub) Messages

func (h *Hub) Messages(projectID, boardID int64) *Pool[[]MessageInfo]

Messages returns a project-scoped pool of message board posts.

func (*Hub) Metrics

func (h *Hub) Metrics() *PoolMetrics

Metrics returns the pool metrics collector.

func (*Hub) MultiStore

func (h *Hub) MultiStore() *MultiStore

MultiStore returns the cross-account SDK access layer.

func (*Hub) People

func (h *Hub) People() *Pool[[]PersonInfo]

People returns an account-scoped pool of people in the current account. Panics if no account realm is active — callers must EnsureAccount first.

func (*Hub) PinMessage

func (h *Hub) PinMessage(ctx context.Context, accountID string, projectID, messageID int64) error

PinMessage pins a message to the top of its board.

func (*Hub) PingRooms

func (h *Hub) PingRooms() *Pool[[]PingRoomInfo]

PingRooms returns a global-scope pool of 1:1 campfire threads.

func (*Hub) Poller

func (h *Hub) Poller() *Poller

Poller returns the shared polling coordinator.

func (*Hub) Project

func (h *Hub) Project() *Realm

Project returns the active project realm, or nil.

func (*Hub) ProjectContext

func (h *Hub) ProjectContext() context.Context

ProjectContext returns the project realm's context, or the account/global context as fallback. Views should pass this to pool Fetch calls for project-scoped data so that LeaveProject cancels in-flight fetches.

func (*Hub) ProjectTimeline

func (h *Hub) ProjectTimeline(projectID int64) *Pool[[]TimelineEventInfo]

ProjectTimeline returns a project-scoped pool of timeline events.

func (*Hub) Projects

func (h *Hub) Projects() *Pool[[]ProjectInfo]

Projects returns a global-scope pool of all projects across accounts. Each project carries account attribution for cross-account navigation. Used by Home (bookmarks), Projects view, and Dock.

func (*Hub) Pulse

func (h *Hub) Pulse() *Pool[[]ActivityEntryInfo]

Pulse returns a global-scope pool of cross-account recent activity. Like HeyActivity but includes more recording types and groups by account.

func (*Hub) ScheduleEntries

func (h *Hub) ScheduleEntries(projectID, scheduleID int64) *Pool[[]ScheduleEntryInfo]

ScheduleEntries returns a project-scoped pool of schedule entries.

func (*Hub) Shutdown

func (h *Hub) Shutdown()

Shutdown tears down all realms. Call on program exit.

func (*Hub) Subscribe

func (h *Hub) Subscribe(ctx context.Context, accountID string, projectID, recordingID int64) error

Subscribe subscribes the current user to a recording.

func (*Hub) SwitchAccount

func (h *Hub) SwitchAccount(accountID string)

SwitchAccount tears down the project and account realms, then creates a fresh account realm. Replaces the store.Clear() + router.Reset() sledgehammer with targeted realm teardown.

func (*Hub) Timeline

func (h *Hub) Timeline() *Pool[[]TimelineEventInfo]

Timeline returns a global-scope pool of cross-account timeline events. Uses the richer Timeline.Progress() API instead of Recordings.List.

func (*Hub) Todolists

func (h *Hub) Todolists(projectID, todosetID int64) *Pool[[]TodolistInfo]

Todolists returns a project-scoped pool of todolists.

func (*Hub) Todos

func (h *Hub) Todos(projectID, todolistID int64) *MutatingPool[[]TodoInfo]

Todos returns a project-scoped MutatingPool of todos for a specific todolist. The MutatingPool supports optimistic todo completion via TodoCompleteMutation.

func (*Hub) TrashComment

func (h *Hub) TrashComment(ctx context.Context, accountID string, projectID, commentID int64) error

TrashComment moves a comment to the trash.

func (*Hub) TrashRecording

func (h *Hub) TrashRecording(ctx context.Context, accountID string, projectID, recordingID int64) error

TrashRecording moves a recording to the trash.

func (*Hub) TrashTodolist

func (h *Hub) TrashTodolist(ctx context.Context, accountID string, projectID, todolistID int64) error

TrashTodolist moves a todolist to the trash.

func (*Hub) UncompleteTodo

func (h *Hub) UncompleteTodo(ctx context.Context, accountID string, projectID, todoID int64) error

UncompleteTodo reopens a completed todo.

func (*Hub) UnpinMessage

func (h *Hub) UnpinMessage(ctx context.Context, accountID string, projectID, messageID int64) error

UnpinMessage unpins a message from the board.

func (*Hub) Unsubscribe

func (h *Hub) Unsubscribe(ctx context.Context, accountID string, projectID, recordingID int64) error

Unsubscribe unsubscribes the current user from a recording.

func (*Hub) UpdateCard

func (h *Hub) UpdateCard(ctx context.Context, accountID string, projectID, cardID int64, req *basecamp.UpdateCardRequest) error

UpdateCard updates a card's fields.

func (*Hub) UpdateComment

func (h *Hub) UpdateComment(ctx context.Context, accountID string, projectID, commentID int64, content string) error

UpdateComment updates a comment's content.

func (*Hub) UpdateMessage

func (h *Hub) UpdateMessage(ctx context.Context, accountID string, projectID, messageID int64, req *basecamp.UpdateMessageRequest) error

UpdateMessage updates a message's fields.

func (*Hub) UpdateTodo

func (h *Hub) UpdateTodo(ctx context.Context, accountID string, projectID, todoID int64, req *basecamp.UpdateTodoRequest) error

UpdateTodo updates a todo's fields.

func (*Hub) UpdateTodolist

func (h *Hub) UpdateTodolist(ctx context.Context, accountID string, projectID, todolistID int64, name string) error

UpdateTodolist renames a todolist.

type KeyedPool

type KeyedPool[K comparable, T any] struct {
	// contains filtered or unexported fields
}

KeyedPool manages sub-collection pools keyed by a parent ID. For data keyed by a parent: todos by todolist, campfire lines by campfire, comments by recording.

func NewKeyedPool

func NewKeyedPool[K comparable, T any](factory func(key K) *Pool[T]) *KeyedPool[K, T]

NewKeyedPool creates a KeyedPool with the given factory for creating new pools on demand. All pools share the factory's config but have independent fetch state.

func (*KeyedPool[K, T]) Clear

func (kp *KeyedPool[K, T]) Clear()

Clear removes all sub-pools and their data.

func (*KeyedPool[K, T]) Get

func (kp *KeyedPool[K, T]) Get(key K) *Pool[T]

Get returns the Pool for the given key, creating one if it doesn't exist.

func (*KeyedPool[K, T]) Has

func (kp *KeyedPool[K, T]) Has(key K) bool

Has returns true if a pool exists for the given key.

func (*KeyedPool[K, T]) Invalidate

func (kp *KeyedPool[K, T]) Invalidate()

Invalidate marks all sub-pools as stale.

type MessageInfo

type MessageInfo struct {
	ID         int64
	Subject    string
	Creator    string
	CreatedAt  string
	Category   string
	Pinned     bool
	BoostEmbed // embedded boost support
}

MessageInfo represents a message board post.

type MetricsSummary

type MetricsSummary struct {
	ActivePools int
	P50Latency  time.Duration
	ErrorRate   float64
	Apdex       float64
}

MetricsSummary provides a point-in-time snapshot of pool health.

type MultiStore

type MultiStore struct {
	// contains filtered or unexported fields
}

MultiStore manages cross-account data access. It discovers all accessible accounts, caches per-account SDK clients, and provides FanOut for running the same query against all accounts concurrently with semaphore limiting.

func NewMultiStore

func NewMultiStore(sdk *basecamp.Client) *MultiStore

NewMultiStore creates a MultiStore backed by the given SDK client.

func (*MultiStore) Accounts

func (ms *MultiStore) Accounts() []AccountInfo

Accounts returns the discovered account list. Returns nil before DiscoverAccounts.

func (*MultiStore) ClientFor

func (ms *MultiStore) ClientFor(accountID string) *basecamp.AccountClient

ClientFor returns an AccountClient for the given account ID. Clients are cached and reused across calls. Returns nil if the SDK is not initialized.

func (*MultiStore) DiscoverAccounts

func (ms *MultiStore) DiscoverAccounts(ctx context.Context) ([]AccountInfo, error)

DiscoverAccounts fetches all accessible accounts and initializes the store. Safe to call multiple times; subsequent calls refresh the account list.

func (*MultiStore) FanOut

func (ms *MultiStore) FanOut(ctx context.Context, fn func(acct AccountInfo, client *basecamp.AccountClient) (any, error)) []AccountResult

FanOut runs fn against all discovered accounts concurrently, limiting parallelism to maxConcurrent. Results are returned in account order. Individual account failures are captured in AccountResult.Err — the overall operation only fails if no accounts are discovered yet.

func (*MultiStore) FanOutSingle

func (ms *MultiStore) FanOutSingle(ctx context.Context, accountID string, fn func(ctx context.Context, client *basecamp.AccountClient) (any, error)) (any, error)

FanOutSingle runs fn against a single account by ID. Returns ctx.Err() immediately if the context is already canceled.

func (*MultiStore) Identity

func (ms *MultiStore) Identity() *basecamp.Identity

Identity returns the logged-in user's identity, or nil before discovery.

func (*MultiStore) SetAccountsForTest

func (ms *MultiStore) SetAccountsForTest(accounts []AccountInfo)

SetAccountsForTest sets the account list directly, bypassing DiscoverAccounts. Only for use in tests.

type MutatingPool

type MutatingPool[T any] struct {
	*Pool[T]
	// contains filtered or unexported fields
}

MutatingPool extends Pool with optimistic mutation support. Go equivalent of iOS BaseConcurrentDataStore / Android BaseConcurrentRepository.

func NewMutatingPool

func NewMutatingPool[T any](key string, config PoolConfig, fetchFn FetchFunc[T]) *MutatingPool[T]

NewMutatingPool creates a MutatingPool with the given key, config, and fetch function.

func (*MutatingPool[T]) Apply

func (mp *MutatingPool[T]) Apply(ctx context.Context, mutation Mutation[T]) tea.Cmd

Apply executes an optimistic mutation:

  1. Applies locally to the snapshot (immediate, synchronous)
  2. Returns a Cmd that applies remotely, then re-fetches and reconciles

The caller should read pool.Get() after calling Apply to get the optimistic data for immediate rendering.

func (*MutatingPool[T]) Clear

func (mp *MutatingPool[T]) Clear()

Clear overrides Pool.Clear to also reset mutation state.

func (*MutatingPool[T]) Fetch

func (mp *MutatingPool[T]) Fetch(ctx context.Context) tea.Cmd

Fetch overrides Pool.Fetch to reconcile pending mutations after a successful fetch rather than overwriting them.

func (*MutatingPool[T]) FetchIfStale

func (mp *MutatingPool[T]) FetchIfStale(ctx context.Context) tea.Cmd

FetchIfStale overrides Pool.FetchIfStale to route through MutatingPool.Fetch.

type Mutation

type Mutation[T any] interface {
	// ApplyLocally modifies the current state optimistically.
	ApplyLocally(current T) T

	// ApplyRemotely performs the server-side operation.
	ApplyRemotely(ctx context.Context) error

	// IsReflectedIn returns true when the remote data already contains
	// this mutation's effect (for pending-mutation pruning on re-fetch).
	IsReflectedIn(remote T) bool
}

Mutation describes an optimistic mutation lifecycle. Go equivalent of iOS's Mutation protocol / Android's Mutation<T, A>.

type MutationErrorMsg

type MutationErrorMsg struct {
	Key string
	Err error
}

MutationErrorMsg is sent when a mutation's remote apply fails.

type NavigationEvent struct {
	Timestamp time.Time
	ViewTitle string
	PoolKey   string
	Quality   float64 // 1.0=Fresh, 0.5=Stale, 0.0=Empty
}

NavigationEvent records a view navigation with data quality.

type PersonInfo

type PersonInfo struct {
	ID         int64
	Name       string
	Email      string
	Title      string
	Admin      bool
	Owner      bool
	Client     bool
	PersonType string // "User", "Client", etc.
	Company    string
}

PersonInfo is a lightweight representation of a person for the view.

type PingRoomInfo

type PingRoomInfo struct {
	CampfireID  int64
	ProjectID   int64
	PersonName  string
	Account     string
	AccountID   string
	LastMessage string
	LastAt      string
	LastAtTS    int64 // unix timestamp for sorting
}

PingRoomInfo represents a 1:1 campfire thread.

type PollConfig

type PollConfig struct {
	Tag        string
	Base       time.Duration // interval when focused and active
	Background time.Duration // interval when blurred
	Max        time.Duration // maximum backoff
	// contains filtered or unexported fields
}

PollConfig configures a single polling channel.

type PollMsg

type PollMsg struct {
	Tag string
}

PollMsg is sent when a poll interval fires. Tag identifies which poller triggered.

type Poller

type Poller struct {
	// contains filtered or unexported fields
}

Poller manages adaptive polling for multiple data channels.

func NewPoller

func NewPoller() *Poller

NewPoller creates an empty poller.

func (*Poller) Add

func (p *Poller) Add(cfg PollConfig)

Add registers a polling channel.

func (*Poller) RecordHit

func (p *Poller) RecordHit(tag string)

RecordHit resets the interval for a channel (new data arrived).

func (*Poller) RecordMiss

func (p *Poller) RecordMiss(tag string)

RecordMiss increases the interval (no new data).

func (*Poller) Schedule

func (p *Poller) Schedule(tag string) tea.Cmd

Schedule returns a tea.Cmd for the next tick of the given channel.

func (*Poller) SetFocused

func (p *Poller) SetFocused(tag string, focused bool)

SetFocused adjusts base interval for focus/blur.

func (*Poller) Start

func (p *Poller) Start() tea.Cmd

Start returns tea.Cmds to begin all registered polls.

type Pool

type Pool[T any] struct {
	// contains filtered or unexported fields
}

Pool is a typed, self-refreshing data source. Go equivalent of iOS RemoteReadService / Android BaseApiRepository. One Pool per logical data set (projects, hey-activity, campfire-lines).

The Pool does not subscribe or push — it's a typed cache with fetch capabilities. TEA's polling mechanism (PollMsg -> view calls FetchIfStale) drives the refresh cycle.

func NewPool

func NewPool[T any](key string, config PoolConfig, fetchFn FetchFunc[T]) *Pool[T]

NewPool creates a Pool with the given key, config, and fetch function.

func (*Pool[T]) Clear

func (p *Pool[T]) Clear()

Clear resets the pool to its initial empty state.

func (*Pool[T]) Fetch

func (p *Pool[T]) Fetch(ctx context.Context) tea.Cmd

Fetch returns a Cmd that fetches fresh data and emits PoolUpdatedMsg. Concurrent fetches are deduped — returns nil if a fetch is in progress.

func (*Pool[T]) FetchIfStale

func (p *Pool[T]) FetchIfStale(ctx context.Context) tea.Cmd

FetchIfStale returns a Fetch Cmd if data is stale or empty, nil if fresh.

func (*Pool[T]) Get

func (p *Pool[T]) Get() Snapshot[T]

Get returns the current snapshot. Never blocks. Recalculates state based on TTL — a snapshot stored as Fresh may be returned as Stale if FreshTTL has elapsed, or expired (HasData=false) if StaleTTL has also elapsed.

func (*Pool[T]) Invalidate

func (p *Pool[T]) Invalidate()

Invalidate marks current data as stale. Next FetchIfStale will re-fetch.

func (*Pool[T]) Key

func (p *Pool[T]) Key() string

Key returns the pool's identifier.

func (*Pool[T]) PollInterval

func (p *Pool[T]) PollInterval() time.Duration

PollInterval returns the current recommended polling interval, accounting for focus state, push mode, and miss backoff.

func (*Pool[T]) RecordHit

func (p *Pool[T]) RecordHit()

RecordHit resets the miss counter (new data arrived).

func (*Pool[T]) RecordMiss

func (p *Pool[T]) RecordMiss()

RecordMiss increments the miss counter for adaptive backoff.

func (*Pool[T]) Set

func (p *Pool[T]) Set(data T)

Set writes data directly into the pool (for prefetch / dual-write patterns).

func (*Pool[T]) SetFocused

func (p *Pool[T]) SetFocused(focused bool)

SetFocused marks whether the view consuming this pool has focus.

func (*Pool[T]) SetMetrics

func (p *Pool[T]) SetMetrics(m *PoolMetrics)

SetMetrics sets the metrics collector for this pool.

func (*Pool[T]) SetPushMode

func (p *Pool[T]) SetPushMode(enabled bool)

SetPushMode enables/disables push mode (SSE connected). In push mode, poll intervals are extended significantly.

func (*Pool[T]) Status

func (p *Pool[T]) Status() PoolStatus

Status returns a live status snapshot for the metrics panel.

func (*Pool[T]) Version

func (p *Pool[T]) Version() uint64

Version returns the current data version.

type PoolConfig

type PoolConfig struct {
	FreshTTL time.Duration // how long data is "fresh" (0 = no expiry)
	StaleTTL time.Duration // how long stale data is served during revalidation
	PollBase time.Duration // base polling interval when focused (0 = no auto-poll)
	PollBg   time.Duration // background polling interval when blurred
	PollMax  time.Duration // max interval after consecutive misses
}

PoolConfig configures a Pool's timing behavior.

type PoolEvent

type PoolEvent struct {
	Timestamp time.Time
	PoolKey   string
	EventType PoolEventType
	Duration  time.Duration
	DataSize  int
}

PoolEvent records a single pool fetch event.

type PoolEventType

type PoolEventType int

PoolEventType classifies pool fetch events.

const (
	FetchStart PoolEventType = iota
	FetchComplete
	FetchError
)

type PoolMetrics

type PoolMetrics struct {
	// contains filtered or unexported fields
}

PoolMetrics collects pool fetch telemetry for status bar display.

func NewPoolMetrics

func NewPoolMetrics() *PoolMetrics

NewPoolMetrics creates an empty metrics collector.

func (*PoolMetrics) Apdex

func (m *PoolMetrics) Apdex() float64

Apdex returns the navigation quality score (0.0-1.0). Fresh = satisfied (1.0), Stale = tolerating (0.5), Empty = frustrated (0.0).

func (*PoolMetrics) PoolStatsList

func (m *PoolMetrics) PoolStatsList() []PoolStatus

PoolStatsList returns live status from all registered pools. Copies the reporter slice under lock, then invokes reporters without holding the metrics lock to avoid lock-order inversion with pool mutexes.

func (*PoolMetrics) Record

func (m *PoolMetrics) Record(e PoolEvent)

Record adds a pool event to the ring buffer and updates stats.

func (*PoolMetrics) RecordNavigation

func (m *PoolMetrics) RecordNavigation(e NavigationEvent)

RecordNavigation logs a view navigation with data quality.

func (*PoolMetrics) RegisterPool

func (m *PoolMetrics) RegisterPool(key string, reporter func() PoolStatus)

RegisterPool adds a live status reporter for a pool.

func (*PoolMetrics) Summary

func (m *PoolMetrics) Summary() MetricsSummary

Summary returns aggregate metrics for status bar display.

func (*PoolMetrics) UnregisterPool

func (m *PoolMetrics) UnregisterPool(key string)

UnregisterPool removes a pool's status reporter.

type PoolStats

type PoolStats struct {
	FetchCount  int
	ErrorCount  int
	TotalTimeMs int64
	LastFetch   time.Time
}

PoolStats holds aggregate statistics for a single pool.

type PoolStatus

type PoolStatus struct {
	Key          string
	State        SnapshotState
	FetchedAt    time.Time
	PollInterval time.Duration
	HitCount     int
	MissCount    int
	FetchCount   int
	ErrorCount   int
	AvgLatency   time.Duration
}

PoolStatus is a live status snapshot from a registered pool.

type PoolUpdatedMsg

type PoolUpdatedMsg struct {
	Key string
}

PoolUpdatedMsg is sent when a pool's snapshot changes. Views match on Key to identify which pool updated, then read typed data via the pool's Get() method.

type Pooler

type Pooler interface {
	Invalidate()
	Clear()
}

Pooler is the non-generic interface for pool lifecycle management. Realm uses this to manage pools of different types uniformly.

type ProjectInfo

type ProjectInfo struct {
	ID          int64
	Name        string
	Description string
	Purpose     string
	Bookmarked  bool
	AccountID   string
	AccountName string
	Dock        []DockToolInfo
}

ProjectInfo wraps a project with account attribution for multi-account pools. basecamp.Project doesn't carry which account it belongs to, so the Hub's Projects() FetchFunc annotates each project during fan-out.

type Realm

type Realm struct {
	// contains filtered or unexported fields
}

Realm manages a group of pools with a shared lifecycle. Pools belong to realms. Realm teardown cancels all owned pools' in-flight fetches and clears their data.

Three realms mirror the mobile architecture:

  • Global: app lifetime (identity, account list)
  • Account: active account session (projects, hey, assignments)
  • Project: active project context (todos, campfire, messages)

func NewRealm

func NewRealm(name string, parent context.Context) *Realm

NewRealm creates a realm with a cancellable context derived from parent.

func (*Realm) Context

func (r *Realm) Context() context.Context

Context returns the realm's context. Canceled on teardown. Pass this to pool fetch functions so they abort when the realm dies.

func (*Realm) Invalidate

func (r *Realm) Invalidate()

Invalidate marks all pools in this realm as stale.

func (*Realm) Name

func (r *Realm) Name() string

Name returns the realm's identifier.

func (*Realm) Pool

func (r *Realm) Pool(key string) Pooler

Pool returns a registered pool by key, or nil if not found.

func (*Realm) Register

func (r *Realm) Register(key string, p Pooler)

Register adds a pool to this realm for lifecycle management.

func (*Realm) Teardown

func (r *Realm) Teardown()

Teardown cancels the realm's context and clears all pools. After teardown, the realm should not be reused.

type RecordingWithBoosts

type RecordingWithBoosts interface {
	GetBoosts() BoostSummary
}

RecordingWithBoosts extends recording info types to include boost metadata.

type ScheduleEntryInfo

type ScheduleEntryInfo struct {
	ID           int64
	Summary      string
	StartsAt     string
	EndsAt       string
	AllDay       bool
	Participants []string
}

ScheduleEntryInfo is a lightweight representation of a schedule entry.

type SearchResultInfo

type SearchResultInfo struct {
	ID          int64
	Title       string
	Excerpt     string
	Type        string // "todo", "message", "document", etc.
	Project     string
	ProjectID   int64
	Account     string // account name (populated in multi-account mode)
	AccountID   string // account ID for navigation
	CreatedAt   string
	CreatedAtTS int64 // unix timestamp for sorting
}

SearchResultInfo represents a single search result.

type Snapshot

type Snapshot[T any] struct {
	Data      T
	State     SnapshotState
	Err       error
	FetchedAt time.Time
	HasData   bool // distinguishes zero-value T from "never fetched"
}

Snapshot holds typed data along with its fetch state. Every pool exposes Snapshot[T] — consumers never see any.

func (Snapshot[T]) Fresh

func (s Snapshot[T]) Fresh() bool

Fresh returns true if the snapshot has data in the Fresh state.

func (Snapshot[T]) Loading

func (s Snapshot[T]) Loading() bool

Loading returns true if a fetch is in progress.

func (Snapshot[T]) Usable

func (s Snapshot[T]) Usable() bool

Usable returns true if the snapshot has data, regardless of freshness.

type SnapshotState

type SnapshotState int

SnapshotState represents the freshness state of a data snapshot. Go equivalent of iOS DataUpdate<T> / Android Data<T>.

const (
	StateEmpty   SnapshotState = iota // no data yet
	StateFresh                        // data within TTL
	StateStale                        // data past TTL, usable for SWR
	StateLoading                      // fetch in progress (may have stale data)
	StateError                        // fetch failed (may have stale data)
)

func (SnapshotState) String

func (s SnapshotState) String() string

type TimelineEventInfo

type TimelineEventInfo struct {
	ID             int64
	RecordingID    int64  // parent recording ID for navigation/detail
	CreatedAt      string // formatted time
	CreatedAtTS    int64  // unix timestamp for sorting
	Kind           string // "todo_completed", "message_created", etc.
	Action         string // "completed", "created", "commented on"
	Target         string // "Todo", "Message", "Document"
	Title          string
	SummaryExcerpt string // first ~100 chars of body
	Creator        string
	Project        string
	ProjectID      int64
	Account        string
	AccountID      string
}

TimelineEventInfo is a lightweight representation of a timeline event.

type TodoCompleteMutation

type TodoCompleteMutation struct {
	TodoID    int64
	Completed bool // target state (true = complete, false = uncomplete)
	Client    *basecamp.AccountClient
	ProjectID int64
}

TodoCompleteMutation toggles a todo's completion state. Implements Mutation[[]TodoInfo] for use with MutatingPool.

func (TodoCompleteMutation) ApplyLocally

func (m TodoCompleteMutation) ApplyLocally(todos []TodoInfo) []TodoInfo

ApplyLocally toggles the todo's Completed field in the local data.

func (TodoCompleteMutation) ApplyRemotely

func (m TodoCompleteMutation) ApplyRemotely(ctx context.Context) error

ApplyRemotely calls the SDK to complete or uncomplete the todo.

func (TodoCompleteMutation) IsReflectedIn

func (m TodoCompleteMutation) IsReflectedIn(todos []TodoInfo) bool

IsReflectedIn returns true when the remote data shows the todo in the target completion state.

type TodoCreateMutation

type TodoCreateMutation struct {
	Content    string
	TodolistID int64
	ProjectID  int64
	Client     *basecamp.AccountClient
	// contains filtered or unexported fields
}

TodoCreateMutation optimistically prepends a new todo to a todolist. Implements Mutation[[]TodoInfo] for use with MutatingPool.

The createdID field uses atomic.Int64 because ApplyRemotely runs in the mutation's tea.Cmd goroutine while a concurrent background fetch can trigger reconcile → IsReflectedIn under the pool lock in a different goroutine. Pointer receiver required so the atomic field is shared.

func (*TodoCreateMutation) ApplyLocally

func (m *TodoCreateMutation) ApplyLocally(todos []TodoInfo) []TodoInfo

ApplyLocally prepends a placeholder todo with a temporary negative ID.

func (*TodoCreateMutation) ApplyRemotely

func (m *TodoCreateMutation) ApplyRemotely(ctx context.Context) error

ApplyRemotely calls the SDK to create the todo.

func (*TodoCreateMutation) IsReflectedIn

func (m *TodoCreateMutation) IsReflectedIn(todos []TodoInfo) bool

IsReflectedIn returns true when the created todo appears in the remote data. Returns false if ApplyRemotely hasn't completed yet (createdID == 0).

type TodoInfo

type TodoInfo struct {
	ID          int64
	Content     string
	Description string
	Completed   bool
	DueOn       string
	Assignees   []string // names
	Position    int
	BoostEmbed  // embedded boost support
}

TodoInfo is a lightweight representation of a todo for the view.

type TodolistInfo

type TodolistInfo struct {
	ID             int64
	Title          string
	CompletedRatio string
	TodosURL       string
}

TodolistInfo is a lightweight representation of a todolist for the view.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL