Add storage adapters
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import type { ISigner } from "applesauce-signers"
|
||||
import { getJson, setJson } from "@welshman/lib"
|
||||
import type { Hex, QuorumMember } from "./protocol"
|
||||
|
||||
// ── Model types ───────────────────────────────────────────────────────────────
|
||||
// ── Quorum & shard records ────────────────────────────────────────────────────
|
||||
|
||||
/** A known quorum — no secret material */
|
||||
export type QuorumRecord = {
|
||||
@@ -25,13 +23,16 @@ export type ShardRecord = {
|
||||
encryptedShard: string
|
||||
}
|
||||
|
||||
// ── Session helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
/** Proof-of-knowledge from a round-1 message */
|
||||
export type PoKProof = { R: Hex; s: Hex }
|
||||
|
||||
/** Round-1 broadcast from one participant */
|
||||
export type Round1Data = { commitments: Hex[]; proof: PoKProof }
|
||||
|
||||
/** Phase of a DKG session */
|
||||
// ── DKG session ───────────────────────────────────────────────────────────────
|
||||
|
||||
export type DkgPhase = "round1" | "round2" | "confirming" | "complete"
|
||||
|
||||
/** In-progress DKG session, keyed by the kind 7050 inner event ID */
|
||||
@@ -52,7 +53,8 @@ export type DkgSession = {
|
||||
declines: Record<Hex, string>
|
||||
}
|
||||
|
||||
/** Phase of a resharing session */
|
||||
// ── Resharing session ─────────────────────────────────────────────────────────
|
||||
|
||||
export type ResharingPhase = "round1" | "round2" | "confirming" | "complete"
|
||||
|
||||
/** In-progress resharing session, keyed by the kind 7054 inner event ID */
|
||||
@@ -73,7 +75,8 @@ export type ResharingSession = {
|
||||
declines: Record<Hex, string>
|
||||
}
|
||||
|
||||
/** Phase of a signing session */
|
||||
// ── Signing session ───────────────────────────────────────────────────────────
|
||||
|
||||
export type SigningPhase = "round1" | "round2" | "complete"
|
||||
|
||||
/** In-progress signing session, keyed by the kind 7058 inner event ID */
|
||||
@@ -94,52 +97,7 @@ export type SigningSession = {
|
||||
declines: Record<Hex, string>
|
||||
}
|
||||
|
||||
// ── Generic reactive store backed by localStorage ─────────────────────────────
|
||||
|
||||
type RecordStore<T> = Record<string, T>
|
||||
|
||||
type StoreActions<T> = {
|
||||
get(id: string): T | undefined
|
||||
upsert(id: string, value: T): void
|
||||
patch(id: string, partial: Partial<T>): void
|
||||
remove(id: string): void
|
||||
values(): T[]
|
||||
}
|
||||
|
||||
function makeLocalStore<T>(lsKey: string): [RecordStore<T>, StoreActions<T>] {
|
||||
const [store, setStore] = createStore<RecordStore<T>>(getJson(lsKey) ?? {})
|
||||
|
||||
const persist = () => setJson(lsKey, store)
|
||||
|
||||
return [store, {
|
||||
get: (id) => store[id],
|
||||
upsert(id, value) {
|
||||
setStore(produce(s => { s[id] = value }))
|
||||
persist()
|
||||
},
|
||||
patch(id, partial) {
|
||||
setStore(produce(s => {
|
||||
if (s[id]) { Object.assign(s[id] as object, partial) }
|
||||
}))
|
||||
persist()
|
||||
},
|
||||
remove(id) {
|
||||
setStore(produce(s => { delete s[id] }))
|
||||
persist()
|
||||
},
|
||||
values: () => Object.values(store),
|
||||
}]
|
||||
}
|
||||
|
||||
// ── Per-model stores ──────────────────────────────────────────────────────────
|
||||
|
||||
export const [quora, quorumStore] = makeLocalStore<QuorumRecord>("nq:quora")
|
||||
export const [shards, shardStore] = makeLocalStore<ShardRecord>("nq:shards")
|
||||
export const [dkgSessions, dkgStore] = makeLocalStore<DkgSession>("nq:dkg")
|
||||
export const [resharingSessions, resharingStore] = makeLocalStore<ResharingSession>("nq:resharing")
|
||||
export const [signingSessions, signingStore] = makeLocalStore<SigningSession>("nq:signing")
|
||||
|
||||
// ── Shard encryption helpers ──────────────────────────────────────────────────
|
||||
// ── Shard / polynomial encryption ────────────────────────────────────────────
|
||||
// Shards and polynomials are self-encrypted: NIP-44 encrypt(myPubkey, ...).
|
||||
// The signer derives the conversation key from (myPrivkey, myPubkey), which is
|
||||
// unique to the key and opaque to any other party.
|
||||
@@ -3,14 +3,25 @@ import { EventStore } from "applesauce-core"
|
||||
import type { NostrEvent } from "applesauce-core/helpers/event"
|
||||
import type { ISigner } from "applesauce-signers"
|
||||
import { parseJson } from "@welshman/lib"
|
||||
import { storeEvent, getAllEvents, demoteOldEvents } from "./storage"
|
||||
|
||||
export const eventStore = new EventStore()
|
||||
|
||||
// Rumors are unsigned inner events — disable sig verification so they store cleanly.
|
||||
eventStore.verifyEvent = undefined
|
||||
|
||||
// On startup: run demotion pass then load all stored protocol events into memory.
|
||||
;(async () => {
|
||||
await demoteOldEvents()
|
||||
const events = await getAllEvents()
|
||||
for (const event of events) {
|
||||
eventStore.add(event)
|
||||
}
|
||||
})()
|
||||
|
||||
export function addEvent(event: NostrEvent): void {
|
||||
eventStore.add(event)
|
||||
storeEvent(event)
|
||||
}
|
||||
|
||||
// Shape of the object returned by Observable.subscribe()
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { NostrEvent } from "applesauce-core/helpers/event"
|
||||
|
||||
export interface EventAdapter {
|
||||
put(event: NostrEvent): Promise<void>
|
||||
get(id: string): Promise<NostrEvent | undefined>
|
||||
getAll(): Promise<NostrEvent[]>
|
||||
getByKind(kind: number): Promise<NostrEvent[]>
|
||||
getByTag(name: string, value: string): Promise<NostrEvent[]>
|
||||
delete(id: string): Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { openDB } from "idb"
|
||||
import type { IDBPDatabase, DBSchema } from "idb"
|
||||
import type { NostrEvent } from "applesauce-core/helpers/event"
|
||||
import type { EventAdapter } from "../adapter"
|
||||
|
||||
interface NQDBSchema extends DBSchema {
|
||||
events: {
|
||||
key: string
|
||||
value: NostrEvent
|
||||
indexes: {
|
||||
"by-kind": number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let dbPromise: Promise<IDBPDatabase<NQDBSchema>> | null = null
|
||||
|
||||
function getDB(): Promise<IDBPDatabase<NQDBSchema>> {
|
||||
if (!dbPromise) {
|
||||
dbPromise = openDB<NQDBSchema>("nq", 1, {
|
||||
upgrade(db) {
|
||||
const store = db.createObjectStore("events", { keyPath: "id" })
|
||||
store.createIndex("by-kind", "kind")
|
||||
},
|
||||
})
|
||||
}
|
||||
return dbPromise
|
||||
}
|
||||
|
||||
export const indexedDBAdapter: EventAdapter = {
|
||||
async put(event) {
|
||||
const db = await getDB()
|
||||
await db.put("events", event)
|
||||
},
|
||||
|
||||
async get(id) {
|
||||
const db = await getDB()
|
||||
return db.get("events", id)
|
||||
},
|
||||
|
||||
async getAll() {
|
||||
const db = await getDB()
|
||||
return db.getAll("events")
|
||||
},
|
||||
|
||||
async getByKind(kind) {
|
||||
const db = await getDB()
|
||||
return db.getAllFromIndex("events", "by-kind", kind)
|
||||
},
|
||||
|
||||
async getByTag(name, value) {
|
||||
const db = await getDB()
|
||||
const all = await db.getAll("events")
|
||||
return all.filter(ev => ev.tags.some(t => t[0] === name && t[1] === value))
|
||||
},
|
||||
|
||||
async delete(id) {
|
||||
const db = await getDB()
|
||||
await db.delete("events", id)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { NostrEvent } from "applesauce-core/helpers/event"
|
||||
import type { EventAdapter } from "../adapter"
|
||||
|
||||
const PREFIX_EV = "nq:ev:"
|
||||
const PREFIX_KI = "nq:ki:"
|
||||
|
||||
function readEvent(id: string): NostrEvent | undefined {
|
||||
const raw = localStorage.getItem(PREFIX_EV + id)
|
||||
return raw ? JSON.parse(raw) as NostrEvent : undefined
|
||||
}
|
||||
|
||||
function addToKindIndex(kind: number, id: string): void {
|
||||
const key = PREFIX_KI + kind
|
||||
const ids: string[] = JSON.parse(localStorage.getItem(key) ?? "[]")
|
||||
if (!ids.includes(id)) {
|
||||
ids.push(id)
|
||||
localStorage.setItem(key, JSON.stringify(ids))
|
||||
}
|
||||
}
|
||||
|
||||
function removeFromKindIndex(kind: number, id: string): void {
|
||||
const key = PREFIX_KI + kind
|
||||
const ids: string[] = JSON.parse(localStorage.getItem(key) ?? "[]")
|
||||
const filtered = ids.filter(x => x !== id)
|
||||
localStorage.setItem(key, JSON.stringify(filtered))
|
||||
}
|
||||
|
||||
export const localStorageAdapter: EventAdapter = {
|
||||
async put(event) {
|
||||
const existing = readEvent(event.id)
|
||||
if (!existing) {
|
||||
addToKindIndex(event.kind, event.id)
|
||||
}
|
||||
localStorage.setItem(PREFIX_EV + event.id, JSON.stringify(event))
|
||||
},
|
||||
|
||||
async get(id) {
|
||||
return readEvent(id)
|
||||
},
|
||||
|
||||
async getAll() {
|
||||
const events: NostrEvent[] = []
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i)
|
||||
if (key?.startsWith(PREFIX_EV)) {
|
||||
const raw = localStorage.getItem(key)
|
||||
if (raw) {
|
||||
events.push(JSON.parse(raw) as NostrEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
return events
|
||||
},
|
||||
|
||||
async getByKind(kind) {
|
||||
const ids: string[] = JSON.parse(localStorage.getItem(PREFIX_KI + kind) ?? "[]")
|
||||
return ids.map(id => readEvent(id)).filter(Boolean) as NostrEvent[]
|
||||
},
|
||||
|
||||
async getByTag(name, value) {
|
||||
const all = await this.getAll()
|
||||
return all.filter(ev => ev.tags.some(t => t[0] === name && t[1] === value))
|
||||
},
|
||||
|
||||
async delete(id) {
|
||||
const event = readEvent(id)
|
||||
if (event) {
|
||||
removeFromKindIndex(event.kind, id)
|
||||
localStorage.removeItem(PREFIX_EV + id)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { NostrEvent } from "applesauce-core/helpers/event"
|
||||
import { now } from "@welshman/lib"
|
||||
import { localStorageAdapter } from "./adapters/localstorage"
|
||||
import { indexedDBAdapter } from "./adapters/indexeddb"
|
||||
|
||||
export type { EventAdapter } from "./adapter"
|
||||
|
||||
// Only protocol inner events are worth persisting — all other kinds (profiles,
|
||||
// relay lists, etc.) can be re-fetched from relays on demand.
|
||||
const PROTOCOL_KIND_MIN = 7050
|
||||
const PROTOCOL_KIND_MAX = 7061
|
||||
|
||||
// Signing-related kinds are demoted to localStorage after 30 days.
|
||||
// All other protocol events stay in IndexedDB indefinitely.
|
||||
const SIGNING_KINDS = new Set([7058, 7059, 7060])
|
||||
const THIRTY_DAYS_S = 30 * 24 * 60 * 60
|
||||
|
||||
function isOld(event: NostrEvent): boolean {
|
||||
return now() - event.created_at > THIRTY_DAYS_S
|
||||
}
|
||||
|
||||
function isLowPriority(event: NostrEvent): boolean {
|
||||
return SIGNING_KINDS.has(event.kind) && isOld(event)
|
||||
}
|
||||
|
||||
function isHighPriority(event: NostrEvent): boolean {
|
||||
return event.kind >= PROTOCOL_KIND_MIN && event.kind <= PROTOCOL_KIND_MAX
|
||||
}
|
||||
|
||||
export async function storeEvent(event: NostrEvent): Promise<void> {
|
||||
if (isLowPriority(event)) {
|
||||
await localStorageAdapter.put(event)
|
||||
} else if (isHighPriority(event)) {
|
||||
await indexedDBAdapter.put(event)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEvent(id: string): Promise<NostrEvent | undefined> {
|
||||
return (await indexedDBAdapter.get(id)) ?? (await localStorageAdapter.get(id))
|
||||
}
|
||||
|
||||
export async function getEventsByKind(kind: number): Promise<NostrEvent[]> {
|
||||
const high = await indexedDBAdapter.getByKind(kind)
|
||||
if (SIGNING_KINDS.has(kind)) {
|
||||
const low = await localStorageAdapter.getByKind(kind)
|
||||
return [...high, ...low]
|
||||
}
|
||||
return high
|
||||
}
|
||||
|
||||
export async function getEventsByTag(name: string, value: string): Promise<NostrEvent[]> {
|
||||
const [high, low] = await Promise.all([
|
||||
indexedDBAdapter.getByTag(name, value),
|
||||
localStorageAdapter.getByTag(name, value),
|
||||
])
|
||||
return [...high, ...low]
|
||||
}
|
||||
|
||||
export async function deleteEvent(id: string): Promise<void> {
|
||||
await Promise.all([
|
||||
indexedDBAdapter.delete(id),
|
||||
localStorageAdapter.delete(id),
|
||||
])
|
||||
}
|
||||
|
||||
export async function getAllEvents(): Promise<NostrEvent[]> {
|
||||
const [high, low] = await Promise.all([
|
||||
indexedDBAdapter.getAll(),
|
||||
localStorageAdapter.getAll(),
|
||||
])
|
||||
return [...high, ...low]
|
||||
}
|
||||
|
||||
/** Move signing events older than 30 days from IndexedDB to localStorage. Call at startup. */
|
||||
export async function demoteOldEvents(): Promise<void> {
|
||||
for (const kind of SIGNING_KINDS) {
|
||||
const events = await indexedDBAdapter.getByKind(kind)
|
||||
for (const event of events) {
|
||||
if (isOld(event)) {
|
||||
await localStorageAdapter.put(event)
|
||||
await indexedDBAdapter.delete(event.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user