732 karma · joined October 2, 2008
Co-founder Papyrs. Wrote hobby OS, TriangleOS.
Feel free to contact me, wim at stunf.com or https://x.com/wcools/
Because of end-to-end-encryption and privacy/ownership concerns, we can't run plugin code on a server node somewhere. But for some types of apps, server-side extensions alone also feel too restrictive. An advantage of a malleable app over just one-shotting a completely custom app (and to be clear I think both will be popular!) is that extending and customizing something is often exactly what you want vs starting over, but you can't really do that when you can't do things like deeply customizing the UI. So you can just ask a (local) agent "add me this functionality" and it will likely be possible.
It's still sandboxed inside the browser though, so this way you can do things like permissions, allowing admins to restrict who installs team plugins and so on (which is also important for malleable multiplayer apps).
We can't just merge any type of operation by replaying unconditionally, because that can cause tree cycles for example. Like the linked list certain operations can be valid locally for an offline client, but not globally in the converged order.
Our sync engine distinguishes between different types of operations: unconditional operations and guarded operations.
Unconditional operations can't violate structural/data-model invariants. For example SetCompleted(task_guid, true). Simple last-writer-wins.
Guarded operations can mutate topology, like InsertMove(node_guid, parent_guid, after_guid). These are checked against the current state, not the state when they were created. During replay we first revert optimistic local operations and replay the incoming canonical operations in order. The mutator function for each operation validates the current state before applying it and if it would violate a condition (for example by creating a cycle because another client made another move in the meantime) it is deterministically rejected. In our case we also have an authoritative server so we can also use the same guarded operations for conditions beyond data structure, like permissions, so the AddUser(workspace_guid, user_guid) mutator function can check permissions state first for example.
https://github.com/chrislgarry/Apollo-11/blob/master/Comanch...
# THE POSITION OF THE MOON IS STORED IN THE COMPUTER IN THE FORM OF # A NINTH DEGREE POLYNOMIAL APPROXIMATION WHICH IS VALID OVER A 15 # DAY INTERVAL BEGINNING SHORTLY BEFORE LAUNCH. THEREFORE THE TIME # INPUT BY THE USER SHOULD FALL WITHIN THIS 15 DAY INTERVAL.
Better be back home in 15 days! ;)
If that passes that's not just EU funds being misused but the EU directly mandating spyware on a continental scale!
With structured apps (task managers, outliners) you lose the illusion of editing plain text, but plain text alone lacks things like structure, links, dates, and collaboration. We've spent the last few years building an editor completely from scratch to keep the ease of text editing while adding planning and structure.
I posted a little clip [1] of development on a multiplayer IDE for tasks/notes (local-first+e2ee), and a lot of people asked if it was native, rust, GPU rendered or similar. But it's just web tech.
The only "secret ingredients" here are using plain ES6 (no frameworks/libs), having data local-first with background sync, and using a worker for off-UI-thread tasks. Fast web apps are totally doable on the modern web, and sync engines are a big part of it.
Not saying frameworks are never the right answer of course, but it's as much a trade-off for complex apps as it is for blogs. Things like performance, bundle size, tooling complexity, easy of debugging and call stack depth, API stability, risk of hitting hard-to-work-around constraints all matter at scale too.
We developed our own sync engine for an offline-first IDE for notes/tasks [1] we're building, where the data structure is a tree (or graph actually) to support outlining operations. Conflict resolution is always the challenge, and especially with trees multiple offline players can optimistically commit local changes which would result in an invalid tree state once globally merged.
The way we handle this is by rolling back tree inserts when a client comes online and receives other tree insert deltas. From what I understand from the description of SQLSync, the best way to handle this would be to pull in a latest snapshot and then replay. Pulling in a full snapshot sounds rather expensive though. We have some sort of heuristic where we can do this if the number of incoming deltas would be very large, but for most offline syncing we never need it. Just curious how SQLSync defines these snapshots? Sticking with the note-taking example, in our case we can't really have a snapshot of a single "note" because of graph features like transclusions. Does SQLSync have some clever way to avoid having to send all state in order to "reset and replay"?
I definitely believe sync engines are the future as they make it so much easier to enable things like no-spinners browsing your data, optimistic rendering, offline use, real-time collaboration and so on.
I'm also not entirely convinced yet though that it's possible to get away with something that's not custom-built, or at least large parts of it. There were so many micro decisions and trade-offs going into the engine: what is the granularity of updates (characters, rows?) that we need and how does that affect the performance. Do we need a central server for things like permissions and real-time collaboration? If so do we want just deltas or also state snapshots for speedup. How much versioning do we need, what are implications of that? Is there end-to-end-encryption, how does that affect what the server can do. What kind of data structure is being synced, a simple list/map, or a graph with potential cycles? What kind of conflict resolution business logic do we need, where does that live?
It would be cool to have something general purpose so you don’t need to build any of this, but I wonder how much time it will save in practice. Maybe the answer really is to have all kinds of different sync engines to pick from and then you can decide whether it's worth the trade-off not having everything custom-built.
I'm not convinced at all free market economics are holding us back. Rather other parts of the world are leapfrogging us despite not adopting it.
It was always obvious EVs would be the future. I remember being at a friend's party in Germany more than a decade ago, talking to some guests who worked in the auto industry. Told them about this new upcoming car company called Tesla and how it would transform the entire transportation sector and eat their lunch. The entire group laughed me out of the room and talked about the amazing research they were doing on "clean diesel" or something, I kid you not.
It's simply a combination of innovator's dilemma, complacency and no appetite for risk taking.
We're very much still working on it though and it's finally at the point where it's just some polish, so pretty sure next Q.
You can always start with the cloud version and then transfer to self-hosted if you change your mind and continue right where you left off. Not just that, you can also move from self-hosted back to cloud. That makes it easier to get start with a service because you're free to change your mind without lock-in or risk of painful migrations.