@flowcore/data-pump 0.22.1 → 0.23.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/dist/mod.js CHANGED
@@ -17,58 +17,227 @@ import {
17
17
  import { FlowcoreClient } from "@flowcore/sdk";
18
18
 
19
19
  // src/data-pump/metrics.ts
20
- import PromClient, { Counter, Gauge } from "prom-client";
21
- var dataPumpPromRegistry = new PromClient.Registry();
22
- var bufferEventCountGauge = new Gauge({
23
- name: "flowcore_data_pump_buffer_events_gauge",
24
- help: "The number of events in the buffer",
25
- labelNames: ["tenant", "data_core", "flow_type", "event_type"]
26
- });
27
- var bufferReservedEventCountGauge = new Gauge({
28
- name: "flowcore_data_pump_buffer_reserved_events_gauge",
29
- help: "The number of reserved events in the buffer",
30
- labelNames: ["tenant", "data_core", "flow_type", "event_type"]
31
- });
32
- var bufferSizeBytesGauge = new Gauge({
33
- name: "flowcore_data_pump_buffer_size_bytes_gauge",
34
- help: "The size of the buffer in bytes",
35
- labelNames: ["tenant", "data_core", "flow_type", "event_type"]
36
- });
37
- var eventsAcknowledgedCounter = new Counter({
38
- name: "flowcore_data_pump_events_acknowledged_counter",
39
- help: "The number of events acknowledged",
40
- labelNames: ["tenant", "data_core", "flow_type", "event_type"]
41
- });
42
- var eventsFailedCounter = new Counter({
43
- name: "flowcore_data_pump_events_failed_counter",
44
- help: "The number of events failed",
45
- labelNames: ["tenant", "data_core", "flow_type", "event_type"]
46
- });
47
- var eventsPulledSizeBytesCounter = new Counter({
48
- name: "flowcore_data_pump_events_pulled_size_bytes_counter",
49
- help: "The size of the events pulled in bytes",
50
- labelNames: ["tenant", "data_core", "flow_type", "event_type"]
51
- });
52
- var sdkCommandsCounter = new Counter({
53
- name: "flowcore_data_pump_sdk_commands_counter",
54
- help: "The number of SDK commands",
55
- labelNames: ["command"]
56
- });
57
- dataPumpPromRegistry.registerMetric(bufferEventCountGauge);
58
- dataPumpPromRegistry.registerMetric(bufferReservedEventCountGauge);
59
- dataPumpPromRegistry.registerMetric(bufferSizeBytesGauge);
60
- dataPumpPromRegistry.registerMetric(eventsAcknowledgedCounter);
61
- dataPumpPromRegistry.registerMetric(eventsFailedCounter);
62
- dataPumpPromRegistry.registerMetric(eventsPulledSizeBytesCounter);
63
- dataPumpPromRegistry.registerMetric(sdkCommandsCounter);
64
- var metrics = {
65
- bufferEventCountGauge,
66
- bufferReservedEventCountGauge,
67
- bufferSizeBytesGauge,
68
- eventsAcknowledgedCounter,
69
- eventsFailedCounter,
70
- eventsPulledSizeBytesCounter,
71
- sdkCommandsCounter
20
+ import { Counter, Gauge, Histogram, register as defaultPromRegistry, Registry } from "prom-client";
21
+ var dataPumpPromRegistry = new Registry();
22
+ var SOURCE_LABELS = ["tenant", "data_core", "flow_type"];
23
+ var EVENT_SOURCE_LABELS = [...SOURCE_LABELS, "event_type"];
24
+ var REPLAY_DURATION_BUCKETS_SECONDS = [
25
+ 5e-3,
26
+ 0.01,
27
+ 0.025,
28
+ 0.05,
29
+ 0.1,
30
+ 0.25,
31
+ 0.5,
32
+ 1,
33
+ 2,
34
+ 5,
35
+ 10,
36
+ 20,
37
+ 30,
38
+ 60,
39
+ 120
40
+ ];
41
+ var REPLAY_EVENT_COUNT_BUCKETS = [1, 10, 50, 100, 250, 500, 1e3, 2e3, 5e3, 1e4];
42
+ var REPLAY_BATCH_SIZE_BUCKETS = ["empty", "1", "2-10", "11-100", "101-1000", "1001+"];
43
+ var REPLAY_FETCH_RESULTS = ["success", "no_events", "error"];
44
+ var REPLAY_STAGE_RESULTS = ["success", "error"];
45
+ var REPLAY_IDLE_REASONS = ["no_events", "buffer_full", "waiting_for_events"];
46
+ function replayBatchSizeBucket(size) {
47
+ if (size <= 0) return "empty";
48
+ if (size === 1) return "1";
49
+ if (size <= 10) return "2-10";
50
+ if (size <= 100) return "11-100";
51
+ if (size <= 1e3) return "101-1000";
52
+ return "1001+";
53
+ }
54
+ function createDataPumpMetrics(registry, includeDefaultRegistry = false) {
55
+ const registers = includeDefaultRegistry ? [registry, defaultPromRegistry] : [registry];
56
+ const bufferEventCountGauge = new Gauge({
57
+ name: "flowcore_data_pump_buffer_events_gauge",
58
+ help: "The number of events in the buffer",
59
+ labelNames: EVENT_SOURCE_LABELS,
60
+ registers
61
+ });
62
+ const bufferReservedEventCountGauge = new Gauge({
63
+ name: "flowcore_data_pump_buffer_reserved_events_gauge",
64
+ help: "The number of reserved events in the buffer",
65
+ labelNames: EVENT_SOURCE_LABELS,
66
+ registers
67
+ });
68
+ const bufferSizeBytesGauge = new Gauge({
69
+ name: "flowcore_data_pump_buffer_size_bytes_gauge",
70
+ help: "The size of the buffer in bytes",
71
+ labelNames: EVENT_SOURCE_LABELS,
72
+ registers
73
+ });
74
+ const eventsAcknowledgedCounter = new Counter({
75
+ name: "flowcore_data_pump_events_acknowledged_counter",
76
+ help: "The number of events acknowledged",
77
+ labelNames: EVENT_SOURCE_LABELS,
78
+ registers
79
+ });
80
+ const eventsFailedCounter = new Counter({
81
+ name: "flowcore_data_pump_events_failed_counter",
82
+ help: "The number of events failed",
83
+ labelNames: EVENT_SOURCE_LABELS,
84
+ registers
85
+ });
86
+ const eventsPulledSizeBytesCounter = new Counter({
87
+ name: "flowcore_data_pump_events_pulled_size_bytes_counter",
88
+ help: "The size of the events pulled in bytes",
89
+ labelNames: EVENT_SOURCE_LABELS,
90
+ registers
91
+ });
92
+ const sdkCommandsCounter = new Counter({
93
+ name: "flowcore_data_pump_sdk_commands_counter",
94
+ help: "The number of SDK commands",
95
+ labelNames: ["command"],
96
+ registers
97
+ });
98
+ const replayFetchDuration = new Histogram({
99
+ name: "flowcore_data_pump_replay_fetch_duration_seconds",
100
+ help: "Replay event fetch duration in seconds",
101
+ labelNames: [...SOURCE_LABELS, "result"],
102
+ buckets: [...REPLAY_DURATION_BUCKETS_SECONDS],
103
+ registers
104
+ });
105
+ const replayFetchEvents = new Histogram({
106
+ name: "flowcore_data_pump_replay_fetch_events",
107
+ help: "Number of events returned by a replay fetch",
108
+ labelNames: SOURCE_LABELS,
109
+ buckets: [...REPLAY_EVENT_COUNT_BUCKETS],
110
+ registers
111
+ });
112
+ const replayHandlerDuration = new Histogram({
113
+ name: "flowcore_data_pump_replay_handler_duration_seconds",
114
+ help: "Replay handler duration in seconds",
115
+ labelNames: [...SOURCE_LABELS, "result", "batch_size_bucket"],
116
+ buckets: [...REPLAY_DURATION_BUCKETS_SECONDS],
117
+ registers
118
+ });
119
+ const replayAcknowledgementDuration = new Histogram({
120
+ name: "flowcore_data_pump_replay_acknowledgement_duration_seconds",
121
+ help: "Replay acknowledgement duration in seconds, excluding checkpointing",
122
+ labelNames: [...SOURCE_LABELS, "result"],
123
+ buckets: [...REPLAY_DURATION_BUCKETS_SECONDS],
124
+ registers
125
+ });
126
+ const replayCheckpointDuration = new Histogram({
127
+ name: "flowcore_data_pump_replay_checkpoint_duration_seconds",
128
+ help: "Replay state checkpoint duration in seconds",
129
+ labelNames: [...SOURCE_LABELS, "result"],
130
+ buckets: [...REPLAY_DURATION_BUCKETS_SECONDS],
131
+ registers
132
+ });
133
+ const replayIdleDuration = new Histogram({
134
+ name: "flowcore_data_pump_replay_idle_duration_seconds",
135
+ help: "Time the replay loop spends idle",
136
+ labelNames: [...SOURCE_LABELS, "reason"],
137
+ buckets: [...REPLAY_DURATION_BUCKETS_SECONDS, 300],
138
+ registers
139
+ });
140
+ return {
141
+ bufferEventCountGauge,
142
+ bufferReservedEventCountGauge,
143
+ bufferSizeBytesGauge,
144
+ eventsAcknowledgedCounter,
145
+ eventsFailedCounter,
146
+ eventsPulledSizeBytesCounter,
147
+ sdkCommandsCounter,
148
+ replayFetchDuration,
149
+ replayFetchEvents,
150
+ replayHandlerDuration,
151
+ replayAcknowledgementDuration,
152
+ replayCheckpointDuration,
153
+ replayIdleDuration
154
+ };
155
+ }
156
+ var metrics = createDataPumpMetrics(dataPumpPromRegistry, true);
157
+ var ReplayStageObserver = class {
158
+ constructor(stageMetrics, source, now = () => performance.now()) {
159
+ this.stageMetrics = stageMetrics;
160
+ this.source = source;
161
+ this.now = now;
162
+ }
163
+ stageMetrics;
164
+ source;
165
+ now;
166
+ async observeFetch(operation) {
167
+ const startedAt = this.now();
168
+ try {
169
+ const result = await operation();
170
+ this.stageMetrics.replayFetchDuration.observe(
171
+ { ...this.source, result: result.events.length ? "success" : "no_events" },
172
+ this.elapsedSeconds(startedAt)
173
+ );
174
+ this.stageMetrics.replayFetchEvents.observe(this.source, result.events.length);
175
+ return result;
176
+ } catch (error) {
177
+ this.stageMetrics.replayFetchDuration.observe({ ...this.source, result: "error" }, this.elapsedSeconds(startedAt));
178
+ throw error;
179
+ }
180
+ }
181
+ async observeHandler(events, operation) {
182
+ const startedAt = this.now();
183
+ const batchSize = replayBatchSizeBucket(events.length);
184
+ try {
185
+ const result = await operation();
186
+ this.stageMetrics.replayHandlerDuration.observe(
187
+ { ...this.source, result: "success", batch_size_bucket: batchSize },
188
+ this.elapsedSeconds(startedAt)
189
+ );
190
+ return result;
191
+ } catch (error) {
192
+ this.stageMetrics.replayHandlerDuration.observe(
193
+ { ...this.source, result: "error", batch_size_bucket: batchSize },
194
+ this.elapsedSeconds(startedAt)
195
+ );
196
+ throw error;
197
+ }
198
+ }
199
+ observeAcknowledgement(operation) {
200
+ const startedAt = this.now();
201
+ try {
202
+ const result = operation();
203
+ this.stageMetrics.replayAcknowledgementDuration.observe(
204
+ { ...this.source, result: "success" },
205
+ this.elapsedSeconds(startedAt)
206
+ );
207
+ return result;
208
+ } catch (error) {
209
+ this.stageMetrics.replayAcknowledgementDuration.observe(
210
+ { ...this.source, result: "error" },
211
+ this.elapsedSeconds(startedAt)
212
+ );
213
+ throw error;
214
+ }
215
+ }
216
+ observeCheckpoint(operation) {
217
+ return this.observeResult(this.stageMetrics.replayCheckpointDuration, operation);
218
+ }
219
+ async observeIdle(reason, operation) {
220
+ const startedAt = this.now();
221
+ try {
222
+ return await operation();
223
+ } finally {
224
+ this.stageMetrics.replayIdleDuration.observe({ ...this.source, reason }, this.elapsedSeconds(startedAt));
225
+ }
226
+ }
227
+ async observeResult(histogram, operation) {
228
+ const startedAt = this.now();
229
+ try {
230
+ const result = await operation();
231
+ histogram.observe({ ...this.source, result: "success" }, this.elapsedSeconds(startedAt));
232
+ return result;
233
+ } catch (error) {
234
+ histogram.observe({ ...this.source, result: "error" }, this.elapsedSeconds(startedAt));
235
+ throw error;
236
+ }
237
+ }
238
+ elapsedSeconds(startedAt) {
239
+ return Math.max(0, this.now() - startedAt) / 1e3;
240
+ }
72
241
  };
73
242
  var activeWorkersGauge = new Gauge({
74
243
  name: "flowcore_data_pump_cluster_active_workers_gauge",
@@ -145,6 +314,8 @@ var FlowcoreDataSource = class {
145
314
  eventTypeIds;
146
315
  /** Cached time buckets */
147
316
  timeBuckets;
317
+ /** Index for each cached bucket. */
318
+ timeBucketIndexes = /* @__PURE__ */ new Map();
148
319
  /**
149
320
  * Gets the tenant name from the data source configuration
150
321
  * @returns Tenant name
@@ -277,16 +448,46 @@ var FlowcoreDataSource = class {
277
448
  tenant: this.options.dataSource.tenant,
278
449
  eventTypeId: await this.getEventTypeIds(),
279
450
  cursor: cursor || void 0,
280
- pageSize: 1e4
451
+ pageSize: 1e4,
452
+ order: "asc"
281
453
  }),
282
454
  this.options.directMode
283
455
  );
284
456
  timeBuckets.push(...result.timeBuckets);
285
457
  cursor = result.nextCursor;
286
458
  } while (cursor !== void 0);
287
- this.timeBuckets = timeBuckets;
459
+ const normalizedTimeBuckets = [...new Set(timeBuckets.sort())];
460
+ this.timeBuckets = normalizedTimeBuckets;
461
+ this.timeBucketIndexes = /* @__PURE__ */ new Map();
462
+ for (let index = 0; index < normalizedTimeBuckets.length; index++) {
463
+ this.timeBucketIndexes.set(normalizedTimeBuckets[index], index);
464
+ }
288
465
  return this.timeBuckets;
289
466
  }
467
+ lowerBoundTimeBucket(timeBucket) {
468
+ const timeBuckets = this.timeBuckets ?? [];
469
+ const target = Number.parseFloat(timeBucket);
470
+ let low = 0;
471
+ let high = timeBuckets.length;
472
+ while (low < high) {
473
+ const middle = low + Math.floor((high - low) / 2);
474
+ if (Number.parseFloat(timeBuckets[middle]) < target) low = middle + 1;
475
+ else high = middle;
476
+ }
477
+ return low;
478
+ }
479
+ upperBoundTimeBucket(timeBucket) {
480
+ const timeBuckets = this.timeBuckets ?? [];
481
+ const target = Number.parseFloat(timeBucket);
482
+ let low = 0;
483
+ let high = timeBuckets.length;
484
+ while (low < high) {
485
+ const middle = low + Math.floor((high - low) / 2);
486
+ if (Number.parseFloat(timeBuckets[middle]) <= target) low = middle + 1;
487
+ else high = middle;
488
+ }
489
+ return low;
490
+ }
290
491
  /**
291
492
  * Gets the next time bucket after the specified time bucket
292
493
  * @param timeBucket - The reference time bucket
@@ -298,8 +499,8 @@ var FlowcoreDataSource = class {
298
499
  return null;
299
500
  }
300
501
  const timeBuckets = await this.getTimeBuckets();
301
- const index = timeBuckets.indexOf(closestTimeBucket);
302
- if (index === -1) {
502
+ const index = this.timeBucketIndexes.get(closestTimeBucket);
503
+ if (index === void 0) {
303
504
  throw new Error(`Could not get next timeBucket, timeBucket ${timeBucket} not found`);
304
505
  }
305
506
  return timeBuckets[index + 1] ?? null;
@@ -316,10 +517,14 @@ var FlowcoreDataSource = class {
316
517
  if (!timeBucket.match(/^\d{14}$/)) {
317
518
  throw new Error(`Invalid timebucket: ${timeBucket}`);
318
519
  }
520
+ if (!timeBuckets.length) {
521
+ return null;
522
+ }
319
523
  if (getBefore) {
320
- return timeBuckets.findLast((t) => Number.parseFloat(t) <= Number.parseFloat(timeBucket)) ?? timeBuckets[timeBuckets.length - 1];
524
+ const index = this.upperBoundTimeBucket(timeBucket) - 1;
525
+ return timeBuckets[index] ?? timeBuckets[timeBuckets.length - 1];
321
526
  }
322
- return timeBuckets.find((t) => Number.parseFloat(t) >= Number.parseFloat(timeBucket)) ?? timeBuckets[timeBuckets.length - 1];
527
+ return timeBuckets[this.lowerBoundTimeBucket(timeBucket)] ?? timeBuckets[timeBuckets.length - 1];
323
528
  }
324
529
  /**
325
530
  * Gets events from the data source starting from a specific state
@@ -620,6 +825,9 @@ var PulseEmitter = class {
620
825
  timeBucket: snapshot.timeBucket,
621
826
  eventId: snapshot.eventId ?? null,
622
827
  isLive: snapshot.isLive,
828
+ // Same widening as `sourceId` above: the CP accepts `paused`, the pinned sdk
829
+ // type does not declare it yet. The cast below absorbs it.
830
+ paused: snapshot.paused,
623
831
  buffer: {
624
832
  depth: snapshot.bufferDepth,
625
833
  reserved: snapshot.bufferReserved,
@@ -644,6 +852,7 @@ var PulseEmitter = class {
644
852
  };
645
853
 
646
854
  // src/data-pump/data-pump.ts
855
+ var textEncoder = new TextEncoder();
647
856
  var FlowcoreDataPump = class _FlowcoreDataPump {
648
857
  constructor(dataSource, notifier, stateManager, options, logger) {
649
858
  this.dataSource = dataSource;
@@ -651,6 +860,14 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
651
860
  this.stateManager = stateManager;
652
861
  this.options = options;
653
862
  this.logger = logger;
863
+ this.replayObserver = new ReplayStageObserver(metrics, {
864
+ tenant: this.dataSource.tenant,
865
+ data_core: this.dataSource.dataCore,
866
+ flow_type: this.dataSource.flowType
867
+ });
868
+ for (const eventType of this.dataSource.eventTypes) {
869
+ this.bufferStats.set(eventType, { eventCount: 0, eventReservedCount: 0, eventSizeBytes: 0 });
870
+ }
654
871
  this.bufferState = {
655
872
  timeBucket: format(startOfHour(utc(/* @__PURE__ */ new Date())), "yyyyMMddHH0000"),
656
873
  eventId: TimeUuid.now().toString()
@@ -663,7 +880,18 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
663
880
  logger;
664
881
  nextCursor;
665
882
  running = false;
883
+ // Delivery pause. Independent of `running`: a paused pump keeps fetching, keeps its
884
+ // buffer, keeps its cursor and keeps emitting pulses — it only stops handing events
885
+ // to the processor. The flag is sticky across `restart()` and `stop()`/`start()`, so
886
+ // a repositioned or bounced pump stays paused until `resume()` is called.
887
+ paused = false;
666
888
  restartTo;
889
+ // Invalidates delivery work that crossed a stop/restart boundary. Event IDs
890
+ // can reappear during replay, so `running` alone cannot identify the owner.
891
+ processLoopGeneration = 0;
892
+ activeProcessLoopGeneration;
893
+ processLoopBackoffGeneration;
894
+ processLoopRestartTimer;
667
895
  abortController;
668
896
  buffer = [];
669
897
  bufferState;
@@ -677,13 +905,23 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
677
905
  pulledCount = 0;
678
906
  processLoopRestartAttempts = 0;
679
907
  mainLoopRestartAttempts = 0;
908
+ replayObserver;
909
+ bufferStats = /* @__PURE__ */ new Map();
910
+ bufferReservedCount = 0;
911
+ bufferSizeBytes = 0;
912
+ gaugePublicationScheduled = false;
913
+ /**
914
+ * Whether delivery to the processor is currently paused.
915
+ * A paused pump is still running: it fetches, buffers and pulses.
916
+ */
917
+ get isPaused() {
918
+ return this.paused;
919
+ }
680
920
  get isRunning() {
681
921
  return this.running;
682
922
  }
683
923
  getSnapshot() {
684
924
  if (!this.running) return null;
685
- const reserved = this.buffer.filter((b) => b.status === "reserved").length;
686
- const sizeBytes = this.buffer.reduce((sum, b) => sum + JSON.stringify(b.event.payload).length, 0);
687
925
  return {
688
926
  pathwayId: this.pulseEmitter ? "" : "",
689
927
  // set by caller
@@ -691,9 +929,10 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
691
929
  timeBucket: this.bufferState.timeBucket,
692
930
  eventId: this.bufferState.eventId,
693
931
  isLive: this.isLive,
932
+ paused: this.paused,
694
933
  bufferDepth: this.buffer.length,
695
- bufferReserved: reserved,
696
- bufferSizeBytes: sizeBytes,
934
+ bufferReserved: this.bufferReservedCount,
935
+ bufferSizeBytes: this.bufferSizeBytes,
697
936
  acknowledgedTotal: this.acknowledgedCount,
698
937
  failedTotal: this.failedCount,
699
938
  pulledTotal: this.pulledCount,
@@ -739,6 +978,9 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
739
978
  },
740
979
  options.logger
741
980
  );
981
+ if (options.paused && options.processor) {
982
+ pump.paused = true;
983
+ }
742
984
  if (options.pulse) {
743
985
  const pathwayId = options.pulse.pathwayId;
744
986
  const sourceId = options.pulse.sourceId;
@@ -771,7 +1013,7 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
771
1013
  this.running = true;
772
1014
  this.startedAt = Date.now();
773
1015
  this.nextCursor = void 0;
774
- this.updateMetricsGauges();
1016
+ this.updateMetricsGauges(true);
775
1017
  this.pulseEmitter?.start();
776
1018
  const currentState = await this.stateManager.getState();
777
1019
  const timeBucket = currentState ? await this.dataSource.getClosestTimeBucket(currentState.timeBucket) : format(startOfHour(utc(/* @__PURE__ */ new Date())), "yyyyMMddHH0000");
@@ -826,23 +1068,68 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
826
1068
  if (stopAt !== void 0) {
827
1069
  this.options.stopAt = stopAt ?? void 0;
828
1070
  }
1071
+ this.isLive = false;
829
1072
  this.stop(true);
830
1073
  }
1074
+ /**
1075
+ * Pause delivery to the processor.
1076
+ *
1077
+ * The fetch loop keeps running and tops the buffer up to `bufferSize`, then blocks on
1078
+ * normal backpressure. The buffer, the cursor and the pulse emitter are untouched, so
1079
+ * the control plane still sees a live pump. An in-flight batch finishes its handler
1080
+ * and acknowledges, so the checkpoint stays accurate and nothing is redelivered
1081
+ * needlessly.
1082
+ *
1083
+ * Idempotent. A paused pump holds up to `bufferSize` events in memory.
1084
+ */
1085
+ pause() {
1086
+ if (!this.options.processor) {
1087
+ this.logger?.warn("pause() ignored: this pump has no processor. Stop calling reserve() instead.");
1088
+ return;
1089
+ }
1090
+ if (this.paused) return;
1091
+ this.paused = true;
1092
+ this.logger?.info("Data pump paused");
1093
+ this.notifyEventWaiters(false);
1094
+ }
1095
+ /**
1096
+ * Resume delivery to the processor from the exact position where {@link pause} stopped it.
1097
+ * Idempotent.
1098
+ */
1099
+ resume() {
1100
+ if (!this.paused) return;
1101
+ this.paused = false;
1102
+ if (this.processLoopRestartTimer) {
1103
+ clearTimeout(this.processLoopRestartTimer);
1104
+ this.processLoopRestartTimer = void 0;
1105
+ }
1106
+ this.processLoopBackoffGeneration = void 0;
1107
+ this.processLoopRestartAttempts = 0;
1108
+ this.logger?.info("Data pump resumed");
1109
+ this.ensureProcessLoop();
1110
+ }
831
1111
  stop(isRestart = false) {
832
1112
  this.running = false;
1113
+ this.processLoopGeneration++;
1114
+ this.activeProcessLoopGeneration = void 0;
1115
+ this.processLoopBackoffGeneration = void 0;
1116
+ if (this.processLoopRestartTimer) {
1117
+ clearTimeout(this.processLoopRestartTimer);
1118
+ this.processLoopRestartTimer = void 0;
1119
+ }
833
1120
  this.processLoopRestartAttempts = 0;
834
1121
  this.mainLoopRestartAttempts = 0;
835
1122
  this.buffer = [];
836
- this.updateMetricsGauges();
1123
+ this.resetBufferStats();
1124
+ this.updateMetricsGauges(true);
837
1125
  this.pulseEmitter?.stop();
838
1126
  this.abortController?.abort();
839
1127
  this.waiterBufferThreshold?.();
840
- if (!isRestart) {
841
- this.waiterEvents?.();
842
- }
1128
+ this.notifyEventWaiters(!isRestart);
843
1129
  }
844
1130
  updateState(eventId) {
845
- if (!this.stateManager.setState) {
1131
+ const stateManager = this.stateManager;
1132
+ if (!stateManager.setState) {
846
1133
  return;
847
1134
  }
848
1135
  const stateEventId = eventId ?? this.buffer[0]?.event.eventId;
@@ -851,23 +1138,26 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
851
1138
  }
852
1139
  const date = TimeUuid.fromString(stateEventId).getDate();
853
1140
  const timeBucket = format(startOfHour(utc(date)), "yyyyMMddHH0000");
854
- return this.stateManager.setState?.({ timeBucket, eventId: stateEventId });
1141
+ return this.replayObserver.observeCheckpoint(() => stateManager.setState({ timeBucket, eventId: stateEventId }));
855
1142
  }
856
1143
  async loop() {
857
1144
  do {
1145
+ this.ensureProcessLoop();
858
1146
  const amountToFetch = this.options.bufferSize - this.buffer.length;
859
1147
  if (amountToFetch <= 0) {
860
1148
  this.logger?.info("Buffer is full, waiting for space");
861
- await this.waitForBufferThreshold();
1149
+ await this.replayObserver.observeIdle("buffer_full", () => this.waitForBufferThreshold());
862
1150
  continue;
863
1151
  }
864
1152
  this.logger?.debug(`fetching ${amountToFetch} events from ${this.bufferState.timeBucket}(${this.nextCursor})`);
865
- const { events, nextCursor } = await this.dataSource.getEvents(
866
- this.bufferState,
867
- amountToFetch,
868
- this.stopAtState?.eventId,
869
- this.nextCursor,
870
- this.options.includeSensitiveData
1153
+ const { events, nextCursor } = await this.replayObserver.observeFetch(
1154
+ () => this.dataSource.getEvents(
1155
+ this.bufferState,
1156
+ amountToFetch,
1157
+ this.stopAtState?.eventId,
1158
+ this.nextCursor,
1159
+ this.options.includeSensitiveData
1160
+ )
871
1161
  );
872
1162
  if (!this.running) {
873
1163
  break;
@@ -875,10 +1165,10 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
875
1165
  this.logger?.debug(`fetched ${events.length} events`);
876
1166
  this.pulledCount += events.length;
877
1167
  this.mainLoopRestartAttempts = 0;
878
- this.buffer.push(...events.map((event) => ({ event, status: "open", deliveryCount: 0 })));
1168
+ this.addEventsToBuffer(events);
879
1169
  this.nextCursor = nextCursor;
880
1170
  this.updateMetricsGauges();
881
- events.length && this.waiterEvents?.();
1171
+ events.length && this.notifyEventWaiters();
882
1172
  this.bufferState.eventId = events[events.length - 1]?.eventId ?? this.bufferState.eventId;
883
1173
  if (this.stopAtState?.timeBucket && this.bufferState.timeBucket >= this.stopAtState.timeBucket && !events.length) {
884
1174
  this.logger?.info("Stopping at stopAt state");
@@ -900,9 +1190,12 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
900
1190
  this.isLive = true;
901
1191
  this.logger?.debug("Going live...");
902
1192
  this.abortController = new AbortController();
903
- await this.notifier.wait(this.abortController.signal);
1193
+ await this.replayObserver.observeIdle("no_events", () => this.notifier.wait(this.abortController.signal));
904
1194
  } else if (this.isLive) {
905
- await new Promise((resolve) => setTimeout(resolve, 1e3));
1195
+ await this.replayObserver.observeIdle(
1196
+ "no_events",
1197
+ () => new Promise((resolve) => setTimeout(resolve, 1e3))
1198
+ );
906
1199
  }
907
1200
  }
908
1201
  }
@@ -915,18 +1208,27 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
915
1208
  this.bufferState = this.restartTo;
916
1209
  this.restartTo = void 0;
917
1210
  this.running = true;
1211
+ this.ensureProcessLoop();
1212
+ this.pulseEmitter?.start();
918
1213
  return this.loop();
919
1214
  } catch (error) {
920
1215
  this.logger?.error("Failed to consume restartTo, dropping it", { error });
921
1216
  this.restartTo = void 0;
1217
+ this.notifyEventWaiters();
922
1218
  return;
923
1219
  }
924
1220
  }
925
1221
  this.logger?.info("Data pump stopped");
926
1222
  }
927
1223
  // #region Puller
928
- async reserve(amount) {
929
- if (!this.running) {
1224
+ reserve(amount) {
1225
+ return this.reserveInternal(amount);
1226
+ }
1227
+ async reserveInternal(amount, generation) {
1228
+ if (!this.running || generation !== void 0 && generation !== this.processLoopGeneration) {
1229
+ return [];
1230
+ }
1231
+ if (generation !== void 0 && this.paused) {
930
1232
  return [];
931
1233
  }
932
1234
  const events = [];
@@ -936,23 +1238,27 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
936
1238
  event.status = "reserved";
937
1239
  event.deliveryId = deliveryId;
938
1240
  event.deliveryCount++;
1241
+ this.updateReservedStats(event, 1);
939
1242
  events.push(event.event);
940
- this.incMetricsCounter("pulled", event.event.eventType, JSON.stringify(event.event).length);
1243
+ event.eventSizeBytes ??= textEncoder.encode(JSON.stringify(event.event)).byteLength;
1244
+ this.incMetricsCounter("pulled", event.event.eventType, event.eventSizeBytes);
941
1245
  if (events.length === amount) {
942
1246
  break;
943
1247
  }
944
1248
  }
945
1249
  }
946
1250
  if (!events.length) {
947
- await this.waitForEvents();
948
- return this.reserve(amount);
1251
+ await this.waitForEvents(generation);
1252
+ return this.reserveInternal(amount, generation);
949
1253
  }
950
1254
  this.updateMetricsGauges();
951
1255
  setTimeout(() => {
952
- this.reOpen(
1256
+ void this.reOpen(
953
1257
  events.map((event) => event.eventId),
954
1258
  deliveryId
955
- );
1259
+ ).catch((error) => {
1260
+ this.logger?.error("Failed to reopen events after acknowledgement timeout", { error });
1261
+ });
956
1262
  }, this.options.achknowledgeTimeoutMs);
957
1263
  return events;
958
1264
  }
@@ -963,22 +1269,28 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
963
1269
  if (!this.running) {
964
1270
  return;
965
1271
  }
966
- const lastEventInBuffer = this.buffer[this.buffer.length - 1];
967
- this.buffer = this.buffer.filter((event) => {
968
- if (eventIds.includes(event.event.eventId)) {
969
- this.incMetricsCounter("acknowledged", event.event.eventType, 1);
970
- this.acknowledgedCount++;
971
- return false;
1272
+ const eventIdSet = new Set(eventIds);
1273
+ const checkpointEventId = this.replayObserver.observeAcknowledgement(() => {
1274
+ const lastEventInBuffer = this.buffer[this.buffer.length - 1];
1275
+ this.buffer = this.buffer.filter((event) => {
1276
+ if (eventIdSet.has(event.event.eventId)) {
1277
+ this.incMetricsCounter("acknowledged", event.event.eventType, 1);
1278
+ this.acknowledgedCount++;
1279
+ this.removeFromBufferStats(event);
1280
+ return false;
1281
+ }
1282
+ return true;
1283
+ });
1284
+ if (this.buffer.length <= this.options.bufferSize - this.options.bufferThreshold) {
1285
+ this.waiterBufferThreshold?.();
972
1286
  }
973
- return true;
1287
+ return this.buffer.length ? void 0 : lastEventInBuffer?.event.eventId;
974
1288
  });
975
- if (this.buffer.length <= this.options.bufferSize - this.options.bufferThreshold) {
976
- this.waiterBufferThreshold?.();
977
- }
978
- await this.updateState(this.buffer.length ? void 0 : lastEventInBuffer?.event.eventId);
979
1289
  this.updateMetricsGauges();
980
- if (!this.buffer.length) {
981
- this.waiterBufferEmpty?.();
1290
+ try {
1291
+ await this.updateState(checkpointEventId);
1292
+ } finally {
1293
+ this.notifyBufferEmpty();
982
1294
  }
983
1295
  }
984
1296
  async fail(eventIds) {
@@ -986,84 +1298,140 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
986
1298
  return;
987
1299
  }
988
1300
  const lastEventInBuffer = this.buffer[this.buffer.length - 1];
1301
+ const eventIdSet = new Set(eventIds);
989
1302
  const failedEvents = [];
990
1303
  this.buffer = this.buffer.filter((event) => {
991
- if (eventIds.includes(event.event.eventId)) {
1304
+ if (eventIdSet.has(event.event.eventId)) {
992
1305
  this.incMetricsCounter("failed", event.event.eventType, 1);
993
1306
  this.failedCount++;
994
1307
  failedEvents.push(event.event);
1308
+ this.removeFromBufferStats(event);
995
1309
  return false;
996
1310
  }
997
1311
  return true;
998
1312
  });
999
1313
  this.logger?.info(`Failed ${failedEvents.length} events`);
1000
- void this.options.processor?.failedHandler?.(failedEvents);
1001
1314
  if (this.buffer.length <= this.options.bufferSize - this.options.bufferThreshold) {
1002
1315
  this.waiterBufferThreshold?.();
1003
1316
  }
1004
- await this.updateState(this.buffer.length ? void 0 : lastEventInBuffer?.event.eventId);
1005
- if (!this.buffer.length) {
1006
- this.waiterBufferEmpty?.();
1317
+ this.updateMetricsGauges();
1318
+ try {
1319
+ await this.options.processor?.failedHandler?.(failedEvents);
1320
+ await this.updateState(this.buffer.length ? void 0 : lastEventInBuffer?.event.eventId);
1321
+ } finally {
1322
+ this.notifyBufferEmpty();
1007
1323
  }
1008
1324
  }
1009
1325
  async reOpen(eventIds, deliveryId) {
1326
+ const eventIdSet = new Set(eventIds);
1010
1327
  let lastEvent;
1011
1328
  const failedEvents = [];
1012
1329
  const reopenedEvents = [];
1013
1330
  this.buffer = this.buffer.filter((event) => {
1014
- if (event.deliveryId !== deliveryId || !eventIds.includes(event.event.eventId)) {
1331
+ if (event.deliveryId !== deliveryId || !eventIdSet.has(event.event.eventId)) {
1015
1332
  return true;
1016
1333
  }
1017
1334
  if (this.options.maxRedeliveryCount > -1 && event.deliveryCount > this.options.maxRedeliveryCount) {
1018
1335
  this.incMetricsCounter("failed", event.event.eventType, 1);
1336
+ this.failedCount++;
1019
1337
  failedEvents.push(event.event);
1020
1338
  lastEvent = event.event;
1339
+ this.removeFromBufferStats(event);
1021
1340
  return false;
1022
1341
  }
1023
1342
  event.status = "open";
1024
1343
  event.deliveryId = void 0;
1344
+ this.updateReservedStats(event, -1);
1025
1345
  reopenedEvents.push(event.event);
1026
1346
  return true;
1027
1347
  });
1028
1348
  this.updateMetricsGauges();
1029
1349
  if (reopenedEvents.length) {
1030
1350
  this.logger?.info(`Reopened ${reopenedEvents.length} events`);
1031
- await this.waiterEvents?.();
1351
+ this.notifyEventWaiters();
1032
1352
  }
1033
1353
  if (!failedEvents.length) {
1034
1354
  return;
1035
1355
  }
1036
1356
  this.logger?.info(`Failed ${failedEvents.length} events`);
1037
- void this.options.processor?.failedHandler?.(failedEvents);
1038
- void this.finallyFailedHandler?.(failedEvents);
1039
1357
  if (this.buffer.length <= this.options.bufferSize - this.options.bufferThreshold) {
1040
1358
  this.waiterBufferThreshold?.();
1041
1359
  }
1042
- await this.updateState(this.buffer.length ? void 0 : lastEvent?.eventId);
1043
- if (!this.buffer.length) {
1044
- this.waiterBufferEmpty?.();
1360
+ try {
1361
+ const callbackResults = await Promise.allSettled([
1362
+ Promise.resolve().then(() => this.options.processor?.failedHandler?.(failedEvents)),
1363
+ Promise.resolve().then(() => this.finallyFailedHandler?.(failedEvents))
1364
+ ]);
1365
+ const callbackFailure = callbackResults.find(
1366
+ (result) => result.status === "rejected"
1367
+ );
1368
+ if (callbackFailure) {
1369
+ throw callbackFailure.reason;
1370
+ }
1371
+ await this.updateState(this.buffer.length ? void 0 : lastEvent?.eventId);
1372
+ } finally {
1373
+ this.notifyBufferEmpty();
1045
1374
  }
1046
1375
  }
1047
1376
  // #endregion
1048
1377
  // #region Pusher
1378
+ /**
1379
+ * Guarantee a live process loop whenever the pump is running with a
1380
+ * processor. Called from the fetch loop, so a delivery loop that exited for
1381
+ * any reason — most importantly a restart, which clears `running` while the
1382
+ * loop is mid-batch — comes back within one fetch iteration instead of
1383
+ * leaving a pump that pulls but never delivers.
1384
+ */
1385
+ ensureProcessLoop() {
1386
+ if (!this.options.processor || !this.running || this.paused || this.activeProcessLoopGeneration === this.processLoopGeneration || this.processLoopBackoffGeneration === this.processLoopGeneration) {
1387
+ return;
1388
+ }
1389
+ this.startProcessLoop();
1390
+ }
1049
1391
  startProcessLoop() {
1050
- this.processLoop().catch((error) => {
1392
+ const generation = this.processLoopGeneration;
1393
+ if (!this.options.processor || this.paused || !this.isCurrentProcessLoop(generation) || this.activeProcessLoopGeneration === generation || this.processLoopBackoffGeneration === generation) {
1394
+ return;
1395
+ }
1396
+ this.activeProcessLoopGeneration = generation;
1397
+ this.processLoop(generation).then(() => {
1398
+ if (this.activeProcessLoopGeneration !== generation) return;
1399
+ this.activeProcessLoopGeneration = void 0;
1400
+ if (this.isCurrentProcessLoop(generation)) {
1401
+ this.startProcessLoop();
1402
+ }
1403
+ }).catch((error) => {
1404
+ if (this.activeProcessLoopGeneration !== generation) return;
1405
+ this.activeProcessLoopGeneration = void 0;
1051
1406
  this.logger?.error("Error in processor", { error });
1052
- if (!this.running) return;
1407
+ if (!this.isCurrentProcessLoop(generation)) return;
1053
1408
  this.processLoopRestartAttempts++;
1054
1409
  const delay = Math.min(1e3 * Math.pow(2, this.processLoopRestartAttempts - 1), 3e4);
1055
1410
  this.logger?.warn(`Restarting process loop in ${delay}ms (attempt ${this.processLoopRestartAttempts})`);
1056
- setTimeout(() => {
1057
- if (!this.running) return;
1411
+ this.processLoopBackoffGeneration = generation;
1412
+ this.processLoopRestartTimer = setTimeout(() => {
1413
+ if (this.processLoopBackoffGeneration !== generation) return;
1414
+ this.processLoopBackoffGeneration = void 0;
1415
+ this.processLoopRestartTimer = void 0;
1416
+ if (!this.isCurrentProcessLoop(generation)) return;
1058
1417
  this.startProcessLoop();
1059
1418
  }, delay);
1060
1419
  });
1061
1420
  }
1062
- async processLoop() {
1063
- while (this.running) {
1421
+ isCurrentProcessLoop(generation) {
1422
+ return this.running && generation === this.processLoopGeneration;
1423
+ }
1424
+ async processLoop(generation) {
1425
+ while (this.isCurrentProcessLoop(generation)) {
1426
+ if (this.paused) return;
1064
1427
  try {
1065
- const events = await this.reserve(this.options.processor?.concurrency ?? 1);
1066
- await this.options.processor?.handler(events);
1428
+ const events = await this.reserveInternal(this.options.processor?.concurrency ?? 1, generation);
1429
+ if (!this.isCurrentProcessLoop(generation)) return;
1430
+ if (!events.length) continue;
1431
+ await this.replayObserver.observeHandler(events, async () => {
1432
+ await this.options.processor?.handler(events);
1433
+ });
1434
+ if (!this.isCurrentProcessLoop(generation)) return;
1067
1435
  await this.acknowledge(events.map((event) => event.eventId));
1068
1436
  this.processLoopRestartAttempts = 0;
1069
1437
  } catch (error) {
@@ -1074,54 +1442,71 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
1074
1442
  }
1075
1443
  // #endregion
1076
1444
  // #region Metrics
1077
- updateMetricsGauges() {
1078
- const stats = /* @__PURE__ */ new Map();
1079
- for (const eventType of this.dataSource.eventTypes) {
1080
- stats.set(eventType, {
1081
- eventCount: 0,
1082
- eventReservedCount: 0,
1083
- eventSizeBytes: 0
1084
- });
1085
- }
1086
- for (const item of this.buffer) {
1087
- const stat = stats.get(item.event.eventType);
1088
- if (!stat) {
1089
- continue;
1090
- }
1091
- stat.eventCount++;
1092
- if (item.status === "reserved") {
1093
- stat.eventReservedCount++;
1445
+ addEventsToBuffer(events) {
1446
+ for (const event of events) {
1447
+ const item = {
1448
+ event,
1449
+ status: "open",
1450
+ deliveryCount: 0,
1451
+ payloadSizeBytes: textEncoder.encode(JSON.stringify(event.payload)).byteLength
1452
+ };
1453
+ this.buffer.push(item);
1454
+ this.bufferSizeBytes += item.payloadSizeBytes;
1455
+ const stat = this.bufferStats.get(event.eventType);
1456
+ if (stat) {
1457
+ stat.eventCount++;
1458
+ stat.eventSizeBytes += item.payloadSizeBytes;
1094
1459
  }
1095
- stat.eventSizeBytes += JSON.stringify(item.event.payload).length;
1096
1460
  }
1097
- for (const [eventType, stat] of stats) {
1098
- metrics.bufferEventCountGauge.set(
1099
- {
1100
- tenant: this.dataSource.tenant,
1101
- data_core: this.dataSource.dataCore,
1102
- flow_type: this.dataSource.flowType,
1103
- event_type: eventType
1104
- },
1105
- stat.eventCount
1106
- );
1107
- metrics.bufferReservedEventCountGauge.set(
1108
- {
1109
- tenant: this.dataSource.tenant,
1110
- data_core: this.dataSource.dataCore,
1111
- flow_type: this.dataSource.flowType,
1112
- event_type: eventType
1113
- },
1114
- stat.eventReservedCount
1115
- );
1116
- metrics.bufferSizeBytesGauge.set(
1117
- {
1118
- tenant: this.dataSource.tenant,
1119
- data_core: this.dataSource.dataCore,
1120
- flow_type: this.dataSource.flowType,
1121
- event_type: eventType
1122
- },
1123
- stat.eventSizeBytes
1124
- );
1461
+ }
1462
+ updateReservedStats(item, delta) {
1463
+ this.bufferReservedCount += delta;
1464
+ const stat = this.bufferStats.get(item.event.eventType);
1465
+ if (stat) stat.eventReservedCount += delta;
1466
+ }
1467
+ removeFromBufferStats(item) {
1468
+ this.bufferSizeBytes -= item.payloadSizeBytes;
1469
+ const stat = this.bufferStats.get(item.event.eventType);
1470
+ if (stat) {
1471
+ stat.eventCount--;
1472
+ stat.eventSizeBytes -= item.payloadSizeBytes;
1473
+ }
1474
+ if (item.status === "reserved") this.updateReservedStats(item, -1);
1475
+ }
1476
+ resetBufferStats() {
1477
+ this.bufferReservedCount = 0;
1478
+ this.bufferSizeBytes = 0;
1479
+ for (const stat of this.bufferStats.values()) {
1480
+ stat.eventCount = 0;
1481
+ stat.eventReservedCount = 0;
1482
+ stat.eventSizeBytes = 0;
1483
+ }
1484
+ }
1485
+ updateMetricsGauges(synchronous = false) {
1486
+ if (synchronous) {
1487
+ this.gaugePublicationScheduled = false;
1488
+ this.publishMetricsGauges();
1489
+ return;
1490
+ }
1491
+ if (this.gaugePublicationScheduled) return;
1492
+ this.gaugePublicationScheduled = true;
1493
+ queueMicrotask(() => {
1494
+ if (!this.gaugePublicationScheduled) return;
1495
+ this.gaugePublicationScheduled = false;
1496
+ this.publishMetricsGauges();
1497
+ });
1498
+ }
1499
+ publishMetricsGauges() {
1500
+ for (const [eventType, stat] of this.bufferStats) {
1501
+ const labels = {
1502
+ tenant: this.dataSource.tenant,
1503
+ data_core: this.dataSource.dataCore,
1504
+ flow_type: this.dataSource.flowType,
1505
+ event_type: eventType
1506
+ };
1507
+ metrics.bufferEventCountGauge.set(labels, stat.eventCount);
1508
+ metrics.bufferReservedEventCountGauge.set(labels, stat.eventReservedCount);
1509
+ metrics.bufferSizeBytesGauge.set(labels, stat.eventSizeBytes);
1125
1510
  }
1126
1511
  }
1127
1512
  incMetricsCounter(name, eventType, value) {
@@ -1163,12 +1548,27 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
1163
1548
  }
1164
1549
  // #endregion
1165
1550
  // #region Waiters
1166
- waiterEvents;
1167
- async waitForEvents() {
1551
+ publicEventWaiter;
1552
+ processEventWaiters = /* @__PURE__ */ new Map();
1553
+ notifyEventWaiters(includePublic = true) {
1554
+ if (includePublic) {
1555
+ const publicWaiter = this.publicEventWaiter;
1556
+ this.publicEventWaiter = void 0;
1557
+ publicWaiter?.();
1558
+ }
1559
+ const processWaiters = [...this.processEventWaiters.values()];
1560
+ this.processEventWaiters.clear();
1561
+ for (const waiter of processWaiters) waiter();
1562
+ }
1563
+ async waitForEvents(generation) {
1168
1564
  const promise = new Promise((resolve) => {
1169
- this.waiterEvents = resolve;
1565
+ if (generation === void 0) {
1566
+ this.publicEventWaiter = resolve;
1567
+ } else {
1568
+ this.processEventWaiters.set(generation, resolve);
1569
+ }
1170
1570
  });
1171
- await promise;
1571
+ await this.replayObserver.observeIdle("waiting_for_events", () => promise);
1172
1572
  }
1173
1573
  waiterBufferThreshold;
1174
1574
  async waitForBufferThreshold() {
@@ -1178,6 +1578,11 @@ var FlowcoreDataPump = class _FlowcoreDataPump {
1178
1578
  await promise;
1179
1579
  }
1180
1580
  waiterBufferEmpty;
1581
+ notifyBufferEmpty() {
1582
+ if (!this.buffer.length) {
1583
+ this.waiterBufferEmpty?.();
1584
+ }
1585
+ }
1181
1586
  async waitForBufferEmpty() {
1182
1587
  if (!this.buffer.length) {
1183
1588
  return;
@@ -1429,6 +1834,7 @@ var FlowcoreDataPumpCluster = class {
1429
1834
  this.leaseRenewIntervalMs = options.leaseRenewIntervalMs ?? DEFAULT_LEASE_RENEW_INTERVAL_MS;
1430
1835
  this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
1431
1836
  this.workerConcurrency = options.workerConcurrency ?? DEFAULT_WORKER_CONCURRENCY;
1837
+ this.paused = options.paused ?? false;
1432
1838
  this.logger = options.logger;
1433
1839
  this.workerHandler = options.processor?.handler;
1434
1840
  this.workerFailedHandler = options.processor?.failedHandler;
@@ -1443,6 +1849,10 @@ var FlowcoreDataPumpCluster = class {
1443
1849
  logger;
1444
1850
  running = false;
1445
1851
  isLeader = false;
1852
+ // Delivery pause held on the CLUSTER, not on the pump. A leader change destroys the
1853
+ // pump instance and builds a new one, so a pause stored on the pump would silently
1854
+ // evaporate on failover and delivery would resume with no operator action.
1855
+ paused;
1446
1856
  pump;
1447
1857
  leaderConnection;
1448
1858
  // leader state
@@ -1546,6 +1956,33 @@ var FlowcoreDataPumpCluster = class {
1546
1956
  this.startHeartbeat();
1547
1957
  this.startElectionLoop();
1548
1958
  }
1959
+ /**
1960
+ * Whether delivery is paused across the cluster.
1961
+ * The flag lives on the cluster, so it survives a leader change.
1962
+ */
1963
+ get isPaused() {
1964
+ return this.paused;
1965
+ }
1966
+ /**
1967
+ * Pause delivery. A leader applies it to its pump immediately. A follower records it,
1968
+ * so the pause is re-applied if this instance later becomes leader.
1969
+ *
1970
+ * The flag is in-memory. It does NOT survive a process restart or a full rolling
1971
+ * deploy — persist it in the coordinator or the control plane if you need that.
1972
+ */
1973
+ pause() {
1974
+ if (this.paused) return;
1975
+ this.paused = true;
1976
+ this.pump?.pause();
1977
+ this.logger?.info("Cluster delivery paused", { instanceId: this.instanceId, isLeader: this.isLeader });
1978
+ }
1979
+ /** Resume delivery from the position where {@link pause} stopped it. */
1980
+ resume() {
1981
+ if (!this.paused) return;
1982
+ this.paused = false;
1983
+ this.pump?.resume();
1984
+ this.logger?.info("Cluster delivery resumed", { instanceId: this.instanceId, isLeader: this.isLeader });
1985
+ }
1549
1986
  async stop() {
1550
1987
  if (!this.running) return;
1551
1988
  this.running = false;
@@ -1668,6 +2105,10 @@ var FlowcoreDataPumpCluster = class {
1668
2105
  startPumpAsLeader() {
1669
2106
  const pumpOptions = {
1670
2107
  ...this.options,
2108
+ // Build the pump already paused. Pausing it after `start()` would let the new
2109
+ // leader deliver events in the gap, which is exactly what the operator asked
2110
+ // us not to do.
2111
+ paused: this.paused,
1671
2112
  processor: {
1672
2113
  concurrency: this.workerConcurrency,
1673
2114
  handler: async (events) => {
@@ -1678,6 +2119,9 @@ var FlowcoreDataPumpCluster = class {
1678
2119
  }
1679
2120
  };
1680
2121
  this.pump = FlowcoreDataPump.create(pumpOptions, this.options.dataSourceOverride);
2122
+ if (this.paused) {
2123
+ this.logger?.info("Leader pump started paused", { instanceId: this.instanceId });
2124
+ }
1681
2125
  this.pump.start().catch((error) => {
1682
2126
  this.logger?.error("Pump error in leader mode", { error });
1683
2127
  if (!this.running || !this.isLeader) return;
@@ -1902,11 +2346,20 @@ export {
1902
2346
  NatsDistributionLeader,
1903
2347
  NatsDistributionWorker,
1904
2348
  PulseEmitter,
2349
+ REPLAY_BATCH_SIZE_BUCKETS,
2350
+ REPLAY_DURATION_BUCKETS_SECONDS,
2351
+ REPLAY_EVENT_COUNT_BUCKETS,
2352
+ REPLAY_FETCH_RESULTS,
2353
+ REPLAY_IDLE_REASONS,
2354
+ REPLAY_STAGE_RESULTS,
2355
+ ReplayStageObserver,
1905
2356
  WsConnection,
1906
2357
  clusterMetrics,
2358
+ createDataPumpMetrics,
1907
2359
  dataPumpPromRegistry,
1908
2360
  deserializeMessage,
1909
2361
  metrics,
1910
2362
  noOpLogger,
2363
+ replayBatchSizeBucket,
1911
2364
  serializeMessage
1912
2365
  };