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

@protoplasm/recall

v0.2.4

Published

lightweight memoization

Downloads

32,213

Readme

recall

npm i @protoplasm/recall

this package lets you memoize functions and generators.

main entry points:

  • recall memoizes functions
  • replay memoizes generators
  • report reports non-fatal errors and messages which will also be memoized by recall and replay

recall memoizes functions

recall takes a function and returns a memoized version:

import recall from '@protoplasm/recall'

let calls = 0;
const hi = recall((data?: any) => {
  ++calls
  return { hello: 'world', data }
})

expect(hi()).toBe(hi())
expect(calls).toBe(1)

expect(hi(42)).toBe(hi(42))
expect(calls).toBe(2)

recalled functions cache both normal and exceptional return paths. if the underlying function throws the first time it's called for a set of arguments, it will always throw the same error when invoked again.

arguments are shallowly compared by ===. there is no way to change this.

fn.getResult returns the stored result

recalled functions also make Results available via a .getResult method. getResult has the same signature as the underlying function, but returns Result<T> rather than T:

const evenSquare = recall((n: number) => {
  if (n % 2 !== 0) throw new Error('odd numbers unsupported')
  return n ** 2
})

evenSquare.getResult(2).isReturn() // -> true
evenSquare.getResult(2).data       // -> 4
evenSquare.getResult(2).unwrap()   // -> 4

evenSquare.getResult(3).isReturn() // -> false
evenSquare.getResult(3).isThrow()  // -> true
evenSquare.getResult(3).error      // -> Error: odd numbers unsupported
evenSquare.getResult(3).unwrap()   // !! throws Error: odd numbers unsupported

fn.getExisting soft queries the cache

the .getExisting method works just like .getResult, only it will return undefined rather than calling the underlying if no entry for the arguments exists in the cache.

replay memoizes generators

if you try to use recall on a generator, you're gonna have a bad time:

const gen = recall(function *() {
  yield 1
  yield 2
  yield 3
})
gen() === gen() // -> true
[...gen()]      // -> [1, 2, 3]
[...gen()]      // -> []  (shit.)

this happens because generators return iterators, which are consumed as you iterate over them.

replay fixes this:

const gen = replay(function *() {
  yield 1
  yield 2
  yield 3
})
gen() === gen() // -> true
[...gen()]      // -> [1, 2, 3]
[...gen()]      // -> [1, 2, 3]

replay wraps the generator in an Iterable which lazily stores each value the generator emits.

reporting errors

report lets functions report messages independent of how they return. for example, you can report errors while still returning data:

import { report } from '@protoplasm/recall'

function errorsAndData() {
  report(new Error('something bad happened'))
  report(new Error('something else bad happened'))
  return "but it's still ok"
}

getResult collects all messages reported from a block. it returns these as part of a Result:

import { Result, getResult } from '@protoplasm/recall'

const result: Result<string> = getResult(() => errorsAndData())
for (const error of result.errors()) {
  console.log(error) // -> 'something bad happened'
                     // -> 'something else bad happened'
}
result.unwrap()   // -> 'but it's still ok'

reports bubble up

report works no matter how deep you are in the call tree:

function a() {
  report(new Error("error from a"))
  b()
}

function b() {
  report(new Error("error from b"));
  threeThingsFail();
}

function threeThingsFail() {
  report(new Error("a"));
  report(new Error("b"));
  report(new Error("c"));
}

const result = getResult(() => {
  a()
  report(new Error('one last problem'))
})
result.errors()
  // -> error from a
  // -> error from b
  // -> a
  // -> b
  // -> c
  // -> one last problem

recipes

recall memoizes pure functions

the simplest case:

const sum = recall((ary: number[]) => ary.reduce((a, b) => a + b))
const a = [1, 2, 3, 4]
sum(a)
sum(a) // cache hit

recall composite keys

you can use recall as a composite key map:

const song = recall((artist: string, title: string) => new Song(artist, title))
const coversOf = recall((song: Song) => [])
coversOf(song("Cher", "Believe")).push(song("Okay Kaya", "Believe"))
coversOf(song("Cher", "Believe")).find(song("Okay Kaya", "Believe"))

recall a cache

recall can accept async functions. it simply caches the promise result:

const textOf = recall(async (url: string) => await (await fetch(url)).response.text)
const result = await textOf("https://...)

report multiple errors while returning data

const doStuff = recall(things => {
  let goodThings = []
  for (const thing of things) {
    if (isBad(thing)) {
      report(new BadThingError("this thing is bad:", thing))
    } else {
      goodThings.push(thing)   
    }
  }
  if (!goodThings.length)
    throw new Error("no good things")
  return processKnownGoodThings(goodThings)
})

const output = doStuff
  .getResult(someThings)
  // prints non-fatal errors to console.log and unwraps:
  .unwrap(console.log)

report errors while yielding data

const processedThings = replay(function *(things) {
  for (const thing of things) {
    if (isBad(thing)) {
      report(new BadThingError("this thing is bad:", thing))
    } else {
      yield processGoodThing(thing)
      goodThings.push(thing)   
    }
  }
})

const output = getResult(() =>
    // iterate over all processedThings to collect
    // errors on all of them    
    [...processedThings(someThings)]
  ).unwrap(console.log)

using getResult as a reporting boundary

getResult and fn.getResult do not bubble reports up, so you can use them as error boundaries:

function importantStuff() {
  report(new Error('error in importantStuff'))
}

function optionalSetup() {
  report(new Error('error in optionalStuff'))
}

getResult(() => {
  importantStuff()
  optionalStuff()
}).errors()
  // -> [Error: error in importantStuff]
  // -> [Error: error in optionalStuff]

getResult(() => {
  importantStuff()
  getResult(() => optionalStuff())
    // swallowing the result swallows the error
}).errors()
  // -> [Error: error in importantStuff]

manually bubbling

you can report(someResult.log) to explicitly bubble up messages from someResult.

this is useful if you want to use getResult (for example, to inspect the errors) but still want bubbling to happen:

getResult(() => {
  importantStuff()
  const result = getResult(() => optionalStuff())
  for (const error in result.errors()) doSomethingElseWith(error)  
  report(result.log)
}).errors()
  // -> [Error: error in importantStuff]
  // -> [Error: error in optionalStuff]