@larc-iu/plaid-client 0.2.0-alpha.3 → 0.2.0-alpha.5
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 +542 -105
- package/package.json +1 -1
- package/src/http.js +70 -5
- package/src/index.js +920 -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.
|
|
@@ -91,6 +132,13 @@ export function timeoutSignal(timeout) {
|
|
|
91
132
|
* formData - If true, body is FormData; skip Content-Type header
|
|
92
133
|
* queryParams - Object of query param key/values to append
|
|
93
134
|
* noBatch - If true, throw when in batch mode
|
|
135
|
+
* bypassBatch - If true, go over the wire even while a batch is open.
|
|
136
|
+
* For READS only: a batch is a write transaction, its
|
|
137
|
+
* ops return no value to their caller until submit, and
|
|
138
|
+
* the sub-request runs against the tx Connection rather
|
|
139
|
+
* than the pool. A read swallowed by an ambient batch is
|
|
140
|
+
* therefore useless to the caller AND can fail the whole
|
|
141
|
+
* batch (see `query`).
|
|
94
142
|
* skipResponseTransform - Return raw parsed JSON (no transformResponse)
|
|
95
143
|
* noAuth - Skip Authorization header
|
|
96
144
|
* binaryResponse - Return arrayBuffer instead of JSON/text
|
|
@@ -105,6 +153,7 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
105
153
|
formData,
|
|
106
154
|
queryParams,
|
|
107
155
|
noBatch,
|
|
156
|
+
bypassBatch,
|
|
108
157
|
skipResponseTransform,
|
|
109
158
|
noAuth,
|
|
110
159
|
binaryResponse,
|
|
@@ -178,8 +227,11 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
178
227
|
group.written = true;
|
|
179
228
|
}
|
|
180
229
|
|
|
181
|
-
// Batch mode
|
|
182
|
-
|
|
230
|
+
// Batch mode. A `bypassBatch` read is deliberately NOT queued: it belongs to
|
|
231
|
+
// whoever called it, not to whatever batch happens to be open on this shared
|
|
232
|
+
// client (a document reconcile, an import), and it reads the pre-batch state
|
|
233
|
+
// exactly as it would have if the batch were not running.
|
|
234
|
+
if (client.isBatching && !bypassBatch) {
|
|
183
235
|
if (noBatch) {
|
|
184
236
|
throw new Error(`This endpoint cannot be used in batch mode: ${path}`);
|
|
185
237
|
}
|
|
@@ -207,11 +259,24 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
207
259
|
if (requestBody !== undefined) {
|
|
208
260
|
fetchOptions.body = formData ? requestBody : JSON.stringify(requestBody);
|
|
209
261
|
}
|
|
210
|
-
|
|
211
|
-
|
|
262
|
+
|
|
263
|
+
// A fresh AbortSignal.timeout per attempt: the signal fires once and stays
|
|
264
|
+
// aborted, so reusing it would make every retry fail instantly.
|
|
265
|
+
const attemptOptions = () => {
|
|
266
|
+
const signal = timeoutSignal(timeout !== undefined ? timeout : client.timeout);
|
|
267
|
+
return signal ? { ...fetchOptions, signal } : fetchOptions;
|
|
268
|
+
};
|
|
212
269
|
|
|
213
270
|
try {
|
|
214
|
-
const response = await
|
|
271
|
+
const response = await retryWhileBusy(async () => {
|
|
272
|
+
const res = await fetch(url, attemptOptions());
|
|
273
|
+
// Surface a 503 as a throw so retryWhileBusy can see it; other failures
|
|
274
|
+
// are re-thrown from here and handled by the caller below.
|
|
275
|
+
if (res.status === 503) {
|
|
276
|
+
throw makeHttpError(res, await parseErrorBody(res), url, method);
|
|
277
|
+
}
|
|
278
|
+
return res;
|
|
279
|
+
});
|
|
215
280
|
|
|
216
281
|
if (!response.ok) {
|
|
217
282
|
const error = makeHttpError(response, await parseErrorBody(response), url, method);
|