@chipmobilesdk/rn-notification 0.1.2 → 0.2.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 (69) hide show
  1. package/CHANGELOG.md +80 -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 +71 -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 +336 -333
@@ -1,123 +1,130 @@
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
-
28
- // --- Reconciliation ------------------------------------------------------
29
- DUPLICATE_IDENTIFIER: 'DUPLICATE_IDENTIFIER',
30
- TRUNCATED_OVER_CEILING: 'TRUNCATED_OVER_CEILING',
31
- RECONCILE_INTERRUPTED: 'RECONCILE_INTERRUPTED',
32
- PENDING_SET_DISCREPANCY: 'PENDING_SET_DISCREPANCY',
33
-
34
- // --- Permissions ---------------------------------------------------------
35
- DISPLAY_PERMISSION_DENIED: 'DISPLAY_PERMISSION_DENIED',
36
- DISPLAY_PERMISSION_PERMANENT: 'DISPLAY_PERMISSION_PERMANENT',
37
- EXACT_ALARM_UNAVAILABLE: 'EXACT_ALARM_UNAVAILABLE',
38
- SETTINGS_UNAVAILABLE: 'SETTINGS_UNAVAILABLE',
39
- BACKGROUND_RESTRICTED: 'BACKGROUND_RESTRICTED',
40
- BACKGROUND_RESTRICTION_UNKNOWN: 'BACKGROUND_RESTRICTION_UNKNOWN',
41
-
42
- // --- Channels ------------------------------------------------------------
43
- CHANNEL_MIGRATION_REQUIRED: 'CHANNEL_MIGRATION_REQUIRED',
44
- CHANNEL_MIGRATION_FAILED: 'CHANNEL_MIGRATION_FAILED',
45
- CHANNEL_ORPHANS_FOUND: 'CHANNEL_ORPHANS_FOUND',
46
- DND_BYPASS_UNAVAILABLE: 'DND_BYPASS_UNAVAILABLE',
47
-
48
- // --- Interaction ---------------------------------------------------------
49
- TARGET_MISSING: 'TARGET_MISSING',
50
- INTERACTION_BUFFER_EVICTED: 'INTERACTION_BUFFER_EVICTED',
51
- BACKGROUND_WINDOW_EXCEEDED: 'BACKGROUND_WINDOW_EXCEEDED',
52
-
53
- // --- Push ----------------------------------------------------------------
54
- PUSH_UNAVAILABLE: 'PUSH_UNAVAILABLE',
55
- TOKEN_UNAVAILABLE: 'TOKEN_UNAVAILABLE',
56
- TOKEN_INVALIDATION_FAILED: 'TOKEN_INVALIDATION_FAILED',
57
- PAYLOAD_MALFORMED: 'PAYLOAD_MALFORMED',
58
- PAYLOAD_VERSION_SKEW: 'PAYLOAD_VERSION_SKEW',
59
- PAYLOAD_DUPLICATE: 'PAYLOAD_DUPLICATE',
60
- PUSH_UNDELIVERABLE: 'PUSH_UNDELIVERABLE',
61
-
62
- // --- Persisted state -----------------------------------------------------
63
- STATE_CORRUPT_REBUILT: 'STATE_CORRUPT_REBUILT',
64
- STATE_WRITE_FAILED: 'STATE_WRITE_FAILED',
65
- STATE_CAP_EVICTED: 'STATE_CAP_EVICTED',
66
-
67
- // --- Volume --------------------------------------------------------------
68
- BURST_THRESHOLD_EXCEEDED: 'BURST_THRESHOLD_EXCEEDED',
69
- } as const;
70
-
71
- export type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[keyof typeof DIAGNOSTIC_CODES];
72
-
73
- /** Every identifier, for exhaustiveness tests and the documentation table. */
74
- export const ALL_DIAGNOSTIC_CODES: readonly DiagnosticCode[] = Object.freeze(
75
- Object.values(DIAGNOSTIC_CODES),
76
- );
77
-
78
- /**
79
- * A typed package failure.
80
- *
81
- * Carries enough context to choose a recovery, and never carries notification
82
- * body text, routing payload values, or token material. Where a value violated
83
- * a rule, the *shape* of the violation is reported — a length, a character
84
- * position — rather than the value itself.
85
- */
86
- export class NotificationError extends Error {
87
- readonly code: DiagnosticCode;
88
- readonly context?: Readonly<Record<string, unknown>>;
89
-
90
- constructor(code: DiagnosticCode, message: string, context?: Readonly<Record<string, unknown>>) {
91
- super(message);
92
- this.name = 'NotificationError';
93
- this.code = code;
94
- this.context = context;
95
- Object.setPrototypeOf(this, NotificationError.prototype);
96
- }
97
- }
98
-
99
- export function isNotificationError(value: unknown): value is NotificationError {
100
- return value instanceof NotificationError;
101
- }
102
-
103
- /**
104
- * Conditions that are reported outcomes rather than failures: the operation
105
- * succeeded, and something the caller needs to know happened alongside it.
106
- */
107
- export const NON_FAILURE_CODES: readonly DiagnosticCode[] = Object.freeze([
108
- DIAGNOSTIC_CODES.TRUNCATED_OVER_CEILING,
109
- DIAGNOSTIC_CODES.PENDING_SET_DISCREPANCY,
110
- DIAGNOSTIC_CODES.EXACT_ALARM_UNAVAILABLE,
111
- DIAGNOSTIC_CODES.TARGET_MISSING,
112
- DIAGNOSTIC_CODES.BURST_THRESHOLD_EXCEEDED,
113
- DIAGNOSTIC_CODES.PAYLOAD_VERSION_SKEW,
114
- DIAGNOSTIC_CODES.PAYLOAD_DUPLICATE,
115
- DIAGNOSTIC_CODES.STATE_CAP_EVICTED,
116
- DIAGNOSTIC_CODES.STATE_CORRUPT_REBUILT,
117
- DIAGNOSTIC_CODES.BACKGROUND_RESTRICTION_UNKNOWN,
118
- DIAGNOSTIC_CODES.DND_BYPASS_UNAVAILABLE,
119
- ]);
120
-
121
- export function isNonFailure(code: DiagnosticCode): boolean {
122
- return NON_FAILURE_CODES.includes(code);
123
- }
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
+ } as const;
74
+
75
+ export type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[keyof typeof DIAGNOSTIC_CODES];
76
+
77
+ /** Every identifier, for exhaustiveness tests and the documentation table. */
78
+ export const ALL_DIAGNOSTIC_CODES: readonly DiagnosticCode[] = Object.freeze(
79
+ Object.values(DIAGNOSTIC_CODES),
80
+ );
81
+
82
+ /**
83
+ * A typed package failure.
84
+ *
85
+ * Carries enough context to choose a recovery, and never carries notification
86
+ * body text, routing payload values, or token material. Where a value violated
87
+ * a rule, the *shape* of the violation is reported — a length, a character
88
+ * position — rather than the value itself.
89
+ */
90
+ export class NotificationError extends Error {
91
+ readonly code: DiagnosticCode;
92
+ readonly context?: Readonly<Record<string, unknown>>;
93
+
94
+ constructor(code: DiagnosticCode, message: string, context?: Readonly<Record<string, unknown>>) {
95
+ super(message);
96
+ this.name = 'NotificationError';
97
+ this.code = code;
98
+ this.context = context;
99
+ Object.setPrototypeOf(this, NotificationError.prototype);
100
+ }
101
+ }
102
+
103
+ export function isNotificationError(value: unknown): value is NotificationError {
104
+ return value instanceof NotificationError;
105
+ }
106
+
107
+ /**
108
+ * Conditions that are reported outcomes rather than failures: the operation
109
+ * succeeded, and something the caller needs to know happened alongside it.
110
+ */
111
+ export const NON_FAILURE_CODES: readonly DiagnosticCode[] = Object.freeze([
112
+ DIAGNOSTIC_CODES.TRUNCATED_OVER_CEILING,
113
+ DIAGNOSTIC_CODES.PENDING_SET_DISCREPANCY,
114
+ DIAGNOSTIC_CODES.EXACT_ALARM_UNAVAILABLE,
115
+ DIAGNOSTIC_CODES.TARGET_MISSING,
116
+ DIAGNOSTIC_CODES.BURST_THRESHOLD_EXCEEDED,
117
+ DIAGNOSTIC_CODES.PAYLOAD_VERSION_SKEW,
118
+ DIAGNOSTIC_CODES.PAYLOAD_DUPLICATE,
119
+ DIAGNOSTIC_CODES.STATE_CAP_EVICTED,
120
+ DIAGNOSTIC_CODES.STATE_CORRUPT_REBUILT,
121
+ DIAGNOSTIC_CODES.BACKGROUND_RESTRICTION_UNKNOWN,
122
+ DIAGNOSTIC_CODES.DND_BYPASS_UNAVAILABLE,
123
+ DIAGNOSTIC_CODES.TONE_LABELS_REFRESHED,
124
+ DIAGNOSTIC_CODES.REPEAT_ALERT_UNAVAILABLE,
125
+ DIAGNOSTIC_CODES.REPEAT_STOP_HANDLER_MISSING,
126
+ ]);
127
+
128
+ export function isNonFailure(code: DiagnosticCode): boolean {
129
+ return NON_FAILURE_CODES.includes(code);
130
+ }
@@ -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;
@@ -1,118 +1,118 @@
1
- /**
2
- * Core notification request types.
3
- *
4
- * Nothing here carries business meaning. The package accepts fully-described
5
- * requests and is responsible only for delivering them at the right moment.
6
- */
7
-
8
- /**
9
- * How a delivery moment is pinned to the passage of time.
10
- *
11
- * - `wallClock` — the local clock time named. An 08:00 reminder stays 08:00
12
- * after the user crosses a time zone. This is the default, because reminders
13
- * are wall-clock things and a caller who does not choose should get what
14
- * their users expect.
15
- * - `absolute` — one fixed instant worldwide, unaffected by zone changes.
16
- */
17
- export type AnchoringMode = 'wallClock' | 'absolute';
18
-
19
- export const DEFAULT_ANCHORING_MODE: AnchoringMode = 'wallClock';
20
-
21
- /** When a notification should be presented, and how that moment is anchored. */
22
- export interface DeliveryMoment {
23
- /** Epoch milliseconds. Interpreted per {@link DeliveryMoment.anchoring}. */
24
- readonly at: number;
25
- /** Defaults to `wallClock` when omitted. */
26
- readonly anchoring?: AnchoringMode;
27
- }
28
-
29
- /**
30
- * Resolved display strings. The package performs no translation, formatting,
31
- * or locale resolution — only the consuming app knows its content model.
32
- */
33
- export interface NotificationContent {
34
- readonly title: string;
35
- readonly body: string;
36
- /** Optional short text shown in collapsed form on some platforms. */
37
- readonly subtitle?: string;
38
- }
39
-
40
- /** Structured data carried to the interaction handler, unchanged. */
41
- export type RoutingPayload = Readonly<Record<string, unknown>>;
42
-
43
- /** A button on a notification. */
44
- export interface Action {
45
- /** Stable action identifier, subject to the same charset rules as a notification id. */
46
- readonly id: string;
47
- readonly title: string;
48
- /**
49
- * `foreground` brings the app up; `background` is handled without opening it,
50
- * inside the platform's bounded execution window.
51
- */
52
- readonly mode: 'foreground' | 'background';
53
- /** When present, the action accepts short typed input. */
54
- readonly input?: {
55
- readonly placeholder?: string;
56
- readonly submitLabel?: string;
57
- };
58
- }
59
-
60
- /** The complete, self-contained description of one notification. */
61
- export interface NotificationRequest {
62
- /**
63
- * Caller-supplied, stable, derived from the caller's own data.
64
- * The package never generates, normalizes, or hashes it.
65
- */
66
- readonly id: string;
67
- readonly content: NotificationContent;
68
- /** Must reference a tone declared in the package configuration. */
69
- readonly tone: string;
70
- readonly moment: DeliveryMoment;
71
- readonly routing?: RoutingPayload;
72
- readonly actions?: readonly Action[];
73
- /** Opaque partition label. Never interpreted by the package. */
74
- readonly ownerTag?: string;
75
- /** Opaque cancellation group. Never interpreted by the package. */
76
- readonly groupTag?: string;
77
- }
78
-
79
- /** One entry the platform is currently holding, in enough detail to diff against intent. */
80
- export interface PendingEntry {
81
- readonly id: string;
82
- /** The instant the platform will actually fire, after anchoring resolution. */
83
- readonly resolvedAt: number;
84
- readonly anchoring: AnchoringMode;
85
- /**
86
- * The IANA zone this entry was last anchored against.
87
- * `undefined` for absolute-instant entries, which are never anchored.
88
- *
89
- * Exists so the one case re-anchoring cannot fix — a zone change while the
90
- * app never ran — is detectable rather than invisible.
91
- */
92
- readonly anchoringGeneration?: string;
93
- readonly content: NotificationContent;
94
- readonly tone: string;
95
- readonly routing?: RoutingPayload;
96
- readonly actions?: readonly Action[];
97
- readonly ownerTag?: string;
98
- readonly groupTag?: string;
99
- /** Present when this entry was expanded from a recurrence series. */
100
- readonly seriesId?: string;
101
- /** True when scheduled without exact-alarm capability; delivery may be late. */
102
- readonly mayBeDelayed: boolean;
103
- /** True when created by a push instruction rather than by app code. */
104
- readonly pushCreated: boolean;
105
- }
106
-
107
- /** Outcome of a single schedule call. */
108
- export interface ScheduleResult {
109
- readonly id: string;
110
- readonly resolvedAt: number;
111
- readonly anchoring: AnchoringMode;
112
- /** True when an existing notification under this identifier was replaced. */
113
- readonly replaced: boolean;
114
- /** True when the moment had just passed and was delivered inside the tolerance window. */
115
- readonly deliveredImmediately: boolean;
116
- /** True when scheduled in approximate mode for want of exact-alarm capability. */
117
- readonly mayBeDelayed: boolean;
118
- }
1
+ /**
2
+ * Core notification request types.
3
+ *
4
+ * Nothing here carries business meaning. The package accepts fully-described
5
+ * requests and is responsible only for delivering them at the right moment.
6
+ */
7
+
8
+ /**
9
+ * How a delivery moment is pinned to the passage of time.
10
+ *
11
+ * - `wallClock` — the local clock time named. An 08:00 reminder stays 08:00
12
+ * after the user crosses a time zone. This is the default, because reminders
13
+ * are wall-clock things and a caller who does not choose should get what
14
+ * their users expect.
15
+ * - `absolute` — one fixed instant worldwide, unaffected by zone changes.
16
+ */
17
+ export type AnchoringMode = 'wallClock' | 'absolute';
18
+
19
+ export const DEFAULT_ANCHORING_MODE: AnchoringMode = 'wallClock';
20
+
21
+ /** When a notification should be presented, and how that moment is anchored. */
22
+ export interface DeliveryMoment {
23
+ /** Epoch milliseconds. Interpreted per {@link DeliveryMoment.anchoring}. */
24
+ readonly at: number;
25
+ /** Defaults to `wallClock` when omitted. */
26
+ readonly anchoring?: AnchoringMode;
27
+ }
28
+
29
+ /**
30
+ * Resolved display strings. The package performs no translation, formatting,
31
+ * or locale resolution — only the consuming app knows its content model.
32
+ */
33
+ export interface NotificationContent {
34
+ readonly title: string;
35
+ readonly body: string;
36
+ /** Optional short text shown in collapsed form on some platforms. */
37
+ readonly subtitle?: string;
38
+ }
39
+
40
+ /** Structured data carried to the interaction handler, unchanged. */
41
+ export type RoutingPayload = Readonly<Record<string, unknown>>;
42
+
43
+ /** A button on a notification. */
44
+ export interface Action {
45
+ /** Stable action identifier, subject to the same charset rules as a notification id. */
46
+ readonly id: string;
47
+ readonly title: string;
48
+ /**
49
+ * `foreground` brings the app up; `background` is handled without opening it,
50
+ * inside the platform's bounded execution window.
51
+ */
52
+ readonly mode: 'foreground' | 'background';
53
+ /** When present, the action accepts short typed input. */
54
+ readonly input?: {
55
+ readonly placeholder?: string;
56
+ readonly submitLabel?: string;
57
+ };
58
+ }
59
+
60
+ /** The complete, self-contained description of one notification. */
61
+ export interface NotificationRequest {
62
+ /**
63
+ * Caller-supplied, stable, derived from the caller's own data.
64
+ * The package never generates, normalizes, or hashes it.
65
+ */
66
+ readonly id: string;
67
+ readonly content: NotificationContent;
68
+ /** Must reference a tone declared in the package configuration. */
69
+ readonly tone: string;
70
+ readonly moment: DeliveryMoment;
71
+ readonly routing?: RoutingPayload;
72
+ readonly actions?: readonly Action[];
73
+ /** Opaque partition label. Never interpreted by the package. */
74
+ readonly ownerTag?: string;
75
+ /** Opaque cancellation group. Never interpreted by the package. */
76
+ readonly groupTag?: string;
77
+ }
78
+
79
+ /** One entry the platform is currently holding, in enough detail to diff against intent. */
80
+ export interface PendingEntry {
81
+ readonly id: string;
82
+ /** The instant the platform will actually fire, after anchoring resolution. */
83
+ readonly resolvedAt: number;
84
+ readonly anchoring: AnchoringMode;
85
+ /**
86
+ * The IANA zone this entry was last anchored against.
87
+ * `undefined` for absolute-instant entries, which are never anchored.
88
+ *
89
+ * Exists so the one case re-anchoring cannot fix — a zone change while the
90
+ * app never ran — is detectable rather than invisible.
91
+ */
92
+ readonly anchoringGeneration?: string;
93
+ readonly content: NotificationContent;
94
+ readonly tone: string;
95
+ readonly routing?: RoutingPayload;
96
+ readonly actions?: readonly Action[];
97
+ readonly ownerTag?: string;
98
+ readonly groupTag?: string;
99
+ /** Present when this entry was expanded from a recurrence series. */
100
+ readonly seriesId?: string;
101
+ /** True when scheduled without exact-alarm capability; delivery may be late. */
102
+ readonly mayBeDelayed: boolean;
103
+ /** True when created by a push instruction rather than by app code. */
104
+ readonly pushCreated: boolean;
105
+ }
106
+
107
+ /** Outcome of a single schedule call. */
108
+ export interface ScheduleResult {
109
+ readonly id: string;
110
+ readonly resolvedAt: number;
111
+ readonly anchoring: AnchoringMode;
112
+ /** True when an existing notification under this identifier was replaced. */
113
+ readonly replaced: boolean;
114
+ /** True when the moment had just passed and was delivered inside the tolerance window. */
115
+ readonly deliveredImmediately: boolean;
116
+ /** True when scheduled in approximate mode for want of exact-alarm capability. */
117
+ readonly mayBeDelayed: boolean;
118
+ }