feat: make mobile issue capture idempotent (#177)
This commit is contained in:
1 parent
9293dde1af
commit
6f54d5764b
5 files changed
+316
-19
No files matched your search
@@ -1,4 +1,14 @@
|
||||
function createIssueCapture({ fetchJson, storage }) {
|
||||
function newIssueOperationId() {
|
||||
if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID();
|
||||
if (globalThis.crypto?.getRandomValues) {
|
||||
const bytes = new Uint8Array(16);
|
||||
globalThis.crypto.getRandomValues(bytes);
|
||||
return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
return String(Date.now()) + '-' + Math.random().toString(16).slice(2);
|
||||
}
|
||||
|
||||
function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOperationId }) {
|
||||
const storageKey = 'stackchain.issue-capture.v1';
|
||||
let pending = null;
|
||||
const safeLabelIds = value => Array.from(new Set(
|
||||
@@ -6,30 +16,44 @@ function createIssueCapture({ fetchJson, storage }) {
|
||||
)).slice(0, 20);
|
||||
const emptyDraft = () => ({ repository: '', title: '', body: '', labelIds: [] });
|
||||
|
||||
function loadStored() {
|
||||
try {
|
||||
const parsed = JSON.parse(storage.getItem(storageKey) || 'null');
|
||||
if (!parsed || typeof parsed !== 'object') return {...emptyDraft(), operationId: ''};
|
||||
return {
|
||||
repository: String(parsed.repository || ''),
|
||||
title: String(parsed.title || ''),
|
||||
body: String(parsed.body || ''),
|
||||
labelIds: safeLabelIds(parsed.labelIds),
|
||||
operationId: String(parsed.operationId || '').slice(0, 128),
|
||||
};
|
||||
} catch (_error) {
|
||||
return {...emptyDraft(), operationId: ''};
|
||||
}
|
||||
}
|
||||
|
||||
function writeStored(record) {
|
||||
try { storage.setItem(storageKey, JSON.stringify(record)); }
|
||||
catch (_error) { /* Keep the form as the in-memory fallback. */ }
|
||||
}
|
||||
|
||||
function saveDraft(draft) {
|
||||
const previous = loadStored();
|
||||
const safe = {
|
||||
repository: String(draft?.repository || ''),
|
||||
title: String(draft?.title || ''),
|
||||
body: String(draft?.body || ''),
|
||||
labelIds: safeLabelIds(draft?.labelIds),
|
||||
};
|
||||
try { storage.setItem(storageKey, JSON.stringify(safe)); }
|
||||
catch (_error) { /* Keep the form as the in-memory fallback. */ }
|
||||
const unchanged = ['repository', 'title', 'body'].every(key => previous[key] === safe[key]) &&
|
||||
JSON.stringify(previous.labelIds) === JSON.stringify(safe.labelIds);
|
||||
writeStored({...safe, operationId: unchanged ? previous.operationId : ''});
|
||||
return safe;
|
||||
}
|
||||
|
||||
function loadDraft() {
|
||||
try {
|
||||
const parsed = JSON.parse(storage.getItem(storageKey) || 'null');
|
||||
return parsed && typeof parsed === 'object' ? {
|
||||
repository: String(parsed.repository || ''),
|
||||
title: String(parsed.title || ''),
|
||||
body: String(parsed.body || ''),
|
||||
labelIds: safeLabelIds(parsed.labelIds),
|
||||
} : emptyDraft();
|
||||
} catch (_error) {
|
||||
return emptyDraft();
|
||||
}
|
||||
const {operationId: _operationId, ...draft} = loadStored();
|
||||
return draft;
|
||||
}
|
||||
|
||||
function clearDraft() {
|
||||
@@ -52,10 +76,16 @@ function createIssueCapture({ fetchJson, storage }) {
|
||||
function submit(draft) {
|
||||
if (pending) return pending;
|
||||
const saved = saveDraft(draft);
|
||||
const stored = loadStored();
|
||||
const operationId = stored.operationId || String(createOperationId()).slice(0, 128);
|
||||
writeStored({...saved, operationId});
|
||||
const repository = saved.repository.split('/').map(encodeURIComponent).join('/');
|
||||
pending = fetchJson('api/v1/repos/' + repository + '/issues', {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
Accept: 'application/json', 'Content-Type': 'application/json',
|
||||
'Idempotency-Key': operationId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: saved.title, body: saved.body, label_ids: saved.labelIds,
|
||||
}),
|
||||
|
||||
Reference in new issue
Block a user