@robosystems/client 0.2.24 → 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.
@@ -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', () => {
@@ -2,13 +2,25 @@
2
2
 
3
3
  /**
4
4
  * Graph Management Client
5
- * Provides high-level graph management operations with automatic operation monitoring
5
+ * Provides high-level graph management operations with automatic operation monitoring.
6
+ * Supports SSE (Server-Sent Events) for real-time updates with polling fallback.
6
7
  */
7
8
 
8
9
  import { createGraph, getGraphs, getOperationStatus } from '../sdk.gen'
9
10
  import type { GraphMetadata, InitialEntityData } from '../types.gen'
10
11
  import { OperationClient } from './OperationClient'
11
12
 
13
+ /**
14
+ * Error thrown when a graph operation fails (as reported by the server)
15
+ * This is distinct from connection/SSE errors
16
+ */
17
+ export class GraphOperationError extends Error {
18
+ constructor(message: string) {
19
+ super(message)
20
+ this.name = 'GraphOperationError'
21
+ }
22
+ }
23
+
12
24
  // API Response Types
13
25
  interface GraphCreateResponse {
14
26
  graph_id?: string
@@ -70,6 +82,7 @@ export interface CreateGraphOptions {
70
82
  timeout?: number
71
83
  pollInterval?: number
72
84
  onProgress?: (message: string) => void
85
+ useSSE?: boolean
73
86
  }
74
87
 
75
88
  export class GraphClient {
@@ -94,6 +107,9 @@ export class GraphClient {
94
107
  /**
95
108
  * Create a graph and wait for completion
96
109
  *
110
+ * Uses SSE (Server-Sent Events) for real-time progress updates with
111
+ * automatic fallback to polling if SSE connection fails.
112
+ *
97
113
  * @param metadata - Graph metadata (name, description, etc.)
98
114
  * @param initialEntity - Optional initial entity to create
99
115
  * @param options - Additional options including:
@@ -102,8 +118,9 @@ export class GraphClient {
102
118
  * create graph without populating entity data (useful for file-based ingestion).
103
119
  * Defaults to true.
104
120
  * - timeout: Maximum time to wait in milliseconds (default: 60000)
105
- * - pollInterval: Time between status checks in milliseconds (default: 2000)
121
+ * - pollInterval: Time between status checks in milliseconds (default: 2000, for polling fallback)
106
122
  * - onProgress: Callback for progress updates
123
+ * - useSSE: Whether to try SSE first (default: true). Falls back to polling on failure.
107
124
  * @returns The graph ID when creation completes
108
125
  */
109
126
  async createGraphAndWait(
@@ -111,7 +128,13 @@ export class GraphClient {
111
128
  initialEntity?: InitialEntityInput,
112
129
  options: CreateGraphOptions = {}
113
130
  ): Promise<string> {
114
- const { createEntity = true, timeout = 60000, pollInterval = 2000, onProgress } = options
131
+ const {
132
+ createEntity = true,
133
+ timeout = 60000,
134
+ pollInterval = 2000,
135
+ onProgress,
136
+ useSSE = true,
137
+ } = options
115
138
 
116
139
  if (!this.config.token) {
117
140
  throw new Error('No API key provided. Set token in config.')
@@ -160,51 +183,118 @@ export class GraphClient {
160
183
  }
161
184
 
162
185
  // Otherwise, we have an operation_id to monitor
163
- if (responseData?.operation_id) {
164
- const operationId = responseData.operation_id
165
-
166
- if (onProgress) {
167
- onProgress(`Graph creation queued (operation: ${operationId})`)
168
- }
169
-
170
- // Poll operation status
171
- const maxAttempts = Math.floor(timeout / pollInterval)
172
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
173
- await new Promise((resolve) => setTimeout(resolve, pollInterval))
186
+ if (!responseData?.operation_id) {
187
+ throw new Error('No graph_id or operation_id in response')
188
+ }
174
189
 
175
- const statusResponse = await getOperationStatus({
176
- path: { operation_id: operationId },
177
- })
190
+ const operationId = responseData.operation_id
178
191
 
179
- const statusData = statusResponse.data as OperationStatusResponse
180
- const status = statusData?.status
192
+ if (onProgress) {
193
+ onProgress(`Graph creation queued (operation: ${operationId})`)
194
+ }
181
195
 
196
+ // Try SSE first, fall back to polling
197
+ if (useSSE) {
198
+ try {
199
+ return await this.waitWithSSE(operationId, timeout, onProgress)
200
+ } catch (error) {
201
+ // Only fall back to polling for SSE connection failures
202
+ // If it's a GraphOperationError, the operation actually failed - don't retry with polling
203
+ if (error instanceof GraphOperationError) {
204
+ throw error
205
+ }
206
+ // SSE connection failed, fall back to polling
182
207
  if (onProgress) {
183
- onProgress(`Status: ${status} (attempt ${attempt + 1}/${maxAttempts})`)
208
+ onProgress('SSE unavailable, using polling...')
184
209
  }
210
+ }
211
+ }
185
212
 
186
- if (status === 'completed') {
187
- const result = statusData?.result
188
- const graphId = result?.graph_id
213
+ // Fallback to polling
214
+ return await this.waitWithPolling(operationId, timeout, pollInterval, onProgress)
215
+ }
189
216
 
190
- if (graphId) {
191
- if (onProgress) {
192
- onProgress(`Graph created: ${graphId}`)
193
- }
194
- return graphId
217
+ /**
218
+ * Wait for operation completion using SSE stream
219
+ */
220
+ private async waitWithSSE(
221
+ operationId: string,
222
+ timeout: number,
223
+ onProgress?: (message: string) => void
224
+ ): Promise<string> {
225
+ const result = await this.operationClient.monitorOperation<{ graph_id?: string }>(operationId, {
226
+ timeout,
227
+ onProgress: (progress) => {
228
+ if (onProgress) {
229
+ if (progress.progressPercent !== undefined) {
230
+ onProgress(`${progress.message} (${Math.round(progress.progressPercent)}%)`)
195
231
  } else {
196
- throw new Error('Operation completed but no graph_id in result')
232
+ onProgress(progress.message)
197
233
  }
198
- } else if (status === 'failed') {
199
- const error = statusData?.error || statusData?.message || 'Unknown error'
200
- throw new Error(`Graph creation failed: ${error}`)
201
234
  }
235
+ },
236
+ })
237
+
238
+ if (!result.success) {
239
+ throw new GraphOperationError(result.error || 'Graph creation failed')
240
+ }
241
+
242
+ const graphId = result.result?.graph_id
243
+ if (!graphId) {
244
+ throw new GraphOperationError('Operation completed but no graph_id in result')
245
+ }
246
+
247
+ if (onProgress) {
248
+ onProgress(`Graph created: ${graphId}`)
249
+ }
250
+
251
+ return graphId
252
+ }
253
+
254
+ /**
255
+ * Wait for operation completion using polling
256
+ */
257
+ private async waitWithPolling(
258
+ operationId: string,
259
+ timeout: number,
260
+ pollInterval: number,
261
+ onProgress?: (message: string) => void
262
+ ): Promise<string> {
263
+ const maxAttempts = Math.floor(timeout / pollInterval)
264
+
265
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
266
+ await new Promise((resolve) => setTimeout(resolve, pollInterval))
267
+
268
+ const statusResponse = await getOperationStatus({
269
+ path: { operation_id: operationId },
270
+ })
271
+
272
+ const statusData = statusResponse.data as OperationStatusResponse
273
+ const status = statusData?.status
274
+
275
+ if (onProgress) {
276
+ onProgress(`Status: ${status} (attempt ${attempt + 1}/${maxAttempts})`)
202
277
  }
203
278
 
204
- throw new Error(`Graph creation timed out after ${timeout}ms`)
279
+ if (status === 'completed') {
280
+ const result = statusData?.result
281
+ const graphId = result?.graph_id
282
+
283
+ if (graphId) {
284
+ if (onProgress) {
285
+ onProgress(`Graph created: ${graphId}`)
286
+ }
287
+ return graphId
288
+ } else {
289
+ throw new GraphOperationError('Operation completed but no graph_id in result')
290
+ }
291
+ } else if (status === 'failed') {
292
+ const error = statusData?.error || statusData?.message || 'Unknown error'
293
+ throw new GraphOperationError(`Graph creation failed: ${error}`)
294
+ }
205
295
  }
206
296
 
207
- throw new Error('No graph_id or operation_id in response')
297
+ throw new GraphOperationError(`Graph creation timed out after ${timeout}ms`)
208
298
  }
209
299
 
210
300
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robosystems/client",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
4
4
  "description": "TypeScript client library for RoboSystems Financial Knowledge Graph API",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -70,11 +70,11 @@
70
70
  "lint": "eslint .",
71
71
  "lint:fix": "eslint . --fix",
72
72
  "typecheck": "tsc --noEmit",
73
- "validate": "npm run format:check && npm run lint && npm run typecheck",
73
+ "validate": "npm run validate:fix && npm run lint && npm run typecheck",
74
74
  "feature:create": "./bin/create-feature",
75
75
  "release:create": "./bin/create-release",
76
76
  "pr:create": "./bin/create-pr",
77
- "validate:fix": "npm run format && npm run lint:fix && npm run typecheck",
77
+ "validate:fix": "npm run format && npm run lint:fix",
78
78
  "test": "vitest run",
79
79
  "test:watch": "vitest",
80
80
  "test:all": "npm run validate && npm run test && npm run build",
@@ -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/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', () => {
@@ -2,13 +2,25 @@
2
2
 
3
3
  /**
4
4
  * Graph Management Client
5
- * Provides high-level graph management operations with automatic operation monitoring
5
+ * Provides high-level graph management operations with automatic operation monitoring.
6
+ * Supports SSE (Server-Sent Events) for real-time updates with polling fallback.
6
7
  */
7
8
 
8
9
  import { createGraph, getGraphs, getOperationStatus } from '../sdk/sdk.gen'
9
10
  import type { GraphMetadata, InitialEntityData } from '../sdk/types.gen'
10
11
  import { OperationClient } from './OperationClient'
11
12
 
13
+ /**
14
+ * Error thrown when a graph operation fails (as reported by the server)
15
+ * This is distinct from connection/SSE errors
16
+ */
17
+ export class GraphOperationError extends Error {
18
+ constructor(message: string) {
19
+ super(message)
20
+ this.name = 'GraphOperationError'
21
+ }
22
+ }
23
+
12
24
  // API Response Types
13
25
  interface GraphCreateResponse {
14
26
  graph_id?: string
@@ -70,6 +82,7 @@ export interface CreateGraphOptions {
70
82
  timeout?: number
71
83
  pollInterval?: number
72
84
  onProgress?: (message: string) => void
85
+ useSSE?: boolean
73
86
  }
74
87
 
75
88
  export class GraphClient {
@@ -94,6 +107,9 @@ export class GraphClient {
94
107
  /**
95
108
  * Create a graph and wait for completion
96
109
  *
110
+ * Uses SSE (Server-Sent Events) for real-time progress updates with
111
+ * automatic fallback to polling if SSE connection fails.
112
+ *
97
113
  * @param metadata - Graph metadata (name, description, etc.)
98
114
  * @param initialEntity - Optional initial entity to create
99
115
  * @param options - Additional options including:
@@ -102,8 +118,9 @@ export class GraphClient {
102
118
  * create graph without populating entity data (useful for file-based ingestion).
103
119
  * Defaults to true.
104
120
  * - timeout: Maximum time to wait in milliseconds (default: 60000)
105
- * - pollInterval: Time between status checks in milliseconds (default: 2000)
121
+ * - pollInterval: Time between status checks in milliseconds (default: 2000, for polling fallback)
106
122
  * - onProgress: Callback for progress updates
123
+ * - useSSE: Whether to try SSE first (default: true). Falls back to polling on failure.
107
124
  * @returns The graph ID when creation completes
108
125
  */
109
126
  async createGraphAndWait(
@@ -111,7 +128,13 @@ export class GraphClient {
111
128
  initialEntity?: InitialEntityInput,
112
129
  options: CreateGraphOptions = {}
113
130
  ): Promise<string> {
114
- const { createEntity = true, timeout = 60000, pollInterval = 2000, onProgress } = options
131
+ const {
132
+ createEntity = true,
133
+ timeout = 60000,
134
+ pollInterval = 2000,
135
+ onProgress,
136
+ useSSE = true,
137
+ } = options
115
138
 
116
139
  if (!this.config.token) {
117
140
  throw new Error('No API key provided. Set token in config.')
@@ -160,51 +183,118 @@ export class GraphClient {
160
183
  }
161
184
 
162
185
  // Otherwise, we have an operation_id to monitor
163
- if (responseData?.operation_id) {
164
- const operationId = responseData.operation_id
165
-
166
- if (onProgress) {
167
- onProgress(`Graph creation queued (operation: ${operationId})`)
168
- }
169
-
170
- // Poll operation status
171
- const maxAttempts = Math.floor(timeout / pollInterval)
172
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
173
- await new Promise((resolve) => setTimeout(resolve, pollInterval))
186
+ if (!responseData?.operation_id) {
187
+ throw new Error('No graph_id or operation_id in response')
188
+ }
174
189
 
175
- const statusResponse = await getOperationStatus({
176
- path: { operation_id: operationId },
177
- })
190
+ const operationId = responseData.operation_id
178
191
 
179
- const statusData = statusResponse.data as OperationStatusResponse
180
- const status = statusData?.status
192
+ if (onProgress) {
193
+ onProgress(`Graph creation queued (operation: ${operationId})`)
194
+ }
181
195
 
196
+ // Try SSE first, fall back to polling
197
+ if (useSSE) {
198
+ try {
199
+ return await this.waitWithSSE(operationId, timeout, onProgress)
200
+ } catch (error) {
201
+ // Only fall back to polling for SSE connection failures
202
+ // If it's a GraphOperationError, the operation actually failed - don't retry with polling
203
+ if (error instanceof GraphOperationError) {
204
+ throw error
205
+ }
206
+ // SSE connection failed, fall back to polling
182
207
  if (onProgress) {
183
- onProgress(`Status: ${status} (attempt ${attempt + 1}/${maxAttempts})`)
208
+ onProgress('SSE unavailable, using polling...')
184
209
  }
210
+ }
211
+ }
185
212
 
186
- if (status === 'completed') {
187
- const result = statusData?.result
188
- const graphId = result?.graph_id
213
+ // Fallback to polling
214
+ return await this.waitWithPolling(operationId, timeout, pollInterval, onProgress)
215
+ }
189
216
 
190
- if (graphId) {
191
- if (onProgress) {
192
- onProgress(`Graph created: ${graphId}`)
193
- }
194
- return graphId
217
+ /**
218
+ * Wait for operation completion using SSE stream
219
+ */
220
+ private async waitWithSSE(
221
+ operationId: string,
222
+ timeout: number,
223
+ onProgress?: (message: string) => void
224
+ ): Promise<string> {
225
+ const result = await this.operationClient.monitorOperation<{ graph_id?: string }>(operationId, {
226
+ timeout,
227
+ onProgress: (progress) => {
228
+ if (onProgress) {
229
+ if (progress.progressPercent !== undefined) {
230
+ onProgress(`${progress.message} (${Math.round(progress.progressPercent)}%)`)
195
231
  } else {
196
- throw new Error('Operation completed but no graph_id in result')
232
+ onProgress(progress.message)
197
233
  }
198
- } else if (status === 'failed') {
199
- const error = statusData?.error || statusData?.message || 'Unknown error'
200
- throw new Error(`Graph creation failed: ${error}`)
201
234
  }
235
+ },
236
+ })
237
+
238
+ if (!result.success) {
239
+ throw new GraphOperationError(result.error || 'Graph creation failed')
240
+ }
241
+
242
+ const graphId = result.result?.graph_id
243
+ if (!graphId) {
244
+ throw new GraphOperationError('Operation completed but no graph_id in result')
245
+ }
246
+
247
+ if (onProgress) {
248
+ onProgress(`Graph created: ${graphId}`)
249
+ }
250
+
251
+ return graphId
252
+ }
253
+
254
+ /**
255
+ * Wait for operation completion using polling
256
+ */
257
+ private async waitWithPolling(
258
+ operationId: string,
259
+ timeout: number,
260
+ pollInterval: number,
261
+ onProgress?: (message: string) => void
262
+ ): Promise<string> {
263
+ const maxAttempts = Math.floor(timeout / pollInterval)
264
+
265
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
266
+ await new Promise((resolve) => setTimeout(resolve, pollInterval))
267
+
268
+ const statusResponse = await getOperationStatus({
269
+ path: { operation_id: operationId },
270
+ })
271
+
272
+ const statusData = statusResponse.data as OperationStatusResponse
273
+ const status = statusData?.status
274
+
275
+ if (onProgress) {
276
+ onProgress(`Status: ${status} (attempt ${attempt + 1}/${maxAttempts})`)
202
277
  }
203
278
 
204
- throw new Error(`Graph creation timed out after ${timeout}ms`)
279
+ if (status === 'completed') {
280
+ const result = statusData?.result
281
+ const graphId = result?.graph_id
282
+
283
+ if (graphId) {
284
+ if (onProgress) {
285
+ onProgress(`Graph created: ${graphId}`)
286
+ }
287
+ return graphId
288
+ } else {
289
+ throw new GraphOperationError('Operation completed but no graph_id in result')
290
+ }
291
+ } else if (status === 'failed') {
292
+ const error = statusData?.error || statusData?.message || 'Unknown error'
293
+ throw new GraphOperationError(`Graph creation failed: ${error}`)
294
+ }
205
295
  }
206
296
 
207
- throw new Error('No graph_id or operation_id in response')
297
+ throw new GraphOperationError(`Graph creation timed out after ${timeout}ms`)
208
298
  }
209
299
 
210
300
  /**