@@ -2,8 +2,9 @@ import { assert } from '@ember/debug';
2
2
import { service } from ' @ember/service' ;
3
3
import Component from ' @glimmer/component' ;
4
4
import { cached } from ' @glimmer/tracking' ;
5
+ import { EnableHydration , type RequestInfo } from ' @warp-drive/core-types/request' ;
5
6
import type { Future , StructuredErrorDocument } from ' @ember-data/request' ;
6
-
7
+ import type { RequestState } from ' ./request-state.ts ' ;
7
8
import { importSync , macroCondition , moduleExists } from ' @embroider/macros' ;
8
9
9
10
import type { StoreRequestInput } from ' @ember-data/store' ;
@@ -12,24 +13,49 @@ import type Store from '@ember-data/store';
12
13
import { getRequestState } from ' ./request-state.ts' ;
13
14
import type { RequestLoadingState } from ' ./request-state.ts' ;
14
15
import { and , notNull , Throw } from ' ./await.gts' ;
16
+ import { tracked } from ' @glimmer/tracking' ;
17
+
18
+ const not = (x : unknown ) => ! x ;
19
+ // default to 30 seconds unavailable before we refresh
20
+ const DEFAULT_DEADLINE = 30_000 ;
15
21
16
22
let provide = service ;
17
23
if (macroCondition (moduleExists (' ember-provide-consume-context' ))) {
18
24
const { consume } = importSync (' ember-provide-consume-context' ) as { consume: typeof service };
19
25
provide = consume ;
20
26
}
21
27
28
+ type ContentFeatures <T > = {
29
+ isOnline: boolean ;
30
+ isHidden: boolean ;
31
+ isRefreshing: boolean ;
32
+ refresh: () => Promise <void >;
33
+ reload: () => Promise <void >;
34
+ abort? : () => void ;
35
+ latestRequest? : Future <T >;
36
+ };
37
+
22
38
interface RequestSignature <T > {
23
39
Args: {
24
40
request? : Future <T >;
25
41
query? : StoreRequestInput <T >;
26
42
store? : Store ;
43
+ autorefresh? : boolean ;
44
+ autorefreshThreshold? : number ;
45
+ autorefreshBehavior? : ' refresh' | ' reload' | ' policy' ;
27
46
};
28
47
Blocks: {
29
48
loading: [state : RequestLoadingState ];
30
- cancelled: [error : StructuredErrorDocument ];
31
- error: [error : StructuredErrorDocument ];
32
- content: [value : T ];
49
+ cancelled: [
50
+ error : StructuredErrorDocument ,
51
+ features : { isOnline: boolean ; isHidden: boolean ; retry: () => Promise <void > },
52
+ ];
53
+ error: [
54
+ error : StructuredErrorDocument ,
55
+ features : { isOnline: boolean ; isHidden: boolean ; retry: () => Promise <void > },
56
+ ];
57
+ content: [value : T , features : ContentFeatures <T >];
58
+ always: [state : RequestState <T >];
33
59
};
34
60
}
35
61
@@ -38,15 +64,164 @@ export class Request<T> extends Component<RequestSignature<T>> {
38
64
* @internal
39
65
*/
40
66
@provide (' store' ) declare _store: Store ;
67
+ @tracked isOnline: boolean = true ;
68
+ @tracked isHidden: boolean = true ;
69
+ @tracked isRefreshing: boolean = false ;
70
+ @tracked _localRequest: Future <T > | undefined ;
71
+ @tracked _latestRequest: Future <T > | undefined ;
72
+ declare unavailableStart: number | null ;
73
+ declare onlineChanged: (event : Event ) => void ;
74
+ declare backgroundChanged: (event : Event ) => void ;
75
+ declare _originalRequest: Future <T > | undefined ;
76
+ declare _originalQuery: StoreRequestInput | undefined ;
77
+
78
+ constructor (owner : unknown , args : RequestSignature <T >[' Args' ]) {
79
+ super (owner , args );
80
+ this .installListeners ();
81
+ }
82
+
83
+ installListeners() {
84
+ if (typeof window === ' undefined' ) {
85
+ return ;
86
+ }
87
+
88
+ this .isOnline = window .navigator .onLine ;
89
+ this .unavailableStart = this .isOnline ? null : Date .now ();
90
+ this .isHidden = document .visibilityState === ' hidden' ;
91
+
92
+ this .onlineChanged = (event : Event ) => {
93
+ this .isOnline = event .type === ' online' ;
94
+ if (event .type === ' offline' ) {
95
+ this .unavailableStart = Date .now ();
96
+ }
97
+ this .maybeUpdate ();
98
+ };
99
+ this .backgroundChanged = () => {
100
+ this .isHidden = document .visibilityState === ' hidden' ;
101
+ this .maybeUpdate ();
102
+ };
103
+
104
+ window .addEventListener (' online' , this .onlineChanged , { passive: true , capture: true });
105
+ window .addEventListener (' offline' , this .onlineChanged , { passive: true , capture: true });
106
+ document .addEventListener (' visibilitychange' , this .backgroundChanged , { passive: true , capture: true });
107
+ }
41
108
42
- retry = () => {};
43
- reload = () => {};
44
- refresh = () => {};
109
+ maybeUpdate(mode ? : ' reload' | ' refresh' | ' policy' ): void {
110
+ if (this .isOnline && ! this .isHidden && (mode || this .args .autorefresh )) {
111
+ const deadline =
112
+ typeof this .args .autorefreshThreshold === ' number' ? this .args .autorefreshThreshold : DEFAULT_DEADLINE ;
113
+ const shouldAttempt = mode || (this .unavailableStart && Date .now () - this .unavailableStart > deadline );
114
+ this .unavailableStart = null ;
115
+
116
+ if (shouldAttempt ) {
117
+ const request = Object .assign ({}, this .reqState .request as unknown as RequestInfo );
118
+ const val = mode ?? this .args .autorefreshBehavior ?? ' policy' ;
119
+ switch (val ) {
120
+ case ' reload' :
121
+ request .cacheOptions = Object .assign ({}, request .cacheOptions , { reload: true });
122
+ break ;
123
+ case ' refresh' :
124
+ request .cacheOptions = Object .assign ({}, request .cacheOptions , { backgroundReload: true });
125
+ break ;
126
+ case ' policy' :
127
+ break ;
128
+ default :
129
+ throw new Error (` Invalid ${mode ? ' update mode' : ' @autorefreshBehavior' } for <Request />: ${val } ` );
130
+ }
131
+
132
+ const wasStoreRequest = (request as { [EnableHydration ]: boolean })[EnableHydration ] === true ;
133
+ assert (
134
+ ` Cannot supply a different store via context than was used to create the request ` ,
135
+ ! request .store || request .store === this .store
136
+ );
137
+
138
+ this ._latestRequest = wasStoreRequest
139
+ ? this .store .request <T >(request )
140
+ : this .store .requestManager .request <T >(request );
141
+
142
+ if (val !== ' refresh' ) {
143
+ this ._localRequest = this ._latestRequest ;
144
+ }
145
+ }
146
+ }
147
+
148
+ if (mode ) {
149
+ throw new Error (` Reload not available: the network is not online or the tab is hidden ` );
150
+ }
151
+ }
152
+
153
+ retry = async () => {
154
+ this .maybeUpdate (' reload' );
155
+ await this ._localRequest ;
156
+ };
157
+
158
+ refresh = async () => {
159
+ this .isRefreshing = true ;
160
+ this .maybeUpdate (' refresh' );
161
+ try {
162
+ await this ._latestRequest ;
163
+ } finally {
164
+ this .isRefreshing = false ;
165
+ }
166
+ };
167
+
168
+ @cached
169
+ get errorFeatures() {
170
+ return {
171
+ isHidden: this .isHidden ,
172
+ isOnline: this .isOnline ,
173
+ retry: this .retry ,
174
+ };
175
+ }
176
+
177
+ @cached
178
+ get contentFeatures() {
179
+ const feat: ContentFeatures <T > = {
180
+ isHidden: this .isHidden ,
181
+ isOnline: this .isOnline ,
182
+ reload: this .retry ,
183
+ refresh: this .refresh ,
184
+ isRefreshing: this .isRefreshing ,
185
+ latestRequest: this ._latestRequest ,
186
+ };
187
+
188
+ if (feat .isRefreshing ) {
189
+ feat .abort = () => {
190
+ this ._latestRequest ?.abort ();
191
+ };
192
+ }
193
+
194
+ return feat ;
195
+ }
196
+
197
+ willDestroy() {
198
+ if (typeof window === ' undefined' ) {
199
+ return ;
200
+ }
201
+
202
+ window .removeEventListener (' online' , this .onlineChanged , { passive: true , capture: true } as unknown as boolean );
203
+ window .removeEventListener (' offline' , this .onlineChanged , { passive: true , capture: true } as unknown as boolean );
204
+ document .removeEventListener (' visibilitychange' , this .backgroundChanged , {
205
+ passive: true ,
206
+ capture: true ,
207
+ } as unknown as boolean );
208
+ }
45
209
46
210
@cached
47
211
get request() {
48
212
const { request, query } = this .args ;
49
213
assert (` Cannot use both @request and @query args with the <Request> component ` , ! request || ! query );
214
+ const { _localRequest, _originalRequest, _originalQuery } = this ;
215
+ const isOriginalRequest = request === _originalRequest && query === _originalQuery ;
216
+
217
+ if (_localRequest && isOriginalRequest ) {
218
+ return _localRequest ;
219
+ }
220
+
221
+ // update state checks for the next time
222
+ this ._originalQuery = query ;
223
+ this ._originalRequest = request ;
224
+
50
225
if (request ) {
51
226
return request ;
52
227
}
@@ -73,13 +248,14 @@ export class Request<T> extends Component<RequestSignature<T>> {
73
248
{{#if this . reqState.isLoading }}
74
249
{{yield this . reqState.loadingState to =" loading" }}
75
250
{{else if ( and this . reqState.isCancelled ( has-block " cancelled" ) ) }}
76
- {{yield ( notNull this . reqState.error) to =" cancelled" }}
251
+ {{yield ( notNull this . reqState.error) this . errorFeatures to =" cancelled" }}
77
252
{{else if ( and this . reqState.isError ( has-block " error" ) ) }}
78
- {{yield ( notNull this . reqState.error) to =" error" }}
253
+ {{yield ( notNull this . reqState.error) this . errorFeatures to =" error" }}
79
254
{{else if this . reqState.isSuccess }}
80
- {{yield ( notNull this . reqState.result) to =" content" }}
81
- {{else }}
255
+ {{yield ( notNull this . reqState.result) this . contentFeatures to =" content" }}
256
+ {{else if ( not this . reqState.isCancelled ) }}
82
257
<Throw @ error ={{( notNull this . reqState.error) }} />
83
258
{{/if }}
259
+ {{yield this . reqState to =" always" }}
84
260
</template >
85
261
}
0 commit comments