Files
minstrel/internal/playsessions/service.go
T
bvandeusenandClaude Opus 5.5 08a2c06b85 docs: api comments say what the endpoints do, not how they came to be (#6277)
Rewrites the internal/api comment blocks to rule 32: task, milestone and
rule ids gone, the stories behind them restated as the constraint they
left, stale references corrected (the stream secret is wired from config,
the timezone handler already refreshes the scheduler, the Android app is
the native client, the design docs cited are not in the repo). Also
rewraps comment lines over 80 columns left by the earlier batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:23:58 -04:00

73 lines
2.3 KiB
Go

// Package playsessions implements the listening-session lifecycle: a session is
// a contiguous sequence of plays by a user with no gap longer than the
// configured timeout (default 30 minutes) between events.
package playsessions
import (
"context"
"errors"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)
// FindOrCreate returns the session id for the given user and event time.
// If the user's most recent session was last touched within `timeout`, that
// session is extended (last_event_at and track_count updated). Otherwise a
// new session is inserted.
//
// q is *dbq.Queries; pass either the pool-level Queries or one bound to a
// transaction so callers can compose this into a larger atomic operation.
func FindOrCreate(
ctx context.Context,
q *dbq.Queries,
userID pgtype.UUID,
eventTime time.Time,
clientID string,
timeout time.Duration,
) (pgtype.UUID, error) {
prev, err := q.GetMostRecentPlaySessionForUser(ctx, userID)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return pgtype.UUID{}, err
}
if err == nil {
// "Within window" includes the exact-boundary case: gap <= timeout
// extends.
gap := eventTime.Sub(prev.LastEventAt.Time)
if gap <= timeout {
if err := q.TouchPlaySessionLastEvent(ctx, dbq.TouchPlaySessionLastEventParams{
ID: prev.ID,
LastEventAt: pgtype.Timestamptz{Time: eventTime, Valid: true},
}); err != nil {
return pgtype.UUID{}, err
}
return prev.ID, nil
}
}
// Either no prior session, or the gap exceeded the timeout.
var clientIDPtr *string
if clientID != "" {
clientIDPtr = &clientID
}
row, err := q.InsertPlaySession(ctx, dbq.InsertPlaySessionParams{
UserID: userID,
StartedAt: pgtype.Timestamptz{Time: eventTime, Valid: true},
ClientID: clientIDPtr,
})
if err != nil {
return pgtype.UUID{}, err
}
// Bump track_count to 1 on creation so the contract "every FindOrCreate is
// the start of one play" stays true (insert leaves track_count default 0).
if err := q.TouchPlaySessionLastEvent(ctx, dbq.TouchPlaySessionLastEventParams{
ID: row.ID,
LastEventAt: pgtype.Timestamptz{Time: eventTime, Valid: true},
}); err != nil {
return pgtype.UUID{}, err
}
return row.ID, nil
}