@warp-drive/ember 0.0.1 → 0.0.3

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/LICENSE.md CHANGED
@@ -1,11 +1,23 @@
1
- The MIT License (MIT)
1
+ MIT License
2
2
 
3
- Copyright (C) 2017-2023 Ember.js contributors
4
- Portions Copyright (C) 2011-2017 Tilde, Inc. and contributors.
5
- Portions Copyright (C) 2011 LivingSocial Inc.
3
+ Copyright (c) 2017-2025 Ember.js and contributors
4
+ Copyright (c) 2011-2017 Tilde, Inc. and contributors
5
+ Copyright (c) 2011 LivingSocial Inc.
6
6
 
7
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
8
13
 
9
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
10
16
 
11
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  <p align="center">
2
2
  <img
3
3
  class="project-logo"
4
- src="./NCC-1701-a-blue.svg#gh-light-mode-only"
4
+ src="./logos/NCC-1701-a-blue.svg#gh-light-mode-only"
5
5
  alt="WarpDrive"
6
6
  width="120px"
7
7
  title="WarpDrive" />
8
8
  <img
9
9
  class="project-logo"
10
- src="./NCC-1701-a.svg#gh-dark-mode-only"
10
+ src="./logos/NCC-1701-a.svg#gh-dark-mode-only"
11
11
  alt="WarpDrive"
12
12
  width="120px"
13
13
  title="WarpDrive" />
package/dist/index.js CHANGED
@@ -70,6 +70,10 @@ function initializeDeferredDecorator(target, prop) {
70
70
  });
71
71
  }
72
72
  }
73
+
74
+ /**
75
+ * @module @warp-drive/ember
76
+ */
73
77
  const RequestCache = new WeakMap();
74
78
  function isAbortError(error) {
75
79
  return error instanceof DOMException && error.name === 'AbortError';
@@ -84,8 +88,6 @@ async function watchStream(stream, state) {
84
88
  state._isPending = false;
85
89
  state._isStarted = true;
86
90
  state._startTime = performance.now();
87
-
88
- // eslint-disable-next-line no-constant-condition
89
91
  while (true) {
90
92
  const {
91
93
  value,
@@ -129,6 +131,15 @@ async function watchStream(stream, state) {
129
131
  state._isComplete = true;
130
132
  state._isStarted = false;
131
133
  }
134
+
135
+ /**
136
+ * Lazily consumes the stream of a request, providing a number of
137
+ * reactive properties that can be used to build UIs that respond
138
+ * to the progress of a request.
139
+ *
140
+ * @class RequestLoadingState
141
+ * @public
142
+ */
132
143
  class RequestLoadingState {
133
144
  _stream = null;
134
145
  _future;
@@ -305,6 +316,35 @@ class RequestLoadingState {
305
316
  this._future.abort();
306
317
  };
307
318
  }
319
+
320
+ /**
321
+ * RequestState extends the concept of PromiseState to provide a reactive
322
+ * wrapper for a request `Future` which allows you write declarative code
323
+ * around a Future's control flow.
324
+ *
325
+ * It is useful in both Template and JavaScript contexts, allowing you
326
+ * to quickly derive behaviors and data from pending, error and success
327
+ * states.
328
+ *
329
+ * The key difference between a Promise and a Future is that Futures provide
330
+ * access to a stream of their content, the identity of the request (if any)
331
+ * as well as the ability to attempt to abort the request.
332
+ *
333
+ * ```ts
334
+ * interface Future<T> extends Promise<T>> {
335
+ * getStream(): Promise<ReadableStream>;
336
+ * abort(): void;
337
+ * lid: StableDocumentIdentifier | null;
338
+ * }
339
+ * ```
340
+ *
341
+ * These additional APIs allow us to craft even richer state experiences.
342
+ *
343
+ * To get the state of a request, use `getRequestState`.
344
+ *
345
+ * @class RequestState
346
+ * @public
347
+ */
308
348
  class RequestState {
309
349
  #request;
310
350
  #loadingState = null;
@@ -398,6 +438,65 @@ class RequestState {
398
438
  }
399
439
  }
400
440
  }
441
+
442
+ /**
443
+ *
444
+ *
445
+ * `getRequestState` can be used in both JavaScript and Template contexts.
446
+ *
447
+ * ```ts
448
+ * import { getRequestState } from '@warp-drive/ember';
449
+ *
450
+ * const state = getRequestState(future);
451
+ * ```
452
+ *
453
+ * For instance, we could write a getter on a component that updates whenever
454
+ * the request state advances or the future changes, by combining the function
455
+ * with the use of `@cached`
456
+ *
457
+ * ```ts
458
+ * class Component {
459
+ * @cached
460
+ * get title() {
461
+ * const state = getRequestState(this.args.request);
462
+ * if (state.isPending) {
463
+ * return 'loading...';
464
+ * }
465
+ * if (state.isError) { return null; }
466
+ * return state.result.title;
467
+ * }
468
+ * }
469
+ * ```
470
+ *
471
+ * Or in a template as a helper:
472
+ *
473
+ * ```gjs
474
+ * import { getRequestState } from '@warp-drive/ember';
475
+ *
476
+ * <template>
477
+ * {{#let (getRequestState @request) as |state|}}
478
+ * {{#if state.isPending}}
479
+ * <Spinner />
480
+ * {{else if state.isError}}
481
+ * <ErrorForm @error={{state.error}} />
482
+ * {{else}}
483
+ * <h1>{{state.result.title}}</h1>
484
+ * {{/if}}
485
+ * {{/let}}
486
+ * </template>
487
+ * ```
488
+ *
489
+ * If looking to use in a template, consider also the `<Request />` component
490
+ * which offers a numbe of additional capabilities for requests *beyond* what
491
+ * `RequestState` provides.
492
+ *
493
+ * @method getRequestState
494
+ * @for @warp-drive/ember
495
+ * @static
496
+ * @public
497
+ * @param future
498
+ * @return {RequestState}
499
+ */
401
500
  function getRequestState(future) {
402
501
  let state = RequestCache.get(future);
403
502
  if (!state) {
@@ -406,7 +505,32 @@ function getRequestState(future) {
406
505
  }
407
506
  return state;
408
507
  }
508
+
509
+ /**
510
+ * @module @warp-drive/ember
511
+ */
409
512
  const PromiseCache = new WeakMap();
513
+
514
+ /**
515
+ * PromiseState provides a reactive wrapper for a promise which allows you write declarative
516
+ * code around a promise's control flow. It is useful in both Template and JavaScript contexts,
517
+ * allowing you to quickly derive behaviors and data from pending, error and success states.
518
+ *
519
+ * ```ts
520
+ * interface PromiseState<T = unknown, E = unknown> {
521
+ * isPending: boolean;
522
+ * isSuccess: boolean;
523
+ * isError: boolean;
524
+ * result: T | null;
525
+ * error: E | null;
526
+ * }
527
+ * ```
528
+ *
529
+ * To get the state of a promise, use `getPromiseState`.
530
+ *
531
+ * @class PromiseState
532
+ * @public
533
+ */
410
534
  class PromiseState {
411
535
  static {
412
536
  decorateFieldV2(this.prototype, "result", [tracked], function () {
@@ -478,6 +602,65 @@ function isLegacyAwaitable(promise) {
478
602
  function getPromise(promise) {
479
603
  return isLegacyAwaitable(promise) ? promise.promise : promise;
480
604
  }
605
+
606
+ /**
607
+ * Returns a reactive state-machine for the provided promise or awaitable.
608
+ *
609
+ * Repeat calls to `getPromiseState` with the same promise will return the same state object
610
+ * making is safe and easy to use in templates and JavaScript code to produce reactive
611
+ * behaviors around promises.
612
+ *
613
+ * `getPromiseState` can be used in both JavaScript and Template contexts.
614
+ *
615
+ * ```ts
616
+ * import { getPromiseState } from '@warp-drive/ember';
617
+ *
618
+ * const state = getPromiseState(promise);
619
+ * ```
620
+ *
621
+ * For instance, we could write a getter on a component that updates whenever
622
+ * the promise state advances or the promise changes, by combining the function
623
+ * with the use of `@cached`
624
+ *
625
+ * ```ts
626
+ * class Component {
627
+ * @cached
628
+ * get title() {
629
+ * const state = getPromiseState(this.args.request);
630
+ * if (state.isPending) {
631
+ * return 'loading...';
632
+ * }
633
+ * if (state.isError) { return null; }
634
+ * return state.result.title;
635
+ * }
636
+ * }
637
+ * ```
638
+ *
639
+ * Or in a template as a helper:
640
+ *
641
+ * ```gjs
642
+ * import { getPromiseState } from '@warp-drive/ember';
643
+ *
644
+ * <template>
645
+ * {{#let (getPromiseState @request) as |state|}}
646
+ * {{#if state.isPending}} <Spinner />
647
+ * {{else if state.isError}} <ErrorForm @error={{state.error}} />
648
+ * {{else}}
649
+ * <h1>{{state.result.title}}</h1>
650
+ * {{/if}}
651
+ * {{/let}}
652
+ * </template>
653
+ * ```
654
+ *
655
+ * If looking to use in a template, consider also the `<Await />` component.
656
+ *
657
+ * @method getPromiseState
658
+ * @for @warp-drive/ember
659
+ * @static
660
+ * @public
661
+ * @param {Promise<T> | Awaitable<T, E>} promise
662
+ * @return {PromiseState<T, E>}
663
+ */
481
664
  function getPromiseState(promise) {
482
665
  const _promise = getPromise(promise);
483
666
  let state = PromiseCache.get(_promise);
@@ -487,7 +670,25 @@ function getPromiseState(promise) {
487
670
  }
488
671
  return state;
489
672
  }
673
+
674
+ /**
675
+ * @module @warp-drive/ember
676
+ */
677
+
490
678
  const and = (x, y) => Boolean(x && y);
679
+ /**
680
+ * The `<Throw />` component is used to throw an error in a template.
681
+ *
682
+ * That's all it does. So don't use it unless the application should
683
+ * throw an error if it reaches this point in the template.
684
+ *
685
+ * ```hbs
686
+ * <Throw @error={{anError}} />
687
+ * ```
688
+ *
689
+ * @class <Throw />
690
+ * @public
691
+ */
491
692
  class Throw extends Component {
492
693
  constructor(owner, args) {
493
694
  super(owner, args);
@@ -502,6 +703,41 @@ class Throw extends Component {
502
703
  }), this);
503
704
  }
504
705
  }
706
+ /**
707
+ * The <Await /> component allow you to utilize reactive control flow
708
+ * for asynchronous states in your application.
709
+ *
710
+ * Await is ideal for handling "boundaries", outside which some state is
711
+ * still allowed to be unresolved and within which it MUST be resolved.
712
+ *
713
+ * ```gjs
714
+ * import { Await } from '@warp-drive/ember';
715
+ *
716
+ * <template>
717
+ * <Await @promise={{@request}}>
718
+ * <:pending>
719
+ * <Spinner />
720
+ * </:pending>
721
+ *
722
+ * <:error as |error|>
723
+ * <ErrorForm @error={{error}} />
724
+ * </:error>
725
+ *
726
+ * <:success as |result|>
727
+ * <h1>{{result.title}}</h1>
728
+ * </:success>
729
+ * </Await>
730
+ * </template>
731
+ * ```
732
+ *
733
+ * The <Await /> component requires that error states are properly handled.
734
+ *
735
+ * If no error block is provided and the promise rejects, the error will
736
+ * be thrown.
737
+ *
738
+ * @class <Await />
739
+ * @public
740
+ */
505
741
  class Await extends Component {
506
742
  get state() {
507
743
  return getPromiseState(this.args.promise);
@@ -522,6 +758,11 @@ class Await extends Component {
522
758
  }), this);
523
759
  }
524
760
  }
761
+
762
+ /**
763
+ * @module @warp-drive/ember
764
+ */
765
+
525
766
  function notNull(x) {
526
767
  macroCondition(getGlobalConfig().WarpDrive.env.DEBUG) ? (test => {
527
768
  if (!test) {
@@ -545,11 +786,221 @@ function isNeverString(val) {
545
786
  return val;
546
787
  }
547
788
  /**
548
- * The `<Request>` component is a powerful tool for managing data fetching and
549
- * state in your Ember application. It provides declarative reactive control-flow
550
- * for managing requests and state in your application.
789
+ * The `<Request />` component is a powerful tool for managing data fetching and
790
+ * state in your Ember application. It provides a declarative approach to reactive
791
+ * control-flow for managing requests and state in your application.
792
+ *
793
+ * The `<Request />` component is ideal for handling "boundaries", outside which some
794
+ * state is still allowed to be unresolved and within which it MUST be resolved.
795
+ *
796
+ * ## Request States
797
+ *
798
+ * `<Request />` has five states, only one of which will be active and rendered at a time.
799
+ *
800
+ * - `idle`: The component is waiting to be given a request to monitor
801
+ * - `loading`: The request is in progress
802
+ * - `error`: The request failed
803
+ * - `content`: The request succeeded
804
+ * - `cancelled`: The request was cancelled
805
+ *
806
+ * Additionally, the `content` state has a `refresh` method that can be used to
807
+ * refresh the request in the background, which is available as a sub-state of
808
+ * the `content` state.
809
+ *
810
+ * As with the `<Await />` component, if no error block is provided and the request
811
+ * rejects, the error will be thrown. Cancellation errors are swallowed instead of
812
+ * rethrown if no error block or cancellation block is present.
813
+ *
814
+ * ```gts
815
+ * import { Request } from '@warp-drive/ember';
816
+ *
817
+ * <template>
818
+ * <Request @request={{@request}}>
819
+ * <:loading as |state|>
820
+ * <Spinner @percentDone={{state.completedRatio}} />
821
+ * <button {{on "click" state.abort}}>Cancel</button>
822
+ * </:loading>
823
+ *
824
+ * <:error as |error state|>
825
+ * <ErrorForm @error={{error}} />
826
+ * <button {{on "click" state.retry}}>Retry</button>
827
+ * </:error>
828
+ *
829
+ * <:content as |data state|>
830
+ * <h1>{{data.title}}</h1>
831
+ * {{#if state.isBackgroundReloading}}
832
+ * <SmallSpinner />
833
+ * <button {{on "click" state.abort}}>Cancel</button>
834
+ * {{else}}
835
+ * <button {{on "click" state.refresh}}>Refresh</button>
836
+ * {{/if}}
837
+ * </:content>
838
+ *
839
+ * <:cancelled as |error state|>
840
+ * <h2>The Request was cancelled</h2>
841
+ * <button {{on "click" state.retry}}>Retry</button>
842
+ * </:cancelled>
843
+ *
844
+ * <:idle>
845
+ * <button {{on "click" @kickOffRequest}}>Load Preview?</button>
846
+ * </:idle>
847
+ *
848
+ * </Request>
849
+ * </template>
850
+ * ```
851
+ *
852
+ * ## Streaming Data
853
+ *
854
+ * The loading state exposes the download `ReadableStream` instance for consumption
855
+ *
856
+ * ```gjs
857
+ * import { Request } from '@warp-drive/ember';
858
+ *
859
+ * <template>
860
+ * <Request @request={{@request}}>
861
+ * <:loading as |state|>
862
+ * <Video @stream={{state.stream}} />
863
+ * </:loading>
864
+ *
865
+ * <:error as |error|>
866
+ * <ErrorForm @error={{error}} />
867
+ * </:error>
868
+ * </Request>
869
+ * </template>
870
+ * ```
871
+ *
872
+ * ## Retry
873
+ *
874
+ * Cancelled and error'd requests may be retried by calling the `retry` method.
875
+ *
876
+ * Retry will restart the state progression, using the loading, error, cancelled,
877
+ * and content blocks as appropriate.
878
+ *
879
+ * ## Reloading
880
+ *
881
+ * The `reload` method will force the request to be fully re-executed, bypassing
882
+ * cache and restarting the state progression through the loading, error, and
883
+ * content blocks as appropriate.
884
+ *
885
+ * Background reload (refresh) is a special substate of the content state that
886
+ * allows you to refresh the request in the background. This is useful for when
887
+ * you want to update the data in the background without blocking the UI.
888
+ *
889
+ * Reload and refresh are available as methods on the `content` state.
890
+ *
891
+ * ```gjs
892
+ * import { Request } from '@warp-drive/ember';
893
+ *
894
+ * <template>
895
+ * <Request @request={{@request}}>
896
+ * <:content as |data state|>
897
+ * <h1>{{data.title}}</h1>
898
+ * {{#if state.isBackgroundReloading}}
899
+ * <SmallSpinner />
900
+ * <button {{on "click" state.abort}}>Cancel</button>
901
+ * {{/if}}
902
+ *
903
+ * <button {{on "click" state.refresh}}>Refresh</button>
904
+ * <button {{on "click" state.reload}}>Reload</button>
905
+ * </:content>
906
+ * </Request>
907
+ * </template>
908
+ * ```
909
+ *
910
+ * ## Advanced Reloading
911
+ *
912
+ * We can nest our usage of `<Request />` to handle more advanced
913
+ * reloading scenarios.
914
+ *
915
+ * ```gjs
916
+ * import { Request } from '@warp-drive/ember';
917
+ *
918
+ * <template>
919
+ * <Request @request={{@request}}>
920
+ * <:cancelled>
921
+ * <h2>The Request Cancelled</h2>
922
+ * </:cancelled>
923
+ *
924
+ * <:error as |error|>
925
+ * <ErrorForm @error={{error}} />
926
+ * </:error>
927
+ *
928
+ * <:content as |result state|>
929
+ * <Request @request={{state.latestRequest}}>
930
+ * <!-- Handle Background Request -->
931
+ * </Request>
932
+ *
933
+ * <h1>{{result.title}}</h1>
934
+ *
935
+ * <button {{on "click" state.refresh}}>Refresh</button>
936
+ * </:content>
937
+ * </Request>
938
+ * </template>
939
+ * ```
940
+ *
941
+ * ## Autorefresh
942
+ *
943
+ * `<Request />` supports automatic refresh and reload under certain conditions.
944
+ *
945
+ * - `online`: This occurs when a browser window or tab comes back to the foreground
946
+ * after being backgrounded or when the network reports as being online after
947
+ * having been offline.
948
+ * - `interval`: This occurs when a specified amount of time has passed.
949
+ * - `invalid`: This occurs when the store emits a notification that the request
950
+ * has become invalid.
951
+ *
952
+ * You can specify when autorefresh should occur by setting the `autorefresh` arg
953
+ * to `true` or a comma-separated list of the above values.
954
+ *
955
+ * A value of `true` is equivalent to `'online,invalid'`.
956
+ *
957
+ * By default, an autorefresh will only occur if the browser was backgrounded or
958
+ * offline for more than 30s before coming back available. This amount of time can
959
+ * be tweaked by setting the number of milliseconds via `@autorefreshThreshold`.
960
+ *
961
+ * This arg also controls the interval at which the request will be refreshed
962
+ * if the `interval` autorefresh type is enabled.
963
+ *
964
+ * Finally, the behavior of the request initiated by autorefresh can be adjusted
965
+ * by setting the `autorefreshBehavior` arg to `'refresh'`, `'reload'`, or `'policy'`.
966
+ *
967
+ * - `'refresh'`: Refresh the request in the background
968
+ * - `'reload'`: Force a reload of the request
969
+ * - `'policy'` (**default**): Let the store's configured CachePolicy decide whether to
970
+ * reload, refresh, or do nothing.
971
+ *
972
+ * More advanced refresh and reload behaviors can be created by passing the reload and
973
+ * refresh actions into another component. For instance, refresh could be set up on a
974
+ * timer or on a websocket subscription.
975
+ *
976
+ *
977
+ * ```gjs
978
+ * import { Request } from '@warp-drive/ember';
979
+ *
980
+ * <template>
981
+ * <Request @request={{@request}}>
982
+ * <:content as |result state|>
983
+ * <h1>{{result.title}}</h1>
984
+ *
985
+ * <Interval @period={{30_000}} @fn={{state.refresh}} />
986
+ * <Subscribe @channel={{@someValue}} @fn={{state.refresh}} />
987
+ * </:content>
988
+ * </Request>
989
+ * </template>
990
+ * ```
991
+ *
992
+ * If a matching request is refreshed or reloaded by any other component,
993
+ * the `Request` component will react accordingly.
994
+ *
995
+ * ## Deduping
996
+ *
997
+ * The store dedupes requests by identity. If a request is made for the same identity
998
+ * from multiple `<Request />` components, even if the request is not referentially the
999
+ * same, only one actual request will be made.
1000
+ *
551
1001
  *
552
- * @typedoc
1002
+ * @class <Request />
1003
+ * @public
553
1004
  */
554
1005
  class Request extends Component {
555
1006
  static {
@@ -790,9 +1241,40 @@ class Request extends Component {
790
1241
  {
791
1242
  const latest = this.store.requestManager._deduped.get(requestId);
792
1243
  const priority = latest?.priority;
1244
+ const state = this.reqState;
793
1245
  if (!priority) {
1246
+ // if there is no priority, we have completed whatever request
1247
+ // was occurring and so we are no longer refreshing (if we were)
794
1248
  this.isRefreshing = false;
795
- } else if (priority.blocking) {
1249
+ } else if (priority.blocking && !state.isLoading) {
1250
+ // if we are blocking, there is an active request for this identity
1251
+ // that MUST be fulfilled from network (not cache).
1252
+ // Thus this is not "refreshing" because we should clear out and
1253
+ // block on this request.
1254
+ //
1255
+ // we receive state notifications when either a request initiates
1256
+ // or completes.
1257
+ //
1258
+ // In the completes case: we may receive the state notification
1259
+ // slightly before the request is finalized because the NotificationManager
1260
+ // may sync flush it (and thus deliver it before the microtask completes)
1261
+ //
1262
+ // In the initiates case: we aren't supposed to receive one unless there
1263
+ // is no other request in flight for this identity.
1264
+ //
1265
+ // However, there is a race condition here where the completed
1266
+ // notification can trigger an update that generates a new request
1267
+ // thus giving us an initiated notification before the older request
1268
+ // finalizes.
1269
+ //
1270
+ // When this occurs, if the triggered update happens to have caused
1271
+ // a new request to be made for the same identity AND that request
1272
+ // is the one passed into this component as the @request arg, then
1273
+ // getRequestState will return the state of the new request.
1274
+ // We can detect this by checking if the request state is "loading"
1275
+ // as outside of this case we would have a completed request.
1276
+ //
1277
+ // That is the reason for the `&& !state.isLoading` check above.
796
1278
  // TODO should we just treat this as refreshing?
797
1279
  this.isRefreshing = false;
798
1280
  this.maybeUpdate('policy', true);