reactronic
Version:
Reactronic - Transactional Reactive State Management
313 lines (312 loc) • 10.9 kB
JavaScript
import { misuse } from "./Dbg.js";
export class LinkedList {
constructor(keyExtractor, isStrictOrder = false) {
this.keyOf = keyExtractor;
this.isStrictOrder$ = isStrictOrder;
this.map = new Map();
this.items$ = new LinkedSubList();
this.renovation = undefined;
}
get isStrictOrder() { return this.isStrictOrder$; }
set isStrictOrder(value) {
if (this.renovation !== undefined)
throw misuse("cannot change strict mode in the middle of renovation");
this.isStrictOrder$ = value;
}
get isRenovationInProgress() {
return this.renovation !== undefined;
}
get count() {
var _a, _b;
return this.items$.count + ((_b = (_a = this.renovation) === null || _a === void 0 ? void 0 : _a.lostItemCount) !== null && _b !== void 0 ? _b : 0);
}
items() {
return this.items$.items();
}
lookup(key) {
return this.map.get(key);
}
add(item, before) {
const key = this.keyOf(item);
if (this.map.get(key) !== undefined)
throw misuse(`item with given key already exists: ${key}`);
this.map.set(key, item);
LinkedItem.link$(this.items$, item, before);
}
move(item, before) {
if (item.list !== this.items$)
throw misuse("cannot move item that belongs to another list");
if (!item.isManagedExternally)
throw misuse("cannot move given item outside of renovation cycle");
LinkedList.move$(this, item, before);
}
remove(item) {
if (item.list !== this.items$)
throw misuse("cannot remove item that belongs to another list");
if (!item.isManagedExternally)
throw misuse("cannot remove given item outside of renovation cycle");
LinkedList.remove$(this, item);
}
beginRenovation(diff) {
if (this.renovation !== undefined)
throw misuse("renovation is in progress already");
const former = this.items$;
const renovation = new LinkedListRenovation(this, former, diff);
this.items$ = new LinkedSubList();
this.renovation = renovation;
return renovation;
}
endRenovation(error) {
const renovation = this.renovation;
if (renovation === undefined)
throw misuse("renovation is ended already");
const items = this.items$;
if (error === undefined) {
for (const x of renovation.lostItems()) {
if (!x.isManagedExternally) {
LinkedList.removeKey$(this, this.keyOf(x));
LinkedItem.setStatus$(x, Mark.removed, 0);
}
else
LinkedItem.link$(items, x, undefined);
}
}
else {
for (const x of renovation.lostItems()) {
LinkedItem.link$(items, x, undefined);
LinkedItem.setStatus$(x, Mark.prolonged, items.count);
}
}
this.renovation = undefined;
}
static move$(list, item, before) {
LinkedItem.link$(list.items$, item, before);
}
static remove$(list, item) {
LinkedList.removeKey$(list, list.keyOf(item));
LinkedItem.link$(undefined, item, undefined);
}
static removeKey$(list, key) {
list.map.delete(key);
}
}
export var Mark;
(function (Mark) {
Mark[Mark["prolonged"] = 0] = "prolonged";
Mark[Mark["added"] = 1] = "added";
Mark[Mark["modified"] = 2] = "modified";
Mark[Mark["removed"] = 3] = "removed";
})(Mark || (Mark = {}));
const MARK_MOD = 4;
export class LinkedItem {
constructor() {
this.list$ = undefined;
this.next$ = undefined;
this.prev$ = undefined;
this.status = 0;
}
get list() { return this.list$; }
get next() { return this.next$; }
get prev() { return this.prev$; }
get mark() { return this.status % MARK_MOD; }
get rank() { return Math.trunc(this.status / MARK_MOD); }
get isManagedExternally() { return this.status === 0; }
static setStatus$(item, mark, rank) {
item.status = rank * MARK_MOD + mark;
}
static link$(list, item, before) {
if (before === undefined) {
LinkedItem.unlink(item);
if (list !== undefined) {
item.list$ = list;
const last = list.last;
item.prev$ = last;
item.next$ = undefined;
if (last !== undefined)
list.last = last.next$ = item;
else
list.first = list.last = item;
list.count++;
}
else {
item.list$ = undefined;
item.next$ = undefined;
item.prev$ = undefined;
}
}
else {
if (list === before.list && list !== undefined) {
LinkedItem.unlink(item);
const after = before.prev$;
item.prev$ = after;
item.next$ = before;
before.prev$ = item;
if (after !== undefined)
after.next$ = item;
if (before == list.first)
list.first = item;
item.list$ = list;
list.count++;
}
else {
if (list !== before.list)
throw misuse("sibling is not in the given list");
else if (before.list === undefined)
throw misuse("cannot link to sibling that is not in a list");
else
throw misuse("linked list invariant is broken");
}
}
}
static unlink(item) {
const list = item.list;
if (list) {
const prev = item.prev$;
if (prev !== undefined)
prev.next$ = item.next$;
const next = item.next$;
if (next !== undefined)
next.prev$ = item.prev$;
if (item === list.first)
list.first = item.next$;
if (item === list.last)
list.last = undefined;
list.count--;
}
}
}
export class LinkedSubList {
constructor() {
this.count = 0;
this.first = undefined;
this.last = undefined;
}
*items() {
let x = this.first;
while (x !== undefined) {
const next = x.next;
yield x;
x = next;
}
}
clear() {
this.count = 0;
this.first = undefined;
this.last = undefined;
}
grab(from, join) {
const head = from.first;
if (join !== undefined && head !== undefined) {
this.count += from.count;
}
else {
this.count = from.count;
this.first = head;
this.last = from.last;
}
from.clear();
}
}
export class LinkedListRenovation {
constructor(list, former, diff) {
this.list = list;
this.diff = diff;
this.former = former;
this.expected = former.first;
this.absent = undefined;
}
lookup(key) {
let result = undefined;
if (key !== undefined && key !== this.absent) {
result = this.list.lookup(key);
if (result !== undefined) {
if (this.list.keyOf(result) !== key) {
this.absent = key;
result = undefined;
}
}
else
this.absent = key;
}
return result;
}
tryToProlong(key, resolution, error) {
var _a, _b;
const list = this.list;
if (!list.isRenovationInProgress)
throw misuse(error !== null && error !== void 0 ? error : "renovation is no longer in progress");
let x = this.expected;
if (key !== (x ? list.keyOf(x) : undefined))
x = this.lookup(key);
if (x !== undefined) {
const result = this.list.items$;
if (x.list !== result) {
const next = x.next;
const expected = (_a = grabExternalIfAny(result, x)) !== null && _a !== void 0 ? _a : x;
LinkedItem.link$(result, x, undefined);
if (list.isStrictOrder && expected !== this.expected) {
LinkedItem.setStatus$(x, Mark.modified, result.count);
(_b = this.diff) === null || _b === void 0 ? void 0 : _b.push(x);
}
else
LinkedItem.setStatus$(x, Mark.prolonged, result.count);
this.expected = next;
if (resolution)
resolution.isDuplicate = false;
}
else if (resolution)
resolution.isDuplicate = true;
else
throw misuse(`duplicate linked item key: ${key}`);
}
else if (resolution)
resolution.isDuplicate = false;
return x;
}
thisIsAdded(item, before) {
var _a;
this.list.add(item, before);
LinkedItem.setStatus$(item, Mark.added, this.list.items$.count);
this.absent = undefined;
this.expected = undefined;
(_a = this.diff) === null || _a === void 0 ? void 0 : _a.push(item);
return item;
}
thisIsModified(item) {
if (item.list !== this.list.items$)
throw misuse("only prolonged items can be marked as modified");
const m = item.mark;
if (m === Mark.prolonged)
LinkedItem.setStatus$(item, Mark.modified, item.rank);
else if (m !== Mark.modified)
throw misuse("item is renovated already and cannot be marked as modified");
}
thisIsMoved(item, before) {
var _a;
if (item.list !== this.former)
throw misuse("cannot move item which doesn't belong to former list");
LinkedList.move$(this.list, item, before);
LinkedItem.setStatus$(item, Mark.modified, 0);
(_a = this.diff) === null || _a === void 0 ? void 0 : _a.push(item);
}
thisIsRemoved(item) {
var _a;
if (item.list !== this.former)
throw misuse("cannot remove item which doesn't belong to former list");
LinkedList.remove$(this.list, item);
LinkedItem.setStatus$(item, Mark.removed, 0);
(_a = this.diff) === null || _a === void 0 ? void 0 : _a.push(item);
}
get lostItemCount() { return this.former.count; }
lostItems() { return this.former.items(); }
}
function grabExternalIfAny(list, item) {
let x = item.prev;
let before = undefined;
while (x !== undefined && x.isManagedExternally) {
LinkedItem.link$(list, x, before);
before = x;
x = x.prev;
}
return before;
}