Created
June 13, 2022 23:07
-
-
Save jcmoore/b2ec580255f6ad3fb6a9e2bea836b765 to your computer and use it in GitHub Desktop.
SuperbList -- starter kit for client-side database synchronization patterns
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
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> | |
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet"> | |
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script> | |
<script type="module"> | |
import { createSignal, onCleanup, onMount } from "https://cdn.skypack.dev/solid-js"; | |
import { render } from "https://cdn.skypack.dev/solid-js/web"; | |
import html from "https://cdn.skypack.dev/solid-js/html"; | |
import { SuperbMerge, SuperbUnicode, UnicodeLeaf } from "./superblist.js"; | |
Promise.resolve().then(() => render(App, document.body)); | |
const App = () => { | |
const merge = window.merge = new SuperbMerge(); | |
const unicode = window.list = new SuperbUnicode(); | |
unicode.insertText(0, '[ "B","O","X" ]'); | |
merge.insertIds(0, unicode.size(), true); | |
const { 0: text, 1: setText } = createSignal(unicode.getUTF16()); | |
const sliceIds1 = [ | |
merge.getId(2, true), | |
merge.getId(unicode.size() - 3, true), | |
]; | |
const { 0: slice1, 1: setSlice1 } = createSignal(getSlice(sliceIds1)); | |
const sliceIds2 = [ | |
merge.getId(0, true), | |
merge.getId(2, true), | |
]; | |
const { 0: slice2, 1: setSlice2 } = createSignal(getSlice(sliceIds2)); | |
const sliceIds3 = [ | |
merge.getId(unicode.size() - 3, true), | |
merge.getId(unicode.size() - 1, true), | |
]; | |
const { 0: slice3, 1: setSlice3 } = createSignal(getSlice(sliceIds3)); | |
let quill; | |
onMount(() => { | |
quill = new Quill('#editor', { | |
theme: 'snow' | |
}); | |
quill.on('text-change', onQuillDelta); | |
}) | |
return html` | |
<div> | |
<div> | |
<span id="slice1">${slice1}</span> | |
</div> | |
<div> | |
<span id="slice2">${slice2}</span> | |
</div> | |
<div> | |
<span id="slice3">${slice3}</span> | |
</div> | |
<div id="editor" style="height"> | |
${text()} | |
</div> | |
</div> | |
`; | |
function onQuillDelta (delta, oldDelta, source) { | |
let offset = 0; | |
for (let index = 0; index < delta.ops.length; index += 1) { | |
const { retain, delete: backspace, insert } = delta.ops[index]; | |
// TODO: make unicode-aware | |
offset += 0|retain; | |
offset += 0|backspace; | |
if (backspace) merge.backspaceIds(offset, backspace, true); | |
if (backspace) unicode.backspaceText(offset, backspace); | |
if (insert) merge.insertIds(offset, insert.length, true); | |
if (insert) unicode.insertText(offset, insert); | |
offset += insert ? insert.length : 0; | |
} | |
setText(unicode.getUTF16()); | |
setSlice1(getSlice(sliceIds1)); | |
setSlice2(getSlice(sliceIds2)); | |
setSlice3(getSlice(sliceIds3)); | |
} | |
function getSlice({ 0: begin, 1: finish }) { | |
const start = merge.getOffset(begin, true); | |
const end = 1 + merge.getOffset(finish, true); | |
let utf16 = unicode.getUTF16(); | |
utf16 = utf16.slice(UnicodeLeaf.offset(utf16, start)); | |
utf16 = utf16.slice(0, UnicodeLeaf.offset(utf16, end - start)); | |
return utf16; | |
} | |
// designed for central server | |
// lazy/partial/async loading-friendly | |
// snapshot isolation and time-travel queries | |
// flexible data model -- list, map, queue, probably more... | |
// conditional edits with range-centric constraints | |
// range-based change-detection within a single object or across adjacent objects | |
// composable "structure of arrays" | |
// "mostly" e2e-encryptable | |
// not a library, nor a framework -- a pattern and a forkable starter kit | |
// aspirations of understandability | |
}; | |
</script> | |
</head> | |
<body> | |
</body> | |
</html> |
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
export { | |
ShiftingFrame, | |
LogicalTime, | |
MergeId, | |
MergeDeviation, | |
IdTimeline, | |
IdGraph, | |
IdHandle, | |
IdNode, | |
IdBranch, | |
IdLeaf, | |
MergeHandle, | |
MergeNode, | |
RefLeaf, | |
RefBranch, | |
RefHandle, | |
RefNode, | |
MergeJuncture, | |
SuperbMerge, | |
UnicodeLeaf, | |
UnicodeBranch, | |
UnicodeHandle, | |
UnicodeNode, | |
SuperbUnicode, | |
}; | |
class ShiftingFrame { | |
constructor({ | |
restricted = 0, | |
appendable = 0, | |
enqueuing = 0, | |
dequeuing = 0, | |
} = {}) { | |
if (restricted > appendable) { | |
throw new Error("Invalid frame striping."); | |
} else if (enqueuing >= restricted && enqueuing < appendable) { | |
throw new Error("Invalid enqueuing point."); | |
} else if (dequeuing < restricted || dequeuing >= appendable) { | |
if (dequeuing > enqueuing) throw new Error("Invalid dequeuing point."); | |
} | |
this.restricted = restricted; | |
this.appendable = appendable; | |
this.dequeuing = dequeuing; | |
this.enqueuing = enqueuing; | |
} | |
} | |
class LogicalTime { | |
constructor({ | |
coarse = "", | |
fine = 0, | |
} = {}) { | |
this.coarse = coarse; | |
this.fine = fine; // generally run-length encoding friendly | |
} | |
} | |
class MergeId extends LogicalTime {} | |
class MergeDeviation extends LogicalTime {} | |
// to find the haystack/graph of a node/leaf, match the level of its deviation to juncture of the list being searched | |
// MUTABLE | |
class IdTimeline { | |
constructor({ | |
// TODO: _frame = new ShiftingFrame({}), | |
_points = [new LogicalTime({})], | |
_graphs = [new IdGraph({})], | |
} = {}) { | |
// until properly queuing, IdNode/RefNode trees are practically immutable but not practically peresistent | |
this._points = _points; | |
this._graphs = _graphs; | |
} | |
reset( | |
graph = new IdGraph({}), | |
around = this.now(), | |
) { | |
this._graphs = [graph]; | |
this._points = [around]; | |
} | |
now() { | |
return this._points[0]; | |
} | |
getGraph( | |
around = this.now(), | |
shortcut = this.getShortcut(around), | |
coarse = around.coarse, | |
fine = around.fine, | |
) { | |
// TODO: need queue index mapping to properly support shortcuts | |
// if (shortcut >= 0 && shortcut < this._points.length) { | |
// let pt = this._points[shortcut]; | |
// if (pt.coarse < coarse || pt.coarse === coarse && pt.fine <= fine) { | |
// } | |
// if (point.coarse === around.coarse && point.fine === around.fine) { | |
// return this._graphs[shortcut]; | |
// } | |
// } | |
return this._graphs[0]; | |
} | |
getShortcut( | |
around = this.now(), | |
coarse = around.coarse, | |
fine = around.fine, | |
) { | |
// TODO: binary search the queue | |
return -1; | |
} | |
} | |
class IdGraph { | |
constructor({ | |
haystack = null, | |
} = { | |
haystack: new IdHandle(undefined), | |
}) { | |
this.haystack = haystack; | |
} | |
} | |
class IdHandle { | |
constructor({ | |
// TODO: address = "", | |
_node = null, | |
} = { | |
_node: new IdNode({}), | |
}) { | |
this._node = _node; | |
} | |
node() { | |
return this._node; | |
} | |
} | |
class IdNode { | |
constructor({ | |
merge = new MergeHandle(undefined), | |
branches = [], | |
leaves = [], | |
} = { | |
branches: [new IdBranch({})], | |
leaves: [new IdLeaf({})], | |
}) { | |
let backspaces = 0; | |
let width = 0; | |
if (branches.length) { | |
// calculate backspaces/width from sum total of branches | |
for (let index = 0; index < branches.length; index += 1) { | |
backspaces += branches[index].backspaces; | |
width += branches[index].width; | |
} | |
} else if (leaves) { | |
// calculate backspaces/width from sum total of leaves | |
for (let index = 0; index < leaves.length; index += 1) { | |
backspaces += leaves[index].backspaces; | |
width += leaves[index].width; | |
} | |
} | |
this.backspaces = backspaces; | |
this.width = width; | |
this.merge = merge; | |
this.branches = branches; | |
this.leaves = leaves; | |
} | |
} | |
class IdBranch { | |
constructor({ | |
backspaces = -1, | |
width = -1, | |
handle = null, | |
} = { | |
handle: new IdHandle(undefined), | |
}) { | |
const node = handle ? handle.node() : null; | |
this.backspaces = node ? node.backspaces : backspaces; | |
this.width = node ? node.width : width; | |
this.handle = handle; | |
} | |
} | |
class IdLeaf { | |
constructor({ | |
backspaces = 0, | |
width = 1, | |
id = null, | |
merge = null, | |
} = { | |
id: new MergeId({}), | |
merge: new MergeHandle(undefined), | |
}) { | |
this.backspaces = backspaces; | |
this.width = width; | |
this.id = id; | |
this.merge = merge; | |
} | |
} | |
class MergeHandle { | |
constructor({ | |
// TODO: address = "", | |
_node = null, | |
} = { | |
_node: new MergeNode({}), | |
}) { | |
this._node = _node; | |
} | |
node() { | |
return this._node; | |
} | |
} | |
class MergeNode { | |
constructor({ | |
deviation = new MergeDeviation({}), | |
// MUTABLE | |
_timelines = [{ [String("")]: new IdTimeline({}) }], | |
} = {}) { | |
this.deviation = deviation; | |
this._timelines = _timelines; | |
} | |
} | |
// each IdLeaf instance has exactly one RefLeaf instance that notes the deviation of its instantiation | |
// each reference/containment of an IdLeaf has an entry in the MergeNode timelines to associate a point with its graph (haystack) | |
class RefLeaf extends IdLeaf {} | |
class RefBranch { | |
constructor({ | |
start = null, | |
end = null, | |
handle = null, | |
} = { | |
start: new MergeId({}), | |
end: new MergeId({}), | |
handle: new RefHandle(undefined), | |
}) { | |
this.start = start; | |
this.end = end; | |
this.handle = handle; | |
} | |
} | |
class RefHandle { | |
constructor({ | |
// TODO: address = "", | |
_node = null, | |
} = { | |
_node: new RefNode({}), | |
}) { | |
this._node = _node; | |
} | |
node() { | |
return this._node; | |
} | |
} | |
class RefNode { | |
constructor({ | |
// TODO: derivation || juncture = null, | |
branches = [], | |
leaves = [], | |
} = { | |
branches: [new RefBranch({})], | |
leaves: [new RefLeaf({})], | |
}) { | |
this.branches = branches; | |
this.leaves = leaves; | |
} | |
} | |
// MUTABLE | |
class MergeJuncture { | |
constructor({ | |
deviation = new MergeDeviation({}), | |
_point = new LogicalTime({}), | |
} = {}) { | |
this.deviation = deviation; | |
this._point = _point; | |
} | |
} | |
class SuperbMerge { | |
// insertIds( | |
// offset = 0, | |
// width = 1, | |
// exclusive = false, | |
// id = this._usable, | |
// at = this._tick(), | |
// ) {} | |
// backspaceIds( | |
// offset = 1, // always exclusive -- backspaces are not consumed by already "deleted" content | |
// width = 1, | |
// at = this._tick(), | |
// ) {} | |
fork( | |
timeline = "", | |
at = this._now(), | |
) { | |
const result = new SuperbMerge(this); | |
const fine = result._junctures.length; | |
const deviation = new MergeDeviation({ coarse: timeline, fine }); | |
const juncture = new MergeJuncture({ deviation, _point: at }); | |
result._junctures.push(juncture); | |
return result; | |
} | |
constructor({ | |
ordering = null, | |
sorted = null, | |
// MUTABLE | |
_junctures = [new MergeJuncture({})], | |
_clock = new LogicalTime({}), | |
_usable = new MergeId({}), | |
} = { | |
ordering: new IdHandle(undefined), | |
sorted: new RefHandle(undefined), | |
}) { | |
this.ordering = ordering; | |
this.sorted = sorted; | |
const mutable = _junctures.slice(); | |
mutable[mutable.length - 1] = new MergeJuncture({ | |
deviation: mutable[mutable.length - 1].deviation, | |
_point: new LogicalTime(mutable[mutable.length - 1]._point), | |
}); | |
this._junctures = mutable; | |
this._clock = _clock; | |
this._usable = _usable; | |
} | |
_deviation() { | |
return this._junctures[this._junctures.length - 1].deviation; | |
} | |
_now() { | |
return this._junctures[this._junctures.length - 1]._point; | |
} | |
_tock() { | |
this._junctures[this._junctures.length - 1]._point = this._clock; | |
this._clock = new LogicalTime(this._clock); | |
} | |
_tick() { | |
this._clock.fine += 1; | |
return this._clock; | |
} | |
now( | |
result = new LogicalTime({}), | |
) { | |
const point = this._now(); | |
result.coarse = point.coarse; | |
result.fine = point.fine; | |
return result; | |
} | |
deviation( | |
result = new MergeDeviation({}), | |
) { | |
const deviation = this._deviation(); | |
result.coarse = deviation.coarse; | |
result.fine = deviation.fine; | |
return result; | |
} | |
_writeRef( | |
ref = new RefLeaf({}), | |
) { | |
const mutable = [ref]; | |
const vals = this._nodeWrite(this.sorted, mutable); | |
if (vals.length === 2) { | |
const limb = this._nestSorted(vals[0]); | |
const sibling = this._nestSorted(vals[1]); | |
this.sorted = this._partitionSorted([limb, sibling], [], at)[0]; | |
} else if (vals.length === 1) { | |
this.sorted = vals[0]; | |
} else { | |
throw new Error("Unexpected insertion root change."); | |
} | |
return mutable; | |
} | |
_nodeWrite( | |
handle = this.sorted, | |
mutable = [new RefLeaf({})], | |
) { | |
const temp = handle.node(); | |
const branches = temp.branches.slice(); | |
if (!branches.length) { | |
return this._leafWrite(handle, mutable); | |
} | |
const coarse = mutable[0].id.coarse; | |
const fine = mutable[0].id.fine; | |
let index = 0; | |
for (; index < branches.length; index += 1) { | |
const limb = temp.branches[index]; | |
if (coarse > limb.end.coarse) continue; | |
if (coarse === limb.end.coarse && fine >= limb.end.fine) continue; | |
const vals = this._nodeWrite(limb.handle, mutable); | |
if (vals.length === 2) { | |
const sibling = this._nestSorted(vals[1]); | |
branches.splice(index, 1, this._nestSorted(vals[0]), sibling); | |
} else if (vals.length === 1) { | |
branches.splice(index, 1, this._nestSorted(vals[0])); | |
} else { | |
throw new Error("Unexpected write branch change."); | |
} | |
break; | |
} | |
if (index >= branches.length) throw new Error("Did not write."); | |
else return this._partitionSorted(branches, []); | |
} | |
_leafWrite( | |
handle = this.sorted, | |
mutable = [new RefLeaf({})], | |
) { | |
const temp = handle.node(); | |
const leaves = temp.leaves.slice(); | |
while (mutable.length > 1) mutable.pop(); | |
const ref = mutable.pop(); | |
const coarse = ref.id.coarse; | |
const fine = ref.id.fine; | |
let index = 0; | |
for (; index < leaves.length; index += 1) { | |
const stem = temp.leaves[index]; | |
if (coarse < stem.id.coarse) break; | |
if (coarse > stem.id.coarse) continue; | |
if (fine >= stem.id.fine + stem.width) continue; | |
if (fine < stem.id.fine) throw new Error("Ref writes must be monotonic."); | |
if (fine !== stem.id.fine) throw new Error("Ref writes must be aligned."); | |
if (ref.width >= stem.width) { | |
leaves[index] = ref; | |
} else { | |
const width = stem.width - ref.width; | |
const backspaces = stem.backspaces > width ? width : stem.backspaces; | |
leaves.splice(index, 0, ref); | |
leaves[index + 1] = new RefLeaf({ | |
backspaces, | |
width, | |
id: new MergeId({ coarse, fine: fine + ref.width }), | |
merge: this._makeMerge(), | |
}); | |
mutable[0] = leaves[index + 1]; | |
} | |
index = -1; | |
break; | |
} | |
if (index >= leaves.length) leaves.push(ref); | |
else if (index > -1) leaves.splice(index, 0, ref); | |
if (mutable.length === 0) mutable[0] = null; | |
return this._partitionSorted([], leaves); | |
} | |
_partitionSorted( | |
branches = [new RefBranch({})], | |
leaves = [new RefLeaf({})], | |
) { | |
const LIMIT = 64; | |
const count = branches.length > 0 ? branches.length : leaves.length; | |
const handle = count <= LIMIT | |
? this._makeSorted(branches, leaves) // no need to split node | |
: this._makeSorted( // split node | |
branches.splice(count >> 1, 1 / 0), | |
leaves.splice(count >> 1, 1 / 0), | |
); | |
const results = count <= LIMIT | |
? [handle] // no need to split node | |
: [this._makeSorted(branches, leaves), handle]; // split node | |
return results; | |
} | |
_makeSorted( | |
branches = [new RefBranch({})], | |
leaves = [new RefLeaf({})], | |
) { | |
const _node = new RefNode({ branches, leaves }); | |
const handle = new RefHandle({ _node }); | |
return handle; | |
} | |
_nestSorted( | |
handle = new RefHandle(undefined), | |
) { | |
const node = handle.node(); | |
const start = node.branches.length > 0 | |
? node.branches[0].start | |
: node.leaves[0].id; | |
const end = node.branches.length > 0 | |
? node.branches[node.branches.length - 1].end | |
: node.leaves[node.leaves.length - 1].id; | |
const branch = new RefBranch({ start, end, handle }); | |
return branch; | |
} | |
getRef( | |
needle = new MergeId({}), | |
coarse = needle.coarse, | |
fine = needle.fine, | |
) { | |
let temp = this.sorted.node(); | |
// "recursively" search b+tree branches for leafy-haystack by needle | |
while (temp.branches.length > 0) { | |
for (let index = 0; index < temp.branches.length; index += 1) { | |
const limb = temp.branches[index]; | |
if (coarse > limb.end.coarse) continue; | |
if (coarse === limb.end.coarse && fine >= limb.end.fine) continue; | |
temp = limb.handle.node(); | |
break; | |
} | |
} | |
// search b+tree leaves for leafy-haystack by needle | |
for (let index = 0; index < temp.leaves.length; index += 1) { | |
const stem = temp.leaves[index]; | |
if (coarse < stem.id.coarse) break; | |
if (coarse > stem.id.coarse) continue; | |
if (fine >= stem.id.fine + stem.width) continue; | |
if (fine < stem.id.fine) break; | |
return stem; | |
} | |
return null; | |
} | |
getSnapshot( | |
point = new LogicalTime({}), | |
handle = new MergeHandle(undefined), | |
timeline = this._deviation().coarse, | |
merge = handle.node(), | |
) { | |
const deviation = merge.deviation; | |
let around = point; | |
let label = timeline; | |
let index = this._junctures.length - 1 - deviation.fine; | |
if (index < 0) throw new Error("Cannot use resource from the future."); | |
for (; index > -1; index -= 1) { | |
if (merge._timelines.length > index && merge._timelines[index][label]) { | |
return merge._timelines[index][label].getGraph(around); | |
} | |
const juncture = this._junctures[index + deviation.fine]; | |
label = juncture.deviation.coarse; | |
around = juncture._point; | |
} | |
throw new Error("Could not find snapshot."); | |
} | |
getOffset( | |
needle = new MergeId({}), | |
exclusive = false, // exclude backspaces in offset distance | |
point = this._now(), | |
coarse = needle.coarse, | |
fine = needle.fine, | |
) { | |
const ref = this.getRef(needle, coarse, fine); | |
if (!ref) return -1; | |
let graph = this.getSnapshot(point, ref.merge); | |
let haystack = graph.haystack.node(); | |
let offset = 0; | |
// search leafy-haystack for needle | |
if (!exclusive || haystack.width > haystack.backspaces) { | |
for (let index = 0; index < haystack.leaves.length; index += 1) { | |
const stem = haystack.leaves[index]; | |
const quantity = stem.width - stem.backspaces; | |
if ( | |
coarse !== stem.id.coarse || fine < stem.id.fine || | |
fine >= stem.id.fine + stem.width | |
) { | |
offset += exclusive ? quantity : stem.width; | |
continue; | |
} | |
if (exclusive && quantity < fine - stem.id.fine) offset += quantity; | |
else offset += fine - stem.id.fine; | |
break; | |
} | |
} | |
// "recursively" search for target haystack in branchy-haystacks | |
graph = this.getSnapshot(point, haystack.merge); | |
while (graph.haystack) { | |
const target = haystack; | |
haystack = graph.haystack.node(); | |
for (let index = 0; index < haystack.branches.length; index += 1) { | |
const limb = haystack.branches[index]; | |
const quantity = limb.width - limb.backspaces; | |
if (limb.node() === target) break; | |
offset += exclusive ? quantity : limb.width; | |
} | |
if (exclusive && haystack.width <= haystack.backspaces) offset = 0; | |
if (index >= haystack.branches.length) throw new Error("Offset unknown."); | |
graph = this.getSnapshot(point, haystack.merge); | |
} | |
if (haystack !== this.ordering.node()) throw new Error("Root not found."); | |
return offset; | |
} | |
getBackspaces( | |
offset = 0, | |
) { | |
let count = offset; | |
let node = this.ordering.node(); | |
let backspaces = 0; | |
// "recursively" skip over branches seeking to offset | |
while (node.branches.length > 0) { | |
for (let index = 0; index < node.branches.length; index += 1) { | |
const limb = node.branches[index]; | |
if (count >= limb.width) { | |
count -= limb.width; | |
backspaces += limb.backspaces | |
continue; | |
} | |
node = limb.handle.node(); | |
break; | |
} | |
} | |
// skip over leaves seeking to remaining offset | |
for (let index = 0; index < node.leaves.length; index += 1) { | |
const stem = node.leaves[index]; | |
if (count >= stem.width) { | |
count -= stem.width; | |
backspaces += stem.backspaces; | |
continue; | |
} | |
backspaces += count - (stem.width - stem.backspaces); | |
break; | |
} | |
return backspaces; | |
} | |
getId( | |
offset = 0, | |
exclusive = false, | |
result = new MergeId({}), | |
) { | |
let count = offset; | |
let node = this.ordering.node(); | |
if (offset < 0) return null; | |
else if (!exclusive && offset >= node.width) return null; | |
else if (exclusive && offset >= node.width - node.backspaces) return null; | |
// "recursively" skip over branches seeking to offset | |
while (node.branches.length > 0) { | |
for (let index = 0; index < node.branches.length; index += 1) { | |
const limb = node.branches[index]; | |
const quantity = limb.width - limb.backspaces; | |
const delta = exclusive ? quantity : limb.width; | |
if (count >= delta) { | |
count -= delta; | |
continue; | |
} | |
node = limb.handle.node(); | |
break; | |
} | |
} | |
// skip over leaves seeking to remaining offset | |
for (let index = 0; index < node.leaves.length; index += 1) { | |
const stem = node.leaves[index]; | |
const quantity = stem.width - stem.backspaces; | |
const delta = exclusive ? quantity : stem.width; | |
if (count >= delta) { | |
count -= delta; | |
continue; | |
} | |
result.coarse = stem.id.coarse; | |
result.fine = stem.id.fine + count; | |
return result; | |
} | |
return null; | |
} | |
_makeMerge( | |
deviation = this._deviation(), | |
) { | |
const timeline = new IdTimeline({ | |
_points: [null], | |
_graphs: [null], | |
}); | |
const merge = new MergeHandle({ | |
_node: new MergeNode({ | |
deviation, | |
_timelines: [{ [deviation.coarse]: timeline }], | |
}), | |
}); | |
return merge; | |
} | |
_makeOrdering( | |
branches = [new IdBranch({})], | |
leaves = [new IdLeaf({})], | |
deviation = this._deviation(), | |
) { | |
const merge = this._makeMerge(deviation); | |
const _node = new IdNode({ merge, branches, leaves }); | |
const handle = new IdHandle({ _node }); | |
return handle; | |
} | |
_nestOrdering( | |
handle = new IdHandle(undefined), | |
) { | |
const node = handle.node(); | |
const width = node.width; | |
const branch = new IdBranch({ backspaces: node.backspaces, width, handle }); | |
return branch; | |
} | |
insertIds( | |
offset = 0, | |
width = 1, | |
exclusive = false, | |
id = this._usable, | |
at = this._tick(), | |
) { | |
const handle = this.ordering; | |
const node = handle.node(); | |
if (offset < 0) { | |
throw new Error("Offset must be non-negative."); | |
} else if (!exclusive && offset > node.width) { | |
throw new Error("Offset out of bounds."); | |
} else if (exclusive && offset > node.width - node.backspaces) { | |
throw new Error("Offset out of bounds."); | |
} else if (this.getRef(id)) { | |
throw new Error("Insert must be unique."); | |
} | |
const vals = this._nodeInsert(handle, offset, width, exclusive, id, at); | |
if (vals.length === 2) { | |
const limb = this._nestOrdering(vals[0]); | |
const sibling = this._nestOrdering(vals[1]); | |
this.ordering = this._partitionOrdering([limb, sibling], [], at)[0]; | |
} else if (vals.length === 1) { | |
this.ordering = vals[0]; | |
} else { | |
throw new Error("Unexpected insertion root change."); | |
} | |
const deviation = this._deviation(); | |
const graph = new IdGraph({ haystack: null }); | |
const root = this.ordering.node(); | |
// root.merge = this._makeMerge(); | |
root.merge.node()._timelines[0][deviation.coarse].reset(graph, at); | |
this._usable = new MergeId(id); | |
this._usable.fine += width; | |
this._tock(); | |
} | |
_nodeInsert( | |
handle = this.ordering, | |
offset = 0, | |
width = 1, | |
exclusive = false, | |
id = this._usable, | |
at = this._tick(), | |
zero = false, | |
) { | |
const node = handle.node(); | |
const branches = node.branches.slice(); | |
if (!branches.length) { | |
// insert id into leaf node | |
return exclusive && node.backspaces === node.width | |
? this._leafInsert(handle, offset, width, exclusive, id, at, true) | |
: this._leafInsert(handle, offset, width, exclusive, id, at, zero); | |
} | |
let index = 0; | |
if (zero) { | |
for (index = 0; index < branches.length; index += 1) { | |
branches[index] = new IdBranch(branches[index]); | |
branches[index].backspaces = branches[index].width; | |
} | |
} | |
let count = offset; | |
// recursively insert id into branches covering offset | |
for (index = 0; index < branches.length; index += 1) { | |
const limb = branches[index]; | |
const quantity = limb.width - limb.backspaces; | |
const delta = exclusive ? quantity : limb.width; | |
if (count > delta || count === delta && index < branches.length - 1) { | |
count -= delta; | |
continue; | |
} | |
const vals = exclusive && !quantity | |
? this._nodeInsert(limb.handle, count, width, exclusive, id, at, true) | |
: this._nodeInsert(limb.handle, count, width, exclusive, id, at, zero); | |
if (vals.length === 2) { | |
const sibling = this._nestOrdering(vals[1]); | |
branches.splice(index, 1, this._nestOrdering(vals[0]), sibling); | |
} else if (vals.length === 1) { | |
branches.splice(index, 1, this._nestOrdering(vals[0])); | |
} else { | |
throw new Error("Unexpected insertion branch change."); | |
} | |
break; | |
} | |
if (index >= branches.length) throw new Error("Did not insert."); | |
else return this._partitionOrdering(branches, [], at); | |
} | |
_leafInsert( | |
handle = this.ordering, | |
offset = 0, | |
width = 1, | |
exclusive = false, | |
id = this._usable, | |
at = this._tick(), | |
zero = false, | |
) { | |
let count = offset; | |
const leaves = handle.node().leaves.slice(); | |
if (zero) { | |
for (let index = 0; index < leaves.length; index += 1) { | |
const ref = new RefLeaf(leaves[index]); | |
ref.merge = this._makeMerge(); | |
ref.backspaces = ref.width; | |
leaves[index] = new IdLeaf(ref); | |
this._writeRef(ref); | |
} | |
} | |
if (count === 0) { | |
// insert before all existing leaves | |
const ref = new RefLeaf({ width, id, merge: this._makeMerge() }); | |
leaves.splice(0, 0, new IdLeaf(ref)); | |
this._writeRef(ref); | |
} else { | |
for (let index = 0; index < leaves.length; index += 1) { | |
const stem = leaves[index]; | |
const quantity = stem.width - stem.backspaces; | |
const delta = exclusive ? quantity : stem.width; | |
if (count < delta || exclusive && stem.backspaces && count === delta) { | |
// split existing leaf and insert inbetween | |
leaves.splice( | |
index, | |
0, | |
new IdLeaf({ | |
backspaces: exclusive || count < quantity ? 0 : count - quantity, | |
width: count, | |
id: stem.id, | |
merge: this._makeMerge(), | |
}), | |
stem, | |
); | |
const ref = this._writeRef(new RefLeaf(leaves[index + 0]))[0]; | |
if (ref.id.coarse !== stem.id.coarse) { | |
throw new Error("Mismatched leaf slice."); | |
} else if (stem.width - ref.width !== ref.id.fine - stem.id.fine) { | |
throw new Error("Misaligned leaf slice."); | |
} | |
// ref.merge = this._makeMerge(); | |
leaves[index + 2] = new IdLeaf(ref); | |
const merge = this._makeMerge(); | |
leaves[index + 1] = new IdLeaf({ width, id, merge }); | |
this._writeRef(new RefLeaf(leaves[index + 1])); | |
break; | |
} | |
if (count === delta) { | |
const fusible = id.coarse === stem.id.coarse && | |
id.fine === stem.id.fine + stem.width && | |
!stem.backspaces; | |
if (!fusible) { | |
// insert after existing leaf | |
const ref = new RefLeaf({ width, id, merge: this._makeMerge() }); | |
leaves.splice(index + 1, 0, new IdLeaf(ref)); | |
this._writeRef(ref); | |
} else { | |
// extend existing leaf | |
leaves[index] = new IdLeaf({ | |
backspaces: 0, | |
width: stem.width + width, | |
id: stem.id, | |
merge: this._makeMerge(), | |
}); | |
this._writeRef(new RefLeaf(leaves[index])); | |
} | |
break; | |
} | |
count -= delta; | |
} | |
} | |
return this._partitionOrdering([], leaves, at); | |
} | |
_partitionOrdering( | |
branches = [new IdBranch({})], | |
leaves = [new IdLeaf({})], | |
at = this._tick(), | |
) { | |
const LIMIT = 64; | |
const deviation = this._deviation(); | |
const count = branches.length > 0 ? branches.length : leaves.length; | |
const handle = count <= LIMIT | |
? this._makeOrdering(branches, leaves, deviation) // no need to split node | |
: this._makeOrdering( // split node | |
branches.splice(count >> 1, 1 / 0), | |
leaves.splice(count >> 1, 1 / 0), | |
deviation, | |
); | |
const results = count <= LIMIT | |
? [handle] // no need to split node | |
: [this._makeOrdering(branches, leaves, deviation), handle]; // split node | |
for (let offset = 0; offset < results.length; offset += 1) { | |
const haystack = results[offset]; | |
const graph = new IdGraph({ haystack }); | |
const node = haystack.node(); | |
for (let index = 0; index < count; index += 1) { | |
// node.branches[index].handle.node().merge = this._makeMerge(); | |
// node.leaves[index].merge = this._makeMerge(); | |
const merge = branches.length > 0 | |
? node.branches[index].handle.node().merge.node() | |
: node.leaves[index].merge.node(); | |
const forks = deviation.fine - merge.deviation.fine; | |
if (forks < 0) throw new Error("Should never see future deviation."); | |
while (merge._timelines.length <= forks) merge._timelines.push({}); | |
merge._timelines[forks][deviation.coarse].reset(graph, at); | |
} | |
} | |
return results; | |
} | |
_zeroOrdering( | |
results = [new IdHandle(undefined)], | |
) { | |
for (let offset = 0; offset < results.length; offset += 1) { | |
const node = results[offset].node(); | |
node.backspaces = node.width; | |
} | |
return results; | |
} | |
backspaceIds( | |
offset = 1, // always exclusive -- backspaces are not consumed by already "deleted" content | |
width = 1, | |
at = this._tick(), | |
) { | |
const handle = this.ordering; | |
const node = handle.node(); | |
if (offset < 0) { | |
throw new Error("Offset must be non-negative."); | |
} else if (offset > node.width - node.backspaces) { | |
throw new Error("Offset out of bounds."); | |
} else if (width > offset) { | |
throw new Error("Backspace underflow."); | |
} | |
const vals = this._nodeBackspace(handle, offset, width, at); | |
if (vals.length === 2) { | |
const limb = this._nestOrdering(vals[0]); | |
const sibling = this._nestOrdering(vals[1]); | |
this.ordering = this._partitionOrdering([limb, sibling], [], at)[0]; | |
} else if (vals.length === 1) { | |
this.ordering = vals[0]; | |
} else { | |
throw new Error("Unexpected backspace root change."); | |
} | |
const deviation = this._deviation(); | |
const graph = new IdGraph({ haystack: null }); | |
const root = this.ordering.node(); | |
// root.merge = this._makeMerge(); | |
root.merge.node()._timelines[0][deviation.coarse].reset(graph, at); | |
this._tock(); | |
} | |
_nodeBackspace( | |
handle = this.ordering, | |
offset = 1, | |
width = 1, | |
at = this._tick(), | |
) { | |
const node = handle.node(); | |
const branches = node.branches.slice(); | |
if (!branches.length) { | |
return this._leafBackspace(handle, offset, width, at); | |
} | |
if (offset === width && width === node.width - node.backspaces) { | |
return this._zeroOrdering(this._partitionOrdering(branches, [], at)); | |
} | |
let index = 0; | |
let total = width; | |
let count = offset; | |
// recursively insert id into branches covering offset | |
for (index = 0; index < branches.length; index += 1) { | |
const limb = branches[index]; | |
const quantity = limb.width - limb.backspaces; | |
if (count > quantity) { | |
count -= quantity; | |
continue; | |
} | |
const vals = total > quantity | |
? this._nodeBackspace(limb.handle, count, quantity, at) | |
: this._nodeBackspace(limb.handle, count, total, at); | |
total -= quantity; | |
if (vals.length === 2) { | |
const sibling = this._nestOrdering(vals[1]); | |
branches.splice(index, 1, this._nestOrdering(vals[0]), sibling); | |
} else if (vals.length === 1) { | |
branches.splice(index, 1, this._nestOrdering(vals[0])); | |
} else { | |
throw new Error("Unexpected backspacing branch change."); | |
} | |
break; | |
} | |
for (--index; total > 0 && index > -1; index -= 1) { | |
const limb = branches[index]; | |
const quantity = limb.width - limb.backspaces; | |
const vals = total > quantity | |
? this._nodeBackspace(limb.handle, quantity, quantity, at) | |
: this._nodeBackspace(limb.handle, quantity, total, at); | |
total -= quantity; | |
if (vals.length === 2) { | |
const sibling = this._nestOrdering(vals[1]); | |
branches.splice(index, 1, this._nestOrdering(vals[0]), sibling); | |
} else if (vals.length === 1) { | |
branches.splice(index, 1, this._nestOrdering(vals[0])); | |
} else { | |
throw new Error("Unexpected backspacing branch change."); | |
} | |
} | |
return this._partitionOrdering(branches, [], at); | |
} | |
_leafBackspace( | |
handle = this.ordering, | |
offset = 1, | |
width = 1, | |
at = this._tick(), | |
) { | |
const node = handle.node(); | |
const leaves = node.leaves.slice(); | |
if (offset === width && width === node.width - node.backspaces) { | |
return this._zeroOrdering(this._partitionOrdering(leaves, [], at)); | |
} | |
let index = 0; | |
let total = width; | |
let count = offset; | |
for (index = 0; index < leaves.length; index += 1) { | |
const stem = leaves[index]; | |
const quantity = stem.width - stem.backspaces; | |
if (count > quantity) { | |
count -= quantity; | |
continue; | |
} | |
if (count === quantity) { | |
const ref = new RefLeaf({ | |
backspaces: quantity > total ? stem.backspaces + total : stem.width, | |
width: stem.width, | |
id: stem.id, | |
merge: this._makeMerge(), | |
}); | |
leaves[index] = new IdLeaf(ref); | |
this._writeRef(ref); | |
} else if (total >= count) { | |
let ref = new RefLeaf({ | |
backspaces: count, | |
width: count, | |
id: stem.id, | |
merge: this._makeMerge(), | |
}); | |
leaves.splice(index, 0, new IdLeaf(ref)); | |
ref = this._writeRef(ref)[0]; | |
leaves[index + 1] = new IdLeaf(ref); | |
} else { | |
let ref = new RefLeaf({ | |
backspaces: 0, | |
width: count - total, | |
id: stem.id, | |
merge: this._makeMerge(), | |
}); | |
leaves.splice(index, 0, new IdLeaf(ref), stem); | |
ref = this._writeRef(ref)[0]; | |
ref = new RefLeaf({ | |
backspaces: total, | |
width: total, | |
id: ref.id, | |
merge: this._makeMerge(), | |
}); | |
leaves[index + 1] = new IdLeaf(ref); | |
ref = this._writeRef(ref)[0]; | |
leaves[index + 2] = new IdLeaf(ref); | |
} | |
total -= quantity; | |
break; | |
} | |
for (--index; total > 0 && index > -1; index -= 1) { | |
const stem = leaves[index]; | |
const quantity = stem.width - stem.backspaces; | |
const ref = new RefLeaf({ | |
backspaces: total > quantity ? stem.width : stem.backspaces + total, | |
width: stem.width, | |
id: stem.id, | |
merge: this._makeMerge(), | |
}); | |
total -= quantity; | |
leaves[index] = new IdLeaf(ref); | |
this._writeRef(ref); | |
} | |
return this._partitionOrdering([], leaves, at); | |
} | |
// TODO: compactSpace() {} | |
} | |
class UnicodeLeaf { | |
static offset( | |
utf16 = "", | |
size = 0, | |
) { | |
let count = 0; | |
let index = 0; | |
while (index < utf16.length && count < size) { | |
index += utf16.codePointAt(index) > 0xffff ? 2 : 1; | |
count += 1; | |
} | |
return index; | |
} | |
static amount( | |
utf16 = "", | |
) { | |
let count = 0; | |
let index = 0; | |
while (index < utf16.length) { | |
index += utf16.codePointAt(index) > 0xffff ? 2 : 1; | |
count += 1; | |
} | |
return count; | |
} | |
constructor({ | |
size = 0, | |
utf16 = "", | |
} = {}) { | |
const count = UnicodeLeaf.amount(utf16); | |
if (size && size !== count) throw new Error("Unicode length incorrect."); | |
this.size = count; | |
this.utf16 = utf16; | |
} | |
static sizeReduce( | |
total = 0, | |
each = new UnicodeLeaf({}), | |
) { | |
return total + each.size; | |
} | |
static utf16Map( | |
each = new UnicodeLeaf({}), | |
) { | |
return each.utf16; | |
} | |
} | |
class UnicodeBranch { | |
constructor({ | |
size = 1, | |
_utf16 = "", | |
handle = null, | |
} = { | |
handle: new UnicodeHandle(undefined), | |
}) { | |
this.size = size; | |
this._utf16 = String(_utf16); | |
this.handle = handle; | |
} | |
static sizeReduce( | |
total = 0, | |
each = new UnicodeBranch({}), | |
) { | |
return total + each.size; | |
} | |
static utf16Map( | |
each = new UnicodeBranch({}), | |
) { | |
if (each.size < 1) return ""; | |
if (each._utf16.length > 0) return each._utf16; | |
each._utf16 = each.handle.node().utf16(); | |
return each._utf16; | |
} | |
} | |
class UnicodeHandle { | |
constructor({ | |
_node = null, | |
} = { | |
_node: new UnicodeNode({}), | |
}) { | |
this._node = _node; | |
} | |
node() { | |
return this._node; | |
} | |
} | |
class UnicodeNode { | |
constructor({ | |
branches = [], | |
leaves = [], | |
} = { | |
branches: [new UnicodeBranch({})], | |
leaves: [new UnicodeLeaf({})], | |
}) { | |
const size = branches.length > 0 | |
? 0|branches.reduce(UnicodeBranch.sizeReduce, 0) | |
: 0|leaves.reduce(UnicodeLeaf.sizeReduce, 0); | |
this.size = size; | |
this.utf16 = branches.length > 0 | |
? size && String(branches.map(UnicodeBranch.utf16Map).join("")) || "" | |
: size && String(leaves.map(UnicodeLeaf.utf16Map).join("")) || ""; | |
this.branches = branches; | |
this.leaves = leaves; | |
} | |
} | |
class SuperbUnicode { | |
constructor({ | |
root = null, | |
} = { | |
root: new UnicodeHandle(undefined), | |
}) { | |
this.root = root; | |
} | |
getUTF16( | |
// start = 0, | |
// end = this.size, | |
) { | |
// if (start === 0 && end === size) | |
return String(this.root.node().utf16); | |
// TODO: compose from tree structure | |
} | |
size () { | |
return 0|this.root.node().size; | |
} | |
_partitionText( | |
branches = [new UnicodeBranch({})], | |
leaves = [new UnicodeLeaf({})], | |
) { | |
const LIMIT = 64; | |
const count = branches.length > 0 ? branches.length : leaves.length; | |
const handle = count <= LIMIT | |
? this._makeText(branches, leaves) // no need to split node | |
: this._makeText( // split node | |
branches.splice(count >> 1, 1 / 0), | |
leaves.splice(count >> 1, 1 / 0), | |
); | |
const results = count <= LIMIT | |
? [handle] // no need to split node | |
: [this._makeText(branches, leaves), handle]; // split node | |
return results; | |
} | |
_makeText( | |
branches = [new UnicodeBranch({})], | |
leaves = [new UnicodeLeaf({})], | |
) { | |
const _node = new UnicodeNode({ branches, leaves }); | |
const handle = new UnicodeHandle({ _node }); | |
return handle; | |
} | |
_nestText( | |
handle = new UnicodeHandle(undefined), | |
) { | |
const node = handle.node(); | |
const size = node.branches.length > 0 | |
? node.branches.reduce(UnicodeBranch.sizeReduce, 0) | |
: node.leaves.reduce(UnicodeLeaf.sizeReduce, 0); | |
const branch = new UnicodeBranch({ size, handle }); | |
return branch; | |
} | |
insertText( | |
offset = 0, | |
text = "", | |
) { | |
if (!text) return; | |
const handle = this.root; | |
const node = handle.node(); | |
if (offset < 0) { | |
throw new Error("Offset must be non-negative."); | |
} else if (offset > node.size) { | |
throw new Error("Offset out of bounds."); | |
} | |
const vals = this._nodeInsert(handle, offset, text); | |
if (vals.length === 2) { | |
const limb = this._nestText(vals[0]); | |
const sibling = this._nestText(vals[1]); | |
this.root = this._partitionText([limb, sibling], [])[0]; | |
} else if (vals.length === 1) { | |
this.root = vals[0]; | |
} else { | |
throw new Error("Unexpected insertion root change."); | |
} | |
} | |
_nodeInsert( | |
handle = this.root, | |
offset = 0, | |
text = "", | |
) { | |
const node = handle.node(); | |
const branches = node.branches.slice(); | |
if (!branches.length) { | |
return this._leafInsert(handle, offset, text); | |
} | |
let index = 0; | |
let count = offset; | |
// recursively insert id into branches covering offset | |
for (index = 0; index < branches.length; index += 1) { | |
const limb = branches[index]; | |
const delta = limb.size; | |
if (count > delta || count === delta && index < branches.length - 1) { | |
count -= delta; | |
continue; | |
} | |
const vals = this._nodeInsert(limb.handle, count, text); | |
if (vals.length === 2) { | |
const sibling = this._nestText(vals[1]); | |
branches.splice(index, 1, this._nestText(vals[0]), sibling); | |
} else if (vals.length === 1) { | |
branches.splice(index, 1, this._nestText(vals[0])); | |
} else { | |
throw new Error("Unexpected insertion branch change."); | |
} | |
break; | |
} | |
if (index >= branches.length) throw new Error("Did not insert."); | |
else return this._partitionText(branches, [], at); | |
} | |
_leafInsert( | |
handle = this.root, | |
offset = 0, | |
text = "", | |
) { | |
const LIMIT = 64; | |
let count = offset; | |
const leaves = handle.node().leaves.slice(); | |
if (count === 0) { | |
// insert before all existing leaves | |
leaves.splice(0, 0, new UnicodeLeaf({ utf16: text })); | |
} else { | |
for (let index = 0; index < leaves.length; index += 1) { | |
const stem = leaves[index]; | |
if (count > stem.size) { | |
count -= stem.size; | |
continue; | |
} | |
if (stem.size < LIMIT) { | |
const utf16 = stem.utf16; | |
const at = UnicodeLeaf.offset(utf16, count); | |
leaves[index] = new UnicodeLeaf({ | |
utf16: utf16.slice(0, at) + text + utf16.slice(at), | |
}); | |
} else if (count !== stem.size) { | |
const utf16 = stem.utf16; | |
const at = UnicodeLeaf.offset(utf16, count); | |
leaves.splice( | |
index, | |
0, | |
new UnicodeLeaf({ utf16: utf16.slice(0, at) }), | |
new UnicodeLeaf({ utf16: text }), | |
); | |
leaves[index + 2] = new UnicodeLeaf({ utf16: utf16.slice(at) }); | |
} else { | |
leaves.splice(index + 1, 0, new UnicodeLeaf({ utf16: text })); | |
} | |
break; | |
} | |
} | |
return this._partitionText([], leaves); | |
} | |
backspaceText( | |
offset = 1, | |
size = 1, | |
) { | |
if (size < 1) return; | |
const handle = this.root; | |
const node = handle.node(); | |
if (offset < 0) { | |
throw new Error("Offset must be non-negative."); | |
} else if (offset > node.size) { | |
throw new Error("Offset out of bounds."); | |
} else if (size > offset) { | |
throw new Error("Backspace underflow."); | |
} | |
const vals = this._nodeBackspace(handle, offset, size); | |
if (vals.length === 2) { | |
const limb = this._nestText(vals[0]); | |
const sibling = this._nestText(vals[1]); | |
this.root = this._partitionText([limb, sibling], [])[0]; | |
} else if (vals.length === 1) { | |
this.root = vals[0]; | |
} else { | |
throw new Error("Unexpected backspace root change."); | |
} | |
} | |
_nodeBackspace( | |
handle = this.root, | |
offset = 1, | |
size = 1, | |
) { | |
const node = handle.node(); | |
const branches = node.branches.slice(); | |
if (!branches.length) { | |
return this._leafBackspace(handle, offset, size); | |
} | |
if (offset === size && size === node.size) { | |
return this._partitionText([], []); | |
} | |
let index = 0; | |
let total = size; | |
let count = offset; | |
// recursively insert id into branches covering offset | |
for (index = 0; index < branches.length; index += 1) { | |
const limb = branches[index]; | |
if (count > limb.size) { | |
count -= limb.size; | |
continue; | |
} | |
const vals = total > limb.size | |
? this._nodeBackspace(limb.handle, count, limb.size) | |
: this._nodeBackspace(limb.handle, count, total); | |
total -= limb.size; | |
if (vals.length === 2) { | |
const sibling = this._nestText(vals[1]); | |
branches.splice(index, 1, this._nestText(vals[0]), sibling); | |
} else if (vals.length === 1) { | |
branches.splice(index, 1, this._nestText(vals[0])); | |
} else { | |
throw new Error("Unexpected backspacing branch change."); | |
} | |
break; | |
} | |
for (--index; total > 0 && index > -1; index -= 1) { | |
const limb = branches[index]; | |
const vals = total > limb.size | |
? this._nodeBackspace(limb.handle, limb.size, limb.size) | |
: this._nodeBackspace(limb.handle, limb.size, total); | |
total -= limb.size; | |
if (vals.length === 2) { | |
const sibling = this._nestText(vals[1]); | |
branches.splice(index, 1, this._nestText(vals[0]), sibling); | |
} else if (vals.length === 1) { | |
branches.splice(index, 1, this._nestText(vals[0])); | |
} else { | |
throw new Error("Unexpected backspacing branch change."); | |
} | |
} | |
return this._partitionText(branches, []); | |
} | |
_leafBackspace( | |
handle = this.root, | |
offset = 1, | |
size = 1, | |
) { | |
const node = handle.node(); | |
const leaves = node.leaves.slice(); | |
if (offset === size && size === node.size) { | |
return this._partitionText([], []); | |
} | |
let index = 0; | |
let total = size; | |
let count = offset; | |
for (index = 0; index < leaves.length; index += 1) { | |
const stem = leaves[index]; | |
if (count > stem.size) { | |
count -= stem.size; | |
continue; | |
} | |
if (count === stem.size) { | |
const utf16 = stem.utf16; | |
const at = stem.size > total | |
? UnicodeLeaf.offset(utf16, stem.size - total) | |
: 0; | |
if (at < 1) leaves.splice(index, 1); // TODO: merge nodes... | |
else leaves[index] = new UnicodeLeaf({ utf16: utf16.slice(0, at) }); | |
} else if (total >= count) { | |
const utf16 = stem.utf16; | |
const at = UnicodeLeaf.offset(utf16, count); | |
if (at > 0) leaves[index] = new UnicodeLeaf({ utf16: utf16.slice(at) }); | |
} else { | |
let utf16 = stem.utf16; | |
let at = UnicodeLeaf.offset(utf16, count - total); | |
leaves.splice( | |
index, | |
0, | |
new UnicodeLeaf({ utf16: utf16.slice(0, at) }), | |
); | |
utf16 = utf16.slice(at); | |
at = UnicodeLeaf.offset(utf16, total); | |
leaves[index + 1] = new UnicodeLeaf({ utf16: utf16.slice(at) }); | |
} | |
total -= stem.size; | |
break; | |
} | |
index -= 1; | |
const end = index; | |
for (; index > -1; index -= 1) { | |
const stem = leaves[index]; | |
if (stem.size > total) break; | |
total -= stem.size; | |
} | |
leaves.splice(index + 1, end); // TODO: merge nodes... | |
if (index > -1) { | |
const stem = leaves[index]; | |
const utf16 = stem.utf16; | |
const at = UnicodeLeaf.offset(utf16, stem.size - total); | |
leaves[index] = new UnicodeLeaf({ utf16: utf16.slice(0, at) }); | |
} | |
return this._partitionText([], leaves); | |
} | |
} | |
// exclusive-range: [exclusive, null, null, exclusive] | |
// inclusive-range: [null, inclusive, inclusive, null] | |
class Mutation { | |
XOR = { | |
preferences: [ | |
{ | |
when: [ | |
{ | |
zone: "#map", | |
assertions: [], | |
}, | |
{ | |
zone: "#queuing", | |
assertions: [{}], | |
}, | |
], | |
make: [ | |
{ | |
zone: "#queuing", | |
edits: [{ | |
from: "", | |
splicings: [[0, 2, 0, 0]], // [[retains, deletes, inserts, updates]] -negatives go the "left" | |
contents: [3, -1], // put, get, or swap index from contents array (-negative means "do not put") | |
}], | |
}, | |
{ | |
zone: "#map", | |
edits: [{ | |
from: "id", | |
splicings: [[0, 0, 3, 0]], | |
contents: [2, 1, 0], | |
}], | |
}, | |
], | |
then: [{ | |
zone: "#queuing", | |
assertions: [{}], | |
}], | |
}, | |
{ | |
when: [{ | |
zone: "#map", | |
assertions: [], | |
}], | |
make: [ | |
{ | |
zone: "#map", | |
edits: [{ | |
from: "id", | |
splicings: [[0, 0, 3, 0]], | |
contents: [2, 1, 0], | |
}], | |
}, | |
], | |
then: [{ | |
zone: "#queuing", | |
assertions: [{}], | |
}], | |
}, | |
{ | |
when: [], | |
make: [{ | |
zone: "#queuing", | |
edits: [{ | |
from: "", | |
splicings: [[0, 0, 1, 0]], | |
contents: [4], | |
}], | |
}], | |
}, | |
], | |
zones: [ | |
"#queuing", | |
"#map", | |
], | |
contents: [ | |
{ key: "", value: {} && "scalar", width: 1 }, | |
{ key: "", value: new Uint8Array(), width: 2 } && "base64scalar", | |
{ key: "", value: null, width: 1 }, | |
null, | |
{ key: "intent", value: {} }, | |
], | |
}; | |
} | |
class ValueLeaf { | |
constructor({ | |
width = 1, | |
// [created, updated, left_altered, right_altered, left_deleted, right_deleted] | |
moments = [new LogicalTime({})], | |
value = null, | |
} = {}) {} | |
} | |
class ValueBranch { | |
constructor({ | |
width = 0, | |
floors = [new LogicalTime({})], | |
ceilings = [new LogicalTime({})], | |
} = {}) {} | |
} | |
class KeyLeaf { | |
constructor({ | |
width = 1, // run-length encoding-friendly | |
key = [""], | |
} = {}) {} | |
} | |
class KeyBranch { | |
constructor({ | |
width = 0, | |
ascendding = false, | |
start = [""], | |
end = [""], | |
} = {}) {} | |
} | |
class TrifixLeaf { | |
constructor({ | |
prefix = "", | |
infix = [null], | |
suffix = "", | |
} = {}) {} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment