-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
shape ordering: upgrade fractional indexing to use jitter, avoid conf…
…licts (#4312) We saw several duplicates related to ungrouping of shapes and fractional indexes having conflicts — it's hard to repro but the theory is that it has to do (mainly, but not always) with multiplayer conflicts (especially if you're offline for a while, you come back online and then you both have the same indices). However, we have seen this in single-player mode too. ### Here are some of the related bugs: - #3932 - #4126 - #4210 As I looked into the issue, and looked at the original code it led me to: - this issue on the repo: rocicorp/fractional-indexing#14 - which led to this article: https://madebyevan.com/algos/crdt-fractional-indexing/ - and this repo implementing it: https://github.com/TMeerhof/fractional-indexing-jittered So! This PR now switches tldraw to using the "jittered" version of fractional indexing to help with these conflicts. It still doesn't satisfy my curiosity on why we see these issues in single-player mode (when ungrouping sometimes). But, this should help alleviate this problem in general, whether single or multiplayer. As noted in the jitter repo: > The default character set has a chance of roughly one in 47.000 to generate the same key for the same input at the cost of making the keys 3 characters longer on average. Feels definitely like a good tradeoff! I know @SomeHats mentioned about just allowing for these conflicts but then it sounded like from reading other people running into this, we'd have to repair things when someone tried to insert yet another thing between the conflicting items. ### Reference: - Original fractional index article by dgreensp: https://observablehq.com/@dgreensp/implementing-fractional-indexing ### Change type - [x] `bugfix` - [ ] `improvement` - [ ] `feature` - [ ] `api` - [ ] `other` ### Test plan - [x] Updated the unit tests, it uses the non-jitter version for testability ### Release notes - Shape ordering: upgrade fractional indexing to use jitter, avoid conflicts
- Loading branch information
1 parent
9d3e027
commit 8e29188
Showing
10 changed files
with
175 additions
and
436 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import { generateNJitteredKeysBetween, generateNKeysBetween } from 'fractional-indexing-jittered' | ||
|
||
const SMALLEST_INTEGER = 'A00000000000000000000000000' | ||
|
||
const generateKeysFn = | ||
process.env.NODE_ENV === 'test' ? generateNKeysBetween : generateNJitteredKeysBetween | ||
|
||
/** | ||
* A string made up of an integer part followed by a fraction part. The fraction point consists of | ||
* zero or more digits with no trailing zeros. Based on | ||
* {@link https://observablehq.com/@dgreensp/implementing-fractional-indexing}. | ||
* | ||
* @public | ||
*/ | ||
export type IndexKey = string & { __brand: 'indexKey' } | ||
|
||
/** | ||
* The index key for the first index - 'a0'. | ||
* @public | ||
*/ | ||
export const ZERO_INDEX_KEY = 'a0' as IndexKey | ||
|
||
/** | ||
* Get the integer part of an index. | ||
* | ||
* @param index - The index to use. | ||
*/ | ||
function getIntegerPart(index: string): string { | ||
const integerPartLength = getIntegerLength(index.charAt(0)) | ||
if (integerPartLength > index.length) { | ||
throw new Error('invalid index: ' + index) | ||
} | ||
return index.slice(0, integerPartLength) | ||
} | ||
|
||
/** | ||
* Get the length of an integer. | ||
* | ||
* @param head - The integer to use. | ||
*/ | ||
function getIntegerLength(head: string): number { | ||
if (head >= 'a' && head <= 'z') { | ||
return head.charCodeAt(0) - 'a'.charCodeAt(0) + 2 | ||
} else if (head >= 'A' && head <= 'Z') { | ||
return 'Z'.charCodeAt(0) - head.charCodeAt(0) + 2 | ||
} else { | ||
throw new Error('Invalid index key head: ' + head) | ||
} | ||
} | ||
|
||
/** @internal */ | ||
export function validateIndexKey(index: string): asserts index is IndexKey { | ||
if (index === SMALLEST_INTEGER) { | ||
throw new Error('invalid index: ' + index) | ||
} | ||
// getIntegerPart will throw if the first character is bad, | ||
// or the key is too short. we'd call it to check these things | ||
// even if we didn't need the result | ||
const i = getIntegerPart(index) | ||
const f = index.slice(i.length) | ||
if (f.slice(-1) === '0') { | ||
throw new Error('invalid index: ' + index) | ||
} | ||
} | ||
|
||
/** | ||
* Get a number of indices between two indices. | ||
* @param below - The index below. | ||
* @param above - The index above. | ||
* @param n - The number of indices to get. | ||
* @public | ||
*/ | ||
export function getIndicesBetween( | ||
below: IndexKey | null | undefined, | ||
above: IndexKey | null | undefined, | ||
n: number | ||
) { | ||
return generateKeysFn(below ?? null, above ?? null, n) as IndexKey[] | ||
} | ||
|
||
/** | ||
* Get a number of indices above an index. | ||
* @param below - The index below. | ||
* @param n - The number of indices to get. | ||
* @public | ||
*/ | ||
export function getIndicesAbove(below: IndexKey | null | undefined, n: number) { | ||
return generateKeysFn(below ?? null, null, n) as IndexKey[] | ||
} | ||
|
||
/** | ||
* Get a number of indices below an index. | ||
* @param above - The index above. | ||
* @param n - The number of indices to get. | ||
* @public | ||
*/ | ||
export function getIndicesBelow(above: IndexKey | null | undefined, n: number) { | ||
return generateKeysFn(null, above ?? null, n) as IndexKey[] | ||
} | ||
|
||
/** | ||
* Get the index between two indices. | ||
* @param below - The index below. | ||
* @param above - The index above. | ||
* @public | ||
*/ | ||
export function getIndexBetween( | ||
below: IndexKey | null | undefined, | ||
above: IndexKey | null | undefined | ||
) { | ||
return generateKeysFn(below ?? null, above ?? null, 1)[0] as IndexKey | ||
} | ||
|
||
/** | ||
* Get the index above a given index. | ||
* @param below - The index below. | ||
* @public | ||
*/ | ||
export function getIndexAbove(below: IndexKey | null | undefined = null) { | ||
return generateKeysFn(below, null, 1)[0] as IndexKey | ||
} | ||
|
||
/** | ||
* Get the index below a given index. | ||
* @param above - The index above. | ||
* @public | ||
*/ | ||
export function getIndexBelow(above: IndexKey | null | undefined = null) { | ||
return generateKeysFn(null, above, 1)[0] as IndexKey | ||
} | ||
|
||
/** | ||
* Get n number of indices, starting at an index. | ||
* @param n - The number of indices to get. | ||
* @param start - The index to start at. | ||
* @public | ||
*/ | ||
export function getIndices(n: number, start = 'a1' as IndexKey) { | ||
return [start, ...generateKeysFn(start, null, n)] as IndexKey[] | ||
} | ||
|
||
/** | ||
* Sort by index. | ||
* @param a - An object with an index property. | ||
* @param b - An object with an index property. | ||
* @public */ | ||
export function sortByIndex<T extends { index: IndexKey }>(a: T, b: T) { | ||
if (a.index < b.index) { | ||
return -1 | ||
} else if (a.index > b.index) { | ||
return 1 | ||
} | ||
return 0 | ||
} |
This file was deleted.
Oops, something went wrong.
53 changes: 0 additions & 53 deletions
53
packages/utils/src/lib/reordering/dgreensp/dgreensp.test.ts
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.