@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
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Permission types.
|
|
3
|
-
*
|
|
4
|
-
* Display permission and exact-alarm permission are different things serving
|
|
5
|
-
* different purposes, so they are requested and reported independently. State
|
|
6
|
-
* is always readable without triggering a dialog.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export type PermissionKind = 'display' | 'exactAlarm';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Six states, never collapsed into one another.
|
|
13
|
-
*
|
|
14
|
-
* - `notAsked` — distinguishable from `denied`, which is the whole point of
|
|
15
|
-
* being able to read state without prompting.
|
|
16
|
-
* - `provisional` — iOS quiet delivery. Neither granted nor denied.
|
|
17
|
-
* - `notRequired` — the running platform version has no such concept. Never
|
|
18
|
-
* reported as `denied`, which would imply a refusal that never happened.
|
|
19
|
-
*/
|
|
20
|
-
export type PermissionState =
|
|
21
|
-
| 'notAsked'
|
|
22
|
-
| 'granted'
|
|
23
|
-
| 'denied'
|
|
24
|
-
| 'permanentlyDenied'
|
|
25
|
-
| 'provisional'
|
|
26
|
-
| 'notRequired';
|
|
27
|
-
|
|
28
|
-
export interface PermissionReport {
|
|
29
|
-
readonly display: PermissionState;
|
|
30
|
-
readonly exactAlarm: PermissionState;
|
|
31
|
-
readonly readAt: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Whether a manufacturer-level restriction is in force, where it can be known. */
|
|
35
|
-
export type RestrictionStatus = 'unrestricted' | 'restricted' | 'unknown';
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Manufacturer background restrictions, reported for information only.
|
|
39
|
-
*
|
|
40
|
-
* `unknown` is a first-class answer: some vendors stop apps in ways with no
|
|
41
|
-
* standard detection API, and guessing would be worse than admitting it.
|
|
42
|
-
* The package never changes any of these settings on the user's behalf.
|
|
43
|
-
*/
|
|
44
|
-
export interface RestrictionReport {
|
|
45
|
-
readonly batteryOptimization: RestrictionStatus;
|
|
46
|
-
readonly autoStart: RestrictionStatus;
|
|
47
|
-
readonly manufacturer?: string;
|
|
48
|
-
readonly readAt: number;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Whether a permission request can even be presented on this platform version. */
|
|
52
|
-
export interface PermissionCapability {
|
|
53
|
-
readonly kind: PermissionKind;
|
|
54
|
-
readonly supported: boolean;
|
|
55
|
-
/**
|
|
56
|
-
* True when the platform grants this only through system settings rather
|
|
57
|
-
* than an in-app dialog.
|
|
58
|
-
*/
|
|
59
|
-
readonly settingsOnly: boolean;
|
|
60
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Permission types.
|
|
3
|
+
*
|
|
4
|
+
* Display permission and exact-alarm permission are different things serving
|
|
5
|
+
* different purposes, so they are requested and reported independently. State
|
|
6
|
+
* is always readable without triggering a dialog.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type PermissionKind = 'display' | 'exactAlarm';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Six states, never collapsed into one another.
|
|
13
|
+
*
|
|
14
|
+
* - `notAsked` — distinguishable from `denied`, which is the whole point of
|
|
15
|
+
* being able to read state without prompting.
|
|
16
|
+
* - `provisional` — iOS quiet delivery. Neither granted nor denied.
|
|
17
|
+
* - `notRequired` — the running platform version has no such concept. Never
|
|
18
|
+
* reported as `denied`, which would imply a refusal that never happened.
|
|
19
|
+
*/
|
|
20
|
+
export type PermissionState =
|
|
21
|
+
| 'notAsked'
|
|
22
|
+
| 'granted'
|
|
23
|
+
| 'denied'
|
|
24
|
+
| 'permanentlyDenied'
|
|
25
|
+
| 'provisional'
|
|
26
|
+
| 'notRequired';
|
|
27
|
+
|
|
28
|
+
export interface PermissionReport {
|
|
29
|
+
readonly display: PermissionState;
|
|
30
|
+
readonly exactAlarm: PermissionState;
|
|
31
|
+
readonly readAt: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Whether a manufacturer-level restriction is in force, where it can be known. */
|
|
35
|
+
export type RestrictionStatus = 'unrestricted' | 'restricted' | 'unknown';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Manufacturer background restrictions, reported for information only.
|
|
39
|
+
*
|
|
40
|
+
* `unknown` is a first-class answer: some vendors stop apps in ways with no
|
|
41
|
+
* standard detection API, and guessing would be worse than admitting it.
|
|
42
|
+
* The package never changes any of these settings on the user's behalf.
|
|
43
|
+
*/
|
|
44
|
+
export interface RestrictionReport {
|
|
45
|
+
readonly batteryOptimization: RestrictionStatus;
|
|
46
|
+
readonly autoStart: RestrictionStatus;
|
|
47
|
+
readonly manufacturer?: string;
|
|
48
|
+
readonly readAt: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Whether a permission request can even be presented on this platform version. */
|
|
52
|
+
export interface PermissionCapability {
|
|
53
|
+
readonly kind: PermissionKind;
|
|
54
|
+
readonly supported: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* True when the platform grants this only through system settings rather
|
|
57
|
+
* than an in-app dialog.
|
|
58
|
+
*/
|
|
59
|
+
readonly settingsOnly: boolean;
|
|
60
|
+
}
|
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reconciliation types.
|
|
3
|
-
*
|
|
4
|
-
* A caller hands over the complete set its data currently implies; the package
|
|
5
|
-
* computes the minimal operation set that makes the device match. Running it
|
|
6
|
-
* twice with unchanged input performs zero platform operations — that
|
|
7
|
-
* idempotence is the package's central testable promise.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { NotificationRequest } from './notification';
|
|
11
|
-
|
|
12
|
-
export interface ReconcileInput {
|
|
13
|
-
/**
|
|
14
|
-
* Bounds what may be cancelled. Notifications outside this domain are never
|
|
15
|
-
* touched, including those scheduled by other parts of the app or by other
|
|
16
|
-
* libraries.
|
|
17
|
-
*/
|
|
18
|
-
readonly domain: string;
|
|
19
|
-
readonly desired: readonly NotificationRequest[];
|
|
20
|
-
/**
|
|
21
|
-
* When false (the default), push-created notifications are left alone rather
|
|
22
|
-
* than cancelled for being absent from a locally-computed desired set.
|
|
23
|
-
*/
|
|
24
|
-
readonly includePushCreated?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Why a desired entry was refused rather than applied. */
|
|
28
|
-
export interface RejectedEntry {
|
|
29
|
-
readonly id: string;
|
|
30
|
-
readonly code: string;
|
|
31
|
-
readonly reason: string;
|
|
32
|
-
/** How many times this identifier appeared in the desired set. */
|
|
33
|
-
readonly occurrences: number;
|
|
34
|
-
/**
|
|
35
|
-
* True when a notification already scheduled under this identifier was left
|
|
36
|
-
* in place. Stated explicitly so a rejection is never misread as a
|
|
37
|
-
* cancellation.
|
|
38
|
-
*/
|
|
39
|
-
readonly existingLeftInPlace: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** What was dropped for exceeding the platform's pending ceiling. */
|
|
43
|
-
export interface TruncationReport {
|
|
44
|
-
readonly ceiling: number;
|
|
45
|
-
readonly droppedCount: number;
|
|
46
|
-
readonly droppedIds: readonly string[];
|
|
47
|
-
/**
|
|
48
|
-
* Dropped entries are not lost — they are re-evaluated at each window
|
|
49
|
-
* refresh as nearer entries deliver.
|
|
50
|
-
*/
|
|
51
|
-
readonly reEvaluatedOnNextRefresh: true;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface ReconcileResult {
|
|
55
|
-
readonly domain: string;
|
|
56
|
-
readonly created: readonly string[];
|
|
57
|
-
readonly replaced: readonly string[];
|
|
58
|
-
readonly cancelled: readonly string[];
|
|
59
|
-
readonly unchanged: readonly string[];
|
|
60
|
-
readonly rejected: readonly RejectedEntry[];
|
|
61
|
-
readonly truncated?: TruncationReport;
|
|
62
|
-
/** Total platform operations issued. Zero on a no-change pass. */
|
|
63
|
-
readonly operationCount: number;
|
|
64
|
-
readonly durationMs: number;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** A discrepancy between what the package expects and what the platform holds. */
|
|
68
|
-
export interface PendingDiscrepancy {
|
|
69
|
-
/** Expected from the series ledger or last reconciliation, but absent. */
|
|
70
|
-
readonly missing: readonly string[];
|
|
71
|
-
/** Present on the platform but not recognized by the package. */
|
|
72
|
-
readonly unrecognized: readonly string[];
|
|
73
|
-
readonly detectedAt: number;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Outcome of one automatic maintenance cycle. */
|
|
77
|
-
export interface MaintenanceResult {
|
|
78
|
-
readonly ranAt: number;
|
|
79
|
-
readonly windowRefreshed: boolean;
|
|
80
|
-
readonly occurrencesScheduled: number;
|
|
81
|
-
readonly reAnchored: number;
|
|
82
|
-
readonly timeZoneChanged: boolean;
|
|
83
|
-
readonly permissionChanged: boolean;
|
|
84
|
-
/** Detected and reported only — never repaired. */
|
|
85
|
-
readonly discrepancy?: PendingDiscrepancy;
|
|
86
|
-
readonly operationCount: number;
|
|
87
|
-
readonly durationMs: number;
|
|
88
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Reconciliation types.
|
|
3
|
+
*
|
|
4
|
+
* A caller hands over the complete set its data currently implies; the package
|
|
5
|
+
* computes the minimal operation set that makes the device match. Running it
|
|
6
|
+
* twice with unchanged input performs zero platform operations — that
|
|
7
|
+
* idempotence is the package's central testable promise.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { NotificationRequest } from './notification';
|
|
11
|
+
|
|
12
|
+
export interface ReconcileInput {
|
|
13
|
+
/**
|
|
14
|
+
* Bounds what may be cancelled. Notifications outside this domain are never
|
|
15
|
+
* touched, including those scheduled by other parts of the app or by other
|
|
16
|
+
* libraries.
|
|
17
|
+
*/
|
|
18
|
+
readonly domain: string;
|
|
19
|
+
readonly desired: readonly NotificationRequest[];
|
|
20
|
+
/**
|
|
21
|
+
* When false (the default), push-created notifications are left alone rather
|
|
22
|
+
* than cancelled for being absent from a locally-computed desired set.
|
|
23
|
+
*/
|
|
24
|
+
readonly includePushCreated?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Why a desired entry was refused rather than applied. */
|
|
28
|
+
export interface RejectedEntry {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly code: string;
|
|
31
|
+
readonly reason: string;
|
|
32
|
+
/** How many times this identifier appeared in the desired set. */
|
|
33
|
+
readonly occurrences: number;
|
|
34
|
+
/**
|
|
35
|
+
* True when a notification already scheduled under this identifier was left
|
|
36
|
+
* in place. Stated explicitly so a rejection is never misread as a
|
|
37
|
+
* cancellation.
|
|
38
|
+
*/
|
|
39
|
+
readonly existingLeftInPlace: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What was dropped for exceeding the platform's pending ceiling. */
|
|
43
|
+
export interface TruncationReport {
|
|
44
|
+
readonly ceiling: number;
|
|
45
|
+
readonly droppedCount: number;
|
|
46
|
+
readonly droppedIds: readonly string[];
|
|
47
|
+
/**
|
|
48
|
+
* Dropped entries are not lost — they are re-evaluated at each window
|
|
49
|
+
* refresh as nearer entries deliver.
|
|
50
|
+
*/
|
|
51
|
+
readonly reEvaluatedOnNextRefresh: true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ReconcileResult {
|
|
55
|
+
readonly domain: string;
|
|
56
|
+
readonly created: readonly string[];
|
|
57
|
+
readonly replaced: readonly string[];
|
|
58
|
+
readonly cancelled: readonly string[];
|
|
59
|
+
readonly unchanged: readonly string[];
|
|
60
|
+
readonly rejected: readonly RejectedEntry[];
|
|
61
|
+
readonly truncated?: TruncationReport;
|
|
62
|
+
/** Total platform operations issued. Zero on a no-change pass. */
|
|
63
|
+
readonly operationCount: number;
|
|
64
|
+
readonly durationMs: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A discrepancy between what the package expects and what the platform holds. */
|
|
68
|
+
export interface PendingDiscrepancy {
|
|
69
|
+
/** Expected from the series ledger or last reconciliation, but absent. */
|
|
70
|
+
readonly missing: readonly string[];
|
|
71
|
+
/** Present on the platform but not recognized by the package. */
|
|
72
|
+
readonly unrecognized: readonly string[];
|
|
73
|
+
readonly detectedAt: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Outcome of one automatic maintenance cycle. */
|
|
77
|
+
export interface MaintenanceResult {
|
|
78
|
+
readonly ranAt: number;
|
|
79
|
+
readonly windowRefreshed: boolean;
|
|
80
|
+
readonly occurrencesScheduled: number;
|
|
81
|
+
readonly reAnchored: number;
|
|
82
|
+
readonly timeZoneChanged: boolean;
|
|
83
|
+
readonly permissionChanged: boolean;
|
|
84
|
+
/** Detected and reported only — never repaired. */
|
|
85
|
+
readonly discrepancy?: PendingDiscrepancy;
|
|
86
|
+
readonly operationCount: number;
|
|
87
|
+
readonly durationMs: number;
|
|
88
|
+
}
|
|
@@ -1,123 +1,123 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Recurrence declaration types.
|
|
3
|
-
*
|
|
4
|
-
* A caller declares a rule once; the package expands it inside a finite
|
|
5
|
-
* lookahead window. No caller ever writes an expansion loop, and no unbounded
|
|
6
|
-
* series is ever handed to the platform.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { AnchoringMode, NotificationContent, Action, RoutingPayload } from './notification';
|
|
10
|
-
|
|
11
|
-
/** Days of the week, `0` = Sunday, matching `Date.prototype.getDay`. */
|
|
12
|
-
export type Weekday = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
13
|
-
|
|
14
|
-
/** Every day at the declared clock time. */
|
|
15
|
-
export interface DailyPattern {
|
|
16
|
-
readonly kind: 'daily';
|
|
17
|
-
readonly hour: number;
|
|
18
|
-
readonly minute: number;
|
|
19
|
-
/** Fire every `interval` days. Defaults to 1. */
|
|
20
|
-
readonly interval?: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Selected weekdays at the declared clock time. */
|
|
24
|
-
export interface WeeklyPattern {
|
|
25
|
-
readonly kind: 'weekly';
|
|
26
|
-
readonly weekdays: readonly Weekday[];
|
|
27
|
-
readonly hour: number;
|
|
28
|
-
readonly minute: number;
|
|
29
|
-
/** Fire every `interval` weeks. Defaults to 1. */
|
|
30
|
-
readonly interval?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* A fixed day of the month at the declared clock time.
|
|
35
|
-
*
|
|
36
|
-
* Months shorter than `dayOfMonth` are skipped rather than clamped: a caller
|
|
37
|
-
* asking for the 31st means the 31st, and inventing the 28th would be the
|
|
38
|
-
* package deciding something it was not asked to decide.
|
|
39
|
-
*/
|
|
40
|
-
export interface MonthlyByDayPattern {
|
|
41
|
-
readonly kind: 'monthlyByDay';
|
|
42
|
-
readonly dayOfMonth: number;
|
|
43
|
-
readonly hour: number;
|
|
44
|
-
readonly minute: number;
|
|
45
|
-
/** Fire every `interval` months. Defaults to 1. */
|
|
46
|
-
readonly interval?: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export type RecurrencePattern = DailyPattern | WeeklyPattern | MonthlyByDayPattern;
|
|
50
|
-
|
|
51
|
-
/** When a series stops producing occurrences. */
|
|
52
|
-
export type EndCondition =
|
|
53
|
-
| { readonly kind: 'unbounded' }
|
|
54
|
-
| { readonly kind: 'untilDate'; readonly until: number }
|
|
55
|
-
| { readonly kind: 'count'; readonly count: number };
|
|
56
|
-
|
|
57
|
-
/** Content shared by every occurrence of a series. Identity and moment are derived. */
|
|
58
|
-
export interface OccurrenceTemplate {
|
|
59
|
-
readonly content: NotificationContent;
|
|
60
|
-
readonly tone: string;
|
|
61
|
-
readonly routing?: RoutingPayload;
|
|
62
|
-
readonly actions?: readonly Action[];
|
|
63
|
-
readonly ownerTag?: string;
|
|
64
|
-
readonly groupTag?: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface RecurrenceRule {
|
|
68
|
-
/** Stable series identifier, subject to the same charset rules as a notification id. */
|
|
69
|
-
readonly id: string;
|
|
70
|
-
readonly pattern: RecurrencePattern;
|
|
71
|
-
/**
|
|
72
|
-
* Declared once for the whole series and inherited by every occurrence.
|
|
73
|
-
* A series may not mix modes. Defaults to `wallClock`.
|
|
74
|
-
*/
|
|
75
|
-
readonly anchoring?: AnchoringMode;
|
|
76
|
-
/** The instant from which occurrences are generated. Defaults to now. */
|
|
77
|
-
readonly startAt?: number;
|
|
78
|
-
readonly end: EndCondition;
|
|
79
|
-
readonly template: OccurrenceTemplate;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* How far ahead occurrences are registered with the platform.
|
|
84
|
-
*
|
|
85
|
-
* Bounded by both a duration and a count because either alone fails: a duration
|
|
86
|
-
* alone lets a frequent series exhaust the platform ceiling, and a count alone
|
|
87
|
-
* leaves an annual series scheduled a decade out.
|
|
88
|
-
*/
|
|
89
|
-
export interface LookaheadConfig {
|
|
90
|
-
readonly days: number;
|
|
91
|
-
readonly occurrencesPerSeries: number;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export const DEFAULT_LOOKAHEAD: LookaheadConfig = {
|
|
95
|
-
days: 60,
|
|
96
|
-
occurrencesPerSeries: 32,
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
/** Bounds on consumer adjustment, because the platform ceiling is not negotiable. */
|
|
100
|
-
export const LOOKAHEAD_BOUNDS = {
|
|
101
|
-
minDays: 1,
|
|
102
|
-
maxDays: 365,
|
|
103
|
-
minOccurrences: 1,
|
|
104
|
-
maxOccurrences: 64,
|
|
105
|
-
} as const;
|
|
106
|
-
|
|
107
|
-
export interface SeriesResult {
|
|
108
|
-
readonly seriesId: string;
|
|
109
|
-
/** Occurrences registered with the platform in this pass. */
|
|
110
|
-
readonly scheduled: number;
|
|
111
|
-
/** The horizon expansion reached, in epoch milliseconds. */
|
|
112
|
-
readonly expandedThrough: number;
|
|
113
|
-
/** True when the series has produced its final occurrence. */
|
|
114
|
-
readonly exhausted: boolean;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export interface SeriesSummary {
|
|
118
|
-
readonly seriesId: string;
|
|
119
|
-
readonly anchoring: AnchoringMode;
|
|
120
|
-
readonly expandedThrough: number;
|
|
121
|
-
readonly pendingOccurrences: number;
|
|
122
|
-
readonly exhausted: boolean;
|
|
123
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Recurrence declaration types.
|
|
3
|
+
*
|
|
4
|
+
* A caller declares a rule once; the package expands it inside a finite
|
|
5
|
+
* lookahead window. No caller ever writes an expansion loop, and no unbounded
|
|
6
|
+
* series is ever handed to the platform.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AnchoringMode, NotificationContent, Action, RoutingPayload } from './notification';
|
|
10
|
+
|
|
11
|
+
/** Days of the week, `0` = Sunday, matching `Date.prototype.getDay`. */
|
|
12
|
+
export type Weekday = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
13
|
+
|
|
14
|
+
/** Every day at the declared clock time. */
|
|
15
|
+
export interface DailyPattern {
|
|
16
|
+
readonly kind: 'daily';
|
|
17
|
+
readonly hour: number;
|
|
18
|
+
readonly minute: number;
|
|
19
|
+
/** Fire every `interval` days. Defaults to 1. */
|
|
20
|
+
readonly interval?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Selected weekdays at the declared clock time. */
|
|
24
|
+
export interface WeeklyPattern {
|
|
25
|
+
readonly kind: 'weekly';
|
|
26
|
+
readonly weekdays: readonly Weekday[];
|
|
27
|
+
readonly hour: number;
|
|
28
|
+
readonly minute: number;
|
|
29
|
+
/** Fire every `interval` weeks. Defaults to 1. */
|
|
30
|
+
readonly interval?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A fixed day of the month at the declared clock time.
|
|
35
|
+
*
|
|
36
|
+
* Months shorter than `dayOfMonth` are skipped rather than clamped: a caller
|
|
37
|
+
* asking for the 31st means the 31st, and inventing the 28th would be the
|
|
38
|
+
* package deciding something it was not asked to decide.
|
|
39
|
+
*/
|
|
40
|
+
export interface MonthlyByDayPattern {
|
|
41
|
+
readonly kind: 'monthlyByDay';
|
|
42
|
+
readonly dayOfMonth: number;
|
|
43
|
+
readonly hour: number;
|
|
44
|
+
readonly minute: number;
|
|
45
|
+
/** Fire every `interval` months. Defaults to 1. */
|
|
46
|
+
readonly interval?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type RecurrencePattern = DailyPattern | WeeklyPattern | MonthlyByDayPattern;
|
|
50
|
+
|
|
51
|
+
/** When a series stops producing occurrences. */
|
|
52
|
+
export type EndCondition =
|
|
53
|
+
| { readonly kind: 'unbounded' }
|
|
54
|
+
| { readonly kind: 'untilDate'; readonly until: number }
|
|
55
|
+
| { readonly kind: 'count'; readonly count: number };
|
|
56
|
+
|
|
57
|
+
/** Content shared by every occurrence of a series. Identity and moment are derived. */
|
|
58
|
+
export interface OccurrenceTemplate {
|
|
59
|
+
readonly content: NotificationContent;
|
|
60
|
+
readonly tone: string;
|
|
61
|
+
readonly routing?: RoutingPayload;
|
|
62
|
+
readonly actions?: readonly Action[];
|
|
63
|
+
readonly ownerTag?: string;
|
|
64
|
+
readonly groupTag?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface RecurrenceRule {
|
|
68
|
+
/** Stable series identifier, subject to the same charset rules as a notification id. */
|
|
69
|
+
readonly id: string;
|
|
70
|
+
readonly pattern: RecurrencePattern;
|
|
71
|
+
/**
|
|
72
|
+
* Declared once for the whole series and inherited by every occurrence.
|
|
73
|
+
* A series may not mix modes. Defaults to `wallClock`.
|
|
74
|
+
*/
|
|
75
|
+
readonly anchoring?: AnchoringMode;
|
|
76
|
+
/** The instant from which occurrences are generated. Defaults to now. */
|
|
77
|
+
readonly startAt?: number;
|
|
78
|
+
readonly end: EndCondition;
|
|
79
|
+
readonly template: OccurrenceTemplate;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* How far ahead occurrences are registered with the platform.
|
|
84
|
+
*
|
|
85
|
+
* Bounded by both a duration and a count because either alone fails: a duration
|
|
86
|
+
* alone lets a frequent series exhaust the platform ceiling, and a count alone
|
|
87
|
+
* leaves an annual series scheduled a decade out.
|
|
88
|
+
*/
|
|
89
|
+
export interface LookaheadConfig {
|
|
90
|
+
readonly days: number;
|
|
91
|
+
readonly occurrencesPerSeries: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const DEFAULT_LOOKAHEAD: LookaheadConfig = {
|
|
95
|
+
days: 60,
|
|
96
|
+
occurrencesPerSeries: 32,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** Bounds on consumer adjustment, because the platform ceiling is not negotiable. */
|
|
100
|
+
export const LOOKAHEAD_BOUNDS = {
|
|
101
|
+
minDays: 1,
|
|
102
|
+
maxDays: 365,
|
|
103
|
+
minOccurrences: 1,
|
|
104
|
+
maxOccurrences: 64,
|
|
105
|
+
} as const;
|
|
106
|
+
|
|
107
|
+
export interface SeriesResult {
|
|
108
|
+
readonly seriesId: string;
|
|
109
|
+
/** Occurrences registered with the platform in this pass. */
|
|
110
|
+
readonly scheduled: number;
|
|
111
|
+
/** The horizon expansion reached, in epoch milliseconds. */
|
|
112
|
+
readonly expandedThrough: number;
|
|
113
|
+
/** True when the series has produced its final occurrence. */
|
|
114
|
+
readonly exhausted: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface SeriesSummary {
|
|
118
|
+
readonly seriesId: string;
|
|
119
|
+
readonly anchoring: AnchoringMode;
|
|
120
|
+
readonly expandedThrough: number;
|
|
121
|
+
readonly pendingOccurrences: number;
|
|
122
|
+
readonly exhausted: boolean;
|
|
123
|
+
}
|