Consolidate IndexedDB into a single 'ditto' database with multiple stores
Replace three separate per-hostname databases (nostr-nip05-cache-*, nostr-profile-cache-*, nostr-dm-store-*) with one shared 'ditto' database containing three object stores: nip05, profiles, messages. A single db.ts module manages the connection and upgrade logic, and caches the promise so only one connection is created per page lifetime.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { openDB, type IDBPDatabase } from 'idb';
|
||||
|
||||
// ============================================================================
|
||||
// Unified IndexedDB database for Ditto.
|
||||
//
|
||||
// All persistent client-side data lives in a single "ditto" database with
|
||||
// one object store per data domain. Callers should import `openDatabase()`
|
||||
// rather than managing their own `openDB` calls.
|
||||
// ============================================================================
|
||||
|
||||
const DB_NAME = 'ditto';
|
||||
const DB_VERSION = 1;
|
||||
|
||||
/** Store names — keep in sync with the `upgrade` callback below. */
|
||||
export const STORE = {
|
||||
NIP05: 'nip05',
|
||||
PROFILES: 'profiles',
|
||||
MESSAGES: 'messages',
|
||||
} as const;
|
||||
|
||||
let dbPromise: Promise<IDBPDatabase> | null = null;
|
||||
|
||||
/**
|
||||
* Open (or reuse) the shared Ditto database.
|
||||
*
|
||||
* The returned promise is cached so only one connection is created per
|
||||
* page lifetime, regardless of how many callers import this function.
|
||||
*/
|
||||
export function openDatabase(): Promise<IDBPDatabase> {
|
||||
if (!dbPromise) {
|
||||
dbPromise = openDB(DB_NAME, DB_VERSION, {
|
||||
upgrade(db) {
|
||||
if (!db.objectStoreNames.contains(STORE.NIP05)) {
|
||||
db.createObjectStore(STORE.NIP05);
|
||||
}
|
||||
if (!db.objectStoreNames.contains(STORE.PROFILES)) {
|
||||
db.createObjectStore(STORE.PROFILES);
|
||||
}
|
||||
if (!db.objectStoreNames.contains(STORE.MESSAGES)) {
|
||||
db.createObjectStore(STORE.MESSAGES);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
return dbPromise;
|
||||
}
|
||||
@@ -1,19 +1,10 @@
|
||||
import { openDB, type IDBPDatabase } from 'idb';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
|
||||
// ============================================================================
|
||||
// IndexedDB Schema
|
||||
// ============================================================================
|
||||
import { openDatabase, STORE } from '@/lib/db';
|
||||
|
||||
// Use domain-based naming to avoid conflicts between apps on same domain
|
||||
const getDBName = () => {
|
||||
// Use hostname for unique DB per app (e.g., 'nostr-dm-store-localhost', 'nostr-dm-store-myapp.com')
|
||||
const hostname = typeof window !== 'undefined' ? window.location.hostname : 'default';
|
||||
return `nostr-dm-store-${hostname}`;
|
||||
};
|
||||
const DB_NAME = getDBName();
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = 'messages';
|
||||
// ============================================================================
|
||||
// DM Message IndexedDB Store
|
||||
// ============================================================================
|
||||
|
||||
interface StoredParticipant {
|
||||
messages: NostrEvent[];
|
||||
@@ -34,20 +25,6 @@ export interface MessageStore {
|
||||
// Database Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Open the IndexedDB database
|
||||
*/
|
||||
async function openDatabase(): Promise<IDBPDatabase> {
|
||||
return openDB(DB_NAME, DB_VERSION, {
|
||||
upgrade(db) {
|
||||
// Create the messages store if it doesn't exist
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Write messages to IndexedDB for a specific user
|
||||
* Messages are stored in their original encrypted form (kind 4 or kind 13)
|
||||
@@ -61,9 +38,9 @@ export async function writeMessagesToDB(
|
||||
|
||||
// Store messages in their original encrypted form (no NIP-44 wrapper needed)
|
||||
// Each message content is already encrypted by the sender
|
||||
await db.put(STORE_NAME, messageStore, userPubkey);
|
||||
await db.put(STORE.MESSAGES, messageStore, userPubkey);
|
||||
} catch (error) {
|
||||
console.error('[MessageStore] ❌ Error writing to IndexedDB:', error);
|
||||
console.error('[MessageStore] Error writing to IndexedDB:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -77,7 +54,7 @@ export async function readMessagesFromDB(
|
||||
): Promise<MessageStore | undefined> {
|
||||
try {
|
||||
const db = await openDatabase();
|
||||
const data = await db.get(STORE_NAME, userPubkey);
|
||||
const data = await db.get(STORE.MESSAGES, userPubkey);
|
||||
|
||||
if (!data) {
|
||||
return undefined;
|
||||
@@ -96,7 +73,7 @@ export async function readMessagesFromDB(
|
||||
export async function deleteMessagesFromDB(userPubkey: string): Promise<void> {
|
||||
try {
|
||||
const db = await openDatabase();
|
||||
await db.delete(STORE_NAME, userPubkey);
|
||||
await db.delete(STORE.MESSAGES, userPubkey);
|
||||
} catch (error) {
|
||||
console.error('[MessageStore] Error deleting from IndexedDB:', error);
|
||||
throw error;
|
||||
@@ -109,7 +86,7 @@ export async function deleteMessagesFromDB(userPubkey: string): Promise<void> {
|
||||
export async function clearAllMessages(): Promise<void> {
|
||||
try {
|
||||
const db = await openDatabase();
|
||||
await db.clear(STORE_NAME);
|
||||
await db.clear(STORE.MESSAGES);
|
||||
} catch (error) {
|
||||
console.error('[MessageStore] Error clearing IndexedDB:', error);
|
||||
throw error;
|
||||
|
||||
+5
-28
@@ -1,4 +1,4 @@
|
||||
import { openDB, type IDBPDatabase } from 'idb';
|
||||
import { openDatabase, STORE } from '@/lib/db';
|
||||
|
||||
// ============================================================================
|
||||
// NIP-05 IndexedDB Cache
|
||||
@@ -9,15 +9,6 @@ import { openDB, type IDBPDatabase } from 'idb';
|
||||
// to re-check.
|
||||
// ============================================================================
|
||||
|
||||
const getDBName = () => {
|
||||
const hostname = typeof window !== 'undefined' ? window.location.hostname : 'default';
|
||||
return `nostr-nip05-cache-${hostname}`;
|
||||
};
|
||||
|
||||
const DB_NAME = getDBName();
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = 'nip05';
|
||||
|
||||
export interface Nip05CacheEntry {
|
||||
/** The NIP-05 identifier (e.g. "user@domain.com") */
|
||||
identifier: string;
|
||||
@@ -44,7 +35,7 @@ export function hydrateNip05Cache(): Promise<void> {
|
||||
hydratePromise = (async () => {
|
||||
try {
|
||||
const db = await openDatabase();
|
||||
const entries: Nip05CacheEntry[] = await db.getAll(STORE_NAME);
|
||||
const entries: Nip05CacheEntry[] = await db.getAll(STORE.NIP05);
|
||||
for (const entry of entries) {
|
||||
memoryCache.set(entry.identifier, entry);
|
||||
}
|
||||
@@ -78,7 +69,7 @@ export async function setNip05Cached(identifier: string, pubkey: string): Promis
|
||||
|
||||
try {
|
||||
const db = await openDatabase();
|
||||
await db.put(STORE_NAME, entry, identifier);
|
||||
await db.put(STORE.NIP05, entry, identifier);
|
||||
} catch {
|
||||
// Write failure is non-critical — the in-memory cache still works.
|
||||
}
|
||||
@@ -93,7 +84,7 @@ export async function deleteNip05Cached(identifier: string): Promise<void> {
|
||||
|
||||
try {
|
||||
const db = await openDatabase();
|
||||
await db.delete(STORE_NAME, identifier);
|
||||
await db.delete(STORE.NIP05, identifier);
|
||||
} catch {
|
||||
// Non-critical.
|
||||
}
|
||||
@@ -105,22 +96,8 @@ export async function clearNip05Cache(): Promise<void> {
|
||||
|
||||
try {
|
||||
const db = await openDatabase();
|
||||
await db.clear(STORE_NAME);
|
||||
await db.clear(STORE.NIP05);
|
||||
} catch {
|
||||
// Non-critical.
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function openDatabase(): Promise<IDBPDatabase> {
|
||||
return openDB(DB_NAME, DB_VERSION, {
|
||||
upgrade(db) {
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+6
-28
@@ -1,6 +1,7 @@
|
||||
import { openDB, type IDBPDatabase } from 'idb';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
|
||||
import { openDatabase, STORE } from '@/lib/db';
|
||||
|
||||
// ============================================================================
|
||||
// Kind 0 Profile IndexedDB Cache
|
||||
//
|
||||
@@ -10,15 +11,6 @@ import type { NostrEvent } from '@nostrify/nostrify';
|
||||
// caller can decide when to re-check.
|
||||
// ============================================================================
|
||||
|
||||
const getDBName = () => {
|
||||
const hostname = typeof window !== 'undefined' ? window.location.hostname : 'default';
|
||||
return `nostr-profile-cache-${hostname}`;
|
||||
};
|
||||
|
||||
const DB_NAME = getDBName();
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = 'profiles';
|
||||
|
||||
export interface ProfileCacheEntry {
|
||||
/** Hex pubkey of the profile author */
|
||||
pubkey: string;
|
||||
@@ -45,7 +37,7 @@ export function hydrateProfileCache(): Promise<void> {
|
||||
hydratePromise = (async () => {
|
||||
try {
|
||||
const db = await openDatabase();
|
||||
const entries: ProfileCacheEntry[] = await db.getAll(STORE_NAME);
|
||||
const entries: ProfileCacheEntry[] = await db.getAll(STORE.PROFILES);
|
||||
for (const entry of entries) {
|
||||
memoryCache.set(entry.pubkey, entry);
|
||||
}
|
||||
@@ -86,7 +78,7 @@ export async function setProfileCached(event: NostrEvent): Promise<void> {
|
||||
|
||||
try {
|
||||
const db = await openDatabase();
|
||||
await db.put(STORE_NAME, entry, event.pubkey);
|
||||
await db.put(STORE.PROFILES, entry, event.pubkey);
|
||||
} catch {
|
||||
// Write failure is non-critical — the in-memory cache still works.
|
||||
}
|
||||
@@ -98,7 +90,7 @@ export async function deleteProfileCached(pubkey: string): Promise<void> {
|
||||
|
||||
try {
|
||||
const db = await openDatabase();
|
||||
await db.delete(STORE_NAME, pubkey);
|
||||
await db.delete(STORE.PROFILES, pubkey);
|
||||
} catch {
|
||||
// Non-critical.
|
||||
}
|
||||
@@ -110,22 +102,8 @@ export async function clearProfileCache(): Promise<void> {
|
||||
|
||||
try {
|
||||
const db = await openDatabase();
|
||||
await db.clear(STORE_NAME);
|
||||
await db.clear(STORE.PROFILES);
|
||||
} catch {
|
||||
// Non-critical.
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function openDatabase(): Promise<IDBPDatabase> {
|
||||
return openDB(DB_NAME, DB_VERSION, {
|
||||
upgrade(db) {
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user