-
Notifications
You must be signed in to change notification settings - Fork 113
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
feat(ferry): add IsolateClient #405
Conversation
@@ -0,0 +1,145 @@ | |||
import 'dart:async'; | |||
import 'dart:isolate'; |
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 dart:isolate
is not supported on web, will this be an issue if we try to use this package in a web app? Should it be a separate package?
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 will fail if you try to use the IsolateClient, yes. The standard client will work though. But yes, it worth considering it moving to another package.
import 'package:ferry/ferry.dart'; | ||
|
||
abstract class IsolateCommand { | ||
SendPort sendPort; |
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 shouldn't have to add a sendPort to each command because we could use the sendPort that we receive when the isolate is created, right? (_IsolateInit#sendPort)
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 there can be many multiple Streams and one-off request active at the same time, using just one SendPort would make the communication more complicated, since we would have to track which Message belongs to which request.
It would certainly be possible if we gave every IsolateCommand instance a unique ID for example, but still - this makes the code way simpler IMO. Also in cases where we need to send the information that the main isolate has stopped listening to a stream.
It would be possible by using request IDs for example. However, according to the the flutter team, creating ReceivePorts
is pretty cheap performance wise ( https://discord.com/channels/608014603317936148/608016344520196173/1019723108283850762 ). so I am not sure if it is worth 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.
I see. Sounds fair.
Future<Client> _initClientIsolate(Map<String, dynamic>? params, SendPort? sendPort) async { | ||
// don't use Hive.initFlutter to avoid dealing with method channels in the isolate | ||
// instead, call getApplicationDocumentsDirectory() on the main isolate | ||
// and pass the result to the ferry isolate | ||
Hive.init(params!["hivePath"]); |
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 may as well make params required
if we expect a hivePath and a url.
Actually, it should be a class rather than a Map.
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.
Changed to Map to be non-nullable now.
Good point. Currently, the signature of the function that creates a the client on the new Isolate must be
typedef InitClient = Future<TypedLinkWithCache> Function(
Map<String, dynamic> params,
SendPort? sendMessageToMessageHandler,
);
do you think we should make it generic, like
typedef InitClient<InitParams> = Future<TypedLinkWithCache> Function(
InitParmams params,
SendPort? sendMessageToMessageHandler,
);
and change the create() method of the IsolateClient to
static Future<IsolateClient> create<InitParams>(InitClient<InitParams> initClient,
{InitParams params,
void Function(Object?)? messageHandler,
IsolateSpawn? isolateSpawn})
This would be typesafe, which is definitely a big plus.
However, it could lead to users trying the send objects which cannot be serialized over isolates (like SharedPreferences, HiveBoxes, Closures... basically anything that is not just data), while Map<String, dynamic> is already typically used for json-like 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.
Well, Isolate.spawn is generic so I'd say it's fair to do the same.
We can always add an API comment with ///
to say "Make sure InitParams is serializable as it will be passed to the isolate".
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.
True. I'll update it.
final disposeCommand = command as DisposeCommand; | ||
disposeCommand.sendPort.send(null); | ||
Isolate.exit(disposeCommand.sendPort); | ||
void handleCommand( |
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.
A more SOLID approach would be to add an abstract method called handler
to the IsolateCommand
class.
This way you would force all the implementing classes to implement this method and you wouldn't need this big switch/case or the CommandType enum.
In the future, if we need to add a new command, we will just create a new class that extends IsolateCommand
and we wouldn't have to touch other parts of the code.
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 agree that this would be the right way to do it usually, not sure in this specific instance though.
I didn't really look into the serialization format the dart uses when sending objects through isolates, but wouldn't that lead to overhead? Since the commands are created on the main isolate, then serialized, sent to the ferry isolate, and then deserialized to an IsolateCommand object again, the commands should probably be as lightweight as possible.
So to achieve a SOLID approach without sacrificing performance we would need to maintain a parallel class structure on the ferry isolate side, which I feel is overkill as long as there are only a handful of commands.
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're right.
dec0985
to
685db93
Compare
…tead of a Map<String,dynamic>
- ferry@0.11.1 - ferry_exec@0.1.6 - ferry_flutter@0.6.1+2 - ferry_generator@0.6.0+1 - ferry_cache@0.6.2+1 - ferry_test_graphql@0.1.5-dev.4
added IsolateClient, which runs queries in another isolate in order to avoid jank on expensive normalization
This should solve all remaining issues regarding jank.
Also added example on how to use it in pokemon_explorer.
TODOs:
This is almost ready to be merged now.
I'd appreciate comments, suggestions for improvements, and any feedback.
@GP4cK @2shrestha22 @ValentinVignal