@chipmobilesdk/rn-notification 0.1.2 → 0.2.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 (69) hide show
  1. package/CHANGELOG.md +96 -42
  2. package/README.md +399 -306
  3. package/package.json +2 -1
  4. package/skills/sdk-rn-notification/SKILL.md +68 -3
  5. package/src/channel/migrate.ts +86 -12
  6. package/src/channel/tone-map.ts +130 -90
  7. package/src/channel/validate-tone.ts +61 -0
  8. package/src/contract/channel.ts +188 -89
  9. package/src/contract/diagnostics.ts +102 -102
  10. package/src/contract/errors.ts +130 -123
  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 +60 -60
  15. package/src/contract/reconcile.ts +88 -88
  16. package/src/contract/recurrence.ts +123 -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 +64 -64
  21. package/src/engine/adapter.ts +172 -79
  22. package/src/engine/notify-kit-engine.ts +761 -535
  23. package/src/index.ts +24 -0
  24. package/src/initialize.ts +174 -174
  25. package/src/interaction/index.ts +146 -146
  26. package/src/maintenance/cycle.ts +147 -147
  27. package/src/maintenance/permission-refresh.ts +41 -41
  28. package/src/maintenance/reanchor.ts +95 -95
  29. package/src/maintenance/toggle.ts +60 -60
  30. package/src/maintenance/verify.ts +83 -83
  31. package/src/maintenance/window-refresh.ts +61 -61
  32. package/src/permission/index.ts +183 -183
  33. package/src/permission/state.ts +77 -77
  34. package/src/push/envelope.ts +165 -165
  35. package/src/push/fcm-provider.ts +146 -146
  36. package/src/push/gate.ts +190 -190
  37. package/src/push/instructions.ts +109 -109
  38. package/src/push/provider-adapter.ts +54 -54
  39. package/src/push/signout.ts +65 -65
  40. package/src/reconcile/chunk.ts +53 -53
  41. package/src/reconcile/diff.ts +145 -145
  42. package/src/reconcile/duplicates.ts +73 -73
  43. package/src/reconcile/index.ts +14 -1
  44. package/src/reconcile/truncate.ts +56 -56
  45. package/src/recurrence/clock-guard.ts +71 -71
  46. package/src/recurrence/expand.ts +292 -292
  47. package/src/repeat/expiry.ts +55 -0
  48. package/src/repeat/index.ts +54 -0
  49. package/src/repeat/marker.ts +72 -0
  50. package/src/repeat/place.ts +73 -0
  51. package/src/schedule/anchor.ts +267 -267
  52. package/src/schedule/index.ts +184 -173
  53. package/src/schedule/partition.ts +85 -85
  54. package/src/schedule/resolve-moment.ts +80 -80
  55. package/src/schedule/validate-content.ts +111 -111
  56. package/src/schedule/validate-identifier.ts +126 -114
  57. package/src/state/caps.ts +71 -71
  58. package/src/state/interaction-buffer.ts +73 -73
  59. package/src/state/mmkv-store.ts +75 -71
  60. package/src/state/permission-history.ts +41 -41
  61. package/src/state/record.ts +258 -246
  62. package/src/state/reset.ts +41 -41
  63. package/src/state/series-ledger.ts +84 -84
  64. package/src/state/store.ts +40 -40
  65. package/src/testing/conformance/index.ts +248 -194
  66. package/src/testing/fake-engine.ts +469 -284
  67. package/src/testing/fake-provider.ts +129 -129
  68. package/src/testing/faults.ts +72 -72
  69. package/src/testing/measure.ts +337 -334
@@ -1,79 +1,172 @@
1
- /**
2
- * The port every local notification engine satisfies.
3
- *
4
- * No engine type appears in any signature here. That is what makes the engine
5
- * replaceable — and it has already been exercised once, when the archived
6
- * Notifee was swapped for its maintained fork with no consumer impact.
7
- */
8
-
9
- import type { PendingEntry, NotificationRequest } from '../contract/notification';
10
- import type { PermissionKind, PermissionState, RestrictionReport } from '../contract/permission';
11
- import type { ResolvedTone, ChannelGroup } from '../contract/channel';
12
- import type { InteractionEvent, Unsubscribe } from '../contract/interaction';
13
-
14
- /** A request whose anchoring has already been resolved to a firing instant. */
15
- export interface EngineScheduleRequest {
16
- readonly request: NotificationRequest;
17
- /** The absolute instant the platform should fire, after anchoring resolution. */
18
- readonly resolvedAt: number;
19
- /** Resolved platform channel and category for the request's tone. */
20
- readonly tone: ResolvedTone;
21
- /**
22
- * The IANA time zone this moment was anchored against.
23
- *
24
- * Carried explicitly because the adapter has to round-trip it through the
25
- * platform data bag — `getPending` must report it (FR-010) so a consumer can
26
- * detect the one case re-anchoring cannot fix (FR-007c). Absent for
27
- * absolute-instant moments, which are never anchored to a zone at all.
28
- */
29
- readonly anchoringGeneration?: string;
30
- /** True when exact-alarm capability is unavailable and approximate mode applies. */
31
- readonly approximate: boolean;
32
- readonly seriesId?: string;
33
- readonly pushCreated?: boolean;
34
- }
35
-
36
- export interface EngineCapabilities {
37
- /** iOS holds only the 64 soonest pending notifications and silently drops the rest. */
38
- readonly pendingCeiling: number;
39
- readonly supportsExactAlarms: boolean;
40
- readonly supportsChannels: boolean;
41
- readonly supportsTextInputActions: boolean;
42
- readonly supportsDoNotDisturbBypass: boolean;
43
- /** Milliseconds a background action or silent payload may run for. */
44
- readonly backgroundWindowMs: number;
45
- }
46
-
47
- export interface NotificationEngineAdapter {
48
- readonly name: string;
49
-
50
- getCapabilities(): Promise<EngineCapabilities>;
51
-
52
- /** Scheduling an existing identifier replaces it; exactly one entry results. */
53
- schedule(request: EngineScheduleRequest): Promise<void>;
54
-
55
- cancel(id: string): Promise<void>;
56
- cancelMany(ids: readonly string[]): Promise<void>;
57
- cancelAll(): Promise<void>;
58
-
59
- /** Full detail, so callers can diff rather than merely count. */
60
- getPending(): Promise<readonly PendingEntry[]>;
61
-
62
- /** Applies a channel or category declaration. Returns true when the platform created it. */
63
- declareTone(tone: ResolvedTone): Promise<boolean>;
64
- /** Removes a retired channel. Returns true when one was actually removed. */
65
- retireTone(channelId: string): Promise<boolean>;
66
- declareGroups(groups: readonly ChannelGroup[]): Promise<void>;
67
- /** Channel ids the platform currently holds, which re-declaration cannot reveal. */
68
- listDeclaredChannels(): Promise<readonly string[]>;
69
-
70
- getPermission(kind: PermissionKind): Promise<PermissionState>;
71
- requestPermission(kind: PermissionKind): Promise<PermissionState>;
72
- openSettings(kind: PermissionKind): Promise<void>;
73
- getRestrictions(): Promise<RestrictionReport>;
74
-
75
- /** Interactions arriving before a handler registers are the caller's problem to buffer, not the engine's. */
76
- subscribeToInteractions(handler: (event: InteractionEvent) => void): Unsubscribe;
77
- /** Any interaction that launched the app, consumed exactly once. */
78
- takeInitialInteraction(): Promise<InteractionEvent | undefined>;
79
- }
1
+ /**
2
+ * The port every local notification engine satisfies.
3
+ *
4
+ * No engine type appears in any signature here. That is what makes the engine
5
+ * replaceable — and it has already been exercised once, when the archived
6
+ * Notifee was swapped for its maintained fork with no consumer impact.
7
+ */
8
+
9
+ import type { PendingEntry, NotificationRequest } from '../contract/notification';
10
+ import type { PermissionKind, PermissionState, RestrictionReport } from '../contract/permission';
11
+ import type { ResolvedTone, ChannelGroup } from '../contract/channel';
12
+ import type { InteractionEvent, Unsubscribe } from '../contract/interaction';
13
+
14
+ /** A request whose anchoring has already been resolved to a firing instant. */
15
+ export interface EngineScheduleRequest {
16
+ readonly request: NotificationRequest;
17
+ /** The absolute instant the platform should fire, after anchoring resolution. */
18
+ readonly resolvedAt: number;
19
+ /** Resolved platform channel and category for the request's tone. */
20
+ readonly tone: ResolvedTone;
21
+ /**
22
+ * The IANA time zone this moment was anchored against.
23
+ *
24
+ * Carried explicitly because the adapter has to round-trip it through the
25
+ * platform data bag — `getPending` must report it (FR-010) so a consumer can
26
+ * detect the one case re-anchoring cannot fix (FR-007c). Absent for
27
+ * absolute-instant moments, which are never anchored to a zone at all.
28
+ */
29
+ readonly anchoringGeneration?: string;
30
+ /** True when exact-alarm capability is unavailable and approximate mode applies. */
31
+ readonly approximate: boolean;
32
+ readonly seriesId?: string;
33
+ readonly pushCreated?: boolean;
34
+ /**
35
+ * Marks this request as a package-owned stop marker rather than a consumer
36
+ * notification.
37
+ *
38
+ * Carried here rather than smuggled through `ownerTag` or `routing` so it can
39
+ * never collide with a value a consumer owns, and so the adapter can keep
40
+ * markers out of `getPending` without inspecting consumer data.
41
+ */
42
+ readonly repeatStop?: RepeatStop;
43
+ }
44
+
45
+ export interface EngineCapabilities {
46
+ /** iOS holds only the 64 soonest pending notifications and silently drops the rest. */
47
+ readonly pendingCeiling: number;
48
+ readonly supportsExactAlarms: boolean;
49
+ readonly supportsChannels: boolean;
50
+ readonly supportsTextInputActions: boolean;
51
+ readonly supportsDoNotDisturbBypass: boolean;
52
+ /**
53
+ * Whether a notification's alert sound can be made to repeat until handled.
54
+ *
55
+ * Decides two things: whether a refused repeat is reported to the caller, and
56
+ * whether a stop marker is placed at all. Where this is false no marker is
57
+ * created, so the platform's pending ceiling is untouched by the feature.
58
+ */
59
+ readonly supportsRepeatingAlert: boolean;
60
+ /** Milliseconds a background action or silent payload may run for. */
61
+ readonly backgroundWindowMs: number;
62
+ }
63
+
64
+ /**
65
+ * What applying a tone declaration actually did.
66
+ *
67
+ * A boolean cannot carry this: a caller has to tell a label correction from a
68
+ * newly created channel from a no-op, and reporting a refresh as a creation
69
+ * would make every apply look like a write.
70
+ */
71
+ export type ToneDeclarationOutcome = 'created' | 'labelsRefreshed' | 'unchanged';
72
+
73
+ /**
74
+ * A package-owned artifact scheduled to end a repeating alert.
75
+ *
76
+ * Never a consumer notification. It exists because no platform offers "stop the
77
+ * sound at time T but keep the notification", so something has to run at T.
78
+ */
79
+ export interface RepeatStop {
80
+ readonly markerId: string;
81
+ /** The consumer notification whose alert this stops. */
82
+ readonly targetId: string;
83
+ readonly dueAt: number;
84
+ }
85
+
86
+ /** A notification arriving, as opposed to a user acting on one. */
87
+ export interface DeliveryEvent {
88
+ readonly notificationId: string;
89
+ readonly occurredAt: number;
90
+ /** Present when this delivery is a package-owned stop marker rather than a real notification. */
91
+ readonly repeatStop?: RepeatStop;
92
+ }
93
+
94
+ export interface NotificationEngineAdapter {
95
+ readonly name: string;
96
+
97
+ getCapabilities(): Promise<EngineCapabilities>;
98
+
99
+ /** Scheduling an existing identifier replaces it; exactly one entry results. */
100
+ schedule(request: EngineScheduleRequest): Promise<void>;
101
+
102
+ cancel(id: string): Promise<void>;
103
+ cancelMany(ids: readonly string[]): Promise<void>;
104
+ cancelAll(): Promise<void>;
105
+
106
+ /** Full detail, so callers can diff rather than merely count. */
107
+ getPending(): Promise<readonly PendingEntry[]>;
108
+
109
+ /**
110
+ * Applies a channel or category declaration.
111
+ *
112
+ * Must return `labelsRefreshed` only when a label actually changed. Returning
113
+ * it unconditionally would turn every apply into a write and quietly break
114
+ * the zero-operation guarantee reconciliation idempotence rests on.
115
+ */
116
+ declareTone(tone: ResolvedTone): Promise<ToneDeclarationOutcome>;
117
+ /** Removes a retired channel. Returns true when one was actually removed. */
118
+ retireTone(channelId: string): Promise<boolean>;
119
+ declareGroups(groups: readonly ChannelGroup[]): Promise<void>;
120
+ /** Channel ids the platform currently holds, which re-declaration cannot reveal. */
121
+ listDeclaredChannels(): Promise<readonly string[]>;
122
+
123
+ getPermission(kind: PermissionKind): Promise<PermissionState>;
124
+ requestPermission(kind: PermissionKind): Promise<PermissionState>;
125
+ openSettings(kind: PermissionKind): Promise<void>;
126
+ getRestrictions(): Promise<RestrictionReport>;
127
+
128
+ /** Interactions arriving before a handler registers are the caller's problem to buffer, not the engine's. */
129
+ subscribeToInteractions(handler: (event: InteractionEvent) => void): Unsubscribe;
130
+ /** Any interaction that launched the app, consumed exactly once. */
131
+ takeInitialInteraction(): Promise<InteractionEvent | undefined>;
132
+
133
+ /**
134
+ * Stops a delivered notification's repeating alert without removing it.
135
+ *
136
+ * Re-posts the notification under the same identifier with the alert
137
+ * suppressed. The same identifier means the same tray slot, so it replaces in
138
+ * place: the sound stops and the notification stays. Cancelling instead would
139
+ * be worse than never alerting at all — the user comes home to a silent phone
140
+ * and no idea what they missed.
141
+ *
142
+ * Returns false when the notification is no longer displayed, which is the
143
+ * ordinary case once the user has handled it.
144
+ */
145
+ silenceDelivered(id: string): Promise<boolean>;
146
+
147
+ /** Package-owned stop markers currently scheduled. Never consumer notifications. */
148
+ listRepeatStops(): Promise<readonly RepeatStop[]>;
149
+
150
+ /**
151
+ * Deliveries, as distinct from interactions.
152
+ *
153
+ * `subscribeToInteractions` deliberately discards delivery events, because
154
+ * reporting an arrival as a tap would cancel a notification the user never
155
+ * touched. Repeat expiry needs exactly the events that filter throws away, so
156
+ * it gets its own subscription rather than loosening that one.
157
+ */
158
+ subscribeToDeliveries(handler: (event: DeliveryEvent) => void): Unsubscribe;
159
+
160
+ /**
161
+ * Registers the handler that runs when the app process is not alive.
162
+ *
163
+ * Must be called at module scope, outside the React tree, because that is
164
+ * where the engine registers its headless task. The package cannot do this on
165
+ * a consumer's behalf, which is why it is a documented setup step rather than
166
+ * an internal detail — and why its absence is reported rather than guessed at.
167
+ */
168
+ registerBackgroundDeliveryHandler(handler: (event: DeliveryEvent) => Promise<void>): void;
169
+
170
+ /** Whether a background delivery handler has been registered in this process. */
171
+ hasBackgroundDeliveryHandler(): boolean;
172
+ }