UNPKG

@daily-co/daily-js

Version:

**🚨Our docs have moved! 🚨**

324 lines (276 loc) • 10.3 kB
import { callObjectBundleUrl } from './utils'; function prepareDailyConfig(callFrameId) { // Add a global callFrameId so we can have both iframes and one // call object mode calls live at the same time if (!window._dailyConfig) { window._dailyConfig = {}; } window._dailyConfig.callFrameId = callFrameId; } export default class CallObjectLoader { constructor() { this._currentLoad = null; } /** * Loads the call object bundle (if needed), then invokes the callback * function, which takes one boolean argument whose value is true if the * load was a no-op. * * No-op loads can happen when leaving a meeting and then later joining one. * Since the call object bundle sets up global state in the same scope as the * app code consuming it, it only needs to be loaded and executed once ever. * * @param meetingOrBaseUrl Meeting URL (like https://somecompany.daily.co/hello) * or base URL (like https://somecompany.daily.co), used to determine where * to load the bundle from. * @param callFrameId A string identifying this "call frame", to distinguish it * from other iframe-based calls for message channel purposes. * @param successCallback Callback function that takes a wasNoOp argument * (true if call object script was ever loaded once before). * @param failureCallback Callback function that takes an error message and a * boolean indicating whether an automatic retry is slated to occur. */ load(meetingOrBaseUrl, callFrameId, successCallback, failureCallback) { if (this.loaded) { window._dailyCallObjectSetup(callFrameId); successCallback(true); // true = "this load() was a no-op" return; } prepareDailyConfig(callFrameId); // Cancel current load, if any this._currentLoad && this._currentLoad.cancel(); // Start a new load this._currentLoad = new LoadOperation( meetingOrBaseUrl, callFrameId, () => { successCallback(false); // false = "this load() wasn't a no-op" }, failureCallback ); this._currentLoad.start(); } /** * Cancel loading the call object bundle. No callbacks will be invoked. */ cancel() { this._currentLoad && this._currentLoad.cancel(); } /** * Returns a boolean indicating whether the call object bundle has been * loaded and executed. */ get loaded() { return this._currentLoad && this._currentLoad.succeeded; } } const LOAD_ATTEMPTS = 3; const LOAD_ATTEMPT_DELAY = 3 * 1000; class LoadOperation { // Here failureCallback takes the same parameters as CallObjectLoader.load, // and successCallback takes no parameters. constructor(meetingOrBaseUrl, callFrameId, successCallback, failureCallback) { this._attemptsRemaining = LOAD_ATTEMPTS; this._currentAttempt = null; this._meetingOrBaseUrl = meetingOrBaseUrl; this._callFrameId = callFrameId; this._successCallback = successCallback; this._failureCallback = failureCallback; } start() { // Bail if this load has already started if (this._currentAttempt) { return; } // console.log("[LoadOperation] starting..."); const retryOrFailureCallback = (errorMessage) => { if (this._currentAttempt.cancelled) { // console.log("[LoadOperation] cancelled"); return; } this._attemptsRemaining--; this._failureCallback(errorMessage, this._attemptsRemaining > 0); // true = "will retry" if (this._attemptsRemaining <= 0) { // Should never be <0, but just being extra careful here // console.log("[LoadOperation] ran out of attempts"); return; } setTimeout(() => { if (this._currentAttempt.cancelled) { // console.log("[LoadOperation] cancelled"); return; } this._currentAttempt = new LoadAttempt( this._meetingOrBaseUrl, this._callFrameId, this._successCallback, retryOrFailureCallback ); this._currentAttempt.start(); }, LOAD_ATTEMPT_DELAY); }; this._currentAttempt = new LoadAttempt( this._meetingOrBaseUrl, this._callFrameId, this._successCallback, retryOrFailureCallback ); this._currentAttempt.start(); } cancel() { this._currentAttempt && this._currentAttempt.cancel(); } get cancelled() { return this._currentAttempt && this._currentAttempt.cancelled; } get succeeded() { return this._currentAttempt && this._currentAttempt.succeeded; } } class LoadAttemptAbortedError extends Error {} const LOAD_ATTEMPT_NETWORK_TIMEOUT = 20 * 1000; class LoadAttempt { // Here successCallback takes no parameters, and failureCallback takes a // single error message parameter. constructor(meetingOrBaseUrl, callFrameId, successCallback, failureCallback) { this.cancelled = false; this.succeeded = false; this._networkTimedOut = false; this._networkTimeout = null; this._iosCache = typeof iOSCallObjectBundleCache !== 'undefined' && iOSCallObjectBundleCache; this._refetchHeaders = null; this._meetingOrBaseUrl = meetingOrBaseUrl; this._callFrameId = callFrameId; this._successCallback = successCallback; this._failureCallback = failureCallback; } async start() { // console.log("[LoadAttempt] starting..."); const url = callObjectBundleUrl(this._meetingOrBaseUrl); const loadedFromIOSCache = await this._tryLoadFromIOSCache(url); !loadedFromIOSCache && this._loadFromNetwork(url); } cancel() { clearTimeout(this._networkTimeout); this.cancelled = true; } /** * Try to load the call object bundle from the iOS cache. * This is a React Native-specific workaround for the fact that the iOS HTTP * cache won't cache the call object bundle due to size. * * @param {string} url The url of the call object bundle to try to load. * @returns A Promise that resolves to false if the load failed or true * otherwise (if it succeeded or was cancelled), indicating whether a network * load attempt is needed. */ async _tryLoadFromIOSCache(url) { // console.log("[LoadAttempt] trying to load from iOS cache..."); // Bail if we're not running in iOS if (!this._iosCache) { // console.log("[LoadAttempt] not iOS, so not checking iOS cache"); return false; } try { const cacheResponse = await this._iosCache.get(url); // If load has been cancelled, report work complete (no network load // needed) if (this.cancelled) { return true; } // If cache miss, report failure (network load needed) if (!cacheResponse) { // console.log("[LoadAttempt] iOS cache miss"); return false; } // If cache expired, store refetch headers to use later and report // failure (network load needed) if (!cacheResponse.code) { // console.log( // "[LoadAttempt] iOS cache expired, setting refetch headers", // cacheResponse.refetchHeaders // ); this._refetchHeaders = cacheResponse.refetchHeaders; return false; } // Cache is fresh, so run code and success callback, and report work // complete (no network load needed) // console.log("[LoadAttempt] iOS cache hit"); Function('"use strict";' + cacheResponse.code)(); this.succeeded = true; this._successCallback(); return true; } catch (e) { // Report failure // console.log("[LoadAttempt] failure running bundle from iOS cache", e); return false; } } /** * Try to load the call object bundle from the network. * @param {string} url The url of the call object bundle to load. */ async _loadFromNetwork(url) { // console.log("[LoadAttempt] trying to load from network..."); this._networkTimeout = setTimeout(() => { this._networkTimedOut = true; this._failureCallback( `Timed out (>${LOAD_ATTEMPT_NETWORK_TIMEOUT} ms) when loading call object bundle ${url}` ); }, LOAD_ATTEMPT_NETWORK_TIMEOUT); try { const fetchOptions = this._refetchHeaders ? { headers: this._refetchHeaders } : {}; const response = await fetch(url, fetchOptions); clearTimeout(this._networkTimeout); // Check that load wasn't cancelled or timed out during fetch if (this.cancelled || this._networkTimedOut) { throw new LoadAttemptAbortedError(); } const code = await this._getBundleCodeFromResponse(url, response); // Check again that load wasn't cancelled during reading response if (this.cancelled) { throw new LoadAttemptAbortedError(); } // Execute bundle code Function('"use strict";' + code)(); // Since code ran successfully (no errors thrown), cache it and call // success callback // console.log("[LoadAttempt] succeeded..."); this._iosCache && this._iosCache.set(url, code, response.headers); this.succeeded = true; this._successCallback(); } catch (e) { clearTimeout(this._networkTimeout); // We need to check all these conditions since long outstanding // requests can fail *after* cancellation or timeout (i.e. checking for // LoadAttemptAbortedError is not enough). if ( e instanceof LoadAttemptAbortedError || this.cancelled || this._networkTimedOut ) { // console.log("[LoadAttempt] cancelled or timed out"); return; } this._failureCallback(`Failed to load call object bundle ${url}: ${e}`); } } async _getBundleCodeFromResponse(url, response) { // Normal success case if (response.ok) { return await response.text(); } // React Native iOS-specific case: 304 Not-Modified response // (Since we're doing manual cache management for iOS, the fetch mechanism // doesn't opaquely handle 304s for us) if (this._iosCache && response.status === 304) { const cacheResponse = await this._iosCache.renew(url, response.headers); return cacheResponse.code; } throw new Error(`Received ${response.status} response`); } }