@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/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";
@@ -104,9 +123,8 @@ var ConnectionState;
104
123
  ConnectionState["CLOSING"] = "closing";
105
124
  })(ConnectionState || (ConnectionState = {}));
106
125
  export class PubsubClient {
107
- constructor() {
126
+ constructor(debug = false) {
108
127
  this.nc = null;
109
- this.js = null;
110
128
  this.pullSubscription = null;
111
129
  this.subject = null;
112
130
  this.streamName = null;
@@ -116,100 +134,106 @@ export class PubsubClient {
116
134
  this.urlHandler = null;
117
135
  this.isReconnecting = false;
118
136
  this.connectionState = ConnectionState.DISCONNECTED;
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);
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();
147
+ this.debug = debug;
148
+ }
149
+ log(level, message, data) {
150
+ if (!this.debug && level === "debug")
151
+ return;
152
+ const prefix = `[PubSub]`;
153
+ const fullMessage = `${prefix} ${message}`;
154
+ if (data !== undefined) {
155
+ console[level](fullMessage, data);
156
+ }
157
+ else {
158
+ console[level](fullMessage);
159
+ }
127
160
  }
128
161
  async subscribe(opts) {
129
- console.info("[PubSub] Subscribe called with:", {
162
+ this.log("info", "Subscribe called with:", {
130
163
  subject: opts.subject,
131
164
  consumer: opts.consumer,
132
165
  stream: opts.stream,
133
166
  });
134
167
  // Validate inputs
135
168
  if (!validateSubject(opts.subject)) {
136
- throw new Error(`[PubSub] Invalid subject: "${opts.subject}"`);
169
+ throw new Error(`Invalid subject: "${opts.subject}"`);
137
170
  }
138
171
  if (!opts.consumer || !opts.stream) {
139
- throw new Error("[PubSub] Consumer and stream names are required");
172
+ throw new Error("Consumer and stream names are required");
140
173
  }
141
174
  // If we're already connected to the same subject, just update handlers
142
175
  if (this.connectionState === ConnectionState.CONNECTED &&
143
176
  this.subject === opts.subject &&
144
- this.nc &&
145
- !this.nc.isClosed()) {
146
- console.info("[PubSub] Already connected to same subject, updating handlers only");
177
+ this.streamName === opts.stream &&
178
+ this.consumerName === opts.consumer) {
179
+ this.log("debug", "Already connected to same subject, updating handlers only");
147
180
  this.outputHandler = opts.onOutput || null;
148
181
  this.sessionHandler = opts.onSession || null;
149
182
  this.urlHandler = opts.onUrl || null;
150
183
  return;
151
184
  }
152
185
  if (this.connectionState === ConnectionState.CONNECTING) {
153
- throw new Error("[PubSub] Already connecting, please wait");
186
+ throw new Error("Already connecting, please wait");
154
187
  }
155
- // If connected to a different subject or need to reconnect, close first
188
+ // If connected to a different subject, close first
156
189
  if (this.connectionState === ConnectionState.CONNECTED ||
157
190
  this.connectionState === ConnectionState.CLOSING) {
158
- console.info("[PubSub] Closing existing connection before new subscription");
159
191
  await this.unsubscribe();
160
192
  }
161
193
  try {
162
194
  this.connectionState = ConnectionState.CONNECTING;
163
- // Store the new configuration
195
+ // Store configuration
164
196
  this.subject = opts.subject;
165
197
  this.streamName = opts.stream;
166
198
  this.consumerName = opts.consumer;
167
199
  this.outputHandler = opts.onOutput || null;
168
200
  this.sessionHandler = opts.onSession || null;
169
201
  this.urlHandler = opts.onUrl || null;
170
- // Create authenticator
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
171
207
  const inboxPrefix = `_INBOX_${opts.consumer}`;
172
- console.info(`[PubSub] Connecting to NATS with inbox prefix: ${inboxPrefix}`);
208
+ this.log("debug", `Connecting with inbox prefix: ${inboxPrefix}`);
173
209
  this.nc = await connect({
174
210
  servers: [natsUrl],
175
211
  authenticator: createAuthenticator(opts.credentials),
176
- inboxPrefix: inboxPrefix, // This ensures inbox isolation
212
+ inboxPrefix: inboxPrefix,
177
213
  reconnect: true,
178
214
  noEcho: true,
179
215
  maxReconnectAttempts: -1,
180
216
  reconnectTimeWait: 1000,
181
217
  pingInterval: 10 * 1000,
182
218
  });
183
- // Get JetStream context
184
- this.js = this.nc.jetstream();
185
- // Connection successful
186
219
  this.connectionState = ConnectionState.CONNECTED;
187
- console.info("[PubSub] Connected successfully with JetStream");
188
- // Set up status monitoring
220
+ this.log("info", "Connected successfully");
189
221
  this.setupStatusMonitoring();
190
- // Create JetStream consumer subscription
191
- await this.createSubscriptions(this.streamName, this.consumerName);
222
+ await this.createPullSubscription();
192
223
  }
193
224
  catch (error) {
194
225
  this.connectionState = ConnectionState.DISCONNECTED;
195
- // Clean up on error
196
- this.subject = null;
197
- this.streamName = null;
198
- this.consumerName = null;
199
- this.outputHandler = null;
200
- this.sessionHandler = null;
201
- this.urlHandler = null;
202
- this.js = null;
226
+ this.cleanupState();
203
227
  if (this.nc) {
204
228
  try {
205
229
  await this.nc.close();
206
230
  }
207
231
  catch (closeError) {
208
- console.error("[PubSub] Error closing connection after failed subscribe:", closeError);
232
+ this.log("error", "Error closing connection after failed subscribe:", closeError);
209
233
  }
210
234
  this.nc = null;
211
235
  }
212
- console.error("[PubSub] Failed to subscribe:", error);
236
+ this.log("error", "Failed to subscribe:", error);
213
237
  throw error;
214
238
  }
215
239
  }
@@ -221,10 +245,10 @@ export class PubsubClient {
221
245
  for await (const status of this.nc.status()) {
222
246
  switch (status.type) {
223
247
  case "disconnect":
224
- console.warn("[PubSub] ⚠ Disconnected", { data: status.data });
248
+ this.log("warn", "⚠ Disconnected", { data: status.data });
225
249
  break;
226
250
  case "reconnect":
227
- console.info("[PubSub] ↻ Reconnected");
251
+ this.log("info", "↻ Reconnected");
228
252
  if (!this.isReconnecting &&
229
253
  this.connectionState === ConnectionState.CONNECTED) {
230
254
  this.isReconnecting = true;
@@ -232,7 +256,7 @@ export class PubsubClient {
232
256
  await this.resubscribe();
233
257
  }
234
258
  catch (error) {
235
- console.error("[PubSub] Failed to resubscribe after reconnection:", error);
259
+ this.log("error", "Failed to resubscribe after reconnection:", error);
236
260
  }
237
261
  finally {
238
262
  this.isReconnecting = false;
@@ -240,284 +264,344 @@ export class PubsubClient {
240
264
  }
241
265
  break;
242
266
  case "error":
243
- // Usually permissions errors
244
- console.error("[PubSub] Connection error:", status.data);
267
+ this.log("error", "Connection error:", status.data);
245
268
  break;
246
269
  case "update":
247
- // Cluster configuration update
248
- console.info("[PubSub] Cluster update received:", status.data);
270
+ this.log("debug", "Cluster update received:", status.data);
249
271
  break;
250
272
  case "ldm":
251
- // Lame Duck Mode - server is requesting clients to reconnect
252
- console.warn("[PubSub] Server requesting reconnection (LDM)");
273
+ this.log("warn", "Server requesting reconnection (LDM)");
253
274
  break;
254
- // Debug events - usually can be safely ignored but useful for debugging
255
275
  case "reconnecting":
256
- console.debug("[PubSub] Attempting to reconnect...");
257
- break;
258
- case "pingTimer":
259
- // Ping timer events - very verbose, only log if needed
260
- // console.debug("[PubSub] Ping timer event");
276
+ this.log("debug", "Attempting to reconnect...");
261
277
  break;
262
278
  case "staleConnection":
263
- console.warn("[PubSub] Connection is stale, may reconnect soon");
279
+ this.log("warn", "Connection is stale, may reconnect soon");
264
280
  break;
265
281
  default:
266
- console.debug(`[PubSub] 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
+ }
267
285
  }
268
286
  }
269
287
  }
270
288
  catch (error) {
271
289
  if (this.connectionState !== ConnectionState.CLOSING) {
272
- console.error("[PubSub] Fatal status handler error:", error);
290
+ this.log("error", "Fatal status handler error:", error);
273
291
  }
274
292
  }
275
293
  })();
276
294
  }
277
- async createSubscriptions(streamName, consumerName) {
278
- if (!this.nc || !this.js) {
279
- console.error("[PubSub] Cannot create subscriptions - no connection");
280
- return;
281
- }
282
- if (!this.subject) {
283
- console.error("[PubSub] Cannot create subscriptions - no subject");
284
- return;
285
- }
286
- if (this.nc.isClosed()) {
287
- console.error("[PubSub] Cannot create subscriptions - connection is closed");
288
- return;
295
+ async createPullSubscription() {
296
+ if (!this.nc || !this.streamName || !this.consumerName) {
297
+ throw new Error("Not properly initialized");
289
298
  }
290
299
  await this.clearSubscriptions();
291
300
  try {
292
- console.info(`[PubSub] Creating pull subscription for consumer: ${consumerName} on stream: ${streamName}`);
293
- const jc = JSONCodec();
294
- const pullSubject = `$JS.API.CONSUMER.MSG.NEXT.${streamName}.${consumerName}`;
295
- // Create inbox with proper prefix - let NATS generate the unique part
296
- const inbox = createInbox(`_INBOX_${consumerName}`);
297
- 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);
298
305
  this.pullSubscription = sub;
299
- // Track state
300
- let pending = 0;
301
- let pulling = false;
302
- const maxMessages = 100;
303
- const threshold = 50;
304
- const pollInterval = 1000; // 1 second poll interval
305
- // Pull messages with proper expiration
306
- const pull = async (batch) => {
307
- if (pulling || !this.nc || this.nc.isClosed())
308
- return;
309
- 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
+ }
310
331
  try {
311
- // Set expires to slightly longer than poll interval to avoid accumulation
312
- const request = {
313
- batch,
314
- expires: (pollInterval + 500) * 1000000, // Poll interval + 500ms buffer, in nanoseconds
315
- };
316
- this.nc.publish(pullSubject, jc.encode(request), { reply: inbox });
317
- pending += batch;
332
+ await this.handleMessage(msg);
318
333
  }
319
- finally {
320
- pulling = false;
334
+ catch (error) {
335
+ this.log("error", "Error handling message:", error);
321
336
  }
322
- };
323
- // Initial pull
324
- pull(maxMessages);
325
- // Set up periodic pulling
326
- let pullTimer = null;
327
- const schedulePull = () => {
328
- if (pullTimer)
329
- clearTimeout(pullTimer);
330
- pullTimer = setTimeout(() => {
331
- if (pending < threshold) {
332
- pull(maxMessages - pending);
333
- }
334
- }, pollInterval);
335
- };
336
- // Process messages
337
- (async () => {
338
- try {
339
- for await (const msg of sub) {
340
- if (this.connectionState === ConnectionState.CLOSING ||
341
- this.connectionState === ConnectionState.DISCONNECTED) {
342
- console.info("[PubSub] Subscription closing");
343
- if (pullTimer)
344
- clearTimeout(pullTimer);
345
- break;
346
- }
347
- // Check status header
348
- const status = msg.headers?.get("Status");
349
- if (status === "404") {
350
- // No messages available
351
- console.debug("[PubSub] No messages available (404)");
352
- pending = 0;
353
- schedulePull();
354
- continue;
355
- }
356
- if (status === "408") {
357
- console.debug("[PubSub] Pull request timed out (408)");
358
- pending = 0;
359
- schedulePull();
360
- continue;
361
- }
362
- if (status === "409") {
363
- console.warn("[PubSub] Max ack pending reached (409)");
364
- pending = 0;
365
- setTimeout(() => pull(maxMessages), 5000);
366
- continue;
367
- }
368
- // Skip if not a real message
369
- if (!msg.data || msg.data.length === 0 || msg.subject === inbox) {
370
- continue;
371
- }
372
- // We got a real message
373
- pending = Math.max(0, pending - 1);
374
- // Re-pull if below threshold
375
- if (pending < threshold && !pulling) {
376
- pull(maxMessages - pending);
377
- }
378
- const parts = msg.subject.split(".");
379
- const action = parts[parts.length - 1];
380
- console.info(`[PubSub] ↓ 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
- console.warn("[PubSub] Unknown message action:", action);
411
- }
412
- if (msg.reply) {
413
- msg.respond();
414
- }
415
- }
416
- catch (error) {
417
- console.error("[PubSub] Error handling message:", error);
418
- if (msg.reply) {
419
- msg.respond(new TextEncoder().encode("-NAK"));
420
- }
421
- }
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
+ });
422
403
  }
404
+ break;
423
405
  }
424
- catch (error) {
425
- if (this.connectionState !== ConnectionState.CLOSING) {
426
- console.error("[PubSub] Handler error:", error);
406
+ case "session": {
407
+ if (this.sessionHandler) {
408
+ const session = fromBinary(SessionSchema, msg.data);
409
+ this.sessionHandler(toJson(SessionSchema, session));
427
410
  }
411
+ break;
428
412
  }
429
- finally {
430
- if (pullTimer)
431
- clearTimeout(pullTimer);
413
+ case "url": {
414
+ if (this.urlHandler) {
415
+ const url = fromBinary(UrlSchema, msg.data);
416
+ this.urlHandler(toJson(UrlSchema, url));
417
+ }
418
+ break;
432
419
  }
433
- })();
434
- console.info("[PubSub] Pull subscription created successfully");
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
+ }
435
428
  }
436
429
  catch (error) {
437
- console.error("[PubSub] Failed to create subscriptions:", 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
+ }
438
436
  throw error;
439
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
+ }
440
444
  }
441
- async clearSubscriptions() {
442
- if (this.pullSubscription) {
443
- console.info("[PubSub] Clearing subscription");
444
- try {
445
- this.pullSubscription.unsubscribe();
446
- console.info("[PubSub] Subscription cleared");
447
- }
448
- catch (error) {
449
- console.error("[PubSub] Error clearing subscription:", error);
450
- }
451
- this.pullSubscription = null;
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--;
459
+ }
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);
463
+ }
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 || ""}`);
452
480
  }
453
481
  }
454
- async resubscribe() {
455
- if (!this.subject ||
456
- !this.streamName ||
457
- !this.consumerName ||
458
- (!this.outputHandler && !this.sessionHandler && !this.urlHandler)) {
459
- console.info("[PubSub] No handlers or config configured, skipping resubscribe");
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}`);
460
485
  return;
461
486
  }
487
+ this.pulling = true;
462
488
  try {
463
- await this.createSubscriptions(this.streamName, this.consumerName);
464
- console.info("[PubSub] Resubscribed successfully");
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"}`);
465
505
  }
466
506
  catch (error) {
467
- console.error("[PubSub] Resubscription failed:", error);
507
+ this.log("error", "Error sending pull request:", error);
468
508
  throw error;
469
509
  }
510
+ finally {
511
+ this.pulling = false;
512
+ }
470
513
  }
471
- async unsubscribe() {
472
- if (this.connectionState === ConnectionState.DISCONNECTED) {
473
- console.info("[PubSub] Already disconnected");
514
+ schedulePull() {
515
+ if (this.pullTimer) {
516
+ clearTimeout(this.pullTimer);
517
+ }
518
+ if (this.connectionState !== ConnectionState.CONNECTED) {
474
519
  return;
475
520
  }
476
- if (this.connectionState === ConnectionState.CLOSING) {
477
- console.info("[PubSub] Already closing");
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);
534
+ }
535
+ async clearSubscriptions() {
536
+ if (this.pullTimer) {
537
+ clearTimeout(this.pullTimer);
538
+ this.pullTimer = null;
539
+ }
540
+ if (!this.pullSubscription)
478
541
  return;
542
+ this.log("debug", "Clearing subscription");
543
+ try {
544
+ this.pullSubscription.unsubscribe();
545
+ this.log("debug", "✓ Subscription cleared");
479
546
  }
480
- console.info("[PubSub] Starting unsubscribe process");
481
- this.connectionState = ConnectionState.CLOSING;
482
- // Clear subscriptions first
483
- await this.clearSubscriptions();
484
- // Clear handlers
485
- this.outputHandler = null;
486
- this.sessionHandler = null;
487
- this.urlHandler = null;
547
+ catch (error) {
548
+ this.log("error", "Error clearing subscription:", error);
549
+ }
550
+ this.pullSubscription = null;
551
+ }
552
+ async resubscribe() {
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();
561
+ this.log("info", "✓ Resubscribed successfully");
562
+ }
563
+ cleanupState() {
488
564
  this.subject = null;
489
565
  this.streamName = null;
490
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
+ }
574
+ async unsubscribe() {
575
+ if (this.connectionState === ConnectionState.DISCONNECTED ||
576
+ this.connectionState === ConnectionState.CLOSING) {
577
+ return;
578
+ }
579
+ this.log("info", "Starting unsubscribe process");
580
+ this.connectionState = ConnectionState.CLOSING;
581
+ await this.clearSubscriptions();
582
+ this.cleanupState();
491
583
  this.isReconnecting = false;
492
- this.js = null;
493
- // Close connection
494
- if (this.nc) {
584
+ if (this.nc && !this.nc.isClosed()) {
495
585
  try {
496
- if (!this.nc.isClosed()) {
497
- await this.nc.close();
498
- console.info("[PubSub] Connection closed");
499
- }
586
+ await this.nc.close();
587
+ this.log("debug", "Connection closed");
500
588
  }
501
589
  catch (error) {
502
- console.error("[PubSub] Error closing connection:", error);
503
- }
504
- finally {
505
- this.nc = null;
590
+ this.log("error", "Error closing connection:", error);
506
591
  }
507
592
  }
593
+ this.nc = null;
508
594
  this.connectionState = ConnectionState.DISCONNECTED;
509
- console.info("[PubSub] Unsubscribed successfully");
595
+ this.log("info", "Unsubscribed successfully");
510
596
  }
511
- // Helper method to check connection status
512
597
  isConnected() {
513
- return (this.connectionState === ConnectionState.CONNECTED &&
514
- this.nc !== null &&
515
- !this.nc.isClosed());
598
+ return this.connectionState === ConnectionState.CONNECTED;
516
599
  }
517
600
  getConnectionState() {
518
601
  return this.connectionState;
519
602
  }
520
603
  }
604
+ // Helper functions
521
605
  function validateSubject(subject) {
522
606
  if (!subject || subject.includes(" ") || subject.includes("\t")) {
523
607
  return false;
@@ -535,7 +619,7 @@ function parseOutputData(data) {
535
619
  return [key, JSON.parse(new TextDecoder().decode(value))];
536
620
  }
537
621
  catch (error) {
538
- console.error(`[PubSub] Failed to parse data for key ${key}:`, error);
622
+ console.error(`Failed to parse data for key ${key}:`, error);
539
623
  return [key, null];
540
624
  }
541
625
  }));
@@ -551,4 +635,19 @@ function createAuthenticator(val) {
551
635
  throw new Error("Invalid credentials format");
552
636
  }
553
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
+ }
554
653
  //# sourceMappingURL=index.js.map