@chipmobilesdk/rn-notification 0.4.0 → 0.5.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +257 -166
  2. package/README.md +420 -410
  3. package/package.json +61 -75
  4. package/skills/sdk-rn-notification/SKILL.md +172 -173
  5. package/src/channel/migrate.ts +285 -289
  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 +142 -134
  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 -129
  17. package/src/diagnostics/burst.ts +101 -101
  18. package/src/diagnostics/events.ts +81 -72
  19. package/src/diagnostics/redact.ts +55 -72
  20. package/src/diagnostics/snapshot.ts +100 -64
  21. package/src/engine/adapter.ts +172 -172
  22. package/src/engine/codec.ts +89 -89
  23. package/src/engine/notify-kit-engine.ts +715 -707
  24. package/src/index.ts +208 -208
  25. package/src/initialize.ts +247 -236
  26. package/src/interaction/index.ts +146 -146
  27. package/src/maintenance/cycle.ts +150 -154
  28. package/src/maintenance/permission-refresh.ts +39 -41
  29. package/src/maintenance/reanchor.ts +101 -101
  30. package/src/maintenance/toggle.ts +66 -64
  31. package/src/maintenance/verify.ts +83 -83
  32. package/src/maintenance/window-refresh.ts +64 -64
  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 +150 -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 -180
  46. package/src/reconcile/truncate.ts +56 -56
  47. package/src/recurrence/clock-guard.ts +73 -71
  48. package/src/recurrence/expand.ts +326 -319
  49. package/src/recurrence/index.ts +270 -259
  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 +294 -267
  55. package/src/schedule/index.ts +174 -181
  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 -55
  61. package/src/state/caps.ts +71 -71
  62. package/src/state/dedup-ledger.ts +35 -35
  63. package/src/state/interaction-buffer.ts +101 -73
  64. package/src/state/mmkv-store.ts +92 -75
  65. package/src/state/permission-history.ts +41 -41
  66. package/src/state/record.ts +270 -263
  67. package/src/state/reset.ts +41 -41
  68. package/src/state/series-ledger.ts +91 -91
  69. package/src/state/store.ts +43 -40
  70. package/src/testing/conformance/index.ts +258 -248
  71. package/src/testing/fake-engine.ts +462 -468
  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,134 +1,142 @@
1
- /**
2
- * Typed error taxonomy and stable diagnostic identifiers.
3
- *
4
- * Every failure condition carries its own identifier. None collapses into a
5
- * generic failure — an error that says only "scheduling failed" reproduces the
6
- * problem this package exists to solve.
7
- *
8
- * Identifiers are stable across releases and safe to switch on. Adding new ones
9
- * is an additive minor change.
10
- *
11
- * See `specs/009-rn-notification-package/contracts/errors.md`.
12
- */
13
-
14
- export const DIAGNOSTIC_CODES = {
15
- // --- Validation: raised before any platform call -------------------------
16
- INVALID_ID_EMPTY: 'INVALID_ID_EMPTY',
17
- INVALID_ID_LENGTH: 'INVALID_ID_LENGTH',
18
- INVALID_ID_CHARSET: 'INVALID_ID_CHARSET',
19
- INVALID_OWNER_TAG: 'INVALID_OWNER_TAG',
20
- INVALID_GROUP_TAG: 'INVALID_GROUP_TAG',
21
- MOMENT_IN_PAST: 'MOMENT_IN_PAST',
22
- UNDECLARED_TONE: 'UNDECLARED_TONE',
23
- ROUTING_PAYLOAD_TOO_LARGE: 'ROUTING_PAYLOAD_TOO_LARGE',
24
- CONTENT_TOO_LARGE: 'CONTENT_TOO_LARGE',
25
- TOO_MANY_ACTIONS: 'TOO_MANY_ACTIONS',
26
- INVALID_RECURRENCE: 'INVALID_RECURRENCE',
27
- INVALID_TONE_REPEAT: 'INVALID_TONE_REPEAT',
28
-
29
- // --- Reconciliation ------------------------------------------------------
30
- DUPLICATE_IDENTIFIER: 'DUPLICATE_IDENTIFIER',
31
- TRUNCATED_OVER_CEILING: 'TRUNCATED_OVER_CEILING',
32
- RECONCILE_INTERRUPTED: 'RECONCILE_INTERRUPTED',
33
- PENDING_SET_DISCREPANCY: 'PENDING_SET_DISCREPANCY',
34
-
35
- // --- Permissions ---------------------------------------------------------
36
- DISPLAY_PERMISSION_DENIED: 'DISPLAY_PERMISSION_DENIED',
37
- DISPLAY_PERMISSION_PERMANENT: 'DISPLAY_PERMISSION_PERMANENT',
38
- EXACT_ALARM_UNAVAILABLE: 'EXACT_ALARM_UNAVAILABLE',
39
- SETTINGS_UNAVAILABLE: 'SETTINGS_UNAVAILABLE',
40
- BACKGROUND_RESTRICTED: 'BACKGROUND_RESTRICTED',
41
- BACKGROUND_RESTRICTION_UNKNOWN: 'BACKGROUND_RESTRICTION_UNKNOWN',
42
-
43
- // --- Channels ------------------------------------------------------------
44
- CHANNEL_MIGRATION_REQUIRED: 'CHANNEL_MIGRATION_REQUIRED',
45
- CHANNEL_MIGRATION_FAILED: 'CHANNEL_MIGRATION_FAILED',
46
- CHANNEL_ORPHANS_FOUND: 'CHANNEL_ORPHANS_FOUND',
47
- DND_BYPASS_UNAVAILABLE: 'DND_BYPASS_UNAVAILABLE',
48
- TONE_LABELS_REFRESHED: 'TONE_LABELS_REFRESHED',
49
- REPEAT_ALERT_UNAVAILABLE: 'REPEAT_ALERT_UNAVAILABLE',
50
- REPEAT_STOP_HANDLER_MISSING: 'REPEAT_STOP_HANDLER_MISSING',
51
-
52
- // --- Interaction ---------------------------------------------------------
53
- TARGET_MISSING: 'TARGET_MISSING',
54
- INTERACTION_BUFFER_EVICTED: 'INTERACTION_BUFFER_EVICTED',
55
- BACKGROUND_WINDOW_EXCEEDED: 'BACKGROUND_WINDOW_EXCEEDED',
56
-
57
- // --- Push ----------------------------------------------------------------
58
- PUSH_UNAVAILABLE: 'PUSH_UNAVAILABLE',
59
- TOKEN_UNAVAILABLE: 'TOKEN_UNAVAILABLE',
60
- TOKEN_INVALIDATION_FAILED: 'TOKEN_INVALIDATION_FAILED',
61
- PAYLOAD_MALFORMED: 'PAYLOAD_MALFORMED',
62
- PAYLOAD_VERSION_SKEW: 'PAYLOAD_VERSION_SKEW',
63
- PAYLOAD_DUPLICATE: 'PAYLOAD_DUPLICATE',
64
- PUSH_UNDELIVERABLE: 'PUSH_UNDELIVERABLE',
65
-
66
- // --- Persisted state -----------------------------------------------------
67
- STATE_CORRUPT_REBUILT: 'STATE_CORRUPT_REBUILT',
68
- STATE_WRITE_FAILED: 'STATE_WRITE_FAILED',
69
- STATE_CAP_EVICTED: 'STATE_CAP_EVICTED',
70
-
71
- // --- Volume --------------------------------------------------------------
72
- BURST_THRESHOLD_EXCEEDED: 'BURST_THRESHOLD_EXCEEDED',
73
-
74
- // --- Maintenance (0.4.0) -------------------------------------------------
75
- /** An automatic maintenance cycle threw. Reported here because no caller awaits it. */
76
- MAINTENANCE_FAILED: 'MAINTENANCE_FAILED',
77
- } as const;
78
-
79
- export type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[keyof typeof DIAGNOSTIC_CODES];
80
-
81
- /** Every identifier, for exhaustiveness tests and the documentation table. */
82
- export const ALL_DIAGNOSTIC_CODES: readonly DiagnosticCode[] = Object.freeze(
83
- Object.values(DIAGNOSTIC_CODES),
84
- );
85
-
86
- /**
87
- * A typed package failure.
88
- *
89
- * Carries enough context to choose a recovery, and never carries notification
90
- * body text, routing payload values, or token material. Where a value violated
91
- * a rule, the *shape* of the violation is reported — a length, a character
92
- * position — rather than the value itself.
93
- */
94
- export class NotificationError extends Error {
95
- readonly code: DiagnosticCode;
96
- readonly context?: Readonly<Record<string, unknown>>;
97
-
98
- constructor(code: DiagnosticCode, message: string, context?: Readonly<Record<string, unknown>>) {
99
- super(message);
100
- this.name = 'NotificationError';
101
- this.code = code;
102
- this.context = context;
103
- Object.setPrototypeOf(this, NotificationError.prototype);
104
- }
105
- }
106
-
107
- export function isNotificationError(value: unknown): value is NotificationError {
108
- return value instanceof NotificationError;
109
- }
110
-
111
- /**
112
- * Conditions that are reported outcomes rather than failures: the operation
113
- * succeeded, and something the caller needs to know happened alongside it.
114
- */
115
- export const NON_FAILURE_CODES: readonly DiagnosticCode[] = Object.freeze([
116
- DIAGNOSTIC_CODES.TRUNCATED_OVER_CEILING,
117
- DIAGNOSTIC_CODES.PENDING_SET_DISCREPANCY,
118
- DIAGNOSTIC_CODES.EXACT_ALARM_UNAVAILABLE,
119
- DIAGNOSTIC_CODES.TARGET_MISSING,
120
- DIAGNOSTIC_CODES.BURST_THRESHOLD_EXCEEDED,
121
- DIAGNOSTIC_CODES.PAYLOAD_VERSION_SKEW,
122
- DIAGNOSTIC_CODES.PAYLOAD_DUPLICATE,
123
- DIAGNOSTIC_CODES.STATE_CAP_EVICTED,
124
- DIAGNOSTIC_CODES.STATE_CORRUPT_REBUILT,
125
- DIAGNOSTIC_CODES.BACKGROUND_RESTRICTION_UNKNOWN,
126
- DIAGNOSTIC_CODES.DND_BYPASS_UNAVAILABLE,
127
- DIAGNOSTIC_CODES.TONE_LABELS_REFRESHED,
128
- DIAGNOSTIC_CODES.REPEAT_ALERT_UNAVAILABLE,
129
- DIAGNOSTIC_CODES.REPEAT_STOP_HANDLER_MISSING,
130
- ]);
131
-
132
- export function isNonFailure(code: DiagnosticCode): boolean {
133
- return NON_FAILURE_CODES.includes(code);
134
- }
1
+ /**
2
+ * Typed error taxonomy and stable diagnostic identifiers.
3
+ *
4
+ * Every failure condition carries its own identifier. None collapses into a
5
+ * generic failure — an error that says only "scheduling failed" reproduces the
6
+ * problem this package exists to solve.
7
+ *
8
+ * Identifiers are stable across releases and safe to switch on. Adding new ones
9
+ * is an additive minor change.
10
+ *
11
+ * See `specs/009-rn-notification-package/contracts/errors.md`.
12
+ */
13
+
14
+ import { SdkError } from '@chipmobilesdk/rn-core';
15
+
16
+ import { redactContext } from '../diagnostics/redact';
17
+
18
+ export const DIAGNOSTIC_CODES = {
19
+ // --- Validation: raised before any platform call -------------------------
20
+ INVALID_ID_EMPTY: 'INVALID_ID_EMPTY',
21
+ INVALID_ID_LENGTH: 'INVALID_ID_LENGTH',
22
+ INVALID_ID_CHARSET: 'INVALID_ID_CHARSET',
23
+ INVALID_OWNER_TAG: 'INVALID_OWNER_TAG',
24
+ INVALID_GROUP_TAG: 'INVALID_GROUP_TAG',
25
+ MOMENT_IN_PAST: 'MOMENT_IN_PAST',
26
+ UNDECLARED_TONE: 'UNDECLARED_TONE',
27
+ ROUTING_PAYLOAD_TOO_LARGE: 'ROUTING_PAYLOAD_TOO_LARGE',
28
+ CONTENT_TOO_LARGE: 'CONTENT_TOO_LARGE',
29
+ TOO_MANY_ACTIONS: 'TOO_MANY_ACTIONS',
30
+ INVALID_RECURRENCE: 'INVALID_RECURRENCE',
31
+ INVALID_TONE_REPEAT: 'INVALID_TONE_REPEAT',
32
+
33
+ // --- Reconciliation ------------------------------------------------------
34
+ DUPLICATE_IDENTIFIER: 'DUPLICATE_IDENTIFIER',
35
+ TRUNCATED_OVER_CEILING: 'TRUNCATED_OVER_CEILING',
36
+ RECONCILE_INTERRUPTED: 'RECONCILE_INTERRUPTED',
37
+ PENDING_SET_DISCREPANCY: 'PENDING_SET_DISCREPANCY',
38
+
39
+ // --- Permissions ---------------------------------------------------------
40
+ DISPLAY_PERMISSION_DENIED: 'DISPLAY_PERMISSION_DENIED',
41
+ DISPLAY_PERMISSION_PERMANENT: 'DISPLAY_PERMISSION_PERMANENT',
42
+ EXACT_ALARM_UNAVAILABLE: 'EXACT_ALARM_UNAVAILABLE',
43
+ SETTINGS_UNAVAILABLE: 'SETTINGS_UNAVAILABLE',
44
+ BACKGROUND_RESTRICTED: 'BACKGROUND_RESTRICTED',
45
+ BACKGROUND_RESTRICTION_UNKNOWN: 'BACKGROUND_RESTRICTION_UNKNOWN',
46
+
47
+ // --- Channels ------------------------------------------------------------
48
+ CHANNEL_MIGRATION_REQUIRED: 'CHANNEL_MIGRATION_REQUIRED',
49
+ CHANNEL_MIGRATION_FAILED: 'CHANNEL_MIGRATION_FAILED',
50
+ CHANNEL_ORPHANS_FOUND: 'CHANNEL_ORPHANS_FOUND',
51
+ DND_BYPASS_UNAVAILABLE: 'DND_BYPASS_UNAVAILABLE',
52
+ TONE_LABELS_REFRESHED: 'TONE_LABELS_REFRESHED',
53
+ REPEAT_ALERT_UNAVAILABLE: 'REPEAT_ALERT_UNAVAILABLE',
54
+ REPEAT_STOP_HANDLER_MISSING: 'REPEAT_STOP_HANDLER_MISSING',
55
+
56
+ // --- Interaction ---------------------------------------------------------
57
+ TARGET_MISSING: 'TARGET_MISSING',
58
+ INTERACTION_BUFFER_EVICTED: 'INTERACTION_BUFFER_EVICTED',
59
+ BACKGROUND_WINDOW_EXCEEDED: 'BACKGROUND_WINDOW_EXCEEDED',
60
+
61
+ // --- Push ----------------------------------------------------------------
62
+ PUSH_UNAVAILABLE: 'PUSH_UNAVAILABLE',
63
+ TOKEN_UNAVAILABLE: 'TOKEN_UNAVAILABLE',
64
+ TOKEN_INVALIDATION_FAILED: 'TOKEN_INVALIDATION_FAILED',
65
+ PAYLOAD_MALFORMED: 'PAYLOAD_MALFORMED',
66
+ PAYLOAD_VERSION_SKEW: 'PAYLOAD_VERSION_SKEW',
67
+ PAYLOAD_DUPLICATE: 'PAYLOAD_DUPLICATE',
68
+ PUSH_UNDELIVERABLE: 'PUSH_UNDELIVERABLE',
69
+
70
+ // --- Persisted state -----------------------------------------------------
71
+ STATE_CORRUPT_REBUILT: 'STATE_CORRUPT_REBUILT',
72
+ STATE_WRITE_FAILED: 'STATE_WRITE_FAILED',
73
+ /** The durable store could not open; state lasts for this session only. */
74
+ STATE_STORE_DEGRADED: 'STATE_STORE_DEGRADED',
75
+ STATE_CAP_EVICTED: 'STATE_CAP_EVICTED',
76
+
77
+ // --- Volume --------------------------------------------------------------
78
+ BURST_THRESHOLD_EXCEEDED: 'BURST_THRESHOLD_EXCEEDED',
79
+
80
+ // --- Maintenance (0.4.0) -------------------------------------------------
81
+ /** An automatic maintenance cycle threw. Reported here because no caller awaits it. */
82
+ MAINTENANCE_FAILED: 'MAINTENANCE_FAILED',
83
+ } as const;
84
+
85
+ export type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[keyof typeof DIAGNOSTIC_CODES];
86
+
87
+ /** Every identifier, for exhaustiveness tests and the documentation table. */
88
+ export const ALL_DIAGNOSTIC_CODES: readonly DiagnosticCode[] = Object.freeze(
89
+ Object.values(DIAGNOSTIC_CODES),
90
+ );
91
+
92
+ /**
93
+ * A typed package failure.
94
+ *
95
+ * Carries enough context to choose a recovery, and never carries notification
96
+ * body text, routing payload values, or token material. Where a value violated
97
+ * a rule, the *shape* of the violation is reported — a length, a character
98
+ * position — rather than the value itself.
99
+ */
100
+ export class NotificationError extends SdkError<DiagnosticCode> {
101
+ readonly context?: Readonly<Record<string, unknown>>;
102
+
103
+ constructor(code: DiagnosticCode, message: string, context?: Readonly<Record<string, unknown>>) {
104
+ super('rn-notification', code, { message });
105
+ this.name = 'NotificationError';
106
+ // Redacted on the way in: an error object reaches crash reporters, which
107
+ // never run with verbose diagnostics.
108
+ const redacted = redactContext(context, false);
109
+ if (redacted !== undefined) {
110
+ this.context = redacted;
111
+ }
112
+ }
113
+ }
114
+
115
+ export function isNotificationError(value: unknown): value is NotificationError {
116
+ return value instanceof NotificationError;
117
+ }
118
+
119
+ /**
120
+ * Conditions that are reported outcomes rather than failures: the operation
121
+ * succeeded, and something the caller needs to know happened alongside it.
122
+ */
123
+ export const NON_FAILURE_CODES: readonly DiagnosticCode[] = Object.freeze([
124
+ DIAGNOSTIC_CODES.TRUNCATED_OVER_CEILING,
125
+ DIAGNOSTIC_CODES.PENDING_SET_DISCREPANCY,
126
+ DIAGNOSTIC_CODES.EXACT_ALARM_UNAVAILABLE,
127
+ DIAGNOSTIC_CODES.TARGET_MISSING,
128
+ DIAGNOSTIC_CODES.BURST_THRESHOLD_EXCEEDED,
129
+ DIAGNOSTIC_CODES.PAYLOAD_VERSION_SKEW,
130
+ DIAGNOSTIC_CODES.PAYLOAD_DUPLICATE,
131
+ DIAGNOSTIC_CODES.STATE_CAP_EVICTED,
132
+ DIAGNOSTIC_CODES.STATE_CORRUPT_REBUILT,
133
+ DIAGNOSTIC_CODES.BACKGROUND_RESTRICTION_UNKNOWN,
134
+ DIAGNOSTIC_CODES.DND_BYPASS_UNAVAILABLE,
135
+ DIAGNOSTIC_CODES.TONE_LABELS_REFRESHED,
136
+ DIAGNOSTIC_CODES.REPEAT_ALERT_UNAVAILABLE,
137
+ DIAGNOSTIC_CODES.REPEAT_STOP_HANDLER_MISSING,
138
+ ]);
139
+
140
+ export function isNonFailure(code: DiagnosticCode): boolean {
141
+ return NON_FAILURE_CODES.includes(code);
142
+ }
@@ -1,10 +1,10 @@
1
- /** Public contract types. Types only — no logic lives in this layer. */
2
-
3
- export * from './notification';
4
- export * from './recurrence';
5
- export * from './reconcile';
6
- export * from './permission';
7
- export * from './channel';
8
- export * from './interaction';
9
- export * from './diagnostics';
10
- export * from './errors';
1
+ /** Public contract types. Types only — no logic lives in this layer. */
2
+
3
+ export * from './notification';
4
+ export * from './recurrence';
5
+ export * from './reconcile';
6
+ export * from './permission';
7
+ export * from './channel';
8
+ export * from './interaction';
9
+ export * from './diagnostics';
10
+ export * from './errors';
@@ -1,62 +1,62 @@
1
- /**
2
- * Interaction types.
3
- *
4
- * One event shape covers foreground, background, and cold start, so a consumer
5
- * writes one handler. Events that occur before a handler registers are buffered
6
- * durably and delivered on registration — the cold-start case is the one most
7
- * integrations quietly get wrong.
8
- */
9
-
10
- import type { RoutingPayload } from './notification';
11
-
12
- export type InteractionKind = 'tap' | 'action' | 'dismiss';
13
-
14
- /** Which app state the interaction arrived in. Informational; the shape never varies. */
15
- export type AppStateAtInteraction = 'foreground' | 'background' | 'coldStart';
16
-
17
- export interface InteractionEvent {
18
- readonly kind: InteractionKind;
19
- readonly notificationId: string;
20
- /** Present when `kind` is `action`. */
21
- readonly actionId?: string;
22
- /** Present when the action accepted typed input. */
23
- readonly inputText?: string;
24
- /** Round-trips unchanged from schedule time. */
25
- readonly routing?: RoutingPayload;
26
- readonly ownerTag?: string;
27
- readonly occurredAt: number;
28
- readonly appState: AppStateAtInteraction;
29
- /**
30
- * True when the event was held in the durable buffer before a handler
31
- * existed to receive it — including across a process death.
32
- */
33
- readonly wasBuffered: boolean;
34
- }
35
-
36
- /**
37
- * Signals the routing target no longer exists.
38
- *
39
- * The consumer chooses its own fallback screen. A platform error is never
40
- * shown to the user for this — a notification pointing at deleted data is an
41
- * ordinary condition, not a crash.
42
- */
43
- export interface TargetMissingSignal {
44
- readonly notificationId: string;
45
- readonly routing?: RoutingPayload;
46
- }
47
-
48
- export type InteractionHandler = (event: InteractionEvent) => void | Promise<void>;
49
-
50
- export type Unsubscribe = () => void;
51
-
52
- /** Limits on the durable interaction buffer, so it can never grow without bound. */
53
- export const INTERACTION_BUFFER_LIMITS = {
54
- maxEntries: 50,
55
- retentionMs: 7 * 24 * 60 * 60 * 1000,
56
- } as const;
57
-
58
- /** Maximum size of a routing payload, validated at schedule time rather than discovered at tap time. */
59
- export const MAX_ROUTING_PAYLOAD_BYTES = 4 * 1024;
60
-
61
- /** Maximum action buttons on one notification. */
62
- export const MAX_ACTIONS_PER_NOTIFICATION = 10;
1
+ /**
2
+ * Interaction types.
3
+ *
4
+ * One event shape covers foreground, background, and cold start, so a consumer
5
+ * writes one handler. Events that occur before a handler registers are buffered
6
+ * durably and delivered on registration — the cold-start case is the one most
7
+ * integrations quietly get wrong.
8
+ */
9
+
10
+ import type { RoutingPayload } from './notification';
11
+
12
+ export type InteractionKind = 'tap' | 'action' | 'dismiss';
13
+
14
+ /** Which app state the interaction arrived in. Informational; the shape never varies. */
15
+ export type AppStateAtInteraction = 'foreground' | 'background' | 'coldStart';
16
+
17
+ export interface InteractionEvent {
18
+ readonly kind: InteractionKind;
19
+ readonly notificationId: string;
20
+ /** Present when `kind` is `action`. */
21
+ readonly actionId?: string;
22
+ /** Present when the action accepted typed input. */
23
+ readonly inputText?: string;
24
+ /** Round-trips unchanged from schedule time. */
25
+ readonly routing?: RoutingPayload;
26
+ readonly ownerTag?: string;
27
+ readonly occurredAt: number;
28
+ readonly appState: AppStateAtInteraction;
29
+ /**
30
+ * True when the event was held in the durable buffer before a handler
31
+ * existed to receive it — including across a process death.
32
+ */
33
+ readonly wasBuffered: boolean;
34
+ }
35
+
36
+ /**
37
+ * Signals the routing target no longer exists.
38
+ *
39
+ * The consumer chooses its own fallback screen. A platform error is never
40
+ * shown to the user for this — a notification pointing at deleted data is an
41
+ * ordinary condition, not a crash.
42
+ */
43
+ export interface TargetMissingSignal {
44
+ readonly notificationId: string;
45
+ readonly routing?: RoutingPayload;
46
+ }
47
+
48
+ export type InteractionHandler = (event: InteractionEvent) => void | Promise<void>;
49
+
50
+ export type Unsubscribe = () => void;
51
+
52
+ /** Limits on the durable interaction buffer, so it can never grow without bound. */
53
+ export const INTERACTION_BUFFER_LIMITS = {
54
+ maxEntries: 50,
55
+ retentionMs: 7 * 24 * 60 * 60 * 1000,
56
+ } as const;
57
+
58
+ /** Maximum size of a routing payload, validated at schedule time rather than discovered at tap time. */
59
+ export const MAX_ROUTING_PAYLOAD_BYTES = 4 * 1024;
60
+
61
+ /** Maximum action buttons on one notification. */
62
+ export const MAX_ACTIONS_PER_NOTIFICATION = 10;