@robosystems/core 0.11.2 → 0.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,30 @@
1
1
  'use client';
2
2
  import { changeSubscriptionPlan, client, createCheckoutSession, createGraph as createGraphAPI, createRepositorySubscription, getOrgBillingCustomer, } from '@robosystems/client';
3
3
  import { useCallback, useEffect, useRef, useState } from 'react';
4
- import { operationMonitor } from './operationMonitor';
4
+ import { toApiError } from '../lib/sdk-errors';
5
+ import { MONITORING_STOPPED, operationMonitor } from './operationMonitor';
6
+ /**
7
+ * Thrown by `startMonitoring` (and so by `createGraph`) when an operation
8
+ * ends in any state but `completed`. `status` is the terminal state;
9
+ * `'cancelled'` with `stopped: true` means monitoring was stopped locally
10
+ * and the operation may still be running.
11
+ */
12
+ export class OperationOutcomeError extends Error {
13
+ constructor(result) {
14
+ const stopped = result.error === MONITORING_STOPPED;
15
+ super(stopped
16
+ ? STOPPED_MESSAGE
17
+ : result.error ||
18
+ (result.status === 'cancelled'
19
+ ? 'Operation was cancelled'
20
+ : 'Operation failed'));
21
+ this.name = 'OperationOutcomeError';
22
+ this.status = result.status;
23
+ this.operationId = result.operation_id;
24
+ this.stopped = stopped;
25
+ }
26
+ }
27
+ const STOPPED_MESSAGE = 'Stopped monitoring. The operation continues on the server.';
5
28
  /**
6
29
  * Hook for monitoring operations using SSE
7
30
  */
@@ -30,11 +53,13 @@ export function useOperationMonitoring() {
30
53
  });
31
54
  currentOperationId.current = null;
32
55
  }, []);
56
+ // Stops watching; the server-side operation is not cancelled (graph
57
+ // creation, for one, cannot be cancelled mid-provision).
33
58
  const cancelOperation = useCallback(async () => {
34
59
  if (currentOperationId.current) {
35
60
  try {
36
- await operationMonitor.cancelOperation(currentOperationId.current);
37
- setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: false, isMonitoring: false, status: 'cancelled', error: 'Operation was cancelled', result: null })));
61
+ operationMonitor.cancelOperation(currentOperationId.current);
62
+ setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: false, isMonitoring: false, status: 'cancelled', error: STOPPED_MESSAGE, result: null })));
38
63
  }
39
64
  catch (error) {
40
65
  console.error('Failed to cancel operation:', error);
@@ -43,6 +68,10 @@ export function useOperationMonitoring() {
43
68
  }
44
69
  }, []);
45
70
  const startMonitoring = useCallback(async (operationId, options) => {
71
+ if (currentOperationId.current &&
72
+ currentOperationId.current !== operationId) {
73
+ operationMonitor.cancelOperation(currentOperationId.current);
74
+ }
46
75
  currentOperationId.current = operationId;
47
76
  setState({
48
77
  isLoading: true,
@@ -68,15 +97,20 @@ export function useOperationMonitoring() {
68
97
  setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: false, isMonitoring: false, error: errorMsg, result: null, status: 'failed' })));
69
98
  },
70
99
  });
100
+ if (result.status !== 'completed') {
101
+ throw new OperationOutcomeError(result);
102
+ }
71
103
  return result.result || result;
72
104
  }
73
105
  catch (error) {
74
106
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
75
- setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: false, isMonitoring: false, error: errorMessage, result: null, status: 'failed' })));
107
+ setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: false, isMonitoring: false, error: errorMessage, result: null, status: error instanceof OperationOutcomeError ? error.status : 'failed' })));
76
108
  throw error;
77
109
  }
78
110
  finally {
79
- currentOperationId.current = null;
111
+ // A newer run may have started meanwhile; leave its id in place.
112
+ if (currentOperationId.current === operationId)
113
+ currentOperationId.current = null;
80
114
  }
81
115
  }, []);
82
116
  // Cleanup on unmount
@@ -91,6 +125,19 @@ export function useOperationMonitoring() {
91
125
  cancelOperation, cancelMonitoring: cancelOperation, // Alias for consistency
92
126
  reset });
93
127
  }
128
+ /**
129
+ * The schema sent for a generic graph: the caller's, or an empty one named
130
+ * after the graph (a bare database the user fills in later).
131
+ */
132
+ function genericSchema(graphData) {
133
+ if (graphData.custom_schema)
134
+ return graphData.custom_schema;
135
+ const name = graphData.graph_name
136
+ .toLowerCase()
137
+ .replace(/[^a-z0-9]+/g, '_')
138
+ .replace(/^_+|_+$/g, '') || 'custom_graph';
139
+ return { name, nodes: [], relationships: [] };
140
+ }
94
141
  /**
95
142
  * Hook for graph creation using the new unified endpoint
96
143
  */
@@ -98,7 +145,7 @@ export function useGraphCreation() {
98
145
  const operationMonitoring = useOperationMonitoring();
99
146
  const [isCreating, setIsCreating] = useState(false);
100
147
  const createGraph = useCallback(async (graphData) => {
101
- var _a, _b, _c;
148
+ var _a, _b, _c, _d;
102
149
  setIsCreating(true);
103
150
  try {
104
151
  // Check if billing is enabled and user needs to complete checkout
@@ -129,6 +176,9 @@ export function useGraphCreation() {
129
176
  description: graphData.description,
130
177
  tags: graphData.tags,
131
178
  schema_extensions: graphData.schema_extensions,
179
+ custom_schema: graphData.graph_type === 'generic'
180
+ ? genericSchema(graphData)
181
+ : undefined,
132
182
  create_entity: graphData.create_entity,
133
183
  entity_name: graphData.entity_name,
134
184
  entity_identifier: graphData.entity_identifier,
@@ -140,11 +190,7 @@ export function useGraphCreation() {
140
190
  },
141
191
  });
142
192
  if (checkoutResponse.error) {
143
- const errorMsg = typeof checkoutResponse.error === 'object' &&
144
- 'detail' in checkoutResponse.error
145
- ? String(checkoutResponse.error.detail)
146
- : 'Failed to create checkout session';
147
- throw new Error(errorMsg);
193
+ throw toApiError(checkoutResponse.error, checkoutResponse.response);
148
194
  }
149
195
  // Check if billing is disabled on the backend
150
196
  // @ts-ignore - billing_disabled field will be in next SDK release
@@ -173,11 +219,19 @@ export function useGraphCreation() {
173
219
  tags: graphData.tags,
174
220
  schema_extensions: graphData.schema_extensions || [],
175
221
  },
222
+ // The API files tags from the top-level field; `metadata.tags` is
223
+ // kept for older servers.
224
+ tags: (_c = graphData.tags) !== null && _c !== void 0 ? _c : [],
176
225
  instance_tier: graphData.instance_tier || 'ladybug-standard',
177
226
  create_entity: graphData.graph_type === 'generic'
178
227
  ? false
179
- : ((_c = graphData.create_entity) !== null && _c !== void 0 ? _c : true),
228
+ : ((_d = graphData.create_entity) !== null && _d !== void 0 ? _d : true),
180
229
  };
230
+ // A generic graph is defined by its schema; the API requires either
231
+ // a custom_schema or an initial_entity.
232
+ if (graphData.graph_type === 'generic') {
233
+ requestBody.custom_schema = genericSchema(graphData);
234
+ }
181
235
  // Add initial_entity for entity or company graphs
182
236
  if (graphData.graph_type === 'entity' && graphData.entity_name) {
183
237
  requestBody.initial_entity = {
@@ -310,11 +364,7 @@ export function useRepositorySubscription() {
310
364
  },
311
365
  });
312
366
  if (checkoutResponse.error) {
313
- const errorMsg = typeof checkoutResponse.error === 'object' &&
314
- 'detail' in checkoutResponse.error
315
- ? String(checkoutResponse.error.detail)
316
- : 'Failed to create checkout session';
317
- throw new Error(errorMsg);
367
+ throw toApiError(checkoutResponse.error, checkoutResponse.response);
318
368
  }
319
369
  // Check if billing is disabled on the backend
320
370
  // @ts-ignore - billing_disabled field will be in next SDK release
@@ -31,8 +31,15 @@ export interface OperationMonitorOptions {
31
31
  onComplete?: (result: any) => void;
32
32
  onError?: (error: string) => void;
33
33
  onEvent?: (event: OperationEvent) => void;
34
+ /**
35
+ * Give up watching after this many milliseconds. No default: an operation
36
+ * runs as long as it runs (backups, large graph creations), and a monitor
37
+ * that times out reports a failure for work that is still succeeding.
38
+ */
34
39
  timeout?: number;
35
40
  }
41
+ /** Error message on a result whose monitoring was stopped locally. */
42
+ export declare const MONITORING_STOPPED = "Monitoring stopped";
36
43
  export declare class OperationMonitor {
37
44
  private static instance;
38
45
  private activeOperations;
@@ -43,7 +50,9 @@ export declare class OperationMonitor {
43
50
  */
44
51
  monitorOperation({ operationId, onProgress, onComplete, onError, onEvent, timeout, }: OperationMonitorOptions): Promise<OperationResult>;
45
52
  /**
46
- * Cancel operation monitoring (does not cancel the operation itself)
53
+ * Stop monitoring an operation: closes its stream and settles its
54
+ * `monitorOperation` promise with `status: 'cancelled'` and
55
+ * `error: MONITORING_STOPPED`. Does not cancel the operation itself.
47
56
  */
48
57
  cancelOperation(operationId: string): boolean;
49
58
  /**
@@ -16,6 +16,30 @@ const getSDKExtensions = async () => {
16
16
  return null;
17
17
  }
18
18
  };
19
+ /** Error message on a result whose monitoring was stopped locally. */
20
+ export const MONITORING_STOPPED = 'Monitoring stopped';
21
+ function createActiveOperation() {
22
+ let resolveStopped = () => undefined;
23
+ const entry = {
24
+ client: null,
25
+ stopped: false,
26
+ stoppedPromise: new Promise((resolve) => {
27
+ resolveStopped = resolve;
28
+ }),
29
+ stop: () => {
30
+ var _a;
31
+ entry.stopped = true;
32
+ resolveStopped(null);
33
+ (_a = entry.client) === null || _a === void 0 ? void 0 : _a.closeAll();
34
+ },
35
+ };
36
+ return entry;
37
+ }
38
+ const stoppedResult = (operationId) => ({
39
+ operation_id: operationId,
40
+ status: 'cancelled',
41
+ error: MONITORING_STOPPED,
42
+ });
19
43
  export class OperationMonitor {
20
44
  constructor() {
21
45
  this.activeOperations = new Map();
@@ -30,40 +54,55 @@ export class OperationMonitor {
30
54
  /**
31
55
  * Monitor an operation using SSE streaming
32
56
  */
33
- async monitorOperation({ operationId, onProgress, onComplete, onError, onEvent, timeout = 300000, // 5 minutes default
34
- }) {
35
- // Get SDK extensions at runtime
36
- const sdkExtensions = await getSDKExtensions();
37
- // Require SDK extensions - fail fast if not available
38
- if (!(sdkExtensions === null || sdkExtensions === void 0 ? void 0 : sdkExtensions.OperationClient)) {
39
- throw new Error('SDK extensions not available. Please install @robosystems/client with extensions support.');
57
+ async monitorOperation({ operationId, onProgress, onComplete, onError, onEvent, timeout, }) {
58
+ // Registered before any await, so a stop issued while the SDK loads
59
+ // (an immediate cancel, an unmount, a StrictMode remount) is honoured.
60
+ if (this.activeOperations.has(operationId)) {
61
+ this.cancelOperation(operationId);
62
+ }
63
+ const entry = createActiveOperation();
64
+ this.activeOperations.set(operationId, entry);
65
+ try {
66
+ // Get SDK extensions at runtime
67
+ const sdkExtensions = await getSDKExtensions();
68
+ if (entry.stopped)
69
+ return stoppedResult(operationId);
70
+ // Require SDK extensions - fail fast if not available
71
+ if (!(sdkExtensions === null || sdkExtensions === void 0 ? void 0 : sdkExtensions.OperationClient)) {
72
+ throw new Error('SDK extensions not available. Please install @robosystems/client with extensions support.');
73
+ }
74
+ return await this.monitorWithSDKExtensions({
75
+ operationId,
76
+ onProgress,
77
+ onComplete,
78
+ onError,
79
+ onEvent,
80
+ timeout,
81
+ }, sdkExtensions, entry);
82
+ }
83
+ finally {
84
+ if (this.activeOperations.get(operationId) === entry) {
85
+ this.activeOperations.delete(operationId);
86
+ }
40
87
  }
41
- return this.monitorWithSDKExtensions({
42
- operationId,
43
- onProgress,
44
- onComplete,
45
- onError,
46
- onEvent,
47
- timeout,
48
- }, sdkExtensions);
49
88
  }
50
89
  /**
51
- * Cancel operation monitoring (does not cancel the operation itself)
90
+ * Stop monitoring an operation: closes its stream and settles its
91
+ * `monitorOperation` promise with `status: 'cancelled'` and
92
+ * `error: MONITORING_STOPPED`. Does not cancel the operation itself.
52
93
  */
53
94
  cancelOperation(operationId) {
54
- try {
55
- const eventSource = this.activeOperations.get(operationId);
56
- if (eventSource) {
57
- eventSource.close();
58
- this.activeOperations.delete(operationId);
59
- return true;
60
- }
95
+ const active = this.activeOperations.get(operationId);
96
+ if (!active)
61
97
  return false;
98
+ this.activeOperations.delete(operationId);
99
+ try {
100
+ active.stop();
62
101
  }
63
102
  catch (error) {
64
- console.error('Failed to cancel operation:', error);
65
- return false;
103
+ console.error('Failed to stop operation monitoring:', error);
66
104
  }
105
+ return true;
67
106
  }
68
107
  /**
69
108
  * Get cached operation status/result
@@ -80,49 +119,63 @@ export class OperationMonitor {
80
119
  * Clean up operation monitoring resources
81
120
  */
82
121
  cleanupOperation(operationId) {
83
- const eventSource = this.activeOperations.get(operationId);
84
- if (eventSource) {
85
- eventSource.close();
86
- this.activeOperations.delete(operationId);
87
- }
122
+ this.cancelOperation(operationId);
88
123
  }
89
124
  /**
90
125
  * Monitor operation using SDK extensions
91
126
  */
92
- async monitorWithSDKExtensions(options, sdkExtensions) {
127
+ async monitorWithSDKExtensions(options, sdkExtensions, entry) {
93
128
  var _a, _b, _c;
94
129
  const { OperationClient, extractTokenFromSDKClient } = sdkExtensions;
95
- const { getToken } = await import('../auth-core/token-storage');
130
+ const { getToken, getValidToken } = await import('../auth-core/token-storage');
131
+ if (entry.stopped)
132
+ return stoppedResult(options.operationId);
96
133
  const config = client.getConfig();
97
- const jwtToken = getToken() || extractTokenFromSDKClient();
98
- // Create operation client with proper configuration
134
+ // One client per operation, tracked so `cancelOperation` can close it.
135
+ // The credential is resolved per connect (`tokenProvider`), so a stream
136
+ // that reconnects after the JWT rotates presents the current token; the
137
+ // static `token` only serves SDK versions that predate the provider.
99
138
  const operationClient = new OperationClient({
100
139
  baseUrl: config.baseUrl || 'http://localhost:8000',
101
140
  credentials: 'include',
102
- token: jwtToken,
141
+ token: getToken() || (extractTokenFromSDKClient === null || extractTokenFromSDKClient === void 0 ? void 0 : extractTokenFromSDKClient()) || undefined,
142
+ tokenProvider: async () => { var _a; return (_a = (await getValidToken())) !== null && _a !== void 0 ? _a : null; },
103
143
  maxRetries: 3,
104
144
  retryDelay: 1000,
105
145
  });
146
+ entry.client = operationClient;
147
+ const stopped = () => entry.stopped;
106
148
  try {
107
- const result = await operationClient.monitorOperation(options.operationId, {
108
- onProgress: (progress) => {
109
- var _a, _b, _c, _d;
110
- (_a = options.onProgress) === null || _a === void 0 ? void 0 : _a.call(options, {
111
- percent: progress.progressPercent || 0,
112
- message: progress.message || '',
113
- current_step: (_c = (_b = progress.details) === null || _b === void 0 ? void 0 : _b.current_step) === null || _c === void 0 ? void 0 : _c.toString(),
114
- total_steps: (_d = progress.details) === null || _d === void 0 ? void 0 : _d.total_steps,
115
- });
116
- },
117
- onEvent: (event) => {
118
- var _a;
119
- (_a = options.onEvent) === null || _a === void 0 ? void 0 : _a.call(options, {
120
- event: event.type,
121
- data: event.data,
122
- });
123
- },
124
- timeout: options.timeout,
125
- });
149
+ const result = await Promise.race([
150
+ operationClient.monitorOperation(options.operationId, {
151
+ onProgress: (progress) => {
152
+ var _a, _b, _c, _d;
153
+ if (stopped())
154
+ return;
155
+ (_a = options.onProgress) === null || _a === void 0 ? void 0 : _a.call(options, {
156
+ percent: progress.progressPercent || 0,
157
+ message: progress.message || '',
158
+ current_step: (_c = (_b = progress.details) === null || _b === void 0 ? void 0 : _b.current_step) === null || _c === void 0 ? void 0 : _c.toString(),
159
+ total_steps: (_d = progress.details) === null || _d === void 0 ? void 0 : _d.total_steps,
160
+ });
161
+ },
162
+ onEvent: (event) => {
163
+ var _a;
164
+ if (stopped())
165
+ return;
166
+ (_a = options.onEvent) === null || _a === void 0 ? void 0 : _a.call(options, {
167
+ event: event.type,
168
+ data: event.data,
169
+ });
170
+ },
171
+ timeout: options.timeout,
172
+ }),
173
+ entry.stoppedPromise,
174
+ ]);
175
+ if (result === null) {
176
+ // Stopped locally: report it without firing the outcome callbacks.
177
+ return stoppedResult(options.operationId);
178
+ }
126
179
  // Map OperationResult (success: boolean) to status string
127
180
  const status = result.success
128
181
  ? 'completed'
@@ -158,10 +211,9 @@ export class OperationMonitor {
158
211
  * Clean up all active operations
159
212
  */
160
213
  cleanup() {
161
- for (const [, eventSource] of this.activeOperations.entries()) {
162
- eventSource.close();
214
+ for (const operationId of Array.from(this.activeOperations.keys())) {
215
+ this.cancelOperation(operationId);
163
216
  }
164
- this.activeOperations.clear();
165
217
  }
166
218
  /**
167
219
  * Get list of active operation IDs
@@ -1,9 +1,22 @@
1
1
  import type { TaskPollingOptions, TaskStatusResponse } from './types';
2
+ /** Rejection message when watching stopped locally (`stopPolling`). */
3
+ export declare const POLLING_CANCELLED = "Task polling was cancelled";
2
4
  export declare class TaskMonitor {
3
5
  private static instance;
4
6
  private activeTasks;
5
7
  static getInstance(): TaskMonitor;
6
- pollTask({ taskId, onProgress, onComplete, onError, pollInterval, maxAttempts, }: TaskPollingOptions): Promise<TaskStatusResponse>;
8
+ pollTask({ taskId, onProgress, onComplete, onError, pollInterval, maxAttempts, // 5 minutes at 2s intervals
9
+ maxConsecutiveErrors, }: TaskPollingOptions): Promise<TaskStatusResponse>;
10
+ /**
11
+ * Stop watching a task locally. The operation itself keeps running on the
12
+ * server — this is what leaving a page should do.
13
+ */
14
+ stopPolling(taskId: string): boolean;
15
+ /**
16
+ * Cancel the operation on the server, then stop watching it. Only for an
17
+ * explicit user action. Returns false when the server refused — the task
18
+ * is then still being watched, and its real outcome still arrives.
19
+ */
7
20
  cancelTask(taskId: string): Promise<boolean>;
8
21
  cancelAllTasks(): void;
9
22
  getActiveTasks(): string[];
@@ -11,4 +24,5 @@ export declare class TaskMonitor {
11
24
  export declare const taskMonitor: TaskMonitor;
12
25
  export declare const pollTaskStatus: (options: TaskPollingOptions) => Promise<TaskStatusResponse>;
13
26
  export declare const cancelTask: (taskId: string) => Promise<boolean>;
27
+ export declare const stopPollingTask: (taskId: string) => boolean;
14
28
  export declare const getActiveTasks: () => string[];