A calm place to write long-form, and publish it to the open social web.
skypress.blog/
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 */
7export 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. */
14export 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
29const DEFAULT_DB = 'skypress-write';
30const DEFAULT_STORE = 'assets';
31
32function 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 */
49export 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}