reminders: add recurring reminder support
Add cron expression support for recurring reminders using robfig/cron/v3. Changes: - Migration #2: ALTER TABLE reminders ADD COLUMN cron TEXT + next_fire_ts INTEGER - Reminder struct: add Cron and NextFireTs fields - scanReminder helper extracts full row including nullable cron - CreateReminder: accept optional cron param, store next_fire_ts = fire_ts - DueReminders: query on next_fire_ts instead of fire_ts - RescheduleReminder: new method — parse cron, compute next fire, update next_fire_ts or mark fired if no more valid times - Dispatcher: call RescheduleReminder for cron reminders, MarkReminder for one-shots (preserving existing behavior for ID=0 digest skip) - ReminderCompleter interface: add RescheduleReminder method - storeAPI adapter: forward RescheduleReminder - All callers updated: CreateReminder signature includes cron param - Tests: TestRecurringReminder (store), TestDispatchRecurringReminderReschedules - Existing tests updated for new signature
This commit is contained in:
+10
-7
@@ -51,13 +51,15 @@ type Note struct {
|
||||
Score float64 `json:"score"`
|
||||
}
|
||||
|
||||
// Reminder — user-stated future intent; fires once.
|
||||
// Reminder — user-stated future intent; fires once or recurring (if cron set).
|
||||
type Reminder struct {
|
||||
ID int64 `json:"id"`
|
||||
CreatedTs time.Time `json:"created_ts"`
|
||||
FireTs time.Time `json:"fire_ts"`
|
||||
Payload string `json:"payload"`
|
||||
Status string `json:"status"` // pending|fired|cancelled
|
||||
ID int64 `json:"id"`
|
||||
CreatedTs time.Time `json:"created_ts"`
|
||||
FireTs time.Time `json:"fire_ts"`
|
||||
NextFireTs time.Time `json:"next_fire_ts"`
|
||||
Payload string `json:"payload"`
|
||||
Status string `json:"status"` // pending|fired|cancelled
|
||||
Cron string `json:"cron"`
|
||||
}
|
||||
|
||||
// Presence — the read the phraser / delivery modules need to decide channel
|
||||
@@ -137,6 +139,7 @@ type queryNotesReq struct {
|
||||
type createReminderReq struct {
|
||||
Fire time.Time `json:"fire"`
|
||||
Payload string `json:"payload"`
|
||||
Cron string `json:"cron"`
|
||||
}
|
||||
type recordNudgeReq struct {
|
||||
Rule string `json:"rule"`
|
||||
@@ -212,7 +215,7 @@ type CoreAPI interface {
|
||||
LatestFactBySource(ctx context.Context, key, source string) (Fact, error)
|
||||
Since(ctx context.Context, key string, now time.Time) (time.Duration, error)
|
||||
Presence(ctx context.Context) (Presence, error)
|
||||
CreateReminder(ctx context.Context, fire time.Time, payload string) (int64, error)
|
||||
CreateReminder(ctx context.Context, fire time.Time, payload, cron string) (int64, error)
|
||||
MarkReminder(ctx context.Context, id int64, status string) error
|
||||
RecordNudge(ctx context.Context, rule, channel, message string, ts time.Time) (int64, error)
|
||||
ResolveNudge(ctx context.Context, id int64, outcome string, ts time.Time) error
|
||||
|
||||
@@ -237,9 +237,9 @@ func (c *Client) Presence(ctx context.Context) (Presence, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (c *Client) CreateReminder(ctx context.Context, fire time.Time, payload string) (int64, error) {
|
||||
func (c *Client) CreateReminder(ctx context.Context, fire time.Time, payload, cron string) (int64, error) {
|
||||
var r idResp
|
||||
if err := c.call(ctx, MethodCreateReminder, createReminderReq{Fire: fire, Payload: payload}, &r); err != nil {
|
||||
if err := c.call(ctx, MethodCreateReminder, createReminderReq{Fire: fire, Payload: payload, Cron: cron}, &r); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return r.ID, nil
|
||||
|
||||
@@ -311,7 +311,7 @@ func TestClient_E2E(t *testing.T) {
|
||||
}
|
||||
|
||||
// reminder lifecycle: create → mark fired → re-mark ⇒ ErrReminderState.
|
||||
rid, err := cli.CreateReminder(ctx, now.Add(time.Hour), `{"text":"wake me 7"}`)
|
||||
rid, err := cli.CreateReminder(ctx, now.Add(time.Hour), `{"text":"wake me 7"}`, "")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateReminder: %v", err)
|
||||
}
|
||||
|
||||
@@ -68,8 +68,8 @@ func (a *storeAPI) Presence(ctx context.Context) (Presence, error) {
|
||||
return Presence{Bucket: Bucket(b), Score: score, Updated: upd}, nil
|
||||
}
|
||||
|
||||
func (a *storeAPI) CreateReminder(ctx context.Context, fire time.Time, payload string) (int64, error) {
|
||||
id, err := a.s.CreateReminder(ctx, fire, payload)
|
||||
func (a *storeAPI) CreateReminder(ctx context.Context, fire time.Time, payload, cron string) (int64, error) {
|
||||
id, err := a.s.CreateReminder(ctx, fire, payload, cron)
|
||||
return id, mapErr(err)
|
||||
}
|
||||
|
||||
@@ -77,6 +77,10 @@ func (a *storeAPI) MarkReminder(ctx context.Context, id int64, status string) er
|
||||
return mapErr(a.s.MarkReminder(ctx, id, status))
|
||||
}
|
||||
|
||||
func (a *storeAPI) RescheduleReminder(ctx context.Context, id int64, now time.Time) error {
|
||||
return mapErr(a.s.RescheduleReminder(ctx, id, now))
|
||||
}
|
||||
|
||||
func (a *storeAPI) RecordNudge(ctx context.Context, rule, channel, message string, ts time.Time) (int64, error) {
|
||||
id, err := a.s.RecordNudge(ctx, rule, channel, message, ts)
|
||||
return id, mapErr(err)
|
||||
@@ -457,7 +461,7 @@ func (s *Server) dispatch(ctx context.Context, req Request) (json.RawMessage, er
|
||||
if err := unmarshalParams(req.Params, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := s.api.CreateReminder(ctx, p.Fire, p.Payload)
|
||||
id, err := s.api.CreateReminder(ctx, p.Fire, p.Payload, p.Cron)
|
||||
return marshalResult(idResp{ID: id}), err
|
||||
|
||||
case MethodMarkReminder:
|
||||
|
||||
Reference in New Issue
Block a user