@robosystems/core 0.11.2 → 0.11.4

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.
@@ -17,6 +17,18 @@ const getSDKExtensions = async () => {
17
17
  return null;
18
18
  }
19
19
  };
20
+ /**
21
+ * Invalidate the current query run and end its stream. The generator (and
22
+ * its request) ends once its pending read settles; the read loop also checks
23
+ * the run number, so nothing from the old run is written.
24
+ */
25
+ function stopRun(runRef, iteratorRef) {
26
+ var _a, _b, _c;
27
+ runRef.current++;
28
+ const iterator = iteratorRef.current;
29
+ iteratorRef.current = null;
30
+ (_c = (_b = (_a = iterator === null || iterator === void 0 ? void 0 : iterator.return) === null || _a === void 0 ? void 0 : _a.call(iterator)) === null || _b === void 0 ? void 0 : _b.catch) === null || _c === void 0 ? void 0 : _c.call(_b, () => undefined);
31
+ }
20
32
  /**
21
33
  * Hook for streaming query results using SDK extensions when available
22
34
  * Falls back to manual SSE implementation for older SDK versions
@@ -34,19 +46,14 @@ export function useStreamingQuery() {
34
46
  cached: false,
35
47
  duration: null,
36
48
  });
37
- const eventSourceRef = useRef(null);
38
49
  const startTimeRef = useRef(null);
39
- const queryClientRef = useRef(null);
50
+ // Each query run gets a number; cancelling or resetting moves it on, and a
51
+ // run whose number is no longer current stops and writes nothing.
52
+ const runRef = useRef(0);
53
+ const iteratorRef = useRef(null);
54
+ const stopCurrentRun = useCallback(() => stopRun(runRef, iteratorRef), []);
40
55
  const reset = useCallback(() => {
41
- var _a;
42
- if (eventSourceRef.current) {
43
- eventSourceRef.current.close();
44
- eventSourceRef.current = null;
45
- }
46
- if ((_a = queryClientRef.current) === null || _a === void 0 ? void 0 : _a.close) {
47
- queryClientRef.current.close();
48
- queryClientRef.current = null;
49
- }
56
+ stopCurrentRun();
50
57
  setState({
51
58
  isStreaming: false,
52
59
  results: [],
@@ -59,31 +66,30 @@ export function useStreamingQuery() {
59
66
  cached: false,
60
67
  duration: null,
61
68
  });
62
- }, []);
69
+ }, [stopCurrentRun]);
63
70
  const cancelQuery = useCallback(() => {
64
- var _a;
65
- if (eventSourceRef.current) {
66
- eventSourceRef.current.close();
67
- eventSourceRef.current = null;
68
- }
69
- if ((_a = queryClientRef.current) === null || _a === void 0 ? void 0 : _a.close) {
70
- queryClientRef.current.close();
71
- queryClientRef.current = null;
72
- }
71
+ stopCurrentRun();
73
72
  setState((prev) => (Object.assign(Object.assign({}, prev), { isStreaming: false, status: 'cancelled', error: 'Query was cancelled' })));
74
- }, []);
73
+ }, [stopCurrentRun]);
75
74
  const executeQuery = useCallback(async (graphId, query, parameters) => {
75
+ // The run starts now, so a cancel while the SDK loads retires it.
76
+ stopRun(runRef, iteratorRef);
77
+ const runId = runRef.current;
76
78
  // Get SDK extensions at runtime
77
79
  const sdkExtensions = await getSDKExtensions();
80
+ if (runRef.current !== runId)
81
+ return;
78
82
  // Require SDK extensions - fail fast if not available
79
83
  if (!(sdkExtensions === null || sdkExtensions === void 0 ? void 0 : sdkExtensions.streamQuery)) {
80
84
  throw new Error('SDK extensions not available. Please install @robosystems/client with extensions support.');
81
85
  }
82
- return executeQueryWithExtensions(graphId, query, sdkExtensions, parameters);
86
+ return executeQueryWithExtensions(runId, graphId, query, sdkExtensions, parameters);
83
87
  }, []);
84
88
  // Helper function to execute query with SDK extensions
85
- const executeQueryWithExtensions = async (graphId, query, sdkExtensions, parameters) => {
89
+ const executeQueryWithExtensions = async (runId, graphId, query, sdkExtensions, parameters) => {
86
90
  var _a, e_1, _b, _c;
91
+ var _d;
92
+ const isCurrent = () => runRef.current === runId;
87
93
  try {
88
94
  // Reset state for new query
89
95
  setState({
@@ -101,14 +107,17 @@ export function useStreamingQuery() {
101
107
  startTimeRef.current = Date.now();
102
108
  // Use the centralized streamQuery function with proper authentication
103
109
  const iterator = sdkExtensions.streamQuery(graphId, query, parameters, 100);
110
+ iteratorRef.current = iterator;
104
111
  setState((prev) => (Object.assign(Object.assign({}, prev), { status: 'streaming' })));
105
112
  let rowCount = 0;
106
113
  const allResults = [];
107
114
  try {
108
- for (var _d = true, iterator_1 = __asyncValues(iterator), iterator_1_1; iterator_1_1 = await iterator_1.next(), _a = iterator_1_1.done, !_a; _d = true) {
115
+ for (var _e = true, iterator_1 = __asyncValues(iterator), iterator_1_1; iterator_1_1 = await iterator_1.next(), _a = iterator_1_1.done, !_a; _e = true) {
109
116
  _c = iterator_1_1.value;
110
- _d = false;
117
+ _e = false;
111
118
  const batch = _c;
119
+ if (!isCurrent())
120
+ return;
112
121
  // Handle both single rows and batches
113
122
  const rows = Array.isArray(batch) ? batch : [batch];
114
123
  for (const row of rows) {
@@ -124,14 +133,20 @@ export function useStreamingQuery() {
124
133
  catch (e_1_1) { e_1 = { error: e_1_1 }; }
125
134
  finally {
126
135
  try {
127
- if (!_d && !_a && (_b = iterator_1.return)) await _b.call(iterator_1);
136
+ if (!_e && !_a && (_b = iterator_1.return)) await _b.call(iterator_1);
128
137
  }
129
138
  finally { if (e_1) throw e_1.error; }
130
139
  }
131
- const duration = Date.now() - startTimeRef.current;
140
+ if (!isCurrent())
141
+ return;
142
+ if (iteratorRef.current === iterator)
143
+ iteratorRef.current = null;
144
+ const duration = Date.now() - ((_d = startTimeRef.current) !== null && _d !== void 0 ? _d : Date.now());
132
145
  setState((prev) => (Object.assign(Object.assign({}, prev), { isStreaming: false, status: 'completed', results: allResults, totalRows: rowCount, currentRow: rowCount, progress: 100, duration })));
133
146
  }
134
147
  catch (error) {
148
+ if (!isCurrent())
149
+ return;
135
150
  const errorMessage = error instanceof Error ? error.message : 'Query execution failed';
136
151
  setState((prev) => (Object.assign(Object.assign({}, prev), { isStreaming: false, status: 'error', error: errorMessage })));
137
152
  }
package/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  export * as AuthCore from './auth-core';
2
- export { clearToken, getAuthHeader, getToken, getValidToken, handleAuthResponse, hasValidToken, storeToken, } from './auth-core/token-storage';
2
+ export { clearToken, getAuthHeader, getRefreshToken, getToken, getValidToken, handleAuthResponse, hasValidToken, storeToken, } from './auth-core/token-storage';
3
3
  export { clearUserCookies, clearUserLocalStorage, clearUserSessionStorage, listUserData, performLogoutCleanup, resetUIState, } from './auth-core/cleanup';
4
4
  export { AppSwitcher, AuthGuard, AuthProvider, LoginRedirector, SignInForm, SignUpForm, useAuth, } from './auth-components';
5
5
  export { useApiError, useMediaQuery, useToast, useUser, useUserLimits, type ToastMessage, } from './hooks';
6
6
  export { createGraphContext, createGraphProvider, createUseGraphContext, EntityProvider, GraphContext, GraphProvider, OrgProvider, ServiceOfferingsProvider, SidebarProvider, useEntity, useGraphContext, useOrg, useServiceOfferings, useSidebarContext, type EntityContextValue, type EntityProviderProps, type GraphContextValue, type GraphProviderProps, type GraphState, type OrgContextState, type OrgContextValue, type OrgProviderProps, type ServiceOfferings, } from './contexts';
7
7
  export { clientGraphCookie, clientSidebarCookie, createMcpConnectorUrl, entityCookie, graphCookie, sidebarCookie, type EntityCookie, type GraphCookie, type McpConnectorUrl, type SidebarCookie, } from './lib';
8
+ export { ApiError, errorStatus, extractErrorDetail, isApiError, isSessionRejection, isTransientError, toApiError, unwrapSdk, type SdkResult, } from './lib/sdk-errors';
8
9
  export { generateEntityUri, isUUID, UUID_REGEX } from './utils';
9
10
  export { clearGraphSelection, getGraphSelection, persistGraphSelection, } from './actions/graph-actions';
10
11
  export { clearEntitySelection, getEntitySelection, persistEntitySelection, } from './actions/entity-actions';
@@ -24,7 +25,9 @@ export { clients, EventType, executeQuery, monitorOperation, OperationClient, Qu
24
25
  export { cancelTask, getActiveTasks, pollTaskStatus, taskMonitor, } from './task-monitoring/taskMonitor';
25
26
  export { getProgressFromStep, useEntityCreationTask, useTaskMonitoring, type UseTaskMonitoringResult, } from './task-monitoring/hooks';
26
27
  export type { TaskCreateResponse, TaskMonitorState, TaskPollingOptions, TaskStatus, TaskStatusResponse, } from './task-monitoring';
27
- export { useGraphCreation, useOperationMonitoring, useRepositorySubscription, type OperationMonitorState, type UseOperationMonitoringResult, } from './task-monitoring/operationHooks';
28
+ export { OperationOutcomeError, useGraphCreation, useOperationMonitoring, useRepositorySubscription, type OperationMonitorState, type UseOperationMonitoringResult, } from './task-monitoring/operationHooks';
29
+ export { MONITORING_STOPPED } from './task-monitoring/operationMonitor';
30
+ export { POLLING_CANCELLED } from './task-monitoring/taskMonitor';
28
31
  import { client } from '@robosystems/client';
29
32
  export { client };
30
33
  export type { APIKey, AppName, AuthContextType, AuthFlowStatus, AuthProviders, AuthUser, PasskeyEnrollmentResult, } from './auth-core';
package/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // Export auth-core with namespace to avoid conflicts
3
3
  export * as AuthCore from './auth-core';
4
4
  // Export token storage utilities for JWT authentication
5
- export { clearToken, getAuthHeader, getToken, getValidToken, handleAuthResponse, hasValidToken, storeToken, } from './auth-core/token-storage';
5
+ export { clearToken, getAuthHeader, getRefreshToken, getToken, getValidToken, handleAuthResponse, hasValidToken, storeToken, } from './auth-core/token-storage';
6
6
  // Export auth cleanup utilities
7
7
  export { clearUserCookies, clearUserLocalStorage, clearUserSessionStorage, listUserData, performLogoutCleanup, resetUIState, } from './auth-core/cleanup';
8
8
  // Export auth-components
@@ -21,6 +21,8 @@ OrgProvider,
21
21
  ServiceOfferingsProvider, SidebarProvider, useEntity, useGraphContext, useOrg, useServiceOfferings, useSidebarContext, } from './contexts';
22
22
  // Export lib utilities
23
23
  export { clientGraphCookie, clientSidebarCookie, createMcpConnectorUrl, entityCookie, graphCookie, sidebarCookie, } from './lib';
24
+ // Export the SDK error seam (unwrap an SDK result or throw ApiError)
25
+ export { ApiError, errorStatus, extractErrorDetail, isApiError, isSessionRejection, isTransientError, toApiError, unwrapSdk, } from './lib/sdk-errors';
24
26
  // Export utils
25
27
  export { generateEntityUri, isUUID, UUID_REGEX } from './utils';
26
28
  // Export server actions
@@ -49,7 +51,9 @@ export { clients, EventType, executeQuery, monitorOperation, OperationClient, Qu
49
51
  export { cancelTask, getActiveTasks, pollTaskStatus, taskMonitor, } from './task-monitoring/taskMonitor';
50
52
  export { getProgressFromStep, useEntityCreationTask, useTaskMonitoring, } from './task-monitoring/hooks';
51
53
  // Export operation monitoring hooks
52
- export { useGraphCreation, useOperationMonitoring, useRepositorySubscription, } from './task-monitoring/operationHooks';
54
+ export { OperationOutcomeError, useGraphCreation, useOperationMonitoring, useRepositorySubscription, } from './task-monitoring/operationHooks';
55
+ export { MONITORING_STOPPED } from './task-monitoring/operationMonitor';
56
+ export { POLLING_CANCELLED } from './task-monitoring/taskMonitor';
53
57
  // Configure and export client directly for convenience (since it's commonly used)
54
58
  import { client } from '@robosystems/client';
55
59
  import { getAuthHeader } from './auth-core/token-storage';
package/lib/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export { clientSidebarCookie, sidebarCookie, type SidebarCookie, } from './sideb
2
2
  export { clientGraphCookie, graphCookie, type GraphCookie, } from './graph-cookie';
3
3
  export { entityCookie, type EntityCookie } from './entity-cookie';
4
4
  export { createMcpConnectorUrl, type McpConnectorUrl } from './mcp-connector';
5
+ export { ApiError, errorStatus, extractErrorDetail, isApiError, isSessionRejection, isTransientError, toApiError, unwrapSdk, type SdkResult, } from './sdk-errors';
package/lib/index.js CHANGED
@@ -2,3 +2,4 @@ export { clientSidebarCookie, sidebarCookie, } from './sidebar-cookie';
2
2
  export { clientGraphCookie, graphCookie, } from './graph-cookie';
3
3
  export { entityCookie } from './entity-cookie';
4
4
  export { createMcpConnectorUrl } from './mcp-connector';
5
+ export { ApiError, errorStatus, extractErrorDetail, isApiError, isSessionRejection, isTransientError, toApiError, unwrapSdk, } from './sdk-errors';
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The error seam for `@robosystems/client` calls.
3
+ *
4
+ * The generated SDK is built with `throwOnError: false`: an HTTP error, or a
5
+ * request that never reached the server, *resolves* with
6
+ * `{ data: undefined, error, response }` instead of rejecting. Code written
7
+ * as if the SDK throws (a `try/catch` around the call, or `response.data`
8
+ * read without checking `response.error`) treats every failure as success.
9
+ *
10
+ * `unwrapSdk` restores the throwing contract for one call at a time:
11
+ *
12
+ * const graphs = unwrapSdk(await SDK.getGraphs())
13
+ *
14
+ * It returns `data`, or throws an `ApiError` carrying the HTTP status. The
15
+ * shared client's global `throwOnError` stays false on purpose — some call
16
+ * sites read `response.error` directly, and flipping the default would change
17
+ * all of them at once.
18
+ */
19
+ /** Minimal shape of an SDK result (`responseStyle: 'fields'`, the default). */
20
+ export interface SdkResult<T> {
21
+ data?: T;
22
+ error?: unknown;
23
+ response?: {
24
+ status?: number;
25
+ } | Response;
26
+ }
27
+ export declare class ApiError extends Error {
28
+ /**
29
+ * HTTP status of the refusal, or 0 when no response was received (network
30
+ * failure, CORS rejection, DNS). Use `isNetworkError` rather than testing 0.
31
+ */
32
+ readonly status: number;
33
+ /** Human-readable detail from the response body (FastAPI `detail`). */
34
+ readonly detail: string;
35
+ /** Machine-readable code when the body carries one. */
36
+ readonly code?: string;
37
+ /** The raw error value the SDK resolved with. */
38
+ readonly body: unknown;
39
+ constructor(init: {
40
+ status: number;
41
+ detail: string;
42
+ code?: string;
43
+ body?: unknown;
44
+ });
45
+ get isNetworkError(): boolean;
46
+ }
47
+ export declare function isApiError(error: unknown): error is ApiError;
48
+ /**
49
+ * Pull `{ detail, code }` out of whatever the SDK resolved as `error`: a
50
+ * FastAPI body (`{ detail }`, `{ detail: { detail, code } }`,
51
+ * `{ detail: [...] }`), a plain string, or a thrown `Error`.
52
+ */
53
+ export declare function extractErrorDetail(error: unknown): {
54
+ detail?: string;
55
+ code?: string;
56
+ };
57
+ /** Build an `ApiError` from an SDK result's `error` and `response`. */
58
+ export declare function toApiError(error: unknown, response?: {
59
+ status?: number;
60
+ } | Response | null): ApiError;
61
+ /**
62
+ * Return the SDK result's `data`, or throw an `ApiError` when the call
63
+ * failed. A result whose `error` is set is a failure regardless of `data`.
64
+ */
65
+ export declare function unwrapSdk<T>(result: SdkResult<T>): T;
66
+ /** Status of an unknown error, or undefined when it carries none. */
67
+ export declare function errorStatus(error: unknown): number | undefined;
68
+ /**
69
+ * Whether a failed call is worth retrying: no response (network), rate
70
+ * limited, or a server error. A 4xx refusal is an answer, not a blip. An
71
+ * error that is not an `ApiError` (a malformed body, a thrown TypeError) is
72
+ * treated as transient.
73
+ */
74
+ export declare function isTransientError(error: unknown): boolean;
75
+ /**
76
+ * Whether an error is the server refusing the session itself (401, or 403
77
+ * for a deactivated account) rather than an outage or a network drop.
78
+ */
79
+ export declare function isSessionRejection(error: unknown): boolean;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * The error seam for `@robosystems/client` calls.
3
+ *
4
+ * The generated SDK is built with `throwOnError: false`: an HTTP error, or a
5
+ * request that never reached the server, *resolves* with
6
+ * `{ data: undefined, error, response }` instead of rejecting. Code written
7
+ * as if the SDK throws (a `try/catch` around the call, or `response.data`
8
+ * read without checking `response.error`) treats every failure as success.
9
+ *
10
+ * `unwrapSdk` restores the throwing contract for one call at a time:
11
+ *
12
+ * const graphs = unwrapSdk(await SDK.getGraphs())
13
+ *
14
+ * It returns `data`, or throws an `ApiError` carrying the HTTP status. The
15
+ * shared client's global `throwOnError` stays false on purpose — some call
16
+ * sites read `response.error` directly, and flipping the default would change
17
+ * all of them at once.
18
+ */
19
+ export class ApiError extends Error {
20
+ constructor(init) {
21
+ super(init.detail);
22
+ this.name = 'ApiError';
23
+ this.status = init.status;
24
+ this.detail = init.detail;
25
+ this.code = init.code;
26
+ this.body = init.body;
27
+ }
28
+ get isNetworkError() {
29
+ return this.status === 0;
30
+ }
31
+ }
32
+ export function isApiError(error) {
33
+ return error instanceof ApiError;
34
+ }
35
+ const asString = (value) => typeof value === 'string' && value.length > 0 ? value : undefined;
36
+ /** Join a FastAPI 422 `detail[]` list into one line. */
37
+ function formatValidationList(items) {
38
+ return items
39
+ .map((item) => {
40
+ if (typeof item === 'string')
41
+ return item;
42
+ if (item && typeof item === 'object') {
43
+ const entry = item;
44
+ const msg = asString(entry.msg);
45
+ if (msg) {
46
+ const loc = Array.isArray(entry.loc)
47
+ ? entry.loc.filter((part) => part !== 'body').join('.')
48
+ : '';
49
+ return loc ? `${loc}: ${msg}` : msg;
50
+ }
51
+ }
52
+ return JSON.stringify(item);
53
+ })
54
+ .join(', ');
55
+ }
56
+ /**
57
+ * Pull `{ detail, code }` out of whatever the SDK resolved as `error`: a
58
+ * FastAPI body (`{ detail }`, `{ detail: { detail, code } }`,
59
+ * `{ detail: [...] }`), a plain string, or a thrown `Error`.
60
+ */
61
+ export function extractErrorDetail(error) {
62
+ var _a, _b, _c, _d;
63
+ if (error == null)
64
+ return {};
65
+ if (typeof error === 'string')
66
+ return { detail: asString(error) };
67
+ if (error instanceof Error)
68
+ return { detail: asString(error.message) };
69
+ if (typeof error !== 'object')
70
+ return { detail: String(error) };
71
+ const body = error;
72
+ let code = (_a = asString(body.code)) !== null && _a !== void 0 ? _a : asString(body.error_code);
73
+ let detail;
74
+ const raw = body.detail;
75
+ if (typeof raw === 'string') {
76
+ detail = asString(raw);
77
+ }
78
+ else if (Array.isArray(raw)) {
79
+ detail = asString(formatValidationList(raw));
80
+ }
81
+ else if (raw && typeof raw === 'object') {
82
+ const nested = raw;
83
+ code = (_c = (_b = asString(nested.code)) !== null && _b !== void 0 ? _b : asString(nested.error_code)) !== null && _c !== void 0 ? _c : code;
84
+ if (typeof nested.detail === 'string') {
85
+ detail = asString(nested.detail);
86
+ }
87
+ else if (Array.isArray(nested.detail)) {
88
+ detail = asString(formatValidationList(nested.detail));
89
+ }
90
+ else {
91
+ detail = asString(nested.message);
92
+ }
93
+ }
94
+ detail = (_d = detail !== null && detail !== void 0 ? detail : asString(body.message)) !== null && _d !== void 0 ? _d : asString(body.error);
95
+ return { detail, code };
96
+ }
97
+ /** Build an `ApiError` from an SDK result's `error` and `response`. */
98
+ export function toApiError(error, response) {
99
+ if (error instanceof ApiError)
100
+ return error;
101
+ const status = response && typeof response.status === 'number' ? response.status : 0;
102
+ const { detail, code } = extractErrorDetail(error);
103
+ return new ApiError({
104
+ status,
105
+ detail: detail !== null && detail !== void 0 ? detail : (status === 0
106
+ ? 'Unable to reach the server'
107
+ : `Request failed with status ${status}`),
108
+ code,
109
+ body: error,
110
+ });
111
+ }
112
+ /**
113
+ * Return the SDK result's `data`, or throw an `ApiError` when the call
114
+ * failed. A result whose `error` is set is a failure regardless of `data`.
115
+ */
116
+ export function unwrapSdk(result) {
117
+ if (result && result.error !== undefined && result.error !== null) {
118
+ throw toApiError(result.error, result.response);
119
+ }
120
+ return result.data;
121
+ }
122
+ /** Status of an unknown error, or undefined when it carries none. */
123
+ export function errorStatus(error) {
124
+ var _a, _b;
125
+ if (error instanceof ApiError)
126
+ return error.status;
127
+ const err = error;
128
+ const status = (_a = err === null || err === void 0 ? void 0 : err.status) !== null && _a !== void 0 ? _a : (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.status;
129
+ return typeof status === 'number' ? status : undefined;
130
+ }
131
+ /**
132
+ * Whether a failed call is worth retrying: no response (network), rate
133
+ * limited, or a server error. A 4xx refusal is an answer, not a blip. An
134
+ * error that is not an `ApiError` (a malformed body, a thrown TypeError) is
135
+ * treated as transient.
136
+ */
137
+ export function isTransientError(error) {
138
+ if (!(error instanceof ApiError))
139
+ return true;
140
+ return error.status === 0 || error.status === 429 || error.status >= 500;
141
+ }
142
+ /**
143
+ * Whether an error is the server refusing the session itself (401, or 403
144
+ * for a deactivated account) rather than an outage or a network drop.
145
+ */
146
+ export function isSessionRejection(error) {
147
+ return (error instanceof ApiError && (error.status === 401 || error.status === 403));
148
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robosystems/core",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "description": "Shared RoboSystems frontend core library (auth, contexts, task monitoring, UI components) for the RoboSystems Next.js apps",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
  import * as SDK from '@robosystems/client';
3
3
  import { useCallback, useEffect, useRef, useState } from 'react';
4
- import { taskMonitor } from './taskMonitor';
4
+ import { unwrapSdk } from '../lib/sdk-errors';
5
+ import { POLLING_CANCELLED, taskMonitor } from './taskMonitor';
5
6
  export function useTaskMonitoring() {
6
7
  const [state, setState] = useState({
7
8
  isLoading: false,
@@ -25,17 +26,16 @@ export function useTaskMonitoring() {
25
26
  }, []);
26
27
  const cancelTask = useCallback(async () => {
27
28
  if (currentTaskId.current) {
28
- try {
29
- await taskMonitor.cancelTask(currentTaskId.current);
30
- setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: false, error: 'Task was cancelled', result: null })));
31
- }
32
- catch (error) {
33
- console.error('Failed to cancel task:', error);
34
- setState((prev) => (Object.assign(Object.assign({}, prev), { error: 'Failed to cancel task', result: null })));
35
- }
29
+ const cancelled = await taskMonitor.cancelTask(currentTaskId.current);
30
+ setState((prev) => cancelled
31
+ ? Object.assign(Object.assign({}, prev), { isLoading: false, error: 'Task was cancelled', result: null }) : // Refused (typically already finished): monitoring continues and
32
+ Object.assign(Object.assign({}, prev), { error: 'Failed to cancel task' }));
36
33
  }
37
34
  }, []);
38
35
  const startMonitoring = useCallback(async (taskId, options) => {
36
+ if (currentTaskId.current && currentTaskId.current !== taskId) {
37
+ taskMonitor.stopPolling(currentTaskId.current);
38
+ }
39
39
  currentTaskId.current = taskId;
40
40
  setState({
41
41
  isLoading: true,
@@ -63,19 +63,29 @@ export function useTaskMonitoring() {
63
63
  return result.details || result;
64
64
  }
65
65
  catch (error) {
66
+ if (error instanceof Error && error.message === POLLING_CANCELLED) {
67
+ // Stopped locally (cancelTask or unmount); cancelTask reports
68
+ // its own outcome.
69
+ setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: false })));
70
+ throw error;
71
+ }
66
72
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
67
73
  setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: false, error: errorMessage, result: null })));
68
74
  throw error;
69
75
  }
70
76
  finally {
71
- currentTaskId.current = null;
77
+ // A newer run may have started meanwhile; leave its id in place.
78
+ if (currentTaskId.current === taskId)
79
+ currentTaskId.current = null;
72
80
  }
73
81
  }, []);
74
- // Cleanup on unmount
82
+ // Unmount stops watching; the operation itself keeps running. Cancelling
83
+ // it on the server is an explicit user action (`cancelTask`), never a side
84
+ // effect of navigating away.
75
85
  useEffect(() => {
76
86
  return () => {
77
87
  if (currentTaskId.current) {
78
- taskMonitor.cancelTask(currentTaskId.current);
88
+ taskMonitor.stopPolling(currentTaskId.current);
79
89
  }
80
90
  };
81
91
  }, []);
@@ -94,7 +104,7 @@ export function useEntityCreationTask() {
94
104
  const response = await SDK.createGraph({
95
105
  body: entityData,
96
106
  });
97
- const responseData = response.data;
107
+ const responseData = unwrapSdk(response);
98
108
  if (!(responseData === null || responseData === void 0 ? void 0 : responseData.operationId)) {
99
109
  throw new Error('No operation ID returned from company creation');
100
110
  }
@@ -1,4 +1,16 @@
1
- import type { OperationStatus } from './operationMonitor';
1
+ import type { OperationResult, OperationStatus } from './operationMonitor';
2
+ /**
3
+ * Thrown by `startMonitoring` (and so by `createGraph`) when an operation
4
+ * ends in any state but `completed`. `status` is the terminal state;
5
+ * `'cancelled'` with `stopped: true` means monitoring was stopped locally
6
+ * and the operation may still be running.
7
+ */
8
+ export declare class OperationOutcomeError extends Error {
9
+ readonly status: OperationStatus;
10
+ readonly operationId: string;
11
+ readonly stopped: boolean;
12
+ constructor(result: OperationResult);
13
+ }
2
14
  export interface OperationMonitorState {
3
15
  isLoading: boolean;
4
16
  isMonitoring: boolean;
@@ -33,6 +45,11 @@ export declare function useGraphCreation(): {
33
45
  tags?: string[];
34
46
  instance_tier?: string;
35
47
  schema_extensions?: string[];
48
+ /**
49
+ * Generic graphs only: the node/relationship schema to apply. When
50
+ * omitted, a generic graph is created with an empty schema.
51
+ */
52
+ custom_schema?: Record<string, unknown>;
36
53
  create_entity?: boolean;
37
54
  company_name?: string;
38
55
  company_identifier?: string;
@@ -56,8 +73,10 @@ export declare function useGraphCreation(): {
56
73
  createGenericGraph: (graphData: {
57
74
  graph_name: string;
58
75
  description?: string;
76
+ tags?: string[];
59
77
  instance_tier?: string;
60
78
  schema_extensions?: string[];
79
+ custom_schema?: Record<string, unknown>;
61
80
  org_id?: string;
62
81
  }) => Promise<any>;
63
82
  startMonitoring: (operationId: string, options?: {
@@ -102,7 +121,7 @@ export declare function useMultipleOperations(): {
102
121
  operations: OperationMonitorState[];
103
122
  addOperation: (operationId: string, options?: {
104
123
  timeout?: number;
105
- }) => Promise<import("./operationMonitor").OperationResult>;
124
+ }) => Promise<OperationResult>;
106
125
  cancelOperation: (operationId: string) => Promise<void>;
107
126
  removeOperation: (operationId: string) => void;
108
127
  clearAll: () => void;