@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.
- package/CHANGELOG.md +174 -109
- package/README.md +414 -399
- package/package.json +4 -21
- package/skills/sdk-rn-notification/SKILL.md +5 -4
- package/src/channel/migrate.ts +285 -285
- package/src/channel/tone-map.ts +130 -130
- package/src/channel/validate-tone.ts +61 -61
- package/src/contract/channel.ts +192 -188
- package/src/contract/diagnostics.ts +102 -102
- package/src/contract/errors.ts +134 -130
- package/src/contract/index.ts +10 -10
- package/src/contract/interaction.ts +62 -62
- package/src/contract/notification.ts +118 -118
- package/src/contract/permission.ts +55 -60
- package/src/contract/reconcile.ts +88 -88
- package/src/contract/recurrence.ts +129 -123
- package/src/diagnostics/burst.ts +101 -101
- package/src/diagnostics/events.ts +72 -72
- package/src/diagnostics/redact.ts +72 -72
- package/src/diagnostics/snapshot.ts +67 -64
- package/src/engine/adapter.ts +172 -172
- package/src/engine/codec.ts +89 -0
- package/src/engine/notify-kit-engine.ts +706 -761
- package/src/index.ts +208 -208
- package/src/initialize.ts +240 -174
- package/src/interaction/index.ts +146 -146
- package/src/maintenance/cycle.ts +150 -147
- package/src/maintenance/permission-refresh.ts +39 -41
- package/src/maintenance/reanchor.ts +101 -95
- package/src/maintenance/toggle.ts +66 -60
- package/src/maintenance/verify.ts +83 -83
- package/src/maintenance/window-refresh.ts +64 -61
- package/src/permission/index.ts +183 -183
- package/src/permission/state.ts +77 -77
- package/src/push/envelope.ts +165 -165
- package/src/push/fcm-provider.ts +143 -146
- package/src/push/gate.ts +190 -190
- package/src/push/index.ts +33 -29
- package/src/push/instructions.ts +114 -109
- package/src/push/provider-adapter.ts +54 -54
- package/src/push/signout.ts +65 -65
- package/src/reconcile/chunk.ts +53 -53
- package/src/reconcile/diff.ts +142 -145
- package/src/reconcile/duplicates.ts +73 -73
- package/src/reconcile/index.ts +180 -188
- package/src/reconcile/truncate.ts +56 -56
- package/src/recurrence/clock-guard.ts +73 -71
- package/src/recurrence/expand.ts +326 -292
- package/src/recurrence/index.ts +270 -177
- package/src/repeat/expiry.ts +53 -55
- package/src/repeat/index.ts +54 -54
- package/src/repeat/marker.ts +72 -72
- package/src/repeat/place.ts +73 -73
- package/src/schedule/anchor.ts +278 -267
- package/src/schedule/index.ts +174 -184
- package/src/schedule/partition.ts +88 -85
- package/src/schedule/resolve-moment.ts +80 -80
- package/src/schedule/validate-content.ts +111 -111
- package/src/schedule/validate-identifier.ts +126 -126
- package/src/schedule/writer.ts +55 -0
- package/src/state/caps.ts +71 -71
- package/src/state/dedup-ledger.ts +35 -35
- package/src/state/interaction-buffer.ts +73 -73
- package/src/state/mmkv-store.ts +75 -75
- package/src/state/permission-history.ts +41 -41
- package/src/state/record.ts +266 -258
- package/src/state/reset.ts +41 -41
- package/src/state/series-ledger.ts +91 -84
- package/src/state/store.ts +40 -40
- package/src/testing/conformance/index.ts +258 -248
- package/src/testing/fake-engine.ts +462 -469
- package/src/testing/fake-provider.ts +126 -129
- package/src/testing/faults.ts +72 -72
- package/src/testing/index.ts +23 -28
- package/src/testing/measure.ts +337 -337
|
@@ -1,129 +1,126 @@
|
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
readonly
|
|
22
|
-
readonly
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
let
|
|
45
|
-
let token
|
|
46
|
-
let
|
|
47
|
-
let
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 { PushAvailability, PushProviderAdapter } from '../push/provider-adapter';
|
|
14
|
+
import type { Unsubscribe } from '../contract/interaction';
|
|
15
|
+
|
|
16
|
+
export interface FakeProviderOptions {
|
|
17
|
+
readonly name?: string;
|
|
18
|
+
readonly availability?: PushAvailability;
|
|
19
|
+
readonly initialToken?: string;
|
|
20
|
+
/** When true, `getToken` resolves only after `releaseToken()` is called. */
|
|
21
|
+
readonly withholdToken?: boolean;
|
|
22
|
+
readonly invalidationSucceeds?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FakeProviderControls {
|
|
26
|
+
/** Delivers a raw payload as the provider would. */
|
|
27
|
+
deliver(raw: unknown): void;
|
|
28
|
+
/** Queues the payload that launched the app. */
|
|
29
|
+
queueInitial(raw: unknown): void;
|
|
30
|
+
/** Simulates the platform rotating the registration token. */
|
|
31
|
+
rotateToken(token: string): void;
|
|
32
|
+
/** Resolves a withheld token, so "waits for availability" is testable. */
|
|
33
|
+
releaseToken(token: string): void;
|
|
34
|
+
setAvailability(availability: PushAvailability): void;
|
|
35
|
+
readonly invalidated: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type FakeProvider = PushProviderAdapter & FakeProviderControls;
|
|
39
|
+
|
|
40
|
+
export function createFakeProvider(options: FakeProviderOptions = {}): FakeProvider {
|
|
41
|
+
let availability: PushAvailability = options.availability ?? 'available';
|
|
42
|
+
let token = options.withholdToken === true ? undefined : (options.initialToken ?? 'fake-token-1');
|
|
43
|
+
let invalidated = false;
|
|
44
|
+
let messageHandlers: ((raw: unknown) => void)[] = [];
|
|
45
|
+
let tokenHandlers: ((token: string) => void)[] = [];
|
|
46
|
+
let initial: unknown | undefined;
|
|
47
|
+
let waiters: ((token: string) => void)[] = [];
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
name: options.name ?? 'fake-provider',
|
|
51
|
+
|
|
52
|
+
async getAvailability() {
|
|
53
|
+
return availability;
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
async getToken() {
|
|
57
|
+
if (token !== undefined) return token;
|
|
58
|
+
// Waits rather than returning a placeholder — an empty string presented
|
|
59
|
+
// as a token is worse than not answering yet.
|
|
60
|
+
return new Promise<string>(resolve => {
|
|
61
|
+
waiters.push(resolve);
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
onTokenChange(handler): Unsubscribe {
|
|
66
|
+
tokenHandlers.push(handler);
|
|
67
|
+
return () => {
|
|
68
|
+
tokenHandlers = tokenHandlers.filter(h => h !== handler);
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async invalidateToken() {
|
|
73
|
+
invalidated = true;
|
|
74
|
+
token = undefined;
|
|
75
|
+
return options.invalidationSucceeds ?? true;
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
subscribeToMessages(handler): Unsubscribe {
|
|
79
|
+
messageHandlers.push(handler);
|
|
80
|
+
return () => {
|
|
81
|
+
messageHandlers = messageHandlers.filter(h => h !== handler);
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
async takeInitialMessage() {
|
|
86
|
+
const raw = initial;
|
|
87
|
+
initial = undefined;
|
|
88
|
+
return raw;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
// --- FakeProviderControls ---------------------------------------------
|
|
92
|
+
|
|
93
|
+
deliver(raw) {
|
|
94
|
+
for (const handler of messageHandlers) {
|
|
95
|
+
handler(raw);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
queueInitial(raw) {
|
|
100
|
+
initial = raw;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
rotateToken(next) {
|
|
104
|
+
token = next;
|
|
105
|
+
for (const handler of tokenHandlers) {
|
|
106
|
+
handler(next);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
releaseToken(next) {
|
|
111
|
+
token = next;
|
|
112
|
+
for (const resolve of waiters) {
|
|
113
|
+
resolve(next);
|
|
114
|
+
}
|
|
115
|
+
waiters = [];
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
setAvailability(next) {
|
|
119
|
+
availability = next;
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
get invalidated() {
|
|
123
|
+
return invalidated;
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
package/src/testing/faults.ts
CHANGED
|
@@ -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>;
|
package/src/testing/index.ts
CHANGED
|
@@ -1,28 +1,23 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test-only surface: `@chipmobilesdk/rn-notification/testing`
|
|
3
|
-
*
|
|
4
|
-
* Kept behind its own subpath so fakes and fault injection never reach a
|
|
5
|
-
* production bundle. Everything here exists so that scheduling, reconciliation,
|
|
6
|
-
* permission transitions, tone mapping, and interaction routing are provable
|
|
7
|
-
* with no device, no emulator, and no live provider.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export { createFakeEngine } from './fake-engine';
|
|
11
|
-
export { createFakeProvider } from './fake-provider';
|
|
12
|
-
export { createFaultInjector } from './faults';
|
|
13
|
-
export { runConformanceSuite } from './conformance';
|
|
14
|
-
export { runMeasurementHarness, PERF_BUDGETS } from './measure';
|
|
15
|
-
export type { MeasurementReport, Measurement, BudgetName, MeasureOptions } from './measure';
|
|
16
|
-
export type { ConformanceReport, ConformanceCheck, ConformanceOptions } from './conformance';
|
|
17
|
-
export type { FaultInjector, FaultKind, FaultRecord, Disposable } from './faults';
|
|
18
|
-
export type { FakeProvider, FakeProviderOptions, FakeProviderControls } from './fake-provider';
|
|
19
|
-
export type {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from './fake-engine';
|
|
25
|
-
|
|
26
|
-
/** In-memory state store, for testing state behavior without a native module. */
|
|
27
|
-
export { createMemoryStore } from '../state/store';
|
|
28
|
-
export type { StateStore } from '../state/store';
|
|
1
|
+
/**
|
|
2
|
+
* Test-only surface: `@chipmobilesdk/rn-notification/testing`
|
|
3
|
+
*
|
|
4
|
+
* Kept behind its own subpath so fakes and fault injection never reach a
|
|
5
|
+
* production bundle. Everything here exists so that scheduling, reconciliation,
|
|
6
|
+
* permission transitions, tone mapping, and interaction routing are provable
|
|
7
|
+
* with no device, no emulator, and no live provider.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { createFakeEngine } from './fake-engine';
|
|
11
|
+
export { createFakeProvider } from './fake-provider';
|
|
12
|
+
export { createFaultInjector } from './faults';
|
|
13
|
+
export { runConformanceSuite } from './conformance';
|
|
14
|
+
export { runMeasurementHarness, PERF_BUDGETS } from './measure';
|
|
15
|
+
export type { MeasurementReport, Measurement, BudgetName, MeasureOptions } from './measure';
|
|
16
|
+
export type { ConformanceReport, ConformanceCheck, ConformanceOptions } from './conformance';
|
|
17
|
+
export type { FaultInjector, FaultKind, FaultRecord, Disposable } from './faults';
|
|
18
|
+
export type { FakeProvider, FakeProviderOptions, FakeProviderControls } from './fake-provider';
|
|
19
|
+
export type { FakeEngine, FakeEngineOptions, FakeControls, RecordedOperation } from './fake-engine';
|
|
20
|
+
|
|
21
|
+
/** In-memory state store, for testing state behavior without a native module. */
|
|
22
|
+
export { createMemoryStore } from '../state/store';
|
|
23
|
+
export type { StateStore } from '../state/store';
|