@larc-iu/plaid-client 0.2.0-alpha.29 → 0.2.0-alpha.30

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.
Files changed (49) hide show
  1. package/package.json +3 -2
  2. package/src/apiAnnotations.d.ts +508 -0
  3. package/src/apiAnnotations.js +543 -0
  4. package/src/apiDocuments.d.ts +569 -0
  5. package/src/apiDocuments.js +551 -0
  6. package/src/apiLayers.d.ts +369 -0
  7. package/src/apiLayers.js +222 -0
  8. package/src/apiMessages.d.ts +293 -0
  9. package/src/apiMessages.js +265 -0
  10. package/src/apiPeople.d.ts +626 -0
  11. package/src/apiPeople.js +698 -0
  12. package/src/apiProject.d.ts +761 -0
  13. package/src/apiProject.js +774 -0
  14. package/src/apiServer.d.ts +455 -0
  15. package/src/apiServer.js +422 -0
  16. package/src/apiVocab.d.ts +456 -0
  17. package/src/apiVocab.js +425 -0
  18. package/src/batch.d.ts +54 -0
  19. package/src/batch.js +323 -0
  20. package/src/client.d.ts +1321 -0
  21. package/src/client.js +743 -0
  22. package/src/constraints.d.ts +8 -0
  23. package/src/constraints.js +14 -0
  24. package/src/documentLock.d.ts +18 -4
  25. package/src/documentLock.js +43 -9
  26. package/src/entryForms.d.ts +63 -0
  27. package/src/entryForms.js +152 -0
  28. package/src/events.js +2 -1
  29. package/src/families.d.ts +349 -0
  30. package/src/families.js +396 -0
  31. package/src/http.d.ts +39 -18
  32. package/src/http.js +127 -87
  33. package/src/index.d.ts +90 -4557
  34. package/src/index.js +113 -5172
  35. package/src/operations.d.ts +36 -0
  36. package/src/operations.js +164 -0
  37. package/src/provenance.d.ts +47 -4
  38. package/src/provenance.js +168 -12
  39. package/src/roles.d.ts +39 -0
  40. package/src/roles.js +44 -0
  41. package/src/serviceSchema.d.ts +19 -13
  42. package/src/serviceSchema.js +30 -15
  43. package/src/services.d.ts +20 -3
  44. package/src/services.js +45 -8
  45. package/src/sse.js +10 -1
  46. package/src/testing.d.ts +128 -0
  47. package/src/testing.js +379 -0
  48. package/src/types.d.ts +867 -0
  49. package/src/types.js +663 -0
package/src/http.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { markReplayed } from "./replayed.js";
2
2
  import { transformRequest, transformResponse } from "./transforms.js";
3
- import { takeRefs } from "./batchRef.js";
4
- import { uuidv7 } from "./ids.js";
3
+ import { nextIdempotencyKey } from "./operations.js";
5
4
 
6
5
  // ---------------------------------------------------------------------------
7
6
  // Batches: which calls a batch carries, and which go over the wire anyway.
@@ -268,7 +267,7 @@ export async function retryWhileBusy(
268
267
  // keyed write for a day, and the same key sent again for the same request is
269
268
  // answered from it with `Idempotent-Replayed: true`, writing nothing. So a
270
269
  // write whose answer was lost (no response, 502, 504) can be sent again
271
- // safely: it lands once. `makeRequest` and `_postBatch` do that three times
270
+ // safely: it lands once. `makeRequest` and `postBatch` do that three times
272
271
  // themselves (retryUnknown), and the error that escapes carries the key as
273
272
  // `idempotencyKey`.
274
273
  //
@@ -338,29 +337,6 @@ export async function retryUnknown(
338
337
  }
339
338
  }
340
339
 
341
- /**
342
- * The key for the next keyed request, and the strict-mode stamp it must
343
- * claim when an earlier run of its operation already sent it. `joins` says
344
- * whether the request joins the open logical operation.
345
- * Returns `{ key, pin, record }`: `pin` is undefined when nothing is pinned,
346
- * and `record(version)` pins what this request claimed, the first time.
347
- */
348
- export function nextIdempotencyKey(client, joins) {
349
- const frame = joins ? client.operationGroup?.frames.at(-1) : null;
350
- const keys = frame?.keys;
351
- if (!keys) return { key: uuidv7(), pin: undefined, record: () => {} };
352
- const n = frame.count;
353
- frame.count += 1;
354
- const pinned = keys.stamps.has(n);
355
- return {
356
- key: `${keys.seed}.${n}`,
357
- pin: pinned ? keys.stamps.get(n) : undefined,
358
- record: (version) => {
359
- if (!pinned) keys.stamps.set(n, version ?? null);
360
- },
361
- };
362
- }
363
-
364
340
  /**
365
341
  * Build a fetch AbortSignal that fires after `timeout` ms, or undefined when
366
342
  * timeouts are disabled / unsupported.
@@ -699,43 +675,49 @@ async function learnOmittedVersionQuietly(client) {
699
675
  }
700
676
 
701
677
  /**
702
- * A call made on a batch (see `PlaidClient#batch`). A write of project data is
703
- * queued as one operation of the batch and answers `{ batched: true }`; its
704
- * result is the matching entry of what `submit()` resolves to. A read, and a
705
- * signal marked `outOfBand`, is the client's to make and goes over the wire
706
- * now, exactly as if it had been made on the client.
678
+ * One send of a request by fetch, under a fresh timeout signal: an
679
+ * AbortSignal.timeout fires once and stays aborted, so a retry needs its own.
680
+ * `timeoutMs` 0 or null sends with none.
707
681
  */
708
- export async function queueRequest(batch, method, path, options = {}) {
709
- if (method === "GET" || options.outOfBand) {
710
- return batch.client._request(method, path, options);
711
- }
712
- if (!batch.open) {
713
- throw new Error(`This batch was already submitted or aborted: ${path}`);
714
- }
715
- if (options.noBatch) {
716
- throw new Error(`This endpoint cannot be used in a batch: ${path}`);
717
- }
718
- const { url, requestBody, stampedDocument, stampedGroup } = prepareRequest(
719
- batch.client,
720
- method,
721
- path,
722
- options,
723
- );
724
- const operation = {
725
- path: url.replace(batch.client.baseUrl, ""),
726
- method: method.toUpperCase(),
727
- };
728
- if (requestBody !== undefined) {
729
- // Each b.ref() in the body goes beside it, as the path to a null the
730
- // server fills with the id (batchRef.js).
731
- const { body, refs } = takeRefs(batch, requestBody);
732
- operation.body = body;
733
- if (refs) operation.refs = refs;
682
+ export function fetchWithTimeout(url, fetchOptions, timeoutMs) {
683
+ const signal = timeoutSignal(timeoutMs);
684
+ return fetch(url, signal ? { ...fetchOptions, signal } : fetchOptions);
685
+ }
686
+
687
+ /**
688
+ * Run `attempt` with the client's retries: a 503 is retried (unless
689
+ * `noBusyRetry`), and a keyed request whose answer was lost is sent again
690
+ * under its key (retryUnknown). Each retry is told to `onRetry` listeners.
691
+ */
692
+ export function withRetries(client, attempt, { keyed = false, noBusyRetry = false } = {}) {
693
+ const onRetry = (info) => client._noteRetry?.(info);
694
+ const busy = noBusyRetry ? { onRetry, retries: 0 } : { onRetry };
695
+ return keyed
696
+ ? retryUnknown(() => retryWhileBusy(attempt, busy), {
697
+ delaysMs: client.retryDelaysMs,
698
+ onRetry,
699
+ })
700
+ : retryWhileBusy(attempt, busy);
701
+ }
702
+
703
+ /**
704
+ * A 401 means the token is missing, expired or invalid. Fire the app's
705
+ * auth-error handler once per client (it discards the token and routes back
706
+ * to login). A 403 (authenticated but not permitted) deliberately does not.
707
+ */
708
+ export function noteAuthError(client, error) {
709
+ if (
710
+ error?.status === 401 &&
711
+ typeof client.onAuthError === "function" &&
712
+ !client._authErrorFired
713
+ ) {
714
+ client._authErrorFired = true;
715
+ try {
716
+ client.onAuthError(error);
717
+ } catch (_) {
718
+ /* handler must not mask the original error */
719
+ }
734
720
  }
735
- batch.operations.push(operation);
736
- batch.stampedDocuments.push(stampedDocument);
737
- batch.stampedGroups.push(stampedGroup);
738
- return { batched: true };
739
721
  }
740
722
 
741
723
  /**
@@ -762,6 +744,10 @@ export async function queueRequest(batch, method, path, options = {}) {
762
744
  * skipResponseTransform - Return raw parsed JSON (no transformResponse)
763
745
  * noAuth - Skip Authorization header
764
746
  * binaryResponse - Return arrayBuffer instead of JSON/text
747
+ * blobResponse - Return a Blob of the body, its `type` the response's
748
+ * Content-Type (a recording, whose reader needs both)
749
+ * onDownloadProgress - With blobResponse, called with `{ loaded, total }`
750
+ * (bytes; total null when unknown) as the body arrives
765
751
  * noBusyRetry - If true, a 503 is not retried. For a query: core
766
752
  * answers 503 when its queue of large queries stays full
767
753
  * until the query's deadline, and asking again at once
@@ -775,12 +761,72 @@ export async function queueRequest(batch, method, path, options = {}) {
775
761
  * where the timeout only fires when the upload stalls;
776
762
  * elsewhere the callback is ignored and fetch is used.
777
763
  */
764
+ /**
765
+ * What a write about to go out needs to tell the lock keepers it renewed:
766
+ * each held document's keeper, its version now and the send time. Empty when
767
+ * no `documents.locked()` block on `client` keeps a lock. The server renews a
768
+ * live lock on every write its holder makes to the document, so a write that
769
+ * moved the held document's version renewed it (`writesLanded`).
770
+ * @private
771
+ */
772
+ export function writesStarted(client) {
773
+ const keepers = client.lockKeepers;
774
+ if (!keepers || keepers.size === 0) return [];
775
+ return [...keepers.entries()].map(([docId, keeper]) => [
776
+ keeper,
777
+ client.documentVersions?.[docId],
778
+ keeper.now(),
779
+ ]);
780
+ }
781
+
782
+ /**
783
+ * The write `started` was read before has been taken, and its answer's
784
+ * versions learned. A held document whose version it moved was written by
785
+ * this run. A replayed answer was not taken again, and renewed nothing.
786
+ * @private
787
+ */
788
+ export function writesLanded(client, started, replayed = false) {
789
+ if (replayed || !started?.length) return;
790
+ for (const [keeper, before, sent] of started) {
791
+ const after = client.documentVersions?.[keeper.documentId];
792
+ if (before != null && after != null && after !== before) {
793
+ keeper.noteWrite(sent);
794
+ }
795
+ }
796
+ }
797
+
798
+ /**
799
+ * A response's body as a Blob typed by its Content-Type, `onProgress` told
800
+ * `{ loaded, total }` after each piece when given (total null when the
801
+ * response has no Content-Length).
802
+ * @private
803
+ */
804
+ async function readBlob(response, onProgress) {
805
+ const type = response.headers.get("content-type") ?? "";
806
+ const reader = onProgress ? response.body?.getReader?.() : null;
807
+ if (!reader) return new Blob([await response.arrayBuffer()], { type });
808
+ const length = Number(response.headers.get("content-length"));
809
+ const total = length > 0 ? length : null;
810
+ const parts = [];
811
+ let loaded = 0;
812
+ for (;;) {
813
+ const { done, value } = await reader.read();
814
+ if (done) break;
815
+ parts.push(value);
816
+ loaded += value.byteLength;
817
+ onProgress({ loaded, total });
818
+ }
819
+ return new Blob(parts, { type });
820
+ }
821
+
778
822
  export async function makeRequest(client, method, path, options = {}) {
779
823
  const {
780
824
  formData,
781
825
  skipResponseTransform,
782
826
  noAuth,
783
827
  binaryResponse,
828
+ blobResponse,
829
+ onDownloadProgress,
784
830
  timeout,
785
831
  onUploadProgress,
786
832
  learningOmittedVersion,
@@ -829,13 +875,19 @@ export async function makeRequest(client, method, path, options = {}) {
829
875
  timeout: timeoutMs,
830
876
  });
831
877
  }
832
- const signal = timeoutSignal(timeoutMs);
833
- return fetch(url, signal ? { ...fetchOptions, signal } : fetchOptions);
878
+ return fetchWithTimeout(url, fetchOptions, timeoutMs);
834
879
  };
835
880
 
836
881
  // One attempt: a 503, and for a keyed write a 502 or 504, surfaces as a
837
882
  // throw so the retries below can see it. Other failures are thrown from
838
883
  // the checks after it.
884
+ // A write that lands on a held document renews its lock on the server, so
885
+ // its keeper hears of it (writesLanded).
886
+ const started =
887
+ method !== "GET" && !options.outOfBand ? writesStarted(client) : [];
888
+ const landed = (headers) =>
889
+ writesLanded(client, started, headers.get(REPLAYED_HEADER) === "true");
890
+
839
891
  const attempt = async () => {
840
892
  let res;
841
893
  try {
@@ -853,14 +905,7 @@ export async function makeRequest(client, method, path, options = {}) {
853
905
  };
854
906
 
855
907
  try {
856
- const onRetry = (info) => client._noteRetry?.(info);
857
- const busy = noBusyRetry ? { onRetry, retries: 0 } : { onRetry };
858
- const response = keyed
859
- ? await retryUnknown(() => retryWhileBusy(attempt, busy), {
860
- delaysMs: client.retryDelaysMs,
861
- onRetry,
862
- })
863
- : await retryWhileBusy(attempt, busy);
908
+ const response = await withRetries(client, attempt, { keyed, noBusyRetry });
864
909
  noteServerClock(client, response.headers);
865
910
 
866
911
  if (!response.ok) {
@@ -876,32 +921,25 @@ export async function makeRequest(client, method, path, options = {}) {
876
921
  return transformResponse({ id: errorData.id });
877
922
  }
878
923
  const error = makeHttpError(response, errorData, url, method);
879
- // 401 means the token is missing/expired/invalid. Fire the app's auth-error
880
- // handler once (it discards the token and routes back to login). 403
881
- // (forbidden — authenticated but not permitted) deliberately does NOT.
882
- if (
883
- response.status === 401 &&
884
- typeof client.onAuthError === "function" &&
885
- !client._authErrorFired
886
- ) {
887
- client._authErrorFired = true;
888
- try {
889
- client.onAuthError(error);
890
- } catch (_) {
891
- /* handler must not mask the original error */
892
- }
893
- }
924
+ noteAuthError(client, error);
894
925
  throw error;
895
926
  }
896
927
  // The server took it, so the operation's group exists.
897
928
  if (stampedGroup) stampedGroup.written = true;
898
929
 
899
- // Binary response (getMedia)
930
+ // Binary response (users.getAvatar)
900
931
  if (binaryResponse) {
901
932
  extractDocumentVersions(client, response.headers);
933
+ landed(response.headers);
902
934
  if (!learningOmittedVersion) await learnOmittedVersionQuietly(client);
903
935
  return await response.arrayBuffer();
904
936
  }
937
+ if (blobResponse) {
938
+ extractDocumentVersions(client, response.headers);
939
+ landed(response.headers);
940
+ if (!learningOmittedVersion) await learnOmittedVersionQuietly(client);
941
+ return await readBlob(response, onDownloadProgress);
942
+ }
905
943
 
906
944
  // JSON or text response
907
945
  const contentType = response.headers.get("content-type");
@@ -910,6 +948,7 @@ export async function makeRequest(client, method, path, options = {}) {
910
948
  extractDocumentVersions(client, response.headers, data, {
911
949
  historical: /[?&]as-of=/.test(url),
912
950
  });
951
+ landed(response.headers);
913
952
  if (!learningOmittedVersion) await learnOmittedVersionQuietly(client);
914
953
  const answer = skipResponseTransform ? data : transformResponse(data);
915
954
  return response.headers.get(REPLAYED_HEADER) === "true"
@@ -917,6 +956,7 @@ export async function makeRequest(client, method, path, options = {}) {
917
956
  : answer;
918
957
  } else {
919
958
  extractDocumentVersions(client, response.headers);
959
+ landed(response.headers);
920
960
  if (!learningOmittedVersion) await learnOmittedVersionQuietly(client);
921
961
  return await response.text();
922
962
  }