Compare commits
No commits in common. "187b0decaee43f5473c83c10bbc4f4f6e79f56a7" and "544ffb4ae7f706875e643ae08b81efebf1cf5fc1" have entirely different histories.
187b0decae
...
544ffb4ae7
|
|
@ -70,12 +70,11 @@ function createTodayWeekReschedule({
|
||||||
throw conflict;
|
throw conflict;
|
||||||
}
|
}
|
||||||
|
|
||||||
function flush(retryConflict = false) {
|
function flush() {
|
||||||
if (flushing) return flushing;
|
if (flushing) return flushing;
|
||||||
if (!pending()) return Promise.resolve(false);
|
if (!pending()) return Promise.resolve(false);
|
||||||
flushing = (async () => {
|
flushing = (async () => {
|
||||||
let result = null;
|
let result = null;
|
||||||
let conflictRetried = false;
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const records = readQueue();
|
const records = readQueue();
|
||||||
const record = records[0];
|
const record = records[0];
|
||||||
|
|
@ -84,25 +83,7 @@ function createTodayWeekReschedule({
|
||||||
result = await api('api/v1/week/reschedule', {
|
result = await api('api/v1/week/reschedule', {
|
||||||
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(record.body),
|
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(record.body),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) { await restoreConflict(error); }
|
||||||
if (error?.status === 409 && retryConflict && !conflictRetried) {
|
|
||||||
const [today, weekState] = await Promise.all([api('api/v1/today'), api('api/v1/week')]);
|
|
||||||
adoptToday(today); week.adopt(weekState);
|
|
||||||
const latest = readQueue();
|
|
||||||
if (today?.ids?.includes(record.body.identity) &&
|
|
||||||
latest[0]?.body?.operation_id === record.body.operation_id) {
|
|
||||||
latest[0] = {...latest[0], body:{
|
|
||||||
...latest[0].body,
|
|
||||||
today_revision:today.revision,
|
|
||||||
week_revision:weekState.revision,
|
|
||||||
}};
|
|
||||||
if (!saveQueue(latest)) throw new Error('Could not update this saved move after plans changed.');
|
|
||||||
conflictRetried = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await restoreConflict(error);
|
|
||||||
}
|
|
||||||
adoptToday(result.today);
|
adoptToday(result.today);
|
||||||
week.adopt(result.week);
|
week.adopt(result.week);
|
||||||
const latest = readQueue();
|
const latest = readQueue();
|
||||||
|
|
@ -226,24 +207,9 @@ function createTodayWeekReschedule({
|
||||||
}
|
}
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
if (admitted) {
|
result = admitted ? await flush() : await api('api/v1/week/reschedule', {
|
||||||
result = await flush(true);
|
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body),
|
||||||
} else {
|
});
|
||||||
try {
|
|
||||||
result = await api('api/v1/week/reschedule', {
|
|
||||||
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (error?.status !== 409) throw error;
|
|
||||||
const [today, weekState] = await Promise.all([api('api/v1/today'), api('api/v1/week')]);
|
|
||||||
adoptToday(today); week.adopt(weekState);
|
|
||||||
if (!today?.ids?.includes(body.identity)) throw error;
|
|
||||||
body.today_revision = today.revision; body.week_revision = weekState.revision;
|
|
||||||
result = await api('api/v1/week/reschedule', {
|
|
||||||
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
if (admitted && error?.status === 409) await restoreConflict(error);
|
if (admitted && error?.status === 409) await restoreConflict(error);
|
||||||
if (admitted && error?.status !== 401 && error?.status !== 403) {
|
if (admitted && error?.status !== 401 && error?.status !== 403) {
|
||||||
|
|
|
||||||
|
|
@ -64,73 +64,6 @@ console.log(JSON.stringify({opened,confirmed,requests,adoptedToday,weekState}));
|
||||||
assert result["confirmed"]["today"]["revision"] == 5
|
assert result["confirmed"]["today"]["revision"] == 5
|
||||||
|
|
||||||
|
|
||||||
def test_reschedule_controller_retries_one_stale_revision_when_active_intent_is_unchanged():
|
|
||||||
result = run_controller("""
|
|
||||||
const requests=[];
|
|
||||||
let attempts=0;
|
|
||||||
const weekState={revision:7,offline_snapshot:false,timezone:'UTC',days:[
|
|
||||||
{plan_date:'2026-08-25',ids:[],capacity_minutes:90,estimates:{}}
|
|
||||||
]};
|
|
||||||
const week={
|
|
||||||
load:async()=>weekState,
|
|
||||||
review:()=>({days:[{...weekState.days[0],label:'Tue',planned_minutes:0}]}),
|
|
||||||
adopt:value=>Object.assign(weekState,value),
|
|
||||||
};
|
|
||||||
const api=async(url,options)=>{
|
|
||||||
if(url==='api/v1/today')return {revision:5,ids:['active'],estimates:{active:45}};
|
|
||||||
if(url==='api/v1/week')return {revision:8,offline_snapshot:false,timezone:'UTC',days:[
|
|
||||||
{plan_date:'2026-08-25',ids:[],capacity_minutes:90,estimates:{}}
|
|
||||||
]};
|
|
||||||
const body=JSON.parse(options.body);requests.push(body);attempts++;
|
|
||||||
if(attempts===1){const error=new Error('stale revision');error.status=409;throw error;}
|
|
||||||
return {today:{revision:6,ids:[],estimates:{}},week:{revision:9,timezone:'UTC',days:[
|
|
||||||
{plan_date:'2026-08-25',ids:['active'],capacity_minutes:90,estimates:{active:45}}
|
|
||||||
]}};
|
|
||||||
};
|
|
||||||
const controller=createReschedule({week,api,getToday:()=>null,adoptToday:()=>{},operationId:()=> 'move-active'});
|
|
||||||
await controller.open('active');
|
|
||||||
let message='',confirmed=null;
|
|
||||||
try{confirmed=await controller.confirm('2026-08-25',45);}catch(error){message=error.message;}
|
|
||||||
console.log(JSON.stringify({message,confirmed,requests}));
|
|
||||||
""")
|
|
||||||
|
|
||||||
assert result["message"] == ""
|
|
||||||
assert result["confirmed"]["today"]["revision"] == 6
|
|
||||||
assert [request["today_revision"] for request in result["requests"]] == [5, 5]
|
|
||||||
assert [request["week_revision"] for request in result["requests"]] == [7, 8]
|
|
||||||
assert result["requests"][0]["operation_id"] == result["requests"][1]["operation_id"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_reschedule_controller_rebases_admitted_move_once_after_stale_revision():
|
|
||||||
result = run_controller("""
|
|
||||||
const values=new Map();
|
|
||||||
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
|
||||||
const requests=[];let attempts=0;
|
|
||||||
const initialWeek={revision:7,offline_snapshot:false,timezone:'UTC',days:[
|
|
||||||
{plan_date:'2026-08-25',ids:[],capacity_minutes:90,estimates:{}}
|
|
||||||
]};
|
|
||||||
const week={load:async()=>initialWeek,review:()=>({days:[{...initialWeek.days[0],label:'Tue',planned_minutes:0}]}),adopt:()=>{}};
|
|
||||||
const api=async(url,options)=>{
|
|
||||||
if(url==='api/v1/today')return {revision:5,ids:['active'],estimates:{active:45}};
|
|
||||||
if(url==='api/v1/week')return {revision:8,offline_snapshot:false,timezone:'UTC',days:initialWeek.days};
|
|
||||||
const body=JSON.parse(options.body);requests.push(body);attempts++;
|
|
||||||
if(attempts===1){const error=new Error('stale revision');error.status=409;throw error;}
|
|
||||||
return {today:{revision:6,ids:[],estimates:{}},week:{revision:9,timezone:'UTC',days:[]}};
|
|
||||||
};
|
|
||||||
const controller=createReschedule({week,api,adoptToday:()=>{},storage,getLogin:()=> 'timmy',operationId:()=> 'move-active'});
|
|
||||||
await controller.open('active');
|
|
||||||
let message='',confirmed=null;
|
|
||||||
try{confirmed=await controller.confirm('2026-08-25',45);}catch(error){message=error.message;}
|
|
||||||
console.log(JSON.stringify({message,confirmed,requests,remaining:values.size}));
|
|
||||||
""")
|
|
||||||
|
|
||||||
assert result["message"] == ""
|
|
||||||
assert result["confirmed"]["today"]["revision"] == 6
|
|
||||||
assert [request["today_revision"] for request in result["requests"]] == [5, 5]
|
|
||||||
assert [request["week_revision"] for request in result["requests"]] == [7, 8]
|
|
||||||
assert result["remaining"] == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_reschedule_controller_blocks_offline_full_and_overloaded_days_without_mutation():
|
def test_reschedule_controller_blocks_offline_full_and_overloaded_days_without_mutation():
|
||||||
result = run_controller("""
|
result = run_controller("""
|
||||||
let calls=0;
|
let calls=0;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user