feat: deliver authored messages after app close (#252)
This commit is contained in:
1 parent
5f6dd265a5
commit
c3e8ead47b
8 files changed
+235
-31
No files matched your search
@@ -152,6 +152,63 @@ process.stdout.write(JSON.stringify({{first,second,items:outbox.list()}}));
|
||||
assert len(output["items"]) == 1
|
||||
|
||||
|
||||
def test_authored_outbox_mirrors_to_background_sync_and_reconciles_worker_results():
|
||||
script = f"""
|
||||
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
|
||||
const values = new Map(); const mirrors=[]; let requested=0;
|
||||
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
|
||||
const backgroundSync = {{
|
||||
reconcile: async (items, lane) => mirrors.push({{items:items.map(item=>({{...item}})),lane}}),
|
||||
requestSync: async () => {{requested += 1;}},
|
||||
}};
|
||||
const outbox=createAuthoredOutbox({{storage,getOwnerLogin:()=>'timmy',backgroundSync}});
|
||||
const sent=outbox.enqueue({{kind:'issue-comment',repository:'o/r',number:1,body:'Sent',operationId:'sent'}});
|
||||
const failed=outbox.enqueue({{kind:'update-reply',notificationId:2,body:'Fix me',operationId:'failed'}});
|
||||
setTimeout(() => {{
|
||||
const reconciled=outbox.reconcileBackground([
|
||||
{{...sent,status:'sent'}},
|
||||
{{...failed,status:'attention',error:'Reply rejected'}},
|
||||
]);
|
||||
setTimeout(() => process.stdout.write(JSON.stringify({{mirrors,requested,reconciled}})), 0);
|
||||
}}, 0);
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["mirrors"][0]["lane"] == "authored"
|
||||
assert [item["operationId"] for item in output["mirrors"][-1]["items"]] == ["failed"]
|
||||
assert output["requested"] >= 1
|
||||
assert [(item["operationId"], item["status"]) for item in output["reconciled"]] == [
|
||||
("failed", "attention")
|
||||
]
|
||||
|
||||
|
||||
def test_authored_outbox_foreground_send_uses_atomic_background_delivery():
|
||||
script = f"""
|
||||
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
|
||||
const values = new Map(); let backgroundCalls=0; let directCalls=0;
|
||||
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
|
||||
const backgroundSync = {{
|
||||
reconcile: async()=>{{}}, requestSync:async()=>{{}},
|
||||
send: async (item, owner) => {{backgroundCalls += 1; return {{message:{{id:3,owner}}}};}},
|
||||
}};
|
||||
const outbox=createAuthoredOutbox({{
|
||||
storage,getOwnerLogin:()=>'timmy',backgroundSync,
|
||||
fetchJson:async()=>{{directCalls += 1; return {{id:4}};}},
|
||||
}});
|
||||
outbox.enqueue({{kind:'pull-comment',repository:'o/r',number:2,body:'Review',operationId:'once'}});
|
||||
(async()=>{{
|
||||
const result=await outbox.flush('timmy');
|
||||
process.stdout.write(JSON.stringify({{backgroundCalls,directCalls,result,remaining:outbox.list()}}));
|
||||
}})();
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output["backgroundCalls"] == 1
|
||||
assert output["directCalls"] == 0
|
||||
assert output["result"]["confirmed"] == [{"id": 3, "owner": "timmy"}]
|
||||
assert output["remaining"] == []
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_dashboard_loads_and_operates_authored_message_outbox():
|
||||
html = await dashboard()
|
||||
@@ -164,3 +221,5 @@ async def test_mobile_dashboard_loads_and_operates_authored_message_outbox():
|
||||
assert "authoredOutbox.retry(item.outbox_id, activeFlushLogin)" in html
|
||||
assert "authoredOutbox.discard(item.outbox_id)" in html
|
||||
assert "if (result?.queued)" in html
|
||||
assert "backgroundSync: backgroundIssueSync" in html
|
||||
assert "authoredOutbox.reconcileBackground(records)" in html
|
||||
@@ -64,6 +64,83 @@ const fetchJson = async (url, options = {{}}) => {{
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("item", "expected_url"),
|
||||
[
|
||||
(
|
||||
{"kind": "issue-comment", "repository": "stackchain/api", "number": 7},
|
||||
"api/v1/repos/stackchain/api/issues/7/comments",
|
||||
),
|
||||
(
|
||||
{"kind": "pull-comment", "repository": "stackchain/web", "number": 8},
|
||||
"api/v1/repos/stackchain/web/pulls/8/comments",
|
||||
),
|
||||
(
|
||||
{"kind": "update-reply", "notificationId": 9},
|
||||
"api/v1/notifications/9/reply",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_closed_app_sync_delivers_each_authored_message_kind(item, expected_url):
|
||||
authored = {
|
||||
"id": "message-op",
|
||||
"operationId": "message-op",
|
||||
"ownerLogin": "timmy",
|
||||
"status": "queued",
|
||||
"body": "Ship this reply",
|
||||
**item,
|
||||
}
|
||||
script = f"""
|
||||
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
|
||||
let queued = {json.dumps(authored)};
|
||||
const calls = [];
|
||||
const store = {{
|
||||
claimNext: async owner => queued?.ownerLogin === owner ? (queued = null, {json.dumps(authored)}) : null,
|
||||
complete: async () => {{}}, release: async () => {{}}, fail: async () => {{}},
|
||||
countBlocked: async () => 0,
|
||||
}};
|
||||
const fetchJson = async (url, options = {{}}) => {{
|
||||
calls.push({{url, options}});
|
||||
if (url === 'api/v1/background-identity') return {{login:'timmy'}};
|
||||
return {{id:42}};
|
||||
}};
|
||||
(async () => {{
|
||||
const result = await createBackgroundIssueSync({{store,fetchJson}}).flush();
|
||||
process.stdout.write(JSON.stringify({{calls,result}}));
|
||||
}})();
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
mutation = output["calls"][1]
|
||||
assert mutation["url"] == expected_url
|
||||
assert mutation["options"]["headers"]["Idempotency-Key"] == "message-op"
|
||||
assert json.loads(mutation["options"]["body"]) == {"body": "Ship this reply"}
|
||||
assert output["result"]["confirmed"] == [{"id": 42}]
|
||||
|
||||
|
||||
def test_reconciling_one_outbox_lane_preserves_the_other_lane():
|
||||
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:'issue',ownerLogin:'timmy',status:'queued'}}], 'issue');
|
||||
await store.reconcile([{{id:'message',kind:'issue-comment',ownerLogin:'timmy',status:'queued'}}], 'authored');
|
||||
await store.reconcile([], 'issue');
|
||||
process.stdout.write(JSON.stringify(await store.snapshot()));
|
||||
}})();
|
||||
"""
|
||||
output = run_node(script)
|
||||
|
||||
assert output == [
|
||||
{"id": "message", "kind": "issue-comment", "ownerLogin": "timmy", "status": "queued", "outboxLane": "authored"}
|
||||
]
|
||||
|
||||
|
||||
def test_closed_app_sync_leaves_another_accounts_issue_queued():
|
||||
script = f"""
|
||||
const createBackgroundIssueSync = require({json.dumps(str(SYNC))});
|
||||
@@ -255,7 +332,7 @@ const transaction=work=>{{const run=tail.then(()=>work({{
|
||||
|
||||
assert output["replay"] is None
|
||||
assert output["snapshot"] == [
|
||||
{"id": "done", "ownerLogin": "timmy", "status": "sent", "claimUntil": 0}
|
||||
{"id": "done", "ownerLogin": "timmy", "status": "sent", "outboxLane": "issue", "claimUntil": 0}
|
||||
]
|
||||
|
||||
|
||||
@@ -311,5 +388,7 @@ async def test_dashboard_wires_indexeddb_outbox_and_background_sync_fallback():
|
||||
assert "createBackgroundIssueSync({" in html
|
||||
assert "backgroundSync: backgroundIssueSync" in html
|
||||
assert "registration.sync.register('stackchain-issue-outbox-v1')" in html
|
||||
assert "backgroundIssueSync.snapshot().then(records => issueOutbox.reconcileBackground(records))" in html
|
||||
assert "backgroundIssueSync.snapshot().then(records => {" in html
|
||||
assert "issueOutbox.reconcileBackground(records);" in html
|
||||
assert "authoredOutbox.reconcileBackground(records);" in html
|
||||
assert "if ('indexedDB' in window)" in html
|
||||
@@ -70,10 +70,10 @@ async function dispatchSync(tag) {{
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def test_background_issue_sync_ships_in_a_new_shell_cache():
|
||||
def test_background_authored_sync_ships_in_a_new_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v12" in source
|
||||
assert "stackchain-dashboard-shell-v13" in source
|
||||
|
||||
|
||||
def test_background_sync_event_flushes_closed_app_issue_outbox_only_for_its_tag():
|
||||
|
||||
Reference in new issue
Block a user