@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.
- package/extensions/GraphClient.d.ts +21 -1
- package/extensions/GraphClient.js +100 -32
- package/extensions/GraphClient.test.ts +176 -6
- package/extensions/GraphClient.ts +124 -34
- package/package.json +3 -3
- package/sdk-extensions/GraphClient.d.ts +21 -1
- package/sdk-extensions/GraphClient.js +100 -32
- package/sdk-extensions/GraphClient.test.ts +176 -6
- package/sdk-extensions/GraphClient.ts +124 -34
|
@@ -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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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(
|
|
110
|
+
onProgress('SSE unavailable, using polling...');
|
|
90
111
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
129
|
+
onProgress(progress.message);
|
|
102
130
|
}
|
|
103
131
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
vi.
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
path: { operation_id: operationId },
|
|
177
|
-
})
|
|
190
|
+
const operationId = responseData.operation_id
|
|
178
191
|
|
|
179
|
-
|
|
180
|
-
|
|
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(
|
|
208
|
+
onProgress('SSE unavailable, using polling...')
|
|
184
209
|
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
185
212
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
213
|
+
// Fallback to polling
|
|
214
|
+
return await this.waitWithPolling(operationId, timeout, pollInterval, onProgress)
|
|
215
|
+
}
|
|
189
216
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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(
|
|
110
|
+
onProgress('SSE unavailable, using polling...');
|
|
90
111
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
129
|
+
onProgress(progress.message);
|
|
102
130
|
}
|
|
103
131
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
vi.
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
path: { operation_id: operationId },
|
|
177
|
-
})
|
|
190
|
+
const operationId = responseData.operation_id
|
|
178
191
|
|
|
179
|
-
|
|
180
|
-
|
|
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(
|
|
208
|
+
onProgress('SSE unavailable, using polling...')
|
|
184
209
|
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
185
212
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
213
|
+
// Fallback to polling
|
|
214
|
+
return await this.waitWithPolling(operationId, timeout, pollInterval, onProgress)
|
|
215
|
+
}
|
|
189
216
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
297
|
+
throw new GraphOperationError(`Graph creation timed out after ${timeout}ms`)
|
|
208
298
|
}
|
|
209
299
|
|
|
210
300
|
/**
|