@chipmobilesdk/rn-notification 0.4.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 -166
- package/README.md +414 -410
- package/package.json +58 -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 +134 -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 +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 -89
- package/src/engine/notify-kit-engine.ts +706 -707
- package/src/index.ts +208 -208
- package/src/initialize.ts +240 -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 +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 -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 +278 -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 +73 -73
- package/src/state/mmkv-store.ts +75 -75
- package/src/state/permission-history.ts +41 -41
- package/src/state/record.ts +266 -263
- package/src/state/reset.ts +41 -41
- package/src/state/series-ledger.ts +91 -91
- package/src/state/store.ts +40 -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/initialize.ts
CHANGED
|
@@ -1,236 +1,240 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Package initialization.
|
|
3
|
-
*
|
|
4
|
-
* Composes the parts that must be in place before the first schedule call:
|
|
5
|
-
* validate configuration, load persisted state, apply tone declarations, run
|
|
6
|
-
* any pending channel migration, and start the maintenance registry.
|
|
7
|
-
*
|
|
8
|
-
* Budgeted at 100 ms of app startup, which is why state loading is synchronous
|
|
9
|
-
* and tone declaration is the only platform round-trip.
|
|
10
|
-
*
|
|
11
|
-
* Automatic maintenance is started here too: whatever steps are registered on
|
|
12
|
-
* `runtime.registry` run when the app returns to the foreground, with the
|
|
13
|
-
* trigger reported as `timeZoneChange` when the device zone moved meanwhile.
|
|
14
|
-
* Until 0.4.0 nothing ever called `runAutomatic`, so "automatic" maintenance
|
|
15
|
-
* only ran when the app invoked it by hand.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { AppState } from 'react-native';
|
|
19
|
-
import type { InitializeResult, NotificationConfig } from './contract/diagnostics';
|
|
20
|
-
import { DEFAULT_LOOKAHEAD, LOOKAHEAD_BOUNDS, type LookaheadConfig } from './contract/recurrence';
|
|
21
|
-
import type { MaintenanceResult } from './contract/reconcile';
|
|
22
|
-
import { DIAGNOSTIC_CODES, NotificationError, type DiagnosticCode } from './contract/errors';
|
|
23
|
-
import { DiagnosticEmitter } from './diagnostics/events';
|
|
24
|
-
import { MaintenanceRegistry } from './maintenance/cycle';
|
|
25
|
-
import { MaintenanceController } from './maintenance/toggle';
|
|
26
|
-
import { StateManager } from './state/record';
|
|
27
|
-
import { ownedIdentifiers } from './state/series-ledger';
|
|
28
|
-
import type { StateStore } from './state/store';
|
|
29
|
-
import type { NotificationEngineAdapter } from './engine/adapter';
|
|
30
|
-
import { createChannels, type Channels } from './channel/migrate';
|
|
31
|
-
import { createPlatformTimeZoneReader, type TimeZoneReader } from './schedule/anchor';
|
|
32
|
-
|
|
33
|
-
/** The slice of React Native's `AppState` the lifecycle observer uses. */
|
|
34
|
-
export interface AppStateSource {
|
|
35
|
-
addEventListener(type: 'change', listener: (state: string) => void): { remove(): void };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface RuntimeDependencies {
|
|
39
|
-
readonly engine: NotificationEngineAdapter;
|
|
40
|
-
readonly store: StateStore;
|
|
41
|
-
readonly now?: () => number;
|
|
42
|
-
/** Injected in tests; defaults to React Native's `AppState`. */
|
|
43
|
-
readonly appState?: AppStateSource;
|
|
44
|
-
/** Injected in tests; defaults to the platform zone reader. */
|
|
45
|
-
readonly timeZone?: TimeZoneReader;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Everything assembled at initialization, shared by the operation modules. */
|
|
49
|
-
export interface NotificationRuntime {
|
|
50
|
-
readonly engine: NotificationEngineAdapter;
|
|
51
|
-
readonly state: StateManager;
|
|
52
|
-
readonly emitter: DiagnosticEmitter;
|
|
53
|
-
readonly registry: MaintenanceRegistry;
|
|
54
|
-
readonly maintenance: MaintenanceController;
|
|
55
|
-
/** The channel module that applied `config.tones`. Added in 0.4.0. */
|
|
56
|
-
readonly channels: Channels;
|
|
57
|
-
readonly lookahead: LookaheadConfig;
|
|
58
|
-
readonly config: NotificationConfig;
|
|
59
|
-
readonly now: () => number;
|
|
60
|
-
/** The result of the latest automatic cycle, if one has run. Added in 0.4.0. */
|
|
61
|
-
readonly lastAutomaticMaintenance: () => MaintenanceResult | undefined;
|
|
62
|
-
/** Stops the lifecycle observer. Added in 0.4.0. */
|
|
63
|
-
dispose(): void;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Clamps the lookahead into its documented bounds.
|
|
68
|
-
*
|
|
69
|
-
* Bounded on adjustment because the platform ceiling is not negotiable: a
|
|
70
|
-
* consumer asking for a five-year window would silently exhaust the 64-entry
|
|
71
|
-
* iOS ceiling on the first frequent series.
|
|
72
|
-
*/
|
|
73
|
-
export function resolveLookahead(partial: Partial<LookaheadConfig> | undefined): LookaheadConfig {
|
|
74
|
-
const days = partial?.days ?? DEFAULT_LOOKAHEAD.days;
|
|
75
|
-
const occurrencesPerSeries =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
LOOKAHEAD_BOUNDS.
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
case DIAGNOSTIC_CODES.
|
|
230
|
-
return '
|
|
231
|
-
case DIAGNOSTIC_CODES.
|
|
232
|
-
return '
|
|
233
|
-
|
|
234
|
-
return '
|
|
235
|
-
|
|
236
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Package initialization.
|
|
3
|
+
*
|
|
4
|
+
* Composes the parts that must be in place before the first schedule call:
|
|
5
|
+
* validate configuration, load persisted state, apply tone declarations, run
|
|
6
|
+
* any pending channel migration, and start the maintenance registry.
|
|
7
|
+
*
|
|
8
|
+
* Budgeted at 100 ms of app startup, which is why state loading is synchronous
|
|
9
|
+
* and tone declaration is the only platform round-trip.
|
|
10
|
+
*
|
|
11
|
+
* Automatic maintenance is started here too: whatever steps are registered on
|
|
12
|
+
* `runtime.registry` run when the app returns to the foreground, with the
|
|
13
|
+
* trigger reported as `timeZoneChange` when the device zone moved meanwhile.
|
|
14
|
+
* Until 0.4.0 nothing ever called `runAutomatic`, so "automatic" maintenance
|
|
15
|
+
* only ran when the app invoked it by hand.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { AppState } from 'react-native';
|
|
19
|
+
import type { InitializeResult, NotificationConfig } from './contract/diagnostics';
|
|
20
|
+
import { DEFAULT_LOOKAHEAD, LOOKAHEAD_BOUNDS, type LookaheadConfig } from './contract/recurrence';
|
|
21
|
+
import type { MaintenanceResult } from './contract/reconcile';
|
|
22
|
+
import { DIAGNOSTIC_CODES, NotificationError, type DiagnosticCode } from './contract/errors';
|
|
23
|
+
import { DiagnosticEmitter } from './diagnostics/events';
|
|
24
|
+
import { MaintenanceRegistry } from './maintenance/cycle';
|
|
25
|
+
import { MaintenanceController } from './maintenance/toggle';
|
|
26
|
+
import { StateManager } from './state/record';
|
|
27
|
+
import { ownedIdentifiers } from './state/series-ledger';
|
|
28
|
+
import type { StateStore } from './state/store';
|
|
29
|
+
import type { NotificationEngineAdapter } from './engine/adapter';
|
|
30
|
+
import { createChannels, type Channels } from './channel/migrate';
|
|
31
|
+
import { createPlatformTimeZoneReader, type TimeZoneReader } from './schedule/anchor';
|
|
32
|
+
|
|
33
|
+
/** The slice of React Native's `AppState` the lifecycle observer uses. */
|
|
34
|
+
export interface AppStateSource {
|
|
35
|
+
addEventListener(type: 'change', listener: (state: string) => void): { remove(): void };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RuntimeDependencies {
|
|
39
|
+
readonly engine: NotificationEngineAdapter;
|
|
40
|
+
readonly store: StateStore;
|
|
41
|
+
readonly now?: () => number;
|
|
42
|
+
/** Injected in tests; defaults to React Native's `AppState`. */
|
|
43
|
+
readonly appState?: AppStateSource;
|
|
44
|
+
/** Injected in tests; defaults to the platform zone reader. */
|
|
45
|
+
readonly timeZone?: TimeZoneReader;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Everything assembled at initialization, shared by the operation modules. */
|
|
49
|
+
export interface NotificationRuntime {
|
|
50
|
+
readonly engine: NotificationEngineAdapter;
|
|
51
|
+
readonly state: StateManager;
|
|
52
|
+
readonly emitter: DiagnosticEmitter;
|
|
53
|
+
readonly registry: MaintenanceRegistry;
|
|
54
|
+
readonly maintenance: MaintenanceController;
|
|
55
|
+
/** The channel module that applied `config.tones`. Added in 0.4.0. */
|
|
56
|
+
readonly channels: Channels;
|
|
57
|
+
readonly lookahead: LookaheadConfig;
|
|
58
|
+
readonly config: NotificationConfig;
|
|
59
|
+
readonly now: () => number;
|
|
60
|
+
/** The result of the latest automatic cycle, if one has run. Added in 0.4.0. */
|
|
61
|
+
readonly lastAutomaticMaintenance: () => MaintenanceResult | undefined;
|
|
62
|
+
/** Stops the lifecycle observer. Added in 0.4.0. */
|
|
63
|
+
dispose(): void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Clamps the lookahead into its documented bounds.
|
|
68
|
+
*
|
|
69
|
+
* Bounded on adjustment because the platform ceiling is not negotiable: a
|
|
70
|
+
* consumer asking for a five-year window would silently exhaust the 64-entry
|
|
71
|
+
* iOS ceiling on the first frequent series.
|
|
72
|
+
*/
|
|
73
|
+
export function resolveLookahead(partial: Partial<LookaheadConfig> | undefined): LookaheadConfig {
|
|
74
|
+
const days = partial?.days ?? DEFAULT_LOOKAHEAD.days;
|
|
75
|
+
const occurrencesPerSeries =
|
|
76
|
+
partial?.occurrencesPerSeries ?? DEFAULT_LOOKAHEAD.occurrencesPerSeries;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
days: Math.min(Math.max(days, LOOKAHEAD_BOUNDS.minDays), LOOKAHEAD_BOUNDS.maxDays),
|
|
80
|
+
occurrencesPerSeries: Math.min(
|
|
81
|
+
Math.max(occurrencesPerSeries, LOOKAHEAD_BOUNDS.minOccurrences),
|
|
82
|
+
LOOKAHEAD_BOUNDS.maxOccurrences,
|
|
83
|
+
),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Fails before any platform call, so a bad configuration never half-applies. */
|
|
88
|
+
export function validateConfig(config: NotificationConfig): void {
|
|
89
|
+
if (config.tones.length === 0) {
|
|
90
|
+
throw new NotificationError(
|
|
91
|
+
DIAGNOSTIC_CODES.UNDECLARED_TONE,
|
|
92
|
+
'At least one tone must be declared before notifications can be scheduled.',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const seen = new Set<string>();
|
|
97
|
+
for (const tone of config.tones) {
|
|
98
|
+
if (seen.has(tone.id)) {
|
|
99
|
+
throw new NotificationError(
|
|
100
|
+
DIAGNOSTIC_CODES.UNDECLARED_TONE,
|
|
101
|
+
`Tone "${tone.id}" is declared more than once.`,
|
|
102
|
+
{ toneId: tone.id },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
seen.add(tone.id);
|
|
106
|
+
|
|
107
|
+
if (!Number.isInteger(tone.version) || tone.version < 1) {
|
|
108
|
+
throw new NotificationError(
|
|
109
|
+
DIAGNOSTIC_CODES.CHANNEL_MIGRATION_REQUIRED,
|
|
110
|
+
`Tone "${tone.id}" must carry an integer version of 1 or greater, because a channel configuration change is a migration rather than an assignment.`,
|
|
111
|
+
{ toneId: tone.id, version: tone.version },
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const groupIds = new Set((config.channelGroups ?? []).map(group => group.id));
|
|
117
|
+
for (const tone of config.tones) {
|
|
118
|
+
if (tone.groupId !== undefined && !groupIds.has(tone.groupId)) {
|
|
119
|
+
throw new NotificationError(
|
|
120
|
+
DIAGNOSTIC_CODES.UNDECLARED_TONE,
|
|
121
|
+
`Tone "${tone.id}" references undeclared channel group "${tone.groupId}".`,
|
|
122
|
+
{ toneId: tone.id, groupId: tone.groupId },
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function initialize(
|
|
129
|
+
config: NotificationConfig,
|
|
130
|
+
deps: RuntimeDependencies,
|
|
131
|
+
): Promise<{ runtime: NotificationRuntime; result: InitializeResult }> {
|
|
132
|
+
const now = deps.now ?? (() => Date.now());
|
|
133
|
+
const startedAt = now();
|
|
134
|
+
|
|
135
|
+
validateConfig(config);
|
|
136
|
+
|
|
137
|
+
const emitter = new DiagnosticEmitter({
|
|
138
|
+
sink: config.eventSink,
|
|
139
|
+
verbose: config.verboseDiagnostics,
|
|
140
|
+
now,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const state = new StateManager({
|
|
144
|
+
store: deps.store,
|
|
145
|
+
now,
|
|
146
|
+
emit: event => emitter.emit(event.code, describeStateEvent(event.code), event.context),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const channels = createChannels({
|
|
150
|
+
engine: deps.engine,
|
|
151
|
+
state,
|
|
152
|
+
now,
|
|
153
|
+
// The channel module emits only DIAGNOSTIC_CODES values; its port types the
|
|
154
|
+
// code as a string so it stays usable without the emitter.
|
|
155
|
+
emit: (code, message, context) => emitter.emit(code as DiagnosticCode, message, context),
|
|
156
|
+
});
|
|
157
|
+
// Declares groups and tones, and runs any version migration. An unchanged
|
|
158
|
+
// configuration costs no platform work, so doing this on every launch is free.
|
|
159
|
+
const applied = await channels.apply(config.tones, config.channelGroups ?? []);
|
|
160
|
+
|
|
161
|
+
const registry = new MaintenanceRegistry({ clock: now });
|
|
162
|
+
const maintenance = new MaintenanceController({
|
|
163
|
+
registry,
|
|
164
|
+
now,
|
|
165
|
+
// Occurrence identifiers, not series identifiers. Maintenance cancels
|
|
166
|
+
// occurrences, so a set of series ids made every legitimate cancel look
|
|
167
|
+
// like a scope violation.
|
|
168
|
+
ownedIds: async () =>
|
|
169
|
+
ownedIdentifiers(
|
|
170
|
+
state,
|
|
171
|
+
(await deps.engine.getPending()).map(entry => entry.id),
|
|
172
|
+
),
|
|
173
|
+
enabled: config.automaticMaintenance ?? true,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const timeZone = deps.timeZone ?? createPlatformTimeZoneReader();
|
|
177
|
+
let lastZone = timeZone();
|
|
178
|
+
let lastAutomatic: MaintenanceResult | undefined;
|
|
179
|
+
const onAppState = (next: string) => {
|
|
180
|
+
if (next !== 'active') return;
|
|
181
|
+
const zone = timeZone();
|
|
182
|
+
const trigger = zone !== lastZone ? 'timeZoneChange' : 'foreground';
|
|
183
|
+
lastZone = zone;
|
|
184
|
+
maintenance.runAutomatic(trigger).then(
|
|
185
|
+
result => {
|
|
186
|
+
if (result !== undefined) lastAutomatic = result;
|
|
187
|
+
},
|
|
188
|
+
error => {
|
|
189
|
+
// Nothing awaits an automatic cycle, so a failure is reported here or
|
|
190
|
+
// not at all.
|
|
191
|
+
emitter.emit(
|
|
192
|
+
DIAGNOSTIC_CODES.MAINTENANCE_FAILED,
|
|
193
|
+
`Automatic maintenance (${trigger}) failed: ${error instanceof Error ? error.name : 'unknown error'}.`,
|
|
194
|
+
{ trigger },
|
|
195
|
+
);
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
const subscription = (deps.appState ?? AppState).addEventListener('change', onAppState);
|
|
200
|
+
|
|
201
|
+
const runtime: NotificationRuntime = {
|
|
202
|
+
engine: deps.engine,
|
|
203
|
+
state,
|
|
204
|
+
emitter,
|
|
205
|
+
registry,
|
|
206
|
+
maintenance,
|
|
207
|
+
channels,
|
|
208
|
+
lookahead: resolveLookahead(config.lookahead),
|
|
209
|
+
config,
|
|
210
|
+
now,
|
|
211
|
+
lastAutomaticMaintenance: () => lastAutomatic,
|
|
212
|
+
dispose: () => subscription.remove(),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
runtime,
|
|
217
|
+
result: {
|
|
218
|
+
tonesApplied: config.tones.length,
|
|
219
|
+
migrationsRun: applied.migrated.length,
|
|
220
|
+
stateRebuilt: state.health().rebuilt,
|
|
221
|
+
automaticMaintenance: maintenance.isEnabled,
|
|
222
|
+
durationMs: now() - startedAt,
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function describeStateEvent(code: string): string {
|
|
228
|
+
switch (code) {
|
|
229
|
+
case DIAGNOSTIC_CODES.STATE_CORRUPT_REBUILT:
|
|
230
|
+
return 'Persisted state was unreadable and has been rebuilt. Deduplication history and buffered interactions from before the loss are gone; scheduled notifications are unaffected.';
|
|
231
|
+
case DIAGNOSTIC_CODES.STATE_WRITE_FAILED:
|
|
232
|
+
return 'Persisted state could not be written. The affected guarantee degrades to this session only.';
|
|
233
|
+
case DIAGNOSTIC_CODES.STATE_CAP_EVICTED:
|
|
234
|
+
return 'A persisted collection reached its cap and the oldest entries were evicted.';
|
|
235
|
+
case DIAGNOSTIC_CODES.INTERACTION_BUFFER_EVICTED:
|
|
236
|
+
return 'Buffered interactions were evicted at the cap or retention limit.';
|
|
237
|
+
default:
|
|
238
|
+
return 'Persisted state event.';
|
|
239
|
+
}
|
|
240
|
+
}
|