@khanacademy/wonder-blocks-data 3.0.0 → 3.1.2

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/CHANGELOG.md CHANGED
@@ -1,7 +1,33 @@
1
1
  # @khanacademy/wonder-blocks-data
2
2
 
3
+ ## 3.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - @khanacademy/wonder-blocks-core@4.2.1
8
+
9
+ ## 3.1.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 4ff59815: Add GraphQL fetch mock support to wonder-blocks-testing
14
+
15
+ ## 3.1.0
16
+
17
+ ### Minor Changes
18
+
19
+ - b68cedfe: Add GqlRouter component
20
+ - c7233a97: Implement useGql hook
21
+
22
+ ## 3.0.1
23
+
24
+ ### Patch Changes
25
+
26
+ - d281dac8: Ensure server-side request fulfillments can be intercepted
27
+
3
28
  ## 3.0.0
29
+
4
30
  ### Major Changes
5
31
 
6
- - b252d9c8: Remove client-side caching
7
- - b252d9c8: Introduce useData hook
32
+ - b252d9c8: Remove client-side caching
33
+ - b252d9c8: Introduce useData hook
package/dist/es/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Server } from '@khanacademy/wonder-blocks-core';
2
2
  import * as React from 'react';
3
- import { useState, useContext, useRef, useEffect } from 'react';
3
+ import { useState, useContext, useRef, useEffect, useMemo } from 'react';
4
4
  import _extends from '@babel/runtime/helpers/extends';
5
+ import { Errors, KindError } from '@khanacademy/wonder-stuff-core';
5
6
 
6
7
  function deepClone(source) {
7
8
  /**
@@ -582,25 +583,49 @@ const useData = (handler, options) => {
582
583
  // this will have cached data in those cases as it will be present on the
583
584
  // initial render - and subsequent renders on the client it will be null.
584
585
  const cachedResult = ResponseCache.Default.getEntry(handler, options);
585
- const [result, setResult] = useState(cachedResult); // We only track data requests when we are server-side and we don't
586
+ const [result, setResult] = useState(cachedResult); // Lookup to see if there's an interceptor for the handler.
587
+ // If we have one, we need to replace the handler with one that
588
+ // uses the interceptor.
589
+
590
+ const interceptorMap = useContext(InterceptContext);
591
+ const interceptor = interceptorMap[handler.type]; // If we have an interceptor, we need to replace the handler with one that
592
+ // uses the interceptor. This helper function generates a new handler.
593
+ // We need this before we track the request as we want the interceptor
594
+ // to also work for tracked requests to simplify testing the server-side
595
+ // request fulfillment.
596
+
597
+ const getMaybeInterceptedHandler = () => {
598
+ if (interceptor == null) {
599
+ return handler;
600
+ }
601
+
602
+ const fulfillRequestFn = options => {
603
+ var _interceptor$fulfillR;
604
+
605
+ return (_interceptor$fulfillR = interceptor.fulfillRequest(options)) != null ? _interceptor$fulfillR : handler.fulfillRequest(options);
606
+ };
607
+
608
+ return {
609
+ fulfillRequest: fulfillRequestFn,
610
+ getKey: options => handler.getKey(options),
611
+ type: handler.type,
612
+ hydrate: handler.hydrate
613
+ };
614
+ }; // We only track data requests when we are server-side and we don't
586
615
  // already have a result, as given by the cachedData (which is also the
587
616
  // initial value for the result state).
588
617
 
618
+
589
619
  const maybeTrack = useContext(TrackerContext);
590
620
 
591
621
  if (result == null && Server.isServerSide()) {
592
- maybeTrack == null ? void 0 : maybeTrack(handler, options);
593
- } // Lookup to see if there's an interceptor for the handler.
594
- // If we have one, we need to replace the handler with one that
595
- // uses the interceptor.
596
-
597
-
598
- const interceptorMap = useContext(InterceptContext);
599
- const interceptor = interceptorMap[handler.type]; // We need to update our request when the handler changes or the key
622
+ maybeTrack == null ? void 0 : maybeTrack(getMaybeInterceptedHandler(), options);
623
+ } // We need to update our request when the handler changes or the key
600
624
  // to the options change, so we keep track of those.
601
625
  // However, even if we are hydrating from cache, we still need to make the
602
626
  // request at least once, so we do not initialize these references.
603
627
 
628
+
604
629
  const handlerRef = useRef();
605
630
  const keyRef = useRef();
606
631
  const interceptorRef = useRef(); // This effect will ensure that we fulfill the request as desired.
@@ -625,26 +650,7 @@ const useData = (handler, options) => {
625
650
  if (cachedResult == null) {
626
651
  // Mark ourselves as loading.
627
652
  setResult(null);
628
- }
629
-
630
- const getMaybeInterceptedHandler = () => {
631
- if (interceptor == null) {
632
- return handler;
633
- }
634
-
635
- const fulfillRequestFn = options => {
636
- var _interceptor$fulfillR;
637
-
638
- return (_interceptor$fulfillR = interceptor.fulfillRequest(options)) != null ? _interceptor$fulfillR : handler.fulfillRequest(options);
639
- };
640
-
641
- return {
642
- fulfillRequest: fulfillRequestFn,
643
- getKey: options => handler.getKey(options),
644
- type: handler.type,
645
- hydrate: handler.hydrate
646
- };
647
- }; // We aren't server-side, so let's make the request.
653
+ } // We aren't server-side, so let's make the request.
648
654
  // The request handler is in control of whether that request actually
649
655
  // happens or not.
650
656
 
@@ -733,6 +739,173 @@ class InterceptData extends React.Component {
733
739
 
734
740
  }
735
741
 
742
+ const GqlRouterContext = /*#__PURE__*/React.createContext(null);
743
+
744
+ /**
745
+ * Configure GraphQL routing for GraphQL hooks and components.
746
+ *
747
+ * These can be nested. Components and hooks relying on the GraphQL routing
748
+ * will use the configuration from their closest ancestral GqlRouter.
749
+ */
750
+ const GqlRouter = ({
751
+ defaultContext: thisDefaultContext,
752
+ fetch: thisFetch,
753
+ children
754
+ }) => {
755
+ // We don't care if we're nested. We always force our callers to define
756
+ // everything. It makes for a clearer API and requires less error checking
757
+ // code (assuming our flow types are correct). We also don't default fetch
758
+ // to anything - our callers can tell us what function to use quite easily.
759
+ // If code that consumes this wants more nuanced nesting, it can implement
760
+ // it within its own GqlRouter than then defers to this one.
761
+ // We want to always use the same object if things haven't changed to avoid
762
+ // over-rendering consumers of our context, let's memoize the configuration.
763
+ // By doing this, if a component under children that uses this context
764
+ // uses React.memo, we won't force it to re-render every time we render
765
+ // because we'll only change the context value if something has actually
766
+ // changed.
767
+ const configuration = React.useMemo(() => ({
768
+ fetch: thisFetch,
769
+ defaultContext: thisDefaultContext
770
+ }), [thisDefaultContext, thisFetch]);
771
+ return /*#__PURE__*/React.createElement(GqlRouterContext.Provider, {
772
+ value: configuration
773
+ }, children);
774
+ };
775
+
776
+ /**
777
+ * Error kinds for GqlError.
778
+ */
779
+ const GqlErrors = Object.freeze(_extends({}, Errors, {
780
+ Network: "Network",
781
+ Parse: "Parse",
782
+ BadResponse: "BadResponse",
783
+ ErrorResult: "ErrorResult"
784
+ }));
785
+ /**
786
+ * An error from the GQL API.
787
+ */
788
+
789
+ class GqlError extends KindError {
790
+ constructor(message, kind, {
791
+ metadata,
792
+ cause
793
+ } = {}) {
794
+ super(message, kind, {
795
+ metadata,
796
+ cause,
797
+ prefix: "Gql"
798
+ });
799
+ }
800
+
801
+ }
802
+
803
+ /**
804
+ * Validate a GQL operation response and extract the data.
805
+ */
806
+
807
+ const getGqlDataFromResponse = async response => {
808
+ // Get the response as text, that way we can use the text in error
809
+ // messaging, should our parsing fail.
810
+ const bodyText = await response.text();
811
+ let result;
812
+
813
+ try {
814
+ result = JSON.parse(bodyText);
815
+ } catch (e) {
816
+ throw new GqlError("Failed to parse response", GqlErrors.Parse, {
817
+ metadata: {
818
+ statusCode: response.status,
819
+ bodyText
820
+ },
821
+ cause: e
822
+ });
823
+ } // Check for a bad status code.
824
+
825
+
826
+ if (response.status >= 300) {
827
+ throw new GqlError("Response unsuccessful", GqlErrors.Network, {
828
+ metadata: {
829
+ statusCode: response.status,
830
+ result
831
+ }
832
+ });
833
+ } // Check that we have a valid result payload.
834
+
835
+
836
+ if ( // Flow shouldn't be warning about this.
837
+ // $FlowIgnore[method-unbinding]
838
+ !Object.prototype.hasOwnProperty.call(result, "data") && // Flow shouldn't be warning about this.
839
+ // $FlowIgnore[method-unbinding]
840
+ !Object.prototype.hasOwnProperty.call(result, "errors")) {
841
+ throw new GqlError("Server response missing", GqlErrors.BadResponse, {
842
+ metadata: {
843
+ statusCode: response.status,
844
+ result
845
+ }
846
+ });
847
+ } // If the response payload has errors, throw an error.
848
+
849
+
850
+ if (result.errors != null && Array.isArray(result.errors) && result.errors.length > 0) {
851
+ throw new GqlError("GraphQL errors", GqlErrors.ErrorResult, {
852
+ metadata: {
853
+ statusCode: response.status,
854
+ result
855
+ }
856
+ });
857
+ } // We got here, so return the data.
858
+
859
+
860
+ return result.data;
861
+ };
862
+
863
+ /**
864
+ * Hook to obtain a gqlFetch function for performing GraphQL requests.
865
+ *
866
+ * The fetch function will resolve null if the request was aborted, otherwise
867
+ * it will resolve the data returned by the GraphQL server.
868
+ */
869
+ const useGql = () => {
870
+ // This hook only works if the `GqlRouter` has been used to setup context.
871
+ const gqlRouterContext = useContext(GqlRouterContext);
872
+
873
+ if (gqlRouterContext == null) {
874
+ throw new GqlError("No GqlRouter", GqlErrors.Internal);
875
+ }
876
+
877
+ const {
878
+ fetch,
879
+ defaultContext
880
+ } = gqlRouterContext; // Let's memoize the gqlFetch function we create based off our context.
881
+ // That way, even if the context happens to change, if its values don't
882
+ // we give the same function instance back to our callers instead of
883
+ // making a new one. That then means they can safely use the return value
884
+ // in hooks deps without fear of it triggering extra renders.
885
+
886
+ const gqlFetch = useMemo(() => (operation, options = Object.freeze({})) => {
887
+ const {
888
+ variables,
889
+ context
890
+ } = options; // Invoke the fetch and extract the data.
891
+
892
+ return fetch(operation, variables, _extends({}, defaultContext, context)).then(getGqlDataFromResponse, error => {
893
+ // Return null if the request was aborted.
894
+ // The only way to detect this reliably, it seems, is to
895
+ // check the error name and see if it's "AbortError" (this
896
+ // is also what Apollo does).
897
+ // Even then, it's reliant on the fetch supporting aborts.
898
+ if (error.name === "AbortError") {
899
+ return null;
900
+ } // Need to make sure we pass other errors along.
901
+
902
+
903
+ throw error;
904
+ });
905
+ }, [fetch, defaultContext]);
906
+ return gqlFetch;
907
+ };
908
+
736
909
  const initializeCache = source => ResponseCache.Default.initialize(source);
737
910
  const fulfillAllDataRequests = () => {
738
911
  if (!Server.isServerSide()) {
@@ -751,4 +924,4 @@ const hasUnfulfilledRequests = () => {
751
924
  const removeFromCache = (handler, options) => ResponseCache.Default.remove(handler, options);
752
925
  const removeAllFromCache = (handler, predicate) => ResponseCache.Default.removeAll(handler, predicate);
753
926
 
754
- export { Data, InterceptData, RequestHandler, TrackData, fulfillAllDataRequests, hasUnfulfilledRequests, initializeCache, removeAllFromCache, removeFromCache, useData };
927
+ export { Data, GqlError, GqlErrors, GqlRouter, InterceptData, RequestHandler, TrackData, fulfillAllDataRequests, hasUnfulfilledRequests, initializeCache, removeAllFromCache, removeFromCache, useData, useGql };