@onebun/nats 0.1.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/README.md +141 -0
- package/package.json +52 -0
- package/src/index.ts +26 -0
- package/src/jetstream.adapter.ts +588 -0
- package/src/nats-client.ts +198 -0
- package/src/nats.adapter.ts +467 -0
- package/src/types.ts +81 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NATS Client Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Wrapper around @nats-io/transport-node for easier usage.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { NatsConnectionOptions } from './types';
|
|
8
|
+
|
|
9
|
+
// Import NATS types dynamically to handle potential import issues
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
let natsModule: any = null;
|
|
12
|
+
|
|
13
|
+
async function getNatsModule() {
|
|
14
|
+
if (!natsModule) {
|
|
15
|
+
natsModule = await import('@nats-io/transport-node');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return natsModule;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* NATS subscription wrapper
|
|
23
|
+
*/
|
|
24
|
+
export interface NatsSubscriptionHandle {
|
|
25
|
+
/** Unsubscribe from the subject */
|
|
26
|
+
unsubscribe(): void;
|
|
27
|
+
/** Drain the subscription */
|
|
28
|
+
drain(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* NATS message wrapper
|
|
33
|
+
*/
|
|
34
|
+
export interface NatsMessage {
|
|
35
|
+
/** Subject the message was received on */
|
|
36
|
+
subject: string;
|
|
37
|
+
/** Message data as string */
|
|
38
|
+
data: string;
|
|
39
|
+
/** Reply subject if request-reply pattern */
|
|
40
|
+
reply?: string;
|
|
41
|
+
/** Message headers */
|
|
42
|
+
headers?: Map<string, string[]>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* NATS Client
|
|
47
|
+
*
|
|
48
|
+
* Simplified wrapper around the NATS.js client.
|
|
49
|
+
*/
|
|
50
|
+
export class NatsClient {
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
private nc: any = null;
|
|
53
|
+
private readonly options: NatsConnectionOptions;
|
|
54
|
+
|
|
55
|
+
constructor(options: NatsConnectionOptions) {
|
|
56
|
+
this.options = options;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Connect to NATS
|
|
61
|
+
*/
|
|
62
|
+
async connect(): Promise<void> {
|
|
63
|
+
if (this.nc) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const nats = await getNatsModule();
|
|
68
|
+
|
|
69
|
+
this.nc = await nats.connect({
|
|
70
|
+
servers: this.options.servers,
|
|
71
|
+
name: this.options.name,
|
|
72
|
+
token: this.options.token,
|
|
73
|
+
user: this.options.user,
|
|
74
|
+
pass: this.options.pass,
|
|
75
|
+
maxReconnectAttempts: this.options.maxReconnectAttempts,
|
|
76
|
+
reconnectTimeWait: this.options.reconnectTimeWait,
|
|
77
|
+
timeout: this.options.timeout,
|
|
78
|
+
tls: this.options.tls ? {} : undefined,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Disconnect from NATS
|
|
84
|
+
*/
|
|
85
|
+
async disconnect(): Promise<void> {
|
|
86
|
+
if (this.nc) {
|
|
87
|
+
await this.nc.drain();
|
|
88
|
+
await this.nc.close();
|
|
89
|
+
this.nc = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if connected
|
|
95
|
+
*/
|
|
96
|
+
isConnected(): boolean {
|
|
97
|
+
return this.nc !== null && !this.nc.isClosed();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Publish a message
|
|
102
|
+
*/
|
|
103
|
+
async publish(subject: string, data: string, headers?: Record<string, string>): Promise<void> {
|
|
104
|
+
if (!this.nc) {
|
|
105
|
+
throw new Error('Not connected to NATS');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const nats = await getNatsModule();
|
|
109
|
+
const encoder = new TextEncoder();
|
|
110
|
+
|
|
111
|
+
let natsHeaders;
|
|
112
|
+
if (headers) {
|
|
113
|
+
natsHeaders = nats.headers();
|
|
114
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
115
|
+
natsHeaders.set(key, value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.nc.publish(subject, encoder.encode(data), { headers: natsHeaders });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Subscribe to a subject
|
|
124
|
+
*/
|
|
125
|
+
async subscribe(
|
|
126
|
+
subject: string,
|
|
127
|
+
callback: (msg: NatsMessage) => void | Promise<void>,
|
|
128
|
+
options?: { queue?: string },
|
|
129
|
+
): Promise<NatsSubscriptionHandle> {
|
|
130
|
+
if (!this.nc) {
|
|
131
|
+
throw new Error('Not connected to NATS');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const decoder = new TextDecoder();
|
|
135
|
+
const sub = this.nc.subscribe(subject, { queue: options?.queue });
|
|
136
|
+
|
|
137
|
+
// Start consuming messages
|
|
138
|
+
(async () => {
|
|
139
|
+
for await (const msg of sub) {
|
|
140
|
+
const natsMsg: NatsMessage = {
|
|
141
|
+
subject: msg.subject,
|
|
142
|
+
data: decoder.decode(msg.data),
|
|
143
|
+
reply: msg.reply,
|
|
144
|
+
headers: msg.headers ? new Map(msg.headers.entries()) : undefined,
|
|
145
|
+
};
|
|
146
|
+
await callback(natsMsg);
|
|
147
|
+
}
|
|
148
|
+
})();
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
unsubscribe: () => sub.unsubscribe(),
|
|
152
|
+
drain: () => sub.drain(),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Request-reply pattern
|
|
158
|
+
*/
|
|
159
|
+
async request(
|
|
160
|
+
subject: string,
|
|
161
|
+
data: string,
|
|
162
|
+
options?: { timeout?: number },
|
|
163
|
+
): Promise<NatsMessage> {
|
|
164
|
+
if (!this.nc) {
|
|
165
|
+
throw new Error('Not connected to NATS');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const encoder = new TextEncoder();
|
|
169
|
+
const decoder = new TextDecoder();
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
const response = await this.nc.request(subject, encoder.encode(data), {
|
|
173
|
+
timeout: options?.timeout ?? 5000,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
subject: response.subject,
|
|
178
|
+
data: decoder.decode(response.data),
|
|
179
|
+
reply: response.reply,
|
|
180
|
+
headers: response.headers ? new Map(response.headers.entries()) : undefined,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get the underlying NATS connection
|
|
186
|
+
*/
|
|
187
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
188
|
+
getConnection(): any {
|
|
189
|
+
return this.nc;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Create a NATS client
|
|
195
|
+
*/
|
|
196
|
+
export function createNatsClient(options: NatsConnectionOptions): NatsClient {
|
|
197
|
+
return new NatsClient(options);
|
|
198
|
+
}
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NATS Queue Adapter
|
|
3
|
+
*
|
|
4
|
+
* Queue adapter using NATS pub/sub for message delivery.
|
|
5
|
+
* This adapter provides basic pub/sub functionality without persistence.
|
|
6
|
+
* For persistent messaging, use JetStreamQueueAdapter.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { NatsAdapterOptions } from './types';
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
QueueAdapter,
|
|
13
|
+
QueueAdapterType,
|
|
14
|
+
QueueFeature,
|
|
15
|
+
QueueEvents,
|
|
16
|
+
Message,
|
|
17
|
+
MessageMetadata,
|
|
18
|
+
PublishOptions,
|
|
19
|
+
SubscribeOptions,
|
|
20
|
+
Subscription,
|
|
21
|
+
ScheduledJobOptions,
|
|
22
|
+
ScheduledJobInfo,
|
|
23
|
+
MessageHandler,
|
|
24
|
+
QueueScheduler,
|
|
25
|
+
} from '@onebun/core';
|
|
26
|
+
import {
|
|
27
|
+
createQueuePatternMatcher,
|
|
28
|
+
createQueueScheduler,
|
|
29
|
+
type QueuePatternMatch,
|
|
30
|
+
} from '@onebun/core';
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
NatsClient,
|
|
34
|
+
type NatsMessage,
|
|
35
|
+
type NatsSubscriptionHandle,
|
|
36
|
+
} from './nats-client';
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// NATS Message Implementation
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
class NatsQueueMessage<T> implements Message<T> {
|
|
43
|
+
id: string;
|
|
44
|
+
pattern: string;
|
|
45
|
+
data: T;
|
|
46
|
+
timestamp: number;
|
|
47
|
+
redelivered: boolean;
|
|
48
|
+
metadata: MessageMetadata;
|
|
49
|
+
attempt?: number;
|
|
50
|
+
maxAttempts?: number;
|
|
51
|
+
|
|
52
|
+
private acked = false;
|
|
53
|
+
private nacked = false;
|
|
54
|
+
|
|
55
|
+
constructor(
|
|
56
|
+
id: string,
|
|
57
|
+
pattern: string,
|
|
58
|
+
data: T,
|
|
59
|
+
timestamp: number,
|
|
60
|
+
metadata: MessageMetadata,
|
|
61
|
+
) {
|
|
62
|
+
this.id = id;
|
|
63
|
+
this.pattern = pattern;
|
|
64
|
+
this.data = data;
|
|
65
|
+
this.timestamp = timestamp;
|
|
66
|
+
this.metadata = metadata;
|
|
67
|
+
this.redelivered = false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async ack(): Promise<void> {
|
|
71
|
+
// NATS pub/sub doesn't require explicit ack
|
|
72
|
+
this.acked = true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async nack(_requeue = false): Promise<void> {
|
|
76
|
+
// NATS pub/sub doesn't support nack/requeue
|
|
77
|
+
this.nacked = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// NATS Subscription Implementation
|
|
83
|
+
// ============================================================================
|
|
84
|
+
|
|
85
|
+
interface NatsSubscriptionEntry {
|
|
86
|
+
pattern: string;
|
|
87
|
+
handler: MessageHandler;
|
|
88
|
+
options?: SubscribeOptions;
|
|
89
|
+
matcher: (topic: string) => QueuePatternMatch;
|
|
90
|
+
paused: boolean;
|
|
91
|
+
handle?: NatsSubscriptionHandle;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
class NatsSubscription implements Subscription {
|
|
95
|
+
private active = true;
|
|
96
|
+
|
|
97
|
+
constructor(
|
|
98
|
+
private readonly entry: NatsSubscriptionEntry,
|
|
99
|
+
private readonly onUnsubscribe: () => Promise<void>,
|
|
100
|
+
) {}
|
|
101
|
+
|
|
102
|
+
async unsubscribe(): Promise<void> {
|
|
103
|
+
this.active = false;
|
|
104
|
+
await this.onUnsubscribe();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
pause(): void {
|
|
108
|
+
this.entry.paused = true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
resume(): void {
|
|
112
|
+
this.entry.paused = false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get pattern(): string {
|
|
116
|
+
return this.entry.pattern;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
get isActive(): boolean {
|
|
120
|
+
return this.active && !this.entry.paused;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// NATS Queue Adapter
|
|
126
|
+
// ============================================================================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* NATS Queue Adapter
|
|
130
|
+
*
|
|
131
|
+
* Uses NATS pub/sub for message delivery. This is suitable for
|
|
132
|
+
* scenarios where messages don't need to be persisted and can be
|
|
133
|
+
* lost if no subscribers are available.
|
|
134
|
+
*
|
|
135
|
+
* For persistent messaging, use JetStreamQueueAdapter.
|
|
136
|
+
*
|
|
137
|
+
* Features:
|
|
138
|
+
* - Pattern subscriptions (using NATS wildcards)
|
|
139
|
+
* - Consumer groups (using NATS queue groups)
|
|
140
|
+
* - Scheduled jobs (via in-process scheduler)
|
|
141
|
+
*
|
|
142
|
+
* Not supported:
|
|
143
|
+
* - Delayed messages
|
|
144
|
+
* - Priority
|
|
145
|
+
* - Dead letter queues
|
|
146
|
+
* - Retry (message is lost if handler fails)
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```typescript
|
|
150
|
+
* const adapter = new NatsQueueAdapter({
|
|
151
|
+
* servers: 'nats://localhost:4222',
|
|
152
|
+
* });
|
|
153
|
+
* await adapter.connect();
|
|
154
|
+
*
|
|
155
|
+
* await adapter.subscribe('orders.*', async (message) => {
|
|
156
|
+
* console.log('Received:', message.data);
|
|
157
|
+
* });
|
|
158
|
+
*
|
|
159
|
+
* await adapter.publish('orders.created', { orderId: 123 });
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export class NatsQueueAdapter implements QueueAdapter {
|
|
163
|
+
readonly name = 'nats';
|
|
164
|
+
readonly type: QueueAdapterType = 'nats';
|
|
165
|
+
|
|
166
|
+
private client: NatsClient;
|
|
167
|
+
private connected = false;
|
|
168
|
+
private scheduler: QueueScheduler | null = null;
|
|
169
|
+
private subscriptions: NatsSubscriptionEntry[] = [];
|
|
170
|
+
private messageIdCounter = 0;
|
|
171
|
+
|
|
172
|
+
// Event handlers
|
|
173
|
+
private eventHandlers: Map<keyof QueueEvents, Set<(...args: unknown[]) => void>> = new Map();
|
|
174
|
+
|
|
175
|
+
constructor(private readonly options: NatsAdapterOptions) {
|
|
176
|
+
this.client = new NatsClient(options);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// Lifecycle
|
|
181
|
+
// ============================================================================
|
|
182
|
+
|
|
183
|
+
async connect(): Promise<void> {
|
|
184
|
+
if (this.connected) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
await this.client.connect();
|
|
190
|
+
this.connected = true;
|
|
191
|
+
this.scheduler = createQueueScheduler(this);
|
|
192
|
+
|
|
193
|
+
this.emit('onReady');
|
|
194
|
+
} catch (error) {
|
|
195
|
+
this.emit('onError', error as Error);
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async disconnect(): Promise<void> {
|
|
201
|
+
if (!this.connected) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Stop scheduler
|
|
206
|
+
if (this.scheduler) {
|
|
207
|
+
this.scheduler.stop();
|
|
208
|
+
this.scheduler = null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Unsubscribe all
|
|
212
|
+
for (const entry of this.subscriptions) {
|
|
213
|
+
if (entry.handle) {
|
|
214
|
+
entry.handle.unsubscribe();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
this.subscriptions = [];
|
|
218
|
+
|
|
219
|
+
await this.client.disconnect();
|
|
220
|
+
this.connected = false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
isConnected(): boolean {
|
|
224
|
+
return this.connected && this.client.isConnected();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ============================================================================
|
|
228
|
+
// Publishing
|
|
229
|
+
// ============================================================================
|
|
230
|
+
|
|
231
|
+
async publish<T>(pattern: string, data: T, options?: PublishOptions): Promise<string> {
|
|
232
|
+
this.ensureConnected();
|
|
233
|
+
|
|
234
|
+
const messageId = options?.messageId ?? this.generateMessageId();
|
|
235
|
+
const timestamp = Date.now();
|
|
236
|
+
|
|
237
|
+
const messageData = {
|
|
238
|
+
id: messageId,
|
|
239
|
+
pattern,
|
|
240
|
+
data,
|
|
241
|
+
timestamp,
|
|
242
|
+
metadata: options?.metadata ?? {},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Convert headers to string map
|
|
246
|
+
const headers: Record<string, string> = {};
|
|
247
|
+
if (options?.metadata?.headers) {
|
|
248
|
+
Object.assign(headers, options.metadata.headers);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
await this.client.publish(pattern, JSON.stringify(messageData), headers);
|
|
252
|
+
|
|
253
|
+
return messageId;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async publishBatch<T>(
|
|
257
|
+
messages: Array<{ pattern: string; data: T; options?: PublishOptions }>,
|
|
258
|
+
): Promise<string[]> {
|
|
259
|
+
const ids: string[] = [];
|
|
260
|
+
|
|
261
|
+
for (const msg of messages) {
|
|
262
|
+
const id = await this.publish(msg.pattern, msg.data, msg.options);
|
|
263
|
+
ids.push(id);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return ids;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// Subscribing
|
|
271
|
+
// ============================================================================
|
|
272
|
+
|
|
273
|
+
async subscribe<T>(
|
|
274
|
+
pattern: string,
|
|
275
|
+
handler: MessageHandler<T>,
|
|
276
|
+
options?: SubscribeOptions,
|
|
277
|
+
): Promise<Subscription> {
|
|
278
|
+
this.ensureConnected();
|
|
279
|
+
|
|
280
|
+
// Convert OneBun pattern to NATS pattern
|
|
281
|
+
// OneBun uses '.' as separator and '*' for single-level, '#' for multi-level
|
|
282
|
+
// NATS uses '.' as separator and '*' for single-level, '>' for multi-level
|
|
283
|
+
const natsPattern = pattern.replace(/#/g, '>');
|
|
284
|
+
|
|
285
|
+
const entry: NatsSubscriptionEntry = {
|
|
286
|
+
pattern,
|
|
287
|
+
handler: handler as MessageHandler,
|
|
288
|
+
options,
|
|
289
|
+
matcher: createQueuePatternMatcher(pattern),
|
|
290
|
+
paused: false,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Subscribe to NATS
|
|
294
|
+
const handle = await this.client.subscribe(
|
|
295
|
+
natsPattern,
|
|
296
|
+
async (msg) => {
|
|
297
|
+
if (entry.paused) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
await this.processMessage(entry, msg);
|
|
301
|
+
},
|
|
302
|
+
{ queue: options?.group },
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
entry.handle = handle;
|
|
306
|
+
this.subscriptions.push(entry);
|
|
307
|
+
|
|
308
|
+
const subscription = new NatsSubscription(entry, async () => {
|
|
309
|
+
const index = this.subscriptions.indexOf(entry);
|
|
310
|
+
if (index !== -1) {
|
|
311
|
+
this.subscriptions.splice(index, 1);
|
|
312
|
+
}
|
|
313
|
+
if (entry.handle) {
|
|
314
|
+
entry.handle.unsubscribe();
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return subscription;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ============================================================================
|
|
322
|
+
// Scheduled Jobs
|
|
323
|
+
// ============================================================================
|
|
324
|
+
|
|
325
|
+
async addScheduledJob(name: string, options: ScheduledJobOptions): Promise<void> {
|
|
326
|
+
this.ensureConnected();
|
|
327
|
+
|
|
328
|
+
if (!this.scheduler) {
|
|
329
|
+
throw new Error('Scheduler not initialized');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (options.schedule.cron) {
|
|
333
|
+
this.scheduler.addCronJob(name, options.schedule.cron, options.pattern, () => options.data, {
|
|
334
|
+
metadata: options.metadata,
|
|
335
|
+
overlapStrategy: options.overlapStrategy,
|
|
336
|
+
});
|
|
337
|
+
} else if (options.schedule.every) {
|
|
338
|
+
this.scheduler.addIntervalJob(name, options.schedule.every, options.pattern, () => options.data, {
|
|
339
|
+
metadata: options.metadata,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async removeScheduledJob(name: string): Promise<boolean> {
|
|
345
|
+
if (!this.scheduler) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return this.scheduler.removeJob(name);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async getScheduledJobs(): Promise<ScheduledJobInfo[]> {
|
|
353
|
+
if (!this.scheduler) {
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return this.scheduler.getJobs();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ============================================================================
|
|
361
|
+
// Features
|
|
362
|
+
// ============================================================================
|
|
363
|
+
|
|
364
|
+
supports(feature: QueueFeature): boolean {
|
|
365
|
+
switch (feature) {
|
|
366
|
+
case 'pattern-subscriptions':
|
|
367
|
+
case 'consumer-groups':
|
|
368
|
+
case 'scheduled-jobs':
|
|
369
|
+
return true;
|
|
370
|
+
case 'delayed-messages':
|
|
371
|
+
case 'priority':
|
|
372
|
+
case 'dead-letter-queue':
|
|
373
|
+
case 'retry':
|
|
374
|
+
return false;
|
|
375
|
+
default:
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ============================================================================
|
|
381
|
+
// Events
|
|
382
|
+
// ============================================================================
|
|
383
|
+
|
|
384
|
+
on<E extends keyof QueueEvents>(event: E, handler: NonNullable<QueueEvents[E]>): void {
|
|
385
|
+
if (!this.eventHandlers.has(event)) {
|
|
386
|
+
this.eventHandlers.set(event, new Set());
|
|
387
|
+
}
|
|
388
|
+
this.eventHandlers.get(event)!.add(handler as (...args: unknown[]) => void);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
off<E extends keyof QueueEvents>(event: E, handler: NonNullable<QueueEvents[E]>): void {
|
|
392
|
+
const handlers = this.eventHandlers.get(event);
|
|
393
|
+
if (handlers) {
|
|
394
|
+
handlers.delete(handler as (...args: unknown[]) => void);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ============================================================================
|
|
399
|
+
// Private Methods
|
|
400
|
+
// ============================================================================
|
|
401
|
+
|
|
402
|
+
private ensureConnected(): void {
|
|
403
|
+
if (!this.connected) {
|
|
404
|
+
throw new Error('NatsQueueAdapter not connected. Call connect() first.');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private generateMessageId(): string {
|
|
409
|
+
// eslint-disable-next-line no-magic-numbers
|
|
410
|
+
return `nats-${++this.messageIdCounter}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private emit<E extends keyof QueueEvents>(event: E, ...args: unknown[]): void {
|
|
414
|
+
const handlers = this.eventHandlers.get(event);
|
|
415
|
+
if (handlers) {
|
|
416
|
+
for (const handler of handlers) {
|
|
417
|
+
try {
|
|
418
|
+
handler(...args);
|
|
419
|
+
} catch {
|
|
420
|
+
// Silently ignore event handler errors
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private async processMessage(entry: NatsSubscriptionEntry, natsMsg: NatsMessage): Promise<void> {
|
|
427
|
+
try {
|
|
428
|
+
const messageData = JSON.parse(natsMsg.data);
|
|
429
|
+
|
|
430
|
+
// Check if pattern matches
|
|
431
|
+
const match = entry.matcher(messageData.pattern || natsMsg.subject);
|
|
432
|
+
if (!match.matched) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const message = new NatsQueueMessage(
|
|
437
|
+
messageData.id || this.generateMessageId(),
|
|
438
|
+
messageData.pattern || natsMsg.subject,
|
|
439
|
+
messageData.data,
|
|
440
|
+
messageData.timestamp || Date.now(),
|
|
441
|
+
messageData.metadata || {},
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
// Emit received event
|
|
445
|
+
this.emit('onMessageReceived', message);
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
await entry.handler(message);
|
|
449
|
+
|
|
450
|
+
// Emit processed event
|
|
451
|
+
this.emit('onMessageProcessed', message);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
// Emit failed event
|
|
454
|
+
this.emit('onMessageFailed', message, error as Error);
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
// Silently ignore message parsing errors
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Create a NATS queue adapter
|
|
464
|
+
*/
|
|
465
|
+
export function createNatsQueueAdapter(options: NatsAdapterOptions): NatsQueueAdapter {
|
|
466
|
+
return new NatsQueueAdapter(options);
|
|
467
|
+
}
|