refactor: isolate playback and upload use cases
ci / module boundaries / boundaries (push) Successful in 6s
ci / api / api (push) Successful in 47s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m2s
ci / db / db (push) Successful in 1m4s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m30s
ci / ts / ts (packages + web) (push) Successful in 2m54s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m14s
ci / module boundaries / boundaries (push) Successful in 6s
ci / api / api (push) Successful in 47s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m2s
ci / db / db (push) Successful in 1m4s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m30s
ci / ts / ts (packages + web) (push) Successful in 2m54s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m14s
This commit is contained in:
1 parent
c84f90e2cc
commit
e158558da3
56 files changed
+3337
-2196
No files matched your search
@@ -0,0 +1,88 @@
|
||||
package playback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"pipu/api/internal/api/gen"
|
||||
"pipu/api/internal/auth"
|
||||
)
|
||||
|
||||
// EventService owns event acceptance and its transaction; HTTP only authenticates and maps DTOs.
|
||||
type EventService struct {
|
||||
store *Store
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func (s EventService) Accept(ctx context.Context, p auth.Principal, events []gen.PlayEvent) (gen.PostEvents200JSONResponse, error) {
|
||||
now := s.now()
|
||||
ids := make([]uuid.UUID, 0, len(events))
|
||||
for _, e := range events {
|
||||
ids = append(ids, e.TrackId)
|
||||
}
|
||||
durations, err := s.store.TrackDurations(ctx, ids)
|
||||
if err != nil {
|
||||
return gen.PostEvents200JSONResponse{}, err
|
||||
}
|
||||
clientType := s.store.SessionClientType(ctx, p.SessionID)
|
||||
|
||||
out := gen.PostEvents200JSONResponse{}
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return gen.PostEvents200JSONResponse{}, err
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
for _, e := range events {
|
||||
dur, known := durations[e.TrackId]
|
||||
at := e.OccurredAt.UTC()
|
||||
if !known || at.Before(now.Add(-maxEventAge)) {
|
||||
out.Rejected++
|
||||
continue
|
||||
}
|
||||
if at.After(now.Add(maxEventClockSkew)) {
|
||||
at = now // часы клиента спешат
|
||||
}
|
||||
ev := Event{
|
||||
ID: e.Id, Type: string(e.Type), TrackID: e.TrackId, OccurredAt: at, PositionMs: e.PositionMs,
|
||||
PlayedMs: e.PlayedMs, ContextID: e.ContextId, WaveSessionID: e.WaveSessionId,
|
||||
Offline: e.Offline != nil && *e.Offline,
|
||||
}
|
||||
if e.ContextType != nil {
|
||||
ct := string(*e.ContextType)
|
||||
ev.ContextType = &ct
|
||||
}
|
||||
inserted, err := s.store.InsertEvent(ctx, tx, p.UserID, clientType, ev)
|
||||
if err != nil {
|
||||
return gen.PostEvents200JSONResponse{}, err
|
||||
}
|
||||
if !inserted {
|
||||
out.Duplicates++
|
||||
continue
|
||||
}
|
||||
out.Accepted++
|
||||
played := 0
|
||||
if e.PlayedMs != nil {
|
||||
played = *e.PlayedMs
|
||||
}
|
||||
o := Judge(ev.Type, played, dur)
|
||||
if o != (Outcome{}) {
|
||||
if err := s.store.ApplyOutcome(ctx, tx, p.UserID, ev, o); err != nil {
|
||||
return gen.PostEvents200JSONResponse{}, err
|
||||
}
|
||||
}
|
||||
if outcome := impressionOutcome(ev.Type, o); outcome != "" && ev.ContextType != nil && *ev.ContextType == "wave" &&
|
||||
ev.ContextID != nil {
|
||||
if sid, err := uuid.Parse(*ev.ContextID); err == nil {
|
||||
if err := s.store.MarkImpression(ctx, tx, sid, ev.TrackID, outcome); err != nil {
|
||||
return gen.PostEvents200JSONResponse{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return gen.PostEvents200JSONResponse{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in new issue
Block a user