npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@yanfoo/suspense

v1.3.5

Published

Wait until something happens

Downloads

9

Readme

Suspense

Extending asynchronous programming with an encapsulated stateful Promise helper.

Rationale

As useful as the Promise implementation is for asynchronous programming, the current interface lacks certain features that are essential in order to have better control over what's waiting what, and how to manage various asynchronous states. For example :

1. A Promise does not handle waiting interruption

Use case

// will wait as long as it takes until valueManager() returns something
// Ex: how can this be aborted from the current context?
const someValue = await valueManager();

// the program might never reach here and there's nothing we can do about it

Solution

const foo = new Suspense();
// ...

// 1. will wait at most 3000 ms for a value before returning with an error,
//    but without changing the state of the suspense or interrupting it's
//    pending execution
try {
   const value = await foo.wait({ timeout: 3000 }); 
   // NOTE : if foo.resolve("foo") is called somewhre, 
   //        then the following conditions will be true :
   // foo.pending === false
   // foo.value === value === "foo"
} catch (error) {
   console.error(error);  // 'Suspense timeout (3000 ms)'
   // foo.reject("too long!");  /* optionally reject this Suspense with an error message */
}

// 2. will wait until it is manually aborted externally without changing the
//    state of the suspense. This is useful to manually abort waiting before
//    the specified timeout
try {
   const value = await foo.wait({ onWaiting: abort => setTimeout(abort, 200) })
} catch (error) {
   console.error(error);  // 'AbortError' | 'RejectedError'
   // NOTE : if foo.reject() was called, then fo.wait() will throw a RejectedError
}

console.log(foo.pending);
// -> true      (or false if either foo.reject() or foo.resolve() was called)

2. A Promise cannot abort another Promise

Use case

const searchServices = [
   searchServerA(query),   // returns a Promise
   searchServerB(query),   // returns a Promise
];

// if any search resolves, then the other searche won't be aware of it
const result = await Promise.race(searchServices);

// At this point, how do we know wich Promise has resolved, and how
// to cancel the other? The interface does not expose the pending
// states.

Solution

const searchServices = [
   searchServerA(query),   // returns a Suspense
   searchServerB(query),   // returns a Suspense
];

// if any search resolve, all the others waiting will be aborted
const result = await Suspense.race(searchServices);

// notify other searches to stop and abort.
searchServices.forEach(search => search.pending && search.reject());

3. A Promise does not expose it's internal pending state

Use case

const p = new Promise(promiseHandler);

// display a waiting timer periodically while we wait
const timer = setInterval(() => {
   // there may be a race condition, here, because we cannot
   // guarantee that p has been resolved, but has not actually
   // returned, yet.
   console.log("Still waiting...");
}, 1000);

try {
   const value = await p;
   
   console.log("Promise was resolved with", value);

} catch (error) {
   // why are we throwing, now??
   console.error(error);
} finally {
   clearInterval(timer);
}

Solution

const s = new Suspense({ timeout: 1000 });  // do not wait for more than 1 sec each time

// we can use a normal loop now!
while (s.pending) {
   try {
      const value = await s.wait();  // will wait for 1 second

      console.log("Suspense was resolved with", value);

   } catch (error) {
      // Proper state management. We could event have a counter
      // and manually call s.reject() to break out of the loop.
      if (error instanceof TimeoutError) {
         console.log("Still waiting...");
      } else if (error instanceof AbortError) {
         console.log("Waiting has been aborted!");
         break;
      } else if (error instanceof RejectedError) {
         console.log("Rejected!");
         break;
      }
   }
}

4. A Promise does not expose it's resolve or reject callbacks

Use case

const EMPTY_QUEUE = [];

// react example
const ConfirmProvider = ({ children }) => {
   const [ confirmQueue, setConfirmQueue ] = useState(EMPTY_QUEUE);
   const contextValue = useMemo(() => ({
      confirm: async (message, { timeout }) => new Promise(resolve => {
         setConfirmQueue(confirmQueue => [ ...confirmQueue, { message, timeout, resolve }]);
         // At this point, we return from this anonymous function and lose
         // reference to the Promise. We could also keep a reference to the
         // reject callback, but the Promise instance is lost. Exposing a
         // function argument to an external control is bad practice.
      })
   }), []);
   
   const { message, timeout, resolve } = confirmQueue?.[0] || {};
   const open = !!message;

   const handleConfirm = useCallback(event => {
      setConfirmQueue(confirmQueue => confirmQueue.length ? confirmQueue.slice(1) : confirmQueue);
      resolve(event.confirmed);
   }, [resolve]);

   return (
      <ConfirmContext.Provider={ contextValue }>
         { children }

         <ConfirmDialog 
            open={ open } 
            message={ message } 
            timeout={ timeout }
            onConfirm={ handleConfirm }
         />
      </ConfirmContext.Provider>
   );
}

Solution or alternative implementation

const EMPTY_QUEUE = [];

// react example
const ConfirmProvider = ({ children }) => {
   const [ confirmQueue, setConfirmQueue ] = useState(EMPTY_QUEUE);
   const contextValue = useMemo(() => ({
      confirm: async options => {
         const suspense = new Suspense({ name:'Confirm', ...options })

         setConfirmQueue(confirmQueue => [ ...confirmQueue, { message, suspense }]);
         
         // We do not lose the reference to the Promise since it is encapsulated within
         // the wait method. The resolve or reject methods are external to the Suspense,
         // so we do not need to break these functions to external controls.
         return suspense.wait();
      }
   }), []);
   
   const { message, suspense } = confirmQueue?.[0] || {};
   const open = !!message;

   const handleConfirm = useCallback(event => {
      setConfirmQueue(confirmQueue => confirmQueue.length ? confirmQueue.slice(1) : confirmQueue);
      suspense.resolve(event.confirmed);
   }, [suspense]);

   return (
      <ConfirmContext.Provider={ contextValue }>
         { children }

         <ConfirmDialog 
            open={ open } 
            message={ message } 
            timeout={ suspense?.timeout }
            onConfirm={ handleConfirm }
         />
      </ConfirmContext.Provider>
   );
}

Usage

Exemple 1

Suspenses are useful when asynchronously initializing a module while still exporting a public API.

import Suspense, { TimeoutError } from '@yanfoo/suspense';

const cache = {};   // a map of Suspense instances

const setValue = (key, value) => {
   if ((key in cache) && cache[key].pending) {
      cache[key].resolve(value);
   } else {
      cache[key] = Suspense.resolved(value);
   }
};


const getValue = async (key, defaultValue) => {
   if (!(key in cache)) {
      cache[key] = new Suspense({ timeout: 1000 });
   }

   return cache[key].wait().catch(error => {
      if (error instanceof TimeoutError) {
         console.log("Timeout, returning default");
         return defaultValue;  // ingore timeout, return default value
      } else {
         throw error;  // re-throw so getValue() will throw
      }
   });
};

// simulate async iniitalization process...
setTimeout(() => setValue('foo', 'Hello!'), 500);
// preset value
setValue('bar', 'Awesome!');

getValue('missing', 'fallback').then(value => console.log("1.", value));

// wait for initialization to complete
getValue('foo').then(value => console.log("2.", value));
getValue('foo').then(value => console.log("3.", value));
getValue('foo').then(value => console.log("4.", value));

getValue('bar').then(value => console.log("5.", value));

console.log("Waiting...");
// -> Waiting...
// -> 5. Awesome!
// -> 2. Hello!
// -> 3. Hello!
// -> 4. Hello!
// -> Timeout, returning default
// -> 1. fallback

Example 2

Synchronize independent asynchronous objects by exposing the resources and properly disponsing them.

const res = [
   new Suspense({ name: 'dbInstanceA' }),
   new Suspense({ name: 'dbInstanceB' }),
   // ...
];
const lock = new Suspense({ name: 'transactions' });

// NOTE : this function passes a connection used to execute query.
// When the function returns, the connection is released. If the
// function throws, the transaction is rolled back to ensure data
// integrity.
getTransaction('dbInstanceA', async connectionA => {
   if (res[0].pending) {
      res[0].resolve(connectionA);
      await lock.wait(); // will throw if lock.reject() is called
   }
}).catch(err => res[0].pending && res[0].reject(err));

// NOTE : the catch, here, is to prevent waiting if getTransaction
// errors and never calls the function, passing the connection
getTransaction('dbInstanceB', async connectionB => {
   if (res[1].pending) {
      res[1].resolve(connectionB);
      await lock.wait(); // will throw if lock.reject() is called
   }
}).catch(err => res[1].pending && res[1].reject(err));

// will jump to the catch section if either getTransaction fails
Suspense.all(res).thne(async ([ connectionA, connectionB ]) => {

   // ... execute queries on both connectionA and connectionB ...

   lock.resolve();   // release both connections now
}).catch(err => {
   lock.reject(err); // trigger a rollback for any acquired connection
});

API

See TypeScript definitions for more information.