@bett3r-dev/pv3-types 1.0.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +305 -0
- package/build/adminPlugin.d.ts +24 -0
- package/build/adminPlugin.d.ts.map +1 -0
- package/build/{eventsourcing/utils.js → adminPlugin.js} +1 -1
- package/build/adminPlugin.js.map +1 -0
- package/build/aggregateSchema.d.ts +94 -0
- package/build/aggregateSchema.d.ts.map +1 -0
- package/build/aggregateSchema.js +287 -0
- package/build/aggregateSchema.js.map +1 -0
- package/build/authentication.d.ts +179 -78
- package/build/authentication.d.ts.map +1 -1
- package/build/authentication.js +62 -19
- package/build/authentication.js.map +1 -1
- package/build/cache.d.ts +50 -4
- package/build/cache.d.ts.map +1 -1
- package/build/clusterAdministration.d.ts +712 -225
- package/build/clusterAdministration.d.ts.map +1 -1
- package/build/clusterAdministration.js +175 -52
- package/build/clusterAdministration.js.map +1 -1
- package/build/configuration.d.ts +26 -5
- package/build/configuration.d.ts.map +1 -1
- package/build/database.d.ts +240 -56
- package/build/database.d.ts.map +1 -1
- package/build/database.js +102 -20
- package/build/database.js.map +1 -1
- package/build/endpoints.d.ts +141 -41
- package/build/endpoints.d.ts.map +1 -1
- package/build/endpoints.js +50 -26
- package/build/endpoints.js.map +1 -1
- package/build/errors.d.ts +166 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +276 -0
- package/build/errors.js.map +1 -0
- package/build/evenstore.d.ts +47 -7
- package/build/evenstore.d.ts.map +1 -1
- package/build/eventsourcing/aggregateBuilder.d.ts +17 -8
- package/build/eventsourcing/aggregateBuilder.d.ts.map +1 -1
- package/build/eventsourcing/commandBuilder.d.ts +20 -13
- package/build/eventsourcing/commandBuilder.d.ts.map +1 -1
- package/build/eventsourcing/commandHandlers.d.ts +155 -33
- package/build/eventsourcing/commandHandlers.d.ts.map +1 -1
- package/build/eventsourcing/commandHandlers.js +62 -14
- package/build/eventsourcing/commandHandlers.js.map +1 -1
- package/build/eventsourcing/eventHandlers.d.ts +173 -44
- package/build/eventsourcing/eventHandlers.d.ts.map +1 -1
- package/build/eventsourcing/eventHandlers.js +29 -21
- package/build/eventsourcing/eventHandlers.js.map +1 -1
- package/build/eventsourcing/eventHandlersBuilder.d.ts +18 -12
- package/build/eventsourcing/eventHandlersBuilder.d.ts.map +1 -1
- package/build/eventsourcing/eventHandlersBuilder.js.map +1 -1
- package/build/eventsourcing/eventsourcing.types.d.ts +322 -0
- package/build/eventsourcing/eventsourcing.types.d.ts.map +1 -0
- package/build/eventsourcing/eventsourcing.types.js +26 -0
- package/build/eventsourcing/eventsourcing.types.js.map +1 -0
- package/build/eventsourcing/externalSystemBuilder.d.ts +8 -7
- package/build/eventsourcing/externalSystemBuilder.d.ts.map +1 -1
- package/build/eventsourcing/index.d.ts +2 -378
- package/build/eventsourcing/index.d.ts.map +1 -1
- package/build/eventsourcing/index.js +2 -23
- package/build/eventsourcing/index.js.map +1 -1
- package/build/eventsourcing/invariants.d.ts +34 -10
- package/build/eventsourcing/invariants.d.ts.map +1 -1
- package/build/eventsourcing/shapeChangeForSubscriber.d.ts +33 -0
- package/build/eventsourcing/shapeChangeForSubscriber.d.ts.map +1 -0
- package/build/eventsourcing/shapeChangeForSubscriber.js +47 -0
- package/build/eventsourcing/shapeChangeForSubscriber.js.map +1 -0
- package/build/index.d.ts +6 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +6 -1
- package/build/index.js.map +1 -1
- package/build/infrastructure.d.ts +9 -1
- package/build/infrastructure.d.ts.map +1 -1
- package/build/infrastructure.js +33 -1
- package/build/infrastructure.js.map +1 -1
- package/build/livenessWatchdog.d.ts +28 -0
- package/build/livenessWatchdog.d.ts.map +1 -0
- package/build/livenessWatchdog.js +43 -0
- package/build/livenessWatchdog.js.map +1 -0
- package/build/logger.d.ts +8 -9
- package/build/logger.d.ts.map +1 -1
- package/build/logger.js +18 -6
- package/build/logger.js.map +1 -1
- package/build/outboxManager.d.ts +858 -14
- package/build/outboxManager.d.ts.map +1 -1
- package/build/outboxManager.js +173 -15
- package/build/outboxManager.js.map +1 -1
- package/build/permissions.d.ts +47 -0
- package/build/permissions.d.ts.map +1 -0
- package/build/permissions.js +3 -0
- package/build/permissions.js.map +1 -0
- package/build/ports.d.ts +10 -10
- package/build/ports.d.ts.map +1 -1
- package/build/routesAuth.d.ts +17 -25
- package/build/routesAuth.d.ts.map +1 -1
- package/build/routesAuth.js +4 -14
- package/build/routesAuth.js.map +1 -1
- package/build/typedEventEmitter.d.ts +7 -13
- package/build/typedEventEmitter.d.ts.map +1 -1
- package/build/typedEventEmitter.js +2 -2
- package/build/typedEventEmitter.js.map +1 -1
- package/build/userSession.d.ts +8 -0
- package/build/userSession.d.ts.map +1 -0
- package/build/userSession.js +3 -0
- package/build/userSession.js.map +1 -0
- package/package.json +22 -7
- package/build/eventsourcing/utils.d.ts +0 -4
- package/build/eventsourcing/utils.d.ts.map +0 -1
- package/build/eventsourcing/utils.js.map +0 -1
- package/build/validation.d.ts +0 -64
- package/build/validation.d.ts.map +0 -1
- package/build/validation.js +0 -57
- package/build/validation.js.map +0 -1
- package/src/authentication.ts +0 -273
- package/src/cache.ts +0 -109
- package/src/clusterAdministration.ts +0 -216
- package/src/configuration.ts +0 -71
- package/src/database.ts +0 -157
- package/src/endpoints.ts +0 -198
- package/src/evenstore.ts +0 -27
- package/src/eventsourcing/aggregateBuilder.ts +0 -11
- package/src/eventsourcing/commandBuilder.ts +0 -64
- package/src/eventsourcing/commandHandlers.ts +0 -137
- package/src/eventsourcing/eventHandlers.ts +0 -118
- package/src/eventsourcing/eventHandlersBuilder.ts +0 -42
- package/src/eventsourcing/externalSystemBuilder.ts +0 -20
- package/src/eventsourcing/index.ts +0 -111
- package/src/eventsourcing/invariants.ts +0 -27
- package/src/eventsourcing/utils.ts +0 -4
- package/src/index.ts +0 -15
- package/src/infrastructure.ts +0 -41
- package/src/logger.ts +0 -45
- package/src/outboxManager.ts +0 -36
- package/src/ports.ts +0 -42
- package/src/routesAuth.ts +0 -58
- package/src/typedEventEmitter.ts +0 -73
- package/src/validation.ts +0 -89
- package/tsconfig.json +0 -15
package/build/outboxManager.d.ts
CHANGED
|
@@ -1,37 +1,881 @@
|
|
|
1
|
-
import { CommittedEvent } from '
|
|
2
|
-
export declare const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { CommittedEvent, CustomCriteriaRegexpType, StructureAggregateType, StructureDispatcherSubscriptionType, StructureState, StructureSubscriptionType, TypedEventEmitter } from './index';
|
|
2
|
+
export declare const EventDispatcherConfigSchema: import("@bett3r-dev/jsonschema-definer").ObjectSchema<{
|
|
3
|
+
maxConsumerQueueSize?: number | undefined;
|
|
4
|
+
metricsInterval?: number | undefined;
|
|
5
|
+
defaultRetryableStatuses?: number[] | undefined;
|
|
6
|
+
retryAfterSeconds?: number | undefined;
|
|
7
|
+
retriesBeforeFailing?: number | undefined;
|
|
8
|
+
breakerThreshold?: number | undefined;
|
|
9
|
+
breakerCooldownSeconds?: number | undefined;
|
|
10
|
+
breakerCooldownMaxSeconds?: number | undefined;
|
|
11
|
+
commitOrderStream?: {
|
|
12
|
+
streamKey?: string | undefined;
|
|
13
|
+
} | undefined;
|
|
14
|
+
seedDedupToTipOnColdStart?: boolean | undefined;
|
|
15
|
+
structureAckReadiness?: {
|
|
16
|
+
enabled?: boolean | undefined;
|
|
17
|
+
windowMs?: number | undefined;
|
|
18
|
+
} | undefined;
|
|
6
19
|
}, true>;
|
|
7
|
-
export type
|
|
20
|
+
export type EventDispatcherConfigType = typeof EventDispatcherConfigSchema.type;
|
|
8
21
|
export declare const AdminServicesConfigSchema: import("@bett3r-dev/jsonschema-definer").ObjectSchema<{
|
|
9
|
-
udp?: {
|
|
10
|
-
port?: number | undefined;
|
|
11
|
-
multicastAddress?: string | undefined;
|
|
12
|
-
ttl?: number | undefined;
|
|
13
|
-
} | undefined;
|
|
14
22
|
structure?: {
|
|
15
23
|
structureEventstoreStream?: string | undefined;
|
|
16
24
|
structureAggregateCollection?: string | undefined;
|
|
17
25
|
structureEventstoreCollection?: string | undefined;
|
|
26
|
+
defaultAutomaticDataSplits?: number | undefined;
|
|
18
27
|
} | undefined;
|
|
19
28
|
infrastructure?: {
|
|
29
|
+
adapter?: "docker" | "kubernetes" | undefined;
|
|
30
|
+
kubernetes?: {
|
|
31
|
+
namespace?: string | undefined;
|
|
32
|
+
labelSelector?: string | undefined;
|
|
33
|
+
} | undefined;
|
|
20
34
|
infrastructureLeaderRandomBackoff?: number | undefined;
|
|
35
|
+
infrastructureReconcileIntervalMs?: number | undefined;
|
|
36
|
+
} | undefined;
|
|
37
|
+
eventDispatcher?: {
|
|
38
|
+
maxConsumerQueueSize?: number | undefined;
|
|
39
|
+
metricsInterval?: number | undefined;
|
|
40
|
+
defaultRetryableStatuses?: number[] | undefined;
|
|
41
|
+
retryAfterSeconds?: number | undefined;
|
|
42
|
+
retriesBeforeFailing?: number | undefined;
|
|
43
|
+
breakerThreshold?: number | undefined;
|
|
44
|
+
breakerCooldownSeconds?: number | undefined;
|
|
45
|
+
breakerCooldownMaxSeconds?: number | undefined;
|
|
46
|
+
commitOrderStream?: {
|
|
47
|
+
streamKey?: string | undefined;
|
|
48
|
+
} | undefined;
|
|
49
|
+
seedDedupToTipOnColdStart?: boolean | undefined;
|
|
50
|
+
structureAckReadiness?: {
|
|
51
|
+
enabled?: boolean | undefined;
|
|
52
|
+
windowMs?: number | undefined;
|
|
53
|
+
} | undefined;
|
|
21
54
|
} | undefined;
|
|
22
|
-
metricsInterval?: number | undefined;
|
|
23
55
|
}, true>;
|
|
24
56
|
export type AdminServicesConfigType = typeof AdminServicesConfigSchema.type;
|
|
57
|
+
export type StructureChangeQueueType = StructureAggregateType;
|
|
58
|
+
export type EventDispatcherDataSplitType = StructureDispatcherSubscriptionType & {
|
|
59
|
+
id: string;
|
|
60
|
+
};
|
|
61
|
+
export type EventDispatcherSubscriptionState = {
|
|
62
|
+
/** The subscription Definition */
|
|
63
|
+
subscription: StructureSubscriptionType;
|
|
64
|
+
/** The total data splits among all the different outbox services */
|
|
65
|
+
splitCount: number;
|
|
66
|
+
/** The status of the subscription */
|
|
67
|
+
status: StructureSubscriptionType['status'];
|
|
68
|
+
/** The DataSplits handled by the current service */
|
|
69
|
+
dataSplits: Record<string, EventDispatcherDataSplitType>;
|
|
70
|
+
};
|
|
71
|
+
export type CollectServiceStructureAckHelperType = {
|
|
72
|
+
close: () => void;
|
|
73
|
+
waitForRevisionAck: (state: StructureAggregateType) => Promise<boolean>;
|
|
74
|
+
};
|
|
75
|
+
export type ChannelType = {
|
|
76
|
+
send: (event: CommittedEvent, host: DispatcherHostType, endpoint: string) => Promise<void>;
|
|
77
|
+
};
|
|
78
|
+
export type ChannelsType = {
|
|
79
|
+
[name: string]: ChannelType;
|
|
80
|
+
};
|
|
81
|
+
export type InstrumentChannelSend = (send: ChannelType['send']) => ChannelType['send'];
|
|
82
|
+
export type DispatcherHostType = {
|
|
83
|
+
host: string;
|
|
84
|
+
service: string;
|
|
85
|
+
channel: string;
|
|
86
|
+
port: number;
|
|
87
|
+
};
|
|
88
|
+
export declare const EventDispatcherDataSplitErrorSchema: import("@bett3r-dev/jsonschema-definer").ObjectSchema<{
|
|
89
|
+
status?: number | undefined;
|
|
90
|
+
error?: string | undefined;
|
|
91
|
+
event: {
|
|
92
|
+
id: string;
|
|
93
|
+
position: number;
|
|
94
|
+
name?: any;
|
|
95
|
+
timestamp: string;
|
|
96
|
+
data?: import("@bett3r-dev/jsonschema-definer/dist/base").Any;
|
|
97
|
+
version: number | null;
|
|
98
|
+
stream: string;
|
|
99
|
+
metadata: {
|
|
100
|
+
causationId?: string | undefined;
|
|
101
|
+
correlationId?: string | undefined;
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
}, true>;
|
|
105
|
+
export type EventDispatcherDataSplitErrorType = typeof EventDispatcherDataSplitErrorSchema.type;
|
|
106
|
+
export declare const EventDispatcherDataSplitStatusSchema: import("@bett3r-dev/jsonschema-definer").ObjectSchema<{
|
|
107
|
+
position: number;
|
|
108
|
+
status: "critical" | "ok" | "retryable";
|
|
109
|
+
lastError?: {
|
|
110
|
+
status?: number | undefined;
|
|
111
|
+
error?: string | undefined;
|
|
112
|
+
event: {
|
|
113
|
+
id: string;
|
|
114
|
+
position: number;
|
|
115
|
+
name?: any;
|
|
116
|
+
timestamp: string;
|
|
117
|
+
data?: import("@bett3r-dev/jsonschema-definer/dist/base").Any;
|
|
118
|
+
version: number | null;
|
|
119
|
+
stream: string;
|
|
120
|
+
metadata: {
|
|
121
|
+
causationId?: string | undefined;
|
|
122
|
+
correlationId?: string | undefined;
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
} | undefined;
|
|
126
|
+
}, true>;
|
|
127
|
+
export type EventDispatcherDataSplitStatusType = typeof EventDispatcherDataSplitStatusSchema.type;
|
|
128
|
+
export declare const EventDispatcherSubscriptionEventSchema: import("@bett3r-dev/jsonschema-definer").ObjectSchema<{
|
|
129
|
+
ok: number[];
|
|
130
|
+
retryable: number[];
|
|
131
|
+
critical: number[];
|
|
132
|
+
ignored: number[];
|
|
133
|
+
}, true>;
|
|
134
|
+
export type EventDispatcherSubscriptionEventType = typeof EventDispatcherSubscriptionEventSchema.type;
|
|
135
|
+
export declare const ProcessingByDataSplitSchema: import("@bett3r-dev/jsonschema-definer").ObjectSchema<{
|
|
136
|
+
events: {
|
|
137
|
+
[x: string]: string | null;
|
|
138
|
+
};
|
|
139
|
+
lastProcessed?: number | undefined;
|
|
140
|
+
}, true>;
|
|
141
|
+
export type ProcessingByDataSplitType = typeof ProcessingByDataSplitSchema.type;
|
|
142
|
+
export type EventDispatcherHistoricDataSplitByClusterRevision = {
|
|
143
|
+
dataSplits: {
|
|
144
|
+
[index: number]: {
|
|
145
|
+
id: string;
|
|
146
|
+
index: number;
|
|
147
|
+
position: number;
|
|
148
|
+
status: 'ok' | 'retryable' | 'critical';
|
|
149
|
+
customCriteriaRegexp: CustomCriteriaRegexpType[];
|
|
150
|
+
lastError?: EventDispatcherDataSplitErrorType;
|
|
151
|
+
events?: {
|
|
152
|
+
[event: string]: EventDispatcherSubscriptionEventType;
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
clusterRevision: number;
|
|
157
|
+
dataSplitsCount: number;
|
|
158
|
+
};
|
|
159
|
+
export type PullEventsForDataSplitParamsType = {
|
|
160
|
+
subscriptionId: string;
|
|
161
|
+
dataSplitId: string;
|
|
162
|
+
position: number;
|
|
163
|
+
shouldReprocess?: boolean;
|
|
164
|
+
shouldIgnoreSequence?: boolean;
|
|
165
|
+
retryAttempt?: number;
|
|
166
|
+
onBeforeUnpause?: () => Promise<void>;
|
|
167
|
+
};
|
|
168
|
+
export type ProcessEventForDataSplitParamsType = {
|
|
169
|
+
subscriptionId: string;
|
|
170
|
+
event: CommittedEvent;
|
|
171
|
+
dataSplit: EventDispatcherDataSplitType;
|
|
172
|
+
eventstorePosition?: number;
|
|
173
|
+
shouldReprocess?: boolean;
|
|
174
|
+
shouldIgnoreSequence?: boolean;
|
|
175
|
+
retryAttempt?: number;
|
|
176
|
+
streamEntryId?: string;
|
|
177
|
+
asFirstDelivery?: boolean;
|
|
178
|
+
isBreakerProbe?: boolean;
|
|
179
|
+
asNewReplay?: boolean;
|
|
180
|
+
};
|
|
181
|
+
export type EventDispatcherQueueType = ProcessEventForDataSplitParamsType;
|
|
182
|
+
export type ErroredStreamDetailType = {
|
|
183
|
+
stream: string;
|
|
184
|
+
position: number;
|
|
185
|
+
error: string;
|
|
186
|
+
errorDetails?: Record<string, any>;
|
|
187
|
+
stackTrace?: string;
|
|
188
|
+
event?: CommittedEvent;
|
|
189
|
+
erroredAt?: string;
|
|
190
|
+
};
|
|
191
|
+
export type OutboxCriticalErrorPayloadType = {
|
|
192
|
+
subscriptionId: string;
|
|
193
|
+
dataSplit: EventDispatcherDataSplitType;
|
|
194
|
+
event: CommittedEvent;
|
|
195
|
+
dataSplitPosition: number;
|
|
196
|
+
retryAttempt: number;
|
|
197
|
+
error: Error;
|
|
198
|
+
};
|
|
199
|
+
export type OutboxEventEmitterType = {
|
|
200
|
+
'stream-errored': [OutboxCriticalErrorPayloadType];
|
|
201
|
+
'stream-fixed': [{
|
|
202
|
+
subscriptionId: string;
|
|
203
|
+
stream: string;
|
|
204
|
+
}];
|
|
205
|
+
'split-halted': [OutboxCriticalErrorPayloadType];
|
|
206
|
+
'split-restarted': [{
|
|
207
|
+
subscriptionId: string;
|
|
208
|
+
dataSplitIndex: string;
|
|
209
|
+
}];
|
|
210
|
+
};
|
|
211
|
+
export type EventProcessingType = {
|
|
212
|
+
pullFromAllThisBrokerSubscriptions: (status?: StructureSubscriptionType['status'][]) => Promise<void>;
|
|
213
|
+
pullEventForErroredStream: (subscriptionId: string, stream: string) => Promise<boolean>;
|
|
214
|
+
pullBrokerSubscriptions: (subscriptionId: string) => Promise<void>;
|
|
215
|
+
pullEventsForDataSplit: (params: PullEventsForDataSplitParamsType, retryAttempt?: number) => Promise<void>;
|
|
216
|
+
/**
|
|
217
|
+
* The commit-order stream dispatch (subWm dedup). `version > wm` ⇒ send then CAS-advance the
|
|
218
|
+
* shared per-(sub,stream) watermark; `version <= wm` ⇒ skip — UNLESS `shouldReprocess` (a
|
|
219
|
+
* force-replay) bypasses the skip and re-sends an already-watermarked version, which is then
|
|
220
|
+
* stamped `event.metadata.isRedelivery = true` on a shallow copy. TV1-1950.
|
|
221
|
+
*/
|
|
222
|
+
processStreamEventForDataSplit: (params: ProcessEventForDataSplitParamsType) => Promise<void>;
|
|
223
|
+
resumeDataSplit: (dataSplitId: string) => void;
|
|
224
|
+
resumeAllOwnedDataSplits: () => void;
|
|
225
|
+
/**
|
|
226
|
+
* Operator scoped replay (TV1-1950 slice 9; generalized to bulk scope — a stream list or the whole
|
|
227
|
+
* subscription — by TV1-1957). The forward stream loop is cursor-driven and won't react to a stale
|
|
228
|
+
* watermark, so a replay must POKE the recovery path: this rolls back the by-position recovery cursor
|
|
229
|
+
* of EVERY in-scope split and writes one field per in-scope split index onto the per-`(sub)` refill
|
|
230
|
+
* request HASH (`subRefill:{sub}`) that the owning instance's `pumpOnce` honors — each split index has
|
|
231
|
+
* exactly one owner, so its field is written once and cleared once (no atomic RMW / Lua needed).
|
|
232
|
+
* Selector: a non-empty `streams` list scopes to `unique(streams.map(getDataSplitIndex))`;
|
|
233
|
+
* `allStreams: true` scopes to every split `0..splitCount-1`. `stream` is DEPRECATED sugar for
|
|
234
|
+
* `streams: [stream]` (kept for the pre-TV1-1957 single-stream call sites). Neither mode mutates any
|
|
235
|
+
* dedup state (watermark/bitmap) here (TV1-1957 slice 3) — the mode alone decides how `pumpOnce`'s
|
|
236
|
+
* refill pass dispatches each replayed event: `as-new` ⇒ `asNewReplay` bypass (isRedelivery UNSET);
|
|
237
|
+
* `force-replay` ⇒ `shouldReprocess` bypass (isRedelivery = true). See `asNewReplay` on
|
|
238
|
+
* `ProcessEventForDataSplitParamsType` for the dispatch-time mechanics.
|
|
239
|
+
*/
|
|
240
|
+
requestScopedRefill: (subscriptionId: string, params: {
|
|
241
|
+
fromPosition: number;
|
|
242
|
+
mode: RefillModeType;
|
|
243
|
+
stream?: string;
|
|
244
|
+
streams?: string[];
|
|
245
|
+
allStreams?: boolean;
|
|
246
|
+
}) => Promise<void>;
|
|
247
|
+
/**
|
|
248
|
+
* Cold-start dedup seed to eventstore tip (TV1-1980 — "Option C"). Seeds this subscription's dedup
|
|
249
|
+
* state (per-(sub,stream) version watermark + null-class position bitmap) to the eventstore TIP so a
|
|
250
|
+
* cold-started (sandbox) instance lifts already "caught up" and dispatches NOTHING at boot, while an
|
|
251
|
+
* operator force-replay still re-emits events stamped `isRedelivery = true`. Gated by the flag
|
|
252
|
+
* (`config.seedDedupToTipOnColdStart`, default false ⇒ dead path in production) AND the persistent
|
|
253
|
+
* `subColdSeeded:{sub}` marker (seed iff flag ON AND marker ABSENT; marker SET after the scan). NEVER
|
|
254
|
+
* sends, NEVER drops (ADR-013) — boot-silence comes from advancing monotonic/idempotent dedup state.
|
|
255
|
+
* Awaited in the subscription bootstrap before the pump; exposed for deterministic test drive.
|
|
256
|
+
*/
|
|
257
|
+
seedDedupToTip: (subscriptionId: string) => Promise<void>;
|
|
258
|
+
/**
|
|
259
|
+
* Operator DLQ-drain (TV1-1954 slice 6). Re-reads each dead-lettered ref `(stream, position)` from
|
|
260
|
+
* the EVENTSTORE by position and re-dispatches it as a GENUINE FIRST delivery (`asFirstDelivery` ⇒
|
|
261
|
+
* `isRedelivery = false`) — a dead-lettered event was NEVER successfully delivered, so stamping it a
|
|
262
|
+
* redelivery would suppress the consumer's redelivery-detection signal on what is actually its first
|
|
263
|
+
* successful receipt. On a CONFIRMED successful re-dispatch the entry is REMOVED from `subDlq:{sub}`;
|
|
264
|
+
* a re-dispatch that FAILS AGAIN stays in the DLQ (not silently dropped, and NOT duplicated). The
|
|
265
|
+
* re-dispatch goes through the SAME send→commit boundary (`commitNullEvent` — SETBIT idempotent +
|
|
266
|
+
* breaker reset). Returns the counts `{ drained, remaining }`. With `only` set, retries just the
|
|
267
|
+
* entries matching that (stream, position) ref — the operator single-event retry.
|
|
268
|
+
*/
|
|
269
|
+
drainDlq: (subscriptionId: string, only?: {
|
|
270
|
+
stream: string;
|
|
271
|
+
position: number;
|
|
272
|
+
}) => Promise<{
|
|
273
|
+
drained: number;
|
|
274
|
+
remaining: number;
|
|
275
|
+
}>;
|
|
276
|
+
/**
|
|
277
|
+
* Operator restart of a HALTED datasplit (TV1-1954 split-halt). A split whose packed status is
|
|
278
|
+
* `critical` was stopped by the retryable-exhausted escalation (the live dispatch gate drops its
|
|
279
|
+
* events without advancing any watermark, so nothing is lost). This clears the split back to `ok`
|
|
280
|
+
* and replays it BY POSITION from `min(halt position, lowest gate-dropped position) − 1` through the
|
|
281
|
+
* normal subWm/bitmap-deduped dispatch, then live flow resumes. If the consumer is still sick the
|
|
282
|
+
* replay burns another retry budget and re-halts. Returns false (admin maps to 404) for an unknown
|
|
283
|
+
* subscription, a split this instance does not own, or a split that is not halted.
|
|
284
|
+
*/
|
|
285
|
+
restartDataSplit: (subscriptionId: string, dataSplitIndex: string) => Promise<boolean>;
|
|
286
|
+
eventEmitter: TypedEventEmitter<OutboxEventEmitterType>;
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* Consumer lag on the commit-order stream, measured on the POSITION axis (events of the global
|
|
290
|
+
* feed still ahead of the instance's read cursor). Works identically for versioned (subWm) and
|
|
291
|
+
* null-class (bitmap) subscriptions — the cursor is dedup-agnostic. Computed from EXISTING state
|
|
292
|
+
* only: `subCursor:{sub}` (per-instance read cursor), the TTL'd `subHb` heartbeat (liveness +
|
|
293
|
+
* host), and the stream itself (tail / cursor entry positions).
|
|
294
|
+
*/
|
|
295
|
+
export type SubscriptionLagType = {
|
|
296
|
+
/** Global position of the stream's live tail entry (undefined when the stream is empty). */
|
|
297
|
+
tailPosition?: number;
|
|
298
|
+
/** Highest lag across live instances — the subscription's headline "events behind" figure. */
|
|
299
|
+
maxLag: number;
|
|
300
|
+
/** Per LIVE instance (dead instances' cursors are excluded via the TTL'd heartbeat). */
|
|
301
|
+
instances: Record<string, {
|
|
302
|
+
host?: string;
|
|
303
|
+
cursor: string;
|
|
304
|
+
lag: number;
|
|
305
|
+
}>;
|
|
306
|
+
};
|
|
307
|
+
export type EventDispatcherType = EventProcessingType & {
|
|
308
|
+
start: (structureState?: StructureState) => Promise<void>;
|
|
309
|
+
stop: () => Promise<void>;
|
|
310
|
+
/**
|
|
311
|
+
* Consumer lag per subscription (see SubscriptionLagType). Optional — wired at the OutboxManager
|
|
312
|
+
* composition root (it needs the commit-order stream handle); bare EventDispatcher test stubs omit it.
|
|
313
|
+
*/
|
|
314
|
+
getSubscriptionsLag?: (subscriptionId?: string) => Promise<Record<string, SubscriptionLagType>>;
|
|
315
|
+
};
|
|
316
|
+
export declare const EventDispatcherDataSplitStateSchema: import("@bett3r-dev/jsonschema-definer").ObjectSchema<{
|
|
317
|
+
host: string;
|
|
318
|
+
index: number;
|
|
319
|
+
isManual?: boolean | undefined;
|
|
320
|
+
customCriteriaRegexp?: {
|
|
321
|
+
value: string;
|
|
322
|
+
regexp: string;
|
|
323
|
+
initialPosition?: number | undefined;
|
|
324
|
+
isErrorDataSplit?: boolean | undefined;
|
|
325
|
+
lastErrorDataSplitIndex?: number | undefined;
|
|
326
|
+
lastErrorClusterRevision?: number | undefined;
|
|
327
|
+
}[] | undefined;
|
|
328
|
+
position: number;
|
|
329
|
+
status: "critical" | "ok" | "retryable";
|
|
330
|
+
lastError?: {
|
|
331
|
+
status?: number | undefined;
|
|
332
|
+
error?: string | undefined;
|
|
333
|
+
event: {
|
|
334
|
+
id: string;
|
|
335
|
+
position: number;
|
|
336
|
+
name?: any;
|
|
337
|
+
timestamp: string;
|
|
338
|
+
data?: import("@bett3r-dev/jsonschema-definer/dist/base").Any;
|
|
339
|
+
version: number | null;
|
|
340
|
+
stream: string;
|
|
341
|
+
metadata: {
|
|
342
|
+
causationId?: string | undefined;
|
|
343
|
+
correlationId?: string | undefined;
|
|
344
|
+
};
|
|
345
|
+
};
|
|
346
|
+
} | undefined;
|
|
347
|
+
id: string;
|
|
348
|
+
subscription_id: string;
|
|
349
|
+
cluster_revision: number;
|
|
350
|
+
data_splits_count: number;
|
|
351
|
+
events?: {
|
|
352
|
+
[x: string]: {
|
|
353
|
+
ok: number[];
|
|
354
|
+
retryable: number[];
|
|
355
|
+
critical: number[];
|
|
356
|
+
ignored: number[];
|
|
357
|
+
};
|
|
358
|
+
} | undefined;
|
|
359
|
+
last_updated?: string | undefined;
|
|
360
|
+
}, true>;
|
|
361
|
+
export type EventDispatcherDataSplitStateType = typeof EventDispatcherDataSplitStateSchema.type;
|
|
362
|
+
export declare const ErrorHandlingStrategyMap: {
|
|
363
|
+
readonly 'custom-datasplit': 0;
|
|
364
|
+
readonly 'subscriptionStop': 1;
|
|
365
|
+
};
|
|
366
|
+
export type ErrorHandlingStrategy = keyof typeof ErrorHandlingStrategyMap;
|
|
367
|
+
export declare const DataSplitStatusIntMap: {
|
|
368
|
+
readonly ok: 0;
|
|
369
|
+
readonly retryable: 1;
|
|
370
|
+
readonly critical: 2;
|
|
371
|
+
readonly splitHalted: 3;
|
|
372
|
+
};
|
|
373
|
+
export type DataSplitStatusIntMapType = keyof typeof DataSplitStatusIntMap;
|
|
374
|
+
export declare const DataSplitIntStatusMap: {
|
|
375
|
+
readonly 0: 'ok';
|
|
376
|
+
readonly 1: 'retryable';
|
|
377
|
+
readonly 2: 'critical';
|
|
378
|
+
};
|
|
379
|
+
export type DataSplitIntStatusMapType = keyof typeof DataSplitIntStatusMap;
|
|
380
|
+
export declare const SubscriptionStatusIntMap: {
|
|
381
|
+
readonly off: 0;
|
|
382
|
+
readonly ok: 1;
|
|
383
|
+
readonly withErrors: 2;
|
|
384
|
+
readonly retrying: 3;
|
|
385
|
+
readonly critical: 4;
|
|
386
|
+
};
|
|
387
|
+
export type SubscriptionStatuses = keyof typeof SubscriptionStatusIntMap;
|
|
388
|
+
export declare const SubscriptionIntStatusMap: {
|
|
389
|
+
0: string;
|
|
390
|
+
1: string;
|
|
391
|
+
2: string;
|
|
392
|
+
3: string;
|
|
393
|
+
4: string;
|
|
394
|
+
};
|
|
395
|
+
export type AllSubscriptionsStatusType = {
|
|
396
|
+
status: string;
|
|
397
|
+
errorStreams: Record<string, number>;
|
|
398
|
+
dataSplits: Record<string, {
|
|
399
|
+
position: number;
|
|
400
|
+
status: number;
|
|
401
|
+
} | undefined>;
|
|
402
|
+
delivered: number;
|
|
403
|
+
streamsTracked: number;
|
|
404
|
+
dlq?: number;
|
|
405
|
+
};
|
|
406
|
+
/**
|
|
407
|
+
* Operator scoped-replay mode (TV1-1950 slice 9). The admin replay endpoint pokes the cursor-driven
|
|
408
|
+
* recovery path by writing a per-`(sub)` refill marker; the owning instance's `pumpOnce` honors it.
|
|
409
|
+
* Both modes roll back ONLY the recovery cursor — neither touches the watermark/bitmap up front
|
|
410
|
+
* (TV1-1957 slice 3 removed that off-queue reset). The dedup-bypass instead happens at DISPATCH TIME:
|
|
411
|
+
* - `as-new`: the refill dispatches each replayed event with the `asNewReplay` bypass flag ⇒ the
|
|
412
|
+
* redelivery skip is bypassed and the outgoing copy is stamped `isRedelivery = false` (fresh).
|
|
413
|
+
* - `force-replay`: the refill dispatches with `shouldReprocess` ⇒ the skip is bypassed and the
|
|
414
|
+
* outgoing copy is stamped `isRedelivery = true` (consistent with the slice-4 stamp).
|
|
415
|
+
* Either way the only dedup MUTATION is the existing post-send monotonic `advanceSubWatermark` /
|
|
416
|
+
* idempotent `setSubBit`, run on the split's concurrency:1 queue — never off-queue (ADR-007).
|
|
417
|
+
*/
|
|
418
|
+
export type RefillModeType = 'as-new' | 'force-replay';
|
|
419
|
+
/**
|
|
420
|
+
* Per-in-scope-split payload of the bulk scoped-refill request HASH (`subRefill:{sub}`, TV1-1957 —
|
|
421
|
+
* generalizes the pre-existing single-key `RefillRequestType` marker, which it REPLACES; a single
|
|
422
|
+
* marker cleared by the first owner to pump silently skipped splits owned by other instances).
|
|
423
|
+
* `streams` (a list selector) and `allStreams` (the whole-subscription selector) are mutually
|
|
424
|
+
* exclusive; `allStreams: true` means "no stream filter within this split" (every stream routed to it).
|
|
425
|
+
*/
|
|
426
|
+
export type ScopedRefillEntryType = {
|
|
427
|
+
mode: RefillModeType;
|
|
428
|
+
fromPosition: number;
|
|
429
|
+
streams?: string[];
|
|
430
|
+
allStreams?: boolean;
|
|
431
|
+
};
|
|
432
|
+
/**
|
|
433
|
+
* Live retry progress for a split whose send loop is in the in-place retryable backoff (TV1-1954
|
|
434
|
+
* retry visibility). Written on EACH retry attempt (the loop is about to sleep `retryAfterSecs`, so
|
|
435
|
+
* one HSET per attempt is noise), cleared on every loop exit (success / split-halt / quarantine /
|
|
436
|
+
* null dead-letter) so it can never go stale. `nextRetryAt` is epoch millis — the backoff is a FIXED
|
|
437
|
+
* `retryAfterSecs` per attempt, so the admin UI renders a live "retry N of M · next in Xs" countdown.
|
|
438
|
+
*/
|
|
439
|
+
export type SplitRetryStateType = {
|
|
440
|
+
attempt: number;
|
|
441
|
+
maxRetries: number;
|
|
442
|
+
retryAfterSecs: number;
|
|
443
|
+
nextRetryAt: number;
|
|
444
|
+
stream: string;
|
|
445
|
+
position: number;
|
|
446
|
+
error?: {
|
|
447
|
+
status?: number;
|
|
448
|
+
message?: string;
|
|
449
|
+
};
|
|
450
|
+
};
|
|
451
|
+
export type StateType = {
|
|
452
|
+
getDataSplitStateForPosition: (subscriptionId: string, dataSplitId: string, position: number) => Promise<{
|
|
453
|
+
status: 'alreadyProcessed' | 'subscriptionOff' | 'subscriptionHasErrors' | 'ok';
|
|
454
|
+
dataSplitState?: {
|
|
455
|
+
position: number;
|
|
456
|
+
status: DataSplitStatusIntMapType;
|
|
457
|
+
};
|
|
458
|
+
errorStreams?: Record<string, number>;
|
|
459
|
+
}>;
|
|
460
|
+
getErrorStreamPosition: (subscription: string, stream: string) => Promise<number | undefined>;
|
|
461
|
+
getSubscriptionDataSplits: (subscription: string) => Promise<Record<string, {
|
|
462
|
+
position: number;
|
|
463
|
+
status: DataSplitStatusIntMapType;
|
|
464
|
+
delivered: number;
|
|
465
|
+
erroredStreams: number;
|
|
466
|
+
retry?: SplitRetryStateType;
|
|
467
|
+
heldSinceHalt?: number;
|
|
468
|
+
} | undefined>>;
|
|
469
|
+
getSubscriptionStatus: (subscription: string) => Promise<{
|
|
470
|
+
status?: string;
|
|
471
|
+
errorStreams?: Record<string, number>;
|
|
472
|
+
delivered?: number;
|
|
473
|
+
streamsTracked?: number;
|
|
474
|
+
}>;
|
|
475
|
+
getAllSubscriptionsStatus: () => Promise<Record<string, AllSubscriptionsStatusType>>;
|
|
476
|
+
/**
|
|
477
|
+
* Per-`(sub,stream)` version watermark (TV1-1950): the gapless-commit-order-stream dedup.
|
|
478
|
+
* `subWm:{sub}` is a single HASH (field = stream → highest dispatched version); the `{sub}` hash
|
|
479
|
+
* tag co-locates all of a subscription's streams in ONE Redis slot so the CAS advance is a
|
|
480
|
+
* single-slot Lua script. The dispatch decide is `version > storedWm ⇒ send` and the commit is a
|
|
481
|
+
* monotonic CAS advance AFTER a successful send, so an interruption between send and advance
|
|
482
|
+
* re-dispatches (at-least-once, never drop). Because the watermark is SHARED (keyed on
|
|
483
|
+
* `(sub,stream)`, not a per-instance cursor) a rebalance hands a split over cleanly regardless of
|
|
484
|
+
* cursor skew — progress travels with the stream.
|
|
485
|
+
*/
|
|
486
|
+
getSubWatermark: (subscription: string, stream: string) => Promise<number | undefined>;
|
|
487
|
+
getSubWatermarks: (subscription: string) => Promise<Record<string, number>>;
|
|
488
|
+
/** Monotonic CAS advance of `subWm:{sub}[stream]` to `version` (never regresses); returns the resulting watermark. */
|
|
489
|
+
advanceSubWatermark: (subscription: string, stream: string, version: number) => Promise<number>;
|
|
490
|
+
/**
|
|
491
|
+
* Entry-id-recording monotonic CAS advance (TV1-1950 slice 8). Same as `advanceSubWatermark`, but it
|
|
492
|
+
* ALSO records — atomically, in the parallel `subWmId:{sub}` HASH (field = stream → entryId) — the
|
|
493
|
+
* stream ENTRY-ID at which the watermark last advanced. That recorded id is the bridge the bounded
|
|
494
|
+
* watermark GC needs: `subWm` stores a per-stream VERSION, but the trim horizon (`firstId()`) is a
|
|
495
|
+
* stream ENTRY-ID, so the GC compares the recorded entry-id (not the version) against the horizon.
|
|
496
|
+
* The dispatch path uses this when it has the live stream entry-id; the by-position refill falls back
|
|
497
|
+
* to `advanceSubWatermark` (no stream entry-id), leaving the field absent from `subWmId` → the GC
|
|
498
|
+
* conservatively retains it.
|
|
499
|
+
*/
|
|
500
|
+
advanceSubWatermarkAt: (subscription: string, stream: string, version: number, entryId: string) => Promise<number>;
|
|
501
|
+
/**
|
|
502
|
+
* Bounded watermark GC (TV1-1950 slice 8): drop every `(sub,stream)` watermark (from BOTH `subWm`
|
|
503
|
+
* and `subWmId`) whose recorded entry-id is STRICTLY BELOW the trim horizon (the global stream's
|
|
504
|
+
* first-entry-id, `firstId()`, after the slice-8a XTRIM). An entry strictly below the horizon was
|
|
505
|
+
* already trimmed away and can never be re-read off the live stream, so dropping its watermark
|
|
506
|
+
* induces NO re-dispatch; an entry AT or ABOVE the horizon is still in the live window and is
|
|
507
|
+
* RETAINED. A `subWm` field with no recorded `subWmId` is conservatively retained. An `undefined`
|
|
508
|
+
* horizon (empty stream — nothing trimmed) is a no-op. Returns the dropped stream fields.
|
|
509
|
+
*/
|
|
510
|
+
gcSubWatermarks: (subscription: string, trimHorizon: string | undefined) => Promise<string[]>;
|
|
511
|
+
/**
|
|
512
|
+
* Per-`(sub,instance)` read cursor + TTL'd heartbeat for `XREAD` resume. The cursor is
|
|
513
|
+
* instance-local and may skew across instances; correctness rests on the SHARED `subWm` dedup,
|
|
514
|
+
* not on the cursor. The heartbeat is TTL'd so a dead instance can no longer pin the stream
|
|
515
|
+
* (its cursor stops counting toward the trim floor once the heartbeat expires).
|
|
516
|
+
*/
|
|
517
|
+
getStreamCursor: (subscription: string, instance: string) => Promise<string | undefined>;
|
|
518
|
+
setStreamCursor: (subscription: string, instance: string, entryId: string) => Promise<void>;
|
|
519
|
+
/**
|
|
520
|
+
* `host` is embedded in the heartbeat VALUE (the key's existence/TTL is the liveness contract and
|
|
521
|
+
* stays untouched — the WAL Reader never parses the value). It maps the per-process instance UUID
|
|
522
|
+
* back to the structure's broker host/IP, so the admin lag view can attribute a cursor to the
|
|
523
|
+
* host that owns each data split without any additional state.
|
|
524
|
+
*/
|
|
525
|
+
heartbeatStreamConsumer: (subscription: string, instance: string, ttlSeconds?: number, host?: string) => Promise<void>;
|
|
526
|
+
/**
|
|
527
|
+
* Per-`(sub,split)` by-position recovery cursor (TV1-1950 slice 9). The CONSERVATIVE start point
|
|
528
|
+
* for the by-position refill (the KEPT `reduceStream`; never a by-version read) — it only BOUNDS
|
|
529
|
+
* the re-scan; correctness rests on the version watermark (design critique #3). A refill advances
|
|
530
|
+
* it to the highest scanned position; an operator replay rolls it back to re-open a range. Keyed by
|
|
531
|
+
* split INDEX (deterministic from the stream) so the admin can address it from `(sub,stream)` alone.
|
|
532
|
+
*/
|
|
533
|
+
getRecoveryCursor: (subscription: string, splitIndex: string) => Promise<number | undefined>;
|
|
534
|
+
setRecoveryCursor: (subscription: string, splitIndex: string, position: number) => Promise<void>;
|
|
535
|
+
/**
|
|
536
|
+
* Persistent per-`(sub)` cold-seed marker (TV1-1980 — "Option C"). Existence of `subColdSeeded:{sub}`
|
|
537
|
+
* means this subscription's dedup state was already seeded to the eventstore tip on a prior cold start.
|
|
538
|
+
* `seedDedupToTip` seeds iff the flag is ON AND `getColdSeeded` is FALSE, then `setColdSeeded` AFTER the
|
|
539
|
+
* scan (self-healing — a failed seed leaves the marker unset and retries next cycle). The marker gates
|
|
540
|
+
* RE-seeding unconditionally: a warm restart must never re-seed, else a new event that advanced the
|
|
541
|
+
* watermark past the old tip would be masked to a silent drop (violates ADR-013). Torn down with the
|
|
542
|
+
* subscription in `deleteSubscription`.
|
|
543
|
+
*/
|
|
544
|
+
getColdSeeded: (subscription: string) => Promise<boolean>;
|
|
545
|
+
setColdSeeded: (subscription: string) => Promise<void>;
|
|
546
|
+
/**
|
|
547
|
+
* Null-class position bitmap dedup (TV1-1954 slice 2 — hybrid dedup). VERSION-NULL streams
|
|
548
|
+
* (external/integration, no OCC) are delivered in the WAL Reader's commit-LSN order, NOT position
|
|
549
|
+
* order, so the ordered `subWm` high-water would skip (silently DROP) a lower-position event that
|
|
550
|
+
* commits AFTER a higher one on the same stream. Instead each null-version event dedups ORDER-FREE
|
|
551
|
+
* on its POSITION bit in `subBitmap:{sub}:{epoch}` (epoch = floor(position / EPOCH_SIZE), offset =
|
|
552
|
+
* position % EPOCH_SIZE, EPOCH_SIZE = 1_000_000 so a single key stays ≤ 2^32 bits): `getSubBit` is
|
|
553
|
+
* the DECIDE (set ⇒ redelivery ⇒ skip) and `setSubBit` is the COMMIT, run ONLY AFTER a successful
|
|
554
|
+
* send (send → commit boundary preserved for both classes → at-least-once, never drop). The `{sub}`
|
|
555
|
+
* hash tag co-locates a subscription's epoch keys in ONE Redis slot.
|
|
556
|
+
*/
|
|
557
|
+
getSubBit: (subscription: string, position: number) => Promise<boolean>;
|
|
558
|
+
setSubBit: (subscription: string, position: number) => Promise<void>;
|
|
559
|
+
/**
|
|
560
|
+
* Batched, set-only null-class SETBIT (TV1-1980 slice 2 — cold-seed flush). The same set-only,
|
|
561
|
+
* idempotent semantics as `setSubBit` (SETBIT 1, never CLRBIT) but for MANY positions at once. It
|
|
562
|
+
* groups positions by epoch (each `subBitmap:{sub}:{epoch}` key) and issues one PIPELINED batch of
|
|
563
|
+
* SETBITs PER epoch — collapsing an O(positions) per-event round-trip loop to O(epochs) round trips,
|
|
564
|
+
* the cold-seed batching gate. Correctness rests, like `setSubBit`, on consumer idempotency
|
|
565
|
+
* (at-least-once, ADR-013): it only BOUNDS duplicate volume, it is not itself the never-drop mechanism.
|
|
566
|
+
*/
|
|
567
|
+
setSubBits: (subscription: string, positions: number[]) => Promise<void>;
|
|
568
|
+
/**
|
|
569
|
+
* Bounded null-class bitmap GC (TV1-1954 slice 3) — the POSITION-axis analogue of `gcSubWatermarks`.
|
|
570
|
+
* The watermark GC compares ENTRY-IDs (commit order); the null-class bitmap is indexed by POSITION —
|
|
571
|
+
* a DIFFERENT axis — so the caller crosses axes FIRST: it maps the trim floor (`firstId()` ENTRY-ID)
|
|
572
|
+
* to a POSITION by reading the first live stream entry's `event.position` and passes that as
|
|
573
|
+
* `floorPosition`. This DELs every `subBitmap:{sub}:{epoch}` key whose epoch is STRICTLY BELOW
|
|
574
|
+
* `floor((floorPosition − KEEP_FLOOR_MARGIN) / EPOCH_SIZE)` (`KEEP_FLOOR_MARGIN` = one epoch = 1_000_000
|
|
575
|
+
* positions). The one-epoch margin absorbs the commit-order↔position skew: a low-position event that
|
|
576
|
+
* committed LATE is still LIVE (entry-id ≥ floor) and its bit is still needed, so its epoch (down to
|
|
577
|
+
* floorEpoch − 1) is RETAINED. If the skew ever exceeds the margin the effect is OVER-prune → a
|
|
578
|
+
* duplicate (idempotent-safe), NEVER a drop, so the margin is a dup-tuning knob, not a correctness
|
|
579
|
+
* bound. An undefined/negative/small `floorPosition` (pruneBelowEpoch <= 0) is a no-op. Returns the
|
|
580
|
+
* count of epoch keys deleted (observability).
|
|
581
|
+
*/
|
|
582
|
+
pruneBitmap: (subscription: string, floorPosition: number) => Promise<number>;
|
|
583
|
+
/**
|
|
584
|
+
* Null-class dead-letter + circuit-breaker counter (TV1-1954 slice 4 — licensed by order-freeness).
|
|
585
|
+
* A version-null event has NO per-stream order to protect, so on retries-exhausted OR a critical send
|
|
586
|
+
* failure it is DEAD-LETTERED and the split keeps flowing — unlike the versioned per-stream halt. This
|
|
587
|
+
* runs ONE atomic Lua (all three keys share the `{sub}` slot) that (1) LPUSHes a durable ref onto
|
|
588
|
+
* `subDlq:{sub}`, (2) SETBITs the `position` bit (resolved-but-not-delivered — a recovery re-read sees
|
|
589
|
+
* GETBIT=1 and does NOT re-blast it; SETBIT-without-send is SAFE because the DLQ IS the durable
|
|
590
|
+
* capture, so there is no crash window where the event is neither delivered nor captured), and (3)
|
|
591
|
+
* INCRs the per-`(sub,host)` breaker count. Returns the NEW breaker count. Slice 4 only RECORDS the
|
|
592
|
+
* count; the trip/pause + success-reset is slice 5. `error` is SANITIZED to `{ status, message }` —
|
|
593
|
+
* stacks/secrets are NEVER written to the durable DLQ.
|
|
594
|
+
*/
|
|
595
|
+
deadLetterNullEvent: (subscription: string, host: string, stream: string, position: number, name: string, error: {
|
|
596
|
+
status?: number;
|
|
597
|
+
message?: string;
|
|
598
|
+
name?: string;
|
|
599
|
+
details?: any;
|
|
600
|
+
}) => Promise<number>;
|
|
601
|
+
/**
|
|
602
|
+
* Read the dead-letter queue for a subscription, newest-first (matching the LPUSH in
|
|
603
|
+
* `deadLetterNullEvent`). Each ref is the `{ stream, position, name, error }` tuple captured on a
|
|
604
|
+
* null-class failure; the operator DLQ-drain (slice 6) re-reads each from the eventstore by position.
|
|
605
|
+
*/
|
|
606
|
+
getDlq: (subscription: string) => Promise<Array<{
|
|
607
|
+
stream: string;
|
|
608
|
+
position: number;
|
|
609
|
+
name: string;
|
|
610
|
+
error: {
|
|
611
|
+
status?: number;
|
|
612
|
+
message?: string;
|
|
613
|
+
name?: string;
|
|
614
|
+
details?: any;
|
|
615
|
+
};
|
|
616
|
+
}>>;
|
|
617
|
+
/**
|
|
618
|
+
* The RAW dead-letter entries for a subscription — the EXACT stored strings (`LRANGE subDlq:{sub}
|
|
619
|
+
* 0 -1`), NOT re-parsed. The operator DLQ-drain (TV1-1954 slice 6) reads these so it can hand each
|
|
620
|
+
* back BYTE-FOR-BYTE to `removeDlqEntry` on a confirmed re-dispatch, avoiding re-serialization drift
|
|
621
|
+
* (`getDlq` re-parses for assertions/display and MUST NOT be used as the LREM key).
|
|
622
|
+
*/
|
|
623
|
+
getDlqRaw: (subscription: string) => Promise<string[]>;
|
|
624
|
+
/**
|
|
625
|
+
* Remove ONE exact-match occurrence of a raw dead-letter entry (`LREM subDlq:{sub} 1 rawEntry`) —
|
|
626
|
+
* the DLQ-drain's confirmed-success removal (TV1-1954 slice 6). Using the RAW stored string (from
|
|
627
|
+
* `getDlqRaw`) guarantees the LREM matches byte-for-byte; only ONE occurrence is removed so a
|
|
628
|
+
* duplicate ref (same event dead-lettered twice) is not over-deleted.
|
|
629
|
+
*/
|
|
630
|
+
removeDlqEntry: (subscription: string, rawEntry: string) => Promise<void>;
|
|
631
|
+
/**
|
|
632
|
+
* The per-`(sub,host)` consecutive-failure count `deadLetterNullEvent` INCRs. SHARED across outbox
|
|
633
|
+
* instances (one Redis counter) so the effective breaker threshold does not multiply by instance
|
|
634
|
+
* count. Absent key ⇒ 0 (no failures recorded yet).
|
|
635
|
+
*/
|
|
636
|
+
getBreakerCount: (subscription: string, host: string) => Promise<number>;
|
|
637
|
+
/**
|
|
638
|
+
* Null-class HAPPY-PATH commit (TV1-1954 slice 5) — REPLACES the bare `setSubBit` at the null commit
|
|
639
|
+
* site so a SUCCESS resets the circuit breaker ATOMICALLY with the position-bit commit (no extra
|
|
640
|
+
* hot-path round-trip). ONE Lua (all keys share the `{sub}` slot) that (1) SETBITs the `position` bit
|
|
641
|
+
* (order-free dedup commit, run ONLY after a successful send — send→commit boundary preserved →
|
|
642
|
+
* at-least-once, never drop), (2) DELs `breaker:{sub}:{host}` (any success resets the consecutive
|
|
643
|
+
* failure count to 0), (3) DELs `breakerCooldown:{sub}:{host}` (reset the half-open backoff to base),
|
|
644
|
+
* and (4) DELs `breakerOpenedAt:{sub}:{host}`. Returns whether the breaker WAS open — i.e. this
|
|
645
|
+
* success CLOSED it — so the caller can resume the host's paused splits + emit `onBreakerClose`.
|
|
646
|
+
*/
|
|
647
|
+
/**
|
|
648
|
+
* Happy-path null commit: SETBIT the position bit, bump the per-split delivered counter
|
|
649
|
+
* (`subNullSplit:{sub}` HASH, incremented only when the bit flips 0→1 so redelivered duplicates
|
|
650
|
+
* don't inflate it — the bitmap is streamless, so split attribution only exists at commit time),
|
|
651
|
+
* and reset the (sub,host) circuit breaker — all in ONE atomic Lua. Returns whether the breaker
|
|
652
|
+
* WAS open (this success closed it).
|
|
653
|
+
*/
|
|
654
|
+
commitNullEvent: (subscription: string, host: string, position: number, splitIndex: string | number) => Promise<boolean>;
|
|
655
|
+
/**
|
|
656
|
+
* OPEN the per-`(sub,host)` circuit breaker (TV1-1954 slice 5). ONE Lua on the shared breaker clock:
|
|
657
|
+
* if `breakerOpenedAt:{sub}:{host}` is ABSENT → stamp it with redis `TIME` seconds (the SHARED clock,
|
|
658
|
+
* not a local one) and init `breakerCooldown` to `baseCooldownSeconds`, returning `true` (freshly
|
|
659
|
+
* opened). If ALREADY set → return `false` WITHOUT resetting the clock. The cooldown is DOUBLED to the
|
|
660
|
+
* `maxCooldownSeconds` cap ONLY when `probeFailed` is true (a FAILED half-open probe → exponential
|
|
661
|
+
* backoff, measured from the probe the tryProbeBreaker reset stamped); an already-open in-flight-batch
|
|
662
|
+
* failure (`probeFailed` false) is a NO-OP on the clock/cooldown, so a post-trip burst can neither
|
|
663
|
+
* inflate the cooldown to the cap nor storm. Idempotent under concurrent trips.
|
|
664
|
+
*/
|
|
665
|
+
openBreaker: (subscription: string, host: string, baseCooldownSeconds: number, maxCooldownSeconds: number, probeFailed: boolean) => Promise<boolean>;
|
|
666
|
+
/**
|
|
667
|
+
* Half-open PROBE decision (TV1-1954 slice 5) — the Lua is the AUTHORITY, a local wake-up timer is
|
|
668
|
+
* only a HINT it can reject (kills local/shared clock drift). ONE Lua that reads
|
|
669
|
+
* `breakerOpenedAt:{sub}:{host}` + redis `TIME`: if `now - openedAt >= cooldownSeconds` it ALLOWS the
|
|
670
|
+
* probe (returns `true`) AND resets `openedAt = now` so the NEXT cooldown is measured from this probe;
|
|
671
|
+
* else it REJECTS (`false`, stay paused). Absent `openedAt` (breaker not open) → `false`.
|
|
672
|
+
*/
|
|
673
|
+
tryProbeBreaker: (subscription: string, host: string, cooldownSeconds: number) => Promise<boolean>;
|
|
674
|
+
/**
|
|
675
|
+
* The current half-open cooldown (seconds) for `(sub,host)` — `breakerCooldown:{sub}:{host}`. Set to
|
|
676
|
+
* the base on a fresh open, DOUBLED on each probe failure (cap max), reset (deleted) on close. Absent
|
|
677
|
+
* ⇒ `undefined` (no breaker open / no failures recorded); callers default to the configured base.
|
|
678
|
+
*/
|
|
679
|
+
getBreakerCooldown: (subscription: string, host: string) => Promise<number | undefined>;
|
|
680
|
+
/**
|
|
681
|
+
* Per-`(sub)` pending scoped-refill request HASH (TV1-1950 slice 9; generalized to bulk scope by
|
|
682
|
+
* TV1-1957) — field = in-scope SPLIT INDEX → payload — the operator poke the cursor-driven forward
|
|
683
|
+
* loop honors as a THIRD refill trigger (besides cold-start / eviction). REPLACES the earlier
|
|
684
|
+
* single-key `RefillRequestType` marker (first-clearer-wins, so a multi-split request would let the
|
|
685
|
+
* first owning instance to pump clear it before other owners ever saw it). A split index has EXACTLY
|
|
686
|
+
* ONE owner, so `setRefillRequest`/`clearRefillRequest` are single-field HSET/HDEL — no atomic
|
|
687
|
+
* read-modify-write or Lua needed. `getRefillRequests` reads the whole hash (one round-trip); the
|
|
688
|
+
* owning instance's `pumpOnce` filters to the fields it owns, batches them into ONE `refillByPosition`
|
|
689
|
+
* pass, then clears ONLY those fields — and only AFTER that pass's covered splits drain and their
|
|
690
|
+
* recovery cursors checkpoint (HDEL-after-drain).
|
|
691
|
+
*/
|
|
692
|
+
getRefillRequests: (subscription: string) => Promise<Record<string, ScopedRefillEntryType>>;
|
|
693
|
+
setRefillRequest: (subscription: string, splitIndex: string, request: ScopedRefillEntryType) => Promise<void>;
|
|
694
|
+
clearRefillRequest: (subscription: string, splitIndex: string) => Promise<void>;
|
|
695
|
+
deleteSubscription: (subscription: string) => Promise<void>;
|
|
696
|
+
storeDataSplitPosition: (subscription: string, dataSplitId: string, stream: string, position: number, markAsProcessed?: boolean, dataSplitStatus?: DataSplitStatusIntMapType, errorHandlingStrategy?: ErrorHandlingStrategy, forceSet?: boolean) => Promise<void>;
|
|
697
|
+
storeErroredStream: (subscription: string, stream: string, position: number, isLastEvent?: boolean) => Promise<void>;
|
|
698
|
+
removeErrorStream: (subscription: string, stream: string, dataSplitId?: string) => Promise<void>;
|
|
699
|
+
/**
|
|
700
|
+
* Rich error detail for a quarantined (sub,stream) — the operator-facing companion of the
|
|
701
|
+
* `subErrorStreams` ZSET (which only holds stream → position). One HASH `subErrorDetails:{sub}`
|
|
702
|
+
* (field = stream → JSON detail) written at quarantine time, removed with the quarantine
|
|
703
|
+
* (removeErrorStream HDELs it; deleteSubscription drops the key). Best-effort bookkeeping: the
|
|
704
|
+
* quarantine itself never depends on it.
|
|
705
|
+
*/
|
|
706
|
+
storeErroredStreamDetail: (subscription: string, stream: string, detail: ErroredStreamDetailType) => Promise<void>;
|
|
707
|
+
getErroredStreamDetails: (subscription: string) => Promise<Record<string, ErroredStreamDetailType>>;
|
|
708
|
+
setPositionForAllDataSplits: (subscription: string, splitCount: number, clusterVersion: number) => Promise<number>;
|
|
709
|
+
/**
|
|
710
|
+
* The packed per-split status (one ZSCORE on `subSplit:{sub}` + unpack) — the live dispatch gate's
|
|
711
|
+
* read. `critical` here means the split is HALTED (only the splitHalted write packs critical bits
|
|
712
|
+
* since TV1-1954 split-halt; the per-stream quarantine no longer paints the split).
|
|
713
|
+
*/
|
|
714
|
+
getDataSplitStatus: (subscription: string, dataSplitId: string) => Promise<'ok' | 'retryable' | 'critical' | undefined>;
|
|
715
|
+
/**
|
|
716
|
+
* Halt-floor bookkeeping (TV1-1954 split-halt). While a split is halted the dispatch gate DROPS its
|
|
717
|
+
* events; a null-version event can arrive with a position BELOW the halt position (commit-LSN order
|
|
718
|
+
* ≠ position order), so the restart replay must start from the LOWEST dropped position, not just the
|
|
719
|
+
* halt position. `recordHaltDrop` is an atomic min (`ZADD LT` on `subHaltFloor:{sub}`, member =
|
|
720
|
+
* split index); restart reads it to bound the replay and clears it.
|
|
721
|
+
*/
|
|
722
|
+
recordHaltDrop: (subscription: string, splitIndex: string, position: number) => Promise<void>;
|
|
723
|
+
getHaltFloor: (subscription: string, splitIndex: string) => Promise<number | undefined>;
|
|
724
|
+
clearHaltFloor: (subscription: string, splitIndex: string) => Promise<void>;
|
|
725
|
+
/**
|
|
726
|
+
* Rich failure detail for a HALTED split — the split-level analogue of `storeErroredStreamDetail`
|
|
727
|
+
* (`subHaltDetails:{sub}` HASH, field = split index → JSON). Written at halt time, removed by
|
|
728
|
+
* restart-datasplit; best-effort operator bookkeeping, never part of the halt correctness contract.
|
|
729
|
+
*/
|
|
730
|
+
storeSplitHaltDetail: (subscription: string, splitIndex: string, detail: ErroredStreamDetailType) => Promise<void>;
|
|
731
|
+
getSplitHaltDetails: (subscription: string) => Promise<Record<string, ErroredStreamDetailType>>;
|
|
732
|
+
removeSplitHaltDetail: (subscription: string, splitIndex: string) => Promise<void>;
|
|
733
|
+
/**
|
|
734
|
+
* Live per-split retry progress (TV1-1954 retry visibility) — `subRetryState:{sub}` HASH, field =
|
|
735
|
+
* split index → JSON SplitRetryStateType. Written on each retryable attempt, cleared on every loop
|
|
736
|
+
* exit; surfaced through getSubscriptionDataSplits as each split's `retry` field. Best-effort
|
|
737
|
+
* operator bookkeeping, never part of the retry correctness contract.
|
|
738
|
+
*/
|
|
739
|
+
setSplitRetryState: (subscription: string, splitIndex: string, retry: SplitRetryStateType) => Promise<void>;
|
|
740
|
+
clearSplitRetryState: (subscription: string, splitIndex: string) => Promise<void>;
|
|
741
|
+
getSplitRetryStates: (subscription: string) => Promise<Record<string, SplitRetryStateType>>;
|
|
742
|
+
start: () => Promise<void>;
|
|
743
|
+
};
|
|
25
744
|
export type OutboxManagerType = {
|
|
26
|
-
|
|
745
|
+
/** Receives an optional Structure State Variable with the result of the infrastructure change that happened when the service started */
|
|
746
|
+
start: (structureState?: StructureState) => Promise<void>;
|
|
27
747
|
stop: () => Promise<void>;
|
|
748
|
+
state: StateType;
|
|
749
|
+
eventDispatcher: EventDispatcherType;
|
|
750
|
+
};
|
|
751
|
+
/**
|
|
752
|
+
* Callbacks for outbox event processing instrumentation.
|
|
753
|
+
* Implementations MUST NOT throw — callbacks are invoked inline during event processing
|
|
754
|
+
* and an unhandled exception would disrupt outbox behavior. Use try/catch internally.
|
|
755
|
+
* The built-in `instrumentEventProcessing()` factory wraps each callback in try/catch.
|
|
756
|
+
*/
|
|
757
|
+
export type EventProcessingInstrumentation = {
|
|
758
|
+
onErrorClassified: (params: {
|
|
759
|
+
type: 'retryable' | 'critical' | 'sequence';
|
|
760
|
+
subscription: string;
|
|
761
|
+
event: string;
|
|
762
|
+
statusCode?: number;
|
|
763
|
+
}) => void;
|
|
764
|
+
onRetryAttempt: (params: {
|
|
765
|
+
subscription: string;
|
|
766
|
+
event: string;
|
|
767
|
+
attempt: number;
|
|
768
|
+
}) => void;
|
|
769
|
+
onRetryExhausted: (params: {
|
|
770
|
+
subscription: string;
|
|
771
|
+
event: string;
|
|
772
|
+
}) => void;
|
|
773
|
+
onStreamErrored: (params: {
|
|
774
|
+
subscription: string;
|
|
775
|
+
stream: string;
|
|
776
|
+
}) => void;
|
|
777
|
+
onStreamFixed: (params: {
|
|
778
|
+
subscription: string;
|
|
779
|
+
stream: string;
|
|
780
|
+
}) => void;
|
|
781
|
+
onDataSplitStatusChange: (params: {
|
|
782
|
+
subscription: string;
|
|
783
|
+
datasplit: string;
|
|
784
|
+
previousStatus: string;
|
|
785
|
+
newStatus: string;
|
|
786
|
+
}) => void;
|
|
787
|
+
onBackpressure: (params: {
|
|
788
|
+
subscription: string;
|
|
789
|
+
datasplit: string;
|
|
790
|
+
}) => void;
|
|
791
|
+
onSequenceRepull: (params: {
|
|
792
|
+
subscription: string;
|
|
793
|
+
datasplit: string;
|
|
794
|
+
}) => void;
|
|
795
|
+
onCacheHit?: (params: {
|
|
796
|
+
subscription: string;
|
|
797
|
+
source: 'debounce' | 'wait' | 'pull';
|
|
798
|
+
eventsServed: number;
|
|
799
|
+
}) => void;
|
|
800
|
+
onCacheMiss?: (params: {
|
|
801
|
+
subscription: string;
|
|
802
|
+
eventsRead: number;
|
|
803
|
+
durationMs: number;
|
|
804
|
+
}) => void;
|
|
805
|
+
onHydrate?: (params: {
|
|
806
|
+
eventstore: string;
|
|
807
|
+
source: 'redis' | 'db';
|
|
808
|
+
eventsRead: number;
|
|
809
|
+
durationMs: number;
|
|
810
|
+
}) => void;
|
|
811
|
+
onLazyFetch?: (params: {
|
|
812
|
+
subscription: string;
|
|
813
|
+
event: string;
|
|
814
|
+
durationMs: number;
|
|
815
|
+
}) => void;
|
|
816
|
+
/**
|
|
817
|
+
* Global commit-order stream DEPTH (`tail − floor`): how many entry-ids separate the stream's
|
|
818
|
+
* last entry from its trim floor (`firstId()`). Emitted per pump from the stream source, which is
|
|
819
|
+
* the layer that has both the tail and the floor. The backlog of un-trimmed events on the stream.
|
|
820
|
+
*/
|
|
821
|
+
onStreamDepth?: (params: {
|
|
822
|
+
subscription: string;
|
|
823
|
+
depth: number;
|
|
824
|
+
}) => void;
|
|
825
|
+
/**
|
|
826
|
+
* Per-`(sub,instance)` consumer LAG = `tail − this instance's read cursor`: how far behind the
|
|
827
|
+
* live stream tail this consumer's XREAD cursor is (in entry-id distance). Emitted per pump BEFORE
|
|
828
|
+
* the cursor advances, from the stream source (the only layer that holds the per-instance cursor).
|
|
829
|
+
*/
|
|
830
|
+
onConsumerLag?: (params: {
|
|
831
|
+
subscription: string;
|
|
832
|
+
instance: string;
|
|
833
|
+
lag: number;
|
|
834
|
+
}) => void;
|
|
835
|
+
/**
|
|
836
|
+
* By-position REFILL activity (cold-start / eviction / operator-replay recovery): the count of
|
|
837
|
+
* events re-dispatched by one `refillByPosition` pass. Emitted once per refill; a rising rate
|
|
838
|
+
* signals recovery churn (evictions, replays).
|
|
839
|
+
*/
|
|
840
|
+
onRefill?: (params: {
|
|
841
|
+
subscription: string;
|
|
842
|
+
refilled: number;
|
|
843
|
+
}) => void;
|
|
844
|
+
/**
|
|
845
|
+
* Per-`(sub,stream)` WATERMARK progress: the version the shared `subWm` advanced to after a
|
|
846
|
+
* successful send (dispatch progress). Emitted on every watermark advance in the stream dispatch.
|
|
847
|
+
*/
|
|
848
|
+
onWatermarkAdvance?: (params: {
|
|
849
|
+
subscription: string;
|
|
850
|
+
stream: string;
|
|
851
|
+
version: number;
|
|
852
|
+
}) => void;
|
|
853
|
+
/**
|
|
854
|
+
* Null-class circuit breaker TRIPPED (TV1-1954 slice 5): `breakerThreshold` consecutive failures for
|
|
855
|
+
* `(subscription, host)` reached, the host's split queues were PAUSED. The alarm the operator acts on
|
|
856
|
+
* (a dead consumer). Emitted from the dead-letter branch when the count crosses the threshold.
|
|
857
|
+
*/
|
|
858
|
+
onBreakerOpen?: (params: {
|
|
859
|
+
subscription: string;
|
|
860
|
+
host: string;
|
|
861
|
+
count: number;
|
|
862
|
+
}) => void;
|
|
863
|
+
/**
|
|
864
|
+
* Null-class circuit breaker CLOSED (TV1-1954 slice 5): a successful null send (a recovered probe)
|
|
865
|
+
* reset the count and cleared the open clock, the host's split queues resumed. The auto-recovery signal.
|
|
866
|
+
*/
|
|
867
|
+
onBreakerClose?: (params: {
|
|
868
|
+
subscription: string;
|
|
869
|
+
host: string;
|
|
870
|
+
}) => void;
|
|
28
871
|
};
|
|
29
872
|
export type TcpEventMessageRequest = {
|
|
30
873
|
endpoint: string;
|
|
31
874
|
event: CommittedEvent;
|
|
32
875
|
};
|
|
33
876
|
export type TcpEventMessageResponse = {
|
|
34
|
-
id:
|
|
877
|
+
id: string;
|
|
878
|
+
position: number;
|
|
35
879
|
status: number;
|
|
36
880
|
error?: string;
|
|
37
881
|
};
|