11import { startCore } from "./wasm.js" ;
22import { animateAfterFrame , animateBeforeFrame , render , setCoreReady } from "./view.js" ;
33
4+ const AUTO_COMMAND_LIMIT = 30 ;
5+ const AUTO_WINDOW = 3 ;
6+
47const state = {
58 dispatch : null ,
69 catalog : [ ] ,
@@ -15,6 +18,13 @@ const state = {
1518 inspectorOpen : false ,
1619 tourPlaying : false ,
1720 labRunning : false ,
21+ financeAuto : false ,
22+ autoAnchor : "" ,
23+ autoPreviousFrom : "" ,
24+ autoGenerated : 0 ,
25+ autoCompleted : false ,
26+ autoCommandLimit : AUTO_COMMAND_LIMIT ,
27+ autoWindow : AUTO_WINDOW ,
1828 speed : 1 ,
1929 busy : false ,
2030 error : "" ,
@@ -201,6 +211,9 @@ async function handleAction(action, control) {
201211 case "transfer" :
202212 await transfer ( ) ;
203213 break ;
214+ case "toggle-finance-auto" :
215+ await toggleFinanceAuto ( ) ;
216+ break ;
204217 case "bootstrap-node" :
205218 await dispatchLab ( { kind : "bootstrap" , replica : Number ( control . dataset . replica ) } ) ;
206219 break ;
@@ -427,6 +440,11 @@ async function resetFinance(confirmIfNeeded, updateURL = true) {
427440 state . labFrames = new Map ( [ [ 0 , result . frame ] ] ) ;
428441 state . financeDraft = { from : "northwind" , to : "contoso" , amount : "2500.00" } ;
429442 state . error = "" ;
443+ state . financeAuto = false ;
444+ state . autoAnchor = "" ;
445+ state . autoPreviousFrom = "" ;
446+ state . autoGenerated = 0 ;
447+ state . autoCompleted = false ;
430448 state . inspectorOpen = false ;
431449 if ( updateURL ) {
432450 setURL ( { mode : "finance" } ) ;
@@ -459,6 +477,108 @@ async function transfer() {
459477 } ) ;
460478}
461479
480+ async function toggleFinanceAuto ( ) {
481+ if ( state . mode !== "finance" || state . busy ) {
482+ return ;
483+ }
484+ if ( state . financeAuto ) {
485+ state . financeAuto = false ;
486+ state . labRunning = false ;
487+ state . autoCompleted = false ;
488+ clearTimeout ( state . timer ) ;
489+ render ( state ) ;
490+ return ;
491+ }
492+
493+ for ( const replica of state . frame . snapshot . replicas . filter ( ( item ) => ! item . booted ) ) {
494+ if ( ! await dispatchLab ( { kind : "bootstrap" , replica : replica . id } ) ) {
495+ return ;
496+ }
497+ }
498+ const eligible = eligibleAutoAccounts ( ) ;
499+ if ( eligible . length < 2 || state . frame . snapshot . commands . length >= AUTO_COMMAND_LIMIT ) {
500+ state . error = eligible . length < 2
501+ ? "Auto mode needs two accounts whose home pairs have a running node."
502+ : "The bounded automatic trace is full. Reset the simulation to run another batch." ;
503+ render ( state ) ;
504+ return ;
505+ }
506+
507+ state . financeAuto = true ;
508+ state . labRunning = true ;
509+ state . autoAnchor = eligible [ randomIndex ( eligible . length ) ] . id ;
510+ state . autoPreviousFrom = "" ;
511+ state . autoGenerated = 0 ;
512+ state . autoCompleted = false ;
513+ state . error = "" ;
514+ render ( state ) ;
515+ scheduleLabStep ( ) ;
516+ }
517+
518+ function nextAutomaticTransfer ( ) {
519+ const eligible = eligibleAutoAccounts ( ) ;
520+ if ( eligible . length < 2 ) {
521+ return null ;
522+ }
523+ let from = eligible . find ( ( account ) => account . id === state . autoAnchor ) ;
524+ if ( ! from ) {
525+ from = eligible . find ( ( account ) => account . id === state . autoPreviousFrom ) ;
526+ }
527+ if ( ! from ) {
528+ from = eligible [ randomIndex ( eligible . length ) ] ;
529+ }
530+ const destinations = eligible . filter ( ( account ) => account . id !== from . id ) ;
531+ const to = destinations [ randomIndex ( destinations . length ) ] ;
532+ const balance = accountBalance ( from . id ) ;
533+ const maxWholeDollars = Math . max ( 1 , Math . min ( 500 , Math . floor ( balance / 2000 ) ) ) ;
534+ const amount = ( 1 + randomIndex ( maxWholeDollars ) ) * 100 ;
535+
536+ state . autoPreviousFrom = from . id ;
537+ state . autoAnchor = to . id ;
538+ state . financeDraft = { from : from . id , to : to . id , amount : ( amount / 100 ) . toFixed ( 2 ) } ;
539+ return { kind : "transfer" , from : from . id , to : to . id , amount } ;
540+ }
541+
542+ function eligibleAutoAccounts ( ) {
543+ const running = new Set ( state . frame . snapshot . replicas
544+ . filter ( ( replica ) => replica . booted && ! replica . paused && ! replica . crashed )
545+ . map ( ( replica ) => replica . id ) ) ;
546+ return state . frame . snapshot . accounts . filter ( ( account ) => account . home . some ( ( replica ) => running . has ( replica ) ) ) ;
547+ }
548+
549+ function accountBalance ( account ) {
550+ const key = `acct_${ account } ` ;
551+ for ( const replica of state . frame . snapshot . replicas ) {
552+ if ( ! replica . booted || replica . crashed ) {
553+ continue ;
554+ }
555+ const item = replica . state . find ( ( entry ) => entry . key === key ) ;
556+ if ( item ) {
557+ return Number ( item . value ) ;
558+ }
559+ }
560+ return 10_000 ;
561+ }
562+
563+ function randomIndex ( length ) {
564+ return Math . floor ( Math . random ( ) * length ) ;
565+ }
566+
567+ function automaticOutstanding ( ) {
568+ const available = state . frame . snapshot . replicas . filter ( ( replica ) => replica . booted && ! replica . paused && ! replica . crashed ) ;
569+ if ( available . length === 0 ) {
570+ return state . frame . snapshot . commands . length ;
571+ }
572+ const applied = Math . min ( ...available . map ( ( replica ) => replica . applied . length ) ) ;
573+ return Math . max ( 0 , state . frame . snapshot . commands . length - applied ) ;
574+ }
575+
576+ function automaticCanGenerate ( ) {
577+ return state . financeAuto &&
578+ state . frame . snapshot . commands . length < AUTO_COMMAND_LIMIT &&
579+ automaticOutstanding ( ) < AUTO_WINDOW ;
580+ }
581+
462582async function dispatchLab ( action , { quietBlocked = false } = { } ) {
463583 if ( ! isSessionMode ( ) || state . busy ) {
464584 return false ;
@@ -482,7 +602,13 @@ async function dispatchLab(action, { quietBlocked = false } = {}) {
482602 } catch ( error ) {
483603 if ( quietBlocked && error . code === "blocked" ) {
484604 state . labRunning = false ;
485- state . error = "" ;
605+ if ( state . financeAuto ) {
606+ state . financeAuto = false ;
607+ state . autoCompleted = false ;
608+ state . error = "Auto mode stopped because the current account chain has no available coordinator." ;
609+ } else {
610+ state . error = "" ;
611+ }
486612 return ;
487613 }
488614 throw error ;
@@ -512,6 +638,8 @@ function toggleLabRun() {
512638 }
513639 if ( state . labRunning ) {
514640 state . labRunning = false ;
641+ state . financeAuto = false ;
642+ state . autoCompleted = false ;
515643 clearTimeout ( state . timer ) ;
516644 render ( state ) ;
517645 return ;
@@ -534,11 +662,21 @@ function scheduleLabStep() {
534662 return ;
535663 }
536664 const beforeTicks = state . frame . snapshot . replicas . map ( ( replica ) => replica . tick ) ;
537- const deliverable = state . frame . snapshot . messages . some ( ( message ) => ! message . blocked ) ;
538665 let moved = false ;
539- if ( deliverable ) {
666+ if ( automaticCanGenerate ( ) ) {
667+ const action = nextAutomaticTransfer ( ) ;
668+ if ( action ) {
669+ moved = await dispatchLab ( action , { quietBlocked : true } ) ;
670+ if ( moved ) {
671+ state . autoGenerated ++ ;
672+ render ( state ) ;
673+ }
674+ }
675+ }
676+ const deliverable = state . frame . snapshot . messages . some ( ( message ) => ! message . blocked ) ;
677+ if ( ! moved && deliverable ) {
540678 moved = await dispatchLab ( { kind : "deliver-next" } , { quietBlocked : true } ) ;
541- } else if ( state . mode === "finance" ) {
679+ } else if ( ! moved && state . mode === "finance" ) {
542680 const wait = nextNetworkWait ( ) ;
543681 if ( wait > 0 ) {
544682 moved = await dispatchLab ( { kind : "advance-network" , milliseconds : wait } , { quietBlocked : true } ) ;
@@ -551,7 +689,15 @@ function scheduleLabStep() {
551689 render ( state ) ;
552690 return ;
553691 }
554- if ( ! moved || ! state . labRunning || ! sessionCanAdvance ( ) ) {
692+ const canContinue = state . financeAuto ? automaticCanGenerate ( ) || sessionCanAdvance ( ) : sessionCanAdvance ( ) ;
693+ if ( ! moved || ! state . labRunning || ! canContinue ) {
694+ if ( state . financeAuto ) {
695+ state . autoCompleted = state . frame . snapshot . commands . length >= AUTO_COMMAND_LIMIT && automaticOutstanding ( ) === 0 ;
696+ state . financeAuto = false ;
697+ if ( ! state . autoCompleted && ! state . error ) {
698+ state . error = "Auto mode stopped because the cluster cannot make progress." ;
699+ }
700+ }
555701 state . labRunning = false ;
556702 render ( state ) ;
557703 return ;
@@ -577,7 +723,7 @@ async function transitionFrame(frame, mode) {
577723 . map ( ( [ index , item ] ) => ( { index, events : item . events } ) ) ;
578724 }
579725 render ( state ) ;
580- await animateAfterFrame ( frame ) ;
726+ await animateAfterFrame ( frame , state . speed ) ;
581727 const details = frame . events . map ( ( event ) => event . detail ) . join ( " " ) ;
582728 if ( details ) {
583729 announce ( details ) ;
@@ -707,6 +853,9 @@ function setURL(values) {
707853function stopPlayback ( ) {
708854 state . tourPlaying = false ;
709855 state . labRunning = false ;
856+ state . financeAuto = false ;
857+ state . autoAnchor = "" ;
858+ state . autoPreviousFrom = "" ;
710859 clearTimeout ( state . timer ) ;
711860}
712861
0 commit comments