@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/esm/index.js CHANGED
@@ -2,8 +2,26 @@ import { fromBinary, toJson } from "@bufbuild/protobuf";
2
2
  import { timestampDate } from "@bufbuild/protobuf/wkt";
3
3
  import { connect, createInbox, credsAuthenticator, JSONCodec, } from "nats.ws";
4
4
  import { OutputSchema, SessionSchema, UrlSchema, } from "./genproto/mochabugapis/adapt/automations/v1/automations_pb.js";
5
+ // Constants for JetStream protocol
6
+ const CONSUMER_MSG_NEXT_PREFIX = "$JS.API.CONSUMER.MSG.NEXT.";
7
+ const ACK_POSITIVE = new TextEncoder().encode("+ACK");
8
+ const ACK_NEGATIVE = new TextEncoder().encode("-NAK");
9
+ //const ACK_PREFIX = "$JS.ACK.";
10
+ //const ACK_IN_PROGRESS = new TextEncoder().encode("+WPI");
11
+ //const ACK_TERMINATE = new TextEncoder().encode("+TERM");
12
+ // JetStream Status Codes
13
+ const STATUS_NO_MESSAGES = "404";
14
+ const STATUS_REQUEST_TIMEOUT = "408";
15
+ const STATUS_MAX_ACK_PENDING = "409";
16
+ const STATUS_FLOW_CONTROL = "100";
17
+ // Default configuration
18
+ const DEFAULT_MAX_MESSAGES = 100;
19
+ const DEFAULT_EXPIRES_MS = 30000;
20
+ const DEFAULT_POLL_INTERVAL_MS = 1000;
21
+ const DEFAULT_THRESHOLD_RATIO = 0.5;
5
22
  const natsUrl = "wss://adapt-dev.mochabugapis.com:443/pubsub";
6
23
  const baseUrl = "https://adapt-dev.mochabugapis.com/v1/automations";
24
+ // REST API functions (unchanged from original)
7
25
  export async function startSession(req, token) {
8
26
  const headers = new Headers({
9
27
  "Content-Type": "application/json",
@@ -96,6 +114,7 @@ export class RestClientError extends Error {
96
114
  this.response = response;
97
115
  }
98
116
  }
117
+ // Connection states
99
118
  var ConnectionState;
100
119
  (function (ConnectionState) {
101
120
  ConnectionState["DISCONNECTED"] = "disconnected";
@@ -115,15 +134,17 @@ export class PubsubClient {
115
134
  this.urlHandler = null;
116
135
  this.isReconnecting = false;
117
136
  this.connectionState = ConnectionState.DISCONNECTED;
137
+ // Pull consumer state
138
+ this.pending = 0;
139
+ this.pulling = false;
140
+ this.pullTimer = null;
141
+ this.maxMessages = DEFAULT_MAX_MESSAGES;
142
+ this.expiresMs = DEFAULT_EXPIRES_MS;
143
+ this.pollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
144
+ this.threshold = DEFAULT_MAX_MESSAGES * DEFAULT_THRESHOLD_RATIO;
145
+ this.inbox = null;
146
+ this.jc = JSONCodec();
118
147
  this.debug = debug;
119
- this.subscribe = this.subscribe.bind(this);
120
- this.unsubscribe = this.unsubscribe.bind(this);
121
- this.createSubscriptions = this.createSubscriptions.bind(this);
122
- this.clearSubscriptions = this.clearSubscriptions.bind(this);
123
- this.resubscribe = this.resubscribe.bind(this);
124
- this.setupStatusMonitoring = this.setupStatusMonitoring.bind(this);
125
- this.isConnected = this.isConnected.bind(this);
126
- this.getConnectionState = this.getConnectionState.bind(this);
127
148
  }
128
149
  log(level, message, data) {
129
150
  if (!this.debug && level === "debug")
@@ -152,7 +173,9 @@ export class PubsubClient {
152
173
  }
153
174
  // If we're already connected to the same subject, just update handlers
154
175
  if (this.connectionState === ConnectionState.CONNECTED &&
155
- this.subject === opts.subject) {
176
+ this.subject === opts.subject &&
177
+ this.streamName === opts.stream &&
178
+ this.consumerName === opts.consumer) {
156
179
  this.log("debug", "Already connected to same subject, updating handlers only");
157
180
  this.outputHandler = opts.onOutput || null;
158
181
  this.sessionHandler = opts.onSession || null;
@@ -169,13 +192,18 @@ export class PubsubClient {
169
192
  }
170
193
  try {
171
194
  this.connectionState = ConnectionState.CONNECTING;
172
- // Store the new configuration
195
+ // Store configuration
173
196
  this.subject = opts.subject;
174
197
  this.streamName = opts.stream;
175
198
  this.consumerName = opts.consumer;
176
199
  this.outputHandler = opts.onOutput || null;
177
200
  this.sessionHandler = opts.onSession || null;
178
201
  this.urlHandler = opts.onUrl || null;
202
+ this.maxMessages = opts.maxMessages || DEFAULT_MAX_MESSAGES;
203
+ this.expiresMs = opts.expiresMs || DEFAULT_EXPIRES_MS;
204
+ this.pollIntervalMs = opts.pollIntervalMs || DEFAULT_POLL_INTERVAL_MS;
205
+ this.threshold = Math.floor(this.maxMessages * DEFAULT_THRESHOLD_RATIO);
206
+ // Connect to NATS
179
207
  const inboxPrefix = `_INBOX_${opts.consumer}`;
180
208
  this.log("debug", `Connecting with inbox prefix: ${inboxPrefix}`);
181
209
  this.nc = await connect({
@@ -191,16 +219,11 @@ export class PubsubClient {
191
219
  this.connectionState = ConnectionState.CONNECTED;
192
220
  this.log("info", "Connected successfully");
193
221
  this.setupStatusMonitoring();
194
- await this.createSubscriptions(this.streamName, this.consumerName);
222
+ await this.createPullSubscription();
195
223
  }
196
224
  catch (error) {
197
225
  this.connectionState = ConnectionState.DISCONNECTED;
198
- this.subject = null;
199
- this.streamName = null;
200
- this.consumerName = null;
201
- this.outputHandler = null;
202
- this.sessionHandler = null;
203
- this.urlHandler = null;
226
+ this.cleanupState();
204
227
  if (this.nc) {
205
228
  try {
206
229
  await this.nc.close();
@@ -215,6 +238,8 @@ export class PubsubClient {
215
238
  }
216
239
  }
217
240
  setupStatusMonitoring() {
241
+ if (!this.nc)
242
+ return;
218
243
  (async () => {
219
244
  try {
220
245
  for await (const status of this.nc.status()) {
@@ -239,32 +264,24 @@ export class PubsubClient {
239
264
  }
240
265
  break;
241
266
  case "error":
242
- // Usually permissions errors
243
267
  this.log("error", "Connection error:", status.data);
244
268
  break;
245
269
  case "update":
246
- // Cluster configuration update
247
270
  this.log("debug", "Cluster update received:", status.data);
248
271
  break;
249
272
  case "ldm":
250
- // Lame Duck Mode - server is requesting clients to reconnect
251
273
  this.log("warn", "Server requesting reconnection (LDM)");
252
274
  break;
253
- // Debug events - usually can be safely ignored but useful for debugging
254
275
  case "reconnecting":
255
276
  this.log("debug", "Attempting to reconnect...");
256
277
  break;
257
- case "pingTimer":
258
- // Ping timer events - very verbose, only log if debug is enabled
259
- if (this.debug) {
260
- this.log("debug", "Ping timer event");
261
- }
262
- break;
263
278
  case "staleConnection":
264
279
  this.log("warn", "Connection is stale, may reconnect soon");
265
280
  break;
266
281
  default:
267
- this.log("debug", `Unknown status event: ${status.type}`, status.data);
282
+ if (status.type !== "pingTimer" || this.debug) {
283
+ this.log("debug", `Status event: ${status.type}`, status.data);
284
+ }
268
285
  }
269
286
  }
270
287
  }
@@ -275,180 +292,251 @@ export class PubsubClient {
275
292
  }
276
293
  })();
277
294
  }
278
- async createSubscriptions(streamName, consumerName) {
295
+ async createPullSubscription() {
296
+ if (!this.nc || !this.streamName || !this.consumerName) {
297
+ throw new Error("Not properly initialized");
298
+ }
279
299
  await this.clearSubscriptions();
280
300
  try {
281
- this.log("info", `Creating pull subscription for consumer: ${consumerName} on stream: ${streamName}`);
282
- const jc = JSONCodec();
283
- const pullSubject = `$JS.API.CONSUMER.MSG.NEXT.${streamName}.${consumerName}`;
284
- // Create inbox with proper prefix - let the system generate the unique part
285
- const inbox = createInbox(`_INBOX_${consumerName}`);
286
- const sub = this.nc.subscribe(inbox);
301
+ this.log("info", `Creating pull subscription for consumer: ${this.consumerName} on stream: ${this.streamName}`);
302
+ // Create inbox for receiving messages
303
+ this.inbox = createInbox(`_INBOX_${this.consumerName}`);
304
+ const sub = this.nc.subscribe(this.inbox);
287
305
  this.pullSubscription = sub;
288
- // Track state
289
- let pending = 0;
290
- let pulling = false;
291
- const maxMessages = 100;
292
- const threshold = 50;
293
- const pollInterval = 1000; // 1 second poll interval
294
- // Pull messages with proper expiration
295
- const pull = async (batch) => {
296
- if (pulling)
297
- return;
298
- pulling = true;
306
+ // Reset state
307
+ this.pending = 0;
308
+ this.pulling = false;
309
+ // Start message processing loop
310
+ this.processMessages(sub);
311
+ // Initial pull - use no_wait to check for immediate messages
312
+ this.log("debug", `Initial no_wait pull for ${this.maxMessages} messages`);
313
+ await this.pull(this.maxMessages, true);
314
+ // Schedule periodic pulls for long polling
315
+ this.schedulePull();
316
+ this.log("info", "Pull subscription created successfully");
317
+ }
318
+ catch (error) {
319
+ this.log("error", "Failed to create subscriptions:", error);
320
+ throw error;
321
+ }
322
+ }
323
+ async processMessages(sub) {
324
+ try {
325
+ for await (const msg of sub) {
326
+ if (this.connectionState === ConnectionState.CLOSING ||
327
+ this.connectionState === ConnectionState.DISCONNECTED) {
328
+ this.log("info", "Subscription closing");
329
+ break;
330
+ }
299
331
  try {
300
- // Set expires to slightly longer than poll interval to avoid accumulation
301
- const request = {
302
- batch,
303
- expires: (pollInterval + 500) * 1000000, // Poll interval + 500ms buffer, in nanoseconds
304
- };
305
- this.nc.publish(pullSubject, jc.encode(request), { reply: inbox });
306
- pending += batch;
307
- this.log("debug", `Pull request sent: batch=${batch}, pending=${pending}, expires=${request.expires}ns`);
332
+ await this.handleMessage(msg);
308
333
  }
309
- finally {
310
- pulling = false;
334
+ catch (error) {
335
+ this.log("error", "Error handling message:", error);
311
336
  }
312
- };
313
- // Initial pull
314
- this.log("debug", `Initial pull request for ${maxMessages} messages`);
315
- pull(maxMessages);
316
- // Set up periodic pulling
317
- let pullTimer = null;
318
- const schedulePull = () => {
319
- if (pullTimer)
320
- clearTimeout(pullTimer);
321
- pullTimer = setTimeout(() => {
322
- if (pending < threshold) {
323
- const pullCount = maxMessages - pending;
324
- this.log("debug", `Scheduled pull: pending=${pending} < threshold=${threshold}, pulling ${pullCount} more`);
325
- pull(pullCount);
326
- }
327
- else {
328
- this.log("debug", `Skipping scheduled pull: pending=${pending} >= threshold=${threshold}`);
337
+ }
338
+ }
339
+ catch (error) {
340
+ if (this.connectionState !== ConnectionState.CLOSING) {
341
+ this.log("error", "Message processing error:", error);
342
+ }
343
+ }
344
+ }
345
+ async handleMessage(msg) {
346
+ this.log("debug", "header", msg.headers);
347
+ const status = msg.headers?.get("Status");
348
+ const description = msg.headers?.get("Description");
349
+ if (status) {
350
+ this.log("debug", `Received status message: ${status} ${description || ""}`);
351
+ await this.handleStatusMessage(status, description, msg);
352
+ return;
353
+ }
354
+ // Check for flow control messages (also may have empty data)
355
+ const flowControl = msg.headers?.get("Nats-Msg-Size");
356
+ if (flowControl === "FlowControl") {
357
+ this.log("debug", "Received flow control request, responding");
358
+ msg.respond();
359
+ return;
360
+ }
361
+ // Alternative flow control check
362
+ if (msg.headers) {
363
+ const headers = Array.from(msg.headers.keys());
364
+ if (headers.some((h) => h.includes("FlowControl"))) {
365
+ this.log("debug", "Received flow control message, responding");
366
+ if (msg.reply) {
367
+ msg.respond();
368
+ }
369
+ return;
370
+ }
371
+ }
372
+ // Now check if this is an actual data message
373
+ // Status and flow control messages will have already been handled above
374
+ if (!msg.data || msg.data.length === 0) {
375
+ // This should be rare now - log with more detail for debugging
376
+ this.log("debug", `Received empty message on ${msg.subject}, headers: ${msg.headers ? Array.from(msg.headers.keys()).join(", ") : "none"}`);
377
+ return;
378
+ }
379
+ // Get the actual stream subject from headers
380
+ const streamSubject = msg.headers?.get("Nats-Subject");
381
+ if (!streamSubject) {
382
+ this.log("debug", "Message without stream subject, skipping");
383
+ return;
384
+ }
385
+ // We got a real message
386
+ this.pending = Math.max(0, this.pending - 1);
387
+ this.log("debug", `Message received on subject ${streamSubject}, pending decreased to ${this.pending}`);
388
+ // Process the message
389
+ const parts = streamSubject.split(".");
390
+ const action = parts[parts.length - 1];
391
+ this.log("info", `↓ message: ${action} on ${streamSubject}`);
392
+ try {
393
+ switch (action) {
394
+ case "output": {
395
+ if (this.outputHandler) {
396
+ const output = fromBinary(OutputSchema, msg.data);
397
+ this.outputHandler({
398
+ vertex: output.vertex,
399
+ fork: output.fork,
400
+ data: parseOutputData(output.data),
401
+ created: timestampDate(output.created),
402
+ });
329
403
  }
330
- }, pollInterval);
331
- };
332
- // Process messages
333
- (async () => {
334
- try {
335
- for await (const msg of sub) {
336
- if (this.connectionState === ConnectionState.CLOSING ||
337
- this.connectionState === ConnectionState.DISCONNECTED) {
338
- this.log("info", "Subscription closing");
339
- if (pullTimer)
340
- clearTimeout(pullTimer);
341
- break;
342
- }
343
- // Check status header
344
- const status = msg.headers?.get("Status");
345
- if (status === "404") {
346
- // No messages available
347
- this.log("debug", "No messages available (404), resetting pending to 0");
348
- pending = 0;
349
- schedulePull();
350
- continue;
351
- }
352
- if (status === "408") {
353
- this.log("debug", "Pull request timed out (408), resetting pending and pulling immediately");
354
- pending = 0;
355
- pull(maxMessages); // Immediately pull again instead of scheduling
356
- continue;
357
- }
358
- if (status === "409") {
359
- this.log("warn", "Max ack pending reached (409), waiting 5 seconds before retry");
360
- pending = 0;
361
- setTimeout(() => pull(maxMessages), 5000);
362
- continue;
363
- }
364
- // Skip if not a real message
365
- if (!msg.data || msg.data.length === 0 || msg.subject === inbox) {
366
- this.log("debug", `Skipping non-message: data=${msg.data?.length || 0} bytes, subject=${msg.subject}`);
367
- continue;
368
- }
369
- // We got a real message
370
- pending = Math.max(0, pending - 1);
371
- this.log("debug", `Message received, pending decreased to ${pending}`);
372
- // Re-pull if below threshold
373
- if (pending < threshold && !pulling) {
374
- const pullCount = maxMessages - pending;
375
- this.log("debug", `Pending ${pending} < threshold ${threshold}, pulling ${pullCount} more messages`);
376
- pull(pullCount);
377
- }
378
- const parts = msg.subject.split(".");
379
- const action = parts[parts.length - 1];
380
- this.log("info", `↓ message: ${action} on ${msg.subject}`);
381
- try {
382
- switch (action) {
383
- case "output": {
384
- if (this.outputHandler) {
385
- const output = fromBinary(OutputSchema, msg.data);
386
- this.outputHandler({
387
- vertex: output.vertex,
388
- fork: output.fork,
389
- data: parseOutputData(output.data),
390
- created: timestampDate(output.created),
391
- });
392
- }
393
- break;
394
- }
395
- case "session": {
396
- if (this.sessionHandler) {
397
- const session = fromBinary(SessionSchema, msg.data);
398
- this.sessionHandler(toJson(SessionSchema, session));
399
- }
400
- break;
401
- }
402
- case "url": {
403
- if (this.urlHandler) {
404
- const url = fromBinary(UrlSchema, msg.data);
405
- this.urlHandler(toJson(UrlSchema, url));
406
- }
407
- break;
408
- }
409
- default:
410
- this.log("warn", `Unknown message action: ${action}`);
411
- }
412
- // ACK the message
413
- const ackSuccess = msg.respond();
414
- if (!ackSuccess) {
415
- this.log("warn", "Failed to ACK message");
416
- }
417
- else {
418
- this.log("debug", `Message ACKed successfully for action: ${action}`);
419
- }
420
- }
421
- catch (error) {
422
- this.log("error", "Error handling message:", error);
423
- // NAK the message for redelivery
424
- const nakSuccess = msg.respond(new TextEncoder().encode("-NAK"));
425
- if (!nakSuccess) {
426
- this.log("warn", "Failed to NAK message");
427
- }
428
- else {
429
- this.log("debug", "Message NAKed for redelivery");
430
- }
431
- }
404
+ break;
405
+ }
406
+ case "session": {
407
+ if (this.sessionHandler) {
408
+ const session = fromBinary(SessionSchema, msg.data);
409
+ this.sessionHandler(toJson(SessionSchema, session));
432
410
  }
411
+ break;
433
412
  }
434
- catch (error) {
435
- if (this.connectionState !== ConnectionState.CLOSING) {
436
- this.log("error", "Handler error:", error);
413
+ case "url": {
414
+ if (this.urlHandler) {
415
+ const url = fromBinary(UrlSchema, msg.data);
416
+ this.urlHandler(toJson(UrlSchema, url));
437
417
  }
418
+ break;
419
+ }
420
+ default:
421
+ this.log("warn", `Unknown message action: ${action}`);
422
+ }
423
+ // ACK the message
424
+ if (msg.reply) {
425
+ msg.respond(ACK_POSITIVE);
426
+ this.log("debug", `Message ACKed successfully for action: ${action}`);
427
+ }
428
+ }
429
+ catch (error) {
430
+ this.log("error", "Error processing message:", error);
431
+ // NAK the message for redelivery
432
+ if (msg.reply) {
433
+ msg.respond(ACK_NEGATIVE);
434
+ this.log("debug", "Message NAKed for redelivery");
435
+ }
436
+ throw error;
437
+ }
438
+ // Pull more if below threshold
439
+ if (this.pending < this.threshold && !this.pulling) {
440
+ const pullCount = this.maxMessages - this.pending;
441
+ this.log("debug", `Pending ${this.pending} < threshold ${this.threshold}, pulling ${pullCount} more messages`);
442
+ this.pull(pullCount, false);
443
+ }
444
+ }
445
+ async handleStatusMessage(status, description, msg) {
446
+ switch (status) {
447
+ case STATUS_NO_MESSAGES:
448
+ this.log("debug", `No messages available (404): ${description || "No Messages"}`);
449
+ this.pending = 0;
450
+ // This is expected when using no_wait or when there are no messages
451
+ // Don't immediately pull again, let the scheduler handle it
452
+ break;
453
+ case STATUS_REQUEST_TIMEOUT:
454
+ this.log("debug", `Pull request expired (408): ${description || "Request Timeout"}`);
455
+ // This can happen for each unfilled slot in a batch
456
+ // Decrement pending for each 408 received
457
+ if (this.pending > 0) {
458
+ this.pending--;
438
459
  }
439
- finally {
440
- if (pullTimer)
441
- clearTimeout(pullTimer);
460
+ // Don't immediately pull, let threshold logic handle it
461
+ if (this.pending < this.threshold && !this.pulling) {
462
+ await this.pull(this.maxMessages - this.pending, false);
442
463
  }
443
- })();
444
- this.log("info", "Pull subscription created successfully");
464
+ break;
465
+ case STATUS_MAX_ACK_PENDING:
466
+ this.log("warn", `Max ack pending reached (409): ${description || "Exceeded MaxAckPending"}`);
467
+ this.pending = 0;
468
+ // Wait before retry
469
+ setTimeout(() => this.pull(this.maxMessages, false), 5000);
470
+ break;
471
+ case STATUS_FLOW_CONTROL:
472
+ this.log("debug", `Received heartbeat/flow control (100): ${description || ""}`);
473
+ // Respond if needed
474
+ if (msg.reply) {
475
+ msg.respond();
476
+ }
477
+ break;
478
+ default:
479
+ this.log("debug", `Received unknown status: ${status} - ${description || ""}`);
480
+ }
481
+ }
482
+ async pull(batch, noWait = false) {
483
+ if (this.pulling || !this.nc || !this.inbox || batch <= 0) {
484
+ this.log("debug", `Skipping pull: pulling=${this.pulling}, batch=${batch}`);
485
+ return;
486
+ }
487
+ this.pulling = true;
488
+ try {
489
+ const pullSubject = `${CONSUMER_MSG_NEXT_PREFIX}${this.streamName}.${this.consumerName}`;
490
+ const request = {
491
+ batch,
492
+ };
493
+ if (noWait) {
494
+ request.no_wait = true;
495
+ }
496
+ else if (this.expiresMs > 0) {
497
+ // Convert milliseconds to nanoseconds
498
+ request.expires = this.expiresMs * 1000000;
499
+ }
500
+ this.nc.publish(pullSubject, this.jc.encode(request), {
501
+ reply: this.inbox,
502
+ });
503
+ this.pending += batch;
504
+ this.log("debug", `Pull request sent: batch=${batch}, pending=${this.pending}, noWait=${noWait}, expires=${request.expires ? `${this.expiresMs}ms` : "none"}`);
445
505
  }
446
506
  catch (error) {
447
- this.log("error", "Failed to create subscriptions:", error);
507
+ this.log("error", "Error sending pull request:", error);
448
508
  throw error;
449
509
  }
510
+ finally {
511
+ this.pulling = false;
512
+ }
513
+ }
514
+ schedulePull() {
515
+ if (this.pullTimer) {
516
+ clearTimeout(this.pullTimer);
517
+ }
518
+ if (this.connectionState !== ConnectionState.CONNECTED) {
519
+ return;
520
+ }
521
+ this.pullTimer = setTimeout(() => {
522
+ if (this.pending < this.threshold && !this.pulling) {
523
+ const pullCount = this.maxMessages - this.pending;
524
+ this.log("debug", `Scheduled pull: pending=${this.pending} < threshold=${this.threshold}, pulling ${pullCount} more`);
525
+ // For scheduled pulls, use long polling (with expires)
526
+ this.pull(pullCount, false);
527
+ }
528
+ else {
529
+ this.log("debug", `Skipping scheduled pull: pending=${this.pending} >= threshold=${this.threshold} or pulling=${this.pulling}`);
530
+ }
531
+ // Reschedule
532
+ this.schedulePull();
533
+ }, this.pollIntervalMs);
450
534
  }
451
535
  async clearSubscriptions() {
536
+ if (this.pullTimer) {
537
+ clearTimeout(this.pullTimer);
538
+ this.pullTimer = null;
539
+ }
452
540
  if (!this.pullSubscription)
453
541
  return;
454
542
  this.log("debug", "Clearing subscription");
@@ -462,10 +550,27 @@ export class PubsubClient {
462
550
  this.pullSubscription = null;
463
551
  }
464
552
  async resubscribe() {
465
- // Only called when we have an active connection that needs resubscription
466
- await this.createSubscriptions(this.streamName, this.consumerName);
553
+ if (!this.streamName || !this.consumerName)
554
+ return;
555
+ this.log("info", "Resubscribing after reconnection");
556
+ // Clear old state
557
+ this.pending = 0;
558
+ this.pulling = false;
559
+ // Recreate subscription
560
+ await this.createPullSubscription();
467
561
  this.log("info", "✓ Resubscribed successfully");
468
562
  }
563
+ cleanupState() {
564
+ this.subject = null;
565
+ this.streamName = null;
566
+ this.consumerName = null;
567
+ this.outputHandler = null;
568
+ this.sessionHandler = null;
569
+ this.urlHandler = null;
570
+ this.inbox = null;
571
+ this.pending = 0;
572
+ this.pulling = false;
573
+ }
469
574
  async unsubscribe() {
470
575
  if (this.connectionState === ConnectionState.DISCONNECTED ||
471
576
  this.connectionState === ConnectionState.CLOSING) {
@@ -474,12 +579,7 @@ export class PubsubClient {
474
579
  this.log("info", "Starting unsubscribe process");
475
580
  this.connectionState = ConnectionState.CLOSING;
476
581
  await this.clearSubscriptions();
477
- this.outputHandler = null;
478
- this.sessionHandler = null;
479
- this.urlHandler = null;
480
- this.subject = null;
481
- this.streamName = null;
482
- this.consumerName = null;
582
+ this.cleanupState();
483
583
  this.isReconnecting = false;
484
584
  if (this.nc && !this.nc.isClosed()) {
485
585
  try {
@@ -501,6 +601,7 @@ export class PubsubClient {
501
601
  return this.connectionState;
502
602
  }
503
603
  }
604
+ // Helper functions
504
605
  function validateSubject(subject) {
505
606
  if (!subject || subject.includes(" ") || subject.includes("\t")) {
506
607
  return false;
@@ -518,7 +619,6 @@ function parseOutputData(data) {
518
619
  return [key, JSON.parse(new TextDecoder().decode(value))];
519
620
  }
520
621
  catch (error) {
521
- // Note: We don't have access to the log method here, so we keep console.error
522
622
  console.error(`Failed to parse data for key ${key}:`, error);
523
623
  return [key, null];
524
624
  }
@@ -535,4 +635,19 @@ function createAuthenticator(val) {
535
635
  throw new Error("Invalid credentials format");
536
636
  }
537
637
  }
638
+ // Usage example
639
+ export async function createPubsubClient(credentials, automation, handlers, options) {
640
+ const client = new PubsubClient(options?.debug);
641
+ await client.subscribe({
642
+ credentials,
643
+ subject: `automations.${automation.organization}.${automation.group}.${automation.automation}.>`,
644
+ stream: "automations",
645
+ consumer: `${automation.automation}-consumer`,
646
+ onOutput: handlers.onOutput,
647
+ onSession: handlers.onSession,
648
+ onUrl: handlers.onUrl,
649
+ ...options,
650
+ });
651
+ return client;
652
+ }
538
653
  //# sourceMappingURL=index.js.map