@mochabug/adapt-web 0.0.41 → 0.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.js +309 -193
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +308 -193
- package/dist/esm/index.js.map +1 -1
- package/dist/types/index.d.ts +30 -1
- package/package.json +1 -1
package/dist/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
|
|
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.
|
|
222
|
+
await this.createPullSubscription();
|
|
195
223
|
}
|
|
196
224
|
catch (error) {
|
|
197
225
|
this.connectionState = ConnectionState.DISCONNECTED;
|
|
198
|
-
this.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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
|
-
|
|
310
|
-
|
|
334
|
+
catch (error) {
|
|
335
|
+
this.log("error", "Error handling message:", error);
|
|
311
336
|
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
435
|
-
if (this.
|
|
436
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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", "
|
|
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
|
-
|
|
466
|
-
|
|
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.
|
|
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
|