tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

io-v2-host-work.test.mjs (6677B)


      1 import test from 'node:test';
      2 import assert from 'node:assert/strict';
      3 import {Tasks} from '../dist/io-v2-tasks.js';
      4 import {HostWork} from '../dist/io-v2-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 });