@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 +2 -0
- package/index.js +3 -0
- package/mailx-api.d.ts +17 -0
- package/package.json +1 -1
- package/reminder-state.d.ts +42 -0
- package/reminder-state.js +85 -0
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
|
@@ -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
|