GithubHelp home page GithubHelp logo

Comments (15)

isaacs avatar isaacs commented on June 10, 2024 4

Switched to Promise<V | undefined> in v10.

from node-lru-cache.

isaacs avatar isaacs commented on June 10, 2024 1

Well, the issue is that the promise might resolve without a value if the fetchMethod doesn't return a value when it's aborted. Which, there's no reason to require it to return undefined rather than void, especially since JavaScript doesn't even make a distinction between the two.

What problem does it cause to have void there?

At the very least, removing the void would be a semver major change.

Here's an example:

import { LRUCache } from './'
const c = new LRUCache<number, string>({
  max: 3,
  fetchMethod: async (key, _oldValue, { signal }) => {
    await new Promise<void>(res => setTimeout(() => res(), 100))
    if (signal.aborted) {
      console.error('aborted, returning void', key)
      // this would have to be `return undefined`, whereas `return` is allowed now,
      // because fetch() resolves to the promise that the fetchMethod returns.
      return
    }
    return String(key)
  },
  ignoreFetchAbort: true,
  allowStaleOnFetchAbort: true,
})

Promise.all([
  c.fetch(1),
  c.fetch(2),
  c.fetch(3),
  c.fetch(4),
]).then(console.log)
$ ts-node fetch-void.ts 
aborted, returning void 1
[ undefined, '2', '3', '4' ]

from node-lru-cache.

Thenkei avatar Thenkei commented on June 10, 2024

I just run into the same issue. Worst case scenario it should be undefined | V isn't it?

from node-lru-cache.

isaacs avatar isaacs commented on June 10, 2024

If the fetch is aborted due to the key being evicted or deleted prior to the fetchMethod returning, then it might not return a value.

It's an edge case, but it is possible, and the previous declared type was incorrect by not including it.

For full type safety, test that the return type is the type you want, and handle the error case. If you know that can't happen and wanna yolo it, you can coerce it with an explicit as.

from node-lru-cache.

Thenkei avatar Thenkei commented on June 10, 2024

If you know that can't happen and wanna yolo it, you can coerce it with an explicit as.

Indeed that is what I've done in my case but to be fair it doesn't seem like a really good practice. If you Lru-cache change the behavior (and the returned types) of the fetch method I won't be warned.

Could returning undefined or null in this case make everyone happy?

from node-lru-cache.

Thenkei avatar Thenkei commented on June 10, 2024

What problem does it cause to have void there?

That's just an issue when writing pure TS but I guess that void is more appropriate when nothing occurs (aborted).

Monkey patch - We don't want that fetch return void but undefined instead

@HuyAms we can use a monkey patch like this in our case.

class LRUCacheMonkeyPatched<K extends {}, V extends {}, FC extends unknown = unknown> extends LRUCache<K, V, FC> {
  override async fetch(
    k: K,
  ): Promise<undefined | V> {
    const data = await super.fetch(k as never);
    
    // When we fetch void let's return undefined
    if (typeof data === 'undefined') {
      return undefined;
    }

    return data;
  }
}

Or even more straight forward

 override async fetch(
    k: K,
  ): Promise<undefined | V> {
    return super.fetch(k as never) as unknown as undefined | V;
  }

from node-lru-cache.

isaacs avatar isaacs commented on June 10, 2024

I write pure TS, but what's the issue, exactly? What situation are you using it in, and what error occurs? I'm ok with making it undefined instead of void, just want to understand why.

The reason I'm pushing back a bit on this is that it means changing the type signature of a public method in an incompatible way, so it's a SemVer breaking change, as it may require downstream users to update their code.

from node-lru-cache.

Thenkei avatar Thenkei commented on June 10, 2024

I hit the issue working on this PR for an open-source project. I ended up doing the yolo mode (coercing it with an explicit as), but that's not our standard/guidelines.

If we don't use the explicit cast. We can't use the optional chaining operator to explicitly say "That's ok, this value might be null or undefined".

image
permissions has just been fetched from the LRU cache. If for some normal reason or obscure reason, it doesn't resolve an expected value we handle this using some optional chaining operator

from node-lru-cache.

ghusse avatar ghusse commented on June 10, 2024

According to the TS documentation:

void represents the return value of functions which don’t return a value. It’s the inferred type any time a function doesn’t have any return statements, or doesn’t return any explicit value from those return statements:

The fetch function has multiple return statements, and every one of them is returning a value. The keyword void is not appropriate here according to the documentation.

The problem with void when using this function is that TS does not treat it as if it was undefined, even if in JS there is no difference. So in this case void | V is not equivalent to undefined | V and it does not cast easily to this type.

from node-lru-cache.

isaacs avatar isaacs commented on June 10, 2024

@ghusse It doesn't return actual void, it returns Promise<V | undefined | void>, and there is a code path where it returns the return value of fetchMethod, which may be a Promise<void>. "Return" is being used as a shorthand for "return a promise that resolves to", because it's an async function.

from node-lru-cache.

dennyok avatar dennyok commented on June 10, 2024

Hey there. I see the reason for having void as one of the possible promise outcomes. However, testing whether an actual value is returned becomes a bit harder as we cannot simply check for undefined. Instead, we have to check for what we expect (for example, an object). At least I had some problems satisfying TS in this regard.

Below is an example of a simple wrapper which takes a function, initialises a cache and returns a cached version of the input function (same signature). Upon void, an error is thrown, assuming we don't expect aborts and the like. This may help.

export function memoizeLru<Fn extends (...args: Args) => Promise<Result>, Args extends any[], Result extends object>(
  fn: Fn,
  options:
    | Omit<LRUCache.OptionsMaxLimit<string, Result, { args: Args }>, 'fetchMethod'>
    | Omit<LRUCache.OptionsTTLLimit<string, Result, { args: Args }>, 'fetchMethod'>,
  resolveKey: (...args: Args) => string
): (...args: Args) => Promise<Result> {
  const cache = new LRUCache<string, Result, { args: Args }>({
    ...options,
    fetchMethod: async (_key, _staleValue, { context }) => {
      return await fn(...context.args);
    }
  });

  return async (...args: Args) => {
    const result = await cache.fetch(resolveKey(...args), { context: { args } });
    if (!(result instanceof Object)) {
      // result might be of type void (in case of abort)
      // should never happen here (we use no signals)
      throw new Error('fetch aborted');
    }
    return result;
  };
}

Hey, and thanks for this great lib ❤️

from node-lru-cache.

ghusse avatar ghusse commented on June 10, 2024

The TS doc is pretty clear about the usage of void : as soon as a function CAN return something, void should not be used

from node-lru-cache.

dennyok avatar dennyok commented on June 10, 2024

@ghusse There is a difference between returning void and Promise<void> and Promise<void|X|Y>. Apart from that, whether fn's return type is void or contains void solely depends on what TS infers based on the paths and return statements of the function, which means that you cannot choose to use void or not. However, you can choose not to mix returns without a value and returns with a value in the same function. If that's what you mean, I haven't seen a statement in the TS docs which forbids this. Maybe there is a linter rule out there ...

from node-lru-cache.

isaacs avatar isaacs commented on June 10, 2024

Ok, so, semantic details about what specifically void means, when to use it, whether it's always/never/ever a good idea, etc, is kind of off topic here.

The point is, LRUCache.fetch() can return Promise<void> because it returns the promise returned by the fetchMethod option, and that can return Promise<void>. It's not an ideological point worth debating, tbh, it was just allowed in the JS version for the function to not return anything when it receives an abort signal, and it got ported through to the TS version. TS is weird about this, it is what it is.

It's not super hard to make it Promise<V | undefined> without changing the fetchMethod signature qv. But it does of course change the fetch() signature, so it's a semver major change.

It'll be fixed in version 10. I've been dragging my feet just because I wanted to make sure it's worth the disruption when bumping major on a project like this, and also because I'm feeling lazy, but I guess it's time to update all of my projects again lol.

from node-lru-cache.

ghusse avatar ghusse commented on June 10, 2024

👍 thanks a lot for the update

from node-lru-cache.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.