@robosystems/client 0.1.17 → 0.1.19

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.
@@ -0,0 +1,438 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Enhanced Copy Client with SSE support
5
+ * Provides intelligent data copy operations with progress monitoring
6
+ */
7
+
8
+ import { copyDataToGraph } from '../sdk.gen'
9
+ import type {
10
+ CopyDataToGraphData,
11
+ CopyResponse,
12
+ DataFrameCopyRequest,
13
+ S3CopyRequest,
14
+ UrlCopyRequest,
15
+ } from '../types.gen'
16
+ import { EventType, SSEClient } from './SSEClient'
17
+
18
+ export type CopySourceType = 's3' | 'url' | 'dataframe'
19
+
20
+ export interface CopyOptions {
21
+ onProgress?: (message: string, progressPercent?: number) => void
22
+ onQueueUpdate?: (position: number, estimatedWait: number) => void
23
+ onWarning?: (warning: string) => void
24
+ timeout?: number
25
+ testMode?: boolean
26
+ }
27
+
28
+ export interface CopyResult {
29
+ status: 'completed' | 'failed' | 'partial' | 'accepted'
30
+ rowsImported?: number
31
+ rowsSkipped?: number
32
+ bytesProcessed?: number
33
+ executionTimeMs?: number
34
+ warnings?: string[]
35
+ error?: string
36
+ operationId?: string
37
+ sseUrl?: string
38
+ message?: string
39
+ }
40
+
41
+ export interface CopyStatistics {
42
+ totalRows: number
43
+ importedRows: number
44
+ skippedRows: number
45
+ bytesProcessed: number
46
+ duration: number
47
+ throughput: number // rows per second
48
+ }
49
+
50
+ export class CopyClient {
51
+ private sseClient?: SSEClient
52
+ private config: {
53
+ baseUrl: string
54
+ credentials?: 'include' | 'same-origin' | 'omit'
55
+ headers?: Record<string, string>
56
+ }
57
+
58
+ constructor(config: {
59
+ baseUrl: string
60
+ credentials?: 'include' | 'same-origin' | 'omit'
61
+ headers?: Record<string, string>
62
+ }) {
63
+ this.config = config
64
+ }
65
+
66
+ /**
67
+ * Copy data from S3 to graph database
68
+ */
69
+ async copyFromS3(
70
+ graphId: string,
71
+ request: S3CopyRequest,
72
+ options: CopyOptions = {}
73
+ ): Promise<CopyResult> {
74
+ return this.executeCopy(graphId, request, 's3', options)
75
+ }
76
+
77
+ /**
78
+ * Copy data from URL to graph database (when available)
79
+ */
80
+ async copyFromUrl(
81
+ graphId: string,
82
+ request: UrlCopyRequest,
83
+ options: CopyOptions = {}
84
+ ): Promise<CopyResult> {
85
+ return this.executeCopy(graphId, request, 'url', options)
86
+ }
87
+
88
+ /**
89
+ * Copy data from DataFrame to graph database (when available)
90
+ */
91
+ async copyFromDataFrame(
92
+ graphId: string,
93
+ request: DataFrameCopyRequest,
94
+ options: CopyOptions = {}
95
+ ): Promise<CopyResult> {
96
+ return this.executeCopy(graphId, request, 'dataframe', options)
97
+ }
98
+
99
+ /**
100
+ * Execute copy operation with automatic SSE monitoring for long-running operations
101
+ */
102
+ private async executeCopy(
103
+ graphId: string,
104
+ request: S3CopyRequest | UrlCopyRequest | DataFrameCopyRequest,
105
+ _sourceType: CopySourceType,
106
+ options: CopyOptions = {}
107
+ ): Promise<CopyResult> {
108
+ const startTime = Date.now()
109
+
110
+ const data: CopyDataToGraphData = {
111
+ url: '/v1/{graph_id}/copy' as const,
112
+ path: { graph_id: graphId },
113
+ body: request,
114
+ }
115
+
116
+ try {
117
+ // Execute the copy request
118
+ const response = await copyDataToGraph(data)
119
+ const responseData = response.data as CopyResponse
120
+
121
+ // Check if this is an accepted (async) operation
122
+ if (responseData.status === 'accepted' && responseData.operation_id) {
123
+ // This is a long-running operation with SSE monitoring
124
+ options.onProgress?.(`Copy operation started. Monitoring progress...`)
125
+
126
+ // If SSE URL is provided, use it for monitoring
127
+ if (responseData.sse_url) {
128
+ return this.monitorCopyOperation(responseData.operation_id, options, startTime)
129
+ }
130
+
131
+ // Otherwise return the accepted response
132
+ return {
133
+ status: 'accepted',
134
+ operationId: responseData.operation_id,
135
+ sseUrl: responseData.sse_url,
136
+ message: responseData.message,
137
+ }
138
+ }
139
+
140
+ // This is a synchronous response - operation completed immediately
141
+ return this.buildCopyResult(responseData, Date.now() - startTime)
142
+ } catch (error) {
143
+ return {
144
+ status: 'failed',
145
+ error: error instanceof Error ? error.message : String(error),
146
+ executionTimeMs: Date.now() - startTime,
147
+ }
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Monitor a copy operation using SSE
153
+ */
154
+ private async monitorCopyOperation(
155
+ operationId: string,
156
+ options: CopyOptions,
157
+ startTime: number
158
+ ): Promise<CopyResult> {
159
+ return new Promise((resolve, reject) => {
160
+ const sseClient = new SSEClient(this.config)
161
+ const timeoutMs = options.timeout || 3600000 // Default 1 hour for copy operations
162
+
163
+ const timeoutHandle = setTimeout(() => {
164
+ sseClient.close()
165
+ reject(new Error(`Copy operation timeout after ${timeoutMs}ms`))
166
+ }, timeoutMs)
167
+
168
+ sseClient
169
+ .connect(operationId)
170
+ .then(() => {
171
+ let result: CopyResult = { status: 'failed' }
172
+ const warnings: string[] = []
173
+
174
+ // Listen for queue updates
175
+ sseClient.on(EventType.QUEUE_UPDATE, (data) => {
176
+ options.onQueueUpdate?.(
177
+ data.position || data.queue_position,
178
+ data.estimated_wait_seconds || 0
179
+ )
180
+ })
181
+
182
+ // Listen for progress updates
183
+ sseClient.on(EventType.OPERATION_PROGRESS, (data) => {
184
+ const message = data.message || data.status || 'Processing...'
185
+ const progressPercent = data.progress_percent || data.progress
186
+
187
+ options.onProgress?.(message, progressPercent)
188
+
189
+ // Check for warnings in progress updates
190
+ if (data.warnings) {
191
+ warnings.push(...data.warnings)
192
+ data.warnings.forEach((warning: string) => {
193
+ options.onWarning?.(warning)
194
+ })
195
+ }
196
+ })
197
+
198
+ // Listen for completion
199
+ sseClient.on(EventType.OPERATION_COMPLETED, (data) => {
200
+ clearTimeout(timeoutHandle)
201
+
202
+ const completionData = data.result || data
203
+ result = {
204
+ status: completionData.status || 'completed',
205
+ rowsImported: completionData.rows_imported,
206
+ rowsSkipped: completionData.rows_skipped,
207
+ bytesProcessed: completionData.bytes_processed,
208
+ executionTimeMs: Date.now() - startTime,
209
+ warnings: warnings.length > 0 ? warnings : completionData.warnings,
210
+ message: completionData.message,
211
+ }
212
+
213
+ sseClient.close()
214
+ resolve(result)
215
+ })
216
+
217
+ // Listen for errors
218
+ sseClient.on(EventType.OPERATION_ERROR, (error) => {
219
+ clearTimeout(timeoutHandle)
220
+
221
+ result = {
222
+ status: 'failed',
223
+ error: error.message || error.error || 'Copy operation failed',
224
+ executionTimeMs: Date.now() - startTime,
225
+ warnings: warnings.length > 0 ? warnings : undefined,
226
+ }
227
+
228
+ sseClient.close()
229
+ resolve(result) // Resolve with error result, not reject
230
+ })
231
+
232
+ // Listen for cancellation
233
+ sseClient.on(EventType.OPERATION_CANCELLED, () => {
234
+ clearTimeout(timeoutHandle)
235
+
236
+ result = {
237
+ status: 'failed',
238
+ error: 'Copy operation cancelled',
239
+ executionTimeMs: Date.now() - startTime,
240
+ warnings: warnings.length > 0 ? warnings : undefined,
241
+ }
242
+
243
+ sseClient.close()
244
+ resolve(result)
245
+ })
246
+ })
247
+ .catch((error) => {
248
+ clearTimeout(timeoutHandle)
249
+ reject(error)
250
+ })
251
+ })
252
+ }
253
+
254
+ /**
255
+ * Build copy result from response data
256
+ */
257
+ private buildCopyResult(responseData: CopyResponse, executionTimeMs: number): CopyResult {
258
+ return {
259
+ status: responseData.status,
260
+ rowsImported: responseData.rows_imported || undefined,
261
+ rowsSkipped: responseData.rows_skipped || undefined,
262
+ bytesProcessed: responseData.bytes_processed || undefined,
263
+ executionTimeMs: responseData.execution_time_ms || executionTimeMs,
264
+ warnings: responseData.warnings || undefined,
265
+ message: responseData.message,
266
+ error: responseData.error_details ? String(responseData.error_details) : undefined,
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Calculate copy statistics from result
272
+ */
273
+ calculateStatistics(result: CopyResult): CopyStatistics | null {
274
+ if (result.status === 'failed' || !result.rowsImported) {
275
+ return null
276
+ }
277
+
278
+ const totalRows = (result.rowsImported || 0) + (result.rowsSkipped || 0)
279
+ const duration = (result.executionTimeMs || 0) / 1000 // Convert to seconds
280
+ const throughput = duration > 0 ? (result.rowsImported || 0) / duration : 0
281
+
282
+ return {
283
+ totalRows,
284
+ importedRows: result.rowsImported || 0,
285
+ skippedRows: result.rowsSkipped || 0,
286
+ bytesProcessed: result.bytesProcessed || 0,
287
+ duration,
288
+ throughput,
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Convenience method for simple S3 copy with default options
294
+ */
295
+ async copyS3(
296
+ graphId: string,
297
+ tableName: string,
298
+ s3Uri: string,
299
+ accessKeyId: string,
300
+ secretAccessKey: string,
301
+ options?: {
302
+ region?: string
303
+ fileFormat?: 'csv' | 'parquet' | 'json' | 'delta' | 'iceberg'
304
+ ignoreErrors?: boolean
305
+ }
306
+ ): Promise<CopyResult> {
307
+ const request: S3CopyRequest = {
308
+ table_name: tableName,
309
+ source_type: 's3',
310
+ s3_path: s3Uri,
311
+ s3_access_key_id: accessKeyId,
312
+ s3_secret_access_key: secretAccessKey,
313
+ s3_region: options?.region || 'us-east-1',
314
+ file_format: options?.fileFormat,
315
+ ignore_errors: options?.ignoreErrors || false,
316
+ }
317
+
318
+ return this.copyFromS3(graphId, request)
319
+ }
320
+
321
+ /**
322
+ * Monitor multiple copy operations concurrently
323
+ */
324
+ async monitorMultipleCopies(
325
+ operationIds: string[],
326
+ options: CopyOptions = {}
327
+ ): Promise<Map<string, CopyResult>> {
328
+ const results = await Promise.all(
329
+ operationIds.map(async (id) => {
330
+ const result = await this.monitorCopyOperation(id, options, Date.now())
331
+ return [id, result] as [string, CopyResult]
332
+ })
333
+ )
334
+ return new Map(results)
335
+ }
336
+
337
+ /**
338
+ * Batch copy multiple tables from S3
339
+ */
340
+ async batchCopyFromS3(
341
+ graphId: string,
342
+ copies: Array<{
343
+ request: S3CopyRequest
344
+ options?: CopyOptions
345
+ }>
346
+ ): Promise<CopyResult[]> {
347
+ return Promise.all(
348
+ copies.map(({ request, options }) => this.copyFromS3(graphId, request, options || {}))
349
+ )
350
+ }
351
+
352
+ /**
353
+ * Copy with retry logic for transient failures
354
+ */
355
+ async copyWithRetry(
356
+ graphId: string,
357
+ request: S3CopyRequest | UrlCopyRequest | DataFrameCopyRequest,
358
+ sourceType: CopySourceType,
359
+ maxRetries: number = 3,
360
+ options: CopyOptions = {}
361
+ ): Promise<CopyResult> {
362
+ let lastError: Error | undefined
363
+ let attempt = 0
364
+
365
+ while (attempt < maxRetries) {
366
+ attempt++
367
+
368
+ try {
369
+ const result = await this.executeCopy(graphId, request, sourceType, options)
370
+
371
+ // If successful or partially successful, return
372
+ if (result.status === 'completed' || result.status === 'partial') {
373
+ return result
374
+ }
375
+
376
+ // If failed, check if it's retryable
377
+ if (result.status === 'failed') {
378
+ const isRetryable = this.isRetryableError(result.error)
379
+ if (!isRetryable || attempt === maxRetries) {
380
+ return result
381
+ }
382
+
383
+ // Wait before retry with exponential backoff
384
+ const waitTime = Math.min(1000 * Math.pow(2, attempt - 1), 30000)
385
+ options.onProgress?.(
386
+ `Retrying copy operation (attempt ${attempt}/${maxRetries}) in ${waitTime}ms...`
387
+ )
388
+ await new Promise((resolve) => setTimeout(resolve, waitTime))
389
+ }
390
+ } catch (error) {
391
+ lastError = error instanceof Error ? error : new Error(String(error))
392
+
393
+ if (attempt === maxRetries) {
394
+ throw lastError
395
+ }
396
+
397
+ // Wait before retry
398
+ const waitTime = Math.min(1000 * Math.pow(2, attempt - 1), 30000)
399
+ options.onProgress?.(
400
+ `Retrying after error (attempt ${attempt}/${maxRetries}) in ${waitTime}ms...`
401
+ )
402
+ await new Promise((resolve) => setTimeout(resolve, waitTime))
403
+ }
404
+ }
405
+
406
+ throw lastError || new Error('Copy operation failed after all retries')
407
+ }
408
+
409
+ /**
410
+ * Check if an error is retryable
411
+ */
412
+ private isRetryableError(error?: string): boolean {
413
+ if (!error) return false
414
+
415
+ const retryablePatterns = [
416
+ 'timeout',
417
+ 'network',
418
+ 'connection',
419
+ 'temporary',
420
+ 'unavailable',
421
+ 'rate limit',
422
+ 'throttl',
423
+ ]
424
+
425
+ const lowerError = error.toLowerCase()
426
+ return retryablePatterns.some((pattern) => lowerError.includes(pattern))
427
+ }
428
+
429
+ /**
430
+ * Cancel any active SSE connections
431
+ */
432
+ close(): void {
433
+ if (this.sseClient) {
434
+ this.sseClient.close()
435
+ this.sseClient = undefined
436
+ }
437
+ }
438
+ }
@@ -1,3 +1,6 @@
1
+ import type { S3CopyRequest } from '../types.gen';
2
+ import type { CopyOptions, CopyResult } from './CopyClient';
3
+ import { CopyClient } from './CopyClient';
1
4
  import type { OperationProgress, OperationResult } from './OperationClient';
2
5
  import { OperationClient } from './OperationClient';
3
6
  import type { QueryOptions, QueryResult } from './QueryClient';
@@ -105,6 +108,38 @@ export declare function useMultipleOperations<T = any>(): {
105
108
  * ```
106
109
  */
107
110
  export declare function useSDKClients(): {
111
+ copy: CopyClient | null;
108
112
  query: QueryClient | null;
109
113
  operations: OperationClient | null;
110
114
  };
115
+ /**
116
+ * Hook for copying data from S3 to graph database with progress monitoring
117
+ *
118
+ * @example
119
+ * ```tsx
120
+ * const { copyFromS3, loading, progress, error, result } = useCopy('graph_123')
121
+ *
122
+ * const handleImport = async () => {
123
+ * const result = await copyFromS3({
124
+ * table_name: 'companies',
125
+ * source_type: 's3',
126
+ * s3_uri: 's3://my-bucket/data.csv',
127
+ * aws_access_key_id: 'KEY',
128
+ * aws_secret_access_key: 'SECRET',
129
+ * })
130
+ * }
131
+ * ```
132
+ */
133
+ export declare function useCopy(graphId: string): {
134
+ copyFromS3: (request: S3CopyRequest, options?: CopyOptions) => Promise<CopyResult | null>;
135
+ copyWithRetry: (request: S3CopyRequest, maxRetries?: number) => Promise<CopyResult | null>;
136
+ getStatistics: () => import("./CopyClient").CopyStatistics;
137
+ loading: boolean;
138
+ error: Error;
139
+ result: CopyResult;
140
+ progress: {
141
+ message: string;
142
+ percent?: number;
143
+ };
144
+ queuePosition: number;
145
+ };
@@ -6,6 +6,7 @@ exports.useStreamingQuery = useStreamingQuery;
6
6
  exports.useOperation = useOperation;
7
7
  exports.useMultipleOperations = useMultipleOperations;
8
8
  exports.useSDKClients = useSDKClients;
9
+ exports.useCopy = useCopy;
9
10
  /**
10
11
  * React hooks for SDK extensions
11
12
  * Provides easy-to-use hooks for Next.js/React applications
@@ -13,6 +14,7 @@ exports.useSDKClients = useSDKClients;
13
14
  const react_1 = require("react");
14
15
  const client_gen_1 = require("../client.gen");
15
16
  const config_1 = require("./config");
17
+ const CopyClient_1 = require("./CopyClient");
16
18
  const OperationClient_1 = require("./OperationClient");
17
19
  const QueryClient_1 = require("./QueryClient");
18
20
  /**
@@ -345,6 +347,7 @@ function useMultipleOperations() {
345
347
  */
346
348
  function useSDKClients() {
347
349
  const [clients, setClients] = (0, react_1.useState)({
350
+ copy: null,
348
351
  query: null,
349
352
  operations: null,
350
353
  });
@@ -356,16 +359,136 @@ function useSDKClients() {
356
359
  credentials: sdkConfig.credentials,
357
360
  headers: sdkConfig.headers,
358
361
  };
362
+ const copyClient = new CopyClient_1.CopyClient(baseConfig);
359
363
  const queryClient = new QueryClient_1.QueryClient(baseConfig);
360
364
  const operationsClient = new OperationClient_1.OperationClient(baseConfig);
361
365
  setClients({
366
+ copy: copyClient,
362
367
  query: queryClient,
363
368
  operations: operationsClient,
364
369
  });
365
370
  return () => {
371
+ copyClient.close();
366
372
  queryClient.close();
367
373
  operationsClient.closeAll();
368
374
  };
369
375
  }, []);
370
376
  return clients;
371
377
  }
378
+ /**
379
+ * Hook for copying data from S3 to graph database with progress monitoring
380
+ *
381
+ * @example
382
+ * ```tsx
383
+ * const { copyFromS3, loading, progress, error, result } = useCopy('graph_123')
384
+ *
385
+ * const handleImport = async () => {
386
+ * const result = await copyFromS3({
387
+ * table_name: 'companies',
388
+ * source_type: 's3',
389
+ * s3_uri: 's3://my-bucket/data.csv',
390
+ * aws_access_key_id: 'KEY',
391
+ * aws_secret_access_key: 'SECRET',
392
+ * })
393
+ * }
394
+ * ```
395
+ */
396
+ function useCopy(graphId) {
397
+ const [loading, setLoading] = (0, react_1.useState)(false);
398
+ const [error, setError] = (0, react_1.useState)(null);
399
+ const [result, setResult] = (0, react_1.useState)(null);
400
+ const [progress, setProgress] = (0, react_1.useState)(null);
401
+ const [queuePosition, setQueuePosition] = (0, react_1.useState)(null);
402
+ const clientRef = (0, react_1.useRef)(null);
403
+ // Initialize client
404
+ (0, react_1.useEffect)(() => {
405
+ const sdkConfig = (0, config_1.getSDKExtensionsConfig)();
406
+ const clientConfig = client_gen_1.client.getConfig();
407
+ clientRef.current = new CopyClient_1.CopyClient({
408
+ baseUrl: sdkConfig.baseUrl || clientConfig.baseUrl || 'http://localhost:8000',
409
+ credentials: sdkConfig.credentials,
410
+ headers: sdkConfig.headers,
411
+ });
412
+ return () => {
413
+ clientRef.current?.close();
414
+ };
415
+ }, []);
416
+ const copyFromS3 = (0, react_1.useCallback)(async (request, options) => {
417
+ if (!clientRef.current)
418
+ return null;
419
+ setLoading(true);
420
+ setError(null);
421
+ setResult(null);
422
+ setProgress(null);
423
+ setQueuePosition(null);
424
+ try {
425
+ const copyResult = await clientRef.current.copyFromS3(graphId, request, {
426
+ ...options,
427
+ onProgress: (message, progressPercent) => {
428
+ setProgress({ message, percent: progressPercent });
429
+ setQueuePosition(null); // Clear queue position when executing
430
+ },
431
+ onQueueUpdate: (position, estimatedWait) => {
432
+ setQueuePosition(position);
433
+ setProgress({
434
+ message: `Queue position: ${position} (est. ${estimatedWait}s)`,
435
+ });
436
+ },
437
+ onWarning: (warning) => {
438
+ console.warn('Copy warning:', warning);
439
+ },
440
+ });
441
+ setResult(copyResult);
442
+ return copyResult;
443
+ }
444
+ catch (err) {
445
+ const error = err;
446
+ setError(error);
447
+ return null;
448
+ }
449
+ finally {
450
+ setLoading(false);
451
+ setQueuePosition(null);
452
+ }
453
+ }, [graphId]);
454
+ // Simple copy method with retry logic
455
+ const copyWithRetry = (0, react_1.useCallback)(async (request, maxRetries = 3) => {
456
+ if (!clientRef.current)
457
+ return null;
458
+ setLoading(true);
459
+ setError(null);
460
+ try {
461
+ const result = await clientRef.current.copyWithRetry(graphId, request, 's3', maxRetries, {
462
+ onProgress: (message, progressPercent) => {
463
+ setProgress({ message, percent: progressPercent });
464
+ },
465
+ });
466
+ setResult(result);
467
+ return result;
468
+ }
469
+ catch (err) {
470
+ const error = err;
471
+ setError(error);
472
+ return null;
473
+ }
474
+ finally {
475
+ setLoading(false);
476
+ }
477
+ }, [graphId]);
478
+ // Get statistics from the last copy operation
479
+ const getStatistics = (0, react_1.useCallback)(() => {
480
+ if (!clientRef.current || !result)
481
+ return null;
482
+ return clientRef.current.calculateStatistics(result);
483
+ }, [result]);
484
+ return {
485
+ copyFromS3,
486
+ copyWithRetry,
487
+ getStatistics,
488
+ loading,
489
+ error,
490
+ result,
491
+ progress,
492
+ queuePosition,
493
+ };
494
+ }