@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +174 -109
  2. package/README.md +414 -399
  3. package/package.json +4 -21
  4. package/skills/sdk-rn-notification/SKILL.md +5 -4
  5. package/src/channel/migrate.ts +285 -285
  6. package/src/channel/tone-map.ts +130 -130
  7. package/src/channel/validate-tone.ts +61 -61
  8. package/src/contract/channel.ts +192 -188
  9. package/src/contract/diagnostics.ts +102 -102
  10. package/src/contract/errors.ts +134 -130
  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 +55 -60
  15. package/src/contract/reconcile.ts +88 -88
  16. package/src/contract/recurrence.ts +129 -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 +67 -64
  21. package/src/engine/adapter.ts +172 -172
  22. package/src/engine/codec.ts +89 -0
  23. package/src/engine/notify-kit-engine.ts +706 -761
  24. package/src/index.ts +208 -208
  25. package/src/initialize.ts +240 -174
  26. package/src/interaction/index.ts +146 -146
  27. package/src/maintenance/cycle.ts +150 -147
  28. package/src/maintenance/permission-refresh.ts +39 -41
  29. package/src/maintenance/reanchor.ts +101 -95
  30. package/src/maintenance/toggle.ts +66 -60
  31. package/src/maintenance/verify.ts +83 -83
  32. package/src/maintenance/window-refresh.ts +64 -61
  33. package/src/permission/index.ts +183 -183
  34. package/src/permission/state.ts +77 -77
  35. package/src/push/envelope.ts +165 -165
  36. package/src/push/fcm-provider.ts +143 -146
  37. package/src/push/gate.ts +190 -190
  38. package/src/push/index.ts +33 -29
  39. package/src/push/instructions.ts +114 -109
  40. package/src/push/provider-adapter.ts +54 -54
  41. package/src/push/signout.ts +65 -65
  42. package/src/reconcile/chunk.ts +53 -53
  43. package/src/reconcile/diff.ts +142 -145
  44. package/src/reconcile/duplicates.ts +73 -73
  45. package/src/reconcile/index.ts +180 -188
  46. package/src/reconcile/truncate.ts +56 -56
  47. package/src/recurrence/clock-guard.ts +73 -71
  48. package/src/recurrence/expand.ts +326 -292
  49. package/src/recurrence/index.ts +270 -177
  50. package/src/repeat/expiry.ts +53 -55
  51. package/src/repeat/index.ts +54 -54
  52. package/src/repeat/marker.ts +72 -72
  53. package/src/repeat/place.ts +73 -73
  54. package/src/schedule/anchor.ts +278 -267
  55. package/src/schedule/index.ts +174 -184
  56. package/src/schedule/partition.ts +88 -85
  57. package/src/schedule/resolve-moment.ts +80 -80
  58. package/src/schedule/validate-content.ts +111 -111
  59. package/src/schedule/validate-identifier.ts +126 -126
  60. package/src/schedule/writer.ts +55 -0
  61. package/src/state/caps.ts +71 -71
  62. package/src/state/dedup-ledger.ts +35 -35
  63. package/src/state/interaction-buffer.ts +73 -73
  64. package/src/state/mmkv-store.ts +75 -75
  65. package/src/state/permission-history.ts +41 -41
  66. package/src/state/record.ts +266 -258
  67. package/src/state/reset.ts +41 -41
  68. package/src/state/series-ledger.ts +91 -84
  69. package/src/state/store.ts +40 -40
  70. package/src/testing/conformance/index.ts +258 -248
  71. package/src/testing/fake-engine.ts +462 -469
  72. package/src/testing/fake-provider.ts +126 -129
  73. package/src/testing/faults.ts +72 -72
  74. package/src/testing/index.ts +23 -28
  75. package/src/testing/measure.ts +337 -337
@@ -1,258 +1,266 @@
1
- /**
2
- * The persisted state record.
3
- *
4
- * Six collections, holding only what the platform will not tell us. No
5
- * notification content, no push token, no desired set. The interaction buffer
6
- * is the sole content-bearing exception, and it evicts on delivery.
7
- *
8
- * Corrupt or incompatible state is discarded and rebuilt rather than blocking:
9
- * everything already scheduled lives in the OS and is unaffected, which is what
10
- * makes discarding safe rather than destructive.
11
- */
12
-
13
- import type { StateStore } from './store';
14
- import { evict, COLLECTION_CAPS, type CollectionName, type Timestamped } from './caps';
15
- import type { AnchoringMode, RoutingPayload } from '../contract/notification';
16
- import type { CollectionHealth, StateHealth } from '../contract/diagnostics';
17
- import { DIAGNOSTIC_CODES, type DiagnosticCode } from '../contract/errors';
18
-
19
- const STATE_KEY = 'chipmobilesdk.notification.state';
20
-
21
- /**
22
- * Bumped whenever the shape below changes. A mismatch discards and rebuilds —
23
- * never a migration, because none of this data is worth migrating and all of it
24
- * is reconstructible.
25
- */
26
- export const STATE_SCHEMA_VERSION = 1;
27
-
28
- export interface DedupEntry extends Timestamped {
29
- readonly dedupId: string;
30
- readonly ownerTag?: string;
31
- }
32
-
33
- export interface BufferedInteraction extends Timestamped {
34
- readonly id: string;
35
- readonly kind: 'tap' | 'action' | 'dismiss';
36
- readonly notificationId: string;
37
- readonly actionId?: string;
38
- readonly inputText?: string;
39
- /** The one content-bearing thing persisted. Evicted the moment it is delivered. */
40
- readonly routing?: RoutingPayload;
41
- readonly ownerTag?: string;
42
- }
43
-
44
- export interface MigrationEntry extends Timestamped {
45
- readonly toneId: string;
46
- readonly appliedVersion: number;
47
- readonly channelId: string;
48
- readonly retired: boolean;
49
- /**
50
- * Fingerprint of the labels last pushed to the platform.
51
- *
52
- * Optional because records written before labels were carried at all have
53
- * none — and their absence is exactly right: an unknown fingerprint refreshes
54
- * once, which is the one-time correction every existing device needs, since
55
- * every existing channel currently carries the tone id as its name.
56
- *
57
- * A fingerprint rather than the labels themselves. Persisted state holds no
58
- * consumer text, and a hash answers the only question asked of it.
59
- */
60
- readonly labelFingerprint?: string;
61
- }
62
-
63
- export interface SeriesEntry extends Timestamped {
64
- readonly seriesId: string;
65
- readonly anchoring: AnchoringMode;
66
- readonly expandedThrough: number;
67
- readonly emittedCount: number;
68
- readonly exhausted: boolean;
69
- }
70
-
71
- export interface PermissionHistoryEntry extends Timestamped {
72
- readonly key: string;
73
- readonly requested: boolean;
74
- }
75
-
76
- export interface StateRecord {
77
- readonly schemaVersion: number;
78
- readonly dedupLedger: readonly DedupEntry[];
79
- readonly interactionBuffer: readonly BufferedInteraction[];
80
- readonly migrationLedger: readonly MigrationEntry[];
81
- readonly seriesLedger: readonly SeriesEntry[];
82
- readonly permissionHistory: readonly PermissionHistoryEntry[];
83
- readonly lastReconcileSummary?: Readonly<Record<string, unknown>>;
84
- readonly lastMaintenanceSummary?: Readonly<Record<string, unknown>>;
85
- }
86
-
87
- export function emptyRecord(): StateRecord {
88
- return {
89
- schemaVersion: STATE_SCHEMA_VERSION,
90
- dedupLedger: [],
91
- interactionBuffer: [],
92
- migrationLedger: [],
93
- seriesLedger: [],
94
- permissionHistory: [],
95
- };
96
- }
97
-
98
- export interface StateEvent {
99
- readonly code: DiagnosticCode;
100
- readonly context?: Readonly<Record<string, unknown>>;
101
- }
102
-
103
- export interface StateManagerOptions {
104
- readonly store: StateStore;
105
- readonly now: () => number;
106
- readonly emit?: (event: StateEvent) => void;
107
- }
108
-
109
- /**
110
- * Owns the record's lifecycle. Reads are synchronous and cheap; writes are
111
- * best-effort and never propagate a failure to the caller's operation.
112
- */
113
- export class StateManager {
114
- private record: StateRecord;
115
- private rebuilt = false;
116
- private readonly store: StateStore;
117
- private readonly now: () => number;
118
- private readonly emit: (event: StateEvent) => void;
119
-
120
- constructor(options: StateManagerOptions) {
121
- this.store = options.store;
122
- this.now = options.now;
123
- this.emit = options.emit ?? (() => {});
124
- this.record = this.load();
125
- }
126
-
127
- private load(): StateRecord {
128
- const raw = this.store.getString(STATE_KEY);
129
- if (raw === undefined) {
130
- return emptyRecord();
131
- }
132
-
133
- try {
134
- const parsed = JSON.parse(raw) as Partial<StateRecord>;
135
- if (parsed.schemaVersion !== STATE_SCHEMA_VERSION) {
136
- return this.discardAndRebuild('schema-version-mismatch', {
137
- found: parsed.schemaVersion,
138
- expected: STATE_SCHEMA_VERSION,
139
- });
140
- }
141
- return {
142
- schemaVersion: STATE_SCHEMA_VERSION,
143
- dedupLedger: parsed.dedupLedger ?? [],
144
- interactionBuffer: parsed.interactionBuffer ?? [],
145
- migrationLedger: parsed.migrationLedger ?? [],
146
- seriesLedger: parsed.seriesLedger ?? [],
147
- permissionHistory: parsed.permissionHistory ?? [],
148
- lastReconcileSummary: parsed.lastReconcileSummary,
149
- lastMaintenanceSummary: parsed.lastMaintenanceSummary,
150
- };
151
- } catch {
152
- return this.discardAndRebuild('unparseable');
153
- }
154
- }
155
-
156
- private discardAndRebuild(
157
- reason: string,
158
- extra?: Readonly<Record<string, unknown>>,
159
- ): StateRecord {
160
- this.rebuilt = true;
161
- this.store.delete(STATE_KEY);
162
- this.emit({
163
- code: DIAGNOSTIC_CODES.STATE_CORRUPT_REBUILT,
164
- // Note what is actually lost, so the consequence is stated rather than implied.
165
- context: { reason, lost: ['dedupLedger', 'interactionBuffer'], ...extra },
166
- });
167
- return emptyRecord();
168
- }
169
-
170
- /** Never throws. A storage failure degrades the affected guarantee, nothing more. */
171
- private persist(): void {
172
- try {
173
- this.store.setString(STATE_KEY, JSON.stringify(this.record));
174
- } catch (error) {
175
- this.emit({
176
- code: DIAGNOSTIC_CODES.STATE_WRITE_FAILED,
177
- context: { reason: error instanceof Error ? error.name : 'unknown' },
178
- });
179
- }
180
- }
181
-
182
- read(): StateRecord {
183
- return this.record;
184
- }
185
-
186
- /** Applies a change, evicts the touched collection, and persists. */
187
- update<K extends CollectionName>(
188
- collection: K,
189
- mutate: (current: StateRecord[K]) => StateRecord[K],
190
- ): void {
191
- const next = mutate(this.record[collection]);
192
- const { kept, evicted, reason } = evict(next as readonly Timestamped[], collection, this.now());
193
-
194
- if (evicted.length > 0) {
195
- this.emit({
196
- code:
197
- collection === 'interactionBuffer'
198
- ? DIAGNOSTIC_CODES.INTERACTION_BUFFER_EVICTED
199
- : DIAGNOSTIC_CODES.STATE_CAP_EVICTED,
200
- context: { collection, evicted: evicted.length, reason },
201
- });
202
- }
203
-
204
- this.record = { ...this.record, [collection]: kept } as StateRecord;
205
- this.persist();
206
- }
207
-
208
- setSummary(key: 'lastReconcileSummary' | 'lastMaintenanceSummary', value: Readonly<Record<string, unknown>>): void {
209
- this.record = { ...this.record, [key]: value };
210
- this.persist();
211
- }
212
-
213
- /** Clears everything. Public through `resetState()`. */
214
- reset(): void {
215
- this.record = emptyRecord();
216
- this.rebuilt = false;
217
- this.store.delete(STATE_KEY);
218
- }
219
-
220
- /**
221
- * Clears one owner tag's entries, leaving every other tag untouched.
222
- *
223
- * Collections without an owner tag are left alone: a migration ledger and a
224
- * series ledger describe the device, not an account.
225
- */
226
- clearOwner(ownerTag: string): void {
227
- this.record = {
228
- ...this.record,
229
- dedupLedger: this.record.dedupLedger.filter(e => e.ownerTag !== ownerTag),
230
- interactionBuffer: this.record.interactionBuffer.filter(e => e.ownerTag !== ownerTag),
231
- permissionHistory: this.record.permissionHistory.filter(
232
- e => !e.key.startsWith(`${ownerTag}:`),
233
- ),
234
- };
235
- this.persist();
236
- }
237
-
238
- health(): StateHealth {
239
- const collections: CollectionHealth[] = (
240
- Object.keys(COLLECTION_CAPS) as CollectionName[]
241
- ).map(name => {
242
- const entries = this.record[name] as readonly Timestamped[];
243
- return {
244
- name,
245
- size: entries.length,
246
- cap: COLLECTION_CAPS[name].maxEntries,
247
- oldestEntryAt: entries.length > 0 ? Math.min(...entries.map(e => e.at)) : undefined,
248
- };
249
- });
250
-
251
- return {
252
- readable: true,
253
- rebuilt: this.rebuilt,
254
- collections,
255
- approximateBytes: this.store.approximateBytes(),
256
- };
257
- }
258
- }
1
+ /**
2
+ * The persisted state record.
3
+ *
4
+ * Six collections, holding only what the platform will not tell us. No
5
+ * notification content, no push token, no desired set. The interaction buffer
6
+ * is the sole content-bearing exception, and it evicts on delivery.
7
+ *
8
+ * Corrupt or incompatible state is discarded and rebuilt rather than blocking:
9
+ * everything already scheduled lives in the OS and is unaffected, which is what
10
+ * makes discarding safe rather than destructive.
11
+ */
12
+
13
+ import type { StateStore } from './store';
14
+ import { evict, COLLECTION_CAPS, type CollectionName, type Timestamped } from './caps';
15
+ import type { AnchoringMode, RoutingPayload } from '../contract/notification';
16
+ import type { CollectionHealth, StateHealth } from '../contract/diagnostics';
17
+ import { DIAGNOSTIC_CODES, type DiagnosticCode } from '../contract/errors';
18
+
19
+ const STATE_KEY = 'chipmobilesdk.notification.state';
20
+
21
+ /**
22
+ * Bumped whenever the shape below changes. A mismatch discards and rebuilds —
23
+ * never a migration, because none of this data is worth migrating and all of it
24
+ * is reconstructible.
25
+ */
26
+ export const STATE_SCHEMA_VERSION = 1;
27
+
28
+ export interface DedupEntry extends Timestamped {
29
+ readonly dedupId: string;
30
+ readonly ownerTag?: string;
31
+ }
32
+
33
+ export interface BufferedInteraction extends Timestamped {
34
+ readonly id: string;
35
+ readonly kind: 'tap' | 'action' | 'dismiss';
36
+ readonly notificationId: string;
37
+ readonly actionId?: string;
38
+ readonly inputText?: string;
39
+ /** The one content-bearing thing persisted. Evicted the moment it is delivered. */
40
+ readonly routing?: RoutingPayload;
41
+ readonly ownerTag?: string;
42
+ }
43
+
44
+ export interface MigrationEntry extends Timestamped {
45
+ readonly toneId: string;
46
+ readonly appliedVersion: number;
47
+ readonly channelId: string;
48
+ readonly retired: boolean;
49
+ /**
50
+ * Fingerprint of the labels last pushed to the platform.
51
+ *
52
+ * Optional because records written before labels were carried at all have
53
+ * none — and their absence is exactly right: an unknown fingerprint refreshes
54
+ * once, which is the one-time correction every existing device needs, since
55
+ * every existing channel currently carries the tone id as its name.
56
+ *
57
+ * A fingerprint rather than the labels themselves. Persisted state holds no
58
+ * consumer text, and a hash answers the only question asked of it.
59
+ */
60
+ readonly labelFingerprint?: string;
61
+ }
62
+
63
+ export interface SeriesEntry extends Timestamped {
64
+ readonly seriesId: string;
65
+ readonly anchoring: AnchoringMode;
66
+ readonly expandedThrough: number;
67
+ readonly emittedCount: number;
68
+ readonly exhausted: boolean;
69
+ /**
70
+ * Calendar key of the latest occurrence counted into `emittedCount`. Absent in
71
+ * entries written before 0.4.0; recovered from the device on next expansion.
72
+ */
73
+ readonly lastEmittedKey?: string;
74
+ }
75
+
76
+ export interface PermissionHistoryEntry extends Timestamped {
77
+ readonly key: string;
78
+ readonly requested: boolean;
79
+ }
80
+
81
+ export interface StateRecord {
82
+ readonly schemaVersion: number;
83
+ readonly dedupLedger: readonly DedupEntry[];
84
+ readonly interactionBuffer: readonly BufferedInteraction[];
85
+ readonly migrationLedger: readonly MigrationEntry[];
86
+ readonly seriesLedger: readonly SeriesEntry[];
87
+ readonly permissionHistory: readonly PermissionHistoryEntry[];
88
+ readonly lastReconcileSummary?: Readonly<Record<string, unknown>>;
89
+ readonly lastMaintenanceSummary?: Readonly<Record<string, unknown>>;
90
+ }
91
+
92
+ export function emptyRecord(): StateRecord {
93
+ return {
94
+ schemaVersion: STATE_SCHEMA_VERSION,
95
+ dedupLedger: [],
96
+ interactionBuffer: [],
97
+ migrationLedger: [],
98
+ seriesLedger: [],
99
+ permissionHistory: [],
100
+ };
101
+ }
102
+
103
+ export interface StateEvent {
104
+ readonly code: DiagnosticCode;
105
+ readonly context?: Readonly<Record<string, unknown>>;
106
+ }
107
+
108
+ export interface StateManagerOptions {
109
+ readonly store: StateStore;
110
+ readonly now: () => number;
111
+ readonly emit?: (event: StateEvent) => void;
112
+ }
113
+
114
+ /**
115
+ * Owns the record's lifecycle. Reads are synchronous and cheap; writes are
116
+ * best-effort and never propagate a failure to the caller's operation.
117
+ */
118
+ export class StateManager {
119
+ private record: StateRecord;
120
+ private rebuilt = false;
121
+ private readonly store: StateStore;
122
+ private readonly now: () => number;
123
+ private readonly emit: (event: StateEvent) => void;
124
+
125
+ constructor(options: StateManagerOptions) {
126
+ this.store = options.store;
127
+ this.now = options.now;
128
+ this.emit = options.emit ?? (() => {});
129
+ this.record = this.load();
130
+ }
131
+
132
+ private load(): StateRecord {
133
+ const raw = this.store.getString(STATE_KEY);
134
+ if (raw === undefined) {
135
+ return emptyRecord();
136
+ }
137
+
138
+ try {
139
+ const parsed = JSON.parse(raw) as Partial<StateRecord>;
140
+ if (parsed.schemaVersion !== STATE_SCHEMA_VERSION) {
141
+ return this.discardAndRebuild('schema-version-mismatch', {
142
+ found: parsed.schemaVersion,
143
+ expected: STATE_SCHEMA_VERSION,
144
+ });
145
+ }
146
+ return {
147
+ schemaVersion: STATE_SCHEMA_VERSION,
148
+ dedupLedger: parsed.dedupLedger ?? [],
149
+ interactionBuffer: parsed.interactionBuffer ?? [],
150
+ migrationLedger: parsed.migrationLedger ?? [],
151
+ seriesLedger: parsed.seriesLedger ?? [],
152
+ permissionHistory: parsed.permissionHistory ?? [],
153
+ lastReconcileSummary: parsed.lastReconcileSummary,
154
+ lastMaintenanceSummary: parsed.lastMaintenanceSummary,
155
+ };
156
+ } catch {
157
+ return this.discardAndRebuild('unparseable');
158
+ }
159
+ }
160
+
161
+ private discardAndRebuild(
162
+ reason: string,
163
+ extra?: Readonly<Record<string, unknown>>,
164
+ ): StateRecord {
165
+ this.rebuilt = true;
166
+ this.store.delete(STATE_KEY);
167
+ this.emit({
168
+ code: DIAGNOSTIC_CODES.STATE_CORRUPT_REBUILT,
169
+ // Note what is actually lost, so the consequence is stated rather than implied.
170
+ context: { reason, lost: ['dedupLedger', 'interactionBuffer'], ...extra },
171
+ });
172
+ return emptyRecord();
173
+ }
174
+
175
+ /** Never throws. A storage failure degrades the affected guarantee, nothing more. */
176
+ private persist(): void {
177
+ try {
178
+ this.store.setString(STATE_KEY, JSON.stringify(this.record));
179
+ } catch (error) {
180
+ this.emit({
181
+ code: DIAGNOSTIC_CODES.STATE_WRITE_FAILED,
182
+ context: { reason: error instanceof Error ? error.name : 'unknown' },
183
+ });
184
+ }
185
+ }
186
+
187
+ read(): StateRecord {
188
+ return this.record;
189
+ }
190
+
191
+ /** Applies a change, evicts the touched collection, and persists. */
192
+ update<K extends CollectionName>(
193
+ collection: K,
194
+ mutate: (current: StateRecord[K]) => StateRecord[K],
195
+ ): void {
196
+ const next = mutate(this.record[collection]);
197
+ const { kept, evicted, reason } = evict(next as readonly Timestamped[], collection, this.now());
198
+
199
+ if (evicted.length > 0) {
200
+ this.emit({
201
+ code:
202
+ collection === 'interactionBuffer'
203
+ ? DIAGNOSTIC_CODES.INTERACTION_BUFFER_EVICTED
204
+ : DIAGNOSTIC_CODES.STATE_CAP_EVICTED,
205
+ context: { collection, evicted: evicted.length, reason },
206
+ });
207
+ }
208
+
209
+ this.record = { ...this.record, [collection]: kept } as StateRecord;
210
+ this.persist();
211
+ }
212
+
213
+ setSummary(
214
+ key: 'lastReconcileSummary' | 'lastMaintenanceSummary',
215
+ value: Readonly<Record<string, unknown>>,
216
+ ): void {
217
+ this.record = { ...this.record, [key]: value };
218
+ this.persist();
219
+ }
220
+
221
+ /** Clears everything. Public through `resetState()`. */
222
+ reset(): void {
223
+ this.record = emptyRecord();
224
+ this.rebuilt = false;
225
+ this.store.delete(STATE_KEY);
226
+ }
227
+
228
+ /**
229
+ * Clears one owner tag's entries, leaving every other tag untouched.
230
+ *
231
+ * Collections without an owner tag are left alone: a migration ledger and a
232
+ * series ledger describe the device, not an account.
233
+ */
234
+ clearOwner(ownerTag: string): void {
235
+ this.record = {
236
+ ...this.record,
237
+ dedupLedger: this.record.dedupLedger.filter(e => e.ownerTag !== ownerTag),
238
+ interactionBuffer: this.record.interactionBuffer.filter(e => e.ownerTag !== ownerTag),
239
+ permissionHistory: this.record.permissionHistory.filter(
240
+ e => !e.key.startsWith(`${ownerTag}:`),
241
+ ),
242
+ };
243
+ this.persist();
244
+ }
245
+
246
+ health(): StateHealth {
247
+ const collections: CollectionHealth[] = (Object.keys(COLLECTION_CAPS) as CollectionName[]).map(
248
+ name => {
249
+ const entries = this.record[name] as readonly Timestamped[];
250
+ return {
251
+ name,
252
+ size: entries.length,
253
+ cap: COLLECTION_CAPS[name].maxEntries,
254
+ oldestEntryAt: entries.length > 0 ? Math.min(...entries.map(e => e.at)) : undefined,
255
+ };
256
+ },
257
+ );
258
+
259
+ return {
260
+ readable: true,
261
+ rebuilt: this.rebuilt,
262
+ collections,
263
+ approximateBytes: this.store.approximateBytes(),
264
+ };
265
+ }
266
+ }
@@ -1,41 +1,41 @@
1
- /**
2
- * Public state reset and per-owner clearing.
3
- *
4
- * These are the only two ways a consumer may touch the persisted record. The
5
- * record itself is unreachable — a consumer can discard state, but can never
6
- * read or write it.
7
- */
8
-
9
- import type { StateManager } from './record';
10
-
11
- export interface ResetOutcome {
12
- readonly clearedCollections: readonly string[];
13
- }
14
-
15
- /** Discards the entire record. Nothing already scheduled with the OS is affected. */
16
- export function resetState(state: StateManager): ResetOutcome {
17
- state.reset();
18
- return {
19
- clearedCollections: [
20
- 'dedupLedger',
21
- 'interactionBuffer',
22
- 'migrationLedger',
23
- 'seriesLedger',
24
- 'permissionHistory',
25
- ],
26
- };
27
- }
28
-
29
- /**
30
- * Clears one owner tag's entries on sign-out.
31
- *
32
- * Deliberately narrower than a full reset: the migration and series ledgers
33
- * describe the device rather than an account, so signing one account out must
34
- * not discard channel migration history that every account depends on.
35
- */
36
- export function clearOwner(state: StateManager, ownerTag: string): ResetOutcome {
37
- state.clearOwner(ownerTag);
38
- return {
39
- clearedCollections: ['dedupLedger', 'interactionBuffer', 'permissionHistory'],
40
- };
41
- }
1
+ /**
2
+ * Public state reset and per-owner clearing.
3
+ *
4
+ * These are the only two ways a consumer may touch the persisted record. The
5
+ * record itself is unreachable — a consumer can discard state, but can never
6
+ * read or write it.
7
+ */
8
+
9
+ import type { StateManager } from './record';
10
+
11
+ export interface ResetOutcome {
12
+ readonly clearedCollections: readonly string[];
13
+ }
14
+
15
+ /** Discards the entire record. Nothing already scheduled with the OS is affected. */
16
+ export function resetState(state: StateManager): ResetOutcome {
17
+ state.reset();
18
+ return {
19
+ clearedCollections: [
20
+ 'dedupLedger',
21
+ 'interactionBuffer',
22
+ 'migrationLedger',
23
+ 'seriesLedger',
24
+ 'permissionHistory',
25
+ ],
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Clears one owner tag's entries on sign-out.
31
+ *
32
+ * Deliberately narrower than a full reset: the migration and series ledgers
33
+ * describe the device rather than an account, so signing one account out must
34
+ * not discard channel migration history that every account depends on.
35
+ */
36
+ export function clearOwner(state: StateManager, ownerTag: string): ResetOutcome {
37
+ state.clearOwner(ownerTag);
38
+ return {
39
+ clearedCollections: ['dedupLedger', 'interactionBuffer', 'permissionHistory'],
40
+ };
41
+ }