@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 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
- 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);
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
- console.info("[PubSub] Subscribe called with:", {
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(`[PubSub] Invalid subject: "${opts.subject}"`);
180
+ throw new Error(`Invalid subject: "${opts.subject}"`);
147
181
  }
148
182
  if (!opts.consumer || !opts.stream) {
149
- throw new Error("[PubSub] Consumer and stream names are required");
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.nc &&
155
- !this.nc.isClosed()) {
156
- console.info("[PubSub] Already connected to same subject, updating handlers only");
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("[PubSub] Already connecting, please wait");
197
+ throw new Error("Already connecting, please wait");
164
198
  }
165
- // If connected to a different subject or need to reconnect, close first
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 the new configuration
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
- // Create authenticator
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
- console.info(`[PubSub] Connecting to NATS with inbox prefix: ${inboxPrefix}`);
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, // This ensures inbox isolation
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
- console.info("[PubSub] Connected successfully with JetStream");
198
- // Set up status monitoring
231
+ this.log("info", "Connected successfully");
199
232
  this.setupStatusMonitoring();
200
- // Create JetStream consumer subscription
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
- // Clean up on error
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
- console.error("[PubSub] Error closing connection after failed subscribe:", closeError);
243
+ this.log("error", "Error closing connection after failed subscribe:", closeError);
219
244
  }
220
245
  this.nc = null;
221
246
  }
222
- console.error("[PubSub] Failed to subscribe:", error);
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
- console.warn("[PubSub] ⚠ Disconnected", { data: status.data });
259
+ this.log("warn", "⚠ Disconnected", { data: status.data });
235
260
  break;
236
261
  case "reconnect":
237
- console.info("[PubSub] ↻ Reconnected");
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
- console.error("[PubSub] Failed to resubscribe after reconnection:", error);
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
- // Usually permissions errors
254
- console.error("[PubSub] Connection error:", status.data);
278
+ this.log("error", "Connection error:", status.data);
255
279
  break;
256
280
  case "update":
257
- // Cluster configuration update
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
- // Lame Duck Mode - server is requesting clients to reconnect
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
- console.debug("[PubSub] Attempting to reconnect...");
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
- console.warn("[PubSub] Connection is stale, may reconnect soon");
290
+ this.log("warn", "Connection is stale, may reconnect soon");
274
291
  break;
275
292
  default:
276
- console.debug(`[PubSub] 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
+ }
277
296
  }
278
297
  }
279
298
  }
280
299
  catch (error) {
281
300
  if (this.connectionState !== ConnectionState.CLOSING) {
282
- console.error("[PubSub] Fatal status handler error:", error);
301
+ this.log("error", "Fatal status handler error:", error);
283
302
  }
284
303
  }
285
304
  })();
286
305
  }
287
- async createSubscriptions(streamName, consumerName) {
288
- if (!this.nc || !this.js) {
289
- console.error("[PubSub] Cannot create subscriptions - no connection");
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
- console.info(`[PubSub] Creating pull subscription for consumer: ${consumerName} on stream: ${streamName}`);
303
- const jc = (0, nats_ws_1.JSONCodec)();
304
- const pullSubject = `$JS.API.CONSUMER.MSG.NEXT.${streamName}.${consumerName}`;
305
- // Create inbox with proper prefix - let NATS generate the unique part
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
- // Track state
310
- let pending = 0;
311
- let pulling = false;
312
- const maxMessages = 100;
313
- const threshold = 50;
314
- const pollInterval = 1000; // 1 second poll interval
315
- // Pull messages with proper expiration
316
- const pull = async (batch) => {
317
- if (pulling || !this.nc || this.nc.isClosed())
318
- return;
319
- 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
+ }
320
342
  try {
321
- // Set expires to slightly longer than poll interval to avoid accumulation
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
- finally {
330
- pulling = false;
345
+ catch (error) {
346
+ this.log("error", "Error handling message:", error);
331
347
  }
332
- };
333
- // Initial pull
334
- pull(maxMessages);
335
- // Set up periodic pulling
336
- let pullTimer = null;
337
- const schedulePull = () => {
338
- if (pullTimer)
339
- clearTimeout(pullTimer);
340
- pullTimer = setTimeout(() => {
341
- if (pending < threshold) {
342
- pull(maxMessages - pending);
343
- }
344
- }, pollInterval);
345
- };
346
- // Process messages
347
- (async () => {
348
- try {
349
- for await (const msg of sub) {
350
- if (this.connectionState === ConnectionState.CLOSING ||
351
- this.connectionState === ConnectionState.DISCONNECTED) {
352
- console.info("[PubSub] Subscription closing");
353
- if (pullTimer)
354
- clearTimeout(pullTimer);
355
- break;
356
- }
357
- // Check status header
358
- const status = msg.headers?.get("Status");
359
- if (status === "404") {
360
- // No messages available
361
- console.debug("[PubSub] No messages available (404)");
362
- pending = 0;
363
- schedulePull();
364
- continue;
365
- }
366
- if (status === "408") {
367
- console.debug("[PubSub] Pull request timed out (408)");
368
- pending = 0;
369
- schedulePull();
370
- continue;
371
- }
372
- if (status === "409") {
373
- console.warn("[PubSub] Max ack pending reached (409)");
374
- pending = 0;
375
- setTimeout(() => pull(maxMessages), 5000);
376
- continue;
377
- }
378
- // Skip if not a real message
379
- if (!msg.data || msg.data.length === 0 || msg.subject === inbox) {
380
- continue;
381
- }
382
- // We got a real message
383
- pending = Math.max(0, pending - 1);
384
- // Re-pull if below threshold
385
- if (pending < threshold && !pulling) {
386
- pull(maxMessages - pending);
387
- }
388
- const parts = msg.subject.split(".");
389
- const action = parts[parts.length - 1];
390
- console.info(`[PubSub] ↓ 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
- 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
- catch (error) {
435
- if (this.connectionState !== ConnectionState.CLOSING) {
436
- console.error("[PubSub] Handler error:", error);
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
- finally {
440
- if (pullTimer)
441
- clearTimeout(pullTimer);
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
- console.info("[PubSub] Pull subscription created successfully");
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
- console.error("[PubSub] Failed to create subscriptions:", 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
+ }
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 clearSubscriptions() {
452
- if (this.pullSubscription) {
453
- console.info("[PubSub] Clearing subscription");
454
- try {
455
- this.pullSubscription.unsubscribe();
456
- console.info("[PubSub] Subscription cleared");
457
- }
458
- catch (error) {
459
- console.error("[PubSub] Error clearing subscription:", error);
460
- }
461
- this.pullSubscription = null;
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 resubscribe() {
465
- if (!this.subject ||
466
- !this.streamName ||
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
- await this.createSubscriptions(this.streamName, this.consumerName);
474
- console.info("[PubSub] Resubscribed successfully");
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
- console.error("[PubSub] Resubscription failed:", error);
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
- async unsubscribe() {
482
- if (this.connectionState === ConnectionState.DISCONNECTED) {
483
- console.info("[PubSub] Already disconnected");
525
+ schedulePull() {
526
+ if (this.pullTimer) {
527
+ clearTimeout(this.pullTimer);
528
+ }
529
+ if (this.connectionState !== ConnectionState.CONNECTED) {
484
530
  return;
485
531
  }
486
- if (this.connectionState === ConnectionState.CLOSING) {
487
- console.info("[PubSub] Already closing");
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
- console.info("[PubSub] Starting unsubscribe process");
491
- this.connectionState = ConnectionState.CLOSING;
492
- // Clear subscriptions first
493
- await this.clearSubscriptions();
494
- // Clear handlers
495
- this.outputHandler = null;
496
- this.sessionHandler = null;
497
- this.urlHandler = null;
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.js = null;
503
- // Close connection
504
- if (this.nc) {
595
+ if (this.nc && !this.nc.isClosed()) {
505
596
  try {
506
- if (!this.nc.isClosed()) {
507
- await this.nc.close();
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
- console.error("[PubSub] Error closing connection:", error);
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
- console.info("[PubSub] Unsubscribed successfully");
606
+ this.log("info", "Unsubscribed successfully");
520
607
  }
521
- // Helper method to check connection status
522
608
  isConnected() {
523
- return (this.connectionState === ConnectionState.CONNECTED &&
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(`[PubSub] Failed to parse data for key ${key}:`, 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