-
Notifications
You must be signed in to change notification settings - Fork 253
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
api: Add update_message
and delete_message
events
#212
api: Add update_message
and delete_message
events
#212
Conversation
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.
Thanks! Comments below.
lib/api/model/events.dart
Outdated
@@ -31,6 +31,8 @@ sealed class Event { | |||
default: return UnexpectedEvent.fromJson(json); | |||
} | |||
case 'message': return MessageEvent.fromJson(json); | |||
case 'update_message': return UpdateMessageEvent.fromJson(json); | |||
case 'delete_message': return UpdateMessageEvent.fromJson(json); |
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.
case 'delete_message': return UpdateMessageEvent.fromJson(json); | |
case 'delete_message': return DeleteMessageEvent.fromJson(json); |
🙂
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.
Eep! Thanks for the catch.
lib/api/model/events.dart
Outdated
final bool? renderingOnly; // TODO(server-5) | ||
final int messageId; | ||
final List<int> messageIds; | ||
final List<String> flags; |
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.
final List<String> flags; | |
final List<String> flags; // TODO enum |
like on Message
lib/api/model/events.dart
Outdated
final String? streamName; | ||
final int? streamId; | ||
final int? newStreamId; | ||
final String? propagateMode; |
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.
final String? propagateMode; | |
final String? propagateMode; // TODO enum |
Or maybe just make the enum.
lib/api/model/events.dart
Outdated
final String? propagateMode; | ||
final String? origSubject; | ||
final String? subject; | ||
final List<Object>? topicLinks; // TODO shape of objects |
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.
More precisely, the elements of this list will be Map<String, dynamic>
. (More precisely than Object
, that is.)
But if you don't feel like writing down the details, probably best to leave the whole field for later, like in Message
:
final List<Object>? topicLinks; // TODO shape of objects | |
// final List<TopicLink> topicLinks; // TODO handle |
In fact, given that we leave the field out in Message
, probably best to leave it out here too and leave that work for a single follow-up task across Message
and these events.
lib/api/model/events.dart
Outdated
final List<Object>? topicLinks; // TODO shape of objects | ||
final String? origContent; | ||
final String? origRenderedContent; | ||
final int? prevRenderedContentVersion; |
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.
Hmm, the docs say "Clients should ignore this field."
I'd take that as a deprecation, even though it doesn't use the word "deprecated":
final int? prevRenderedContentVersion; | |
// final int? prevRenderedContentVersion; // deprecated |
lib/api/model/events.dart
Outdated
|
||
final List<int> messageIds; | ||
// final int messageId; // Not present; we support the bulk_message_deletion capability | ||
final String messageType; |
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 is really an enum in the API, not an arbitrary string, so it should be an enum here.
… Or given the behavior of streamId
and topic
, the most type-safe version would be to make DeleteMessageEvent
abstract and have two subclasses DeleteStreamMessageEvent
and DeleteDmMessageEvent
, akin to Message
/StreamMessage
/DmMessage
. But the amount of code that's going to care about these objects is pretty limited — should be a small amount of code for handling the event in various data structures, and then we discard the event — so sticking with one class is fine so long as that code comes out reasonably clean.
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.
Yeah. Also, before using messageType
, streamId
, or topic
for anything, I'd want to better nail down what they actually mean, since (with bulk_message_deletion
) we're given a list of message IDs, not a single message ID. Is the list guaranteed to be all stream messages or all DMs? If all stream messages, will they definitely all be in the same stream/topic?
For any given message in message_ids
, it seems natural to just look up the message in our data structures and find out what its type is, and its stream/topic if relevant, if we need that information.
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.
Is the list guaranteed to be all stream messages or all DMs? If all stream messages, will they definitely all be in the same stream/topic?
Pretty sure if those properties are present, they'll apply to all the messages in the event. (And I guess one of them is always present.) But it'd be good to nail that down.
For any given message in
message_ids
, it seems natural to just look up the message in our data structures and find out what its type is, and its stream/topic if relevant, if we need that information.
Yeah. I'm not sure what the backstory was for why that other information is included here.
800b554
to
1744683
Compare
Thanks for the review! Revision pushed. |
1744683
to
c049e57
Compare
Thanks for the in-office discussion! Revision pushed. |
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.
Thanks! Looks good; merging.
FTR the main conclusion in our in-office discussion was that we're happy for this fromJson
to throw an exception if it sees a propagate_mode
that isn't one of the documented values.
Recording the reasoning there:
- In general, an exception in any
fromJson
method in our API will result in aMalformedServerResponseException
thrown from the endpoint binding. - There are lots and lots of conditions like that; in particular all the
as
operators in the generatedfromJson
implementations. If some value is a string or object where we expected int, or vice versa, or is missing when we expected it to be present, that will cause an exception. - The cost of this is that any server API change which would break one of those expectations is a breaking change, and we have to manage it with tools like client capabilities.
- But fundamentally such a change is going to be a breaking change anyway, unless what the client would actually do with the unexpected new form of data would be valid.
- In the case of
propagate_mode
: if there's some novel value for that, should the client stay on the same narrow, or navigate to a new one? It's impossible to know in advance; in practice we'd probably effectively default to one of the known enum values, which might or might not turn out to give the right behavior.
- In the case of
- And conversely, if we propagate the uncertainty (by e.g. having an event object but having one of its fields represent that there's something unknown), then that adds complexity on any code that's consuming the data.
- So there's a balance: if we can sensibly handle the unknown, then it leaves a wider swath of future API changes to be able to skip the overhead of managing compatibility (e.g. with client capabilities), but it adds client complexity. And often we can't sensibly handle the unknown with much confidence anyway.
Then some further thoughts that come to mind as I write that down.
Because of that balance, there are a few things we routinely do to leave things unspecified:
-
We never check for unexpected properties in a JSON object; adding a new property is always compatible. Effectively we assume that the old properties keep meaning what they meant; when that's not true, that's a breaking change after all.
This is valuable because a large fraction of API changes are to add a new property.
-
We have
UnexpectedEvent
for unexpectedtype
values in an event. The assumption we're relying on here is that if the event has a type we don't know about, then it's for some obscure new feature, so that we can sensibly handle it by ignoring that feature. Conversely this is is valuable because we regularly do add event types for new features.(Plus in the prototype phase it helps manage the existence of a bunch of event types we just haven't implemented yet. A lot of those are on basically the same logic: they're for some feature that, while not new or obscure, we haven't yet implemented and so there's nothing for us to do with that event until we have.)
-
When a property is deprecated, or when it seems like it should be deprecated even though the docs don't yet say so, then we treat it as if it already may not exist, even if the docs say it's always present.
Where possible, this means we leave it out of our types (with a comment for the sake of anyone comparing our types to the docs — like
UpdateMessageEvent.prevRenderedContentVersion
in this PR); if we do need to consult it (perhaps to handle older servers), we make it nullable.By definition, if the property should be deprecated, then we know what to do without it. And conversely this is valuable because we do regularly remove old bits of API; the main bottleneck to doing so is when it'd be a breaking change and require managing compatibility.
But in the great majority of unexpected situations other than adding a new property, it's not clear in advance what a client should do. So I think there are few situations outside the ones described above where we do take any steps to handle unexpected data from the API.
propagateMode: | ||
$enumDecodeNullable(_$PropagateModeEnumMap, json['propagate_mode']), |
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 find it pretty puzzling that the implementation of this goes and does a linear scan through all the possible values of the enum to find the one that matches:
for (var entry in enumValues.entries) {
if (entry.value == source) {
return entry.key;
}
}
But this enum has only three values, and the other one in this PR has only two, so it's fine.
If we had an enum with like 100 values that we were going to decode frequently, then I'd want to intervene to make this a map lookup instead.
c049e57
to
9b92676
Compare
No description provided.