io-host-work.test.mjs (6671B)
1 import test from 'node:test'; 2 import assert from 'node:assert/strict'; 3 import {Tasks} from '../dist/io-tasks.js'; 4 import {HostWork} from '../dist/io-host-work.js'; 5 const ok=value=>({kind:'completed',value}),failed=value=>({kind:'failed',value}); 6 const finish=(tasks,scope)=>{tasks.bodyDone(scope,ok(null));assert(tasks.beginRelease(scope));return tasks.settle(scope);}; 7 const deferred=()=>{let resolve;const promise=new Promise(done=>{resolve=done;});return {promise,resolve};}; 8 const setup=()=>{const tasks=new Tasks(1n,'owner-stopped','cycle');return {tasks,work:new HostWork(tasks)};}; 9 10 test('pending read never blocks sibling work; cancellation waits for actual provider cleanup',async()=>{ 11 const {tasks,work}=setup(),read=tasks.spawn(0,true,true),writer=tasks.spawn(0,true,false),cleanup=deferred(),events=[]; 12 const opaque=new Proxy({},{get(){throw Error('late value forced');}}); 13 work.start(read.key,async signal=>{ 14 events.push('read started'); 15 await new Promise(resolve=>signal.addEventListener('abort',()=>{events.push('abort requested');resolve();},{once:true})); 16 await cleanup.promise;events.push('read cleanup finished');return opaque; 17 }); 18 work.start(writer.key,()=>{events.push('sibling wrote');return 7;}); 19 await work.wait();assert.equal(work.take().outcome.value,7);finish(tasks,writer.key); 20 tasks.cancel(0,read,'stop');work.cancelStopped();work.cancelStopped(); 21 tasks.bodyDone(read.key,ok(null));assert.equal(tasks.beginRelease(read.key),false); 22 await Promise.resolve();assert.deepEqual(events,['read started','sibling wrote','abort requested']); 23 const settled=work.wait();cleanup.resolve();await settled; 24 assert.equal(work.take(),null);assert.deepEqual(work.counts,{pending:0,replies:0}); 25 assert(tasks.beginRelease(read.key));tasks.settle(read.key); 26 tasks.claimJoin(0,read);assert.equal(tasks.pollJoin(0).kind,'cancelled');finish(tasks,0); 27 assert.equal(events.at(-1),'read cleanup finished');assert.deepEqual(tasks.counts,{scopes:0,tasks:0,hosts:0}); 28 }); 29 test('completion queued before cancellation is discarded and cannot revive the body',async()=>{ 30 const {tasks,work}=setup(),child=tasks.spawn(0,true,false),value={retained:'reply'}; 31 work.start(child.key,()=>value);await work.wait();assert.deepEqual([...work.values()],[value]); 32 tasks.cancel(0,child,'stop');work.cancelStopped();assert.deepEqual([...work.values()],[]);assert.equal(work.take(),null); 33 finish(tasks,child.key);finish(tasks,0);assert.deepEqual(work.counts,{pending:0,replies:0}); 34 }); 35 test('queued data for an already settled scope is dropped; no scope history required',async()=>{ 36 const {tasks,work}=setup(),child=tasks.spawn(0,true,false); 37 work.start(child.key,()=>42);await work.wait();finish(tasks,child.key); 38 work.cancelStopped();assert.equal(work.take(),null);assert.deepEqual(work.counts,{pending:0,replies:0});finish(tasks,0); 39 }); 40 test('synchronous throws and async rejections settle ownership once without inspecting errors',async()=>{ 41 for(const asynchronous of [false,true]){ 42 const {tasks,work}=setup(),error=new Proxy({},{get(){throw Error('opaque error forced');}}); 43 work.start(0,()=>{if(asynchronous)return Promise.reject(error);throw error;}); 44 await work.wait();const reply=work.take();assert.equal(reply.outcome.ok,false);assert.equal(reply.outcome.error,error); 45 assert.equal(work.take(),null);assert.equal(tasks.state(0).hostCalls,0);finish(tasks,0); 46 } 47 }); 48 test('read rejection after cancellation is acknowledgement, not a fresh task failure',async()=>{ 49 const {tasks,work}=setup(),child=tasks.spawn(0,true,false); 50 work.start(child.key,signal=>new Promise((_resolve,reject)=>signal.addEventListener('abort',()=>reject(Error('abort')),{once:true}))); 51 tasks.stop(child.key,failed('original'));work.cancelStopped();tasks.bodyDone(child.key,ok(null)); 52 await work.wait();assert.equal(work.take(),null);assert(tasks.beginRelease(child.key));assert.equal(tasks.settle(child.key).value,'original'); 53 finish(tasks,0); 54 }); 55 test('multiple waiters share notification; out-of-order replies have correct identities',async()=>{ 56 const {tasks,work}=setup(),a=deferred(),b=deferred(); 57 const first=work.start(0,()=>a.promise),second=work.start(0,()=>b.promise); 58 const one=work.wait(),two=work.wait();assert.equal(one,two);b.resolve('second');await one; 59 assert.deepEqual(work.take(),{token:second,outcome:{ok:true,value:'second'}}); 60 const next=work.wait();a.resolve('first');await next; 61 assert.deepEqual(work.take(),{token:first,outcome:{ok:true,value:'first'}});finish(tasks,0); 62 }); 63 test('sequential host operations retain neither callback history nor consumed reply data',async()=>{ 64 const {tasks,work}=setup(); 65 for(let i=0;i<1000;i++){ 66 const value={iteration:i};work.start(0,()=>value);await work.wait();assert.equal(work.take().outcome.value,value); 67 assert.deepEqual(work.counts,{pending:0,replies:0});assert.equal(tasks.state(0).hostCalls,0);assert.deepEqual([...work.values()],[]); 68 } 69 finish(tasks,0); 70 }); 71 test('async physical release survives new cancellation and reports cleanup failure',async()=>{ 72 const {tasks,work}=setup(),release=deferred(); 73 assert.throws(()=>work.release(0,()=>null),/invalid-transition/); 74 tasks.bodyDone(0,ok('body'));assert(tasks.beginRelease(0)); 75 const token=work.release(0,async()=>{await release.promise;throw 'cleanup failed';}); 76 assert.equal(token.kind,'release');assert.throws(()=>tasks.settle(0),/invalid-transition/); 77 tasks.stop(0,{kind:'cancelled',value:'primary'});work.cancelStopped(); 78 assert.deepEqual(tasks.takeCancellations(),[]);assert.throws(()=>tasks.settle(0),/invalid-transition/); 79 const waiting=work.wait();release.resolve();await waiting; 80 const reply=work.take();assert.equal(reply.outcome.ok,false);assert.equal(reply.outcome.error,'cleanup failed'); 81 const result=tasks.settle(0,{items:[reply.outcome.error],omitted:0}); 82 assert.equal(result.kind,'cancelled');assert.equal(result.value,'primary');assert.deepEqual(result.cleanup.items,['cleanup failed']); 83 assert.deepEqual(work.counts,{pending:0,replies:0}); 84 }); 85 test('body callback identity exhaustion cannot prevent the reserved cleanup batch',async()=>{ 86 const tasks=new Tasks(1n,'stop','cycle',1),work=new HostWork(tasks); 87 work.start(0,()=>1);await work.wait();work.take(); 88 assert.throws(()=>work.start(0,()=>2),/identity-exhausted/); 89 tasks.bodyDone(0,ok(null));assert(tasks.beginRelease(0)); 90 const release=work.release(0,()=>3);await work.wait();assert.equal(work.take().outcome.value,3); 91 assert.equal(release.kind,'release');assert.equal(tasks.endHost(release),'stale'); 92 assert.throws(()=>work.release(0,()=>4),/invalid-transition/);tasks.settle(0); 93 assert.deepEqual(tasks.counts,{scopes:0,tasks:0,hosts:0}); 94 });