This moves the bulk of the sync logic into the base client, to avoid
deadlocks while someone tires to send messages from a event callback the
base client needed to get a bunch of locks.
Ideally the AsyncClient would not need a lock for the base client at all
but we're not there yet.