PhaseLock

Optimistic updates should be free

PhaseLock is a sync engine built around a simple idea: save your events as a log, which is easy to replicate, and write a reducer that builds state from those events, which can be run anywhere. The result is one sync engine for your web app, your mobile app, and even your backend.

Imagine this nightmarish UX: a user edits a text field, then hits enter. The text immediately returns to the original value. Then a half second later, it snaps back to the updated value. This flicker happens after every single user edit.

That's exactly what happens when an app built on a sync engine doesn't solve the "optimistic updates" problem.

A sync engine is supposed to cache server state locally and render from that cache. As server updates arrive, the cache updates and so does the UI. But if the app only renders from the cache, then the client can't update the UI without a network round trip, causing the edit flicker.

"Optimistic updates" means that what renders in the UI is a combination of known server state plus in-flight client edits.

Additionally, if multiple clients submit conflicting sequences of edits, then one of them will lose and have to roll back the effect of its failed edits in its UI. And if it had multiple edits in flight, it might need to know how to rebase its subsequent edits against the new server state.

This situation is surprisingly similar to a bad git merge conflict, both in mechanism and in how bad it hurts your brain to think about it:

In PhaseLock, all of these situations are resolved automatically using business logic you already had to write.

The forecast function

A quick reminder of the two basic kinds of functions you write for PhaseLock:

Optimistic updates require a third kind of function: forecast. forecast is the client-side mirror of precommit. While precommit runs on the server, can access global state, and emits authoritative events, forecast runs on the client, can only see its client's view of state, and emits optimistic events (meaning they may not match what the server eventually produces).

"How can you claim optimistic updates are resolved automatically if I need to write a new function?" you ask. Great question. It turns out the extra function usually requires only a refactor of existing business logic.

Think of it this way. Processing a command to generate events requires answering two questions:

For any given command, forecast only answers event generation, while precommit must answer both. So usually precommit is just an auth check followed by a call to the forecast function; no new logic is needed.

Putting it all together

Here's the whole cycle:

The app UI turns user edits into commands. Each command forks: one branch goes over the network, where precommit() appends events to the event log and the server sends them back to be reduced into the client store; the other branch is forecast() on the client into events that are reduced into an overlay. The client store shows through the overlay, and the app UI queries the overlay. Life of a PhaseLock UI Update User edits are sent to the server as commands. CLIENT SERVER precommit() reducer() forecast() reducer() OPTIMISTIC PATH Forecasted events update the UI immediately. DURABLE PATH Persisted events are sent back to the client. App UI Commands Events Events Event Log Client Store Overlay

The optimistic path completes locally with zero latency. A PhaseLock app's UI shows the forecasted effect of a user edit immediately. Even offline mode works, because offline mode is just optimistic updates in front of a pathologically slow network.

So the implementation is roughly:

If multiple commands were in flight and only the first is resolved, or if unrelated events arrive from the server, then the old overlay is discarded and a fresh forecast sequence occurs with the remaining in-flight commands and the updated client store. A new overlay is built and the UI reflects an updated optimistic state.

Conclusion

Now we have enough detail to answer every difficult question from before:

All you define is what events to generate per command given current state, and PhaseLock does the rest.