Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,24 +43,24 @@ import { Fugue } from 'fugue';
export const fugue = new Fugue('client1');
```

Generate positions between two points using `createBetween`. Pass `null` for start/end to generate positions at the beginning/end:
Generate positions between two points using `between`. Pass `null` for start/end to generate positions at the beginning/end:

```ts
import { fugue } from "./fugue";

const first = fugue.createBetween(null, null); // "client1.B"
const first = fugue.first(); // "client1.B" - equivalent to `between(null, null)`

// Insert after first
const second = fugue.createBetween(first, null); // "client1.D"
const second = fugue.after(first); // "client1.D" - equivalent to `between(first, null)`

// Insert after second
const third = fugue.createBetween(second, null); // "client1.F"
const third = fugue.after(second); // "client1.F" - equivalent to `between(second, null)`

// Insert before first
const zeroth = fugue.createBetween(null, first); // "client1.A0B"
const zeroth = fugue.before(first); // "client1.A0B" - equivalent to `between(null, first)`

// Insert between second and third (midpoint)
const secondAndHalf = fugue.createBetween(second, third); // "client1.D0B"
const secondAndHalf = fugue.between(second, third); // "client1.D0B"
```

The biggest benefit of using `fugue` over other fractional indexing libraries is that multiple independent clients
Expand All @@ -75,12 +75,12 @@ const fugue1 = new Fugue('client1');
const fugue2 = new Fugue('client2');

// create some initial starting first and last
const first = fugue1.createBetween(null, null); // "client1.B"
const last = fugue1.createBetween(initialFirst, null); // "client1.D"
const first = fugue1.first(); // "client1.B"
const last = fugue1.after(initialFirst); // "client1.D"

// simulating these happening in parallel (e.g. across multiple independent clients)
const middle1 = fugue1.createBetween(first, last); // "client1.B0B"
const middle2 = fugue2.createBetween(first, last); // "client1.B,client2.B"
const middle1 = fugue1.between(first, last); // "client1.B0B"
const middle2 = fugue2.between(first, last); // "client1.B,client2.B"

// these eventually grow to e.g.: client0.B0A0B0aH0B,client1.D,client2.B,client3.L,client4.B,client5.D,client6.B,client7.B,client10.B,client23.B,client35.B,client36.B
```
Expand Down Expand Up @@ -133,7 +133,7 @@ async function insertItemBetween(listId, beforeItemId, afterItemId, itemData) {
if (afterItem) afterPosition = afterItem.position;

// Generate a position between the two
const position = fugue.createBetween(beforePosition, afterPosition);
const position = fugue.between(beforePosition, afterPosition);

// Insert the new item with the generated position
await tx.insert(items).values({
Expand All @@ -145,7 +145,6 @@ async function insertItemBetween(listId, beforeItemId, afterItemId, itemData) {
}
```


## Features

- Generates unique, ordered keys for inserting items between other items.
Expand Down
4 changes: 2 additions & 2 deletions algorithm.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ The 3 node/layer types are:
2. **valueIndex nodes**: Labeled by an integer, sorted by magnitude. When a `Fugue` creates positions in a left-to-right sequence, instead of appending a new waypoint node each time, it reuses the first waypoint node and just increases the valueIndex. That causes the position string length to grow logarithmically instead of linearly.
3. **Side nodes**: Labeled by a bit "left side" (0) or "right side" (1). The actual position at a node, and all of the node's right-side descendants, use "right side"; all of its left-side descendants use "left side". This ensures that all left descendants are less than the position at a node, which is less than all right descendants.

### `createBetween`
### `between`

In terms of the tree structure, `Fugue.createBetween(left, right)` does the following:
In terms of the tree structure, `Fugue.between(left, right)` does the following:

1. If `right` is a descendant of `left`, create a left descendant of `right` as follows. First, create a waypoint node that is a left child of `right` (replacing `right`'s final "right side" bit with "left side"). Then append the next new valueIndex node (usually 0) and a "right side" node, to fill out the 3 layers. Return that final node.
2. Otherwise, see if we can just increase `left`'s final valueIndex, instead of lengthing its path. This is allowed if (a) `left`'s final waypoint node uses our ID, and (b) `right` doesn't use that same waypoint node. If so, look up the next unused valueIndex for that waypoint (stored in `Fugue`), then use `left` but with that final valueIndex.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fugue",
"version": "1.0.1",
"version": "2.0.0",
"description": "Fractional indexing without conflicts.",
"type": "module",
"scripts": {
Expand Down
83 changes: 76 additions & 7 deletions src/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,27 @@ export class Fugue<TClientID extends string = string> {
}

/**
* Returns a new position between `a` and `b`
* (`a < new < b`).
*
* @param a an existing position, or null to insert at the beginning.
*
* @param b an existing position, or null to insert at the end.
* Creates a new position between two existing positions.
* The new position will be greater than `a` and less than `b`.
*
* @param a - An existing position to insert after, or null to insert at the beginning
* @param b - An existing position to insert before, or null to insert at the end
* @returns A new position that satisfies `a < new < b`
*
* @example
* ```typescript
* const fugue = new Fugue("client1");
* const pos1 = fugue.between(null, null); // First position
* const pos2 = fugue.between(pos1, null); // Insert after pos1
* const pos3 = fugue.between(pos1, pos2); // Insert between pos1 and pos2
* // pos1 < pos3 < pos2
* ```
*
* @throws Will warn and adjust inputs if:
* - `a >= b` (when both are non-null)
* - `b > Fugue.LAST`
*/
createBetween(a: string | null, b: string | null) {
between(a: string | null, b: string | null) {
let left = a;
let right = b;

Expand Down Expand Up @@ -121,6 +134,62 @@ export class Fugue<TClientID extends string = string> {
return ans as FuguePosition<TClientID> & {};
}

/**
* Creates a new position immediately after the given position.
* This is equivalent to calling `between(position, null)`.
*
* @param position - The existing position to insert after
* @returns A new position that is greater than the given position
*
* @example
* ```typescript
* const fugue = new Fugue("client1");
* const pos1 = fugue.between(null, null); // First position
* const pos2 = fugue.after(pos1); // Insert after pos1
* // pos1 < pos2
* ```
*/
after(position: string) {
return this.between(position, null);
}

/**
* Creates a new position immediately before the given position.
* This is equivalent to calling `between(null, position)`.
*
* @param position - The existing position to insert before
* @returns A new position that is less than the given position
*
* @example
* ```typescript
* const fugue = new Fugue("client1");
* const pos1 = fugue.between(null, null); // First position
* const pos2 = fugue.before(pos1); // Insert before pos1
* // pos2 < pos1
* ```
*/
before(position: string) {
return this.between(null, position);
}

/**
* Creates the first position in a sequence.
* This is equivalent to calling `between(null, null)`.
*
* @returns A new position that is greater than all existing positions
*
* @example
* ```typescript
* const fugue = new Fugue("client1");
* const pos1 = fugue.first(); // First position
* const pos2 = fugue.after(pos1); // Insert after pos1
* // pos1 < pos2
* ```
*/
first() {
return this.between(null, null);
}

/**
* Appends a waypoint to the ancestor.
*/
Expand Down
14 changes: 7 additions & 7 deletions tests/strings.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ describe("benchmarks", () => {
bench("single", () => {
const internal = new Fugue("test");

const pos1 = internal.createBetween(null, null);
const pos2 = internal.createBetween(pos1, null);
internal.createBetween(pos1, pos2);
const pos1 = internal.between(null, null);
const pos2 = internal.between(pos1, null);
internal.between(pos1, pos2);
});

bench("multiple instances", () => {
Expand All @@ -22,16 +22,16 @@ describe("benchmarks", () => {
// Create initial position for first instance
const firstInstance = instances[0];
if (firstInstance) {
firstKey = firstInstance.createBetween(null, null);
lastKey = firstInstance.createBetween(firstKey, null);
firstKey = firstInstance.between(null, null);
lastKey = firstInstance.between(firstKey, null);
}

let previousKey: string | null = firstKey;

for (let j = 0; j < 10; j++) {
for (const instance of instances) {
const newPos = instance.createBetween(previousKey, lastKey);
const newPos2 = instance.createBetween(previousKey, newPos);
const newPos = instance.between(previousKey, lastKey);
const newPos2 = instance.between(previousKey, newPos);

previousKey = newPos2;
}
Expand Down
Loading