request-scheduler.test.mjs 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142
  1. import {test} from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import {readFileSync,existsSync} from 'node:fs';
  4. import {createRequire} from 'node:module';
  5. import vm from 'node:vm';
  6. import {Subject,of,firstValueFrom} from 'rxjs';
  7. import ts from 'typescript';
  8. const require=createRequire(import.meta.url),file=new URL('../../projects/shared/request-scheduler.ts',import.meta.url);
  9. function scheduler(options){assert.ok(existsSync(file),'shared scheduler must exist');const exports={};vm.runInNewContext(ts.transpileModule(readFileSync(file,'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText,{exports,require,setTimeout,clearTimeout,Date,Error,Map,JSON});return new exports.RequestScheduler(options);}
  10. const flush=()=>new Promise(r=>setImmediate(r));
  11. test('caps reads at three, reserves a fourth slot for writes, releases queued work',async()=>{
  12. const s=scheduler(),sources=Array.from({length:6},()=>new Subject()),started=[];
  13. const subscriptions=sources.map((source,i)=>s.schedule(()=>{started.push(i);return source;},{read:i!==5}).subscribe());
  14. assert.deepEqual(started,[0,1,2,5]);sources[0].complete();assert.deepEqual(started,[0,1,2,5,3]);subscriptions.forEach(s=>s.unsubscribe());assert.equal(s.snapshot().active,0);
  15. });
  16. test('coalesces identical reads and aborts only after the last consumer leaves',()=>{
  17. const s=scheduler(),source=new Subject();let starts=0,stops=0;
  18. const run=()=>{starts++;return new (require('rxjs').Observable)(subscriber=>{const sub=source.subscribe(subscriber);return()=>{stops++;sub.unsubscribe();};});};
  19. const a=s.schedule(run,{read:true,key:'identity'}).subscribe(),b=s.schedule(run,{read:true,key:'identity'}).subscribe();
  20. assert.equal(starts,1);a.unsubscribe();assert.equal(stops,0);b.unsubscribe();assert.equal(stops,1);
  21. });
  22. test('caches only approved successes, invalidation fences in-flight results, never caches failures',async()=>{
  23. let now=0;const s=scheduler({now:()=>now}),p={read:true,key:'same',cacheMs:30_000,cacheable:v=>v.success===true};let starts=0;
  24. const run=()=>{starts++;return of({success:true});};await firstValueFrom(s.schedule(run,p));now=29_999;await firstValueFrom(s.schedule(run,p));assert.equal(starts,1);now=30_000;await firstValueFrom(s.schedule(run,p));assert.equal(starts,2);
  25. s.invalidate();const old=new Subject(),pending=firstValueFrom(s.schedule(()=>old,p));s.invalidate();old.next({success:true});old.complete();await pending;await firstValueFrom(s.schedule(run,p));assert.equal(starts,3);
  26. s.invalidate();await firstValueFrom(s.schedule(()=>of({success:false}),p));await firstValueFrom(s.schedule(run,p));assert.equal(starts,4);
  27. });
  28. test('drops stale queued sessions and enforces one heavy read at a time',async()=>{
  29. const s=scheduler(),hold=new Subject();let live=true,started=0;const one=s.schedule(()=>hold,{read:true,heavy:true}).subscribe();
  30. const result=firstValueFrom(s.schedule(()=>{started++;return of(1);},{read:true,heavy:true,valid:()=>live}));live=false;hold.complete();await assert.rejects(result,/会话/);assert.equal(started,0);one.unsubscribe();await flush();assert.equal(s.snapshot().queued,0);
  31. });
  32. test('bounds queue wait without starting timed-out work',async()=>{
  33. const s=scheduler({queueTimeoutMs:5}),hold=new Subject(),sub=s.schedule(()=>hold,{read:true,heavy:true}).subscribe();
  34. await assert.rejects(firstValueFrom(s.schedule(()=>of(1),{read:true,heavy:true})),/排队/);sub.unsubscribe();assert.equal(s.snapshot().queued,0);
  35. });
  36. test('fifty concurrent consumers remain bounded and a submission passes a read backlog',async()=>{
  37. const s=scheduler();let active=0,max=0;const order=[];
  38. const read=i=>s.schedule(()=>new (require('rxjs').Observable)(subscriber=>{active++;max=Math.max(max,active);const timer=setTimeout(()=>{order.push(i);subscriber.next(i);subscriber.complete();},2);return()=>{clearTimeout(timer);active--;};}),{read:true,key:'r'+i});
  39. const reads=Array.from({length:50},(_,i)=>firstValueFrom(read(i)));
  40. const write=firstValueFrom(s.schedule(()=>{order.push('write');return of('saved');},{read:false}));
  41. assert.equal(await write,'saved');assert.equal(order[0],'write');assert.deepEqual(await Promise.all(reads),Array.from({length:50},(_,i)=>i));assert.ok(max<=3);assert.equal(s.snapshot().active,0);assert.equal(s.snapshot().queued,0);
  42. });