A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.html 58KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang="fr">
  4. <!-- Has to be within the first 1024 bytes, hence before the `title` element
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset="utf-8">
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name="viewport" content="width=device-width,initial-scale=1">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>A comparison of JavaScript CRDTs (archive) — David Larlet</title>
  13. <meta name="description" content="Publication mise en cache pour en conserver une trace.">
  14. <!-- That good ol' feed, subscribe :). -->
  15. <link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
  16. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  17. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
  18. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
  19. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
  20. <link rel="manifest" href="/static/david/icons2/site.webmanifest">
  21. <link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
  22. <link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
  23. <meta name="msapplication-TileColor" content="#f7f7f7">
  24. <meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
  25. <meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
  26. <meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
  27. <!-- Is that even respected? Retrospectively? What a shAItshow…
  28. https://neil-clarke.com/block-the-bots-that-feed-ai-models-by-scraping-your-website/ -->
  29. <meta name="robots" content="noai, noimageai">
  30. <!-- Documented, feel free to shoot an email. -->
  31. <link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
  32. <!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
  33. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  34. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  35. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  36. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  37. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  38. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  39. <script>
  40. function toggleTheme(themeName) {
  41. document.documentElement.classList.toggle(
  42. 'forced-dark',
  43. themeName === 'dark'
  44. )
  45. document.documentElement.classList.toggle(
  46. 'forced-light',
  47. themeName === 'light'
  48. )
  49. }
  50. const selectedTheme = localStorage.getItem('theme')
  51. if (selectedTheme !== 'undefined') {
  52. toggleTheme(selectedTheme)
  53. }
  54. </script>
  55. <meta name="robots" content="noindex, nofollow">
  56. <meta content="origin-when-cross-origin" name="referrer">
  57. <!-- Canonical URL for SEO purposes -->
  58. <link rel="canonical" href="https://blog.notmyidea.org/a-comparison-of-javascript-crdts.html">
  59. <body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">
  60. <article>
  61. <header>
  62. <h1>A comparison of JavaScript CRDTs</h1>
  63. </header>
  64. <nav>
  65. <p class="center">
  66. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  67. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  68. </svg> Accueil</a> •
  69. <a href="https://blog.notmyidea.org/a-comparison-of-javascript-crdts.html" title="Lien vers le contenu original">Source originale</a>
  70. <br>
  71. Mis en cache le 2024-03-25
  72. </p>
  73. </nav>
  74. <hr>
  75. <p>Collaboration is one of the most requested features on <a href="https://umap-project.org">uMap</a>.
  76. I’ve talked <a href="https://blog.notmyidea.org/tag/umap.html">in previous articles</a> how
  77. we could add real-time features “the simple way”, by:</p>
  78. <ul>
  79. <li>a) catching when changes are done on the interface ;</li>
  80. <li>b) sending messages to the other parties and ;</li>
  81. <li>c) applying the changes on the receiving client.</li>
  82. </ul>
  83. <p>This works well in general, but it doesn’t take care of conflicts handling, especially when a disconnect can happen.</p>
  84. <p>For this reason, I got more into “Conflict-free Resolution Data Types” (CRDTs), with the goal of understanding what they are, how they work, what are the different libraries out there, and which one would be a good fit for us, if any.</p>
  85. <p>As things are changing quickly in this field, note that this article was written in March 2024.</p>
  86. <hr>
  87. <div class="toc">
  88. <ul>
  89. <li><a href="#part-1-what-are-crdts">Part 1 - What are CRDTs?</a><ul>
  90. <li><a href="#why-using-crdts">Why using&nbsp;CRDTs?</a></li>
  91. <li><a href="#traditional-data-synchronization-methods">Traditional data synchronization&nbsp;methods</a></li>
  92. <li><a href="#solving-complex-cases">Solving complex&nbsp;cases</a></li>
  93. <li><a href="#last-write-wins-registers">Last Write Wins&nbsp;Registers</a></li>
  94. <li><a href="#state-based-vs-operation-based">State-based vs Operation&nbsp;based</a></li>
  95. <li><a href="#how-the-server-fits-in-the-picture">How the server fits in the&nbsp;picture</a></li>
  96. <li><a href="#how-offline-is-handled">How offline is&nbsp;handled</a></li>
  97. </ul>
  98. </li>
  99. <li><a href="#part-2-javascript-crdts">Part 2: JavaScript CRDTs</a><ul>
  100. <li><a href="#the-demo-application">The demo&nbsp;application</a></li>
  101. <li><a href="#yjs">Y.js</a></li>
  102. <li><a href="#automerge">Automerge</a></li>
  103. <li><a href="#json-joy"><span class="caps">JSON</span>&nbsp;Joy</a></li>
  104. </ul>
  105. </li>
  106. <li><a href="#part-3-comparison-table">Part 3: Comparison table</a><ul>
  107. <li><a href="#working-with-patches">Working with&nbsp;patches</a></li>
  108. <li><a href="#conclusion">Conclusion</a></li>
  109. </ul>
  110. </li>
  111. <li><a href="#extra-notes">Extra notes</a><ul>
  112. <li><a href="#yata-and-rga"><span class="caps">YATA</span> and <span class="caps">RGA</span></a></li>
  113. <li><a href="#resources">Resources</a></li>
  114. </ul>
  115. </li>
  116. </ul>
  117. </div>
  118. <hr>
  119. <h2 id="part-1-what-are-crdts">Part 1 - What are CRDTs?</h2>
  120. <p>Conflict-free Resolution Data Types are a family of data types able to merge their states with other states without generating conflicts. They handle consistency in distributed systems, making them particularly well-suited for collaborative real-time applications.</p>
  121. <p>CRDTs ensure that multiple participants can make changes without strict coordination, and all replicas converge to the same state upon synchronization, without conflicts.</p>
  122. <p><span class="dquo">“</span>Append-only sets” are probably one of the most common type of <span class="caps">CRDT</span>: you can add the same element again and again, it will only be present once in the set. It’s our old friend <code>Set</code>, as we can find in many programming languages.</p>
  123. <h3 id="why-using-crdts">Why using CRDTs?</h3>
  124. <p>For uMap, CRDTs offer a solution to several challenges:</p>
  125. <ol>
  126. <li>
  127. <p><strong>Simultaneous Editing</strong>: When multiple users interact with the same map, their changes must not only be reflected in real-time but also merged seamlessly without overwriting each other’s contributions. We need all the replicas to converge to the same state.</p>
  128. </li>
  129. <li>
  130. <p><strong>Network Latency and Partition</strong>: uMap operates over networks that can experience delays or temporary outages (think editing on the ground). CRDTs can handle these conditions gracefully, enabling offline editing and eventual consistency.</p>
  131. </li>
  132. <li>
  133. <p><strong>Simplified Conflict Resolution</strong>: Traditional methods often require complex algorithms to resolve conflicts, while CRDTs inherently minimize the occurrence of conflicts altogether.</p>
  134. </li>
  135. <li>
  136. <p><strong>Server load</strong>: uMap currently relies on central servers (one per instance). Adopting CRDTs could help lower the work done on the server, increasing resilience and scalability.</p>
  137. </li>
  138. </ol>
  139. <h3 id="traditional-data-synchronization-methods">Traditional data synchronization methods</h3>
  140. <p>Traditional data synchronization methods typically rely on a central source of truth (the server) to manage and resolve conflicts. When changes are made by different users, these traditional systems require a round-trip to the server for conflict resolution and thus can be slow or inadequate for real-time collaboration.</p>
  141. <p>In contrast, CRDTs leverage mathematical properties (the fact that the data types can converge) to ensure that every replica independently reaches the same state, without the need for a central authority, thus minimizing the amount of coordination and communication needed between nodes.</p>
  142. <p>This ability to maintain consistency sets CRDTs apart from conventional synchronization approaches and makes them particularly valuable for the development of collaborative tools like uMap, where real-time updates and reliability are important.</p>
  143. <h3 id="solving-complex-cases">Solving complex cases</h3>
  144. <p>At first, I found CRDTs somewhat confusing, owing to their role in addressing complex challenges. CRDTs come in various forms, with much of their intricacy tied to resolving content conflicts within textual data or elaborate hierarchical structures.</p>
  145. <p>Fortunately for us, our use case is comparatively straightforward, and we probably only need <span class="caps">LWW</span> registers.</p>
  146. <h3 id="last-write-wins-registers">Last Write Wins Registers</h3>
  147. <p>As you might have guessed from the name, a <span class="caps">LWW</span> register is a specific type of <span class="caps">CRDT</span> which “just” replaces the value with the last write. The main concern is establishing the sequence of updates, to order them together (who is the last one?).</p>
  148. <p>In a single-client scenario or with a central time reference, sequencing is straightforward. However, in a distributed environment, time discrepancies across peers can complicate things, as clocks may drift and lose synchronization.</p>
  149. <p>To address this, CRDTs use vector clocks — a specialized data structure that helps to solve the relative timing of events across distributed systems and pinpoint any inconsistencies.</p>
  150. <blockquote>
  151. <p>A vector clock is a data structure used for determining the partial ordering of events in a distributed system and detecting causality violations.</p>
  152. <p>– <a href="https://en.wikipedia.org/wiki/Vector_clock">Wikipedia</a></p>
  153. </blockquote>
  154. <p>Note that we could also use a library such as <a href="https://github.com/pubkey/rxdb">rxdb</a> — to handle the syncing, offline, etc. — because we have a master: we use the server, and we can use it to handle the merge conflicts.
  155. But by doing so, we would give more responsibility to the server, whereas when using CRDTs it’s possible to do the merge only on the clients (enabling no-master replications).</p>
  156. <h3 id="state-based-vs-operation-based">State-based vs Operation based</h3>
  157. <p>While reading the literature, I found that there are two kinds of CRDTs: state-based and operation-based. It turns out most of the CRDTs implementation I looked at are operation-based, and propose an <span class="caps">API</span> to interact with them as you’re changing the state, so <strong>it doesn’t really matter</strong> in practice.</p>
  158. <blockquote>
  159. <p>The two alternatives are theoretically equivalent, as each can emulate the
  160. other. However, there are practical differences. State-based CRDTs are
  161. often simpler to design and to implement; their only requirement from the
  162. communication substrate is some kind of gossip protocol. <strong>Their drawback is that
  163. the entire state of every <span class="caps">CRDT</span> must be transmitted eventually to every other
  164. replica, which may be costly</strong>. In contrast, operation-based CRDTs transmit only
  165. the update operations, which are typically small.</p>
  166. <p><a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type">Wikipedia on CRDTs</a></p>
  167. </blockquote>
  168. <h3 id="how-the-server-fits-in-the-picture">How the server fits in the picture</h3>
  169. <p>While discussing with the automerge team, I understood that I was expecting the server to pass along the messages to the other parties, and that would be the way the synchronization would be done. It turns out I was mistaken: in this approach, the clients send updates to the server, which merges everything together and only then sends the updates to the other peers. It makes it easy for the server to send back the needed information to the clients (for new peers, or if the peers didn’t cache the data locally).</p>
  170. <p>In order to have peers working with each other, I would need to change the way the provider works, so we can have the server be “brainless” and just relay the messages.</p>
  171. <p>For automerge, it would mean the provider will “just” handle the websocket connection (disconnect and reconnect) and all the peers would be able to talk with each other. The other solution for us would be to have the merge algorithm working on the server side, which comes with upsides (no need to find when the document should be saved by the client to the server) and downsides (it takes some cpu and memory to run the CRDTs on the server)</p>
  172. <h3 id="how-offline-is-handled">How offline is handled</h3>
  173. <p>I was curious about how offline editing might work, and what would happen when going back online. Changes can happen both online and offline, making no difference for the “reconciliation” step. When going back online, a “patch” is computed by the newly reconnected peer, and sent to the other peers.</p>
  174. <hr>
  175. <h2 id="part-2-javascript-crdts">Part 2: JavaScript CRDTs</h2>
  176. <p>Now that we’re familiar with CRDTs and how they can help us, let’s create a map application which syncs marker positions, on different browsers.</p>
  177. <p>We’ll be comparing three JavaScript libraries: <a href="https://yjs.dev/">Y.js</a>, <a href="https://automerge.org/">Automerge</a> and <a href="https://jsonjoy.com"><span class="caps">JSON</span> Joy</a>, considering:</p>
  178. <ol>
  179. <li><strong>Their external <span class="caps">API</span></strong>: is it easy to use in our case? What are the challenging parts?</li>
  180. <li><strong>Community and Support</strong>: What is the size and activity of the developer community / ecosystem?</li>
  181. <li><strong>Size of the <span class="caps">JS</span> library</strong>, because we want to limit the impact on our users browsers.</li>
  182. <li><strong>Efficiency</strong>: Probe the bandwidth when doing edits. What’s being transmitted over the wire? </li>
  183. </ol>
  184. <p>I setup a demo application for each of the libraries. Everything is available <a href="https://gitlab.com/umap-project/leaflet-sync">in a git repository</a> if you want to try it out yourself.</p>
  185. <h3 id="the-demo-application">The demo application</h3>
  186. <p>All the demos are made against the same set of features. It</p>
  187. <ul>
  188. <li>Creates a marker when the map is clicked</li>
  189. <li>Moves the markers on hover.</li>
  190. </ul>
  191. <p>This should probably be enough for us to try out.</p>
  192. <p>Here’s the whole code for this, using <a href="https://leafletjs.com/">Leaflet - a JavaScript library for interactive maps</a>. </p>
  193. <div class="highlight"><pre><span></span><code><span class="k">import</span><span class="w"> </span><span class="nx">L</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s2">"leaflet"</span><span class="p">;</span>
  194. <span class="k">import</span><span class="w"> </span><span class="s2">"leaflet/dist/leaflet.css"</span><span class="p">;</span>
  195. <span class="c1">// Create a map with a default tilelayer.</span>
  196. <span class="kd">const</span><span class="w"> </span><span class="nx">map</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">L</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="s2">"map"</span><span class="p">).</span><span class="nx">setView</span><span class="p">([</span><span class="mf">48.1173</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mf">1.6778</span><span class="p">],</span><span class="w"> </span><span class="mf">13</span><span class="p">);</span>
  197. <span class="nx">L</span><span class="p">.</span><span class="nx">tileLayer</span><span class="p">(</span><span class="s2">"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
  198. <span class="w"> </span><span class="nx">maxZoom</span><span class="o">:</span><span class="w"> </span><span class="mf">19</span><span class="p">,</span>
  199. <span class="w"> </span><span class="nx">attribution</span><span class="o">:</span><span class="w"> </span><span class="s2">"© OpenStreetMap contributors"</span><span class="p">,</span>
  200. <span class="p">}).</span><span class="nx">addTo</span><span class="p">(</span><span class="nx">map</span><span class="p">);</span>
  201. <span class="c1">// Features contains a reference to the marker objects, mapped by the uuids</span>
  202. <span class="kd">let</span><span class="w"> </span><span class="nx">features</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{};</span>
  203. <span class="c1">// An upsert function, creating a marker at the passed latlng.</span>
  204. <span class="c1">// If an uuid is provided, it changes the coordinates at the given address</span>
  205. <span class="kd">function</span><span class="w"> </span><span class="nx">upsertMarker</span><span class="p">({</span><span class="w"> </span><span class="nx">latlng</span><span class="p">,</span><span class="w"> </span><span class="nx">uuid</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">{</span>
  206. <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">uuid</span><span class="p">)</span><span class="w"> </span><span class="nx">uuid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">crypto</span><span class="p">.</span><span class="nx">randomUUID</span><span class="p">();</span>
  207. <span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">marker</span><span class="p">;</span><span class="w"> </span>
  208. <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">features</span><span class="p">).</span><span class="nx">includes</span><span class="p">(</span><span class="nx">uuid</span><span class="p">))</span><span class="w"> </span><span class="p">{</span>
  209. <span class="w"> </span><span class="nx">marker</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">features</span><span class="p">[</span><span class="nx">uuid</span><span class="p">];</span>
  210. <span class="w"> </span><span class="nx">marker</span><span class="p">.</span><span class="nx">setLatLng</span><span class="p">(</span><span class="nx">latlng</span><span class="p">);</span>
  211. <span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
  212. <span class="w"> </span><span class="nx">marker</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">L</span><span class="p">.</span><span class="nx">marker</span><span class="p">(</span><span class="nx">latlng</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
  213. <span class="w"> </span><span class="nx">draggable</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
  214. <span class="w"> </span><span class="nx">autoPan</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
  215. <span class="w"> </span><span class="nx">uuid</span><span class="o">:</span><span class="w"> </span><span class="nx">uuid</span><span class="p">,</span>
  216. <span class="w"> </span><span class="p">});</span>
  217. <span class="w"> </span><span class="nx">features</span><span class="p">[</span><span class="nx">uuid</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">marker</span><span class="p">;</span>
  218. <span class="w"> </span><span class="nx">marker</span><span class="p">.</span><span class="nx">addTo</span><span class="p">(</span><span class="nx">map</span><span class="p">);</span>
  219. <span class="w"> </span><span class="nx">marker</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">"dragend"</span><span class="p">,</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">target</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
  220. <span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"dragend"</span><span class="p">);</span>
  221. <span class="w"> </span><span class="p">});</span>
  222. <span class="w"> </span><span class="p">}</span>
  223. <span class="p">}</span>
  224. <span class="c1">// Add new features to the map with a click</span>
  225. <span class="nx">map</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">"click"</span><span class="p">,</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">latlng</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
  226. <span class="w"> </span><span class="nx">upsertMarker</span><span class="p">({</span><span class="w"> </span><span class="nx">latlng</span><span class="w"> </span><span class="p">});</span>
  227. <span class="p">});</span>
  228. </code></pre></div>
  229. <p>It does the following:</p>
  230. <ul>
  231. <li>Creates a map zoomed on Rennes, France</li>
  232. <li>Maintains a <code>features</code> object, referencing the added markers</li>
  233. <li>Provides a <code>upsertMarker</code> function, creating a marker on the map at the given latitude and longitude, and updating its latitude and longitude if it already exists.</li>
  234. <li>It listens to the <code>click</code> event on the map, calling <code>upsertMarker</code> with the appropriate arguments.</li>
  235. </ul>
  236. <p>Note that the data is not “reactive” (in the sense of React apps): there is no central state that’s updated and triggers the rendering of the user interface.</p>
  237. <h3 id="yjs">Y.js</h3>
  238. <p>Y.js is the first library I’ve looked at, probably because it’s the oldest one, and the more commonly referred to.</p>
  239. <p>The <span class="caps">API</span> seem to offer what we look for, and provides a way to <a href="https://docs.yjs.dev/api/shared-types/y.map#observing-changes-y.mapevent">observe changes</a>. Here’s how it works:</p>
  240. <div class="highlight"><pre><span></span><code><span class="k">import</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">Y</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s2">"yjs"</span><span class="p">;</span>
  241. <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">WebsocketProvider</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s2">"y-websocket"</span><span class="p">;</span>
  242. <span class="c1">// Instanciate a document</span>
  243. <span class="kd">const</span><span class="w"> </span><span class="nx">doc</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">Y</span><span class="p">.</span><span class="nx">Doc</span><span class="p">();</span>
  244. </code></pre></div>
  245. <p>When we add a new marker, we update the <span class="caps">CRDT</span> (<code>markers.set</code>).</p>
  246. <div class="highlight"><pre><span></span><code><span class="kd">let</span><span class="w"> </span><span class="nx">markers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">doc</span><span class="p">.</span><span class="nx">getMap</span><span class="p">(</span><span class="s2">"markers"</span><span class="p">);</span>
  247. <span class="nx">markers</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">target</span><span class="p">.</span><span class="nx">options</span><span class="p">.</span><span class="nx">uuid</span><span class="p">,</span><span class="w"> </span><span class="nx">target</span><span class="p">.</span><span class="nx">_latlng</span><span class="p">);</span>
  248. </code></pre></div>
  249. <p>Another connected peer can observe the changes, like this:</p>
  250. <div class="highlight"><pre><span></span><code><span class="nx">markers</span><span class="p">.</span><span class="nx">observe</span><span class="p">((</span><span class="nx">event</span><span class="p">,</span><span class="w"> </span><span class="nx">transaction</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
  251. <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">transaction</span><span class="p">.</span><span class="nx">local</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
  252. <span class="w"> </span><span class="nx">event</span><span class="p">.</span><span class="nx">changes</span><span class="p">.</span><span class="nx">keys</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">change</span><span class="p">,</span><span class="w"> </span><span class="nx">key</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
  253. <span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">markers</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">key</span><span class="p">);</span>
  254. <span class="w"> </span><span class="k">switch</span><span class="p">(</span><span class="nx">change</span><span class="p">.</span><span class="nx">action</span><span class="p">){</span>
  255. <span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'add'</span><span class="o">:</span>
  256. <span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'update'</span><span class="o">:</span>
  257. <span class="w"> </span><span class="nx">upsertMarker</span><span class="p">({</span><span class="w"> </span><span class="nx">latlng</span><span class="o">:</span><span class="w"> </span><span class="nx">value</span><span class="p">,</span><span class="w"> </span><span class="nx">uuid</span><span class="o">:</span><span class="w"> </span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">local</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">});</span>
  258. <span class="w"> </span><span class="k">break</span><span class="p">;</span>
  259. <span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'delete'</span><span class="o">:</span>
  260. <span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`Property "</span><span class="si">${</span><span class="nx">key</span><span class="si">}</span><span class="sb">" was deleted. ".`</span><span class="p">);</span>
  261. <span class="w"> </span><span class="p">}</span>
  262. <span class="w"> </span><span class="p">});</span>
  263. <span class="w"> </span><span class="p">}</span>
  264. <span class="p">});</span>
  265. </code></pre></div>
  266. <p>We cycle on the received changes, and then apply them on our map. In the case of an offline peer coming back online after some time, the <code>observe</code> event will be called only once. </p>
  267. <p>I’m not dealing with the case of marker deletions here, but deleted items are also taken into account. The data isn’t really deleted in this case, but a “tombstone” is used, making it possible to resolve conflicts (for instance, if one people deleted a marker and some other updated it during the same time).</p>
  268. <p>Y.js comes with multiple “connection providers”, which make it possible to sync with different protocols (there is even <a href="https://github.com/yousefED/matrix-crdt">a way to sync over the matrix protocol</a> 😇).</p>
  269. <p>More usefully for us, there is <a href="https://github.com/yjs/y-websocket">an implemented protocol for WebSockets</a>. Here is how to use one of these providers:</p>
  270. <div class="highlight"><pre><span></span><code><span class="c1">// Sync clients with the y-websocket provider</span>
  271. <span class="kd">const</span><span class="w"> </span><span class="nx">provider</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">WebsocketProvider</span><span class="p">(</span>
  272. <span class="w"> </span><span class="s2">"ws://localhost:1234"</span><span class="p">,</span>
  273. <span class="w"> </span><span class="s2">"leaflet-sync"</span><span class="p">,</span>
  274. <span class="w"> </span><span class="nx">doc</span>
  275. <span class="p">);</span>
  276. </code></pre></div>
  277. <p>This code setups a WebSocket connection with a server that will maintain a local copy of the <span class="caps">CRDT</span>, as explained above.</p>
  278. <p>It’s also possible to send “awareness” information (some state you don’t want to persist, like the position of the cursor). It contains some useful meta information, such as the number of connected peers, if they’re connected, etc.</p>
  279. <div class="highlight"><pre><span></span><code><span class="nx">map</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">"mousemove"</span><span class="p">,</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">latlng</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
  280. <span class="w"> </span><span class="nx">awareness</span><span class="p">.</span><span class="nx">setLocalStateField</span><span class="p">(</span><span class="s2">"user"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
  281. <span class="w"> </span><span class="nx">cursor</span><span class="o">:</span><span class="w"> </span><span class="nx">latlng</span><span class="p">,</span>
  282. <span class="w"> </span><span class="p">});</span>
  283. <span class="p">});</span><span class="w"> </span>
  284. </code></pre></div>
  285. <p>I made <a href="https://gitlab.com/umap-project/leaflet-sync/-/tree/yjs">a quick proof of concept with Y.js</a> in a few hours flawlessly. It handles offline and reconnects, and exposes awareness information.</p>
  286. <h4 id="python-bindings">Python bindings</h4>
  287. <p>Y.js has been ported to rust with <a href="https://github.com/y-crdt/y-crdt">the Y.rs project</a>, making it possible to have binding in other languages, like ruby and python (see <a href="https://github.com/y-crdt/ypy">Y.py</a>). The python bindings are currently looking for a maintainer.</p>
  288. <h4 id="library-size">Library size</h4>
  289. <ul>
  290. <li>Y.js is 4,16 Ko</li>
  291. <li>Y-Websocket is 21,14 Ko</li>
  292. </ul>
  293. <h4 id="the-data-being-transmitted">The data being transmitted</h4>
  294. <p>In a scenario where all clients connect to a central server, which handles the <span class="caps">CRDT</span> locally and then transmits back to other parties, adding 20 points on one client, then 20 points in another generates ~5 Ko of data (so approximately 16 bytes per edit).</p>
  295. <p>Pros: </p>
  296. <ul>
  297. <li>The <span class="caps">API</span> was feeling natural to me: it handles plain old JavaScript objects, making it easy to integrate.</li>
  298. <li>It seems to be widely used, and the community seems active.</li>
  299. <li>It is <a href="https://docs.yjs.dev/">well documented</a></li>
  300. <li>There is awareness support</li>
  301. </ul>
  302. <p>Cons:</p>
  303. <ul>
  304. <li>It doesn’t seem to work well <a href="https://github.com/yjs/yjs/issues/325">without a <span class="caps">JS</span> bundler</a> which could be a problem for us.</li>
  305. <li>The Websocket connection provider doesn’t do what I was expecting it to, as it requires the server to run a <span class="caps">CRDT</span> locally.</li>
  306. </ul>
  307. <hr>
  308. <h3 id="automerge">Automerge</h3>
  309. <p><a href="https://automerge.org/">Automerge</a> is another <span class="caps">CRDT</span> library started by the folks at <a href="https://www.inkandswitch.com/">Ink <span class="amp">&amp;</span> Switch</a> with Martin Kleppmann. Automerge is actually the low level interface. There is a higher-level interface named <a href="https://automerge.org/docs/repositories/">Automerge-repo</a>. Here is how to use it:</p>
  310. <div class="highlight"><pre><span></span><code><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">Repo</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s2">"@automerge/automerge-repo"</span><span class="p">;</span>
  311. <span class="kd">const</span><span class="w"> </span><span class="nx">repo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">Repo</span><span class="p">();</span>
  312. <span class="kd">let</span><span class="w"> </span><span class="nx">handle</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">repo</span><span class="p">.</span><span class="nx">create</span><span class="p">()</span>
  313. <span class="c1">// or repo.find(name)</span>
  314. </code></pre></div>
  315. <p>To change the document, call <code>handle.change</code> and pass it a function that will make changes to the document.</p>
  316. <p>Here, when a new marker is added:</p>
  317. <div class="highlight"><pre><span></span><code><span class="nx">handle</span><span class="p">.</span><span class="nx">change</span><span class="p">((</span><span class="nx">doc</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
  318. <span class="w"> </span><span class="nx">doc</span><span class="p">[</span><span class="nx">uuid</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">cleanLatLng</span><span class="p">(</span><span class="nx">target</span><span class="p">.</span><span class="nx">_latlng</span><span class="p">);</span>
  319. <span class="p">});</span>
  320. </code></pre></div>
  321. <p>I had to use a <code>cleanLatLng</code> function in order to not pass the whole object, otherwise it wouldn’t serialize. It’s really just a simple helper taking the properties of interest for us (and letting away all the rest).</p>
  322. <p>Another peer can observe the changes, like this:</p>
  323. <div class="highlight"><pre><span></span><code><span class="nx">handle</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">"change"</span><span class="p">,</span><span class="w"> </span><span class="p">({</span><span class="w"> </span><span class="nx">doc</span><span class="p">,</span><span class="w"> </span><span class="nx">patches</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
  324. <span class="w"> </span><span class="nx">patches</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(({</span><span class="w"> </span><span class="nx">action</span><span class="p">,</span><span class="w"> </span><span class="nx">path</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
  325. <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">path</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mf">2</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nx">action</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s2">"insert"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
  326. <span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">doc</span><span class="p">[</span><span class="nx">path</span><span class="p">[</span><span class="mf">0</span><span class="p">]];</span>
  327. <span class="w"> </span><span class="nx">upsertMarker</span><span class="p">({</span><span class="w"> </span><span class="nx">latlng</span><span class="o">:</span><span class="w"> </span><span class="nx">value</span><span class="p">,</span><span class="w"> </span><span class="nx">uuid</span><span class="o">:</span><span class="w"> </span><span class="nx">path</span><span class="p">[</span><span class="mf">0</span><span class="p">],</span><span class="w"> </span><span class="nx">local</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">});</span>
  328. <span class="w"> </span><span class="p">}</span>
  329. <span class="w"> </span><span class="p">});</span>
  330. <span class="p">});</span>
  331. </code></pre></div>
  332. <p>The “patch” interface is a bit less verbose than the one from Y.js. It wasn’t well documented, so I had to play around with the messages to understand exactly what the possible actions were. In the end, it gets me what I’m looking for, the changes that occurred to my data.</p>
  333. <p>Here I’m using <code>path.length == 2 &amp;&amp; action === 'insert'</code> to detect that a change occurred to the marker. Since we don’t make a difference between the creation of a marker and its update (when being moved), it works well for us.</p>
  334. <h4 id="python-bindings_1">Python bindings</h4>
  335. <p>There are <a href="https://github.com/automerge/automerge-py">python bindings</a> for automerge “core”. (It doesn’t — yet — provide ways to interact with “repos”).</p>
  336. <h4 id="library-size_1">Library size</h4>
  337. <ul>
  338. <li>Size of the automerge + automerge repo: 1,64 mb</li>
  339. <li>Size of the WebSocket provider: 0,10 mb </li>
  340. </ul>
  341. <p>This is quite a large bundle size, and the team behind automerge is aware of it and working on a solution.</p>
  342. <h4 id="the-data-being-transmitted_1">The data being transmitted</h4>
  343. <p>In the same scenario, adding 20 points on one client, then 20 points in another generates 90 messages and 24,94 Ko of data transmitted (~12 Ko sent and ~12Ko received), so approximately 75 bytes per edit.</p>
  344. <p>Pros:</p>
  345. <ul>
  346. <li>There is an <span class="caps">API</span> to <a href="https://automerge.org/docs/documents/conflicts/">get informed when a conflict occurred</a></li>
  347. <li>Python bindings are currently being worked on, soon to reach a stable version</li>
  348. <li>The team was overall very responsive and trying to help.</li>
  349. </ul>
  350. <p>Cons:</p>
  351. <ul>
  352. <li>The JavaScript is currently generated via Web Assembly, which could make it harder to debug.</li>
  353. <li>The large bundle size of the generated files.</li>
  354. </ul>
  355. <hr>
  356. <h3 id="json-joy"><span class="caps">JSON</span> Joy</h3>
  357. <p><a href="https://jsonjoy.com"><span class="caps">JSON</span> Joy</a> is the latest to the party.</p>
  358. <p>It takes another stake by providing multiple libraries with a small functional perimeter. It sounds promising, even if still quite new, and would leave us with the hands free in order to implement the protocol that would work for us.</p>
  359. <p>Here is how to use it. On the different peers you start with different forks of the same document:</p>
  360. <div class="highlight"><pre><span></span><code><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="nx">Model</span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'json-joy/es2020/json-crdt'</span><span class="p">;</span>
  361. <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">s</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s2">"json-joy/es6/json-crdt-patch"</span><span class="p">;</span>
  362. <span class="c1">// Initiate a model with a custom ID</span>
  363. <span class="kd">const</span><span class="w"> </span><span class="nx">rootModel</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">Model</span><span class="p">.</span><span class="nx">withLogicalClock</span><span class="p">(</span><span class="mf">11111111</span><span class="p">);</span>
  364. <span class="c1">// populate it with default data</span>
  365. <span class="nx">rootModel</span><span class="p">.</span><span class="nx">api</span><span class="p">.</span><span class="nx">root</span><span class="p">({</span>
  366. <span class="w"> </span><span class="nx">markers</span><span class="o">:</span><span class="w"> </span><span class="p">{},</span>
  367. <span class="p">});</span>
  368. <span class="c1">// Fork it on each client</span>
  369. <span class="kd">let</span><span class="w"> </span><span class="nx">userModel</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">rootModel</span><span class="p">.</span><span class="nx">fork</span><span class="p">();</span>
  370. </code></pre></div>
  371. <p>When adding a new marker, we can define a new constant, by using <code>s.con</code>…</p>
  372. <div class="highlight"><pre><span></span><code><span class="nx">userModel</span><span class="p">.</span><span class="nx">api</span><span class="p">.</span><span class="nx">obj</span><span class="p">([</span><span class="s2">"markers"</span><span class="p">]).</span><span class="nx">set</span><span class="p">({</span>
  373. <span class="w"> </span><span class="p">[</span><span class="nx">uuid</span><span class="p">]</span><span class="o">:</span><span class="w"> </span><span class="nx">s</span><span class="p">.</span><span class="nx">con</span><span class="p">(</span><span class="nx">target</span><span class="p">.</span><span class="nx">_latlng</span><span class="p">),</span>
  374. <span class="p">});</span>
  375. </code></pre></div>
  376. <p>… and then create a patch and send it to the other peers:</p>
  377. <div class="highlight"><pre><span></span><code><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">encode</span><span class="p">,</span><span class="w"> </span><span class="nx">decode</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s2">"json-joy/es6/json-crdt-patch/codec/verbose"</span><span class="p">;</span>
  378. <span class="kd">let</span><span class="w"> </span><span class="nx">patch</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">userModel</span><span class="p">.</span><span class="nx">api</span><span class="p">.</span><span class="nx">flush</span><span class="p">();</span>
  379. <span class="kd">let</span><span class="w"> </span><span class="nx">payload</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">encode</span><span class="p">(</span><span class="nx">patch</span><span class="p">);</span>
  380. </code></pre></div>
  381. <p>On the other peers, when we receive a patch message, decode it and apply it:</p>
  382. <div class="highlight"><pre><span></span><code><span class="kd">let</span><span class="w"> </span><span class="nx">patch</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">decode</span><span class="p">(</span><span class="nx">payload</span><span class="p">);</span>
  383. <span class="nx">model</span><span class="p">.</span><span class="nx">api</span><span class="p">.</span><span class="nx">apply</span><span class="p">(</span><span class="nx">patch</span><span class="p">);</span>
  384. </code></pre></div>
  385. <p>We can observe the changes this way:</p>
  386. <div class="highlight"><pre><span></span><code><span class="nx">userModel</span><span class="p">.</span><span class="nx">api</span><span class="p">.</span><span class="nx">onPatch</span><span class="p">.</span><span class="nx">listen</span><span class="p">((</span><span class="nx">patch</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
  387. <span class="w"> </span><span class="nx">patch</span><span class="p">.</span><span class="nx">ops</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">op</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
  388. <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">op</span><span class="p">.</span><span class="nx">name</span><span class="p">()</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s2">"ins_obj"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
  389. <span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">op</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mf">0</span><span class="p">][</span><span class="mf">0</span><span class="p">];</span>
  390. <span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">userModel</span><span class="p">.</span><span class="nx">view</span><span class="p">().</span><span class="nx">markers</span><span class="p">[</span><span class="nx">key</span><span class="p">];</span>
  391. <span class="w"> </span><span class="nx">upsertMarker</span><span class="p">({</span><span class="w"> </span><span class="nx">latlng</span><span class="o">:</span><span class="w"> </span><span class="nx">value</span><span class="p">,</span><span class="w"> </span><span class="nx">uuid</span><span class="o">:</span><span class="w"> </span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">local</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">});</span>
  392. <span class="w"> </span><span class="p">}</span>
  393. <span class="w"> </span><span class="p">});</span>
  394. <span class="p">});</span>
  395. </code></pre></div>
  396. <p>Similarly to what we did with automerge, we’re having a look into the patch, and filter on the operations of interest for us (<code>ins_obj</code>). The names of the operations aren’t clearly specified by the spec.</p>
  397. <p>Metrics:</p>
  398. <ul>
  399. <li>Size: 143 ko</li>
  400. <li>Data transmitted for 2 peers and 40 edits: (35 bytes per edit)</li>
  401. </ul>
  402. <p>Pros:</p>
  403. <ul>
  404. <li>Small atomic libraries, making it easy to use only the parts we need.</li>
  405. <li>The interface <a href="https://jsonjoy.com/libs/json-joy-js/json-crdt/guide/node-types">proposes to store different type of data</a> (constants, values, arrays, etc.)</li>
  406. <li>Distributed as different type of <span class="caps">JS</span> bundles (modules, wasm, etc.)</li>
  407. <li>Low level, so you know what you’re doing</li>
  408. </ul>
  409. <p>Cons:</p>
  410. <ul>
  411. <li>It doesn’t provide a high level interface for sync</li>
  412. <li>It’s currently a one-person project, without clear community channels to gather with other interested folks.</li>
  413. <li>Quite recent, so probably rough spots are to be found</li>
  414. </ul>
  415. <hr>
  416. <h2 id="part-3-comparison-table">Part 3: Comparison table</h2>
  417. <p>I found Y.js and Automerge quite similar for my use case, while <span class="caps">JSON</span> Joy was taking a different (less “all-included”) approach. Here is a summary table to help read the differences I found.</p>
  418. <table>
  419. <thead>
  420. <tr>
  421. <th></th>
  422. <th>Y.js</th>
  423. <th>Automerge</th>
  424. <th><span class="caps">JSON</span> Joy</th>
  425. </tr>
  426. </thead>
  427. <tbody>
  428. <tr>
  429. <td>Python bindings</td>
  430. <td><a href="https://github.com/y-crdt/ypy">Yes</a></td>
  431. <td><a href="https://github.com/automerge/automerge-py">Yes</a></td>
  432. <td>No</td>
  433. </tr>
  434. <tr>
  435. <td>Syncing</td>
  436. <td>Native <span class="caps">JS</span> structures</td>
  437. <td>Transactional functions</td>
  438. <td>Specific types (bonus points for handling constants)</td>
  439. </tr>
  440. <tr>
  441. <td>Coded in</td>
  442. <td>JavaScript / Rust</td>
  443. <td>TypeScript / Rust</td>
  444. <td>Typescript</td>
  445. </tr>
  446. <tr>
  447. <td>Awareness protocol</td>
  448. <td>Yes, with presence</td>
  449. <td>Yes, without presence</td>
  450. <td>No</td>
  451. </tr>
  452. <tr>
  453. <td>Conflict-detection <span class="caps">API</span></td>
  454. <td>No</td>
  455. <td>Yes</td>
  456. <td>No</td>
  457. </tr>
  458. <tr>
  459. <td>Library size</td>
  460. <td><strong>24.3Ko</strong> §<br></td>
  461. <td><strong>1,74 mb</strong> §</td>
  462. <td><strong>143 ko</strong></td>
  463. </tr>
  464. </tbody>
  465. </table>
  466. <p>§ size of the connectors included</p>
  467. <h3 id="working-with-patches">Working with patches</h3>
  468. <p>In order to observe the changes, we need to inspect the given patches and work on what we find. I found the different libraries expose different sets of APIs. All of these APIs were quite a bit hard to find, and it’s not clear if they are public or private.</p>
  469. <p>One thing to keep in mind is that these “patch” events happen only once per patch received. You can see it as a “diff” of the state between the current and the incoming states.</p>
  470. <ul>
  471. <li>Y.js exposes a utility which is able to tell you what the action on the key is (“delete”, “update” and “add”)</li>
  472. <li>Automerge and <span class="caps">JSON</span> Joy, on the other hand, don’t provide such utility functions, meaning you would need to find that out yourself.</li>
  473. </ul>
  474. <h3 id="conclusion">Conclusion</h3>
  475. <p>The goal here is not to tell which one of these libraries is the best one. They’re all great and have their strenghs. None of them implement the high-level <span class="caps">API</span> I was expecting, where the clients talk with each other and the server just relays messages, but maybe it’s because it’s better in general to have the server have the representation of the data, saving a roundtrip for the clients.</p>
  476. <p>I wasn’t expecting to have a look at patches to understand what changed at the low level. The way it’s currently implemented is very suitable for “reactive” applications, but require more involvement to sync between the CRDTs state and the application state.</p>
  477. <p>In the end, adding CRDTs is made very simple, probably due to the fact all we really need is a sort of distributed key/value store.</p>
  478. <p>I’m not sure yet which library we will end up using for uMap (if any), but my understanding is clearer than it was when I started. I guess that’s what progress looks like 😎</p>
  479. <h3 id="yata-and-rga"><span class="caps">YATA</span> and <span class="caps">RGA</span></h3>
  480. <p>While researching, I found that the two popular CRDTs implementation out there use different approaches for the virtual counter:</p>
  481. <blockquote>
  482. <ul>
  483. <li><strong><span class="caps">RGA</span></strong> [used by Automerge] maintains a single globally incremented counter (which can be ordinary integer value), that’s updated anytime we detect that remote insert has an id
  484. with sequence number higher that local counter. Therefore, every time we produce
  485. a new insert operation, we give it a highest counter value known at the time.</li>
  486. <li><strong><span class="caps">YATA</span></strong> [used by Yjs] also uses a single integer value, however unlike in case of <span class="caps">RGA</span> we
  487. don’t use a single counter shared with other replicas, but rather let each
  488. peer keep its own, which is incremented monotonically only by that peer. Since
  489. increments are monotonic, we can also use them to detect missing operations eg.
  490. updates marked as A:1 and A:3 imply, that there must be another (potentially
  491. missing) update A:2.</li>
  492. </ul>
  493. </blockquote>
  494. <h3 id="resources">Resources</h3>
  495. <ul>
  496. <li><a href="https://www.youtube.com/watch?v=x7drE24geUw">CRDTs: The Hard Parts</a>, a video by Martin Kleppmann where he explains the current state of the art of CRDTs, and why some problems aren’t solved yet.</li>
  497. <li><a href="https://jakelazaroff.com/words/an-interactive-intro-to-crdts/">An Interactive Intro to CRDTs</a> gets you trough different steps to understand what are CRDTs, and how to implement a <span class="caps">LWW</span> Register.</li>
  498. <li><a href="https://www.bartoszsypytkowski.com/the-state-of-a-state-based-crdts/">Bartosz Sypytkowski</a> introduction on CRDTs, with practical examples is very intuitive.</li>
  499. <li><a href="https://jzhao.xyz/thoughts/CRDT-Implementations"><span class="caps">CRDT</span> Implementations</a> are notes by Jacky which were useful to me when understanding CRDTs.</li>
  500. </ul>
  501. </article>
  502. <hr>
  503. <footer>
  504. <p>
  505. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  506. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  507. </svg> Accueil</a> •
  508. <a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
  509. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
  510. </svg> Suivre</a> •
  511. <a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
  512. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
  513. </svg> Pro</a> •
  514. <a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
  515. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
  516. </svg> Email</a> •
  517. <abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
  518. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
  519. </svg> Légal</abbr>
  520. </p>
  521. <template id="theme-selector">
  522. <form>
  523. <fieldset>
  524. <legend><svg class="icon icon-brightness-contrast">
  525. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
  526. </svg> Thème</legend>
  527. <label>
  528. <input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
  529. </label>
  530. <label>
  531. <input type="radio" value="dark" name="chosen-color-scheme"> Foncé
  532. </label>
  533. <label>
  534. <input type="radio" value="light" name="chosen-color-scheme"> Clair
  535. </label>
  536. </fieldset>
  537. </form>
  538. </template>
  539. </footer>
  540. <script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
  541. <script>
  542. function loadThemeForm(templateName) {
  543. const themeSelectorTemplate = document.querySelector(templateName)
  544. const form = themeSelectorTemplate.content.firstElementChild
  545. themeSelectorTemplate.replaceWith(form)
  546. form.addEventListener('change', (e) => {
  547. const chosenColorScheme = e.target.value
  548. localStorage.setItem('theme', chosenColorScheme)
  549. toggleTheme(chosenColorScheme)
  550. })
  551. const selectedTheme = localStorage.getItem('theme')
  552. if (selectedTheme && selectedTheme !== 'undefined') {
  553. form.querySelector(`[value="${selectedTheme}"]`).checked = true
  554. }
  555. }
  556. const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
  557. window.addEventListener('load', () => {
  558. let hasDarkRules = false
  559. for (const styleSheet of Array.from(document.styleSheets)) {
  560. let mediaRules = []
  561. for (const cssRule of styleSheet.cssRules) {
  562. if (cssRule.type !== CSSRule.MEDIA_RULE) {
  563. continue
  564. }
  565. // WARNING: Safari does not have/supports `conditionText`.
  566. if (cssRule.conditionText) {
  567. if (cssRule.conditionText !== prefersColorSchemeDark) {
  568. continue
  569. }
  570. } else {
  571. if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
  572. continue
  573. }
  574. }
  575. mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
  576. }
  577. // WARNING: do not try to insert a Rule to a styleSheet you are
  578. // currently iterating on, otherwise the browser will be stuck
  579. // in a infinite loop…
  580. for (const mediaRule of mediaRules) {
  581. styleSheet.insertRule(mediaRule.cssText)
  582. hasDarkRules = true
  583. }
  584. }
  585. if (hasDarkRules) {
  586. loadThemeForm('#theme-selector')
  587. }
  588. })
  589. </script>
  590. </body>
  591. </html>