UNPKG

reactronic

Version:

Reactronic - Transactional Reactive State Management

313 lines (312 loc) 10.9 kB
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; }