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 🙏

© 2026 – Pkg Stats / Ryan Hefner

mooncat-checkpoint

v0.5.1

Published

轻量级 step 幂等/断点续跑 checkpoint 服务。默认文件存储,SQLite 可选,全异步 API,状态机判别联合。不依赖 daemon,不是 workflow engine。

Readme

mooncat-checkpoint

轻量级 step 幂等 / 断点续跑 checkpoint。 默认文件存储,SQLite 可选,全异步 API,状态机判别联合,lease fencing token。 不是 workflow engine,不是 daemon——只是一个库 + 可选 CLI。

它解决什么

flow / 批处理 / 采集里最常见的痛点:"这一步跑过了没?跑过了就跳过,没跑过就干活并标记"。 没有它,每个项目都手写一份 if (exists(cache)) skip else run + save,而且并发时会撞车。

import { CheckpointClient } from "mooncat-checkpoint";

const cp = new CheckpointClient();
const c = await cp.claim("vision", imageHash, { ownerRunId: "run-1" });
switch (c.kind) {
  case "done":    return c.checkpoint.result;      // 跑过了,复用结果
  case "busy":    return;                           // 别的 run 正在跑,跳过
  case "failed":  return;                           // 之前失败了,需 reset 才能重试
  case "claimed": break;                            // 你抢到了,拿 c.leaseToken 去干活
}
try {
  const result = await expensiveCall();
  await cp.done("vision", imageHash, { leaseToken: c.leaseToken, result });
} catch (e) {
  await cp.fail("vision", imageHash, e.message, { leaseToken: c.leaseToken });
  throw e;
}

状态机(严格 + fencing token)

none(不存在)→ claimed(leaseUntil, leaseToken)→ done | failed
                     ↑                                  终态
                     └── lease 过期可重新 claim(生成新 token)
  • done:已完成。claim 直接返回 done + result,不重跑。
  • claimed:正在处理。带 lease + leaseToken。到期后别人可抢(新 token)。
  • failed:失败终态。claim 返回 failed不会自动重试——重试必须显式 reset

严格状态机done/fail 要求记录处于 claimed 且 leaseToken 匹配,否则抛 LeaseLostError。 不支持"无 claim 直接建终态"(防语义不一致)。

API

claim(ns, key, { ownerRunId?, leaseMs? })            → ClaimResult(claimed kind 含 leaseToken)
touch(ns, key, { leaseToken, extendMs? })             → Checkpoint | null   // 续 lease
done(ns, key, { leaseToken, result?, resultRef? })    → Checkpoint           // token 不匹配抛 LeaseLostError
fail(ns, key, error, { leaseToken })                   → Checkpoint           // token 不匹配抛 LeaseLostError
get(ns, key)                                           → Checkpoint | null
reset(ns, key)                                         → boolean              // 删记录,允许重跑
list(ns, { status? })                                  → Checkpoint[]
gc({ olderThan? })                                     → number               // 清过期 lease

lease fencing(防 lease 过期后旧 owner 覆盖新 owner)

claim 成功返回 leaseToken(32 字符 hex)。done/fail/touch 必须回传这个 token, backend 只在 token 匹配当前 claimed 记录时才允许变更。

为什么需要:A claim → lease 过期 → B 重抢成功。此时 A 完成工作调 done, 若无 token 校验,A 会把 B 的 claimed 记录改成 done,破坏"过期可重抢"语义。 fencing token 保证 A 的旧 token 被拒绝(抛 LeaseLostError),B 的工作不被覆盖。

import { LeaseLostError } from "mooncat-checkpoint";
try {
  await cp.done("ns", key, { leaseToken, result });
} catch (e) {
  if (e instanceof LeaseLostError) {
    // lease 已不属于你(被重抢或过期),你的工作成果可能已被接管,放弃
    return;
  }
  throw e;
}

claim 的四种结果(判别联合)

claim 返回 { kind, checkpoint }kind"done" | "claimed" | "busy" | "failed"claimed 额外带 leaseToken。用 switch(c.kind) 处理,编译器保证分支完备。

result vs resultRef

  • result:内联小结果(≤ 64KB UTF-8 字节)。claim 命中 done 时直接拿回,零摩擦。 不可 JSON 序列化(BigInt/循环引用)抛 CheckpointResultSerializationError
  • resultRef:大产物引用(如 artifact 文件路径)。超过 64KB 必须用 resultRef。
await cp.done("vision", hash, {
  leaseToken,
  result: { count: 42 },                  // 小结果,内联
  resultRef: "artifacts/vision/abc.png",  // 大产物,引用
});

lease + touch(长 step 必须续期)

claim 默认 lease 60 秒。如果你的 step 跑 10 分钟,必须定期 touch 续期(带 leaseToken),否则 lease 到期会被别的 run 抢走:

const interval = setInterval(() => cp.touch(ns, key, { leaseToken, extendMs: 60000 }), 30000);
try {
  await longRunningWork();
  await cp.done(ns, key, { leaseToken, result });
} finally {
  clearInterval(interval);
}

后端

| 后端 | URL 格式 | 并发安全 | 说明 | |------|----------|---------|------| | file(默认) | file://./.mooncat/checkpoints | 单进程 | JSON 文件,透明可读,可 git 跟踪 | | sqlite | sqlite://./checkpoints.db | 跨进程 | WAL + 事务原子,推荐用于多 run 并发 | | memory | memory:// | 单进程 | 进程内 Map,测试用,退出丢失 |

优先级:显式 url 参数 > MOONCAT_CHECKPOINT_URL 环境变量 > 默认 file://./.mooncat/checkpoints

文件后端 vs SQLite

  • file:单进程/低并发。透明可读(JSON 直接看/手改/git)。跨进程只靠 lease 语义(过期可抢),不保证强一致。
  • sqlite:多 run 并发。单文件、无服务、WAL 模式。claimBEGIN IMMEDIATE 事务跨进程原子。 需要可选依赖 better-sqlite3(原生模块,需编译)。
// 默认 file
import { CheckpointClient } from "mooncat-checkpoint";
const cp = new CheckpointClient();

// 指定 sqlite
const cp = new CheckpointClient({ url: "sqlite://./checkpoints.db" });

安装 SQLite 支持

npm i better-sqlite3   # 原生模块,需要编译工具链

未安装时 SQLite 后端不可用(构造函数抛清晰错误),file/memory 后端不受影响。

CLI

mooncat-checkpoint url                                    # 打印当前后端 URL
mooncat-checkpoint list <ns> [--status done|failed|claimed]
mooncat-checkpoint get <ns> <key>
mooncat-checkpoint reset <ns> <key>
mooncat-checkpoint gc [--older-than <ms>]
# 所有命令支持 --url <backend-url> 覆盖后端

文件后端的目录结构

.mooncat/checkpoints/
  vision/                          ← namespace
    img-001.json                   ← key(安全字符直接用文件名)
    sha256-a1b2c3....json          ← 含特殊字符的 key → sha256 hash 文件名
  sycm-export/
    overview.json
    daily-data.json

namespace 只允许 [a-zA-Z0-9_-]+(防 path traversal)。key 任意,安全字符直接用文件名,否则 hash。 文件名用 hash,但 JSON 内 record.key 存原始 key——get/list 始终返回原始 key,三后端一致。

和 workflow engine 的关系

mooncat-checkpoint 只管状态(claim/done/fail),不管调度、不管编排、不管执行。 它是一个可被任何 flow runner(Inngest / Dagu / 自定义循环)调用的状态原语。

workflow engine(调度/编排)→ 调用 → checkpoint(状态原语)
                                    ↕
                                  后端(file/sqlite/memory)

边界清晰:engine 换了,checkpoint 不用动;checkpoint 换后端,engine 不用动。

设计决策

  1. 全异步:file/sqlite/redis 的共同接口只能是 async。这不是 mooncat state.checkpoint(同步)的 drop-in 替换——是 API 迁移。
  2. fencing token:done/fail/touch 必须 leaseToken 匹配,防 lease 过期后旧 owner 覆盖新 owner 的 claim。
  3. 严格状态机:done/fail 要求 claimed 态 + token 匹配,不支持"无 claim 直接建终态"。
  4. failed 是终态:失败不自动重试(高危场景轰炸风险)。重试必须显式 reset。
  5. 不需要 daemon:checkpoint 无常驻资源(不像 browserd 持有 Chrome 会话),是库 + CLI。
  6. file 不保证跨进程强一致:单进程靠 MutexMap,跨进程靠 lease 语义。需要跨进程原子 → SQLite。
  7. result 软上限 64KB(UTF-8 字节):超过拒绝内联,强制用 resultRef。防止 JSON 文件膨胀。
  8. key 一致性:三后端 get/list/reset 始终用原始 key(sqlite 用 storage_key 作 PK 但 record.key 返回原值)。

License

MIT