blob: 558925c5af328a8a574c299094f62e4500986101 [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
@JS()
library require_reloading_manager;
import 'dart:async';
import 'dart:collection';
import 'dart:html';
import 'dart:js_util';
import 'package:graphs/graphs.dart' as graphs;
import 'package:js/js.dart';
import 'package:js/js_util.dart';
import '../promise.dart';
import '../run_main.dart';
import 'restarter.dart';
/// The last known digests of all the modules in the application.
///
/// This is updated in place during calls to hotRestart.
/// TODO(annagrin): can this be a private field in RequireRestarter?
late Map<String, String> _lastKnownDigests;
@JS(r'$requireLoader')
external RequireLoader get requireLoader;
@JS(r'$loadModuleConfig')
external Object Function(String module) get require;
@JS(r'$dartRunMain')
external set dartRunMain(Function() func);
@JS(r'$dartRunMain')
external Function() get dartRunMain;
List<K> keys<K, V>(JsMap<K, V> map) {
return List.from(_jsArrayFrom(map.keys()));
}
@JS('Array.from')
external List _jsArrayFrom(Object any);
@JS('Object.values')
external List _jsObjectValues(Object any);
@anonymous
@JS()
class RequireLoader {
@JS()
external String get digestsPath;
@JS()
external JsMap<String, List<String>> get moduleParentsGraph;
@JS()
external void forceLoadModule(String moduleId, void Function() callback,
void Function(JsError e) onError);
}
class HotReloadFailedException implements Exception {
final String _s;
HotReloadFailedException(this._s);
@override
String toString() => "HotReloadFailedException: '$_s'";
}
@JS('Error')
abstract class JsError {
@JS()
external String get message;
@JS()
external String get stack;
}
@JS('Map')
abstract class JsMap<K, V> {
@JS()
external V? get(K key);
@JS()
external Object keys();
}
/// Handles hot restart reloading for use with the require module system.
class RequireRestarter implements Restarter {
final _moduleOrdering = HashMap<String, int>();
late SplayTreeSet<String> _dirtyModules;
var _running = Completer<bool>()..complete(true);
var count = 0;
RequireRestarter._() {
_dirtyModules = SplayTreeSet(_moduleTopologicalCompare);
}
@override
Future<bool> restart({String? runId}) async {
final developer = getProperty(require('dart_sdk'), 'developer');
if (callMethod(getProperty(developer, '_extensions'), 'containsKey',
['ext.flutter.disassemble']) as bool) {
await toFuture(callMethod(
developer, 'invokeExtension', ['ext.flutter.disassemble', '{}'])
as Promise<void>);
}
final newDigests = await _getDigests();
final modulesToLoad = <String>[];
for (var moduleId in newDigests.keys) {
if (!_lastKnownDigests.containsKey(moduleId)) {
print('Error during script reloading, refreshing the page. \n'
'Unable to find an existing digest for module: $moduleId.');
_reloadPage();
} else if (_lastKnownDigests[moduleId] != newDigests[moduleId]) {
_lastKnownDigests[moduleId] = newDigests[moduleId]!;
modulesToLoad.add(moduleId);
}
}
var result = true;
if (modulesToLoad.isNotEmpty) {
_updateGraph();
result = await _reload(modulesToLoad);
}
callMethod(getProperty(require('dart_sdk'), 'dart'), 'hotRestart', []);
runMain();
return result;
}
List<String> _allModules() => keys(requireLoader.moduleParentsGraph);
Future<Map<String, String>> _getDigests() async {
final request = await HttpRequest.request(requireLoader.digestsPath,
responseType: 'json', method: 'GET');
return (request.response as Map).cast<String, String>();
}
Future<void> _initialize() async {
_lastKnownDigests = await _getDigests();
}
List<String> _moduleParents(String module) =>
requireLoader.moduleParentsGraph.get(module)?.cast() ?? [];
int _moduleTopologicalCompare(String module1, String module2) {
var topological = 0;
final order1 = _moduleOrdering[module1];
final order2 = _moduleOrdering[module2];
if (order1 == null || order2 == null) {
final missing = order1 == null ? module1 : module2;
throw HotReloadFailedException(
'Unable to fetch ordering info for module: $missing');
}
topological = Comparable.compare(
_moduleOrdering[module2]!, _moduleOrdering[module1]!);
if (topological == 0) {
// If modules are in cycle (same strongly connected component) compare
// their string id, to ensure total ordering for SplayTreeSet uniqueness.
topological = module1.compareTo(module2);
}
return topological;
}
/// Returns `true` if the reload was fully handled, `false` if it failed
/// explicitly, or `null` for an unhandled reload.
Future<bool> _reload(List<String> modules) async {
// As function is async, it can potentially be called second time while
// first invocation is still running. In this case just mark as dirty and
// wait until loop from the first call will do the work
if (!_running.isCompleted) return await _running.future;
_running = Completer();
var reloadedModules = 0;
try {
_dirtyModules.addAll(modules);
String? previousModuleId;
while (_dirtyModules.isNotEmpty) {
final moduleId = _dirtyModules.first;
_dirtyModules.remove(moduleId);
final parentIds = _moduleParents(moduleId);
// Check if this is the root / bootstrap module.
if (parentIds.isEmpty) {
// The bootstrap module is not reloaded but we need to update the
// $dartRunMain reference to the newly loaded child module.
final childModule = callMethod(
getProperty(require('dart_sdk'), 'dart'),
'getModuleLibraries',
[previousModuleId]);
dartRunMain = allowInterop(() {
callMethod(_jsObjectValues(childModule).first, 'main', []);
});
} else {
++reloadedModules;
await _reloadModule(moduleId);
parentIds.sort(_moduleTopologicalCompare);
_dirtyModules.addAll(parentIds);
previousModuleId = moduleId;
}
}
print('$reloadedModules module(s) were hot-reloaded.');
_running.complete(true);
} on HotReloadFailedException catch (e) {
print('Error during script reloading. Firing full page reload. $e');
_reloadPage();
_running.complete(false);
}
return _running.future;
}
Future<void> _reloadModule(String moduleId) {
final completer = Completer();
final stackTrace = StackTrace.current;
requireLoader.forceLoadModule(
moduleId,
allowInterop(completer.complete),
allowInterop((e) {
completer.completeError(
HotReloadFailedException(e.message), stackTrace);
}),
);
return completer.future;
}
void _reloadPage() {
window.location.reload();
}
void _updateGraph() {
final allModules = _allModules();
final stronglyConnectedComponents =
graphs.stronglyConnectedComponents(allModules, _moduleParents);
_moduleOrdering.clear();
for (var i = 0; i < stronglyConnectedComponents.length; i++) {
for (var module in stronglyConnectedComponents[i]) {
_moduleOrdering[module] = i;
}
}
}
static Future<RequireRestarter> create() async {
final reloader = RequireRestarter._();
await reloader._initialize();
return reloader;
}
}