@agentuity/react 0.0.99 → 0.0.101
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/dist/api.d.ts +1 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +1 -2
- package/dist/api.js.map +1 -1
- package/dist/client.d.ts +90 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +102 -0
- package/dist/client.js.map +1 -0
- package/dist/context.d.ts +3 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +21 -5
- package/dist/context.js.map +1 -1
- package/dist/eventstream.d.ts +8 -22
- package/dist/eventstream.d.ts.map +1 -1
- package/dist/eventstream.js +62 -136
- package/dist/eventstream.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/memo.d.ts +2 -3
- package/dist/memo.d.ts.map +1 -1
- package/dist/memo.js +2 -20
- package/dist/memo.js.map +1 -1
- package/dist/websocket.d.ts +9 -22
- package/dist/websocket.d.ts.map +1 -1
- package/dist/websocket.js +80 -169
- package/dist/websocket.js.map +1 -1
- package/package.json +5 -2
- package/src/api.ts +1 -3
- package/src/client.ts +148 -0
- package/src/context.tsx +30 -6
- package/src/eventstream.ts +77 -177
- package/src/index.ts +43 -3
- package/src/memo.ts +2 -18
- package/src/websocket.ts +105 -225
- package/dist/env.d.ts +0 -2
- package/dist/env.d.ts.map +0 -1
- package/dist/env.js +0 -10
- package/dist/env.js.map +0 -1
- package/dist/reconnect.d.ts +0 -22
- package/dist/reconnect.d.ts.map +0 -1
- package/dist/reconnect.js +0 -47
- package/dist/reconnect.js.map +0 -1
- package/dist/serialization.d.ts +0 -6
- package/dist/serialization.d.ts.map +0 -1
- package/dist/serialization.js +0 -16
- package/dist/serialization.js.map +0 -1
- package/dist/types.d.ts +0 -19
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/url.d.ts +0 -3
- package/dist/url.d.ts.map +0 -1
- package/dist/url.js +0 -24
- package/dist/url.js.map +0 -1
- package/src/env.ts +0 -9
- package/src/reconnect.ts +0 -73
- package/src/serialization.ts +0 -14
- package/src/types.ts +0 -29
- package/src/url.ts +0 -32
package/src/client.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createClient as coreCreateClient,
|
|
3
|
+
type Client,
|
|
4
|
+
type ClientOptions,
|
|
5
|
+
} from '@agentuity/frontend';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* RPC Route Registry interface that gets augmented by generated code.
|
|
9
|
+
* Applications should not define this directly - it's populated by the build system.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // Generated code augments this interface:
|
|
14
|
+
* declare module '@agentuity/react' {
|
|
15
|
+
* export interface RPCRouteRegistry {
|
|
16
|
+
* hello: { post: { input: HelloInput; output: HelloOutput; type: 'api' } };
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
22
|
+
export interface RPCRouteRegistry {}
|
|
23
|
+
|
|
24
|
+
let globalBaseUrl: string | undefined;
|
|
25
|
+
let globalAuthHeader: string | null | undefined;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Set the global base URL for RPC clients.
|
|
29
|
+
* This is automatically called by AgentuityProvider.
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export function setGlobalBaseUrl(url: string): void {
|
|
33
|
+
globalBaseUrl = url;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the global base URL for RPC clients.
|
|
38
|
+
* Returns the configured base URL or falls back to window.location.origin.
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
export function getGlobalBaseUrl(): string {
|
|
42
|
+
return globalBaseUrl || (typeof window !== 'undefined' ? window.location.origin : '');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Set the global auth header for RPC clients.
|
|
47
|
+
* This is automatically called by AgentuityProvider when auth state changes.
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export function setGlobalAuthHeader(authHeader: string | null): void {
|
|
51
|
+
globalAuthHeader = authHeader;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the global auth header for RPC clients.
|
|
56
|
+
* Returns the current auth header or undefined if not set.
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
export function getGlobalAuthHeader(): string | null | undefined {
|
|
60
|
+
return globalAuthHeader;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create a type-safe RPC client for React applications.
|
|
65
|
+
*
|
|
66
|
+
* This is a React-specific wrapper around @agentuity/core's createClient that
|
|
67
|
+
* automatically uses the baseUrl and auth headers from AgentuityProvider context.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* import { createClient } from '@agentuity/react';
|
|
72
|
+
* import type { RPCRouteRegistry } from '@agentuity/react';
|
|
73
|
+
*
|
|
74
|
+
* const client = createClient<RPCRouteRegistry>();
|
|
75
|
+
*
|
|
76
|
+
* // Inside component
|
|
77
|
+
* const result = await client.hello.post({ name: 'World' });
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export function createClient<R>(
|
|
81
|
+
options?: Omit<ClientOptions, 'baseUrl' | 'headers'> & {
|
|
82
|
+
baseUrl?: string | (() => string);
|
|
83
|
+
headers?: Record<string, string> | (() => Record<string, string>);
|
|
84
|
+
},
|
|
85
|
+
metadata?: unknown
|
|
86
|
+
): Client<R> {
|
|
87
|
+
// Merge user headers with auth headers
|
|
88
|
+
// User-provided headers take precedence over global auth header
|
|
89
|
+
const mergedHeaders = (): Record<string, string> => {
|
|
90
|
+
const authHeader = getGlobalAuthHeader();
|
|
91
|
+
const userHeaders =
|
|
92
|
+
typeof options?.headers === 'function' ? options.headers() : options?.headers || {};
|
|
93
|
+
|
|
94
|
+
const headers: Record<string, string> = {};
|
|
95
|
+
|
|
96
|
+
// Add global auth header first (lower priority)
|
|
97
|
+
if (authHeader) {
|
|
98
|
+
headers.Authorization = authHeader;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// User headers override global auth
|
|
102
|
+
return { ...headers, ...userHeaders };
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return coreCreateClient<R>(
|
|
106
|
+
{
|
|
107
|
+
...options,
|
|
108
|
+
baseUrl: (options?.baseUrl || getGlobalBaseUrl) as string | (() => string),
|
|
109
|
+
headers: mergedHeaders,
|
|
110
|
+
} as ClientOptions,
|
|
111
|
+
metadata
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create a type-safe API client with optional configuration.
|
|
117
|
+
*
|
|
118
|
+
* This is the recommended way to create an API client in React applications.
|
|
119
|
+
* It automatically includes auth headers from AgentuityProvider and allows
|
|
120
|
+
* custom headers to be passed.
|
|
121
|
+
*
|
|
122
|
+
* The generic type parameter defaults to RPCRouteRegistry which is augmented
|
|
123
|
+
* by generated code, so you don't need to specify it manually.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* import { createAPIClient } from '@agentuity/react';
|
|
128
|
+
*
|
|
129
|
+
* // Types are automatically inferred from generated routes
|
|
130
|
+
* const api = createAPIClient();
|
|
131
|
+
* await api.hello.post({ name: 'World' });
|
|
132
|
+
*
|
|
133
|
+
* // With custom headers
|
|
134
|
+
* const api = createAPIClient({ headers: { 'X-Custom': 'value' } });
|
|
135
|
+
* await api.hello.post({ name: 'World' });
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function createAPIClient<R = RPCRouteRegistry>(
|
|
139
|
+
options?: Omit<ClientOptions, 'baseUrl' | 'headers'> & {
|
|
140
|
+
baseUrl?: string | (() => string);
|
|
141
|
+
headers?: Record<string, string> | (() => Record<string, string>);
|
|
142
|
+
}
|
|
143
|
+
): Client<R> {
|
|
144
|
+
// This function is designed to be used with generated metadata
|
|
145
|
+
// The metadata will be provided by the code generator
|
|
146
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
147
|
+
return createClient<R>(options, (globalThis as any).__rpcRouteMetadata);
|
|
148
|
+
}
|
package/src/context.tsx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { createContext, useContext, type Context
|
|
3
|
-
import { defaultBaseUrl } from '
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { createContext, useContext, type Context } from 'react';
|
|
3
|
+
import { defaultBaseUrl } from '@agentuity/frontend';
|
|
4
|
+
import { setGlobalBaseUrl, setGlobalAuthHeader } from './client';
|
|
4
5
|
|
|
5
6
|
export interface ContextProviderArgs {
|
|
6
7
|
children?: React.ReactNode;
|
|
7
8
|
baseUrl?: string;
|
|
9
|
+
authHeader?: string | null;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export interface AgentuityContextValue {
|
|
@@ -18,14 +20,36 @@ export interface AgentuityContextValue {
|
|
|
18
20
|
export const AgentuityContext: Context<AgentuityContextValue | null> =
|
|
19
21
|
createContext<AgentuityContextValue | null>(null);
|
|
20
22
|
|
|
21
|
-
export const AgentuityProvider = ({
|
|
22
|
-
|
|
23
|
+
export const AgentuityProvider = ({
|
|
24
|
+
baseUrl,
|
|
25
|
+
authHeader: authHeaderProp,
|
|
26
|
+
children,
|
|
27
|
+
}: ContextProviderArgs): React.JSX.Element => {
|
|
28
|
+
const [authHeader, setAuthHeader] = useState<string | null>(authHeaderProp ?? null);
|
|
23
29
|
const [authLoading, setAuthLoading] = useState<boolean>(false);
|
|
30
|
+
const resolvedBaseUrl = baseUrl || defaultBaseUrl;
|
|
31
|
+
|
|
32
|
+
// Set global baseUrl for RPC clients
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setGlobalBaseUrl(resolvedBaseUrl);
|
|
35
|
+
}, [resolvedBaseUrl]);
|
|
36
|
+
|
|
37
|
+
// Sync authHeader to global state for RPC clients
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
setGlobalAuthHeader(authHeader);
|
|
40
|
+
}, [authHeader]);
|
|
41
|
+
|
|
42
|
+
// Sync authHeader prop changes to state
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (authHeaderProp !== undefined) {
|
|
45
|
+
setAuthHeader(authHeaderProp);
|
|
46
|
+
}
|
|
47
|
+
}, [authHeaderProp]);
|
|
24
48
|
|
|
25
49
|
return (
|
|
26
50
|
<AgentuityContext.Provider
|
|
27
51
|
value={{
|
|
28
|
-
baseUrl:
|
|
52
|
+
baseUrl: resolvedBaseUrl,
|
|
29
53
|
authHeader,
|
|
30
54
|
setAuthHeader,
|
|
31
55
|
authLoading,
|
package/src/eventstream.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import type { InferOutput } from '@agentuity/core';
|
|
3
|
+
import {
|
|
4
|
+
buildUrl,
|
|
5
|
+
EventStreamManager,
|
|
6
|
+
jsonEqual,
|
|
7
|
+
type SSERouteRegistry,
|
|
8
|
+
} from '@agentuity/frontend';
|
|
3
9
|
import { AgentuityContext } from './context';
|
|
4
|
-
import { buildUrl } from './url';
|
|
5
|
-
import { deserializeData } from './serialization';
|
|
6
|
-
import { createReconnectManager } from './reconnect';
|
|
7
|
-
import { jsonEqual } from './memo';
|
|
8
|
-
import type { SSERouteRegistry } from './types';
|
|
9
|
-
|
|
10
|
-
type onMessageHandler<T = unknown> = (data: T) => void;
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* Extract SSE route keys (e.g., '/events', '/notifications')
|
|
@@ -43,216 +42,117 @@ export interface EventStreamOptions {
|
|
|
43
42
|
signal?: AbortSignal;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Type-safe EventStream (SSE) hook for connecting to SSE routes.
|
|
47
|
+
*
|
|
48
|
+
* Provides automatic type inference for route outputs based on
|
|
49
|
+
* the SSERouteRegistry generated from your routes.
|
|
50
|
+
*
|
|
51
|
+
* @template TRoute - SSE route key from SSERouteRegistry (e.g., '/events', '/notifications')
|
|
52
|
+
*
|
|
53
|
+
* @example Simple SSE connection
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const { isConnected, data } = useEventStream('/events');
|
|
56
|
+
*
|
|
57
|
+
* // data is fully typed based on route output schema!
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example SSE with query parameters
|
|
61
|
+
* ```typescript
|
|
62
|
+
* const { isConnected, data } = useEventStream('/notifications', {
|
|
63
|
+
* query: new URLSearchParams({ userId: '123' })
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function useEventStream<TRoute extends SSERouteKey>(
|
|
68
|
+
route: TRoute,
|
|
69
|
+
options?: EventStreamOptions
|
|
70
|
+
): {
|
|
48
71
|
isConnected: boolean;
|
|
49
|
-
|
|
50
|
-
data?:
|
|
51
|
-
/** Error if connection or message failed */
|
|
72
|
+
close: () => void;
|
|
73
|
+
data?: SSERouteOutput<TRoute>;
|
|
52
74
|
error: Error | null;
|
|
53
|
-
/** Whether an error has occurred */
|
|
54
75
|
isError: boolean;
|
|
55
|
-
/** Set handler for incoming messages (use data property instead) */
|
|
56
|
-
setHandler: (handler: onMessageHandler<TOutput>) => void;
|
|
57
|
-
/** EventStream connection state (CONNECTING=0, OPEN=1, CLOSED=2) */
|
|
58
|
-
readyState: number;
|
|
59
|
-
/** Close the EventStream connection */
|
|
60
|
-
close: () => void;
|
|
61
|
-
/** Reset state to initial values */
|
|
62
76
|
reset: () => void;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const useEventStreamInternal = <TOutput>(
|
|
66
|
-
path: string,
|
|
67
|
-
options?: EventStreamOptions
|
|
68
|
-
): EventStreamResponseInternal<TOutput> => {
|
|
77
|
+
readyState: number;
|
|
78
|
+
} {
|
|
69
79
|
const context = useContext(AgentuityContext);
|
|
70
80
|
|
|
71
81
|
if (!context) {
|
|
72
82
|
throw new Error('useEventStream must be used within a AgentuityProvider');
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
const
|
|
76
|
-
const esRef = useRef<EventSource | undefined>(undefined);
|
|
77
|
-
const pending = useRef<TOutput[]>([]);
|
|
78
|
-
const handler = useRef<onMessageHandler<TOutput> | undefined>(undefined);
|
|
79
|
-
const reconnectManagerRef = useRef<ReturnType<typeof createReconnectManager> | undefined>(
|
|
80
|
-
undefined
|
|
81
|
-
);
|
|
85
|
+
const managerRef = useRef<EventStreamManager<SSERouteOutput<TRoute>> | null>(null);
|
|
82
86
|
|
|
83
|
-
const [data, setData] = useState<
|
|
87
|
+
const [data, setData] = useState<SSERouteOutput<TRoute>>();
|
|
84
88
|
const [error, setError] = useState<Error | null>(null);
|
|
85
89
|
const [isError, setIsError] = useState(false);
|
|
86
90
|
const [isConnected, setIsConnected] = useState(false);
|
|
91
|
+
const [readyState, setReadyState] = useState<number>(2); // EventSource.CLOSED = 2
|
|
87
92
|
|
|
93
|
+
// Build EventStream URL
|
|
88
94
|
const esUrl = useMemo(
|
|
89
|
-
() => buildUrl(context.baseUrl!,
|
|
90
|
-
[context.baseUrl,
|
|
95
|
+
() => buildUrl(context.baseUrl!, route as string, options?.subpath, options?.query),
|
|
96
|
+
[context.baseUrl, route, options?.subpath, options?.query?.toString()]
|
|
91
97
|
);
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
if (manualClose.current) return;
|
|
95
|
-
|
|
96
|
-
esRef.current = new EventSource(esUrl);
|
|
97
|
-
let firstMessageReceived = false;
|
|
98
|
-
|
|
99
|
-
esRef.current.onopen = () => {
|
|
100
|
-
reconnectManagerRef.current?.recordSuccess();
|
|
101
|
-
setIsConnected(true);
|
|
102
|
-
setError(null);
|
|
103
|
-
setIsError(false);
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
esRef.current.onerror = () => {
|
|
107
|
-
setError(new Error('EventStream error'));
|
|
108
|
-
setIsError(true);
|
|
109
|
-
setIsConnected(false);
|
|
110
|
-
|
|
111
|
-
if (manualClose.current) {
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const result = reconnectManagerRef.current?.recordFailure();
|
|
116
|
-
if (result?.scheduled) {
|
|
117
|
-
const es = esRef.current;
|
|
118
|
-
if (es) {
|
|
119
|
-
es.onopen = null;
|
|
120
|
-
es.onerror = null;
|
|
121
|
-
es.onmessage = null;
|
|
122
|
-
es.close();
|
|
123
|
-
}
|
|
124
|
-
esRef.current = undefined;
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
esRef.current.onmessage = (event: MessageEvent) => {
|
|
129
|
-
if (!firstMessageReceived) {
|
|
130
|
-
reconnectManagerRef.current?.recordSuccess();
|
|
131
|
-
firstMessageReceived = true;
|
|
132
|
-
}
|
|
133
|
-
const payload = deserializeData<TOutput>(event.data);
|
|
134
|
-
// Use JSON memoization to prevent re-renders when data hasn't changed
|
|
135
|
-
setData((prev) => (prev !== undefined && jsonEqual(prev, payload) ? prev : payload));
|
|
136
|
-
if (handler.current) {
|
|
137
|
-
handler.current(payload);
|
|
138
|
-
} else {
|
|
139
|
-
pending.current.push(payload);
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
}, [esUrl]);
|
|
143
|
-
|
|
99
|
+
// Initialize manager and connect
|
|
144
100
|
useEffect(() => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
101
|
+
const manager = new EventStreamManager<SSERouteOutput<TRoute>>({
|
|
102
|
+
url: esUrl,
|
|
103
|
+
callbacks: {
|
|
104
|
+
onConnect: () => {
|
|
105
|
+
setIsConnected(true);
|
|
106
|
+
setError(null);
|
|
107
|
+
setIsError(false);
|
|
108
|
+
setReadyState(1); // EventSource.OPEN = 1
|
|
109
|
+
},
|
|
110
|
+
onDisconnect: () => {
|
|
111
|
+
setIsConnected(false);
|
|
112
|
+
setReadyState(2); // EventSource.CLOSED = 2
|
|
113
|
+
},
|
|
114
|
+
onError: (err) => {
|
|
115
|
+
setError(err);
|
|
116
|
+
setIsError(true);
|
|
117
|
+
},
|
|
118
|
+
},
|
|
153
119
|
});
|
|
154
|
-
return () => reconnectManagerRef.current?.dispose();
|
|
155
|
-
}, [connect]);
|
|
156
120
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (es) {
|
|
162
|
-
es.onopen = null;
|
|
163
|
-
es.onerror = null;
|
|
164
|
-
es.onmessage = null;
|
|
165
|
-
es.close();
|
|
166
|
-
}
|
|
167
|
-
esRef.current = undefined;
|
|
168
|
-
handler.current = undefined;
|
|
169
|
-
pending.current = [];
|
|
170
|
-
setIsConnected(false);
|
|
171
|
-
}, []);
|
|
121
|
+
// Set message handler with JSON memoization
|
|
122
|
+
manager.setMessageHandler((message) => {
|
|
123
|
+
setData((prev) => (prev !== undefined && jsonEqual(prev, message) ? prev : message));
|
|
124
|
+
});
|
|
172
125
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
connect();
|
|
126
|
+
manager.connect();
|
|
127
|
+
managerRef.current = manager;
|
|
176
128
|
|
|
177
129
|
return () => {
|
|
178
|
-
|
|
130
|
+
manager.dispose();
|
|
131
|
+
managerRef.current = null;
|
|
179
132
|
};
|
|
180
|
-
}, [
|
|
133
|
+
}, [esUrl]);
|
|
181
134
|
|
|
135
|
+
// Handle abort signal
|
|
182
136
|
useEffect(() => {
|
|
183
137
|
if (options?.signal) {
|
|
184
138
|
const listener = () => {
|
|
185
|
-
|
|
139
|
+
managerRef.current?.close();
|
|
186
140
|
};
|
|
187
141
|
options.signal.addEventListener('abort', listener);
|
|
188
142
|
return () => {
|
|
189
143
|
options.signal?.removeEventListener('abort', listener);
|
|
190
144
|
};
|
|
191
145
|
}
|
|
192
|
-
}, [options?.signal
|
|
146
|
+
}, [options?.signal]);
|
|
193
147
|
|
|
194
|
-
const reset = () => {
|
|
148
|
+
const reset = useCallback(() => {
|
|
195
149
|
setError(null);
|
|
196
150
|
setIsError(false);
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const setHandler = useCallback((h: onMessageHandler<TOutput>) => {
|
|
200
|
-
handler.current = h;
|
|
201
|
-
pending.current.forEach(h);
|
|
202
|
-
pending.current = [];
|
|
203
151
|
}, []);
|
|
204
152
|
|
|
205
|
-
const close = () => {
|
|
206
|
-
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
isConnected,
|
|
211
|
-
close,
|
|
212
|
-
data,
|
|
213
|
-
error,
|
|
214
|
-
isError,
|
|
215
|
-
setHandler,
|
|
216
|
-
reset,
|
|
217
|
-
readyState: esRef.current?.readyState ?? EventSource.CLOSED,
|
|
218
|
-
};
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Type-safe EventStream (SSE) hook for connecting to SSE routes.
|
|
223
|
-
*
|
|
224
|
-
* Provides automatic type inference for route outputs based on
|
|
225
|
-
* the SSERouteRegistry generated from your routes.
|
|
226
|
-
*
|
|
227
|
-
* @template TRoute - SSE route key from SSERouteRegistry (e.g., '/events', '/notifications')
|
|
228
|
-
*
|
|
229
|
-
* @example Simple SSE connection
|
|
230
|
-
* ```typescript
|
|
231
|
-
* const { isConnected, data } = useEventStream('/events');
|
|
232
|
-
*
|
|
233
|
-
* // data is fully typed based on route output schema!
|
|
234
|
-
* ```
|
|
235
|
-
*
|
|
236
|
-
* @example SSE with query parameters
|
|
237
|
-
* ```typescript
|
|
238
|
-
* const { isConnected, data } = useEventStream('/notifications', {
|
|
239
|
-
* query: new URLSearchParams({ userId: '123' })
|
|
240
|
-
* });
|
|
241
|
-
* ```
|
|
242
|
-
*/
|
|
243
|
-
export function useEventStream<TRoute extends SSERouteKey>(
|
|
244
|
-
route: TRoute,
|
|
245
|
-
options?: EventStreamOptions
|
|
246
|
-
): Omit<EventStreamResponseInternal<SSERouteOutput<TRoute>>, 'setHandler'> & {
|
|
247
|
-
data?: SSERouteOutput<TRoute>;
|
|
248
|
-
} {
|
|
249
|
-
const [data, setData] = useState<SSERouteOutput<TRoute>>();
|
|
250
|
-
const { isConnected, close, setHandler, readyState, error, isError, reset } =
|
|
251
|
-
useEventStreamInternal<SSERouteOutput<TRoute>>(route as string, options);
|
|
252
|
-
|
|
253
|
-
useEffect(() => {
|
|
254
|
-
setHandler(setData);
|
|
255
|
-
}, [route, setHandler]);
|
|
153
|
+
const close = useCallback(() => {
|
|
154
|
+
managerRef.current?.close();
|
|
155
|
+
}, []);
|
|
256
156
|
|
|
257
157
|
return {
|
|
258
158
|
isConnected,
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,15 @@ export {
|
|
|
8
8
|
type AgentuityHookValue,
|
|
9
9
|
type AuthContextValue,
|
|
10
10
|
} from './context';
|
|
11
|
+
export {
|
|
12
|
+
createClient,
|
|
13
|
+
createAPIClient,
|
|
14
|
+
setGlobalBaseUrl,
|
|
15
|
+
getGlobalBaseUrl,
|
|
16
|
+
setGlobalAuthHeader,
|
|
17
|
+
getGlobalAuthHeader,
|
|
18
|
+
type RPCRouteRegistry,
|
|
19
|
+
} from './client';
|
|
11
20
|
export {
|
|
12
21
|
useWebsocket,
|
|
13
22
|
type WebSocketRouteKey,
|
|
@@ -21,7 +30,6 @@ export {
|
|
|
21
30
|
type SSERouteOutput,
|
|
22
31
|
type EventStreamOptions,
|
|
23
32
|
} from './eventstream';
|
|
24
|
-
export { type RouteRegistry, type WebSocketRouteRegistry, type SSERouteRegistry } from './types';
|
|
25
33
|
export {
|
|
26
34
|
useAPI,
|
|
27
35
|
type RouteKey,
|
|
@@ -32,5 +40,37 @@ export {
|
|
|
32
40
|
type UseAPIOptions,
|
|
33
41
|
type UseAPIResult,
|
|
34
42
|
} from './api';
|
|
35
|
-
export {
|
|
36
|
-
|
|
43
|
+
export { useJsonMemo } from './memo';
|
|
44
|
+
|
|
45
|
+
// Re-export web utilities for convenience
|
|
46
|
+
export {
|
|
47
|
+
buildUrl,
|
|
48
|
+
defaultBaseUrl,
|
|
49
|
+
deserializeData,
|
|
50
|
+
createReconnectManager,
|
|
51
|
+
jsonEqual,
|
|
52
|
+
getProcessEnv,
|
|
53
|
+
WebSocketManager,
|
|
54
|
+
EventStreamManager,
|
|
55
|
+
type RouteRegistry,
|
|
56
|
+
type WebSocketRouteRegistry,
|
|
57
|
+
type SSERouteRegistry,
|
|
58
|
+
type ReconnectOptions,
|
|
59
|
+
type ReconnectManager,
|
|
60
|
+
type WebSocketMessageHandler,
|
|
61
|
+
type WebSocketCallbacks,
|
|
62
|
+
type WebSocketManagerOptions,
|
|
63
|
+
type WebSocketManagerState,
|
|
64
|
+
type EventStreamMessageHandler,
|
|
65
|
+
type EventStreamCallbacks,
|
|
66
|
+
type EventStreamManagerOptions,
|
|
67
|
+
type EventStreamManagerState,
|
|
68
|
+
// Client type exports (createClient is exported from ./client.ts)
|
|
69
|
+
type Client,
|
|
70
|
+
type ClientOptions,
|
|
71
|
+
type RouteEndpoint,
|
|
72
|
+
type WebSocketClient,
|
|
73
|
+
type EventStreamClient,
|
|
74
|
+
type StreamClient,
|
|
75
|
+
type EventHandler,
|
|
76
|
+
} from '@agentuity/frontend';
|
package/src/memo.ts
CHANGED
|
@@ -1,26 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Compares stringified JSON to avoid deep equality overhead.
|
|
4
|
-
*/
|
|
5
|
-
export function jsonEqual<T>(a: T, b: T): boolean {
|
|
6
|
-
if (a === b) return true;
|
|
7
|
-
if (a === undefined || b === undefined) return false;
|
|
8
|
-
if (a === null || b === null) return a === b;
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
12
|
-
} catch {
|
|
13
|
-
// Fallback for non-serializable values
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import { jsonEqual } from '@agentuity/frontend';
|
|
17
3
|
|
|
18
4
|
/**
|
|
19
5
|
* Hook to memoize a value based on JSON equality instead of reference equality.
|
|
20
6
|
* Prevents unnecessary re-renders when data hasn't actually changed.
|
|
21
7
|
*/
|
|
22
|
-
import { useRef } from 'react';
|
|
23
|
-
|
|
24
8
|
export function useJsonMemo<T>(value: T): T {
|
|
25
9
|
const ref = useRef<T>(value);
|
|
26
10
|
|