@classytic/arc-next 0.4.0 → 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/README.md +272 -616
- package/dist/api.d.ts +64 -127
- package/dist/api.js +55 -121
- package/dist/cache.d.ts +108 -0
- package/dist/cache.js +197 -0
- package/dist/client.d.ts +340 -8
- package/dist/client.js +427 -22
- package/dist/hooks.d.ts +96 -20
- package/dist/hooks.js +247 -115
- package/dist/mutation.d.ts +18 -4
- package/dist/mutation.js +20 -4
- package/dist/prefetch.d.ts +37 -3
- package/dist/prefetch.js +56 -17
- package/dist/presets/bulk.d.ts +42 -0
- package/dist/presets/bulk.js +50 -0
- package/dist/presets/search.d.ts +55 -0
- package/dist/presets/search.js +60 -0
- package/dist/presets/slug.d.ts +28 -0
- package/dist/presets/slug.js +27 -0
- package/dist/presets/soft-delete.d.ts +33 -0
- package/dist/presets/soft-delete.js +45 -0
- package/dist/presets/tree.d.ts +31 -0
- package/dist/presets/tree.js +47 -0
- package/dist/query.d.ts +86 -64
- package/dist/query.js +69 -151
- package/dist/sse.d.ts +116 -30
- package/dist/sse.js +179 -102
- package/dist/upload.d.ts +181 -0
- package/dist/upload.js +346 -0
- package/dist/ws.d.ts +167 -0
- package/dist/ws.js +274 -0
- package/package.json +37 -1
package/dist/sse.js
CHANGED
|
@@ -1,144 +1,221 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { buildStreamUrl, getAuthMode } from "./client.js";
|
|
4
4
|
import { useQueryClient } from "@tanstack/react-query";
|
|
5
|
-
import {
|
|
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`
|
|
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: '
|
|
18
|
-
* invalidateQueries: [
|
|
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,
|
|
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
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const params = new URLSearchParams();
|
|
44
|
-
const patterns = patternsRef.current;
|
|
45
|
-
if (patterns.length > 0) params.set("patterns", patterns.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 = `${basePath}/${resource}/events/stream`;
|
|
50
|
-
return qs ? `${base}?${qs}` : base;
|
|
51
|
-
}, [
|
|
52
|
-
url,
|
|
53
|
-
resource,
|
|
54
|
-
basePath
|
|
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
|
-
connect();
|
|
114
|
-
}, [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]);
|
|
115
166
|
useEffect(() => {
|
|
116
167
|
if (!enabled) {
|
|
117
|
-
close();
|
|
168
|
+
handleRef.current?.close();
|
|
169
|
+
handleRef.current = null;
|
|
118
170
|
return;
|
|
119
171
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 });
|
|
127
190
|
}
|
|
191
|
+
});
|
|
192
|
+
handleRef.current = handle;
|
|
193
|
+
return () => {
|
|
194
|
+
handle.close();
|
|
195
|
+
handleRef.current = null;
|
|
128
196
|
};
|
|
129
197
|
}, [
|
|
130
198
|
enabled,
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
133
210
|
]);
|
|
134
211
|
return {
|
|
135
212
|
isConnected,
|
|
136
213
|
lastEvent,
|
|
137
214
|
eventCount,
|
|
138
|
-
close,
|
|
139
|
-
reconnect
|
|
215
|
+
close: () => handleRef.current?.close(),
|
|
216
|
+
reconnect: () => handleRef.current?.reconnect()
|
|
140
217
|
};
|
|
141
218
|
}
|
|
142
219
|
|
|
143
220
|
//#endregion
|
|
144
|
-
export { useEventStream };
|
|
221
|
+
export { buildSseUrl, subscribeToEvents, useEventStream };
|
package/dist/upload.d.ts
ADDED
|
@@ -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 };
|