// A copy of the place rules in internal/store/places.go, so the editor can // show a change the moment it's made. The server's reply is the final word. export class Places { /** @param {{id: number, rank: number}[]} items @param {number} limit */ constructor(items, limit) { this.at = new Map(items.map((it) => [it.rank, it.id])); this.limit = limit; } last() { let n = 0; for (const r of this.at.keys()) n = Math.max(n, r); return n; } rankOf(id) { for (const [r, x] of this.at) if (x === id) return r; return 0; } shift(from, to, d) { if (from > to) return; const order = []; for (let r = from; r <= to; r++) order.push(r); if (d > 0) order.reverse(); for (const r of order) { if (this.at.has(r)) { this.at.set(r + d, this.at.get(r)); this.at.delete(r); } } } nextEmpty(from) { let r = from; while (this.at.has(r)) r++; return r; } /** Frees place at by pushing entries down to the next empty place. */ makeRoom(at) { if (!this.at.has(at)) return true; const gap = this.nextEmpty(at); if (gap > this.limit) return false; this.shift(at, gap - 1, +1); return true; } move(from, to) { const id = this.at.get(from); this.at.delete(from); if (this.at.has(to)) { if (to < from) { this.shift(to, this.nextEmpty(to + 1) - 1, +1); } else { let gap = to - 1; while (this.at.has(gap)) gap--; this.shift(gap + 1, to, -1); } } this.at.set(to, id); } remove(id, closeUp) { const r = this.rankOf(id); this.at.delete(r); if (closeUp) this.shift(r + 1, this.nextEmpty(r + 1) - 1, -1); } removePlaces(at, count) { for (let i = 0; i < count; i++) this.shift(at + 1, this.last(), -1); } compact() { const ids = [...this.at.entries()].sort((a, b) => a[0] - b[0]).map((e) => e[1]); this.at = new Map(ids.map((id, i) => [i + 1, id])); } /** Where a new entry goes when no place is asked for. */ appendPlace() { const last = this.last(); if (last < this.limit) return last + 1; for (let r = 1; r <= this.limit; r++) if (!this.at.has(r)) return r; return this.limit + 1; } }