@larc-iu/plaid-client 0.2.0-alpha.16 → 0.2.0-alpha.18
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 +409 -65
- package/package.json +1 -1
- package/src/created.js +29 -0
- package/src/documentLock.js +14 -9
- package/src/events.js +256 -0
- package/src/http.js +168 -56
- package/src/index.js +495 -208
- package/src/metadataOps.js +117 -0
- package/src/provenance.js +7 -17
- package/src/requester.js +56 -0
- package/src/services.js +62 -7
- package/src/sse.js +29 -3
- package/src/transforms.js +7 -1
package/src/http.js
CHANGED
|
@@ -32,15 +32,21 @@ import { transformRequest, transformResponse } from "./transforms.js";
|
|
|
32
32
|
// written and leave the relabel PATCH 404ing on a group nothing ever
|
|
33
33
|
// created.
|
|
34
34
|
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
35
|
+
// A broadcast message (`messages.sendMessage`) sits between a write and a
|
|
36
|
+
// signal: it stays batchable, so a message queued after the writes goes out
|
|
37
|
+
// after them, but it writes nothing to the audit log either. `noOperation`
|
|
38
|
+
// keeps it out of an open logical operation without making it out of band.
|
|
39
39
|
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// and
|
|
43
|
-
//
|
|
40
|
+
// Strict mode stamps the expected document-version onto every write, queued
|
|
41
|
+
// or not. The server checks the first write of a batch request that names the
|
|
42
|
+
// document and skips the rest of that document's, so the bump one op causes
|
|
43
|
+
// does not 409 the next. A batch split past MAX_BATCH_OPS restamps each later
|
|
44
|
+
// request with the version the one before it left (see submitBatch).
|
|
45
|
+
//
|
|
46
|
+
// `noBatch` is not part of that judgment. It marks the four calls the batch
|
|
47
|
+
// transport cannot carry at all (the multipart media and avatar uploads, the
|
|
48
|
+
// user-data store's put and delete) and raises so the caller finds out. A
|
|
49
|
+
// batch inside a batch is refused by the batch itself. Never put it on a read: it turns a swallowed read into a
|
|
44
50
|
// thrown one, which is what the chrome hit when an unrelated import was
|
|
45
51
|
// running. A blobless DELETE beside an upload is not one of them: it carries
|
|
46
52
|
// nothing the transport cannot express, so it takes its class from the three
|
|
@@ -58,6 +64,18 @@ export const DEFAULT_TIMEOUT_MS = 30000;
|
|
|
58
64
|
// nothing and costs a retry stacked on a write that is still running.
|
|
59
65
|
export const DEFAULT_BATCH_TIMEOUT_MS = 180000;
|
|
60
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Note how far the server's clock is from this machine's, from a response's
|
|
69
|
+
* Date header, for `client.serverNow()`. A time the server stamped (an audit
|
|
70
|
+
* entry's `ts`) has to be judged against the server's clock: a browser clock
|
|
71
|
+
* minutes off would misjudge it. A response with no readable Date header
|
|
72
|
+
* leaves the last offset as it was.
|
|
73
|
+
*/
|
|
74
|
+
export function noteServerClock(client, headers) {
|
|
75
|
+
const at = Date.parse(headers?.get?.("date") ?? "");
|
|
76
|
+
if (!Number.isNaN(at)) client.serverClockOffsetMs = at - Date.now();
|
|
77
|
+
}
|
|
78
|
+
|
|
61
79
|
/**
|
|
62
80
|
* Extract and update document versions from response headers and body.
|
|
63
81
|
*
|
|
@@ -73,6 +91,13 @@ export function extractDocumentVersions(
|
|
|
73
91
|
responseBody = null,
|
|
74
92
|
{ historical = false } = {},
|
|
75
93
|
) {
|
|
94
|
+
// Past fifty documents the server leaves the list out and says how many it
|
|
95
|
+
// left out. Any version held may be one of them, so all are forgotten, and
|
|
96
|
+
// makeRequest reads the strict-mode document's again (learnOmittedVersion).
|
|
97
|
+
if (responseHeaders.get("X-Document-Versions-Omitted")) {
|
|
98
|
+
client.documentVersions = {};
|
|
99
|
+
client.documentVersionsOmitted = true;
|
|
100
|
+
}
|
|
76
101
|
const docVersionsHeader = responseHeaders.get("X-Document-Versions");
|
|
77
102
|
if (docVersionsHeader) {
|
|
78
103
|
try {
|
|
@@ -292,51 +317,25 @@ export function xhrSend(
|
|
|
292
317
|
});
|
|
293
318
|
}
|
|
294
319
|
|
|
295
|
-
/**
|
|
296
|
-
* Generic request method handling all fetch logic.
|
|
297
|
-
*
|
|
298
|
-
* Options:
|
|
299
|
-
* body - Object body, run through transformRequest
|
|
300
|
-
* rawBody - Body value passed directly (no transform). Mutually exclusive with body.
|
|
301
|
-
* formData - If true, body is FormData; skip Content-Type header
|
|
302
|
-
* queryParams - Object of query param key/values to append
|
|
303
|
-
* noBatch - If true, throw when made on a batch. Only for calls the
|
|
304
|
-
* batch transport cannot carry at all (see the note at the
|
|
305
|
-
* top of this file); never for a read.
|
|
306
|
-
* outOfBand - If true, the call is a signal rather than a write of
|
|
307
|
-
* project data: made on a batch it still goes over the
|
|
308
|
-
* wire, and it never joins an open logical operation (see
|
|
309
|
-
* the note at the top of this file).
|
|
310
|
-
* skipResponseTransform - Return raw parsed JSON (no transformResponse)
|
|
311
|
-
* noAuth - Skip Authorization header
|
|
312
|
-
* binaryResponse - Return arrayBuffer instead of JSON/text
|
|
313
|
-
* timeout - Per-request timeout in ms overriding client.timeout
|
|
314
|
-
* for this call (0/null disables). Used for known-long
|
|
315
|
-
* ops like project delete.
|
|
316
|
-
* onUploadProgress - Called with `{ loaded, total }` (bytes; total null when
|
|
317
|
-
* unknown) as the request body goes up. In a browser the
|
|
318
|
-
* request then travels by XMLHttpRequest (see xhrSend),
|
|
319
|
-
* where the timeout only fires when the upload stalls;
|
|
320
|
-
* elsewhere the callback is ignored and fetch is used.
|
|
321
|
-
*/
|
|
322
320
|
/**
|
|
323
321
|
* Everything a request is before it goes anywhere: the URL with its query
|
|
324
322
|
* params and the stamps strict mode, a per-call audit message and an open
|
|
325
323
|
* logical operation add, plus the transformed body. Shared by the wire path
|
|
326
324
|
* (`makeRequest`) and the batch path (`queueRequest`), so a queued op is
|
|
327
|
-
* exactly the request that would have gone out.
|
|
328
|
-
*
|
|
329
|
-
*
|
|
325
|
+
* exactly the request that would have gone out. Strict mode stamps every
|
|
326
|
+
* write, queued or not, and `stampedDocument` names the document it stamped
|
|
327
|
+
* for (null when it stamped nothing).
|
|
330
328
|
*/
|
|
331
|
-
export function prepareRequest(
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
329
|
+
export function prepareRequest(client, method, path, options = {}) {
|
|
330
|
+
const {
|
|
331
|
+
body,
|
|
332
|
+
rawBody,
|
|
333
|
+
formData,
|
|
334
|
+
queryParams,
|
|
335
|
+
outOfBand,
|
|
336
|
+
noOperation,
|
|
337
|
+
auditMessage,
|
|
338
|
+
} = options;
|
|
340
339
|
|
|
341
340
|
// A write must not go out on a lock that lapsed. `documents.locked()`
|
|
342
341
|
// records the loss here when its keep-alive cannot renew, and from that
|
|
@@ -383,20 +382,23 @@ export function prepareRequest(
|
|
|
383
382
|
// one. Stamping only the first write was silently no check at all
|
|
384
383
|
// whenever that write was one the route ignores, such as a vocabulary
|
|
385
384
|
// entry's metadata, which is exactly what the igt editor queues first.
|
|
385
|
+
// `stampedDocument` names the document the stamp is for, so a batch split
|
|
386
|
+
// into several requests can restamp its later ones (see submitBatch).
|
|
387
|
+
let stampedDocument = null;
|
|
386
388
|
if (client.strictModeDocumentId && method !== "GET") {
|
|
387
389
|
const docId = client.strictModeDocumentId;
|
|
388
390
|
if (client.documentVersions[docId]) {
|
|
389
391
|
const docVersion = client.documentVersions[docId];
|
|
390
392
|
const separator = url.includes("?") ? "&" : "?";
|
|
391
393
|
url += `${separator}document-version=${encodeURIComponent(docVersion)}`;
|
|
394
|
+
stampedDocument = docId;
|
|
392
395
|
}
|
|
393
396
|
}
|
|
394
397
|
|
|
395
398
|
// Per-call custom audit-log message (overrides the auto-generated
|
|
396
|
-
// description of THIS write)
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
// own path/query/body params.
|
|
399
|
+
// description of THIS write), stamped on the one write it was given to. The
|
|
400
|
+
// server templates `{param}` placeholders against the endpoint's own
|
|
401
|
+
// path/query/body params.
|
|
400
402
|
if (auditMessage && method !== "GET") {
|
|
401
403
|
const separator = url.includes("?") ? "&" : "?";
|
|
402
404
|
url += `${separator}audit-message=${encodeURIComponent(auditMessage)}`;
|
|
@@ -409,17 +411,89 @@ export function prepareRequest(
|
|
|
409
411
|
// An out-of-band signal is not one of those writes (see the note at the top
|
|
410
412
|
// of this file): it never lands in the audit log, so a stamp does nothing
|
|
411
413
|
// server-side while `written` promises a group that will never exist, and
|
|
412
|
-
// the relabel PATCH then 404s.
|
|
413
|
-
|
|
414
|
+
// the relabel PATCH then 404s. A broadcast message (`noOperation`) is
|
|
415
|
+
// never audited either.
|
|
416
|
+
if (
|
|
417
|
+
client.operationGroup &&
|
|
418
|
+
method !== "GET" &&
|
|
419
|
+
!outOfBand &&
|
|
420
|
+
!noOperation
|
|
421
|
+
) {
|
|
414
422
|
const group = client.operationGroup;
|
|
415
423
|
const separator = url.includes("?") ? "&" : "?";
|
|
416
424
|
url += `${separator}group-id=${encodeURIComponent(group.id)}`;
|
|
417
425
|
if (group.message)
|
|
418
426
|
url += `&group-message=${encodeURIComponent(group.message)}`;
|
|
427
|
+
if (group.kind) url += `&group-kind=${encodeURIComponent(group.kind)}`;
|
|
428
|
+
if (group.ref) url += `&group-ref=${encodeURIComponent(group.ref)}`;
|
|
419
429
|
group.written = true;
|
|
420
430
|
}
|
|
421
431
|
|
|
422
|
-
return { url, requestBody };
|
|
432
|
+
return { url, requestBody, stampedDocument };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* `path` with its strict-mode `document-version` claim replaced by `version`,
|
|
437
|
+
* or dropped when `version` is unknown. For the later requests of a batch
|
|
438
|
+
* split past MAX_BATCH_OPS: each op was stamped with the version the document
|
|
439
|
+
* had when it was queued, and every request before it has since moved the
|
|
440
|
+
* document on.
|
|
441
|
+
*/
|
|
442
|
+
export function restampDocumentVersion(path, version) {
|
|
443
|
+
const [base, query = ""] = path.split(/\?(.*)/s);
|
|
444
|
+
const params = query
|
|
445
|
+
.split("&")
|
|
446
|
+
.filter((p) => p && !p.startsWith("document-version="));
|
|
447
|
+
if (version !== undefined && version !== null) {
|
|
448
|
+
params.push(`document-version=${encodeURIComponent(version)}`);
|
|
449
|
+
}
|
|
450
|
+
return params.length ? `${base}?${params.join("&")}` : base;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The strict-mode document whose version a response left out
|
|
455
|
+
* (X-Document-Versions-Omitted) and the client has not learned since, or
|
|
456
|
+
* null. Such a write would go out unstamped, which is no check at all.
|
|
457
|
+
*/
|
|
458
|
+
function omittedStrictDocument(client) {
|
|
459
|
+
const docId = client.strictModeDocumentId;
|
|
460
|
+
return docId &&
|
|
461
|
+
client.documentVersionsOmitted &&
|
|
462
|
+
client.documentVersions[docId] === undefined
|
|
463
|
+
? docId
|
|
464
|
+
: null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Read the strict-mode document once to learn the version a response left
|
|
469
|
+
* out. Done right after that response, so the client's own large write is
|
|
470
|
+
* not mistaken for somebody else's edit at the next save, and again before a
|
|
471
|
+
* strict write when that read failed.
|
|
472
|
+
*/
|
|
473
|
+
async function learnOmittedVersion(client) {
|
|
474
|
+
const docId = omittedStrictDocument(client);
|
|
475
|
+
if (!docId) return;
|
|
476
|
+
// One read at a time: a write made while it is in flight waits for it
|
|
477
|
+
// rather than going out unstamped.
|
|
478
|
+
if (!client._learningOmittedVersion) {
|
|
479
|
+
client._learningOmittedVersion = makeRequest(
|
|
480
|
+
client,
|
|
481
|
+
"GET",
|
|
482
|
+
`/api/v1/documents/${docId}`,
|
|
483
|
+
{ learningOmittedVersion: true },
|
|
484
|
+
).finally(() => {
|
|
485
|
+
client._learningOmittedVersion = null;
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
await client._learningOmittedVersion;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function learnOmittedVersionQuietly(client) {
|
|
492
|
+
try {
|
|
493
|
+
await learnOmittedVersion(client);
|
|
494
|
+
} catch (_) {
|
|
495
|
+
/* the write went through; the next strict write asks again */
|
|
496
|
+
}
|
|
423
497
|
}
|
|
424
498
|
|
|
425
499
|
/**
|
|
@@ -439,12 +513,11 @@ export async function queueRequest(batch, method, path, options = {}) {
|
|
|
439
513
|
if (options.noBatch) {
|
|
440
514
|
throw new Error(`This endpoint cannot be used in a batch: ${path}`);
|
|
441
515
|
}
|
|
442
|
-
const { url, requestBody } = prepareRequest(
|
|
516
|
+
const { url, requestBody, stampedDocument } = prepareRequest(
|
|
443
517
|
batch.client,
|
|
444
518
|
method,
|
|
445
519
|
path,
|
|
446
520
|
options,
|
|
447
|
-
batch,
|
|
448
521
|
);
|
|
449
522
|
const operation = {
|
|
450
523
|
path: url.replace(batch.client.baseUrl, ""),
|
|
@@ -454,9 +527,40 @@ export async function queueRequest(batch, method, path, options = {}) {
|
|
|
454
527
|
operation.body = requestBody;
|
|
455
528
|
}
|
|
456
529
|
batch.operations.push(operation);
|
|
530
|
+
batch.stampedDocuments.push(stampedDocument);
|
|
457
531
|
return { batched: true };
|
|
458
532
|
}
|
|
459
533
|
|
|
534
|
+
/**
|
|
535
|
+
* Generic request method handling all fetch logic.
|
|
536
|
+
*
|
|
537
|
+
* Options:
|
|
538
|
+
* body - Object body, run through transformRequest
|
|
539
|
+
* rawBody - Body value passed directly (no transform). Mutually exclusive with body.
|
|
540
|
+
* formData - If true, body is FormData; skip Content-Type header
|
|
541
|
+
* queryParams - Object of query param key/values to append
|
|
542
|
+
* noBatch - If true, throw when made on a batch. Only for calls the
|
|
543
|
+
* batch transport cannot carry at all (see the note at the
|
|
544
|
+
* top of this file); never for a read.
|
|
545
|
+
* outOfBand - If true, the call is a signal rather than a write of
|
|
546
|
+
* project data: made on a batch it still goes over the
|
|
547
|
+
* wire, and it never joins an open logical operation (see
|
|
548
|
+
* the note at the top of this file).
|
|
549
|
+
* noOperation - If true, the call never joins an open logical operation
|
|
550
|
+
* but still queues on a batch like a write. For a
|
|
551
|
+
* broadcast message, which is never audited.
|
|
552
|
+
* skipResponseTransform - Return raw parsed JSON (no transformResponse)
|
|
553
|
+
* noAuth - Skip Authorization header
|
|
554
|
+
* binaryResponse - Return arrayBuffer instead of JSON/text
|
|
555
|
+
* timeout - Per-request timeout in ms overriding client.timeout
|
|
556
|
+
* for this call (0/null disables). Used for known-long
|
|
557
|
+
* ops like project delete.
|
|
558
|
+
* onUploadProgress - Called with `{ loaded, total }` (bytes; total null when
|
|
559
|
+
* unknown) as the request body goes up. In a browser the
|
|
560
|
+
* request then travels by XMLHttpRequest (see xhrSend),
|
|
561
|
+
* where the timeout only fires when the upload stalls;
|
|
562
|
+
* elsewhere the callback is ignored and fetch is used.
|
|
563
|
+
*/
|
|
460
564
|
export async function makeRequest(client, method, path, options = {}) {
|
|
461
565
|
const {
|
|
462
566
|
formData,
|
|
@@ -465,7 +569,11 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
465
569
|
binaryResponse,
|
|
466
570
|
timeout,
|
|
467
571
|
onUploadProgress,
|
|
572
|
+
learningOmittedVersion,
|
|
468
573
|
} = options;
|
|
574
|
+
if (method !== "GET" && omittedStrictDocument(client)) {
|
|
575
|
+
await learnOmittedVersion(client);
|
|
576
|
+
}
|
|
469
577
|
const { url, requestBody } = prepareRequest(client, method, path, options);
|
|
470
578
|
|
|
471
579
|
// Build fetch options
|
|
@@ -506,6 +614,7 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
506
614
|
}
|
|
507
615
|
return res;
|
|
508
616
|
});
|
|
617
|
+
noteServerClock(client, response.headers);
|
|
509
618
|
|
|
510
619
|
if (!response.ok) {
|
|
511
620
|
const error = makeHttpError(
|
|
@@ -535,6 +644,7 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
535
644
|
// Binary response (getMedia)
|
|
536
645
|
if (binaryResponse) {
|
|
537
646
|
extractDocumentVersions(client, response.headers);
|
|
647
|
+
if (!learningOmittedVersion) await learnOmittedVersionQuietly(client);
|
|
538
648
|
return await response.arrayBuffer();
|
|
539
649
|
}
|
|
540
650
|
|
|
@@ -545,12 +655,14 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
545
655
|
extractDocumentVersions(client, response.headers, data, {
|
|
546
656
|
historical: /[?&]as-of=/.test(url),
|
|
547
657
|
});
|
|
658
|
+
if (!learningOmittedVersion) await learnOmittedVersionQuietly(client);
|
|
548
659
|
if (skipResponseTransform) {
|
|
549
660
|
return data;
|
|
550
661
|
}
|
|
551
662
|
return transformResponse(data);
|
|
552
663
|
} else {
|
|
553
664
|
extractDocumentVersions(client, response.headers);
|
|
665
|
+
if (!learningOmittedVersion) await learnOmittedVersionQuietly(client);
|
|
554
666
|
return await response.text();
|
|
555
667
|
}
|
|
556
668
|
} catch (error) {
|