@robosystems/client 0.2.23 → 0.2.25

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.
@@ -43,7 +43,7 @@ class AgentClient {
43
43
  timestamp: new Date().toISOString(),
44
44
  };
45
45
  }
46
- // Check if this is a queued response (async Celery execution)
46
+ // Check if this is a queued response (async background task execution)
47
47
  if (responseData?.operation_id) {
48
48
  const queuedResponse = responseData;
49
49
  // If user doesn't want to wait, throw with queue info
@@ -87,7 +87,7 @@ class AgentClient {
87
87
  timestamp: new Date().toISOString(),
88
88
  };
89
89
  }
90
- // Check if this is a queued response (async Celery execution)
90
+ // Check if this is a queued response (async background task execution)
91
91
  if (responseData?.operation_id) {
92
92
  const queuedResponse = responseData;
93
93
  // If user doesn't want to wait, throw with queue info
@@ -102,7 +102,7 @@ export class AgentClient {
102
102
  }
103
103
  }
104
104
 
105
- // Check if this is a queued response (async Celery execution)
105
+ // Check if this is a queued response (async background task execution)
106
106
  if (responseData?.operation_id) {
107
107
  const queuedResponse = responseData as QueuedAgentResponse
108
108
 
@@ -158,7 +158,7 @@ export class AgentClient {
158
158
  }
159
159
  }
160
160
 
161
- // Check if this is a queued response (async Celery execution)
161
+ // Check if this is a queued response (async background task execution)
162
162
  if (responseData?.operation_id) {
163
163
  const queuedResponse = responseData as QueuedAgentResponse
164
164
 
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Error thrown when a graph operation fails (as reported by the server)
3
+ * This is distinct from connection/SSE errors
4
+ */
5
+ export declare class GraphOperationError extends Error {
6
+ constructor(message: string);
7
+ }
1
8
  export interface GraphMetadataInput {
2
9
  graphName: string;
3
10
  description?: string;
@@ -25,6 +32,7 @@ export interface CreateGraphOptions {
25
32
  timeout?: number;
26
33
  pollInterval?: number;
27
34
  onProgress?: (message: string) => void;
35
+ useSSE?: boolean;
28
36
  }
29
37
  export declare class GraphClient {
30
38
  private operationClient;
@@ -38,6 +46,9 @@ export declare class GraphClient {
38
46
  /**
39
47
  * Create a graph and wait for completion
40
48
  *
49
+ * Uses SSE (Server-Sent Events) for real-time progress updates with
50
+ * automatic fallback to polling if SSE connection fails.
51
+ *
41
52
  * @param metadata - Graph metadata (name, description, etc.)
42
53
  * @param initialEntity - Optional initial entity to create
43
54
  * @param options - Additional options including:
@@ -46,11 +57,20 @@ export declare class GraphClient {
46
57
  * create graph without populating entity data (useful for file-based ingestion).
47
58
  * Defaults to true.
48
59
  * - timeout: Maximum time to wait in milliseconds (default: 60000)
49
- * - pollInterval: Time between status checks in milliseconds (default: 2000)
60
+ * - pollInterval: Time between status checks in milliseconds (default: 2000, for polling fallback)
50
61
  * - onProgress: Callback for progress updates
62
+ * - useSSE: Whether to try SSE first (default: true). Falls back to polling on failure.
51
63
  * @returns The graph ID when creation completes
52
64
  */
53
65
  createGraphAndWait(metadata: GraphMetadataInput, initialEntity?: InitialEntityInput, options?: CreateGraphOptions): Promise<string>;
66
+ /**
67
+ * Wait for operation completion using SSE stream
68
+ */
69
+ private waitWithSSE;
70
+ /**
71
+ * Wait for operation completion using polling
72
+ */
73
+ private waitWithPolling;
54
74
  /**
55
75
  * Get information about a graph
56
76
  */
@@ -1,13 +1,25 @@
1
1
  'use client';
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.GraphClient = void 0;
4
+ exports.GraphClient = exports.GraphOperationError = void 0;
5
5
  /**
6
6
  * Graph Management Client
7
- * Provides high-level graph management operations with automatic operation monitoring
7
+ * Provides high-level graph management operations with automatic operation monitoring.
8
+ * Supports SSE (Server-Sent Events) for real-time updates with polling fallback.
8
9
  */
9
10
  const sdk_gen_1 = require("../sdk.gen");
10
11
  const OperationClient_1 = require("./OperationClient");
12
+ /**
13
+ * Error thrown when a graph operation fails (as reported by the server)
14
+ * This is distinct from connection/SSE errors
15
+ */
16
+ class GraphOperationError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = 'GraphOperationError';
20
+ }
21
+ }
22
+ exports.GraphOperationError = GraphOperationError;
11
23
  class GraphClient {
12
24
  constructor(config) {
13
25
  this.config = config;
@@ -16,6 +28,9 @@ class GraphClient {
16
28
  /**
17
29
  * Create a graph and wait for completion
18
30
  *
31
+ * Uses SSE (Server-Sent Events) for real-time progress updates with
32
+ * automatic fallback to polling if SSE connection fails.
33
+ *
19
34
  * @param metadata - Graph metadata (name, description, etc.)
20
35
  * @param initialEntity - Optional initial entity to create
21
36
  * @param options - Additional options including:
@@ -24,12 +39,13 @@ class GraphClient {
24
39
  * create graph without populating entity data (useful for file-based ingestion).
25
40
  * Defaults to true.
26
41
  * - timeout: Maximum time to wait in milliseconds (default: 60000)
27
- * - pollInterval: Time between status checks in milliseconds (default: 2000)
42
+ * - pollInterval: Time between status checks in milliseconds (default: 2000, for polling fallback)
28
43
  * - onProgress: Callback for progress updates
44
+ * - useSSE: Whether to try SSE first (default: true). Falls back to polling on failure.
29
45
  * @returns The graph ID when creation completes
30
46
  */
31
47
  async createGraphAndWait(metadata, initialEntity, options = {}) {
32
- const { createEntity = true, timeout = 60000, pollInterval = 2000, onProgress } = options;
48
+ const { createEntity = true, timeout = 60000, pollInterval = 2000, onProgress, useSSE = true, } = options;
33
49
  if (!this.config.token) {
34
50
  throw new Error('No API key provided. Set token in config.');
35
51
  }
@@ -71,44 +87,96 @@ class GraphClient {
71
87
  return responseData.graph_id;
72
88
  }
73
89
  // Otherwise, we have an operation_id to monitor
74
- if (responseData?.operation_id) {
75
- const operationId = responseData.operation_id;
76
- if (onProgress) {
77
- onProgress(`Graph creation queued (operation: ${operationId})`);
90
+ if (!responseData?.operation_id) {
91
+ throw new Error('No graph_id or operation_id in response');
92
+ }
93
+ const operationId = responseData.operation_id;
94
+ if (onProgress) {
95
+ onProgress(`Graph creation queued (operation: ${operationId})`);
96
+ }
97
+ // Try SSE first, fall back to polling
98
+ if (useSSE) {
99
+ try {
100
+ return await this.waitWithSSE(operationId, timeout, onProgress);
78
101
  }
79
- // Poll operation status
80
- const maxAttempts = Math.floor(timeout / pollInterval);
81
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
82
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
83
- const statusResponse = await (0, sdk_gen_1.getOperationStatus)({
84
- path: { operation_id: operationId },
85
- });
86
- const statusData = statusResponse.data;
87
- const status = statusData?.status;
102
+ catch (error) {
103
+ // Only fall back to polling for SSE connection failures
104
+ // If it's a GraphOperationError, the operation actually failed - don't retry with polling
105
+ if (error instanceof GraphOperationError) {
106
+ throw error;
107
+ }
108
+ // SSE connection failed, fall back to polling
88
109
  if (onProgress) {
89
- onProgress(`Status: ${status} (attempt ${attempt + 1}/${maxAttempts})`);
110
+ onProgress('SSE unavailable, using polling...');
90
111
  }
91
- if (status === 'completed') {
92
- const result = statusData?.result;
93
- const graphId = result?.graph_id;
94
- if (graphId) {
95
- if (onProgress) {
96
- onProgress(`Graph created: ${graphId}`);
97
- }
98
- return graphId;
112
+ }
113
+ }
114
+ // Fallback to polling
115
+ return await this.waitWithPolling(operationId, timeout, pollInterval, onProgress);
116
+ }
117
+ /**
118
+ * Wait for operation completion using SSE stream
119
+ */
120
+ async waitWithSSE(operationId, timeout, onProgress) {
121
+ const result = await this.operationClient.monitorOperation(operationId, {
122
+ timeout,
123
+ onProgress: (progress) => {
124
+ if (onProgress) {
125
+ if (progress.progressPercent !== undefined) {
126
+ onProgress(`${progress.message} (${Math.round(progress.progressPercent)}%)`);
99
127
  }
100
128
  else {
101
- throw new Error('Operation completed but no graph_id in result');
129
+ onProgress(progress.message);
102
130
  }
103
131
  }
104
- else if (status === 'failed') {
105
- const error = statusData?.error || statusData?.message || 'Unknown error';
106
- throw new Error(`Graph creation failed: ${error}`);
132
+ },
133
+ });
134
+ if (!result.success) {
135
+ throw new GraphOperationError(result.error || 'Graph creation failed');
136
+ }
137
+ const graphId = result.result?.graph_id;
138
+ if (!graphId) {
139
+ throw new GraphOperationError('Operation completed but no graph_id in result');
140
+ }
141
+ if (onProgress) {
142
+ onProgress(`Graph created: ${graphId}`);
143
+ }
144
+ return graphId;
145
+ }
146
+ /**
147
+ * Wait for operation completion using polling
148
+ */
149
+ async waitWithPolling(operationId, timeout, pollInterval, onProgress) {
150
+ const maxAttempts = Math.floor(timeout / pollInterval);
151
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
152
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
153
+ const statusResponse = await (0, sdk_gen_1.getOperationStatus)({
154
+ path: { operation_id: operationId },
155
+ });
156
+ const statusData = statusResponse.data;
157
+ const status = statusData?.status;
158
+ if (onProgress) {
159
+ onProgress(`Status: ${status} (attempt ${attempt + 1}/${maxAttempts})`);
160
+ }
161
+ if (status === 'completed') {
162
+ const result = statusData?.result;
163
+ const graphId = result?.graph_id;
164
+ if (graphId) {
165
+ if (onProgress) {
166
+ onProgress(`Graph created: ${graphId}`);
167
+ }
168
+ return graphId;
169
+ }
170
+ else {
171
+ throw new GraphOperationError('Operation completed but no graph_id in result');
107
172
  }
108
173
  }
109
- throw new Error(`Graph creation timed out after ${timeout}ms`);
174
+ else if (status === 'failed') {
175
+ const error = statusData?.error || statusData?.message || 'Unknown error';
176
+ throw new GraphOperationError(`Graph creation failed: ${error}`);
177
+ }
110
178
  }
111
- throw new Error('No graph_id or operation_id in response');
179
+ throw new GraphOperationError(`Graph creation timed out after ${timeout}ms`);
112
180
  }
113
181
  /**
114
182
  * Get information about a graph
@@ -1,4 +1,4 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest'
1
+ import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest'
2
2
  import type { GraphMetadataInput, InitialEntityInput } from './GraphClient'
3
3
  import { GraphClient } from './GraphClient'
4
4
 
@@ -19,20 +19,29 @@ function createMockResponse(data: any, options: { ok?: boolean; status?: number
19
19
  describe('GraphClient', () => {
20
20
  let graphClient: GraphClient
21
21
  let mockFetch: any
22
+ let mockMonitorOperation: MockInstance
23
+ let mockCloseAll: MockInstance
22
24
 
23
25
  beforeEach(() => {
26
+ // Mock global fetch first
27
+ mockFetch = vi.fn()
28
+ global.fetch = mockFetch
29
+
30
+ // Create graphClient
24
31
  graphClient = new GraphClient({
25
32
  baseUrl: 'http://localhost:8000',
26
33
  token: 'test-api-key',
27
34
  headers: { 'X-API-Key': 'test-api-key' },
28
35
  })
29
36
 
30
- // Mock global fetch
31
- mockFetch = vi.fn()
32
- global.fetch = mockFetch
37
+ // Access the internal operationClient and spy on its methods
38
+ const internalOperationClient = (graphClient as any).operationClient
39
+ mockMonitorOperation = vi.spyOn(internalOperationClient, 'monitorOperation')
40
+ mockCloseAll = vi.spyOn(internalOperationClient, 'closeAll').mockImplementation(() => {})
41
+ })
33
42
 
34
- // Reset all mocks
35
- vi.clearAllMocks()
43
+ afterEach(() => {
44
+ vi.restoreAllMocks()
36
45
  })
37
46
 
38
47
  describe('createGraphAndWait', () => {
@@ -174,6 +183,167 @@ describe('GraphClient', () => {
174
183
  'No API key provided'
175
184
  )
176
185
  })
186
+
187
+ describe('SSE mode', () => {
188
+ it('should use SSE when operation_id is returned and SSE succeeds', async () => {
189
+ // createGraph returns operation_id
190
+ mockFetch.mockResolvedValueOnce(
191
+ createMockResponse({
192
+ operation_id: 'op_sse_success',
193
+ })
194
+ )
195
+
196
+ mockMonitorOperation.mockResolvedValueOnce({
197
+ success: true,
198
+ result: { graph_id: 'graph_from_sse' },
199
+ })
200
+
201
+ const graphId = await graphClient.createGraphAndWait(mockMetadata)
202
+
203
+ expect(graphId).toBe('graph_from_sse')
204
+ expect(mockMonitorOperation).toHaveBeenCalledWith(
205
+ 'op_sse_success',
206
+ expect.objectContaining({ timeout: 60000 })
207
+ )
208
+ // Should only call fetch once (createGraph), no polling calls
209
+ expect(mockFetch).toHaveBeenCalledTimes(1)
210
+ })
211
+
212
+ it('should format SSE progress with percentage', async () => {
213
+ mockFetch.mockResolvedValueOnce(
214
+ createMockResponse({
215
+ operation_id: 'op_progress',
216
+ })
217
+ )
218
+
219
+ mockMonitorOperation.mockImplementationOnce(
220
+ async (_opId: string, options: { onProgress?: (p: any) => void }) => {
221
+ // Simulate progress callback with percentage
222
+ if (options.onProgress) {
223
+ options.onProgress({ message: 'Processing', progressPercent: 50 })
224
+ }
225
+ return { success: true, result: { graph_id: 'graph_progress' } }
226
+ }
227
+ )
228
+
229
+ const onProgress = vi.fn()
230
+ await graphClient.createGraphAndWait(mockMetadata, undefined, { onProgress })
231
+
232
+ expect(onProgress).toHaveBeenCalledWith('Processing (50%)')
233
+ })
234
+
235
+ it('should format SSE progress without percentage', async () => {
236
+ mockFetch.mockResolvedValueOnce(
237
+ createMockResponse({
238
+ operation_id: 'op_no_percent',
239
+ })
240
+ )
241
+
242
+ mockMonitorOperation.mockImplementationOnce(
243
+ async (_opId: string, options: { onProgress?: (p: any) => void }) => {
244
+ if (options.onProgress) {
245
+ options.onProgress({ message: 'Initializing' })
246
+ }
247
+ return { success: true, result: { graph_id: 'graph_init' } }
248
+ }
249
+ )
250
+
251
+ const onProgress = vi.fn()
252
+ await graphClient.createGraphAndWait(mockMetadata, undefined, { onProgress })
253
+
254
+ expect(onProgress).toHaveBeenCalledWith('Initializing')
255
+ })
256
+
257
+ it('should fall back to polling when SSE fails', async () => {
258
+ // createGraph returns operation_id
259
+ mockFetch.mockResolvedValueOnce(
260
+ createMockResponse({
261
+ operation_id: 'op_sse_fail',
262
+ })
263
+ )
264
+
265
+ // SSE fails
266
+ mockMonitorOperation.mockRejectedValueOnce(new Error('SSE connection failed'))
267
+
268
+ // Polling succeeds
269
+ mockFetch.mockResolvedValueOnce(
270
+ createMockResponse({
271
+ status: 'completed',
272
+ result: { graph_id: 'graph_from_polling' },
273
+ })
274
+ )
275
+
276
+ const onProgress = vi.fn()
277
+ const graphId = await graphClient.createGraphAndWait(mockMetadata, undefined, {
278
+ pollInterval: 100,
279
+ onProgress,
280
+ })
281
+
282
+ expect(graphId).toBe('graph_from_polling')
283
+ expect(onProgress).toHaveBeenCalledWith('SSE unavailable, using polling...')
284
+ // createGraph + 1 polling call
285
+ expect(mockFetch).toHaveBeenCalledTimes(2)
286
+ })
287
+
288
+ it('should skip SSE when useSSE is false', async () => {
289
+ mockFetch.mockResolvedValueOnce(
290
+ createMockResponse({
291
+ operation_id: 'op_no_sse',
292
+ })
293
+ )
294
+
295
+ // Polling succeeds
296
+ mockFetch.mockResolvedValueOnce(
297
+ createMockResponse({
298
+ status: 'completed',
299
+ result: { graph_id: 'graph_polling_only' },
300
+ })
301
+ )
302
+
303
+ const graphId = await graphClient.createGraphAndWait(mockMetadata, undefined, {
304
+ useSSE: false,
305
+ pollInterval: 100,
306
+ })
307
+
308
+ expect(graphId).toBe('graph_polling_only')
309
+ // SSE should never be called
310
+ expect(mockMonitorOperation).not.toHaveBeenCalled()
311
+ })
312
+
313
+ it('should throw error when SSE operation fails', async () => {
314
+ mockFetch.mockResolvedValueOnce(
315
+ createMockResponse({
316
+ operation_id: 'op_sse_error',
317
+ })
318
+ )
319
+
320
+ mockMonitorOperation.mockResolvedValueOnce({
321
+ success: false,
322
+ error: 'Operation failed on server',
323
+ })
324
+
325
+ await expect(
326
+ graphClient.createGraphAndWait(mockMetadata, undefined, { useSSE: true })
327
+ ).rejects.toThrow('Operation failed on server')
328
+ })
329
+
330
+ it('should throw error when SSE completes but no graph_id in result', async () => {
331
+ mockFetch.mockResolvedValueOnce(
332
+ createMockResponse({
333
+ operation_id: 'op_no_graph_id',
334
+ })
335
+ )
336
+
337
+ mockMonitorOperation.mockResolvedValueOnce({
338
+ success: true,
339
+ result: {}, // No graph_id
340
+ })
341
+
342
+ await expect(graphClient.createGraphAndWait(mockMetadata)).rejects.toThrow(
343
+ 'Operation completed but no graph_id in result'
344
+ )
345
+ })
346
+ })
177
347
  })
178
348
 
179
349
  describe('getGraphInfo', () => {