Forget pushed event ids when Google Calendar is disconnected (Feature 16)
Disconnect deleted the connection but left every pushed plan synced with its stored event ids, though no calendar was chosen any more. It now deletes the connection and runs the same invalidation as choosing another calendar (forgetEvents: MarkCalendarPlansEdited, ClearCalendarEvents) in one transaction, under the account lock. A refused revoke still changes nothing. Tests push three weeks, disconnect, and check each is changed with no event id; then connect again, choose a calendar and push the whole week to it. Also: a refused revoke keeps every week synced, and a disconnect during a push waits for its record and then forgets it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+22
-11
@@ -96,10 +96,11 @@ func (s *Service) SelectCalendar(ctx context.Context, id string) (Selection, err
|
||||
// saveSelection stores sel as the chosen calendar. Choosing another calendar
|
||||
// than the one stored also forgets every plan day's calendar event id, in
|
||||
// the same transaction, since those events are on the calendar no longer
|
||||
// chosen: each plan with one is edited, so a synced plan becomes changed and
|
||||
// its next push writes every day to sel. The caller holds s.account, which a
|
||||
// push also holds from its event writes through recording them, so a push
|
||||
// never records events written to another calendar than the chosen one.
|
||||
// chosen (forgetEvents): each plan with one is edited, so a synced plan
|
||||
// becomes changed and its next push writes every day to sel. The caller
|
||||
// holds s.account, which a push also holds from its event writes through
|
||||
// recording them, so a push never records events written to another
|
||||
// calendar than the chosen one.
|
||||
func (s *Service) saveSelection(ctx context.Context, sel Selection) error {
|
||||
return s.st.WithTx(ctx, func(tx *sql.Tx) error {
|
||||
q := db.New(tx)
|
||||
@@ -123,16 +124,26 @@ func (s *Service) saveSelection(ctx context.Context, sel Selection) error {
|
||||
if row.CalendarID.Valid && row.CalendarID.String == sel.CalendarID {
|
||||
return nil
|
||||
}
|
||||
if err := q.MarkCalendarPlansEdited(ctx, now); err != nil {
|
||||
return fmt.Errorf("gcal: mark plans pushed to the earlier calendar edited: %w", err)
|
||||
}
|
||||
if err := q.ClearCalendarEvents(ctx, now); err != nil {
|
||||
return fmt.Errorf("gcal: forget events on the earlier calendar: %w", err)
|
||||
}
|
||||
return nil
|
||||
return forgetEvents(ctx, q, now)
|
||||
})
|
||||
}
|
||||
|
||||
// forgetEvents records, in the caller's transaction q, that no calendar
|
||||
// event id stored is on the chosen calendar any more: every plan with one is
|
||||
// edited (a synced plan becomes changed, and a push that read it before is
|
||||
// refused), then every id is cleared. Every change that ends the chosen
|
||||
// calendar — choosing another (saveSelection) or disconnecting
|
||||
// (Disconnect) — runs it in the transaction that makes the change.
|
||||
func forgetEvents(ctx context.Context, q *db.Queries, now string) error {
|
||||
if err := q.MarkCalendarPlansEdited(ctx, now); err != nil {
|
||||
return fmt.Errorf("gcal: mark plans pushed to the earlier calendar edited: %w", err)
|
||||
}
|
||||
if err := q.ClearCalendarEvents(ctx, now); err != nil {
|
||||
return fmt.Errorf("gcal: forget events on the earlier calendar: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// calendars lists the writable calendars, each once, with the stored refresh
|
||||
// token. The caller holds s.account.
|
||||
func (s *Service) calendars(ctx context.Context) ([]Calendar, error) {
|
||||
|
||||
@@ -342,6 +342,11 @@ func (s *Service) store(ctx context.Context, sealed []byte) error {
|
||||
// connection. A token Google no longer knows (the household already removed
|
||||
// MealQuest's access in their Google account) counts as revoked. Any other
|
||||
// revoke failure keeps the connection so the household can retry.
|
||||
//
|
||||
// No calendar is chosen once disconnected, so the same transaction that
|
||||
// deletes the connection forgets every pushed event (forgetEvents): every
|
||||
// synced plan becomes changed, and the next push after connecting again and
|
||||
// choosing a calendar writes the whole week.
|
||||
func (s *Service) Disconnect(ctx context.Context) error {
|
||||
s.account.Lock()
|
||||
defer s.account.Unlock()
|
||||
@@ -359,10 +364,13 @@ func (s *Service) Disconnect(ctx context.Context) error {
|
||||
if err := s.revoke(ctx, string(token)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.New(s.st.Writer).DeleteIntegrationsGoogle(ctx); err != nil {
|
||||
return fmt.Errorf("gcal: delete Google integration: %w", err)
|
||||
}
|
||||
return nil
|
||||
return s.st.WithTx(ctx, func(tx *sql.Tx) error {
|
||||
q := db.New(tx)
|
||||
if err := q.DeleteIntegrationsGoogle(ctx); err != nil {
|
||||
return fmt.Errorf("gcal: delete Google integration: %w", err)
|
||||
}
|
||||
return forgetEvents(ctx, q, store.Timestamp{Time: s.now().UTC().Truncate(time.Millisecond)}.String())
|
||||
})
|
||||
}
|
||||
|
||||
// RevokeError is a revoke Google refused or that failed in transit. Its text
|
||||
|
||||
Reference in New Issue
Block a user