@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 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
- /** The `code` is returned ONCE and is not recoverable afterward. */
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(documentId: string, auditMessage?: string): Promise<any>;
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?: { id?: string; kind?: OperationKind; ref?: string; keys?: KeySeed; minted?: Set<string> },
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; id?: string; keys?: KeySeed; minted?: Set<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 asking Plaid to take a text edit plainly on the layer (see the manual's "Changing a text's body"). */
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larc-iu/plaid-client",
3
- "version": "0.2.0-alpha.20",
3
+ "version": "0.2.0-alpha.22",
4
4
  "description": "JavaScript client for the Plaid annotation API",
5
5
  "type": "module",
6
6
  "repository": {
@@ -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?,
@@ -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 stamp the lock endpoints return.
66
- * Comparing it against this machine's clock is the only way to learn a window
67
- * an operator has retuned, and it is also the one place a clock skew can get
68
- * in, so an answer outside a believable band falls back to the documented
69
- * default rather than to a beat that never fires or fires constantly.
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
- * @param {number|undefined} expiresAt
72
- * @param {number} nowMs
73
- * @param {number} [fallback]
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, nowMs, fallback = DOCUMENT_LOCK_TTL_MS) {
77
- if (typeof expiresAt === "number" && Number.isFinite(expiresAt)) {
78
- const ttl = expiresAt - nowMs;
79
- if (ttl > 0 && ttl <= MAX_BELIEVABLE_TTL_MS) return ttl;
80
- }
81
- return fallback;
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
- // A window under two seconds leaves no room for a retry; treat it as a
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
- * Tries a `locked()` block makes at an acquire whose outcome is unknown, and the
223
- * pause before the next, which grows by this much each time.
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, which the server
241
- * answers 200 while that holder has it. When every attempt is unknown, the lock
242
- * it may hold is released on the way out, so it does not stand in everyone's
243
- * way until it expires.
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
- for (let attempt = 0; ; attempt += 1) {
247
- try {
248
- return await client.documents.acquireLock(documentId, undefined, lockId);
249
- } catch (error) {
250
- if (error?.status === 423) {
251
- // The error body is raw JSON off the wire, so it is still kebab-cased.
252
- const body = error.responseData || {};
253
- const holder = body.userId || body["user-id"] || "another user";
254
- const readable = new Error(
255
- `This document is being edited by ${holder}. Try again once they're done.`,
256
- );
257
- readable.status = 423;
258
- readable.statusText = error.statusText;
259
- readable.url = error.url;
260
- readable.method = error.method;
261
- readable.responseData = error.responseData;
262
- readable.cause = error;
263
- throw readable;
264
- }
265
- if (!UNKNOWN_OUTCOME_STATUSES.has(error?.status)) throw error;
266
- if (attempt + 1 < LOCK_ACQUIRE_ATTEMPTS) {
267
- await new Promise((resolve) =>
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?.expiresAt, Date.now());
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 four calls the batch
50
- // transport cannot carry at all (the multipart media and avatar uploads, the
51
- // user-data store's put and delete) and raises so the caller finds out. A
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`), an upload (`noBatch`) or a
244
- // call that mints a secret (`noIdempotency`) goes out with an
245
- // `Idempotency-Key` header, and so does every batch request (a queued op has
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 the
709
- * batch transport cannot carry at all (see the note at the
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
- // Only a single create: a bulk refused whole made none of its rows.
820
- // Never a deleted row: there is nothing to open.
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 === 409 &&
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;