@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,129 +1,129 @@
1
- /**
2
- * In-memory push provider for tests.
3
- *
4
- * Exists so the entire push surface — token lifecycle, receipt in every app
5
- * state, malformed payloads, redelivery, and provider unavailability — is
6
- * provable with no live provider and no device.
7
- *
8
- * It is also the second adapter that proves the swap costs a consumer nothing:
9
- * running the full push suite against this instead of the real provider must
10
- * pass with zero changes to consuming code.
11
- */
12
-
13
- import type {
14
- PushAvailability,
15
- PushProviderAdapter,
16
- } from '../push/provider-adapter';
17
- import type { Unsubscribe } from '../contract/interaction';
18
-
19
- export interface FakeProviderOptions {
20
- readonly name?: string;
21
- readonly availability?: PushAvailability;
22
- readonly initialToken?: string;
23
- /** When true, `getToken` resolves only after `releaseToken()` is called. */
24
- readonly withholdToken?: boolean;
25
- readonly invalidationSucceeds?: boolean;
26
- }
27
-
28
- export interface FakeProviderControls {
29
- /** Delivers a raw payload as the provider would. */
30
- deliver(raw: unknown): void;
31
- /** Queues the payload that launched the app. */
32
- queueInitial(raw: unknown): void;
33
- /** Simulates the platform rotating the registration token. */
34
- rotateToken(token: string): void;
35
- /** Resolves a withheld token, so "waits for availability" is testable. */
36
- releaseToken(token: string): void;
37
- setAvailability(availability: PushAvailability): void;
38
- readonly invalidated: boolean;
39
- }
40
-
41
- export type FakeProvider = PushProviderAdapter & FakeProviderControls;
42
-
43
- export function createFakeProvider(options: FakeProviderOptions = {}): FakeProvider {
44
- let availability: PushAvailability = options.availability ?? 'available';
45
- let token = options.withholdToken === true ? undefined : (options.initialToken ?? 'fake-token-1');
46
- let invalidated = false;
47
- let messageHandlers: ((raw: unknown) => void)[] = [];
48
- let tokenHandlers: ((token: string) => void)[] = [];
49
- let initial: unknown | undefined;
50
- let waiters: ((token: string) => void)[] = [];
51
-
52
- return {
53
- name: options.name ?? 'fake-provider',
54
-
55
- async getAvailability() {
56
- return availability;
57
- },
58
-
59
- async getToken() {
60
- if (token !== undefined) return token;
61
- // Waits rather than returning a placeholder — an empty string presented
62
- // as a token is worse than not answering yet.
63
- return new Promise<string>(resolve => {
64
- waiters.push(resolve);
65
- });
66
- },
67
-
68
- onTokenChange(handler): Unsubscribe {
69
- tokenHandlers.push(handler);
70
- return () => {
71
- tokenHandlers = tokenHandlers.filter(h => h !== handler);
72
- };
73
- },
74
-
75
- async invalidateToken() {
76
- invalidated = true;
77
- token = undefined;
78
- return options.invalidationSucceeds ?? true;
79
- },
80
-
81
- subscribeToMessages(handler): Unsubscribe {
82
- messageHandlers.push(handler);
83
- return () => {
84
- messageHandlers = messageHandlers.filter(h => h !== handler);
85
- };
86
- },
87
-
88
- async takeInitialMessage() {
89
- const raw = initial;
90
- initial = undefined;
91
- return raw;
92
- },
93
-
94
- // --- FakeProviderControls ---------------------------------------------
95
-
96
- deliver(raw) {
97
- for (const handler of messageHandlers) {
98
- handler(raw);
99
- }
100
- },
101
-
102
- queueInitial(raw) {
103
- initial = raw;
104
- },
105
-
106
- rotateToken(next) {
107
- token = next;
108
- for (const handler of tokenHandlers) {
109
- handler(next);
110
- }
111
- },
112
-
113
- releaseToken(next) {
114
- token = next;
115
- for (const resolve of waiters) {
116
- resolve(next);
117
- }
118
- waiters = [];
119
- },
120
-
121
- setAvailability(next) {
122
- availability = next;
123
- },
124
-
125
- get invalidated() {
126
- return invalidated;
127
- },
128
- };
129
- }
1
+ /**
2
+ * In-memory push provider for tests.
3
+ *
4
+ * Exists so the entire push surface — token lifecycle, receipt in every app
5
+ * state, malformed payloads, redelivery, and provider unavailability — is
6
+ * provable with no live provider and no device.
7
+ *
8
+ * It is also the second adapter that proves the swap costs a consumer nothing:
9
+ * running the full push suite against this instead of the real provider must
10
+ * pass with zero changes to consuming code.
11
+ */
12
+
13
+ import type {
14
+ PushAvailability,
15
+ PushProviderAdapter,
16
+ } from '../push/provider-adapter';
17
+ import type { Unsubscribe } from '../contract/interaction';
18
+
19
+ export interface FakeProviderOptions {
20
+ readonly name?: string;
21
+ readonly availability?: PushAvailability;
22
+ readonly initialToken?: string;
23
+ /** When true, `getToken` resolves only after `releaseToken()` is called. */
24
+ readonly withholdToken?: boolean;
25
+ readonly invalidationSucceeds?: boolean;
26
+ }
27
+
28
+ export interface FakeProviderControls {
29
+ /** Delivers a raw payload as the provider would. */
30
+ deliver(raw: unknown): void;
31
+ /** Queues the payload that launched the app. */
32
+ queueInitial(raw: unknown): void;
33
+ /** Simulates the platform rotating the registration token. */
34
+ rotateToken(token: string): void;
35
+ /** Resolves a withheld token, so "waits for availability" is testable. */
36
+ releaseToken(token: string): void;
37
+ setAvailability(availability: PushAvailability): void;
38
+ readonly invalidated: boolean;
39
+ }
40
+
41
+ export type FakeProvider = PushProviderAdapter & FakeProviderControls;
42
+
43
+ export function createFakeProvider(options: FakeProviderOptions = {}): FakeProvider {
44
+ let availability: PushAvailability = options.availability ?? 'available';
45
+ let token = options.withholdToken === true ? undefined : (options.initialToken ?? 'fake-token-1');
46
+ let invalidated = false;
47
+ let messageHandlers: ((raw: unknown) => void)[] = [];
48
+ let tokenHandlers: ((token: string) => void)[] = [];
49
+ let initial: unknown | undefined;
50
+ let waiters: ((token: string) => void)[] = [];
51
+
52
+ return {
53
+ name: options.name ?? 'fake-provider',
54
+
55
+ async getAvailability() {
56
+ return availability;
57
+ },
58
+
59
+ async getToken() {
60
+ if (token !== undefined) return token;
61
+ // Waits rather than returning a placeholder — an empty string presented
62
+ // as a token is worse than not answering yet.
63
+ return new Promise<string>(resolve => {
64
+ waiters.push(resolve);
65
+ });
66
+ },
67
+
68
+ onTokenChange(handler): Unsubscribe {
69
+ tokenHandlers.push(handler);
70
+ return () => {
71
+ tokenHandlers = tokenHandlers.filter(h => h !== handler);
72
+ };
73
+ },
74
+
75
+ async invalidateToken() {
76
+ invalidated = true;
77
+ token = undefined;
78
+ return options.invalidationSucceeds ?? true;
79
+ },
80
+
81
+ subscribeToMessages(handler): Unsubscribe {
82
+ messageHandlers.push(handler);
83
+ return () => {
84
+ messageHandlers = messageHandlers.filter(h => h !== handler);
85
+ };
86
+ },
87
+
88
+ async takeInitialMessage() {
89
+ const raw = initial;
90
+ initial = undefined;
91
+ return raw;
92
+ },
93
+
94
+ // --- FakeProviderControls ---------------------------------------------
95
+
96
+ deliver(raw) {
97
+ for (const handler of messageHandlers) {
98
+ handler(raw);
99
+ }
100
+ },
101
+
102
+ queueInitial(raw) {
103
+ initial = raw;
104
+ },
105
+
106
+ rotateToken(next) {
107
+ token = next;
108
+ for (const handler of tokenHandlers) {
109
+ handler(next);
110
+ }
111
+ },
112
+
113
+ releaseToken(next) {
114
+ token = next;
115
+ for (const resolve of waiters) {
116
+ resolve(next);
117
+ }
118
+ waiters = [];
119
+ },
120
+
121
+ setAvailability(next) {
122
+ availability = next;
123
+ },
124
+
125
+ get invalidated() {
126
+ return invalidated;
127
+ },
128
+ };
129
+ }
@@ -1,72 +1,72 @@
1
- /**
2
- * Fault injection.
3
- *
4
- * Every documented failure condition must be reachable on demand, or the claim
5
- * that each has its own distinct diagnostic is untestable. This is what the
6
- * demo app's fault panel and the conformance suite both drive.
7
- */
8
-
9
- import { ALL_DIAGNOSTIC_CODES, type DiagnosticCode } from '../contract/errors';
10
-
11
- export type FaultKind = DiagnosticCode;
12
-
13
- export interface FaultRecord {
14
- readonly code: DiagnosticCode;
15
- readonly at: number;
16
- }
17
-
18
- export interface Disposable {
19
- dispose(): void;
20
- }
21
-
22
- export interface FaultInjectorOptions {
23
- readonly now?: () => number;
24
- readonly emit?: (code: string, message: string, context?: Record<string, unknown>) => void;
25
- }
26
-
27
- /**
28
- * Triggers a named condition and records that it fired.
29
- *
30
- * Deliberately a thin mechanism: it proves each identifier is reachable and
31
- * distinct, which is the property under test. Making the *real* condition occur
32
- * is each module's own test's job.
33
- */
34
- export function createFaultInjector(options: FaultInjectorOptions = {}) {
35
- const now = options.now ?? (() => Date.now());
36
- const emit = options.emit ?? (() => {});
37
- const fired: FaultRecord[] = [];
38
-
39
- return {
40
- /** Every condition a consumer can reach from the fault panel. */
41
- available: ALL_DIAGNOSTIC_CODES,
42
-
43
- inject(kind: FaultKind): Disposable {
44
- const record: FaultRecord = { code: kind, at: now() };
45
- fired.push(record);
46
- emit(kind, `Injected fault: ${kind}`, { injected: true });
47
-
48
- return {
49
- dispose() {
50
- const index = fired.indexOf(record);
51
- if (index >= 0) fired.splice(index, 1);
52
- },
53
- };
54
- },
55
-
56
- get fired(): readonly FaultRecord[] {
57
- return fired;
58
- },
59
-
60
- /** Which documented conditions have not yet been exercised. */
61
- unexercised(): readonly DiagnosticCode[] {
62
- const seen = new Set(fired.map(record => record.code));
63
- return ALL_DIAGNOSTIC_CODES.filter(code => !seen.has(code));
64
- },
65
-
66
- reset(): void {
67
- fired.length = 0;
68
- },
69
- };
70
- }
71
-
72
- export type FaultInjector = ReturnType<typeof createFaultInjector>;
1
+ /**
2
+ * Fault injection.
3
+ *
4
+ * Every documented failure condition must be reachable on demand, or the claim
5
+ * that each has its own distinct diagnostic is untestable. This is what the
6
+ * demo app's fault panel and the conformance suite both drive.
7
+ */
8
+
9
+ import { ALL_DIAGNOSTIC_CODES, type DiagnosticCode } from '../contract/errors';
10
+
11
+ export type FaultKind = DiagnosticCode;
12
+
13
+ export interface FaultRecord {
14
+ readonly code: DiagnosticCode;
15
+ readonly at: number;
16
+ }
17
+
18
+ export interface Disposable {
19
+ dispose(): void;
20
+ }
21
+
22
+ export interface FaultInjectorOptions {
23
+ readonly now?: () => number;
24
+ readonly emit?: (code: string, message: string, context?: Record<string, unknown>) => void;
25
+ }
26
+
27
+ /**
28
+ * Triggers a named condition and records that it fired.
29
+ *
30
+ * Deliberately a thin mechanism: it proves each identifier is reachable and
31
+ * distinct, which is the property under test. Making the *real* condition occur
32
+ * is each module's own test's job.
33
+ */
34
+ export function createFaultInjector(options: FaultInjectorOptions = {}) {
35
+ const now = options.now ?? (() => Date.now());
36
+ const emit = options.emit ?? (() => {});
37
+ const fired: FaultRecord[] = [];
38
+
39
+ return {
40
+ /** Every condition a consumer can reach from the fault panel. */
41
+ available: ALL_DIAGNOSTIC_CODES,
42
+
43
+ inject(kind: FaultKind): Disposable {
44
+ const record: FaultRecord = { code: kind, at: now() };
45
+ fired.push(record);
46
+ emit(kind, `Injected fault: ${kind}`, { injected: true });
47
+
48
+ return {
49
+ dispose() {
50
+ const index = fired.indexOf(record);
51
+ if (index >= 0) fired.splice(index, 1);
52
+ },
53
+ };
54
+ },
55
+
56
+ get fired(): readonly FaultRecord[] {
57
+ return fired;
58
+ },
59
+
60
+ /** Which documented conditions have not yet been exercised. */
61
+ unexercised(): readonly DiagnosticCode[] {
62
+ const seen = new Set(fired.map(record => record.code));
63
+ return ALL_DIAGNOSTIC_CODES.filter(code => !seen.has(code));
64
+ },
65
+
66
+ reset(): void {
67
+ fired.length = 0;
68
+ },
69
+ };
70
+ }
71
+
72
+ export type FaultInjector = ReturnType<typeof createFaultInjector>;