Skip to content
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

presence: Modelify; use Immutable; make more extensible #5697

Merged
merged 11 commits into from
Mar 29, 2023

Conversation

gnprice
Copy link
Member

@gnprice gnprice commented Mar 29, 2023

This is preparation for #5669.

For that issue, we'll want to store a couple more pieces of data from the server and use them in various presence-related logic. The way that logic is currently structured makes it pretty inflexible for this kind of change — it's a lot of disparate fragments of the state passed in as separate arguments to each of many different helper functions. So, here's a series of refactorings that takes us most of the way toward being able to straightforwardly add new information of the kind needed in #5669.

As a bonus, we replace the object-as-map in state.presence with an Immutable.Map.

gnprice added 5 commits March 28, 2023 18:09
This reduces the amount of code that thinks it knows what a
PresenceState looks like on the inside.  That will help set us up
to rearrange those and add more data to them, for zulip#5669.
Now the only code that interacts with this detail is one getter,
the reducer, and the type definition.
@gnprice gnprice added P1 high-priority api migrations a-presence/status Presence, and "away status" server release goal Things we should try to coordinate with a major Zulip Server release. labels Mar 29, 2023
Copy link
Contributor

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, LGTM! Some small things below; please merge at will after seeing those.

// mark it required.
const data = action.data.presences ?? {};
return {
byEmail: Immutable.Map(data),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, should this style-guide entry—

Don't construct an Immutable.Map with an object-as-map: Whenever you
create a non-empty Immutable.Map, pass an array of entries instead of an
object-as-map. For example:

Immutable.Map([[HOME_NARROW_STR, [1, 2]]]) // good

Immutable.Map({ [HOME_NARROW_STR]: [1, 2] }) // BAD -- don't do

This is essential in order to get effective type-checking of the value
passed to the Immutable.Map function when a key is computed (like
HOME_NARROW_STR) instead of literal (like 'foo'). In the Immutable type
definition, the object-as-map branch involves an object type with an indexer
property, and in general Flow seems to have trouble with those.

—only forbid passing a literal object-as-map? If I assert that the object-as-map data has number values instead of UserPresence values, Flow does complain about that.

Oddly I don't get a Flow error with this:

diff --git src/presence/presenceModel.js src/presence/presenceModel.js
index a46559acb..9a3e8f2e3 100644
--- src/presence/presenceModel.js
+++ src/presence/presenceModel.js
@@ -225,7 +225,7 @@ export function reducer(
       //   mark it required.
       const data = action.data.presences ?? {};
       return {
-        byEmail: Immutable.Map(data),
+        byEmail: Immutable.Map<string, number>(data),
       };
     }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm interesting. I'd forgotten that style-guide entry, thanks 😅

It seems like the type-checking is effective in this case. For example if I make the value type in the input data more general:

        byEmail: Immutable.Map((data: {| +[string]: mixed |})),

then I get an appropriate error:

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/presence/presenceModel.js:228:53

Cannot return object literal because mixed [1] is incompatible with
`UserPresence` [2] in type argument `V` [3] of property `byEmail`.
[incompatible-return]

   src/presence/presenceModel.js:228:53
    228│         byEmail: Immutable.Map((data: {| +[string]: mixed |})),
                                                             ^^^^^ [1]

References:
   src/reduxTypes.js:221:34
    221│   byEmail: Immutable.Map<string, UserPresence>,
                                          ^^^^^^^^^^^^ [2]
   node_modules/immutable/dist/immutable.js.flow:1056:23
   1056│ declare class Map<K, +V>
                               ^ [3]

Looking back at the chat thread we foresightedly linked to in that style-guide entry, and experimenting some more, I think the key thing that triggers that Flow issue is that it's not only an object literal but one with a computed property.

Compare:

      (Immutable.Map({ ['a' + 'b']: { older: true } }): Immutable.Map<string, UserPresence>);
      (Immutable.Map({ ab: { older: true } }): Immutable.Map<string, UserPresence>);

The first one gets no error, just like in that thread. But the first one correctly gets an error.

So I think we can narrow that style-guide proscription to apply only to computed property names — not to objects-as-maps in general, or even to object literals as maps.

//

const initialState: PresenceState = {
byEmail: Immutable.Map(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, from style guide:

Always provide a type when writing an empty Immutable value:
Whenever you create an empty Immutable.Map, Immutable.List, or
so on, specify the intended type explicitly. For example:

const map: Immutable.Map<number, string> = Immutable.Map(); // good

const map = Immutable.Map(); // BAD -- don't do

This is essential in order to get effective type-checking of the
code that uses the new collection. (It's not clear if this is a bug
in Flow, or a design limitation of Flow, or an issue in the Flow types
provided by Immutable.js, or some combination.)

I think it could look like this:

diff --git src/presence/presenceModel.js src/presence/presenceModel.js
index a46559acb..a9f112efa 100644
--- src/presence/presenceModel.js
+++ src/presence/presenceModel.js
@@ -205,7 +205,7 @@ export const getPresenceStatusForUserId = (
 //
 
 const initialState: PresenceState = {
-  byEmail: Immutable.Map(),
+  byEmail: Immutable.Map<string, UserPresence>(),
 };

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'd forgotten to consider that.

I think this is covered, though, by the type annotation on the object as a whole — that : PresenceState. That's explicit in much the same way as the annotation in the "good" example.

And you can confirm it's effective by writing an expression like initialState.byEmail.values() and hovering in the IDE, to see it returns an iterator of UserPresence.

// whether any servers actually omit the data. The API doc
// doesn't mention any servers that omit it, and our Flow types
// mark it required.
const data = action.data.presences ?? {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think the TODO is ripe for taking care of: we do enforce a threshold for ancient servers we refuse to connect to. (I noticed this when I saw the change from || to ?? and wondered if that might change any behavior, given our imperfect knowledge of ancient servers.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yeah, I guess that's right. I'll add a commit for that.

gnprice added 6 commits March 29, 2023 14:47
For zulip#5669, we'll want to add a bit more information here alongside the
per-user data.  Insert an object sitting above the per-user map, to
be a place we can add those; and while we're here, make that per-user
map an Immutable.Map, instead of an object-as-map.
Now that zulip#5102 is done, there are definitely no servers we expect
to connect to that would send us data missing this property.
This is one of many, many "human" ways of describing a time.
And on its own it's really not a representation of "presence"
at all.
… data needs

This will make it easier for this selector to add yet one more little
piece of data it needs to consult.

It'll also give us the opportunity to simplify the logic a bit;
we'll do that next.

While here, reduce the number of separate times we consult Date.now.
…combining

This cuts a dead codepath (for falsy presence), and defers some
data lookups until we actually know we'll need them.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a-presence/status Presence, and "away status" api migrations P1 high-priority server release goal Things we should try to coordinate with a major Zulip Server release.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants