Skip to content

Instantly share code, notes, and snippets.

@SaladHut
Last active July 19, 2022 02:19
Show Gist options
  • Save SaladHut/a9d1c06f8daae3010df5b38f3c704b8b to your computer and use it in GitHub Desktop.
Save SaladHut/a9d1c06f8daae3010df5b38f3c704b8b to your computer and use it in GitHub Desktop.
A class utility for regenerating reusable Promise-like behaviors.

MLIResume

A utility class for regenerating reusable Promise behavior.

Usage

Syncing

It works just like Promise, since it's a Promise.

import { MLIResume as Resume } from './MLIResume';

const mySync = new Resume();

mySync.reset().then( result => console.log('got', result ));
mySync.resolve('my result'); // got my result

mySync.reset().then( result => console.log('2nd got', result ));
mySync.resolve('my 2nd result'); // 2nd got my 2nd result

mySync.reset().then( result => console.log('3rd got', result ));
mySync.reset().then( result => console.log('4th got', result ));
mySync.reset().then( result => console.log('5th got', result ));

mySync.resolve('same result');
// 3rd got same result
// 4th got same result
// 5th got same result

reset() creates and returns a new Promise for a resolve()/reject(). Keep calling it will return the same reference to the current Promise until either resolve() or reject() is called then the current Promise will be replaced with a new one (or not if passing false as the 2nd argument, then a release() is needed to unfreeze this state.)

Repeating

import { MLIResume as Resume } from './MLIResume';

const mySync = new Resume();

(async () => {
  for await ( const x of mySync.repeat( 3 )){ console.log('Resolved:', x )}
})();

mySync.resolve( 10 ); // Resolved: 10
mySync.resolve('is'); // Resolved: is
mySync.resolve({ a: Number }); // Resolved: {a: Ζ’}

// repeat(3), no more resolve.

Without any argument, it will be repeating infinitely but can be terminated by calling reject()

(async () => {
  for await ( const x of mySync.repeat()){ console.log('Resolved:', x )}
})();

mySync.resolve('I');
mySync.resolve('love');
mySync.reject('me.');

You can pass a function as argument. Returning false will end the repeat().

(async () => {
  for await ( const x of mySync.repeat( v => v !== 'end')){ console.log('Resolved:', x )}
})();

mySync.resolve( 10 ); // Resolved: 10
mySync.resolve('end'); // end!

Returning

Poll until the callback argument return true.

import { MLIResume as Resume } from './MLIResume';

(async () => {
  const resume = new Resume();
  const result = await resume.return( v => v > 2 ));
  console.log( result, "> 2 !" ); // 3
})();

//...

mySub.resolve(1); // ...Nothing
mySub.resolve(2); // ...Nothing
mySub.resolve(3); // 3 > 2 !

An error-handling callback can be provided as the 2nd argument, which may return true to ignore rejected values, that are ignored by default.

Subscription

Like repeat(), subscribe() will be polling for the callback until the returned unsubscribe function is called.

import { MLIResume as Subscription } from './MLIResume';

const mySub = new Subscription();
const unsub = mySub.subscribe( cbValue => console.log('Resolved:', cbValue ));

mySub.resolve(1); // Resolved: 1
mySub.resolve(2); // Resolved: 2
unsub();
mySub.resolve(3); // ...Nothing!

Switching

import { MLIResume as Resume } from './MLIResume';

const mySync = new Resume();

(async () => {
    while( true )
    switch( await mySync.reset()){
        case 1: console.log('I'); break;
        case 2: console.log('love'); break;
        case 3: console.log('you.'); break;
    }
})();

mySync.resolve(1);
mySync.resolve(2);
mySync.resolve(3);

Resolving, rejecting

Adding false to resolve( myResult, false ) will prevent reset() from creating new Promise which means that all future reset() will immediately resolved to the last result. The same goes for reject( myError, false ). You can still call release() to reset this state.

/*
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
WTFPL Version 1.060377, March 2021
Copyright Β© 2021 SaladHut Resort <info@saladhut.com>
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. DO WHAT THE FUCK YOU WANT TO.
*/
export class MLIResume {
__state = {
retains: 0,
};
constructor( name = ''){
this.name = name;
console.debug(`πŸ”„ ${ this.name } created.` );
}
/*
reset() : Will create and return new unsolved Promise.
resolve() / reject() : resolve/reject and clear the state pending (retains).
retains : Number of times reset() was called before resolve/reset.
repeat( count = Infinity ) : Return a generator that can hook waiters
in for...await resolving loop.
*/
reset( tag, tagValue ){
if( tag ){
( this.__state.tags ??= new WeakMap()).set( tag, tagValue );
}
if( !this.__state.retains ){
console.debug(`πŸ”„ ${ this.name } πŸ” reset` );
const promise = new Promise(( resolve, reject ) =>{
const tags = this.__state.tags;
this.__state = {
retains: 1,
resolve,
reject,
...( tags && { tags }),
};
});
this.__state.promise = promise;
} else {
console.debug(`πŸ”„ ${ this.name } πŸ”‚ retains.`);
this.__state.retains++;
}
return this.__state.promise;
}
release(){
this.__state.retains = 0;
return this;
}
get retains(){
return this.__state.retains;
}
resolve( value, reset = true, queue = true ){
if( this.__state.retains || !queue ){
this.__resolve( value, reset )
} else {
setTimeout(() => this.__resolve( value, reset ));
}
}
__resolve( value, reset = true ){
if( !this.__state.promise ){
this.reset();
}
console.debug(`πŸ”„ ${ this.name } πŸ”† resolve:`, value,'reset:', reset );
const res = this.__state.resolve;
// If reset is false, state retains will never be cleared.
// This is like marking end to the promise until releass().
if( reset ) this.__state.retains = 0;
// ...same for reject.
res( value );
}
reject( value, reset = true, queue = true ){
if( this.__state.retains || !queue ){
this.__reject( value, reset );
} else setTimeout(() => this.__reject( value, reset ));
}
__reject( value, reset = true ){
if( !this.__state.promise ){
this.reset()
.catch( error => {
console.debug(`πŸ”„ ${ this.name } πŸ’’ catch:`, error );
});
}
console.debug(`πŸ”„ ${ this.name } πŸ’’ rejected:`, value, 'reset:', reset );
const rej = this.__state.reject;
if( reset ) this.__state.retains = 0;
rej( value );
}
async *repeat( countOrUntil = Infinity, errorCB = err => false ){
const testFunction = ( typeof countOrUntil === 'function' )
? countOrUntil
: undefined;
const count = testFunction ? Infinity : countOrUntil;
console.log(count,'zzz', testFunction);
let i = 0;
while( i < count ){
try {
const value = await this.reset();
console.debug(`πŸ”„ ${ this.name } πŸ”‚ yield [${ i }]:`, value );
if( false === testFunction?.( value )) break;
else i++;
yield value;
} catch( error ) {
if( errCB( error )){
console.debug(`πŸ”„ ${ this.name } πŸ’’ repeat error:`,error);
} else {
throw error;
}
}
}
console.debug(`πŸ”„ ${ this.name } ⏹️ end repeat.`);
}
// Default implementation will ignore err.
// Make a false return from a callback to throw.
async return( testCB, errCB ){
let value;
do {
try {
value = await this.reset();
console.debug(`πŸ”„ ${ this.name } πŸ”‚ test:`, value );
} catch( error ){
if( !errCB || errCB( error )){
console.debug(`πŸ”„ ${ this.name } ignore:`,error);
} else {
console.debug(`πŸ”„ ${ this.name } πŸ’’ throw:`,error);
throw error;
}
}
} while( !testCB( value ));
return value;
}
// Handy.
get then(){
const promise = this.__state.promise || this.reset();
return promise.then.bind( promise );
}
get catch(){
const promise = this.__state.promise || this.reset();
return promise.catch.bind( promise );
}
subscribe( callback ){
let unsub;
const subscription = new Promise(( res, rej ) => unsub = rej );
setTimeout( async () => {
let subscribed = true;
do {
const result = await Promise.race([
subscription,
this.reset(),
]).then( callback )
.catch(() => subscribed = false );
console.debug(`πŸ”„ ${ this.name } raced:`, result );
} while( subscribed );
console.debug(`πŸ”„ ${ this.name } πŸ›‘ unsubscribed.` );
});
return unsub;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment