@chipmobilesdk/rn-notification 0.1.2 → 0.2.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 +96 -42
- package/README.md +399 -306
- package/package.json +2 -1
- package/skills/sdk-rn-notification/SKILL.md +68 -3
- package/src/channel/migrate.ts +86 -12
- package/src/channel/tone-map.ts +130 -90
- package/src/channel/validate-tone.ts +61 -0
- package/src/contract/channel.ts +188 -89
- package/src/contract/diagnostics.ts +102 -102
- package/src/contract/errors.ts +130 -123
- 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 +60 -60
- package/src/contract/reconcile.ts +88 -88
- package/src/contract/recurrence.ts +123 -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 +64 -64
- package/src/engine/adapter.ts +172 -79
- package/src/engine/notify-kit-engine.ts +761 -535
- package/src/index.ts +24 -0
- package/src/initialize.ts +174 -174
- package/src/interaction/index.ts +146 -146
- package/src/maintenance/cycle.ts +147 -147
- package/src/maintenance/permission-refresh.ts +41 -41
- package/src/maintenance/reanchor.ts +95 -95
- package/src/maintenance/toggle.ts +60 -60
- package/src/maintenance/verify.ts +83 -83
- package/src/maintenance/window-refresh.ts +61 -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 +146 -146
- package/src/push/gate.ts +190 -190
- package/src/push/instructions.ts +109 -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 +145 -145
- package/src/reconcile/duplicates.ts +73 -73
- package/src/reconcile/index.ts +14 -1
- package/src/reconcile/truncate.ts +56 -56
- package/src/recurrence/clock-guard.ts +71 -71
- package/src/recurrence/expand.ts +292 -292
- package/src/repeat/expiry.ts +55 -0
- package/src/repeat/index.ts +54 -0
- package/src/repeat/marker.ts +72 -0
- package/src/repeat/place.ts +73 -0
- package/src/schedule/anchor.ts +267 -267
- package/src/schedule/index.ts +184 -173
- package/src/schedule/partition.ts +85 -85
- package/src/schedule/resolve-moment.ts +80 -80
- package/src/schedule/validate-content.ts +111 -111
- package/src/schedule/validate-identifier.ts +126 -114
- package/src/state/caps.ts +71 -71
- package/src/state/interaction-buffer.ts +73 -73
- package/src/state/mmkv-store.ts +75 -71
- package/src/state/permission-history.ts +41 -41
- package/src/state/record.ts +258 -246
- package/src/state/reset.ts +41 -41
- package/src/state/series-ledger.ts +84 -84
- package/src/state/store.ts +40 -40
- package/src/testing/conformance/index.ts +248 -194
- package/src/testing/fake-engine.ts +469 -284
- package/src/testing/fake-provider.ts +129 -129
- package/src/testing/faults.ts +72 -72
- package/src/testing/measure.ts +337 -334
package/src/state/caps.ts
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Caps and eviction for the persisted collections.
|
|
3
|
-
*
|
|
4
|
-
* No collection may grow without bound. Eviction is oldest-first and always
|
|
5
|
-
* observable — a silent eviction would turn a bounded guarantee into an
|
|
6
|
-
* unexplained one.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/** Every persisted collection with a size or age bound. */
|
|
10
|
-
export const COLLECTION_CAPS = {
|
|
11
|
-
dedupLedger: { maxEntries: 500, retentionMs: 24 * 60 * 60 * 1000 },
|
|
12
|
-
interactionBuffer: { maxEntries: 50, retentionMs: 7 * 24 * 60 * 60 * 1000 },
|
|
13
|
-
migrationLedger: { maxEntries: 20, retentionMs: Number.POSITIVE_INFINITY },
|
|
14
|
-
seriesLedger: { maxEntries: 32, retentionMs: Number.POSITIVE_INFINITY },
|
|
15
|
-
permissionHistory: { maxEntries: 64, retentionMs: Number.POSITIVE_INFINITY },
|
|
16
|
-
} as const;
|
|
17
|
-
|
|
18
|
-
export type CollectionName = keyof typeof COLLECTION_CAPS;
|
|
19
|
-
|
|
20
|
-
/** Anything stored with a timestamp, which is everything that can age out. */
|
|
21
|
-
export interface Timestamped {
|
|
22
|
-
readonly at: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface EvictionOutcome<T> {
|
|
26
|
-
readonly kept: readonly T[];
|
|
27
|
-
readonly evicted: readonly T[];
|
|
28
|
-
readonly reason: 'none' | 'cap' | 'retention' | 'cap+retention';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Applies retention first, then the size cap.
|
|
33
|
-
*
|
|
34
|
-
* Order matters: expiring stale entries before trimming means a burst of new
|
|
35
|
-
* entries does not evict fresh ones while stale ones sit protected by
|
|
36
|
-
* recency ordering.
|
|
37
|
-
*/
|
|
38
|
-
export function evict<T extends Timestamped>(
|
|
39
|
-
entries: readonly T[],
|
|
40
|
-
collection: CollectionName,
|
|
41
|
-
now: number,
|
|
42
|
-
): EvictionOutcome<T> {
|
|
43
|
-
const { maxEntries, retentionMs } = COLLECTION_CAPS[collection];
|
|
44
|
-
|
|
45
|
-
const expired: T[] = [];
|
|
46
|
-
const live: T[] = [];
|
|
47
|
-
for (const entry of entries) {
|
|
48
|
-
if (Number.isFinite(retentionMs) && now - entry.at > retentionMs) {
|
|
49
|
-
expired.push(entry);
|
|
50
|
-
} else {
|
|
51
|
-
live.push(entry);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Oldest first, so the tail is what we keep when trimming.
|
|
56
|
-
live.sort((a, b) => a.at - b.at);
|
|
57
|
-
|
|
58
|
-
const overflow = live.length > maxEntries ? live.slice(0, live.length - maxEntries) : [];
|
|
59
|
-
const kept = live.length > maxEntries ? live.slice(live.length - maxEntries) : live;
|
|
60
|
-
|
|
61
|
-
let reason: EvictionOutcome<T>['reason'] = 'none';
|
|
62
|
-
if (expired.length > 0 && overflow.length > 0) {
|
|
63
|
-
reason = 'cap+retention';
|
|
64
|
-
} else if (overflow.length > 0) {
|
|
65
|
-
reason = 'cap';
|
|
66
|
-
} else if (expired.length > 0) {
|
|
67
|
-
reason = 'retention';
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return { kept, evicted: [...expired, ...overflow], reason };
|
|
71
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Caps and eviction for the persisted collections.
|
|
3
|
+
*
|
|
4
|
+
* No collection may grow without bound. Eviction is oldest-first and always
|
|
5
|
+
* observable — a silent eviction would turn a bounded guarantee into an
|
|
6
|
+
* unexplained one.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Every persisted collection with a size or age bound. */
|
|
10
|
+
export const COLLECTION_CAPS = {
|
|
11
|
+
dedupLedger: { maxEntries: 500, retentionMs: 24 * 60 * 60 * 1000 },
|
|
12
|
+
interactionBuffer: { maxEntries: 50, retentionMs: 7 * 24 * 60 * 60 * 1000 },
|
|
13
|
+
migrationLedger: { maxEntries: 20, retentionMs: Number.POSITIVE_INFINITY },
|
|
14
|
+
seriesLedger: { maxEntries: 32, retentionMs: Number.POSITIVE_INFINITY },
|
|
15
|
+
permissionHistory: { maxEntries: 64, retentionMs: Number.POSITIVE_INFINITY },
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
export type CollectionName = keyof typeof COLLECTION_CAPS;
|
|
19
|
+
|
|
20
|
+
/** Anything stored with a timestamp, which is everything that can age out. */
|
|
21
|
+
export interface Timestamped {
|
|
22
|
+
readonly at: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface EvictionOutcome<T> {
|
|
26
|
+
readonly kept: readonly T[];
|
|
27
|
+
readonly evicted: readonly T[];
|
|
28
|
+
readonly reason: 'none' | 'cap' | 'retention' | 'cap+retention';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Applies retention first, then the size cap.
|
|
33
|
+
*
|
|
34
|
+
* Order matters: expiring stale entries before trimming means a burst of new
|
|
35
|
+
* entries does not evict fresh ones while stale ones sit protected by
|
|
36
|
+
* recency ordering.
|
|
37
|
+
*/
|
|
38
|
+
export function evict<T extends Timestamped>(
|
|
39
|
+
entries: readonly T[],
|
|
40
|
+
collection: CollectionName,
|
|
41
|
+
now: number,
|
|
42
|
+
): EvictionOutcome<T> {
|
|
43
|
+
const { maxEntries, retentionMs } = COLLECTION_CAPS[collection];
|
|
44
|
+
|
|
45
|
+
const expired: T[] = [];
|
|
46
|
+
const live: T[] = [];
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (Number.isFinite(retentionMs) && now - entry.at > retentionMs) {
|
|
49
|
+
expired.push(entry);
|
|
50
|
+
} else {
|
|
51
|
+
live.push(entry);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Oldest first, so the tail is what we keep when trimming.
|
|
56
|
+
live.sort((a, b) => a.at - b.at);
|
|
57
|
+
|
|
58
|
+
const overflow = live.length > maxEntries ? live.slice(0, live.length - maxEntries) : [];
|
|
59
|
+
const kept = live.length > maxEntries ? live.slice(live.length - maxEntries) : live;
|
|
60
|
+
|
|
61
|
+
let reason: EvictionOutcome<T>['reason'] = 'none';
|
|
62
|
+
if (expired.length > 0 && overflow.length > 0) {
|
|
63
|
+
reason = 'cap+retention';
|
|
64
|
+
} else if (overflow.length > 0) {
|
|
65
|
+
reason = 'cap';
|
|
66
|
+
} else if (expired.length > 0) {
|
|
67
|
+
reason = 'retention';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { kept, evicted: [...expired, ...overflow], reason };
|
|
71
|
+
}
|
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The durable interaction buffer.
|
|
3
|
-
*
|
|
4
|
-
* A cold-start tap arrives before any handler exists. Holding it in memory
|
|
5
|
-
* would be enough if the process always survived — but the process is exactly
|
|
6
|
-
* what does not survive a cold start, so the buffer is persisted.
|
|
7
|
-
*
|
|
8
|
-
* This is the one content-bearing thing the package stores: an interaction
|
|
9
|
-
* cannot be delivered without its routing payload. It is evicted the moment it
|
|
10
|
-
* is delivered, and time-limited regardless, so an event nobody ever collects
|
|
11
|
-
* cannot sit on disk forever.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { StateManager, BufferedInteraction } from './record';
|
|
15
|
-
import type { InteractionEvent } from '../contract/interaction';
|
|
16
|
-
|
|
17
|
-
/** Identity for a buffered event, derived so re-buffering cannot duplicate. */
|
|
18
|
-
function bufferedId(event: InteractionEvent): string {
|
|
19
|
-
return `${event.kind}:${event.notificationId}:${event.actionId ?? ''}:${event.occurredAt}`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function buffer(state: StateManager, event: InteractionEvent): void {
|
|
23
|
-
const id = bufferedId(event);
|
|
24
|
-
|
|
25
|
-
state.update('interactionBuffer', current => {
|
|
26
|
-
if (current.some(entry => entry.id === id)) return current;
|
|
27
|
-
|
|
28
|
-
const entry: BufferedInteraction = {
|
|
29
|
-
at: event.occurredAt,
|
|
30
|
-
id,
|
|
31
|
-
kind: event.kind,
|
|
32
|
-
notificationId: event.notificationId,
|
|
33
|
-
actionId: event.actionId,
|
|
34
|
-
inputText: event.inputText,
|
|
35
|
-
routing: event.routing,
|
|
36
|
-
ownerTag: event.ownerTag,
|
|
37
|
-
};
|
|
38
|
-
return [...current, entry];
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Takes every buffered event in occurrence order and clears them.
|
|
44
|
-
*
|
|
45
|
-
* Draining and clearing together is deliberate: delivering an event twice is
|
|
46
|
-
* worse than the alternative, because a consumer that acts on an interaction
|
|
47
|
-
* (marking a task done, sending a reply) would act twice.
|
|
48
|
-
*/
|
|
49
|
-
export function drain(state: StateManager): readonly InteractionEvent[] {
|
|
50
|
-
const buffered = [...state.read().interactionBuffer].sort((a, b) => a.at - b.at);
|
|
51
|
-
|
|
52
|
-
if (buffered.length === 0) return [];
|
|
53
|
-
|
|
54
|
-
state.update('interactionBuffer', () => []);
|
|
55
|
-
|
|
56
|
-
return buffered.map(entry => ({
|
|
57
|
-
kind: entry.kind,
|
|
58
|
-
notificationId: entry.notificationId,
|
|
59
|
-
actionId: entry.actionId,
|
|
60
|
-
inputText: entry.inputText,
|
|
61
|
-
routing: entry.routing,
|
|
62
|
-
ownerTag: entry.ownerTag,
|
|
63
|
-
occurredAt: entry.at,
|
|
64
|
-
// The state it arrived in is gone; what matters to a consumer is that it
|
|
65
|
-
// waited, which is what `wasBuffered` says.
|
|
66
|
-
appState: 'coldStart' as const,
|
|
67
|
-
wasBuffered: true,
|
|
68
|
-
}));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function bufferedCount(state: StateManager): number {
|
|
72
|
-
return state.read().interactionBuffer.length;
|
|
73
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The durable interaction buffer.
|
|
3
|
+
*
|
|
4
|
+
* A cold-start tap arrives before any handler exists. Holding it in memory
|
|
5
|
+
* would be enough if the process always survived — but the process is exactly
|
|
6
|
+
* what does not survive a cold start, so the buffer is persisted.
|
|
7
|
+
*
|
|
8
|
+
* This is the one content-bearing thing the package stores: an interaction
|
|
9
|
+
* cannot be delivered without its routing payload. It is evicted the moment it
|
|
10
|
+
* is delivered, and time-limited regardless, so an event nobody ever collects
|
|
11
|
+
* cannot sit on disk forever.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { StateManager, BufferedInteraction } from './record';
|
|
15
|
+
import type { InteractionEvent } from '../contract/interaction';
|
|
16
|
+
|
|
17
|
+
/** Identity for a buffered event, derived so re-buffering cannot duplicate. */
|
|
18
|
+
function bufferedId(event: InteractionEvent): string {
|
|
19
|
+
return `${event.kind}:${event.notificationId}:${event.actionId ?? ''}:${event.occurredAt}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buffer(state: StateManager, event: InteractionEvent): void {
|
|
23
|
+
const id = bufferedId(event);
|
|
24
|
+
|
|
25
|
+
state.update('interactionBuffer', current => {
|
|
26
|
+
if (current.some(entry => entry.id === id)) return current;
|
|
27
|
+
|
|
28
|
+
const entry: BufferedInteraction = {
|
|
29
|
+
at: event.occurredAt,
|
|
30
|
+
id,
|
|
31
|
+
kind: event.kind,
|
|
32
|
+
notificationId: event.notificationId,
|
|
33
|
+
actionId: event.actionId,
|
|
34
|
+
inputText: event.inputText,
|
|
35
|
+
routing: event.routing,
|
|
36
|
+
ownerTag: event.ownerTag,
|
|
37
|
+
};
|
|
38
|
+
return [...current, entry];
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Takes every buffered event in occurrence order and clears them.
|
|
44
|
+
*
|
|
45
|
+
* Draining and clearing together is deliberate: delivering an event twice is
|
|
46
|
+
* worse than the alternative, because a consumer that acts on an interaction
|
|
47
|
+
* (marking a task done, sending a reply) would act twice.
|
|
48
|
+
*/
|
|
49
|
+
export function drain(state: StateManager): readonly InteractionEvent[] {
|
|
50
|
+
const buffered = [...state.read().interactionBuffer].sort((a, b) => a.at - b.at);
|
|
51
|
+
|
|
52
|
+
if (buffered.length === 0) return [];
|
|
53
|
+
|
|
54
|
+
state.update('interactionBuffer', () => []);
|
|
55
|
+
|
|
56
|
+
return buffered.map(entry => ({
|
|
57
|
+
kind: entry.kind,
|
|
58
|
+
notificationId: entry.notificationId,
|
|
59
|
+
actionId: entry.actionId,
|
|
60
|
+
inputText: entry.inputText,
|
|
61
|
+
routing: entry.routing,
|
|
62
|
+
ownerTag: entry.ownerTag,
|
|
63
|
+
occurredAt: entry.at,
|
|
64
|
+
// The state it arrived in is gone; what matters to a consumer is that it
|
|
65
|
+
// waited, which is what `wasBuffered` says.
|
|
66
|
+
appState: 'coldStart' as const,
|
|
67
|
+
wasBuffered: true,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function bufferedCount(state: StateManager): number {
|
|
72
|
+
return state.read().interactionBuffer.length;
|
|
73
|
+
}
|
package/src/state/mmkv-store.ts
CHANGED
|
@@ -1,71 +1,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 { Platform } from 'react-native';
|
|
20
|
-
import type { StateStore } from './store';
|
|
21
|
-
import { createMemoryStore } from './store';
|
|
22
|
-
|
|
23
|
-
const INSTANCE_ID = 'chipmobilesdk.notification';
|
|
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
|
-
id:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
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 { Platform } from 'react-native';
|
|
20
|
+
import type { StateStore } from './store';
|
|
21
|
+
import { createMemoryStore } from './store';
|
|
22
|
+
|
|
23
|
+
const INSTANCE_ID = 'chipmobilesdk.notification';
|
|
24
|
+
|
|
25
|
+
/** The slice of a react-native-mmkv v4 instance this store uses (v4 names). */
|
|
26
|
+
interface MmkvLike {
|
|
27
|
+
getString(key: string): string | undefined;
|
|
28
|
+
set(key: string, value: string): void;
|
|
29
|
+
remove(key: string): unknown;
|
|
30
|
+
clearAll(): void;
|
|
31
|
+
getAllKeys(): string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates the persistent store, falling back to memory when MMKV is absent.
|
|
36
|
+
*
|
|
37
|
+
* The fallback is deliberate: a missing optional native module degrades
|
|
38
|
+
* deduplication and cold-start buffering to session scope, which is a
|
|
39
|
+
* documented degradation. It must never stop notifications being delivered.
|
|
40
|
+
*/
|
|
41
|
+
export function createMmkvStore(): StateStore {
|
|
42
|
+
let instance: MmkvLike;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Required lazily so the package loads in environments without the native module.
|
|
46
|
+
// v4's factory: up to 0.2.0 this called the v3 `new MMKV()`, which v4 — the
|
|
47
|
+
// only major the peer range allows — no longer exports as a value, so every
|
|
48
|
+
// device silently got the session-scoped memory store.
|
|
49
|
+
const { createMMKV } = require('react-native-mmkv') as {
|
|
50
|
+
createMMKV: (config: { id: string; path?: string }) => MmkvLike;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
instance = createMMKV({
|
|
54
|
+
id: INSTANCE_ID,
|
|
55
|
+
// Caches is backup-excluded by platform design; see the note above.
|
|
56
|
+
path: Platform.OS === 'ios' ? 'Library/Caches/chipmobilesdk-notification' : undefined,
|
|
57
|
+
});
|
|
58
|
+
} catch {
|
|
59
|
+
return createMemoryStore();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
getString: key => instance.getString(key),
|
|
64
|
+
setString: (key, value) => instance.set(key, value),
|
|
65
|
+
delete: key => instance.remove(key),
|
|
66
|
+
clearAll: () => instance.clearAll(),
|
|
67
|
+
approximateBytes: () => {
|
|
68
|
+
let total = 0;
|
|
69
|
+
for (const key of instance.getAllKeys()) {
|
|
70
|
+
total += key.length + (instance.getString(key)?.length ?? 0);
|
|
71
|
+
}
|
|
72
|
+
return total;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -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
|
+
}
|