From d3000e188a85ab9fd07acb19360c65d86dfb27a9 Mon Sep 17 00:00:00 2001 From: OFF0 Date: Tue, 3 Jan 2023 17:00:35 +0100 Subject: [PATCH] nip-13: add timeout and show user facing error if it exceeds --- src/error.css | 36 +++++++++++++ src/index.html | 1 + src/main.css | 2 + src/main.js | 142 +++++++++++++++++++++++++++++++++---------------- src/tabs.css | 2 +- src/worker.js | 17 +++--- 6 files changed, 141 insertions(+), 59 deletions(-) create mode 100644 src/error.css diff --git a/src/error.css b/src/error.css new file mode 100644 index 0000000..8e24758 --- /dev/null +++ b/src/error.css @@ -0,0 +1,36 @@ +#errorOverlay { + background: var(--bgcolor-danger); + bottom: 0; + display: flex; + flex-direction: column; + left: 0; + overflow: auto; + padding: var(--gap); + position: fixed; + right: 0; + top: 0; + z-index: 100; +} + +.error-title { + margin-top: 0; +} + +#errorOverlay button { + background-color: rgba(0 0 0 / .5); + border: none; + display: inline-block; +} +#errorOverlay button:focus { + outline: 2px solid white; + outline-offset: var(--focus-outline-offset); +} + +#errorOverlay .buttons { + max-width: var(--max-width); +} +@media (orientation: portrait) { + #errorOverlay .buttons { + flex-basis: 4rem; + } +} diff --git a/src/index.html b/src/index.html index 23f0f3e..8db0b89 100644 --- a/src/index.html +++ b/src/index.html @@ -52,6 +52,7 @@
+
diff --git a/src/main.css b/src/main.css index a60d2f1..01e10b9 100644 --- a/src/main.css +++ b/src/main.css @@ -2,6 +2,7 @@ @import "cards.css"; @import "form.css"; @import "write.css"; +@import "error.css"; :root { /* 5px auto Highlight */ @@ -14,6 +15,7 @@ --focus-outline: var(--focus-outline-width) var(--focus-outline-style) var(--focus-outline-color); --font-small: 1.2rem; --gap: 2.4rem; + --max-width: 96ch; } ::selection { diff --git a/src/main.js b/src/main.js index 5bb97e0..292d6d5 100644 --- a/src/main.js +++ b/src/main.js @@ -740,6 +740,9 @@ function appendReplyForm(el) { requestAnimationFrame(() => writeInput.focus()); } +const lockScroll = () => document.body.style.overflow = 'hidden'; +const unlockScroll = () => document.body.style.removeProperty('overflow'); + const newMessageDiv = document.querySelector('#newMessage'); document.querySelector('#bubble').addEventListener('click', (e) => { localStorage.removeItem('reply_to'); // should it forget old replyto context? @@ -749,7 +752,7 @@ document.querySelector('#bubble').addEventListener('click', (e) => { if (writeInput.value.trimRight()) { writeInput.style.removeProperty('height'); } - document.body.style.overflow = 'hidden'; + lockScroll(); requestAnimationFrame(() => updateElemHeight(writeInput)); }); @@ -760,7 +763,7 @@ document.body.addEventListener('keyup', (e) => { }); function hideNewMessage(hide) { - document.body.style.removeProperty('overflow'); + unlockScroll(); newMessageDiv.hidden = hide; } @@ -772,17 +775,19 @@ async function upvote(eventId, relay) { content: '+', tags: [['e', eventId, relay, 'reply']], created_at: Math.floor(Date.now() * 0.001), - }, difficulty); - const sig = await signEvent(newReaction, privatekey).catch(console.error); - if (sig) { - const ev = await pool.publish({...newReaction, sig}, (status, url) => { - if (status === 0) { - console.info(`publish request sent to ${url}`); - } - if (status === 1) { - console.info(`event published by ${url}`); - } - }).catch(console.error); + }, difficulty, 10).catch(console.warn); + if (newReaction) { + const sig = await signEvent(newReaction, privatekey).catch(console.error); + if (sig) { + const ev = await pool.publish({...newReaction, sig}, (status, url) => { + if (status === 0) { + console.info(`publish request sent to ${url}`); + } + if (status === 1) { + console.info(`event published by ${url}`); + } + }).catch(console.error); + } } } @@ -809,26 +814,28 @@ writeForm.addEventListener('submit', async (e) => { pubkey, tags, created_at: Math.floor(Date.now() * 0.001), - }, difficulty); - const sig = await signEvent(newEvent, privatekey).catch(onSendError); - if (sig) { - const ev = await pool.publish({...newEvent, sig}, (status, url) => { - if (status === 0) { - console.info(`publish request sent to ${url}`); - } - if (status === 1) { - sendStatus.textContent = ''; - writeInput.value = ''; - writeInput.style.removeProperty('height'); - publish.disabled = true; - if (replyTo) { - localStorage.removeItem('reply_to'); - newMessageDiv.append(writeForm); + }, difficulty, 10).catch(console.warn); + if (newEvent) { + const sig = await signEvent(newEvent, privatekey).catch(onSendError); + if (sig) { + const ev = await pool.publish({...newEvent, sig}, (status, url) => { + if (status === 0) { + console.info(`publish request sent to ${url}`); } - hideNewMessage(true); - // console.info(`event published by ${url}`, ev); - } - }); + if (status === 1) { + sendStatus.textContent = ''; + writeInput.value = ''; + writeInput.style.removeProperty('height'); + publish.disabled = true; + if (replyTo) { + localStorage.removeItem('reply_to'); + newMessageDiv.append(writeForm); + } + hideNewMessage(true); + // console.info(`event published by ${url}`, ev); + } + }); + } } }); @@ -951,22 +958,57 @@ profileForm.addEventListener('submit', async (e) => { content: JSON.stringify(Object.fromEntries(form)), tags: [], created_at: Math.floor(Date.now() * 0.001), - }, difficulty); - const sig = await signEvent(newProfile, privatekey).catch(console.error); - if (sig) { - const ev = await pool.publish({...newProfile, sig}, (status, url) => { - if (status === 0) { - console.info(`publish request sent to ${url}`); - } - if (status === 1) { - profileStatus.textContent = 'profile metadata successfully published'; - profileStatus.hidden = false; - profileSubmit.disabled = true; - } - }).catch(console.error); + }, difficulty, 10).catch(console.warn); + if (newProfile) { + const sig = await signEvent(newProfile, privatekey).catch(console.error); + if (sig) { + const ev = await pool.publish({...newProfile, sig}, (status, url) => { + if (status === 0) { + console.info(`publish request sent to ${url}`); + } + if (status === 1) { + profileStatus.textContent = 'profile metadata successfully published'; + profileStatus.hidden = false; + profileSubmit.disabled = true; + } + }).catch(console.error); + } } }); +const errorOverlay = document.querySelector('#errorOverlay'); + +function promptError(error, options = {}) { + const {onAgain, onCancel} = options; + lockScroll(); + errorOverlay.replaceChildren( + elem('h1', {className: 'error-title'}, error), + elem('p', {}, 'something went wrong'), + elem('div', {className: 'buttons'}, [ + onCancel ? elem('button', {data: {action: 'close'}}, 'close') : '', + onAgain ? elem('button', {data: {action: 'again'}}, 'try again') : '', + ]), + ); + const handleOverlayClick = (e) => { + const button = e.target.closest('button'); + if (button) { + switch(button.dataset.action) { + case 'close': + onCancel(); + break; + case 'again': + onAgain(); + break; + } + errorOverlay.removeEventListener('click', handleOverlayClick); + errorOverlay.hidden = true; + unlockScroll(); + } + }; + errorOverlay.addEventListener('click', handleOverlayClick); + errorOverlay.hidden = false; +} + /** * check that the event has the id has the desired number of leading zero bits * @param {EventObj} evt to validate @@ -989,14 +1031,20 @@ function validatePow(evt) { * powEvent returns a rejected promise if the funtion runs for longer than timeout. * a zero timeout makes mineEvent run without a time limit. */ -function powEvent(evt, difficulty, timeout) { +function powEvent(evt, difficulty, timeout = 0) { return new Promise((resolve, reject) => { const worker = new Worker('./worker.js'); worker.onmessage = (msg) => { worker.terminate(); if (msg.data.error) { - reject(msg.data.error); + promptError(msg.data.error, { + onCancel: () => reject('canceled'), + onAgain: async () => { + const result = await powEvent(evt, difficulty, timeout).catch(console.warn); + resolve(result); + } + }) } else { resolve(msg.data.event); } diff --git a/src/tabs.css b/src/tabs.css index 57a0c07..39cf5ec 100644 --- a/src/tabs.css +++ b/src/tabs.css @@ -43,7 +43,7 @@ input[type="radio"]:checked + label { } .tab-content { - max-width: 96ch; + max-width: var(--max-width); min-height: 200px; padding: calc(.5 * var(--gap)) 0 100px 0; } diff --git a/src/worker.js b/src/worker.js index c72967c..726adb3 100644 --- a/src/worker.js +++ b/src/worker.js @@ -10,14 +10,14 @@ function mine(event, difficulty, timeout = 5) { let n = BigInt(0); event.tags.unshift(['nonce', n.toString(), `${difficulty}`]); - const start = Math.floor(Date.now() * 0.001); + const until = Math.floor(Date.now() * 0.001) + timeout; console.time('pow'); while (true) { const now = Math.floor(Date.now() * 0.001); - // if (now > start + 15) { - // console.timeEnd('pow'); - // return false; - // } + if (timeout !== 0 && (now > until)) { + console.timeEnd('pow'); + throw 'timeout'; + } if (now !== event.created_at) { event.created_at = now; // n = BigInt(0); // could reset nonce as we have a new timestamp @@ -25,7 +25,6 @@ function mine(event, difficulty, timeout = 5) { event.tags[0][1] = (++n).toString(); const id = getEventHash(event); if (zeroLeadingBitsCount(id) === difficulty) { - console.log(event.tags[0][1], id); console.timeEnd('pow'); return event; } @@ -33,11 +32,7 @@ function mine(event, difficulty, timeout = 5) { } addEventListener('message', async (msg) => { - const { - difficulty, - event, - timeout, - } = msg.data; + const {difficulty, event, timeout} = msg.data; try { const minedEvent = mine(event, difficulty, timeout); postMessage({event: minedEvent});