@bobfrankston/mailx-types 0.1.32 → 0.1.36

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/index.d.ts CHANGED
@@ -8,6 +8,8 @@ export type { MailxApi } from "./mailx-api.js";
8
8
  export { addContactsDenylistEntry, addContactsPreferredEntry, type PreferredContactEntry, type CloudReadFn, type CloudWriteFn, } from "./contacts-config.js";
9
9
  export { expandRecipients, splitRecipients, isAddressToken, extractAddress, } from "./groups.js";
10
10
  export type { GroupMap, RecipientToken, ExpansionResult } from "./groups.js";
11
+ export { REMINDER_STATE_FILE, normalizeReminderState, mergeReminderStates, reminderStatesEqual, } from "./reminder-state.js";
12
+ export type { ReminderState } from "./reminder-state.js";
11
13
  /** Supported authentication methods */
12
14
  export type AuthMethod = "password" | "oauth2";
13
15
  /** Mail account configuration */
package/index.js CHANGED
@@ -14,6 +14,9 @@ export { addContactsDenylistEntry, addContactsPreferredEntry, } from "./contacts
14
14
  // send time. Both desktop and Android send paths consume this expander
15
15
  // against contacts.jsonc → groups.
16
16
  export { expandRecipients, splitRecipients, isAddressToken, extractAddress, } from "./groups.js";
17
+ // Reminder dismissed/snoozed cross-device state — one merge implementation
18
+ // for the desktop service (mailx-settings) and Android/web (mailx-store-web).
19
+ export { REMINDER_STATE_FILE, normalizeReminderState, mergeReminderStates, reminderStatesEqual, } from "./reminder-state.js";
17
20
  // ── Message flag state ──
18
21
  //
19
22
  // External API surface for the IMAP system flags. The literal strings
package/mailx-api.d.ts CHANGED
@@ -205,11 +205,28 @@ export interface MailxApi {
205
205
  statusText: string;
206
206
  }>;
207
207
  consumePendingMailto(): any | Promise<any>;
208
+ /** C46 Windows Share target — startup-race twin of the daemon's
209
+ * fs.watch on pending-share.json. Optional: desktop-only. */
210
+ consumePendingShare?(): any | Promise<any>;
208
211
  getReminderSound(): Promise<{
209
212
  mute?: boolean;
210
213
  dataBase64?: string;
211
214
  mime?: string;
212
215
  }>;
216
+ /** Cross-device reminder dismissed/snoozed state (reminders.jsonc on the
217
+ * shared Drive folder). merge sends the device's local maps and returns
218
+ * the merged union — push + pull in one round-trip. */
219
+ getReminderState?(): Promise<{
220
+ dismissed: Record<string, number>;
221
+ snoozed: Record<string, number>;
222
+ }>;
223
+ mergeReminderState?(patch: {
224
+ dismissed: Record<string, number>;
225
+ snoozed: Record<string, number>;
226
+ }): Promise<{
227
+ dismissed: Record<string, number>;
228
+ snoozed: Record<string, number>;
229
+ }>;
213
230
  logClientEvent?(...args: any[]): void | Promise<void>;
214
231
  getVersion?(): any | Promise<any>;
215
232
  openInWord?(editId: string, html: string): Promise<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-types",
3
- "version": "0.1.32",
3
+ "version": "0.1.36",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Reminder dismissed/snoozed state — shared across devices.
3
+ *
4
+ * The alarm subsystem (client/components/alarms.ts) keys every reminder by a
5
+ * per-occurrence string (`providerId:startMs[@offsetMs]` for calendar,
6
+ * `taskUuid:dueMs` for tasks). Those keys are built from Google-stable ids,
7
+ * so the SAME occurrence produces the SAME key on every device — which is
8
+ * what makes cross-device sync meaningful: dismiss a reminder on the desktop
9
+ * and the laptop's poller sees the dismissal before it fires its own popup.
10
+ *
11
+ * State lives in `reminders.jsonc` in the shared GDrive folder (same rails
12
+ * as allowlist.jsonc / userdict.csv). Values are epoch-ms timestamps:
13
+ * dismissed[key] = when the user dismissed it (retention: 30 days)
14
+ * snoozed[key] = fire again after this time (retention: 7 days past)
15
+ *
16
+ * Merge is per-key max — a dismissal or longer snooze on ANY device wins.
17
+ * Deletions don't propagate (union semantics); the only deletion in the
18
+ * alarm flow is the Open-button re-evaluate, which by construction touches
19
+ * a key that was never pushed as dismissed. Timestamps double as the prune
20
+ * criterion so the file can't grow forever.
21
+ *
22
+ * This module is dependency-free (mailx-types) so the desktop service
23
+ * (mailx-settings) and the Android/web service (mailx-store-web) share one
24
+ * merge implementation.
25
+ */
26
+ export interface ReminderState {
27
+ /** occurrence-key → epoch-ms when dismissed */
28
+ dismissed: Record<string, number>;
29
+ /** occurrence-key → epoch-ms snooze-until */
30
+ snoozed: Record<string, number>;
31
+ }
32
+ export declare const REMINDER_STATE_FILE = "reminders.jsonc";
33
+ /** Coerce arbitrary parsed JSON (or the client's localStorage maps, whose
34
+ * legacy dismissed values are boolean `true`) into a clean ReminderState.
35
+ * Legacy booleans become `now` so they get a full retention window. */
36
+ export declare function normalizeReminderState(raw: any, now: number): ReminderState;
37
+ /** Union merge with per-key max, pruned by retention. Inputs must already
38
+ * be normalized. Pure — does not mutate its arguments. */
39
+ export declare function mergeReminderStates(a: ReminderState, b: ReminderState, now: number): ReminderState;
40
+ /** Deep-equal for change detection so a no-op merge skips the cloud write. */
41
+ export declare function reminderStatesEqual(a: ReminderState, b: ReminderState): boolean;
42
+ //# sourceMappingURL=reminder-state.d.ts.map
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Reminder dismissed/snoozed state — shared across devices.
3
+ *
4
+ * The alarm subsystem (client/components/alarms.ts) keys every reminder by a
5
+ * per-occurrence string (`providerId:startMs[@offsetMs]` for calendar,
6
+ * `taskUuid:dueMs` for tasks). Those keys are built from Google-stable ids,
7
+ * so the SAME occurrence produces the SAME key on every device — which is
8
+ * what makes cross-device sync meaningful: dismiss a reminder on the desktop
9
+ * and the laptop's poller sees the dismissal before it fires its own popup.
10
+ *
11
+ * State lives in `reminders.jsonc` in the shared GDrive folder (same rails
12
+ * as allowlist.jsonc / userdict.csv). Values are epoch-ms timestamps:
13
+ * dismissed[key] = when the user dismissed it (retention: 30 days)
14
+ * snoozed[key] = fire again after this time (retention: 7 days past)
15
+ *
16
+ * Merge is per-key max — a dismissal or longer snooze on ANY device wins.
17
+ * Deletions don't propagate (union semantics); the only deletion in the
18
+ * alarm flow is the Open-button re-evaluate, which by construction touches
19
+ * a key that was never pushed as dismissed. Timestamps double as the prune
20
+ * criterion so the file can't grow forever.
21
+ *
22
+ * This module is dependency-free (mailx-types) so the desktop service
23
+ * (mailx-settings) and the Android/web service (mailx-store-web) share one
24
+ * merge implementation.
25
+ */
26
+ export const REMINDER_STATE_FILE = "reminders.jsonc";
27
+ /** Dismissed entries older than this are pruned — by then the occurrence is
28
+ * long past the alarm lookback window on every device. */
29
+ const DISMISSED_RETENTION_MS = 30 * 86400_000;
30
+ /** Snoozed entries whose until-time is this far past are pruned. */
31
+ const SNOOZED_RETENTION_MS = 7 * 86400_000;
32
+ /** Coerce arbitrary parsed JSON (or the client's localStorage maps, whose
33
+ * legacy dismissed values are boolean `true`) into a clean ReminderState.
34
+ * Legacy booleans become `now` so they get a full retention window. */
35
+ export function normalizeReminderState(raw, now) {
36
+ const out = { dismissed: {}, snoozed: {} };
37
+ if (raw && typeof raw === "object") {
38
+ for (const [k, v] of Object.entries(raw.dismissed || {})) {
39
+ if (v === true)
40
+ out.dismissed[k] = now;
41
+ else if (Number.isFinite(v) && v > 0)
42
+ out.dismissed[k] = v;
43
+ }
44
+ for (const [k, v] of Object.entries(raw.snoozed || {})) {
45
+ if (Number.isFinite(v) && v > 0)
46
+ out.snoozed[k] = v;
47
+ }
48
+ }
49
+ return out;
50
+ }
51
+ /** Union merge with per-key max, pruned by retention. Inputs must already
52
+ * be normalized. Pure — does not mutate its arguments. */
53
+ export function mergeReminderStates(a, b, now) {
54
+ const out = { dismissed: {}, snoozed: {} };
55
+ const dismissedCutoff = now - DISMISSED_RETENTION_MS;
56
+ const snoozedCutoff = now - SNOOZED_RETENTION_MS;
57
+ for (const src of [a.dismissed, b.dismissed]) {
58
+ for (const [k, v] of Object.entries(src)) {
59
+ if (v < dismissedCutoff)
60
+ continue;
61
+ if (!(k in out.dismissed) || v > out.dismissed[k])
62
+ out.dismissed[k] = v;
63
+ }
64
+ }
65
+ for (const src of [a.snoozed, b.snoozed]) {
66
+ for (const [k, v] of Object.entries(src)) {
67
+ if (v < snoozedCutoff)
68
+ continue;
69
+ if (!(k in out.snoozed) || v > out.snoozed[k])
70
+ out.snoozed[k] = v;
71
+ }
72
+ }
73
+ return out;
74
+ }
75
+ /** Deep-equal for change detection so a no-op merge skips the cloud write. */
76
+ export function reminderStatesEqual(a, b) {
77
+ const mapsEqual = (x, y) => {
78
+ const xk = Object.keys(x);
79
+ if (xk.length !== Object.keys(y).length)
80
+ return false;
81
+ return xk.every(k => y[k] === x[k]);
82
+ };
83
+ return mapsEqual(a.dismissed, b.dismissed) && mapsEqual(a.snoozed, b.snoozed);
84
+ }
85
+ //# sourceMappingURL=reminder-state.js.map