@mochabug/adapt-web 0.0.39 → 0.0.42
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/cjs/index.js +355 -255
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +354 -255
- package/dist/esm/index.js.map +1 -1
- package/dist/types/index.d.ts +33 -3
- package/package.json +1 -1
package/dist/cjs/index.js
CHANGED
|
@@ -7,12 +7,31 @@ exports.getSession = getSession;
|
|
|
7
7
|
exports.readOutput = readOutput;
|
|
8
8
|
exports.readUrls = readUrls;
|
|
9
9
|
exports.stopSession = stopSession;
|
|
10
|
+
exports.createPubsubClient = createPubsubClient;
|
|
10
11
|
const protobuf_1 = require("@bufbuild/protobuf");
|
|
11
12
|
const wkt_1 = require("@bufbuild/protobuf/wkt");
|
|
12
13
|
const nats_ws_1 = require("nats.ws");
|
|
13
14
|
const automations_pb_js_1 = require("./genproto/mochabugapis/adapt/automations/v1/automations_pb.js");
|
|
15
|
+
// Constants for JetStream protocol
|
|
16
|
+
const CONSUMER_MSG_NEXT_PREFIX = "$JS.API.CONSUMER.MSG.NEXT.";
|
|
17
|
+
const ACK_POSITIVE = new TextEncoder().encode("+ACK");
|
|
18
|
+
const ACK_NEGATIVE = new TextEncoder().encode("-NAK");
|
|
19
|
+
//const ACK_PREFIX = "$JS.ACK.";
|
|
20
|
+
//const ACK_IN_PROGRESS = new TextEncoder().encode("+WPI");
|
|
21
|
+
//const ACK_TERMINATE = new TextEncoder().encode("+TERM");
|
|
22
|
+
// JetStream Status Codes
|
|
23
|
+
const STATUS_NO_MESSAGES = "404";
|
|
24
|
+
const STATUS_REQUEST_TIMEOUT = "408";
|
|
25
|
+
const STATUS_MAX_ACK_PENDING = "409";
|
|
26
|
+
const STATUS_FLOW_CONTROL = "100";
|
|
27
|
+
// Default configuration
|
|
28
|
+
const DEFAULT_MAX_MESSAGES = 100;
|
|
29
|
+
const DEFAULT_EXPIRES_MS = 30000;
|
|
30
|
+
const DEFAULT_POLL_INTERVAL_MS = 1000;
|
|
31
|
+
const DEFAULT_THRESHOLD_RATIO = 0.5;
|
|
14
32
|
const natsUrl = "wss://adapt-dev.mochabugapis.com:443/pubsub";
|
|
15
33
|
const baseUrl = "https://adapt-dev.mochabugapis.com/v1/automations";
|
|
34
|
+
// REST API functions (unchanged from original)
|
|
16
35
|
async function startSession(req, token) {
|
|
17
36
|
const headers = new Headers({
|
|
18
37
|
"Content-Type": "application/json",
|
|
@@ -106,6 +125,7 @@ class RestClientError extends Error {
|
|
|
106
125
|
}
|
|
107
126
|
}
|
|
108
127
|
exports.RestClientError = RestClientError;
|
|
128
|
+
// Connection states
|
|
109
129
|
var ConnectionState;
|
|
110
130
|
(function (ConnectionState) {
|
|
111
131
|
ConnectionState["DISCONNECTED"] = "disconnected";
|
|
@@ -114,9 +134,8 @@ var ConnectionState;
|
|
|
114
134
|
ConnectionState["CLOSING"] = "closing";
|
|
115
135
|
})(ConnectionState || (ConnectionState = {}));
|
|
116
136
|
class PubsubClient {
|
|
117
|
-
constructor() {
|
|
137
|
+
constructor(debug = false) {
|
|
118
138
|
this.nc = null;
|
|
119
|
-
this.js = null;
|
|
120
139
|
this.pullSubscription = null;
|
|
121
140
|
this.subject = null;
|
|
122
141
|
this.streamName = null;
|
|
@@ -126,100 +145,106 @@ class PubsubClient {
|
|
|
126
145
|
this.urlHandler = null;
|
|
127
146
|
this.isReconnecting = false;
|
|
128
147
|
this.connectionState = ConnectionState.DISCONNECTED;
|
|
129
|
-
|
|
130
|
-
this.
|
|
131
|
-
this.
|
|
132
|
-
this.
|
|
133
|
-
this.
|
|
134
|
-
this.
|
|
135
|
-
this.
|
|
136
|
-
this.
|
|
148
|
+
// Pull consumer state
|
|
149
|
+
this.pending = 0;
|
|
150
|
+
this.pulling = false;
|
|
151
|
+
this.pullTimer = null;
|
|
152
|
+
this.maxMessages = DEFAULT_MAX_MESSAGES;
|
|
153
|
+
this.expiresMs = DEFAULT_EXPIRES_MS;
|
|
154
|
+
this.pollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
|
|
155
|
+
this.threshold = DEFAULT_MAX_MESSAGES * DEFAULT_THRESHOLD_RATIO;
|
|
156
|
+
this.inbox = null;
|
|
157
|
+
this.jc = (0, nats_ws_1.JSONCodec)();
|
|
158
|
+
this.debug = debug;
|
|
159
|
+
}
|
|
160
|
+
log(level, message, data) {
|
|
161
|
+
if (!this.debug && level === "debug")
|
|
162
|
+
return;
|
|
163
|
+
const prefix = `[PubSub]`;
|
|
164
|
+
const fullMessage = `${prefix} ${message}`;
|
|
165
|
+
if (data !== undefined) {
|
|
166
|
+
console[level](fullMessage, data);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console[level](fullMessage);
|
|
170
|
+
}
|
|
137
171
|
}
|
|
138
172
|
async subscribe(opts) {
|
|
139
|
-
|
|
173
|
+
this.log("info", "Subscribe called with:", {
|
|
140
174
|
subject: opts.subject,
|
|
141
175
|
consumer: opts.consumer,
|
|
142
176
|
stream: opts.stream,
|
|
143
177
|
});
|
|
144
178
|
// Validate inputs
|
|
145
179
|
if (!validateSubject(opts.subject)) {
|
|
146
|
-
throw new Error(`
|
|
180
|
+
throw new Error(`Invalid subject: "${opts.subject}"`);
|
|
147
181
|
}
|
|
148
182
|
if (!opts.consumer || !opts.stream) {
|
|
149
|
-
throw new Error("
|
|
183
|
+
throw new Error("Consumer and stream names are required");
|
|
150
184
|
}
|
|
151
185
|
// If we're already connected to the same subject, just update handlers
|
|
152
186
|
if (this.connectionState === ConnectionState.CONNECTED &&
|
|
153
187
|
this.subject === opts.subject &&
|
|
154
|
-
this.
|
|
155
|
-
|
|
156
|
-
|
|
188
|
+
this.streamName === opts.stream &&
|
|
189
|
+
this.consumerName === opts.consumer) {
|
|
190
|
+
this.log("debug", "Already connected to same subject, updating handlers only");
|
|
157
191
|
this.outputHandler = opts.onOutput || null;
|
|
158
192
|
this.sessionHandler = opts.onSession || null;
|
|
159
193
|
this.urlHandler = opts.onUrl || null;
|
|
160
194
|
return;
|
|
161
195
|
}
|
|
162
196
|
if (this.connectionState === ConnectionState.CONNECTING) {
|
|
163
|
-
throw new Error("
|
|
197
|
+
throw new Error("Already connecting, please wait");
|
|
164
198
|
}
|
|
165
|
-
// If connected to a different subject
|
|
199
|
+
// If connected to a different subject, close first
|
|
166
200
|
if (this.connectionState === ConnectionState.CONNECTED ||
|
|
167
201
|
this.connectionState === ConnectionState.CLOSING) {
|
|
168
|
-
console.info("[PubSub] Closing existing connection before new subscription");
|
|
169
202
|
await this.unsubscribe();
|
|
170
203
|
}
|
|
171
204
|
try {
|
|
172
205
|
this.connectionState = ConnectionState.CONNECTING;
|
|
173
|
-
// Store
|
|
206
|
+
// Store configuration
|
|
174
207
|
this.subject = opts.subject;
|
|
175
208
|
this.streamName = opts.stream;
|
|
176
209
|
this.consumerName = opts.consumer;
|
|
177
210
|
this.outputHandler = opts.onOutput || null;
|
|
178
211
|
this.sessionHandler = opts.onSession || null;
|
|
179
212
|
this.urlHandler = opts.onUrl || null;
|
|
180
|
-
|
|
213
|
+
this.maxMessages = opts.maxMessages || DEFAULT_MAX_MESSAGES;
|
|
214
|
+
this.expiresMs = opts.expiresMs || DEFAULT_EXPIRES_MS;
|
|
215
|
+
this.pollIntervalMs = opts.pollIntervalMs || DEFAULT_POLL_INTERVAL_MS;
|
|
216
|
+
this.threshold = Math.floor(this.maxMessages * DEFAULT_THRESHOLD_RATIO);
|
|
217
|
+
// Connect to NATS
|
|
181
218
|
const inboxPrefix = `_INBOX_${opts.consumer}`;
|
|
182
|
-
|
|
219
|
+
this.log("debug", `Connecting with inbox prefix: ${inboxPrefix}`);
|
|
183
220
|
this.nc = await (0, nats_ws_1.connect)({
|
|
184
221
|
servers: [natsUrl],
|
|
185
222
|
authenticator: createAuthenticator(opts.credentials),
|
|
186
|
-
inboxPrefix: inboxPrefix,
|
|
223
|
+
inboxPrefix: inboxPrefix,
|
|
187
224
|
reconnect: true,
|
|
188
225
|
noEcho: true,
|
|
189
226
|
maxReconnectAttempts: -1,
|
|
190
227
|
reconnectTimeWait: 1000,
|
|
191
228
|
pingInterval: 10 * 1000,
|
|
192
229
|
});
|
|
193
|
-
// Get JetStream context
|
|
194
|
-
this.js = this.nc.jetstream();
|
|
195
|
-
// Connection successful
|
|
196
230
|
this.connectionState = ConnectionState.CONNECTED;
|
|
197
|
-
|
|
198
|
-
// Set up status monitoring
|
|
231
|
+
this.log("info", "Connected successfully");
|
|
199
232
|
this.setupStatusMonitoring();
|
|
200
|
-
|
|
201
|
-
await this.createSubscriptions(this.streamName, this.consumerName);
|
|
233
|
+
await this.createPullSubscription();
|
|
202
234
|
}
|
|
203
235
|
catch (error) {
|
|
204
236
|
this.connectionState = ConnectionState.DISCONNECTED;
|
|
205
|
-
|
|
206
|
-
this.subject = null;
|
|
207
|
-
this.streamName = null;
|
|
208
|
-
this.consumerName = null;
|
|
209
|
-
this.outputHandler = null;
|
|
210
|
-
this.sessionHandler = null;
|
|
211
|
-
this.urlHandler = null;
|
|
212
|
-
this.js = null;
|
|
237
|
+
this.cleanupState();
|
|
213
238
|
if (this.nc) {
|
|
214
239
|
try {
|
|
215
240
|
await this.nc.close();
|
|
216
241
|
}
|
|
217
242
|
catch (closeError) {
|
|
218
|
-
|
|
243
|
+
this.log("error", "Error closing connection after failed subscribe:", closeError);
|
|
219
244
|
}
|
|
220
245
|
this.nc = null;
|
|
221
246
|
}
|
|
222
|
-
|
|
247
|
+
this.log("error", "Failed to subscribe:", error);
|
|
223
248
|
throw error;
|
|
224
249
|
}
|
|
225
250
|
}
|
|
@@ -231,10 +256,10 @@ class PubsubClient {
|
|
|
231
256
|
for await (const status of this.nc.status()) {
|
|
232
257
|
switch (status.type) {
|
|
233
258
|
case "disconnect":
|
|
234
|
-
|
|
259
|
+
this.log("warn", "⚠ Disconnected", { data: status.data });
|
|
235
260
|
break;
|
|
236
261
|
case "reconnect":
|
|
237
|
-
|
|
262
|
+
this.log("info", "↻ Reconnected");
|
|
238
263
|
if (!this.isReconnecting &&
|
|
239
264
|
this.connectionState === ConnectionState.CONNECTED) {
|
|
240
265
|
this.isReconnecting = true;
|
|
@@ -242,7 +267,7 @@ class PubsubClient {
|
|
|
242
267
|
await this.resubscribe();
|
|
243
268
|
}
|
|
244
269
|
catch (error) {
|
|
245
|
-
|
|
270
|
+
this.log("error", "Failed to resubscribe after reconnection:", error);
|
|
246
271
|
}
|
|
247
272
|
finally {
|
|
248
273
|
this.isReconnecting = false;
|
|
@@ -250,285 +275,345 @@ class PubsubClient {
|
|
|
250
275
|
}
|
|
251
276
|
break;
|
|
252
277
|
case "error":
|
|
253
|
-
|
|
254
|
-
console.error("[PubSub] Connection error:", status.data);
|
|
278
|
+
this.log("error", "Connection error:", status.data);
|
|
255
279
|
break;
|
|
256
280
|
case "update":
|
|
257
|
-
|
|
258
|
-
console.info("[PubSub] Cluster update received:", status.data);
|
|
281
|
+
this.log("debug", "Cluster update received:", status.data);
|
|
259
282
|
break;
|
|
260
283
|
case "ldm":
|
|
261
|
-
|
|
262
|
-
console.warn("[PubSub] Server requesting reconnection (LDM)");
|
|
284
|
+
this.log("warn", "Server requesting reconnection (LDM)");
|
|
263
285
|
break;
|
|
264
|
-
// Debug events - usually can be safely ignored but useful for debugging
|
|
265
286
|
case "reconnecting":
|
|
266
|
-
|
|
267
|
-
break;
|
|
268
|
-
case "pingTimer":
|
|
269
|
-
// Ping timer events - very verbose, only log if needed
|
|
270
|
-
// console.debug("[PubSub] Ping timer event");
|
|
287
|
+
this.log("debug", "Attempting to reconnect...");
|
|
271
288
|
break;
|
|
272
289
|
case "staleConnection":
|
|
273
|
-
|
|
290
|
+
this.log("warn", "Connection is stale, may reconnect soon");
|
|
274
291
|
break;
|
|
275
292
|
default:
|
|
276
|
-
|
|
293
|
+
if (status.type !== "pingTimer" || this.debug) {
|
|
294
|
+
this.log("debug", `Status event: ${status.type}`, status.data);
|
|
295
|
+
}
|
|
277
296
|
}
|
|
278
297
|
}
|
|
279
298
|
}
|
|
280
299
|
catch (error) {
|
|
281
300
|
if (this.connectionState !== ConnectionState.CLOSING) {
|
|
282
|
-
|
|
301
|
+
this.log("error", "Fatal status handler error:", error);
|
|
283
302
|
}
|
|
284
303
|
}
|
|
285
304
|
})();
|
|
286
305
|
}
|
|
287
|
-
async
|
|
288
|
-
if (!this.nc || !this.
|
|
289
|
-
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
if (!this.subject) {
|
|
293
|
-
console.error("[PubSub] Cannot create subscriptions - no subject");
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
if (this.nc.isClosed()) {
|
|
297
|
-
console.error("[PubSub] Cannot create subscriptions - connection is closed");
|
|
298
|
-
return;
|
|
306
|
+
async createPullSubscription() {
|
|
307
|
+
if (!this.nc || !this.streamName || !this.consumerName) {
|
|
308
|
+
throw new Error("Not properly initialized");
|
|
299
309
|
}
|
|
300
310
|
await this.clearSubscriptions();
|
|
301
311
|
try {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const inbox = (0, nats_ws_1.createInbox)(`_INBOX_${consumerName}`);
|
|
307
|
-
const sub = this.nc.subscribe(inbox);
|
|
312
|
+
this.log("info", `Creating pull subscription for consumer: ${this.consumerName} on stream: ${this.streamName}`);
|
|
313
|
+
// Create inbox for receiving messages
|
|
314
|
+
this.inbox = (0, nats_ws_1.createInbox)(`_INBOX_${this.consumerName}`);
|
|
315
|
+
const sub = this.nc.subscribe(this.inbox);
|
|
308
316
|
this.pullSubscription = sub;
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
317
|
+
// Reset state
|
|
318
|
+
this.pending = 0;
|
|
319
|
+
this.pulling = false;
|
|
320
|
+
// Start message processing loop
|
|
321
|
+
this.processMessages(sub);
|
|
322
|
+
// Initial pull - use no_wait to check for immediate messages
|
|
323
|
+
this.log("debug", `Initial no_wait pull for ${this.maxMessages} messages`);
|
|
324
|
+
await this.pull(this.maxMessages, true);
|
|
325
|
+
// Schedule periodic pulls for long polling
|
|
326
|
+
this.schedulePull();
|
|
327
|
+
this.log("info", "Pull subscription created successfully");
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
this.log("error", "Failed to create subscriptions:", error);
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async processMessages(sub) {
|
|
335
|
+
try {
|
|
336
|
+
for await (const msg of sub) {
|
|
337
|
+
if (this.connectionState === ConnectionState.CLOSING ||
|
|
338
|
+
this.connectionState === ConnectionState.DISCONNECTED) {
|
|
339
|
+
this.log("info", "Subscription closing");
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
320
342
|
try {
|
|
321
|
-
|
|
322
|
-
const request = {
|
|
323
|
-
batch,
|
|
324
|
-
expires: (pollInterval + 500) * 1000000, // Poll interval + 500ms buffer, in nanoseconds
|
|
325
|
-
};
|
|
326
|
-
this.nc.publish(pullSubject, jc.encode(request), { reply: inbox });
|
|
327
|
-
pending += batch;
|
|
343
|
+
await this.handleMessage(msg);
|
|
328
344
|
}
|
|
329
|
-
|
|
330
|
-
|
|
345
|
+
catch (error) {
|
|
346
|
+
this.log("error", "Error handling message:", error);
|
|
331
347
|
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
fork: output.fork,
|
|
399
|
-
data: parseOutputData(output.data),
|
|
400
|
-
created: (0, wkt_1.timestampDate)(output.created),
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
break;
|
|
404
|
-
}
|
|
405
|
-
case "session": {
|
|
406
|
-
if (this.sessionHandler) {
|
|
407
|
-
const session = (0, protobuf_1.fromBinary)(automations_pb_js_1.SessionSchema, msg.data);
|
|
408
|
-
this.sessionHandler((0, protobuf_1.toJson)(automations_pb_js_1.SessionSchema, session));
|
|
409
|
-
}
|
|
410
|
-
break;
|
|
411
|
-
}
|
|
412
|
-
case "url": {
|
|
413
|
-
if (this.urlHandler) {
|
|
414
|
-
const url = (0, protobuf_1.fromBinary)(automations_pb_js_1.UrlSchema, msg.data);
|
|
415
|
-
this.urlHandler((0, protobuf_1.toJson)(automations_pb_js_1.UrlSchema, url));
|
|
416
|
-
}
|
|
417
|
-
break;
|
|
418
|
-
}
|
|
419
|
-
default:
|
|
420
|
-
console.warn("[PubSub] Unknown message action:", action);
|
|
421
|
-
}
|
|
422
|
-
if (msg.reply) {
|
|
423
|
-
msg.respond();
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
catch (error) {
|
|
427
|
-
console.error("[PubSub] Error handling message:", error);
|
|
428
|
-
if (msg.reply) {
|
|
429
|
-
msg.respond(new TextEncoder().encode("-NAK"));
|
|
430
|
-
}
|
|
431
|
-
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (this.connectionState !== ConnectionState.CLOSING) {
|
|
352
|
+
this.log("error", "Message processing error:", error);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async handleMessage(msg) {
|
|
357
|
+
this.log("debug", "header", msg.headers);
|
|
358
|
+
const status = msg.headers?.get("Status");
|
|
359
|
+
const description = msg.headers?.get("Description");
|
|
360
|
+
if (status) {
|
|
361
|
+
this.log("debug", `Received status message: ${status} ${description || ""}`);
|
|
362
|
+
await this.handleStatusMessage(status, description, msg);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
// Check for flow control messages (also may have empty data)
|
|
366
|
+
const flowControl = msg.headers?.get("Nats-Msg-Size");
|
|
367
|
+
if (flowControl === "FlowControl") {
|
|
368
|
+
this.log("debug", "Received flow control request, responding");
|
|
369
|
+
msg.respond();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
// Alternative flow control check
|
|
373
|
+
if (msg.headers) {
|
|
374
|
+
const headers = Array.from(msg.headers.keys());
|
|
375
|
+
if (headers.some((h) => h.includes("FlowControl"))) {
|
|
376
|
+
this.log("debug", "Received flow control message, responding");
|
|
377
|
+
if (msg.reply) {
|
|
378
|
+
msg.respond();
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Now check if this is an actual data message
|
|
384
|
+
// Status and flow control messages will have already been handled above
|
|
385
|
+
if (!msg.data || msg.data.length === 0) {
|
|
386
|
+
// This should be rare now - log with more detail for debugging
|
|
387
|
+
this.log("debug", `Received empty message on ${msg.subject}, headers: ${msg.headers ? Array.from(msg.headers.keys()).join(", ") : "none"}`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
// Get the actual stream subject from headers
|
|
391
|
+
const streamSubject = msg.headers?.get("Nats-Subject");
|
|
392
|
+
if (!streamSubject) {
|
|
393
|
+
this.log("debug", "Message without stream subject, skipping");
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
// We got a real message
|
|
397
|
+
this.pending = Math.max(0, this.pending - 1);
|
|
398
|
+
this.log("debug", `Message received on subject ${streamSubject}, pending decreased to ${this.pending}`);
|
|
399
|
+
// Process the message
|
|
400
|
+
const parts = streamSubject.split(".");
|
|
401
|
+
const action = parts[parts.length - 1];
|
|
402
|
+
this.log("info", `↓ message: ${action} on ${streamSubject}`);
|
|
403
|
+
try {
|
|
404
|
+
switch (action) {
|
|
405
|
+
case "output": {
|
|
406
|
+
if (this.outputHandler) {
|
|
407
|
+
const output = (0, protobuf_1.fromBinary)(automations_pb_js_1.OutputSchema, msg.data);
|
|
408
|
+
this.outputHandler({
|
|
409
|
+
vertex: output.vertex,
|
|
410
|
+
fork: output.fork,
|
|
411
|
+
data: parseOutputData(output.data),
|
|
412
|
+
created: (0, wkt_1.timestampDate)(output.created),
|
|
413
|
+
});
|
|
432
414
|
}
|
|
415
|
+
break;
|
|
433
416
|
}
|
|
434
|
-
|
|
435
|
-
if (this.
|
|
436
|
-
|
|
417
|
+
case "session": {
|
|
418
|
+
if (this.sessionHandler) {
|
|
419
|
+
const session = (0, protobuf_1.fromBinary)(automations_pb_js_1.SessionSchema, msg.data);
|
|
420
|
+
this.sessionHandler((0, protobuf_1.toJson)(automations_pb_js_1.SessionSchema, session));
|
|
437
421
|
}
|
|
422
|
+
break;
|
|
438
423
|
}
|
|
439
|
-
|
|
440
|
-
if (
|
|
441
|
-
|
|
424
|
+
case "url": {
|
|
425
|
+
if (this.urlHandler) {
|
|
426
|
+
const url = (0, protobuf_1.fromBinary)(automations_pb_js_1.UrlSchema, msg.data);
|
|
427
|
+
this.urlHandler((0, protobuf_1.toJson)(automations_pb_js_1.UrlSchema, url));
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
442
430
|
}
|
|
443
|
-
|
|
444
|
-
|
|
431
|
+
default:
|
|
432
|
+
this.log("warn", `Unknown message action: ${action}`);
|
|
433
|
+
}
|
|
434
|
+
// ACK the message
|
|
435
|
+
if (msg.reply) {
|
|
436
|
+
msg.respond(ACK_POSITIVE);
|
|
437
|
+
this.log("debug", `Message ACKed successfully for action: ${action}`);
|
|
438
|
+
}
|
|
445
439
|
}
|
|
446
440
|
catch (error) {
|
|
447
|
-
|
|
441
|
+
this.log("error", "Error processing message:", error);
|
|
442
|
+
// NAK the message for redelivery
|
|
443
|
+
if (msg.reply) {
|
|
444
|
+
msg.respond(ACK_NEGATIVE);
|
|
445
|
+
this.log("debug", "Message NAKed for redelivery");
|
|
446
|
+
}
|
|
448
447
|
throw error;
|
|
449
448
|
}
|
|
449
|
+
// Pull more if below threshold
|
|
450
|
+
if (this.pending < this.threshold && !this.pulling) {
|
|
451
|
+
const pullCount = this.maxMessages - this.pending;
|
|
452
|
+
this.log("debug", `Pending ${this.pending} < threshold ${this.threshold}, pulling ${pullCount} more messages`);
|
|
453
|
+
this.pull(pullCount, false);
|
|
454
|
+
}
|
|
450
455
|
}
|
|
451
|
-
async
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
this.
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
456
|
+
async handleStatusMessage(status, description, msg) {
|
|
457
|
+
switch (status) {
|
|
458
|
+
case STATUS_NO_MESSAGES:
|
|
459
|
+
this.log("debug", `No messages available (404): ${description || "No Messages"}`);
|
|
460
|
+
this.pending = 0;
|
|
461
|
+
// This is expected when using no_wait or when there are no messages
|
|
462
|
+
// Don't immediately pull again, let the scheduler handle it
|
|
463
|
+
break;
|
|
464
|
+
case STATUS_REQUEST_TIMEOUT:
|
|
465
|
+
this.log("debug", `Pull request expired (408): ${description || "Request Timeout"}`);
|
|
466
|
+
// This can happen for each unfilled slot in a batch
|
|
467
|
+
// Decrement pending for each 408 received
|
|
468
|
+
if (this.pending > 0) {
|
|
469
|
+
this.pending--;
|
|
470
|
+
}
|
|
471
|
+
// Don't immediately pull, let threshold logic handle it
|
|
472
|
+
if (this.pending < this.threshold && !this.pulling) {
|
|
473
|
+
await this.pull(this.maxMessages - this.pending, false);
|
|
474
|
+
}
|
|
475
|
+
break;
|
|
476
|
+
case STATUS_MAX_ACK_PENDING:
|
|
477
|
+
this.log("warn", `Max ack pending reached (409): ${description || "Exceeded MaxAckPending"}`);
|
|
478
|
+
this.pending = 0;
|
|
479
|
+
// Wait before retry
|
|
480
|
+
setTimeout(() => this.pull(this.maxMessages, false), 5000);
|
|
481
|
+
break;
|
|
482
|
+
case STATUS_FLOW_CONTROL:
|
|
483
|
+
this.log("debug", `Received heartbeat/flow control (100): ${description || ""}`);
|
|
484
|
+
// Respond if needed
|
|
485
|
+
if (msg.reply) {
|
|
486
|
+
msg.respond();
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
default:
|
|
490
|
+
this.log("debug", `Received unknown status: ${status} - ${description || ""}`);
|
|
462
491
|
}
|
|
463
492
|
}
|
|
464
|
-
async
|
|
465
|
-
if (!this.
|
|
466
|
-
|
|
467
|
-
!this.consumerName ||
|
|
468
|
-
(!this.outputHandler && !this.sessionHandler && !this.urlHandler)) {
|
|
469
|
-
console.info("[PubSub] No handlers or config configured, skipping resubscribe");
|
|
493
|
+
async pull(batch, noWait = false) {
|
|
494
|
+
if (this.pulling || !this.nc || !this.inbox || batch <= 0) {
|
|
495
|
+
this.log("debug", `Skipping pull: pulling=${this.pulling}, batch=${batch}`);
|
|
470
496
|
return;
|
|
471
497
|
}
|
|
498
|
+
this.pulling = true;
|
|
472
499
|
try {
|
|
473
|
-
|
|
474
|
-
|
|
500
|
+
const pullSubject = `${CONSUMER_MSG_NEXT_PREFIX}${this.streamName}.${this.consumerName}`;
|
|
501
|
+
const request = {
|
|
502
|
+
batch,
|
|
503
|
+
};
|
|
504
|
+
if (noWait) {
|
|
505
|
+
request.no_wait = true;
|
|
506
|
+
}
|
|
507
|
+
else if (this.expiresMs > 0) {
|
|
508
|
+
// Convert milliseconds to nanoseconds
|
|
509
|
+
request.expires = this.expiresMs * 1000000;
|
|
510
|
+
}
|
|
511
|
+
this.nc.publish(pullSubject, this.jc.encode(request), {
|
|
512
|
+
reply: this.inbox,
|
|
513
|
+
});
|
|
514
|
+
this.pending += batch;
|
|
515
|
+
this.log("debug", `Pull request sent: batch=${batch}, pending=${this.pending}, noWait=${noWait}, expires=${request.expires ? `${this.expiresMs}ms` : "none"}`);
|
|
475
516
|
}
|
|
476
517
|
catch (error) {
|
|
477
|
-
|
|
518
|
+
this.log("error", "Error sending pull request:", error);
|
|
478
519
|
throw error;
|
|
479
520
|
}
|
|
521
|
+
finally {
|
|
522
|
+
this.pulling = false;
|
|
523
|
+
}
|
|
480
524
|
}
|
|
481
|
-
|
|
482
|
-
if (this.
|
|
483
|
-
|
|
525
|
+
schedulePull() {
|
|
526
|
+
if (this.pullTimer) {
|
|
527
|
+
clearTimeout(this.pullTimer);
|
|
528
|
+
}
|
|
529
|
+
if (this.connectionState !== ConnectionState.CONNECTED) {
|
|
484
530
|
return;
|
|
485
531
|
}
|
|
486
|
-
|
|
487
|
-
|
|
532
|
+
this.pullTimer = setTimeout(() => {
|
|
533
|
+
if (this.pending < this.threshold && !this.pulling) {
|
|
534
|
+
const pullCount = this.maxMessages - this.pending;
|
|
535
|
+
this.log("debug", `Scheduled pull: pending=${this.pending} < threshold=${this.threshold}, pulling ${pullCount} more`);
|
|
536
|
+
// For scheduled pulls, use long polling (with expires)
|
|
537
|
+
this.pull(pullCount, false);
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
this.log("debug", `Skipping scheduled pull: pending=${this.pending} >= threshold=${this.threshold} or pulling=${this.pulling}`);
|
|
541
|
+
}
|
|
542
|
+
// Reschedule
|
|
543
|
+
this.schedulePull();
|
|
544
|
+
}, this.pollIntervalMs);
|
|
545
|
+
}
|
|
546
|
+
async clearSubscriptions() {
|
|
547
|
+
if (this.pullTimer) {
|
|
548
|
+
clearTimeout(this.pullTimer);
|
|
549
|
+
this.pullTimer = null;
|
|
550
|
+
}
|
|
551
|
+
if (!this.pullSubscription)
|
|
488
552
|
return;
|
|
553
|
+
this.log("debug", "Clearing subscription");
|
|
554
|
+
try {
|
|
555
|
+
this.pullSubscription.unsubscribe();
|
|
556
|
+
this.log("debug", "✓ Subscription cleared");
|
|
489
557
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
this.
|
|
497
|
-
|
|
558
|
+
catch (error) {
|
|
559
|
+
this.log("error", "Error clearing subscription:", error);
|
|
560
|
+
}
|
|
561
|
+
this.pullSubscription = null;
|
|
562
|
+
}
|
|
563
|
+
async resubscribe() {
|
|
564
|
+
if (!this.streamName || !this.consumerName)
|
|
565
|
+
return;
|
|
566
|
+
this.log("info", "Resubscribing after reconnection");
|
|
567
|
+
// Clear old state
|
|
568
|
+
this.pending = 0;
|
|
569
|
+
this.pulling = false;
|
|
570
|
+
// Recreate subscription
|
|
571
|
+
await this.createPullSubscription();
|
|
572
|
+
this.log("info", "✓ Resubscribed successfully");
|
|
573
|
+
}
|
|
574
|
+
cleanupState() {
|
|
498
575
|
this.subject = null;
|
|
499
576
|
this.streamName = null;
|
|
500
577
|
this.consumerName = null;
|
|
578
|
+
this.outputHandler = null;
|
|
579
|
+
this.sessionHandler = null;
|
|
580
|
+
this.urlHandler = null;
|
|
581
|
+
this.inbox = null;
|
|
582
|
+
this.pending = 0;
|
|
583
|
+
this.pulling = false;
|
|
584
|
+
}
|
|
585
|
+
async unsubscribe() {
|
|
586
|
+
if (this.connectionState === ConnectionState.DISCONNECTED ||
|
|
587
|
+
this.connectionState === ConnectionState.CLOSING) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
this.log("info", "Starting unsubscribe process");
|
|
591
|
+
this.connectionState = ConnectionState.CLOSING;
|
|
592
|
+
await this.clearSubscriptions();
|
|
593
|
+
this.cleanupState();
|
|
501
594
|
this.isReconnecting = false;
|
|
502
|
-
this.
|
|
503
|
-
// Close connection
|
|
504
|
-
if (this.nc) {
|
|
595
|
+
if (this.nc && !this.nc.isClosed()) {
|
|
505
596
|
try {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
console.info("[PubSub] Connection closed");
|
|
509
|
-
}
|
|
597
|
+
await this.nc.close();
|
|
598
|
+
this.log("debug", "Connection closed");
|
|
510
599
|
}
|
|
511
600
|
catch (error) {
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
finally {
|
|
515
|
-
this.nc = null;
|
|
601
|
+
this.log("error", "Error closing connection:", error);
|
|
516
602
|
}
|
|
517
603
|
}
|
|
604
|
+
this.nc = null;
|
|
518
605
|
this.connectionState = ConnectionState.DISCONNECTED;
|
|
519
|
-
|
|
606
|
+
this.log("info", "Unsubscribed successfully");
|
|
520
607
|
}
|
|
521
|
-
// Helper method to check connection status
|
|
522
608
|
isConnected() {
|
|
523
|
-
return
|
|
524
|
-
this.nc !== null &&
|
|
525
|
-
!this.nc.isClosed());
|
|
609
|
+
return this.connectionState === ConnectionState.CONNECTED;
|
|
526
610
|
}
|
|
527
611
|
getConnectionState() {
|
|
528
612
|
return this.connectionState;
|
|
529
613
|
}
|
|
530
614
|
}
|
|
531
615
|
exports.PubsubClient = PubsubClient;
|
|
616
|
+
// Helper functions
|
|
532
617
|
function validateSubject(subject) {
|
|
533
618
|
if (!subject || subject.includes(" ") || subject.includes("\t")) {
|
|
534
619
|
return false;
|
|
@@ -546,7 +631,7 @@ function parseOutputData(data) {
|
|
|
546
631
|
return [key, JSON.parse(new TextDecoder().decode(value))];
|
|
547
632
|
}
|
|
548
633
|
catch (error) {
|
|
549
|
-
console.error(`
|
|
634
|
+
console.error(`Failed to parse data for key ${key}:`, error);
|
|
550
635
|
return [key, null];
|
|
551
636
|
}
|
|
552
637
|
}));
|
|
@@ -562,4 +647,19 @@ function createAuthenticator(val) {
|
|
|
562
647
|
throw new Error("Invalid credentials format");
|
|
563
648
|
}
|
|
564
649
|
}
|
|
650
|
+
// Usage example
|
|
651
|
+
async function createPubsubClient(credentials, automation, handlers, options) {
|
|
652
|
+
const client = new PubsubClient(options?.debug);
|
|
653
|
+
await client.subscribe({
|
|
654
|
+
credentials,
|
|
655
|
+
subject: `automations.${automation.organization}.${automation.group}.${automation.automation}.>`,
|
|
656
|
+
stream: "automations",
|
|
657
|
+
consumer: `${automation.automation}-consumer`,
|
|
658
|
+
onOutput: handlers.onOutput,
|
|
659
|
+
onSession: handlers.onSession,
|
|
660
|
+
onUrl: handlers.onUrl,
|
|
661
|
+
...options,
|
|
662
|
+
});
|
|
663
|
+
return client;
|
|
664
|
+
}
|
|
565
665
|
//# sourceMappingURL=index.js.map
|