@robosystems/client 0.1.10 → 0.1.11

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,245 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Enhanced Query Client with SSE support
5
+ * Provides intelligent query execution with automatic strategy selection
6
+ */
7
+
8
+ import { executeCypherQuery } from '../sdk.gen'
9
+ import { ExecuteCypherQueryData } from '../types.gen'
10
+ import { EventType, SSEClient } from './SSEClient'
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+ export class QueryClient {
21
+ private sseClient?
22
+ private config
23
+
24
+ constructor(config) {
25
+ this.config = config
26
+ }
27
+
28
+ async executeQuery(
29
+ graphId,
30
+ request,
31
+ options = {}
32
+ )> {
33
+ const data = {
34
+ url: '/v1/{graph_id}/query',
35
+ path,
36
+ body,
37
+ query,
38
+ }
39
+
40
+ // Execute the query
41
+ const response = await executeCypherQuery(data)
42
+ const responseData = response.data
43
+
44
+ // Check if this is an immediate response
45
+ if (responseData?.data !== undefined && responseData?.columns) {
46
+ return {
47
+ data.data,
48
+ columns.columns,
49
+ row_count.row_count || responseData.data.length,
50
+ execution_time_ms.execution_time_ms || 0,
51
+ graph_id,
52
+ timestamp.timestamp || new Date().toISOString(),
53
+ }
54
+ }
55
+
56
+ // Check if this is a queued response
57
+ if (responseData?.status === 'queued' && responseData?.operation_id) {
58
+ const queuedResponse = responseData
59
+
60
+ // Notify about queue status
61
+ options.onQueueUpdate?.(queuedResponse.queue_position, queuedResponse.estimated_wait_seconds)
62
+
63
+ // If user doesn't want to wait, throw with queue info
64
+ if (options.maxWait === 0) {
65
+ throw new QueuedQueryError(queuedResponse)
66
+ }
67
+
68
+ // Use SSE to monitor the operation
69
+ if (options.mode === 'stream') {
70
+ return this.streamQueryResults(queuedResponse.operation_id, options)
71
+ } else {
72
+ return this.waitForQueryCompletion(queuedResponse.operation_id, options)
73
+ }
74
+ }
75
+
76
+ // Unexpected response format
77
+ throw new Error('Unexpected response format from query endpoint')
78
+ }
79
+
80
+ private async *streamQueryResults(
81
+ operationId,
82
+ options
83
+ ) {
84
+ const buffer = []
85
+ let completed = false
86
+ let error | null = null
87
+
88
+ // Set up SSE connection
89
+ this.sseClient = new SSEClient(this.config)
90
+ await this.sseClient.connect(operationId)
91
+
92
+ // Listen for data chunks
93
+ this.sseClient.on(EventType.DATA_CHUNK, (data) => {
94
+ if (Array.isArray(data.rows)) {
95
+ buffer.push(...data.rows)
96
+ } else if (data.data) {
97
+ buffer.push(...data.data)
98
+ }
99
+ })
100
+
101
+ // Listen for queue updates
102
+ this.sseClient.on(EventType.QUEUE_UPDATE, (data) => {
103
+ options.onQueueUpdate?.(data.position, data.estimated_wait_seconds)
104
+ })
105
+
106
+ // Listen for progress
107
+ this.sseClient.on(EventType.OPERATION_PROGRESS, (data) => {
108
+ options.onProgress?.(data.message)
109
+ })
110
+
111
+ // Listen for completion
112
+ this.sseClient.on(EventType.OPERATION_COMPLETED, (data) => {
113
+ if (data.result?.data) {
114
+ buffer.push(...data.result.data)
115
+ }
116
+ completed = true
117
+ })
118
+
119
+ // Listen for errors
120
+ this.sseClient.on(EventType.OPERATION_ERROR, (err) => {
121
+ error = new Error(err.message || err.error)
122
+ completed = true
123
+ })
124
+
125
+ // Yield buffered results
126
+ while (!completed || buffer.length > 0) {
127
+ if (error) throw error
128
+
129
+ if (buffer.length > 0) {
130
+ const chunk = buffer.splice(0, options.chunkSize || 100)
131
+ for (const item of chunk) {
132
+ yield item
133
+ }
134
+ } else if (!completed) {
135
+ // Wait for more data
136
+ await new Promise((resolve) => setTimeout(resolve, 100))
137
+ }
138
+ }
139
+
140
+ this.sseClient.close()
141
+ this.sseClient = undefined
142
+ }
143
+
144
+ private async waitForQueryCompletion(
145
+ operationId,
146
+ options
147
+ ) {
148
+ return new Promise((resolve, reject) => {
149
+ const sseClient = new SSEClient(this.config)
150
+
151
+ sseClient
152
+ .connect(operationId)
153
+ .then(() => {
154
+ let result | null = null
155
+
156
+ // Listen for queue updates
157
+ sseClient.on(EventType.QUEUE_UPDATE, (data) => {
158
+ options.onQueueUpdate?.(data.position, data.estimated_wait_seconds)
159
+ })
160
+
161
+ // Listen for progress
162
+ sseClient.on(EventType.OPERATION_PROGRESS, (data) => {
163
+ options.onProgress?.(data.message)
164
+ })
165
+
166
+ sseClient.on(EventType.OPERATION_COMPLETED, (data) => {
167
+ const queryResult = data.result || data
168
+ result = {
169
+ data.data || [],
170
+ columns.columns || [],
171
+ row_count.row_count || 0,
172
+ execution_time_ms.execution_time_ms || 0,
173
+ graph_id.graph_id,
174
+ timestamp.timestamp || new Date().toISOString(),
175
+ }
176
+ sseClient.close()
177
+ resolve(result)
178
+ })
179
+
180
+ sseClient.on(EventType.OPERATION_ERROR, (error) => {
181
+ sseClient.close()
182
+ reject(new Error(error.message || error.error))
183
+ })
184
+
185
+ sseClient.on(EventType.OPERATION_CANCELLED, () => {
186
+ sseClient.close()
187
+ reject(new Error('Query cancelled'))
188
+ })
189
+ })
190
+ .catch(reject)
191
+ })
192
+ }
193
+
194
+ // Convenience method for simple queries
195
+ async query(
196
+ graphId,
197
+ cypher,
198
+ parameters?
199
+ ) {
200
+ return this.executeQuery(
201
+ graphId,
202
+ { query, parameters },
203
+ { mode }
204
+ )
205
+ }
206
+
207
+ // Streaming query for large results
208
+ async *streamQuery(
209
+ graphId,
210
+ cypher,
211
+ parameters?,
212
+ chunkSize = 1000
213
+ ) {
214
+ const result = await this.executeQuery(
215
+ graphId,
216
+ { query, parameters },
217
+ { mode, chunkSize }
218
+ )
219
+
220
+ if (Symbol.asyncIterator in (result)) {
221
+ yield* result
222
+ } else {
223
+ // If not streaming, yield all results at once
224
+ yield* (result).data
225
+ }
226
+ }
227
+
228
+ // Cancel any active SSE connections
229
+ close() {
230
+ if (this.sseClient) {
231
+ this.sseClient.close()
232
+ this.sseClient = undefined
233
+ }
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Error thrown when query is queued and maxWait is 0
239
+ */
240
+ export class QueuedQueryError extends Error {
241
+ constructor(public queueInfo) {
242
+ super('Query was queued')
243
+ this.name = 'QueuedQueryError'
244
+ }
245
+ }
@@ -0,0 +1,283 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Enhanced Query Client with SSE support
5
+ * Provides intelligent query execution with automatic strategy selection
6
+ */
7
+
8
+ import { executeCypherQuery } from '../sdk.gen'
9
+ import type { ExecuteCypherQueryData } from '../types.gen'
10
+ import { EventType, SSEClient } from './SSEClient'
11
+
12
+ export interface QueryRequest {
13
+ query: string
14
+ parameters?: Record<string, any>
15
+ timeout?: number
16
+ }
17
+
18
+ export interface QueryOptions {
19
+ mode?: 'auto' | 'sync' | 'async' | 'stream'
20
+ chunkSize?: number
21
+ testMode?: boolean
22
+ maxWait?: number
23
+ onQueueUpdate?: (position: number, estimatedWait: number) => void
24
+ onProgress?: (message: string) => void
25
+ }
26
+
27
+ export interface QueryResult {
28
+ data: any[]
29
+ columns: string[]
30
+ row_count: number
31
+ execution_time_ms: number
32
+ graph_id?: string
33
+ timestamp?: string
34
+ }
35
+
36
+ export interface QueuedQueryResponse {
37
+ status: 'queued'
38
+ operation_id: string
39
+ queue_position: number
40
+ estimated_wait_seconds: number
41
+ message: string
42
+ }
43
+
44
+ export class QueryClient {
45
+ private sseClient?: SSEClient
46
+ private config: {
47
+ baseUrl: string
48
+ credentials?: 'include' | 'same-origin' | 'omit'
49
+ headers?: Record<string, string>
50
+ }
51
+
52
+ constructor(config: {
53
+ baseUrl: string
54
+ credentials?: 'include' | 'same-origin' | 'omit'
55
+ headers?: Record<string, string>
56
+ }) {
57
+ this.config = config
58
+ }
59
+
60
+ async executeQuery(
61
+ graphId: string,
62
+ request: QueryRequest,
63
+ options: QueryOptions = {}
64
+ ): Promise<QueryResult | AsyncIterableIterator<any>> {
65
+ const data: ExecuteCypherQueryData = {
66
+ url: '/v1/{graph_id}/query' as const,
67
+ path: { graph_id: graphId },
68
+ body: {
69
+ query: request.query,
70
+ parameters: request.parameters,
71
+ },
72
+ query: {
73
+ mode: options.mode,
74
+ test_mode: options.testMode,
75
+ },
76
+ }
77
+
78
+ // Execute the query
79
+ const response = await executeCypherQuery(data)
80
+ const responseData = response.data as any
81
+
82
+ // Check if this is an immediate response
83
+ if (responseData?.data !== undefined && responseData?.columns) {
84
+ return {
85
+ data: responseData.data,
86
+ columns: responseData.columns,
87
+ row_count: responseData.row_count || responseData.data.length,
88
+ execution_time_ms: responseData.execution_time_ms || 0,
89
+ graph_id: graphId,
90
+ timestamp: responseData.timestamp || new Date().toISOString(),
91
+ }
92
+ }
93
+
94
+ // Check if this is a queued response
95
+ if (responseData?.status === 'queued' && responseData?.operation_id) {
96
+ const queuedResponse = responseData as QueuedQueryResponse
97
+
98
+ // Notify about queue status
99
+ options.onQueueUpdate?.(queuedResponse.queue_position, queuedResponse.estimated_wait_seconds)
100
+
101
+ // If user doesn't want to wait, throw with queue info
102
+ if (options.maxWait === 0) {
103
+ throw new QueuedQueryError(queuedResponse)
104
+ }
105
+
106
+ // Use SSE to monitor the operation
107
+ if (options.mode === 'stream') {
108
+ return this.streamQueryResults(queuedResponse.operation_id, options)
109
+ } else {
110
+ return this.waitForQueryCompletion(queuedResponse.operation_id, options)
111
+ }
112
+ }
113
+
114
+ // Unexpected response format
115
+ throw new Error('Unexpected response format from query endpoint')
116
+ }
117
+
118
+ private async *streamQueryResults(
119
+ operationId: string,
120
+ options: QueryOptions
121
+ ): AsyncIterableIterator<any> {
122
+ const buffer: any[] = []
123
+ let completed = false
124
+ let error: Error | null = null
125
+
126
+ // Set up SSE connection
127
+ this.sseClient = new SSEClient(this.config)
128
+ await this.sseClient.connect(operationId)
129
+
130
+ // Listen for data chunks
131
+ this.sseClient.on(EventType.DATA_CHUNK, (data) => {
132
+ if (Array.isArray(data.rows)) {
133
+ buffer.push(...data.rows)
134
+ } else if (data.data) {
135
+ buffer.push(...data.data)
136
+ }
137
+ })
138
+
139
+ // Listen for queue updates
140
+ this.sseClient.on(EventType.QUEUE_UPDATE, (data) => {
141
+ options.onQueueUpdate?.(data.position, data.estimated_wait_seconds)
142
+ })
143
+
144
+ // Listen for progress
145
+ this.sseClient.on(EventType.OPERATION_PROGRESS, (data) => {
146
+ options.onProgress?.(data.message)
147
+ })
148
+
149
+ // Listen for completion
150
+ this.sseClient.on(EventType.OPERATION_COMPLETED, (data) => {
151
+ if (data.result?.data) {
152
+ buffer.push(...data.result.data)
153
+ }
154
+ completed = true
155
+ })
156
+
157
+ // Listen for errors
158
+ this.sseClient.on(EventType.OPERATION_ERROR, (err) => {
159
+ error = new Error(err.message || err.error)
160
+ completed = true
161
+ })
162
+
163
+ // Yield buffered results
164
+ while (!completed || buffer.length > 0) {
165
+ if (error) throw error
166
+
167
+ if (buffer.length > 0) {
168
+ const chunk = buffer.splice(0, options.chunkSize || 100)
169
+ for (const item of chunk) {
170
+ yield item
171
+ }
172
+ } else if (!completed) {
173
+ // Wait for more data
174
+ await new Promise((resolve) => setTimeout(resolve, 100))
175
+ }
176
+ }
177
+
178
+ this.sseClient.close()
179
+ this.sseClient = undefined
180
+ }
181
+
182
+ private async waitForQueryCompletion(
183
+ operationId: string,
184
+ options: QueryOptions
185
+ ): Promise<QueryResult> {
186
+ return new Promise((resolve, reject) => {
187
+ const sseClient = new SSEClient(this.config)
188
+
189
+ sseClient
190
+ .connect(operationId)
191
+ .then(() => {
192
+ let result: QueryResult | null = null
193
+
194
+ // Listen for queue updates
195
+ sseClient.on(EventType.QUEUE_UPDATE, (data) => {
196
+ options.onQueueUpdate?.(data.position, data.estimated_wait_seconds)
197
+ })
198
+
199
+ // Listen for progress
200
+ sseClient.on(EventType.OPERATION_PROGRESS, (data) => {
201
+ options.onProgress?.(data.message)
202
+ })
203
+
204
+ sseClient.on(EventType.OPERATION_COMPLETED, (data) => {
205
+ const queryResult = data.result || data
206
+ result = {
207
+ data: queryResult.data || [],
208
+ columns: queryResult.columns || [],
209
+ row_count: queryResult.row_count || 0,
210
+ execution_time_ms: queryResult.execution_time_ms || 0,
211
+ graph_id: queryResult.graph_id,
212
+ timestamp: queryResult.timestamp || new Date().toISOString(),
213
+ }
214
+ sseClient.close()
215
+ resolve(result)
216
+ })
217
+
218
+ sseClient.on(EventType.OPERATION_ERROR, (error) => {
219
+ sseClient.close()
220
+ reject(new Error(error.message || error.error))
221
+ })
222
+
223
+ sseClient.on(EventType.OPERATION_CANCELLED, () => {
224
+ sseClient.close()
225
+ reject(new Error('Query cancelled'))
226
+ })
227
+ })
228
+ .catch(reject)
229
+ })
230
+ }
231
+
232
+ // Convenience method for simple queries
233
+ async query(
234
+ graphId: string,
235
+ cypher: string,
236
+ parameters?: Record<string, any>
237
+ ): Promise<QueryResult> {
238
+ return this.executeQuery(
239
+ graphId,
240
+ { query: cypher, parameters },
241
+ { mode: 'auto' }
242
+ ) as Promise<QueryResult>
243
+ }
244
+
245
+ // Streaming query for large results
246
+ async *streamQuery(
247
+ graphId: string,
248
+ cypher: string,
249
+ parameters?: Record<string, any>,
250
+ chunkSize: number = 1000
251
+ ): AsyncIterableIterator<any> {
252
+ const result = await this.executeQuery(
253
+ graphId,
254
+ { query: cypher, parameters },
255
+ { mode: 'stream', chunkSize }
256
+ )
257
+
258
+ if (Symbol.asyncIterator in (result as any)) {
259
+ yield* result as AsyncIterableIterator<any>
260
+ } else {
261
+ // If not streaming, yield all results at once
262
+ yield* (result as QueryResult).data
263
+ }
264
+ }
265
+
266
+ // Cancel any active SSE connections
267
+ close(): void {
268
+ if (this.sseClient) {
269
+ this.sseClient.close()
270
+ this.sseClient = undefined
271
+ }
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Error thrown when query is queued and maxWait is 0
277
+ */
278
+ export class QueuedQueryError extends Error {
279
+ constructor(public queueInfo: QueuedQueryResponse) {
280
+ super('Query was queued')
281
+ this.name = 'QueuedQueryError'
282
+ }
283
+ }
@@ -0,0 +1,166 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Core SSE (Server-Sent Events) client for RoboSystems API
5
+ * Provides automatic reconnection, event replay, and type-safe event handling
6
+ */
7
+
8
+
9
+
10
+
11
+
12
+ export enum EventType {
13
+ OPERATION_STARTED = 'operation_started',
14
+ OPERATION_PROGRESS = 'operation_progress',
15
+ OPERATION_COMPLETED = 'operation_completed',
16
+ OPERATION_ERROR = 'operation_error',
17
+ OPERATION_CANCELLED = 'operation_cancelled',
18
+ DATA_CHUNK = 'data_chunk',
19
+ METADATA = 'metadata',
20
+ HEARTBEAT = 'heartbeat',
21
+ QUEUE_UPDATE = 'queue_update',
22
+ }
23
+
24
+ export class SSEClient {
25
+ private config
26
+ private eventSource?
27
+ private reconnectAttempts = 0
28
+ private lastEventId?
29
+ private closed = false
30
+ private listeners void>> = new Map()
31
+
32
+ constructor(config) {
33
+ this.config = {
34
+ maxRetries,
35
+ retryDelay,
36
+ heartbeatInterval,
37
+ ...config,
38
+ }
39
+ }
40
+
41
+ async connect(operationId, fromSequence = 0) {
42
+ return new Promise((resolve, reject) => {
43
+ const url = `${this.config.baseUrl}/v1/operations/${operationId}/stream?from_sequence=${fromSequence}`
44
+
45
+ this.eventSource = new EventSource(url, {
46
+ withCredentials.config.credentials === 'include',
47
+ })
48
+
49
+ const connectionTimeout = setTimeout(() => {
50
+ reject(new Error('Connection timeout'))
51
+ this.close()
52
+ }, 10000)
53
+
54
+ this.eventSource.onopen = () => {
55
+ clearTimeout(connectionTimeout)
56
+ this.reconnectAttempts = 0
57
+ this.emit('connected', null)
58
+ resolve()
59
+ }
60
+
61
+ this.eventSource.onerror = (error) => {
62
+ clearTimeout(connectionTimeout)
63
+ if (!this.closed) {
64
+ this.handleError(error, operationId, fromSequence)
65
+ }
66
+ }
67
+
68
+ this.eventSource.onmessage = (event) => {
69
+ this.handleMessage(event)
70
+ }
71
+
72
+ // Set up specific event listeners
73
+ Object.values(EventType).forEach((eventType) => {
74
+ this.eventSource!.addEventListener(eventType, (event) => {
75
+ this.handleTypedEvent(eventType, event)
76
+ })
77
+ })
78
+ })
79
+ }
80
+
81
+ private handleMessage(event) {
82
+ try {
83
+ const data = JSON.parse(event.data)
84
+ const sseEvent = {
85
+ event.type || 'message',
86
+ data,
87
+ id.lastEventId,
88
+ timestamp Date(),
89
+ }
90
+
91
+ this.lastEventId = event.lastEventId
92
+ this.emit('event', sseEvent)
93
+ } catch (error) {
94
+ this.emit('parse_error', { error, rawData.data })
95
+ }
96
+ }
97
+
98
+ private handleTypedEvent(eventType, event) {
99
+ try {
100
+ const data = JSON.parse(event.data)
101
+ this.lastEventId = event.lastEventId
102
+ this.emit(eventType, data)
103
+
104
+ // Check for completion events
105
+ if (
106
+ eventType === EventType.OPERATION_COMPLETED ||
107
+ eventType === EventType.OPERATION_ERROR ||
108
+ eventType === EventType.OPERATION_CANCELLED
109
+ ) {
110
+ this.close()
111
+ }
112
+ } catch (error) {
113
+ this.emit('parse_error', { error, rawData.data })
114
+ }
115
+ }
116
+
117
+ private async handleError(error, operationId, fromSequence) {
118
+ if (this.closed) return
119
+
120
+ if (this.reconnectAttempts {
121
+ const resumeFrom = this.lastEventId ? parseInt(this.lastEventId) + 1
122
+ this.connect(operationId, resumeFrom).catch(() => {
123
+ // Error handled in connect
124
+ })
125
+ }, delay)
126
+ } else {
127
+ this.emit('max_retries_exceeded', error)
128
+ this.close()
129
+ }
130
+ }
131
+
132
+ on(event, listener) {
133
+ if (!this.listeners.has(event)) {
134
+ this.listeners.set(event, new Set())
135
+ }
136
+ this.listeners.get(event)!.add(listener)
137
+ }
138
+
139
+ off(event, listener) {
140
+ const listeners = this.listeners.get(event)
141
+ if (listeners) {
142
+ listeners.delete(listener)
143
+ }
144
+ }
145
+
146
+ private emit(event, data) {
147
+ const listeners = this.listeners.get(event)
148
+ if (listeners) {
149
+ listeners.forEach((listener) => listener(data))
150
+ }
151
+ }
152
+
153
+ close() {
154
+ this.closed = true
155
+ if (this.eventSource) {
156
+ this.eventSource.close()
157
+ this.eventSource = undefined
158
+ }
159
+ this.emit('closed', null)
160
+ this.listeners.clear()
161
+ }
162
+
163
+ isConnected() {
164
+ return this.eventSource !== undefined && this.eventSource.readyState === EventSource.OPEN
165
+ }
166
+ }