2015-01-30 04:10:49 +03:00
|
|
|
/**
|
2018-09-12 01:27:47 +03:00
|
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
2015-03-24 01:07:33 +03:00
|
|
|
*
|
2018-02-17 05:24:55 +03:00
|
|
|
* This source code is licensed under the MIT license found in the
|
|
|
|
* LICENSE file in the root directory of this source tree.
|
2015-01-30 04:10:49 +03:00
|
|
|
*
|
2015-03-25 05:34:12 +03:00
|
|
|
* @flow
|
2017-09-25 08:57:35 +03:00
|
|
|
* @format
|
2015-01-30 04:10:49 +03:00
|
|
|
*/
|
2019-10-16 20:03:47 +03:00
|
|
|
|
2019-05-08 18:44:25 +03:00
|
|
|
const BatchedBridge = require('../BatchedBridge/BatchedBridge');
|
|
|
|
const BugReporting = require('../BugReporting/BugReporting');
|
|
|
|
const ReactNative = require('../Renderer/shims/ReactNative');
|
|
|
|
const SceneTracker = require('../Utilities/SceneTracker');
|
2015-12-09 02:57:34 +03:00
|
|
|
|
2019-05-08 18:44:25 +03:00
|
|
|
const infoLog = require('../Utilities/infoLog');
|
2018-12-03 10:49:12 +03:00
|
|
|
const invariant = require('invariant');
|
2019-05-08 18:44:25 +03:00
|
|
|
const renderApplication = require('./renderApplication');
|
|
|
|
import type {IPerformanceLogger} from '../Utilities/createPerformanceLogger';
|
2016-09-29 13:43:59 +03:00
|
|
|
|
2021-04-12 10:03:16 +03:00
|
|
|
import {coerceDisplayMode} from './DisplayMode';
|
2020-10-23 17:12:13 +03:00
|
|
|
import createPerformanceLogger from '../Utilities/createPerformanceLogger';
|
2019-05-21 04:00:34 +03:00
|
|
|
import NativeHeadlessJsTaskSupport from './NativeHeadlessJsTaskSupport';
|
Allow headless JS tasks to retry (#23231)
Summary:
`setTimeout` inside a headless JS task does not always works; the function does not get invoked until the user starts an `Activity`.
This was attempted to be used in the context of widgets. When the widget update or user interaction causes the process and React context to be created, the headless JS task may run before other app-specific JS initialisation logic has completed. If it's not possible to change the behaviour of the pre-requisites to be synchronous, then the headless JS task blocks such asynchronous JS work that it may depend on. A primitive solution is the use of `setTimeout` in order to wait for the pre-conditions to be met before continuing with the rest of the headless JS task. But as the function passed to `setTimeout` is not always called, the task will not run to completion.
This PR solves this scenario by allowing the task to be retried again with a delay. If the task returns a promise that resolves to a `{'timeout': number}` object, `AppRegistry.js` will not notify that the task has finished as per master, instead it will tell `HeadlessJsContext` to `startTask` again (cleaning up any posted `Runnable`s beforehand) via a `Handler` within the `HeadlessJsContext`.
Documentation also updated here: https://github.com/facebook/react-native-website/pull/771
### AppRegistry.js
If the task provider does not return any data, or if the data it returns does not contain `timeout` as a number, then it behaves as `master`; notifies that the task has finished. If the response does contain `{timeout: number}`, then it will attempt to queue a retry. If that fails, then it will behaves as if the task provider returned no response i.e. behaves as `master` again. If the retry was successfully queued, then there is nothing to do as we do not want the `Service` to stop itself.
### HeadlessJsTaskSupportModule.java
Similar to notify start/finished, we simply check if the context is running, and if so, pass the request onto `HeadlessJsTaskContext`. The only difference here is that we return a `Promise`, so that `AppRegistry`, as above, knows whether the enqueuing failed and thus needs to perform the usual task clean-up.
### HeadlessJsTaskContext.java
Before retrying, we need to clean-up any timeout `Runnable`'s posted for the first attempt. Then we need to copy the task config so that if this retry (second attempt) also fails, then on the third attempt (second retry) we do not run into a consumed exception. This is also why in `startTask` we copy the config before putting it in the `Map`, so that the initial attempt does leave the config's in the map as consumed. Then we post a `Runnable` to call `startTask` on the main thread's `Handler`. We use the same `taskId` because the `Service` is keeping track of active task IDs in order to calculate whether it needs to `stopSelf`. This negates the need to inform the `Service` of a new task id and us having to remove the old one.
## Changelog
[Android][added] - Allow headless JS tasks to return a promise that will cause the task to be retried again with the specified delay
Pull Request resolved: https://github.com/facebook/react-native/pull/23231
Differential Revision: D15646870
fbshipit-source-id: 4440f4b4392f1fa5c69aab7908b51b7007ba2c40
2019-06-06 21:54:08 +03:00
|
|
|
import HeadlessJsTaskError from './HeadlessJsTaskError';
|
2021-04-27 08:56:28 +03:00
|
|
|
import type {RootTag} from 'react-native/Libraries/Types/RootTagTypes';
|
2019-05-21 04:00:34 +03:00
|
|
|
|
2016-09-29 13:43:59 +03:00
|
|
|
type Task = (taskData: any) => Promise<void>;
|
2019-09-26 00:41:11 +03:00
|
|
|
export type TaskProvider = () => Task;
|
2019-02-15 19:23:40 +03:00
|
|
|
type TaskCanceller = () => void;
|
|
|
|
type TaskCancelProvider = () => TaskCanceller;
|
|
|
|
|
2017-08-18 04:36:54 +03:00
|
|
|
export type ComponentProvider = () => React$ComponentType<any>;
|
2017-09-25 08:57:35 +03:00
|
|
|
export type ComponentProviderInstrumentationHook = (
|
|
|
|
component: ComponentProvider,
|
2019-02-28 14:35:59 +03:00
|
|
|
scopedPerformanceLogger: IPerformanceLogger,
|
2017-09-25 08:57:35 +03:00
|
|
|
) => React$ComponentType<any>;
|
2017-02-15 13:15:22 +03:00
|
|
|
export type AppConfig = {
|
2016-08-09 16:32:41 +03:00
|
|
|
appKey: string,
|
|
|
|
component?: ComponentProvider,
|
|
|
|
run?: Function,
|
2017-02-10 23:38:50 +03:00
|
|
|
section?: boolean,
|
2019-11-21 20:38:13 +03:00
|
|
|
...
|
2017-02-10 23:38:50 +03:00
|
|
|
};
|
2017-02-15 13:15:22 +03:00
|
|
|
export type Runnable = {
|
2017-02-10 23:38:50 +03:00
|
|
|
component?: ComponentProvider,
|
|
|
|
run: Function,
|
2019-11-21 20:38:13 +03:00
|
|
|
...
|
2017-02-10 23:38:50 +03:00
|
|
|
};
|
2019-11-21 20:38:13 +03:00
|
|
|
export type Runnables = {[appKey: string]: Runnable, ...};
|
2017-02-15 13:15:22 +03:00
|
|
|
export type Registry = {
|
2017-02-15 02:21:42 +03:00
|
|
|
sections: Array<string>,
|
|
|
|
runnables: Runnables,
|
2019-11-21 20:38:13 +03:00
|
|
|
...
|
2017-02-15 02:21:42 +03:00
|
|
|
};
|
2020-03-25 07:35:58 +03:00
|
|
|
export type WrapperComponentProvider = any => React$ComponentType<*>;
|
2015-03-25 05:34:12 +03:00
|
|
|
|
2017-02-10 23:38:50 +03:00
|
|
|
const runnables: Runnables = {};
|
|
|
|
let runCount = 1;
|
|
|
|
const sections: Runnables = {};
|
2019-02-15 19:23:40 +03:00
|
|
|
const taskProviders: Map<string, TaskProvider> = new Map();
|
|
|
|
const taskCancelProviders: Map<string, TaskCancelProvider> = new Map();
|
2017-09-25 08:57:35 +03:00
|
|
|
let componentProviderInstrumentationHook: ComponentProviderInstrumentationHook = (
|
|
|
|
component: ComponentProvider,
|
|
|
|
) => component();
|
2017-02-10 23:38:50 +03:00
|
|
|
|
2017-06-27 02:20:39 +03:00
|
|
|
let wrapperComponentProvider: ?WrapperComponentProvider;
|
2019-08-26 20:59:36 +03:00
|
|
|
let showArchitectureIndicator = false;
|
2017-06-27 02:20:39 +03:00
|
|
|
|
2015-02-19 17:57:05 +03:00
|
|
|
/**
|
2017-11-21 00:07:10 +03:00
|
|
|
* `AppRegistry` is the JavaScript entry point to running all React Native apps.
|
2017-12-07 04:25:06 +03:00
|
|
|
*
|
2020-02-25 00:06:47 +03:00
|
|
|
* See https://reactnative.dev/docs/appregistry.html
|
2015-02-19 17:57:05 +03:00
|
|
|
*/
|
2017-02-10 23:38:50 +03:00
|
|
|
const AppRegistry = {
|
2017-06-27 02:20:39 +03:00
|
|
|
setWrapperComponentProvider(provider: WrapperComponentProvider) {
|
|
|
|
wrapperComponentProvider = provider;
|
|
|
|
},
|
|
|
|
|
2019-08-26 20:59:36 +03:00
|
|
|
enableArchitectureIndicator(enabled: boolean): void {
|
|
|
|
showArchitectureIndicator = enabled;
|
2019-04-10 23:54:00 +03:00
|
|
|
},
|
|
|
|
|
2017-02-10 23:38:50 +03:00
|
|
|
registerConfig(config: Array<AppConfig>): void {
|
2020-03-25 07:35:58 +03:00
|
|
|
config.forEach(appConfig => {
|
2015-03-25 05:34:12 +03:00
|
|
|
if (appConfig.run) {
|
|
|
|
AppRegistry.registerRunnable(appConfig.appKey, appConfig.run);
|
2015-01-30 04:10:49 +03:00
|
|
|
} else {
|
2017-02-10 23:38:50 +03:00
|
|
|
invariant(
|
|
|
|
appConfig.component != null,
|
|
|
|
'AppRegistry.registerConfig(...): Every config is expected to set ' +
|
2017-09-25 08:57:35 +03:00
|
|
|
'either `run` or `component`, but `%s` has neither.',
|
|
|
|
appConfig.appKey,
|
2017-02-10 23:38:50 +03:00
|
|
|
);
|
|
|
|
AppRegistry.registerComponent(
|
|
|
|
appConfig.appKey,
|
|
|
|
appConfig.component,
|
|
|
|
appConfig.section,
|
|
|
|
);
|
2015-01-30 04:10:49 +03:00
|
|
|
}
|
2017-02-10 23:38:50 +03:00
|
|
|
});
|
2015-02-19 17:57:05 +03:00
|
|
|
},
|
2015-01-30 04:10:49 +03:00
|
|
|
|
2017-11-21 00:07:10 +03:00
|
|
|
/**
|
|
|
|
* Registers an app's root component.
|
2017-12-07 04:25:06 +03:00
|
|
|
*
|
2020-02-25 00:06:47 +03:00
|
|
|
* See https://reactnative.dev/docs/appregistry.html#registercomponent
|
2017-11-21 00:07:10 +03:00
|
|
|
*/
|
2017-02-10 23:38:50 +03:00
|
|
|
registerComponent(
|
|
|
|
appKey: string,
|
2017-05-04 03:58:59 +03:00
|
|
|
componentProvider: ComponentProvider,
|
2017-02-10 23:38:50 +03:00
|
|
|
section?: boolean,
|
|
|
|
): string {
|
2019-02-28 14:35:59 +03:00
|
|
|
let scopedPerformanceLogger = createPerformanceLogger();
|
2015-01-30 04:10:49 +03:00
|
|
|
runnables[appKey] = {
|
2017-05-04 03:58:59 +03:00
|
|
|
componentProvider,
|
2021-04-12 10:03:16 +03:00
|
|
|
run: (appParameters, displayMode) => {
|
2018-04-19 21:03:50 +03:00
|
|
|
renderApplication(
|
2019-02-28 14:35:59 +03:00
|
|
|
componentProviderInstrumentationHook(
|
|
|
|
componentProvider,
|
|
|
|
scopedPerformanceLogger,
|
|
|
|
),
|
2017-03-22 17:54:19 +03:00
|
|
|
appParameters.initialProps,
|
2017-06-27 02:20:39 +03:00
|
|
|
appParameters.rootTag,
|
|
|
|
wrapperComponentProvider && wrapperComponentProvider(appParameters),
|
2018-04-19 21:03:50 +03:00
|
|
|
appParameters.fabric,
|
2019-08-26 20:59:36 +03:00
|
|
|
showArchitectureIndicator,
|
2019-02-28 14:35:59 +03:00
|
|
|
scopedPerformanceLogger,
|
2019-12-10 13:28:06 +03:00
|
|
|
appKey === 'LogBox',
|
2021-02-25 13:58:34 +03:00
|
|
|
appKey,
|
2021-04-12 10:03:16 +03:00
|
|
|
coerceDisplayMode(displayMode),
|
2021-06-11 14:22:28 +03:00
|
|
|
appParameters.concurrentRoot,
|
2018-03-15 02:01:32 +03:00
|
|
|
);
|
|
|
|
},
|
2015-01-30 04:10:49 +03:00
|
|
|
};
|
2017-02-10 23:38:50 +03:00
|
|
|
if (section) {
|
|
|
|
sections[appKey] = runnables[appKey];
|
|
|
|
}
|
2015-01-30 04:10:49 +03:00
|
|
|
return appKey;
|
2015-02-19 17:57:05 +03:00
|
|
|
},
|
2015-01-30 04:10:49 +03:00
|
|
|
|
2017-02-10 23:38:50 +03:00
|
|
|
registerRunnable(appKey: string, run: Function): string {
|
|
|
|
runnables[appKey] = {run};
|
2015-01-30 04:10:49 +03:00
|
|
|
return appKey;
|
2015-02-19 17:57:05 +03:00
|
|
|
},
|
2015-01-30 04:10:49 +03:00
|
|
|
|
2017-02-10 23:38:50 +03:00
|
|
|
registerSection(appKey: string, component: ComponentProvider): void {
|
|
|
|
AppRegistry.registerComponent(appKey, component, true);
|
|
|
|
},
|
|
|
|
|
|
|
|
getAppKeys(): Array<string> {
|
2015-08-07 21:50:23 +03:00
|
|
|
return Object.keys(runnables);
|
|
|
|
},
|
|
|
|
|
2017-02-10 23:38:50 +03:00
|
|
|
getSectionKeys(): Array<string> {
|
|
|
|
return Object.keys(sections);
|
|
|
|
},
|
|
|
|
|
|
|
|
getSections(): Runnables {
|
|
|
|
return {
|
2017-09-25 08:57:35 +03:00
|
|
|
...sections,
|
2017-02-10 23:38:50 +03:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
getRunnable(appKey: string): ?Runnable {
|
|
|
|
return runnables[appKey];
|
|
|
|
},
|
|
|
|
|
2017-02-15 02:21:42 +03:00
|
|
|
getRegistry(): Registry {
|
|
|
|
return {
|
|
|
|
sections: AppRegistry.getSectionKeys(),
|
|
|
|
runnables: {...runnables},
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2017-09-25 08:57:35 +03:00
|
|
|
setComponentProviderInstrumentationHook(
|
|
|
|
hook: ComponentProviderInstrumentationHook,
|
|
|
|
) {
|
2017-03-22 17:54:19 +03:00
|
|
|
componentProviderInstrumentationHook = hook;
|
|
|
|
},
|
|
|
|
|
2017-11-21 00:07:10 +03:00
|
|
|
/**
|
|
|
|
* Loads the JavaScript bundle and runs the app.
|
2017-12-07 04:25:06 +03:00
|
|
|
*
|
2020-02-25 00:06:47 +03:00
|
|
|
* See https://reactnative.dev/docs/appregistry.html#runapplication
|
2017-11-21 00:07:10 +03:00
|
|
|
*/
|
2021-04-12 10:03:16 +03:00
|
|
|
runApplication(
|
|
|
|
appKey: string,
|
|
|
|
appParameters: any,
|
|
|
|
displayMode?: number,
|
|
|
|
): void {
|
2019-12-10 13:28:06 +03:00
|
|
|
if (appKey !== 'LogBox') {
|
|
|
|
const msg =
|
|
|
|
'Running "' + appKey + '" with ' + JSON.stringify(appParameters);
|
|
|
|
infoLog(msg);
|
|
|
|
BugReporting.addSource(
|
|
|
|
'AppRegistry.runApplication' + runCount++,
|
|
|
|
() => msg,
|
|
|
|
);
|
|
|
|
}
|
2015-01-30 04:10:49 +03:00
|
|
|
invariant(
|
|
|
|
runnables[appKey] && runnables[appKey].run,
|
2019-07-04 17:07:50 +03:00
|
|
|
`"${appKey}" has not been registered. This can happen if:\n` +
|
|
|
|
'* Metro (the local dev server) is run from the wrong folder. ' +
|
|
|
|
'Check if Metro is running, stop it and restart it in the current project.\n' +
|
|
|
|
"* A module failed to load due to an error and `AppRegistry.registerComponent` wasn't called.",
|
2015-01-30 04:10:49 +03:00
|
|
|
);
|
2017-06-01 14:07:43 +03:00
|
|
|
|
2017-04-07 10:48:49 +03:00
|
|
|
SceneTracker.setActiveScene({name: appKey});
|
2021-04-12 10:03:16 +03:00
|
|
|
runnables[appKey].run(appParameters, displayMode);
|
2015-02-19 17:57:05 +03:00
|
|
|
},
|
2015-12-09 02:57:34 +03:00
|
|
|
|
2021-04-12 10:03:16 +03:00
|
|
|
/**
|
|
|
|
* Update initial props for a surface that's already rendered
|
|
|
|
*/
|
2021-04-12 10:03:16 +03:00
|
|
|
setSurfaceProps(
|
|
|
|
appKey: string,
|
|
|
|
appParameters: any,
|
|
|
|
displayMode?: number,
|
|
|
|
): void {
|
2021-04-12 10:03:16 +03:00
|
|
|
if (appKey !== 'LogBox') {
|
|
|
|
const msg =
|
|
|
|
'Updating props for Surface "' +
|
|
|
|
appKey +
|
|
|
|
'" with ' +
|
|
|
|
JSON.stringify(appParameters);
|
|
|
|
infoLog(msg);
|
|
|
|
BugReporting.addSource(
|
|
|
|
'AppRegistry.setSurfaceProps' + runCount++,
|
|
|
|
() => msg,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
invariant(
|
|
|
|
runnables[appKey] && runnables[appKey].run,
|
|
|
|
`"${appKey}" has not been registered. This can happen if:\n` +
|
|
|
|
'* Metro (the local dev server) is run from the wrong folder. ' +
|
|
|
|
'Check if Metro is running, stop it and restart it in the current project.\n' +
|
|
|
|
"* A module failed to load due to an error and `AppRegistry.registerComponent` wasn't called.",
|
|
|
|
);
|
|
|
|
|
2021-04-12 10:03:16 +03:00
|
|
|
runnables[appKey].run(appParameters, displayMode);
|
2021-04-12 10:03:16 +03:00
|
|
|
},
|
|
|
|
|
2017-11-21 00:07:10 +03:00
|
|
|
/**
|
2017-12-07 04:25:06 +03:00
|
|
|
* Stops an application when a view should be destroyed.
|
|
|
|
*
|
2020-02-25 00:06:47 +03:00
|
|
|
* See https://reactnative.dev/docs/appregistry.html#unmountapplicationcomponentatroottag
|
2017-11-21 00:07:10 +03:00
|
|
|
*/
|
2021-04-27 08:56:28 +03:00
|
|
|
unmountApplicationComponentAtRootTag(rootTag: RootTag): void {
|
|
|
|
// NOTE: RootTag type
|
|
|
|
// $FlowFixMe[incompatible-call] RootTag: RootTag is incompatible with number, needs an updated synced version of the ReactNativeTypes.js file
|
2015-12-09 02:57:34 +03:00
|
|
|
ReactNative.unmountComponentAtNodeAndRemoveContainer(rootTag);
|
|
|
|
},
|
|
|
|
|
2016-09-29 13:43:59 +03:00
|
|
|
/**
|
|
|
|
* Register a headless task. A headless task is a bit of code that runs without a UI.
|
2017-12-07 04:25:06 +03:00
|
|
|
*
|
2020-02-25 00:06:47 +03:00
|
|
|
* See https://reactnative.dev/docs/appregistry.html#registerheadlesstask
|
2016-09-29 13:43:59 +03:00
|
|
|
*/
|
2019-02-15 19:23:40 +03:00
|
|
|
registerHeadlessTask(taskKey: string, taskProvider: TaskProvider): void {
|
|
|
|
this.registerCancellableHeadlessTask(taskKey, taskProvider, () => () => {
|
|
|
|
/* Cancel is no-op */
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Register a cancellable headless task. A headless task is a bit of code that runs without a UI.
|
|
|
|
*
|
2020-02-25 00:06:47 +03:00
|
|
|
* See https://reactnative.dev/docs/appregistry.html#registercancellableheadlesstask
|
2019-02-15 19:23:40 +03:00
|
|
|
*/
|
|
|
|
registerCancellableHeadlessTask(
|
|
|
|
taskKey: string,
|
|
|
|
taskProvider: TaskProvider,
|
|
|
|
taskCancelProvider: TaskCancelProvider,
|
|
|
|
): void {
|
|
|
|
if (taskProviders.has(taskKey)) {
|
2017-09-25 08:57:35 +03:00
|
|
|
console.warn(
|
2019-02-15 19:23:40 +03:00
|
|
|
`registerHeadlessTask or registerCancellableHeadlessTask called multiple times for same key '${taskKey}'`,
|
2017-09-25 08:57:35 +03:00
|
|
|
);
|
2016-09-29 13:43:59 +03:00
|
|
|
}
|
2019-02-15 19:23:40 +03:00
|
|
|
taskProviders.set(taskKey, taskProvider);
|
|
|
|
taskCancelProviders.set(taskKey, taskCancelProvider);
|
2016-09-29 13:43:59 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Only called from native code. Starts a headless task.
|
|
|
|
*
|
2020-02-25 00:06:47 +03:00
|
|
|
* See https://reactnative.dev/docs/appregistry.html#startheadlesstask
|
2016-09-29 13:43:59 +03:00
|
|
|
*/
|
2017-02-10 23:38:50 +03:00
|
|
|
startHeadlessTask(taskId: number, taskKey: string, data: any): void {
|
2019-02-15 19:23:40 +03:00
|
|
|
const taskProvider = taskProviders.get(taskKey);
|
2016-09-29 13:43:59 +03:00
|
|
|
if (!taskProvider) {
|
2019-05-01 12:17:11 +03:00
|
|
|
console.warn(`No task registered for key ${taskKey}`);
|
2019-05-21 04:00:34 +03:00
|
|
|
if (NativeHeadlessJsTaskSupport) {
|
|
|
|
NativeHeadlessJsTaskSupport.notifyTaskFinished(taskId);
|
|
|
|
}
|
2019-05-01 12:17:11 +03:00
|
|
|
return;
|
2016-09-29 13:43:59 +03:00
|
|
|
}
|
|
|
|
taskProvider()(data)
|
2019-05-21 04:00:34 +03:00
|
|
|
.then(() => {
|
|
|
|
if (NativeHeadlessJsTaskSupport) {
|
|
|
|
NativeHeadlessJsTaskSupport.notifyTaskFinished(taskId);
|
|
|
|
}
|
|
|
|
})
|
2020-03-25 07:35:58 +03:00
|
|
|
.catch(reason => {
|
2016-09-29 13:43:59 +03:00
|
|
|
console.error(reason);
|
Allow headless JS tasks to retry (#23231)
Summary:
`setTimeout` inside a headless JS task does not always works; the function does not get invoked until the user starts an `Activity`.
This was attempted to be used in the context of widgets. When the widget update or user interaction causes the process and React context to be created, the headless JS task may run before other app-specific JS initialisation logic has completed. If it's not possible to change the behaviour of the pre-requisites to be synchronous, then the headless JS task blocks such asynchronous JS work that it may depend on. A primitive solution is the use of `setTimeout` in order to wait for the pre-conditions to be met before continuing with the rest of the headless JS task. But as the function passed to `setTimeout` is not always called, the task will not run to completion.
This PR solves this scenario by allowing the task to be retried again with a delay. If the task returns a promise that resolves to a `{'timeout': number}` object, `AppRegistry.js` will not notify that the task has finished as per master, instead it will tell `HeadlessJsContext` to `startTask` again (cleaning up any posted `Runnable`s beforehand) via a `Handler` within the `HeadlessJsContext`.
Documentation also updated here: https://github.com/facebook/react-native-website/pull/771
### AppRegistry.js
If the task provider does not return any data, or if the data it returns does not contain `timeout` as a number, then it behaves as `master`; notifies that the task has finished. If the response does contain `{timeout: number}`, then it will attempt to queue a retry. If that fails, then it will behaves as if the task provider returned no response i.e. behaves as `master` again. If the retry was successfully queued, then there is nothing to do as we do not want the `Service` to stop itself.
### HeadlessJsTaskSupportModule.java
Similar to notify start/finished, we simply check if the context is running, and if so, pass the request onto `HeadlessJsTaskContext`. The only difference here is that we return a `Promise`, so that `AppRegistry`, as above, knows whether the enqueuing failed and thus needs to perform the usual task clean-up.
### HeadlessJsTaskContext.java
Before retrying, we need to clean-up any timeout `Runnable`'s posted for the first attempt. Then we need to copy the task config so that if this retry (second attempt) also fails, then on the third attempt (second retry) we do not run into a consumed exception. This is also why in `startTask` we copy the config before putting it in the `Map`, so that the initial attempt does leave the config's in the map as consumed. Then we post a `Runnable` to call `startTask` on the main thread's `Handler`. We use the same `taskId` because the `Service` is keeping track of active task IDs in order to calculate whether it needs to `stopSelf`. This negates the need to inform the `Service` of a new task id and us having to remove the old one.
## Changelog
[Android][added] - Allow headless JS tasks to return a promise that will cause the task to be retried again with the specified delay
Pull Request resolved: https://github.com/facebook/react-native/pull/23231
Differential Revision: D15646870
fbshipit-source-id: 4440f4b4392f1fa5c69aab7908b51b7007ba2c40
2019-06-06 21:54:08 +03:00
|
|
|
|
|
|
|
if (
|
|
|
|
NativeHeadlessJsTaskSupport &&
|
|
|
|
reason instanceof HeadlessJsTaskError
|
|
|
|
) {
|
|
|
|
NativeHeadlessJsTaskSupport.notifyTaskRetry(taskId).then(
|
2020-03-25 07:35:58 +03:00
|
|
|
retryPosted => {
|
Allow headless JS tasks to retry (#23231)
Summary:
`setTimeout` inside a headless JS task does not always works; the function does not get invoked until the user starts an `Activity`.
This was attempted to be used in the context of widgets. When the widget update or user interaction causes the process and React context to be created, the headless JS task may run before other app-specific JS initialisation logic has completed. If it's not possible to change the behaviour of the pre-requisites to be synchronous, then the headless JS task blocks such asynchronous JS work that it may depend on. A primitive solution is the use of `setTimeout` in order to wait for the pre-conditions to be met before continuing with the rest of the headless JS task. But as the function passed to `setTimeout` is not always called, the task will not run to completion.
This PR solves this scenario by allowing the task to be retried again with a delay. If the task returns a promise that resolves to a `{'timeout': number}` object, `AppRegistry.js` will not notify that the task has finished as per master, instead it will tell `HeadlessJsContext` to `startTask` again (cleaning up any posted `Runnable`s beforehand) via a `Handler` within the `HeadlessJsContext`.
Documentation also updated here: https://github.com/facebook/react-native-website/pull/771
### AppRegistry.js
If the task provider does not return any data, or if the data it returns does not contain `timeout` as a number, then it behaves as `master`; notifies that the task has finished. If the response does contain `{timeout: number}`, then it will attempt to queue a retry. If that fails, then it will behaves as if the task provider returned no response i.e. behaves as `master` again. If the retry was successfully queued, then there is nothing to do as we do not want the `Service` to stop itself.
### HeadlessJsTaskSupportModule.java
Similar to notify start/finished, we simply check if the context is running, and if so, pass the request onto `HeadlessJsTaskContext`. The only difference here is that we return a `Promise`, so that `AppRegistry`, as above, knows whether the enqueuing failed and thus needs to perform the usual task clean-up.
### HeadlessJsTaskContext.java
Before retrying, we need to clean-up any timeout `Runnable`'s posted for the first attempt. Then we need to copy the task config so that if this retry (second attempt) also fails, then on the third attempt (second retry) we do not run into a consumed exception. This is also why in `startTask` we copy the config before putting it in the `Map`, so that the initial attempt does leave the config's in the map as consumed. Then we post a `Runnable` to call `startTask` on the main thread's `Handler`. We use the same `taskId` because the `Service` is keeping track of active task IDs in order to calculate whether it needs to `stopSelf`. This negates the need to inform the `Service` of a new task id and us having to remove the old one.
## Changelog
[Android][added] - Allow headless JS tasks to return a promise that will cause the task to be retried again with the specified delay
Pull Request resolved: https://github.com/facebook/react-native/pull/23231
Differential Revision: D15646870
fbshipit-source-id: 4440f4b4392f1fa5c69aab7908b51b7007ba2c40
2019-06-06 21:54:08 +03:00
|
|
|
if (!retryPosted) {
|
|
|
|
NativeHeadlessJsTaskSupport.notifyTaskFinished(taskId);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
2019-05-21 04:00:34 +03:00
|
|
|
}
|
2016-09-29 13:43:59 +03:00
|
|
|
});
|
2017-09-25 08:57:35 +03:00
|
|
|
},
|
2019-02-15 19:23:40 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Only called from native code. Cancels a headless task.
|
|
|
|
*
|
2020-02-25 00:06:47 +03:00
|
|
|
* See https://reactnative.dev/docs/appregistry.html#cancelheadlesstask
|
2019-02-15 19:23:40 +03:00
|
|
|
*/
|
|
|
|
cancelHeadlessTask(taskId: number, taskKey: string): void {
|
|
|
|
const taskCancelProvider = taskCancelProviders.get(taskKey);
|
|
|
|
if (!taskCancelProvider) {
|
|
|
|
throw new Error(`No task canceller registered for key '${taskKey}'`);
|
|
|
|
}
|
|
|
|
taskCancelProvider()();
|
|
|
|
},
|
2015-02-19 17:57:05 +03:00
|
|
|
};
|
2015-01-30 04:10:49 +03:00
|
|
|
|
2017-09-25 08:57:35 +03:00
|
|
|
BatchedBridge.registerCallableModule('AppRegistry', AppRegistry);
|
2015-12-09 02:57:34 +03:00
|
|
|
|
2019-12-10 13:28:06 +03:00
|
|
|
if (__DEV__) {
|
2019-12-10 13:28:06 +03:00
|
|
|
const LogBoxInspector = require('../LogBox/LogBoxInspectorContainer').default;
|
|
|
|
AppRegistry.registerComponent('LogBox', () => LogBoxInspector);
|
2019-12-11 19:35:10 +03:00
|
|
|
} else {
|
|
|
|
AppRegistry.registerComponent(
|
|
|
|
'LogBox',
|
|
|
|
() =>
|
|
|
|
function NoOp() {
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
);
|
2019-12-10 13:28:06 +03:00
|
|
|
}
|
|
|
|
|
2015-02-19 17:57:05 +03:00
|
|
|
module.exports = AppRegistry;
|