experiments in a post-browser web
at main 182 lines 4.4 kB view raw
1/** 2 * In-Memory Storage Adapter 3 * 4 * Map-based storage for fast unit tests. Implements the full StorageAdapter interface. 5 * No external dependencies. 6 */ 7 8export function createMemoryAdapter() { 9 let items = new Map(); 10 let tags = new Map(); 11 let itemTags = []; // { itemId, tagId, createdAt } 12 let settings = new Map(); 13 14 return { 15 // ==================== Lifecycle ==================== 16 17 async open() { 18 items = new Map(); 19 tags = new Map(); 20 itemTags = []; 21 settings = new Map(); 22 }, 23 24 async close() { 25 items.clear(); 26 tags.clear(); 27 itemTags = []; 28 settings.clear(); 29 }, 30 31 // ==================== Items ==================== 32 33 async getItem(id) { 34 const item = items.get(id); 35 if (!item || item.deletedAt) return null; 36 return { ...item }; 37 }, 38 39 async getItems(filter = {}) { 40 let results = [...items.values()]; 41 if (!filter.includeDeleted) { 42 results = results.filter(i => !i.deletedAt); 43 } 44 if (filter.type) { 45 results = results.filter(i => i.type === filter.type); 46 } 47 if (filter.since) { 48 results = results.filter(i => i.updatedAt > filter.since); 49 } 50 return results.map(i => ({ ...i })); 51 }, 52 53 async insertItem(item) { 54 items.set(item.id, { ...item }); 55 }, 56 57 async updateItem(id, fields) { 58 const item = items.get(id); 59 if (!item) return; 60 for (const [key, value] of Object.entries(fields)) { 61 if (value !== undefined) { 62 item[key] = value; 63 } 64 } 65 }, 66 67 async deleteItem(id) { 68 const item = items.get(id); 69 if (!item || item.deletedAt) return; 70 const timestamp = Date.now(); 71 item.deletedAt = timestamp; 72 item.updatedAt = timestamp; 73 }, 74 75 async hardDeleteItem(id) { 76 items.delete(id); 77 itemTags = itemTags.filter(link => link.itemId !== id); 78 }, 79 80 // ==================== Tags ==================== 81 82 async getTag(id) { 83 const tag = tags.get(id); 84 return tag ? { ...tag } : null; 85 }, 86 87 async getTagByName(name) { 88 const lower = name.toLowerCase(); 89 for (const tag of tags.values()) { 90 if (tag.name.toLowerCase() === lower) { 91 return { ...tag }; 92 } 93 } 94 return null; 95 }, 96 97 async insertTag(tag) { 98 tags.set(tag.id, { ...tag }); 99 }, 100 101 async updateTag(id, fields) { 102 const tag = tags.get(id); 103 if (!tag) return; 104 for (const [key, value] of Object.entries(fields)) { 105 if (value !== undefined) { 106 tag[key] = value; 107 } 108 } 109 }, 110 111 // ==================== Item-Tags ==================== 112 113 async getItemTags(itemId) { 114 const tagIds = itemTags 115 .filter(l => l.itemId === itemId) 116 .map(l => l.tagId); 117 return tagIds 118 .map(id => tags.get(id)) 119 .filter(Boolean) 120 .map(t => ({ ...t })); 121 }, 122 123 async getItemsByTag(tagId) { 124 const itemIds = itemTags 125 .filter(l => l.tagId === tagId) 126 .map(l => l.itemId); 127 return itemIds 128 .map(id => items.get(id)) 129 .filter(i => i && !i.deletedAt) 130 .map(i => ({ ...i })); 131 }, 132 133 async tagItem(itemId, tagId) { 134 const exists = itemTags.some( 135 l => l.itemId === itemId && l.tagId === tagId 136 ); 137 if (!exists) { 138 itemTags.push({ itemId, tagId, createdAt: Date.now() }); 139 } 140 }, 141 142 async untagItem(itemId, tagId) { 143 itemTags = itemTags.filter( 144 l => !(l.itemId === itemId && l.tagId === tagId) 145 ); 146 }, 147 148 async clearItemTags(itemId) { 149 itemTags = itemTags.filter(l => l.itemId !== itemId); 150 }, 151 152 // ==================== Settings ==================== 153 154 async getSetting(key) { 155 return settings.get(key) ?? null; 156 }, 157 158 async setSetting(key, value) { 159 settings.set(key, value); 160 }, 161 162 // ==================== Query Helpers ==================== 163 164 async findItemBySyncId(syncId) { 165 // Check by direct ID first (device re-pushes with server-assigned ID) 166 const byId = items.get(syncId); 167 if (byId) return { ...byId }; 168 169 // Check by syncId field 170 for (const item of items.values()) { 171 if (item.syncId === syncId) { 172 return { ...item }; 173 } 174 } 175 return null; 176 }, 177 178 async getAllTags() { 179 return [...tags.values()].map(t => ({ ...t })); 180 }, 181 }; 182}