nip-13: add timeout and show user facing error if it exceeds
depending on the difficulty target mining events could take a while. 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.
parent
998b9dbf58
commit
dbb4970256
|
@ -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 {
|
||||
|
|
142
src/main.js
142
src/main.js
|
@ -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 the difficulty target matches the number of leading zero bits
|
||||
* @param {EventObj} evt to validate
|
||||
|
@ -985,14 +1027,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…
Reference in New Issue