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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@loro-dev/peer-lease

v2.0.2

Published

TypeScript library for safely reusing CRDT peer IDs without collisions.

Readme

peer-lease

@loro-dev/peer-lease is a TypeScript library for safely reusing CRDT peer IDs without collisions.

Installation

pnpm add @loro-dev/peer-lease
# or
npm install @loro-dev/peer-lease

Usage

import { LoroDoc } from "loro-crdt";
import { acquirePeerId, tryReuseLoroPeerId } from "@loro-dev/peer-lease";

const doc = new LoroDoc();
// ... Import local data into doc first
const lease = await acquirePeerId(
  "doc-123",
  () => new LoroDoc().peerIdStr,
  JSON.stringify(doc.frontiers()),
  (a, b) => {
    const fA = JSON.parse(a);
    const fB = JSON.parse(b);
    return doc.cmpFrontiers(fA, fB);
  },
);

try {
  console.log("Using peer", lease.value);
  doc.setPeerId(lease.value);
  // use doc here...
} finally {
  await lease.release(JSON.stringify(doc.frontiers()));
  // Or use FinalizeRegistry to release the lease
  // Note: release can be invoked exactly once; a second call throws.
}

// Later, when you reopen the same document, try to reuse the cached peer id
const release = await tryReuseLoroPeerId("doc-123", doc);
try {
  // doc.peerIdStr now matches the previously leased id when the cache is still valid
} finally {
  await release();
}

The first argument is the document identifier that scopes locking and cache entries, ensuring leases only coordinate with peers working on the same document.

acquirePeerId first tries to coordinate through the Web Locks API. When that API is unavailable it falls back to a localStorage-backed mutex with a TTL, heartbeat refresh, and release notifications. A released ID is cached together with the document version that produced it and is only handed out when the caller proves their version has advanced, preventing stale edits from reusing a peer ID.

tryReuseLoroPeerId(docId, doc) wraps the caching flow so you can reopen a document and automatically load the most recent peer ID if the stored frontiers prove the local state is up to date. The returned release handle is callable (await release()) and is also safe to invoke in synchronous lifecycle handlers—release() stages the result synchronously and finishes the mutex flush in the background:

window.addEventListener("pagehide", () => {
  // Only synchronous work is allowed here; release() stages the data right away.
  release(JSON.stringify(doc.frontiers()));
});

window.addEventListener("pageshow", () => {
  if (!release.isReleased()) {
    return;
  }
  // Optionally restart peer work if the page returned from BFCache.
});

release() writes the lease outcome to synchronous storage before returning, so browsers terminating the page (e.g. during pagehide on mobile) still mark the peer ID available even if the returned promise never resolves. If the page survives the lifecycle event, you can still await release() later; repeated calls reuse the same promise and do not restage.

To wire these lifecycle hooks without repeating boilerplate, use the helper exported as attachPeerLeaseLifecycle:

import { attachPeerLeaseLifecycle } from "@loro-dev/peer-lease";

const detachLifecycle = attachPeerLeaseLifecycle({
  release,
  doc,
  onResume: async () => {
    // Re-acquire a lease or restart transports when the tab resumes from BFCache.
  }
});

// Later, when tearing down the document entirely
detachLifecycle();

The helper stages the latest frontiers while the page is visible, calls release() during pagehide, and invokes onResume after pageshow if the handle was released. Provide an onFreeze callback if you need to pause background work when a BFCache transition is detected.

Coordination strategy

  • Lock negotiation – Calls use navigator.locks.request in supporting browsers so the lease state is mutated under an exclusive Web Lock. Fallback tabs use a fencing localStorage record with TTL heartbeats, and wake waiters via storage events plus a BroadcastChannel.
  • Version gating – Every lease carries document metadata. We only recycle a peer ID after the releasing tab supplies the version it used, and a future caller provides a strictly newer version according to the supplied comparator. This stops pre-load editing sessions from replaying IDs once the real document snapshot arrives.
  • Explicit release – A lease is only recycled when the releasing tab provides its final version metadata. If a tab crashes or never releases, the ID stays reserved so it cannot be handed out again accidentally; any lease left active for 24 hours is simply discarded instead of being returned to the available pool.

Lock implementation details

When Web Locks are available the mutex is just a thin wrapper around navigator.locks.request, enforcing an acquire timeout. In browsers without that API we fall back to a localStorage-backed mutex that writes a JSON record containing a token, fence, and expiry. The holder extends the expiry with a heartbeat (a setInterval that calls refresh) so long tasks don’t lose the lock, while waiters observe the fence value and storage/BroadcastChannel notifications to wake up promptly. If the tab crashes the record expires after lockTtlMs, letting another peer take over without manual cleanup.

The mutex implementation is exported so advanced users can coordinate other shared state:

import { createMutex, type AsyncMutex } from "@loro-dev/peer-lease";

const mutex: AsyncMutex = createMutex({
  storage: window.localStorage,
  lockKey: "my-lock",
  fenceKey: "my-lock:fence",
  channelName: "my-lock:channel",
  webLockName: "my-lock:web",
  options: {
    lockTtlMs: 10_000,
    acquireTimeoutMs: 5_000,
    retryDelayMs: 40,
    retryJitterMs: 60,
  },
});

await mutex.runExclusive(async () => {
  // critical section
});

You can reuse the same mutex that acquirePeerId does by passing the document id to keep coordination scoped per document.

Development

  • pnpm install – install dependencies
  • pnpm build – produce ESM/CJS/d.ts bundles via tsdown
  • pnpm dev – run tsdown in watch mode
  • pnpm test – run Vitest
  • pnpm lint – run oxlint
  • pnpm typecheck – run the TypeScript compiler without emitting files
  • pnpm check – type check, lint, update snapshots, and test

Release workflow

  • Push Conventional Commits to main; Release Please opens or updates a release PR with the changelog and semver bump.
  • Merging that PR tags the release and triggers .github/workflows/publish-on-tag.yml, which publishes to npm using NODE_AUTH_TOKEN derived from the NPM_TOKEN secret.
  • Publish provenance is enabled via .npmrc and publishConfig.provenance.

Continuous integration

The CI workflow installs dependencies, lints, type-checks, runs Vitest in run mode, and builds the library on pushes and pull requests.