function createUnfiledCaptures({ storage, attachmentStore = null, getCaptureLogin = () => '', getCurrentLogin = () => '', createId = () => globalThis.crypto?.randomUUID?.() || String(Date.now()) + '-' + Math.random().toString(16).slice(2), now = () => Date.now(), maxItems = 20, }) { const storageKey = 'stackchain.unfiled-issues.v1'; function read() { try { const record = JSON.parse(storage?.getItem(storageKey) || 'null'); if (record?.version !== 1 || !Array.isArray(record.items)) return []; return record.items.filter(item => item && typeof item.id === 'string' && typeof item.ownerLogin === 'string' && typeof item.title === 'string' && item.title.trim() && typeof item.body === 'string' ); } catch (_error) { return []; } } function write(items) { storage?.setItem(storageKey, JSON.stringify({version:1, items})); } function list() { const currentLogin = String(getCurrentLogin() || '').trim(); return read().slice().sort((left, right) => Number(right.savedAt) - Number(left.savedAt)) .map(item => ({...item, quarantined: !currentLogin || currentLogin !== item.ownerLogin})); } function capacity() { const items = read().slice().sort((left, right) => Number(right.savedAt) - Number(left.savedAt)); return {full:items.length >= maxItems, count:items.length, maxItems, oldest:items.at(-1) || null}; } function prepare(note) { const title = String(note?.title || '').trim().slice(0, 255); const body = String(note?.body || '').trim().slice(0, 10000); if (!title) throw new Error('Add a title before saving.'); const ownerLogin = String(getCaptureLogin() || '').trim(); if (!ownerLogin) throw new Error('Offline identity is unavailable. Reconnect once before saving private work.'); const attachment = note?.attachment; const hasAttachment = Boolean(attachment?.blob && attachment?.filename && ['image/png', 'image/jpeg', 'image/webp'].includes(String(attachment?.contentType || ''))); if (attachment && !hasAttachment) throw new Error('The screenshot is unavailable. Choose it again before saving.'); if (hasAttachment && !attachmentStore) { throw new Error('Screenshot storage is unavailable. Your capture is still open; retry after reloading.'); } return {title, body, ownerLogin, attachment, hasAttachment}; } function persist(prepared, existing, removed = null) { const item = { id:String(createId()), ownerLogin:prepared.ownerLogin, title:prepared.title, body:prepared.body, savedAt:Number(now()), ...(prepared.hasAttachment ? {hasAttachment:true} : {}), }; const items = [item, ...existing.filter(candidate => candidate.id !== item.id)]; const writeItems = () => { try { write(items); } catch (error) { if (prepared.hasAttachment) Promise.resolve(attachmentStore.delete(item.id)).catch(() => {}); throw error; } return item; }; const stage = prepared.hasAttachment ? Promise.resolve(attachmentStore.put(item.id, { filename:String(prepared.attachment.filename).slice(0, 255), contentType:String(prepared.attachment.contentType), blob:prepared.attachment.blob, })) : null; if (!stage && !removed?.hasAttachment) return writeItems(); return Promise.resolve(stage).then(async () => { if (!removed?.hasAttachment) return writeItems(); const removedAttachment = await attachmentStore.get(removed.id); try { await attachmentStore.delete(removed.id); } catch (error) { if (prepared.hasAttachment) await Promise.resolve(attachmentStore.delete(item.id)).catch(() => {}); throw error; } try { return writeItems(); } catch (error) { if (removedAttachment) await Promise.resolve(attachmentStore.put(removed.id, removedAttachment)).catch(() => {}); throw error; } }); } function save(note) { const prepared = prepare(note); const existing = read(); if (existing.length >= maxItems) throw new Error('Drafts full — nothing was deleted.'); return persist(prepared, existing); } function replaceOldest(note, expectedOldestId) { const prepared = prepare(note); const existing = read().slice().sort((left, right) => Number(right.savedAt) - Number(left.savedAt)); const oldest = existing.at(-1); if (existing.length < maxItems || !oldest || oldest.id !== expectedOldestId) { throw new Error('Drafts changed. Review them before replacing anything.'); } return persist(prepared, existing.filter(item => item.id !== oldest.id), oldest); } function discard(id) { const items = read(); const remaining = items.filter(item => item.id !== id); if (remaining.length === items.length) return false; const removed = items.find(item => item.id === id); if (!removed?.hasAttachment) { write(remaining); return true; } return Promise.resolve(attachmentStore?.get(id)).then(removedAttachment => Promise.resolve(attachmentStore?.delete(id)).then(async () => { try { write(remaining); } catch (error) { if (removedAttachment) await Promise.resolve(attachmentStore?.put(id, removedAttachment)).catch(() => {}); throw error; } return true; }) ); } function resume(id, confirmedLogin) { const item = read().find(candidate => candidate.id === id); if (!item) throw new Error('This capture is no longer available.'); if (!confirmedLogin || String(confirmedLogin).trim() !== item.ownerLogin) { throw new Error('Reconnect with the account that saved this capture.'); } const draft = {repository:'', title:item.title, body:item.body, labelIds:[]}; if (!item.hasAttachment) return draft; if (!attachmentStore) throw new Error('The saved screenshot is unavailable. Retry after reloading.'); return Promise.resolve(attachmentStore.get(id)).then(attachment => { if (!attachment?.blob || !attachment?.filename || !attachment?.contentType) { throw new Error('The saved screenshot is unavailable. Keep this Draft and retry.'); } return {...draft, attachment}; }); } function completeResume(id) { return discard(id); } return {list, capacity, save, replaceOldest, discard, resume, completeResume}; } if (typeof module !== 'undefined' && module.exports) module.exports = createUnfiledCaptures;