@webqit/quantum-js
Version:
Runtime extension to JavaScript that let's us do Imperative Reactive Programming (IRP) in the very language.
125 lines (113 loc) • 5.72 kB
JavaScript
/**
* @import
*/
import { _await } from '../util.js';
import Observer from '@webqit/observer';
import Autorun from './Autorun.js';
import Scope from './Scope.js';
export default class AutoIterator extends Autorun {
rounds = new Map;
constructor( context, type, spec, serial, scope, closure ) {
spec.$closure = closure;
super( context, type, spec, serial, scope );
this.manage( () => {
delete this.breakpoint;
this.rounds.clear();
} );
}
pseudorun( callback ) {
this.runtime.iThread.unshift( this );
return _await( callback(), returnValue => {
this.runtime.iThread.pop();
return returnValue;
} );
}
createIterator() {
if ( this.spec.kind === 'for-in' ) return ( function* () { for ( let key in this.iteratee ) yield key; } ).call( this );
if ( this.spec.kind === 'for-of' ) return ( function* () { for ( let val of this.iteratee ) yield val; } ).call( this );
return { next: () => ({ done: !this.pseudorun( () => this.spec.test( this ) ), }), };
}
closure() {
if ( [ 'for-of', 'for-in' ].includes( this.spec.kind ) ) {
[ this.production, this.iteratee ] = this.spec.parameters( this );
this.iterator = this.createIterator();
this.iterator.original = true;
this.watchMode();
} else {
if ( this.spec.kind === 'for' ) { this.spec.init( this ); }
this.iterator = this.createIterator();
}
this.iterate();
}
terminated() { return this.breakpoint && !this.breakpoint.flowControlApplied( 'continue', this.spec.label ) && this.breakpoint.flowControlApplied(); }
advance() { if ( this.spec.kind === 'for' ) { this.pseudorun( () => this.spec.advance( this ) ); } }
iterate() {
this.iterating = true;
const $test = () => !this.terminated()/* must come before */ && !( this.cursor = this.iterator.next() ).done;
const $round = () => {
const round = this.createRound( this.cursor.value );
round.execute();
this.advance();
};
if ( this.spec.kind === 'do-while' ) { do $round(); while ( $test() ); }
else { while ( $test() ) $round(); }
this.iterating = false;
}
createRound( production ) {
const index = this.rounds.size, spec = { index };
// The scope instance starts life with the variables that the iteration round should see.
const state = [ 'for-in', 'for-of' ].includes( this.spec.kind ) ? { [ this.production ]: production } : { ...this.scope.state };
const scope = new Scope( this.scope, 'round', state );
this.scope.symbols.forEach( ( meta, name ) => { scope.symbols.set( name, meta ); } );
const round = new Autorun( this, 'round', spec, this.serial, scope, this.spec.$closure );
const key = this.spec.kind === 'for-in' ? production : index;
this.rounds.set( key, round );
if ( this.lastRound ) {
this.lastRound.nextRound = round;
round.prevRound = this.lastRound;
}
this.lastRound = round;
return round;
}
watchMode() {
if ( this.spec.static ) return;
const handleMutations = ( mutations, currentCursor ) => {
const deletions = new Set, extension = new Set;
for ( const mutation of mutations ) {
if ( Array.isArray( this.iteratee ) && mutation.key === 'length' ) continue;
const production = this.spec.kind === 'for-in' ? mutation.key : mutation.value;
const key = this.spec.kind === 'for-in' ? mutation.key : parseInt( mutation.key );
const existingRound = this.rounds.get( key ); // Note that we're not using has()
if ( existingRound ) {
// This is an existing round
Observer.set( existingRound.scope.state, this.production, production );
if ( mutation.type === 'delete' ) {
this.rounds.set( key, undefined ); // No actual deletions here as this.rounds.size is our index
if ( existingRound.prevRound ) { existingRound.prevRound.nextRound = existingRound.nextRound; }
if ( existingRound.nextRound ) { existingRound.nextRound.prevRound = existingRound.prevRound; }
// Fired above for the last time and now should die
deletions.add( existingRound );
}
} else if ( mutation.type !== 'delete' && !mutation.isUpdate ) {
// This is a brand new entry
// Deletions and updates that didn't have an existing round above are obviously awaiting visit from a paused loop
if ( this.spec.kind === 'for-of' && this.iterator.original && !currentCursor.done ) continue; // A live iteration is going on
extension.add( production );
}
}
this.runtime.on( 'reflection', () => {
deletions.forEach( deletion => deletion.abort( true ) );
}, { once: true } );
if ( extension.size ) {
this.iterator = ( function* ( parent ) {
yield* parent;
yield* extension;
} )( this.iterator );
if ( currentCursor.done ) { this.iterate(); }
}
};
this.once( Observer.observe( this.iteratee, mutations => {
_await( this.cursor, currentCursor => handleMutations( mutations, currentCursor ) );
} ) );
}
}