package storage import ( "database/sql" ) // The visit clock behind the adventure section's "while you were away" panel. // // The panel answers "what happened to my adventurer since I last looked", which // needs a per-user stamp — and the obvious one-column version of that is broken // in a way that only shows up in a browser: show the news, move the stamp to now, // and the reader's first refresh renders an empty box over the same events. So // there are two stamps. See the adventure_visit schema comment. // advVisitSessionGap is how long a gap in page loads counts as having gone away. // Thirty minutes: long enough that a reader clicking through a dispatch and back // keeps the same panel, short enough that "since last time" means something after // a lunch break rather than only after a day. const advVisitSessionGap = 30 * 60 // AdvVisitWindow stamps this visit and reports the instant the panel should read // from — every dispatch after it is news to this user. // // firstVisit is true the first time a user is ever seen, and the caller must show // nothing for it. The row is created stamped to now, so their history is not // news: somebody signing in for the first time has not been "away", and greeting // them with every death their character ever suffered would be a worse // introduction than silence. func AdvVisitWindow(userSub string, now int64) (from int64, firstVisit bool, err error) { if userSub == "" { return 0, true, nil } var windowFrom, lastSeen int64 err = Get().QueryRow( `SELECT window_from, last_seen_at FROM adventure_visit WHERE user_sub = ?`, userSub).Scan(&windowFrom, &lastSeen) if err == sql.ErrNoRows { _, ierr := Get().Exec( `INSERT INTO adventure_visit (user_sub, window_from, last_seen_at) VALUES (?, ?, ?)`, userSub, now, now) return now, true, ierr } if err != nil { return 0, true, err } // A new visit starts when the heartbeat has gone quiet for longer than the // session gap. Only then does the window move — and it moves to where the // reader actually left off (lastSeen), never to now, or the events between // their last page load and this one would fall down the crack between the two. if now-lastSeen > advVisitSessionGap { windowFrom = lastSeen } _, err = Get().Exec( `UPDATE adventure_visit SET window_from = ?, last_seen_at = ? WHERE user_sub = ?`, windowFrom, now, userSub) return windowFrom, false, err } // EventsBySubjectSince is EventsBySubject narrowed to what is new. The limit is // applied to the *window*, not to the subject's whole history, so a player back // from a long absence gets the most recent N of what they missed rather than N // rows scanned from a history that might all predate the window. func EventsBySubjectSince(name string, sinceUnix int64, limit int) ([]AdvEvent, error) { if name == "" || limit <= 0 { return nil, nil } rows, err := Get().Query(` SELECT guid, event_type, tier, subject, opponent, boss, zone, region, level, tally, outcome, milestone, stakes, run_id, occurred_at FROM adventure_events WHERE (subject = ? OR opponent = ?) AND occurred_at > ? ORDER BY occurred_at DESC LIMIT ?`, name, name, sinceUnix, limit) if err != nil { return nil, err } defer rows.Close() var out []AdvEvent for rows.Next() { var e AdvEvent var tier, subject, opponent, boss, zone, region sql.NullString var outcome, milestone, stakes, runID sql.NullString if err := rows.Scan(&e.GUID, &e.EventType, &tier, &subject, &opponent, &boss, &zone, ®ion, &e.Level, &e.Tally, &outcome, &milestone, &stakes, &runID, &e.OccurredAt); err != nil { return nil, err } e.Tier, e.Subject, e.Opponent = tier.String, subject.String, opponent.String e.Boss, e.Zone, e.Region = boss.String, zone.String, region.String e.Outcome, e.Milestone, e.Stakes = outcome.String, milestone.String, stakes.String e.RunID = runID.String out = append(out, e) } return out, rows.Err() }