@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 +21 -0
- package/README.md +123 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.global.js +96 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +98 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +73 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -0
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)
|
package/dist/index.d.mts
ADDED
|
@@ -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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|