├── bun.lockb ├── src ├── index.ts ├── debounce.ts ├── types.ts ├── debounce.test.ts ├── lock.test.ts └── lock.ts ├── examples └── upstash-lock-demo │ ├── .eslintrc.json │ ├── app │ ├── globals.css │ ├── favicon.ico │ ├── layout.tsx │ ├── api │ │ └── acquire-lock │ │ │ └── route.ts │ └── page.tsx │ ├── bun.lockb │ ├── .env.example │ ├── public │ └── github-mark.png │ ├── next.config.js │ ├── postcss.config.js │ ├── README.md │ ├── .gitignore │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── package.json ├── context77.json ├── tsup.config.js ├── biome.json ├── .github └── workflows │ ├── tests.yaml │ └── release.yaml ├── package.json ├── LICENSE ├── .gitignore ├── README.md └── tsconfig.json /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/lock/main/bun.lockb -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Lock } from "./lock"; 2 | export * from "./types"; 3 | -------------------------------------------------------------------------------- /examples/upstash-lock-demo/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/upstash-lock-demo/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /examples/upstash-lock-demo/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/lock/main/examples/upstash-lock-demo/bun.lockb -------------------------------------------------------------------------------- /examples/upstash-lock-demo/.env.example: -------------------------------------------------------------------------------- 1 | UPSTASH_REDIS_REST_URL="" 2 | UPSTASH_REDIS_REST_TOKEN="" -------------------------------------------------------------------------------- /examples/upstash-lock-demo/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/lock/main/examples/upstash-lock-demo/app/favicon.ico -------------------------------------------------------------------------------- /examples/upstash-lock-demo/public/github-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/lock/main/examples/upstash-lock-demo/public/github-mark.png -------------------------------------------------------------------------------- /examples/upstash-lock-demo/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /examples/upstash-lock-demo/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /context77.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectTitle": "Upstash Lock JS", 3 | "description": "Basic lock library based on Upstash Redis", 4 | "folders": [], 5 | "excludeFolders": [ 6 | "src", 7 | "cmd" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /examples/upstash-lock-demo/README.md: -------------------------------------------------------------------------------- 1 | ### Upstash Lock Demo 2 | 3 | This is a demo NextJS application that uses `@upstash/lock` as a distributed lock provider. 4 | 5 | Hosted on Vercel: [https://lock-upstash.vercel.app](https://lock-upstash.vercel.app) 6 | -------------------------------------------------------------------------------- /tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | format: ["cjs", "esm"], 6 | splitting: false, 7 | sourcemap: true, 8 | clean: true, 9 | bundle: true, 10 | dts: true, 11 | }); 12 | -------------------------------------------------------------------------------- /examples/upstash-lock-demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/upstash-lock-demo/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /examples/upstash-lock-demo/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { Inter } from 'next/font/google' 3 | import './globals.css' 4 | import { Toaster } from 'sonner' 5 | 6 | 7 | const inter = Inter({ subsets: ['latin'] }) 8 | 9 | export const metadata: Metadata = { 10 | title: 'Upstash Lock Demo', 11 | description: 'Acquire a lock using Upstash Lock', 12 | } 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode 18 | }) { 19 | return ( 20 | 21 | 22 | {children} 23 | 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /examples/upstash-lock-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.3.1/schema.json", 3 | "linter": { 4 | "enabled": true, 5 | "rules": { 6 | "recommended": true, 7 | "correctness": { 8 | "noUnusedVariables": "warn" 9 | }, 10 | "security": { 11 | "noDangerouslySetInnerHtml": "off" 12 | }, 13 | "style": { 14 | "useBlockStatements": "error", 15 | "noNonNullAssertion": "off" 16 | }, 17 | "performance": { 18 | "noDelete": "off" 19 | } 20 | }, 21 | "ignore": ["node_modules", "dist"] 22 | }, 23 | "formatter": { 24 | "indentStyle": "space", 25 | "indentWidth": 2, 26 | "enabled": true, 27 | "lineWidth": 100, 28 | "ignore": ["node_modules", "dist"] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/upstash-lock-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "upstash-lock-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@upstash/lock": "^0.1.0", 13 | "@upstash/redis": "^1.24.3", 14 | "next": "14.2.35", 15 | "react": "^18", 16 | "react-dom": "^18", 17 | "sonner": "^1.2.0" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^20", 21 | "@types/react": "^18", 22 | "@types/react-dom": "^18", 23 | "autoprefixer": "^10.0.1", 24 | "eslint": "^8", 25 | "eslint-config-next": "14.0.1", 26 | "postcss": "^8", 27 | "tailwindcss": "^3.3.0", 28 | "typescript": "^5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | schedule: 8 | - cron: "0 0 * * *" # daily 9 | 10 | env: 11 | UPSTASH_REDIS_REST_URL: ${{ secrets.UPSTASH_REDIS_REST_URL }} 12 | UPSTASH_REDIS_REST_TOKEN: ${{ secrets.UPSTASH_REDIS_REST_TOKEN }} 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | concurrency: test 17 | 18 | name: Tests 19 | steps: 20 | - name: Setup repo 21 | uses: actions/checkout@v3 22 | 23 | - name: Install bun 24 | run: curl -fsSL https://bun.sh/install | bash 25 | - name: Install dependencies 26 | run: ~/.bun/bin/bun install 27 | 28 | - name: Run tests 29 | run: ~/.bun/bin/bun test --coverage 30 | 31 | - name: Build 32 | run: ~/.bun/bin/bun run build 33 | 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@upstash/lock", 3 | "description": "A distributed lock implementation using Upstash Redis", 4 | "module": "./dist/index.js", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "version": "0.2.0", 8 | "keywords": ["redis", "lock", "upstash", "redlock"], 9 | "author": "Meshan Khosla ", 10 | "license": "MIT", 11 | "files": ["dist"], 12 | "scripts": { 13 | "build": "tsup", 14 | "test": "bun test src --coverage", 15 | "fmt": "bunx @biomejs/biome check --apply ." 16 | }, 17 | "devDependencies": { 18 | "@biomejs/biome": "1.3.1", 19 | "@types/node": "^20.8.9", 20 | "@upstash/redis": "^1.24.1", 21 | "bun-types": "^1.0.7", 22 | "tsup": "^7.2.0", 23 | "typescript": "^5.2.2" 24 | }, 25 | "peerDependencies": { 26 | "typescript": "^5.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Upstash 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@v3 15 | 16 | - name: Set env 17 | run: echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 18 | 19 | - name: Setup Node 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 18 23 | 24 | - name: Set package version 25 | run: echo $(jq --arg v "${{ env.VERSION }}" '(.version) = $v' package.json) > package.json 26 | 27 | - name: Install bun 28 | run: npm install -g bun 29 | 30 | - name: Install dependencies 31 | run: bun install 32 | 33 | - name: Build 34 | run: bun run build 35 | 36 | - name: Add npm token 37 | run: echo "//registry.npmjs.org/:_authToken=${{secrets.NPM_TOKEN}}" > .npmrc 38 | 39 | - name: Publish release candidate 40 | if: "github.event.release.prerelease" 41 | run: npm publish --access public --tag=canary --no-git-checks 42 | 43 | - name: Publish 44 | if: "!github.event.release.prerelease" 45 | run: npm publish --access public --no-git-checks 46 | -------------------------------------------------------------------------------- /examples/upstash-lock-demo/app/api/acquire-lock/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { createHash } from 'crypto'; 3 | import { Lock } from '@upstash/lock'; 4 | import { Redis } from '@upstash/redis'; 5 | 6 | export async function GET(req: NextRequest) { 7 | const ip = req.headers.get('x-forwarded-for')?.split(',')[0]; 8 | 9 | if (!ip) { 10 | return NextResponse.json({}, { status: 400, statusText: 'Unable to get IP Address. Try a custom lock ID instead!' }); 11 | } 12 | 13 | const customId = req.nextUrl.searchParams.get('customId'); 14 | const lockKey = createHash('sha256').update(customId ?? ip).digest('hex'); 15 | 16 | const leaseTime = parseInt(req.nextUrl.searchParams.get('leaseTime') ?? '10000'); 17 | 18 | const leaseDuration = isNaN(leaseTime) ? 10000 : leaseTime; 19 | 20 | const lock = new Lock({ 21 | id: lockKey, 22 | lease: leaseTime, 23 | redis: Redis.fromEnv(), 24 | }) 25 | 26 | if (await lock.acquire()) { 27 | return NextResponse.json({ 28 | message: 'Lock acquired successfully.', 29 | lockAcquired: true, 30 | leaseTime: leaseDuration, 31 | lockKey, 32 | }); 33 | } 34 | 35 | return NextResponse.json({ 36 | message: 'Failed to acquire lock.', 37 | lockAcquired: false, 38 | leaseTime: leaseDuration, 39 | lockKey, 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /src/debounce.ts: -------------------------------------------------------------------------------- 1 | import { DebounceConfig } from "./types"; 2 | 3 | /** 4 | * A distributed debounce utility that ensures a function is only called once within a specified wait time. 5 | * Using a Redis instance, this utility can be used across multiple instances of an application 6 | * to debounce a function call. 7 | */ 8 | export class Debounce { 9 | private readonly config: DebounceConfig; 10 | private DEFAULT_WAIT_MS: number = 1000; 11 | 12 | constructor(config: DebounceConfig) { 13 | this.config = { 14 | redis: config.redis, 15 | id: config.id, 16 | wait: config.wait ?? this.DEFAULT_WAIT_MS, 17 | callback: config.callback, 18 | }; 19 | } 20 | 21 | /** 22 | * Calls the callback function after the specified wait time has passed. 23 | * If the function is called multiple times within the wait time, the callback will only be called once. 24 | * This is useful for debouncing a function across multiple instances of an application. 25 | */ 26 | public async call(...args: any[]): Promise { 27 | // Increment the counter 28 | const thisTaskIncr = await this.config.redis.incr(this.config.id); 29 | 30 | // Wait for a delay 31 | await new Promise((resolve) => setTimeout(resolve, this.config.wait)); 32 | 33 | // Get the current counter 34 | const currentTaskIncr = await this.config.redis.get(this.config.id); 35 | 36 | // If the counter has changed, it means another task has called the function 37 | // So we should not call the callback 38 | // We should only run the callback if the counter has not changed in the last wait time 39 | // This is to ensure that the callback is only called once per wait time 40 | if (thisTaskIncr !== currentTaskIncr) { 41 | return; 42 | } 43 | 44 | // We were the last task to increment the counter 45 | // So we can call the callback 46 | await this.config.callback(...args); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Redis } from "@upstash/redis"; 2 | 3 | export type RetryConfig = { 4 | /** 5 | * The number of times to retry acquiring the lock before giving up. 6 | * Default: 3. 7 | */ 8 | attempts: number; 9 | 10 | /** 11 | * The amount of time to wait between retries (in ms) 12 | * Default: 0.1. 13 | */ 14 | delay: number; 15 | }; 16 | 17 | export type LockAcquireConfig = { 18 | /** 19 | * The amount of time to hold the lock for (in ms). 20 | * Default: 10000 ms. 21 | */ 22 | lease?: number; 23 | 24 | /** 25 | * The config for retrying to acquire the lock. 26 | */ 27 | retry?: RetryConfig; 28 | 29 | /** 30 | * The uuid to be used for the lock. 31 | */ 32 | uuid?: string; 33 | }; 34 | 35 | export type LockConfig = { 36 | /** 37 | * Upstash Redis client instance for locking operations. 38 | */ 39 | redis: Redis; 40 | 41 | /** 42 | * Unique identifier associated with the lock. 43 | */ 44 | id: string; 45 | 46 | /** 47 | * Duration (in ms) for which the lock should be held. 48 | */ 49 | lease: number; 50 | 51 | /** 52 | * A unique value assigned when the lock is acquired. 53 | * It's set to null if the lock isn't successfully acquired. 54 | */ 55 | UUID: string | null; 56 | 57 | /** 58 | * The config for retrying to acquire the lock. 59 | */ 60 | retry: RetryConfig; 61 | }; 62 | 63 | export type LockCreateConfig = { 64 | /** 65 | * Unique identifier associated with the lock. 66 | */ 67 | id: string; 68 | 69 | /** 70 | * Upstash Redis client instance for locking operations. 71 | */ 72 | redis: Redis; 73 | 74 | /** 75 | * Duration (in ms) for which the lock should be held. 76 | */ 77 | lease?: number; 78 | 79 | /** 80 | * The config for retrying to acquire the lock. 81 | */ 82 | retry?: RetryConfig; 83 | }; 84 | 85 | export type LockStatus = "ACQUIRED" | "FREE"; 86 | 87 | export type DebounceConfig = { 88 | /** 89 | * Upstash Redis client instance for locking operations. 90 | */ 91 | redis: Redis; 92 | 93 | /** 94 | * Unique identifier associated with the lock. 95 | */ 96 | id: string; 97 | 98 | /** 99 | * Duration (in ms) for which to wait before executing the callback. 100 | */ 101 | wait: number; 102 | 103 | /** 104 | * The callback function to execute after the wait time. 105 | */ 106 | callback: (...args: any[]) => any; 107 | }; -------------------------------------------------------------------------------- /src/debounce.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "bun:test"; 2 | import { Redis } from "@upstash/redis"; 3 | import { Debounce } from "./debounce"; 4 | 5 | function getUniqueFunctionId() { 6 | return `debounce-test-${Math.random().toString(36).substr(2, 9)}`; 7 | } 8 | 9 | test("Debounced function is only called once per wait time", async () => { 10 | // Initialize a counter 11 | // We will use this to check how many times the debounced function is called 12 | let count = 0; 13 | 14 | const uniqueId = getUniqueFunctionId(); 15 | const debouncedFunction = new Debounce({ 16 | id: uniqueId, 17 | redis: Redis.fromEnv(), 18 | 19 | // Wait time of 1 second 20 | // The debounced function will only be called once per second 21 | wait: 1000, 22 | 23 | // Callback function to be debounced 24 | callback: () => { 25 | // Increment the counter 26 | count++; 27 | }, 28 | }); 29 | 30 | for (let i = 0; i < 10; i++) { 31 | debouncedFunction.call(); 32 | } 33 | 34 | // Wait 2 seconds 35 | await new Promise((resolve) => setTimeout(resolve, 2000)); 36 | 37 | // The debounced function should only be called once 38 | expect(count).toBe(1); 39 | }); 40 | 41 | test("Debounced function with arguments is called correctly", async () => { 42 | let coolWord = ""; 43 | 44 | const uniqueId = getUniqueFunctionId(); 45 | const debouncedFunction = new Debounce({ 46 | id: uniqueId, 47 | redis: Redis.fromEnv(), 48 | wait: 1000, 49 | callback: (word: string) => { 50 | coolWord = word; 51 | }, 52 | }); 53 | 54 | const words = ["Upstash", "Is", "A", "Serverless", "Database", "Provider"]; 55 | 56 | for (const word of words) { 57 | debouncedFunction.call(word); 58 | } 59 | 60 | // Wait 2 seconds 61 | await new Promise((resolve) => setTimeout(resolve, 2000)); 62 | 63 | // Our coolWord should be one of the words we passed to the debounced function 64 | expect(words).toContain(coolWord); 65 | }); 66 | 67 | test("Debounced async functions trigger correctly", async () => { 68 | // Initialize a counter 69 | // We will use this to check how many times the debounced function is called 70 | let count = 0; 71 | 72 | const uniqueId = getUniqueFunctionId(); 73 | const debouncedFunction = new Debounce({ 74 | id: uniqueId, 75 | redis: Redis.fromEnv(), 76 | 77 | // Wait time of 1 second 78 | // The debounced function will only be called once per second 79 | wait: 1000, 80 | 81 | // Callback function to be debounced 82 | callback: async () => { 83 | 84 | // wait for 1 second 85 | await new Promise((resolve) => setTimeout(resolve, 1000)); 86 | 87 | // Increment the counter 88 | count++; 89 | }, 90 | }); 91 | 92 | for (let i = 0; i < 10; i++) { 93 | debouncedFunction.call(); 94 | } 95 | 96 | // Wait 2 seconds 97 | await new Promise((resolve) => setTimeout(resolve, 3000)); 98 | 99 | // The debounced function should only be called once 100 | expect(count).toBe(1); 101 | }); 102 | -------------------------------------------------------------------------------- /src/lock.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "bun:test"; 2 | import { Redis } from "@upstash/redis"; 3 | import { Lock } from "./lock"; 4 | 5 | function getUniqueLockId() { 6 | return `lock-test-${Math.random().toString(36).substr(2, 9)}`; 7 | } 8 | 9 | test("lock created, extended, and released with defaults", async () => { 10 | const uniqueId = getUniqueLockId(); 11 | const lock = new Lock({ 12 | id: uniqueId, 13 | redis: Redis.fromEnv(), 14 | }); 15 | 16 | expect(lock.id).toBe(uniqueId); 17 | expect(await lock.getStatus()).toBe("FREE"); 18 | const acquired = await lock.acquire(); 19 | expect(acquired).toBe(true); 20 | const extended = await lock.extend(10000); 21 | expect(extended).toBe(true); 22 | const released = await lock.release(); 23 | expect(released).toBe(true); 24 | expect(await lock.getStatus()).toBe("FREE"); 25 | }); 26 | 27 | test("lock created, extended, and released with values", async () => { 28 | const uniqueId = getUniqueLockId(); 29 | const lock = new Lock({ 30 | id: uniqueId, 31 | redis: Redis.fromEnv(), 32 | lease: 5000, 33 | retry: { 34 | attempts: 1, 35 | delay: 100, 36 | }, 37 | }); 38 | 39 | expect(lock.id).toBe(uniqueId); 40 | expect(await lock.getStatus()).toBe("FREE"); 41 | const acquired = await lock.acquire(); 42 | expect(acquired).toBe(true); 43 | const extended = await lock.extend(10000); 44 | expect(extended).toBe(true); 45 | const released = await lock.release(); 46 | expect(released).toBe(true); 47 | expect(await lock.getStatus()).toBe("FREE"); 48 | }); 49 | 50 | test("double lock acquisition fails", async () => { 51 | const uniqueId = getUniqueLockId(); 52 | const lock = new Lock({ 53 | id: uniqueId, 54 | redis: Redis.fromEnv(), 55 | lease: 5000, 56 | retry: { 57 | attempts: 1, 58 | delay: 100, 59 | }, 60 | }); 61 | 62 | expect(await lock.acquire()).toBe(true); 63 | 64 | // Attempt to acquire the same lock, which should fail 65 | expect(await lock.acquire()).toBe(false); 66 | }); 67 | 68 | test("lock acquisition fails by other instance", async () => { 69 | const uniqueId = getUniqueLockId(); 70 | const lock = new Lock({ 71 | id: uniqueId, 72 | redis: Redis.fromEnv(), 73 | }); 74 | 75 | expect(await lock.acquire()).toBe(true); 76 | 77 | // Attempt to acquire the same lock using a different lock instance, which should fail 78 | const lockFail = new Lock({ 79 | id: uniqueId, 80 | redis: Redis.fromEnv(), 81 | }); 82 | 83 | expect(await lockFail.acquire()).toBe(false); 84 | }); 85 | 86 | test("lock lease times out", async () => { 87 | const uniqueId = getUniqueLockId(); 88 | const lock = new Lock({ 89 | id: uniqueId, 90 | redis: Redis.fromEnv(), 91 | lease: 100, 92 | retry: { 93 | attempts: 1, 94 | delay: 100, 95 | }, 96 | }); 97 | 98 | expect(await lock.acquire()).toBe(true); 99 | 100 | // Wait for the lock to expire 101 | await new Promise((resolve) => setTimeout(resolve, 200)); 102 | 103 | expect(await lock.getStatus()).toBe("FREE"); 104 | }); 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.\* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | 177 | -------------------------------------------------------------------------------- /src/lock.ts: -------------------------------------------------------------------------------- 1 | import type { LockAcquireConfig, LockConfig, LockCreateConfig, LockStatus } from "./types"; 2 | 3 | export class Lock { 4 | private readonly config: LockConfig; 5 | private readonly DEFAULT_LEASE_MS = 10000; 6 | private readonly DEFAULT_RETRY_ATTEMPTS = 3; 7 | private readonly DEFAULT_RETRY_DELAY_MS = 100; 8 | 9 | constructor(config: LockCreateConfig) { 10 | this.config = { 11 | redis: config.redis, 12 | id: config.id, 13 | lease: config.lease ?? this.DEFAULT_LEASE_MS, 14 | UUID: null, // set when lock is acquired 15 | retry: { 16 | attempts: config.retry?.attempts ?? this.DEFAULT_RETRY_ATTEMPTS, 17 | delay: config.retry?.delay ?? this.DEFAULT_RETRY_DELAY_MS, 18 | }, 19 | }; 20 | } 21 | 22 | /** 23 | * Tries to acquire a lock with the given configuration. 24 | * If initially unsuccessful, the method will retry based on the provided retry configuration. 25 | * 26 | * @param config - Optional configuration for the lock acquisition to override the constructor config. 27 | * @returns {Promise} True if the lock was acquired, otherwise false. 28 | */ 29 | public async acquire(acquireConfig?: LockAcquireConfig): Promise { 30 | // Allow for overriding the constructor lease and retry config 31 | const lease = acquireConfig?.lease ?? this.config.lease; 32 | this.config.lease = lease; 33 | const retryAttempts = acquireConfig?.retry?.attempts ?? this.config.retry?.attempts; 34 | const retryDelay = acquireConfig?.retry?.delay ?? this.config.retry?.delay; 35 | 36 | let attempts = 0; 37 | 38 | let UUID: string; 39 | if (acquireConfig?.uuid) { 40 | UUID = acquireConfig.uuid; 41 | } else { 42 | try { 43 | UUID = crypto.randomUUID(); 44 | } catch (error) { 45 | throw new Error('No UUID provided and crypto module is not available in this environment.'); 46 | } 47 | } 48 | 49 | while (attempts < retryAttempts) { 50 | const upstashResult = await this.config.redis.set(this.config.id, UUID, { 51 | nx: true, 52 | px: lease, 53 | }); 54 | 55 | if (upstashResult === "OK") { 56 | this.config.UUID = UUID; 57 | return true; 58 | } 59 | 60 | attempts += 1; 61 | 62 | // Wait for the specified delay before retrying 63 | await new Promise((resolve) => setTimeout(resolve, retryDelay)); 64 | } 65 | 66 | // Lock acquisition failed 67 | this.config.UUID = null; 68 | return false; 69 | } 70 | 71 | /** 72 | * Safely releases the lock ensuring the UUID matches. 73 | * This operation utilizes a Lua script to interact with Redis and 74 | * guarantees atomicity of the unlock operation. 75 | * @returns {Promise} True if the lock was released, otherwise false. 76 | */ 77 | public async release(): Promise { 78 | const script = ` 79 | -- Check if the current UUID still holds the lock 80 | if redis.call("get", KEYS[1]) == ARGV[1] then 81 | return redis.call("del", KEYS[1]) 82 | else 83 | return 0 84 | end 85 | `; 86 | 87 | const numReleased = await this.config.redis.eval(script, [this.config.id], [this.config.UUID]); 88 | return numReleased === 1; 89 | } 90 | 91 | /** 92 | * Extends the duration for which the lock is held by a given amount of milliseconds. 93 | * @param amt - The number of milliseconds by which the lock duration should be extended. 94 | * @returns {Promise} True if the lock duration was extended, otherwise false. 95 | */ 96 | public async extend(amt: number): Promise { 97 | const script = ` 98 | -- Check if the current UUID still holds the lock 99 | if redis.call("get", KEYS[1]) ~= ARGV[1] then 100 | return 0 101 | end 102 | 103 | -- Get the current TTL and extend it by the specified amount 104 | local ttl = redis.call("ttl", KEYS[1]) 105 | if ttl > 0 then 106 | return redis.call("expire", KEYS[1], ttl + ARGV[2]) 107 | else 108 | return 0 109 | end 110 | `; 111 | 112 | const extendBy = amt / 1000; // convert to seconds 113 | const extended = await this.config.redis.eval( 114 | script, 115 | [this.config.id], 116 | [this.config.UUID, extendBy], 117 | ); 118 | 119 | if (extended === 1) { 120 | this.config.lease += amt; 121 | } 122 | return extended === 1; 123 | } 124 | 125 | get id(): string { 126 | return this.config.id; 127 | } 128 | 129 | /** 130 | * Gets the status of the lock, ie: ACQUIRED or FREE. 131 | * @returns {Promise} The status of the lock. 132 | */ 133 | async getStatus(): Promise { 134 | if (this.config.UUID === null) { 135 | return "FREE"; 136 | } 137 | 138 | const UUID = await this.config.redis.get(this.config.id); 139 | if (UUID === this.config.UUID) { 140 | return "ACQUIRED"; 141 | } 142 | 143 | return "FREE"; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /examples/upstash-lock-demo/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState, useEffect } from 'react'; 4 | import { toast } from 'sonner'; 5 | 6 | type RequestResult = { 7 | id: number; 8 | result: string; 9 | leaseTime: number; 10 | lockKey: string; 11 | }; 12 | 13 | export default function Home() { 14 | const [leaseTime, setLeaseTime] = useState(10000); 15 | const [customId, setCustomId] = useState(''); 16 | const [countdown, setCountdown] = useState(null); 17 | const [requests, setRequests] = useState([]); 18 | 19 | const acquireLock = async () => { 20 | if (countdown === null) { 21 | setRequests([]); 22 | } 23 | // Construct the URL with a conditional customId parameter 24 | let url = `/api/acquire-lock?leaseTime=${leaseTime}`; 25 | if (customId.trim() !== '') { 26 | url += `&customId=${encodeURIComponent(customId)}`; 27 | } 28 | 29 | const res = await fetch(url); 30 | if (!res.ok) { 31 | toast.error(res.statusText); 32 | return; 33 | } 34 | 35 | const data = await res.json(); 36 | 37 | setRequests(prevRequests => [ 38 | ...prevRequests, 39 | { id: prevRequests.length + 1, result: data.message, leaseTime: data.leaseTime, lockKey: data.lockKey } 40 | ]); 41 | 42 | if (data.lockAcquired) { 43 | setCountdown(data.leaseTime); 44 | } 45 | }; 46 | 47 | useEffect(() => { 48 | let interval: NodeJS.Timeout | null = null; 49 | 50 | if (countdown !== null) { 51 | interval = setInterval(() => { 52 | setCountdown((currentCountdown) => { 53 | if (currentCountdown !== null && currentCountdown <= 0) { 54 | clearInterval(interval!); 55 | return null; 56 | } 57 | return currentCountdown !== null ? currentCountdown - 1000 : null; 58 | }); 59 | }, 1000); 60 | } 61 | 62 | return () => { 63 | if (interval) { 64 | clearInterval(interval); 65 | } 66 | }; 67 | }, [countdown]); 68 | 69 | return ( 70 |
71 |
72 |

Upstash Lock Demo

73 |

74 | This demo shows how Upstash Lock enforces mutual exclusion: only one user can hold the lock with the same ID at any given time. 75 |

76 | 77 | 88 | 89 |
90 | 93 |

94 | If you would like to try to acquire a lock on different devices, you can enter a custom lock ID here. 95 | If you leave this field empty, the lock ID will be your hashed IP Address. 96 |

97 | setCustomId(e.target.value)} 102 | placeholder="Enter custom lock ID here" 103 | className="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-[#00E9A3] focus:border-[#00E9A3]" 104 | /> 105 |
106 | 107 |
108 | 111 | setLeaseTime(Number(e.target.value))} 118 | className="w-full h-2 bg-gray-200 rounded-lg cursor-pointer dark:bg-gray-700" 119 | /> 120 |
121 | 122 | 129 | 130 | {countdown !== null && ( 131 |
132 | Lease expires in {Math.max(0, Math.floor(countdown / 1000))} seconds 133 |
134 | )} 135 | 136 |
137 |

Request Results

138 |
139 |
Request ID
140 |
Lock Key (Hashed)
141 |
Result
142 |
143 |
144 |
    145 | {requests.map((request) => ( 146 |
  • 147 | {request.id} 148 | 149 | {request.lockKey.slice(0, 4)}...{request.lockKey.slice(-4)} 150 | 151 | 152 | {request.result} 153 | 154 |
  • 155 | ))} 156 |
157 |
158 | 159 | 160 |
161 |
162 | ); 163 | } 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

@upstash/lock

3 |
Distributed Lock using Upstash Redis
4 |
5 | 6 |
7 | upstash.com 8 |
9 |
10 | 11 | 12 | > [!NOTE] 13 | > **This project is a Community Project.** 14 | > 15 | > The project is maintained and supported by the community. Upstash may contribute but does not officially support or assume responsibility for it. 16 | 17 | 18 | 19 | `@upstash/lock` offers a distributed lock and debounce implementation using Upstash Redis. 20 | 21 | ### Disclaimer 22 | 23 | Please use this lock implementation for efficiency purposes; for example to avoid doing an expensive work more than once or to perform a task _mostly_ once in a best-effort manner. 24 | Do not use it to guarantee correctness of your system; such as leader-election or for the tasks requiring _exactly_ once execution. 25 | 26 | Upstash Redis uses async replication between replicas, and a lock can be acquired by multiple clients in case of a crash or network partition. Please read the post [How to do distributed locking](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html) by Martin Kleppman to learn more about the topic. 27 | 28 | ### Quick Start 29 | 30 | NPM 31 | 32 | ```bash 33 | npm install @upstash/lock 34 | ``` 35 | 36 | PNPM 37 | 38 | ```bash 39 | pnpm add @upstash/lock 40 | ``` 41 | 42 | Bun 43 | 44 | ```bash 45 | bun add @upstash/lock 46 | ``` 47 | 48 | ### Locking Demo 49 | 50 | To see a demo of the lock in action, visit [https://lock-upstash.vercel.app](https://lock-upstash.vercel.app) 51 | 52 | To create the Redis instance, you can use the `Redis.fromEnv()` method to use an Upstash Redis instance from environment variables. More options can be found [here](https://github.com/upstash/upstash-redis#quick-start). 53 | 54 | ### Lock Example Usage 55 | 56 | ```typescript 57 | import { Lock } from "@upstash/lock"; 58 | import { Redis } from "@upstash/redis"; 59 | 60 | async function handleOperation() { 61 | const lock = new Lock({ 62 | id: "unique-lock-id", 63 | redis: Redis.fromEnv(), 64 | }); 65 | 66 | if (await lock.acquire()) { 67 | // Perform your critical section that requires mutual exclusion 68 | await criticalSection(); 69 | await lock.release(); 70 | } else { 71 | // handle lock acquisition failure 72 | } 73 | } 74 | ``` 75 | 76 | ### Debounce Example Usage 77 | 78 | ```typescript 79 | import { Lock } from "@upstash/lock"; 80 | import { Redis } from "@upstash/redis"; 81 | import { expensiveWork } from "my-app"; 82 | 83 | const debouncedFunction = new Debounce({ 84 | id: "unique-function-id", 85 | redis: Redis.fromEnv(), 86 | 87 | // Wait time of 1 second 88 | // The debounced function will only be called once per second across all instances 89 | wait: 1000, 90 | 91 | // Callback function to be debounced 92 | callback: (arg) => { 93 | doExpensiveWork(arg); 94 | }, 95 | }); 96 | 97 | // This example function is called by our app to trigger work we want to only happen once per wait period 98 | async function triggerExpensiveWork(arg: string) { 99 | // Call the debounced function 100 | // This will only call the callback function once per wait period 101 | await debouncedFunction.call(arg) 102 | } 103 | ``` 104 | 105 | ### Lock API 106 | 107 | #### `Lock` 108 | 109 | ```typescript 110 | new Lock({ 111 | id: string, 112 | redis: Redis, // ie. Redis.fromEnv(), new Redis({...}) 113 | lease: number, // default: 10000 ms 114 | retry: { 115 | attempts: number, // default: 3 116 | delay: number, // default: 100 ms 117 | }, 118 | }); 119 | ``` 120 | 121 | #### `Lock#acquire` 122 | 123 | Attempts to acquire the lock. Returns `true` if the lock is acquired, `false` otherwise. 124 | 125 | You can pass a `config` object to override the default `lease` and `retry` options. 126 | 127 | ```typescript 128 | async acquire(config?: LockAcquireConfig): Promise 129 | ``` 130 | 131 | #### `Lock#release` 132 | 133 | Attempts to release the lock. Returns `true` if the lock is released, `false` otherwise. 134 | 135 | ```typescript 136 | async release(): Promise 137 | ``` 138 | 139 | #### `Lock#extend` 140 | 141 | Attempts to extend the lock lease. Returns `true` if the lock lease is extended, `false` otherwise. 142 | 143 | ```typescript 144 | async extend(amt: number): Promise 145 | ``` 146 | 147 | #### `Lock#getStatus` 148 | 149 | Returns whether the lock is `ACQUIRED` or `FREE`. 150 | 151 | ```typescript 152 | async getStatus(): Promise 153 | ``` 154 | 155 | | Option | Default Value | Description | 156 | | ---------------- | ------------- | --------------------------------------------------------------------------------- | 157 | | `lease` | `10000` | The lease duration in milliseconds. After this expires, the lock will be released | 158 | | `retry.attempts` | `3` | The number of attempts to acquire the lock. | 159 | | `retry.delay` | `100` | The delay between attempts in milliseconds. | 160 | 161 | ### Debounce API 162 | 163 | #### `Debounce` 164 | 165 | Creates a new debounced function. 166 | 167 | ```typescript 168 | new Debounce({ 169 | id: string, 170 | redis: Redis, // ie. Redis.fromEnv(), new Redis({...}) 171 | wait: number, // default: 1000 ms 172 | callback: (...arg: any[]) => any // The function to be debounced 173 | }); 174 | ``` 175 | 176 | #### `Debounce#call` 177 | 178 | Calls the debounced function. The function will only be called once per `wait` period. 179 | When called there is a best-effort guarantee that the function will be called once per `wait` period. 180 | 181 | Note: Due to the implementation of the debounce, there is always a delay of `wait` milliseconds before the function is called (even if the callback is not triggered when you use the call function). 182 | 183 | ```typescript 184 | async call(...args: any[]): Promise 185 | ``` 186 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | "types": [ 36 | "bun-types" 37 | ], /* Specify type package names to be included without being referenced in a source file. */ 38 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 39 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 40 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 41 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 42 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 43 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 44 | // "resolveJsonModule": true, /* Enable importing .json files. */ 45 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 46 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 47 | 48 | /* JavaScript Support */ 49 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 50 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 51 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 52 | 53 | /* Emit */ 54 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 55 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 56 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 57 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 60 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 61 | // "removeComments": true, /* Disable emitting comments. */ 62 | // "noEmit": true, /* Disable emitting files from a compilation. */ 63 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 64 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 65 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 66 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 67 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 68 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 69 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 70 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 71 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 72 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 73 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 74 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 75 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 76 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 77 | 78 | /* Interop Constraints */ 79 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 80 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 81 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 82 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 83 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 84 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 85 | 86 | /* Type Checking */ 87 | "strict": true, /* Enable all strict type-checking options. */ 88 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 89 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 90 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 91 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 92 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 93 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 94 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 95 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 96 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 97 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 98 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 99 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 100 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 101 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 102 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 103 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 104 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 105 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 106 | 107 | /* Completeness */ 108 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 109 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 110 | } 111 | } --------------------------------------------------------------------------------