io-tasks.test.mjs (8668B)
1 import test from 'node:test'; 2 import assert from 'node:assert/strict'; 3 import {Tasks} from '../dist/io-tasks.js'; 4 const ok=value=>({kind:'completed',value}),failed=value=>({kind:'failed',value}),cancelled=value=>({kind:'cancelled',value}); 5 const diagnostics=items=>({items,omitted:0}); 6 const fresh=(run=1n,limit)=>new Tasks(run,'owner-stopped','join-cycle',limit); 7 function finish(s,id,outcome=ok(null),cleanup){s.bodyDone(id,outcome);assert.equal(s.beginRelease(id),true);return s.settle(id,cleanup);} 8 function retired(s){assert.deepEqual(s.counts,{scopes:0,tasks:0,hosts:0});assert.deepEqual([...s.payloads()],['owner-stopped','join-cycle']);} 9 10 test('normal exit closes admission but waits for children; ordinary errors remain values',()=>{ 11 const s=fresh(),child=s.spawn(0,false,false),ordinary={err:'domain error'}; 12 s.bodyDone(0,ok(ordinary));assert.equal(s.beginRelease(0),false); 13 assert.throws(()=>s.spawn(0,false,false),/invalid-transition/); 14 const grandchild=s.spawn(child.key,false,false); // existing child remains live during ancestor join 15 finish(s,grandchild.key);finish(s,child.key); 16 assert.equal(s.beginRelease(0),true);const out=s.settle(0); 17 assert.equal(out.kind,'completed');assert.equal(out.value,ordinary);retired(s); 18 }); 19 test('normal joining remains cancellable and the first reason is sticky',()=>{ 20 const s=fresh(),child=s.spawn(0,false,false); 21 s.bodyDone(0,ok('return'));s.stop(0,cancelled('stop'));s.stop(0,failed('late')); 22 assert.deepEqual(s.state(child.key).reason,cancelled('owner-stopped')); 23 finish(s,child.key,failed('late child')); 24 assert(s.beginRelease(0));assert.equal(s.settle(0).value,'stop');retired(s); 25 }); 26 test('supervision contains failure; unsupervised failure stops siblings immediately',()=>{ 27 const s=fresh(),contained=s.spawn(0,true,true); 28 finish(s,contained.key,failed('contained'));assert.equal(s.state(0).reason,null); 29 s.claimJoin(0,contained);assert.equal(s.pollJoin(0).value,'contained'); 30 const a=s.spawn(0,false,true),b=s.spawn(0,false,true); 31 s.stop(a.key,failed('first'));s.stop(b.key,failed('second')); 32 assert.deepEqual(s.state(0).reason,failed('first')); 33 assert.deepEqual(s.state(b.key).reason,cancelled('owner-stopped')); 34 finish(s,a.key);finish(s,b.key);assert.equal(finish(s,0).value,'first');retired(s); 35 }); 36 test('joins consume before waiting; abandoned joins never restore tickets or retain results',()=>{ 37 const s=fresh(),child=s.spawn(0,true,true),waiter=s.spawn(0,true,true); 38 s.claimJoin(waiter.key,child);assert.equal(s.pollJoin(waiter.key),null); 39 assert.throws(()=>s.discard(0,child),/stale-task/); 40 s.cancel(0,waiter,'cancel wait');finish(s,waiter.key); 41 const result={unique:'unclaimed'};finish(s,child.key,ok(result)); 42 assert(![...s.payloads()].includes(result)); 43 s.claimJoin(0,waiter);assert.equal(s.pollJoin(0).value,'cancel wait'); 44 assert.throws(()=>s.claimJoin(0,waiter),/stale-task/); 45 finish(s,0);retired(s); 46 }); 47 test('explicit and implicit cycles use task identities across lexical scopes',()=>{ 48 const s=fresh(),a=s.spawn(0,true,true);let b=s.spawn(0,true,true); 49 const inner=s.nested(a.key); 50 assert.throws(()=>s.claimJoin(inner,a),/join-cycle/); 51 s.claimJoin(inner,b);assert.throws(()=>s.claimJoin(b.key,a),/join-cycle/); 52 s.abandonJoin(inner);finish(s,b.key);b=s.spawn(0,true,true); 53 const c=s.spawn(inner,true,true); 54 s.claimJoin(b.key,a);s.claimJoin(c.key,b);s.bodyDone(inner,ok(null)); 55 assert.equal(s.state(inner).reason.value,'join-cycle');assert.equal(s.state(a.key).reason.value,'join-cycle'); 56 finish(s,c.key);assert(s.beginRelease(inner));s.settle(inner);finish(s,a.key); 57 assert.equal(s.pollJoin(b.key).value,'join-cycle');finish(s,b.key);finish(s,0);retired(s); 58 }); 59 test('out-of-scope, cross-run and completed task cancellation reject without consumption',()=>{ 60 const s=fresh(),a=s.spawn(0,true,true),b=s.spawn(0,true,true),owned=s.spawn(a.key,true,true); 61 assert.throws(()=>s.claimJoin(b.key,owned),/out-of-scope/); 62 assert.throws(()=>s.cancel(0,{...a,run:2n},'bad'),/stale-task/); 63 finish(s,owned.key,ok('child'));s.claimJoin(a.key,owned);assert.equal(s.pollJoin(a.key).value,'child'); 64 finish(s,a.key);assert.throws(()=>s.cancel(0,a,'late'),/stale-task/);s.discard(0,a); 65 finish(s,b.key);finish(s,0);retired(s); 66 }); 67 test('cleanup defeats success, preserves the primary and propagates bounded diagnostics once',()=>{ 68 const s=fresh(),inner=s.nested(0),child=s.spawn(inner,false,false); 69 assert.equal(finish(s,child.key,ok(null),diagnostics([17])).value,17); 70 const lexical=finish(s,inner,ok(null),diagnostics(Array.from({length:25},(_,i)=>100+i))); 71 assert.equal(lexical.cleanup,undefined); 72 const out=finish(s,0,failed(999),diagnostics([200])); 73 assert.equal(out.kind,'failed');assert.equal(out.value,17); 74 assert.deepEqual(out.cleanup,{items:Array.from({length:16},(_,i)=>100+i),omitted:10});retired(s); 75 }); 76 test('pending host cancellation is delivered once but release requires acknowledgement',async()=>{ 77 const s=fresh(),reader=s.spawn(0,true,true),read=s.beginHost(reader.key),controller=new AbortController(),events=[]; 78 let acknowledge; 79 const physicalWork=new Promise(resolve=>{acknowledge=resolve;}); 80 controller.signal.addEventListener('abort',()=>events.push('abort-requested'),{once:true}); 81 s.cancel(0,reader,'cancel input');s.bodyDone(reader.key,ok('ignored value')); 82 assert.deepEqual(s.takeCancellations(),[read]);controller.abort(); 83 assert.deepEqual(s.takeCancellations(),[]);assert.equal(s.beginRelease(reader.key),false); 84 s.bodyDone(0,ok('parent'));assert.equal(s.beginRelease(0),false); 85 await Promise.resolve();assert.deepEqual(events,['abort-requested']);assert.equal(s.beginRelease(reader.key),false); 86 acknowledge();await physicalWork;events.push('physical-work-settled'); 87 assert.equal(s.endHost(read),'ignored');assert.equal(s.endHost(read),'stale'); 88 assert(s.beginRelease(reader.key));assert.equal(s.settle(reader.key).kind,'cancelled'); 89 assert(s.beginRelease(0));s.settle(0);retired(s); 90 }); 91 test('wrong/stale host resumes leave the correct wait intact',()=>{ 92 const s=fresh(),child=s.spawn(0,true,false),host=s.beginHost(child.key); 93 for(const bad of [{...host,scope:0},{...host,run:2n},{...host,key:host.key+1}])assert.equal(s.endHost(bad),'stale'); 94 assert.equal(s.state(child.key).hostCalls,1);assert.equal(s.endHost(host),'reply'); 95 const next=s.beginHost(child.key);assert.notEqual(next.key,host.key);assert.equal(s.endHost(host),'stale'); 96 assert.equal(s.endHost(next),'reply');finish(s,child.key);finish(s,0);retired(s); 97 }); 98 test('all callback completion orders retain cancelled scopes until their own work settles',()=>{ 99 for(const order of [[0,1,2],[0,2,1],[1,0,2],[1,2,0],[2,0,1],[2,1,0]]){ 100 const s=fresh(),children=order.map(()=>s.spawn(0,false,false)),hosts=children.map(c=>s.beginHost(c.key)); 101 s.stop(0,failed('stop'));s.bodyDone(0,ok(null)); 102 for(const child of children)s.bodyDone(child.key,ok(null)); 103 assert.equal(s.takeCancellations().length,3);assert.equal(s.takeCancellations().length,0); 104 for(const index of order){ 105 assert.equal(s.beginRelease(0),false);assert.equal(s.beginRelease(children[index].key),false); 106 assert.equal(s.endHost(hosts[index]),'ignored');assert(s.beginRelease(children[index].key));s.settle(children[index].key); 107 } 108 assert(s.beginRelease(0));assert.equal(s.settle(0).value,'stop');retired(s); 109 } 110 }); 111 test('nonwrapping identities, scope state copies and immutable diagnostic capture',()=>{ 112 for(const bad of [0n,-1n,1,2n**64n])assert.throws(()=>fresh(bad),/identity/); 113 const s=fresh(1n,2),a=s.spawn(0,true,true),copy=s.state(a.key);copy.phase='releasing'; 114 assert.equal(s.state(a.key).phase,'open'); 115 const items=['cleanup'];s.bodyDone(a.key,failed('primary'));assert(s.beginRelease(a.key)); 116 const result=s.settle(a.key,diagnostics(items));items[0]='changed';result.cleanup.items[0]='also changed'; 117 s.claimJoin(0,a);assert.equal(s.pollJoin(0).cleanup.items[0],'cleanup'); 118 const b=s.spawn(0,true,false);finish(s,b.key); 119 assert.throws(()=>s.spawn(0,true,false),/identity-exhausted/);assert.deepEqual(s.counts,{scopes:1,tasks:0,hosts:0}); 120 const h=s.beginHost(0);assert(Object.isFrozen(h));s.endHost(h);s.endHost(s.beginHost(0)); 121 assert.throws(()=>s.beginHost(0),/identity-exhausted/);finish(s,0);retired(s); 122 }); 123 test('joined, discarded and non-result tasks retain no history or payload roots',()=>{ 124 const s=fresh(); 125 for(let i=0;i<2000;i++){ 126 const t=s.spawn(0,true,i%3!==2),payload={unique:i};finish(s,t.key,ok(payload)); 127 if(i%3===0){s.claimJoin(0,t);assert.equal(s.pollJoin(0).value,payload);} 128 if(i%3===1)s.discard(0,t); 129 assert.deepEqual(s.counts,{scopes:1,tasks:0,hosts:0});assert(![...s.payloads()].includes(payload)); 130 } 131 finish(s,0);retired(s); 132 });