@smthrs/notifications 0.0.0-stage → 1.0.0-rc.12
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 +127 -0
- package/LICENSE +21 -0
- package/README.md +93 -2
- package/dist/cjs/AlertPolicy.d.ts +284 -0
- package/dist/cjs/AlertPolicy.d.ts.map +1 -0
- package/dist/cjs/AlertPolicy.js +124 -0
- package/dist/cjs/AlertPolicy.js.map +7 -0
- package/dist/cjs/AlertRuntime.d.ts +90 -0
- package/dist/cjs/AlertRuntime.d.ts.map +1 -0
- package/dist/cjs/AlertRuntime.js +166 -0
- package/dist/cjs/AlertRuntime.js.map +7 -0
- package/dist/cjs/AlertSink.d.ts +101 -0
- package/dist/cjs/AlertSink.d.ts.map +1 -0
- package/dist/cjs/AlertSink.js +44 -0
- package/dist/cjs/AlertSink.js.map +7 -0
- package/dist/cjs/Alerts.d.ts +57 -0
- package/dist/cjs/Alerts.d.ts.map +1 -0
- package/dist/cjs/Alerts.js +46 -0
- package/dist/cjs/Alerts.js.map +7 -0
- package/dist/cjs/Notification.d.ts +214 -0
- package/dist/cjs/Notification.d.ts.map +1 -0
- package/dist/cjs/Notification.js +62 -0
- package/dist/cjs/Notification.js.map +7 -0
- package/dist/cjs/NotificationEvent.d.ts +214 -0
- package/dist/cjs/NotificationEvent.d.ts.map +1 -0
- package/dist/cjs/NotificationEvent.js +84 -0
- package/dist/cjs/NotificationEvent.js.map +7 -0
- package/dist/cjs/NotificationQueue.d.ts +194 -0
- package/dist/cjs/NotificationQueue.d.ts.map +1 -0
- package/dist/cjs/NotificationQueue.js +477 -0
- package/dist/cjs/NotificationQueue.js.map +7 -0
- package/dist/cjs/NotificationState.d.ts +169 -0
- package/dist/cjs/NotificationState.d.ts.map +1 -0
- package/dist/cjs/NotificationState.js +110 -0
- package/dist/cjs/NotificationState.js.map +7 -0
- package/dist/cjs/Projection.d.ts +30 -0
- package/dist/cjs/Projection.d.ts.map +1 -0
- package/dist/cjs/Projection.js +53 -0
- package/dist/cjs/Projection.js.map +7 -0
- package/dist/cjs/SteerPayload.d.ts +177 -0
- package/dist/cjs/SteerPayload.d.ts.map +1 -0
- package/dist/cjs/SteerPayload.js +61 -0
- package/dist/cjs/SteerPayload.js.map +7 -0
- package/dist/cjs/index.d.ts +41 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +47 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/foldCache.d.ts +36 -0
- package/dist/cjs/internal/foldCache.d.ts.map +1 -0
- package/dist/cjs/internal/foldCache.js +54 -0
- package/dist/cjs/internal/foldCache.js.map +7 -0
- package/dist/cjs/internal/replay.d.ts +22 -0
- package/dist/cjs/internal/replay.d.ts.map +1 -0
- package/dist/cjs/internal/replay.js +46 -0
- package/dist/cjs/internal/replay.js.map +7 -0
- package/dist/cjs/layerWebhook.d.ts +60 -0
- package/dist/cjs/layerWebhook.d.ts.map +1 -0
- package/dist/cjs/layerWebhook.js +145 -0
- package/dist/cjs/layerWebhook.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/AlertPolicy.d.ts +284 -0
- package/dist/esm/AlertPolicy.d.ts.map +1 -0
- package/dist/esm/AlertPolicy.js +284 -0
- package/dist/esm/AlertPolicy.js.map +1 -0
- package/dist/esm/AlertRuntime.d.ts +90 -0
- package/dist/esm/AlertRuntime.d.ts.map +1 -0
- package/dist/esm/AlertRuntime.js +191 -0
- package/dist/esm/AlertRuntime.js.map +1 -0
- package/dist/esm/AlertSink.d.ts +101 -0
- package/dist/esm/AlertSink.d.ts.map +1 -0
- package/dist/esm/AlertSink.js +63 -0
- package/dist/esm/AlertSink.js.map +1 -0
- package/dist/esm/Alerts.d.ts +57 -0
- package/dist/esm/Alerts.d.ts.map +1 -0
- package/dist/esm/Alerts.js +57 -0
- package/dist/esm/Alerts.js.map +1 -0
- package/dist/esm/Notification.d.ts +214 -0
- package/dist/esm/Notification.d.ts.map +1 -0
- package/dist/esm/Notification.js +121 -0
- package/dist/esm/Notification.js.map +1 -0
- package/dist/esm/NotificationEvent.d.ts +214 -0
- package/dist/esm/NotificationEvent.d.ts.map +1 -0
- package/dist/esm/NotificationEvent.js +158 -0
- package/dist/esm/NotificationEvent.js.map +1 -0
- package/dist/esm/NotificationQueue.d.ts +194 -0
- package/dist/esm/NotificationQueue.d.ts.map +1 -0
- package/dist/esm/NotificationQueue.js +625 -0
- package/dist/esm/NotificationQueue.js.map +1 -0
- package/dist/esm/NotificationState.d.ts +169 -0
- package/dist/esm/NotificationState.d.ts.map +1 -0
- package/dist/esm/NotificationState.js +191 -0
- package/dist/esm/NotificationState.js.map +1 -0
- package/dist/esm/Projection.d.ts +30 -0
- package/dist/esm/Projection.d.ts.map +1 -0
- package/dist/esm/Projection.js +47 -0
- package/dist/esm/Projection.js.map +1 -0
- package/dist/esm/SteerPayload.d.ts +177 -0
- package/dist/esm/SteerPayload.d.ts.map +1 -0
- package/dist/esm/SteerPayload.js +139 -0
- package/dist/esm/SteerPayload.js.map +1 -0
- package/dist/esm/index.d.ts +41 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +41 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/foldCache.d.ts +36 -0
- package/dist/esm/internal/foldCache.d.ts.map +1 -0
- package/dist/esm/internal/foldCache.js +58 -0
- package/dist/esm/internal/foldCache.js.map +1 -0
- package/dist/esm/internal/replay.d.ts +22 -0
- package/dist/esm/internal/replay.d.ts.map +1 -0
- package/dist/esm/internal/replay.js +31 -0
- package/dist/esm/internal/replay.js.map +1 -0
- package/dist/esm/layerWebhook.d.ts +60 -0
- package/dist/esm/layerWebhook.d.ts.map +1 -0
- package/dist/esm/layerWebhook.js +186 -0
- package/dist/esm/layerWebhook.js.map +1 -0
- package/docs/README.md +146 -0
- package/docs/api.md +443 -0
- package/docs/concepts/admission-and-promotion.md +144 -0
- package/docs/concepts/alerting.md +138 -0
- package/docs/concepts/journal-records.md +132 -0
- package/docs/guides/alert-on-a-stuck-run.md +140 -0
- package/docs/guides/drain-at-a-turn-boundary.md +95 -0
- package/docs/guides/handle-a-full-queue.md +102 -0
- package/docs/guides/report-pending-notifications.md +92 -0
- package/docs/guides/send-alerts-to-a-webhook.md +155 -0
- package/docs/guides/steer-a-run.md +127 -0
- package/docs/guides/testing.md +161 -0
- package/docs/installation.md +123 -0
- package/docs/quickstart.md +159 -0
- package/docs/troubleshooting.md +254 -0
- package/package.json +182 -3
- package/src/AlertPolicy.ts +377 -0
- package/src/AlertRuntime.ts +280 -0
- package/src/AlertSink.ts +105 -0
- package/src/Alerts.ts +71 -0
- package/src/Notification.ts +171 -0
- package/src/NotificationEvent.ts +213 -0
- package/src/NotificationQueue.ts +856 -0
- package/src/NotificationState.ts +295 -0
- package/src/Projection.ts +48 -0
- package/src/SteerPayload.ts +198 -0
- package/src/index.ts +47 -0
- package/src/internal/foldCache.ts +72 -0
- package/src/internal/replay.ts +39 -0
- package/src/layerWebhook.ts +239 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journal event types owned by the notification queue.
|
|
3
|
+
*
|
|
4
|
+
* @since 0.1.0
|
|
5
|
+
*/
|
|
6
|
+
import { JournalEvent } from "@smthrs/journal";
|
|
7
|
+
import * as Option from "effect/Option";
|
|
8
|
+
import * as Schema from "effect/Schema";
|
|
9
|
+
/**
|
|
10
|
+
* The journal `eventType` recorded for one admission decision.
|
|
11
|
+
*
|
|
12
|
+
* The spelling is slashes and a PascalCase leaf, where every other event type
|
|
13
|
+
* in this repository is dot-separated and lowercase (`control.run.parked`,
|
|
14
|
+
* `flows.alerts.delivered`). The value is already durable in every engine
|
|
15
|
+
* database, and a rename would silently stop matching the projections that
|
|
16
|
+
* consumers key on, so it stays as written. A foreign projection must match
|
|
17
|
+
* this literal rather than the repository convention, which is why the two
|
|
18
|
+
* spellings are frozen by `test/WireFormat.test.ts`.
|
|
19
|
+
*
|
|
20
|
+
* @category constants
|
|
21
|
+
* @since 0.1.0
|
|
22
|
+
*/
|
|
23
|
+
export declare const AdmittedEventType = "flows/notifications/Admitted";
|
|
24
|
+
/**
|
|
25
|
+
* The journal `eventType` recorded when pending notifications are promoted.
|
|
26
|
+
*
|
|
27
|
+
* Spelled like {@link AdmittedEventType}, and frozen for the same reason.
|
|
28
|
+
*
|
|
29
|
+
* @category constants
|
|
30
|
+
* @since 0.1.0
|
|
31
|
+
*/
|
|
32
|
+
export declare const PromotedEventType = "flows/notifications/Promoted";
|
|
33
|
+
/**
|
|
34
|
+
* What a durable admission record says was decided.
|
|
35
|
+
*
|
|
36
|
+
* This is the one declaration of the admission vocabulary: it validates every
|
|
37
|
+
* journal entry through {@link fromEntry}, and `NotificationState` types its
|
|
38
|
+
* public signatures against it, so a fourth decision cannot be added to one
|
|
39
|
+
* half and forgotten in the other.
|
|
40
|
+
*
|
|
41
|
+
* @category models
|
|
42
|
+
* @since 1.0.0
|
|
43
|
+
*/
|
|
44
|
+
export declare const AdmissionDecision: Schema.Literals<readonly ["admitted", "coalesced", "rejected-full"]>;
|
|
45
|
+
/**
|
|
46
|
+
* The decoded form of {@link AdmissionDecision}.
|
|
47
|
+
*
|
|
48
|
+
* @category models
|
|
49
|
+
* @since 1.0.0
|
|
50
|
+
*/
|
|
51
|
+
export type AdmissionDecision = typeof AdmissionDecision.Type;
|
|
52
|
+
/**
|
|
53
|
+
* A durable admission record: the notification a run was told about, and what
|
|
54
|
+
* the queue decided to do with it.
|
|
55
|
+
*
|
|
56
|
+
* A `rejected-full` decision is never written. The queue refuses a full queue
|
|
57
|
+
* in the receipt alone, so the notification id stays admissible once a
|
|
58
|
+
* boundary drains. The literal remains in {@link AdmissionDecision} because a
|
|
59
|
+
* reader must stay total over a record any writer could have produced.
|
|
60
|
+
*
|
|
61
|
+
* @category models
|
|
62
|
+
* @since 0.1.0
|
|
63
|
+
*/
|
|
64
|
+
export declare const Admitted: Schema.Struct<{
|
|
65
|
+
readonly notification: Schema.Union<readonly [Schema.TaggedStruct<"human-steer", {
|
|
66
|
+
readonly delivery: Schema.Literal<"steer">;
|
|
67
|
+
readonly id: Schema.NonEmptyString;
|
|
68
|
+
readonly targetLineageId: Schema.NonEmptyString;
|
|
69
|
+
readonly provenance: Schema.Struct<{
|
|
70
|
+
readonly sourceRunId: Schema.String;
|
|
71
|
+
readonly sourceLineageId: Schema.String;
|
|
72
|
+
readonly sourceTurn: Schema.Int;
|
|
73
|
+
readonly sourceActor: Schema.String;
|
|
74
|
+
readonly attribution: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
|
|
75
|
+
}>;
|
|
76
|
+
readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
|
|
77
|
+
}>, Schema.TaggedStruct<"human-followup", {
|
|
78
|
+
readonly delivery: Schema.Literal<"queue">;
|
|
79
|
+
readonly id: Schema.NonEmptyString;
|
|
80
|
+
readonly targetLineageId: Schema.NonEmptyString;
|
|
81
|
+
readonly provenance: Schema.Struct<{
|
|
82
|
+
readonly sourceRunId: Schema.String;
|
|
83
|
+
readonly sourceLineageId: Schema.String;
|
|
84
|
+
readonly sourceTurn: Schema.Int;
|
|
85
|
+
readonly sourceActor: Schema.String;
|
|
86
|
+
readonly attribution: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
|
|
87
|
+
}>;
|
|
88
|
+
readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
|
|
89
|
+
}>, Schema.TaggedStruct<"system-event", {
|
|
90
|
+
readonly delivery: Schema.Literal<"queue">;
|
|
91
|
+
readonly coalescingKey: Schema.optional<Schema.String>;
|
|
92
|
+
readonly id: Schema.NonEmptyString;
|
|
93
|
+
readonly targetLineageId: Schema.NonEmptyString;
|
|
94
|
+
readonly provenance: Schema.Struct<{
|
|
95
|
+
readonly sourceRunId: Schema.String;
|
|
96
|
+
readonly sourceLineageId: Schema.String;
|
|
97
|
+
readonly sourceTurn: Schema.Int;
|
|
98
|
+
readonly sourceActor: Schema.String;
|
|
99
|
+
readonly attribution: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
|
|
100
|
+
}>;
|
|
101
|
+
readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
|
|
102
|
+
}>]>;
|
|
103
|
+
readonly decision: Schema.Literals<readonly ["admitted", "coalesced", "rejected-full"]>;
|
|
104
|
+
/** SHA-256 of canonical validated input, absent in legacy admission rows. */
|
|
105
|
+
readonly fingerprint: Schema.optional<Schema.String>;
|
|
106
|
+
readonly version: Schema.optional<Schema.Number>;
|
|
107
|
+
}>;
|
|
108
|
+
/**
|
|
109
|
+
* The decoded form of {@link Admitted}.
|
|
110
|
+
*
|
|
111
|
+
* @category models
|
|
112
|
+
* @since 0.1.0
|
|
113
|
+
*/
|
|
114
|
+
export type Admitted = typeof Admitted.Type;
|
|
115
|
+
/**
|
|
116
|
+
* A durable promotion record: which notifications one boundary of one lineage
|
|
117
|
+
* delivered.
|
|
118
|
+
*
|
|
119
|
+
* The triple `(runId, targetLineageId, boundary)` is the unit of promotion, so
|
|
120
|
+
* a second lineage draining the same boundary name is a separate record rather
|
|
121
|
+
* than a repeat of the first.
|
|
122
|
+
*
|
|
123
|
+
* @category models
|
|
124
|
+
* @since 0.1.0
|
|
125
|
+
*/
|
|
126
|
+
export declare const Promoted: Schema.Struct<{
|
|
127
|
+
readonly boundary: Schema.String;
|
|
128
|
+
readonly targetLineageId: Schema.String;
|
|
129
|
+
readonly ids: Schema.$Array<Schema.String>;
|
|
130
|
+
}>;
|
|
131
|
+
/**
|
|
132
|
+
* The decoded form of {@link Promoted}.
|
|
133
|
+
*
|
|
134
|
+
* @category models
|
|
135
|
+
* @since 0.1.0
|
|
136
|
+
*/
|
|
137
|
+
export type Promoted = typeof Promoted.Type;
|
|
138
|
+
/**
|
|
139
|
+
* Any journal event owned by this package.
|
|
140
|
+
*
|
|
141
|
+
* The two members are told apart with {@link isAdmitted} and
|
|
142
|
+
* {@link isPromoted} rather than a stored discriminant, because the discriminant
|
|
143
|
+
* would be a durable field this vocabulary does not have.
|
|
144
|
+
*
|
|
145
|
+
* @category models
|
|
146
|
+
* @since 0.1.0
|
|
147
|
+
*/
|
|
148
|
+
export type Event = Admitted | Promoted;
|
|
149
|
+
/**
|
|
150
|
+
* Whether an owned event is an admission record.
|
|
151
|
+
*
|
|
152
|
+
* @param event an owned event
|
|
153
|
+
* @category refinements
|
|
154
|
+
* @since 1.0.0
|
|
155
|
+
*/
|
|
156
|
+
export declare const isAdmitted: (event: Event) => event is Admitted;
|
|
157
|
+
/**
|
|
158
|
+
* Whether an owned event is a promotion record.
|
|
159
|
+
*
|
|
160
|
+
* @param event an owned event
|
|
161
|
+
* @category refinements
|
|
162
|
+
* @since 1.0.0
|
|
163
|
+
*/
|
|
164
|
+
export declare const isPromoted: (event: Event) => event is Promoted;
|
|
165
|
+
/**
|
|
166
|
+
* The journal source every admission of one notification is written under.
|
|
167
|
+
*
|
|
168
|
+
* @param notificationId the admitted notification's id
|
|
169
|
+
* @category constructors
|
|
170
|
+
* @since 1.0.0
|
|
171
|
+
*/
|
|
172
|
+
export declare const admissionSourceId: (notificationId: string) => JournalEvent.SourceId;
|
|
173
|
+
/**
|
|
174
|
+
* The identity of one drain: the lineage and the boundary, each encoded so a
|
|
175
|
+
* value containing a slash cannot forge another pair's identity.
|
|
176
|
+
*
|
|
177
|
+
* @param targetLineageId the lineage the boundary closed on
|
|
178
|
+
* @param boundary the boundary name
|
|
179
|
+
* @category constructors
|
|
180
|
+
* @since 1.0.0
|
|
181
|
+
*/
|
|
182
|
+
export declare const drainKey: (targetLineageId: string, boundary: string) => string;
|
|
183
|
+
/**
|
|
184
|
+
* The journal source one drain's promotion record is written under.
|
|
185
|
+
*
|
|
186
|
+
* @param targetLineageId the lineage the boundary closed on
|
|
187
|
+
* @param boundary the boundary name
|
|
188
|
+
* @category constructors
|
|
189
|
+
* @since 1.0.0
|
|
190
|
+
*/
|
|
191
|
+
export declare const drainSourceId: (targetLineageId: string, boundary: string) => JournalEvent.SourceId;
|
|
192
|
+
/**
|
|
193
|
+
* Decodes an owned notification event from a journal entry.
|
|
194
|
+
*
|
|
195
|
+
* Foreign entries and structurally invalid payloads decode to `None` so a
|
|
196
|
+
* projection over a shared journal stays total.
|
|
197
|
+
*
|
|
198
|
+
* The event type alone does not make an entry owned: any producer on a shared
|
|
199
|
+
* journal can choose one. An admission counts only under
|
|
200
|
+
* {@link admissionSourceId} for the notification it carries, and a promotion
|
|
201
|
+
* only under the `/notifications/drain/` source family the queue writes, so
|
|
202
|
+
* another producer's record with the same event type stays out of the
|
|
203
|
+
* projection. This scopes records; it does not authenticate the writer. The
|
|
204
|
+
* journal fences writes on run ownership, not on source, so a writer that
|
|
205
|
+
* sets this package's source id is read as this package. The drain check is
|
|
206
|
+
* a prefix because records written before the lineage joined the identity
|
|
207
|
+
* carry the boundary alone.
|
|
208
|
+
*
|
|
209
|
+
* @param entry one journal entry, owned or foreign
|
|
210
|
+
* @category constructors
|
|
211
|
+
* @since 0.1.0
|
|
212
|
+
*/
|
|
213
|
+
export declare const fromEntry: (entry: JournalEvent.Entry) => Option.Option<Event>;
|
|
214
|
+
//# sourceMappingURL=NotificationEvent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NotificationEvent.d.ts","sourceRoot":"","sources":["../../src/NotificationEvent.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,iCAAiC,CAAA;AAE/D;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,iCAAiC,CAAA;AAE/D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,sEAA8D,CAAA;AAE5F;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,OAAO,iBAAiB,CAAC,IAAI,CAAA;AAE7D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAGnB,6EAA6E;;;EAG7E,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAE3C;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ;;;;EAInB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAE3C;;;;;;;;;GASG;AACH,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,KAAK,KAAG,KAAK,IAAI,QAAmC,CAAA;AAEtF;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,KAAK,KAAG,KAAK,IAAI,QAA8B,CAAA;AAEjF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,gBAAgB,MAAM,KAAG,YAAY,CAAC,QACC,CAAA;AAEzE;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,GAAI,iBAAiB,MAAM,EAAE,UAAU,MAAM,KAAG,MACK,CAAA;AAE1E;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAI,iBAAiB,MAAM,EAAE,UAAU,MAAM,KAAG,YAAY,CAAC,QACG,CAAA;AAQ1F;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,YAAY,CAAC,KAAK,KAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAWxE,CAAA"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journal event types owned by the notification queue.
|
|
3
|
+
*
|
|
4
|
+
* @since 0.1.0
|
|
5
|
+
*/
|
|
6
|
+
import { JournalEvent } from "@smthrs/journal";
|
|
7
|
+
import * as Option from "effect/Option";
|
|
8
|
+
import * as Schema from "effect/Schema";
|
|
9
|
+
import * as Notification from "./Notification.js";
|
|
10
|
+
/**
|
|
11
|
+
* The journal `eventType` recorded for one admission decision.
|
|
12
|
+
*
|
|
13
|
+
* The spelling is slashes and a PascalCase leaf, where every other event type
|
|
14
|
+
* in this repository is dot-separated and lowercase (`control.run.parked`,
|
|
15
|
+
* `flows.alerts.delivered`). The value is already durable in every engine
|
|
16
|
+
* database, and a rename would silently stop matching the projections that
|
|
17
|
+
* consumers key on, so it stays as written. A foreign projection must match
|
|
18
|
+
* this literal rather than the repository convention, which is why the two
|
|
19
|
+
* spellings are frozen by `test/WireFormat.test.ts`.
|
|
20
|
+
*
|
|
21
|
+
* @category constants
|
|
22
|
+
* @since 0.1.0
|
|
23
|
+
*/
|
|
24
|
+
export const AdmittedEventType = "flows/notifications/Admitted";
|
|
25
|
+
/**
|
|
26
|
+
* The journal `eventType` recorded when pending notifications are promoted.
|
|
27
|
+
*
|
|
28
|
+
* Spelled like {@link AdmittedEventType}, and frozen for the same reason.
|
|
29
|
+
*
|
|
30
|
+
* @category constants
|
|
31
|
+
* @since 0.1.0
|
|
32
|
+
*/
|
|
33
|
+
export const PromotedEventType = "flows/notifications/Promoted";
|
|
34
|
+
/**
|
|
35
|
+
* What a durable admission record says was decided.
|
|
36
|
+
*
|
|
37
|
+
* This is the one declaration of the admission vocabulary: it validates every
|
|
38
|
+
* journal entry through {@link fromEntry}, and `NotificationState` types its
|
|
39
|
+
* public signatures against it, so a fourth decision cannot be added to one
|
|
40
|
+
* half and forgotten in the other.
|
|
41
|
+
*
|
|
42
|
+
* @category models
|
|
43
|
+
* @since 1.0.0
|
|
44
|
+
*/
|
|
45
|
+
export const AdmissionDecision = Schema.Literals(["admitted", "coalesced", "rejected-full"]);
|
|
46
|
+
/**
|
|
47
|
+
* A durable admission record: the notification a run was told about, and what
|
|
48
|
+
* the queue decided to do with it.
|
|
49
|
+
*
|
|
50
|
+
* A `rejected-full` decision is never written. The queue refuses a full queue
|
|
51
|
+
* in the receipt alone, so the notification id stays admissible once a
|
|
52
|
+
* boundary drains. The literal remains in {@link AdmissionDecision} because a
|
|
53
|
+
* reader must stay total over a record any writer could have produced.
|
|
54
|
+
*
|
|
55
|
+
* @category models
|
|
56
|
+
* @since 0.1.0
|
|
57
|
+
*/
|
|
58
|
+
export const Admitted = Schema.Struct({
|
|
59
|
+
notification: Notification.Notification,
|
|
60
|
+
decision: AdmissionDecision,
|
|
61
|
+
/** SHA-256 of canonical validated input, absent in legacy admission rows. */
|
|
62
|
+
fingerprint: Schema.optional(Schema.String),
|
|
63
|
+
version: Schema.optional(Schema.Number)
|
|
64
|
+
});
|
|
65
|
+
/**
|
|
66
|
+
* A durable promotion record: which notifications one boundary of one lineage
|
|
67
|
+
* delivered.
|
|
68
|
+
*
|
|
69
|
+
* The triple `(runId, targetLineageId, boundary)` is the unit of promotion, so
|
|
70
|
+
* a second lineage draining the same boundary name is a separate record rather
|
|
71
|
+
* than a repeat of the first.
|
|
72
|
+
*
|
|
73
|
+
* @category models
|
|
74
|
+
* @since 0.1.0
|
|
75
|
+
*/
|
|
76
|
+
export const Promoted = Schema.Struct({
|
|
77
|
+
boundary: Schema.String,
|
|
78
|
+
targetLineageId: Schema.String,
|
|
79
|
+
ids: Schema.Array(Schema.String)
|
|
80
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* Whether an owned event is an admission record.
|
|
83
|
+
*
|
|
84
|
+
* @param event an owned event
|
|
85
|
+
* @category refinements
|
|
86
|
+
* @since 1.0.0
|
|
87
|
+
*/
|
|
88
|
+
export const isAdmitted = (event) => "notification" in event;
|
|
89
|
+
/**
|
|
90
|
+
* Whether an owned event is a promotion record.
|
|
91
|
+
*
|
|
92
|
+
* @param event an owned event
|
|
93
|
+
* @category refinements
|
|
94
|
+
* @since 1.0.0
|
|
95
|
+
*/
|
|
96
|
+
export const isPromoted = (event) => !isAdmitted(event);
|
|
97
|
+
/**
|
|
98
|
+
* The journal source every admission of one notification is written under.
|
|
99
|
+
*
|
|
100
|
+
* @param notificationId the admitted notification's id
|
|
101
|
+
* @category constructors
|
|
102
|
+
* @since 1.0.0
|
|
103
|
+
*/
|
|
104
|
+
export const admissionSourceId = (notificationId) => JournalEvent.SourceId.make(`${admissionSourcePrefix}${notificationId}`);
|
|
105
|
+
/**
|
|
106
|
+
* The identity of one drain: the lineage and the boundary, each encoded so a
|
|
107
|
+
* value containing a slash cannot forge another pair's identity.
|
|
108
|
+
*
|
|
109
|
+
* @param targetLineageId the lineage the boundary closed on
|
|
110
|
+
* @param boundary the boundary name
|
|
111
|
+
* @category constructors
|
|
112
|
+
* @since 1.0.0
|
|
113
|
+
*/
|
|
114
|
+
export const drainKey = (targetLineageId, boundary) => `${encodeURIComponent(targetLineageId)}/${encodeURIComponent(boundary)}`;
|
|
115
|
+
/**
|
|
116
|
+
* The journal source one drain's promotion record is written under.
|
|
117
|
+
*
|
|
118
|
+
* @param targetLineageId the lineage the boundary closed on
|
|
119
|
+
* @param boundary the boundary name
|
|
120
|
+
* @category constructors
|
|
121
|
+
* @since 1.0.0
|
|
122
|
+
*/
|
|
123
|
+
export const drainSourceId = (targetLineageId, boundary) => JournalEvent.SourceId.make(`${drainSourcePrefix}${drainKey(targetLineageId, boundary)}`);
|
|
124
|
+
const admissionSourcePrefix = "/notifications/admission/";
|
|
125
|
+
const drainSourcePrefix = "/notifications/drain/";
|
|
126
|
+
const decodeAdmitted = Schema.decodeUnknownOption(Admitted);
|
|
127
|
+
const decodePromoted = Schema.decodeUnknownOption(Promoted);
|
|
128
|
+
/**
|
|
129
|
+
* Decodes an owned notification event from a journal entry.
|
|
130
|
+
*
|
|
131
|
+
* Foreign entries and structurally invalid payloads decode to `None` so a
|
|
132
|
+
* projection over a shared journal stays total.
|
|
133
|
+
*
|
|
134
|
+
* The event type alone does not make an entry owned: any producer on a shared
|
|
135
|
+
* journal can choose one. An admission counts only under
|
|
136
|
+
* {@link admissionSourceId} for the notification it carries, and a promotion
|
|
137
|
+
* only under the `/notifications/drain/` source family the queue writes, so
|
|
138
|
+
* another producer's record with the same event type stays out of the
|
|
139
|
+
* projection. This scopes records; it does not authenticate the writer. The
|
|
140
|
+
* journal fences writes on run ownership, not on source, so a writer that
|
|
141
|
+
* sets this package's source id is read as this package. The drain check is
|
|
142
|
+
* a prefix because records written before the lineage joined the identity
|
|
143
|
+
* carry the boundary alone.
|
|
144
|
+
*
|
|
145
|
+
* @param entry one journal entry, owned or foreign
|
|
146
|
+
* @category constructors
|
|
147
|
+
* @since 0.1.0
|
|
148
|
+
*/
|
|
149
|
+
export const fromEntry = (entry) => {
|
|
150
|
+
if (entry.eventType === AdmittedEventType) {
|
|
151
|
+
return Option.filter(decodeAdmitted(entry.payload), (event) => entry.sourceId === admissionSourceId(event.notification.id));
|
|
152
|
+
}
|
|
153
|
+
if (entry.eventType === PromotedEventType && entry.sourceId.startsWith(drainSourcePrefix)) {
|
|
154
|
+
return decodePromoted(entry.payload);
|
|
155
|
+
}
|
|
156
|
+
return Option.none();
|
|
157
|
+
};
|
|
158
|
+
//# sourceMappingURL=NotificationEvent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NotificationEvent.js","sourceRoot":"","sources":["../../src/NotificationEvent.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Journal event types owned by the notification queue.\n *\n * @since 0.1.0\n */\n\nimport { JournalEvent } from \"@smthrs/journal\"\nimport * as Option from \"effect/Option\"\nimport * as Schema from \"effect/Schema\"\nimport * as Notification from \"./Notification.ts\"\n\n/**\n * The journal `eventType` recorded for one admission decision.\n *\n * The spelling is slashes and a PascalCase leaf, where every other event type\n * in this repository is dot-separated and lowercase (`control.run.parked`,\n * `flows.alerts.delivered`). The value is already durable in every engine\n * database, and a rename would silently stop matching the projections that\n * consumers key on, so it stays as written. A foreign projection must match\n * this literal rather than the repository convention, which is why the two\n * spellings are frozen by `test/WireFormat.test.ts`.\n *\n * @category constants\n * @since 0.1.0\n */\nexport const AdmittedEventType = \"flows/notifications/Admitted\"\n\n/**\n * The journal `eventType` recorded when pending notifications are promoted.\n *\n * Spelled like {@link AdmittedEventType}, and frozen for the same reason.\n *\n * @category constants\n * @since 0.1.0\n */\nexport const PromotedEventType = \"flows/notifications/Promoted\"\n\n/**\n * What a durable admission record says was decided.\n *\n * This is the one declaration of the admission vocabulary: it validates every\n * journal entry through {@link fromEntry}, and `NotificationState` types its\n * public signatures against it, so a fourth decision cannot be added to one\n * half and forgotten in the other.\n *\n * @category models\n * @since 1.0.0\n */\nexport const AdmissionDecision = Schema.Literals([\"admitted\", \"coalesced\", \"rejected-full\"])\n\n/**\n * The decoded form of {@link AdmissionDecision}.\n *\n * @category models\n * @since 1.0.0\n */\nexport type AdmissionDecision = typeof AdmissionDecision.Type\n\n/**\n * A durable admission record: the notification a run was told about, and what\n * the queue decided to do with it.\n *\n * A `rejected-full` decision is never written. The queue refuses a full queue\n * in the receipt alone, so the notification id stays admissible once a\n * boundary drains. The literal remains in {@link AdmissionDecision} because a\n * reader must stay total over a record any writer could have produced.\n *\n * @category models\n * @since 0.1.0\n */\nexport const Admitted = Schema.Struct({\n notification: Notification.Notification,\n decision: AdmissionDecision,\n /** SHA-256 of canonical validated input, absent in legacy admission rows. */\n fingerprint: Schema.optional(Schema.String),\n version: Schema.optional(Schema.Number)\n})\n\n/**\n * The decoded form of {@link Admitted}.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Admitted = typeof Admitted.Type\n\n/**\n * A durable promotion record: which notifications one boundary of one lineage\n * delivered.\n *\n * The triple `(runId, targetLineageId, boundary)` is the unit of promotion, so\n * a second lineage draining the same boundary name is a separate record rather\n * than a repeat of the first.\n *\n * @category models\n * @since 0.1.0\n */\nexport const Promoted = Schema.Struct({\n boundary: Schema.String,\n targetLineageId: Schema.String,\n ids: Schema.Array(Schema.String)\n})\n\n/**\n * The decoded form of {@link Promoted}.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Promoted = typeof Promoted.Type\n\n/**\n * Any journal event owned by this package.\n *\n * The two members are told apart with {@link isAdmitted} and\n * {@link isPromoted} rather than a stored discriminant, because the discriminant\n * would be a durable field this vocabulary does not have.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Event = Admitted | Promoted\n\n/**\n * Whether an owned event is an admission record.\n *\n * @param event an owned event\n * @category refinements\n * @since 1.0.0\n */\nexport const isAdmitted = (event: Event): event is Admitted => \"notification\" in event\n\n/**\n * Whether an owned event is a promotion record.\n *\n * @param event an owned event\n * @category refinements\n * @since 1.0.0\n */\nexport const isPromoted = (event: Event): event is Promoted => !isAdmitted(event)\n\n/**\n * The journal source every admission of one notification is written under.\n *\n * @param notificationId the admitted notification's id\n * @category constructors\n * @since 1.0.0\n */\nexport const admissionSourceId = (notificationId: string): JournalEvent.SourceId =>\n JournalEvent.SourceId.make(`${admissionSourcePrefix}${notificationId}`)\n\n/**\n * The identity of one drain: the lineage and the boundary, each encoded so a\n * value containing a slash cannot forge another pair's identity.\n *\n * @param targetLineageId the lineage the boundary closed on\n * @param boundary the boundary name\n * @category constructors\n * @since 1.0.0\n */\nexport const drainKey = (targetLineageId: string, boundary: string): string =>\n `${encodeURIComponent(targetLineageId)}/${encodeURIComponent(boundary)}`\n\n/**\n * The journal source one drain's promotion record is written under.\n *\n * @param targetLineageId the lineage the boundary closed on\n * @param boundary the boundary name\n * @category constructors\n * @since 1.0.0\n */\nexport const drainSourceId = (targetLineageId: string, boundary: string): JournalEvent.SourceId =>\n JournalEvent.SourceId.make(`${drainSourcePrefix}${drainKey(targetLineageId, boundary)}`)\n\nconst admissionSourcePrefix = \"/notifications/admission/\"\nconst drainSourcePrefix = \"/notifications/drain/\"\n\nconst decodeAdmitted = Schema.decodeUnknownOption(Admitted)\nconst decodePromoted = Schema.decodeUnknownOption(Promoted)\n\n/**\n * Decodes an owned notification event from a journal entry.\n *\n * Foreign entries and structurally invalid payloads decode to `None` so a\n * projection over a shared journal stays total.\n *\n * The event type alone does not make an entry owned: any producer on a shared\n * journal can choose one. An admission counts only under\n * {@link admissionSourceId} for the notification it carries, and a promotion\n * only under the `/notifications/drain/` source family the queue writes, so\n * another producer's record with the same event type stays out of the\n * projection. This scopes records; it does not authenticate the writer. The\n * journal fences writes on run ownership, not on source, so a writer that\n * sets this package's source id is read as this package. The drain check is\n * a prefix because records written before the lineage joined the identity\n * carry the boundary alone.\n *\n * @param entry one journal entry, owned or foreign\n * @category constructors\n * @since 0.1.0\n */\nexport const fromEntry = (entry: JournalEvent.Entry): Option.Option<Event> => {\n if (entry.eventType === AdmittedEventType) {\n return Option.filter(\n decodeAdmitted(entry.payload),\n (event) => entry.sourceId === admissionSourceId(event.notification.id)\n )\n }\n if (entry.eventType === PromotedEventType && entry.sourceId.startsWith(drainSourcePrefix)) {\n return decodePromoted(entry.payload)\n }\n return Option.none()\n}\n"]}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journal-backed durable notification admission and turn-boundary drain.
|
|
3
|
+
*
|
|
4
|
+
* Governing contract: `packages/smithers/notifications/docs/api.md`, published as
|
|
5
|
+
* `/api/notifications`.
|
|
6
|
+
*
|
|
7
|
+
* @since 0.1.0
|
|
8
|
+
*/
|
|
9
|
+
import { Journal } from "@smthrs/journal";
|
|
10
|
+
import { Context, Effect, Layer, Schema } from "effect";
|
|
11
|
+
import * as NotificationModel from "./Notification.ts";
|
|
12
|
+
import * as NotificationState from "./NotificationState.ts";
|
|
13
|
+
declare const NotificationError_base: Schema.Class<NotificationError, Schema.TaggedStruct<"/notifications/NotificationError", {
|
|
14
|
+
readonly code: Schema.withConstructorDefault<Schema.Literals<readonly ["notification_unavailable", "notification_closed", "notification_full", "notification_id_reused", "notification_invalid", "notification_refused"]>>;
|
|
15
|
+
readonly message: Schema.String;
|
|
16
|
+
/** The notification the failure is about, when one was readable. */
|
|
17
|
+
readonly notificationId: Schema.optional<Schema.String>;
|
|
18
|
+
/** Dotted path of the offending field, for `notification_invalid`. */
|
|
19
|
+
readonly path: Schema.optional<Schema.String>;
|
|
20
|
+
}>, import("effect/Cause").YieldableError>;
|
|
21
|
+
/**
|
|
22
|
+
* The queue could not be reached, or it refused what a caller handed it.
|
|
23
|
+
*
|
|
24
|
+
* `code` is the stable half and the one to branch on. `notification_unavailable`
|
|
25
|
+
* is the seam reporting that it serves nothing; `notification_id_reused` says a
|
|
26
|
+
* stable id was admitted once already with different content, which is a
|
|
27
|
+
* producer bug rather than a storage failure; `notification_invalid` says the
|
|
28
|
+
* value is not a notification, and `path` names the field inside it that failed.
|
|
29
|
+
* `notification_closed` means the receiver has finished and the caller must
|
|
30
|
+
* start a new request; `notification_full` means nothing was retained and the
|
|
31
|
+
* same notification may be retried after pending work drains. Neither the
|
|
32
|
+
* message nor `path` ever carries the offending value.
|
|
33
|
+
* `notification_refused` rejects reserved outside-change payloads before admission.
|
|
34
|
+
*
|
|
35
|
+
* @category errors
|
|
36
|
+
* @since 0.1.0
|
|
37
|
+
*/
|
|
38
|
+
export declare class NotificationError extends NotificationError_base {
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* What admitting one notification decided.
|
|
42
|
+
*
|
|
43
|
+
* `seq` is the journal sequence the admission committed at, and it is absent
|
|
44
|
+
* exactly when nothing was written, which today means a `rejected-full`
|
|
45
|
+
* refusal. `duplicate` says the id was already admitted, in which case
|
|
46
|
+
* `decision` and `seq` are read back from the committed record rather than
|
|
47
|
+
* recomputed here.
|
|
48
|
+
*
|
|
49
|
+
* A caller MUST inspect `decision`: `rejected-full` means the queue was at
|
|
50
|
+
* capacity and retained nothing, so the notification never reaches the model
|
|
51
|
+
* unless the caller admits it again once a boundary has drained.
|
|
52
|
+
*
|
|
53
|
+
* @category models
|
|
54
|
+
* @since 0.1.0
|
|
55
|
+
*/
|
|
56
|
+
export interface AdmissionReceipt {
|
|
57
|
+
readonly notificationId: string;
|
|
58
|
+
readonly decision: NotificationState.AdmissionDecision;
|
|
59
|
+
readonly seq: number | undefined;
|
|
60
|
+
readonly duplicate: boolean;
|
|
61
|
+
readonly consumed?: boolean;
|
|
62
|
+
readonly consumedNotification?: NotificationModel.Notification;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The boundary a drain is attempted at, and whether the run would go idle
|
|
66
|
+
* if nothing were delivered.
|
|
67
|
+
*
|
|
68
|
+
* `(runId, targetLineageId, boundary)` is the unit of drain: two lineages
|
|
69
|
+
* closing a turn under the same boundary name are two drains, and each is
|
|
70
|
+
* recorded separately.
|
|
71
|
+
*
|
|
72
|
+
* @category models
|
|
73
|
+
* @since 0.1.0
|
|
74
|
+
*/
|
|
75
|
+
export interface DrainInput {
|
|
76
|
+
readonly runId: string;
|
|
77
|
+
readonly targetLineageId: string;
|
|
78
|
+
readonly boundary: string;
|
|
79
|
+
readonly wouldIdle: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* The journal sequence that opened this turn. A steer admitted after it is
|
|
82
|
+
* held for the next boundary, which is what keeps a message that arrived
|
|
83
|
+
* mid-turn out of the turn already in flight. Omitting it delivers
|
|
84
|
+
* everything pending for the lineage. A value that is not a non-negative
|
|
85
|
+
* integer fails the drain with `notification_invalid` and records nothing.
|
|
86
|
+
*/
|
|
87
|
+
readonly cutoffSeq?: number | undefined;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The notifications this boundary delivers, and whether the boundary had
|
|
91
|
+
* already drained.
|
|
92
|
+
*
|
|
93
|
+
* The notifications are the ones the committed promotion record names, so two
|
|
94
|
+
* processes draining one boundary report the same delivery rather than two
|
|
95
|
+
* divergent guesses.
|
|
96
|
+
*
|
|
97
|
+
* @category models
|
|
98
|
+
* @since 0.1.0
|
|
99
|
+
*/
|
|
100
|
+
export interface DrainReceipt {
|
|
101
|
+
readonly notifications: ReadonlyArray<NotificationModel.Notification>;
|
|
102
|
+
readonly boundary: string;
|
|
103
|
+
readonly duplicate: boolean;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The durable pending queue: admit a notification exactly once, and drain
|
|
107
|
+
* what a boundary is allowed to deliver.
|
|
108
|
+
*
|
|
109
|
+
* @category services
|
|
110
|
+
* @since 0.1.0
|
|
111
|
+
*/
|
|
112
|
+
export interface Service {
|
|
113
|
+
readonly admit: (runId: string, notification: NotificationModel.Notification, version?: number) => Effect.Effect<AdmissionReceipt, Journal.JournalError | NotificationError>;
|
|
114
|
+
readonly drain: (input: DrainInput) => Effect.Effect<DrainReceipt, Journal.JournalError | NotificationError>;
|
|
115
|
+
/**
|
|
116
|
+
* What this run has been told that no boundary has delivered yet, in
|
|
117
|
+
* admission order.
|
|
118
|
+
*
|
|
119
|
+
* The queue is the owner of that fact: pending is admitted minus promoted,
|
|
120
|
+
* and both halves are its own journal records. A supervisor that counted
|
|
121
|
+
* admissions alone would report a steer as waiting forever.
|
|
122
|
+
*/
|
|
123
|
+
readonly pending: (runId: string) => Effect.Effect<ReadonlyArray<NotificationModel.Notification>, Journal.JournalError | NotificationError>;
|
|
124
|
+
}
|
|
125
|
+
declare const NotificationQueue_base: Context.ServiceClass<NotificationQueue, "/notifications/NotificationQueue", Service>;
|
|
126
|
+
/**
|
|
127
|
+
* The {@link Service} tag.
|
|
128
|
+
*
|
|
129
|
+
* @category services
|
|
130
|
+
* @since 0.1.0
|
|
131
|
+
*/
|
|
132
|
+
export declare class NotificationQueue extends NotificationQueue_base {
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Builds a {@link Service} from an implementation of its methods.
|
|
136
|
+
*
|
|
137
|
+
* @param implementation the methods to serve
|
|
138
|
+
* @category constructors
|
|
139
|
+
* @since 0.1.0
|
|
140
|
+
*/
|
|
141
|
+
export declare const make: (implementation: Service) => Service;
|
|
142
|
+
/**
|
|
143
|
+
* A {@link Service} that fails every method as unavailable. Overrides
|
|
144
|
+
* replace individual methods.
|
|
145
|
+
*
|
|
146
|
+
* @param overrides methods to serve instead of failing
|
|
147
|
+
* @category constructors
|
|
148
|
+
* @since 0.1.0
|
|
149
|
+
*/
|
|
150
|
+
export declare const makeNoop: (overrides?: Partial<Service>) => Service;
|
|
151
|
+
/**
|
|
152
|
+
* Provides {@link makeNoop}.
|
|
153
|
+
*
|
|
154
|
+
* @param overrides methods to serve instead of failing
|
|
155
|
+
* @category layers
|
|
156
|
+
* @since 0.1.0
|
|
157
|
+
*/
|
|
158
|
+
export declare const layerNoop: (overrides?: Partial<Service>) => Layer.Layer<NotificationQueue>;
|
|
159
|
+
/**
|
|
160
|
+
* Journal-backed production layer, with the pending capacity a composition
|
|
161
|
+
* chooses.
|
|
162
|
+
*
|
|
163
|
+
* Admission and drain evidence is written through `emitDurableUnfenced`, so
|
|
164
|
+
* both return only after the corresponding entry is durably committed and a
|
|
165
|
+
* dropped lifecycle event is unrepresentable. The channel is the unfenced one
|
|
166
|
+
* on purpose: the notifying process owns no run, so there is no ownership
|
|
167
|
+
* fence to hand over, and both records are first-writer-wins on their own
|
|
168
|
+
* identity instead.
|
|
169
|
+
*
|
|
170
|
+
* The capacity guard is a conditional write at the storage layer, not a check
|
|
171
|
+
* against a fold one process happened to load: the fold, the guard, and the
|
|
172
|
+
* admission record are decided and written inside ONE `journal.transact`
|
|
173
|
+
* write transaction, which the durable writer serializes against every other
|
|
174
|
+
* writer of the database and replays when a concurrent writer commits between
|
|
175
|
+
* the guard's reads and the insert. Two processes admitting to one full queue
|
|
176
|
+
* therefore produce one admission and one `rejected-full`, never two
|
|
177
|
+
* admissions.
|
|
178
|
+
*
|
|
179
|
+
* @param options the pending capacity, defaulting to `NotificationState.defaultCapacity`
|
|
180
|
+
* @category layers
|
|
181
|
+
* @since 1.0.0
|
|
182
|
+
*/
|
|
183
|
+
export declare const layerWith: (options?: {
|
|
184
|
+
readonly capacity?: number | undefined;
|
|
185
|
+
}) => Layer.Layer<NotificationQueue, never, Journal.Journal>;
|
|
186
|
+
/**
|
|
187
|
+
* Journal-backed production layer at the default capacity.
|
|
188
|
+
*
|
|
189
|
+
* @category layers
|
|
190
|
+
* @since 0.1.0
|
|
191
|
+
*/
|
|
192
|
+
export declare const layer: Layer.Layer<NotificationQueue, never, Journal.Journal>;
|
|
193
|
+
export {};
|
|
194
|
+
//# sourceMappingURL=NotificationQueue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NotificationQueue.d.ts","sourceRoot":"","sources":["../../src/NotificationQueue.ts"],"names":[],"mappings":"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"}
|