@chipmobilesdk/rn-notification 0.3.0 → 0.4.1

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +174 -109
  2. package/README.md +414 -399
  3. package/package.json +4 -21
  4. package/skills/sdk-rn-notification/SKILL.md +5 -4
  5. package/src/channel/migrate.ts +285 -285
  6. package/src/channel/tone-map.ts +130 -130
  7. package/src/channel/validate-tone.ts +61 -61
  8. package/src/contract/channel.ts +192 -188
  9. package/src/contract/diagnostics.ts +102 -102
  10. package/src/contract/errors.ts +134 -130
  11. package/src/contract/index.ts +10 -10
  12. package/src/contract/interaction.ts +62 -62
  13. package/src/contract/notification.ts +118 -118
  14. package/src/contract/permission.ts +55 -60
  15. package/src/contract/reconcile.ts +88 -88
  16. package/src/contract/recurrence.ts +129 -123
  17. package/src/diagnostics/burst.ts +101 -101
  18. package/src/diagnostics/events.ts +72 -72
  19. package/src/diagnostics/redact.ts +72 -72
  20. package/src/diagnostics/snapshot.ts +67 -64
  21. package/src/engine/adapter.ts +172 -172
  22. package/src/engine/codec.ts +89 -0
  23. package/src/engine/notify-kit-engine.ts +706 -761
  24. package/src/index.ts +208 -208
  25. package/src/initialize.ts +240 -174
  26. package/src/interaction/index.ts +146 -146
  27. package/src/maintenance/cycle.ts +150 -147
  28. package/src/maintenance/permission-refresh.ts +39 -41
  29. package/src/maintenance/reanchor.ts +101 -95
  30. package/src/maintenance/toggle.ts +66 -60
  31. package/src/maintenance/verify.ts +83 -83
  32. package/src/maintenance/window-refresh.ts +64 -61
  33. package/src/permission/index.ts +183 -183
  34. package/src/permission/state.ts +77 -77
  35. package/src/push/envelope.ts +165 -165
  36. package/src/push/fcm-provider.ts +143 -146
  37. package/src/push/gate.ts +190 -190
  38. package/src/push/index.ts +33 -29
  39. package/src/push/instructions.ts +114 -109
  40. package/src/push/provider-adapter.ts +54 -54
  41. package/src/push/signout.ts +65 -65
  42. package/src/reconcile/chunk.ts +53 -53
  43. package/src/reconcile/diff.ts +142 -145
  44. package/src/reconcile/duplicates.ts +73 -73
  45. package/src/reconcile/index.ts +180 -188
  46. package/src/reconcile/truncate.ts +56 -56
  47. package/src/recurrence/clock-guard.ts +73 -71
  48. package/src/recurrence/expand.ts +326 -292
  49. package/src/recurrence/index.ts +270 -177
  50. package/src/repeat/expiry.ts +53 -55
  51. package/src/repeat/index.ts +54 -54
  52. package/src/repeat/marker.ts +72 -72
  53. package/src/repeat/place.ts +73 -73
  54. package/src/schedule/anchor.ts +278 -267
  55. package/src/schedule/index.ts +174 -184
  56. package/src/schedule/partition.ts +88 -85
  57. package/src/schedule/resolve-moment.ts +80 -80
  58. package/src/schedule/validate-content.ts +111 -111
  59. package/src/schedule/validate-identifier.ts +126 -126
  60. package/src/schedule/writer.ts +55 -0
  61. package/src/state/caps.ts +71 -71
  62. package/src/state/dedup-ledger.ts +35 -35
  63. package/src/state/interaction-buffer.ts +73 -73
  64. package/src/state/mmkv-store.ts +75 -75
  65. package/src/state/permission-history.ts +41 -41
  66. package/src/state/record.ts +266 -258
  67. package/src/state/reset.ts +41 -41
  68. package/src/state/series-ledger.ts +91 -84
  69. package/src/state/store.ts +40 -40
  70. package/src/testing/conformance/index.ts +258 -248
  71. package/src/testing/fake-engine.ts +462 -469
  72. package/src/testing/fake-provider.ts +126 -129
  73. package/src/testing/faults.ts +72 -72
  74. package/src/testing/index.ts +23 -28
  75. package/src/testing/measure.ts +337 -337
@@ -1,188 +1,192 @@
1
- /**
2
- * Channel, category, and tone declaration types.
3
- *
4
- * A tone is declared once and mapped by the package to each platform's
5
- * equivalent concepts, so consumer code contains no platform branches.
6
- *
7
- * Two tones must never be merged into one: channel configuration belongs to
8
- * the user, and collapsing two tones collapses two independent user choices.
9
- */
10
-
11
- /** How insistently a notification asks for attention. */
12
- export type ToneUrgency = 'min' | 'low' | 'default' | 'high' | 'max';
13
-
14
- export interface ToneSound {
15
- /** Consumer-supplied resource name, resolved by the platform. */
16
- readonly name?: string;
17
- /** When false, the tone is silent. Defaults to true. */
18
- readonly enabled?: boolean;
19
- }
20
-
21
- /**
22
- * A repeating alert and the bound on how long it may repeat.
23
- *
24
- * The two are one object rather than two fields because an unbounded repeating
25
- * alert must be *unrepresentable*, not merely discouraged. A boolean `repeat`
26
- * beside an optional duration would make the dangerous state the easiest one to
27
- * write, and the dangerous state here is a phone that keeps sounding until its
28
- * owner comes home to it.
29
- */
30
- export interface ToneRepeatAlert {
31
- /**
32
- * How long the alert may keep repeating, measured from delivery.
33
- *
34
- * Required. Bounded by REPEAT_ALERT_BOUNDS, which is enforced at declaration
35
- * time — a type can forbid an absent duration but not a zero one.
36
- */
37
- readonly forMs: number;
38
- }
39
-
40
- /**
41
- * The declarable range for a repeating alert.
42
- *
43
- * The floor exists because a repeat shorter than a minute is indistinguishable
44
- * from a single chime and buys nothing for the complexity it costs. The ceiling
45
- * exists because an hour is already past the point where a person who has not
46
- * responded is going to.
47
- */
48
- export const REPEAT_ALERT_BOUNDS = {
49
- minMs: 60_000,
50
- maxMs: 3_600_000,
51
- } as const;
52
-
53
- export interface ToneDeclaration {
54
- /** Stable tone identifier, subject to the same charset rules as a notification id. */
55
- readonly id: string;
56
- /** Human-readable name shown in system settings. */
57
- readonly name: string;
58
- readonly description?: string;
59
- readonly urgency: ToneUrgency;
60
- readonly sound?: ToneSound;
61
- readonly vibration?: boolean;
62
- readonly badge?: boolean;
63
- /** Optional grouping in system settings. */
64
- readonly groupId?: string;
65
- /**
66
- * Request the platform's Do Not Disturb bypass. Honoured where the platform
67
- * permits it and reported honestly where it does not. This is *not* an
68
- * override of the device's silent mode, which is out of scope.
69
- */
70
- readonly bypassDoNotDisturb?: boolean;
71
- /**
72
- * Present when the alert sound should repeat until the user handles the
73
- * notification, rather than sounding once.
74
- *
75
- * Realized per notification rather than as channel configuration, so adding
76
- * or changing it never obliges a consumer to bump `version`. Honoured where
77
- * the platform permits it and reported through `repeatUnavailable` where it
78
- * does not.
79
- */
80
- readonly repeatAlert?: ToneRepeatAlert;
81
- /**
82
- * Ask the platform to treat notifications on this tone as time-sensitive
83
- * alerts. Like `repeatAlert`, realized per notification, so it needs no
84
- * version bump.
85
- */
86
- readonly timeSensitive?: boolean;
87
- /**
88
- * Incremented by the consumer when a field the platform freezes changes:
89
- * `urgency`, `sound`, `vibration`, `badge`, `groupId`, or
90
- * `bypassDoNotDisturb`.
91
- *
92
- * Android freezes those once a channel exists, so changing one is a
93
- * migration rather than an assignment, and this version is what makes the
94
- * migration detectable.
95
- *
96
- * `name`, `description`, `repeatAlert`, and `timeSensitive` are deliberately
97
- * *not* on that list. Android updates a channel's labels in place, and the
98
- * two alert attributes are realized per notification — so none of them needs
99
- * a bump. This matters: a bump means a new channel, and a new channel means
100
- * the user loses every setting they adjusted. Nobody should have to pay that
101
- * to correct a typo.
102
- */
103
- readonly version: number;
104
- }
105
-
106
- export interface ChannelGroup {
107
- readonly id: string;
108
- readonly name: string;
109
- readonly description?: string;
110
- }
111
-
112
- /** What a tone resolves to on each platform. Internal to the engine adapter. */
113
- export interface ResolvedTone {
114
- readonly toneId: string;
115
- readonly version: number;
116
- readonly android: {
117
- /** Platform channel id, derived from tone id and version so a change creates a new channel. */
118
- readonly channelId: string;
119
- /**
120
- * The channel's user-visible name.
121
- *
122
- * Required rather than optional on purpose. Its absence is what let the
123
- * adapter fall back to the tone id, so users read a technical identifier
124
- * on the one screen these strings exist to serve. A type that permits no
125
- * name permits that bug back.
126
- */
127
- readonly name: string;
128
- /** The channel's user-visible description. */
129
- readonly description?: string;
130
- readonly importance: 'none' | 'min' | 'low' | 'default' | 'high';
131
- readonly sound?: string;
132
- readonly vibration: boolean;
133
- readonly badge: boolean;
134
- readonly bypassDnd: boolean;
135
- readonly groupId?: string;
136
- /** Repeat the alert sound until handled. Per notification, not channel configuration. */
137
- readonly loopSound: boolean;
138
- /** Mark the notification as an alarm-category alert. Per notification. */
139
- readonly alarmCategory: boolean;
140
- /** How long repetition may continue, so the adapter knows when to place the stop marker. */
141
- readonly repeatForMs?: number;
142
- };
143
- readonly ios: {
144
- readonly categoryId: string;
145
- readonly interruptionLevel: 'passive' | 'active' | 'timeSensitive' | 'critical';
146
- readonly sound?: string;
147
- readonly badge: boolean;
148
- /**
149
- * True when a repeating alert was declared and iOS will not honour it.
150
- *
151
- * iOS has no looping-sound API, and the one level that overrides the mute
152
- * switch is entitlement-gated and does not loop either. Recording the
153
- * refusal as data rather than as a comment is what lets `apply()` report
154
- * it instead of dropping it silently.
155
- */
156
- readonly repeatRefused: boolean;
157
- };
158
- }
159
-
160
- /**
161
- * Outcome of applying a channel configuration, including any migration performed.
162
- *
163
- * A tone appears in exactly one of `created`, `migrated`, `labelsRefreshed`, and
164
- * `unchanged`. The `*Unavailable` arrays are orthogonal and may accompany any of
165
- * them: a platform refusing an attribute says nothing about whether the tone was
166
- * newly created.
167
- */
168
- export interface ChannelApplyResult {
169
- readonly created: readonly string[];
170
- readonly migrated: readonly { readonly toneId: string; readonly from: number; readonly to: number }[];
171
- /**
172
- * Tones whose name or description was updated in place on a channel that
173
- * already existed. No channel was recreated and no user setting was reset —
174
- * the platform itself refuses to overwrite the rest of the configuration,
175
- * which is what makes a label correction free.
176
- */
177
- readonly labelsRefreshed: readonly string[];
178
- readonly retired: readonly string[];
179
- /** Notifications moved to the new channel during migration. */
180
- readonly reHomed: readonly string[];
181
- /** Notifications cancelled because they could not be re-homed. */
182
- readonly cancelled: readonly string[];
183
- /** Tones whose requested DND bypass the platform refused. */
184
- readonly bypassUnavailable: readonly string[];
185
- /** Tones whose requested repeating alert the platform refused. */
186
- readonly repeatUnavailable: readonly string[];
187
- readonly unchanged: readonly string[];
188
- }
1
+ /**
2
+ * Channel, category, and tone declaration types.
3
+ *
4
+ * A tone is declared once and mapped by the package to each platform's
5
+ * equivalent concepts, so consumer code contains no platform branches.
6
+ *
7
+ * Two tones must never be merged into one: channel configuration belongs to
8
+ * the user, and collapsing two tones collapses two independent user choices.
9
+ */
10
+
11
+ /** How insistently a notification asks for attention. */
12
+ export type ToneUrgency = 'min' | 'low' | 'default' | 'high' | 'max';
13
+
14
+ export interface ToneSound {
15
+ /** Consumer-supplied resource name, resolved by the platform. */
16
+ readonly name?: string;
17
+ /** When false, the tone is silent. Defaults to true. */
18
+ readonly enabled?: boolean;
19
+ }
20
+
21
+ /**
22
+ * A repeating alert and the bound on how long it may repeat.
23
+ *
24
+ * The two are one object rather than two fields because an unbounded repeating
25
+ * alert must be *unrepresentable*, not merely discouraged. A boolean `repeat`
26
+ * beside an optional duration would make the dangerous state the easiest one to
27
+ * write, and the dangerous state here is a phone that keeps sounding until its
28
+ * owner comes home to it.
29
+ */
30
+ export interface ToneRepeatAlert {
31
+ /**
32
+ * How long the alert may keep repeating, measured from delivery.
33
+ *
34
+ * Required. Bounded by REPEAT_ALERT_BOUNDS, which is enforced at declaration
35
+ * time — a type can forbid an absent duration but not a zero one.
36
+ */
37
+ readonly forMs: number;
38
+ }
39
+
40
+ /**
41
+ * The declarable range for a repeating alert.
42
+ *
43
+ * The floor exists because a repeat shorter than a minute is indistinguishable
44
+ * from a single chime and buys nothing for the complexity it costs. The ceiling
45
+ * exists because an hour is already past the point where a person who has not
46
+ * responded is going to.
47
+ */
48
+ export const REPEAT_ALERT_BOUNDS = {
49
+ minMs: 60_000,
50
+ maxMs: 3_600_000,
51
+ } as const;
52
+
53
+ export interface ToneDeclaration {
54
+ /** Stable tone identifier, subject to the same charset rules as a notification id. */
55
+ readonly id: string;
56
+ /** Human-readable name shown in system settings. */
57
+ readonly name: string;
58
+ readonly description?: string;
59
+ readonly urgency: ToneUrgency;
60
+ readonly sound?: ToneSound;
61
+ readonly vibration?: boolean;
62
+ readonly badge?: boolean;
63
+ /** Optional grouping in system settings. */
64
+ readonly groupId?: string;
65
+ /**
66
+ * Request the platform's Do Not Disturb bypass. Honoured where the platform
67
+ * permits it and reported honestly where it does not. This is *not* an
68
+ * override of the device's silent mode, which is out of scope.
69
+ */
70
+ readonly bypassDoNotDisturb?: boolean;
71
+ /**
72
+ * Present when the alert sound should repeat until the user handles the
73
+ * notification, rather than sounding once.
74
+ *
75
+ * Realized per notification rather than as channel configuration, so adding
76
+ * or changing it never obliges a consumer to bump `version`. Honoured where
77
+ * the platform permits it and reported through `repeatUnavailable` where it
78
+ * does not.
79
+ */
80
+ readonly repeatAlert?: ToneRepeatAlert;
81
+ /**
82
+ * Ask the platform to treat notifications on this tone as time-sensitive
83
+ * alerts. Like `repeatAlert`, realized per notification, so it needs no
84
+ * version bump.
85
+ */
86
+ readonly timeSensitive?: boolean;
87
+ /**
88
+ * Incremented by the consumer when a field the platform freezes changes:
89
+ * `urgency`, `sound`, `vibration`, `badge`, `groupId`, or
90
+ * `bypassDoNotDisturb`.
91
+ *
92
+ * Android freezes those once a channel exists, so changing one is a
93
+ * migration rather than an assignment, and this version is what makes the
94
+ * migration detectable.
95
+ *
96
+ * `name`, `description`, `repeatAlert`, and `timeSensitive` are deliberately
97
+ * *not* on that list. Android updates a channel's labels in place, and the
98
+ * two alert attributes are realized per notification — so none of them needs
99
+ * a bump. This matters: a bump means a new channel, and a new channel means
100
+ * the user loses every setting they adjusted. Nobody should have to pay that
101
+ * to correct a typo.
102
+ */
103
+ readonly version: number;
104
+ }
105
+
106
+ export interface ChannelGroup {
107
+ readonly id: string;
108
+ readonly name: string;
109
+ readonly description?: string;
110
+ }
111
+
112
+ /** What a tone resolves to on each platform. Internal to the engine adapter. */
113
+ export interface ResolvedTone {
114
+ readonly toneId: string;
115
+ readonly version: number;
116
+ readonly android: {
117
+ /** Platform channel id, derived from tone id and version so a change creates a new channel. */
118
+ readonly channelId: string;
119
+ /**
120
+ * The channel's user-visible name.
121
+ *
122
+ * Required rather than optional on purpose. Its absence is what let the
123
+ * adapter fall back to the tone id, so users read a technical identifier
124
+ * on the one screen these strings exist to serve. A type that permits no
125
+ * name permits that bug back.
126
+ */
127
+ readonly name: string;
128
+ /** The channel's user-visible description. */
129
+ readonly description?: string;
130
+ readonly importance: 'none' | 'min' | 'low' | 'default' | 'high';
131
+ readonly sound?: string;
132
+ readonly vibration: boolean;
133
+ readonly badge: boolean;
134
+ readonly bypassDnd: boolean;
135
+ readonly groupId?: string;
136
+ /** Repeat the alert sound until handled. Per notification, not channel configuration. */
137
+ readonly loopSound: boolean;
138
+ /** Mark the notification as an alarm-category alert. Per notification. */
139
+ readonly alarmCategory: boolean;
140
+ /** How long repetition may continue, so the adapter knows when to place the stop marker. */
141
+ readonly repeatForMs?: number;
142
+ };
143
+ readonly ios: {
144
+ readonly categoryId: string;
145
+ readonly interruptionLevel: 'passive' | 'active' | 'timeSensitive' | 'critical';
146
+ readonly sound?: string;
147
+ readonly badge: boolean;
148
+ /**
149
+ * True when a repeating alert was declared and iOS will not honour it.
150
+ *
151
+ * iOS has no looping-sound API, and the one level that overrides the mute
152
+ * switch is entitlement-gated and does not loop either. Recording the
153
+ * refusal as data rather than as a comment is what lets `apply()` report
154
+ * it instead of dropping it silently.
155
+ */
156
+ readonly repeatRefused: boolean;
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Outcome of applying a channel configuration, including any migration performed.
162
+ *
163
+ * A tone appears in exactly one of `created`, `migrated`, `labelsRefreshed`, and
164
+ * `unchanged`. The `*Unavailable` arrays are orthogonal and may accompany any of
165
+ * them: a platform refusing an attribute says nothing about whether the tone was
166
+ * newly created.
167
+ */
168
+ export interface ChannelApplyResult {
169
+ readonly created: readonly string[];
170
+ readonly migrated: readonly {
171
+ readonly toneId: string;
172
+ readonly from: number;
173
+ readonly to: number;
174
+ }[];
175
+ /**
176
+ * Tones whose name or description was updated in place on a channel that
177
+ * already existed. No channel was recreated and no user setting was reset —
178
+ * the platform itself refuses to overwrite the rest of the configuration,
179
+ * which is what makes a label correction free.
180
+ */
181
+ readonly labelsRefreshed: readonly string[];
182
+ readonly retired: readonly string[];
183
+ /** Notifications moved to the new channel during migration. */
184
+ readonly reHomed: readonly string[];
185
+ /** Notifications cancelled because they could not be re-homed. */
186
+ readonly cancelled: readonly string[];
187
+ /** Tones whose requested DND bypass the platform refused. */
188
+ readonly bypassUnavailable: readonly string[];
189
+ /** Tones whose requested repeating alert the platform refused. */
190
+ readonly repeatUnavailable: readonly string[];
191
+ readonly unchanged: readonly string[];
192
+ }
@@ -1,102 +1,102 @@
1
- /**
2
- * Diagnostics and configuration types.
3
- *
4
- * The package's stated purpose is to make invisible notification failures
5
- * diagnosable, so every lifecycle transition emits a structured event with a
6
- * stable identifier — and none of those events carries content by default.
7
- */
8
-
9
- import type { PermissionReport, RestrictionReport } from './permission';
10
- import type { PendingDiscrepancy, MaintenanceResult, ReconcileResult } from './reconcile';
11
- import type { ToneDeclaration, ChannelGroup } from './channel';
12
- import type { LookaheadConfig } from './recurrence';
13
- import type { PendingEntry } from './notification';
14
-
15
- export type DiagnosticSeverity = 'debug' | 'info' | 'warn' | 'error';
16
-
17
- export interface DiagnosticEvent {
18
- /** Stable identifier, safe to switch on. See contracts/errors.md. */
19
- readonly code: string;
20
- readonly severity: DiagnosticSeverity;
21
- readonly message: string;
22
- readonly at: number;
23
- /**
24
- * Structured context. Never contains notification body text, routing payload
25
- * values, or push tokens unless verbose diagnostics is explicitly enabled.
26
- */
27
- readonly context?: Readonly<Record<string, unknown>>;
28
- }
29
-
30
- export type EventSink = (event: DiagnosticEvent) => void;
31
-
32
- /** Health of one persisted collection against its cap. */
33
- export interface CollectionHealth {
34
- readonly name: string;
35
- readonly size: number;
36
- readonly cap: number;
37
- readonly oldestEntryAt?: number;
38
- }
39
-
40
- export interface StateHealth {
41
- readonly readable: boolean;
42
- /** True when the record was discarded and rebuilt, e.g. after corruption. */
43
- readonly rebuilt: boolean;
44
- readonly collections: readonly CollectionHealth[];
45
- readonly approximateBytes: number;
46
- }
47
-
48
- /** The readable answer to "why didn't it arrive". */
49
- export interface StateSnapshot {
50
- readonly permissions: PermissionReport;
51
- readonly restrictions: RestrictionReport;
52
- readonly tones: readonly { readonly id: string; readonly appliedVersion: number }[];
53
- readonly pendingCount: number;
54
- readonly pending: readonly PendingEntry[];
55
- readonly lastReconcile?: ReconcileResult;
56
- readonly lastMaintenance?: MaintenanceResult;
57
- readonly discrepancy?: PendingDiscrepancy;
58
- readonly stateHealth: StateHealth;
59
- /** Presence only. The token value is never exposed here or anywhere else. */
60
- readonly pushTokenPresent: boolean;
61
- readonly capturedAt: number;
62
- }
63
-
64
- /** Abnormal notification volume. Reported, never acted upon. */
65
- export interface BurstThreshold {
66
- readonly perMinute: number;
67
- readonly perHour: number;
68
- }
69
-
70
- export const DEFAULT_BURST_THRESHOLD: BurstThreshold = {
71
- perMinute: 10,
72
- perHour: 60,
73
- };
74
-
75
- export interface NotificationConfig {
76
- /** Every tone, category, and channel in one place. */
77
- readonly tones: readonly ToneDeclaration[];
78
- readonly channelGroups?: readonly ChannelGroup[];
79
- readonly lookahead?: Partial<LookaheadConfig>;
80
- /**
81
- * Automatic maintenance covers only what the package alone can know:
82
- * window refresh, expansion, re-anchoring, verification, permission re-read.
83
- * Full desired-set reconciliation is never automatic. Defaults to true.
84
- */
85
- readonly automaticMaintenance?: boolean;
86
- readonly burstThreshold?: BurstThreshold;
87
- /** With no sink supplied the package emits nothing rather than writing to a console. */
88
- readonly eventSink?: EventSink;
89
- /**
90
- * Includes content values in diagnostics. Documented as unsuitable for
91
- * production, and off by default.
92
- */
93
- readonly verboseDiagnostics?: boolean;
94
- }
95
-
96
- export interface InitializeResult {
97
- readonly tonesApplied: number;
98
- readonly migrationsRun: number;
99
- readonly stateRebuilt: boolean;
100
- readonly automaticMaintenance: boolean;
101
- readonly durationMs: number;
102
- }
1
+ /**
2
+ * Diagnostics and configuration types.
3
+ *
4
+ * The package's stated purpose is to make invisible notification failures
5
+ * diagnosable, so every lifecycle transition emits a structured event with a
6
+ * stable identifier — and none of those events carries content by default.
7
+ */
8
+
9
+ import type { PermissionReport, RestrictionReport } from './permission';
10
+ import type { PendingDiscrepancy, MaintenanceResult, ReconcileResult } from './reconcile';
11
+ import type { ToneDeclaration, ChannelGroup } from './channel';
12
+ import type { LookaheadConfig } from './recurrence';
13
+ import type { PendingEntry } from './notification';
14
+
15
+ export type DiagnosticSeverity = 'debug' | 'info' | 'warn' | 'error';
16
+
17
+ export interface DiagnosticEvent {
18
+ /** Stable identifier, safe to switch on. See contracts/errors.md. */
19
+ readonly code: string;
20
+ readonly severity: DiagnosticSeverity;
21
+ readonly message: string;
22
+ readonly at: number;
23
+ /**
24
+ * Structured context. Never contains notification body text, routing payload
25
+ * values, or push tokens unless verbose diagnostics is explicitly enabled.
26
+ */
27
+ readonly context?: Readonly<Record<string, unknown>>;
28
+ }
29
+
30
+ export type EventSink = (event: DiagnosticEvent) => void;
31
+
32
+ /** Health of one persisted collection against its cap. */
33
+ export interface CollectionHealth {
34
+ readonly name: string;
35
+ readonly size: number;
36
+ readonly cap: number;
37
+ readonly oldestEntryAt?: number;
38
+ }
39
+
40
+ export interface StateHealth {
41
+ readonly readable: boolean;
42
+ /** True when the record was discarded and rebuilt, e.g. after corruption. */
43
+ readonly rebuilt: boolean;
44
+ readonly collections: readonly CollectionHealth[];
45
+ readonly approximateBytes: number;
46
+ }
47
+
48
+ /** The readable answer to "why didn't it arrive". */
49
+ export interface StateSnapshot {
50
+ readonly permissions: PermissionReport;
51
+ readonly restrictions: RestrictionReport;
52
+ readonly tones: readonly { readonly id: string; readonly appliedVersion: number }[];
53
+ readonly pendingCount: number;
54
+ readonly pending: readonly PendingEntry[];
55
+ readonly lastReconcile?: ReconcileResult;
56
+ readonly lastMaintenance?: MaintenanceResult;
57
+ readonly discrepancy?: PendingDiscrepancy;
58
+ readonly stateHealth: StateHealth;
59
+ /** Presence only. The token value is never exposed here or anywhere else. */
60
+ readonly pushTokenPresent: boolean;
61
+ readonly capturedAt: number;
62
+ }
63
+
64
+ /** Abnormal notification volume. Reported, never acted upon. */
65
+ export interface BurstThreshold {
66
+ readonly perMinute: number;
67
+ readonly perHour: number;
68
+ }
69
+
70
+ export const DEFAULT_BURST_THRESHOLD: BurstThreshold = {
71
+ perMinute: 10,
72
+ perHour: 60,
73
+ };
74
+
75
+ export interface NotificationConfig {
76
+ /** Every tone, category, and channel in one place. */
77
+ readonly tones: readonly ToneDeclaration[];
78
+ readonly channelGroups?: readonly ChannelGroup[];
79
+ readonly lookahead?: Partial<LookaheadConfig>;
80
+ /**
81
+ * Automatic maintenance covers only what the package alone can know:
82
+ * window refresh, expansion, re-anchoring, verification, permission re-read.
83
+ * Full desired-set reconciliation is never automatic. Defaults to true.
84
+ */
85
+ readonly automaticMaintenance?: boolean;
86
+ readonly burstThreshold?: BurstThreshold;
87
+ /** With no sink supplied the package emits nothing rather than writing to a console. */
88
+ readonly eventSink?: EventSink;
89
+ /**
90
+ * Includes content values in diagnostics. Documented as unsuitable for
91
+ * production, and off by default.
92
+ */
93
+ readonly verboseDiagnostics?: boolean;
94
+ }
95
+
96
+ export interface InitializeResult {
97
+ readonly tonesApplied: number;
98
+ readonly migrationsRun: number;
99
+ readonly stateRebuilt: boolean;
100
+ readonly automaticMaintenance: boolean;
101
+ readonly durationMs: number;
102
+ }