@astropods/messaging 0.0.0 → 0.0.2
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/messaging-client.d.ts +44 -5
- package/dist/messaging-client.js +138 -29
- package/package.json +5 -4
|
@@ -135,6 +135,20 @@ export interface ConversationRequest {
|
|
|
135
135
|
agentConfig?: AgentConfig;
|
|
136
136
|
agentResponse?: AgentResponse;
|
|
137
137
|
}
|
|
138
|
+
export interface ReconnectOptions {
|
|
139
|
+
/** Maximum number of reconnect attempts. Default: Infinity */
|
|
140
|
+
maxRetries?: number;
|
|
141
|
+
/** Initial delay before first retry in ms. Default: 500 */
|
|
142
|
+
initialDelayMs?: number;
|
|
143
|
+
/** Maximum delay between retries in ms. Default: 30_000 */
|
|
144
|
+
maxDelayMs?: number;
|
|
145
|
+
/** Apply full jitter to backoff delay. Default: true */
|
|
146
|
+
jitter?: boolean;
|
|
147
|
+
/** Maximum number of writes to queue during reconnect. Default: 1000 */
|
|
148
|
+
maxBufferSize?: number;
|
|
149
|
+
/** gRPC status codes that trigger a reconnect attempt. Default: UNAVAILABLE, DEADLINE_EXCEEDED, INTERNAL, RESOURCE_EXHAUSTED */
|
|
150
|
+
retryableStatusCodes?: number[];
|
|
151
|
+
}
|
|
138
152
|
/**
|
|
139
153
|
* MessagingClient provides a TypeScript interface to the Astro Messaging gRPC service
|
|
140
154
|
*/
|
|
@@ -149,9 +163,14 @@ export declare class MessagingClient extends EventEmitter {
|
|
|
149
163
|
*/
|
|
150
164
|
connect(): Promise<void>;
|
|
151
165
|
/**
|
|
152
|
-
*
|
|
166
|
+
* Connect with automatic retry on failure (exponential backoff).
|
|
167
|
+
* Emits 'reconnecting' before each retry and 'reconnected' on success after failures.
|
|
168
|
+
*/
|
|
169
|
+
connectWithRetry(options?: ReconnectOptions): Promise<void>;
|
|
170
|
+
/**
|
|
171
|
+
* Create a bidirectional conversation stream with optional reconnect support
|
|
153
172
|
*/
|
|
154
|
-
createConversationStream(): ConversationStream;
|
|
173
|
+
createConversationStream(options?: ReconnectOptions): ConversationStream;
|
|
155
174
|
/**
|
|
156
175
|
* Process a single message (server-side streaming)
|
|
157
176
|
*/
|
|
@@ -176,11 +195,31 @@ export declare class MessagingClient extends EventEmitter {
|
|
|
176
195
|
close(): void;
|
|
177
196
|
}
|
|
178
197
|
/**
|
|
179
|
-
* ConversationStream wraps a bidirectional gRPC stream
|
|
198
|
+
* ConversationStream wraps a bidirectional gRPC stream with automatic reconnection.
|
|
199
|
+
*
|
|
200
|
+
* Events:
|
|
201
|
+
* - 'response' — AgentResponse received from server
|
|
202
|
+
* - 'reconnecting' — { attempt, reason, delayMs } — before each retry delay
|
|
203
|
+
* - 'reconnected' — { attempt } — after a successful stream recreation
|
|
204
|
+
* - 'error' — non-retryable error OR max retries exceeded
|
|
205
|
+
* - 'end' — only on intentional close(), not on unexpected stream drop
|
|
180
206
|
*/
|
|
181
207
|
export declare class ConversationStream extends EventEmitter {
|
|
208
|
+
private streamFactory;
|
|
182
209
|
private stream;
|
|
183
|
-
|
|
210
|
+
private writeBuffer;
|
|
211
|
+
private reconnecting;
|
|
212
|
+
private closed;
|
|
213
|
+
private retryCount;
|
|
214
|
+
private readonly opts;
|
|
215
|
+
constructor(streamFactory: () => any, options?: ReconnectOptions);
|
|
216
|
+
private attachHandlers;
|
|
217
|
+
private isRetryable;
|
|
218
|
+
private calculateDelay;
|
|
219
|
+
private scheduleReconnect;
|
|
220
|
+
private doReconnect;
|
|
221
|
+
private flushBuffer;
|
|
222
|
+
private write;
|
|
184
223
|
/**
|
|
185
224
|
* Send a message through the stream
|
|
186
225
|
*/
|
|
@@ -206,7 +245,7 @@ export declare class ConversationStream extends EventEmitter {
|
|
|
206
245
|
*/
|
|
207
246
|
sendStatusUpdate(conversationId: string, status: StatusUpdate): void;
|
|
208
247
|
/**
|
|
209
|
-
* End the stream
|
|
248
|
+
* End the stream intentionally. Emits 'end' and prevents any further reconnects.
|
|
210
249
|
*/
|
|
211
250
|
end(): void;
|
|
212
251
|
}
|
package/dist/messaging-client.js
CHANGED
|
@@ -38,6 +38,18 @@ const grpc = __importStar(require("@grpc/grpc-js"));
|
|
|
38
38
|
const protoLoader = __importStar(require("@grpc/proto-loader"));
|
|
39
39
|
const path_1 = require("path");
|
|
40
40
|
const events_1 = require("events");
|
|
41
|
+
// gRPC status codes: DEADLINE_EXCEEDED=4, INTERNAL=13, UNAVAILABLE=14, RESOURCE_EXHAUSTED=8
|
|
42
|
+
const DEFAULT_RETRYABLE_STATUS_CODES = [4, 8, 13, 14];
|
|
43
|
+
function resolveReconnectOptions(options) {
|
|
44
|
+
return {
|
|
45
|
+
maxRetries: options.maxRetries ?? Infinity,
|
|
46
|
+
initialDelayMs: options.initialDelayMs ?? 500,
|
|
47
|
+
maxDelayMs: options.maxDelayMs ?? 30000,
|
|
48
|
+
jitter: options.jitter ?? true,
|
|
49
|
+
maxBufferSize: options.maxBufferSize ?? 1000,
|
|
50
|
+
retryableStatusCodes: options.retryableStatusCodes ?? DEFAULT_RETRYABLE_STATUS_CODES,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
41
53
|
/**
|
|
42
54
|
* MessagingClient provides a TypeScript interface to the Astro Messaging gRPC service
|
|
43
55
|
*/
|
|
@@ -45,6 +57,7 @@ class MessagingClient extends events_1.EventEmitter {
|
|
|
45
57
|
constructor(serverAddress) {
|
|
46
58
|
super();
|
|
47
59
|
this.serverAddress = serverAddress;
|
|
60
|
+
this.conversationStream = null;
|
|
48
61
|
this.isConnected = false;
|
|
49
62
|
}
|
|
50
63
|
/**
|
|
@@ -67,14 +80,42 @@ class MessagingClient extends events_1.EventEmitter {
|
|
|
67
80
|
this.emit('connected');
|
|
68
81
|
}
|
|
69
82
|
/**
|
|
70
|
-
*
|
|
83
|
+
* Connect with automatic retry on failure (exponential backoff).
|
|
84
|
+
* Emits 'reconnecting' before each retry and 'reconnected' on success after failures.
|
|
85
|
+
*/
|
|
86
|
+
async connectWithRetry(options = {}) {
|
|
87
|
+
const opts = resolveReconnectOptions(options);
|
|
88
|
+
let retryCount = 0;
|
|
89
|
+
while (true) {
|
|
90
|
+
try {
|
|
91
|
+
await this.connect();
|
|
92
|
+
if (retryCount > 0) {
|
|
93
|
+
this.emit('reconnected', { attempt: retryCount });
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
if (retryCount >= opts.maxRetries) {
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
const base = Math.min(opts.initialDelayMs * Math.pow(2, retryCount), opts.maxDelayMs);
|
|
102
|
+
const delayMs = opts.jitter ? base * (0.5 + Math.random() * 0.5) : base;
|
|
103
|
+
this.emit('reconnecting', { attempt: retryCount + 1, reason: err, delayMs });
|
|
104
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
105
|
+
retryCount++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Create a bidirectional conversation stream with optional reconnect support
|
|
71
111
|
*/
|
|
72
|
-
createConversationStream() {
|
|
112
|
+
createConversationStream(options) {
|
|
73
113
|
if (!this.isConnected) {
|
|
74
114
|
throw new Error('Client not connected. Call connect() first.');
|
|
75
115
|
}
|
|
76
|
-
|
|
77
|
-
|
|
116
|
+
const factory = () => this.client.ProcessConversation();
|
|
117
|
+
this.conversationStream = new ConversationStream(factory, options);
|
|
118
|
+
return this.conversationStream;
|
|
78
119
|
}
|
|
79
120
|
/**
|
|
80
121
|
* Process a single message (server-side streaming)
|
|
@@ -169,57 +210,122 @@ class MessagingClient extends events_1.EventEmitter {
|
|
|
169
210
|
}
|
|
170
211
|
exports.MessagingClient = MessagingClient;
|
|
171
212
|
/**
|
|
172
|
-
* ConversationStream wraps a bidirectional gRPC stream
|
|
213
|
+
* ConversationStream wraps a bidirectional gRPC stream with automatic reconnection.
|
|
214
|
+
*
|
|
215
|
+
* Events:
|
|
216
|
+
* - 'response' — AgentResponse received from server
|
|
217
|
+
* - 'reconnecting' — { attempt, reason, delayMs } — before each retry delay
|
|
218
|
+
* - 'reconnected' — { attempt } — after a successful stream recreation
|
|
219
|
+
* - 'error' — non-retryable error OR max retries exceeded
|
|
220
|
+
* - 'end' — only on intentional close(), not on unexpected stream drop
|
|
173
221
|
*/
|
|
174
222
|
class ConversationStream extends events_1.EventEmitter {
|
|
175
|
-
constructor(
|
|
223
|
+
constructor(streamFactory, options = {}) {
|
|
176
224
|
super();
|
|
177
|
-
this.
|
|
178
|
-
this.
|
|
225
|
+
this.streamFactory = streamFactory;
|
|
226
|
+
this.writeBuffer = [];
|
|
227
|
+
this.reconnecting = false;
|
|
228
|
+
this.closed = false;
|
|
229
|
+
this.retryCount = 0;
|
|
230
|
+
this.opts = resolveReconnectOptions(options);
|
|
231
|
+
this.stream = this.streamFactory();
|
|
232
|
+
this.attachHandlers(this.stream);
|
|
233
|
+
}
|
|
234
|
+
attachHandlers(stream) {
|
|
235
|
+
stream.on('data', (response) => {
|
|
236
|
+
this.retryCount = 0;
|
|
179
237
|
this.emit('response', response);
|
|
180
238
|
});
|
|
181
|
-
|
|
182
|
-
this.
|
|
239
|
+
stream.on('error', (error) => {
|
|
240
|
+
if (!this.closed && this.isRetryable(error)) {
|
|
241
|
+
this.scheduleReconnect(error);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
this.emit('error', error);
|
|
245
|
+
}
|
|
183
246
|
});
|
|
184
|
-
|
|
185
|
-
this.
|
|
247
|
+
stream.on('end', () => {
|
|
248
|
+
if (!this.closed) {
|
|
249
|
+
this.scheduleReconnect(new Error('Stream ended unexpectedly'));
|
|
250
|
+
}
|
|
251
|
+
// If closed, 'end' was already emitted by end() — do nothing
|
|
186
252
|
});
|
|
187
253
|
}
|
|
254
|
+
isRetryable(error) {
|
|
255
|
+
return this.opts.retryableStatusCodes.includes(error.code);
|
|
256
|
+
}
|
|
257
|
+
calculateDelay() {
|
|
258
|
+
const base = Math.min(this.opts.initialDelayMs * Math.pow(2, this.retryCount), this.opts.maxDelayMs);
|
|
259
|
+
return this.opts.jitter ? base * (0.5 + Math.random() * 0.5) : base;
|
|
260
|
+
}
|
|
261
|
+
scheduleReconnect(reason) {
|
|
262
|
+
if (this.reconnecting || this.closed)
|
|
263
|
+
return;
|
|
264
|
+
if (this.retryCount >= this.opts.maxRetries) {
|
|
265
|
+
this.emit('error', new Error(`Max reconnection attempts (${this.opts.maxRetries}) exceeded`));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const delayMs = this.calculateDelay();
|
|
269
|
+
this.reconnecting = true;
|
|
270
|
+
this.emit('reconnecting', { attempt: this.retryCount + 1, reason, delayMs });
|
|
271
|
+
setTimeout(() => this.doReconnect(), delayMs);
|
|
272
|
+
}
|
|
273
|
+
doReconnect() {
|
|
274
|
+
if (this.closed)
|
|
275
|
+
return;
|
|
276
|
+
this.retryCount++;
|
|
277
|
+
try {
|
|
278
|
+
this.stream = this.streamFactory();
|
|
279
|
+
this.attachHandlers(this.stream);
|
|
280
|
+
this.reconnecting = false;
|
|
281
|
+
this.emit('reconnected', { attempt: this.retryCount });
|
|
282
|
+
this.flushBuffer();
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
this.reconnecting = false;
|
|
286
|
+
this.scheduleReconnect(err);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
flushBuffer() {
|
|
290
|
+
const toFlush = this.writeBuffer.splice(0);
|
|
291
|
+
for (const request of toFlush) {
|
|
292
|
+
this.stream.write(request);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
write(request) {
|
|
296
|
+
if (this.reconnecting || this.closed) {
|
|
297
|
+
if (this.writeBuffer.length >= this.opts.maxBufferSize) {
|
|
298
|
+
this.writeBuffer.shift(); // drop oldest
|
|
299
|
+
}
|
|
300
|
+
this.writeBuffer.push(request);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
this.stream.write(request);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
188
306
|
/**
|
|
189
307
|
* Send a message through the stream
|
|
190
308
|
*/
|
|
191
309
|
sendMessage(message) {
|
|
192
|
-
|
|
193
|
-
message,
|
|
194
|
-
};
|
|
195
|
-
this.stream.write(request);
|
|
310
|
+
this.write({ message });
|
|
196
311
|
}
|
|
197
312
|
/**
|
|
198
313
|
* Send platform feedback through the stream
|
|
199
314
|
*/
|
|
200
315
|
sendFeedback(feedback) {
|
|
201
|
-
|
|
202
|
-
feedback,
|
|
203
|
-
};
|
|
204
|
-
this.stream.write(request);
|
|
316
|
+
this.write({ feedback });
|
|
205
317
|
}
|
|
206
318
|
/**
|
|
207
319
|
* Send agent configuration through the stream
|
|
208
320
|
*/
|
|
209
321
|
sendAgentConfig(config) {
|
|
210
|
-
|
|
211
|
-
agentConfig: config,
|
|
212
|
-
};
|
|
213
|
-
this.stream.write(request);
|
|
322
|
+
this.write({ agentConfig: config });
|
|
214
323
|
}
|
|
215
324
|
/**
|
|
216
325
|
* Send a typed AgentResponse through the stream
|
|
217
326
|
*/
|
|
218
327
|
sendAgentResponse(response) {
|
|
219
|
-
|
|
220
|
-
agentResponse: response,
|
|
221
|
-
};
|
|
222
|
-
this.stream.write(request);
|
|
328
|
+
this.write({ agentResponse: response });
|
|
223
329
|
}
|
|
224
330
|
/**
|
|
225
331
|
* Send a content chunk (START/DELTA/END) for a conversation
|
|
@@ -240,10 +346,13 @@ class ConversationStream extends events_1.EventEmitter {
|
|
|
240
346
|
});
|
|
241
347
|
}
|
|
242
348
|
/**
|
|
243
|
-
* End the stream
|
|
349
|
+
* End the stream intentionally. Emits 'end' and prevents any further reconnects.
|
|
244
350
|
*/
|
|
245
351
|
end() {
|
|
352
|
+
this.closed = true;
|
|
353
|
+
this.writeBuffer = [];
|
|
246
354
|
this.stream.end();
|
|
355
|
+
this.emit('end');
|
|
247
356
|
}
|
|
248
357
|
}
|
|
249
358
|
exports.ConversationStream = ConversationStream;
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astropods/messaging",
|
|
3
|
-
"
|
|
3
|
+
"license": "Apache-2.0",
|
|
4
|
+
"version": "0.0.2",
|
|
4
5
|
"description": "TypeScript SDK for Astro Messaging",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"types": "dist/index.d.ts",
|
|
@@ -8,7 +9,7 @@
|
|
|
8
9
|
"dist"
|
|
9
10
|
],
|
|
10
11
|
"scripts": {
|
|
11
|
-
"postinstall": "ln -
|
|
12
|
+
"postinstall": "rm -rf proto && ln -sf ../../proto proto",
|
|
12
13
|
"build": "tsc && cp -r ../../proto dist/proto",
|
|
13
14
|
"watch": "tsc --watch",
|
|
14
15
|
"test": "bun test",
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
},
|
|
17
18
|
"repository": {
|
|
18
19
|
"type": "git",
|
|
19
|
-
"url": "https://github.com/astropods/messaging.git"
|
|
20
|
+
"url": "git+https://github.com/astropods/messaging.git"
|
|
20
21
|
},
|
|
21
22
|
"publishConfig": {
|
|
22
23
|
"registry": "https://registry.npmjs.org"
|
|
@@ -29,4 +30,4 @@
|
|
|
29
30
|
"@types/node": "^20.0.0",
|
|
30
31
|
"typescript": "^5.3.0"
|
|
31
32
|
}
|
|
32
|
-
}
|
|
33
|
+
}
|