If anything getting this right on the front-end is often extra problematic because of the high cost of round-tripping state to the server.
It's dangerous to assume that just because JS has a single main thread that you don't need to think about sequencing of operations and locking.
> All this async code without decent locking primitives is leading to a rabbit hole of race conditions...
No, it's not. Here's a lock:
let lock = Promise.resolve()
// ...
lock = lock.then(() => { /* critical section */ })
For the rare instances where more complex async flow control is required, there are a bunch of libraries for that (e.g. async[0], p-queue[1], etc.).To lay out one example as clearly as I can: suppose you had two calls to transfer with the same args.
* The 2nd call to `await acc1.balance()` was very quick beating the 1st call
* The `current_balance` is greater than `amount` and so it runs `await acct1.sub(amount)`
* Before that can finish, the 1st call to `balance()` returns with the old balance before subtraction begins.
* Now another transfer is initiated with the incorrect amount.
If `amount` is larger than `current balance` you've got an issue on your hand.
I believe one could implement a mechanism around this using a set of booleans/ints for each account and managing the call to `transfer` with each one of those. But that's the point - we could have primitives to do that.
By the way, there is a particular issue with this specific example. The `transfer` function signature must be marked as `async`. You would need to add this to make it run properly.