{"id":14969922,"url":"https://github.com/kontsedal/locco","last_synced_at":"2025-10-26T10:31:28.824Z","repository":{"id":57679979,"uuid":"492208509","full_name":"Kontsedal/locco","owner":"Kontsedal","description":"A node.js locks library with support of Redis and MongoDB","archived":false,"fork":false,"pushed_at":"2024-02-15T12:47:39.000Z","size":706,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-10-30T04:37:06.394Z","etag":null,"topics":["distributed-locks","mongodb","nodejs","redis","redlock","synchronization"],"latest_commit_sha":null,"homepage":"","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/Kontsedal.png","metadata":{"files":{"readme":"README.md","changelog":null,"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}},"created_at":"2022-05-14T12:22:19.000Z","updated_at":"2024-05-12T13:22:42.000Z","dependencies_parsed_at":"2024-09-03T09:28:02.157Z","dependency_job_id":"f125a5be-0b7b-44e9-8167-b6315bf9697d","html_url":"https://github.com/Kontsedal/locco","commit_stats":{"total_commits":61,"total_committers":1,"mean_commits":61.0,"dds":0.0,"last_synced_commit":"7785f661fd8614b69a5a73ad8e34ebd92f5aacf8"},"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kontsedal%2Flocco","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kontsedal%2Flocco/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kontsedal%2Flocco/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kontsedal%2Flocco/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Kontsedal","download_url":"https://codeload.github.com/Kontsedal/locco/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":238310343,"owners_count":19450841,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["distributed-locks","mongodb","nodejs","redis","redlock","synchronization"],"created_at":"2024-09-24T13:42:41.598Z","updated_at":"2025-10-26T10:31:28.436Z","avatar_url":"https://github.com/Kontsedal.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Buuild and Test](https://github.com/kontsedal/locco/workflows/Build%20and%20Test/badge.svg)](https://github.com/kontsedal/locco/actions/workflows/status.yml?query=branch%3Amain++)\n![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/Kontsedal/e0ad01840d30efd4c1766e5ba5845567/raw/bf778a4aa5262997514945e024aa6722d5f72016/locco__heads_main.json)\n\n# locco\n\nA small and simple library to deal with race conditions in distributed systems by applying locks on resources. Currently, supports locking via Redis, MongoDB, and in-memory object.\n\n## Installation\n\n```shell\nnpm i @kontsedal/locco\n```\n\n## Core logic\n\nWith locks, user can just say _\"I'm doing some stuff with this user, please lock him and don't allow anybody to change him\"_ and no one will, till a lock is valid.\n\nThe core logic is simple. When we create a lock we generate a unique string identifying a current lock operation.\nThen, we search for a valid lock with a same key in the storage(Redis, Mongo, js object) and if it doesn't exist we add one and proceed.\nIf a valid lock already exists we retry this operation for some time and then fail.\n\nWhen we release or extend a lock, we check that lock exists in the storage and has the same unique identifier with a current lock. It makes impossible to release or extend other process lock.\n\n## Usage\n\nThere are two ways to create a resource lock. In the first one, you should manually lock and unlock a resource. Here is an example with a Redis:\n\n```typescript\nimport { Locker, IoRedisAdapter } from \"@kontsedal/locco\";\nimport Redis from \"ioredis\";\n\nconst redisAdapter = new IoRedisAdapter({ client: new Redis() });\nconst locker = new Locker({\n  adapter: redisAdapter,\n  retrySettings: { retryDelay: 200, retryTimes: 10 },\n});\n\nconst lock = await locker.lock(\"user:123\", 3000).aquire();\ntry {\n  //do some risky stuff here\n  //...\n  //\n  await lock.extend(2000);\n  //do more risky stuff\n  //...\n} catch (error) {\n} finally {\n  await lock.release();\n}\n```\n\nIn the second one, you pass a function in the **acquire** method and a lock will be released automatically when a function finishes. Here is an example with a mongo:\n\n```typescript\nimport { Locker, IoRedisAdapter, MongoAdapter } from \"@kontsedal/Locker\";\nimport { MongoClient } from \"mongodb\";\n\nconst mongoAdapter = new MongoAdapter({\n  client: new MongoClient(process.env.MONGO_URL),\n});\nconst locker = new Locker({\n  adapter: mongoAdapter,\n  retrySettings: { retryDelay: 200, retryTimes: 10 },\n});\n\nawait locker.lock(\"user:123\", 3000).setRetrySettings({retryDelay: 200, retryTimes: 50}).aquire(async (lock) =\u003e {\n  //do some risky stuff here\n  //...\n  await lock.extend(2000);\n  //do some risky stuff here\n  //...\n});\n```\n\n## API\n\n### Locker\n\nThe main class is responsible for the creation of new locks and passing them a storage adapter and default retrySettings.\n\nConstructor params:\n\n| parameter                         | type                 | isRequired | description                                                                                             |\n| --------------------------------- | -------------------- | ---------- |---------------------------------------------------------------------------------------------------------|\n| params.adapter                    | ILockAdapter         | true       | Adapter to work with a lock keys storage. Currently Redis, Mongo and in-memory adapters are implemented |\n| params.retrySettings              | object               | true       |                                                                                                         |\n| params.retrySettings.retryTimes   | number(milliseconds) | false      | How many times we should retry lock before fail                                                         |\n| params.retrySettings.retryDelay   | number(milliseconds) | false      | How much time should pass between retries                                                               |\n| params.retrySettings.totalTime    | number(milliseconds) | false      | How much time should all retries last in total                                                          |\n| params.retrySettings.retryDelayFn | function             | false      | Function which returns a retryDelay for each attempt. Allows to implement an own delay logic            |\n\nExample of a retryDelayFn usage:\n\n```typescript\nconst locker = new Locker({\n  adapter: new InMemoryAdapter(),\n  retrySettings: {\n    retryDelayFn: ({\n      attemptNumber, // starts from 0\n      startedAt, // date of start in milliseconds\n      previousDelay,\n      settings, // retrySettings\n      stop, // function to stop a retries, throws an error\n    }) =\u003e {\n      if (attemptNumber === 4) {\n        stop();\n      }\n      return (attemptNumber + 1) * 50;\n    },\n  },\n});\n```\n\nProvided example will do the same as providing retryTimes = 5, retryDelay = 50\n\n#### Methods\n\n##### _lock(key: string, ttl: number) =\u003e Lock_\n\nCreates a **Lock** instance with provided key and time to live in milliseconds.\nIt won't lock a resource at this point. Need to call an **aquire()** to do so\n\n##### _Lock.aquire(cb?: (lock: Lock) =\u003e void) =\u003e Promise\\\u003cLock\u003e_\n\nLocks a resource if possible. If not, it retries as much as specified in the retrySettings.\nIf callback is provided, lock will be released after a callback execution.\n\n##### _Lock.release({ throwOnFail?: boolean }) =\u003e Promise\\\u003cvoid\u003e_\n\nUnlocks a resource. If a resource is invalid (already taken by other lock or expired) it won't throw an error.\nTo make it throw an error, need to provide `{throwOnFail:true}`.\n\n##### _Lock.extend(ttl: number) =\u003e Promise\\\u003cvoid\u003e_\n\nExtends a lock for a provided milliseconds from now. Will throw an error if current lock is already invalid\n\n##### _Lock.isLocked() =\u003e Promise\\\u003cboolean\u003e_\n\nChecks if a lock is still valid\n\n##### _Lock.setRetrySettings(settings: RetrySettings) =\u003e Promise\\\u003cLock\u003e_\n\nOverrides a default retry settings of the lock.\n\n---\n\n### Redis adapter\n\nRequires only a compatible with ioredis client:\n\n```typescript\nimport { IoRedisAdapter } from \"@kontsedal/locco\";\nimport Redis from \"ioredis\";\n\nconst redisAdapter = new IoRedisAdapter({ client: new Redis() });\n```\n\n#### How it works\n\nIt relies on a Redis **SET** command with options **NX** and **PX**.\n\n**NX** - ensures that a record will be removed after provided time\n\n**PX** - ensures that if a record already exists it won't be replaced with a new one\n\nSo, to create a lock we just execute a **SET** command and if it returns \"OK\"\nresponse means that lock is created, if it returns null - a resource is locked.\n\nTo release or extend a lock, firstly, it gets a current key value(which is a unique string for each lock) and\ncompares it with a current one. If it matches we either remove the key or set a new TTL for it.\n\n\n---\n\n### Mongo adapter\n\nRequires a mongo client and optional database name and lock collection name:\n\n```typescript\nimport { MongoAdapter } from \"@kontsedal/locco\";\nimport { MongoClient } from \"mongodb\";\n\nconst mongoAdapter = new MongoAdapter({\n  client: new MongoClient(process.env.MONGO_URL),\n  dbName: \"my-db\", // optional parameter\n  locksCollectionName: \"locks\", //optional parameter, defaults to \"locco-locks\"\n});\n```\n\n#### How it works\n\nWe create a collection of locks in the database with the next fields:\n\n- key: string\n- uniqueValue: string\n- expireAt: Date\n\nFor this collection we create a special index `{ key: 1 }, { unique: true }`, so mongo will throw an error\nif we try to create a new record with an existing key.\n\nTo create a lock, we use an **updateOne** method with an `upsert = true` option:\n\n```typescript\ncollection.updateOne(\n  {\n    key,\n    expireAt: { $lt: new Date() },\n  },\n  { $set: { key, uniqueValue, expireAt: new Date(Date.now() + ttl) } },\n  { upsert: true }\n);\n```\nSo, let's imagine that we want to create a lock and there is a valid lock in the DB.\nIf the lock is valid, it won't pass ```expireAt: { $lt: new Date() }``` check, because its expireAt\nwill be later than a current date. In this case **updateOne** will try to create a new record in the collection, because of ```{ upsert: true }``` option.\nBut it will throw an error because we have a **unique** index. So this operation can only be successful when\nthere is no valid lock in the DB. If there is an invalid lock in the DB, it will be replaced by a new one.\n\nRelease and extend relies on the same logic, but we also compare with a key unique string.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkontsedal%2Flocco","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkontsedal%2Flocco","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkontsedal%2Flocco/lists"}