@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 +355 -255
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +354 -255
- package/dist/esm/index.js.map +1 -1
- package/dist/types/index.d.ts +33 -3
- 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";
|
|
@@ -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
|
-
|
|
120
|
-
this.
|
|
121
|
-
this.
|
|
122
|
-
this.
|
|
123
|
-
this.
|
|
124
|
-
this.
|
|
125
|
-
this.
|
|
126
|
-
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
|
-
|
|
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(`
|
|
169
|
+
throw new Error(`Invalid subject: "${opts.subject}"`);
|
|
137
170
|
}
|
|
138
171
|
if (!opts.consumer || !opts.stream) {
|
|
139
|
-
throw new Error("
|
|
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.
|
|
145
|
-
|
|
146
|
-
|
|
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("
|
|
186
|
+
throw new Error("Already connecting, please wait");
|
|
154
187
|
}
|
|
155
|
-
// If connected to a different subject
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
188
|
-
// Set up status monitoring
|
|
220
|
+
this.log("info", "Connected successfully");
|
|
189
221
|
this.setupStatusMonitoring();
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
232
|
+
this.log("error", "Error closing connection after failed subscribe:", closeError);
|
|
209
233
|
}
|
|
210
234
|
this.nc = null;
|
|
211
235
|
}
|
|
212
|
-
|
|
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
|
-
|
|
248
|
+
this.log("warn", "⚠ Disconnected", { data: status.data });
|
|
225
249
|
break;
|
|
226
250
|
case "reconnect":
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
console.error("[PubSub] Connection error:", status.data);
|
|
267
|
+
this.log("error", "Connection error:", status.data);
|
|
245
268
|
break;
|
|
246
269
|
case "update":
|
|
247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
279
|
+
this.log("warn", "Connection is stale, may reconnect soon");
|
|
264
280
|
break;
|
|
265
281
|
default:
|
|
266
|
-
|
|
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
|
-
|
|
290
|
+
this.log("error", "Fatal status handler error:", error);
|
|
273
291
|
}
|
|
274
292
|
}
|
|
275
293
|
})();
|
|
276
294
|
}
|
|
277
|
-
async
|
|
278
|
-
if (!this.nc || !this.
|
|
279
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
320
|
-
|
|
334
|
+
catch (error) {
|
|
335
|
+
this.log("error", "Error handling message:", error);
|
|
321
336
|
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
425
|
-
if (this.
|
|
426
|
-
|
|
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
|
-
|
|
430
|
-
if (
|
|
431
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
this.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
|
455
|
-
if (!this.
|
|
456
|
-
|
|
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
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
472
|
-
if (this.
|
|
473
|
-
|
|
514
|
+
schedulePull() {
|
|
515
|
+
if (this.pullTimer) {
|
|
516
|
+
clearTimeout(this.pullTimer);
|
|
517
|
+
}
|
|
518
|
+
if (this.connectionState !== ConnectionState.CONNECTED) {
|
|
474
519
|
return;
|
|
475
520
|
}
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
this.
|
|
487
|
-
|
|
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.
|
|
493
|
-
// Close connection
|
|
494
|
-
if (this.nc) {
|
|
584
|
+
if (this.nc && !this.nc.isClosed()) {
|
|
495
585
|
try {
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
595
|
+
this.log("info", "Unsubscribed successfully");
|
|
510
596
|
}
|
|
511
|
-
// Helper method to check connection status
|
|
512
597
|
isConnected() {
|
|
513
|
-
return
|
|
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(`
|
|
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
|