@upnextapp/core 0.17.2 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- export type { ReminderStatus, ScheduledReminder, RecurrenceType, ResponsibilityDef, AppSettings, StoreSchema } from './types.js';
2
- export { calculateNextTrigger, calculateNextTriggerFromNow, firstSlotForDay, isWithinActiveHours, matchesRecurrenceInterval, shouldTriggerOnDay, isStaleActive, isOccurrenceOverdue, STALE_ACTIVE_THRESHOLD_MS, HOURLY_STALE_THRESHOLD_MS } from './scheduling.js';
1
+ export type { ReminderStatus, ScheduledReminder, RecurrenceType, ResponsibilityDef, AppSettings, StoreSchema, WriteKind } from './types.js';
2
+ export { authorityOverride } from './writeAuthority.js';
3
+ export type { AuthorityRow } from './writeAuthority.js';
4
+ export { calculateNextTrigger, calculateNextTriggerFromNow, firstSlotForDay, isWithinActiveHours, matchesRecurrenceInterval, shouldTriggerOnDay, isStaleActive, isOccurrenceOverdue, STALE_ACTIVE_THRESHOLD_MS } from './scheduling.js';
3
5
  export { isTimeOfDayOutsideActiveHours } from './activeHours.js';
4
6
  export { coarsenIdleTime, IDLE_TIME_BUCKET_SECONDS } from './coarsenIdleTime.js';
5
7
  export { formatNextTrigger } from './formatNextTrigger.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- export { calculateNextTrigger, calculateNextTriggerFromNow, firstSlotForDay, isWithinActiveHours, matchesRecurrenceInterval, shouldTriggerOnDay, isStaleActive, isOccurrenceOverdue, STALE_ACTIVE_THRESHOLD_MS, HOURLY_STALE_THRESHOLD_MS } from './scheduling.js';
1
+ export { authorityOverride } from './writeAuthority.js';
2
+ export { calculateNextTrigger, calculateNextTriggerFromNow, firstSlotForDay, isWithinActiveHours, matchesRecurrenceInterval, shouldTriggerOnDay, isStaleActive, isOccurrenceOverdue, STALE_ACTIVE_THRESHOLD_MS } from './scheduling.js';
2
3
  export { isTimeOfDayOutsideActiveHours } from './activeHours.js';
3
4
  export { coarsenIdleTime, IDLE_TIME_BUCKET_SECONDS } from './coarsenIdleTime.js';
4
5
  export { formatNextTrigger } from './formatNextTrigger.js';
@@ -1,6 +1,5 @@
1
1
  import { type ResponsibilityDef, type ScheduledReminder } from './types.js';
2
2
  export declare const STALE_ACTIVE_THRESHOLD_MS: number;
3
- export declare const HOURLY_STALE_THRESHOLD_MS: number;
4
3
  export declare function shouldTriggerOnDay(def: ResponsibilityDef, date: Date): boolean;
5
4
  export declare function isWithinActiveHours(now: number, startTime: string, endTime: string): boolean;
6
5
  export declare function matchesRecurrenceInterval(def: ResponsibilityDef, day: Date, startDate: Date, anchorDayStartMs: number): boolean;
@@ -1,5 +1,4 @@
1
1
  export const STALE_ACTIVE_THRESHOLD_MS = 48 * 60 * 60 * 1000;
2
- export const HOURLY_STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
3
2
  export function shouldTriggerOnDay(def, date) {
4
3
  if (!def.daysOfWeek || def.daysOfWeek.length === 0)
5
4
  return true;
@@ -173,10 +172,18 @@ export function calculateNextTriggerFromNow(def, fromTime, activeStart, activeEn
173
172
  return calculateNextTrigger(def, fromTime, activeStart, activeEnd);
174
173
  }
175
174
  export function isStaleActive(reminder, now, def) {
176
- if (!reminder.activatedAt)
175
+ // Single 48h threshold for every recurrence type, aligned with the server's P3
176
+ // progression floor (sync_lww_guard) and writeAuthority.ts — the sweep fires only
177
+ // when the server would accept the dismissal, so attempts succeed first try.
178
+ // Keyed to activated_at — the alive-as-active clock — because updated_at is
179
+ // re-stamped by every plugin tick, which starved the sweep on ticking rows (Obs 3).
180
+ // Legacy rows with a null activated_at fall back to updated_at (server COALESCE).
181
+ // def is retained for signature compatibility.
182
+ void def;
183
+ const aliveAsActiveAt = reminder.activatedAt ?? reminder.updatedAt;
184
+ if (aliveAsActiveAt == null)
177
185
  return true;
178
- const threshold = def?.recurrenceType === 'hourly' ? HOURLY_STALE_THRESHOLD_MS : STALE_ACTIVE_THRESHOLD_MS;
179
- return (now - reminder.activatedAt) > threshold;
186
+ return (now - aliveAsActiveAt) > STALE_ACTIVE_THRESHOLD_MS;
180
187
  }
181
188
  export function isOccurrenceOverdue(def, reminder, now, activeStart, activeEnd) {
182
189
  // Auto-advance only when the next due slot is on a NEW calendar day than the
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type ReminderStatus = 'scheduled' | 'active' | 'delayed' | 'completed';
2
+ export type WriteKind = 'user' | 'auto';
2
3
  export interface ScheduledReminder {
3
4
  defId: string;
4
5
  status: ReminderStatus;
@@ -7,6 +8,8 @@ export interface ScheduledReminder {
7
8
  delayUntil?: number;
8
9
  lastCompletedAt?: number;
9
10
  pluginData?: Record<string, unknown>;
11
+ writeKind?: WriteKind;
12
+ updatedAt?: number;
10
13
  }
11
14
  export type RecurrenceType = 'once' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly';
12
15
  export interface ResponsibilityDef {
@@ -21,6 +24,7 @@ export interface ResponsibilityDef {
21
24
  pluginId?: string;
22
25
  reminderType?: string;
23
26
  pluginData?: Record<string, unknown>;
27
+ writeKind?: WriteKind;
24
28
  }
25
29
  export interface AppSettings {
26
30
  activeStartTime: string;
@@ -0,0 +1,9 @@
1
+ export interface AuthorityRow {
2
+ writeKind?: string | null;
3
+ status?: string | null;
4
+ delayUntil?: number | string | null;
5
+ nextTriggerTime?: number | string | null;
6
+ activatedAt?: number | string | null;
7
+ updatedAt?: number | string | null;
8
+ }
9
+ export declare function authorityOverride(local: AuthorityRow, incoming: AuthorityRow, now: number): boolean | null;
@@ -0,0 +1,42 @@
1
+ import { STALE_ACTIVE_THRESHOLD_MS } from './scheduling.js';
2
+ // Matches the server's jsonb BIGINT casts: NULL/missing coerces to 0.
3
+ function stamp(value) {
4
+ return Number(value) || 0;
5
+ }
6
+ // Client mirror of sync_lww_guard (supabase/migrations/20260920200000_p3_staleness_activation.sql):
7
+ // incoming user over local auto always wins; incoming auto over local user wins only via a
8
+ // progression rule — the user's own data says it's time:
9
+ // P1 delayed -> active once the user-set delay_until has passed
10
+ // P2 completed -> scheduled/active with a strictly LATER next_trigger_time (forward rebook only)
11
+ // P3 active -> scheduled/completed once the row is 48h stale by activated_at — the
12
+ // alive-as-active clock, falling back to updated_at when activated_at is NULL (legacy
13
+ // rows). updated_at alone is wrong: ticking plugins re-stamp it every tick, which
14
+ // starved the sweep on actively-ticking rows (Obs 3).
15
+ // P4 scheduled -> active once the user-scheduled next_trigger_time has arrived (the fire)
16
+ // Returns null when kinds match so the caller falls back to updated_at comparison.
17
+ // Missing kinds default to 'auto' (S9: pre-migration rows, old clients).
18
+ export function authorityOverride(local, incoming, now) {
19
+ const localKind = local.writeKind ?? 'auto';
20
+ const incomingKind = incoming.writeKind ?? 'auto';
21
+ if (localKind === incomingKind)
22
+ return null;
23
+ if (incomingKind === 'user')
24
+ return true;
25
+ if (local.status === 'delayed'
26
+ && incoming.status === 'active'
27
+ && stamp(local.delayUntil) <= now)
28
+ return true;
29
+ if (local.status === 'completed'
30
+ && (incoming.status === 'scheduled' || incoming.status === 'active')
31
+ && stamp(incoming.nextTriggerTime) > stamp(local.nextTriggerTime))
32
+ return true;
33
+ if (local.status === 'active'
34
+ && (incoming.status === 'scheduled' || incoming.status === 'completed')
35
+ && stamp(local.activatedAt ?? local.updatedAt) <= now - STALE_ACTIVE_THRESHOLD_MS)
36
+ return true;
37
+ if (local.status === 'scheduled'
38
+ && incoming.status === 'active'
39
+ && stamp(local.nextTriggerTime) <= now)
40
+ return true;
41
+ return false;
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upnextapp/core",
3
- "version": "0.17.2",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "description": "Platform-agnostic core for UpNext: domain types, scheduling math, integrity payloads",
6
6
  "license": "MIT",