position-strings
Version:
Lexicographically-ordered position strings for collaborative lists and text
128 lines (127 loc) • 4.98 kB
TypeScript
/**
* A source of lexicographically-ordered "position strings" for
* collaborative lists and text.
*
* In a collaborative list (or text string), you need a way to refer
* to "positions" within that list that:
* 1. Point to a specific list element (or text character).
* 2. Are global (all users agree on them) and immutable (they do not
* change over time).
* 3. Can be sorted.
* 4. Are unique, even if different users concurrently create positions
* at the same place.
*
* `PositionSource` gives you such positions, in the form
* of lexicographically-ordered strings. Specifically, `createBetween`
* returns a new "position string" in between two existing position strings.
*
* These strings have the bonus properties:
* - 5. (Non-Interleaving) If two `PositionSource`s concurrently create a (forward or backward)
* sequence of positions at the same place,
* their sequences will not be interleaved.
* For example, if
* Alice types "Hello" while Bob types "World" at the same place,
* and they each use a `PositionSource` to create a position for each
* character, then
* the resulting order will be "HelloWorld" or "WorldHello", not
* "HWeolrllod".
* - 6. If a `PositionSource` creates positions in a forward (increasing)
* sequence, their lengths as strings will only grow logarithmically,
* not linearly.
*
* Position strings are printable ASCII. Specifically, they
* contain alphanumeric characters, `','`, and `'.'`.
* Also, the special string `PositionSource.LAST` is `'~'`.
*
* Further reading:
* - [Fractional indexing](https://www.figma.com/blog/realtime-editing-of-ordered-sequences/#fractional-indexing),
* a related scheme that satisfies 1-3 but not 4-6.
* - [List CRDTs](https://mattweidner.com/2022/10/21/basic-list-crdt.html)
* and how they map to position strings. `PositionSource` uses an optimized
* variant of that link's string implementation.
* - [Paper about interleaving](https://www.repository.cam.ac.uk/handle/1810/290391)
* in collaborative text editors.
*/
export declare class PositionSource {
/**
* A string that is less than all positions.
*
* Value: `""`.
*/
static readonly FIRST: string;
/**
* A string that is greater than all positions.
*
* Value: `"~"`.
*/
static readonly LAST: string;
/**
* The unique ID for this `PositionSource`.
*/
readonly ID: string;
/**
* Our waypoints' long name: `,${ID}.`.
*/
private readonly longName;
/**
* Variant of longName used for a position's first ID: `${ID}.`.
* (Otherwise every position would start with a redundant ','.)
*/
private readonly firstName;
/**
* For each waypoint that we created, maps a prefix (see getPrefix)
* for that waypoint to its last (most recent) valueSeq.
* We always store the right-side version (odd valueSeq).
*/
private lastValueSeqs;
/**
* Constructs a new `PositionSource`.
*
* It is okay to share a single `PositionSource` between
* all documents (lists/text strings) in the same JavaScript runtime.
*
* For efficiency (shorter position strings),
* within each JavaScript runtime, you should not use
* more than one `PositionSource` for the same document.
* An exception is if multiple logical users share the same runtime;
* we then recommend one `PositionSource` per user.
*
* @param options.ID A unique ID for this `PositionSource`. Defaults to
* `IDs.random()`.
*
* If provided, `options.ID` must satisfy:
* - It is unique across the entire collaborative application, i.e.,
* all `PositionSource`s whose positions may be compared to ours. This
* includes past `PositionSource`s, even if they correspond to the same
* user/device.
* - It does not contain `','` or `'.'`.
* - The first character is lexicographically less than `'~'` (code point 126).
*
* If `options.ID` contains non-alphanumeric characters, then created
* positions will contain those characters in addition to
* alphanumeric characters, `','`, and `'.'`.
*/
constructor(options?: {
ID?: string;
});
/**
* Returns a new position between `left` and `right`
* (`left < new < right`).
*
* The new position is unique across the entire collaborative application,
* even in the face of concurrent calls to this method on other
* `PositionSource`s.
*
* @param left Defaults to `PositionSource.FIRST` (insert at the beginning).
*
* @param right Defaults to `PositionSource.LAST` (insert at the end).
*/
createBetween(left?: string, right?: string): string;
/**
* Appends a wayoint to the given ancestor (= prefix adjusted for
* side), returning a unique new position using that waypoint.
*
* lastValueSeqs is also updated as needed for the waypoint.
*/
private appendWaypoint;
}