@smthrs/notifications 0.0.0-stage → 1.0.0-rc.4
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 +91 -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 +193 -0
- package/dist/cjs/NotificationQueue.d.ts.map +1 -0
- package/dist/cjs/NotificationQueue.js +478 -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 +27 -0
- package/dist/cjs/Projection.d.ts.map +1 -0
- package/dist/cjs/Projection.js +54 -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/layerWebhook.d.ts +53 -0
- package/dist/cjs/layerWebhook.d.ts.map +1 -0
- package/dist/cjs/layerWebhook.js +107 -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 +193 -0
- package/dist/esm/NotificationQueue.d.ts.map +1 -0
- package/dist/esm/NotificationQueue.js +626 -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 +27 -0
- package/dist/esm/Projection.d.ts.map +1 -0
- package/dist/esm/Projection.js +43 -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/layerWebhook.d.ts +53 -0
- package/dist/esm/layerWebhook.d.ts.map +1 -0
- package/dist/esm/layerWebhook.js +118 -0
- package/dist/esm/layerWebhook.js.map +1 -0
- package/docs/README.md +144 -0
- package/docs/api.md +439 -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 +130 -0
- package/docs/guides/steer-a-run.md +127 -0
- package/docs/guides/testing.md +161 -0
- package/docs/installation.md +119 -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 +857 -0
- package/src/NotificationState.ts +295 -0
- package/src/Projection.ts +46 -0
- package/src/SteerPayload.ts +198 -0
- package/src/index.ts +47 -0
- package/src/internal/foldCache.ts +72 -0
- package/src/layerWebhook.ts +161 -0
|
@@ -0,0 +1,193 @@
|
|
|
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.
|
|
85
|
+
*/
|
|
86
|
+
readonly cutoffSeq?: number | undefined;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The notifications this boundary delivers, and whether the boundary had
|
|
90
|
+
* already drained.
|
|
91
|
+
*
|
|
92
|
+
* The notifications are the ones the committed promotion record names, so two
|
|
93
|
+
* processes draining one boundary report the same delivery rather than two
|
|
94
|
+
* divergent guesses.
|
|
95
|
+
*
|
|
96
|
+
* @category models
|
|
97
|
+
* @since 0.1.0
|
|
98
|
+
*/
|
|
99
|
+
export interface DrainReceipt {
|
|
100
|
+
readonly notifications: ReadonlyArray<NotificationModel.Notification>;
|
|
101
|
+
readonly boundary: string;
|
|
102
|
+
readonly duplicate: boolean;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The durable pending queue: admit a notification exactly once, and drain
|
|
106
|
+
* what a boundary is allowed to deliver.
|
|
107
|
+
*
|
|
108
|
+
* @category services
|
|
109
|
+
* @since 0.1.0
|
|
110
|
+
*/
|
|
111
|
+
export interface Service {
|
|
112
|
+
readonly admit: (runId: string, notification: NotificationModel.Notification, version?: number) => Effect.Effect<AdmissionReceipt, Journal.JournalError | NotificationError>;
|
|
113
|
+
readonly drain: (input: DrainInput) => Effect.Effect<DrainReceipt, Journal.JournalError | NotificationError>;
|
|
114
|
+
/**
|
|
115
|
+
* What this run has been told that no boundary has delivered yet, in
|
|
116
|
+
* admission order.
|
|
117
|
+
*
|
|
118
|
+
* The queue is the owner of that fact: pending is admitted minus promoted,
|
|
119
|
+
* and both halves are its own journal records. A supervisor that counted
|
|
120
|
+
* admissions alone would report a steer as waiting forever.
|
|
121
|
+
*/
|
|
122
|
+
readonly pending: (runId: string) => Effect.Effect<ReadonlyArray<NotificationModel.Notification>, Journal.JournalError | NotificationError>;
|
|
123
|
+
}
|
|
124
|
+
declare const NotificationQueue_base: Context.ServiceClass<NotificationQueue, "/notifications/NotificationQueue", Service>;
|
|
125
|
+
/**
|
|
126
|
+
* The {@link Service} tag.
|
|
127
|
+
*
|
|
128
|
+
* @category services
|
|
129
|
+
* @since 0.1.0
|
|
130
|
+
*/
|
|
131
|
+
export declare class NotificationQueue extends NotificationQueue_base {
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Builds a {@link Service} from an implementation of its methods.
|
|
135
|
+
*
|
|
136
|
+
* @param implementation the methods to serve
|
|
137
|
+
* @category constructors
|
|
138
|
+
* @since 0.1.0
|
|
139
|
+
*/
|
|
140
|
+
export declare const make: (implementation: Service) => Service;
|
|
141
|
+
/**
|
|
142
|
+
* A {@link Service} that fails every method as unavailable. Overrides
|
|
143
|
+
* replace individual methods.
|
|
144
|
+
*
|
|
145
|
+
* @param overrides methods to serve instead of failing
|
|
146
|
+
* @category constructors
|
|
147
|
+
* @since 0.1.0
|
|
148
|
+
*/
|
|
149
|
+
export declare const makeNoop: (overrides?: Partial<Service>) => Service;
|
|
150
|
+
/**
|
|
151
|
+
* Provides {@link makeNoop}.
|
|
152
|
+
*
|
|
153
|
+
* @param overrides methods to serve instead of failing
|
|
154
|
+
* @category layers
|
|
155
|
+
* @since 0.1.0
|
|
156
|
+
*/
|
|
157
|
+
export declare const layerNoop: (overrides?: Partial<Service>) => Layer.Layer<NotificationQueue>;
|
|
158
|
+
/**
|
|
159
|
+
* Journal-backed production layer, with the pending capacity a composition
|
|
160
|
+
* chooses.
|
|
161
|
+
*
|
|
162
|
+
* Admission and drain evidence is written through `emitDurableUnfenced`, so
|
|
163
|
+
* both return only after the corresponding entry is durably committed and a
|
|
164
|
+
* dropped lifecycle event is unrepresentable. The channel is the unfenced one
|
|
165
|
+
* on purpose: the notifying process owns no run, so there is no ownership
|
|
166
|
+
* fence to hand over, and both records are first-writer-wins on their own
|
|
167
|
+
* identity instead.
|
|
168
|
+
*
|
|
169
|
+
* The capacity guard is a conditional write at the storage layer, not a check
|
|
170
|
+
* against a fold one process happened to load: the fold, the guard, and the
|
|
171
|
+
* admission record are decided and written inside ONE `journal.transact`
|
|
172
|
+
* write transaction, which the durable writer serializes against every other
|
|
173
|
+
* writer of the database and replays when a concurrent writer commits between
|
|
174
|
+
* the guard's reads and the insert. Two processes admitting to one full queue
|
|
175
|
+
* therefore produce one admission and one `rejected-full`, never two
|
|
176
|
+
* admissions.
|
|
177
|
+
*
|
|
178
|
+
* @param options the pending capacity, defaulting to `NotificationState.defaultCapacity`
|
|
179
|
+
* @category layers
|
|
180
|
+
* @since 1.0.0
|
|
181
|
+
*/
|
|
182
|
+
export declare const layerWith: (options?: {
|
|
183
|
+
readonly capacity?: number | undefined;
|
|
184
|
+
}) => Layer.Layer<NotificationQueue, never, Journal.Journal>;
|
|
185
|
+
/**
|
|
186
|
+
* Journal-backed production layer at the default capacity.
|
|
187
|
+
*
|
|
188
|
+
* @category layers
|
|
189
|
+
* @since 0.1.0
|
|
190
|
+
*/
|
|
191
|
+
export declare const layer: Layer.Layer<NotificationQueue, never, Journal.Journal>;
|
|
192
|
+
export {};
|
|
193
|
+
//# 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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"}
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var NotificationQueue_exports = {};
|
|
30
|
+
__export(NotificationQueue_exports, {
|
|
31
|
+
NotificationError: () => NotificationError,
|
|
32
|
+
NotificationQueue: () => NotificationQueue,
|
|
33
|
+
layer: () => layer,
|
|
34
|
+
layerNoop: () => layerNoop,
|
|
35
|
+
layerWith: () => layerWith,
|
|
36
|
+
make: () => make,
|
|
37
|
+
makeNoop: () => makeNoop
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(NotificationQueue_exports);
|
|
40
|
+
var import_canonical = require("@smthrs/canonical");
|
|
41
|
+
var import_Sha256 = require("@smthrs/crypto/Sha256");
|
|
42
|
+
var import_journal = require("@smthrs/journal");
|
|
43
|
+
var import_effect = require("effect");
|
|
44
|
+
var FoldCache = __toESM(require("./internal/foldCache.js"), 1);
|
|
45
|
+
var NotificationModel = __toESM(require("./Notification.js"), 1);
|
|
46
|
+
var NotificationEvent = __toESM(require("./NotificationEvent.js"), 1);
|
|
47
|
+
var NotificationState = __toESM(require("./NotificationState.js"), 1);
|
|
48
|
+
class NotificationError extends import_effect.Schema.TaggedError()(
|
|
49
|
+
"/notifications/NotificationError",
|
|
50
|
+
{
|
|
51
|
+
code: import_effect.Schema.Literals([
|
|
52
|
+
"notification_unavailable",
|
|
53
|
+
"notification_closed",
|
|
54
|
+
"notification_full",
|
|
55
|
+
"notification_id_reused",
|
|
56
|
+
"notification_invalid",
|
|
57
|
+
"notification_refused"
|
|
58
|
+
]).pipe(
|
|
59
|
+
import_effect.Schema.withConstructorDefault(import_effect.Effect.succeed("notification_unavailable"))
|
|
60
|
+
),
|
|
61
|
+
message: import_effect.Schema.String,
|
|
62
|
+
/** The notification the failure is about, when one was readable. */
|
|
63
|
+
notificationId: import_effect.Schema.optional(import_effect.Schema.String),
|
|
64
|
+
/** Dotted path of the offending field, for `notification_invalid`. */
|
|
65
|
+
path: import_effect.Schema.optional(import_effect.Schema.String)
|
|
66
|
+
}
|
|
67
|
+
) {
|
|
68
|
+
}
|
|
69
|
+
class NotificationQueue extends import_effect.Context.Service()(
|
|
70
|
+
"/notifications/NotificationQueue"
|
|
71
|
+
) {
|
|
72
|
+
}
|
|
73
|
+
const make = (implementation) => NotificationQueue.of(implementation);
|
|
74
|
+
const unavailable = (operation) => new NotificationError({ code: "notification_unavailable", message: `${operation} is unavailable` });
|
|
75
|
+
const makeNoop = (overrides = {}) => make({
|
|
76
|
+
admit: import_effect.Effect.fn("NotificationQueue.admit")(() => import_effect.Effect.fail(unavailable("admit"))),
|
|
77
|
+
drain: import_effect.Effect.fn("NotificationQueue.drain")(() => import_effect.Effect.fail(unavailable("drain"))),
|
|
78
|
+
pending: import_effect.Effect.fn("NotificationQueue.pending")(() => import_effect.Effect.fail(unavailable("pending"))),
|
|
79
|
+
...overrides
|
|
80
|
+
});
|
|
81
|
+
const layerNoop = (overrides = {}) => import_effect.Layer.succeed(NotificationQueue)(makeNoop(overrides));
|
|
82
|
+
const maximumPayloadDepth = 256;
|
|
83
|
+
const maximumNotificationCodeUnits = 1048576;
|
|
84
|
+
const maximumIssueCodeUnits = 200;
|
|
85
|
+
const exceededBound = (value) => {
|
|
86
|
+
const pending = [{ value, depth: 0 }];
|
|
87
|
+
let size = 0;
|
|
88
|
+
while (pending.length > 0) {
|
|
89
|
+
const next = pending.pop();
|
|
90
|
+
size += typeof next.value === "string" ? next.value.length : 1;
|
|
91
|
+
if (size > maximumNotificationCodeUnits) return "size";
|
|
92
|
+
if (typeof next.value !== "object" || next.value === null) continue;
|
|
93
|
+
if (next.depth >= maximumPayloadDepth) return "depth";
|
|
94
|
+
if (Array.isArray(next.value)) {
|
|
95
|
+
for (const child of next.value) pending.push({ value: child, depth: next.depth + 1 });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
for (const [key, child] of Object.entries(next.value)) {
|
|
99
|
+
size += key.length;
|
|
100
|
+
pending.push({ value: child, depth: next.depth + 1 });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return void 0;
|
|
104
|
+
};
|
|
105
|
+
const copied = (value) => {
|
|
106
|
+
if (Array.isArray(value)) return value.map(copied);
|
|
107
|
+
if (typeof value === "object" && value !== null) {
|
|
108
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, copied(nested)]));
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
};
|
|
112
|
+
const formatIssue = import_effect.SchemaIssue.makeFormatterStandardSchemaV1({ leafHook: (issue) => issue._tag });
|
|
113
|
+
const bounded = (text) => text.length <= maximumIssueCodeUnits ? text : `${text.slice(0, maximumIssueCodeUnits - 3)}...`;
|
|
114
|
+
const identifiedBy = (value) => {
|
|
115
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
116
|
+
const id = value["id"];
|
|
117
|
+
return typeof id === "string" ? id : void 0;
|
|
118
|
+
};
|
|
119
|
+
const decodeNotification = import_effect.Schema.decodeUnknownEffect(NotificationModel.Notification);
|
|
120
|
+
const validated = (notification) => import_effect.Effect.gen(function* () {
|
|
121
|
+
const notificationId = identifiedBy(notification);
|
|
122
|
+
const exceeded = exceededBound(notification);
|
|
123
|
+
if (exceeded !== void 0) {
|
|
124
|
+
return yield* new NotificationError({
|
|
125
|
+
code: "notification_invalid",
|
|
126
|
+
message: exceeded === "depth" ? `A notification nests deeper than the ${maximumPayloadDepth} level bound` : `A notification is larger than the ${maximumNotificationCodeUnits} code unit bound`,
|
|
127
|
+
...notificationId === void 0 ? {} : { notificationId }
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return yield* decodeNotification(copied(notification)).pipe(
|
|
131
|
+
import_effect.Effect.mapError((error) => {
|
|
132
|
+
const issue = formatIssue(error.issue).issues[0];
|
|
133
|
+
const path = issue.path.map(String).join(".");
|
|
134
|
+
return new NotificationError({
|
|
135
|
+
code: "notification_invalid",
|
|
136
|
+
message: bounded(issue.message),
|
|
137
|
+
...notificationId === void 0 ? {} : { notificationId },
|
|
138
|
+
...path === "" ? {} : { path }
|
|
139
|
+
});
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
const canonical = (value) => (0, import_canonical.canonicalize)(value, { loneSurrogates: "escape" });
|
|
144
|
+
const fingerprint = (notification) => import_effect.Effect.sync(() => (0, import_Sha256.digestSync)(canonical(notification)));
|
|
145
|
+
const freeze = (value) => {
|
|
146
|
+
if (typeof value !== "object" || value === null) return;
|
|
147
|
+
for (const nested of Object.values(value)) freeze(nested);
|
|
148
|
+
Object.freeze(value);
|
|
149
|
+
};
|
|
150
|
+
const maximumCachedRuns = 64;
|
|
151
|
+
const layerWith = (options = {}) => import_effect.Layer.effect(
|
|
152
|
+
NotificationQueue,
|
|
153
|
+
import_effect.Effect.gen(function* () {
|
|
154
|
+
const journal = yield* import_journal.Journal.Journal;
|
|
155
|
+
const operations = yield* import_effect.Semaphore.make(1);
|
|
156
|
+
const capacity = options.capacity ?? NotificationState.defaultCapacity;
|
|
157
|
+
const folded = FoldCache.make(maximumCachedRuns);
|
|
158
|
+
const entryAt = (runId, seq) => import_effect.Effect.map(
|
|
159
|
+
journal.entries({
|
|
160
|
+
runId,
|
|
161
|
+
...seq === 0 ? {} : { after: import_journal.JournalEvent.Seq.make(seq - 1) },
|
|
162
|
+
limit: 1
|
|
163
|
+
}),
|
|
164
|
+
(page) => {
|
|
165
|
+
const found = page.entries[0];
|
|
166
|
+
return found !== void 0 && found.seq === seq ? import_effect.Option.some(found) : import_effect.Option.none();
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
const admittedAt = (runId, seq) => import_effect.Effect.map(entryAt(runId, seq), (entry) => {
|
|
170
|
+
if (import_effect.Option.isNone(entry)) return import_effect.Option.none();
|
|
171
|
+
const decoded = NotificationEvent.fromEntry(entry.value);
|
|
172
|
+
if (import_effect.Option.isNone(decoded) || !NotificationEvent.isAdmitted(decoded.value)) return import_effect.Option.none();
|
|
173
|
+
freeze(decoded.value.notification);
|
|
174
|
+
return import_effect.Option.some(decoded.value);
|
|
175
|
+
});
|
|
176
|
+
const promotedAt = (runId, seq) => import_effect.Effect.map(entryAt(runId, seq), (entry) => {
|
|
177
|
+
if (import_effect.Option.isNone(entry)) return import_effect.Option.none();
|
|
178
|
+
const decoded = NotificationEvent.fromEntry(entry.value);
|
|
179
|
+
if (import_effect.Option.isNone(decoded) || !NotificationEvent.isPromoted(decoded.value)) return import_effect.Option.none();
|
|
180
|
+
return import_effect.Option.some(decoded.value);
|
|
181
|
+
});
|
|
182
|
+
const load = (runId) => import_effect.Effect.gen(function* () {
|
|
183
|
+
const base = folded.get(runId) ?? {
|
|
184
|
+
state: NotificationState.empty(capacity),
|
|
185
|
+
admissions: import_effect.HashMap.empty(),
|
|
186
|
+
promotions: import_effect.HashMap.empty(),
|
|
187
|
+
cursor: void 0
|
|
188
|
+
};
|
|
189
|
+
const fresh = [];
|
|
190
|
+
let after = base.cursor === void 0 ? void 0 : import_journal.JournalEvent.Seq.make(base.cursor);
|
|
191
|
+
while (true) {
|
|
192
|
+
const page = yield* journal.entries({
|
|
193
|
+
runId,
|
|
194
|
+
eventTypes: [NotificationEvent.AdmittedEventType, NotificationEvent.PromotedEventType],
|
|
195
|
+
...after === void 0 ? {} : { after },
|
|
196
|
+
limit: 512
|
|
197
|
+
});
|
|
198
|
+
fresh.push(...page.entries);
|
|
199
|
+
if (!page.hasMore || page.entries.length === 0) break;
|
|
200
|
+
after = page.entries.at(-1).seq;
|
|
201
|
+
}
|
|
202
|
+
if (fresh.length === 0) return base;
|
|
203
|
+
let state = base.state;
|
|
204
|
+
let cursor = base.cursor;
|
|
205
|
+
let admissions = base.admissions;
|
|
206
|
+
let promotions = base.promotions;
|
|
207
|
+
for (const entry of fresh) {
|
|
208
|
+
cursor = entry.seq;
|
|
209
|
+
const decoded = NotificationEvent.fromEntry(entry);
|
|
210
|
+
if (import_effect.Option.isNone(decoded)) continue;
|
|
211
|
+
const event = decoded.value;
|
|
212
|
+
if (NotificationEvent.isAdmitted(event)) {
|
|
213
|
+
freeze(event.notification);
|
|
214
|
+
admissions = import_effect.HashMap.set(admissions, event.notification.id, {
|
|
215
|
+
version: event.version,
|
|
216
|
+
fingerprint: event.fingerprint,
|
|
217
|
+
decision: event.decision,
|
|
218
|
+
seq: entry.seq
|
|
219
|
+
});
|
|
220
|
+
if (event.version !== void 0 && state.items.some((item) => item.notification.id === event.notification.id)) {
|
|
221
|
+
state = Object.freeze({
|
|
222
|
+
...state,
|
|
223
|
+
items: Object.freeze(state.items.map(
|
|
224
|
+
(item) => item.notification.id === event.notification.id ? Object.freeze({ notification: event.notification, seq: item.seq }) : item
|
|
225
|
+
))
|
|
226
|
+
});
|
|
227
|
+
} else {
|
|
228
|
+
state = NotificationState.applyAdmission(state, event.notification, entry.seq, event.decision);
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
promotions = import_effect.HashMap.set(
|
|
232
|
+
promotions,
|
|
233
|
+
NotificationEvent.drainKey(event.targetLineageId, event.boundary),
|
|
234
|
+
entry.seq
|
|
235
|
+
);
|
|
236
|
+
state = NotificationState.applyPromoted(state, event.ids);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const loaded = { state, admissions, promotions, cursor };
|
|
240
|
+
yield* journal.whenCommitted(import_effect.Effect.sync(() => folded.put(runId, loaded)));
|
|
241
|
+
return loaded;
|
|
242
|
+
});
|
|
243
|
+
const admitInTransaction = (runId, admitted, admittedFingerprint, version) => import_effect.Effect.gen(function* () {
|
|
244
|
+
const loaded = yield* load(runId);
|
|
245
|
+
const prior = import_effect.Option.getOrUndefined(import_effect.HashMap.get(loaded.admissions, admitted.id));
|
|
246
|
+
const replacing = prior !== void 0 && version !== void 0 && version > (prior.version ?? 0);
|
|
247
|
+
if (replacing) {
|
|
248
|
+
const original = yield* admittedAt(runId, prior.seq);
|
|
249
|
+
if (import_effect.Option.isNone(original)) {
|
|
250
|
+
return yield* new NotificationError({
|
|
251
|
+
code: "notification_unavailable",
|
|
252
|
+
notificationId: admitted.id,
|
|
253
|
+
message: "Original steering input is unreadable"
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
const identity = (input) => ({ ...input, payload: null });
|
|
257
|
+
if (canonical(identity(original.value.notification)) !== canonical(identity(admitted))) {
|
|
258
|
+
return yield* new NotificationError({
|
|
259
|
+
code: "notification_id_reused",
|
|
260
|
+
notificationId: admitted.id,
|
|
261
|
+
message: "An edit cannot change the notification owner or delivery binding"
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (replacing && !loaded.state.items.some((item) => item.notification.id === admitted.id)) {
|
|
266
|
+
const original = yield* admittedAt(runId, prior.seq);
|
|
267
|
+
if (import_effect.Option.isNone(original)) {
|
|
268
|
+
return yield* new NotificationError({
|
|
269
|
+
code: "notification_unavailable",
|
|
270
|
+
message: "Consumed input is unreadable"
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
notificationId: admitted.id,
|
|
275
|
+
decision: prior.decision,
|
|
276
|
+
seq: prior.seq,
|
|
277
|
+
duplicate: true,
|
|
278
|
+
consumed: true,
|
|
279
|
+
consumedNotification: original.value.notification
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (prior !== void 0 && version !== void 0 && version < (prior.version ?? 0)) {
|
|
283
|
+
return { notificationId: admitted.id, decision: prior.decision, seq: prior.seq, duplicate: true };
|
|
284
|
+
}
|
|
285
|
+
if (prior !== void 0 && !replacing) {
|
|
286
|
+
let same = prior.fingerprint === admittedFingerprint;
|
|
287
|
+
if (prior.fingerprint === void 0) {
|
|
288
|
+
const original = yield* admittedAt(runId, prior.seq);
|
|
289
|
+
if (import_effect.Option.isNone(original)) {
|
|
290
|
+
return yield* new NotificationError({
|
|
291
|
+
code: "notification_unavailable",
|
|
292
|
+
notificationId: admitted.id,
|
|
293
|
+
message: `The admission for notification ${admitted.id} is no longer readable`
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
same = canonical(original.value.notification) === canonical(admitted);
|
|
297
|
+
}
|
|
298
|
+
if (!same) {
|
|
299
|
+
return yield* new NotificationError({
|
|
300
|
+
code: "notification_id_reused",
|
|
301
|
+
notificationId: admitted.id,
|
|
302
|
+
message: `Notification id ${admitted.id} was already admitted with different content`
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
notificationId: admitted.id,
|
|
307
|
+
decision: prior.decision,
|
|
308
|
+
seq: prior.seq,
|
|
309
|
+
duplicate: true
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const admission = replacing ? { decision: "coalesced" } : NotificationState.admit(
|
|
313
|
+
loaded.state,
|
|
314
|
+
admitted,
|
|
315
|
+
loaded.cursor === void 0 ? 0 : loaded.cursor + 1
|
|
316
|
+
);
|
|
317
|
+
if (admission.decision === "rejected-full") {
|
|
318
|
+
return {
|
|
319
|
+
notificationId: admitted.id,
|
|
320
|
+
decision: admission.decision,
|
|
321
|
+
seq: void 0,
|
|
322
|
+
duplicate: false
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const receipt = yield* journal.emitDurableUnfenced(
|
|
326
|
+
new import_journal.JournalEvent.Input({
|
|
327
|
+
runId,
|
|
328
|
+
sourceId: NotificationEvent.admissionSourceId(admitted.id),
|
|
329
|
+
sourceSeq: import_journal.JournalEvent.SourceSeq.make(version ?? 0),
|
|
330
|
+
// The sequence is derived from the notification's own id, so
|
|
331
|
+
// a collision IS this admission observed twice. Comparing
|
|
332
|
+
// bytes instead would fail the second writer over `decision`,
|
|
333
|
+
// a field neither caller supplied and each derives from the
|
|
334
|
+
// fill level it happened to load at.
|
|
335
|
+
dedupe: "identity",
|
|
336
|
+
eventType: NotificationEvent.AdmittedEventType,
|
|
337
|
+
payload: {
|
|
338
|
+
notification: admitted,
|
|
339
|
+
decision: admission.decision,
|
|
340
|
+
fingerprint: admittedFingerprint,
|
|
341
|
+
...version === void 0 ? {} : { version }
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
);
|
|
345
|
+
const committed = import_effect.Option.getOrUndefined(import_effect.HashMap.get((yield* load(runId)).admissions, admitted.id));
|
|
346
|
+
if (committed === void 0) {
|
|
347
|
+
return yield* new NotificationError({
|
|
348
|
+
code: "notification_id_reused",
|
|
349
|
+
notificationId: admitted.id,
|
|
350
|
+
message: `The journal identity for notification ${admitted.id} holds an event this queue did not write`
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
notificationId: admitted.id,
|
|
355
|
+
decision: committed.decision,
|
|
356
|
+
seq: committed.seq,
|
|
357
|
+
duplicate: receipt._tag === "Duplicate"
|
|
358
|
+
};
|
|
359
|
+
});
|
|
360
|
+
return make({
|
|
361
|
+
admit: import_effect.Effect.fn("NotificationQueue.admit")(
|
|
362
|
+
(rawRunId, notification, version) => import_effect.Effect.gen(function* () {
|
|
363
|
+
if (version !== void 0 && (!Number.isSafeInteger(version) || version < 1)) {
|
|
364
|
+
return yield* new NotificationError({
|
|
365
|
+
code: "notification_invalid",
|
|
366
|
+
message: "version must be a positive safe integer"
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
const admitted = yield* validated(notification);
|
|
370
|
+
const payload = admitted.payload;
|
|
371
|
+
if (typeof payload === "object" && payload !== null && !Array.isArray(payload) && payload["kind"] === "outside_change") {
|
|
372
|
+
return yield* new NotificationError({
|
|
373
|
+
code: "notification_refused",
|
|
374
|
+
notificationId: admitted.id,
|
|
375
|
+
message: "Outside-change delivery requires authenticated watcher facts and stale-write enforcement"
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
const admittedFingerprint = yield* fingerprint(admitted);
|
|
379
|
+
return yield* journal.transact(
|
|
380
|
+
operations.withPermits(1)(
|
|
381
|
+
admitInTransaction(import_journal.JournalEvent.RunId.make(rawRunId), admitted, admittedFingerprint, version)
|
|
382
|
+
)
|
|
383
|
+
);
|
|
384
|
+
})
|
|
385
|
+
),
|
|
386
|
+
drain: import_effect.Effect.fn("NotificationQueue.drain")(
|
|
387
|
+
(input) => journal.transact(
|
|
388
|
+
operations.withPermits(1)(import_effect.Effect.gen(function* () {
|
|
389
|
+
const runId = import_journal.JournalEvent.RunId.make(input.runId);
|
|
390
|
+
const key = NotificationEvent.drainKey(input.targetLineageId, input.boundary);
|
|
391
|
+
const loaded = yield* load(runId);
|
|
392
|
+
const delivered = (record, admissions) => import_effect.Effect.forEach(record.ids, (id) => import_effect.Effect.gen(function* () {
|
|
393
|
+
const committed2 = import_effect.HashMap.get(admissions, id);
|
|
394
|
+
const event = import_effect.Option.isNone(committed2) ? import_effect.Option.none() : yield* admittedAt(runId, committed2.value.seq);
|
|
395
|
+
if (import_effect.Option.isNone(event)) {
|
|
396
|
+
return yield* new NotificationError({
|
|
397
|
+
code: "notification_unavailable",
|
|
398
|
+
notificationId: id,
|
|
399
|
+
message: `The delivered notification ${id} is no longer readable`
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return event.value.notification;
|
|
403
|
+
}));
|
|
404
|
+
const replayed = (seq, admissions) => import_effect.Effect.gen(function* () {
|
|
405
|
+
const record = yield* promotedAt(runId, seq);
|
|
406
|
+
if (import_effect.Option.isNone(record)) {
|
|
407
|
+
return yield* new NotificationError({
|
|
408
|
+
code: "notification_unavailable",
|
|
409
|
+
message: `The drain record for boundary ${input.boundary} is no longer readable`
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
notifications: yield* delivered(record.value, admissions),
|
|
414
|
+
boundary: input.boundary,
|
|
415
|
+
duplicate: true
|
|
416
|
+
};
|
|
417
|
+
});
|
|
418
|
+
const prior = import_effect.HashMap.get(loaded.promotions, key);
|
|
419
|
+
if (import_effect.Option.isSome(prior)) return yield* replayed(prior.value, loaded.admissions);
|
|
420
|
+
const cutoff = input.cutoffSeq ?? loaded.cursor ?? 0;
|
|
421
|
+
const promotionState = /^\d+:[a-f0-9]{64}:park:\d+$/.test(input.boundary) ? {
|
|
422
|
+
...loaded.state,
|
|
423
|
+
items: loaded.state.items.filter((item) => {
|
|
424
|
+
const payload = item.notification.payload;
|
|
425
|
+
return !(typeof payload === "object" && payload !== null && !Array.isArray(payload) && payload.kind === "outside_change");
|
|
426
|
+
})
|
|
427
|
+
} : loaded.state;
|
|
428
|
+
const steers = NotificationState.promoteSteers(promotionState, cutoff, input.targetLineageId);
|
|
429
|
+
const queued = input.wouldIdle && steers.promoted.length === 0 ? NotificationState.promoteQueued(steers.state, input.targetLineageId) : { state: steers.state, promoted: [] };
|
|
430
|
+
const promoted = [...steers.promoted, ...queued.promoted].map((item) => item.notification);
|
|
431
|
+
const receipt = yield* journal.emitDurableUnfenced(
|
|
432
|
+
new import_journal.JournalEvent.Input({
|
|
433
|
+
runId,
|
|
434
|
+
sourceId: NotificationEvent.drainSourceId(input.targetLineageId, input.boundary),
|
|
435
|
+
sourceSeq: import_journal.JournalEvent.SourceSeq.make(0),
|
|
436
|
+
// The identity is the drain: this lineage closing this
|
|
437
|
+
// boundary. Two processes that both reach it have observed one
|
|
438
|
+
// event, and the first record committed is the delivery.
|
|
439
|
+
dedupe: "identity",
|
|
440
|
+
eventType: NotificationEvent.PromotedEventType,
|
|
441
|
+
payload: {
|
|
442
|
+
boundary: input.boundary,
|
|
443
|
+
targetLineageId: input.targetLineageId,
|
|
444
|
+
ids: promoted.map((notification) => notification.id)
|
|
445
|
+
}
|
|
446
|
+
})
|
|
447
|
+
);
|
|
448
|
+
const settled = yield* load(runId);
|
|
449
|
+
const committed = import_effect.HashMap.get(settled.promotions, key);
|
|
450
|
+
if (import_effect.Option.isNone(committed)) {
|
|
451
|
+
return yield* new NotificationError({
|
|
452
|
+
code: "notification_unavailable",
|
|
453
|
+
message: `The journal identity for boundary ${input.boundary} holds an event this queue did not write`
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
if (receipt._tag === "Accepted") {
|
|
457
|
+
return { notifications: promoted, boundary: input.boundary, duplicate: false };
|
|
458
|
+
}
|
|
459
|
+
return yield* replayed(committed.value, settled.admissions);
|
|
460
|
+
}))
|
|
461
|
+
)
|
|
462
|
+
),
|
|
463
|
+
pending: import_effect.Effect.fn("NotificationQueue.pending")(
|
|
464
|
+
(rawRunId) => import_effect.Effect.map(
|
|
465
|
+
load(import_journal.JournalEvent.RunId.make(rawRunId)),
|
|
466
|
+
// The fold `load` already performs: admissions add, promotions
|
|
467
|
+
// remove. Read outside the operations semaphore, because a count
|
|
468
|
+
// taken while a boundary drains is a count taken at some instant
|
|
469
|
+
// either side of it, and blocking a supervisor behind a turn
|
|
470
|
+
// boundary would be the worse answer.
|
|
471
|
+
(loaded) => loaded.state.items.map((item) => item.notification)
|
|
472
|
+
)
|
|
473
|
+
)
|
|
474
|
+
});
|
|
475
|
+
})
|
|
476
|
+
);
|
|
477
|
+
const layer = layerWith();
|
|
478
|
+
//# sourceMappingURL=NotificationQueue.js.map
|