{"id":42229431,"url":"https://github.com/loro-dev/peer-lease","last_synced_at":"2026-01-27T02:43:09.678Z","repository":{"id":316231114,"uuid":"1062400917","full_name":"loro-dev/peer-lease","owner":"loro-dev","description":"A TypeScript library for safely reusing CRDT peer IDs without collisions","archived":false,"fork":false,"pushed_at":"2025-10-03T03:42:44.000Z","size":104,"stargazers_count":1,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-13T03:34:35.220Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/loro-dev.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-09-23T07:56:32.000Z","updated_at":"2025-10-03T03:42:26.000Z","dependencies_parsed_at":"2025-09-23T13:10:02.993Z","dependency_job_id":"ac91af34-7c85-449a-80d4-7f8ee6df96c3","html_url":"https://github.com/loro-dev/peer-lease","commit_stats":null,"previous_names":["loro-dev/peer-lease"],"tags_count":4,"template":false,"template_full_name":"zxch3n/tslib-template","purl":"pkg:github/loro-dev/peer-lease","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loro-dev%2Fpeer-lease","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loro-dev%2Fpeer-lease/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loro-dev%2Fpeer-lease/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loro-dev%2Fpeer-lease/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/loro-dev","download_url":"https://codeload.github.com/loro-dev/peer-lease/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loro-dev%2Fpeer-lease/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28796988,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-27T01:07:07.743Z","status":"online","status_checked_at":"2026-01-27T02:00:07.755Z","response_time":168,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-27T02:43:07.687Z","updated_at":"2026-01-27T02:43:09.662Z","avatar_url":"https://github.com/loro-dev.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# peer-lease\n\n`@loro-dev/peer-lease` is a TypeScript library for safely reusing CRDT peer IDs without collisions.\n\n## Installation\n\n```sh\npnpm add @loro-dev/peer-lease\n# or\nnpm install @loro-dev/peer-lease\n```\n\n## Usage\n\n```ts\nimport { LoroDoc } from \"loro-crdt\";\nimport { acquirePeerId, tryReuseLoroPeerId } from \"@loro-dev/peer-lease\";\n\nconst doc = new LoroDoc();\n// ... Import local data into doc first\nconst lease = await acquirePeerId(\n  \"doc-123\",\n  () =\u003e new LoroDoc().peerIdStr,\n  JSON.stringify(doc.frontiers()),\n  (a, b) =\u003e {\n    const fA = JSON.parse(a);\n    const fB = JSON.parse(b);\n    return doc.cmpFrontiers(fA, fB);\n  },\n);\n\ntry {\n  console.log(\"Using peer\", lease.value);\n  doc.setPeerId(lease.value);\n  // use doc here...\n} finally {\n  await lease.release(JSON.stringify(doc.frontiers()));\n  // Or use FinalizeRegistry to release the lease\n  // Note: release can be invoked exactly once; a second call throws.\n}\n\n// Later, when you reopen the same document, try to reuse the cached peer id\nconst reuseHandle = await tryReuseLoroPeerId(\"doc-123\", doc);\ntry {\n  // doc.peerIdStr now matches the previously leased id when the cache is still valid\n  console.log(\"Reused peer\", reuseHandle.value);\n} finally {\n  await reuseHandle.release();\n}\n```\n\nThe first argument is the document identifier that scopes locking and cache entries, ensuring leases only coordinate with peers working on the same document.\n\n`acquirePeerId` first tries to coordinate through the [Web Locks API](https://developer.mozilla.org/en-US/docs/Web/API/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.\n\n`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 handle exposes `release()`, `isReleased()`, and `value`; its `release()` method resolves once the lease is flushed and it is safe to call from synchronous lifecycle handlers thanks to internal staging of the latest document version:\n\n```ts\nwindow.addEventListener(\"pagehide\", () =\u003e {\n  // Only synchronous work is allowed here; reuseHandle.release() stages the data right away.\n  reuseHandle.release();\n});\n\nwindow.addEventListener(\"pageshow\", () =\u003e {\n  if (!reuseHandle.isReleased()) {\n    return;\n  }\n  // Optionally restart peer work if the page returned from BFCache.\n});\n```\n\n`reuseHandle.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 reuseHandle.release()` later; repeated calls reuse the same promise and do not restage.\n\nTo wire these lifecycle hooks without repeating boilerplate, pass the same handle to the helper exported as `attachPeerLeaseLifecycle`:\n\n```ts\nimport { attachPeerLeaseLifecycle } from \"@loro-dev/peer-lease\";\n\nconst detachLifecycle = attachPeerLeaseLifecycle({\n  release: reuseHandle,\n  doc,\n  onResume: async () =\u003e {\n    // Re-acquire a lease or restart transports when the tab resumes from BFCache.\n  }\n});\n\n// Later, when tearing down the document entirely\ndetachLifecycle();\n```\n\nThe helper stages the latest frontiers while the page is visible, calls `reuseHandle.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.\n\n## Coordination strategy\n\n- **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`.\n- **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.\n- **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.\n\n### Lock implementation details\n\nWhen 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.\n\nThe mutex implementation is exported so advanced users can coordinate other shared state:\n\n```ts\nimport { createMutex, type AsyncMutex } from \"@loro-dev/peer-lease\";\n\nconst mutex: AsyncMutex = createMutex({\n  storage: window.localStorage,\n  lockKey: \"my-lock\",\n  fenceKey: \"my-lock:fence\",\n  channelName: \"my-lock:channel\",\n  webLockName: \"my-lock:web\",\n  options: {\n    lockTtlMs: 10_000,\n    acquireTimeoutMs: 5_000,\n    retryDelayMs: 40,\n    retryJitterMs: 60,\n  },\n});\n\nawait mutex.runExclusive(async () =\u003e {\n  // critical section\n});\n```\n\nYou can reuse the same mutex that `acquirePeerId` does by passing the document id to keep coordination scoped per document.\n\n## Development\n\n- `pnpm install` – install dependencies\n- `pnpm build` – produce ESM/CJS/d.ts bundles via tsdown\n- `pnpm dev` – run tsdown in watch mode\n- `pnpm test` – run Vitest\n- `pnpm lint` – run oxlint\n- `pnpm typecheck` – run the TypeScript compiler without emitting files\n- `pnpm check` – type check, lint, update snapshots, and test\n\n## Release workflow\n\n- Push Conventional Commits to `main`; Release Please opens or updates a release PR with the changelog and semver bump.\n- 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.\n- Publish provenance is enabled via `.npmrc` and `publishConfig.provenance`.\n\n## Continuous integration\n\nThe `CI` workflow installs dependencies, lints, type-checks, runs Vitest in run mode, and builds the library on pushes and pull requests.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Floro-dev%2Fpeer-lease","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Floro-dev%2Fpeer-lease","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Floro-dev%2Fpeer-lease/lists"}