@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,297 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * General Operations Client for monitoring async operations
5
+ * Handles graph creation, backups, imports, and other long-running tasks
6
+ */
7
+
8
+ import { cancelOperation, getOperationStatus } from '../sdk.gen'
9
+ import { EventType, SSEClient } from './SSEClient'
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+ export class OperationClient {
18
+ private sseClients = new Map()
19
+ private cleanupTimeouts> = new Map()
20
+ private config
21
+ private cleanupIntervalMs = 300000 // 5 minutes
22
+ private cleanupInterval?
23
+
24
+ constructor(config) {
25
+ this.config = config
26
+
27
+ // Start periodic cleanup check every 5 minutes
28
+ this.cleanupInterval = setInterval(() => {
29
+ this.performPeriodicCleanup()
30
+ }, this.cleanupIntervalMs)
31
+ }
32
+
33
+ async monitorOperation(
34
+ operationId,
35
+ options = {}
36
+ )> {
37
+ return new Promise((resolve, reject) => {
38
+ const sseClient = new SSEClient(this.config)
39
+ this.sseClients.set(operationId, sseClient)
40
+
41
+ const timeoutHandle = options.timeout
42
+ ? setTimeout(() => {
43
+ this.cleanupClient(operationId)
44
+ reject(new Error(`Operation timeout after ${options.timeout}ms`))
45
+ }, options.timeout)
46
+
47
+
48
+ sseClient
49
+ .connect(operationId)
50
+ .then(() => {
51
+ let result = { success }
52
+
53
+ // Track queue updates
54
+ if (options.onQueueUpdate) {
55
+ sseClient.on(EventType.QUEUE_UPDATE, (data) => {
56
+ options.onQueueUpdate!(
57
+ data.position || data.queue_position,
58
+ data.estimated_wait_seconds || 0
59
+ )
60
+ })
61
+ }
62
+
63
+ // Track progress
64
+ if (options.onProgress) {
65
+ sseClient.on(EventType.OPERATION_PROGRESS, (data) => {
66
+ options.onProgress!({
67
+ message.message || data.status || 'Processing...',
68
+ progressPercent.progress_percent || data.progress,
69
+ details,
70
+ })
71
+ })
72
+ }
73
+
74
+ // Handle completion
75
+ sseClient.on(EventType.OPERATION_COMPLETED, (data) => {
76
+ if (timeoutHandle) clearTimeout(timeoutHandle)
77
+ result = {
78
+ success,
79
+ result.result || data,
80
+ metadata.metadata,
81
+ }
82
+ // Schedule cleanup after a short delay to ensure all data is received
83
+ this.scheduleCleanup(operationId, 5000)
84
+ resolve(result)
85
+ })
86
+
87
+ // Handle errors
88
+ sseClient.on(EventType.OPERATION_ERROR, (error) => {
89
+ if (timeoutHandle) clearTimeout(timeoutHandle)
90
+ result = {
91
+ success,
92
+ error.message || error.error || 'Operation failed',
93
+ metadata.metadata,
94
+ }
95
+ // Schedule cleanup after a short delay
96
+ this.scheduleCleanup(operationId, 5000)
97
+ resolve(result) // Resolve with error result, not reject
98
+ })
99
+
100
+ // Handle cancellation
101
+ sseClient.on(EventType.OPERATION_CANCELLED, (data) => {
102
+ if (timeoutHandle) clearTimeout(timeoutHandle)
103
+ result = {
104
+ success,
105
+ error: 'Operation cancelled',
106
+ metadata,
107
+ }
108
+ // Schedule cleanup after a short delay
109
+ this.scheduleCleanup(operationId, 5000)
110
+ resolve(result)
111
+ })
112
+ })
113
+ .catch((error) => {
114
+ if (timeoutHandle) clearTimeout(timeoutHandle)
115
+ this.cleanupClient(operationId)
116
+ reject(error)
117
+ })
118
+ })
119
+ }
120
+
121
+ /**
122
+ * Monitor multiple operations concurrently
123
+ */
124
+ async monitorMultiple(
125
+ operationIds,
126
+ options = {}
127
+ )>> {
128
+ const results = await Promise.all(
129
+ operationIds.map(async (id) => {
130
+ const result = await this.monitorOperation(id, options)
131
+ return [id, result] as [string, OperationResult]
132
+ })
133
+ )
134
+ return new Map(results)
135
+ }
136
+
137
+ /**
138
+ * Get the current status of an operation (point-in-time check)
139
+ */
140
+ async getStatus(operationId) {
141
+ const response = await getOperationStatus({
142
+ path,
143
+ })
144
+ return response.data
145
+ }
146
+
147
+ /**
148
+ * Cancel a pending or running operation
149
+ */
150
+ async cancelOperation(operationId) {
151
+ // First close any active SSE connection
152
+ this.cleanupClient(operationId)
153
+
154
+ // Then cancel the operation
155
+ await cancelOperationSDK({
156
+ path,
157
+ })
158
+ }
159
+
160
+ /**
161
+ * Wait for an operation with a simple promise interface
162
+ */
163
+ async waitForOperation(operationId, timeoutMs?) {
164
+ const result = await this.monitorOperation(operationId, {
165
+ timeout,
166
+ })
167
+
168
+ if (!result.success) {
169
+ throw new Error(result.error || 'Operation failed')
170
+ }
171
+
172
+ return result.result
173
+ }
174
+
175
+ /**
176
+ * Monitor operation with async iterator for progress updates
177
+ */
178
+ async *monitorWithProgress(
179
+ operationId
180
+ )> {
181
+ const progressQueue: (OperationProgress | OperationResult)[] = []
182
+ let completed = false
183
+ let finalResult | null = null
184
+
185
+ // Start monitoring in background
186
+ const monitorPromise = this.monitorOperation(operationId, {
187
+ onProgress: (progress) => {
188
+ progressQueue.push(progress)
189
+ },
190
+ onQueueUpdate: (position, estimatedWait) => {
191
+ progressQueue.push({
192
+ message: `Queue position: ${position}`,
193
+ progressPercent,
194
+ details,
195
+ })
196
+ },
197
+ })
198
+
199
+ // Handle completion
200
+ monitorPromise
201
+ .then((result) => {
202
+ finalResult = result
203
+ completed = true
204
+ })
205
+ .catch((error) => {
206
+ finalResult = {
207
+ success,
208
+ error.message,
209
+ }
210
+ completed = true
211
+ })
212
+
213
+ // Yield progress updates come
214
+ while (!completed || progressQueue.length > 0) {
215
+ if (progressQueue.length > 0) {
216
+ yield progressQueue.shift()!
217
+ } else if (!completed) {
218
+ // Wait for more progress
219
+ await new Promise((resolve) => setTimeout(resolve, 100))
220
+ }
221
+ }
222
+
223
+ // Yield final result
224
+ if (finalResult) {
225
+ yield finalResult
226
+ }
227
+ }
228
+
229
+ private cleanupClient(operationId) {
230
+ const client = this.sseClients.get(operationId)
231
+ if (client) {
232
+ client.close()
233
+ this.sseClients.delete(operationId)
234
+ }
235
+
236
+ // Clear any cleanup timeout for this operation
237
+ const timeout = this.cleanupTimeouts.get(operationId)
238
+ if (timeout) {
239
+ clearTimeout(timeout)
240
+ this.cleanupTimeouts.delete(operationId)
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Schedule automatic cleanup of SSE client after a delay
246
+ */
247
+ private scheduleCleanup(operationId, delayMs = 60000) {
248
+ // Clear any existing timeout
249
+ const existingTimeout = this.cleanupTimeouts.get(operationId)
250
+ if (existingTimeout) {
251
+ clearTimeout(existingTimeout)
252
+ }
253
+
254
+ // Schedule new cleanup
255
+ const timeout = setTimeout(() => {
256
+ this.cleanupClient(operationId)
257
+ this.cleanupTimeouts.delete(operationId)
258
+ }, delayMs)
259
+
260
+ this.cleanupTimeouts.set(operationId, timeout)
261
+ }
262
+
263
+ /**
264
+ * Perform periodic cleanup of stale SSE connections
265
+ */
266
+ private performPeriodicCleanup() {
267
+ // Check each SSE client for staleness
268
+ this.sseClients.forEach((client, operationId) => {
269
+ if (!client.isConnected()) {
270
+ // Clean up disconnected clients immediately
271
+ this.cleanupClient(operationId)
272
+ }
273
+ })
274
+ }
275
+
276
+ /**
277
+ * Close all active SSE connections and clean up resources
278
+ */
279
+ closeAll() {
280
+ // Clear periodic cleanup interval
281
+ if (this.cleanupInterval) {
282
+ clearInterval(this.cleanupInterval)
283
+ this.cleanupInterval = undefined
284
+ }
285
+
286
+ // Clear all cleanup timeouts
287
+ this.cleanupTimeouts.forEach((timeout) => {
288
+ clearTimeout(timeout)
289
+ })
290
+ this.cleanupTimeouts.clear()
291
+
292
+ // Close all SSE clients
293
+ this.sseClients.forEach((client, operationId) => {
294
+ this.cleanupClient(operationId)
295
+ })
296
+ }
297
+ }
@@ -0,0 +1,322 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * General Operations Client for monitoring async operations
5
+ * Handles graph creation, backups, imports, and other long-running tasks
6
+ */
7
+
8
+ import { cancelOperation as cancelOperationSDK, getOperationStatus } from '../sdk.gen'
9
+ import { EventType, SSEClient } from './SSEClient'
10
+
11
+ export interface OperationProgress {
12
+ message: string
13
+ progressPercent?: number
14
+ details?: Record<string, any>
15
+ }
16
+
17
+ export interface OperationResult<T = any> {
18
+ success: boolean
19
+ result?: T
20
+ error?: string
21
+ metadata?: Record<string, any>
22
+ }
23
+
24
+ export interface OperationMonitorOptions {
25
+ onProgress?: (progress: OperationProgress) => void
26
+ onQueueUpdate?: (position: number, estimatedWait: number) => void
27
+ timeout?: number
28
+ }
29
+
30
+ export class OperationClient {
31
+ private sseClients: Map<string, SSEClient> = new Map()
32
+ private cleanupTimeouts: Map<string, ReturnType<typeof setTimeout>> = new Map()
33
+ private config: {
34
+ baseUrl: string
35
+ credentials?: 'include' | 'same-origin' | 'omit'
36
+ headers?: Record<string, string>
37
+ maxRetries?: number
38
+ retryDelay?: number
39
+ }
40
+ private cleanupIntervalMs = 300000 // 5 minutes
41
+ private cleanupInterval?: ReturnType<typeof setInterval>
42
+
43
+ constructor(config: {
44
+ baseUrl: string
45
+ credentials?: 'include' | 'same-origin' | 'omit'
46
+ headers?: Record<string, string>
47
+ maxRetries?: number
48
+ retryDelay?: number
49
+ }) {
50
+ this.config = config
51
+
52
+ // Start periodic cleanup check every 5 minutes
53
+ this.cleanupInterval = setInterval(() => {
54
+ this.performPeriodicCleanup()
55
+ }, this.cleanupIntervalMs)
56
+ }
57
+
58
+ async monitorOperation<T = any>(
59
+ operationId: string,
60
+ options: OperationMonitorOptions = {}
61
+ ): Promise<OperationResult<T>> {
62
+ return new Promise((resolve, reject) => {
63
+ const sseClient = new SSEClient(this.config)
64
+ this.sseClients.set(operationId, sseClient)
65
+
66
+ const timeoutHandle = options.timeout
67
+ ? setTimeout(() => {
68
+ this.cleanupClient(operationId)
69
+ reject(new Error(`Operation timeout after ${options.timeout}ms`))
70
+ }, options.timeout)
71
+ : undefined
72
+
73
+ sseClient
74
+ .connect(operationId)
75
+ .then(() => {
76
+ let result: OperationResult<T> = { success: false }
77
+
78
+ // Track queue updates
79
+ if (options.onQueueUpdate) {
80
+ sseClient.on(EventType.QUEUE_UPDATE, (data) => {
81
+ options.onQueueUpdate!(
82
+ data.position || data.queue_position,
83
+ data.estimated_wait_seconds || 0
84
+ )
85
+ })
86
+ }
87
+
88
+ // Track progress
89
+ if (options.onProgress) {
90
+ sseClient.on(EventType.OPERATION_PROGRESS, (data) => {
91
+ options.onProgress!({
92
+ message: data.message || data.status || 'Processing...',
93
+ progressPercent: data.progress_percent || data.progress,
94
+ details: data,
95
+ })
96
+ })
97
+ }
98
+
99
+ // Handle completion
100
+ sseClient.on(EventType.OPERATION_COMPLETED, (data) => {
101
+ if (timeoutHandle) clearTimeout(timeoutHandle)
102
+ result = {
103
+ success: true,
104
+ result: data.result || data,
105
+ metadata: data.metadata,
106
+ }
107
+ // Schedule cleanup after a short delay to ensure all data is received
108
+ this.scheduleCleanup(operationId, 5000)
109
+ resolve(result)
110
+ })
111
+
112
+ // Handle errors
113
+ sseClient.on(EventType.OPERATION_ERROR, (error) => {
114
+ if (timeoutHandle) clearTimeout(timeoutHandle)
115
+ result = {
116
+ success: false,
117
+ error: error.message || error.error || 'Operation failed',
118
+ metadata: error.metadata,
119
+ }
120
+ // Schedule cleanup after a short delay
121
+ this.scheduleCleanup(operationId, 5000)
122
+ resolve(result) // Resolve with error result, not reject
123
+ })
124
+
125
+ // Handle cancellation
126
+ sseClient.on(EventType.OPERATION_CANCELLED, (data) => {
127
+ if (timeoutHandle) clearTimeout(timeoutHandle)
128
+ result = {
129
+ success: false,
130
+ error: 'Operation cancelled',
131
+ metadata: data,
132
+ }
133
+ // Schedule cleanup after a short delay
134
+ this.scheduleCleanup(operationId, 5000)
135
+ resolve(result)
136
+ })
137
+ })
138
+ .catch((error) => {
139
+ if (timeoutHandle) clearTimeout(timeoutHandle)
140
+ this.cleanupClient(operationId)
141
+ reject(error)
142
+ })
143
+ })
144
+ }
145
+
146
+ /**
147
+ * Monitor multiple operations concurrently
148
+ */
149
+ async monitorMultiple<T = any>(
150
+ operationIds: string[],
151
+ options: OperationMonitorOptions = {}
152
+ ): Promise<Map<string, OperationResult<T>>> {
153
+ const results = await Promise.all(
154
+ operationIds.map(async (id) => {
155
+ const result = await this.monitorOperation<T>(id, options)
156
+ return [id, result] as [string, OperationResult<T>]
157
+ })
158
+ )
159
+ return new Map(results)
160
+ }
161
+
162
+ /**
163
+ * Get the current status of an operation (point-in-time check)
164
+ */
165
+ async getStatus(operationId: string): Promise<any> {
166
+ const response = await getOperationStatus({
167
+ path: { operation_id: operationId },
168
+ })
169
+ return response.data
170
+ }
171
+
172
+ /**
173
+ * Cancel a pending or running operation
174
+ */
175
+ async cancelOperation(operationId: string): Promise<void> {
176
+ // First close any active SSE connection
177
+ this.cleanupClient(operationId)
178
+
179
+ // Then cancel the operation
180
+ await cancelOperationSDK({
181
+ path: { operation_id: operationId },
182
+ })
183
+ }
184
+
185
+ /**
186
+ * Wait for an operation with a simple promise interface
187
+ */
188
+ async waitForOperation<T = any>(operationId: string, timeoutMs?: number): Promise<T> {
189
+ const result = await this.monitorOperation<T>(operationId, {
190
+ timeout: timeoutMs,
191
+ })
192
+
193
+ if (!result.success) {
194
+ throw new Error(result.error || 'Operation failed')
195
+ }
196
+
197
+ return result.result as T
198
+ }
199
+
200
+ /**
201
+ * Monitor operation with async iterator for progress updates
202
+ */
203
+ async *monitorWithProgress<T = any>(
204
+ operationId: string
205
+ ): AsyncIterableIterator<OperationProgress | OperationResult<T>> {
206
+ const progressQueue: (OperationProgress | OperationResult<T>)[] = []
207
+ let completed = false
208
+ let finalResult: OperationResult<T> | null = null
209
+
210
+ // Start monitoring in background
211
+ const monitorPromise = this.monitorOperation<T>(operationId, {
212
+ onProgress: (progress) => {
213
+ progressQueue.push(progress)
214
+ },
215
+ onQueueUpdate: (position, estimatedWait) => {
216
+ progressQueue.push({
217
+ message: `Queue position: ${position}`,
218
+ progressPercent: 0,
219
+ details: { position, estimatedWait },
220
+ })
221
+ },
222
+ })
223
+
224
+ // Handle completion
225
+ monitorPromise
226
+ .then((result) => {
227
+ finalResult = result
228
+ completed = true
229
+ })
230
+ .catch((error) => {
231
+ finalResult = {
232
+ success: false,
233
+ error: error.message,
234
+ }
235
+ completed = true
236
+ })
237
+
238
+ // Yield progress updates as they come
239
+ while (!completed || progressQueue.length > 0) {
240
+ if (progressQueue.length > 0) {
241
+ yield progressQueue.shift()!
242
+ } else if (!completed) {
243
+ // Wait for more progress
244
+ await new Promise((resolve) => setTimeout(resolve, 100))
245
+ }
246
+ }
247
+
248
+ // Yield final result
249
+ if (finalResult) {
250
+ yield finalResult
251
+ }
252
+ }
253
+
254
+ private cleanupClient(operationId: string): void {
255
+ const client = this.sseClients.get(operationId)
256
+ if (client) {
257
+ client.close()
258
+ this.sseClients.delete(operationId)
259
+ }
260
+
261
+ // Clear any cleanup timeout for this operation
262
+ const timeout = this.cleanupTimeouts.get(operationId)
263
+ if (timeout) {
264
+ clearTimeout(timeout)
265
+ this.cleanupTimeouts.delete(operationId)
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Schedule automatic cleanup of SSE client after a delay
271
+ */
272
+ private scheduleCleanup(operationId: string, delayMs: number = 60000): void {
273
+ // Clear any existing timeout
274
+ const existingTimeout = this.cleanupTimeouts.get(operationId)
275
+ if (existingTimeout) {
276
+ clearTimeout(existingTimeout)
277
+ }
278
+
279
+ // Schedule new cleanup
280
+ const timeout = setTimeout(() => {
281
+ this.cleanupClient(operationId)
282
+ this.cleanupTimeouts.delete(operationId)
283
+ }, delayMs)
284
+
285
+ this.cleanupTimeouts.set(operationId, timeout)
286
+ }
287
+
288
+ /**
289
+ * Perform periodic cleanup of stale SSE connections
290
+ */
291
+ private performPeriodicCleanup(): void {
292
+ // Check each SSE client for staleness
293
+ this.sseClients.forEach((client, operationId) => {
294
+ if (!client.isConnected()) {
295
+ // Clean up disconnected clients immediately
296
+ this.cleanupClient(operationId)
297
+ }
298
+ })
299
+ }
300
+
301
+ /**
302
+ * Close all active SSE connections and clean up resources
303
+ */
304
+ closeAll(): void {
305
+ // Clear periodic cleanup interval
306
+ if (this.cleanupInterval) {
307
+ clearInterval(this.cleanupInterval)
308
+ this.cleanupInterval = undefined
309
+ }
310
+
311
+ // Clear all cleanup timeouts
312
+ this.cleanupTimeouts.forEach((timeout) => {
313
+ clearTimeout(timeout)
314
+ })
315
+ this.cleanupTimeouts.clear()
316
+
317
+ // Close all SSE clients
318
+ this.sseClients.forEach((client, operationId) => {
319
+ this.cleanupClient(operationId)
320
+ })
321
+ }
322
+ }