clone

tiny dependency-free nostr libraries for plain html, each under 5kb and named for an element. public domain

main
nobel / lithium / lithium.js
lithium.js 5.0K
1(()=>{2 let g = globalThis, d = g.document3 if (g.lithium) return4 let n = 0, INF = 1/05 let hex = (b)=>[...b].map((x)=>x.toString(16).padStart(2, "0")).join("")6 let bytes = (h)=>(h.match(/../g) ?? []).map((x)=>parseInt(x, 16))7 let varint = (o, v, a = [v % 128])=>{8 while (v = Math.floor(v / 128)) a.unshift(v % 128 | 128)9 o.push(...a)10 }11 let session = (items)=>{12 let rt, wt, out13 let fp = async(lo, hi, sum = Array(32).fill(0))=>{14 for (let i = lo; i < hi; i++) for (let j = 0, c = 0; j < 32; j++) c = (c >> 8) + sum[j] + items[i].b[j], sum[j] = c & 25515 varint(sum, hi - lo)16 return hex(new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(sum)), 0, 16))17 }18 let bound = (t, id = "")=>{19 varint(out, t == INF ? 0 : t - wt + 1)20 wt = t21 varint(out, id.length / 2)22 out.push(...bytes(id))23 }24 let split = async(lo, hi, t, id, c = hi - lo)=>{25 if (c < 32){26 bound(t, id)27 out.push(2)28 varint(out, c)29 for (let i = lo; i < hi; i++) out.push(...items[i].b)30 }31 else for (let i = 0, cur = lo; i < 16; i++){32 let f = await fp(cur, cur += (c >> 4) + (i < c % 16)), p = items[cur - 1], q = items[cur], k = 033 if (cur == hi) bound(t, id)34 else if (p.t != q.t) bound(q.t)35 else {36 while (p.b[k] == q.b[k]) k++37 bound(q.t, q.id.slice(0, 2 * k + 2))38 }39 out.push(1, ...bytes(f))40 }41 }42 let reconcile = async(msg)=>{43 let b = bytes(msg), p = 1, have = [], need = [], prev = 0, skip, pb44 let read = (v = 0, x)=>{45 do v = v * 128 + ((x = b[p++]) & 127)46 while (x & 128)47 if (x == null) throw Error("short")48 return v49 }50 if (b[0] != 97) throw Error("v1 only")51 rt = wt = 052 out = [97]53 while (p < b.length){54 let v = read(), t = rt = v ? rt + v - 1 : INF, len = read(), id = hex(b.slice(p, p += len)), mode = read(), hi = prev, x55 while ((x = items[hi]) && (x.t < t || x.t == t && x.id < id.padEnd(64, "0"))) hi++56 if (mode == 1 && hex(b.slice(p, p += 16)) != await fp(prev, hi)){57 if (skip) bound(...pb), out.push(0)58 skip = 059 await split(prev, hi, t, id)60 } else if (mode == 2){61 let ids = new Set()62 for (let c = read(); c--;) ids.add(hex(b.slice(p, p += 32)))63 for (let i = prev; i < hi; i++) ids.delete(items[i].id) || have.push(items[i].id)64 need.push(...ids)65 skip = 166 } else skip = 167 prev = hi68 pb = [t, id]69 }70 return {reply:out.length > 1 && hex(out), have, need}71 }72 return {start:async()=>(wt = 0, out = [97], await split(0, items.length, INF, ""), hex(out)), reconcile}73 }74 let sync = async(filter, url, mode)=>{75 let items = (await argon.query([filter])).map((e)=>({t:e.created_at, id:e.id, b:bytes(e.id)})).sort((a, b)=>a.t - b.t || (a.id < b.id ? -1 : 1))76 let s = session(items), id = "nhe" + ++n, have = [], need = [], pulled = 0, pushed = 0, q = [], wake, closed77 let ws = new WebSocket(url), tx = (...m)=>ws.send(JSON.stringify(m)), t, idle = ()=>(clearTimeout(t), t = setTimeout(()=>ws.close(), 6e4))78 let rx = async(f, i)=>{79 while ((i = q.findIndex(f)) < 0) if (closed) throw Error("closed"); else await new Promise((r)=>wake = r)80 return q.splice(i, 1)[0]81 }82 ws.onmessage = (m)=>(idle(), q.push(JSON.parse(m.data)), wake?.())83 ws.onclose = ()=>(closed = 1, wake?.())84 try {85 await new Promise((r)=>ws.onopen = wake = r, idle())86 tx("NEG-OPEN", id, filter, await s.start())87 for (let k, m, r; ([k, , m] = await rx((x)=>x[1] == id))[0] != "NEG-CLOSE";){88 if (k != "NEG-MSG") throw Error(m)89 r = await s.reconcile(m)90 have.push(...r.have)91 need.push(...r.need)92 tx(...r.reply ? ["NEG-MSG", id, r.reply] : ["NEG-CLOSE", id])93 if (!r.reply) break94 }95 if (mode != "push") for (let i = 0; i < need.length; i += 250){96 tx("REQ", id, {ids:need.slice(i, i + 250)})97 for (let x; (x = await rx((x)=>x[1] == id))[0] == "EVENT";)98 if (need.includes(x[2].id) && await (g.helium?.check(x[2]) ?? 1) && await argon.put(x[2])) pulled++99 tx("CLOSE", id)100 }101 if (mode != "pull") for (let e of await argon.query([{ids:have}])) for (let k, m; ;){102 tx("EVENT", e)103 ;[, , k, m] = await rx((x)=>x[0] == "OK" && x[1] == e.id)104 if (k && ++pushed || !(m = /retry in (\d+)/.exec(m))) break105 await new Promise((r)=>setTimeout(r, m[1] * 1e3, idle()))106 }107 return {relay:url, have, need, pulled, pushed}108 } finally {109 clearTimeout(t)110 ws.close()111 }112 }113 g.lithium = {sync, session}114 if (!d) return115 let send = (el, type, detail)=>el.dispatchEvent(new CustomEvent("nli:" + type, {detail, cancelable:1, bubbles:1}))116 let run = g.lithium.run = (el)=>{117 let cfg = {filter:JSON.parse(el.getAttribute("nli-sync")), relays:hydrogen.relays(el), mode:el.getAttribute("nli-mode")}118 if (send(el, "sync", {cfg})) for (let url of cfg.relays) sync(cfg.filter, url, cfg.mode).then((result)=>send(el, "synced", {cfg, result}), (error)=>send(el, "error", {cfg, relay:url, error}))119 }120 let all = ()=>d.querySelectorAll("[nli-sync]").forEach(run)121 g.addEventListener("online", all)122 d.readyState == "loading" ? d.addEventListener("DOMContentLoaded", all) : all()123})()