Matthew Weidner |
Apr 13th, 2023
Home | RSS Feed
Keywords: position-strings, collaborative text editing, CRDTs, fractional indexing, Fugue
I recently published a new TypeScript library on npm: position-strings.
You can read the docs there for full info, but basically, it provides “position strings” for use in collaborative lists or text strings. Each position string points to a specific list element (or text character), and the list order is given by the lexicographic order on position strings.
For example, a collaborative text editor using the library could represent the text "Hello world"
as:
[
{ position: "r95sU223.B", char: 'H' },
{ position: "r95sU223.D", char: 'e' },
{ position: "r95sU223.F", char: 'l' },
{ position: "r95sU223.H", char: 'l' },
{ position: "r95sU223.J", char: 'o' },
{ position: "r95sU223.N", char: ' ' },
{ position: "r95sU223.N,Y2FsVQbt.B", char: 'w' },
{ position: "r95sU223.N,Y2FsVQbt.D", char: 'o' },
{ position: "r95sU223.N,Y2FsVQbt.F", char: 'r' },
{ position: "r95sU223.N,Y2FsVQbt.H", char: 'l' },
{ position: "r95sU223.N,Y2FsVQbt.J", char: 'd' }
]
Here the array is sorted lexicographically by position
. (The funny-looking substrings "r95sU223"
and "Y2FsVQbt"
are random IDs, used to ensure uniqueness when multiple users type concurrently.)
When the user types ','
between "Hello "
and "world"
, the text editor will ask the library for a new position string in between the positions of ' '
and 'w'
:
// At the start of the app:
import { PositionSource } from "position-strings";
const source = new PositionSource();
// When the user types between ' ' @ "r95sU223.N" and 'w' @ "r95sU223.N,Y2FsVQbt.B":
const newPos = source.createBetween("r95sU223.N", "r95sU223.N,Y2FsVQbt.B");
// newPos is "r95sU223.J,bLBGHJmO.B", which is indeed between:
// "r95sU223.N" < "r95sU223.J,bLBGHJmO.B" < "r95sU223.N,Y2FsVQbt.B".
The text editor then adds { position: newPos, char: ',' }
to its array in sorted order. It also broadcasts this object to collaborators, who add it to their own arrays. Likewise, when the user deletes a character, the text editor broadcasts the deleted position and all collaborators delete its list entry.
Position-strings implements the hard part of a list CRDT, with a minimal, lightweight API. Unlike most CRDT implementations, position-strings doesn’t “own” the list state or store extra metadata1; instead, you are free to store the positions and chars (or list values) wherever you like. For example, you could store each { position, char }
as a row in a cloud-synced database like Firebase Realtime DB - I provide an example app that implements collaborative text editing this way.
You can also think of position-strings like fractional indexing, but with a few extra features: global uniqueness, non-interleaving, and slower (logarithmic) length growth in sequences.
The catch is that a list/text CRDT using position-strings will have more storage and network overhead than a dedicated data structure like Y.Array
. In my benchmarks, the position strings are typically 10-100 characters long, so you’ll have at least 10-100 bytes of storage overhead per list element/text character. This is fine for short documents and simple apps, but could be a problem for fancier uses.
Under the hood, position-strings uses an optimized version of Fugue’s string implementation. You can read about the full algorithm here.
Except for a small amount of ephemeral, local state that you don’t need to store or broadcast - it only exists for the lifetime of a PositionSource
object. ↩
Home • Matthew Weidner • PhD student at CMU CSD • mweidner037 [at] gmail.com • @MatthewWeidner3 • LinkedIn • GitHub