@substrat-run/engine-notifications 0.0.0-stage → 0.3.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/LICENSE +661 -0
- package/README.md +172 -2
- package/dist/entities.d.ts +200 -0
- package/dist/entities.d.ts.map +1 -0
- package/dist/entities.js +150 -0
- package/dist/entities.js.map +1 -0
- package/dist/events.d.ts +212 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +20 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +411 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +889 -0
- package/dist/index.js.map +1 -0
- package/dist/model.d.ts +11 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +18 -0
- package/dist/model.js.map +1 -0
- package/dist/operations.d.ts +243 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +139 -0
- package/dist/operations.js.map +1 -0
- package/dist/render.d.ts +60 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +84 -0
- package/dist/render.js.map +1 -0
- package/dist/seam.d.ts +5 -0
- package/dist/seam.d.ts.map +1 -0
- package/dist/seam.js +14 -0
- package/dist/seam.js.map +1 -0
- package/package.json +50 -3
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine-notifications' event contract — what a VERTICAL imports so that reading this
|
|
3
|
+
* engine's outcomes by event is checked rather than guessed.
|
|
4
|
+
*
|
|
5
|
+
* Types plus one stamp, the same shape as every other engine's `events.ts`: the runtime
|
|
6
|
+
* contract is the fat payload and the consumer's own Zod parse; `emitNotificationsEvent`
|
|
7
|
+
* forwards to `ctx.emit`, adding only the `schemaVersion` from
|
|
8
|
+
* `notificationsEventVersions` (#1597).
|
|
9
|
+
*
|
|
10
|
+
* **No address appears.** Each payload but `topic-recorded`'s names a principal (pseudonymous, and the event's
|
|
11
|
+
* `subjectId`) and never where their mail goes: the address is erasable and lives only on
|
|
12
|
+
* the engine's recipient row and on the platform's send intent, both of which an erasure
|
|
13
|
+
* reaches. A field added here must keep that true.
|
|
14
|
+
*
|
|
15
|
+
* Additive, like every other engine surface: a new event type may appear, an existing
|
|
16
|
+
* payload field does not change shape without a `schemaVersion` bump.
|
|
17
|
+
*/
|
|
18
|
+
import type { DomainEventInput, EntityRef } from '@substrat-run/contracts';
|
|
19
|
+
import type { OperationContext } from '@substrat-run/kernel';
|
|
20
|
+
/**
|
|
21
|
+
* What `notify` did for one (occasion, topic, person, channel).
|
|
22
|
+
*
|
|
23
|
+
* `requested`: a send intent was written, and `request` is its id. `skipped`: nothing was
|
|
24
|
+
* sent on its own, by preference (`reason: 'muted'`), for want of an address (`'no_address'`),
|
|
25
|
+
* or because the person takes this topic in a digest (`'digest'`): the occasion is held for
|
|
26
|
+
* their next one. What becomes of a held occasion is told later, naming this `deliveryId`: the
|
|
27
|
+
* `notifications.digest-sent` that carried it, the `notifications.preference-set` whose
|
|
28
|
+
* switch-off withdrew it (its delivery then reads `skipped`/`muted`), or the
|
|
29
|
+
* `notifications.digest-quarantined` that set it aside unreadable (`skipped`/`quarantined`, until
|
|
30
|
+
* a `notifications.digest-released` holds it again), or the `notifications.digest-withdrawn` that
|
|
31
|
+
* withdrew it because the topic was switched off since (`skipped`/`muted`). An erasure of the person
|
|
32
|
+
* deletes it with no event, like a held send it cancels, and its delivery stays as it was.
|
|
33
|
+
* `dedupeKey` is the occasion as the caller named it — from a consumer, the triggering event's id.
|
|
34
|
+
*
|
|
35
|
+
* A digest is a delivery as well, under the reserved topic `@digest` (no vertical topic can be
|
|
36
|
+
* spelled that way), with `about: null`.
|
|
37
|
+
*/
|
|
38
|
+
export interface NotificationsDeliveryRecordedPayload {
|
|
39
|
+
deliveryId: string;
|
|
40
|
+
topic: string;
|
|
41
|
+
principal: string;
|
|
42
|
+
channel: string;
|
|
43
|
+
status: 'requested' | 'skipped';
|
|
44
|
+
reason: string | null;
|
|
45
|
+
request: string | null;
|
|
46
|
+
dedupeKey: string;
|
|
47
|
+
about: EntityRef | null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* What the platform reported back for a `requested` delivery: the provider accepted it
|
|
51
|
+
* (`sent`), refused it for good (`refused`), or every attempt failed (`dead_lettered`).
|
|
52
|
+
* `code` is the platform's taxonomy code for a refusal or the last failure; null on `sent`.
|
|
53
|
+
*/
|
|
54
|
+
export interface NotificationsDeliverySettledPayload {
|
|
55
|
+
deliveryId: string;
|
|
56
|
+
topic: string;
|
|
57
|
+
principal: string;
|
|
58
|
+
channel: string;
|
|
59
|
+
status: 'sent' | 'refused' | 'dead_lettered';
|
|
60
|
+
code: string | null;
|
|
61
|
+
request: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A person switched one topic on or off for one channel, and perhaps chose when it arrives.
|
|
65
|
+
* A switch-off also withdraws that topic's occasions still held for their digest.
|
|
66
|
+
*/
|
|
67
|
+
export interface NotificationsPreferenceSetPayload {
|
|
68
|
+
principal: string;
|
|
69
|
+
topic: string;
|
|
70
|
+
channel: string;
|
|
71
|
+
enabled: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* When the topic reaches them after the call — always `immediate` for a required topic.
|
|
74
|
+
* Absent on events written before digests existed.
|
|
75
|
+
*/
|
|
76
|
+
cadence?: 'immediate' | 'daily-digest';
|
|
77
|
+
/**
|
|
78
|
+
* The deliveries of the occasions this switch-off withdrew from the person's digest, each now
|
|
79
|
+
* `skipped`/`muted`. Absent when it withdrew none.
|
|
80
|
+
*/
|
|
81
|
+
withdrawn?: string[];
|
|
82
|
+
}
|
|
83
|
+
/** A person set or cleared where their notifications go — whether, never what. */
|
|
84
|
+
export interface NotificationsRecipientUpdatedPayload {
|
|
85
|
+
principal: string;
|
|
86
|
+
hasAddress: boolean;
|
|
87
|
+
}
|
|
88
|
+
/** A topic was first used, or its description or `required` changed. Names nobody. */
|
|
89
|
+
export interface NotificationsTopicRecordedPayload {
|
|
90
|
+
topic: string;
|
|
91
|
+
description: string;
|
|
92
|
+
required: boolean;
|
|
93
|
+
}
|
|
94
|
+
/** One occasion a digest carried, named by its own (`skipped`, `digest`) delivery. */
|
|
95
|
+
export interface NotificationsDigestItem {
|
|
96
|
+
deliveryId: string;
|
|
97
|
+
topic: string;
|
|
98
|
+
dedupeKey: string;
|
|
99
|
+
about: EntityRef | null;
|
|
100
|
+
queuedAt: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* One person's digest was composed from everything held for them: the occasions it carried,
|
|
104
|
+
* oldest first, and the digest's own delivery (topic `@digest`). `status` is that delivery's:
|
|
105
|
+
* `requested` with the send intent's `request`, or `skipped` with `reason: 'no_address'` when
|
|
106
|
+
* the person had cleared their address since. Either way the items are no longer held, and the
|
|
107
|
+
* digest's delivery settles like any other.
|
|
108
|
+
*/
|
|
109
|
+
export interface NotificationsDigestSentPayload {
|
|
110
|
+
deliveryId: string;
|
|
111
|
+
principal: string;
|
|
112
|
+
channel: string;
|
|
113
|
+
cadence: 'daily-digest';
|
|
114
|
+
status: 'requested' | 'skipped';
|
|
115
|
+
reason: string | null;
|
|
116
|
+
request: string | null;
|
|
117
|
+
items: NotificationsDigestItem[];
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* A person's digest was due and could not be composed or written — a send or a write that
|
|
121
|
+
* failed. (A held message that no longer reads is set aside instead; see
|
|
122
|
+
* `notifications.digest-quarantined`.) Nothing was sent for them and nothing of theirs was let
|
|
123
|
+
* go: their `items` stay held, and the next attempt is at `retryAt`, a window later. Everyone
|
|
124
|
+
* else's digest in the same pass went out regardless. `code` is the platform's error code; the
|
|
125
|
+
* cause itself is on the error line the pass writes, not here.
|
|
126
|
+
*/
|
|
127
|
+
export interface NotificationsDigestFailedPayload {
|
|
128
|
+
principal: string;
|
|
129
|
+
channel: string;
|
|
130
|
+
cadence: 'daily-digest';
|
|
131
|
+
items: number;
|
|
132
|
+
code: string;
|
|
133
|
+
retryAt: string;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Held occasions of one person's digest no longer read — a stored row that left the shape this
|
|
137
|
+
* engine publishes — and were set aside rather than mailed or allowed to block the rest. Each is
|
|
138
|
+
* marked, its delivery reads `skipped` with reason `quarantined`, and the person's readable
|
|
139
|
+
* messages went out as usual. A set-aside row is read again once a window and released when it
|
|
140
|
+
* reads (`notifications.digest-released`); this event is written when a row is first set aside,
|
|
141
|
+
* never again while it stays unreadable. `occasions` names the deliveries whose id still read;
|
|
142
|
+
* `unidentified` counts the rows whose id did not. Never what a row held.
|
|
143
|
+
*/
|
|
144
|
+
export interface NotificationsDigestQuarantinedPayload {
|
|
145
|
+
principal: string;
|
|
146
|
+
channel: string;
|
|
147
|
+
occasions: string[];
|
|
148
|
+
unidentified: number;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Set-aside occasions of one person read again — whatever left them unreadable, a version that
|
|
152
|
+
* stored a shape this one rejects, say, is gone — and are held for the digest once more: each
|
|
153
|
+
* delivery reads `skipped`/`digest` again, unsettled, and goes out in the person's next digest.
|
|
154
|
+
* One whose topic the person has switched off since is not released but withdrawn
|
|
155
|
+
* (`notifications.digest-withdrawn`).
|
|
156
|
+
*/
|
|
157
|
+
export interface NotificationsDigestReleasedPayload {
|
|
158
|
+
principal: string;
|
|
159
|
+
channel: string;
|
|
160
|
+
occasions: string[];
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Held occasions of one person were withdrawn, unsent, because the person has switched their
|
|
164
|
+
* topic off since they were held, found when a set-aside row read again or when the digest was
|
|
165
|
+
* composed. Each delivery reads `skipped`/`muted`. (A switch-off's own withdrawal is named on
|
|
166
|
+
* `notifications.preference-set`.) A mute always wins over a digest.
|
|
167
|
+
*/
|
|
168
|
+
export interface NotificationsDigestWithdrawnPayload {
|
|
169
|
+
principal: string;
|
|
170
|
+
channel: string;
|
|
171
|
+
occasions: string[];
|
|
172
|
+
}
|
|
173
|
+
export type NotificationsEvents = {
|
|
174
|
+
events: {
|
|
175
|
+
'notifications.delivery-recorded': NotificationsDeliveryRecordedPayload;
|
|
176
|
+
'notifications.delivery-settled': NotificationsDeliverySettledPayload;
|
|
177
|
+
'notifications.preference-set': NotificationsPreferenceSetPayload;
|
|
178
|
+
'notifications.recipient-updated': NotificationsRecipientUpdatedPayload;
|
|
179
|
+
'notifications.topic-recorded': NotificationsTopicRecordedPayload;
|
|
180
|
+
'notifications.digest-sent': NotificationsDigestSentPayload;
|
|
181
|
+
'notifications.digest-failed': NotificationsDigestFailedPayload;
|
|
182
|
+
'notifications.digest-quarantined': NotificationsDigestQuarantinedPayload;
|
|
183
|
+
'notifications.digest-released': NotificationsDigestReleasedPayload;
|
|
184
|
+
'notifications.digest-withdrawn': NotificationsDigestWithdrawnPayload;
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
/** Every event type this engine emits. */
|
|
188
|
+
export type NotificationsEventType = keyof NotificationsEvents['events'];
|
|
189
|
+
/** The one home for each event's `schemaVersion` (#1597); `emits` is read off it. */
|
|
190
|
+
export declare const notificationsEventVersions: {
|
|
191
|
+
readonly 'notifications.delivery-recorded': 1;
|
|
192
|
+
readonly 'notifications.delivery-settled': 1;
|
|
193
|
+
readonly 'notifications.preference-set': 1;
|
|
194
|
+
readonly 'notifications.recipient-updated': 1;
|
|
195
|
+
readonly 'notifications.topic-recorded': 1;
|
|
196
|
+
readonly 'notifications.digest-sent': 1;
|
|
197
|
+
readonly 'notifications.digest-failed': 1;
|
|
198
|
+
readonly 'notifications.digest-quarantined': 1;
|
|
199
|
+
readonly 'notifications.digest-released': 1;
|
|
200
|
+
readonly 'notifications.digest-withdrawn': 1;
|
|
201
|
+
};
|
|
202
|
+
/** The manifest's `events.emits`, read off {@link notificationsEventVersions}. */
|
|
203
|
+
export declare const notificationsEmitDeclarations: {
|
|
204
|
+
type: string;
|
|
205
|
+
schemaVersion: number;
|
|
206
|
+
}[];
|
|
207
|
+
/** `ctx.emit` with the event type and its payload welded together, and the version stamped. */
|
|
208
|
+
export declare function emitNotificationsEvent<K extends NotificationsEventType>(ctx: OperationContext, event: Omit<DomainEventInput, 'type' | 'payload' | 'schemaVersion'> & {
|
|
209
|
+
type: K;
|
|
210
|
+
payload: NotificationsEvents['events'][K];
|
|
211
|
+
}): void;
|
|
212
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAE7D;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,oCAAoC;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,mCAAmC;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,eAAe,CAAC;IAC7C,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,iCAAiC;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,GAAG,cAAc,CAAC;IACvC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,kFAAkF;AAClF,MAAM,WAAW,oCAAoC;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,sFAAsF;AACtF,MAAM,WAAW,iCAAiC;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,sFAAsF;AACtF,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,8BAA8B;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,uBAAuB,EAAE,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gCAAgC;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,qCAAqC;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,kCAAkC;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;;GAKG;AACH,MAAM,WAAW,mCAAmC;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE;QACN,iCAAiC,EAAE,oCAAoC,CAAC;QACxE,gCAAgC,EAAE,mCAAmC,CAAC;QACtE,8BAA8B,EAAE,iCAAiC,CAAC;QAClE,iCAAiC,EAAE,oCAAoC,CAAC;QACxE,8BAA8B,EAAE,iCAAiC,CAAC;QAClE,2BAA2B,EAAE,8BAA8B,CAAC;QAC5D,6BAA6B,EAAE,gCAAgC,CAAC;QAChE,kCAAkC,EAAE,qCAAqC,CAAC;QAC1E,+BAA+B,EAAE,kCAAkC,CAAC;QACpE,gCAAgC,EAAE,mCAAmC,CAAC;KACvE,CAAC;CACH,CAAC;AAEF,0CAA0C;AAC1C,MAAM,MAAM,sBAAsB,GAAG,MAAM,mBAAmB,CAAC,QAAQ,CAAC,CAAC;AAEzE,qFAAqF;AACrF,eAAO,MAAM,0BAA0B;gDACF,CAAC;+CACF,CAAC;6CACH,CAAC;gDACE,CAAC;6CACJ,CAAC;0CACJ,CAAC;4CACC,CAAC;iDACI,CAAC;8CACJ,CAAC;+CACA,CAAC;CACsB,CAAC;AAE5D,kFAAkF;AAClF,eAAO,MAAM,6BAA6B,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,EAExB,CAAC;AAE5D,+FAA+F;AAC/F,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,sBAAsB,EACrE,GAAG,EAAE,gBAAgB,EACrB,KAAK,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,GAAG,eAAe,CAAC,GAAG;IACpE,IAAI,EAAE,CAAC,CAAC;IACR,OAAO,EAAE,mBAAmB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;CAC3C,GACA,IAAI,CAEN"}
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** The one home for each event's `schemaVersion` (#1597); `emits` is read off it. */
|
|
2
|
+
export const notificationsEventVersions = {
|
|
3
|
+
'notifications.delivery-recorded': 1,
|
|
4
|
+
'notifications.delivery-settled': 1,
|
|
5
|
+
'notifications.preference-set': 1,
|
|
6
|
+
'notifications.recipient-updated': 1,
|
|
7
|
+
'notifications.topic-recorded': 1,
|
|
8
|
+
'notifications.digest-sent': 1,
|
|
9
|
+
'notifications.digest-failed': 1,
|
|
10
|
+
'notifications.digest-quarantined': 1,
|
|
11
|
+
'notifications.digest-released': 1,
|
|
12
|
+
'notifications.digest-withdrawn': 1,
|
|
13
|
+
};
|
|
14
|
+
/** The manifest's `events.emits`, read off {@link notificationsEventVersions}. */
|
|
15
|
+
export const notificationsEmitDeclarations = Object.entries(notificationsEventVersions).map(([type, schemaVersion]) => ({ type, schemaVersion }));
|
|
16
|
+
/** `ctx.emit` with the event type and its payload welded together, and the version stamped. */
|
|
17
|
+
export function emitNotificationsEvent(ctx, event) {
|
|
18
|
+
ctx.emit({ ...event, schemaVersion: notificationsEventVersions[event.type] });
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AA0MA,qFAAqF;AACrF,MAAM,CAAC,MAAM,0BAA0B,GAAG;IACxC,iCAAiC,EAAE,CAAC;IACpC,gCAAgC,EAAE,CAAC;IACnC,8BAA8B,EAAE,CAAC;IACjC,iCAAiC,EAAE,CAAC;IACpC,8BAA8B,EAAE,CAAC;IACjC,2BAA2B,EAAE,CAAC;IAC9B,6BAA6B,EAAE,CAAC;IAChC,kCAAkC,EAAE,CAAC;IACrC,+BAA+B,EAAE,CAAC;IAClC,gCAAgC,EAAE,CAAC;CACsB,CAAC;AAE5D,kFAAkF;AAClF,MAAM,CAAC,MAAM,6BAA6B,GAA8C,MAAM,CAAC,OAAO,CACpG,0BAA0B,CAC3B,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC;AAE5D,+FAA+F;AAC/F,MAAM,UAAU,sBAAsB,CACpC,GAAqB,EACrB,KAGC;IAED,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,aAAa,EAAE,0BAA0B,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAChF,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine-notifications — composed **by call**, not by event.
|
|
3
|
+
*
|
|
4
|
+
* The in-scope functions are the surface: `notify`, which turns one occasion into mail to the
|
|
5
|
+
* people it concerns, and the preference functions behind the operations below. A vertical
|
|
6
|
+
* decides WHEN to notify — usually in its own consumer of a domain event, e.g. Handlebar's
|
|
7
|
+
* consumer of `workorder.assigned` — and calls `notify(ctx, …)` there, inside its own
|
|
8
|
+
* transaction, with the template it owns. The engine owns everything after that call: the
|
|
9
|
+
* mute and required check, the address, the escaped render, the send intent, and a delivery
|
|
10
|
+
* row that follows the send to its outcome.
|
|
11
|
+
*
|
|
12
|
+
* Placement (K-47): notifications are domain logic over the kernel's primitives, so they are
|
|
13
|
+
* an engine. The kernel's whole part is the transactional send intent (`requestEmail`, #2102)
|
|
14
|
+
* and the outcome events its settle writes back; the transport behind that intent is
|
|
15
|
+
* `adapter-email`. Nothing here reaches a provider.
|
|
16
|
+
*
|
|
17
|
+
* The registration is one frozen constant like every other engine's. Its only consumers are the
|
|
18
|
+
* three email outcome events, which settle the deliveries it wrote; what triggers a notification
|
|
19
|
+
* is the vertical's code, so nothing about that appears here.
|
|
20
|
+
*
|
|
21
|
+
* Digests are the person's choice, per topic: a topic they take as `daily-digest` is held
|
|
22
|
+
* rather than mailed. The engine's own schedule (`notifications/send-digests`, run by its system
|
|
23
|
+
* principal every `DIGEST_TICK_MINUTES`) sends each person whose oldest held message is a day old
|
|
24
|
+
* one mail of up to `DIGEST_MAX_ITEMS` of them, oldest first, through the same send intent and
|
|
25
|
+
* delivery row as any notification, one person at a time and never more at once than the
|
|
26
|
+
* scope's send backlog has room for. A mute still wins, and a required topic always goes out at
|
|
27
|
+
* once.
|
|
28
|
+
*
|
|
29
|
+
* Deferred, and stated so an absence reads as intent: channels other than email (SMS, push,
|
|
30
|
+
* in-app — each a transport and a `notificationChannel` value), tenant-edited or per-locale
|
|
31
|
+
* templates (a table keyed by topic overriding the template the caller passes, which is also
|
|
32
|
+
* where a digest's own wording would come from), quiet hours and a digest's time of day, other
|
|
33
|
+
* digest cadences (today any cadence row holds a topic for the one daily schedule: a second
|
|
34
|
+
* cadence needs a `digestCadence` value, the cadence recorded on each held item, and a schedule
|
|
35
|
+
* that sends only its own).
|
|
36
|
+
*/
|
|
37
|
+
import { z } from 'zod';
|
|
38
|
+
import { type Page } from '@substrat-run/contracts';
|
|
39
|
+
import { type ModuleRegistration, type OperationContext, type PageParams } from '@substrat-run/kernel';
|
|
40
|
+
/**
|
|
41
|
+
* The conflict reasons this engine raises, narrowing the platform's `conflict` code (#113).
|
|
42
|
+
* Additive only: new reasons may appear, existing ones do not change spelling.
|
|
43
|
+
*/
|
|
44
|
+
export declare const NOTIFICATIONS_CONFLICT_REASONS: readonly ['topic_required'];
|
|
45
|
+
export type NotificationsConflictReason = (typeof NOTIFICATIONS_CONFLICT_REASONS)[number];
|
|
46
|
+
export { notificationsEntities, deliveryRow, myDeliveryRow } from './entities.js';
|
|
47
|
+
import { deliveryRow, myDeliveryRow } from './entities.js';
|
|
48
|
+
export { notificationsOperations, NOTIFICATIONS_PERMISSIONS, notificationCadence, notificationChannel, myPreferences, topicPreference, type NotificationCadence, type NotificationChannel, } from './operations.js';
|
|
49
|
+
import { myPreferences, type NotificationCadence, type NotificationChannel } from './operations.js';
|
|
50
|
+
export { type NotificationsEvents, type NotificationsEventType, type NotificationsDeliveryRecordedPayload, type NotificationsDeliverySettledPayload, type NotificationsDigestFailedPayload, type NotificationsDigestItem, type NotificationsDigestQuarantinedPayload, type NotificationsDigestReleasedPayload, type NotificationsDigestWithdrawnPayload, type NotificationsDigestSentPayload, type NotificationsPreferenceSetPayload, type NotificationsRecipientUpdatedPayload, type NotificationsTopicRecordedPayload, } from './events.js';
|
|
51
|
+
export { type NotificationTemplate } from './render.js';
|
|
52
|
+
export declare const NOTIFICATIONS_PERM: {
|
|
53
|
+
self: string & z.$brand<"PermissionKey">;
|
|
54
|
+
read: string & z.$brand<"PermissionKey">;
|
|
55
|
+
sendDigests: string & z.$brand<"PermissionKey">;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The topic of a digest's own delivery. A vertical's topic is a lowercase slug, so none can be
|
|
59
|
+
* spelled this way: filtering the delivery list on it lists digests and nothing else.
|
|
60
|
+
*/
|
|
61
|
+
export declare const DIGEST_TOPIC = "@digest";
|
|
62
|
+
/**
|
|
63
|
+
* How often the digest schedule looks for people whose digest is due. Each pass is one indexed
|
|
64
|
+
* query when nobody is; a backlog bigger than one pass's room drains over the next passes.
|
|
65
|
+
*/
|
|
66
|
+
export declare const DIGEST_TICK_MINUTES = 15;
|
|
67
|
+
/**
|
|
68
|
+
* The pending-send slots a digest pass leaves free, of the scope's
|
|
69
|
+
* `MAX_PENDING_PLATFORM_REQUESTS`: digests can wait for the drain, and mail that must not —
|
|
70
|
+
* a required topic above all — is never refused because digests filled the backlog.
|
|
71
|
+
*/
|
|
72
|
+
export declare const DIGEST_RESERVE = 8;
|
|
73
|
+
/**
|
|
74
|
+
* The most held occasions one digest carries, oldest first. The rest stay held and are due by
|
|
75
|
+
* the same rule as any — once the oldest of them is a window old — so a person with more held
|
|
76
|
+
* than this gets several digests rather than one unbounded mail, and a pass reads a bounded
|
|
77
|
+
* number of rows for each person.
|
|
78
|
+
*/
|
|
79
|
+
export declare const DIGEST_MAX_ITEMS = 50;
|
|
80
|
+
export declare const notificationsManifest: {
|
|
81
|
+
id: string & z.$brand<"ModuleId">;
|
|
82
|
+
version: string;
|
|
83
|
+
kernelContract: string;
|
|
84
|
+
permissions: {
|
|
85
|
+
key: string & z.$brand<"PermissionKey">;
|
|
86
|
+
description: string;
|
|
87
|
+
holder?: "system" | undefined;
|
|
88
|
+
}[];
|
|
89
|
+
events: {
|
|
90
|
+
emits: {
|
|
91
|
+
type: string;
|
|
92
|
+
schemaVersion: number;
|
|
93
|
+
}[];
|
|
94
|
+
consumes: {
|
|
95
|
+
type: string;
|
|
96
|
+
schemaVersion: number;
|
|
97
|
+
from?: string | undefined;
|
|
98
|
+
}[];
|
|
99
|
+
exports?: {
|
|
100
|
+
type: string;
|
|
101
|
+
schemaVersion: number;
|
|
102
|
+
readPermission: string & z.$brand<"PermissionKey">;
|
|
103
|
+
}[] | undefined;
|
|
104
|
+
};
|
|
105
|
+
migrations: {
|
|
106
|
+
journalDir: string;
|
|
107
|
+
compatibleFrom: string;
|
|
108
|
+
};
|
|
109
|
+
attachmentTargets: {
|
|
110
|
+
entityType: string;
|
|
111
|
+
readPermission: string & z.$brand<"PermissionKey">;
|
|
112
|
+
writePermission?: (string & z.$brand<"PermissionKey">) | undefined;
|
|
113
|
+
uploadLinks?: {
|
|
114
|
+
maxBytes: number;
|
|
115
|
+
contentTypes?: string[] | undefined;
|
|
116
|
+
} | undefined;
|
|
117
|
+
}[];
|
|
118
|
+
liveTargets?: {
|
|
119
|
+
entityType: string;
|
|
120
|
+
readPermission: string & z.$brand<"PermissionKey">;
|
|
121
|
+
}[] | undefined;
|
|
122
|
+
entityRelations?: {
|
|
123
|
+
entityType: string;
|
|
124
|
+
parentType: string;
|
|
125
|
+
}[] | undefined;
|
|
126
|
+
guards?: {
|
|
127
|
+
before: string;
|
|
128
|
+
predicate: string;
|
|
129
|
+
config: Record<string, unknown>;
|
|
130
|
+
}[] | undefined;
|
|
131
|
+
schedules?: {
|
|
132
|
+
operation: string;
|
|
133
|
+
cadence: {
|
|
134
|
+
everyMinutes: number;
|
|
135
|
+
};
|
|
136
|
+
input?: Record<string, unknown> | undefined;
|
|
137
|
+
permissions: (string & z.$brand<"PermissionKey">)[];
|
|
138
|
+
purge?: {
|
|
139
|
+
entityType: string;
|
|
140
|
+
} | undefined;
|
|
141
|
+
}[] | undefined;
|
|
142
|
+
peers?: {
|
|
143
|
+
vertical: string;
|
|
144
|
+
operations: string[];
|
|
145
|
+
permissions: (string & z.$brand<"PermissionKey">)[];
|
|
146
|
+
}[] | undefined;
|
|
147
|
+
connections?: {
|
|
148
|
+
provider: string;
|
|
149
|
+
operation: string;
|
|
150
|
+
permission: string & z.$brand<"PermissionKey">;
|
|
151
|
+
options?: Record<string, unknown> | undefined;
|
|
152
|
+
}[] | undefined;
|
|
153
|
+
freshness?: {
|
|
154
|
+
eventType: string;
|
|
155
|
+
within: {
|
|
156
|
+
hours: number;
|
|
157
|
+
};
|
|
158
|
+
}[] | undefined;
|
|
159
|
+
withdraws?: string[] | undefined;
|
|
160
|
+
entitlementKey: string;
|
|
161
|
+
envSpec?: {
|
|
162
|
+
key: string;
|
|
163
|
+
label?: string | undefined;
|
|
164
|
+
description: string;
|
|
165
|
+
placeholder?: string | undefined;
|
|
166
|
+
required: boolean;
|
|
167
|
+
secret: boolean;
|
|
168
|
+
default?: string | undefined;
|
|
169
|
+
group?: string | undefined;
|
|
170
|
+
}[] | undefined;
|
|
171
|
+
ownerGrants?: (string & z.$brand<"PermissionKey">)[] | undefined;
|
|
172
|
+
entitlements?: string[] | undefined;
|
|
173
|
+
provides?: string[] | undefined;
|
|
174
|
+
requires?: string[] | undefined;
|
|
175
|
+
api?: string | undefined;
|
|
176
|
+
searchables?: {
|
|
177
|
+
entityType: string;
|
|
178
|
+
fields: string[];
|
|
179
|
+
table?: string | undefined;
|
|
180
|
+
idColumn?: string | undefined;
|
|
181
|
+
tokenizer?: "prefix" | "substring" | undefined;
|
|
182
|
+
}[] | undefined;
|
|
183
|
+
lists?: {
|
|
184
|
+
entityType: string;
|
|
185
|
+
sortable: string[];
|
|
186
|
+
filterable?: string[] | undefined;
|
|
187
|
+
table?: string | undefined;
|
|
188
|
+
idColumn?: string | undefined;
|
|
189
|
+
}[] | undefined;
|
|
190
|
+
entityStates?: {
|
|
191
|
+
archivePermission?: (string & z.$brand<"PermissionKey">) | undefined;
|
|
192
|
+
trashPermission?: (string & z.$brand<"PermissionKey">) | undefined;
|
|
193
|
+
purgeAfterDays?: number | undefined;
|
|
194
|
+
purgeEarly?: boolean | undefined;
|
|
195
|
+
table?: string | undefined;
|
|
196
|
+
idColumn?: string | undefined;
|
|
197
|
+
entityType: string;
|
|
198
|
+
}[] | undefined;
|
|
199
|
+
erasure?: {
|
|
200
|
+
tables: string[];
|
|
201
|
+
entities: {
|
|
202
|
+
entityType: string;
|
|
203
|
+
table: string;
|
|
204
|
+
mode: "blank" | "custom" | "delete" | "unreached";
|
|
205
|
+
subjects?: string[] | undefined;
|
|
206
|
+
fields: {
|
|
207
|
+
name: string;
|
|
208
|
+
blank: "" | null;
|
|
209
|
+
}[];
|
|
210
|
+
}[];
|
|
211
|
+
} | undefined;
|
|
212
|
+
updatedStamps?: {
|
|
213
|
+
table: string;
|
|
214
|
+
idColumn: string;
|
|
215
|
+
at?: string | undefined;
|
|
216
|
+
by?: string | undefined;
|
|
217
|
+
entityType: string;
|
|
218
|
+
}[] | undefined;
|
|
219
|
+
ui?: {
|
|
220
|
+
routes?: {
|
|
221
|
+
path: string;
|
|
222
|
+
screen: string;
|
|
223
|
+
permission: string & z.$brand<"PermissionKey">;
|
|
224
|
+
}[] | undefined;
|
|
225
|
+
nav?: {
|
|
226
|
+
label: string;
|
|
227
|
+
icon?: string | undefined;
|
|
228
|
+
to: string;
|
|
229
|
+
permission: string & z.$brand<"PermissionKey">;
|
|
230
|
+
}[] | undefined;
|
|
231
|
+
entityViews?: {
|
|
232
|
+
entityType: string;
|
|
233
|
+
view: string;
|
|
234
|
+
}[] | undefined;
|
|
235
|
+
widgets?: {
|
|
236
|
+
slot: string;
|
|
237
|
+
component: string;
|
|
238
|
+
permission: string & z.$brand<"PermissionKey">;
|
|
239
|
+
}[] | undefined;
|
|
240
|
+
settingsPanels?: {
|
|
241
|
+
label: string;
|
|
242
|
+
component: string;
|
|
243
|
+
permission: string & z.$brand<"PermissionKey">;
|
|
244
|
+
}[] | undefined;
|
|
245
|
+
} | undefined;
|
|
246
|
+
};
|
|
247
|
+
export declare const notificationsMigrations: {
|
|
248
|
+
version: string;
|
|
249
|
+
sql: string;
|
|
250
|
+
}[];
|
|
251
|
+
export declare const notifyInput: z.ZodObject<{
|
|
252
|
+
topic: z.ZodString;
|
|
253
|
+
description: z.ZodString;
|
|
254
|
+
dedupeKey: z.ZodString;
|
|
255
|
+
recipients: z.ZodArray<z.core.$ZodBranded<z.ZodString, "DataSubjectId", "out">>;
|
|
256
|
+
vars: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
257
|
+
about: z.ZodDefault<z.ZodNullable<z.ZodObject<{
|
|
258
|
+
entityType: z.ZodString;
|
|
259
|
+
entityId: z.ZodString;
|
|
260
|
+
}, z.core.$strip>>>;
|
|
261
|
+
template: z.ZodObject<{
|
|
262
|
+
subject: z.ZodString;
|
|
263
|
+
text: z.ZodString;
|
|
264
|
+
html: z.ZodOptional<z.ZodString>;
|
|
265
|
+
}, z.core.$strip>;
|
|
266
|
+
required: z.ZodDefault<z.ZodBoolean>;
|
|
267
|
+
}, z.core.$strip>;
|
|
268
|
+
export type NotifyInput = z.input<typeof notifyInput>;
|
|
269
|
+
/** What `notify` did for one recipient. */
|
|
270
|
+
export interface NotifyOutcome {
|
|
271
|
+
principal: string;
|
|
272
|
+
status: 'requested' | 'skipped';
|
|
273
|
+
/**
|
|
274
|
+
* On a skip: `muted`, `no_address`, `duplicate` (this key already reached them), or `digest`
|
|
275
|
+
* (held for their next digest rather than mailed on its own). Null when requested.
|
|
276
|
+
*/
|
|
277
|
+
reason: string | null;
|
|
278
|
+
/** The send intent's id, when one was written. */
|
|
279
|
+
request: string | null;
|
|
280
|
+
/** The delivery row, or null on a `duplicate`, which writes none. */
|
|
281
|
+
deliveryId: string | null;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Notify `recipients` about one occasion, inside the caller's transaction.
|
|
285
|
+
*
|
|
286
|
+
* For each person: a delivery this `dedupeKey` already made for them is left alone; a muted
|
|
287
|
+
* topic (unless `required`) or a missing address records a `skipped` delivery; a topic they
|
|
288
|
+
* take as a digest (unless `required`) records a `skipped` delivery with reason `digest` and
|
|
289
|
+
* holds the rendered message for `sendDigests`; otherwise a send intent is written with
|
|
290
|
+
* `requestEmail`, naming the person as its `subjectId`, and a `requested` delivery records it.
|
|
291
|
+
* The platform's outcome event settles that delivery later (the engine's own consumers).
|
|
292
|
+
*
|
|
293
|
+
* **It throws `validation_failed`, writing nothing, when the template cannot be filled** — a
|
|
294
|
+
* placeholder with no variable, or a message every word of which was a blank variable — or a
|
|
295
|
+
* recipient is not a principal id. That is a defect in the caller, and the throw rolls back the
|
|
296
|
+
* caller's whole transaction: from a consumer, the event dead-letters in the kernel's delivery
|
|
297
|
+
* journal with the topic and the reason in its error; from an operation, the operation fails.
|
|
298
|
+
* A blank mail is never the fallback.
|
|
299
|
+
*
|
|
300
|
+
* No permission check: like every by-call engine function, the caller checks first. A consumer
|
|
301
|
+
* calling it runs under the kernel's consumer authority.
|
|
302
|
+
*/
|
|
303
|
+
export declare function notify(ctx: OperationContext, rawInput: NotifyInput): NotifyOutcome[];
|
|
304
|
+
/** The caller's address and every known topic's switch and cadence. */
|
|
305
|
+
export declare function myPreferencesOf(ctx: OperationContext): z.infer<typeof myPreferences>;
|
|
306
|
+
/** Set or clear where the caller's notifications go. */
|
|
307
|
+
export declare function setMyAddress(ctx: OperationContext, input: {
|
|
308
|
+
email: string | null;
|
|
309
|
+
}): z.infer<typeof myPreferences>;
|
|
310
|
+
/**
|
|
311
|
+
* Switch one topic on or off for the caller, and with `cadence`, choose whether it arrives at
|
|
312
|
+
* once or in their daily digest (absent leaves that as it is). The topic must be one this scope
|
|
313
|
+
* has used (`not_found` otherwise). A required topic refuses to be switched off, and refuses a
|
|
314
|
+
* digest: mail a person must not miss does not wait a day either.
|
|
315
|
+
*
|
|
316
|
+
* Switching a topic off withdraws its items still held for the caller's digest — a mute wins
|
|
317
|
+
* over the queue. Each withdrawn occasion's delivery is settled `skipped`/`muted`, as if the mute
|
|
318
|
+
* had come first, and the event names them. Switching a digest back to `immediate` leaves what is
|
|
319
|
+
* already held to go out with the next digest.
|
|
320
|
+
*/
|
|
321
|
+
export declare function setMyPreference(ctx: OperationContext, input: {
|
|
322
|
+
topic: string;
|
|
323
|
+
channel: NotificationChannel;
|
|
324
|
+
enabled: boolean;
|
|
325
|
+
cadence?: NotificationCadence | undefined;
|
|
326
|
+
}): z.infer<typeof myPreferences>;
|
|
327
|
+
/** What became of each notification, newest first. */
|
|
328
|
+
export declare function listDeliveries(ctx: OperationContext, page: PageParams): Page<z.infer<typeof deliveryRow>>;
|
|
329
|
+
/**
|
|
330
|
+
* What became of each notification sent to the caller, newest first: the same page as
|
|
331
|
+
* `listDeliveries`, with the `principal` filter pinned to `ctx.principal`, last, so nothing in
|
|
332
|
+
* `page` can name anyone else.
|
|
333
|
+
*
|
|
334
|
+
* Every delivery of theirs is listed, including the ones that were not mailed on their own — a
|
|
335
|
+
* topic they muted (`skipped`/`muted`), a missing address (`skipped`/`no_address`), an occasion
|
|
336
|
+
* held for their digest (`skipped`/`digest`, settled once the digest that carried it is
|
|
337
|
+
* written) — and the digest itself, which is a delivery of its own under `DIGEST_TOPIC`. A held
|
|
338
|
+
* message set aside as unreadable reads `skipped`/`quarantined` until it is released.
|
|
339
|
+
*
|
|
340
|
+
* The row is `myDeliveryRow`, not the administrator's: it names neither the caller's idempotency
|
|
341
|
+
* key nor the platform's send intent, and a `refused` or `dead_lettered` row carries no `reason`,
|
|
342
|
+
* since that is the platform's outcome code (a missing sender grant, a provider refusal) and
|
|
343
|
+
* belongs to whoever reads the whole log. The `reason` of a `skipped` row is the engine's own
|
|
344
|
+
* word for it.
|
|
345
|
+
*
|
|
346
|
+
* No permission check: like every by-call engine function, the caller checks first.
|
|
347
|
+
*/
|
|
348
|
+
export declare function listMyDeliveries(ctx: OperationContext, page: PageParams): Page<z.infer<typeof myDeliveryRow>>;
|
|
349
|
+
/** What one pass of `sendDigests` did. */
|
|
350
|
+
export interface DigestPass {
|
|
351
|
+
/** People whose digest went out (or was recorded skipped for want of an address). */
|
|
352
|
+
digests: number;
|
|
353
|
+
/** The occasions those digests carried. */
|
|
354
|
+
items: number;
|
|
355
|
+
/** People whose digest could not be composed: theirs stay held, retried a window later. */
|
|
356
|
+
failed: number;
|
|
357
|
+
/** People who were due but left for a later pass, because the scope had no room for more sends. */
|
|
358
|
+
deferred: number;
|
|
359
|
+
/** Held occasions found unreadable and set aside this pass, unsent. */
|
|
360
|
+
quarantined: number;
|
|
361
|
+
/** Set-aside occasions that read again this pass and are held for the digest once more. */
|
|
362
|
+
released: number;
|
|
363
|
+
/** Held occasions withdrawn this pass, unsent, because their person has switched the topic off since. */
|
|
364
|
+
withdrawn: number;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Send each person whose digest is due one mail of what is held for them.
|
|
368
|
+
*
|
|
369
|
+
* **Who is due.** A person is due once their oldest held message is a whole window old (a day,
|
|
370
|
+
* for `daily-digest`). So nothing waits much longer than a day, and while no more than
|
|
371
|
+
* `DIGEST_MAX_ITEMS` are held, a person gets one digest a day: everything held after it is
|
|
372
|
+
* younger. Oldest first, so a backlog drains in the order it built up. A person whose last
|
|
373
|
+
* attempt failed waits a window before the next.
|
|
374
|
+
*
|
|
375
|
+
* **How many people.** Each mail is a platform send intent, and a scope may hold only
|
|
376
|
+
* `MAX_PENDING_PLATFORM_REQUESTS` of those waiting for the drain — this transaction's own
|
|
377
|
+
* included. A pass therefore sends to at most the room that is left, less `DIGEST_RESERVE`
|
|
378
|
+
* slots kept for mail that must not wait, and leaves everyone else held for the next pass. It
|
|
379
|
+
* never asks for more room than there is, so it never meets the cap's refusal.
|
|
380
|
+
*
|
|
381
|
+
* **How much each.** One digest carries at most `DIGEST_MAX_ITEMS` occasions, oldest first. The
|
|
382
|
+
* rest stay held, and are due by the same rule: as soon as the oldest of them is a window old —
|
|
383
|
+
* on the very next pass if it already is. That is the one case where a person gets more than one
|
|
384
|
+
* digest a day.
|
|
385
|
+
*
|
|
386
|
+
* **What does not read.** A held row that no longer matches the published shape is set aside
|
|
387
|
+
* (`quarantine`) rather than mailed or allowed to block the rest: the person's readable messages
|
|
388
|
+
* go out, and the set-aside row counts toward nothing — not who is due, not a switch-off's
|
|
389
|
+
* withdrawal, not the stale-hold warning. Its cause may pass (a version that stored a shape this
|
|
390
|
+
* one rejects is rolled forward again), so each set-aside row is read again once a window
|
|
391
|
+
* (`releaseReadable`, at the start of a pass) and held once more when it reads — unless its topic
|
|
392
|
+
* was switched off meanwhile, when it is withdrawn instead.
|
|
393
|
+
*
|
|
394
|
+
* **A mute wins, last of all.** The read that composes a digest carries each row's mute
|
|
395
|
+
* (`MUTED`), and a held message whose topic is switched off is withdrawn (`withdrawMuted`), never
|
|
396
|
+
* mailed, whatever path brought it there.
|
|
397
|
+
*
|
|
398
|
+
* **One person at a time.** Each person's digest is its own `ctx.atomic`: one mail written with
|
|
399
|
+
* `requestEmail`, one delivery under `DIGEST_TOPIC`, the carried occasions settled and no longer
|
|
400
|
+
* held. If any of that throws, none of it happened for that person — their messages stay held,
|
|
401
|
+
* `notifications.digest-failed` and an error line say so — and everyone else's digest still goes
|
|
402
|
+
* out. A person who cleared their address since gets a `skipped` (`no_address`) digest, as an
|
|
403
|
+
* immediate notification would have.
|
|
404
|
+
*
|
|
405
|
+
* The engine's schedule runs it through `notifications/send-digests` every
|
|
406
|
+
* `DIGEST_TICK_MINUTES`; a vertical may also call it, after its own permission check, like every
|
|
407
|
+
* by-call function here. Either way it is one pass, and it sends only the people who are due.
|
|
408
|
+
*/
|
|
409
|
+
export declare function sendDigests(ctx: OperationContext): Promise<DigestPass>;
|
|
410
|
+
export declare const notificationsModule: ModuleRegistration;
|
|
411
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|