UNPKG

create-expo-cljs-app

Version:

Create a react native application with Expo and Shadow-CLJS!

510 lines (450 loc) 14.2 kB
/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @flow */ 'use strict'; const BatchedBridge = require('../../BatchedBridge/BatchedBridge'); const Platform = require('../../Utilities/Platform'); const Systrace = require('../../Performance/Systrace'); const invariant = require('invariant'); import NativeTiming from './NativeTiming'; /** * JS implementation of timer functions. Must be completely driven by an * external clock signal, all that's stored here is timerID, timer type, and * callback. */ export type JSTimerType = | 'setTimeout' | 'setInterval' | 'requestAnimationFrame' | 'setImmediate' | 'requestIdleCallback'; // These timing constants should be kept in sync with the ones in native ios and // android `RCTTiming` module. const FRAME_DURATION = 1000 / 60; const IDLE_CALLBACK_FRAME_DEADLINE = 1; const MAX_TIMER_DURATION_MS = 60 * 1000; const IS_ANDROID = Platform.OS === 'android'; const ANDROID_LONG_TIMER_MESSAGE = 'Setting a timer for a long period of time, i.e. multiple minutes, is a ' + 'performance and correctness issue on Android as it keeps the timer ' + 'module awake, and timers can only be called when the app is in the foreground. ' + 'See https://github.com/facebook/react-native/issues/12981 for more info.'; // Parallel arrays const callbacks: Array<?Function> = []; const types: Array<?JSTimerType> = []; const timerIDs: Array<?number> = []; let immediates: Array<number> = []; let requestIdleCallbacks: Array<number> = []; const requestIdleCallbackTimeouts: {[number]: number, ...} = {}; let GUID = 1; let errors: ?Array<Error> = null; let hasEmittedTimeDriftWarning = false; // Returns a free index if one is available, and the next consecutive index otherwise. function _getFreeIndex(): number { let freeIndex = timerIDs.indexOf(null); if (freeIndex === -1) { freeIndex = timerIDs.length; } return freeIndex; } function _allocateCallback(func: Function, type: JSTimerType): number { const id = GUID++; const freeIndex = _getFreeIndex(); timerIDs[freeIndex] = id; callbacks[freeIndex] = func; types[freeIndex] = type; return id; } /** * Calls the callback associated with the ID. Also unregister that callback * if it was a one time timer (setTimeout), and not unregister it if it was * recurring (setInterval). */ function _callTimer(timerID: number, frameTime: number, didTimeout: ?boolean) { if (timerID > GUID) { console.warn( 'Tried to call timer with ID %s but no such timer exists.', timerID, ); } // timerIndex of -1 means that no timer with that ID exists. There are // two situations when this happens, when a garbage timer ID was given // and when a previously existing timer was deleted before this callback // fired. In both cases we want to ignore the timer id, but in the former // case we warn as well. const timerIndex = timerIDs.indexOf(timerID); if (timerIndex === -1) { return; } const type = types[timerIndex]; const callback = callbacks[timerIndex]; if (!callback || !type) { console.error('No callback found for timerID ' + timerID); return; } if (__DEV__) { Systrace.beginEvent(type + ' [invoke]'); } // Clear the metadata if (type !== 'setInterval') { _clearIndex(timerIndex); } try { if ( type === 'setTimeout' || type === 'setInterval' || type === 'setImmediate' ) { callback(); } else if (type === 'requestAnimationFrame') { callback(global.performance.now()); } else if (type === 'requestIdleCallback') { callback({ timeRemaining: function() { // TODO: Optimisation: allow running for longer than one frame if // there are no pending JS calls on the bridge from native. This // would require a way to check the bridge queue synchronously. return Math.max( 0, FRAME_DURATION - (global.performance.now() - frameTime), ); }, didTimeout: !!didTimeout, }); } else { console.error('Tried to call a callback with invalid type: ' + type); } } catch (e) { // Don't rethrow so that we can run all timers. if (!errors) { errors = [e]; } else { errors.push(e); } } if (__DEV__) { Systrace.endEvent(); } } /** * Performs a single pass over the enqueued immediates. Returns whether * more immediates are queued up (can be used as a condition a while loop). */ function _callImmediatesPass() { if (immediates.length === 0) { return false; } if (__DEV__) { Systrace.beginEvent('callImmediatesPass()'); } // The main reason to extract a single pass is so that we can track // in the system trace const passImmediates = immediates; immediates = []; // Use for loop rather than forEach as per @vjeux's advice // https://github.com/facebook/react-native/commit/c8fd9f7588ad02d2293cac7224715f4af7b0f352#commitcomment-14570051 for (let i = 0; i < passImmediates.length; ++i) { _callTimer(passImmediates[i], 0); } if (__DEV__) { Systrace.endEvent(); } return immediates.length > 0; } function _clearIndex(i: number) { timerIDs[i] = null; callbacks[i] = null; types[i] = null; } function _freeCallback(timerID: number) { // timerIDs contains nulls after timers have been removed; // ignore nulls upfront so indexOf doesn't find them if (timerID == null) { return; } const index = timerIDs.indexOf(timerID); // See corresponding comment in `callTimers` for reasoning behind this if (index !== -1) { const type = types[index]; _clearIndex(index); if (type !== 'setImmediate' && type !== 'requestIdleCallback') { deleteTimer(timerID); } } } /** * JS implementation of timer functions. Must be completely driven by an * external clock signal, all that's stored here is timerID, timer type, and * callback. */ const JSTimers = { /** * @param {function} func Callback to be invoked after `duration` ms. * @param {number} duration Number of milliseconds. */ setTimeout: function(func: Function, duration: number, ...args: any): number { if (__DEV__ && IS_ANDROID && duration > MAX_TIMER_DURATION_MS) { console.warn( ANDROID_LONG_TIMER_MESSAGE + '\n' + '(Saw setTimeout with duration ' + duration + 'ms)', ); } const id = _allocateCallback( () => func.apply(undefined, args), 'setTimeout', ); createTimer(id, duration || 0, Date.now(), /* recurring */ false); return id; }, /** * @param {function} func Callback to be invoked every `duration` ms. * @param {number} duration Number of milliseconds. */ setInterval: function( func: Function, duration: number, ...args: any ): number { if (__DEV__ && IS_ANDROID && duration > MAX_TIMER_DURATION_MS) { console.warn( ANDROID_LONG_TIMER_MESSAGE + '\n' + '(Saw setInterval with duration ' + duration + 'ms)', ); } const id = _allocateCallback( () => func.apply(undefined, args), 'setInterval', ); createTimer(id, duration || 0, Date.now(), /* recurring */ true); return id; }, /** * @param {function} func Callback to be invoked before the end of the * current JavaScript execution loop. */ setImmediate: function(func: Function, ...args: any) { const id = _allocateCallback( () => func.apply(undefined, args), 'setImmediate', ); immediates.push(id); return id; }, /** * @param {function} func Callback to be invoked every frame. */ requestAnimationFrame: function(func: Function) { const id = _allocateCallback(func, 'requestAnimationFrame'); createTimer(id, 1, Date.now(), /* recurring */ false); return id; }, /** * @param {function} func Callback to be invoked every frame and provided * with time remaining in frame. * @param {?object} options */ requestIdleCallback: function(func: Function, options: ?Object) { if (requestIdleCallbacks.length === 0) { setSendIdleEvents(true); } const timeout = options && options.timeout; const id = _allocateCallback( timeout != null ? deadline => { const timeoutId = requestIdleCallbackTimeouts[id]; if (timeoutId) { JSTimers.clearTimeout(timeoutId); delete requestIdleCallbackTimeouts[id]; } return func(deadline); } : func, 'requestIdleCallback', ); requestIdleCallbacks.push(id); if (timeout != null) { const timeoutId = JSTimers.setTimeout(() => { const index = requestIdleCallbacks.indexOf(id); if (index > -1) { requestIdleCallbacks.splice(index, 1); _callTimer(id, global.performance.now(), true); } delete requestIdleCallbackTimeouts[id]; if (requestIdleCallbacks.length === 0) { setSendIdleEvents(false); } }, timeout); requestIdleCallbackTimeouts[id] = timeoutId; } return id; }, cancelIdleCallback: function(timerID: number) { _freeCallback(timerID); const index = requestIdleCallbacks.indexOf(timerID); if (index !== -1) { requestIdleCallbacks.splice(index, 1); } const timeoutId = requestIdleCallbackTimeouts[timerID]; if (timeoutId) { JSTimers.clearTimeout(timeoutId); delete requestIdleCallbackTimeouts[timerID]; } if (requestIdleCallbacks.length === 0) { setSendIdleEvents(false); } }, clearTimeout: function(timerID: number) { _freeCallback(timerID); }, clearInterval: function(timerID: number) { _freeCallback(timerID); }, clearImmediate: function(timerID: number) { _freeCallback(timerID); const index = immediates.indexOf(timerID); if (index !== -1) { immediates.splice(index, 1); } }, cancelAnimationFrame: function(timerID: number) { _freeCallback(timerID); }, /** * This is called from the native side. We are passed an array of timerIDs, * and */ callTimers: function(timersToCall: Array<number>) { invariant( timersToCall.length !== 0, 'Cannot call `callTimers` with an empty list of IDs.', ); errors = (null: ?Array<Error>); for (let i = 0; i < timersToCall.length; i++) { _callTimer(timersToCall[i], 0); } if (errors) { const errorCount = errors.length; if (errorCount > 1) { // Throw all the other errors in a setTimeout, which will throw each // error one at a time for (let ii = 1; ii < errorCount; ii++) { JSTimers.setTimeout( (error => { throw error; }).bind(null, errors[ii]), 0, ); } } throw errors[0]; } }, callIdleCallbacks: function(frameTime: number) { if ( FRAME_DURATION - (global.performance.now() - frameTime) < IDLE_CALLBACK_FRAME_DEADLINE ) { return; } errors = (null: ?Array<Error>); if (requestIdleCallbacks.length > 0) { const passIdleCallbacks = requestIdleCallbacks; requestIdleCallbacks = []; for (let i = 0; i < passIdleCallbacks.length; ++i) { _callTimer(passIdleCallbacks[i], frameTime); } } if (requestIdleCallbacks.length === 0) { setSendIdleEvents(false); } if (errors) { errors.forEach(error => JSTimers.setTimeout(() => { throw error; }, 0), ); } }, /** * This is called after we execute any command we receive from native but * before we hand control back to native. */ callImmediates() { errors = (null: ?Array<Error>); while (_callImmediatesPass()) {} if (errors) { errors.forEach(error => JSTimers.setTimeout(() => { throw error; }, 0), ); } }, /** * Called from native (in development) when environment times are out-of-sync. */ emitTimeDriftWarning(warningMessage: string) { if (hasEmittedTimeDriftWarning) { return; } hasEmittedTimeDriftWarning = true; console.warn(warningMessage); }, }; function createTimer( callbackID: number, duration: number, jsSchedulingTime: number, repeats: boolean, ): void { invariant(NativeTiming, 'NativeTiming is available'); NativeTiming.createTimer(callbackID, duration, jsSchedulingTime, repeats); } function deleteTimer(timerID: number): void { invariant(NativeTiming, 'NativeTiming is available'); NativeTiming.deleteTimer(timerID); } function setSendIdleEvents(sendIdleEvents: boolean): void { invariant(NativeTiming, 'NativeTiming is available'); NativeTiming.setSendIdleEvents(sendIdleEvents); } let ExportedJSTimers: {| callIdleCallbacks: (frameTime: number) => any | void, callImmediates: () => void, callTimers: (timersToCall: Array<number>) => any | void, cancelAnimationFrame: (timerID: number) => void, cancelIdleCallback: (timerID: number) => void, clearImmediate: (timerID: number) => void, clearInterval: (timerID: number) => void, clearTimeout: (timerID: number) => void, emitTimeDriftWarning: (warningMessage: string) => any | void, requestAnimationFrame: (func: any) => any | number, requestIdleCallback: (func: any, options: ?any) => any | number, setImmediate: (func: any, ...args: any) => number, setInterval: (func: any, duration: number, ...args: any) => number, setTimeout: (func: any, duration: number, ...args: any) => number, |}; if (!NativeTiming) { console.warn("Timing native module is not available, can't set timers."); // $FlowFixMe: we can assume timers are generally available ExportedJSTimers = ({ callImmediates: JSTimers.callImmediates, setImmediate: JSTimers.setImmediate, }: typeof JSTimers); } else { ExportedJSTimers = JSTimers; } BatchedBridge.setImmediatesCallback(JSTimers.callImmediates); module.exports = ExportedJSTimers;