blob: 1cb6b44ce95316e9d5ad07bdafe473d32d02413a [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.
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:js_interop';
import 'package:dwds/src/utilities/shared.dart';
import 'package:graphs/graphs.dart' as graphs;
import 'package:http/browser_client.dart';
import 'package:web/web.dart';
import '../run_main.dart';
import '../web_utils.dart';
import 'restarter.dart';
@JS(r'$dartRunMain')
external set dartRunMain(JSFunction func);
@JS(r'$dartRunMain')
external JSFunction get dartRunMain;
@JS(r'$requireLoader')
external RequireLoader get requireLoader;
@anonymous
@JS()
@staticInterop
class RequireLoader {}
extension RequireLoaderExtension on RequireLoader {
external String get digestsPath;
external JsMap<JSString, JSArray> get moduleParentsGraph;
external void forceLoadModule(
JSString moduleId,
JSFunction callback,
JSFunction onError,
);
}
@anonymous
@JS()
@staticInterop
class Sdk {}
@anonymous
@JS()
@staticInterop
class SdkDeveloper {}
@anonymous
@JS()
@staticInterop
class SdkDart {}
@anonymous
@JS()
@staticInterop
class SdkExt {}
@anonymous
@JS()
@staticInterop
class MainLibrary {}
@JS(r'$loadModuleConfig')
external Sdk require(String value);
Sdk get sdk => require('dart_sdk');
extension SdkExtension on Sdk {
external SdkDart get dart;
external SdkDeveloper get developer;
}
extension SdkDeveloperExtension on SdkDeveloper {
external JSPromise<JSString> invokeExtension(String key, String params);
external SdkExt get _extensions;
Future<void> maybeInvokeFlutterDisassemble() async {
final method = 'ext.flutter.disassemble';
if (_extensions.containsKey(method)) {
await invokeExtension(method, '{}').toDart;
}
}
}
extension SdkDartExtension on SdkDart {
external void hotRestart();
external JSObject getModuleLibraries(String? moduleId);
MainLibrary getMainLibrary(String? moduleId) {
final libraries = getModuleLibraries(moduleId).values;
return libraries.first! as MainLibrary;
}
}
extension SdkExtExtension on SdkExt {
external bool containsKey(String key);
}
extension MainLibraryExtension on MainLibrary {
external void main();
}
class HotReloadFailedException implements Exception {
final String _s;
HotReloadFailedException(this._s);
@override
String toString() => "HotReloadFailedException: '$_s'";
}
/// Handles hot restart reloading for use with the require module system.
class RequireRestarter implements Restarter {
/// The last known digests of all the modules in the application.
///
/// This is updated in place during calls to hotRestart.
static late Map<String, String> _lastKnownDigests;
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, JSArray<JSObject>?)> restart({
String? runId,
Future? readyToRunMain,
String? reloadedSourcesPath,
}) async {
assert(
reloadedSourcesPath == null,
"'reloadedSourcesPath' should not be used for the AMD module format.",
);
await sdk.developer.maybeInvokeFlutterDisassemble();
final newDigests = await _getDigests();
final modulesToLoad = <String>[];
for (final 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);
}
sdk.dart.hotRestart();
safeUnawaited(_runMainWhenReady(readyToRunMain));
return (result, null);
}
@override
Future<void> hotReloadEnd() =>
throw UnimplementedError(
'Hot reload is not supported for the AMD module format.',
);
@override
Future<JSArray<JSObject>> hotReloadStart(String reloadedSourcesPath) =>
throw UnimplementedError(
'Hot reload is not supported for the AMD module format.',
);
Future<void> _runMainWhenReady(Future? readyToRunMain) async {
if (readyToRunMain != null) {
await readyToRunMain;
}
runMain();
}
Iterable<String> _allModules() => requireLoader.moduleParentsGraph.modules;
Future<Map<String, String>> _getDigests() async {
final client = BrowserClient();
final response = await client.get(Uri.parse(requireLoader.digestsPath));
return (jsonDecode(response.body) as Map).cast<String, String>();
}
Future<void> _initialize() async {
_lastKnownDigests = await _getDigests();
}
List<String> _moduleParents(String module) =>
requireLoader.moduleParentsGraph.parents(module);
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 {
final dart = sdk.dart;
// 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.
// ignore: unnecessary_lambdas
dartRunMain =
() {
dart.getMainLibrary(previousModuleId).main();
}.toJS;
} 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.toJS,
// Removing the argument type in complete()
// ignore: unnecessary_lambdas
() {
completer.complete();
}.toJS,
(JsError e) {
completer.completeError(
HotReloadFailedException(e.message),
stackTrace,
);
}.toJS,
);
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 (final module in stronglyConnectedComponents[i]) {
_moduleOrdering[module] = i;
}
}
}
static Future<RequireRestarter> create() async {
final reloader = RequireRestarter._();
await reloader._initialize();
return reloader;
}
}