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

idmp

v1.16.0

Published

[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/ha0z1/idmp/deploy.yml)](https://github.com/ha0z1/idmp/actions) [![npm](https://img.shields.io/npm/v/idmp.svg)](https://www.npmjs.com/package/idmp) [![codecov](ht

Downloads

290

Readme

idmp

GitHub Workflow Status (with event) npm codecov contributors LICENSE Size

一个优雅地解决幂等(idempotent) 函数的重复和并发调用的小库,纯函数,Gzip 后不到 1 KB

English | 简体中文

使用

基础用法

import idmp from 'idmp'

const getInfo = async () => {
  const API = `https://haozi.me/?api/your-info`
  return await fetch(API).then((d) => d.text())
}

// 只有这一行代码改动
export const getInfoIdmp = () => idmp('/api/your-info', getInfo)

for (let i = 0; i < 10; ++i) {
  getInfoIdmp().then((d) => {
    console.log(d)
  })
}

查看网络控制台,会发现只有 1 个网络请求,但会正确触发 10 次回调。

高级使用

const getInfoById = async (id: string) => {
  const API = `https://haozi.me/?api/your-info&id=${id}`
  return await fetch(API).then((d) => d.json())
}

// 处理有入参的场景
export const getInfoByIdIdmp = (id: string) =>
  idmp(`/api/your-info?${id}`, () => getInfoById(id))

// 或者更通用的类型体操写法,用于复杂的入参,idmp 会自动推导返回值类型,与原函数保持一致
export const getInfoByIdIdmp = (...args: Parameters<typeof getInfoById>) =>
  idmp(`/api/your-info?${JSON.stringify(args)}`, () => getInfoById(...args))

// 增加更多配置项
export const getInfoByIdIdmp = (id: string) =>
  idmp(`/api/your-info?${id}`, () => getInfoById(id), {
    maxAge: 86400 * 1000,
  })

然后用 getInfoByIdIdmp 替换 getInfoById 方法。

插件

idmp 有一个强大的插件系统。 以下是官方维护的插件列表,您也可以参考源码创建自己的插件:

插件可以以非侵入性方式扩展 idmp 的核心功能,类似于数学函数 $g(f)(x)$。 这种优雅的设计为插件系统提供了极大的灵活性和可扩展性。

Options

declare const idmp: {
  <T>(
    globalKey: IdmpGlobalKey,
    promiseFunc: IdmpPromise<T>,
    options?: IdmpOptions,
  ): Promise<T>
  flush: (globalKey: IdmpGlobalKey) => void
  flushAll: () => void
}

type IdmpPromise<T> = () => Promise<T>
type IdmpGlobalKey = string | number | symbol | false | null | undefined

IdmpOptions:

| Property | Type | Default | Description | | --------------- | ---------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | maxRetry | number | 30 | Maximum number of retry attempts. | | maxAge | number | 3000 | Maximum age in milliseconds. The maximum value is 604800000ms (7 days). | | onBeforeRetry | function | - | Function to be executed before a retry attempt. Takes two parameters: err (any type) and extra (an object with properties globalKey of type IdmpGlobalKey and retryCount of type number). Returns void. |

flush

flushidmp 的静态方法,会立即清除缓存,使得临近的下一次调用不使用缓存。

flush 接受一个 globalKey,没有返回值,重复调用或者 flush 一个不存在的 globalKey 不会有任何提示

const fetchData = () => idmp('key', async () => data)

idmp.flush('key')
fetchData().then(...) // will skip cache

flushAll

flushAllidmp 的静态方法,会立即清除所有缓存,使得临近的下一次所有调用都不使用缓存。

flushAllflush 一样是幂等函数,无入参和返回值,多次执行不会有任何提示。


const fetchData1 = () => idmp('key1', async () => data1)
const fetchData2 = () => idmp('key2', async () => data2)

idmp.flushAll()

fetchData1().then(...) // will skip cache
fetchData2().then(...) // will skip cache

通过 flush 或者 flushAll 可以做一些工作,比如点击了保存按钮后自动刷新列表,这时候应该强行从服务器拿最新的数据渲染。

在 React 中去重请求

在 React 共用请求,可以使用 swr 、 Provider 以及更为复杂的专业状态管理库来复用数据。但存在以下几种问题:

  1. swr: 需要将所有的请求变更为 hooks,不能嵌套和条件分支,对于已有项目有改造成本,下文还会提到更复杂的场景。
  2. Provider 数据共享: 需要一个中心化的数据管理。数据中心无法感知到哪些模块会消费数据,需要长期维护这些数据,而不敢及时删除
  3. Redux 等状态管理库:应该专注的是状态的变化和时序,而非共享数据。idmp 让你更关注于局部状态

查看 demo源码

这样当模块 A 或者模块 B 的代码删除后,是不需要维护他们的缓存的。

模块 A 和 B 有了更大的独立性,可以跨工程复用,而不必包裹在特定的 Provider 下。

在 Hooks 中请求数据的局限性

import useSWR from 'swr'

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher)

  if (error) return <div>failed to load</div>
  if (isLoading) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

swr 的官网示例很优雅,然而实际中一个视图展示很可能并非只来自一个数据源。由于 Hooks 无法嵌套和条件分支。假设有两个接口,B 依赖 A 的结果为入参,代码将迅速劣化成下面形式:

...
const { data: dataA } = useSWR('/api/a', fetchA)
const { data: dataB } = useSWR(dataA ? `/api/b${JSON.stringify(dataA)}` : null, () => dataA ? fetchB(dataA): null)
...

这还没有处理异常状况,还只是 2 个接口, 如果有 n 个相关联接口,其代码复杂度是以 $O(2^n)$速度劣化的

$$ C_{n}^{0} + C_{n}^{1} + C_{n}^{2} + ... + C_{n}^{n} = 2^n $$

这里有几种优化形式:

  1. 放弃 swr, 改用在 useEffect 中请求,这样 swr 带来的收益就没有了,并且即使 useEffect 的第二个参数传空数组,依然可能出现重复请求的问题,详见https://github.com/ha0z1/idmp/blob/main/demo/Item.tsx#L10
  2. 封装 fetchAB 方法,串行请求后一次性返回,在 Hooks 里只调用一个 fetchAB。这里将会造成只依赖 dataA 的视图要等待串行完成后才能展示。另外,一般 dataA 数据很可能是一些公用数据,可能还要封装 fetchAC、fetchABC 等场景,这里面将造成 dataA 的数据请求发生多次

由于 idmp 是纯函数,可以在 Hooks 之外调用,可以很好地配合 swr 完成这样的工作。我们无脑封装两个接口 fetchAIdmp 和 fetchBIdmp:

const fetchAIdmp = () => idmp('/api/a', fetchA)

const fetchBIdmp = async () => {
  const dataA = await fetchAIdmp()
  const dataB = await idmp(`/api/b+${JSON.stringify(dataA)}`, () =>
    fetchB(dataA),
  )
  return dataB
}

然后在 Hooks 里用 swr 同步调用这两个“无依赖”的 fetcher 就好了

...
const { data: dataA } = useSWR('/api/a', fetchAIdmp)
const { data: dataB } = useSWR('/api/b', fetchBIdmp)
...

由于消解了相互间的排列组合,复杂度降低到 $O(n)$

$$ C_{n}^{0} + C_{n}^{0} + C_{n}^{0} + ... + C_{n}^{0} = n $$

当哪天页面不需要直接消费 dataA 的数据时,直接删除请求 dataA 的代码就好了,没有任何心智负担。

鲁棒性

假设一个接口的请求失败率为 10%, 那么通过 3 次尝试后,请求仍失败的可能性将降到 0.1%

使用 idmp 包装的接口,内部会自动在超时或失败后进行重试,这会大大降低异常情况的出现。在每次重试前,你可以通过 onBeforeRetry 勾子函数监听异常,便于做一些埋点统计(注意,它不会捕获最后一次错误)

const getUserData = idmp(
  'using a deduplicated string as a key',
  async () => {
    await fetch(xxx)
  },
  {
    onBeforeRetry: (rejectReason) => {
      log(rejectReason)
    },
    maxRetry: 30, // default
  },
)

idmp 内部实现了类似 指数退避的算法,会动态改变重试时间,避免对服务器造成 DDoS。

优化大计算

虽然 idmp 的第二个参数必须是一个 Promise 函数,但由于同步函数都可以方便地包装成 Promise 对象。故 idmp 除了可以缓存网络请求外,原则上可以缓存任何函数调用。

这是一个没有经过任何优化的斐波那契数列的示例, 算到 45 项大约需要 10s:

const fib = (n) => {
  if (n <= 2) return 1

  return fib(n - 2) + fib(n - 1)
}

const fibIdmp = (n) => idmp(`fib${n}`, async () => fib(n), { maxAge: Infinity })

for (let i = 0; i < 100; i++) {
  fibIdmp(40).then(console.log)
}

缓存后,调用 100 次,实际只计算了 1 次,其他 99 次都是 $O(1)$ 性能的查表。

不可变数据

由于 js 的数据可变性,缓存的数据如果被外部修改,将导致后续的数据不一致,所以 idmp 不允许对返回数据进行写操作。 在开发环境中,会使用 Object.freeze 递归冻结数据,但为了线上运行时性能,这个检查会被忽略。

这应该是最精巧地解决方案了,避免了运行时对数据的深拷贝,也使得 idmp 不光能缓存 JSON 数据,也能缓存更复杂的数据结构。

requestIdmp().then((data) => {
  data.hello = 'world' // Not allow
  const newData = { ...data }
  newData.hello = 'new world' // Allow
  // 注意:由于 js 的特性,对 newData.aaa.bbb 进行写操作,
  // 仍然会改变原始数据,这个在开发阶段也会抛错。
})

配置项不可变

下面这种写法是不允许的:

const config = {
  maxAge: 5000,
}
const getInfoIdmp = () => idmp('/api/your-info', getInfo, config)

getInfoIdmp().then(() => {
  config.maxAge = 0
})

因为这会造成多次调用后,行为可能被外部修改,造成逻辑不一致。这个也会在开发环境下进行自动检测。如果希望执行某些操作后刷新缓存,应该使用 idmp.flushidmp.flushAll 方法

不合适场景

函数内部会进行重试操作、会缓存请求数据, 故不适合以下场景:

  • 非幂等的请求:如 POST/PATCH。注: HTTP 协议只是语义规范,事实上也可以把 GET 实现成非幂等,POST 实现成幂等,在使用前需要自行判断是否真幂等
  • 不能缓存的请求:如每次都要交换新的 token、获取随机种子
  • 短于 16ms 的时效性数据,如获取服务器精准时间

注意:将 maxAge 设为 0 依然会在短时间内缓存数据,因为内部使用了 setTimeout(..., maxAge)清理缓存,而 js 的 setTimeout 是不精准的,且它是一个宏任务慢于微任务。

另外,设置成 0 依然会进行请求重试,可以用它来实现一些对接口健壮性要求高、实效性不严苛的场景。

如果想完全不缓存结果,请把第一个参数设置成假值:'' | false | null | undefined | 0,这时候会完全退化成原始函数,不做失败重试。

idmp(`xxx`, fetchData, { maxAge: 0 }) // 仍会在短时间内共享数据,仍会进行重试操作
idmp(null, fetchData) // 将无视所有配置项,与直接执行 fetchData 完全相同

实现原理

idmp 的核心原理是共用了一块内存地址,使用唯一标识符确定是同一函数的重复调用。 每个 Promise 的 resolve 和 reject 会被记录下来,内部维护了一个状态机,在 fulfilled 或 rejected 时完成回调。

另外,代码在开发环境(process.env.NODE_ENV !== "production")中,使用了非常 geek 的方式判断有没有全局重复使用了相同的 key 值,有兴趣的可以自行阅读源码。

注意事项

idmp 的核心原理是全局维护了一个共用缓存空间和状态机,由于 js 里无法快速比较两个对象实例是否全等,不得不采用了全局 KEY 的方式,所以一定要有一个全局唯一 KEY。

KEY 的可选值类型为 string | number | symbol、以及一值假值 false | null | undefined | '' | 0,注意,0 和 空字符串'' 是作为假值使用的,不会有任何缓存及重试效果。

如果一个方法需要不同的参数进行多次调用,应当使用不同的 KEY,一个经典的方式是将参数 JSON.stringify

const getInfo = async (options) => {
  const { id } = options
  const API = `https://google.com/api/your-info?id=${id}`
  return await fetch(API).then((d) => d.json())
}

export const getInfoIdmp = (options) =>
  idmp(`/api/your-info${JSON.stringify(options)}`, () => getUserData(options))

在开发态环境下,内置了一个检查,警告在不同地方使用了相同的 KEY。如果不同的 Promise 分配了相同的 KEY,可能造成不符合预期的结果。

如果你有更复杂的网络需求,如自动刷新、本地与远端数据竞选等,idmp 由于是纯函数,无法实现相关功能,可以尝试 swrswrv