nip-13: add timeout and show user facing error if it exceeds

mining may take a long time if the mining difficulty is high.

calculating pow for text notes, upvotes and profile meta
data now has a timeout of 10s. if the timeout exceeds a user
facing error is shown with the option to try again.

the error is currently very basic, and only displays timeout -
something went wrong, cancel and try again button.
pull/55/head
OFF0 2 years ago
parent a1b1f3baee
commit d5e9ef18c7
Signed by: offbyn
GPG Key ID: 94A2F643C51F37FA

@ -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;
}
}

@ -106,6 +106,7 @@
</footer> </footer>
</div> </div>
</div> </div>
<div id="errorOverlay" class="form" hidden></div>
</main> </main>
</body> </body>

@ -2,6 +2,7 @@
@import "cards.css"; @import "cards.css";
@import "form.css"; @import "form.css";
@import "write.css"; @import "write.css";
@import "error.css";
:root { :root {
/* 5px auto Highlight */ /* 5px auto Highlight */
@ -14,6 +15,7 @@
--focus-outline: var(--focus-outline-width) var(--focus-outline-style) var(--focus-outline-color); --focus-outline: var(--focus-outline-width) var(--focus-outline-style) var(--focus-outline-color);
--font-small: 1.2rem; --font-small: 1.2rem;
--gap: 2.4rem; --gap: 2.4rem;
--max-width: 96ch;
} }
::selection { ::selection {

@ -729,6 +729,9 @@ function appendReplyForm(el) {
requestAnimationFrame(() => writeInput.focus()); requestAnimationFrame(() => writeInput.focus());
} }
const lockScroll = () => document.body.style.overflow = 'hidden';
const unlockScroll = () => document.body.style.removeProperty('overflow');
const newMessageDiv = document.querySelector('#newMessage'); const newMessageDiv = document.querySelector('#newMessage');
document.querySelector('#bubble').addEventListener('click', (e) => { document.querySelector('#bubble').addEventListener('click', (e) => {
localStorage.removeItem('reply_to'); // should it forget old replyto context? localStorage.removeItem('reply_to'); // should it forget old replyto context?
@ -738,7 +741,7 @@ document.querySelector('#bubble').addEventListener('click', (e) => {
if (writeInput.value.trimRight()) { if (writeInput.value.trimRight()) {
writeInput.style.removeProperty('height'); writeInput.style.removeProperty('height');
} }
document.body.style.overflow = 'hidden'; lockScroll();
requestAnimationFrame(() => updateElemHeight(writeInput)); requestAnimationFrame(() => updateElemHeight(writeInput));
}); });
@ -749,7 +752,7 @@ document.body.addEventListener('keyup', (e) => {
}); });
function hideNewMessage(hide) { function hideNewMessage(hide) {
document.body.style.removeProperty('overflow'); unlockScroll();
newMessageDiv.hidden = hide; newMessageDiv.hidden = hide;
} }
@ -768,7 +771,8 @@ async function upvote(eventId, eventPubkey) {
content: '+', content: '+',
tags, tags,
created_at: Math.floor(Date.now() * 0.001), created_at: Math.floor(Date.now() * 0.001),
}, difficulty); }, difficulty, 10).catch(console.warn);
if (newReaction) {
const sig = await signEvent(newReaction, privatekey).catch(console.error); const sig = await signEvent(newReaction, privatekey).catch(console.error);
if (sig) { if (sig) {
const ev = await pool.publish({...newReaction, sig}, (status, url) => { const ev = await pool.publish({...newReaction, sig}, (status, url) => {
@ -780,6 +784,7 @@ async function upvote(eventId, eventPubkey) {
} }
}).catch(console.error); }).catch(console.error);
} }
}
} }
// send // send
@ -805,7 +810,8 @@ writeForm.addEventListener('submit', async (e) => {
pubkey, pubkey,
tags, tags,
created_at: Math.floor(Date.now() * 0.001), created_at: Math.floor(Date.now() * 0.001),
}, difficulty); }, difficulty, 10).catch(console.warn);
if (newEvent) {
const sig = await signEvent(newEvent, privatekey).catch(onSendError); const sig = await signEvent(newEvent, privatekey).catch(onSendError);
if (sig) { if (sig) {
const ev = await pool.publish({...newEvent, sig}, (status, url) => { const ev = await pool.publish({...newEvent, sig}, (status, url) => {
@ -826,6 +832,7 @@ writeForm.addEventListener('submit', async (e) => {
} }
}); });
} }
}
}); });
writeInput.addEventListener('input', () => { writeInput.addEventListener('input', () => {
@ -947,7 +954,8 @@ profileForm.addEventListener('submit', async (e) => {
content: JSON.stringify(Object.fromEntries(form)), content: JSON.stringify(Object.fromEntries(form)),
tags: [], tags: [],
created_at: Math.floor(Date.now() * 0.001), created_at: Math.floor(Date.now() * 0.001),
}, difficulty); }, difficulty, 10).catch(console.warn);
if (newProfile) {
const sig = await signEvent(newProfile, privatekey).catch(console.error); const sig = await signEvent(newProfile, privatekey).catch(console.error);
if (sig) { if (sig) {
const ev = await pool.publish({...newProfile, sig}, (status, url) => { const ev = await pool.publish({...newProfile, sig}, (status, url) => {
@ -961,8 +969,42 @@ profileForm.addEventListener('submit', async (e) => {
} }
}).catch(console.error); }).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;
}
/** /**
* validate proof-of-work of a nostr event per nip-13. * validate proof-of-work of a nostr event per nip-13.
* the validation always requires difficulty commitment in the nonce tag. * the validation always requires difficulty commitment in the nonce tag.
@ -990,14 +1032,20 @@ function validatePow(evt) {
* powEvent returns a rejected promise if the funtion runs for longer than timeout. * powEvent returns a rejected promise if the funtion runs for longer than timeout.
* a zero timeout makes mineEvent run without a time limit. * 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) => { return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js'); const worker = new Worker('./worker.js');
worker.onmessage = (msg) => { worker.onmessage = (msg) => {
worker.terminate(); worker.terminate();
if (msg.data.error) { 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 { } else {
resolve(msg.data.event); resolve(msg.data.event);
} }

@ -43,7 +43,7 @@ input[type="radio"]:checked + label {
} }
.tab-content { .tab-content {
max-width: 96ch; max-width: var(--max-width);
min-height: 200px; min-height: 200px;
padding: calc(.5 * var(--gap)) 0 100px 0; padding: calc(.5 * var(--gap)) 0 100px 0;
} }

@ -10,14 +10,14 @@ function mine(event, difficulty, timeout = 5) {
let n = BigInt(0); let n = BigInt(0);
event.tags.unshift(['nonce', n.toString(), `${difficulty}`]); 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'); console.time('pow');
while (true) { while (true) {
const now = Math.floor(Date.now() * 0.001); const now = Math.floor(Date.now() * 0.001);
// if (now > start + 15) { if (timeout !== 0 && (now > until)) {
// console.timeEnd('pow'); console.timeEnd('pow');
// return false; throw 'timeout';
// } }
if (now !== event.created_at) { if (now !== event.created_at) {
event.created_at = now; event.created_at = now;
// n = BigInt(0); // could reset nonce as we have a new timestamp // 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(); event.tags[0][1] = (++n).toString();
const id = getEventHash(event); const id = getEventHash(event);
if (zeroLeadingBitsCount(id) === difficulty) { if (zeroLeadingBitsCount(id) === difficulty) {
console.log(event.tags[0][1], id);
console.timeEnd('pow'); console.timeEnd('pow');
return event; return event;
} }
@ -33,11 +32,7 @@ function mine(event, difficulty, timeout = 5) {
} }
addEventListener('message', async (msg) => { addEventListener('message', async (msg) => {
const { const {difficulty, event, timeout} = msg.data;
difficulty,
event,
timeout,
} = msg.data;
try { try {
const minedEvent = mine(event, difficulty, timeout); const minedEvent = mine(event, difficulty, timeout);
postMessage({event: minedEvent}); postMessage({event: minedEvent});

Loading…
Cancel
Save