@beast01/tcurl 1.0.1 → 1.0.2
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 +4 -1
- package/dist/app.js +31 -5
- package/dist/components/Help.js +1 -0
- package/dist/http/client.js +24 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -104,7 +104,10 @@ Fields open in **INSERT** mode so you can type right away; press `Esc` for
|
|
|
104
104
|
- **Auth** — Bearer token or HTTP Basic (username / password).
|
|
105
105
|
- **Redirects** — follow (with method downgrade on 301/302/303) or manual.
|
|
106
106
|
- **TLS control** — toggle certificate verification for self-signed hosts.
|
|
107
|
-
- **Timeouts** — per-request
|
|
107
|
+
- **Timeouts & cancellation** — per-request timeout in milliseconds (default
|
|
108
|
+
30 000; set `0` to disable). While a request is in flight, press `Esc` (or
|
|
109
|
+
`q`) to abort it immediately — even with no timeout set — and `Ctrl+C`
|
|
110
|
+
always quits, so a hung server can never lock you up.
|
|
108
111
|
- **Pretty responses** — JSON is auto-formatted; scrollable body & headers
|
|
109
112
|
with status, timing, and size.
|
|
110
113
|
- **Persistence** — requests are stored as JSON and reused across sessions.
|
package/dist/app.js
CHANGED
|
@@ -39,6 +39,9 @@ export function App({ initialStore, exitState, }) {
|
|
|
39
39
|
const [loading, setLoading] = useState(false);
|
|
40
40
|
const [flash, setFlash] = useState(null);
|
|
41
41
|
const pendingG = useRef(false);
|
|
42
|
+
// In-flight request: controller to abort it, token to ignore stale results.
|
|
43
|
+
const abortRef = useRef(null);
|
|
44
|
+
const sendToken = useRef(0);
|
|
42
45
|
const theme = getTheme(store.theme);
|
|
43
46
|
const requests = store.requests;
|
|
44
47
|
function persist(next) {
|
|
@@ -61,19 +64,41 @@ export function App({ initialStore, exitState, }) {
|
|
|
61
64
|
flashMessage('Set a URL first');
|
|
62
65
|
return;
|
|
63
66
|
}
|
|
67
|
+
const token = ++sendToken.current;
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
abortRef.current = controller;
|
|
64
70
|
setLoading(true);
|
|
65
71
|
setResponse(null);
|
|
66
|
-
const result = await executeRequest(config);
|
|
72
|
+
const result = await executeRequest(config, controller.signal);
|
|
73
|
+
// Ignore results from a request the user cancelled or superseded.
|
|
74
|
+
if (sendToken.current !== token)
|
|
75
|
+
return;
|
|
76
|
+
abortRef.current = null;
|
|
67
77
|
setResponse(result);
|
|
68
78
|
setLoading(false);
|
|
69
79
|
setMode('response');
|
|
70
80
|
}
|
|
71
|
-
|
|
81
|
+
function cancelSend() {
|
|
82
|
+
abortRef.current?.abort();
|
|
83
|
+
abortRef.current = null;
|
|
84
|
+
sendToken.current++; // invalidate the pending result
|
|
85
|
+
setLoading(false);
|
|
86
|
+
flashMessage('Request cancelled');
|
|
87
|
+
}
|
|
88
|
+
// Ctrl+C always quits, from any mode (aborting any in-flight request).
|
|
72
89
|
useInput((input, key) => {
|
|
73
90
|
if (key.ctrl && input === 'c') {
|
|
91
|
+
abortRef.current?.abort();
|
|
74
92
|
exit();
|
|
75
|
-
return;
|
|
76
93
|
}
|
|
94
|
+
});
|
|
95
|
+
// While a request is in flight, Esc (or q) cancels it.
|
|
96
|
+
useInput((input, key) => {
|
|
97
|
+
if (key.escape || input === 'q')
|
|
98
|
+
cancelSend();
|
|
99
|
+
}, { isActive: loading });
|
|
100
|
+
// Global keys — only in list mode and when idle.
|
|
101
|
+
useInput((input, key) => {
|
|
77
102
|
if (loading)
|
|
78
103
|
return;
|
|
79
104
|
if (input === '?') {
|
|
@@ -178,8 +203,9 @@ export function App({ initialStore, exitState, }) {
|
|
|
178
203
|
: [];
|
|
179
204
|
return (React.createElement(Box, { flexDirection: "column", width: cols, minHeight: rows - 1 },
|
|
180
205
|
React.createElement(Header, { theme: theme, subtitle: mode === 'list' ? `${requests.length} saved · ` : '' }),
|
|
181
|
-
React.createElement(Box, { flexGrow: 1, flexDirection: "column", marginTop: 1 }, loading ? (React.createElement(Box, { flexGrow: 1, borderStyle: "round", borderColor: theme.border, paddingX: 1, alignItems: "center", justifyContent: "center" },
|
|
182
|
-
React.createElement(Spinner, { theme: theme, label: "Sending request\u2026" })
|
|
206
|
+
React.createElement(Box, { flexGrow: 1, flexDirection: "column", marginTop: 1 }, loading ? (React.createElement(Box, { flexGrow: 1, borderStyle: "round", borderColor: theme.border, paddingX: 1, alignItems: "center", justifyContent: "center", flexDirection: "column" },
|
|
207
|
+
React.createElement(Spinner, { theme: theme, label: "Sending request\u2026" }),
|
|
208
|
+
React.createElement(Text, { color: theme.muted }, "Esc or q to cancel"))) : mode === 'help' ? (React.createElement(Help, { theme: theme, onClose: () => setMode('list') })) : mode === 'editor' && working ? (React.createElement(RequestEditor, { theme: theme, request: working, isActive: mode === 'editor', onChange: onEditorChange, onSend: onEditorSend, onBack: onEditorBack })) : mode === 'response' && response ? (React.createElement(ResponseViewer, { theme: theme, response: response, isActive: mode === 'response', onBack: () => setMode('list'), width: cols, height: rows })) : (React.createElement(RequestList, { theme: theme, requests: requests, selectedIndex: selected }))),
|
|
183
209
|
React.createElement(Box, { justifyContent: "space-between" },
|
|
184
210
|
React.createElement(StatusBar, { theme: theme, hints: statusHints }),
|
|
185
211
|
flash ? (React.createElement(Box, { paddingX: 1 },
|
package/dist/components/Help.js
CHANGED
|
@@ -9,6 +9,7 @@ const SECTIONS = [
|
|
|
9
9
|
['n', 'New request'],
|
|
10
10
|
['Enter / e', 'Edit request'],
|
|
11
11
|
['s', 'Send request'],
|
|
12
|
+
['Esc / q', 'Cancel a request while it is sending'],
|
|
12
13
|
['d', 'Delete request'],
|
|
13
14
|
['y', 'Copy as curl (prints on exit)'],
|
|
14
15
|
['t', 'Cycle theme (gruvbox / ember)'],
|
package/dist/http/client.js
CHANGED
|
@@ -141,14 +141,28 @@ function doRequest(opts, redirectsLeft) {
|
|
|
141
141
|
req.end();
|
|
142
142
|
});
|
|
143
143
|
}
|
|
144
|
-
/**
|
|
145
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Execute a request config and return a normalized result (never throws).
|
|
146
|
+
*
|
|
147
|
+
* The optional `externalSignal` lets the caller (the UI) cancel an in-flight
|
|
148
|
+
* request; when it fires the result carries `error: "Request cancelled"`.
|
|
149
|
+
* A per-request timeout aborts independently.
|
|
150
|
+
*/
|
|
151
|
+
export async function executeRequest(config, externalSignal) {
|
|
146
152
|
const start = Date.now();
|
|
147
153
|
const controller = new AbortController();
|
|
148
154
|
let timer;
|
|
149
155
|
if (config.timeoutMs > 0) {
|
|
150
156
|
timer = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
151
157
|
}
|
|
158
|
+
// Bridge an external (user) cancellation into our controller.
|
|
159
|
+
const onExternalAbort = () => controller.abort();
|
|
160
|
+
if (externalSignal) {
|
|
161
|
+
if (externalSignal.aborted)
|
|
162
|
+
controller.abort();
|
|
163
|
+
else
|
|
164
|
+
externalSignal.addEventListener('abort', onExternalAbort, { once: true });
|
|
165
|
+
}
|
|
152
166
|
try {
|
|
153
167
|
const url = buildUrl(config);
|
|
154
168
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
@@ -168,6 +182,7 @@ export async function executeRequest(config) {
|
|
|
168
182
|
}
|
|
169
183
|
catch (err) {
|
|
170
184
|
const aborted = err?.name === 'AbortError' || controller.signal.aborted;
|
|
185
|
+
const cancelled = externalSignal?.aborted ?? false;
|
|
171
186
|
return {
|
|
172
187
|
ok: false,
|
|
173
188
|
status: 0,
|
|
@@ -178,13 +193,17 @@ export async function executeRequest(config) {
|
|
|
178
193
|
durationMs: Date.now() - start,
|
|
179
194
|
sizeBytes: 0,
|
|
180
195
|
finalUrl: config.url,
|
|
181
|
-
error:
|
|
182
|
-
?
|
|
183
|
-
:
|
|
196
|
+
error: cancelled
|
|
197
|
+
? 'Request cancelled'
|
|
198
|
+
: aborted
|
|
199
|
+
? `Request timed out after ${config.timeoutMs}ms`
|
|
200
|
+
: String(err?.message ?? err),
|
|
184
201
|
};
|
|
185
202
|
}
|
|
186
203
|
finally {
|
|
187
204
|
if (timer)
|
|
188
205
|
clearTimeout(timer);
|
|
206
|
+
if (externalSignal)
|
|
207
|
+
externalSignal.removeEventListener('abort', onExternalAbort);
|
|
189
208
|
}
|
|
190
209
|
}
|