@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 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 the new configuration
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.createSubscriptions(this.streamName, this.consumerName);
233
+ await this.createPullSubscription();
205
234
  }
206
235
  catch (error) {
207
236
  this.connectionState = ConnectionState.DISCONNECTED;
208
- this.subject = null;
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
- this.log("debug", `Unknown status event: ${status.type}`, status.data);
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 createSubscriptions(streamName, consumerName) {
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
- const jc = (0, nats_ws_1.JSONCodec)();
293
- const pullSubject = `$JS.API.CONSUMER.MSG.NEXT.${streamName}.${consumerName}`;
294
- // Create inbox with proper prefix - let the system generate the unique part
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
- // Track state
299
- let pending = 0;
300
- let pulling = false;
301
- const maxMessages = 100;
302
- const threshold = 50;
303
- const pollInterval = 1000; // 1 second poll interval
304
- // Pull messages with proper expiration
305
- const pull = async (batch) => {
306
- if (pulling)
307
- return;
308
- pulling = true;
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
- // Set expires to slightly longer than poll interval to avoid accumulation
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
- finally {
320
- pulling = false;
345
+ catch (error) {
346
+ this.log("error", "Error handling message:", error);
321
347
  }
322
- };
323
- // Initial pull
324
- this.log("debug", `Initial pull request for ${maxMessages} messages`);
325
- pull(maxMessages);
326
- // Set up periodic pulling
327
- let pullTimer = null;
328
- const schedulePull = () => {
329
- if (pullTimer)
330
- clearTimeout(pullTimer);
331
- pullTimer = setTimeout(() => {
332
- if (pending < threshold) {
333
- const pullCount = maxMessages - pending;
334
- this.log("debug", `Scheduled pull: pending=${pending} < threshold=${threshold}, pulling ${pullCount} more`);
335
- pull(pullCount);
336
- }
337
- else {
338
- this.log("debug", `Skipping scheduled pull: pending=${pending} >= threshold=${threshold}`);
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
- }, pollInterval);
341
- };
342
- // Process messages
343
- (async () => {
344
- try {
345
- for await (const msg of sub) {
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
- catch (error) {
445
- if (this.connectionState !== ConnectionState.CLOSING) {
446
- this.log("error", "Handler error:", error);
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
- finally {
450
- if (pullTimer)
451
- clearTimeout(pullTimer);
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
- this.log("info", "Pull subscription created successfully");
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", "Failed to create subscriptions:", 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
- // Only called when we have an active connection that needs resubscription
476
- await this.createSubscriptions(this.streamName, this.consumerName);
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.outputHandler = null;
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