client.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. const snapshotBtn = document.getElementById('snapshotBtn');
  2. const snapshotList = document.getElementById('snapshotList');
  3. const snapshotComment = document.getElementById('snapshotComment');
  4. snapshotBtn.addEventListener('click', async() => {
  5. const snapshot = await takeSnapshot();
  6. const res = await fetch('/api/snapshots', {
  7. method: 'POST',
  8. headers: {'Content-Type': 'text/plain'},
  9. body: JSON.stringify(snapshot, jsonReplacer)
  10. });
  11. if(res.ok) {
  12. alert('Snapshot saved successfully!');
  13. loadSnapshots();
  14. } else {
  15. alert('Something went wrong while saving the snapshot: ' + res.statusText);
  16. }
  17. });
  18. snapshotComment.innerHTML = `Taken on ${new Date().toDateString()}`;
  19. async function takeSnapshot() {
  20. const localStorageData = {};
  21. for(let i = 0; i < localStorage.length; i++) {
  22. const key = localStorage.key(i);
  23. localStorageData[key] = localStorage.getItem(key);
  24. }
  25. const indexedDBData = {};
  26. const dbs = await indexedDB.databases?.() || [];
  27. for(const {name} of dbs) {
  28. if(!name) continue;
  29. indexedDBData[name] = {};
  30. const db = await openDB(name);
  31. for(const storeName of db.objectStoreNames) {
  32. const tx = db.transaction(storeName, 'readonly');
  33. const store = tx.objectStore(storeName);
  34. const entries = await getAllEntries(store);
  35. indexedDBData[name][storeName] = entries;
  36. }
  37. db.close();
  38. }
  39. return {
  40. comment: snapshotComment.innerHTML,
  41. localStorage: localStorageData,
  42. indexedDB: indexedDBData
  43. };
  44. }
  45. function openDB(name) {
  46. return new Promise((resolve, reject) => {
  47. const req = indexedDB.open(name);
  48. req.onsuccess = () => resolve(req.result);
  49. req.onerror = () => reject(req.error);
  50. });
  51. }
  52. // function getAllRecords(store) {
  53. // return new Promise((resolve, reject) => {
  54. // const request = store.getAll();
  55. // request.onsuccess = () => resolve(request.result);
  56. // request.onerror = () => reject(request.error);
  57. // });
  58. // }
  59. async function loadSnapshots() {
  60. const res = await fetch('/api/snapshots');
  61. const snapshots = await res.json();
  62. snapshotList.innerHTML = '';
  63. snapshots.forEach(({name, comment}) => {
  64. const li = document.createElement('div');
  65. li.classList.add('snapshot-item')
  66. const nameEl = document.createElement('span');
  67. nameEl.textContent = name;
  68. nameEl.classList.add('snapshot-name');
  69. const commentEl = document.createElement('div');
  70. commentEl.innerHTML = comment;
  71. commentEl.classList.add('snapshot-item-comment');
  72. const loadBtn = document.createElement('button');
  73. loadBtn.textContent = 'Load';
  74. loadBtn.onclick = () => loadSnapshot(name);
  75. const deleteBtn = document.createElement('button');
  76. deleteBtn.textContent = 'Delete';
  77. deleteBtn.onclick = async() => {
  78. const confirmed = confirm('Are you sure you want to delete this snapshot?');
  79. if(!confirmed) return;
  80. await fetch(`/api/snapshots/${name}`, {method: 'DELETE'});
  81. alert('Snapshot successfully deleted');
  82. loadSnapshots();
  83. };
  84. li.appendChild(nameEl)
  85. li.appendChild(loadBtn);
  86. li.appendChild(deleteBtn);
  87. snapshotList.appendChild(li);
  88. if((comment || '').trim?.()) snapshotList.appendChild(commentEl);
  89. });
  90. }
  91. window.onload = loadSnapshots;
  92. async function loadSnapshot(filename) {
  93. if(!confirm(`Are you sure you want to load snapshot "${filename}"? This will overwrite your current storage.`)) {
  94. return;
  95. }
  96. const res = await fetch(`/api/snapshots/${filename}`);
  97. if(!res.ok) {
  98. alert('Failed to load snapshot');
  99. return;
  100. }
  101. const snapshotText = await res.text();
  102. const snapshot = JSON.parse(snapshotText, jsonReviver);
  103. // Restore localStorage
  104. localStorage.clear();
  105. for(const [key, value] of Object.entries(snapshot.localStorage || {})) {
  106. localStorage.setItem(key, value);
  107. }
  108. // Restore indexedDB
  109. await clearAllIndexedDB();
  110. for(const [dbName, stores] of Object.entries(snapshot.indexedDB || {})) {
  111. await restoreIndexedDB(dbName, stores);
  112. }
  113. alert('Snapshot loaded successfully!');
  114. }
  115. function clearAllIndexedDB() {
  116. return new Promise(async(resolve) => {
  117. const dbs = await indexedDB.databases?.() || [];
  118. let count = dbs.length;
  119. if(count === 0) resolve();
  120. for(const {name} of dbs) {
  121. if(!name) {
  122. count--;
  123. if(count === 0) resolve();
  124. continue;
  125. }
  126. const req = indexedDB.deleteDatabase(name);
  127. req.onsuccess = () => {
  128. count--;
  129. if(count === 0) resolve();
  130. };
  131. req.onerror = () => {
  132. console.warn(`Failed to delete indexedDB ${name}`);
  133. count--;
  134. if(count === 0) resolve();
  135. };
  136. }
  137. });
  138. }
  139. function restoreIndexedDB(dbName, stores) {
  140. return new Promise((resolve, reject) => {
  141. const req = indexedDB.open(dbName);
  142. req.onupgradeneeded = (event) => {
  143. const db = event.target.result;
  144. // Delete existing object stores if any
  145. Array.from(db.objectStoreNames).forEach(storeName => {
  146. db.deleteObjectStore(storeName);
  147. });
  148. // Create stores from snapshot
  149. for(const storeName of Object.keys(stores)) {
  150. db.createObjectStore(storeName/* , {keyPath: 'id', autoIncrement: false} */);
  151. }
  152. };
  153. req.onsuccess = async(event) => {
  154. const db = event.target.result;
  155. // Write all entries to each store
  156. try {
  157. for(const [storeName, entries] of Object.entries(stores)) {
  158. const tx = db.transaction(storeName, 'readwrite');
  159. const store = tx.objectStore(storeName);
  160. for(const entry of entries) {
  161. store.put(entry.value, entry.key);
  162. }
  163. await tx.complete; // some browsers support promise on tx.complete, others don't
  164. }
  165. } catch(e) {
  166. console.error('Error restoring indexedDB data', e);
  167. }
  168. db.close();
  169. resolve();
  170. };
  171. req.onerror = () => reject(req.error);
  172. });
  173. }
  174. function jsonReplacer(key, value) {
  175. if(value instanceof Uint8Array) {
  176. return {
  177. __type: 'Uint8Array',
  178. data: Array.from(value) // convert to normal array for JSON
  179. };
  180. }
  181. return value;
  182. }
  183. function jsonReviver(key, value) {
  184. if(value && value.__type === 'Uint8Array' && Array.isArray(value.data)) {
  185. return new Uint8Array(value.data);
  186. }
  187. return value;
  188. }
  189. function getAllEntries(store) {
  190. return new Promise((resolve, reject) => {
  191. const entries = [];
  192. const request = store.openCursor();
  193. request.onsuccess = (event) => {
  194. const cursor = event.target.result;
  195. if(cursor) {
  196. entries.push({key: cursor.key, value: cursor.value});
  197. cursor.continue();
  198. } else {
  199. resolve(entries);
  200. }
  201. };
  202. request.onerror = () => reject(request.error);
  203. });
  204. }