@larc-iu/plaid-client 0.2.0-alpha.3 → 0.2.0-alpha.4
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/index.d.ts +493 -105
- package/package.json +1 -1
- package/src/http.js +57 -3
- package/src/index.js +800 -341
- package/src/provenance.js +11 -0
- package/src/serviceSchema.js +49 -31
- package/src/services.js +60 -8
- package/src/sse.js +31 -0
package/package.json
CHANGED
package/src/http.js
CHANGED
|
@@ -5,6 +5,12 @@ import { transformRequest, transformResponse } from './transforms.js';
|
|
|
5
5
|
// also bounds media up/downloads — bump it (or disable) for very large files.
|
|
6
6
|
export const DEFAULT_TIMEOUT_MS = 30000;
|
|
7
7
|
|
|
8
|
+
// Batch submissions get their own, longer budget. A batch runs as ONE server
|
|
9
|
+
// transaction holding the single SQLite write lock for its whole duration, and
|
|
10
|
+
// aborting the HTTP request does not cancel it — so a short timeout buys
|
|
11
|
+
// nothing and costs a retry stacked on a write that is still running.
|
|
12
|
+
export const DEFAULT_BATCH_TIMEOUT_MS = 180000;
|
|
13
|
+
|
|
8
14
|
/**
|
|
9
15
|
* Extract and update document versions from response headers and body.
|
|
10
16
|
*/
|
|
@@ -71,6 +77,41 @@ export function makeNetworkError(originalError, url, method) {
|
|
|
71
77
|
return error;
|
|
72
78
|
}
|
|
73
79
|
|
|
80
|
+
// Retry budget for 503 "Database busy" responses. Plaid serializes writers on
|
|
81
|
+
// a single SQLite write lock; a writer that can't get it within the server's
|
|
82
|
+
// busy_timeout is refused with 503. That refusal is definitive — the
|
|
83
|
+
// transaction never opened, or was rolled back whole — so repeating the
|
|
84
|
+
// request is safe, including for a batch (which is all-or-nothing by
|
|
85
|
+
// construction). Retrying here is what lets a long import ride out someone
|
|
86
|
+
// else's slow write instead of dying partway through.
|
|
87
|
+
export const BUSY_RETRIES = 4;
|
|
88
|
+
export const BUSY_BACKOFF_MS = 250;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Run `attempt`, retrying while it rejects with a 503. Exponential backoff
|
|
92
|
+
* with full jitter, so two clients that collide don't march in lockstep and
|
|
93
|
+
* collide again on every retry.
|
|
94
|
+
*
|
|
95
|
+
* `attempt` must perform the whole request, not just await a prepared one:
|
|
96
|
+
* a fetch body and an AbortSignal.timeout are both single-use.
|
|
97
|
+
*/
|
|
98
|
+
export async function retryWhileBusy(attempt, {
|
|
99
|
+
retries = BUSY_RETRIES,
|
|
100
|
+
baseDelayMs = BUSY_BACKOFF_MS,
|
|
101
|
+
onRetry,
|
|
102
|
+
} = {}) {
|
|
103
|
+
for (let i = 0; ; i += 1) {
|
|
104
|
+
try {
|
|
105
|
+
return await attempt();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error?.status !== 503 || i >= retries) throw error;
|
|
108
|
+
const delay = Math.round(baseDelayMs * 2 ** i * (0.5 + Math.random()));
|
|
109
|
+
onRetry?.({ attempt: i + 1, retries, delay, error });
|
|
110
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
74
115
|
/**
|
|
75
116
|
* Build a fetch AbortSignal that fires after `timeout` ms, or undefined when
|
|
76
117
|
* timeouts are disabled / unsupported.
|
|
@@ -207,11 +248,24 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
207
248
|
if (requestBody !== undefined) {
|
|
208
249
|
fetchOptions.body = formData ? requestBody : JSON.stringify(requestBody);
|
|
209
250
|
}
|
|
210
|
-
|
|
211
|
-
|
|
251
|
+
|
|
252
|
+
// A fresh AbortSignal.timeout per attempt: the signal fires once and stays
|
|
253
|
+
// aborted, so reusing it would make every retry fail instantly.
|
|
254
|
+
const attemptOptions = () => {
|
|
255
|
+
const signal = timeoutSignal(timeout !== undefined ? timeout : client.timeout);
|
|
256
|
+
return signal ? { ...fetchOptions, signal } : fetchOptions;
|
|
257
|
+
};
|
|
212
258
|
|
|
213
259
|
try {
|
|
214
|
-
const response = await
|
|
260
|
+
const response = await retryWhileBusy(async () => {
|
|
261
|
+
const res = await fetch(url, attemptOptions());
|
|
262
|
+
// Surface a 503 as a throw so retryWhileBusy can see it; other failures
|
|
263
|
+
// are re-thrown from here and handled by the caller below.
|
|
264
|
+
if (res.status === 503) {
|
|
265
|
+
throw makeHttpError(res, await parseErrorBody(res), url, method);
|
|
266
|
+
}
|
|
267
|
+
return res;
|
|
268
|
+
});
|
|
215
269
|
|
|
216
270
|
if (!response.ok) {
|
|
217
271
|
const error = makeHttpError(response, await parseErrorBody(response), url, method);
|