-
Notifications
You must be signed in to change notification settings - Fork 213
Basic graph handling for hot reload #1759
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
We still lack handling situations when graph is dynamically updated between reloads.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will need some docs on the hooks that are available and how exactly frameworks/users can take advantage of them. You could add a new markdown file under the docs/
folder and link to it from the readme.
Note that we actually are guaranteed that there are no cycles in the module graph by the build_modules package, although it doesn't hurt to have the bit of supporting logic you have in here since it is relatively trivial.
SplayTreeSet<String> _dirty; | ||
Completer<void> _running = Completer()..complete(); | ||
|
||
int moduleTopologicalCompare(String module1, String module2) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you could make this a static or top level method, and then you can reference it in the initializer list of the constructor so that _dirty
can be final.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't, because it references to instance field _moduleOrdering
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah I missed that somehow, sgtm
final Iterable<String> Function() _allModules; | ||
|
||
final Map<String, int> _moduleOrdering = {}; | ||
SplayTreeSet<String> _dirty; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: _dirtyModules
or something just a bit more descriptive
_dirty = SplayTreeSet(moduleTopologicalCompare); | ||
} | ||
|
||
Future<void> run(List<String> modules) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets rename this to reload
or something along those lines (it isn't entirely clear what run
would do just from the name)
var moduleId = _dirty.first; | ||
_dirty.remove(moduleId); | ||
|
||
var existing = await _loadModule(moduleId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this fail?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume that it should neither fail, nor make any network requests, and should return running module from cache.
This assumption might be wrong with dynamic graph updates, but it isn't handled anywhere yet anyway
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we detect that situation and throw?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what should I detect right here. If for example module is no lodger exists, but it still left in the graph - require.js will throw here.
I feel it's better just to treat any graph changes as undefined behavior right now. I will explore how it can be handled (at least on level of detection and firing page reload) in next PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR is already to complicated to continue major work in it.
Map data; | ||
if (existing.hasOnDestroy) { | ||
data = {}; | ||
existing.onDestroy(data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like it might be better if we just allow this method to return a Map instead of modifying one its given
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be. In this case it may be anything, not only Map. I was trying to make design more compatible with webpack hot reloading api, but it isn't the same anyway
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are passing in a map though right now, making it a return value would actually allow us to support dynamic
or Object
, which is more flexible.
var newVersion = await _reloadModule(moduleId); | ||
bool success; | ||
if (newVersion.hasOnSelfUpdate) { | ||
success = newVersion.onSelfUpdate(data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would probably be better to pass an empty map here in the case where onDestroy
isn't implemented, instead of null (generally we try not to pass nulls for required args). Another option would be changing it to an optional argument.
if (newVersion.hasOnSelfUpdate) { | ||
success = newVersion.onSelfUpdate(data); | ||
} | ||
if (success == true) continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about we change this to return an Enum describing the possible states instead of treating null
in a special way. In general we try to avoid apis that have a return type but might actually return null
in Dart (interestingly I don't see a line item about this in effective dart, but its not a common pattern).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about it. But where we should declare this enum to use it in modules, and how to access to it from raw js code in bootstap?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you need access on the JS side I think you would have to make an enum-like object in JS, and then you can access that via package:js.
Essentially, in JS you would have some global object available that looks like this:
someGlobalScope.MyEnum = { 'bar': Symbol('bar'), 'baz': Symbol('baz') };
And then you can convert that to a Dart class with static getters like this:
@JS('someGlobalScope.MyEnum ')
abstract class MyEnum {
external static int get bar;
external static int get baz;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, but where to place this JS. It will be a part of public api, so I feel it a bad idea to put it inside bootstrap file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be able to inject it in the same place we inject the other hot reloading code (based on the entrypoint marker?)
} | ||
|
||
var parentIds = _moduleParents(moduleId); | ||
if (parentIds == null || parentIds.isEmpty) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What real-world use case is this handling? It would be good to leave a comment here to describe it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we use something different than bootstrap file, we might have no hooks implementations in whole chain. In this case we should reload whole page. We have a test for it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok, so if we bubble up to the root then refresh - makes sense.
@@ -324,6 +336,16 @@ require.config({ | |||
waitSeconds: 0, | |||
paths: customModulePaths | |||
}); | |||
|
|||
requirejs.onResourceLoad = function (context, map, depArray) { | |||
// TODO Handle dynamic changes in module dependancies |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may already be handling this (if this function is invoked whenever new modules are loaded)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New - might be. But deleting or changing parent - no. Currently graph is newer invalidated, that may cause cycles in it and who knows what other weird behavior.
I not feel this api is stabilized yet. I need to try to implement something in client modules first. I anticipate problems with name mangling if I'd try to just export functions with such names from dart code. I will write the docs when it's ready. For now there are doc comments to don't forget what it should do. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets create issues and assign them to the HotRestart github project to track any ongoing work that will be addressed in future PR's, and then we can link them in TODO's as well.
Map data; | ||
if (existing.hasOnDestroy) { | ||
data = {}; | ||
existing.onDestroy(data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are passing in a map though right now, making it a return value would actually allow us to support dynamic
or Object
, which is more flexible.
if (newVersion.hasOnSelfUpdate) { | ||
success = newVersion.onSelfUpdate(data); | ||
} | ||
if (success == true) continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you need access on the JS side I think you would have to make an enum-like object in JS, and then you can access that via package:js.
Essentially, in JS you would have some global object available that looks like this:
someGlobalScope.MyEnum = { 'bar': Symbol('bar'), 'baz': Symbol('baz') };
And then you can convert that to a Dart class with static getters like this:
@JS('someGlobalScope.MyEnum ')
abstract class MyEnum {
external static int get bar;
external static int get baz;
}
} | ||
|
||
var parentIds = _moduleParents(moduleId); | ||
if (parentIds == null || parentIds.isEmpty) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok, so if we bubble up to the root then refresh - makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM I think this is at a good enough point to submit for now and we have issues filed to track future work that needs to happen.
* Add assets digests handler (#1709) This entry point will be used for hot reloading, in order to initially retrieve digests of all assets, as build_runner might not know list of all of them, unlike client. * Emit useful information about build results (#1710) Instead of just 'update' emit json with updated build results and its digests * Move live reload client code to new hot_reload_client.dart file (#1732) It is expected that client code will become complicated, so in separate dart file it would be easier to test and maintain. * Implement basic hot reload (#1741) Reload any changed modules without any specific order, then reload main module and call main func to hopefully apply changes. Still need to get module dependency graph to reload in topological order and find correct ancessor chain where reloaded modules can be saved in closures. * Basic graph handling for hot reload (#1759) We still lack handling situations when graph is dynamically updated between reloads. * Add rough graph changes handling by full page reload (#1763) * Simplify module interface (#1764) * Change HMR API to work with several libraries bundled in one module (#1765) Fixes #1762 * Close Hot Reload listeners on exit (#1775) Closes #1774 * Refactor and improve HMR code (#1770) - Get rid of `roughLibraryKeyDecode` - use library path exposed from dart runtime now. - Get load modules from cache directly and throw if it isn't loaded instead of using require.js and hoping that module already loaded and no network request would be done. - Don't mess with `.ddc` extension in `build_runner` code - encapsulate all of it on build_web_compilers side. Fixes #1760 Fixes #1762 * Finalize hot-reloading feature (#1773) - Rename live-reload to hot-reload - Add debug logging about reloading - Add documentation - Version bump and changelog Fixes #1766 * Restore --live-reload option (#1778)
Fixes #1743
We still lack handling situations when graph is dynamically updated
between reloads.