A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

Add asset store (IndexedDB + in-memory) for held image bytes

+136
+20
src/lib/write/asset-store.test.ts
··· 1 + import { describe, it, expect } from 'vitest'; 2 + import { createMemoryAssetStore } from './asset-store'; 3 + 4 + describe( 'createMemoryAssetStore', () => { 5 + it( 'puts, merges, reads all, and clears', async () => { 6 + const store = createMemoryAssetStore(); 7 + await store.put( { a0: 'data:1' } ); 8 + await store.put( { a1: 'data:2' } ); 9 + expect( await store.getAll() ).toEqual( { a0: 'data:1', a1: 'data:2' } ); 10 + await store.clear(); 11 + expect( await store.getAll() ).toEqual( {} ); 12 + } ); 13 + 14 + it( 'replaces a key on re-put', async () => { 15 + const store = createMemoryAssetStore(); 16 + await store.put( { a0: 'data:1' } ); 17 + await store.put( { a0: 'data:CHANGED' } ); 18 + expect( await store.getAll() ).toEqual( { a0: 'data:CHANGED' } ); 19 + } ); 20 + } );
+116
src/lib/write/asset-store.ts
··· 1 + /** 2 + * A small async key→value store for held image bytes (data: URLs) in the writing-first flow. 3 + * Held bytes can be large, so they live here (IndexedDB) rather than in localStorage with the 4 + * draft metadata. `put` MERGES (does not replace the whole store) so adding an image keeps the 5 + * earlier ones; `clear` empties everything (called after a successful publish or a discard). 6 + */ 7 + export interface AssetStore { 8 + put( assets: Record< string, string > ): Promise< void >; 9 + getAll(): Promise< Record< string, string > >; 10 + clear(): Promise< void >; 11 + } 12 + 13 + /** In-memory store: the test/SSR/quota-failure fallback. Survives nothing past a reload. */ 14 + export function createMemoryAssetStore(): AssetStore { 15 + let map: Record< string, string > = {}; 16 + return { 17 + async put( assets ) { 18 + map = { ...map, ...assets }; 19 + }, 20 + async getAll() { 21 + return { ...map }; 22 + }, 23 + async clear() { 24 + map = {}; 25 + }, 26 + }; 27 + } 28 + 29 + const DEFAULT_DB = 'skypress-write'; 30 + const DEFAULT_STORE = 'assets'; 31 + 32 + function openDb( dbName: string, storeName: string ): Promise< IDBDatabase > { 33 + return new Promise( ( resolve, reject ) => { 34 + const req = indexedDB.open( dbName, 1 ); 35 + req.onupgradeneeded = () => { 36 + if ( ! req.result.objectStoreNames.contains( storeName ) ) { 37 + req.result.createObjectStore( storeName ); 38 + } 39 + }; 40 + req.onsuccess = () => resolve( req.result ); 41 + req.onerror = () => reject( req.error ?? new Error( 'IndexedDB open failed' ) ); 42 + } ); 43 + } 44 + 45 + /** 46 + * IndexedDB-backed asset store. Each token is one record keyed by the token string. Falls back 47 + * to an in-memory store when IndexedDB is unavailable (e.g. SSR/tests) so callers never crash. 48 + */ 49 + export function createIndexedDbAssetStore( 50 + dbName: string = DEFAULT_DB, 51 + storeName: string = DEFAULT_STORE 52 + ): AssetStore { 53 + if ( typeof indexedDB === 'undefined' ) { 54 + return createMemoryAssetStore(); 55 + } 56 + 57 + const tx = async < T >( 58 + mode: IDBTransactionMode, 59 + run: ( store: IDBObjectStore ) => IDBRequest | void, 60 + read?: ( store: IDBObjectStore ) => IDBRequest< T > 61 + ): Promise< T | void > => { 62 + const db = await openDb( dbName, storeName ); 63 + return new Promise< T | void >( ( resolve, reject ) => { 64 + const transaction = db.transaction( storeName, mode ); 65 + const store = transaction.objectStore( storeName ); 66 + let readReq: IDBRequest< T > | undefined; 67 + if ( read ) { 68 + readReq = read( store ); 69 + } else { 70 + run( store ); 71 + } 72 + transaction.oncomplete = () => { 73 + db.close(); 74 + resolve( readReq ? readReq.result : undefined ); 75 + }; 76 + transaction.onerror = () => { 77 + db.close(); 78 + reject( transaction.error ?? new Error( 'IndexedDB transaction failed' ) ); 79 + }; 80 + } ); 81 + }; 82 + 83 + return { 84 + async put( assets ) { 85 + await tx( 'readwrite', ( store ) => { 86 + for ( const [ key, value ] of Object.entries( assets ) ) { 87 + store.put( value, key ); 88 + } 89 + } ); 90 + }, 91 + async getAll() { 92 + const db = await openDb( dbName, storeName ); 93 + return new Promise< Record< string, string > >( ( resolve, reject ) => { 94 + const transaction = db.transaction( storeName, 'readonly' ); 95 + const store = transaction.objectStore( storeName ); 96 + const keysReq = store.getAllKeys(); 97 + const valsReq = store.getAll(); 98 + transaction.oncomplete = () => { 99 + db.close(); 100 + const out: Record< string, string > = {}; 101 + ( keysReq.result as IDBValidKey[] ).forEach( ( k, i ) => { 102 + out[ String( k ) ] = valsReq.result[ i ] as string; 103 + } ); 104 + resolve( out ); 105 + }; 106 + transaction.onerror = () => { 107 + db.close(); 108 + reject( transaction.error ?? new Error( 'IndexedDB read failed' ) ); 109 + }; 110 + } ); 111 + }, 112 + async clear() { 113 + await tx( 'readwrite', ( store ) => store.clear() ); 114 + }, 115 + }; 116 + }