UNPKG

10.3 kBJavaScriptView Raw
1import { callObjectBundleUrl } from './utils';
2
3function prepareDailyConfig(callFrameId) {
4 // Add a global callFrameId so we can have both iframes and one
5 // call object mode calls live at the same time
6 if (!window._dailyConfig) {
7 window._dailyConfig = {};
8 }
9 window._dailyConfig.callFrameId = callFrameId;
10}
11
12export default class CallObjectLoader {
13 constructor() {
14 this._currentLoad = null;
15 }
16
17 /**
18 * Loads the call object bundle (if needed), then invokes the callback
19 * function, which takes one boolean argument whose value is true if the
20 * load was a no-op.
21 *
22 * No-op loads can happen when leaving a meeting and then later joining one.
23 * Since the call object bundle sets up global state in the same scope as the
24 * app code consuming it, it only needs to be loaded and executed once ever.
25 *
26 * @param meetingOrBaseUrl Meeting URL (like https://somecompany.daily.co/hello)
27 * or base URL (like https://somecompany.daily.co), used to determine where
28 * to load the bundle from.
29 * @param callFrameId A string identifying this "call frame", to distinguish it
30 * from other iframe-based calls for message channel purposes.
31 * @param successCallback Callback function that takes a wasNoOp argument
32 * (true if call object script was ever loaded once before).
33 * @param failureCallback Callback function that takes an error message and a
34 * boolean indicating whether an automatic retry is slated to occur.
35 */
36 load(meetingOrBaseUrl, callFrameId, successCallback, failureCallback) {
37 if (this.loaded) {
38 window._dailyCallObjectSetup(callFrameId);
39 successCallback(true); // true = "this load() was a no-op"
40 return;
41 }
42
43 prepareDailyConfig(callFrameId);
44
45 // Cancel current load, if any
46 this._currentLoad && this._currentLoad.cancel();
47
48 // Start a new load
49 this._currentLoad = new LoadOperation(
50 meetingOrBaseUrl,
51 callFrameId,
52 () => {
53 successCallback(false); // false = "this load() wasn't a no-op"
54 },
55 failureCallback
56 );
57 this._currentLoad.start();
58 }
59
60 /**
61 * Cancel loading the call object bundle. No callbacks will be invoked.
62 */
63 cancel() {
64 this._currentLoad && this._currentLoad.cancel();
65 }
66
67 /**
68 * Returns a boolean indicating whether the call object bundle has been
69 * loaded and executed.
70 */
71 get loaded() {
72 return this._currentLoad && this._currentLoad.succeeded;
73 }
74}
75
76const LOAD_ATTEMPTS = 3;
77const LOAD_ATTEMPT_DELAY = 3 * 1000;
78
79class LoadOperation {
80 // Here failureCallback takes the same parameters as CallObjectLoader.load,
81 // and successCallback takes no parameters.
82 constructor(meetingOrBaseUrl, callFrameId, successCallback, failureCallback) {
83 this._attemptsRemaining = LOAD_ATTEMPTS;
84 this._currentAttempt = null;
85
86 this._meetingOrBaseUrl = meetingOrBaseUrl;
87 this._callFrameId = callFrameId;
88 this._successCallback = successCallback;
89 this._failureCallback = failureCallback;
90 }
91
92 start() {
93 // Bail if this load has already started
94 if (this._currentAttempt) {
95 return;
96 }
97
98 // console.log("[LoadOperation] starting...");
99
100 const retryOrFailureCallback = (errorMessage) => {
101 if (this._currentAttempt.cancelled) {
102 // console.log("[LoadOperation] cancelled");
103 return;
104 }
105
106 this._attemptsRemaining--;
107 this._failureCallback(errorMessage, this._attemptsRemaining > 0); // true = "will retry"
108 if (this._attemptsRemaining <= 0) {
109 // Should never be <0, but just being extra careful here
110 // console.log("[LoadOperation] ran out of attempts");
111 return;
112 }
113
114 setTimeout(() => {
115 if (this._currentAttempt.cancelled) {
116 // console.log("[LoadOperation] cancelled");
117 return;
118 }
119 this._currentAttempt = new LoadAttempt(
120 this._meetingOrBaseUrl,
121 this._callFrameId,
122 this._successCallback,
123 retryOrFailureCallback
124 );
125 this._currentAttempt.start();
126 }, LOAD_ATTEMPT_DELAY);
127 };
128
129 this._currentAttempt = new LoadAttempt(
130 this._meetingOrBaseUrl,
131 this._callFrameId,
132 this._successCallback,
133 retryOrFailureCallback
134 );
135 this._currentAttempt.start();
136 }
137
138 cancel() {
139 this._currentAttempt && this._currentAttempt.cancel();
140 }
141
142 get cancelled() {
143 return this._currentAttempt && this._currentAttempt.cancelled;
144 }
145
146 get succeeded() {
147 return this._currentAttempt && this._currentAttempt.succeeded;
148 }
149}
150
151class LoadAttemptAbortedError extends Error {}
152
153const LOAD_ATTEMPT_NETWORK_TIMEOUT = 20 * 1000;
154
155class LoadAttempt {
156 // Here successCallback takes no parameters, and failureCallback takes a
157 // single error message parameter.
158 constructor(meetingOrBaseUrl, callFrameId, successCallback, failureCallback) {
159 this.cancelled = false;
160 this.succeeded = false;
161
162 this._networkTimedOut = false;
163 this._networkTimeout = null;
164
165 this._iosCache =
166 typeof iOSCallObjectBundleCache !== 'undefined' &&
167 iOSCallObjectBundleCache;
168 this._refetchHeaders = null;
169
170 this._meetingOrBaseUrl = meetingOrBaseUrl;
171 this._callFrameId = callFrameId;
172 this._successCallback = successCallback;
173 this._failureCallback = failureCallback;
174 }
175
176 async start() {
177 // console.log("[LoadAttempt] starting...");
178 const url = callObjectBundleUrl(this._meetingOrBaseUrl);
179 const loadedFromIOSCache = await this._tryLoadFromIOSCache(url);
180 !loadedFromIOSCache && this._loadFromNetwork(url);
181 }
182
183 cancel() {
184 clearTimeout(this._networkTimeout);
185 this.cancelled = true;
186 }
187
188 /**
189 * Try to load the call object bundle from the iOS cache.
190 * This is a React Native-specific workaround for the fact that the iOS HTTP
191 * cache won't cache the call object bundle due to size.
192 *
193 * @param {string} url The url of the call object bundle to try to load.
194 * @returns A Promise that resolves to false if the load failed or true
195 * otherwise (if it succeeded or was cancelled), indicating whether a network
196 * load attempt is needed.
197 */
198 async _tryLoadFromIOSCache(url) {
199 // console.log("[LoadAttempt] trying to load from iOS cache...");
200
201 // Bail if we're not running in iOS
202 if (!this._iosCache) {
203 // console.log("[LoadAttempt] not iOS, so not checking iOS cache");
204 return false;
205 }
206
207 try {
208 const cacheResponse = await this._iosCache.get(url);
209
210 // If load has been cancelled, report work complete (no network load
211 // needed)
212 if (this.cancelled) {
213 return true;
214 }
215
216 // If cache miss, report failure (network load needed)
217 if (!cacheResponse) {
218 // console.log("[LoadAttempt] iOS cache miss");
219 return false;
220 }
221
222 // If cache expired, store refetch headers to use later and report
223 // failure (network load needed)
224 if (!cacheResponse.code) {
225 // console.log(
226 // "[LoadAttempt] iOS cache expired, setting refetch headers",
227 // cacheResponse.refetchHeaders
228 // );
229 this._refetchHeaders = cacheResponse.refetchHeaders;
230 return false;
231 }
232
233 // Cache is fresh, so run code and success callback, and report work
234 // complete (no network load needed)
235 // console.log("[LoadAttempt] iOS cache hit");
236 Function('"use strict";' + cacheResponse.code)();
237 this.succeeded = true;
238 this._successCallback();
239 return true;
240 } catch (e) {
241 // Report failure
242 // console.log("[LoadAttempt] failure running bundle from iOS cache", e);
243 return false;
244 }
245 }
246
247 /**
248 * Try to load the call object bundle from the network.
249 * @param {string} url The url of the call object bundle to load.
250 */
251 async _loadFromNetwork(url) {
252 // console.log("[LoadAttempt] trying to load from network...");
253 this._networkTimeout = setTimeout(() => {
254 this._networkTimedOut = true;
255 this._failureCallback(
256 `Timed out (>${LOAD_ATTEMPT_NETWORK_TIMEOUT} ms) when loading call object bundle ${url}`
257 );
258 }, LOAD_ATTEMPT_NETWORK_TIMEOUT);
259
260 try {
261 const fetchOptions = this._refetchHeaders
262 ? { headers: this._refetchHeaders }
263 : {};
264 const response = await fetch(url, fetchOptions);
265 clearTimeout(this._networkTimeout);
266
267 // Check that load wasn't cancelled or timed out during fetch
268 if (this.cancelled || this._networkTimedOut) {
269 throw new LoadAttemptAbortedError();
270 }
271
272 const code = await this._getBundleCodeFromResponse(url, response);
273
274 // Check again that load wasn't cancelled during reading response
275 if (this.cancelled) {
276 throw new LoadAttemptAbortedError();
277 }
278
279 // Execute bundle code
280 Function('"use strict";' + code)();
281
282 // Since code ran successfully (no errors thrown), cache it and call
283 // success callback
284 // console.log("[LoadAttempt] succeeded...");
285 this._iosCache && this._iosCache.set(url, code, response.headers);
286 this.succeeded = true;
287 this._successCallback();
288 } catch (e) {
289 clearTimeout(this._networkTimeout);
290
291 // We need to check all these conditions since long outstanding
292 // requests can fail *after* cancellation or timeout (i.e. checking for
293 // LoadAttemptAbortedError is not enough).
294 if (
295 e instanceof LoadAttemptAbortedError ||
296 this.cancelled ||
297 this._networkTimedOut
298 ) {
299 // console.log("[LoadAttempt] cancelled or timed out");
300 return;
301 }
302
303 this._failureCallback(`Failed to load call object bundle ${url}: ${e}`);
304 }
305 }
306
307 async _getBundleCodeFromResponse(url, response) {
308 // Normal success case
309 if (response.ok) {
310 return await response.text();
311 }
312
313 // React Native iOS-specific case: 304 Not-Modified response
314 // (Since we're doing manual cache management for iOS, the fetch mechanism
315 // doesn't opaquely handle 304s for us)
316 if (this._iosCache && response.status === 304) {
317 const cacheResponse = await this._iosCache.renew(url, response.headers);
318 return cacheResponse.code;
319 }
320
321 throw new Error(`Received ${response.status} response`);
322 }
323}