@larc-iu/plaid-client 0.2.0-alpha.13 → 0.2.0-alpha.16

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/src/http.js CHANGED
@@ -1,5 +1,52 @@
1
1
  import { transformRequest, transformResponse } from "./transforms.js";
2
2
 
3
+ // ---------------------------------------------------------------------------
4
+ // Batches: which calls a batch carries, and which go over the wire anyway.
5
+ //
6
+ // A batch is an OBJECT (`client.batch()`, or the argument `client.batched(fn)`
7
+ // hands `fn`), a view of the client with the same bundles. A write made on the
8
+ // batch queues; a call made on the client itself always goes over the wire,
9
+ // whatever batches happen to be open. So the editor, the importer and the
10
+ // app's chrome can share one client, and a write by code that knows nothing
11
+ // about a batch can never land inside it. Every call reaching this layer is
12
+ // one of three things, and the third column is what a new endpoint has to
13
+ // pick.
14
+ //
15
+ // 1. A WRITE of project data: an entity, its metadata, a layer, a membership,
16
+ // a comment. This is what a batch is for, so on a batch it QUEUES. It is
17
+ // the default and needs no flag.
18
+ //
19
+ // 2. A READ. Every GET is answered from the wire even when made on a batch:
20
+ // the caller gets data rather than `{ batched: true }`, it takes no slot in
21
+ // the batch's results, and server-side it runs against the pool rather than
22
+ // the batch's transaction connection. A read that travels as a POST
23
+ // (`query`) says so with `outOfBand`.
24
+ //
25
+ // 3. An OUT-OF-BAND SIGNAL: stopping a service request, reporting a service's
26
+ // progress or result, taking or dropping a document lock, forgetting a
27
+ // service, an admin action on the server itself. It is shaped like a write
28
+ // but carries no project data, and its whole value is that it happens NOW.
29
+ // Pass `outOfBand`: made on a batch it still goes over the wire, and it
30
+ // never joins an open logical operation. The audit group is a label for
31
+ // the writes, and a signal that is never audited would mark the group
32
+ // written and leave the relabel PATCH 404ing on a group nothing ever
33
+ // created.
34
+ //
35
+ // Strict mode stamps the batch's one expected document-version onto the first
36
+ // write queued on it (whole-batch OCC: stamping every op would 409 the second
37
+ // against the bump the first caused). A call that went over the wire on its
38
+ // own carries its own stamp and spends nothing of any batch's.
39
+ //
40
+ // `noBatch` is not part of that judgment. It marks the five calls the batch
41
+ // transport cannot carry at all (a batch inside a batch, the multipart media
42
+ // and avatar uploads, the user-data store's put and delete) and raises so the
43
+ // caller finds out. Never put it on a read: it turns a swallowed read into a
44
+ // thrown one, which is what the chrome hit when an unrelated import was
45
+ // running. A blobless DELETE beside an upload is not one of them: it carries
46
+ // nothing the transport cannot express, so it takes its class from the three
47
+ // above like anything else.
48
+ // ---------------------------------------------------------------------------
49
+
3
50
  // Default per-request timeout (ms). Applied to every request unless the client
4
51
  // is constructed with a different `timeout` (0 / null disables it). Note: this
5
52
  // also bounds media up/downloads — bump it (or disable) for very large files.
@@ -253,14 +300,13 @@ export function xhrSend(
253
300
  * rawBody - Body value passed directly (no transform). Mutually exclusive with body.
254
301
  * formData - If true, body is FormData; skip Content-Type header
255
302
  * queryParams - Object of query param key/values to append
256
- * noBatch - If true, throw when in batch mode
257
- * bypassBatch - If true, go over the wire even while a batch is open.
258
- * For READS only: a batch is a write transaction, its
259
- * ops return no value to their caller until submit, and
260
- * the sub-request runs against the tx Connection rather
261
- * than the pool. A read swallowed by an ambient batch is
262
- * therefore useless to the caller AND can fail the whole
263
- * batch (see `query`).
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).
264
310
  * skipResponseTransform - Return raw parsed JSON (no transformResponse)
265
311
  * noAuth - Skip Authorization header
266
312
  * binaryResponse - Return arrayBuffer instead of JSON/text
@@ -273,21 +319,34 @@ export function xhrSend(
273
319
  * where the timeout only fires when the upload stalls;
274
320
  * elsewhere the callback is ignored and fetch is used.
275
321
  */
276
- export async function makeRequest(client, method, path, options = {}) {
277
- const {
278
- body,
279
- rawBody,
280
- formData,
281
- queryParams,
282
- noBatch,
283
- bypassBatch,
284
- skipResponseTransform,
285
- noAuth,
286
- binaryResponse,
287
- auditMessage,
288
- timeout,
289
- onUploadProgress,
290
- } = options;
322
+ /**
323
+ * Everything a request is before it goes anywhere: the URL with its query
324
+ * params and the stamps strict mode, a per-call audit message and an open
325
+ * logical operation add, plus the transformed body. Shared by the wire path
326
+ * (`makeRequest`) and the batch path (`queueRequest`), so a queued op is
327
+ * exactly the request that would have gone out. `batch` is the batch the call
328
+ * is being queued on, if any: strict mode stamps its document-version onto the
329
+ * first write queued there and no other.
330
+ */
331
+ export function prepareRequest(
332
+ client,
333
+ method,
334
+ path,
335
+ options = {},
336
+ batch = null,
337
+ ) {
338
+ const { body, rawBody, formData, queryParams, outOfBand, auditMessage } =
339
+ options;
340
+
341
+ // A write must not go out on a lock that lapsed. `documents.locked()`
342
+ // records the loss here when its keep-alive cannot renew, and from that
343
+ // moment the block is holding nothing: an edit by somebody else can already
344
+ // have landed between the read the work was planned from and the write about
345
+ // to go out. Reads pass, and so do the lock routes themselves, which is how
346
+ // the block still releases on its way out.
347
+ if (client.documentLockLost && method !== "GET" && !path.endsWith("/lock")) {
348
+ throw client.documentLockLost;
349
+ }
291
350
 
292
351
  // Build URL
293
352
  let url = `${client.baseUrl}${path}`;
@@ -317,23 +376,19 @@ export async function makeRequest(client, method, path, options = {}) {
317
376
  requestBody = transformRequest(body);
318
377
  }
319
378
 
320
- // Strict mode: append document-version for non-GET requests.
321
- // Inside a batch, stamp ONLY the first write: batches run atomically
322
- // server-side, so a version check on the first op gives whole-batch OCC
323
- // semantics, while stamping every op would 409 the second op against the
324
- // version bump the first op itself caused (every queued op captures the
325
- // same pre-batch version).
326
- if (
327
- client.strictModeDocumentId &&
328
- method !== "GET" &&
329
- !(client.isBatching && client.batchVersionStamped)
330
- ) {
379
+ // Strict mode: append document-version for non-GET requests, on EVERY
380
+ // queued write of a batch. The server validates the first write it can
381
+ // (one whose route resolves a document) and skips the rest of that
382
+ // document's, so the version bump a sub-op causes does not 409 the next
383
+ // one. Stamping only the first write was silently no check at all
384
+ // whenever that write was one the route ignores, such as a vocabulary
385
+ // entry's metadata, which is exactly what the igt editor queues first.
386
+ if (client.strictModeDocumentId && method !== "GET") {
331
387
  const docId = client.strictModeDocumentId;
332
388
  if (client.documentVersions[docId]) {
333
389
  const docVersion = client.documentVersions[docId];
334
390
  const separator = url.includes("?") ? "&" : "?";
335
391
  url += `${separator}document-version=${encodeURIComponent(docVersion)}`;
336
- if (client.isBatching) client.batchVersionStamped = true;
337
392
  }
338
393
  }
339
394
 
@@ -350,7 +405,12 @@ export async function makeRequest(client, method, path, options = {}) {
350
405
  // Logical-operation group (see client.beginOperation): stamp every write
351
406
  // with the group id; the message rides along too so the server can label
352
407
  // the group lazily on whichever tagged write lands first.
353
- if (client.operationGroup && method !== "GET") {
408
+ //
409
+ // An out-of-band signal is not one of those writes (see the note at the top
410
+ // of this file): it never lands in the audit log, so a stamp does nothing
411
+ // server-side while `written` promises a group that will never exist, and
412
+ // the relabel PATCH then 404s.
413
+ if (client.operationGroup && method !== "GET" && !outOfBand) {
354
414
  const group = client.operationGroup;
355
415
  const separator = url.includes("?") ? "&" : "?";
356
416
  url += `${separator}group-id=${encodeURIComponent(group.id)}`;
@@ -359,24 +419,54 @@ export async function makeRequest(client, method, path, options = {}) {
359
419
  group.written = true;
360
420
  }
361
421
 
362
- // Batch mode. A `bypassBatch` read is deliberately NOT queued: it belongs to
363
- // whoever called it, not to whatever batch happens to be open on this shared
364
- // client (a document reconcile, an import), and it reads the pre-batch state
365
- // exactly as it would have if the batch were not running.
366
- if (client.isBatching && !bypassBatch) {
367
- if (noBatch) {
368
- throw new Error(`This endpoint cannot be used in batch mode: ${path}`);
369
- }
370
- const operation = {
371
- path: url.replace(client.baseUrl, ""),
372
- method: method.toUpperCase(),
373
- };
374
- if (requestBody !== undefined) {
375
- operation.body = requestBody;
376
- }
377
- client.batchOperations.push(operation);
378
- return { batched: true };
422
+ return { url, requestBody };
423
+ }
424
+
425
+ /**
426
+ * A call made on a batch (see `PlaidClient#batch`). A write of project data is
427
+ * queued as one operation of the batch and answers `{ batched: true }`; its
428
+ * result is the matching entry of what `submit()` resolves to. A read, and a
429
+ * signal marked `outOfBand`, is the client's to make and goes over the wire
430
+ * now, exactly as if it had been made on the client.
431
+ */
432
+ export async function queueRequest(batch, method, path, options = {}) {
433
+ if (method === "GET" || options.outOfBand) {
434
+ return batch.client._request(method, path, options);
435
+ }
436
+ if (!batch.open) {
437
+ throw new Error(`This batch was already submitted or aborted: ${path}`);
379
438
  }
439
+ if (options.noBatch) {
440
+ throw new Error(`This endpoint cannot be used in a batch: ${path}`);
441
+ }
442
+ const { url, requestBody } = prepareRequest(
443
+ batch.client,
444
+ method,
445
+ path,
446
+ options,
447
+ batch,
448
+ );
449
+ const operation = {
450
+ path: url.replace(batch.client.baseUrl, ""),
451
+ method: method.toUpperCase(),
452
+ };
453
+ if (requestBody !== undefined) {
454
+ operation.body = requestBody;
455
+ }
456
+ batch.operations.push(operation);
457
+ return { batched: true };
458
+ }
459
+
460
+ export async function makeRequest(client, method, path, options = {}) {
461
+ const {
462
+ formData,
463
+ skipResponseTransform,
464
+ noAuth,
465
+ binaryResponse,
466
+ timeout,
467
+ onUploadProgress,
468
+ } = options;
469
+ const { url, requestBody } = prepareRequest(client, method, path, options);
380
470
 
381
471
  // Build fetch options
382
472
  const headers = {};