@larc-iu/plaid-client 0.2.0-alpha.22 → 0.2.0-alpha.26
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 +39 -0
- package/package.json +11 -5
- package/src/batchRef.d.ts +30 -0
- package/src/codepoint.d.ts +94 -0
- package/src/codepoint.js +131 -1
- package/src/constraints.d.ts +77 -0
- package/src/constraints.js +26 -1
- package/src/created.d.ts +2 -0
- package/src/documentLock.d.ts +158 -0
- package/src/documentLock.js +29 -4
- package/src/events.d.ts +60 -0
- package/src/events.js +7 -3
- package/src/http.d.ts +212 -0
- package/src/http.js +7 -0
- package/src/ids.d.ts +26 -0
- package/src/ids.js +42 -0
- package/src/index.d.ts +3814 -0
- package/src/index.js +962 -704
- package/src/metadataOps.d.ts +15 -0
- package/src/metadataOps.js +13 -8
- package/src/pagination.d.ts +49 -0
- package/src/pagination.js +2 -19
- package/src/provenance.d.ts +200 -0
- package/src/provenance.js +34 -6
- package/src/replayed.d.ts +3 -0
- package/src/replayed.js +5 -1
- package/src/requester.d.ts +60 -0
- package/src/requester.js +21 -3
- package/src/roles.d.ts +65 -0
- package/src/roles.js +3 -2
- package/src/serviceSchema.d.ts +331 -0
- package/src/serviceSchema.js +75 -9
- package/src/services.d.ts +302 -0
- package/src/services.js +67 -10
- package/src/sse.d.ts +30 -0
- package/src/sse.js +11 -0
- package/src/textEdits.d.ts +59 -0
- package/src/textEdits.js +26 -1
- package/src/transforms.d.ts +25 -0
- package/index.d.ts +0 -2015
package/src/documentLock.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isUnknownOutcome, retryUnknown } from "./http.js";
|
|
2
|
+
import { uuidv4 } from "./ids.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Keeping a document lock alive for as long as a `locked()` block runs.
|
|
@@ -38,10 +39,18 @@ import { isUnknownOutcome, retryUnknown } from "./http.js";
|
|
|
38
39
|
* landed while the lock was gone.
|
|
39
40
|
*/
|
|
40
41
|
export class DocumentLockLost extends Error {
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} message
|
|
44
|
+
* @param {string} documentId
|
|
45
|
+
* @param {unknown} [cause]
|
|
46
|
+
*/
|
|
41
47
|
constructor(message, documentId, cause) {
|
|
42
48
|
super(message);
|
|
49
|
+
/** @type {"DocumentLockLost"} */
|
|
43
50
|
this.name = "DocumentLockLost";
|
|
51
|
+
/** @type {string} */
|
|
44
52
|
this.documentId = documentId;
|
|
53
|
+
/** @type {unknown} */
|
|
45
54
|
this.cause = cause;
|
|
46
55
|
}
|
|
47
56
|
}
|
|
@@ -100,7 +109,10 @@ export class LockKeeper {
|
|
|
100
109
|
this._stopped = false;
|
|
101
110
|
this._wake = null;
|
|
102
111
|
this._running = null;
|
|
103
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* The `DocumentLockLost` this keeper raised, or null.
|
|
114
|
+
* @type {DocumentLockLost | null}
|
|
115
|
+
*/
|
|
104
116
|
this.lost = null;
|
|
105
117
|
}
|
|
106
118
|
|
|
@@ -192,18 +204,31 @@ export class LockKeeper {
|
|
|
192
204
|
* between steps of work that has not written anything yet.
|
|
193
205
|
*/
|
|
194
206
|
export class DocumentLock {
|
|
207
|
+
/**
|
|
208
|
+
* @param {string} documentId
|
|
209
|
+
* @param {LockKeeper | null} keeper
|
|
210
|
+
* @param {string | null} [lockId]
|
|
211
|
+
*/
|
|
195
212
|
constructor(documentId, keeper, lockId = null) {
|
|
213
|
+
/** @type {string} */
|
|
196
214
|
this.documentId = documentId;
|
|
197
|
-
/**
|
|
215
|
+
/**
|
|
216
|
+
* The holder id the acquire answered with.
|
|
217
|
+
* @type {string | null}
|
|
218
|
+
*/
|
|
198
219
|
this.lockId = lockId;
|
|
199
220
|
this._keeper = keeper;
|
|
200
221
|
}
|
|
201
222
|
|
|
202
|
-
/**
|
|
223
|
+
/**
|
|
224
|
+
* The `DocumentLockLost` if the lock lapsed, else null.
|
|
225
|
+
* @returns {DocumentLockLost | null}
|
|
226
|
+
*/
|
|
203
227
|
get lost() {
|
|
204
228
|
return this._keeper ? this._keeper.lost : null;
|
|
205
229
|
}
|
|
206
230
|
|
|
231
|
+
/** Throw if the lock lapsed. Call it between steps that have not written. */
|
|
207
232
|
raiseIfLost() {
|
|
208
233
|
if (this.lost) throw this.lost;
|
|
209
234
|
}
|
|
@@ -216,7 +241,7 @@ export class DocumentLock {
|
|
|
216
241
|
export const LOCK_ACQUIRE_RETRY_MS = 500;
|
|
217
242
|
|
|
218
243
|
function mintLockId() {
|
|
219
|
-
return
|
|
244
|
+
return uuidv4();
|
|
220
245
|
}
|
|
221
246
|
|
|
222
247
|
/**
|
package/src/events.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** The page's recorder for `client`'s server. */
|
|
2
|
+
export function recorderFor(client: any): any;
|
|
3
|
+
/** Forget every recorder (tests). */
|
|
4
|
+
export function resetEventRecorders(): void;
|
|
5
|
+
/** Every event type the server accepts. */
|
|
6
|
+
export const EVENT_TYPES: readonly string[];
|
|
7
|
+
export const FLUSH_INTERVAL_MS: 10000;
|
|
8
|
+
export const FLUSH_AT: 50;
|
|
9
|
+
export const FLAG_TTL_MS: 10000;
|
|
10
|
+
export const FLAG_OFF_TTL_MS: 60000;
|
|
11
|
+
export class EventRecorder {
|
|
12
|
+
/**
|
|
13
|
+
* @param {object} [opts]
|
|
14
|
+
* @param {function} [opts.now] - Clock, for tests
|
|
15
|
+
* @param {object} [opts.window] - Where `pagehide` fires (default globalThis)
|
|
16
|
+
*/
|
|
17
|
+
constructor({ now, window }?: {
|
|
18
|
+
now?: Function;
|
|
19
|
+
window?: object;
|
|
20
|
+
});
|
|
21
|
+
client: any;
|
|
22
|
+
buffer: any[];
|
|
23
|
+
timer: number;
|
|
24
|
+
now: Function;
|
|
25
|
+
flags: Map<any, any>;
|
|
26
|
+
seen: Set<any>;
|
|
27
|
+
_onPageHide: () => void;
|
|
28
|
+
window: any;
|
|
29
|
+
/** Tell the recorder a project's switch, e.g. right after changing it. */
|
|
30
|
+
setEnabled(projectId: any, on: any): void;
|
|
31
|
+
/**
|
|
32
|
+
* Record one event. Returns true when it was buffered, false when it was
|
|
33
|
+
* not (switch off, a repeat "shown", an unknown type, no project).
|
|
34
|
+
* @param {any} client
|
|
35
|
+
* @param {string} type
|
|
36
|
+
* @param {{projectId?: string, documentId?: string, targetId?: string, data?: Record<string, any>}} [fields]
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
record(client: any, type: string, { projectId, documentId, targetId, data }?: {
|
|
40
|
+
projectId?: string;
|
|
41
|
+
documentId?: string;
|
|
42
|
+
targetId?: string;
|
|
43
|
+
data?: Record<string, any>;
|
|
44
|
+
}): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Send what is buffered for every project whose switch is known to be on,
|
|
47
|
+
* drop what is buffered for one known to be off, and keep the rest until
|
|
48
|
+
* the switch is known. With `keepalive` (the page is going away) nothing is
|
|
49
|
+
* kept.
|
|
50
|
+
*/
|
|
51
|
+
flush({ keepalive }?: {
|
|
52
|
+
keepalive?: boolean;
|
|
53
|
+
}): void;
|
|
54
|
+
/** Stop listening for `pagehide` and forget everything (tests). */
|
|
55
|
+
close(): void;
|
|
56
|
+
_schedule(): void;
|
|
57
|
+
_drop(projectId: any): void;
|
|
58
|
+
_flag(projectId: any): any;
|
|
59
|
+
_send(projectId: any, events: any, keepalive: any): void;
|
|
60
|
+
}
|
package/src/events.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { transformRequest } from "./transforms.js";
|
|
2
2
|
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
-
// Client events: the opt-in research telemetry of a project (
|
|
5
|
-
//
|
|
4
|
+
// Client events: the opt-in research telemetry of a project (defined in
|
|
5
|
+
// plaid-core sql/client_event.clj).
|
|
6
6
|
//
|
|
7
7
|
// `client.events.record(type, fields)` is a fire-and-forget recorder. It
|
|
8
8
|
// buffers events and sends them every FLUSH_INTERVAL_MS or FLUSH_AT events,
|
|
@@ -51,7 +51,7 @@ const PER_REQUEST = FLUSH_AT;
|
|
|
51
51
|
// A suggested or written value is a gloss, not prose. A long one is cut, in
|
|
52
52
|
// code points as the server counts them, so no character is split in two.
|
|
53
53
|
// The server refuses a value past this length, and a key its type does not
|
|
54
|
-
// take (core
|
|
54
|
+
// take (plaid-core sql/client_event.clj).
|
|
55
55
|
const MAX_VALUE_CHARS = 200;
|
|
56
56
|
|
|
57
57
|
const clip = (v) => {
|
|
@@ -92,6 +92,10 @@ export class EventRecorder {
|
|
|
92
92
|
/**
|
|
93
93
|
* Record one event. Returns true when it was buffered, false when it was
|
|
94
94
|
* not (switch off, a repeat "shown", an unknown type, no project).
|
|
95
|
+
* @param {any} client
|
|
96
|
+
* @param {string} type
|
|
97
|
+
* @param {{projectId?: string, documentId?: string, targetId?: string, data?: Record<string, any>}} [fields]
|
|
98
|
+
* @returns {boolean}
|
|
95
99
|
*/
|
|
96
100
|
record(client, type, { projectId, documentId, targetId, data } = {}) {
|
|
97
101
|
try {
|
package/src/http.d.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The timeout for a query: the client's own, raised to
|
|
3
|
+
* DEFAULT_QUERY_TIMEOUT_MS when it is shorter. A disabled timeout (0, null)
|
|
4
|
+
* stays disabled.
|
|
5
|
+
*/
|
|
6
|
+
export function queryTimeout(client: any): any;
|
|
7
|
+
/**
|
|
8
|
+
* Note how far the server's clock is from this machine's, from a response's
|
|
9
|
+
* Date header, for `client.serverNow()`. A time the server stamped (an audit
|
|
10
|
+
* entry's `ts`) has to be judged against the server's clock: a browser clock
|
|
11
|
+
* minutes off would misjudge it. A response with no readable Date header
|
|
12
|
+
* leaves the last offset as it was.
|
|
13
|
+
*/
|
|
14
|
+
export function noteServerClock(client: any, headers: any): void;
|
|
15
|
+
/**
|
|
16
|
+
* Extract and update document versions from response headers and body.
|
|
17
|
+
*
|
|
18
|
+
* `historical` marks a read made with `as-of`: its body carries the version
|
|
19
|
+
* the document HAD then, which is not what a strict-mode write must claim
|
|
20
|
+
* next, so the body is ignored and only the header (always the live
|
|
21
|
+
* version) is learned from. Without this, viewing an old state and then
|
|
22
|
+
* writing 409s on a version that stopped being current long ago.
|
|
23
|
+
*/
|
|
24
|
+
export function extractDocumentVersions(client: any, responseHeaders: any, responseBody?: any, { historical }?: {
|
|
25
|
+
historical?: boolean;
|
|
26
|
+
}): void;
|
|
27
|
+
/**
|
|
28
|
+
* `held` with `learned` merged in. A replayed answer only raises a version:
|
|
29
|
+
* it is what the document was right after the first send.
|
|
30
|
+
*/
|
|
31
|
+
export function mergeVersions(held: any, learned: any, replayed?: boolean): any;
|
|
32
|
+
/**
|
|
33
|
+
* Read a failed response's body as parsed JSON, falling back to text.
|
|
34
|
+
*/
|
|
35
|
+
export function parseErrorBody(response: any): Promise<any>;
|
|
36
|
+
/**
|
|
37
|
+
* Create an enriched error from a failed HTTP response.
|
|
38
|
+
*/
|
|
39
|
+
export function makeHttpError(response: any, errorData: any, url: any, method: any): Error;
|
|
40
|
+
/**
|
|
41
|
+
* Create a network error (status 0). Timeout aborts get a clearer message.
|
|
42
|
+
*/
|
|
43
|
+
export function makeNetworkError(originalError: any, url: any, method: any): Error;
|
|
44
|
+
/**
|
|
45
|
+
* Run `attempt`, retrying while it rejects with a 503. Exponential backoff
|
|
46
|
+
* with full jitter, so two clients that collide don't march in lockstep and
|
|
47
|
+
* collide again on every retry.
|
|
48
|
+
*
|
|
49
|
+
* `attempt` must perform the whole request, not just await a prepared one:
|
|
50
|
+
* a fetch body and an AbortSignal.timeout are both single-use.
|
|
51
|
+
* @param {() => Promise<any>} attempt
|
|
52
|
+
* @param {{retries?: number, baseDelayMs?: number, onRetry?: (info: object) => void}} [options]
|
|
53
|
+
*/
|
|
54
|
+
export function retryWhileBusy(attempt: () => Promise<any>, { retries, baseDelayMs, onRetry }?: {
|
|
55
|
+
retries?: number;
|
|
56
|
+
baseDelayMs?: number;
|
|
57
|
+
onRetry?: (info: object) => void;
|
|
58
|
+
}): Promise<any>;
|
|
59
|
+
/**
|
|
60
|
+
* Whether `error` leaves the outcome unknown: no response came (status 0),
|
|
61
|
+
* or a proxy answered for the server (502, 504). The write may have landed.
|
|
62
|
+
*/
|
|
63
|
+
export function isUnknownOutcome(error: any): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Whether a create refused with `status` and body `data` was made by an
|
|
66
|
+
* earlier send of the same work: `409 id-taken` for an id in `minted` (a Set
|
|
67
|
+
* of the ids the work minted for its creates), and not for a row deleted
|
|
68
|
+
* since (there is nothing to go on with). The one rule for a lost answer's
|
|
69
|
+
* create. The Python twin is `minted_taken`.
|
|
70
|
+
*/
|
|
71
|
+
export function mintedTaken(status: any, data: any, minted: any): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Run `attempt`, sending it again after each delay in `delaysMs` while it
|
|
74
|
+
* fails with an unknown outcome. Only for a keyed write, whose resend is
|
|
75
|
+
* answered from the first send when that one landed. Not when the browser
|
|
76
|
+
* says it is offline: then the request never left, and the caller waits for
|
|
77
|
+
* the network instead. Full jitter, as retryWhileBusy.
|
|
78
|
+
* @param {() => Promise<any>} attempt
|
|
79
|
+
* @param {{delaysMs?: number[], onRetry?: (info: object) => void}} [options]
|
|
80
|
+
*/
|
|
81
|
+
export function retryUnknown(attempt: () => Promise<any>, { delaysMs, onRetry }?: {
|
|
82
|
+
delaysMs?: number[];
|
|
83
|
+
onRetry?: (info: object) => void;
|
|
84
|
+
}): Promise<any>;
|
|
85
|
+
/**
|
|
86
|
+
* The key for the next keyed request, and the strict-mode stamp it must
|
|
87
|
+
* claim when an earlier run of its operation already sent it. `joins` says
|
|
88
|
+
* whether the request joins the open logical operation.
|
|
89
|
+
* Returns `{ key, pin, record }`: `pin` is undefined when nothing is pinned,
|
|
90
|
+
* and `record(version)` pins what this request claimed, the first time.
|
|
91
|
+
*/
|
|
92
|
+
export function nextIdempotencyKey(client: any, joins: any): {
|
|
93
|
+
key: string;
|
|
94
|
+
pin: any;
|
|
95
|
+
record: (version: any) => void;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Build a fetch AbortSignal that fires after `timeout` ms, or undefined when
|
|
99
|
+
* timeouts are disabled / unsupported.
|
|
100
|
+
*/
|
|
101
|
+
export function timeoutSignal(timeout: any): AbortSignal;
|
|
102
|
+
/**
|
|
103
|
+
* Send a request through XMLHttpRequest so the caller can watch the bytes go
|
|
104
|
+
* up: fetch cannot report upload progress. Resolves to a Response, so the rest
|
|
105
|
+
* of the pipeline is the same either way. The timeout is a STALL timeout,
|
|
106
|
+
* re-armed by every progress event, so a large file on a slow link is never
|
|
107
|
+
* cut off while it is still moving, only once nothing has moved for `timeout`
|
|
108
|
+
* ms. Rejects the way fetch does: a TypeError for a network failure, an error
|
|
109
|
+
* named TimeoutError for a stall.
|
|
110
|
+
* @param {string} url
|
|
111
|
+
* @param {{method: string, headers: Record<string, string>, body: any}} request
|
|
112
|
+
* @param {{onUploadProgress?: (progress: {loaded: number, total: number | null}) => void, timeout?: number | null}} [options]
|
|
113
|
+
*/
|
|
114
|
+
export function xhrSend(url: string, { method, headers, body }: {
|
|
115
|
+
method: string;
|
|
116
|
+
headers: Record<string, string>;
|
|
117
|
+
body: any;
|
|
118
|
+
}, { onUploadProgress, timeout }?: {
|
|
119
|
+
onUploadProgress?: (progress: {
|
|
120
|
+
loaded: number;
|
|
121
|
+
total: number | null;
|
|
122
|
+
}) => void;
|
|
123
|
+
timeout?: number | null;
|
|
124
|
+
}): Promise<any>;
|
|
125
|
+
/**
|
|
126
|
+
* Everything a request is before it goes anywhere: the URL with its query
|
|
127
|
+
* params and the stamps strict mode, a per-call audit message and an open
|
|
128
|
+
* logical operation add, plus the transformed body. Shared by the wire path
|
|
129
|
+
* (`makeRequest`) and the batch path (`queueRequest`), so a queued op is
|
|
130
|
+
* exactly the request that would have gone out. Strict mode stamps every
|
|
131
|
+
* write, queued or not, and `stampedDocument` names the document it stamped
|
|
132
|
+
* for (null when it stamped nothing). `stampedGroup` is the open logical
|
|
133
|
+
* operation it joined, if any, for `makeRequest` to mark written once the
|
|
134
|
+
* server has taken the request.
|
|
135
|
+
*/
|
|
136
|
+
export function prepareRequest(client: any, method: any, path: any, options?: {}): {
|
|
137
|
+
url: string;
|
|
138
|
+
requestBody: any;
|
|
139
|
+
stampedDocument: any;
|
|
140
|
+
stampedVersion: any;
|
|
141
|
+
stampedGroup: any;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Whether a write joins the open logical operation (see prepareRequest).
|
|
145
|
+
*/
|
|
146
|
+
export function joinsOperation(client: any, method: any, options?: {}): boolean;
|
|
147
|
+
/**
|
|
148
|
+
* Whether a call made on the client goes out with an Idempotency-Key.
|
|
149
|
+
*/
|
|
150
|
+
export function takesIdempotencyKey(method: any, options?: {}): boolean;
|
|
151
|
+
/**
|
|
152
|
+
* `path` with its strict-mode `document-version` claim replaced by `version`,
|
|
153
|
+
* or dropped when `version` is unknown. For the later requests of a batch
|
|
154
|
+
* split past MAX_BATCH_OPS: each op was stamped with the version the document
|
|
155
|
+
* had when it was queued, and every request before it has since moved the
|
|
156
|
+
* document on.
|
|
157
|
+
*/
|
|
158
|
+
export function restampDocumentVersion(path: any, version: any): any;
|
|
159
|
+
/**
|
|
160
|
+
* A call made on a batch (see `PlaidClient#batch`). A write of project data is
|
|
161
|
+
* queued as one operation of the batch and answers `{ batched: true }`; its
|
|
162
|
+
* result is the matching entry of what `submit()` resolves to. A read, and a
|
|
163
|
+
* signal marked `outOfBand`, is the client's to make and goes over the wire
|
|
164
|
+
* now, exactly as if it had been made on the client.
|
|
165
|
+
*/
|
|
166
|
+
export function queueRequest(batch: any, method: any, path: any, options?: {}): Promise<any>;
|
|
167
|
+
/**
|
|
168
|
+
* Generic request method handling all fetch logic.
|
|
169
|
+
*
|
|
170
|
+
* Options:
|
|
171
|
+
* body - Object body, run through transformRequest
|
|
172
|
+
* rawBody - Body value passed directly (no transform). Mutually exclusive with body.
|
|
173
|
+
* formData - If true, body is FormData; skip Content-Type header
|
|
174
|
+
* queryParams - Object of query param key/values to append
|
|
175
|
+
* noBatch - If true, throw when made on a batch. Only for calls a
|
|
176
|
+
* batch cannot carry at all (see the note at the
|
|
177
|
+
* top of this file); never for a read.
|
|
178
|
+
* outOfBand - If true, the call is a signal rather than a write of
|
|
179
|
+
* project data: made on a batch it still goes over the
|
|
180
|
+
* wire, and it never joins an open logical operation (see
|
|
181
|
+
* the note at the top of this file).
|
|
182
|
+
* noOperation - If true, the call never joins an open logical operation
|
|
183
|
+
* but still queues on a batch like a write. For a
|
|
184
|
+
* broadcast message, which is never audited.
|
|
185
|
+
* versioned - If false, strict mode does not stamp the call with the
|
|
186
|
+
* document version. For a write that carries its own
|
|
187
|
+
* precondition (a text edit with `base`).
|
|
188
|
+
* skipResponseTransform - Return raw parsed JSON (no transformResponse)
|
|
189
|
+
* noAuth - Skip Authorization header
|
|
190
|
+
* binaryResponse - Return arrayBuffer instead of JSON/text
|
|
191
|
+
* noBusyRetry - If true, a 503 is not retried. For a query: core
|
|
192
|
+
* answers 503 when its queue of large queries stays full
|
|
193
|
+
* until the query's deadline, and asking again at once
|
|
194
|
+
* only joins the same queue.
|
|
195
|
+
* timeout - Per-request timeout in ms overriding client.timeout
|
|
196
|
+
* for this call (0/null disables). Used for known-long
|
|
197
|
+
* ops like project delete.
|
|
198
|
+
* onUploadProgress - Called with `{ loaded, total }` (bytes; total null when
|
|
199
|
+
* unknown) as the request body goes up. In a browser the
|
|
200
|
+
* request then travels by XMLHttpRequest (see xhrSend),
|
|
201
|
+
* where the timeout only fires when the upload stalls;
|
|
202
|
+
* elsewhere the callback is ignored and fetch is used.
|
|
203
|
+
*/
|
|
204
|
+
export function makeRequest(client: any, method: any, path: any, options?: {}): Promise<any>;
|
|
205
|
+
export const DEFAULT_TIMEOUT_MS: 30000;
|
|
206
|
+
export const DEFAULT_BATCH_TIMEOUT_MS: 180000;
|
|
207
|
+
export const DEFAULT_QUERY_TIMEOUT_MS: 35000;
|
|
208
|
+
export const BUSY_RETRIES: 4;
|
|
209
|
+
export const BUSY_BACKOFF_MS: 250;
|
|
210
|
+
export const IDEMPOTENCY_HEADER: "Idempotency-Key";
|
|
211
|
+
export const REPLAYED_HEADER: "Idempotent-Replayed";
|
|
212
|
+
export const UNKNOWN_RETRY_DELAYS_MS: number[];
|
package/src/http.js
CHANGED
|
@@ -239,6 +239,8 @@ export const BUSY_BACKOFF_MS = 250;
|
|
|
239
239
|
*
|
|
240
240
|
* `attempt` must perform the whole request, not just await a prepared one:
|
|
241
241
|
* a fetch body and an AbortSignal.timeout are both single-use.
|
|
242
|
+
* @param {() => Promise<any>} attempt
|
|
243
|
+
* @param {{retries?: number, baseDelayMs?: number, onRetry?: (info: object) => void}} [options]
|
|
242
244
|
*/
|
|
243
245
|
export async function retryWhileBusy(
|
|
244
246
|
attempt,
|
|
@@ -315,6 +317,8 @@ export function mintedTaken(status, data, minted) {
|
|
|
315
317
|
* answered from the first send when that one landed. Not when the browser
|
|
316
318
|
* says it is offline: then the request never left, and the caller waits for
|
|
317
319
|
* the network instead. Full jitter, as retryWhileBusy.
|
|
320
|
+
* @param {() => Promise<any>} attempt
|
|
321
|
+
* @param {{delaysMs?: number[], onRetry?: (info: object) => void}} [options]
|
|
318
322
|
*/
|
|
319
323
|
export async function retryUnknown(
|
|
320
324
|
attempt,
|
|
@@ -381,6 +385,9 @@ export function timeoutSignal(timeout) {
|
|
|
381
385
|
* cut off while it is still moving, only once nothing has moved for `timeout`
|
|
382
386
|
* ms. Rejects the way fetch does: a TypeError for a network failure, an error
|
|
383
387
|
* named TimeoutError for a stall.
|
|
388
|
+
* @param {string} url
|
|
389
|
+
* @param {{method: string, headers: Record<string, string>, body: any}} request
|
|
390
|
+
* @param {{onUploadProgress?: (progress: {loaded: number, total: number | null}) => void, timeout?: number | null}} [options]
|
|
384
391
|
*/
|
|
385
392
|
export function xhrSend(
|
|
386
393
|
url,
|
package/src/ids.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fresh UUIDv7, later than every one this page minted before it.
|
|
3
|
+
* @returns {string}
|
|
4
|
+
*/
|
|
5
|
+
export function uuidv7(): string;
|
|
6
|
+
/**
|
|
7
|
+
* A random UUIDv4, for ids that need no order (an operation group, a lock
|
|
8
|
+
* holder). Built on `crypto.getRandomValues`, since `crypto.randomUUID`
|
|
9
|
+
* exists only in a secure context and a page served over plain HTTP from
|
|
10
|
+
* another host than localhost is not one.
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function uuidv4(): string;
|
|
14
|
+
/**
|
|
15
|
+
* A user id as the server stores it. A user's id is their email address,
|
|
16
|
+
* trimmed of every whitespace, control and format character at either end
|
|
17
|
+
* and lowercased, in NFC before and after (a capital with no composed form,
|
|
18
|
+
* such as J̌, lowercases to one that has one, ǰ), so `Ana@Example.org` and
|
|
19
|
+
* `ana@example.org` name one account. Compare a typed address with an id
|
|
20
|
+
* through this. The rule is core's (`plaid.sql.user/normalize-id`), checked
|
|
21
|
+
* against its case table.
|
|
22
|
+
* Anything but a string comes back unchanged.
|
|
23
|
+
* @param {string} id
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeUserId(id: string): string;
|
package/src/ids.js
CHANGED
|
@@ -52,3 +52,45 @@ export function uuidv7() {
|
|
|
52
52
|
}
|
|
53
53
|
return s;
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A random UUIDv4, for ids that need no order (an operation group, a lock
|
|
58
|
+
* holder). Built on `crypto.getRandomValues`, since `crypto.randomUUID`
|
|
59
|
+
* exists only in a secure context and a page served over plain HTTP from
|
|
60
|
+
* another host than localhost is not one.
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
export function uuidv4() {
|
|
64
|
+
const b = new Uint8Array(16);
|
|
65
|
+
crypto.getRandomValues(b);
|
|
66
|
+
b[6] = (b[6] & 0x0f) | 0x40;
|
|
67
|
+
b[8] = (b[8] & 0x3f) | 0x80;
|
|
68
|
+
let s = "";
|
|
69
|
+
for (let i = 0; i < 16; i++) {
|
|
70
|
+
s += hex[b[i]];
|
|
71
|
+
if (i === 3 || i === 5 || i === 7 || i === 9) s += "-";
|
|
72
|
+
}
|
|
73
|
+
return s;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Whitespace, control and format characters at either end of an id (NBSP,
|
|
77
|
+
// the BOM, zero-width and bidi marks as well as ASCII blanks), as core trims.
|
|
78
|
+
const ID_EDGES = /^[\p{Z}\p{Cc}\p{Cf}]+|[\p{Z}\p{Cc}\p{Cf}]+$/gu;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A user id as the server stores it. A user's id is their email address,
|
|
82
|
+
* trimmed of every whitespace, control and format character at either end
|
|
83
|
+
* and lowercased, in NFC before and after (a capital with no composed form,
|
|
84
|
+
* such as J̌, lowercases to one that has one, ǰ), so `Ana@Example.org` and
|
|
85
|
+
* `ana@example.org` name one account. Compare a typed address with an id
|
|
86
|
+
* through this. The rule is core's (`plaid.sql.user/normalize-id`), checked
|
|
87
|
+
* against its case table.
|
|
88
|
+
* Anything but a string comes back unchanged.
|
|
89
|
+
* @param {string} id
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
export function normalizeUserId(id) {
|
|
93
|
+
return typeof id === "string"
|
|
94
|
+
? id.normalize("NFC").replace(ID_EDGES, "").toLowerCase().normalize("NFC")
|
|
95
|
+
: id;
|
|
96
|
+
}
|