1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
// Referenced notification notes are fetched before the page opens, persisted,
// and available on the first paint after a reload.
// Run: bun tools/notif-note-cache-test.js
import puppeteer from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
import { buildHtml } from '../build.js';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let ok = true;
const check = (name, pass, detail = '') => {
console.log(` ${pass ? '✓' : '✗'} ${name}${detail ? ` — ${detail}` : ''}`);
if (!pass) ok = false;
};
const now = Math.floor(Date.now() / 1000);
const actorSk = generateSecretKey();
const actorPk = getPublicKey(actorSk);
const noteSk = generateSecretKey();
const noteText = 'This notification excerpt came from the local note cache.';
const target = finalizeEvent({ kind: 1, created_at: now - 120, tags: [], content: noteText }, noteSk);
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({
port: 5307,
fetch(req) {
const path = new URL(req.url).pathname;
if (path.startsWith('/punks') || path === '/verify-worker.js') return new Response(Bun.file('dist' + path));
return new Response(html, { headers: { 'content-type': 'text/html' } });
},
});
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
await page.evaluateOnNewDocument((targetEvent) => {
window.__targetReqs = 0;
const Real = window.WebSocket;
window.WebSocket = function (...args) {
const ws = new Real(...args);
const send = ws.send.bind(ws);
ws.send = (data) => {
try {
const msg = JSON.parse(data);
if (msg[0] === 'REQ') {
const filter = msg[2] || {};
const hit = (filter.ids || []).includes(targetEvent.id);
if (hit) window.__targetReqs++;
setTimeout(() => {
if (hit) ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EVENT', msg[1], targetEvent]) }));
ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EOSE', msg[1]]) }));
}, 0);
return;
}
if (msg[0] === 'EVENT') return;
} catch {}
return send(data);
};
return ws;
};
window.WebSocket.prototype = Real.prototype;
Object.assign(window.WebSocket, Real);
}, target);
const clickText = (text) => page.evaluate((needle) => {
const el = [...document.querySelectorAll('button')].find((node) => node.textContent.trim().toLowerCase().includes(needle));
if (!el) return false;
el.click(); return true;
}, text);
const waitText = async (text, timeout = 20_000) => {
for (let elapsed = 0; elapsed < timeout; elapsed += 200) {
if ((await page.evaluate(() => document.body.innerText)).toLowerCase().includes(text.toLowerCase())) return true;
await sleep(200);
}
return false;
};
const openNotifications = () => page.evaluate(() => {
const el = [...document.querySelectorAll('button')].find((node) => /notifications/i.test(node.getAttribute('aria-label') || ''));
if (!el) return false;
el.click(); return true;
});
try {
await page.setViewport({ width: 390, height: 844 });
await page.goto('http://localhost:5307/', { waitUntil: 'domcontentloaded' });
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(400);
await clickText('create a new wallet');
await page.waitForSelector('.words .w .t');
const mnemonic = (await page.$$eval('.words .w .t', (nodes) => nodes.map((node) => node.textContent.trim()))).join(' ');
await clickText('skip verification');
await waitText('receive');
const base = cacheKeyFor(mnemonic + '\n');
const me = getPublicKey(privateKeyFromSeedWords(mnemonic));
const item = { id: 'a'.repeat(64), what: 'react', actor: actorPk, target: target.id, hint: [], emoji: '❤️', ts: now - 30 };
await page.evaluate(([key, owner, notification]) => {
const state = JSON.parse(localStorage.getItem(key + ':messages') || '{}');
state.notifs = [notification]; state.notifsPk = owner; state.notifsV = 2;
localStorage.setItem(key + ':messages', JSON.stringify(state));
}, [base, me, item]);
await page.reload({ waitUntil: 'domcontentloaded' });
await waitText('receive');
let cached = false;
for (let i = 0; i < 75; i++) {
cached = await page.evaluate(([key, id]) => {
const state = JSON.parse(localStorage.getItem(key + ':messages') || '{}');
return state.notifNoteCache && state.notifNoteCache[id]?.content;
}, [base, target.id]);
if (cached) break;
await sleep(200);
}
check('the referenced note is warmed and persisted', cached === noteText);
check('the relay was queried while the page was still closed', (await page.evaluate(() => window.__targetReqs)) > 0);
await page.reload({ waitUntil: 'domcontentloaded' });
await waitText('receive');
check('notifications opens from the bottom navigation', await openNotifications());
await sleep(80);
const firstPaint = await page.evaluate(() => document.body.innerText);
check('the cached excerpt is present immediately', firstPaint.includes('This notification excerpt'));
check('there is no Fetching note placeholder', !firstPaint.includes('Fetching note'));
check('the persisted note avoids another target query', (await page.evaluate(() => window.__targetReqs)) === 0);
} finally {
await browser.close();
server.stop(true);
}
console.log(ok ? '\n✅ notification note cache works' : '\n❌ notification note cache failed');
process.exit(ok ? 0 : 1);
|