@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.
- package/auth-components/AuthProvider.js +166 -27
- package/auth-components/SessionWarningDialog.js +2 -2
- package/auth-components/SignInForm.d.ts +2 -1
- package/auth-components/SignInForm.js +8 -2
- package/auth-components/SignUpForm.js +4 -1
- package/auth-core/client.d.ts +8 -1
- package/auth-core/client.js +142 -100
- package/auth-core/sso.d.ts +4 -9
- package/auth-core/sso.js +18 -58
- package/auth-core/token-storage.d.ts +12 -0
- package/auth-core/token-storage.js +42 -16
- package/auth-core/types.d.ts +1 -0
- package/components/console/ConsoleContent.js +30 -4
- package/components/console/graphAwareConfig.js +27 -17
- package/components/repositories/ActiveSubscriptions.js +18 -6
- package/components/repositories/BrowseRepositories.js +17 -5
- package/components/search/SearchContent.js +30 -4
- package/contexts/graph-context.js +23 -13
- package/hooks/use-streaming-query.js +43 -28
- package/index.d.ts +5 -2
- package/index.js +6 -2
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/sdk-errors.d.ts +79 -0
- package/lib/sdk-errors.js +148 -0
- package/package.json +1 -1
- package/task-monitoring/hooks.js +23 -13
- package/task-monitoring/operationHooks.d.ts +21 -2
- package/task-monitoring/operationHooks.js +67 -17
- package/task-monitoring/operationMonitor.d.ts +10 -1
- package/task-monitoring/operationMonitor.js +109 -57
- package/task-monitoring/taskMonitor.d.ts +15 -1
- package/task-monitoring/taskMonitor.js +120 -73
- package/task-monitoring/types.d.ts +2 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
package/task-monitoring/hooks.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
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
|
|
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<
|
|
124
|
+
}) => Promise<OperationResult>;
|
|
106
125
|
cancelOperation: (operationId: string) => Promise<void>;
|
|
107
126
|
removeOperation: (operationId: string) => void;
|
|
108
127
|
clearAll: () => void;
|