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 1 year 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>
</div>
</div>
<div id="errorOverlay" class="form" hidden></div>
</main>
</body>

@ -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 {

@ -729,6 +729,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?
@ -738,7 +741,7 @@ document.querySelector('#bubble').addEventListener('click', (e) => {
if (writeInput.value.trimRight()) {
writeInput.style.removeProperty('height');
}
document.body.style.overflow = 'hidden';
lockScroll();
requestAnimationFrame(() => updateElemHeight(writeInput));
});
@ -749,7 +752,7 @@ document.body.addEventListener('keyup', (e) => {
});
function hideNewMessage(hide) {
document.body.style.removeProperty('overflow');
unlockScroll();
newMessageDiv.hidden = hide;
}
@ -768,17 +771,19 @@ async function upvote(eventId, eventPubkey) {
content: '+',
tags,
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);
}
}
}
@ -805,26 +810,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);
}
});
}
}
});
@ -947,22 +954,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;
}
/**
* validate proof-of-work of a nostr event per nip-13.
* 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.
* 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);
}

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

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

Loading…
Cancel
Save