'use strict'; const taulut = { luokat: 'luokat', opettajat: 'opettajat', tilat: 'tilat', tunnit: 'tunnit', }; class Transaktio { peruttu = false; muutokset = []; constructor(tietokanta) { this.tietokanta = tietokanta; this.seuraavaId = tietokanta.seuraavaId; this.taulut = new Map; for (const taulu of tietokanta.taulut.keys()) { this.taulut.set(taulu, new Map); } } peru() { this.peruttu = true; } hae(taulu, id) { if (!this.taulut.has(taulu)) { throw new Error(`ei taulua ${taulu}`); } if (this.taulut.get(taulu).has(id)) { return this.taulut.get(taulu).get(id); } else { return this.tietokanta.taulut.get(taulu).get(id); } } lisää(taulu, sisältö) { if (this.peruttu) { throw new Error(`yritys lisätä rivi perutussa transaktiossa`); } if (!this.taulut.has(taulu)) { throw new Error(`ei taulua ${taulu}`); } const id = this.seuraavaId++; this.muutokset.push({ taulu, id, uusi: sisältö, }); this.taulut.get(taulu).set(id, sisältö); return id; } poista(taulu, id) { if (this.peruttu) { throw new Error(`yritys poistaa rivi perutussa transaktiossa`); } const vanha = this.hae(taulu, id); if (vanha === undefined) { throw new Error(`ei riviä ${id} taulussa ${taulu}`); } this.muutokset.push({ taulu, id, vanha, }); this.taulut.get(taulu).set(id, undefined); } suodata(taulu, suodatin) { if (!this.taulut.has(taulu)) { throw new Error(`ei taulua ${taulu}`); } const suodatetut = []; for (const [id, sisältö] of this.taulut.get(taulu)) { // Jos sisältö on undefined, rivi on poistettu, eikä sitä tule ottaa // huomioon suodatettaessa if (sisältö !== undefined && suodatin(sisältö)) { suodatetut.push(id); } } for (const [id, sisältö] of this.tietokanta.taulut.get(taulu)) { // Älä huomio rivejä, jotka löytyvät transaktion tauluista. Ne on // joko käsitelty jo edellisessä silmukassa (jos ne on päivitetty) // tai niitä ei tulisi käsitellä ollenkaan (jos ne on poistettu). if (!this.taulut.get(taulu).has(id) && suodatin(sisältö)) { suodatetut.push(id); } } return suodatetut; } } class Tietokanta { seuraavaId = 0; taulut = new Map; historia = []; static serialisoidusta(serialisoitu) { const parsittu = JSON.parse(serialisoitu); const tietokanta = new this; tietokanta.seuraavaId = parsittu.seuraavaId; const muutokset = []; for (const taulu in parsittu.taulut) { for (let id in parsittu.taulut[taulu]) { id = Number.parseInt(id); const sisältö = parsittu.taulut[taulu][id]; tietokanta.taulut.get(taulu).set(id, sisältö); muutokset.push({taulu, id, uusi: sisältö}); } } return [tietokanta, muutokset]; } constructor() { for (let taulu in taulut) { this.taulut.set(taulu, new Map); } } transaktio(funktio) { const transaktio = new Transaktio(this); funktio(transaktio); return [this, this.suorita(transaktio)]; } suorita(transaktio) { if (transaktio.peruttu || transaktio.muutokset.length === 0) { return []; } // Varmista, että invariantit ovat yhä totta for (const {taulu, id, vanha, uusi} of transaktio.muutokset) { if (uusi === undefined && taulu !== taulut.tunnit) { // Poistettu luokka, opettaja tai tila ei ole tunnin käytössä const roikkuvat = transaktio.suodata(taulut.tunnit, (tunti) => { if (taulu === taulut.luokat) { return tunti.luokat.includes(id); } else if (taulu === taulut.opettajat) { return tunti.opettajat.includes(id); } else if (taulu === taulut.tilat) { return tunti.tilat.includes(id); } else { throw new Error(`Ei-tunnettu taulu ${taulu}`); } }); if (roikkuvat.length !== 0) { throw new Error(`Yritetty poistaa ${taulu}:${id}, joka on ${roikkuvat} käytössä`); } } else if (taulu === taulut.tunnit) { // Uusi tunti käyttää vain olemassaolevia luokkia, opettajia ja // tiloja for (const luokka of uusi.luokat) { if (transaktio.hae(taulut.luokat, luokka) === undefined) { throw new Error(`Yritetty luoda tunti ${id} olemattomalla luokalla ${luokka}`); } } for (const opettaja of uusi.opettajat) { if (transaktio.hae(taulut.opettajat, opettaja) === undefined) { throw new Error(`Yritetty luoda tunti ${id} olemattomalla opettajalla ${opettaja}`); } } for (const tila of uusi.tilat) { if (transaktio.hae(taulut.tilat, tila) === undefined) { throw new Error(`Yritetty luoda tunti ${id} olemattomalla tilalla ${tila}`); } } } } // Suorita muutokset for (const {taulu, id, uusi} of transaktio.muutokset) { if (uusi !== undefined) { this.taulut.get(taulu).set(id, uusi); } else { this.taulut.get(taulu).delete(id); } } this.historia.push({ muutokset: transaktio.muutokset, idMuutos: transaktio.seuraavaId - this.seuraavaId, }); this.seuraavaId = transaktio.seuraavaId; return transaktio.muutokset; } kumoa() { if (this.historia.length === 0) { return [this, []]; } const {muutokset, idMuutos} = this.historia.pop(); this.seuraavaId -= idMuutos; const kumotut = []; for (const {taulu, id, vanha, uusi} of muutokset) { if (vanha !== undefined) { this.taulut.get(taulu).set(id, vanha); } else { this.taulut.get(taulu).delete(id); } kumotut.push({ taulu, id, vanha: uusi, uusi: vanha, }); } return [this, kumotut]; } hae(taulu, id) { if (!this.taulut.has(taulu)) { throw new Error(`ei taulua ${taulu}`); } return this.taulut.get(taulu).get(id); } järjestyksessä(taulu, järjestys) { if (!this.taulut.has(taulu)) { throw new Error(`ei taulua ${taulu}`); } const taulukko = Array.from(this.taulut.get(taulu).entries()); taulukko.sort(([xId, x], [yId, y]) => { const vertaus = järjestys(x, y); if (vertaus < 0 || vertaus > 0) { return vertaus; } else { return xId - yId; } }); return taulukko.map(([id, _]) => id); } serialisoi() { return JSON.stringify(this, (avain, arvo) => { if (avain === 'historia') { return undefined; } if (arvo instanceof Map) { return Object.fromEntries(arvo.entries()); } return arvo; }); } } function tallennaTietokanta(tietokanta) { window.localStorage.setItem('tietokanta', tietokanta.serialisoi()); } function lataaTietokanta() { const serialisoitu = window.localStorage.getItem('tietokanta'); if (serialisoitu === null) { return; } let [tietokanta, muutokset] = Tietokanta.serialisoidusta(serialisoitu); _tietokanta = tietokanta; suorita([tietokanta, muutokset]); }