@classytic/arc-next 0.4.1 → 0.5.0

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/sse.d.ts CHANGED
@@ -1,69 +1,148 @@
1
1
  import { QueryKey } from "@tanstack/react-query";
2
2
 
3
3
  //#region src/sse.d.ts
4
- interface ArcServerEvent {
4
+ /**
5
+ * Build an authenticated SSE URL using the global client + auth singletons.
6
+ *
7
+ * Thin alias for {@link import('./client.js').buildStreamUrl} with the HTTP
8
+ * protocol — kept as a named export so SSE consumers don't have to think about
9
+ * the `protocol` arg.
10
+ *
11
+ * @example
12
+ * const es = new EventSource(buildSseUrl('/jobs/stream', { jobId }), {
13
+ * withCredentials: getAuthMode() === 'cookie',
14
+ * });
15
+ */
16
+ declare function buildSseUrl(path: string, params?: Record<string, string | number | boolean | null | undefined>): string;
17
+ /**
18
+ * Generic Arc server event envelope.
19
+ *
20
+ * Defaults to `unknown` payload — narrow with the generic when you control
21
+ * the broadcast shape (`ArcServerEvent<Todo>`). For the canonical CRUD shape,
22
+ * use {@link CrudEvent} which constrains `type` to `<resource>.<operation>`
23
+ * and `operation` to the three lifecycle verbs.
24
+ */
25
+ interface ArcServerEvent<TData = unknown> {
5
26
  type: string;
6
27
  resource: string;
7
- data: unknown;
28
+ data: TData;
8
29
  timestamp: string;
9
30
  id?: string;
10
31
  }
11
- interface EventStreamOptions {
12
- /** Full SSE endpoint URL. When set, overrides `path` and `baseUrl`. Use for non-Arc backends or custom URLs. */
32
+ /** Lifecycle operations Arc emits on every CRUD broadcast. */
33
+ type CrudOperation = "created" | "updated" | "deleted";
34
+ /**
35
+ * Typed CRUD event narrowed to Arc's `<resource>.<operation>` envelope.
36
+ *
37
+ * Arc auto-emits this shape from `BaseController` for every list/get/create/
38
+ * update/delete. Pass `<TDoc>` so SSE/WS callbacks get inference for free:
39
+ *
40
+ * ```ts
41
+ * subscribeToEvents<CrudEvent<Todo>>({
42
+ * resource: 'todo',
43
+ * onEvent: (e) => console.log(e.operation, e.data.title),
44
+ * });
45
+ * ```
46
+ */
47
+ interface CrudEvent<TDoc = unknown> extends ArcServerEvent<TDoc> {
48
+ /** Always `<resource>.<operation>` (e.g. `'todo.created'`). */
49
+ type: string;
50
+ operation: CrudOperation;
51
+ }
52
+ interface SubscribeToEventsOptions<TData = unknown> {
53
+ /** Full SSE endpoint URL. When set, overrides `path` and `baseUrl`. */
13
54
  url?: string;
14
- /**
15
- * Resource name for automatic pattern filtering.
16
- * When set and `patterns` is empty, auto-generates `['resource.*']` filter.
17
- * Does NOT affect the endpoint URL — use `path` for that.
18
- */
55
+ /** Resource name for auto pattern filtering + named-event derivation. */
19
56
  resource?: string;
20
- /**
21
- * SSE endpoint path (appended to baseUrl). Default: '/events/stream'.
22
- * Matches Arc's ssePlugin default. Override if your backend uses a custom path.
23
- */
57
+ /** Endpoint path (default: `/events/stream`). */
24
58
  path?: string;
25
- /** Event patterns to listen for (e.g., ['agents.created', 'agents.updated']). When empty, all events are received. */
59
+ /** Event patterns to listen for (e.g. `['todo.*']`). Empty all events. */
26
60
  patterns?: string[];
27
- /** Query keys to invalidate when any event is received. */
28
- invalidateQueries?: QueryKey[];
29
- /** Callback for each event. */
30
- onEvent?: (event: ArcServerEvent) => void;
31
- /** Callback for connection state changes. */
61
+ /**
62
+ * Named SSE event types to subscribe to via `addEventListener`.
63
+ * Falls back to `patterns` (literal entries) or `[<resource>.created|updated|deleted]`.
64
+ * Pass `[]` to opt out of named-event subscription entirely.
65
+ */
66
+ eventTypes?: string[];
67
+ /** Per-event callback. */
68
+ onEvent?: (event: ArcServerEvent<TData>) => void;
69
+ /** Connection-state callback. */
32
70
  onConnectionChange?: (connected: boolean) => void;
33
- /** Whether the stream is enabled. Default: true */
34
- enabled?: boolean;
35
- /** Reconnect delay in ms. Default: 3000 */
71
+ /** Reconnect delay in ms. Default: 3000. */
36
72
  reconnectDelay?: number;
37
- /** Maximum reconnect attempts before giving up. Default: Infinity */
73
+ /** Maximum reconnect attempts. Default: Infinity. */
38
74
  maxReconnectAttempts?: number;
39
75
  /** Whether to include credentials (cookies). Derived from authMode when not set. */
40
76
  withCredentials?: boolean;
41
77
  }
42
- interface EventStreamResult {
43
- /** Whether the EventSource is currently connected. */
78
+ /** Handle returned by {@link subscribeToEvents}. Stable across reconnects. */
79
+ interface SubscribeToEventsHandle {
80
+ close: () => void;
81
+ reconnect: () => void;
82
+ isConnected: () => boolean;
83
+ }
84
+ interface EventStreamOptions<TData = unknown> extends SubscribeToEventsOptions<TData> {
85
+ /** Query keys to invalidate when any matching event arrives. */
86
+ invalidateQueries?: QueryKey[];
87
+ /** Whether the stream is active. Default: true. */
88
+ enabled?: boolean;
89
+ /**
90
+ * Whether to track `lastEvent` in React state. Default: true.
91
+ *
92
+ * Set to `false` for high-volume streams (telemetry, live tickers) where
93
+ * the consumer uses `onEvent` for fire-and-forget handling and never reads
94
+ * `result.lastEvent`. Each inbound frame skips a `setState` call,
95
+ * eliminating per-event re-renders.
96
+ */
97
+ trackLastEvent?: boolean;
98
+ /**
99
+ * Whether to track `eventCount` in React state. Default: true.
100
+ *
101
+ * Same trade-off as `trackLastEvent`: skip the counter `setState` for
102
+ * high-volume streams that don't read it.
103
+ */
104
+ trackEventCount?: boolean;
105
+ }
106
+ interface EventStreamResult<TData = unknown> {
44
107
  isConnected: boolean;
45
- /** The most recently received event. */
46
- lastEvent: ArcServerEvent | null;
47
- /** Number of events received since connection. */
108
+ lastEvent: ArcServerEvent<TData> | null;
48
109
  eventCount: number;
49
- /** Close the connection manually. */
50
110
  close: () => void;
51
- /** Reconnect after a manual close. */
52
111
  reconnect: () => void;
53
112
  }
113
+ /**
114
+ * Subscribe to an Arc SSE stream from any JS context (React, Node, Bun, tests).
115
+ * Pure function — no React hook required. Returns a handle with `close()` /
116
+ * `reconnect()` / `isConnected()`.
117
+ *
118
+ * Reconnect uses exponential backoff (×1.5 per attempt, capped at 30s).
119
+ * Subscriptions persist across reconnect.
120
+ *
121
+ * @example
122
+ * const sub = subscribeToEvents<CrudEvent<Todo>>({
123
+ * resource: 'todo',
124
+ * onEvent: (e) => console.log(e.operation, e.data.title),
125
+ * });
126
+ * // ...later
127
+ * sub.close();
128
+ */
129
+ declare function subscribeToEvents<TData = unknown>(options: SubscribeToEventsOptions<TData>): SubscribeToEventsHandle;
54
130
  /**
55
131
  * Subscribe to Arc server-sent events for real-time cache invalidation.
56
132
  *
57
- * Uses the browser's native `EventSource` API for automatic reconnection
133
+ * Uses the browser's native `EventSource` for automatic reconnection
58
134
  * and efficient server-push. Events trigger query invalidation so TanStack Query
59
135
  * refetches affected data automatically.
60
136
  *
137
+ * Internally delegates to {@link subscribeToEvents} — for non-React contexts
138
+ * (Node, tests, plain JS) call that directly.
139
+ *
61
140
  * @example
62
- * const { isConnected } = useEventStream({
63
- * resource: 'agents',
64
- * invalidateQueries: [agentKeys.lists()],
141
+ * const { isConnected } = useEventStream<CrudEvent<Todo>>({
142
+ * resource: 'todo',
143
+ * invalidateQueries: [todoKeys.lists()],
65
144
  * });
66
145
  */
67
- declare function useEventStream(options: EventStreamOptions): EventStreamResult;
146
+ declare function useEventStream<TData = unknown>(options: EventStreamOptions<TData>): EventStreamResult<TData>;
68
147
  //#endregion
69
- export { ArcServerEvent, EventStreamOptions, EventStreamResult, useEventStream };
148
+ export { ArcServerEvent, CrudEvent, CrudOperation, EventStreamOptions, EventStreamResult, SubscribeToEventsHandle, SubscribeToEventsOptions, buildSseUrl, subscribeToEvents, useEventStream };
package/dist/sse.js CHANGED
@@ -1,145 +1,221 @@
1
1
  "use client";
2
2
 
3
- import { getAuthContext, getAuthMode, getBaseUrl } from "./client.js";
3
+ import { buildStreamUrl, getAuthMode } from "./client.js";
4
4
  import { useQueryClient } from "@tanstack/react-query";
5
- import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { useEffect, useMemo, useRef, useState } from "react";
6
6
 
7
7
  //#region src/sse.ts
8
8
  /**
9
+ * Build an authenticated SSE URL using the global client + auth singletons.
10
+ *
11
+ * Thin alias for {@link import('./client.js').buildStreamUrl} with the HTTP
12
+ * protocol — kept as a named export so SSE consumers don't have to think about
13
+ * the `protocol` arg.
14
+ *
15
+ * @example
16
+ * const es = new EventSource(buildSseUrl('/jobs/stream', { jobId }), {
17
+ * withCredentials: getAuthMode() === 'cookie',
18
+ * });
19
+ */
20
+ function buildSseUrl(path, params = {}) {
21
+ return buildStreamUrl(path, params, "http");
22
+ }
23
+ /**
24
+ * Subscribe to an Arc SSE stream from any JS context (React, Node, Bun, tests).
25
+ * Pure function — no React hook required. Returns a handle with `close()` /
26
+ * `reconnect()` / `isConnected()`.
27
+ *
28
+ * Reconnect uses exponential backoff (×1.5 per attempt, capped at 30s).
29
+ * Subscriptions persist across reconnect.
30
+ *
31
+ * @example
32
+ * const sub = subscribeToEvents<CrudEvent<Todo>>({
33
+ * resource: 'todo',
34
+ * onEvent: (e) => console.log(e.operation, e.data.title),
35
+ * });
36
+ * // ...later
37
+ * sub.close();
38
+ */
39
+ function subscribeToEvents(options) {
40
+ const { url, resource, path: ssePath = "/events/stream", patterns = [], reconnectDelay = 3e3, maxReconnectAttempts = Infinity, withCredentials } = options;
41
+ let es = null;
42
+ let reconnectAttempts = 0;
43
+ let reconnectTimer = null;
44
+ let manualClose = false;
45
+ let connected = false;
46
+ const resolvedEventTypes = options.eventTypes !== void 0 ? options.eventTypes : patterns.length > 0 ? patterns.filter((p) => !p.includes("*")) : resource ? [
47
+ `${resource}.created`,
48
+ `${resource}.updated`,
49
+ `${resource}.deleted`
50
+ ] : [];
51
+ const buildUrl = () => {
52
+ if (url) return url;
53
+ const effectivePatterns = patterns.length > 0 ? patterns : resource ? [`${resource}.*`] : [];
54
+ const params = {};
55
+ if (effectivePatterns.length > 0) params.patterns = effectivePatterns.join(",");
56
+ return buildSseUrl(ssePath, params);
57
+ };
58
+ const dispatch = (parsed) => {
59
+ if (patterns.length > 0 && !patterns.includes(parsed.type)) return;
60
+ options.onEvent?.(parsed);
61
+ };
62
+ const connect = () => {
63
+ if (es) try {
64
+ es.close();
65
+ } catch {}
66
+ manualClose = false;
67
+ const credentials = withCredentials ?? getAuthMode() === "cookie";
68
+ es = new EventSource(buildUrl(), { withCredentials: credentials });
69
+ es.onopen = () => {
70
+ reconnectAttempts = 0;
71
+ connected = true;
72
+ options.onConnectionChange?.(true);
73
+ };
74
+ es.onmessage = (event) => {
75
+ try {
76
+ dispatch(JSON.parse(event.data));
77
+ } catch {}
78
+ };
79
+ for (const eventType of resolvedEventTypes) es.addEventListener(eventType, (event) => {
80
+ let payload;
81
+ try {
82
+ payload = JSON.parse(event.data);
83
+ } catch {
84
+ payload = event.data;
85
+ }
86
+ dispatch(typeof payload === "object" && payload !== null && "type" in payload && "data" in payload ? payload : {
87
+ type: eventType,
88
+ resource: resource ?? eventType.split(".")[0] ?? "",
89
+ data: payload,
90
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
91
+ ...event.lastEventId ? { id: event.lastEventId } : {}
92
+ });
93
+ });
94
+ es.onerror = () => {
95
+ try {
96
+ es?.close();
97
+ } catch {}
98
+ connected = false;
99
+ options.onConnectionChange?.(false);
100
+ if (manualClose) return;
101
+ if (reconnectAttempts < maxReconnectAttempts) {
102
+ reconnectAttempts += 1;
103
+ const delay = Math.min(reconnectDelay * Math.pow(1.5, reconnectAttempts - 1), 3e4);
104
+ reconnectTimer = setTimeout(connect, delay);
105
+ }
106
+ };
107
+ };
108
+ connect();
109
+ return {
110
+ close: () => {
111
+ manualClose = true;
112
+ if (reconnectTimer) {
113
+ clearTimeout(reconnectTimer);
114
+ reconnectTimer = null;
115
+ }
116
+ if (es) {
117
+ try {
118
+ es.close();
119
+ } catch {}
120
+ es = null;
121
+ }
122
+ connected = false;
123
+ options.onConnectionChange?.(false);
124
+ },
125
+ reconnect: () => {
126
+ reconnectAttempts = 0;
127
+ manualClose = false;
128
+ connect();
129
+ },
130
+ isConnected: () => connected
131
+ };
132
+ }
133
+ /**
9
134
  * Subscribe to Arc server-sent events for real-time cache invalidation.
10
135
  *
11
- * Uses the browser's native `EventSource` API for automatic reconnection
136
+ * Uses the browser's native `EventSource` for automatic reconnection
12
137
  * and efficient server-push. Events trigger query invalidation so TanStack Query
13
138
  * refetches affected data automatically.
14
139
  *
140
+ * Internally delegates to {@link subscribeToEvents} — for non-React contexts
141
+ * (Node, tests, plain JS) call that directly.
142
+ *
15
143
  * @example
16
- * const { isConnected } = useEventStream({
17
- * resource: 'agents',
18
- * invalidateQueries: [agentKeys.lists()],
144
+ * const { isConnected } = useEventStream<CrudEvent<Todo>>({
145
+ * resource: 'todo',
146
+ * invalidateQueries: [todoKeys.lists()],
19
147
  * });
20
148
  */
21
149
  function useEventStream(options) {
22
- const { url, resource, path: ssePath = "/events/stream", enabled = true, reconnectDelay = 3e3, maxReconnectAttempts = Infinity, withCredentials } = options;
150
+ const { url, resource, path, enabled = true, trackLastEvent = true, trackEventCount = true } = options;
23
151
  const queryClient = useQueryClient();
24
152
  const [isConnected, setIsConnected] = useState(false);
25
153
  const [lastEvent, setLastEvent] = useState(null);
26
154
  const [eventCount, setEventCount] = useState(0);
27
- const esRef = useRef(null);
28
- const reconnectAttemptsRef = useRef(0);
29
- const reconnectTimerRef = useRef(null);
30
- const manualCloseRef = useRef(false);
155
+ const handleRef = useRef(null);
31
156
  const onEventRef = useRef(options.onEvent);
32
157
  onEventRef.current = options.onEvent;
33
158
  const onConnectionChangeRef = useRef(options.onConnectionChange);
34
159
  onConnectionChangeRef.current = options.onConnectionChange;
35
- const patternsRef = useRef(options.patterns ?? []);
36
- patternsRef.current = options.patterns ?? [];
37
160
  const invalidateKeysRef = useRef(options.invalidateQueries ?? []);
38
161
  invalidateKeysRef.current = options.invalidateQueries ?? [];
39
- const buildUrl = useCallback(() => {
40
- if (url) return url;
41
- const auth = getAuthContext();
42
- const params = new URLSearchParams();
43
- const patterns = patternsRef.current;
44
- const effectivePatterns = patterns.length > 0 ? patterns : resource ? [`${resource}.*`] : [];
45
- if (effectivePatterns.length > 0) params.set("patterns", effectivePatterns.join(","));
46
- if (auth.organizationId) params.set("organizationId", auth.organizationId);
47
- if (auth.token) params.set("token", auth.token);
48
- const qs = params.toString();
49
- const base = `${getBaseUrl()}${ssePath}`;
50
- return qs ? `${base}?${qs}` : base;
51
- }, [
52
- url,
53
- resource,
54
- ssePath
55
- ]);
56
- const connect = useCallback(() => {
57
- if (esRef.current) esRef.current.close();
58
- manualCloseRef.current = false;
59
- const eventUrl = buildUrl();
60
- const authMode = getAuthMode();
61
- const es = new EventSource(eventUrl, { withCredentials: withCredentials ?? authMode === "cookie" });
62
- esRef.current = es;
63
- es.onopen = () => {
64
- reconnectAttemptsRef.current = 0;
65
- setIsConnected(true);
66
- onConnectionChangeRef.current?.(true);
67
- };
68
- es.onmessage = (event) => {
69
- try {
70
- const parsed = JSON.parse(event.data);
71
- const patterns = patternsRef.current;
72
- if (patterns.length > 0 && !patterns.includes(parsed.type)) return;
73
- setLastEvent(parsed);
74
- setEventCount((c) => c + 1);
75
- onEventRef.current?.(parsed);
76
- const keys = invalidateKeysRef.current;
77
- for (const key of keys) queryClient.invalidateQueries({ queryKey: key });
78
- } catch {}
79
- };
80
- es.onerror = () => {
81
- es.close();
82
- setIsConnected(false);
83
- onConnectionChangeRef.current?.(false);
84
- if (manualCloseRef.current) return;
85
- if (reconnectAttemptsRef.current < maxReconnectAttempts) {
86
- reconnectAttemptsRef.current += 1;
87
- const delay = Math.min(reconnectDelay * Math.pow(1.5, reconnectAttemptsRef.current - 1), 3e4);
88
- reconnectTimerRef.current = setTimeout(connect, delay);
89
- }
90
- };
91
- }, [
92
- buildUrl,
93
- queryClient,
94
- withCredentials,
95
- reconnectDelay,
96
- maxReconnectAttempts
97
- ]);
98
- const close = useCallback(() => {
99
- manualCloseRef.current = true;
100
- if (reconnectTimerRef.current) {
101
- clearTimeout(reconnectTimerRef.current);
102
- reconnectTimerRef.current = null;
103
- }
104
- if (esRef.current) {
105
- esRef.current.close();
106
- esRef.current = null;
107
- }
108
- setIsConnected(false);
109
- onConnectionChangeRef.current?.(false);
110
- }, []);
111
- const reconnect = useCallback(() => {
112
- reconnectAttemptsRef.current = 0;
113
- manualCloseRef.current = false;
114
- connect();
115
- }, [connect]);
162
+ const patternsKey = JSON.stringify(options.patterns ?? null);
163
+ const eventTypesKey = JSON.stringify(options.eventTypes ?? null);
164
+ const patterns = useMemo(() => options.patterns, [patternsKey]);
165
+ const eventTypes = useMemo(() => options.eventTypes, [eventTypesKey]);
116
166
  useEffect(() => {
117
167
  if (!enabled) {
118
- close();
168
+ handleRef.current?.close();
169
+ handleRef.current = null;
119
170
  return;
120
171
  }
121
- connect();
122
- return () => {
123
- manualCloseRef.current = true;
124
- if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
125
- if (esRef.current) {
126
- esRef.current.close();
127
- esRef.current = null;
172
+ const handle = subscribeToEvents({
173
+ url,
174
+ resource,
175
+ path,
176
+ patterns,
177
+ eventTypes,
178
+ reconnectDelay: options.reconnectDelay,
179
+ maxReconnectAttempts: options.maxReconnectAttempts,
180
+ withCredentials: options.withCredentials,
181
+ onConnectionChange: (c) => {
182
+ setIsConnected(c);
183
+ onConnectionChangeRef.current?.(c);
184
+ },
185
+ onEvent: (event) => {
186
+ if (trackLastEvent) setLastEvent(event);
187
+ if (trackEventCount) setEventCount((n) => n + 1);
188
+ onEventRef.current?.(event);
189
+ for (const key of invalidateKeysRef.current) queryClient.invalidateQueries({ queryKey: key });
128
190
  }
191
+ });
192
+ handleRef.current = handle;
193
+ return () => {
194
+ handle.close();
195
+ handleRef.current = null;
129
196
  };
130
197
  }, [
131
198
  enabled,
132
- connect,
133
- close
199
+ url,
200
+ resource,
201
+ path,
202
+ patterns,
203
+ eventTypes,
204
+ options.reconnectDelay,
205
+ options.maxReconnectAttempts,
206
+ options.withCredentials,
207
+ trackLastEvent,
208
+ trackEventCount,
209
+ queryClient
134
210
  ]);
135
211
  return {
136
212
  isConnected,
137
213
  lastEvent,
138
214
  eventCount,
139
- close,
140
- reconnect
215
+ close: () => handleRef.current?.close(),
216
+ reconnect: () => handleRef.current?.reconnect()
141
217
  };
142
218
  }
143
219
 
144
220
  //#endregion
145
- export { useEventStream };
221
+ export { buildSseUrl, subscribeToEvents, useEventStream };
@@ -0,0 +1,181 @@
1
+ import { ArcClient, HttpMethod, ToastHandler } from "./client.js";
2
+ import { MutationMessages } from "./mutation.js";
3
+ import { QueryKey } from "@tanstack/react-query";
4
+
5
+ //#region src/upload.d.ts
6
+ /**
7
+ * Upload-progress snapshot. Emitted on every native `xhr.upload.progress`
8
+ * event. `lengthComputable` reflects whether the browser knows the total —
9
+ * always true for `FormData` bodies in modern browsers, but consumers MUST
10
+ * check it before treating `percent` as meaningful (some embedded WebView
11
+ * environments still emit non-computable progress).
12
+ */
13
+ interface UploadProgress {
14
+ /** 0..100, integer-rounded. Equal to `(loaded / total) * 100` when computable, 0 otherwise. */
15
+ percent: number;
16
+ /** Bytes successfully uploaded so far. */
17
+ loaded: number;
18
+ /** Total bytes the request body announces. 0 when `lengthComputable` is false. */
19
+ total: number;
20
+ /** Whether the runtime knows the total. False ⇒ `percent` is 0 and not meaningful. */
21
+ lengthComputable: boolean;
22
+ }
23
+ interface UploadWithProgressOptions {
24
+ /**
25
+ * Target endpoint. Absolute (`https://api.example.com/...`) or relative
26
+ * (`/api/v1/media/upload`). Relative paths are prefixed with the configured
27
+ * `baseUrl` from `configureClient()` (or `client.config.baseUrl`).
28
+ */
29
+ url: string;
30
+ /**
31
+ * Request body. Pass `FormData` directly — XHR auto-sets the
32
+ * `multipart/form-data` Content-Type with the right boundary. We never set
33
+ * Content-Type ourselves on FormData uploads; doing so strips the boundary.
34
+ */
35
+ formData: FormData;
36
+ /** HTTP method. Default: `'POST'`. */
37
+ method?: HttpMethod;
38
+ /** Per-event progress callback. Called on every `xhr.upload.progress`. */
39
+ onProgress?: (progress: UploadProgress) => void;
40
+ /**
41
+ * Abort signal. When the signal fires (or is already aborted), the XHR is
42
+ * aborted immediately and the returned promise rejects with the signal's
43
+ * `reason` (or `DOMException('AbortError')` if no reason was set).
44
+ */
45
+ signal?: AbortSignal;
46
+ /**
47
+ * Per-client auth context override. When provided, auth headers
48
+ * (`Authorization`, `x-organization-id`, `x-arc-scope`) and `withCredentials`
49
+ * are derived from this client's config. Falls back to the global
50
+ * `configureClient()` / `configureAuth()` singletons when absent.
51
+ */
52
+ client?: ArcClient;
53
+ /** Explicit token, overrides `client?.auth?.getToken()` and global. */
54
+ token?: string | null;
55
+ /** Explicit organization id, overrides `client?.auth?.getOrgId()` and global. */
56
+ organizationId?: string | null;
57
+ /**
58
+ * Extra headers merged on top of the auth-derived headers. Use for
59
+ * per-request needs (`Accept-Version`, `x-foo`, etc.). Setting
60
+ * `Content-Type` here is allowed but discouraged — XHR computes the
61
+ * multipart boundary automatically.
62
+ */
63
+ headers?: Record<string, string>;
64
+ /** Send `x-arc-scope: platform` for arc's elevated-scope upgrade. */
65
+ elevated?: boolean;
66
+ /** Sent as `Idempotency-Key` header. Match the fetch path's contract. */
67
+ idempotencyKey?: string;
68
+ /**
69
+ * Treat the response as a binary blob instead of attempting JSON parse.
70
+ * Use when the upload returns a transformed file (e.g. the server resizes
71
+ * an image and returns the resized binary). Default: false (parse JSON).
72
+ */
73
+ responseType?: "json" | "text" | "blob";
74
+ }
75
+ /**
76
+ * Upload a `FormData` payload via XHR with native progress events.
77
+ *
78
+ * Returns a Promise that resolves with the parsed response body
79
+ * (`responseType: 'json'` by default — pass `'text'` or `'blob'` to opt out).
80
+ * Rejects with {@link ArcApiError} on non-2xx, with abort errors when the
81
+ * provided signal fires, or `Error` on transport failure (network down, CORS).
82
+ *
83
+ * Reuses {@link getClientAuthContext} so the global `configureAuth()` token /
84
+ * orgId — and per-client overrides via `client?.auth` — flow through. Sets
85
+ * `withCredentials = true` automatically for `authMode: 'cookie'`.
86
+ *
87
+ * @example
88
+ * const result = await uploadWithProgress<{ url: string }>({
89
+ * url: '/api/v1/media/upload',
90
+ * formData,
91
+ * onProgress: ({ percent }) => setUiProgress(percent),
92
+ * });
93
+ */
94
+ declare function uploadWithProgress<TResult = unknown>(options: UploadWithProgressOptions): Promise<TResult>;
95
+ interface UseUploadWithProgressOptions<TResult, TVars> {
96
+ /** Endpoint URL or a function that derives it from `vars`. Relative or absolute. */
97
+ url: string | ((vars: TVars) => string);
98
+ /** HTTP method. Default: `'POST'`. */
99
+ method?: HttpMethod;
100
+ /** Build the FormData payload from the call's variables. */
101
+ buildFormData: (vars: TVars) => FormData;
102
+ /** Query keys to invalidate after a successful upload. */
103
+ invalidateQueries?: QueryKey[];
104
+ /** Toast messages. Same shape as `useMutationWithTransition`. */
105
+ messages?: MutationMessages;
106
+ /** Override the global toast handler for this hook only. */
107
+ toastHandler?: ToastHandler;
108
+ /** Per-client auth pipeline (multi-backend apps). */
109
+ client?: ArcClient;
110
+ /**
111
+ * Extra request headers per call. Function form receives the call's vars
112
+ * so you can compute headers from the upload (e.g. an idempotency key
113
+ * derived from the file hash).
114
+ */
115
+ headers?: Record<string, string> | ((vars: TVars) => Record<string, string>);
116
+ /** Send `x-arc-scope: platform`. Per-call dynamic via function form. */
117
+ elevated?: boolean | ((vars: TVars) => boolean);
118
+ /** Per-call idempotency key. Function form gets the vars. */
119
+ idempotencyKey?: string | ((vars: TVars) => string);
120
+ /** `'json'` (default), `'text'`, or `'blob'`. */
121
+ responseType?: "json" | "text" | "blob";
122
+ onSuccess?: (data: TResult, vars: TVars) => void;
123
+ onError?: (error: Error, vars: TVars) => void;
124
+ onSettled?: (data: TResult | undefined, error: Error | null, vars: TVars) => void;
125
+ /** Per-event progress callback (in addition to the React state mirror). */
126
+ onProgress?: (progress: UploadProgress, vars: TVars) => void;
127
+ }
128
+ interface UseUploadWithProgressResult<TResult, TVars> {
129
+ /**
130
+ * Trigger an upload. Returns a Promise that resolves with the parsed
131
+ * response body or rejects with `ArcApiError` / abort error.
132
+ */
133
+ upload: (vars: TVars) => Promise<TResult>;
134
+ /** Latest progress snapshot. Null when no upload is active. */
135
+ progress: UploadProgress | null;
136
+ /** True from `upload()` start until resolve/reject/cancel. */
137
+ isUploading: boolean;
138
+ /** Alias for `isUploading` — matches TanStack mutation naming. */
139
+ isPending: boolean;
140
+ /** Last-resolved data (kept across calls until `reset()` or next upload). */
141
+ data: TResult | null;
142
+ /** Last error (cleared on the next upload or `reset()`). */
143
+ error: Error | null;
144
+ /** Whether the last upload succeeded. */
145
+ isSuccess: boolean;
146
+ /** Whether the last upload errored. */
147
+ isError: boolean;
148
+ /** Abort the in-flight upload. No-op when none is active. */
149
+ cancel: () => void;
150
+ /** Clear progress / data / error so the hook reads as "idle" again. */
151
+ reset: () => void;
152
+ }
153
+ /**
154
+ * React hook that wraps {@link uploadWithProgress} with TanStack-Query-style
155
+ * mutation ergonomics. Progress lives in React state — every progress tick
156
+ * re-renders the consumer so binding `progress.percent` to a `<ProgressBar>`
157
+ * "just works" with no extra plumbing.
158
+ *
159
+ * @example
160
+ * const { upload, progress, isUploading, cancel, error } = useUploadWithProgress<
161
+ * { url: string }[],
162
+ * { files: File[]; folder?: string }
163
+ * >({
164
+ * url: '/api/v1/media/upload-multiple',
165
+ * buildFormData: ({ files, folder }) => {
166
+ * const fd = new FormData();
167
+ * if (folder) fd.append('folder', folder);
168
+ * files.forEach((f) => fd.append('files[]', f));
169
+ * return fd;
170
+ * },
171
+ * invalidateQueries: [mediaKeys.lists()],
172
+ * messages: { success: 'Uploaded', error: 'Upload failed' },
173
+ * });
174
+ *
175
+ * <ProgressBar value={progress?.percent ?? 0} />
176
+ * <button onClick={() => upload({ files })}>Upload</button>
177
+ * {isUploading && <button onClick={cancel}>Cancel</button>}
178
+ */
179
+ declare function useUploadWithProgress<TResult = unknown, TVars = unknown>(options: UseUploadWithProgressOptions<TResult, TVars>): UseUploadWithProgressResult<TResult, TVars>;
180
+ //#endregion
181
+ export { UploadProgress, UploadWithProgressOptions, UseUploadWithProgressOptions, UseUploadWithProgressResult, uploadWithProgress, useUploadWithProgress };