@universal-lock/redis 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Lucas Rainett
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.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # @universal-lock/redis
2
+
3
+ Redis backend for [`universal-lock`](https://github.com/lucasrainett/universal-lock). Provides distributed locking across processes and servers using atomic Lua scripts.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install universal-lock @universal-lock/redis
9
+ ```
10
+
11
+ You also need a Redis client library such as [ioredis](https://www.npmjs.com/package/ioredis) or [node-redis](https://www.npmjs.com/package/redis).
12
+
13
+ ## Usage
14
+
15
+ ### ESM with ioredis
16
+
17
+ ```typescript
18
+ import { lockFactory } from "universal-lock";
19
+ import { createBackend } from "@universal-lock/redis";
20
+ import Redis from "ioredis";
21
+
22
+ const client = new Redis();
23
+ const redisClient = {
24
+ eval: (script: string, keys: string[], args: string[]) => client.eval(script, keys.length, ...keys, ...args),
25
+ };
26
+
27
+ const lock = lockFactory(createBackend(redisClient));
28
+
29
+ const release = await lock.acquire("my-resource");
30
+ try {
31
+ // critical section — safe across processes and servers
32
+ } finally {
33
+ await release();
34
+ }
35
+ ```
36
+
37
+ ### ESM with node-redis
38
+
39
+ ```typescript
40
+ import { lockFactory } from "universal-lock";
41
+ import { createBackend } from "@universal-lock/redis";
42
+ import { createClient } from "redis";
43
+
44
+ const client = createClient();
45
+ await client.connect();
46
+ const redisClient = {
47
+ eval: (script: string, keys: string[], args: string[]) => client.eval(script, { keys, arguments: args }),
48
+ };
49
+
50
+ const lock = lockFactory(createBackend(redisClient));
51
+ ```
52
+
53
+ ### CommonJS
54
+
55
+ ```javascript
56
+ const { lockFactory } = require("universal-lock");
57
+ const { createBackend } = require("@universal-lock/redis");
58
+ const Redis = require("ioredis");
59
+
60
+ const client = new Redis();
61
+ const redisClient = {
62
+ eval: (script, keys, args) => client.eval(script, keys.length, ...keys, ...args),
63
+ };
64
+
65
+ const lock = lockFactory(createBackend(redisClient));
66
+ ```
67
+
68
+ ### Browser (IIFE)
69
+
70
+ ```html
71
+ <script src="https://unpkg.com/@universal-lock/redis/dist/index.global.js"></script>
72
+ <script src="https://unpkg.com/universal-lock/dist/index.global.js"></script>
73
+ <script>
74
+ const backend = UniversalLockRedis.createBackend(redisClient);
75
+ const lock = UniversalLock.lockFactory(backend);
76
+ </script>
77
+ ```
78
+
79
+ ## API
80
+
81
+ ### `createBackend(client, options?)`
82
+
83
+ Creates a Redis backend instance.
84
+
85
+ ```typescript
86
+ import { createBackend } from "@universal-lock/redis";
87
+
88
+ const backend = createBackend(redisClient, {
89
+ prefix: "my-app:", // key prefix (default: "universal-lock:")
90
+ });
91
+ ```
92
+
93
+ ### `RedisClient` interface
94
+
95
+ Any Redis client that implements this interface is supported:
96
+
97
+ ```typescript
98
+ interface RedisClient {
99
+ eval(script: string, keys: string[], args: string[]): Promise<unknown>;
100
+ }
101
+ ```
102
+
103
+ ### Options
104
+
105
+ | Option | Type | Default | Description |
106
+ | -------- | -------- | ------------------- | -------------------------- |
107
+ | `prefix` | `string` | `"universal-lock:"` | Prefix for Redis key names |
108
+
109
+ ## How It Works
110
+
111
+ All operations use atomic Lua scripts executed server-side on Redis:
112
+
113
+ - **Acquire** — `SET key value NX PX ttl` (set only if not exists, with TTL)
114
+ - **Renew** — Verify ownership, then `PEXPIRE` to extend TTL
115
+ - **Release** — Verify ownership, then `DEL` to remove
116
+
117
+ ## Limitations
118
+
119
+ - Works with a **single Redis instance** only. For multi-instance quorum locking (Redlock algorithm), a dedicated implementation is needed.
120
+
121
+ ## License
122
+
123
+ [MIT](https://github.com/lucasrainett/universal-lock/blob/master/LICENSE)
@@ -0,0 +1,11 @@
1
+ import { Backend } from '@universal-lock/types';
2
+
3
+ interface RedisClient {
4
+ eval(script: string, keys: string[], args: string[]): Promise<unknown>;
5
+ }
6
+ interface RedisBackendOptions {
7
+ prefix?: string;
8
+ }
9
+ declare const createBackend: (client: RedisClient, options?: RedisBackendOptions) => Backend;
10
+
11
+ export { type RedisBackendOptions, type RedisClient, createBackend };
@@ -0,0 +1,11 @@
1
+ import { Backend } from '@universal-lock/types';
2
+
3
+ interface RedisClient {
4
+ eval(script: string, keys: string[], args: string[]): Promise<unknown>;
5
+ }
6
+ interface RedisBackendOptions {
7
+ prefix?: string;
8
+ }
9
+ declare const createBackend: (client: RedisClient, options?: RedisBackendOptions) => Backend;
10
+
11
+ export { type RedisBackendOptions, type RedisClient, createBackend };
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ var UniversalLockRedis = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ createBackend: () => createBackend
25
+ });
26
+ var DEFAULT_PREFIX = "universal-lock:";
27
+ var ACQUIRE_SCRIPT = `
28
+ if redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then
29
+ return 1
30
+ end
31
+ return 0
32
+ `;
33
+ var RENEW_SCRIPT = `
34
+ if redis.call('GET', KEYS[1]) == ARGV[1] then
35
+ redis.call('PEXPIRE', KEYS[1], ARGV[2])
36
+ return 1
37
+ end
38
+ return 0
39
+ `;
40
+ var RELEASE_SCRIPT = `
41
+ if redis.call('GET', KEYS[1]) == ARGV[1] then
42
+ redis.call('DEL', KEYS[1])
43
+ return 1
44
+ end
45
+ return 0
46
+ `;
47
+ var createBackend = (client, options = {}) => {
48
+ const prefix = options.prefix ?? DEFAULT_PREFIX;
49
+ const ttls = /* @__PURE__ */ new Map();
50
+ const key = (lockName) => prefix + lockName;
51
+ const setup = async () => {
52
+ };
53
+ const acquire = async (lockName, stale, lockId) => {
54
+ const result = await client.eval(
55
+ ACQUIRE_SCRIPT,
56
+ [key(lockName)],
57
+ [lockId, String(stale)]
58
+ );
59
+ if (result !== 1) {
60
+ throw new Error(`${lockName} already locked`);
61
+ }
62
+ ttls.set(lockName, stale);
63
+ };
64
+ const renew = async (lockName, lockId) => {
65
+ const ttl = ttls.get(lockName);
66
+ if (!ttl) throw new Error(`${lockName} not locked`);
67
+ const result = await client.eval(
68
+ RENEW_SCRIPT,
69
+ [key(lockName)],
70
+ [lockId, String(ttl)]
71
+ );
72
+ if (result !== 1) {
73
+ throw new Error(`${lockName} not owned by caller`);
74
+ }
75
+ };
76
+ const release = async (lockName, lockId) => {
77
+ const result = await client.eval(
78
+ RELEASE_SCRIPT,
79
+ [key(lockName)],
80
+ [lockId]
81
+ );
82
+ ttls.delete(lockName);
83
+ if (result !== 1) {
84
+ throw new Error(`${lockName} not owned by caller`);
85
+ }
86
+ };
87
+ return {
88
+ setup,
89
+ acquire,
90
+ renew,
91
+ release
92
+ };
93
+ };
94
+ return __toCommonJS(index_exports);
95
+ })();
96
+ //# sourceMappingURL=index.global.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type {\n\tBackend,\n\tBackendAcquireFunction,\n\tBackendReleaseFunction,\n\tBackendRenewFunction,\n\tBackendSetupFunction,\n} from \"@universal-lock/types\";\n\nexport interface RedisClient {\n\teval(script: string, keys: string[], args: string[]): Promise<unknown>;\n}\n\nexport interface RedisBackendOptions {\n\tprefix?: string;\n}\n\nconst DEFAULT_PREFIX = \"universal-lock:\";\n\n// Atomically set the lock key only if it doesn't exist (NX), with a TTL in ms (PX).\n// This ensures only one client can hold the lock at a time.\nconst ACQUIRE_SCRIPT = `\nif redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then\n return 1\nend\nreturn 0\n`;\n\n// Renew only if the caller still owns the lock (value matches lockId).\n// Resets the TTL to prevent the lock from expiring while still in use.\nconst RENEW_SCRIPT = `\nif redis.call('GET', KEYS[1]) == ARGV[1] then\n redis.call('PEXPIRE', KEYS[1], ARGV[2])\n return 1\nend\nreturn 0\n`;\n\n// Delete only if the caller owns the lock, preventing one client from\n// releasing another client's lock after a stale takeover.\nconst RELEASE_SCRIPT = `\nif redis.call('GET', KEYS[1]) == ARGV[1] then\n redis.call('DEL', KEYS[1])\n return 1\nend\nreturn 0\n`;\n\nexport const createBackend = (\n\tclient: RedisClient,\n\toptions: RedisBackendOptions = {},\n): Backend => {\n\tconst prefix = options.prefix ?? DEFAULT_PREFIX;\n\t// Cache the stale TTL per lock so renew can re-apply the same expiration\n\tconst ttls = new Map<string, number>();\n\n\tconst key = (lockName: string) => prefix + lockName;\n\n\tconst setup: BackendSetupFunction = async () => {};\n\n\tconst acquire: BackendAcquireFunction = async (lockName, stale, lockId) => {\n\t\tconst result = await client.eval(\n\t\t\tACQUIRE_SCRIPT,\n\t\t\t[key(lockName)],\n\t\t\t[lockId, String(stale)],\n\t\t);\n\t\tif (result !== 1) {\n\t\t\tthrow new Error(`${lockName} already locked`);\n\t\t}\n\t\tttls.set(lockName, stale);\n\t};\n\n\tconst renew: BackendRenewFunction = async (lockName, lockId) => {\n\t\tconst ttl = ttls.get(lockName);\n\t\tif (!ttl) throw new Error(`${lockName} not locked`);\n\t\tconst result = await client.eval(\n\t\t\tRENEW_SCRIPT,\n\t\t\t[key(lockName)],\n\t\t\t[lockId, String(ttl)],\n\t\t);\n\t\tif (result !== 1) {\n\t\t\tthrow new Error(`${lockName} not owned by caller`);\n\t\t}\n\t};\n\n\tconst release: BackendReleaseFunction = async (lockName, lockId) => {\n\t\tconst result = await client.eval(\n\t\t\tRELEASE_SCRIPT,\n\t\t\t[key(lockName)],\n\t\t\t[lockId],\n\t\t);\n\t\tttls.delete(lockName);\n\t\tif (result !== 1) {\n\t\t\tthrow new Error(`${lockName} not owned by caller`);\n\t\t}\n\t};\n\n\treturn {\n\t\tsetup,\n\t\tacquire,\n\t\trenew,\n\t\trelease,\n\t};\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAgBA,MAAM,iBAAiB;AAIvB,MAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AASvB,MAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUrB,MAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQhB,MAAM,gBAAgB,CAC5B,QACA,UAA+B,CAAC,MACnB;AACb,UAAM,SAAS,QAAQ,UAAU;AAEjC,UAAM,OAAO,oBAAI,IAAoB;AAErC,UAAM,MAAM,CAAC,aAAqB,SAAS;AAE3C,UAAM,QAA8B,YAAY;AAAA,IAAC;AAEjD,UAAM,UAAkC,OAAO,UAAU,OAAO,WAAW;AAC1E,YAAM,SAAS,MAAM,OAAO;AAAA,QAC3B;AAAA,QACA,CAAC,IAAI,QAAQ,CAAC;AAAA,QACd,CAAC,QAAQ,OAAO,KAAK,CAAC;AAAA,MACvB;AACA,UAAI,WAAW,GAAG;AACjB,cAAM,IAAI,MAAM,GAAG,QAAQ,iBAAiB;AAAA,MAC7C;AACA,WAAK,IAAI,UAAU,KAAK;AAAA,IACzB;AAEA,UAAM,QAA8B,OAAO,UAAU,WAAW;AAC/D,YAAM,MAAM,KAAK,IAAI,QAAQ;AAC7B,UAAI,CAAC,IAAK,OAAM,IAAI,MAAM,GAAG,QAAQ,aAAa;AAClD,YAAM,SAAS,MAAM,OAAO;AAAA,QAC3B;AAAA,QACA,CAAC,IAAI,QAAQ,CAAC;AAAA,QACd,CAAC,QAAQ,OAAO,GAAG,CAAC;AAAA,MACrB;AACA,UAAI,WAAW,GAAG;AACjB,cAAM,IAAI,MAAM,GAAG,QAAQ,sBAAsB;AAAA,MAClD;AAAA,IACD;AAEA,UAAM,UAAkC,OAAO,UAAU,WAAW;AACnE,YAAM,SAAS,MAAM,OAAO;AAAA,QAC3B;AAAA,QACA,CAAC,IAAI,QAAQ,CAAC;AAAA,QACd,CAAC,MAAM;AAAA,MACR;AACA,WAAK,OAAO,QAAQ;AACpB,UAAI,WAAW,GAAG;AACjB,cAAM,IAAI,MAAM,GAAG,QAAQ,sBAAsB;AAAA,MAClD;AAAA,IACD;AAEA,WAAO;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACD;AAAA,EACD;","names":[]}
package/dist/index.js ADDED
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createBackend: () => createBackend
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var DEFAULT_PREFIX = "universal-lock:";
27
+ var ACQUIRE_SCRIPT = `
28
+ if redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then
29
+ return 1
30
+ end
31
+ return 0
32
+ `;
33
+ var RENEW_SCRIPT = `
34
+ if redis.call('GET', KEYS[1]) == ARGV[1] then
35
+ redis.call('PEXPIRE', KEYS[1], ARGV[2])
36
+ return 1
37
+ end
38
+ return 0
39
+ `;
40
+ var RELEASE_SCRIPT = `
41
+ if redis.call('GET', KEYS[1]) == ARGV[1] then
42
+ redis.call('DEL', KEYS[1])
43
+ return 1
44
+ end
45
+ return 0
46
+ `;
47
+ var createBackend = (client, options = {}) => {
48
+ const prefix = options.prefix ?? DEFAULT_PREFIX;
49
+ const ttls = /* @__PURE__ */ new Map();
50
+ const key = (lockName) => prefix + lockName;
51
+ const setup = async () => {
52
+ };
53
+ const acquire = async (lockName, stale, lockId) => {
54
+ const result = await client.eval(
55
+ ACQUIRE_SCRIPT,
56
+ [key(lockName)],
57
+ [lockId, String(stale)]
58
+ );
59
+ if (result !== 1) {
60
+ throw new Error(`${lockName} already locked`);
61
+ }
62
+ ttls.set(lockName, stale);
63
+ };
64
+ const renew = async (lockName, lockId) => {
65
+ const ttl = ttls.get(lockName);
66
+ if (!ttl) throw new Error(`${lockName} not locked`);
67
+ const result = await client.eval(
68
+ RENEW_SCRIPT,
69
+ [key(lockName)],
70
+ [lockId, String(ttl)]
71
+ );
72
+ if (result !== 1) {
73
+ throw new Error(`${lockName} not owned by caller`);
74
+ }
75
+ };
76
+ const release = async (lockName, lockId) => {
77
+ const result = await client.eval(
78
+ RELEASE_SCRIPT,
79
+ [key(lockName)],
80
+ [lockId]
81
+ );
82
+ ttls.delete(lockName);
83
+ if (result !== 1) {
84
+ throw new Error(`${lockName} not owned by caller`);
85
+ }
86
+ };
87
+ return {
88
+ setup,
89
+ acquire,
90
+ renew,
91
+ release
92
+ };
93
+ };
94
+ // Annotate the CommonJS export names for ESM import in node:
95
+ 0 && (module.exports = {
96
+ createBackend
97
+ });
98
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type {\n\tBackend,\n\tBackendAcquireFunction,\n\tBackendReleaseFunction,\n\tBackendRenewFunction,\n\tBackendSetupFunction,\n} from \"@universal-lock/types\";\n\nexport interface RedisClient {\n\teval(script: string, keys: string[], args: string[]): Promise<unknown>;\n}\n\nexport interface RedisBackendOptions {\n\tprefix?: string;\n}\n\nconst DEFAULT_PREFIX = \"universal-lock:\";\n\n// Atomically set the lock key only if it doesn't exist (NX), with a TTL in ms (PX).\n// This ensures only one client can hold the lock at a time.\nconst ACQUIRE_SCRIPT = `\nif redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then\n return 1\nend\nreturn 0\n`;\n\n// Renew only if the caller still owns the lock (value matches lockId).\n// Resets the TTL to prevent the lock from expiring while still in use.\nconst RENEW_SCRIPT = `\nif redis.call('GET', KEYS[1]) == ARGV[1] then\n redis.call('PEXPIRE', KEYS[1], ARGV[2])\n return 1\nend\nreturn 0\n`;\n\n// Delete only if the caller owns the lock, preventing one client from\n// releasing another client's lock after a stale takeover.\nconst RELEASE_SCRIPT = `\nif redis.call('GET', KEYS[1]) == ARGV[1] then\n redis.call('DEL', KEYS[1])\n return 1\nend\nreturn 0\n`;\n\nexport const createBackend = (\n\tclient: RedisClient,\n\toptions: RedisBackendOptions = {},\n): Backend => {\n\tconst prefix = options.prefix ?? DEFAULT_PREFIX;\n\t// Cache the stale TTL per lock so renew can re-apply the same expiration\n\tconst ttls = new Map<string, number>();\n\n\tconst key = (lockName: string) => prefix + lockName;\n\n\tconst setup: BackendSetupFunction = async () => {};\n\n\tconst acquire: BackendAcquireFunction = async (lockName, stale, lockId) => {\n\t\tconst result = await client.eval(\n\t\t\tACQUIRE_SCRIPT,\n\t\t\t[key(lockName)],\n\t\t\t[lockId, String(stale)],\n\t\t);\n\t\tif (result !== 1) {\n\t\t\tthrow new Error(`${lockName} already locked`);\n\t\t}\n\t\tttls.set(lockName, stale);\n\t};\n\n\tconst renew: BackendRenewFunction = async (lockName, lockId) => {\n\t\tconst ttl = ttls.get(lockName);\n\t\tif (!ttl) throw new Error(`${lockName} not locked`);\n\t\tconst result = await client.eval(\n\t\t\tRENEW_SCRIPT,\n\t\t\t[key(lockName)],\n\t\t\t[lockId, String(ttl)],\n\t\t);\n\t\tif (result !== 1) {\n\t\t\tthrow new Error(`${lockName} not owned by caller`);\n\t\t}\n\t};\n\n\tconst release: BackendReleaseFunction = async (lockName, lockId) => {\n\t\tconst result = await client.eval(\n\t\t\tRELEASE_SCRIPT,\n\t\t\t[key(lockName)],\n\t\t\t[lockId],\n\t\t);\n\t\tttls.delete(lockName);\n\t\tif (result !== 1) {\n\t\t\tthrow new Error(`${lockName} not owned by caller`);\n\t\t}\n\t};\n\n\treturn {\n\t\tsetup,\n\t\tacquire,\n\t\trenew,\n\t\trelease,\n\t};\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,IAAM,iBAAiB;AAIvB,IAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AASvB,IAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUrB,IAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQhB,IAAM,gBAAgB,CAC5B,QACA,UAA+B,CAAC,MACnB;AACb,QAAM,SAAS,QAAQ,UAAU;AAEjC,QAAM,OAAO,oBAAI,IAAoB;AAErC,QAAM,MAAM,CAAC,aAAqB,SAAS;AAE3C,QAAM,QAA8B,YAAY;AAAA,EAAC;AAEjD,QAAM,UAAkC,OAAO,UAAU,OAAO,WAAW;AAC1E,UAAM,SAAS,MAAM,OAAO;AAAA,MAC3B;AAAA,MACA,CAAC,IAAI,QAAQ,CAAC;AAAA,MACd,CAAC,QAAQ,OAAO,KAAK,CAAC;AAAA,IACvB;AACA,QAAI,WAAW,GAAG;AACjB,YAAM,IAAI,MAAM,GAAG,QAAQ,iBAAiB;AAAA,IAC7C;AACA,SAAK,IAAI,UAAU,KAAK;AAAA,EACzB;AAEA,QAAM,QAA8B,OAAO,UAAU,WAAW;AAC/D,UAAM,MAAM,KAAK,IAAI,QAAQ;AAC7B,QAAI,CAAC,IAAK,OAAM,IAAI,MAAM,GAAG,QAAQ,aAAa;AAClD,UAAM,SAAS,MAAM,OAAO;AAAA,MAC3B;AAAA,MACA,CAAC,IAAI,QAAQ,CAAC;AAAA,MACd,CAAC,QAAQ,OAAO,GAAG,CAAC;AAAA,IACrB;AACA,QAAI,WAAW,GAAG;AACjB,YAAM,IAAI,MAAM,GAAG,QAAQ,sBAAsB;AAAA,IAClD;AAAA,EACD;AAEA,QAAM,UAAkC,OAAO,UAAU,WAAW;AACnE,UAAM,SAAS,MAAM,OAAO;AAAA,MAC3B;AAAA,MACA,CAAC,IAAI,QAAQ,CAAC;AAAA,MACd,CAAC,MAAM;AAAA,IACR;AACA,SAAK,OAAO,QAAQ;AACpB,QAAI,WAAW,GAAG;AACjB,YAAM,IAAI,MAAM,GAAG,QAAQ,sBAAsB;AAAA,IAClD;AAAA,EACD;AAEA,SAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD;AACD;","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,73 @@
1
+ // src/index.ts
2
+ var DEFAULT_PREFIX = "universal-lock:";
3
+ var ACQUIRE_SCRIPT = `
4
+ if redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then
5
+ return 1
6
+ end
7
+ return 0
8
+ `;
9
+ var RENEW_SCRIPT = `
10
+ if redis.call('GET', KEYS[1]) == ARGV[1] then
11
+ redis.call('PEXPIRE', KEYS[1], ARGV[2])
12
+ return 1
13
+ end
14
+ return 0
15
+ `;
16
+ var RELEASE_SCRIPT = `
17
+ if redis.call('GET', KEYS[1]) == ARGV[1] then
18
+ redis.call('DEL', KEYS[1])
19
+ return 1
20
+ end
21
+ return 0
22
+ `;
23
+ var createBackend = (client, options = {}) => {
24
+ const prefix = options.prefix ?? DEFAULT_PREFIX;
25
+ const ttls = /* @__PURE__ */ new Map();
26
+ const key = (lockName) => prefix + lockName;
27
+ const setup = async () => {
28
+ };
29
+ const acquire = async (lockName, stale, lockId) => {
30
+ const result = await client.eval(
31
+ ACQUIRE_SCRIPT,
32
+ [key(lockName)],
33
+ [lockId, String(stale)]
34
+ );
35
+ if (result !== 1) {
36
+ throw new Error(`${lockName} already locked`);
37
+ }
38
+ ttls.set(lockName, stale);
39
+ };
40
+ const renew = async (lockName, lockId) => {
41
+ const ttl = ttls.get(lockName);
42
+ if (!ttl) throw new Error(`${lockName} not locked`);
43
+ const result = await client.eval(
44
+ RENEW_SCRIPT,
45
+ [key(lockName)],
46
+ [lockId, String(ttl)]
47
+ );
48
+ if (result !== 1) {
49
+ throw new Error(`${lockName} not owned by caller`);
50
+ }
51
+ };
52
+ const release = async (lockName, lockId) => {
53
+ const result = await client.eval(
54
+ RELEASE_SCRIPT,
55
+ [key(lockName)],
56
+ [lockId]
57
+ );
58
+ ttls.delete(lockName);
59
+ if (result !== 1) {
60
+ throw new Error(`${lockName} not owned by caller`);
61
+ }
62
+ };
63
+ return {
64
+ setup,
65
+ acquire,
66
+ renew,
67
+ release
68
+ };
69
+ };
70
+ export {
71
+ createBackend
72
+ };
73
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type {\n\tBackend,\n\tBackendAcquireFunction,\n\tBackendReleaseFunction,\n\tBackendRenewFunction,\n\tBackendSetupFunction,\n} from \"@universal-lock/types\";\n\nexport interface RedisClient {\n\teval(script: string, keys: string[], args: string[]): Promise<unknown>;\n}\n\nexport interface RedisBackendOptions {\n\tprefix?: string;\n}\n\nconst DEFAULT_PREFIX = \"universal-lock:\";\n\n// Atomically set the lock key only if it doesn't exist (NX), with a TTL in ms (PX).\n// This ensures only one client can hold the lock at a time.\nconst ACQUIRE_SCRIPT = `\nif redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then\n return 1\nend\nreturn 0\n`;\n\n// Renew only if the caller still owns the lock (value matches lockId).\n// Resets the TTL to prevent the lock from expiring while still in use.\nconst RENEW_SCRIPT = `\nif redis.call('GET', KEYS[1]) == ARGV[1] then\n redis.call('PEXPIRE', KEYS[1], ARGV[2])\n return 1\nend\nreturn 0\n`;\n\n// Delete only if the caller owns the lock, preventing one client from\n// releasing another client's lock after a stale takeover.\nconst RELEASE_SCRIPT = `\nif redis.call('GET', KEYS[1]) == ARGV[1] then\n redis.call('DEL', KEYS[1])\n return 1\nend\nreturn 0\n`;\n\nexport const createBackend = (\n\tclient: RedisClient,\n\toptions: RedisBackendOptions = {},\n): Backend => {\n\tconst prefix = options.prefix ?? DEFAULT_PREFIX;\n\t// Cache the stale TTL per lock so renew can re-apply the same expiration\n\tconst ttls = new Map<string, number>();\n\n\tconst key = (lockName: string) => prefix + lockName;\n\n\tconst setup: BackendSetupFunction = async () => {};\n\n\tconst acquire: BackendAcquireFunction = async (lockName, stale, lockId) => {\n\t\tconst result = await client.eval(\n\t\t\tACQUIRE_SCRIPT,\n\t\t\t[key(lockName)],\n\t\t\t[lockId, String(stale)],\n\t\t);\n\t\tif (result !== 1) {\n\t\t\tthrow new Error(`${lockName} already locked`);\n\t\t}\n\t\tttls.set(lockName, stale);\n\t};\n\n\tconst renew: BackendRenewFunction = async (lockName, lockId) => {\n\t\tconst ttl = ttls.get(lockName);\n\t\tif (!ttl) throw new Error(`${lockName} not locked`);\n\t\tconst result = await client.eval(\n\t\t\tRENEW_SCRIPT,\n\t\t\t[key(lockName)],\n\t\t\t[lockId, String(ttl)],\n\t\t);\n\t\tif (result !== 1) {\n\t\t\tthrow new Error(`${lockName} not owned by caller`);\n\t\t}\n\t};\n\n\tconst release: BackendReleaseFunction = async (lockName, lockId) => {\n\t\tconst result = await client.eval(\n\t\t\tRELEASE_SCRIPT,\n\t\t\t[key(lockName)],\n\t\t\t[lockId],\n\t\t);\n\t\tttls.delete(lockName);\n\t\tif (result !== 1) {\n\t\t\tthrow new Error(`${lockName} not owned by caller`);\n\t\t}\n\t};\n\n\treturn {\n\t\tsetup,\n\t\tacquire,\n\t\trenew,\n\t\trelease,\n\t};\n};\n"],"mappings":";AAgBA,IAAM,iBAAiB;AAIvB,IAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AASvB,IAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUrB,IAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQhB,IAAM,gBAAgB,CAC5B,QACA,UAA+B,CAAC,MACnB;AACb,QAAM,SAAS,QAAQ,UAAU;AAEjC,QAAM,OAAO,oBAAI,IAAoB;AAErC,QAAM,MAAM,CAAC,aAAqB,SAAS;AAE3C,QAAM,QAA8B,YAAY;AAAA,EAAC;AAEjD,QAAM,UAAkC,OAAO,UAAU,OAAO,WAAW;AAC1E,UAAM,SAAS,MAAM,OAAO;AAAA,MAC3B;AAAA,MACA,CAAC,IAAI,QAAQ,CAAC;AAAA,MACd,CAAC,QAAQ,OAAO,KAAK,CAAC;AAAA,IACvB;AACA,QAAI,WAAW,GAAG;AACjB,YAAM,IAAI,MAAM,GAAG,QAAQ,iBAAiB;AAAA,IAC7C;AACA,SAAK,IAAI,UAAU,KAAK;AAAA,EACzB;AAEA,QAAM,QAA8B,OAAO,UAAU,WAAW;AAC/D,UAAM,MAAM,KAAK,IAAI,QAAQ;AAC7B,QAAI,CAAC,IAAK,OAAM,IAAI,MAAM,GAAG,QAAQ,aAAa;AAClD,UAAM,SAAS,MAAM,OAAO;AAAA,MAC3B;AAAA,MACA,CAAC,IAAI,QAAQ,CAAC;AAAA,MACd,CAAC,QAAQ,OAAO,GAAG,CAAC;AAAA,IACrB;AACA,QAAI,WAAW,GAAG;AACjB,YAAM,IAAI,MAAM,GAAG,QAAQ,sBAAsB;AAAA,IAClD;AAAA,EACD;AAEA,QAAM,UAAkC,OAAO,UAAU,WAAW;AACnE,UAAM,SAAS,MAAM,OAAO;AAAA,MAC3B;AAAA,MACA,CAAC,IAAI,QAAQ,CAAC;AAAA,MACd,CAAC,MAAM;AAAA,IACR;AACA,SAAK,OAAO,QAAQ;AACpB,QAAI,WAAW,GAAG;AACjB,YAAM,IAAI,MAAM,GAAG,QAAQ,sBAAsB;AAAA,IAClD;AAAA,EACD;AAEA,SAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD;AACD;","names":[]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@universal-lock/redis",
3
+ "version": "1.0.0",
4
+ "description": "Redis backend for universal-lock — distributed locking via Lua scripts",
5
+ "sideEffects": false,
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "main": "dist/index.js",
10
+ "module": "dist/index.mjs",
11
+ "browser": "dist/index.global.js",
12
+ "types": "dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "import": {
16
+ "types": "./dist/index.d.mts",
17
+ "default": "./dist/index.mjs"
18
+ },
19
+ "require": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ }
23
+ }
24
+ },
25
+ "files": [
26
+ "/dist"
27
+ ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/lucasrainett/universal-lock",
31
+ "directory": "packages/redis"
32
+ },
33
+ "keywords": [
34
+ "universal-lock",
35
+ "lock",
36
+ "mutex",
37
+ "redis",
38
+ "distributed-lock",
39
+ "backend"
40
+ ],
41
+ "author": {
42
+ "name": "Lucas Rainett",
43
+ "email": "lucas@rainett.dev",
44
+ "url": "https://github.com/lucasrainett"
45
+ },
46
+ "license": "MIT",
47
+ "dependencies": {
48
+ "@universal-lock/types": "1.0.0"
49
+ },
50
+ "scripts": {
51
+ "build": "tsup",
52
+ "clean": "rm -rf dist"
53
+ }
54
+ }