@larc-iu/plaid-client 0.2.0-alpha.20 → 0.2.0-alpha.22
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 +28 -27
- package/package.json +1 -1
- package/src/constraints.js +0 -11
- package/src/documentLock.js +54 -71
- package/src/http.js +59 -19
- package/src/index.js +188 -44
- package/src/metadataOps.js +4 -2
- package/src/provenance.js +0 -3
- package/src/roles.js +4 -13
- package/src/serviceSchema.js +5 -23
- package/src/services.js +88 -8
- package/src/textEdits.js +18 -2
package/index.d.ts
CHANGED
|
@@ -667,7 +667,7 @@ interface Invite {
|
|
|
667
667
|
id: string;
|
|
668
668
|
/** "signup" | "password-reset" */
|
|
669
669
|
kind: string;
|
|
670
|
-
/** "active" | "used" | "expired" | "revoked" */
|
|
670
|
+
/** "active" | "used" | "expired" | "revoked" | "inactive" */
|
|
671
671
|
status: string;
|
|
672
672
|
createdBy: string;
|
|
673
673
|
createdAt: string;
|
|
@@ -685,7 +685,7 @@ interface Invite {
|
|
|
685
685
|
interface InvitePreview {
|
|
686
686
|
/** "signup" | "password-reset" */
|
|
687
687
|
kind: string;
|
|
688
|
-
/** "active" | "used" | "expired" | "revoked" */
|
|
688
|
+
/** "active" | "used" | "expired" | "revoked" | "inactive" */
|
|
689
689
|
status: string;
|
|
690
690
|
expiresAt: string;
|
|
691
691
|
grantAdmin: boolean;
|
|
@@ -923,6 +923,10 @@ interface AuditPageOptions {
|
|
|
923
923
|
order?: "asc" | "desc";
|
|
924
924
|
limit?: number;
|
|
925
925
|
cursor?: string;
|
|
926
|
+
/** Keep only each entry's oldest N operations in `ops` (1..1000). Every entry has `opCount`. */
|
|
927
|
+
opsLimit?: number;
|
|
928
|
+
/** Read only this entry rather than a page. */
|
|
929
|
+
entryId?: string;
|
|
926
930
|
}
|
|
927
931
|
|
|
928
932
|
/** One user's activity over a scope and window. */
|
|
@@ -977,7 +981,10 @@ interface InvitesBundle {
|
|
|
977
981
|
all?: boolean;
|
|
978
982
|
pageSize?: number;
|
|
979
983
|
}): AsyncGenerator<Invite[]>;
|
|
980
|
-
/**
|
|
984
|
+
/**
|
|
985
|
+
* The `code` is returned ONCE and is not recoverable afterward. Needs a
|
|
986
|
+
* signed-in session: a named API token cannot mint any invite (403).
|
|
987
|
+
*/
|
|
981
988
|
create(
|
|
982
989
|
opts?: CreateInviteOptions,
|
|
983
990
|
auditMessage?: string,
|
|
@@ -1069,9 +1076,6 @@ export declare class DocumentLockLost extends Error {
|
|
|
1069
1076
|
readonly cause?: unknown;
|
|
1070
1077
|
}
|
|
1071
1078
|
|
|
1072
|
-
/** The server's default document-lock window, in ms. */
|
|
1073
|
-
export const DOCUMENT_LOCK_TTL_MS: 60000;
|
|
1074
|
-
|
|
1075
1079
|
interface DocumentsBundle {
|
|
1076
1080
|
checkLock(documentId: string): Promise<any>;
|
|
1077
1081
|
/**
|
|
@@ -1115,7 +1119,14 @@ interface DocumentsBundle {
|
|
|
1115
1119
|
onProgress?: (progress: { loaded: number; total: number | null }) => void;
|
|
1116
1120
|
},
|
|
1117
1121
|
): Promise<any>;
|
|
1118
|
-
deleteMedia(
|
|
1122
|
+
deleteMedia(
|
|
1123
|
+
documentId: string,
|
|
1124
|
+
auditMessage?: string,
|
|
1125
|
+
options?: {
|
|
1126
|
+
/** The recording meant, the `?v=` of the document's `mediaUrl`. A 409 when the stored one is another. */
|
|
1127
|
+
mediaVersion?: string;
|
|
1128
|
+
},
|
|
1129
|
+
): Promise<any>;
|
|
1119
1130
|
setMetadata(
|
|
1120
1131
|
documentId: string,
|
|
1121
1132
|
body: any,
|
|
@@ -1458,7 +1469,7 @@ interface TokensBundle {
|
|
|
1458
1469
|
tokenId: string,
|
|
1459
1470
|
position: number,
|
|
1460
1471
|
auditMessage?: string,
|
|
1461
|
-
options?: { id?: string },
|
|
1472
|
+
options?: { id?: string; keep?: "left" | "right" },
|
|
1462
1473
|
): Promise<any>;
|
|
1463
1474
|
merge(
|
|
1464
1475
|
tokenId: string,
|
|
@@ -1615,6 +1626,10 @@ export declare class PlaidClient {
|
|
|
1615
1626
|
serverClockOffsetMs: number | null;
|
|
1616
1627
|
/** The server's time now, from its last response's Date header (to the second), else this machine's. Judge a time the server stamped, such as an audit entry's `ts`, against this rather than `Date.now()`. */
|
|
1617
1628
|
serverNow(): Date;
|
|
1629
|
+
/** Hear every time a request is sent again: after a 503, or for a keyed write after its answer was lost (no response, 502, 504). Called before the wait. Returns the unsubscribe. */
|
|
1630
|
+
onRetry(
|
|
1631
|
+
listener: (info: { attempt: number; retries: number; delay: number; error: Error }) => void,
|
|
1632
|
+
): () => void;
|
|
1618
1633
|
|
|
1619
1634
|
// Batches.
|
|
1620
1635
|
//
|
|
@@ -1648,7 +1663,7 @@ export declare class PlaidClient {
|
|
|
1648
1663
|
} | null;
|
|
1649
1664
|
beginOperation(
|
|
1650
1665
|
message: string,
|
|
1651
|
-
opts?: {
|
|
1666
|
+
opts?: { groupId?: string; kind?: OperationKind; ref?: string; keys?: KeySeed; minted?: Set<string> },
|
|
1652
1667
|
): string;
|
|
1653
1668
|
/** A seed for the Idempotency-Keys of an operation that may be run again (see beginOperation's `keys`). */
|
|
1654
1669
|
keySeed(): KeySeed;
|
|
@@ -1656,7 +1671,7 @@ export declare class PlaidClient {
|
|
|
1656
1671
|
withOperation<T>(
|
|
1657
1672
|
message: string,
|
|
1658
1673
|
fn: (setMessage: (msg: string) => void) => Promise<T> | T,
|
|
1659
|
-
opts?: { kind?: OperationKind; ref?: string;
|
|
1674
|
+
opts?: { kind?: OperationKind; ref?: string; groupId?: string; keys?: KeySeed; minted?: Set<string> },
|
|
1660
1675
|
): Promise<T>;
|
|
1661
1676
|
operationGroups: OperationGroupsBundle;
|
|
1662
1677
|
|
|
@@ -1702,8 +1717,6 @@ export function cpSlice(s: string, begin: number, end?: number): string;
|
|
|
1702
1717
|
export function cpSlicer(s: string): (begin: number, end?: number) => string;
|
|
1703
1718
|
/** UTF-16 index -> code-point index in `s`. */
|
|
1704
1719
|
export function utf16ToCp(s: string, u: number): number;
|
|
1705
|
-
/** Code-point index -> UTF-16 index in `s` (clamps past the end). */
|
|
1706
|
-
export function cpToUtf16(s: string, cp: number): number;
|
|
1707
1720
|
/** Like indexOf, but the result and `fromCp` are code-point indices; -1 if absent. */
|
|
1708
1721
|
export function cpIndexOf(s: string, sub: string, fromCp?: number): number;
|
|
1709
1722
|
|
|
@@ -1735,13 +1748,13 @@ export const PLAID_NAMESPACE: "plaid";
|
|
|
1735
1748
|
export const ROLE_KEY: "role";
|
|
1736
1749
|
/** Layer config key naming the metadata keys a token born of a SPLIT inherits. */
|
|
1737
1750
|
export const PRESERVE_ON_SPLIT_KEY: "preserveOnSplit";
|
|
1738
|
-
/** Layer config key
|
|
1739
|
-
export const PLAIN_EDITS_KEY: "plainEdits";
|
|
1740
|
-
/** Layer config key: a space typed inside a word of the text splits it (beside plainEdits). */
|
|
1751
|
+
/** Layer config key: a space typed inside a word of the text splits it (see the manual's "Changing a text's body"). */
|
|
1741
1752
|
export const SPLIT_ON_SPACE_KEY: "splitOnSpace";
|
|
1742
1753
|
|
|
1743
1754
|
/** The server's cap on operations per batch request; a larger batch goes as consecutive requests. */
|
|
1744
1755
|
export const MAX_BATCH_OPS: 1000;
|
|
1756
|
+
/** How long `admin.backup()` waits for the server to finish writing a backup, in ms. */
|
|
1757
|
+
export const BACKUP_TIMEOUT_MS: number;
|
|
1745
1758
|
/** The fixed role inventory; only these values are interoperable across apps. */
|
|
1746
1759
|
export const ROLES: {
|
|
1747
1760
|
readonly BASELINE: "baseline";
|
|
@@ -1854,20 +1867,12 @@ export function confirmedInferred(
|
|
|
1854
1867
|
};
|
|
1855
1868
|
/** Classify an entity's metadata into one of the three provenance states. */
|
|
1856
1869
|
export function provState(metadata: object | null | undefined): ProvState;
|
|
1857
|
-
/** The verifying fragment, { provConfirmed: true }: PATCH it over existing metadata. */
|
|
1858
|
-
export const PROV_CONFIRMED: { readonly provConfirmed: true };
|
|
1859
1870
|
/** Machine-made and not yet human-verified (needs review, replaceable, confirmable). */
|
|
1860
1871
|
export function isMachine(metadata: object | null | undefined): boolean;
|
|
1861
1872
|
/** Whether a machine writer must leave this entity alone (human or verified). !isMachine. */
|
|
1862
1873
|
export function isProtected(metadata: object | null | undefined): boolean;
|
|
1863
|
-
/** The fragment a HUMAN edit should merge in: PROV_CONFIRMED iff machine-unverified, else null. */
|
|
1864
|
-
export function verifyOnEdit(
|
|
1865
|
-
metadata: object | null | undefined,
|
|
1866
|
-
): { readonly provConfirmed: true } | null;
|
|
1867
1874
|
/** Canonical provSource for a service: 'service:<serviceId>'. */
|
|
1868
1875
|
export function serviceSource(serviceId: string): string;
|
|
1869
|
-
/** Canonical provSource for a contributor: 'user:<userId>'. */
|
|
1870
|
-
export function userSource(userId: string): string;
|
|
1871
1876
|
/** The metadata fragment a contributor's work carries. */
|
|
1872
1877
|
export function stampContributed(userId: string): {
|
|
1873
1878
|
prov: "contributed";
|
|
@@ -1925,8 +1930,6 @@ export function isReservedMetadataKey(key: string): boolean;
|
|
|
1925
1930
|
// --- Review: whose work is reviewed (a project-config norm) -------------------
|
|
1926
1931
|
/** The config key, under the `plaid` namespace, holding the review lists. */
|
|
1927
1932
|
export const REVIEW_KEY: "review";
|
|
1928
|
-
/** The project roles a review list may name. */
|
|
1929
|
-
export const PROJECT_ROLES: readonly ["reader", "writer", "maintainer"];
|
|
1930
1933
|
/** A project's review lists, normalized. */
|
|
1931
1934
|
export function readReview(config?: object | null): {
|
|
1932
1935
|
users: string[];
|
|
@@ -2004,8 +2007,6 @@ export interface ConstraintViolation {
|
|
|
2004
2007
|
value?: any;
|
|
2005
2008
|
parts?: string[];
|
|
2006
2009
|
}
|
|
2007
|
-
/** The constraint types. */
|
|
2008
|
-
export const CONSTRAINT_TYPES: readonly string[];
|
|
2009
2010
|
/** The camelCased violations of a write refused by a layer constraint, or null. */
|
|
2010
2011
|
export function violationsOf(err: any): ConstraintViolation[] | null;
|
|
2011
2012
|
export function valueSetAllows(
|
package/package.json
CHANGED
package/src/constraints.js
CHANGED
|
@@ -7,17 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
import { transformResponse } from "./transforms.js";
|
|
9
9
|
|
|
10
|
-
/** The constraint types, by the layer kind that may carry them. */
|
|
11
|
-
export const CONSTRAINT_TYPES = Object.freeze([
|
|
12
|
-
"max-in-degree",
|
|
13
|
-
"acyclic",
|
|
14
|
-
"same-ancestor",
|
|
15
|
-
"single-span",
|
|
16
|
-
"value-set",
|
|
17
|
-
"coextensive",
|
|
18
|
-
"single-link",
|
|
19
|
-
]);
|
|
20
|
-
|
|
21
10
|
/**
|
|
22
11
|
* The violations of a write refused by a layer constraint, camelCased
|
|
23
12
|
* ({constraint, namespace, layer, layerName, document, at, ids, value?,
|
package/src/documentLock.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isUnknownOutcome, retryUnknown } from "./http.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Keeping a document lock alive for as long as a `locked()` block runs.
|
|
3
5
|
*
|
|
@@ -27,21 +29,6 @@
|
|
|
27
29
|
* that is missing somewhere.
|
|
28
30
|
*/
|
|
29
31
|
|
|
30
|
-
/**
|
|
31
|
-
* How long plaid-core holds a document lock before it expires, in ms.
|
|
32
|
-
* `plaid.server.locks/default-lock-expiration-ms`. An operator can change it
|
|
33
|
-
* with `:plaid.server.locks/config :expiration-ms`, and a server publishes what
|
|
34
|
-
* it enforces as `lockExpirationMs` in `GET /info`. This is the last resort:
|
|
35
|
-
* what a live lock is renewed against is the `expiresAt` on the acquire
|
|
36
|
-
* response, which names the moment rather than the window.
|
|
37
|
-
*/
|
|
38
|
-
export const DOCUMENT_LOCK_TTL_MS = 60000;
|
|
39
|
-
|
|
40
|
-
// Widest lock lifetime we will believe from a server response. Past this the
|
|
41
|
-
// number is a clock skew between this machine and the server rather than a
|
|
42
|
-
// configured window, and the documented default is the better guess.
|
|
43
|
-
const MAX_BELIEVABLE_TTL_MS = 3600000;
|
|
44
|
-
|
|
45
32
|
/**
|
|
46
33
|
* The lock a `documents.locked()` block was holding is no longer held.
|
|
47
34
|
*
|
|
@@ -62,25 +49,31 @@ export class DocumentLockLost extends Error {
|
|
|
62
49
|
/**
|
|
63
50
|
* How long a freshly taken lock lasts, from the server's own answer.
|
|
64
51
|
*
|
|
65
|
-
* `expiresAt` is the epoch-millisecond
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
52
|
+
* `expiresAt` is the epoch-millisecond moment the lock endpoints return, on
|
|
53
|
+
* the server's clock, so it is read against the server's clock too
|
|
54
|
+
* (`client.serverNow()`): this machine's clock being off cannot stretch or
|
|
55
|
+
* shrink the window. It is how a window an operator has retuned
|
|
56
|
+
* (`:plaid.server.locks/config :expiration-ms`) reaches the keeper.
|
|
70
57
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
58
|
+
* An answer that cannot be a window (none, already over, past an hour) is
|
|
59
|
+
* core's default, 60 s.
|
|
60
|
+
*
|
|
61
|
+
* @param {number} expiresAt
|
|
62
|
+
* @param {number} serverNowMs
|
|
74
63
|
* @returns {number}
|
|
75
64
|
*/
|
|
76
|
-
export function lockTtlMs(expiresAt,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return
|
|
65
|
+
export function lockTtlMs(expiresAt, serverNowMs) {
|
|
66
|
+
const ttl = expiresAt - serverNowMs;
|
|
67
|
+
// No answer at all, or one past an hour or already over: this machine's
|
|
68
|
+
// clock stood in for the server's (a page that cannot read its Date
|
|
69
|
+
// header) and is far off. Core's default window is the better guess.
|
|
70
|
+
return Number.isFinite(ttl) && ttl > 0 && ttl <= MAX_TTL_MS ? ttl : DEFAULT_TTL_MS;
|
|
82
71
|
}
|
|
83
72
|
|
|
73
|
+
// Core's default lock window, and the longest one believed.
|
|
74
|
+
const DEFAULT_TTL_MS = 60000;
|
|
75
|
+
const MAX_TTL_MS = 3600000;
|
|
76
|
+
|
|
84
77
|
/**
|
|
85
78
|
* Renews one document's lock until the block holding it exits.
|
|
86
79
|
*
|
|
@@ -100,9 +93,7 @@ export class LockKeeper {
|
|
|
100
93
|
) {
|
|
101
94
|
this._refresh = refresh;
|
|
102
95
|
this._documentId = documentId;
|
|
103
|
-
|
|
104
|
-
// misconfiguration and beat at the documented rate instead.
|
|
105
|
-
this._ttlMs = ttlMs >= 2000 ? ttlMs : DOCUMENT_LOCK_TTL_MS;
|
|
96
|
+
this._ttlMs = ttlMs;
|
|
106
97
|
this._onLost = onLost;
|
|
107
98
|
this._clock = clock;
|
|
108
99
|
this._sleep = sleep || ((ms) => this._defaultSleep(ms));
|
|
@@ -219,16 +210,11 @@ export class DocumentLock {
|
|
|
219
210
|
}
|
|
220
211
|
|
|
221
212
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
213
|
+
* The pause before sending again an acquire whose outcome is unknown, which
|
|
214
|
+
* grows by this much each time (three sends in all).
|
|
224
215
|
*/
|
|
225
|
-
export const LOCK_ACQUIRE_ATTEMPTS = 3;
|
|
226
216
|
export const LOCK_ACQUIRE_RETRY_MS = 500;
|
|
227
217
|
|
|
228
|
-
// The HTTP status of a request that may or may not have reached the server: no
|
|
229
|
-
// answer at all (0), or a proxy's 502 or 504.
|
|
230
|
-
const UNKNOWN_OUTCOME_STATUSES = new Set([0, 502, 504]);
|
|
231
|
-
|
|
232
218
|
function mintLockId() {
|
|
233
219
|
return globalThis.crypto.randomUUID();
|
|
234
220
|
}
|
|
@@ -237,45 +223,42 @@ function mintLockId() {
|
|
|
237
223
|
* The acquire of a `locked()` block, under the holder id it minted.
|
|
238
224
|
*
|
|
239
225
|
* An acquire whose outcome is unknown (no answer, a timeout, a 502 or a 504)
|
|
240
|
-
* may have taken the lock. It is sent again under the same id
|
|
241
|
-
* answers 200 while that holder has it.
|
|
242
|
-
*
|
|
243
|
-
* way until it
|
|
226
|
+
* may have taken the lock. It is sent again under the same id as any write is
|
|
227
|
+
* (`retryUnknown`), which the server answers 200 while that holder has it.
|
|
228
|
+
* When every send is unknown (or the network went), the lock it may hold is
|
|
229
|
+
* released on the way out, so it does not stand in everyone's way until it
|
|
230
|
+
* expires.
|
|
244
231
|
*/
|
|
245
232
|
async function takeLock(client, documentId, lockId, retryMs) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
setTimeout(resolve, retryMs * (attempt + 1)),
|
|
269
|
-
);
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
233
|
+
try {
|
|
234
|
+
return await retryUnknown(
|
|
235
|
+
() => client.documents.acquireLock(documentId, undefined, lockId),
|
|
236
|
+
{ delaysMs: [retryMs, 2 * retryMs] },
|
|
237
|
+
);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (error?.status === 423) {
|
|
240
|
+
// The error body is raw JSON off the wire, so it is still kebab-cased.
|
|
241
|
+
const body = error.responseData || {};
|
|
242
|
+
const holder = body.userId || body["user-id"] || "another user";
|
|
243
|
+
const readable = new Error(
|
|
244
|
+
`This document is being edited by ${holder}. Try again once they're done.`,
|
|
245
|
+
);
|
|
246
|
+
readable.status = 423;
|
|
247
|
+
readable.statusText = error.statusText;
|
|
248
|
+
readable.url = error.url;
|
|
249
|
+
readable.method = error.method;
|
|
250
|
+
readable.responseData = error.responseData;
|
|
251
|
+
readable.cause = error;
|
|
252
|
+
throw readable;
|
|
253
|
+
}
|
|
254
|
+
if (isUnknownOutcome(error)) {
|
|
272
255
|
try {
|
|
273
256
|
await client.documents.releaseLock(documentId, lockId);
|
|
274
257
|
} catch {
|
|
275
258
|
/* the lock expires on its own */
|
|
276
259
|
}
|
|
277
|
-
throw error;
|
|
278
260
|
}
|
|
261
|
+
throw error;
|
|
279
262
|
}
|
|
280
263
|
}
|
|
281
264
|
|
|
@@ -300,7 +283,7 @@ export async function withDocumentLock(
|
|
|
300
283
|
const lockId = info?.lockId ?? minted;
|
|
301
284
|
let keeper = null;
|
|
302
285
|
if (keepAlive) {
|
|
303
|
-
const ttlMs = lockTtlMs(info
|
|
286
|
+
const ttlMs = lockTtlMs(info.expiresAt, client.serverNow().getTime());
|
|
304
287
|
client.documentLockLost = null;
|
|
305
288
|
keeper = new LockKeeper(
|
|
306
289
|
(id) => client.documents.renewLock(id, lockId),
|
package/src/http.js
CHANGED
|
@@ -46,9 +46,11 @@ import { uuidv7 } from "./ids.js";
|
|
|
46
46
|
// does not 409 the next. A batch split past MAX_BATCH_OPS restamps each later
|
|
47
47
|
// request with the version the one before it left (see submitBatch).
|
|
48
48
|
//
|
|
49
|
-
// `noBatch` is not part of that judgment. It marks the
|
|
50
|
-
//
|
|
51
|
-
//
|
|
49
|
+
// `noBatch` is not part of that judgment. It marks the calls a batch cannot
|
|
50
|
+
// carry at all (the multipart media and avatar uploads, the user-data store's
|
|
51
|
+
// put and delete, and minting an API token or an invite, whose answer is a
|
|
52
|
+
// secret the server refuses to keep with a keyed batch's answer) and raises
|
|
53
|
+
// so the caller finds out. A
|
|
52
54
|
// batch inside a batch is refused by the batch itself. Never put it on a read: it turns a swallowed read into a
|
|
53
55
|
// thrown one, which is what the chrome hit when an unrelated import was
|
|
54
56
|
// running. A blobless DELETE beside an upload is not one of them: it carries
|
|
@@ -67,6 +69,23 @@ export const DEFAULT_TIMEOUT_MS = 30000;
|
|
|
67
69
|
// nothing and costs a retry stacked on a write that is still running.
|
|
68
70
|
export const DEFAULT_BATCH_TIMEOUT_MS = 180000;
|
|
69
71
|
|
|
72
|
+
// A query may run for core's own query limit (30 s, `*query-timeout-ms*` in
|
|
73
|
+
// plaid.sql.query.exec), and core then answers 408. The client waits a little
|
|
74
|
+
// longer than that, so the caller gets core's 408 for a query too broad and
|
|
75
|
+
// not a timeout of its own that reads like a lost connection.
|
|
76
|
+
export const DEFAULT_QUERY_TIMEOUT_MS = 35000;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The timeout for a query: the client's own, raised to
|
|
80
|
+
* DEFAULT_QUERY_TIMEOUT_MS when it is shorter. A disabled timeout (0, null)
|
|
81
|
+
* stays disabled.
|
|
82
|
+
*/
|
|
83
|
+
export function queryTimeout(client) {
|
|
84
|
+
const t = client.timeout;
|
|
85
|
+
if (!t || t <= 0) return t;
|
|
86
|
+
return Math.max(t, DEFAULT_QUERY_TIMEOUT_MS);
|
|
87
|
+
}
|
|
88
|
+
|
|
70
89
|
/**
|
|
71
90
|
* Note how far the server's clock is from this machine's, from a response's
|
|
72
91
|
* Date header, for `client.serverNow()`. A time the server stamped (an audit
|
|
@@ -240,10 +259,10 @@ export async function retryWhileBusy(
|
|
|
240
259
|
// ---------------------------------------------------------------------------
|
|
241
260
|
// The Idempotency-Key.
|
|
242
261
|
//
|
|
243
|
-
// Every write that is not a signal (`outOfBand`)
|
|
244
|
-
// call that mints a secret
|
|
245
|
-
// `Idempotency-Key` header, and so does every batch request (a queued
|
|
246
|
-
// none: its batch request carries one). The server keeps the answer to a
|
|
262
|
+
// Every write that is not a signal (`outOfBand`) or a call a batch cannot
|
|
263
|
+
// carry (`noBatch`: an upload, user data, a call that mints a secret) goes out
|
|
264
|
+
// with an `Idempotency-Key` header, and so does every batch request (a queued
|
|
265
|
+
// op has none: its batch request carries one). The server keeps the answer to a
|
|
247
266
|
// keyed write for a day, and the same key sent again for the same request is
|
|
248
267
|
// answered from it with `Idempotent-Replayed: true`, writing nothing. So a
|
|
249
268
|
// write whose answer was lost (no response, 502, 504) can be sent again
|
|
@@ -274,6 +293,22 @@ export function isUnknownOutcome(error) {
|
|
|
274
293
|
return status === 0 || status === 502 || status === 504;
|
|
275
294
|
}
|
|
276
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Whether a create refused with `status` and body `data` was made by an
|
|
298
|
+
* earlier send of the same work: `409 id-taken` for an id in `minted` (a Set
|
|
299
|
+
* of the ids the work minted for its creates), and not for a row deleted
|
|
300
|
+
* since (there is nothing to go on with). The one rule for a lost answer's
|
|
301
|
+
* create. The Python twin is `minted_taken`.
|
|
302
|
+
*/
|
|
303
|
+
export function mintedTaken(status, data, minted) {
|
|
304
|
+
return (
|
|
305
|
+
status === 409 &&
|
|
306
|
+
data?.error === "id-taken" &&
|
|
307
|
+
!data.deleted &&
|
|
308
|
+
!!minted?.has(data.id)
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
277
312
|
/**
|
|
278
313
|
* Run `attempt`, sending it again after each delay in `delaysMs` while it
|
|
279
314
|
* fails with an unknown outcome. Only for a keyed write, whose resend is
|
|
@@ -588,8 +623,7 @@ export function takesIdempotencyKey(method, options = {}) {
|
|
|
588
623
|
return (
|
|
589
624
|
method !== "GET" &&
|
|
590
625
|
!options.outOfBand &&
|
|
591
|
-
!options.noBatch
|
|
592
|
-
!options.noIdempotency
|
|
626
|
+
!options.noBatch
|
|
593
627
|
);
|
|
594
628
|
}
|
|
595
629
|
|
|
@@ -705,8 +739,8 @@ export async function queueRequest(batch, method, path, options = {}) {
|
|
|
705
739
|
* rawBody - Body value passed directly (no transform). Mutually exclusive with body.
|
|
706
740
|
* formData - If true, body is FormData; skip Content-Type header
|
|
707
741
|
* queryParams - Object of query param key/values to append
|
|
708
|
-
* noBatch - If true, throw when made on a batch. Only for calls
|
|
709
|
-
* batch
|
|
742
|
+
* noBatch - If true, throw when made on a batch. Only for calls a
|
|
743
|
+
* batch cannot carry at all (see the note at the
|
|
710
744
|
* top of this file); never for a read.
|
|
711
745
|
* outOfBand - If true, the call is a signal rather than a write of
|
|
712
746
|
* project data: made on a batch it still goes over the
|
|
@@ -721,6 +755,10 @@ export async function queueRequest(batch, method, path, options = {}) {
|
|
|
721
755
|
* skipResponseTransform - Return raw parsed JSON (no transformResponse)
|
|
722
756
|
* noAuth - Skip Authorization header
|
|
723
757
|
* binaryResponse - Return arrayBuffer instead of JSON/text
|
|
758
|
+
* noBusyRetry - If true, a 503 is not retried. For a query: core
|
|
759
|
+
* answers 503 when its queue of large queries stays full
|
|
760
|
+
* until the query's deadline, and asking again at once
|
|
761
|
+
* only joins the same queue.
|
|
724
762
|
* timeout - Per-request timeout in ms overriding client.timeout
|
|
725
763
|
* for this call (0/null disables). Used for known-long
|
|
726
764
|
* ops like project delete.
|
|
@@ -739,6 +777,7 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
739
777
|
timeout,
|
|
740
778
|
onUploadProgress,
|
|
741
779
|
learningOmittedVersion,
|
|
780
|
+
noBusyRetry,
|
|
742
781
|
} = options;
|
|
743
782
|
if (method !== "GET" && omittedStrictDocument(client)) {
|
|
744
783
|
await learnOmittedVersion(client);
|
|
@@ -807,22 +846,23 @@ export async function makeRequest(client, method, path, options = {}) {
|
|
|
807
846
|
};
|
|
808
847
|
|
|
809
848
|
try {
|
|
849
|
+
const onRetry = (info) => client._noteRetry?.(info);
|
|
850
|
+
const busy = noBusyRetry ? { onRetry, retries: 0 } : { onRetry };
|
|
810
851
|
const response = keyed
|
|
811
|
-
? await retryUnknown(() => retryWhileBusy(attempt), {
|
|
852
|
+
? await retryUnknown(() => retryWhileBusy(attempt, busy), {
|
|
812
853
|
delaysMs: client.retryDelaysMs,
|
|
854
|
+
onRetry,
|
|
813
855
|
})
|
|
814
|
-
: await retryWhileBusy(attempt);
|
|
856
|
+
: await retryWhileBusy(attempt, busy);
|
|
815
857
|
noteServerClock(client, response.headers);
|
|
816
858
|
|
|
817
859
|
if (!response.ok) {
|
|
818
860
|
const errorData = await parseErrorBody(response);
|
|
819
|
-
//
|
|
820
|
-
//
|
|
861
|
+
// Answered as made only for a single create, the one whose answer is
|
|
862
|
+
// the id alone. A bulk or a batch taken so is made whole (it is one
|
|
863
|
+
// transaction), and a caller that sends one decides what it answers.
|
|
821
864
|
if (
|
|
822
|
-
response.status
|
|
823
|
-
errorData?.error === "id-taken" &&
|
|
824
|
-
!errorData.deleted &&
|
|
825
|
-
minted?.has(errorData.id) &&
|
|
865
|
+
mintedTaken(response.status, errorData, minted) &&
|
|
826
866
|
requestBody?.id === errorData.id
|
|
827
867
|
) {
|
|
828
868
|
if (stampedGroup) stampedGroup.written = true;
|