@chipmobilesdk/rn-notification 0.1.2 → 0.2.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 +80 -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 +71 -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 +336 -333
package/src/index.ts
CHANGED
|
@@ -52,6 +52,7 @@ export type {
|
|
|
52
52
|
// Channel
|
|
53
53
|
ToneUrgency,
|
|
54
54
|
ToneSound,
|
|
55
|
+
ToneRepeatAlert,
|
|
55
56
|
ToneDeclaration,
|
|
56
57
|
ChannelGroup,
|
|
57
58
|
ChannelApplyResult,
|
|
@@ -80,6 +81,7 @@ export type {
|
|
|
80
81
|
export {
|
|
81
82
|
DEFAULT_ANCHORING_MODE,
|
|
82
83
|
DEFAULT_LOOKAHEAD,
|
|
84
|
+
REPEAT_ALERT_BOUNDS,
|
|
83
85
|
LOOKAHEAD_BOUNDS,
|
|
84
86
|
INTERACTION_BUFFER_LIMITS,
|
|
85
87
|
MAX_ROUTING_PAYLOAD_BYTES,
|
|
@@ -100,8 +102,11 @@ export type { DiagnosticCode } from './contract';
|
|
|
100
102
|
*/
|
|
101
103
|
export type {
|
|
102
104
|
NotificationEngineAdapter,
|
|
105
|
+
DeliveryEvent,
|
|
103
106
|
EngineCapabilities,
|
|
104
107
|
EngineScheduleRequest,
|
|
108
|
+
RepeatStop,
|
|
109
|
+
ToneDeclarationOutcome,
|
|
105
110
|
} from './engine/adapter';
|
|
106
111
|
|
|
107
112
|
/** The default engine. Consumers may substitute their own via the port above. */
|
|
@@ -128,6 +133,25 @@ export { occurrenceId } from './recurrence/expand';
|
|
|
128
133
|
export { createChannels } from './channel/migrate';
|
|
129
134
|
export type { Channels, ChannelDependencies } from './channel/migrate';
|
|
130
135
|
export { channelIdFor, createToneResolver, parseChannelId, resolveTone } from './channel/tone-map';
|
|
136
|
+
export { validateToneDeclaration, validateToneDeclarations } from './channel/validate-tone';
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Repeat expiry — what bounds an alert that repeats until it is handled.
|
|
140
|
+
*
|
|
141
|
+
* `registerBackgroundNotificationHandler` is the one piece a consumer wires up
|
|
142
|
+
* themselves, at module scope in `index.js`. An app that declares no repeating
|
|
143
|
+
* tone needs none of this.
|
|
144
|
+
*/
|
|
145
|
+
export {
|
|
146
|
+
createRepeatExpiryHandler,
|
|
147
|
+
registerBackgroundNotificationHandler,
|
|
148
|
+
REPEAT_STOP_PREFIX,
|
|
149
|
+
} from './repeat';
|
|
150
|
+
export type {
|
|
151
|
+
BackgroundHandlerRegistration,
|
|
152
|
+
RepeatExpiryHandler,
|
|
153
|
+
RepeatExpiryDependencies,
|
|
154
|
+
} from './repeat';
|
|
131
155
|
|
|
132
156
|
/** Interaction routing — one handler for foreground, background, and cold start. */
|
|
133
157
|
export { createInteractions } from './interaction';
|
package/src/initialize.ts
CHANGED
|
@@ -1,174 +1,174 @@
|
|
|
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
|
-
|
|
12
|
-
import type { InitializeResult, NotificationConfig } from './contract/diagnostics';
|
|
13
|
-
import { DEFAULT_LOOKAHEAD, LOOKAHEAD_BOUNDS, type LookaheadConfig } from './contract/recurrence';
|
|
14
|
-
import { DIAGNOSTIC_CODES, NotificationError } from './contract/errors';
|
|
15
|
-
import { DiagnosticEmitter } from './diagnostics/events';
|
|
16
|
-
import { MaintenanceRegistry } from './maintenance/cycle';
|
|
17
|
-
import { MaintenanceController } from './maintenance/toggle';
|
|
18
|
-
import { StateManager } from './state/record';
|
|
19
|
-
import type { StateStore } from './state/store';
|
|
20
|
-
import type { NotificationEngineAdapter } from './engine/adapter';
|
|
21
|
-
|
|
22
|
-
export interface RuntimeDependencies {
|
|
23
|
-
readonly engine: NotificationEngineAdapter;
|
|
24
|
-
readonly store: StateStore;
|
|
25
|
-
readonly now?: () => number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Everything assembled at initialization, shared by the operation modules. */
|
|
29
|
-
export interface NotificationRuntime {
|
|
30
|
-
readonly engine: NotificationEngineAdapter;
|
|
31
|
-
readonly state: StateManager;
|
|
32
|
-
readonly emitter: DiagnosticEmitter;
|
|
33
|
-
readonly registry: MaintenanceRegistry;
|
|
34
|
-
readonly maintenance: MaintenanceController;
|
|
35
|
-
readonly lookahead: LookaheadConfig;
|
|
36
|
-
readonly config: NotificationConfig;
|
|
37
|
-
readonly now: () => number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Clamps the lookahead into its documented bounds.
|
|
42
|
-
*
|
|
43
|
-
* Bounded on adjustment because the platform ceiling is not negotiable: a
|
|
44
|
-
* consumer asking for a five-year window would silently exhaust the 64-entry
|
|
45
|
-
* iOS ceiling on the first frequent series.
|
|
46
|
-
*/
|
|
47
|
-
export function resolveLookahead(partial: Partial<LookaheadConfig> | undefined): LookaheadConfig {
|
|
48
|
-
const days = partial?.days ?? DEFAULT_LOOKAHEAD.days;
|
|
49
|
-
const occurrencesPerSeries = partial?.occurrencesPerSeries ?? DEFAULT_LOOKAHEAD.occurrencesPerSeries;
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
days: Math.min(Math.max(days, LOOKAHEAD_BOUNDS.minDays), LOOKAHEAD_BOUNDS.maxDays),
|
|
53
|
-
occurrencesPerSeries: Math.min(
|
|
54
|
-
Math.max(occurrencesPerSeries, LOOKAHEAD_BOUNDS.minOccurrences),
|
|
55
|
-
LOOKAHEAD_BOUNDS.maxOccurrences,
|
|
56
|
-
),
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Fails before any platform call, so a bad configuration never half-applies. */
|
|
61
|
-
export function validateConfig(config: NotificationConfig): void {
|
|
62
|
-
if (config.tones.length === 0) {
|
|
63
|
-
throw new NotificationError(
|
|
64
|
-
DIAGNOSTIC_CODES.UNDECLARED_TONE,
|
|
65
|
-
'At least one tone must be declared before notifications can be scheduled.',
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const seen = new Set<string>();
|
|
70
|
-
for (const tone of config.tones) {
|
|
71
|
-
if (seen.has(tone.id)) {
|
|
72
|
-
throw new NotificationError(
|
|
73
|
-
DIAGNOSTIC_CODES.UNDECLARED_TONE,
|
|
74
|
-
`Tone "${tone.id}" is declared more than once.`,
|
|
75
|
-
{ toneId: tone.id },
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
seen.add(tone.id);
|
|
79
|
-
|
|
80
|
-
if (!Number.isInteger(tone.version) || tone.version < 1) {
|
|
81
|
-
throw new NotificationError(
|
|
82
|
-
DIAGNOSTIC_CODES.CHANNEL_MIGRATION_REQUIRED,
|
|
83
|
-
`Tone "${tone.id}" must carry an integer version of 1 or greater, because a channel configuration change is a migration rather than an assignment.`,
|
|
84
|
-
{ toneId: tone.id, version: tone.version },
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const groupIds = new Set((config.channelGroups ?? []).map(group => group.id));
|
|
90
|
-
for (const tone of config.tones) {
|
|
91
|
-
if (tone.groupId !== undefined && !groupIds.has(tone.groupId)) {
|
|
92
|
-
throw new NotificationError(
|
|
93
|
-
DIAGNOSTIC_CODES.UNDECLARED_TONE,
|
|
94
|
-
`Tone "${tone.id}" references undeclared channel group "${tone.groupId}".`,
|
|
95
|
-
{ toneId: tone.id, groupId: tone.groupId },
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export async function initialize(
|
|
102
|
-
config: NotificationConfig,
|
|
103
|
-
deps: RuntimeDependencies,
|
|
104
|
-
): Promise<{ runtime: NotificationRuntime; result: InitializeResult }> {
|
|
105
|
-
const now = deps.now ?? (() => Date.now());
|
|
106
|
-
const startedAt = now();
|
|
107
|
-
|
|
108
|
-
validateConfig(config);
|
|
109
|
-
|
|
110
|
-
const emitter = new DiagnosticEmitter({
|
|
111
|
-
sink: config.eventSink,
|
|
112
|
-
verbose: config.verboseDiagnostics,
|
|
113
|
-
now,
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const state = new StateManager({
|
|
117
|
-
store: deps.store,
|
|
118
|
-
now,
|
|
119
|
-
emit: event => emitter.emit(event.code, describeStateEvent(event.code), event.context),
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
if (config.channelGroups !== undefined && config.channelGroups.length > 0) {
|
|
123
|
-
await deps.engine.declareGroups(config.channelGroups);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const registry = new MaintenanceRegistry();
|
|
127
|
-
const maintenance = new MaintenanceController({
|
|
128
|
-
registry,
|
|
129
|
-
now,
|
|
130
|
-
// Owned identifiers come from the series ledger: those are the entries
|
|
131
|
-
// automatic maintenance created and may therefore retire.
|
|
132
|
-
ownedIds: () => new Set(state.read().seriesLedger.map(entry => entry.seriesId)),
|
|
133
|
-
enabled: config.automaticMaintenance ?? true,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const runtime: NotificationRuntime = {
|
|
137
|
-
engine: deps.engine,
|
|
138
|
-
state,
|
|
139
|
-
emitter,
|
|
140
|
-
registry,
|
|
141
|
-
maintenance,
|
|
142
|
-
lookahead: resolveLookahead(config.lookahead),
|
|
143
|
-
config,
|
|
144
|
-
now,
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
runtime,
|
|
149
|
-
result: {
|
|
150
|
-
// Tone application and migration are performed by the channel module,
|
|
151
|
-
// which registers itself during User Story 5.
|
|
152
|
-
tonesApplied: 0,
|
|
153
|
-
migrationsRun: 0,
|
|
154
|
-
stateRebuilt: state.health().rebuilt,
|
|
155
|
-
automaticMaintenance: maintenance.isEnabled,
|
|
156
|
-
durationMs: now() - startedAt,
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function describeStateEvent(code: string): string {
|
|
162
|
-
switch (code) {
|
|
163
|
-
case DIAGNOSTIC_CODES.STATE_CORRUPT_REBUILT:
|
|
164
|
-
return 'Persisted state was unreadable and has been rebuilt. Deduplication history and buffered interactions from before the loss are gone; scheduled notifications are unaffected.';
|
|
165
|
-
case DIAGNOSTIC_CODES.STATE_WRITE_FAILED:
|
|
166
|
-
return 'Persisted state could not be written. The affected guarantee degrades to this session only.';
|
|
167
|
-
case DIAGNOSTIC_CODES.STATE_CAP_EVICTED:
|
|
168
|
-
return 'A persisted collection reached its cap and the oldest entries were evicted.';
|
|
169
|
-
case DIAGNOSTIC_CODES.INTERACTION_BUFFER_EVICTED:
|
|
170
|
-
return 'Buffered interactions were evicted at the cap or retention limit.';
|
|
171
|
-
default:
|
|
172
|
-
return 'Persisted state event.';
|
|
173
|
-
}
|
|
174
|
-
}
|
|
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
|
+
|
|
12
|
+
import type { InitializeResult, NotificationConfig } from './contract/diagnostics';
|
|
13
|
+
import { DEFAULT_LOOKAHEAD, LOOKAHEAD_BOUNDS, type LookaheadConfig } from './contract/recurrence';
|
|
14
|
+
import { DIAGNOSTIC_CODES, NotificationError } from './contract/errors';
|
|
15
|
+
import { DiagnosticEmitter } from './diagnostics/events';
|
|
16
|
+
import { MaintenanceRegistry } from './maintenance/cycle';
|
|
17
|
+
import { MaintenanceController } from './maintenance/toggle';
|
|
18
|
+
import { StateManager } from './state/record';
|
|
19
|
+
import type { StateStore } from './state/store';
|
|
20
|
+
import type { NotificationEngineAdapter } from './engine/adapter';
|
|
21
|
+
|
|
22
|
+
export interface RuntimeDependencies {
|
|
23
|
+
readonly engine: NotificationEngineAdapter;
|
|
24
|
+
readonly store: StateStore;
|
|
25
|
+
readonly now?: () => number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Everything assembled at initialization, shared by the operation modules. */
|
|
29
|
+
export interface NotificationRuntime {
|
|
30
|
+
readonly engine: NotificationEngineAdapter;
|
|
31
|
+
readonly state: StateManager;
|
|
32
|
+
readonly emitter: DiagnosticEmitter;
|
|
33
|
+
readonly registry: MaintenanceRegistry;
|
|
34
|
+
readonly maintenance: MaintenanceController;
|
|
35
|
+
readonly lookahead: LookaheadConfig;
|
|
36
|
+
readonly config: NotificationConfig;
|
|
37
|
+
readonly now: () => number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Clamps the lookahead into its documented bounds.
|
|
42
|
+
*
|
|
43
|
+
* Bounded on adjustment because the platform ceiling is not negotiable: a
|
|
44
|
+
* consumer asking for a five-year window would silently exhaust the 64-entry
|
|
45
|
+
* iOS ceiling on the first frequent series.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveLookahead(partial: Partial<LookaheadConfig> | undefined): LookaheadConfig {
|
|
48
|
+
const days = partial?.days ?? DEFAULT_LOOKAHEAD.days;
|
|
49
|
+
const occurrencesPerSeries = partial?.occurrencesPerSeries ?? DEFAULT_LOOKAHEAD.occurrencesPerSeries;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
days: Math.min(Math.max(days, LOOKAHEAD_BOUNDS.minDays), LOOKAHEAD_BOUNDS.maxDays),
|
|
53
|
+
occurrencesPerSeries: Math.min(
|
|
54
|
+
Math.max(occurrencesPerSeries, LOOKAHEAD_BOUNDS.minOccurrences),
|
|
55
|
+
LOOKAHEAD_BOUNDS.maxOccurrences,
|
|
56
|
+
),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Fails before any platform call, so a bad configuration never half-applies. */
|
|
61
|
+
export function validateConfig(config: NotificationConfig): void {
|
|
62
|
+
if (config.tones.length === 0) {
|
|
63
|
+
throw new NotificationError(
|
|
64
|
+
DIAGNOSTIC_CODES.UNDECLARED_TONE,
|
|
65
|
+
'At least one tone must be declared before notifications can be scheduled.',
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const seen = new Set<string>();
|
|
70
|
+
for (const tone of config.tones) {
|
|
71
|
+
if (seen.has(tone.id)) {
|
|
72
|
+
throw new NotificationError(
|
|
73
|
+
DIAGNOSTIC_CODES.UNDECLARED_TONE,
|
|
74
|
+
`Tone "${tone.id}" is declared more than once.`,
|
|
75
|
+
{ toneId: tone.id },
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
seen.add(tone.id);
|
|
79
|
+
|
|
80
|
+
if (!Number.isInteger(tone.version) || tone.version < 1) {
|
|
81
|
+
throw new NotificationError(
|
|
82
|
+
DIAGNOSTIC_CODES.CHANNEL_MIGRATION_REQUIRED,
|
|
83
|
+
`Tone "${tone.id}" must carry an integer version of 1 or greater, because a channel configuration change is a migration rather than an assignment.`,
|
|
84
|
+
{ toneId: tone.id, version: tone.version },
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const groupIds = new Set((config.channelGroups ?? []).map(group => group.id));
|
|
90
|
+
for (const tone of config.tones) {
|
|
91
|
+
if (tone.groupId !== undefined && !groupIds.has(tone.groupId)) {
|
|
92
|
+
throw new NotificationError(
|
|
93
|
+
DIAGNOSTIC_CODES.UNDECLARED_TONE,
|
|
94
|
+
`Tone "${tone.id}" references undeclared channel group "${tone.groupId}".`,
|
|
95
|
+
{ toneId: tone.id, groupId: tone.groupId },
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function initialize(
|
|
102
|
+
config: NotificationConfig,
|
|
103
|
+
deps: RuntimeDependencies,
|
|
104
|
+
): Promise<{ runtime: NotificationRuntime; result: InitializeResult }> {
|
|
105
|
+
const now = deps.now ?? (() => Date.now());
|
|
106
|
+
const startedAt = now();
|
|
107
|
+
|
|
108
|
+
validateConfig(config);
|
|
109
|
+
|
|
110
|
+
const emitter = new DiagnosticEmitter({
|
|
111
|
+
sink: config.eventSink,
|
|
112
|
+
verbose: config.verboseDiagnostics,
|
|
113
|
+
now,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const state = new StateManager({
|
|
117
|
+
store: deps.store,
|
|
118
|
+
now,
|
|
119
|
+
emit: event => emitter.emit(event.code, describeStateEvent(event.code), event.context),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (config.channelGroups !== undefined && config.channelGroups.length > 0) {
|
|
123
|
+
await deps.engine.declareGroups(config.channelGroups);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const registry = new MaintenanceRegistry();
|
|
127
|
+
const maintenance = new MaintenanceController({
|
|
128
|
+
registry,
|
|
129
|
+
now,
|
|
130
|
+
// Owned identifiers come from the series ledger: those are the entries
|
|
131
|
+
// automatic maintenance created and may therefore retire.
|
|
132
|
+
ownedIds: () => new Set(state.read().seriesLedger.map(entry => entry.seriesId)),
|
|
133
|
+
enabled: config.automaticMaintenance ?? true,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const runtime: NotificationRuntime = {
|
|
137
|
+
engine: deps.engine,
|
|
138
|
+
state,
|
|
139
|
+
emitter,
|
|
140
|
+
registry,
|
|
141
|
+
maintenance,
|
|
142
|
+
lookahead: resolveLookahead(config.lookahead),
|
|
143
|
+
config,
|
|
144
|
+
now,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
runtime,
|
|
149
|
+
result: {
|
|
150
|
+
// Tone application and migration are performed by the channel module,
|
|
151
|
+
// which registers itself during User Story 5.
|
|
152
|
+
tonesApplied: 0,
|
|
153
|
+
migrationsRun: 0,
|
|
154
|
+
stateRebuilt: state.health().rebuilt,
|
|
155
|
+
automaticMaintenance: maintenance.isEnabled,
|
|
156
|
+
durationMs: now() - startedAt,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function describeStateEvent(code: string): string {
|
|
162
|
+
switch (code) {
|
|
163
|
+
case DIAGNOSTIC_CODES.STATE_CORRUPT_REBUILT:
|
|
164
|
+
return 'Persisted state was unreadable and has been rebuilt. Deduplication history and buffered interactions from before the loss are gone; scheduled notifications are unaffected.';
|
|
165
|
+
case DIAGNOSTIC_CODES.STATE_WRITE_FAILED:
|
|
166
|
+
return 'Persisted state could not be written. The affected guarantee degrades to this session only.';
|
|
167
|
+
case DIAGNOSTIC_CODES.STATE_CAP_EVICTED:
|
|
168
|
+
return 'A persisted collection reached its cap and the oldest entries were evicted.';
|
|
169
|
+
case DIAGNOSTIC_CODES.INTERACTION_BUFFER_EVICTED:
|
|
170
|
+
return 'Buffered interactions were evicted at the cap or retention limit.';
|
|
171
|
+
default:
|
|
172
|
+
return 'Persisted state event.';
|
|
173
|
+
}
|
|
174
|
+
}
|