| 1 | package calendar |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "time" |
| 6 | ) |
| 7 | |
| 8 | // Deciding WHEN a reminder next fires, split from the code that writes |
| 9 | // the queue row so neither file passes the 300-line cap. |
| 10 | |
| 11 | // nextOccurrenceAfter finds the first occurrence whose reminder has not |
| 12 | // yet come due. |
| 13 | // |
| 14 | // It searches from now minus the offset, so a reminder set for an event |
| 15 | // starting in five minutes still fires — the due time is in the past, |
| 16 | // and firing late is the correct answer, not skipping. |
| 17 | func (s *Store) nextOccurrenceAfter(e Event, now int64, offsetMin int) (int64, string, bool) { |
| 18 | if e.RRule == "" { |
| 19 | if e.StartUTC-int64(offsetMin)*60 <= now && e.StartUTC <= now { |
| 20 | return 0, "", false // already started; nothing to warn about |
| 21 | } |
| 22 | return e.StartUTC, e.StartLocal, true |
| 23 | } |
| 24 | rule, err := ParseRule(e.RRule) |
| 25 | if err != nil { |
| 26 | // A foreign rule still has its own first occurrence. |
| 27 | if e.StartUTC > now { |
| 28 | return e.StartUTC, e.StartLocal, true |
| 29 | } |
| 30 | return 0, "", false |
| 31 | } |
| 32 | // A window wide enough to find the next occurrence of anything |
| 33 | // realistic — a yearly series needs more than a year. |
| 34 | const lookahead = 400 * 24 * 3600 |
| 35 | occs := Expand(e.StartLocal, e.EndLocal, e.TZ, e.AllDay, rule, e.Exdates, now-3600, now+lookahead) |
| 36 | for _, o := range occs { |
| 37 | if o.StartUTC > now { |
| 38 | return o.StartUTC, o.Local, true |
| 39 | } |
| 40 | } |
| 41 | return 0, "", false |
| 42 | } |
| 43 | |
| 44 | // NextDueAt is what Options.NextDue answers: the earliest moment this |
| 45 | // instance has work. |
| 46 | // |
| 47 | // Zero means nothing is scheduled, and the platform hibernates |
| 48 | // indefinitely. |
| 49 | func (s *Store) NextDueAt(ctx context.Context) time.Time { |
| 50 | due, _ := s.nextDueAt(ctx) |
| 51 | return due |
| 52 | } |
| 53 | |
| 54 | func (s *Store) nextDueAt(ctx context.Context) (time.Time, error) { |
| 55 | var at *int64 |
| 56 | err := s.g.WithContext(ctx).Raw(`SELECT MIN(at) FROM ( |
| 57 | SELECT MIN(MAX(due_at, COALESCE(retry_at, 0), COALESCE(claimed_at + ?, 0))) AS at |
| 58 | FROM reminder_queues WHERE fired_at IS NULL |
| 59 | UNION ALL SELECT MIN(hold_expires_at) FROM bookings WHERE state = ? |
| 60 | UNION ALL SELECT MIN(due_at) FROM booking_mails WHERE sent_at IS NULL |
| 61 | UNION ALL SELECT MIN(ip_forget_at) FROM bookings WHERE ip_fp IS NOT NULL |
| 62 | )`, int64(claimLease.Seconds()), BookingHeld).Scan(&at).Error |
| 63 | if err != nil || at == nil { |
| 64 | return time.Time{}, err |
| 65 | } |
| 66 | return time.Unix(*at, 0), nil |
| 67 | } |
| 68 | |