// Package playsessions implements the listening-session lifecycle from // spec ยง6: 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 }