Skip to content
This repository has been archived by the owner on Aug 18, 2020. It is now read-only.

Vanilla #31

Merged
merged 99 commits into from
Mar 13, 2018
Merged

Vanilla #31

merged 99 commits into from
Mar 13, 2018

Conversation

callum-oakley
Copy link
Contributor

@callum-oakley callum-oakley commented Feb 13, 2018

The majority of the functionality of the typescript library has now been translated to vanilla js and refactored along the way. Probably worth getting some feedback on this as it is before committing to the finishing touches.

API changes:

  • Promises instead of callbacks wherever that makes sense.
  • The existing "delegate" functionality is basically the same, but I'm proposing we call them "hooks" instead, since it seems like more familiar terminology in the javascript ecosystem.
  • subscribeToRoom will also join room if you're not already a member
  • consistent type checking of arguments
  • chatManager.connect doesn't resolve with the current user object until the full initial state is known (the initial user request has already completed, presence subscription has been established etc)
  • a smattering of method renamings and "was a room but now a roomId"

See luke's demo modified to use this branch: lukejacksonn/react-slack-clone#1

Internal changes:

  • RIP typescript
  • rollup instead of webpack, for no real reason other than that it's what I'm familiar with, and the build process was changing anyway with the move away from typescirpt. If this turns out to be a problem (for react native etc) I'm happy to move back to webpack.
  • abstract a lot of the async logic of "get this thing from the api unless we have it already, in which case just return the one we've already got" in to the user / room / presence store where before it was done case by case
  • (hopefully) general refactoring and simplifying

Tests

The integration tests that currently exist were super useful for doing the rewrite, and will continue to be useful to know we haven't broken stuff. But they aren't very good at testing failure paths, and won't scale much more (they already take >60s to run!). So we might want to unit test things too? I was loathed to do this immediately, but it might make sense once we've more or less settled on a good internal structure.

Remaining work

  • cursors support
  • rich media support
  • everything that currently has a TODO in the code
  • improved logging
  • make sure the error handling is watertight (it certainly isn't at the moment) still not, but probably "good enough for now"
  • a corresponding PR for the docs to reflect the changes to the public API
  • react native and worker builds
  • allow opt out of presence / cursors, etc
  • detailed changelog

Build

yarn lint:build

Run the tests

yarn lint:build:test

@callum-oakley callum-oakley mentioned this pull request Feb 13, 2018
Copy link
Contributor

@hamchapman hamchapman left a comment

Choose a reason for hiding this comment

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

GG this looks great. I can't even remember all of the little comments I left as I went but maybe some of them will be helpful!

/* public */

get rooms () {
return values(this.roomStore.snapshot())
Copy link
Contributor

Choose a reason for hiding this comment

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

[room|user]store.snapshot is schweeeeet ❤️

})
return Promise.all([
currentUser.establishUserSubscription(hooks),
currentUser.establishPresenceSubscription(hooks)
Copy link
Contributor

Choose a reason for hiding this comment

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

Presumably with the way it's set up at the moment, if the presence subscription fails then the promise returned from the call to connect will end up in the catch?

I think this is how I have it in the Swift one at the moment as well, but I'm not completely sure about it. I think it makes the most sense, but more so when we have the ability to disable presence and cursors, if a user so wishes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah good point, if either fails we'll end up in the catch. This is less of an issue when you can disable presence, but even then... if someone has everything on by default, would it be better to go on even if the presence subscription fails on the off chance the client isn't using any of the presence stuff? If they are it would break stuff later on, but maybe partial functionality is better than no functionality?

It wouldn't be a big change, we could just have establishPresenceSubscription always resolve, and the presence stuff would be undefined and users could handle that however they want... 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm loath to add more config but maybe we could have something akin to a strict mode, which is false by default, but it can be set to true if someone wants the behaviour of "if any of the initial connection requests fail then I want the catch block to be hit of the connection promise".

Then we can have the default being that as long as the user subscription succeeds, we resolve the promise. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

might be a good compromise yeah. I'll make a card to do the opting out of presence / cursors etc stuff and include this as part of it. We don't necessarily even have to document it but it would be nice to have if customers were like "wth everything broke horribly because the presence sub failed". That being said... if we're not going to document it, maybe we should just add it later if someone actually runs in to that problem. hmmmm

}
})
.then(res => {
const basicRoom = parseBasicRoom(JSON.parse(res))
Copy link
Contributor

Choose a reason for hiding this comment

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

Why a basic room and not a full room? Maybe this gets handled by adding a basic room to the room store - I've not gotten to that yet though!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, the this.roomStore.set(basicRoom.id, basicRoom) below returns a full Room :)

return this.apiInstance
.request({
method: 'GET',
path: `/users/${encodeURIComponent(this.id)}/rooms?joinable=true`
Copy link
Contributor

Choose a reason for hiding this comment

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

encodeURIComponent could probably just be called once at the point of initialisation to save doing it every time there's the id appearing in a path

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good shout 👍

})
.then(() => this.roomStore.pop(roomId))
.catch(err => {
this.logger.warn(`error joining room ${roomId}:`, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

joining -> leaving

)
return this.userStore.fetchMissingUsers(
uniq(map(prop('senderId'), messages))
).then(() => sort((x, y) => x.id - y.id, messages))
Copy link
Contributor

Choose a reason for hiding this comment

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

Need to figure out what's going on here - it looks clever, but haven't worked it out yet 😛

typeCheckObj('hooks', 'function', hooks)
messageLimit && typeCheck('messageLimit', 'number', messageLimit)
// TODO what is the desired behaviour if there is already a subscription to
// this room? Close the old one? Throw an error? Merge the hooks?
Copy link
Contributor

Choose a reason for hiding this comment

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

Excellent question - I think the easiest thing for us would be to close the existing subscription and just replace the whole thing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That seems like a good solution, in that case this depends on proper subscription cancelling and then it should be easy. I'll put it on the list!

src/message.js Outdated
this.text = basicMessage.text
this.createdAt = basicMessage.createdAt
this.updatedAt = basicMessage.updatedAt
this.userStore = userStore
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if the Message having a room and user store is genius or madness. Probably simplifies the "enrichment" process a lot, but then I can imagine there being a lot of getSync calls happening which makes me squirm a bit. Although I suppose that has to happen whether you get the sender or room later or at the point of message creation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah the reasoning was that I wanted to keep all the mutable state in the three "stores", and then everything else is always kept up to date for free. I don't think it should be too much of a performance hit, since getSync is O(1), but something to try if things get slow though for sure :)

pendingGets = [] // [{ key, resolve }]

initialize = initialStore => {
this.store = clone(initialStore)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why clone and not store the initialStore?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmmmm. The resoning when I wrote this was probably something like: if the consumer mutates initialStore after initialization then that could fuck everything up, buuuut since we only do all the initialization intenally we probably needn't bother.

src/utils.js Outdated
// appendQueryParam :: String -> String -> String -> String
export const appendQueryParam = (key, value, url) => {
const [ before, after ] = split('?', url)
return before + '?' + (after ? after + '&' : '') + urlEncode({ [key]: value })
Copy link
Contributor

Choose a reason for hiding this comment

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

3xr0chfisttai

}

/* internal */

uploadDataAttachment = (roomId, { file, name }) => {
// TODO some validation on allowed file names?
// TODO polyfill FormData?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We'll have to do this at some point (or use lower level methods), but the existing JS sdk uses FormData as well, and the current goal is to match functionality with what already exists asap so that we can get ths released.

@iMerica
Copy link

iMerica commented Feb 20, 2018

"delegate" functionality is basically the same, but I'm proposing we call them "hooks"

+1 for renaming delegate to hooks. 'Delegate' is popular in the Obj-C/Swift scene, but not as intuitive in the JS world.

.then(() => t.end())
.catch(endWithErr(t))
t.timeoutAfter(TEST_TIMEOUT)
})
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we missing a case for data attachments?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mdpye yes! my bad, going to add one now. (will cover fetchAttachment too)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@callum-oakley callum-oakley changed the title [WIP] Vanilla Vanilla Mar 1, 2018
@callum-oakley
Copy link
Contributor Author

Do we need to ship with updateRoom and deleteRoom? I really think this is a server concern (and you can't do either with default permissions). wdyt @hamchapman ?

@hamchapman
Copy link
Contributor

I'm tempted to keep them because I can see it being something that an admin-type user might want to be able to do from the client.

Is there a reason to remove them other than them requiring permissions elevated from the default?

@callum-oakley
Copy link
Contributor Author

nah not at all. Just trying to narrow down the list of "final bits and bobs" to do before releasing this. It's a short job to add them :)

@hamchapman
Copy link
Contributor

updateRoom & deleteRoom changes LGTM 👍

@callum-oakley callum-oakley merged commit 31e18b5 into master Mar 13, 2018
@callum-oakley callum-oakley deleted the vanilla branch March 13, 2018 15:32
@harrisrobin
Copy link

@callum-oakley pretty random but purely out of curiosity, what was the reason to move away from typescript internally? :)

Cheers 🍺

@callum-oakley
Copy link
Contributor Author

replied in slack :)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants