@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.
- package/CHANGELOG.md +257 -166
- package/README.md +420 -410
- package/package.json +61 -75
- package/skills/sdk-rn-notification/SKILL.md +172 -173
- package/src/channel/migrate.ts +285 -289
- 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 +142 -134
- 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 -129
- package/src/diagnostics/burst.ts +101 -101
- package/src/diagnostics/events.ts +81 -72
- package/src/diagnostics/redact.ts +55 -72
- package/src/diagnostics/snapshot.ts +100 -64
- package/src/engine/adapter.ts +172 -172
- package/src/engine/codec.ts +89 -89
- package/src/engine/notify-kit-engine.ts +715 -707
- package/src/index.ts +208 -208
- package/src/initialize.ts +247 -236
- package/src/interaction/index.ts +146 -146
- package/src/maintenance/cycle.ts +150 -154
- package/src/maintenance/permission-refresh.ts +39 -41
- package/src/maintenance/reanchor.ts +101 -101
- package/src/maintenance/toggle.ts +66 -64
- package/src/maintenance/verify.ts +83 -83
- package/src/maintenance/window-refresh.ts +64 -64
- 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 +150 -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 -180
- package/src/reconcile/truncate.ts +56 -56
- package/src/recurrence/clock-guard.ts +73 -71
- package/src/recurrence/expand.ts +326 -319
- package/src/recurrence/index.ts +270 -259
- 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 +294 -267
- package/src/schedule/index.ts +174 -181
- 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 -55
- package/src/state/caps.ts +71 -71
- package/src/state/dedup-ledger.ts +35 -35
- package/src/state/interaction-buffer.ts +101 -73
- package/src/state/mmkv-store.ts +92 -75
- package/src/state/permission-history.ts +41 -41
- package/src/state/record.ts +270 -263
- package/src/state/reset.ts +41 -41
- package/src/state/series-ledger.ts +91 -91
- package/src/state/store.ts +43 -40
- package/src/testing/conformance/index.ts +258 -248
- package/src/testing/fake-engine.ts +462 -468
- 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
package/src/state/mmkv-store.ts
CHANGED
|
@@ -1,75 +1,92 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MMKV-backed state store.
|
|
3
|
-
*
|
|
4
|
-
* Every MMKV import is confined to this file, so the rest of the package stays
|
|
5
|
-
* testable without a native module.
|
|
6
|
-
*
|
|
7
|
-
* Backup exclusion is achieved by *where* the data lives rather than by setting
|
|
8
|
-
* a file attribute: on iOS the instance is rooted under `Library/Caches`, which
|
|
9
|
-
* the platform excludes from iCloud and iTunes backup by design. The OS may
|
|
10
|
-
* purge that directory under storage pressure — acceptable here precisely
|
|
11
|
-
* because discarding and rebuilding this record is already a specified,
|
|
12
|
-
* non-blocking condition.
|
|
13
|
-
*
|
|
14
|
-
* On Android, backup exclusion is declared in the consuming app's
|
|
15
|
-
* `data_extraction_rules.xml` and `full_backup_content`, documented in the
|
|
16
|
-
* README. It is the app's manifest, so it is the app's declaration to make.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* MMKV-backed state store.
|
|
3
|
+
*
|
|
4
|
+
* Every MMKV import is confined to this file, so the rest of the package stays
|
|
5
|
+
* testable without a native module.
|
|
6
|
+
*
|
|
7
|
+
* Backup exclusion is achieved by *where* the data lives rather than by setting
|
|
8
|
+
* a file attribute: on iOS the instance is rooted under `Library/Caches`, which
|
|
9
|
+
* the platform excludes from iCloud and iTunes backup by design. The OS may
|
|
10
|
+
* purge that directory under storage pressure — acceptable here precisely
|
|
11
|
+
* because discarding and rebuilding this record is already a specified,
|
|
12
|
+
* non-blocking condition.
|
|
13
|
+
*
|
|
14
|
+
* On Android, backup exclusion is declared in the consuming app's
|
|
15
|
+
* `data_extraction_rules.xml` and `full_backup_content`, documented in the
|
|
16
|
+
* README. It is the app's manifest, so it is the app's declaration to make.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
createDiagnosticChannel,
|
|
21
|
+
currentPlatform,
|
|
22
|
+
lazyPeer,
|
|
23
|
+
openDurable,
|
|
24
|
+
type MmkvLike,
|
|
25
|
+
} from '@chipmobilesdk/rn-core';
|
|
26
|
+
|
|
27
|
+
import { DIAGNOSTIC_CODES } from '../contract/errors';
|
|
28
|
+
import type { StateStore } from './store';
|
|
29
|
+
|
|
30
|
+
const INSTANCE_ID = 'chipmobilesdk.notification';
|
|
31
|
+
|
|
32
|
+
const mmkv = lazyPeer('react-native-mmkv', () => {
|
|
33
|
+
// Inside `try`, so Metro bundles an app that leaves this optional peer out.
|
|
34
|
+
try {
|
|
35
|
+
return require('react-native-mmkv') as {
|
|
36
|
+
createMMKV: (config: { id: string; path?: string }) => MmkvLike;
|
|
37
|
+
};
|
|
38
|
+
} catch (cause) {
|
|
39
|
+
throw cause;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const channel = createDiagnosticChannel('rn-notification');
|
|
44
|
+
|
|
45
|
+
export interface MmkvStoreOptions {
|
|
46
|
+
/** Told once, with the reason, when the store falls back to memory. */
|
|
47
|
+
readonly onDegraded?: (cause: unknown) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates the persistent store, falling back to memory when MMKV is absent.
|
|
52
|
+
*
|
|
53
|
+
* The fallback is deliberate: a missing optional native module degrades
|
|
54
|
+
* deduplication and cold-start buffering to session scope, which is a
|
|
55
|
+
* documented degradation. It must never stop notifications being delivered, and
|
|
56
|
+
* it is never silent (NOT-15): it reports `STATE_STORE_DEGRADED` to the app's SDK
|
|
57
|
+
* sinks and to `onDegraded`, and the store reports `durable: false`.
|
|
58
|
+
*/
|
|
59
|
+
export function createMmkvStore(options: MmkvStoreOptions = {}): StateStore {
|
|
60
|
+
const port = openDurable(
|
|
61
|
+
() =>
|
|
62
|
+
// v4's factory: up to 0.2.0 this called the v3 `new MMKV()`, which v4 no
|
|
63
|
+
// longer exports as a value, so every device silently got memory.
|
|
64
|
+
mmkv
|
|
65
|
+
.require(peer => new Error(`${peer} is not installed`))
|
|
66
|
+
.createMMKV({
|
|
67
|
+
id: INSTANCE_ID,
|
|
68
|
+
// Caches is backup-excluded by platform design; see the note above.
|
|
69
|
+
...(currentPlatform() === 'ios'
|
|
70
|
+
? { path: 'Library/Caches/chipmobilesdk-notification' }
|
|
71
|
+
: {}),
|
|
72
|
+
}),
|
|
73
|
+
cause => {
|
|
74
|
+
channel.emit(DIAGNOSTIC_CODES.STATE_STORE_DEGRADED, 'warn');
|
|
75
|
+
options.onDegraded?.(cause);
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
return {
|
|
79
|
+
getString: key => port.getString(key),
|
|
80
|
+
setString: (key, value) => port.set(key, value),
|
|
81
|
+
delete: key => port.remove(key),
|
|
82
|
+
clearAll: () => port.clearAll(),
|
|
83
|
+
approximateBytes: () => {
|
|
84
|
+
let total = 0;
|
|
85
|
+
for (const key of port.keys()) {
|
|
86
|
+
total += key.length + (port.getString(key)?.length ?? 0);
|
|
87
|
+
}
|
|
88
|
+
return total;
|
|
89
|
+
},
|
|
90
|
+
durable: port.durable,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Permission-request history.
|
|
3
|
-
*
|
|
4
|
-
* The platform cannot tell you whether you have already asked. On both
|
|
5
|
-
* platforms a second request after a denial is silently a no-op, so an app that
|
|
6
|
-
* does not remember will keep "asking" and keep appearing to be refused.
|
|
7
|
-
*
|
|
8
|
-
* Keyed by owner tag so a multi-account device can clear one account's history
|
|
9
|
-
* on sign-out without forgetting what it asked the others.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { StateManager } from './record';
|
|
13
|
-
import type { PermissionKind } from '../contract/permission';
|
|
14
|
-
|
|
15
|
-
function key(kind: PermissionKind, ownerTag?: string): string {
|
|
16
|
-
return `${ownerTag ?? '*'}:${kind}`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function hasRequested(
|
|
20
|
-
state: StateManager,
|
|
21
|
-
kind: PermissionKind,
|
|
22
|
-
ownerTag?: string,
|
|
23
|
-
): boolean {
|
|
24
|
-
const target = key(kind, ownerTag);
|
|
25
|
-
return state.read().permissionHistory.some(entry => entry.key === target && entry.requested);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function recordRequested(
|
|
29
|
-
state: StateManager,
|
|
30
|
-
kind: PermissionKind,
|
|
31
|
-
now: number,
|
|
32
|
-
ownerTag?: string,
|
|
33
|
-
): void {
|
|
34
|
-
const target = key(kind, ownerTag);
|
|
35
|
-
if (hasRequested(state, kind, ownerTag)) return;
|
|
36
|
-
|
|
37
|
-
state.update('permissionHistory', current => [
|
|
38
|
-
...current.filter(entry => entry.key !== target),
|
|
39
|
-
{ at: now, key: target, requested: true },
|
|
40
|
-
]);
|
|
41
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Permission-request history.
|
|
3
|
+
*
|
|
4
|
+
* The platform cannot tell you whether you have already asked. On both
|
|
5
|
+
* platforms a second request after a denial is silently a no-op, so an app that
|
|
6
|
+
* does not remember will keep "asking" and keep appearing to be refused.
|
|
7
|
+
*
|
|
8
|
+
* Keyed by owner tag so a multi-account device can clear one account's history
|
|
9
|
+
* on sign-out without forgetting what it asked the others.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { StateManager } from './record';
|
|
13
|
+
import type { PermissionKind } from '../contract/permission';
|
|
14
|
+
|
|
15
|
+
function key(kind: PermissionKind, ownerTag?: string): string {
|
|
16
|
+
return `${ownerTag ?? '*'}:${kind}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function hasRequested(
|
|
20
|
+
state: StateManager,
|
|
21
|
+
kind: PermissionKind,
|
|
22
|
+
ownerTag?: string,
|
|
23
|
+
): boolean {
|
|
24
|
+
const target = key(kind, ownerTag);
|
|
25
|
+
return state.read().permissionHistory.some(entry => entry.key === target && entry.requested);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function recordRequested(
|
|
29
|
+
state: StateManager,
|
|
30
|
+
kind: PermissionKind,
|
|
31
|
+
now: number,
|
|
32
|
+
ownerTag?: string,
|
|
33
|
+
): void {
|
|
34
|
+
const target = key(kind, ownerTag);
|
|
35
|
+
if (hasRequested(state, kind, ownerTag)) return;
|
|
36
|
+
|
|
37
|
+
state.update('permissionHistory', current => [
|
|
38
|
+
...current.filter(entry => entry.key !== target),
|
|
39
|
+
{ at: now, key: target, requested: true },
|
|
40
|
+
]);
|
|
41
|
+
}
|