@znight/messaging 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +622 -2
- package/dist/async-CFh_bvex.mjs +216 -0
- package/dist/async-CFh_bvex.mjs.map +1 -0
- package/dist/codec-Dku-NXXU.mjs +83 -0
- package/dist/codec-Dku-NXXU.mjs.map +1 -0
- package/dist/compiler.d.mts +75 -0
- package/dist/compiler.mjs +1023 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/driver-ZxLqy_Ap.d.mts +216 -0
- package/dist/index.d.mts +459 -0
- package/dist/index.mjs +870 -0
- package/dist/index.mjs.map +1 -0
- package/dist/kafka.d.mts +51 -0
- package/dist/kafka.mjs +517 -0
- package/dist/kafka.mjs.map +1 -0
- package/dist/memory.d.mts +124 -0
- package/dist/memory.mjs +429 -0
- package/dist/memory.mjs.map +1 -0
- package/dist/rabbitmq.d.mts +65 -0
- package/dist/rabbitmq.mjs +686 -0
- package/dist/rabbitmq.mjs.map +1 -0
- package/dist/shared-BJBpLS8V.mjs +52 -0
- package/dist/shared-BJBpLS8V.mjs.map +1 -0
- package/dist/sqs.d.mts +38 -0
- package/dist/sqs.mjs +421 -0
- package/dist/sqs.mjs.map +1 -0
- package/package.json +123 -4
- package/src/broker.ts +156 -0
- package/src/codec.ts +98 -0
- package/src/compiler/analyze.ts +800 -0
- package/src/compiler/constants.ts +73 -0
- package/src/compiler/data.ts +75 -0
- package/src/compiler/generate.ts +131 -0
- package/src/compiler/index.ts +52 -0
- package/src/compiler/patterns.ts +54 -0
- package/src/compiler/retry.ts +136 -0
- package/src/compiler/validate.ts +57 -0
- package/src/compiler/values.ts +76 -0
- package/src/consumers.ts +260 -0
- package/src/decorators.ts +131 -0
- package/src/driver.ts +142 -0
- package/src/drivers/kafka.ts +765 -0
- package/src/drivers/memory.ts +609 -0
- package/src/drivers/rabbitmq.ts +944 -0
- package/src/drivers/shared.ts +81 -0
- package/src/drivers/sqs.ts +583 -0
- package/src/errors.ts +95 -0
- package/src/headers.ts +70 -0
- package/src/hooks.ts +2 -0
- package/src/index.ts +89 -0
- package/src/internal/async.ts +115 -0
- package/src/message.ts +96 -0
- package/src/metadata.ts +65 -0
- package/src/plugin.ts +236 -0
- package/src/retry.ts +129 -0
- package/src/runtime.ts +424 -0
- package/src/standard-schema.ts +68 -0
- package/src/tokens.ts +14 -0
- package/src/validation.ts +61 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { c as SubscribeOptions, f as OutboundMessage, i as DriverContext, l as Subscription, n as DeliveryHandler, o as MessagingDriver, r as DriverCapabilities, u as DeadLetterReason } from "./driver-ZxLqy_Ap.mjs";
|
|
2
|
+
/** Options of {@link memory}. */
|
|
3
|
+
export interface MemoryDriverOptions {
|
|
4
|
+
/** Longest retry delay waited in-process. Default: 1000 ms. */
|
|
5
|
+
readonly maxInProcessDelayMs?: number;
|
|
6
|
+
/** Whether longer delays are scheduled by the driver (timers). Default: `true`. */
|
|
7
|
+
readonly nativeDelay?: boolean;
|
|
8
|
+
/** Longest native delay. Default: unlimited. */
|
|
9
|
+
readonly maxNativeDelayMs?: number;
|
|
10
|
+
/** Longest `publish(..., { delay })`. Default: unlimited. */
|
|
11
|
+
readonly maxPublishDelayMs?: number;
|
|
12
|
+
/** Dead-letter naming. Default: `${queue}.dlq`. */
|
|
13
|
+
readonly deadLetterName?: (queue: string) => string;
|
|
14
|
+
}
|
|
15
|
+
/** A message stored by the memory broker. */
|
|
16
|
+
export interface MemoryMessage {
|
|
17
|
+
readonly queue: string;
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly type: string | undefined;
|
|
20
|
+
readonly key: string | undefined;
|
|
21
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
22
|
+
readonly body: Uint8Array | string;
|
|
23
|
+
readonly contentType: string | undefined;
|
|
24
|
+
readonly timestamp: number;
|
|
25
|
+
/** Deliveries made so far (the next delivery is attempt `deliveries + 1`). */
|
|
26
|
+
readonly deliveries: number;
|
|
27
|
+
/** The body decoded with the JSON codec (`undefined` when it cannot be decoded). */
|
|
28
|
+
readonly payload: unknown;
|
|
29
|
+
}
|
|
30
|
+
/** A settlement performed by a consumer, in order. */
|
|
31
|
+
export interface MemorySettlement {
|
|
32
|
+
readonly queue: string;
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly attempt: number;
|
|
35
|
+
readonly action: "ack" | "retry" | "release" | "dead-letter" | "reject";
|
|
36
|
+
/** Retry delay requested (native delays only; in-process delays happen before a `retry` with delay 0). */
|
|
37
|
+
readonly delayMs?: number;
|
|
38
|
+
readonly reason?: DeadLetterReason;
|
|
39
|
+
readonly error?: unknown;
|
|
40
|
+
}
|
|
41
|
+
/** A dead-lettered message. */
|
|
42
|
+
export interface MemoryDeadLetter {
|
|
43
|
+
/** Dead-letter queue (`undefined` when dead-lettering was disabled: the message was discarded). */
|
|
44
|
+
readonly queue: string | undefined;
|
|
45
|
+
readonly originalQueue: string;
|
|
46
|
+
readonly message: MemoryMessage;
|
|
47
|
+
readonly reason: DeadLetterReason;
|
|
48
|
+
readonly error: unknown;
|
|
49
|
+
readonly attempt: number;
|
|
50
|
+
}
|
|
51
|
+
/** A message injected with {@link MemoryDriver.send}, as an external producer would. */
|
|
52
|
+
export interface MemorySendOptions {
|
|
53
|
+
/** Payload encoded with the JSON codec (ignored when `body` is given). */
|
|
54
|
+
readonly payload?: unknown;
|
|
55
|
+
/** Raw body (e.g. malformed JSON). */
|
|
56
|
+
readonly body?: Uint8Array | string;
|
|
57
|
+
readonly contentType?: string;
|
|
58
|
+
readonly type?: string;
|
|
59
|
+
readonly key?: string;
|
|
60
|
+
readonly id?: string;
|
|
61
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
62
|
+
/** Deliveries already made (simulates a redelivered message). Default: 0. */
|
|
63
|
+
readonly deliveries?: number;
|
|
64
|
+
}
|
|
65
|
+
/** Operations whose failure can be injected with {@link MemoryDriver.fail}. */
|
|
66
|
+
export type MemoryOperation = "connect" | "subscribe" | "publish" | "ack" | "retry" | "dead-letter" | "release";
|
|
67
|
+
/** Options of {@link MemoryDriver.fail}. */
|
|
68
|
+
export interface MemoryFailureOptions {
|
|
69
|
+
/** Only for this queue (the destination queue for `publish`, the consumed queue otherwise). */
|
|
70
|
+
readonly queue?: string;
|
|
71
|
+
/** Number of failures before the operation works again. Default: unlimited. */
|
|
72
|
+
readonly times?: number;
|
|
73
|
+
/** Thrown error. Default: an `Error` naming the operation. */
|
|
74
|
+
readonly error?: unknown;
|
|
75
|
+
}
|
|
76
|
+
/** Creates a {@link MemoryDriver}. */
|
|
77
|
+
export declare function memory(options?: MemoryDriverOptions): MemoryDriver;
|
|
78
|
+
/** In-memory {@link MessagingDriver}. See the module documentation. */
|
|
79
|
+
export declare class MemoryDriver implements MessagingDriver {
|
|
80
|
+
#private;
|
|
81
|
+
readonly name = "memory";
|
|
82
|
+
readonly capabilities: DriverCapabilities;
|
|
83
|
+
constructor(options?: MemoryDriverOptions);
|
|
84
|
+
/** Whether `connect()` was called and `close()` was not. */
|
|
85
|
+
get connected(): boolean;
|
|
86
|
+
connect(_context: DriverContext): Promise<void>;
|
|
87
|
+
close(): Promise<void>;
|
|
88
|
+
deadLetterName(queue: string): string;
|
|
89
|
+
publish(queue: string, message: OutboundMessage): Promise<void>;
|
|
90
|
+
subscribe(queue: string, handler: DeliveryHandler, options: SubscribeOptions): Promise<Subscription>;
|
|
91
|
+
/**
|
|
92
|
+
* Injects a message into `queue` as an external producer would (no codec involved when `body` is
|
|
93
|
+
* given). Returns the message id.
|
|
94
|
+
*/
|
|
95
|
+
send(queue: string, options?: MemorySendOptions): string;
|
|
96
|
+
/** Messages published through `publish()` (the `MessageBroker`), optionally for one queue. */
|
|
97
|
+
published(queue?: string): MemoryMessage[];
|
|
98
|
+
/** Messages waiting in `queue` (not delivered yet, delayed messages excluded). */
|
|
99
|
+
pending(queue: string): MemoryMessage[];
|
|
100
|
+
/** Settlements performed by consumers, in order, optionally for one consumed queue. */
|
|
101
|
+
settlements(queue?: string): MemorySettlement[];
|
|
102
|
+
/** Dead-lettered (or discarded) messages, optionally for one consumed queue. */
|
|
103
|
+
deadLetters(queue?: string): MemoryDeadLetter[];
|
|
104
|
+
/** Deliveries currently handed to consumers and not finished. */
|
|
105
|
+
get inFlight(): number;
|
|
106
|
+
/** Whether nothing is in flight, scheduled or deliverable. */
|
|
107
|
+
isIdle(): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Resolves once nothing is in flight, scheduled (delayed messages, retries) or deliverable to an
|
|
110
|
+
* active subscriber. Rejects after `timeoutMs` (default 5 s).
|
|
111
|
+
*/
|
|
112
|
+
idle(timeoutMs?: number): Promise<void>;
|
|
113
|
+
/** Polls `predicate` until it returns `true`; rejects after `timeoutMs` (default 5 s). */
|
|
114
|
+
waitFor(predicate: () => boolean, timeoutMs?: number): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Makes an operation fail (for tests): `publish` (also dead-letter publications, by destination
|
|
117
|
+
* queue), `ack`, `retry`, `dead-letter`, `release` (by consumed queue), `subscribe`, `connect`.
|
|
118
|
+
* Returns a function removing the failure.
|
|
119
|
+
*/
|
|
120
|
+
fail(operation: MemoryOperation, options?: MemoryFailureOptions): () => void;
|
|
121
|
+
/** Forgets every message, settlement and failure (subscriptions are kept). */
|
|
122
|
+
reset(): void;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=memory.d.mts.map
|
package/dist/memory.mjs
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { g as brokerUnavailableError, l as headerRecord, t as MAX_TIMER_DELAY_MS } from "./async-CFh_bvex.mjs";
|
|
2
|
+
import { o as jsonCodec } from "./codec-Dku-NXXU.mjs";
|
|
3
|
+
import { n as deadLetterHeaders, r as durationOption } from "./shared-BJBpLS8V.mjs";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
// src/drivers/memory.ts
|
|
6
|
+
const DEFAULT_IN_PROCESS_DELAY_MS = 1e3;
|
|
7
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 5e3;
|
|
8
|
+
/** Creates a {@link MemoryDriver}. */
|
|
9
|
+
function memory(options = {}) {
|
|
10
|
+
return new MemoryDriver(options);
|
|
11
|
+
}
|
|
12
|
+
/** In-memory {@link MessagingDriver}. See the module documentation. */
|
|
13
|
+
var MemoryDriver = class {
|
|
14
|
+
name = "memory";
|
|
15
|
+
capabilities;
|
|
16
|
+
#deadLetterName;
|
|
17
|
+
#queues = /* @__PURE__ */ new Map();
|
|
18
|
+
#published = [];
|
|
19
|
+
#settlements = [];
|
|
20
|
+
#deadLetters = [];
|
|
21
|
+
#timers = /* @__PURE__ */ new Set();
|
|
22
|
+
#failures = [];
|
|
23
|
+
#idleWaiters = /* @__PURE__ */ new Set();
|
|
24
|
+
#inflight = 0;
|
|
25
|
+
#connected = false;
|
|
26
|
+
#closed = false;
|
|
27
|
+
#idleCheckScheduled = false;
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
const nativeDelay = options.nativeDelay ?? true;
|
|
30
|
+
this.capabilities = Object.freeze({
|
|
31
|
+
nativeDelay,
|
|
32
|
+
maxNativeDelayMs: nativeDelay ? durationOption(options.maxNativeDelayMs, Number.POSITIVE_INFINITY, "maxNativeDelayMs") : 0,
|
|
33
|
+
maxInProcessDelayMs: durationOption(options.maxInProcessDelayMs, DEFAULT_IN_PROCESS_DELAY_MS, "maxInProcessDelayMs"),
|
|
34
|
+
deliveryCount: "native",
|
|
35
|
+
maxPublishDelayMs: durationOption(options.maxPublishDelayMs, Number.POSITIVE_INFINITY, "maxPublishDelayMs")
|
|
36
|
+
});
|
|
37
|
+
this.#deadLetterName = options.deadLetterName ?? ((queue) => `${queue}.dlq`);
|
|
38
|
+
}
|
|
39
|
+
/** Whether `connect()` was called and `close()` was not. */
|
|
40
|
+
get connected() {
|
|
41
|
+
return this.#connected && !this.#closed;
|
|
42
|
+
}
|
|
43
|
+
async connect(_context) {
|
|
44
|
+
await Promise.resolve();
|
|
45
|
+
this.#maybeFail("connect", void 0);
|
|
46
|
+
this.#connected = true;
|
|
47
|
+
this.#closed = false;
|
|
48
|
+
}
|
|
49
|
+
async close() {
|
|
50
|
+
await Promise.resolve();
|
|
51
|
+
this.#closed = true;
|
|
52
|
+
for (const timer of this.#timers) clearTimeout(timer);
|
|
53
|
+
this.#timers.clear();
|
|
54
|
+
for (const state of this.#queues.values()) {
|
|
55
|
+
for (const subscriber of state.subscribers) subscriber.active = false;
|
|
56
|
+
state.subscribers.length = 0;
|
|
57
|
+
}
|
|
58
|
+
this.#checkIdle();
|
|
59
|
+
}
|
|
60
|
+
deadLetterName(queue) {
|
|
61
|
+
return this.#deadLetterName(queue);
|
|
62
|
+
}
|
|
63
|
+
async publish(queue, message) {
|
|
64
|
+
await Promise.resolve();
|
|
65
|
+
this.#assertOpen(`publish to "${queue}"`);
|
|
66
|
+
this.#maybeFail("publish", queue);
|
|
67
|
+
const stored = {
|
|
68
|
+
queue,
|
|
69
|
+
id: message.id,
|
|
70
|
+
type: message.type,
|
|
71
|
+
key: message.key,
|
|
72
|
+
headers: { ...message.headers },
|
|
73
|
+
body: message.body,
|
|
74
|
+
contentType: message.contentType,
|
|
75
|
+
timestamp: message.timestamp,
|
|
76
|
+
deliveries: 0
|
|
77
|
+
};
|
|
78
|
+
this.#published.push(snapshot(stored));
|
|
79
|
+
if (message.delayMs > 0) this.#schedule(() => this.#enqueue(stored, "tail"), message.delayMs);
|
|
80
|
+
else this.#enqueue(stored, "tail");
|
|
81
|
+
}
|
|
82
|
+
async subscribe(queue, handler, options) {
|
|
83
|
+
await Promise.resolve();
|
|
84
|
+
this.#assertOpen(`subscribe to "${queue}"`);
|
|
85
|
+
this.#maybeFail("subscribe", queue);
|
|
86
|
+
const state = this.#queue(queue);
|
|
87
|
+
const subscriber = {
|
|
88
|
+
queue,
|
|
89
|
+
handler,
|
|
90
|
+
concurrency: options.concurrency,
|
|
91
|
+
options,
|
|
92
|
+
inflight: 0,
|
|
93
|
+
active: true
|
|
94
|
+
};
|
|
95
|
+
state.subscribers.push(subscriber);
|
|
96
|
+
this.#pump(queue);
|
|
97
|
+
return {
|
|
98
|
+
cancel: async () => {
|
|
99
|
+
await Promise.resolve();
|
|
100
|
+
subscriber.active = false;
|
|
101
|
+
this.#checkIdle();
|
|
102
|
+
},
|
|
103
|
+
close: async () => {
|
|
104
|
+
await Promise.resolve();
|
|
105
|
+
subscriber.active = false;
|
|
106
|
+
const index = state.subscribers.indexOf(subscriber);
|
|
107
|
+
if (index !== -1) state.subscribers.splice(index, 1);
|
|
108
|
+
this.#checkIdle();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Injects a message into `queue` as an external producer would (no codec involved when `body` is
|
|
114
|
+
* given). Returns the message id.
|
|
115
|
+
*/
|
|
116
|
+
send(queue, options = {}) {
|
|
117
|
+
this.#assertOpen(`send to "${queue}"`);
|
|
118
|
+
const encoded = options.body === void 0 ? jsonCodec.encode(options.payload, options.contentType) : {
|
|
119
|
+
body: options.body,
|
|
120
|
+
contentType: options.contentType
|
|
121
|
+
};
|
|
122
|
+
const stored = {
|
|
123
|
+
queue,
|
|
124
|
+
id: options.id ?? randomUUID(),
|
|
125
|
+
type: options.type,
|
|
126
|
+
key: options.key,
|
|
127
|
+
headers: { ...options.headers ?? {} },
|
|
128
|
+
body: encoded.body,
|
|
129
|
+
contentType: encoded.contentType,
|
|
130
|
+
timestamp: Date.now(),
|
|
131
|
+
deliveries: options.deliveries ?? 0
|
|
132
|
+
};
|
|
133
|
+
this.#enqueue(stored, "tail");
|
|
134
|
+
return stored.id;
|
|
135
|
+
}
|
|
136
|
+
/** Messages published through `publish()` (the `MessageBroker`), optionally for one queue. */
|
|
137
|
+
published(queue) {
|
|
138
|
+
return this.#published.filter((message) => queue === void 0 || message.queue === queue);
|
|
139
|
+
}
|
|
140
|
+
/** Messages waiting in `queue` (not delivered yet, delayed messages excluded). */
|
|
141
|
+
pending(queue) {
|
|
142
|
+
return (this.#queues.get(queue)?.ready ?? []).map(snapshot);
|
|
143
|
+
}
|
|
144
|
+
/** Settlements performed by consumers, in order, optionally for one consumed queue. */
|
|
145
|
+
settlements(queue) {
|
|
146
|
+
return this.#settlements.filter((settlement) => queue === void 0 || settlement.queue === queue);
|
|
147
|
+
}
|
|
148
|
+
/** Dead-lettered (or discarded) messages, optionally for one consumed queue. */
|
|
149
|
+
deadLetters(queue) {
|
|
150
|
+
return this.#deadLetters.filter((entry) => queue === void 0 || entry.originalQueue === queue);
|
|
151
|
+
}
|
|
152
|
+
/** Deliveries currently handed to consumers and not finished. */
|
|
153
|
+
get inFlight() {
|
|
154
|
+
return this.#inflight;
|
|
155
|
+
}
|
|
156
|
+
/** Whether nothing is in flight, scheduled or deliverable. */
|
|
157
|
+
isIdle() {
|
|
158
|
+
if (this.#inflight > 0 || this.#timers.size > 0) return false;
|
|
159
|
+
for (const state of this.#queues.values()) {
|
|
160
|
+
if (state.scheduled) return false;
|
|
161
|
+
if (state.ready.length > 0 && state.subscribers.some((s) => s.active && s.inflight < s.concurrency)) return false;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Resolves once nothing is in flight, scheduled (delayed messages, retries) or deliverable to an
|
|
167
|
+
* active subscriber. Rejects after `timeoutMs` (default 5 s).
|
|
168
|
+
*/
|
|
169
|
+
idle(timeoutMs = DEFAULT_IDLE_TIMEOUT_MS) {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const timer = setTimeout(() => {
|
|
172
|
+
this.#idleWaiters.delete(done);
|
|
173
|
+
reject(/* @__PURE__ */ new Error(`[znight] MemoryDriver.idle(): still busy after ${timeoutMs} ms (${this.#inflight} in flight)`));
|
|
174
|
+
}, timeoutMs);
|
|
175
|
+
const done = () => {
|
|
176
|
+
clearTimeout(timer);
|
|
177
|
+
resolve();
|
|
178
|
+
};
|
|
179
|
+
this.#idleWaiters.add(done);
|
|
180
|
+
this.#checkIdle();
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/** Polls `predicate` until it returns `true`; rejects after `timeoutMs` (default 5 s). */
|
|
184
|
+
async waitFor(predicate, timeoutMs = DEFAULT_IDLE_TIMEOUT_MS) {
|
|
185
|
+
const deadline = Date.now() + timeoutMs;
|
|
186
|
+
while (!predicate()) {
|
|
187
|
+
if (Date.now() > deadline) throw new Error(`[znight] MemoryDriver.waitFor(): condition not met after ${timeoutMs} ms`);
|
|
188
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Makes an operation fail (for tests): `publish` (also dead-letter publications, by destination
|
|
193
|
+
* queue), `ack`, `retry`, `dead-letter`, `release` (by consumed queue), `subscribe`, `connect`.
|
|
194
|
+
* Returns a function removing the failure.
|
|
195
|
+
*/
|
|
196
|
+
fail(operation, options = {}) {
|
|
197
|
+
const failure = {
|
|
198
|
+
operation,
|
|
199
|
+
queue: options.queue,
|
|
200
|
+
remaining: options.times ?? Number.POSITIVE_INFINITY,
|
|
201
|
+
error: options.error ?? /* @__PURE__ */ new Error(`[memory] injected ${operation} failure`)
|
|
202
|
+
};
|
|
203
|
+
this.#failures.push(failure);
|
|
204
|
+
return () => {
|
|
205
|
+
const index = this.#failures.indexOf(failure);
|
|
206
|
+
if (index !== -1) this.#failures.splice(index, 1);
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/** Forgets every message, settlement and failure (subscriptions are kept). */
|
|
210
|
+
reset() {
|
|
211
|
+
for (const timer of this.#timers) clearTimeout(timer);
|
|
212
|
+
this.#timers.clear();
|
|
213
|
+
for (const state of this.#queues.values()) state.ready.length = 0;
|
|
214
|
+
this.#published.length = 0;
|
|
215
|
+
this.#settlements.length = 0;
|
|
216
|
+
this.#deadLetters.length = 0;
|
|
217
|
+
this.#failures.length = 0;
|
|
218
|
+
this.#checkIdle();
|
|
219
|
+
}
|
|
220
|
+
#queue(name) {
|
|
221
|
+
let state = this.#queues.get(name);
|
|
222
|
+
if (state === void 0) {
|
|
223
|
+
state = {
|
|
224
|
+
ready: [],
|
|
225
|
+
subscribers: [],
|
|
226
|
+
next: 0,
|
|
227
|
+
scheduled: false
|
|
228
|
+
};
|
|
229
|
+
this.#queues.set(name, state);
|
|
230
|
+
}
|
|
231
|
+
return state;
|
|
232
|
+
}
|
|
233
|
+
#enqueue(message, position) {
|
|
234
|
+
const state = this.#queue(message.queue);
|
|
235
|
+
if (position === "head") state.ready.unshift(message);
|
|
236
|
+
else state.ready.push(message);
|
|
237
|
+
this.#pump(message.queue);
|
|
238
|
+
}
|
|
239
|
+
/** Runs `fn` after `delayMs` (any duration: a timer waits at most 2^31 - 1 ms, longer delays re-arm). */
|
|
240
|
+
#schedule(fn, delayMs) {
|
|
241
|
+
const wait = Math.min(delayMs, MAX_TIMER_DELAY_MS);
|
|
242
|
+
const timer = setTimeout(() => {
|
|
243
|
+
this.#timers.delete(timer);
|
|
244
|
+
if (delayMs > wait) return this.#schedule(fn, delayMs - wait);
|
|
245
|
+
fn();
|
|
246
|
+
this.#checkIdle();
|
|
247
|
+
}, wait);
|
|
248
|
+
this.#timers.add(timer);
|
|
249
|
+
}
|
|
250
|
+
#pump(queue) {
|
|
251
|
+
const state = this.#queue(queue);
|
|
252
|
+
if (state.scheduled) return;
|
|
253
|
+
state.scheduled = true;
|
|
254
|
+
queueMicrotask(() => {
|
|
255
|
+
state.scheduled = false;
|
|
256
|
+
this.#dispatch(state);
|
|
257
|
+
this.#checkIdle();
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
#dispatch(state) {
|
|
261
|
+
while (state.ready.length > 0) {
|
|
262
|
+
const subscriber = this.#nextSubscriber(state);
|
|
263
|
+
if (subscriber === void 0) return;
|
|
264
|
+
const message = state.ready.shift();
|
|
265
|
+
this.#deliver(subscriber, message);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
#nextSubscriber(state) {
|
|
269
|
+
const count = state.subscribers.length;
|
|
270
|
+
for (let offset = 0; offset < count; offset++) {
|
|
271
|
+
const index = (state.next + offset) % count;
|
|
272
|
+
const subscriber = state.subscribers[index];
|
|
273
|
+
if (subscriber.active && subscriber.inflight < subscriber.concurrency) {
|
|
274
|
+
state.next = (index + 1) % count;
|
|
275
|
+
return subscriber;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
#deliver(subscriber, message) {
|
|
280
|
+
message.deliveries++;
|
|
281
|
+
subscriber.inflight++;
|
|
282
|
+
this.#inflight++;
|
|
283
|
+
const attempt = message.deliveries;
|
|
284
|
+
let settled = false;
|
|
285
|
+
/** Validates a settlement before any side effect (already settled, closed driver, injected failure). */
|
|
286
|
+
const check = (action) => {
|
|
287
|
+
if (settled) throw new Error(`[memory] message ${message.id} was already settled (${action})`);
|
|
288
|
+
this.#assertOpen(`${action} message ${message.id}`);
|
|
289
|
+
this.#maybeFail(action, message.queue);
|
|
290
|
+
};
|
|
291
|
+
const record = (settlement) => {
|
|
292
|
+
this.#settlements.push({
|
|
293
|
+
queue: message.queue,
|
|
294
|
+
id: message.id,
|
|
295
|
+
attempt,
|
|
296
|
+
...settlement
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
const delivery = {
|
|
300
|
+
id: message.id,
|
|
301
|
+
body: message.body,
|
|
302
|
+
contentType: message.contentType,
|
|
303
|
+
headers: message.headers,
|
|
304
|
+
type: message.type,
|
|
305
|
+
key: message.key,
|
|
306
|
+
attempt,
|
|
307
|
+
redelivered: attempt > 1,
|
|
308
|
+
timestamp: message.timestamp,
|
|
309
|
+
raw: snapshot(message),
|
|
310
|
+
ack: async () => {
|
|
311
|
+
await Promise.resolve();
|
|
312
|
+
check("ack");
|
|
313
|
+
settled = true;
|
|
314
|
+
record({ action: "ack" });
|
|
315
|
+
},
|
|
316
|
+
retry: async (request) => {
|
|
317
|
+
await Promise.resolve();
|
|
318
|
+
check("retry");
|
|
319
|
+
settled = true;
|
|
320
|
+
record({
|
|
321
|
+
action: "retry",
|
|
322
|
+
delayMs: request.delayMs,
|
|
323
|
+
error: request.error
|
|
324
|
+
});
|
|
325
|
+
if (request.delayMs > 0) this.#schedule(() => this.#enqueue(message, "tail"), request.delayMs);
|
|
326
|
+
else this.#enqueue(message, "tail");
|
|
327
|
+
},
|
|
328
|
+
deadLetter: async (request) => {
|
|
329
|
+
await Promise.resolve();
|
|
330
|
+
check("dead-letter");
|
|
331
|
+
const queue = request.queue;
|
|
332
|
+
if (queue !== void 0) {
|
|
333
|
+
this.#maybeFail("publish", queue);
|
|
334
|
+
const copy = {
|
|
335
|
+
...message,
|
|
336
|
+
queue,
|
|
337
|
+
headers: deadLetterHeaders(message.headers, request, message.queue),
|
|
338
|
+
deliveries: 0
|
|
339
|
+
};
|
|
340
|
+
this.#enqueue(copy, "tail");
|
|
341
|
+
}
|
|
342
|
+
settled = true;
|
|
343
|
+
record({
|
|
344
|
+
action: queue === void 0 ? "reject" : "dead-letter",
|
|
345
|
+
reason: request.reason,
|
|
346
|
+
error: request.error
|
|
347
|
+
});
|
|
348
|
+
this.#deadLetters.push({
|
|
349
|
+
queue,
|
|
350
|
+
originalQueue: message.queue,
|
|
351
|
+
message: snapshot(message),
|
|
352
|
+
reason: request.reason,
|
|
353
|
+
error: request.error,
|
|
354
|
+
attempt: request.attempt
|
|
355
|
+
});
|
|
356
|
+
},
|
|
357
|
+
release: async () => {
|
|
358
|
+
await Promise.resolve();
|
|
359
|
+
check("release");
|
|
360
|
+
settled = true;
|
|
361
|
+
record({ action: "release" });
|
|
362
|
+
this.#enqueue(message, "head");
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const finish = () => {
|
|
366
|
+
subscriber.inflight--;
|
|
367
|
+
this.#inflight--;
|
|
368
|
+
if (!settled && !this.#closed) {
|
|
369
|
+
settled = true;
|
|
370
|
+
record({ action: "release" });
|
|
371
|
+
this.#enqueue(message, "head");
|
|
372
|
+
}
|
|
373
|
+
this.#pump(message.queue);
|
|
374
|
+
this.#checkIdle();
|
|
375
|
+
};
|
|
376
|
+
let result;
|
|
377
|
+
try {
|
|
378
|
+
result = subscriber.handler(delivery);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
result = Promise.reject(error);
|
|
381
|
+
}
|
|
382
|
+
Promise.resolve(result).then(finish, finish);
|
|
383
|
+
}
|
|
384
|
+
#maybeFail(operation, queue) {
|
|
385
|
+
for (const failure of this.#failures) {
|
|
386
|
+
if (failure.operation !== operation || failure.remaining <= 0) continue;
|
|
387
|
+
if (failure.queue !== void 0 && failure.queue !== queue) continue;
|
|
388
|
+
failure.remaining--;
|
|
389
|
+
throw failure.error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
#assertOpen(action) {
|
|
393
|
+
if (this.#closed) throw brokerUnavailableError(`[memory] Cannot ${action}: the driver is closed`);
|
|
394
|
+
}
|
|
395
|
+
#checkIdle() {
|
|
396
|
+
if (this.#idleWaiters.size === 0 || this.#idleCheckScheduled) return;
|
|
397
|
+
this.#idleCheckScheduled = true;
|
|
398
|
+
queueMicrotask(() => {
|
|
399
|
+
this.#idleCheckScheduled = false;
|
|
400
|
+
if (!this.isIdle()) return;
|
|
401
|
+
const waiters = [...this.#idleWaiters];
|
|
402
|
+
this.#idleWaiters.clear();
|
|
403
|
+
for (const waiter of waiters) waiter();
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
function snapshot(message) {
|
|
408
|
+
let payload;
|
|
409
|
+
try {
|
|
410
|
+
payload = jsonCodec.decode(message.body, message.contentType);
|
|
411
|
+
} catch {
|
|
412
|
+
payload = void 0;
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
queue: message.queue,
|
|
416
|
+
id: message.id,
|
|
417
|
+
type: message.type,
|
|
418
|
+
key: message.key,
|
|
419
|
+
headers: Object.assign(headerRecord(), message.headers),
|
|
420
|
+
body: message.body,
|
|
421
|
+
contentType: message.contentType,
|
|
422
|
+
timestamp: message.timestamp,
|
|
423
|
+
deliveries: message.deliveries,
|
|
424
|
+
payload
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
export { MemoryDriver, memory };
|
|
428
|
+
|
|
429
|
+
//# sourceMappingURL=memory.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.mjs","names":[],"sources":["../src/drivers/memory.ts"],"sourcesContent":["/**\n * `@znight/messaging/memory`: an in-process broker for tests and local development. Deterministic\n * (FIFO queues, round-robin between competing subscribers, microtask dispatch, timers only for\n * delays), full featured (delays, native delivery counts, dead-letter queues, competing consumers)\n * and inspectable (published messages, settlements, dead letters, `idle()`, failure injection).\n *\n * ```ts\n * const driver = memory()\n * const app = await Application.create({ metadata, plugins: [MessagingPlugin.configure({ driver })] })\n * await app.start()\n * await app.get(MessageBroker).publish('orders', { id: 1 }, { type: 'order.created' })\n * await driver.idle()\n * expect(driver.settlements('orders')).toMatchObject([{ action: 'ack' }])\n * ```\n */\nimport type {\n DeadLetterRequest,\n DeliveryHandler,\n DriverCapabilities,\n DriverContext,\n DriverDelivery,\n MessagingDriver,\n RetryRequest,\n SubscribeOptions,\n Subscription,\n} from '../driver.ts'\nimport type { DeadLetterReason, OutboundMessage } from '../message.ts'\n\nimport { randomUUID } from 'node:crypto'\n\nimport { jsonCodec } from '../codec.ts'\nimport { brokerUnavailableError } from '../errors.ts'\nimport { headerRecord } from '../headers.ts'\nimport { MAX_TIMER_DELAY_MS } from '../internal/async.ts'\nimport { deadLetterHeaders, durationOption } from './shared.ts'\n\n/** Options of {@link memory}. */\nexport interface MemoryDriverOptions {\n /** Longest retry delay waited in-process. Default: 1000 ms. */\n readonly maxInProcessDelayMs?: number\n /** Whether longer delays are scheduled by the driver (timers). Default: `true`. */\n readonly nativeDelay?: boolean\n /** Longest native delay. Default: unlimited. */\n readonly maxNativeDelayMs?: number\n /** Longest `publish(..., { delay })`. Default: unlimited. */\n readonly maxPublishDelayMs?: number\n /** Dead-letter naming. Default: `${queue}.dlq`. */\n readonly deadLetterName?: (queue: string) => string\n}\n\n/** A message stored by the memory broker. */\nexport interface MemoryMessage {\n readonly queue: string\n readonly id: string\n readonly type: string | undefined\n readonly key: string | undefined\n readonly headers: Readonly<Record<string, string>>\n readonly body: Uint8Array | string\n readonly contentType: string | undefined\n readonly timestamp: number\n /** Deliveries made so far (the next delivery is attempt `deliveries + 1`). */\n readonly deliveries: number\n /** The body decoded with the JSON codec (`undefined` when it cannot be decoded). */\n readonly payload: unknown\n}\n\n/** A settlement performed by a consumer, in order. */\nexport interface MemorySettlement {\n readonly queue: string\n readonly id: string\n readonly attempt: number\n readonly action: 'ack' | 'retry' | 'release' | 'dead-letter' | 'reject'\n /** Retry delay requested (native delays only; in-process delays happen before a `retry` with delay 0). */\n readonly delayMs?: number\n readonly reason?: DeadLetterReason\n readonly error?: unknown\n}\n\n/** A dead-lettered message. */\nexport interface MemoryDeadLetter {\n /** Dead-letter queue (`undefined` when dead-lettering was disabled: the message was discarded). */\n readonly queue: string | undefined\n readonly originalQueue: string\n readonly message: MemoryMessage\n readonly reason: DeadLetterReason\n readonly error: unknown\n readonly attempt: number\n}\n\n/** A message injected with {@link MemoryDriver.send}, as an external producer would. */\nexport interface MemorySendOptions {\n /** Payload encoded with the JSON codec (ignored when `body` is given). */\n readonly payload?: unknown\n /** Raw body (e.g. malformed JSON). */\n readonly body?: Uint8Array | string\n readonly contentType?: string\n readonly type?: string\n readonly key?: string\n readonly id?: string\n readonly headers?: Readonly<Record<string, string>>\n /** Deliveries already made (simulates a redelivered message). Default: 0. */\n readonly deliveries?: number\n}\n\n/** Operations whose failure can be injected with {@link MemoryDriver.fail}. */\nexport type MemoryOperation = 'connect' | 'subscribe' | 'publish' | 'ack' | 'retry' | 'dead-letter' | 'release'\n\n/** Options of {@link MemoryDriver.fail}. */\nexport interface MemoryFailureOptions {\n /** Only for this queue (the destination queue for `publish`, the consumed queue otherwise). */\n readonly queue?: string\n /** Number of failures before the operation works again. Default: unlimited. */\n readonly times?: number\n /** Thrown error. Default: an `Error` naming the operation. */\n readonly error?: unknown\n}\n\ninterface StoredMessage {\n readonly queue: string\n readonly id: string\n readonly type: string | undefined\n readonly key: string | undefined\n readonly headers: Readonly<Record<string, string>>\n readonly body: Uint8Array | string\n readonly contentType: string | undefined\n readonly timestamp: number\n deliveries: number\n}\n\ninterface Subscriber {\n readonly queue: string\n readonly handler: DeliveryHandler\n readonly concurrency: number\n readonly options: SubscribeOptions\n inflight: number\n active: boolean\n}\n\ninterface QueueState {\n readonly ready: StoredMessage[]\n readonly subscribers: Subscriber[]\n next: number\n scheduled: boolean\n}\n\ninterface Failure {\n readonly operation: MemoryOperation\n readonly queue: string | undefined\n remaining: number\n readonly error: unknown\n}\n\nconst DEFAULT_IN_PROCESS_DELAY_MS = 1000\nconst DEFAULT_IDLE_TIMEOUT_MS = 5000\n\n/** Creates a {@link MemoryDriver}. */\nexport function memory(options: MemoryDriverOptions = {}): MemoryDriver {\n return new MemoryDriver(options)\n}\n\n/** In-memory {@link MessagingDriver}. See the module documentation. */\nexport class MemoryDriver implements MessagingDriver {\n readonly name = 'memory'\n readonly capabilities: DriverCapabilities\n readonly #deadLetterName: (queue: string) => string\n readonly #queues = new Map<string, QueueState>()\n readonly #published: MemoryMessage[] = []\n readonly #settlements: MemorySettlement[] = []\n readonly #deadLetters: MemoryDeadLetter[] = []\n readonly #timers = new Set<ReturnType<typeof setTimeout>>()\n readonly #failures: Failure[] = []\n readonly #idleWaiters = new Set<() => void>()\n #inflight = 0\n #connected = false\n #closed = false\n #idleCheckScheduled = false\n\n constructor(options: MemoryDriverOptions = {}) {\n const nativeDelay = options.nativeDelay ?? true\n this.capabilities = Object.freeze({\n nativeDelay,\n maxNativeDelayMs: nativeDelay\n ? durationOption(options.maxNativeDelayMs, Number.POSITIVE_INFINITY, 'maxNativeDelayMs')\n : 0,\n maxInProcessDelayMs: durationOption(\n options.maxInProcessDelayMs,\n DEFAULT_IN_PROCESS_DELAY_MS,\n 'maxInProcessDelayMs',\n ),\n deliveryCount: 'native',\n maxPublishDelayMs: durationOption(options.maxPublishDelayMs, Number.POSITIVE_INFINITY, 'maxPublishDelayMs'),\n })\n this.#deadLetterName = options.deadLetterName ?? (queue => `${queue}.dlq`)\n }\n\n /** Whether `connect()` was called and `close()` was not. */\n get connected(): boolean {\n return this.#connected && !this.#closed\n }\n\n // -----------------------------------------------------------------------------------------------\n // MessagingDriver\n // -----------------------------------------------------------------------------------------------\n\n async connect(_context: DriverContext): Promise<void> {\n await Promise.resolve()\n this.#maybeFail('connect', undefined)\n this.#connected = true\n this.#closed = false\n }\n\n async close(): Promise<void> {\n await Promise.resolve()\n this.#closed = true\n for (const timer of this.#timers) clearTimeout(timer)\n this.#timers.clear()\n for (const state of this.#queues.values()) {\n for (const subscriber of state.subscribers) subscriber.active = false\n state.subscribers.length = 0\n }\n this.#checkIdle()\n }\n\n deadLetterName(queue: string): string {\n return this.#deadLetterName(queue)\n }\n\n async publish(queue: string, message: OutboundMessage): Promise<void> {\n await Promise.resolve()\n this.#assertOpen(`publish to \"${queue}\"`)\n this.#maybeFail('publish', queue)\n const stored: StoredMessage = {\n queue,\n id: message.id,\n type: message.type,\n key: message.key,\n headers: { ...message.headers },\n body: message.body,\n contentType: message.contentType,\n timestamp: message.timestamp,\n deliveries: 0,\n }\n this.#published.push(snapshot(stored))\n if (message.delayMs > 0) this.#schedule(() => this.#enqueue(stored, 'tail'), message.delayMs)\n else this.#enqueue(stored, 'tail')\n }\n\n async subscribe(queue: string, handler: DeliveryHandler, options: SubscribeOptions): Promise<Subscription> {\n await Promise.resolve()\n this.#assertOpen(`subscribe to \"${queue}\"`)\n this.#maybeFail('subscribe', queue)\n const state = this.#queue(queue)\n const subscriber: Subscriber = {\n queue,\n handler,\n concurrency: options.concurrency,\n options,\n inflight: 0,\n active: true,\n }\n state.subscribers.push(subscriber)\n this.#pump(queue)\n return {\n cancel: async (): Promise<void> => {\n await Promise.resolve()\n subscriber.active = false\n this.#checkIdle()\n },\n close: async (): Promise<void> => {\n await Promise.resolve()\n subscriber.active = false\n const index = state.subscribers.indexOf(subscriber)\n if (index !== -1) state.subscribers.splice(index, 1)\n this.#checkIdle()\n },\n }\n }\n\n // -----------------------------------------------------------------------------------------------\n // Test helpers\n // -----------------------------------------------------------------------------------------------\n\n /**\n * Injects a message into `queue` as an external producer would (no codec involved when `body` is\n * given). Returns the message id.\n */\n send(queue: string, options: MemorySendOptions = {}): string {\n this.#assertOpen(`send to \"${queue}\"`)\n const encoded =\n options.body === undefined\n ? jsonCodec.encode(options.payload, options.contentType)\n : { body: options.body, contentType: options.contentType }\n const stored: StoredMessage = {\n queue,\n id: options.id ?? randomUUID(),\n type: options.type,\n key: options.key,\n headers: { ...(options.headers ?? {}) },\n body: encoded.body,\n contentType: encoded.contentType,\n timestamp: Date.now(),\n deliveries: options.deliveries ?? 0,\n }\n this.#enqueue(stored, 'tail')\n return stored.id\n }\n\n /** Messages published through `publish()` (the `MessageBroker`), optionally for one queue. */\n published(queue?: string): MemoryMessage[] {\n return this.#published.filter(message => queue === undefined || message.queue === queue)\n }\n\n /** Messages waiting in `queue` (not delivered yet, delayed messages excluded). */\n pending(queue: string): MemoryMessage[] {\n return (this.#queues.get(queue)?.ready ?? []).map(snapshot)\n }\n\n /** Settlements performed by consumers, in order, optionally for one consumed queue. */\n settlements(queue?: string): MemorySettlement[] {\n return this.#settlements.filter(settlement => queue === undefined || settlement.queue === queue)\n }\n\n /** Dead-lettered (or discarded) messages, optionally for one consumed queue. */\n deadLetters(queue?: string): MemoryDeadLetter[] {\n return this.#deadLetters.filter(entry => queue === undefined || entry.originalQueue === queue)\n }\n\n /** Deliveries currently handed to consumers and not finished. */\n get inFlight(): number {\n return this.#inflight\n }\n\n /** Whether nothing is in flight, scheduled or deliverable. */\n isIdle(): boolean {\n if (this.#inflight > 0 || this.#timers.size > 0) return false\n for (const state of this.#queues.values()) {\n if (state.scheduled) return false\n if (state.ready.length > 0 && state.subscribers.some(s => s.active && s.inflight < s.concurrency)) return false\n }\n return true\n }\n\n /**\n * Resolves once nothing is in flight, scheduled (delayed messages, retries) or deliverable to an\n * active subscriber. Rejects after `timeoutMs` (default 5 s).\n */\n idle(timeoutMs: number = DEFAULT_IDLE_TIMEOUT_MS): Promise<void> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n this.#idleWaiters.delete(done)\n reject(\n new Error(`[znight] MemoryDriver.idle(): still busy after ${timeoutMs} ms (${this.#inflight} in flight)`),\n )\n }, timeoutMs)\n const done = (): void => {\n clearTimeout(timer)\n resolve()\n }\n this.#idleWaiters.add(done)\n this.#checkIdle()\n })\n }\n\n /** Polls `predicate` until it returns `true`; rejects after `timeoutMs` (default 5 s). */\n async waitFor(predicate: () => boolean, timeoutMs: number = DEFAULT_IDLE_TIMEOUT_MS): Promise<void> {\n const deadline = Date.now() + timeoutMs\n while (!predicate()) {\n if (Date.now() > deadline)\n throw new Error(`[znight] MemoryDriver.waitFor(): condition not met after ${timeoutMs} ms`)\n await new Promise(resolve => setTimeout(resolve, 1))\n }\n }\n\n /**\n * Makes an operation fail (for tests): `publish` (also dead-letter publications, by destination\n * queue), `ack`, `retry`, `dead-letter`, `release` (by consumed queue), `subscribe`, `connect`.\n * Returns a function removing the failure.\n */\n fail(operation: MemoryOperation, options: MemoryFailureOptions = {}): () => void {\n const failure: Failure = {\n operation,\n queue: options.queue,\n remaining: options.times ?? Number.POSITIVE_INFINITY,\n error: options.error ?? new Error(`[memory] injected ${operation} failure`),\n }\n this.#failures.push(failure)\n return () => {\n const index = this.#failures.indexOf(failure)\n if (index !== -1) this.#failures.splice(index, 1)\n }\n }\n\n /** Forgets every message, settlement and failure (subscriptions are kept). */\n reset(): void {\n for (const timer of this.#timers) clearTimeout(timer)\n this.#timers.clear()\n for (const state of this.#queues.values()) state.ready.length = 0\n this.#published.length = 0\n this.#settlements.length = 0\n this.#deadLetters.length = 0\n this.#failures.length = 0\n this.#checkIdle()\n }\n\n // -----------------------------------------------------------------------------------------------\n // Internals\n // -----------------------------------------------------------------------------------------------\n\n #queue(name: string): QueueState {\n let state = this.#queues.get(name)\n if (state === undefined) {\n state = { ready: [], subscribers: [], next: 0, scheduled: false }\n this.#queues.set(name, state)\n }\n return state\n }\n\n #enqueue(message: StoredMessage, position: 'head' | 'tail'): void {\n const state = this.#queue(message.queue)\n if (position === 'head') state.ready.unshift(message)\n else state.ready.push(message)\n this.#pump(message.queue)\n }\n\n /** Runs `fn` after `delayMs` (any duration: a timer waits at most 2^31 - 1 ms, longer delays re-arm). */\n #schedule(fn: () => void, delayMs: number): void {\n const wait = Math.min(delayMs, MAX_TIMER_DELAY_MS)\n const timer = setTimeout(() => {\n this.#timers.delete(timer)\n if (delayMs > wait) return this.#schedule(fn, delayMs - wait)\n fn()\n this.#checkIdle()\n }, wait)\n this.#timers.add(timer)\n }\n\n #pump(queue: string): void {\n const state = this.#queue(queue)\n if (state.scheduled) return\n state.scheduled = true\n queueMicrotask(() => {\n state.scheduled = false\n this.#dispatch(state)\n this.#checkIdle()\n })\n }\n\n #dispatch(state: QueueState): void {\n while (state.ready.length > 0) {\n const subscriber = this.#nextSubscriber(state)\n if (subscriber === undefined) return\n const message = state.ready.shift() as StoredMessage\n this.#deliver(subscriber, message)\n }\n }\n\n #nextSubscriber(state: QueueState): Subscriber | undefined {\n const count = state.subscribers.length\n for (let offset = 0; offset < count; offset++) {\n const index = (state.next + offset) % count\n const subscriber = state.subscribers[index] as Subscriber\n if (subscriber.active && subscriber.inflight < subscriber.concurrency) {\n state.next = (index + 1) % count\n return subscriber\n }\n }\n return undefined\n }\n\n #deliver(subscriber: Subscriber, message: StoredMessage): void {\n message.deliveries++\n subscriber.inflight++\n this.#inflight++\n const attempt = message.deliveries\n let settled = false\n /** Validates a settlement before any side effect (already settled, closed driver, injected failure). */\n const check = (action: MemoryOperation): void => {\n if (settled) throw new Error(`[memory] message ${message.id} was already settled (${action})`)\n this.#assertOpen(`${action} message ${message.id}`)\n this.#maybeFail(action, message.queue)\n }\n const record = (settlement: Omit<MemorySettlement, 'queue' | 'id' | 'attempt'>): void => {\n this.#settlements.push({ queue: message.queue, id: message.id, attempt, ...settlement })\n }\n const delivery: DriverDelivery = {\n id: message.id,\n body: message.body,\n contentType: message.contentType,\n headers: message.headers,\n type: message.type,\n key: message.key,\n attempt,\n redelivered: attempt > 1,\n timestamp: message.timestamp,\n raw: snapshot(message),\n ack: async (): Promise<void> => {\n await Promise.resolve()\n check('ack')\n settled = true\n record({ action: 'ack' })\n },\n retry: async (request: RetryRequest): Promise<void> => {\n await Promise.resolve()\n check('retry')\n settled = true\n record({ action: 'retry', delayMs: request.delayMs, error: request.error })\n if (request.delayMs > 0) this.#schedule(() => this.#enqueue(message, 'tail'), request.delayMs)\n else this.#enqueue(message, 'tail')\n },\n deadLetter: async (request: DeadLetterRequest): Promise<void> => {\n await Promise.resolve()\n check('dead-letter')\n const queue = request.queue\n if (queue !== undefined) {\n this.#maybeFail('publish', queue)\n const copy: StoredMessage = {\n ...message,\n queue,\n headers: deadLetterHeaders(message.headers, request, message.queue),\n deliveries: 0,\n }\n this.#enqueue(copy, 'tail')\n }\n settled = true\n record({ action: queue === undefined ? 'reject' : 'dead-letter', reason: request.reason, error: request.error })\n this.#deadLetters.push({\n queue,\n originalQueue: message.queue,\n message: snapshot(message),\n reason: request.reason,\n error: request.error,\n attempt: request.attempt,\n })\n },\n release: async (): Promise<void> => {\n await Promise.resolve()\n check('release')\n settled = true\n record({ action: 'release' })\n this.#enqueue(message, 'head')\n },\n }\n const finish = (): void => {\n subscriber.inflight--\n this.#inflight--\n if (!settled && !this.#closed) {\n // the consumer returned without settling: give the message back, like a broker would\n settled = true\n record({ action: 'release' })\n this.#enqueue(message, 'head')\n }\n this.#pump(message.queue)\n this.#checkIdle()\n }\n let result: Promise<void>\n try {\n result = subscriber.handler(delivery)\n } catch (error) {\n result = Promise.reject(error)\n }\n Promise.resolve(result).then(finish, finish)\n }\n\n #maybeFail(operation: MemoryOperation, queue: string | undefined): void {\n for (const failure of this.#failures) {\n if (failure.operation !== operation || failure.remaining <= 0) continue\n if (failure.queue !== undefined && failure.queue !== queue) continue\n failure.remaining--\n throw failure.error\n }\n }\n\n #assertOpen(action: string): void {\n if (this.#closed) throw brokerUnavailableError(`[memory] Cannot ${action}: the driver is closed`)\n }\n\n #checkIdle(): void {\n if (this.#idleWaiters.size === 0 || this.#idleCheckScheduled) return\n this.#idleCheckScheduled = true\n queueMicrotask(() => {\n this.#idleCheckScheduled = false\n if (!this.isIdle()) return\n const waiters = [...this.#idleWaiters]\n this.#idleWaiters.clear()\n for (const waiter of waiters) waiter()\n })\n }\n}\n\nfunction snapshot(message: StoredMessage): MemoryMessage {\n let payload: unknown\n try {\n payload = jsonCodec.decode(message.body, message.contentType)\n } catch {\n payload = undefined\n }\n return {\n queue: message.queue,\n id: message.id,\n type: message.type,\n key: message.key,\n headers: Object.assign(headerRecord(), message.headers),\n body: message.body,\n contentType: message.contentType,\n timestamp: message.timestamp,\n deliveries: message.deliveries,\n payload,\n }\n}\n"],"mappings":"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"}
|