@agentuity/react 0.0.100 → 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.
Files changed (61) hide show
  1. package/dist/api.d.ts +1 -1
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +1 -2
  4. package/dist/api.js.map +1 -1
  5. package/dist/client.d.ts +90 -0
  6. package/dist/client.d.ts.map +1 -0
  7. package/dist/client.js +102 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/context.d.ts +3 -2
  10. package/dist/context.d.ts.map +1 -1
  11. package/dist/context.js +21 -5
  12. package/dist/context.js.map +1 -1
  13. package/dist/eventstream.d.ts +8 -22
  14. package/dist/eventstream.d.ts.map +1 -1
  15. package/dist/eventstream.js +62 -136
  16. package/dist/eventstream.js.map +1 -1
  17. package/dist/index.d.ts +3 -3
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +4 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/memo.d.ts +2 -3
  22. package/dist/memo.d.ts.map +1 -1
  23. package/dist/memo.js +2 -20
  24. package/dist/memo.js.map +1 -1
  25. package/dist/websocket.d.ts +9 -22
  26. package/dist/websocket.d.ts.map +1 -1
  27. package/dist/websocket.js +80 -169
  28. package/dist/websocket.js.map +1 -1
  29. package/package.json +5 -2
  30. package/src/api.ts +1 -3
  31. package/src/client.ts +148 -0
  32. package/src/context.tsx +30 -6
  33. package/src/eventstream.ts +77 -177
  34. package/src/index.ts +43 -3
  35. package/src/memo.ts +2 -18
  36. package/src/websocket.ts +105 -225
  37. package/dist/env.d.ts +0 -2
  38. package/dist/env.d.ts.map +0 -1
  39. package/dist/env.js +0 -10
  40. package/dist/env.js.map +0 -1
  41. package/dist/reconnect.d.ts +0 -22
  42. package/dist/reconnect.d.ts.map +0 -1
  43. package/dist/reconnect.js +0 -47
  44. package/dist/reconnect.js.map +0 -1
  45. package/dist/serialization.d.ts +0 -6
  46. package/dist/serialization.d.ts.map +0 -1
  47. package/dist/serialization.js +0 -16
  48. package/dist/serialization.js.map +0 -1
  49. package/dist/types.d.ts +0 -19
  50. package/dist/types.d.ts.map +0 -1
  51. package/dist/types.js +0 -2
  52. package/dist/types.js.map +0 -1
  53. package/dist/url.d.ts +0 -3
  54. package/dist/url.d.ts.map +0 -1
  55. package/dist/url.js +0 -24
  56. package/dist/url.js.map +0 -1
  57. package/src/env.ts +0 -9
  58. package/src/reconnect.ts +0 -73
  59. package/src/serialization.ts +0 -14
  60. package/src/types.ts +0 -29
  61. 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, type ReactElement } from 'react';
3
- import { defaultBaseUrl } from './url';
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 = ({ baseUrl, children }: ContextProviderArgs): ReactElement => {
22
- const [authHeader, setAuthHeader] = useState<string | null>(null);
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: baseUrl || defaultBaseUrl,
52
+ baseUrl: resolvedBaseUrl,
29
53
  authHeader,
30
54
  setAuthHeader,
31
55
  authLoading,
@@ -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
- interface EventStreamResponseInternal<TOutput> {
47
- /** Whether EventStream is currently connected */
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
- /** Most recent data received from EventStream */
50
- data?: TOutput;
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 manualClose = useRef(false);
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<TOutput>();
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!, path, options?.subpath, options?.query),
90
- [context.baseUrl, path, options?.subpath, options?.query?.toString()]
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
- const connect = useCallback(() => {
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
- reconnectManagerRef.current = createReconnectManager({
146
- onReconnect: connect,
147
- threshold: 3,
148
- baseDelay: 500,
149
- factor: 2,
150
- maxDelay: 30000,
151
- jitter: 250,
152
- enabled: () => !manualClose.current,
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
- const cleanup = useCallback(() => {
158
- manualClose.current = true;
159
- reconnectManagerRef.current?.dispose();
160
- const es = esRef.current;
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
- useEffect(() => {
174
- manualClose.current = false;
175
- connect();
126
+ manager.connect();
127
+ managerRef.current = manager;
176
128
 
177
129
  return () => {
178
- cleanup();
130
+ manager.dispose();
131
+ managerRef.current = null;
179
132
  };
180
- }, [connect, cleanup]);
133
+ }, [esUrl]);
181
134
 
135
+ // Handle abort signal
182
136
  useEffect(() => {
183
137
  if (options?.signal) {
184
138
  const listener = () => {
185
- cleanup();
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, cleanup]);
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
- cleanup();
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 { jsonEqual, useJsonMemo } from './memo';
36
- export { buildUrl, defaultBaseUrl } from './url';
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
- * Simple JSON-based equality check for memoization.
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