@requence/service 0.8.2-edge.1151 → 0.8.2-edge.1154

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.
@@ -11,35 +11,130 @@ export declare const DEFAULT_AMQP_PUBLISH_RETRY_ATTEMPTS = 3;
11
11
  * out and retries) and far below the incident's multi-second stalls.
12
12
  */
13
13
  export declare const PUBLISH_STARVATION_THRESHOLD_MS = 1000;
14
- export interface LoopStarvationProbe {
14
+ /**
15
+ * How long to wait, after a confirm deadline elapsed on a starved loop, before
16
+ * reading the link's incarnation for the second time.
17
+ *
18
+ * The stall that delayed the confirm delayed the socket's own close callback
19
+ * too, and Node runs the timers phase BEFORE the poll phase — so the deadline's
20
+ * timer can fire before the disconnect it was caused by has even been read off
21
+ * the socket. Deciding on the spot would call that lost message delivered. One
22
+ * settle turn puts the read after the poll phase; the margin above that costs
23
+ * nothing, because this only ever delays a publish that already spent its full
24
+ * timeout.
25
+ */
26
+ export declare const PUBLISH_LINK_SETTLE_MS = 250;
27
+ /**
28
+ * Reads the publish link's incarnation: a number that changes on every
29
+ * (re)connect, and `null` for as long as the link is down.
30
+ *
31
+ * Encoding "down" as `null` is what lets a single equality carry the whole
32
+ * liveness question — `start !== null && start === end` holds only if the link
33
+ * was up at both ends AND never cycled in between.
34
+ */
35
+ export type PublishLinkEpoch = () => number | null;
36
+ export interface PublishLinkTracker {
37
+ /** Pass this to `createPublishWindowProbe`. */
38
+ epoch: PublishLinkEpoch;
39
+ /** Call when the publish channel is (re)connected and usable. */
40
+ markConnected: () => void;
41
+ /** Call when the connection or the publish channel is lost. */
42
+ markDisconnected: () => void;
43
+ }
44
+ /**
45
+ * Tracks whether a publisher's link to the broker is up, and how many times it
46
+ * has been re-made. Every AMQP client wires this to its own connection and
47
+ * channel events; the counter is deliberately not derived from the library's
48
+ * `isConnected()`, because a drop-and-recover inside one publish window is
49
+ * invisible to a boolean and must not read as "the link held".
50
+ */
51
+ export declare function createPublishLinkTracker(): PublishLinkTracker;
52
+ export interface PublishTimeoutVerdict {
53
+ /** Longest stretch (ms) in which the loop could not run a callback. */
54
+ starvedMs: number;
55
+ /** The loop was starved past the threshold during the window. */
56
+ starved: boolean;
57
+ /** The link was down at some point while the confirm was outstanding. */
58
+ linkLost: boolean;
59
+ /**
60
+ * A reading taken while the message was still the library's responsibility
61
+ * found the publisher's send queue empty, which proves this message was handed
62
+ * to the socket. `false` means unproven, not "provably still queued".
63
+ */
64
+ leftSendQueue: boolean;
65
+ /**
66
+ * Only true when the loop was starved AND the link held throughout AND the
67
+ * message provably left the send queue: it is in the broker's hands and must
68
+ * NOT be re-sent.
69
+ */
70
+ delivered: boolean;
71
+ }
72
+ export interface PublishWindowProbe {
73
+ /**
74
+ * Take a send-queue reading now. Call it synchronously right after handing the
75
+ * message to the publisher: `ChannelWrapper.publish()` enqueues AND runs its
76
+ * send worker synchronously, so on a healthy channel the queue is already
77
+ * empty by the time it returns — which is the sharpest reading available.
78
+ */
79
+ observeSendQueue: () => void;
80
+ /** Stop sampling. Idempotent; call it on the success path. */
81
+ stop: () => void;
15
82
  /**
16
- * Stop sampling and return the longest stretch (ms) during which the loop
17
- * could not run a callback since the probe started. Idempotent — subsequent
18
- * calls return the same value without re-reading the clock.
83
+ * Decide what an elapsed confirm deadline means. Stops sampling, then — only
84
+ * where the answer could still flip — waits out `settleMs` before re-reading
85
+ * the link. Idempotent: subsequent calls return the first verdict.
19
86
  */
20
- stop: () => number;
87
+ readTimeoutVerdict: () => Promise<PublishTimeoutVerdict>;
88
+ }
89
+ interface PublishWindowProbeOptions {
90
+ epoch: PublishLinkEpoch;
91
+ /**
92
+ * Reads how many messages the publisher still holds unsent — the wrapper's
93
+ * `queueLength()`. Required, not optional: a publisher that skipped it would
94
+ * silently inherit the pre-#139 verdict and drop messages again.
95
+ */
96
+ sendQueueLength: () => number;
97
+ now?: () => number;
98
+ settleMs?: number;
21
99
  }
22
100
  /**
23
- * Measure how long the event loop was starved while a single publish is in
24
- * flight. It runs a self-rescheduling timer and, crucially, also reads the
25
- * pending gap synchronously at `stop()`: a stall that blocked the loop right up
26
- * to `stop()` is caught by `now() - expected` even though the timer never got a
27
- * chance to fire. That makes the reading immune to the very stall it measures
28
- * and independent of callback ordering after the loop resumes.
101
+ * Watch a single publish's in-flight window: how long the event loop was
102
+ * starved, whether the link to the broker survived, and whether the message
103
+ * provably left the publisher's send queue.
104
+ *
105
+ * The starvation term runs a self-rescheduling timer and, crucially, also reads
106
+ * the pending gap synchronously when sampling stops: a stall that blocked the
107
+ * loop right up to that moment is caught by `now() - expected` even though the
108
+ * timer never got a chance to fire. That makes the reading immune to the very
109
+ * stall it measures and independent of callback ordering after the loop resumes.
110
+ *
111
+ * The send-queue term samples on the same tick, and the caller adds the sharpest
112
+ * reading of all through `observeSendQueue()` — the synchronous one taken right
113
+ * after the publisher accepted the message, which no stall can delay.
29
114
  *
30
115
  * The clock is monotonic (`performance.now`), never wall time — the operator's
31
116
  * lag monitor uses the same, and a wall-clock step (NTP) must not read as
32
- * starvation and suppress a legitimate retry. `now` is injectable for tests.
117
+ * starvation and suppress a legitimate retry. `now` and `settleMs` are
118
+ * injectable for tests.
33
119
  */
34
- export declare function createLoopStarvationProbe({ now, }?: {
35
- now?: () => number;
36
- }): LoopStarvationProbe;
120
+ export declare function createPublishWindowProbe({ epoch, sendQueueLength, now, settleMs, }: PublishWindowProbeOptions): PublishWindowProbe;
37
121
  /**
38
- * Whether a confirm deadline that elapsed should be read as loop starvation
39
- * rather than a lost publish. When it returns true the publish must NOT be
40
- * re-sent: the message is in the broker's hands and only the confirm was late.
122
+ * The starvation term of the verdict — whether the loop was unresponsive long
123
+ * enough for the confirm deadline to be an artefact of the stall. On its own it
124
+ * is NOT a reason to suppress a re-send; see `createPublishWindowProbe`.
41
125
  */
42
126
  export declare function isStarvedPublishTimeout(starvedMs: number): boolean;
127
+ /**
128
+ * The error a non-delivered confirm deadline becomes. Shared so the operator and
129
+ * the task SDK report the same thing, and worded to say WHICH of the two failure
130
+ * shapes happened — a starved window whose link died reads as a healthy broker
131
+ * timing out unless the message says otherwise.
132
+ *
133
+ * The `RabbitMQ publish timed out` prefix is load-bearing:
134
+ * `isRetryablePublishError` matches on it.
135
+ */
136
+ export declare function formatPublishTimeoutError({ starved, starvedMs, linkLost, leftSendQueue, }: PublishTimeoutVerdict): string;
43
137
  export declare function isRetryablePublishError(error: unknown): boolean;
44
138
  export declare function computeAmqpPublishRetryDelay(attempt: number): number;
139
+ export {};
45
140
  //# sourceMappingURL=amqpPublishRetry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"amqpPublishRetry.d.ts","sourceRoot":"","sources":["../../../../../../helpers/src/utils/amqpPublishRetry.ts"],"names":[],"mappings":"AA8BA,eAAO,MAAM,+BAA+B,QAAS,CAAA;AACrD,eAAO,MAAM,mCAAmC,IAAI,CAAA;AAqBpD;;;;;;;;;GASG;AACH,eAAO,MAAM,+BAA+B,OAAQ,CAAA;AAEpD,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,IAAI,EAAE,MAAM,MAAM,CAAA;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,GAA6B,GAC9B,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAAO,GAAG,mBAAmB,CAyBnD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,WAExD;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,WAQrD;AAID,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,MAAM,UAM3D"}
1
+ {"version":3,"file":"amqpPublishRetry.d.ts","sourceRoot":"","sources":["../../../../../../helpers/src/utils/amqpPublishRetry.ts"],"names":[],"mappings":"AA8BA,eAAO,MAAM,+BAA+B,QAAS,CAAA;AACrD,eAAO,MAAM,mCAAmC,IAAI,CAAA;AAoEpD;;;;;;;;;GASG;AACH,eAAO,MAAM,+BAA+B,OAAQ,CAAA;AAEpD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAA;AAEzC;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,MAAM,GAAG,IAAI,CAAA;AAElD,MAAM,WAAW,kBAAkB;IACjC,+CAA+C;IAC/C,KAAK,EAAE,gBAAgB,CAAA;IACvB,iEAAiE;IACjE,aAAa,EAAE,MAAM,IAAI,CAAA;IACzB,+DAA+D;IAC/D,gBAAgB,EAAE,MAAM,IAAI,CAAA;CAC7B;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,IAAI,kBAAkB,CAc7D;AAED,MAAM,WAAW,qBAAqB;IACpC,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAA;IACjB,iEAAiE;IACjE,OAAO,EAAE,OAAO,CAAA;IAChB,yEAAyE;IACzE,QAAQ,EAAE,OAAO,CAAA;IACjB;;;;OAIG;IACH,aAAa,EAAE,OAAO,CAAA;IACtB;;;;OAIG;IACH,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B,8DAA8D;IAC9D,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB;;;;OAIG;IACH,kBAAkB,EAAE,MAAM,OAAO,CAAC,qBAAqB,CAAC,CAAA;CACzD;AAED,UAAU,yBAAyB;IACjC,KAAK,EAAE,gBAAgB,CAAA;IACvB;;;;OAIG;IACH,eAAe,EAAE,MAAM,MAAM,CAAA;IAC7B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,KAAK,EACL,eAAe,EACf,GAA6B,EAC7B,QAAiC,GAClC,EAAE,yBAAyB,GAAG,kBAAkB,CAuEhD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,WAExD;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,OAAO,EACP,SAAS,EACT,QAAQ,EACR,aAAa,GACd,EAAE,qBAAqB,UAevB;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,WAQrD;AAID,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,MAAM,UAM3D"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * THE SHARED YIELD RULE FOR LONG SYNCHRONOUS WORK (#134).
3
+ *
4
+ * A loop that does real CPU per item blocks every callback for its whole
5
+ * length — the AMQP heartbeat included. On 2026-08-07 that took the operator's
6
+ * connection down and lost every publish issued inside the dead window (#130).
7
+ * The fix in every such loop is the same shape: after each item, ask whether the
8
+ * current slice has run long enough, and if it has, hand the loop back.
9
+ *
10
+ * This is that rule, once. Two loops use it — `protocol/sliceTreeList.ts` (tree
11
+ * validation, the expensive one) and the operator's `machinePacker.unpack`
12
+ * (snapshot restore) — and any third should use it rather than re-time itself.
13
+ *
14
+ * It does NOT make work interruptible. A slice always overruns by the cost of
15
+ * the item that tripped it, because the item itself — an AJV compile, a msgpack
16
+ * decode — cannot be split part-way. The bound is `budgetMs` plus the single
17
+ * most expensive item, which is why the budget is set two orders of magnitude
18
+ * below the threshold anyone cares about.
19
+ */
20
+ /**
21
+ * How long one uninterrupted slice may run before the loop yields.
22
+ *
23
+ * TIME, not a fixed item count, and that is deliberate: #134 asked for "every N
24
+ * items" picked by measurement, but N cannot hold the invariant that actually
25
+ * matters. Item cost spans four orders of magnitude across the callers — an edge
26
+ * validates in 0.004 ms, a service node with three JSON schemas in ~12 ms, a
27
+ * 20 kB snapshot payload in 0.01 ms — so any N tuned for one caller is wrong for
28
+ * the next. A time budget is the same rule expressed on the axis the loop cares
29
+ * about, and it makes N adaptive for free.
30
+ *
31
+ * 20 ms is two orders of magnitude below `LAG_WARN_THRESHOLD` (1000 ms in the
32
+ * operator's `utils/eventLoopLag.ts`), which leaves room for the overrun above.
33
+ */
34
+ export declare const SLICE_BUDGET_MS = 20;
35
+ export type SliceOptions = {
36
+ /** @see SLICE_BUDGET_MS */
37
+ budgetMs?: number;
38
+ /**
39
+ * How the loop hands control back. Injectable so a spec can count the yields
40
+ * instead of waiting for them.
41
+ */
42
+ yieldToEventLoop?: () => Promise<void>;
43
+ };
44
+ export type SliceStats = {
45
+ /** How many uninterrupted slices the work took. */
46
+ slices: number;
47
+ /** The longest of them, in ms — the stall the work actually caused. */
48
+ longestSliceMs: number;
49
+ };
50
+ /**
51
+ * The clock a sliced loop yields on. It measures the slice it is IN, so the
52
+ * reported worst slice includes the item that overran the budget — the honest
53
+ * number.
54
+ */
55
+ export declare function createSliceClock(options?: SliceOptions): {
56
+ /** Called after every item. Yields only when the budget is spent. */
57
+ tick(): Promise<void>;
58
+ stats(): SliceStats;
59
+ };
60
+ export type SliceClock = ReturnType<typeof createSliceClock>;
61
+ //# sourceMappingURL=sliceClock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sliceClock.d.ts","sourceRoot":"","sources":["../../../../../../helpers/src/utils/sliceClock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,KAAK,CAAA;AAEjC,MAAM,MAAM,YAAY,GAAG;IACzB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACvC,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAA;IACd,uEAAuE;IACvE,cAAc,EAAE,MAAM,CAAA;CACvB,CAAA;AAyBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,YAAiB;IASvD,qEAAqE;IAC/D,IAAI;IAWV,KAAK,IAAI,UAAU;EAOtB;AAED,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@requence/service",
3
3
  "type": "module",
4
- "version": "0.8.2-edge.1151",
4
+ "version": "0.8.2-edge.1154",
5
5
  "bin": {
6
6
  "requence-service": "build/cli.js"
7
7
  },