-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi.js
More file actions
163 lines (146 loc) · 5.59 KB
/
Copy pathapi.js
File metadata and controls
163 lines (146 loc) · 5.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/* api.js — unified fetch wrapper with AbortController & error normalization.
*
* Goals:
* - one place to set headers, parse JSON, handle non-2xx, and surface
* a normalized error shape: { ok:false, status, code, message, detail }
* - per-key AbortController registry so callers can cancel previous
* in-flight requests (e.g. typing into ticker input) without leaking
* globals like window._ocAbort.
*
* Usage:
* const data = await api.get('/api/regime/current?ticker=AAPL');
* const data = await api.post('/api/expiry_probability', { body: payload, key: 'payoff_ratio' });
* api.abort('payoff_ratio'); // cancel the previous 'payoff_ratio' request
*
* All thrown errors are ApiError instances (extending Error) with the
* normalized fields above. AbortError is preserved (err.name === 'AbortError')
* so callers can distinguish user-cancellation from real failures.
*/
(function (root) {
'use strict';
const _controllers = new Map(); // key -> AbortController
class ApiError extends Error {
constructor({ status, code, message, detail }) {
super(message || 'Request failed');
this.name = 'ApiError';
this.ok = false;
this.status = status ?? 0;
this.code = code || 'unknown_error';
this.detail = detail ?? null;
}
}
function _registerController(key) {
if (!key) return new AbortController();
// cancel any prior request bound to the same key
const prev = _controllers.get(key);
if (prev) {
try { prev.abort(); } catch (_) { /* ignore */ }
}
const ctrl = new AbortController();
_controllers.set(key, ctrl);
return ctrl;
}
function _clearController(key, ctrl) {
if (!key) return;
if (_controllers.get(key) === ctrl) _controllers.delete(key);
}
function abort(key) {
const ctrl = _controllers.get(key);
if (ctrl) {
try { ctrl.abort(); } catch (_) { /* ignore */ }
_controllers.delete(key);
}
}
async function _normalizeErrorBody(resp) {
const ct = resp.headers.get('content-type') || '';
try {
if (ct.includes('application/json')) {
const j = await resp.json();
return {
code: j.code || j.error_code || `http_${resp.status}`,
message: j.error || j.message || resp.statusText || 'Request failed',
detail: j,
};
}
const text = await resp.text();
return {
code: `http_${resp.status}`,
message: resp.statusText || 'Request failed',
detail: text ? text.slice(0, 500) : null,
};
} catch (_) {
return { code: `http_${resp.status}`, message: resp.statusText || 'Request failed', detail: null };
}
}
async function request(url, opts = {}) {
const {
method = 'GET',
body,
headers = {},
key,
signal,
timeoutMs,
parse = 'auto', // 'auto' | 'json' | 'text' | 'response'
credentials = 'same-origin',
} = opts;
const ctrl = signal ? null : _registerController(key);
const finalSignal = signal || (ctrl ? ctrl.signal : undefined);
let timeoutId = null;
if (timeoutMs && ctrl) {
timeoutId = setTimeout(() => {
try { ctrl.abort(); } catch (_) { /* ignore */ }
}, timeoutMs);
}
const finalHeaders = { Accept: 'application/json', ...headers };
let finalBody = body;
if (body && typeof body === 'object' && !(body instanceof FormData) && !(body instanceof Blob)) {
finalHeaders['Content-Type'] = finalHeaders['Content-Type'] || 'application/json';
finalBody = JSON.stringify(body);
}
let resp;
try {
resp = await fetch(url, {
method,
headers: finalHeaders,
body: finalBody,
signal: finalSignal,
credentials,
});
} catch (err) {
if (timeoutId) clearTimeout(timeoutId);
_clearController(key, ctrl);
if (err && err.name === 'AbortError') throw err;
throw new ApiError({
status: 0,
code: 'network_error',
message: err && err.message ? err.message : 'Network error',
detail: null,
});
}
if (timeoutId) clearTimeout(timeoutId);
_clearController(key, ctrl);
if (!resp.ok) {
const norm = await _normalizeErrorBody(resp);
throw new ApiError({ status: resp.status, ...norm });
}
if (parse === 'response') return resp;
if (parse === 'text') return resp.text();
if (parse === 'json') return resp.json();
// auto
const ct = resp.headers.get('content-type') || '';
if (ct.includes('application/json')) return resp.json();
if (ct.startsWith('text/')) return resp.text();
return resp;
}
const api = {
request,
abort,
ApiError,
get: (url, opts = {}) => request(url, { ...opts, method: 'GET' }),
post: (url, opts = {}) => request(url, { ...opts, method: 'POST' }),
put: (url, opts = {}) => request(url, { ...opts, method: 'PUT' }),
delete: (url, opts = {}) => request(url, { ...opts, method: 'DELETE' }),
};
root.api = api;
root.ApiError = ApiError;
})(window);