fix: move offline screenshots out of localStorage (Closes #473)
This commit is contained in:
1 parent
2b2e2cceb4
commit
8b7c56161c
5 files changed
+208
-19
No files matched your search
@@ -354,6 +354,35 @@ const transaction=work=>{{const run=tail.then(()=>work({{
|
||||
]
|
||||
|
||||
|
||||
def test_reconciling_a_lightweight_attachment_reference_preserves_indexeddb_bytes():
|
||||
script = f"""
|
||||
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
|
||||
const records=new Map();let tail=Promise.resolve();
|
||||
const transaction=work=>{{const run=tail.then(()=>work({{
|
||||
getAll:async()=>[...records.values()].map(value=>({{...value}})),
|
||||
put:async value=>records.set(value.id,{{...value}}),delete:async id=>records.delete(id),
|
||||
}}));tail=run.catch(()=>{{}});return run;}};
|
||||
(async()=>{{
|
||||
const store=createBackgroundIssueSync.createIssueSyncStore({{transaction}});
|
||||
await store.reconcile([{{
|
||||
id:'first',operationId:'first',ownerLogin:'timmy',status:'queued',
|
||||
attachment:{{filename:'one.png',contentType:'image/png',data:'first-image-bytes'}},
|
||||
}}]);
|
||||
await store.reconcile([
|
||||
{{id:'first',operationId:'first',ownerLogin:'timmy',status:'queued',
|
||||
attachment:{{filename:'one.png',contentType:'image/png',stored:true}}}},
|
||||
{{id:'second',operationId:'second',ownerLogin:'timmy',status:'queued',
|
||||
attachment:{{filename:'two.png',contentType:'image/png',data:'second-image-bytes'}}}},
|
||||
]);
|
||||
process.stdout.write(JSON.stringify(await store.snapshot()));
|
||||
}})();
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output[0]["attachment"]["data"] == "first-image-bytes"
|
||||
assert output[1]["attachment"]["data"] == "second-image-bytes"
|
||||
|
||||
|
||||
def test_stale_foreground_upsert_preserves_confirmed_attachment_delivery_stages():
|
||||
script = f"""
|
||||
const createBackgroundIssueSync=require({json.dumps(str(SYNC))});
|
||||
|
||||
@@ -403,6 +403,117 @@ Promise.resolve().then(async () => {{
|
||||
assert output["items"][0]["title"] == "Keep this"
|
||||
|
||||
|
||||
def test_durable_screenshot_is_mirrored_before_a_payload_free_local_index_is_committed():
|
||||
script = f"""
|
||||
const createIssueOutbox = require({json.dumps(str(OUTBOX))});
|
||||
const values = new Map(); const events = []; const snapshots = [];
|
||||
const storage = {{
|
||||
getItem:key => values.get(key) || null,
|
||||
setItem:(key,value) => {{
|
||||
if (value.includes('base64-screenshot-bytes')) throw new Error('screenshot leaked into localStorage');
|
||||
events.push('local-index'); values.set(key,value);
|
||||
}},
|
||||
}};
|
||||
const outbox = createIssueOutbox({{
|
||||
storage, getOwnerLogin:()=> 'timmy', createOperationId:()=> 'quota-safe-1',
|
||||
backgroundSync: {{
|
||||
reconcile: async items => {{ events.push('indexeddb'); snapshots.push(items); }},
|
||||
requestSync: async () => {{ events.push('sync'); }},
|
||||
}},
|
||||
}});
|
||||
(async () => {{
|
||||
const result = await outbox.enqueueDurably({{
|
||||
repository:'stackchain/dashboard', title:'Mobile layout',
|
||||
attachment:{{filename:'phone.webp',contentType:'image/webp',data:'base64-screenshot-bytes'}},
|
||||
}});
|
||||
process.stdout.write(JSON.stringify({{
|
||||
events, mirrored:snapshots[0][0], local:outbox.list()[0], result:result.item,
|
||||
raw:values.get('stackchain.issue-outbox.v1'),
|
||||
}}));
|
||||
}})();
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["events"] == ["indexeddb", "local-index", "sync"]
|
||||
assert output["mirrored"]["attachment"]["data"] == "base64-screenshot-bytes"
|
||||
assert output["local"]["attachment"] == {
|
||||
"filename": "phone.webp",
|
||||
"contentType": "image/webp",
|
||||
"stored": True,
|
||||
}
|
||||
assert "base64-screenshot-bytes" not in output["raw"]
|
||||
assert output["result"]["attachment"] == output["local"]["attachment"]
|
||||
|
||||
|
||||
def test_replacing_a_durable_screenshot_updates_indexeddb_before_the_local_reference():
|
||||
script = f"""
|
||||
const createIssueOutbox = require({json.dumps(str(OUTBOX))});
|
||||
const values = new Map(); const events = []; const snapshots = [];
|
||||
const storage = {{
|
||||
getItem:key => values.get(key) || null,
|
||||
setItem:(key,value) => {{
|
||||
if (value.includes('new-image-bytes')) throw new Error('replacement leaked into localStorage');
|
||||
events.push('local-index'); values.set(key,value);
|
||||
}},
|
||||
}};
|
||||
const backgroundSync = {{
|
||||
reconcile:async items => {{ events.push('indexeddb'); snapshots.push(items); }},
|
||||
requestSync:async () => {{ events.push('sync'); }},
|
||||
}};
|
||||
const outbox = createIssueOutbox({{
|
||||
storage, backgroundSync, getOwnerLogin:()=> 'timmy',
|
||||
createOperationId:(() => {{ let sequence=0; return () => 'replace-' + (++sequence); }})(),
|
||||
}});
|
||||
(async () => {{
|
||||
const admitted = await outbox.enqueueDurably({{repository:'o/r',title:'Visual'}});
|
||||
events.length = 0;
|
||||
const result = await outbox.updateDurably(admitted.item.id, {{
|
||||
...admitted.item,
|
||||
attachment:{{filename:'new.png',contentType:'image/png',data:'new-image-bytes'}},
|
||||
}});
|
||||
process.stdout.write(JSON.stringify({{
|
||||
events, mirrored:snapshots.at(-1)[0], local:outbox.list()[0], result:result.item,
|
||||
}}));
|
||||
}})();
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["events"] == ["indexeddb", "local-index", "sync"]
|
||||
assert output["mirrored"]["attachment"]["data"] == "new-image-bytes"
|
||||
assert output["local"]["attachment"]["stored"] is True
|
||||
assert "data" not in output["local"]["attachment"]
|
||||
assert output["result"] == output["local"]
|
||||
|
||||
|
||||
def test_next_durable_admission_migrates_legacy_screenshot_payloads_out_of_localstorage():
|
||||
script = f"""
|
||||
const createIssueOutbox = require({json.dumps(str(OUTBOX))});
|
||||
const values = new Map([['stackchain.issue-outbox.v1', JSON.stringify({{version:3,items:[{{
|
||||
id:'legacy',operationId:'legacy',repository:'o/r',title:'Queued before upgrade',body:'',
|
||||
labelIds:[],ownerLogin:'timmy',status:'queued',queuedAt:1,
|
||||
attachment:{{filename:'old.png',contentType:'image/png',data:'legacy-image-bytes'}},
|
||||
}}]}})]]);
|
||||
const mirrored = [];
|
||||
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
|
||||
const outbox = createIssueOutbox({{
|
||||
storage, getOwnerLogin:()=> 'timmy', createOperationId:()=> 'new-item',
|
||||
backgroundSync:{{reconcile:async items=>mirrored.push(items),requestSync:async()=>{{}}}},
|
||||
}});
|
||||
(async()=>{{
|
||||
await outbox.enqueueDurably({{repository:'o/r',title:'New queue item'}});
|
||||
process.stdout.write(JSON.stringify({{
|
||||
mirrored:mirrored[0],local:outbox.list(),raw:values.get('stackchain.issue-outbox.v1'),
|
||||
}}));
|
||||
}})();
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["mirrored"][0]["attachment"]["data"] == "legacy-image-bytes"
|
||||
assert output["local"][0]["attachment"]["stored"] is True
|
||||
assert "data" not in output["local"][0]["attachment"]
|
||||
assert "legacy-image-bytes" not in output["raw"]
|
||||
|
||||
|
||||
def test_issue_outbox_reports_degraded_admission_without_losing_foreground_item():
|
||||
script = f"""
|
||||
const createIssueOutbox = require({json.dumps(str(OUTBOX))});
|
||||
|
||||
Reference in new issue
Block a user