Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

limit-once

Package Overview
Dependencies
Maintainers
1
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

limit-once - npm Package Compare versions

Comparing version 0.2.0 to 0.3.0

dist/async-once.d.ts

21

package.json

@@ -13,3 +13,3 @@ {

"license": "MIT",
"version": "0.2.0",
"version": "0.3.0",
"private": false,

@@ -26,12 +26,13 @@ "repository": {

"build:clean": "rimraf dist",
"build:dist": "bun build ./src/once.ts --outfile ./dist/limit-once.js --format esm",
"build:types": "tsc ./src/once.ts --outFile ./dist/limit-once.d.ts --declaration --emitDeclarationOnly --skipLibCheck",
"build:dist": "bun build --entry-points ./src/* --outdir ./dist --format esm --external '*'",
"build:types": "tsc ./src/* --outDir ./dist --declaration --emitDeclarationOnly --skipLibCheck",
"check:prettier": "prettier --debug-check src/**/*.ts test/**/*.ts",
"check:typescript": "tsc --noEmit ./src/** ./test/** --skipLibCheck --target es2022"
},
"module": "./dist/limit-once.js",
"types": "./dist/limit-once.d.ts",
"module": "./dist/once.js",
"types": "./dist/once.d.ts",
"exports": {
".": "./dist/limit-once.js",
"./types": "./dist/limit-once.d.ts"
".": "./dist/once.js",
"./async": "./dist/async-once.js",
"./types": "./dist/once.d.ts"
},

@@ -45,6 +46,10 @@ "files": [

},
"dependencies": {
"bind-event-listener": "^3.0.0"
},
"devDependencies": {
"@types/bun": "latest",
"prettier": "^3.2.5",
"rimraf": "^5.0.7"
"rimraf": "^5.0.7",
"tiny-invariant": "^1.3.3"
},

@@ -51,0 +56,0 @@ "peerDependencies": {

# limit-once
Create a function that caches the result of the first function call. `limit-once` let's you lazily evaluate a value (using a function), and then hold onto the value forever.
Create a `once` function that caches the result of the first function call. `limit-once` let's you lazily evaluate a value (using a function), and then hold onto the value forever.
> [!NOTE]
> This package is still under construction
Features:
- synchronous variant (`0.2 Kb`)
- asynchronous variant for promises (`1Kb`)
- only include the code for the variant(s) you want
- both variants support cache clearing
## Installation
```bash
# yarn
yarn add limit-once
# npm
npm install limit-once
# bun
bun add limit-once
```
## Synchronous variant
```ts
import { once } from 'limit-once';
function sayHello(name: string): string {
function getGreeting(name: string): string {
return `Hello ${name}`;
}
const cached = once(sayHello);
const getGreetingOnce = once(getGreeting);
cached('Alex');
// sayHello called and "Hello Alex" is returned
getGreetingOnce('Alex');
// getGreeting called and "Hello Alex" is returned
// "Hello Alex" is put into the cache.
// returns "Hello Alex"
cached('Sam');
// sayHello is not called
getGreetingOnce('Sam');
// getGreeting is not called
// "Hello Alex" is returned from the cache.
cached('Greg');
// sayHello is not called
getGreetingOnce('Greg');
// getGreeting is not called
// "Hello Alex" is returned from the cache.
```
### Cache clearing (`.clear()`)
You can clear the cache of a onced function by using the `.clear()` function property.
```ts
// is-safari.ts
import { once } from 'limit-once';
// We are caching the result of our 'isSafari()' function as the result
// of `isSafari()` won't change.
export const isSafari = once(function isSafari(): boolean {
const { userAgent } = navigator;
return userAgent.includes('AppleWebKit') && !userAgent.includes('Chrome');
});
function getGreeting(name: string): string {
return `Hello ${name}`;
}
const getGreetingOnce = once(getGreeting);
// app.ts
if (isSafari()) {
applySafariFix();
getGreetingOnce('Alex');
// getGreeting called and "Hello Alex" is returned.
// "Hello Alex" is put into the cache
// getGreetingOnce function returns "Hello Alex"
getGreetingOnce('Sam');
// getGreeting is not called
// "Hello Alex" is returned from cache
getGreetingOnce.clear();
getGreetingOnce('Greg');
// getGreeting is called and "Hello Greg" is returned.
// "Hello Greg" is put into the cache
// "Hello Greg" is returned.
```
## Asynchronous variant
Our async variant allows you to have a `once` functionality for functions that `Promise`.
```ts
import { onceAsync } from 'limit-once/async';
async function getLoggedInUser() {
await fetch('/user').json();
}
// We don't want every call to `getLoggedInUser()` to call `fetch` again.
// Ideally we would store the result of the first successful call and return that!
const getLoggedInUserOnce = asyncOnce(getLoggedInUser);
const user1 = await getLoggedInUserOnce();
// subsequent calls won't call fetch, and will return the previously fulfilled promise value.
const user2 = await getLoggedInUserOnce();
```
## Installation
A "rejected" promise call will not be cached and will allow the wrapped function to be called again
```bash
# yarn
yarn add limit-once
```ts
import { onceAsync } from 'limit-once/async';
# npm
npm install limit-once
let callCount = 0;
async function maybeThrow({ shouldThrow }: { shouldThrow: boolean }): Promise<string> {
callCount++;
# bun
bun add limit-once
if (shouldThrow) {
throw new Error(`Call count: ${callCount}`);
}
return `Call count: ${callCount}`;
}
const maybeThrowOnce = asyncOnce(maybeThrow);
expect(async () => await maybeThrowOnce({ shouldThrow: true })).toThrowError('Call count: 1');
// failure result was not cached, underlying `maybeThrow` called again
expect(async () => await maybeThrowOnce({ shouldThrow: true })).toThrowError('Call count: 2');
// our first successful result will be cached
expect(await maybeThrowOnce({ shouldThrow: false })).toBe('Call count: 3');
expect(await maybeThrowOnce({ shouldThrow: false })).toBe('Call count: 3');
```
## Cache clearing (`.clear()`)
If multiple calls are made to the onced function while the original promise is still `"pending"`, then the original promise is re-used. This prevents multiple calls to the underlying function.
You can clear the cache of a memoized function by using a `.clear()` function that is on your cached function.
```ts
import { onceAsync } from 'limit-once/async';
async function getLoggedInUser() {
await fetch('/user').json();
}
export const getLoggedInUserOnce = asyncOnce(getLoggedInUser);
const promise1 = getLoggedInUserOnce();
// This second call to `getLoggedInUserOnce` while the `getLoggedInUser` promise
// is still "pending" will return the same promise that the first call created.
const promise2 = getLoggedInUserOnce();
console.log(promise1 === promise2); // "true"
```
### Cache clearing (`.clear()`)
You can clear the cache of a onced async function by using the `.clear()` function property.
```ts
import { once } from 'limit-once';
import { onceAsync } from 'limit-once/async';
function sayHello(name: string): string {
return `Hello ${name}`;
let callCount = 0;
async function getCallCount(): Promise<string> {
return `Call count: ${callCount}`;
}
const cached = once(sayHello);
cached('Alex');
// sayHello called and "Hello Alex" is returned.
// "Hello Alex" is put into the cache
// cached function returns "Hello Alex"
const onced = asyncOnce(getCallCount);
cached('Sam');
// sayHello is not called
// "Hello Alex" is returned from cache
expect(await onced({ shouldThrow: false })).toBe('Call count: 1');
expect(await onced({ shouldThrow: false })).toBe('Call count: 1');
cached.clear();
onced.clear();
cached('Greg');
// sayHello is called and "Hello Greg" is returned.
// "Hello Greg" is put into the cache
// "Hello Greg" is returned.
expect(await onced({ shouldThrow: false })).toBe('Call count: 2');
expect(await onced({ shouldThrow: false })).toBe('Call count: 2');
```
If onced async function is `"pending"` when `.clear()` is called, then the promise will be rejected.
```ts
import { onceAsync } from 'limit-once/async';
async function getName(): Promise<string> {
return 'Alex';
}
const getNameOnce = asyncOnce(getName);
const promise1 = getNameOnce().catch(() => {
console.log('rejected');
});
// cached cleared while promise was pending
// will cause `promise1` to be rejected
getNameOnce.clear();
```

@@ -0,4 +1,10 @@

import { bind } from 'bind-event-listener';
type ResultValue<TFunc extends (this: any, ...args: any[]) => Promise<any>> = Awaited<
ReturnType<TFunc>
>;
export type CachedFn<TFunc extends (this: any, ...args: any[]) => Promise<any>> = {
clear: () => void;
(this: ThisParameterType<TFunc>, ...args: Parameters<TFunc>): Promise<Awaited<ReturnType<TFunc>>>;
(this: ThisParameterType<TFunc>, ...args: Parameters<TFunc>): Promise<ResultValue<TFunc>>;
};

@@ -9,3 +15,3 @@

| { type: 'pending'; promise: Promise<T> }
| { type: 'settled'; result: T };
| { type: 'fulfilled'; result: T };

@@ -15,3 +21,3 @@ export function asyncOnce<TFunc extends (...args: any[]) => Promise<any>>(

): CachedFn<TFunc> {
type Result = Awaited<ReturnType<CachedFn<TFunc>>>;
type Result = ResultValue<TFunc>;

@@ -28,6 +34,9 @@ let state: State<Result> = {

): ReturnType<CachedFn<TFunc>> {
if (state.type === 'settled') {
// TODO: do we need the .resolve?
if (state.type === 'fulfilled') {
// Doing a Promise.resolve() so that
// this function _always_ returns a promise.
return Promise.resolve(state.result);
}
// while the promise is pending, all folks
if (state.type === 'pending') {

@@ -38,3 +47,3 @@ return state.promise;

const promise: Promise<Result> = new Promise((resolve, reject) => {
controller.signal.addEventListener('abort', () => reject(), { once: true });
const cleanup = bind(controller.signal, { type: 'abort', listener: () => reject() });

@@ -44,3 +53,3 @@ fn.call(this, ...args)

state = {
type: 'settled',
type: 'fulfilled',
result,

@@ -54,3 +63,7 @@ };

reject(...args);
});
})
// this isn't needed for functionality,
// but it seems like a good idea to unbind the event listener
// to prevent possible memory leaks
.finally(cleanup);
});

@@ -68,5 +81,5 @@

controller.abort();
// TODO: need this?
// Need to create a new controller
// as the old one has been aborted
controller = new AbortController();
// nothing to do
state = {

@@ -73,0 +86,0 @@ type: 'initial',

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc