@mochabug/adapt-web 0.0.41 → 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 +309 -193
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +308 -193
- package/dist/esm/index.js.map +1 -1
- package/dist/types/index.d.ts +30 -1
- 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";
|
|
@@ -125,15 +145,17 @@ class PubsubClient {
|
|
|
125
145
|
this.urlHandler = null;
|
|
126
146
|
this.isReconnecting = false;
|
|
127
147
|
this.connectionState = ConnectionState.DISCONNECTED;
|
|
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)();
|
|
128
158
|
this.debug = debug;
|
|
129
|
-
this.subscribe = this.subscribe.bind(this);
|
|
130
|
-
this.unsubscribe = this.unsubscribe.bind(this);
|
|
131
|
-
this.createSubscriptions = this.createSubscriptions.bind(this);
|
|
132
|
-
this.clearSubscriptions = this.clearSubscriptions.bind(this);
|
|
133
|
-
this.resubscribe = this.resubscribe.bind(this);
|
|
134
|
-
this.setupStatusMonitoring = this.setupStatusMonitoring.bind(this);
|
|
135
|
-
this.isConnected = this.isConnected.bind(this);
|
|
136
|
-
this.getConnectionState = this.getConnectionState.bind(this);
|
|
137
159
|
}
|
|
138
160
|
log(level, message, data) {
|
|
139
161
|
if (!this.debug && level === "debug")
|
|
@@ -162,7 +184,9 @@ class PubsubClient {
|
|
|
162
184
|
}
|
|
163
185
|
// If we're already connected to the same subject, just update handlers
|
|
164
186
|
if (this.connectionState === ConnectionState.CONNECTED &&
|
|
165
|
-
this.subject === opts.subject
|
|
187
|
+
this.subject === opts.subject &&
|
|
188
|
+
this.streamName === opts.stream &&
|
|
189
|
+
this.consumerName === opts.consumer) {
|
|
166
190
|
this.log("debug", "Already connected to same subject, updating handlers only");
|
|
167
191
|
this.outputHandler = opts.onOutput || null;
|
|
168
192
|
this.sessionHandler = opts.onSession || null;
|
|
@@ -179,13 +203,18 @@ class PubsubClient {
|
|
|
179
203
|
}
|
|
180
204
|
try {
|
|
181
205
|
this.connectionState = ConnectionState.CONNECTING;
|
|
182
|
-
// Store
|
|
206
|
+
// Store configuration
|
|
183
207
|
this.subject = opts.subject;
|
|
184
208
|
this.streamName = opts.stream;
|
|
185
209
|
this.consumerName = opts.consumer;
|
|
186
210
|
this.outputHandler = opts.onOutput || null;
|
|
187
211
|
this.sessionHandler = opts.onSession || null;
|
|
188
212
|
this.urlHandler = opts.onUrl || null;
|
|
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
|
|
189
218
|
const inboxPrefix = `_INBOX_${opts.consumer}`;
|
|
190
219
|
this.log("debug", `Connecting with inbox prefix: ${inboxPrefix}`);
|
|
191
220
|
this.nc = await (0, nats_ws_1.connect)({
|
|
@@ -201,16 +230,11 @@ class PubsubClient {
|
|
|
201
230
|
this.connectionState = ConnectionState.CONNECTED;
|
|
202
231
|
this.log("info", "Connected successfully");
|
|
203
232
|
this.setupStatusMonitoring();
|
|
204
|
-
await this.
|
|
233
|
+
await this.createPullSubscription();
|
|
205
234
|
}
|
|
206
235
|
catch (error) {
|
|
207
236
|
this.connectionState = ConnectionState.DISCONNECTED;
|
|
208
|
-
this.
|
|
209
|
-
this.streamName = null;
|
|
210
|
-
this.consumerName = null;
|
|
211
|
-
this.outputHandler = null;
|
|
212
|
-
this.sessionHandler = null;
|
|
213
|
-
this.urlHandler = null;
|
|
237
|
+
this.cleanupState();
|
|
214
238
|
if (this.nc) {
|
|
215
239
|
try {
|
|
216
240
|
await this.nc.close();
|
|
@@ -225,6 +249,8 @@ class PubsubClient {
|
|
|
225
249
|
}
|
|
226
250
|
}
|
|
227
251
|
setupStatusMonitoring() {
|
|
252
|
+
if (!this.nc)
|
|
253
|
+
return;
|
|
228
254
|
(async () => {
|
|
229
255
|
try {
|
|
230
256
|
for await (const status of this.nc.status()) {
|
|
@@ -249,32 +275,24 @@ class PubsubClient {
|
|
|
249
275
|
}
|
|
250
276
|
break;
|
|
251
277
|
case "error":
|
|
252
|
-
// Usually permissions errors
|
|
253
278
|
this.log("error", "Connection error:", status.data);
|
|
254
279
|
break;
|
|
255
280
|
case "update":
|
|
256
|
-
// Cluster configuration update
|
|
257
281
|
this.log("debug", "Cluster update received:", status.data);
|
|
258
282
|
break;
|
|
259
283
|
case "ldm":
|
|
260
|
-
// Lame Duck Mode - server is requesting clients to reconnect
|
|
261
284
|
this.log("warn", "Server requesting reconnection (LDM)");
|
|
262
285
|
break;
|
|
263
|
-
// Debug events - usually can be safely ignored but useful for debugging
|
|
264
286
|
case "reconnecting":
|
|
265
287
|
this.log("debug", "Attempting to reconnect...");
|
|
266
288
|
break;
|
|
267
|
-
case "pingTimer":
|
|
268
|
-
// Ping timer events - very verbose, only log if debug is enabled
|
|
269
|
-
if (this.debug) {
|
|
270
|
-
this.log("debug", "Ping timer event");
|
|
271
|
-
}
|
|
272
|
-
break;
|
|
273
289
|
case "staleConnection":
|
|
274
290
|
this.log("warn", "Connection is stale, may reconnect soon");
|
|
275
291
|
break;
|
|
276
292
|
default:
|
|
277
|
-
|
|
293
|
+
if (status.type !== "pingTimer" || this.debug) {
|
|
294
|
+
this.log("debug", `Status event: ${status.type}`, status.data);
|
|
295
|
+
}
|
|
278
296
|
}
|
|
279
297
|
}
|
|
280
298
|
}
|
|
@@ -285,180 +303,251 @@ class PubsubClient {
|
|
|
285
303
|
}
|
|
286
304
|
})();
|
|
287
305
|
}
|
|
288
|
-
async
|
|
306
|
+
async createPullSubscription() {
|
|
307
|
+
if (!this.nc || !this.streamName || !this.consumerName) {
|
|
308
|
+
throw new Error("Not properly initialized");
|
|
309
|
+
}
|
|
289
310
|
await this.clearSubscriptions();
|
|
290
311
|
try {
|
|
291
|
-
this.log("info", `Creating pull subscription for consumer: ${consumerName} on stream: ${streamName}`);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const inbox = (0, nats_ws_1.createInbox)(`_INBOX_${consumerName}`);
|
|
296
|
-
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);
|
|
297
316
|
this.pullSubscription = sub;
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
+
}
|
|
309
342
|
try {
|
|
310
|
-
|
|
311
|
-
const request = {
|
|
312
|
-
batch,
|
|
313
|
-
expires: (pollInterval + 500) * 1000000, // Poll interval + 500ms buffer, in nanoseconds
|
|
314
|
-
};
|
|
315
|
-
this.nc.publish(pullSubject, jc.encode(request), { reply: inbox });
|
|
316
|
-
pending += batch;
|
|
317
|
-
this.log("debug", `Pull request sent: batch=${batch}, pending=${pending}, expires=${request.expires}ns`);
|
|
343
|
+
await this.handleMessage(msg);
|
|
318
344
|
}
|
|
319
|
-
|
|
320
|
-
|
|
345
|
+
catch (error) {
|
|
346
|
+
this.log("error", "Error handling message:", error);
|
|
321
347
|
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
+
});
|
|
339
414
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (this.connectionState === ConnectionState.CLOSING ||
|
|
347
|
-
this.connectionState === ConnectionState.DISCONNECTED) {
|
|
348
|
-
this.log("info", "Subscription closing");
|
|
349
|
-
if (pullTimer)
|
|
350
|
-
clearTimeout(pullTimer);
|
|
351
|
-
break;
|
|
352
|
-
}
|
|
353
|
-
// Check status header
|
|
354
|
-
const status = msg.headers?.get("Status");
|
|
355
|
-
if (status === "404") {
|
|
356
|
-
// No messages available
|
|
357
|
-
this.log("debug", "No messages available (404), resetting pending to 0");
|
|
358
|
-
pending = 0;
|
|
359
|
-
schedulePull();
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
if (status === "408") {
|
|
363
|
-
this.log("debug", "Pull request timed out (408), resetting pending and pulling immediately");
|
|
364
|
-
pending = 0;
|
|
365
|
-
pull(maxMessages); // Immediately pull again instead of scheduling
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
if (status === "409") {
|
|
369
|
-
this.log("warn", "Max ack pending reached (409), waiting 5 seconds before retry");
|
|
370
|
-
pending = 0;
|
|
371
|
-
setTimeout(() => pull(maxMessages), 5000);
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
// Skip if not a real message
|
|
375
|
-
if (!msg.data || msg.data.length === 0 || msg.subject === inbox) {
|
|
376
|
-
this.log("debug", `Skipping non-message: data=${msg.data?.length || 0} bytes, subject=${msg.subject}`);
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
// We got a real message
|
|
380
|
-
pending = Math.max(0, pending - 1);
|
|
381
|
-
this.log("debug", `Message received, pending decreased to ${pending}`);
|
|
382
|
-
// Re-pull if below threshold
|
|
383
|
-
if (pending < threshold && !pulling) {
|
|
384
|
-
const pullCount = maxMessages - pending;
|
|
385
|
-
this.log("debug", `Pending ${pending} < threshold ${threshold}, pulling ${pullCount} more messages`);
|
|
386
|
-
pull(pullCount);
|
|
387
|
-
}
|
|
388
|
-
const parts = msg.subject.split(".");
|
|
389
|
-
const action = parts[parts.length - 1];
|
|
390
|
-
this.log("info", `↓ message: ${action} on ${msg.subject}`);
|
|
391
|
-
try {
|
|
392
|
-
switch (action) {
|
|
393
|
-
case "output": {
|
|
394
|
-
if (this.outputHandler) {
|
|
395
|
-
const output = (0, protobuf_1.fromBinary)(automations_pb_js_1.OutputSchema, msg.data);
|
|
396
|
-
this.outputHandler({
|
|
397
|
-
vertex: output.vertex,
|
|
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
|
-
this.log("warn", `Unknown message action: ${action}`);
|
|
421
|
-
}
|
|
422
|
-
// ACK the message
|
|
423
|
-
const ackSuccess = msg.respond();
|
|
424
|
-
if (!ackSuccess) {
|
|
425
|
-
this.log("warn", "Failed to ACK message");
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
this.log("debug", `Message ACKed successfully for action: ${action}`);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
catch (error) {
|
|
432
|
-
this.log("error", "Error handling message:", error);
|
|
433
|
-
// NAK the message for redelivery
|
|
434
|
-
const nakSuccess = msg.respond(new TextEncoder().encode("-NAK"));
|
|
435
|
-
if (!nakSuccess) {
|
|
436
|
-
this.log("warn", "Failed to NAK message");
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
this.log("debug", "Message NAKed for redelivery");
|
|
440
|
-
}
|
|
441
|
-
}
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
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));
|
|
442
421
|
}
|
|
422
|
+
break;
|
|
443
423
|
}
|
|
444
|
-
|
|
445
|
-
if (this.
|
|
446
|
-
|
|
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));
|
|
447
428
|
}
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
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
|
+
}
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
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
|
+
}
|
|
447
|
+
throw error;
|
|
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
|
+
}
|
|
455
|
+
}
|
|
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--;
|
|
448
470
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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);
|
|
452
474
|
}
|
|
453
|
-
|
|
454
|
-
|
|
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 || ""}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
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}`);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
this.pulling = true;
|
|
499
|
+
try {
|
|
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"}`);
|
|
455
516
|
}
|
|
456
517
|
catch (error) {
|
|
457
|
-
this.log("error", "
|
|
518
|
+
this.log("error", "Error sending pull request:", error);
|
|
458
519
|
throw error;
|
|
459
520
|
}
|
|
521
|
+
finally {
|
|
522
|
+
this.pulling = false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
schedulePull() {
|
|
526
|
+
if (this.pullTimer) {
|
|
527
|
+
clearTimeout(this.pullTimer);
|
|
528
|
+
}
|
|
529
|
+
if (this.connectionState !== ConnectionState.CONNECTED) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
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);
|
|
460
545
|
}
|
|
461
546
|
async clearSubscriptions() {
|
|
547
|
+
if (this.pullTimer) {
|
|
548
|
+
clearTimeout(this.pullTimer);
|
|
549
|
+
this.pullTimer = null;
|
|
550
|
+
}
|
|
462
551
|
if (!this.pullSubscription)
|
|
463
552
|
return;
|
|
464
553
|
this.log("debug", "Clearing subscription");
|
|
@@ -472,10 +561,27 @@ class PubsubClient {
|
|
|
472
561
|
this.pullSubscription = null;
|
|
473
562
|
}
|
|
474
563
|
async resubscribe() {
|
|
475
|
-
|
|
476
|
-
|
|
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();
|
|
477
572
|
this.log("info", "✓ Resubscribed successfully");
|
|
478
573
|
}
|
|
574
|
+
cleanupState() {
|
|
575
|
+
this.subject = null;
|
|
576
|
+
this.streamName = null;
|
|
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
|
+
}
|
|
479
585
|
async unsubscribe() {
|
|
480
586
|
if (this.connectionState === ConnectionState.DISCONNECTED ||
|
|
481
587
|
this.connectionState === ConnectionState.CLOSING) {
|
|
@@ -484,12 +590,7 @@ class PubsubClient {
|
|
|
484
590
|
this.log("info", "Starting unsubscribe process");
|
|
485
591
|
this.connectionState = ConnectionState.CLOSING;
|
|
486
592
|
await this.clearSubscriptions();
|
|
487
|
-
this.
|
|
488
|
-
this.sessionHandler = null;
|
|
489
|
-
this.urlHandler = null;
|
|
490
|
-
this.subject = null;
|
|
491
|
-
this.streamName = null;
|
|
492
|
-
this.consumerName = null;
|
|
593
|
+
this.cleanupState();
|
|
493
594
|
this.isReconnecting = false;
|
|
494
595
|
if (this.nc && !this.nc.isClosed()) {
|
|
495
596
|
try {
|
|
@@ -512,6 +613,7 @@ class PubsubClient {
|
|
|
512
613
|
}
|
|
513
614
|
}
|
|
514
615
|
exports.PubsubClient = PubsubClient;
|
|
616
|
+
// Helper functions
|
|
515
617
|
function validateSubject(subject) {
|
|
516
618
|
if (!subject || subject.includes(" ") || subject.includes("\t")) {
|
|
517
619
|
return false;
|
|
@@ -529,7 +631,6 @@ function parseOutputData(data) {
|
|
|
529
631
|
return [key, JSON.parse(new TextDecoder().decode(value))];
|
|
530
632
|
}
|
|
531
633
|
catch (error) {
|
|
532
|
-
// Note: We don't have access to the log method here, so we keep console.error
|
|
533
634
|
console.error(`Failed to parse data for key ${key}:`, error);
|
|
534
635
|
return [key, null];
|
|
535
636
|
}
|
|
@@ -546,4 +647,19 @@ function createAuthenticator(val) {
|
|
|
546
647
|
throw new Error("Invalid credentials format");
|
|
547
648
|
}
|
|
548
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
|
+
}
|
|
549
665
|
//# sourceMappingURL=index.js.map
|