@anchan828/nest-sesamecare-redlock 0.2.44
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/README.md +159 -0
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.js +11 -0
- package/dist/cjs/redlock.constants.d.ts +1 -0
- package/dist/cjs/redlock.constants.js +4 -0
- package/dist/cjs/redlock.decorator.d.ts +3 -0
- package/dist/cjs/redlock.decorator.js +60 -0
- package/dist/cjs/redlock.fake-service.d.ts +10 -0
- package/dist/cjs/redlock.fake-service.js +31 -0
- package/dist/cjs/redlock.interface.d.ts +54 -0
- package/dist/cjs/redlock.interface.js +2 -0
- package/dist/cjs/redlock.module-definition.d.ts +2 -0
- package/dist/cjs/redlock.module-definition.js +6 -0
- package/dist/cjs/redlock.module.d.ts +8 -0
- package/dist/cjs/redlock.module.js +53 -0
- package/dist/cjs/redlock.service.d.ts +6 -0
- package/dist/cjs/redlock.service.js +30 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/redlock.constants.d.ts +1 -0
- package/dist/esm/redlock.constants.js +1 -0
- package/dist/esm/redlock.decorator.d.ts +3 -0
- package/dist/esm/redlock.decorator.js +57 -0
- package/dist/esm/redlock.fake-service.d.ts +10 -0
- package/dist/esm/redlock.fake-service.js +27 -0
- package/dist/esm/redlock.interface.d.ts +54 -0
- package/dist/esm/redlock.interface.js +1 -0
- package/dist/esm/redlock.module-definition.d.ts +2 -0
- package/dist/esm/redlock.module-definition.js +2 -0
- package/dist/esm/redlock.module.d.ts +8 -0
- package/dist/esm/redlock.module.js +50 -0
- package/dist/esm/redlock.service.d.ts +6 -0
- package/dist/esm/redlock.service.js +27 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# @anchan828/nest-sesamecare-redlock
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
This is a [Nest](https://github.com/nestjs/nest) implementation of the redlock algorithm for distributed redis locks.
|
|
7
|
+
|
|
8
|
+
This package uses [@sesamecare-oss/redlock](https://github.com/sesamecare/redlock).
|
|
9
|
+
|
|
10
|
+
> [!NOTE]
|
|
11
|
+
> This is one of the solutions to provisionally address the various issues with node-redlock that don't seem likely to be resolved soon. For details, please see https://github.com/anchan828/nest-redlock/pull/723.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
$ npm i --save @anchan828/nest-sesamecare-redlock ioredis
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### 1. Import module
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { RedlockModule } from "@anchan828/nest-sesamecare-redlock";
|
|
25
|
+
import Redis from "ioredis";
|
|
26
|
+
|
|
27
|
+
@Module({
|
|
28
|
+
imports: [
|
|
29
|
+
RedlockModule.register({
|
|
30
|
+
// See https://github.com/sesamecare/redlock#configuration
|
|
31
|
+
clients: [new Redis({ host: "localhost" })],
|
|
32
|
+
settings: {
|
|
33
|
+
driftFactor: 0.01,
|
|
34
|
+
retryCount: 10,
|
|
35
|
+
retryDelay: 200,
|
|
36
|
+
retryJitter: 200,
|
|
37
|
+
automaticExtensionThreshold: 500,
|
|
38
|
+
},
|
|
39
|
+
// Default duratiuon to use with Redlock decorator
|
|
40
|
+
duration: 1000,
|
|
41
|
+
}),
|
|
42
|
+
],
|
|
43
|
+
})
|
|
44
|
+
export class AppModule {}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Add `Redlock` decorator
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { Redlock } from "@anchan828/nest-sesamecare-redlock";
|
|
51
|
+
|
|
52
|
+
@Injectable()
|
|
53
|
+
export class ExampleService {
|
|
54
|
+
@Redlock("lock-key")
|
|
55
|
+
public async addComment(projectId: number, comment: string): Promise<void> {}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This is complete. redlock is working correctly!
|
|
60
|
+
See [node-redlock](https://github.com/sesamecare/redlock) for more information on redlock.
|
|
61
|
+
|
|
62
|
+
## Define complex resources (lock keys)
|
|
63
|
+
|
|
64
|
+
Using constants causes the same lock key to be used for all calls. Let's reduce the scope a bit more.
|
|
65
|
+
|
|
66
|
+
In this example, only certain projects are now locked.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { Redlock } from "@anchan828/nest-sesamecare-redlock";
|
|
70
|
+
|
|
71
|
+
@Injectable()
|
|
72
|
+
export class ExampleService {
|
|
73
|
+
// The arguments define the class object to which the decorator is being added and the method arguments in order.
|
|
74
|
+
@Redlock<ExampleService["addComment"]>(
|
|
75
|
+
(target: ExampleService, projectId: number, comment: string) => `projects/${projectId}/comments`,
|
|
76
|
+
)
|
|
77
|
+
public async addComment(projectId: number, comment: string): Promise<void> {}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Of course, you can lock multiple keys.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
@Injectable()
|
|
85
|
+
export class ExampleService {
|
|
86
|
+
@Redlock<ExampleService["updateComments"]>(
|
|
87
|
+
(target: ExampleService, projectId: number, args: Array<{ commentId: number; comment: string }>) =>
|
|
88
|
+
args.map((arg) => `projects/${projectId}/comments/${arg.commentId}`),
|
|
89
|
+
)
|
|
90
|
+
public async updateComments(projectId: number, args: Array<{ commentId: number; comment: string }>): Promise<void> {}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Using Redlock service
|
|
95
|
+
|
|
96
|
+
If you want to use node-redlock as is, use RedlockService.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { RedlockService } from "@anchan828/nest-sesamecare-redlock";
|
|
100
|
+
|
|
101
|
+
@Injectable()
|
|
102
|
+
export class ExampleService {
|
|
103
|
+
constructor(private readonly redlock: RedlockService) {}
|
|
104
|
+
|
|
105
|
+
public async addComment(projectId: number, comment: string): Promise<void> {
|
|
106
|
+
await this.redlock.using([`projects/${projectId}/comments`], 5000, (signal) => {
|
|
107
|
+
// Do something...
|
|
108
|
+
|
|
109
|
+
if (signal.aborted) {
|
|
110
|
+
throw signal.error;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Using fake RedlockService
|
|
118
|
+
|
|
119
|
+
If you do not want to use Redis in your Unit tests, define the fake class as RedlockService.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const app = await Test.createTestingModule({
|
|
123
|
+
providers: [TestService, { provide: RedlockService, useClass: FakeRedlockService }],
|
|
124
|
+
}).compile();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Troubleshooting
|
|
128
|
+
|
|
129
|
+
### Nest can't resolve dependencies of the XXX. Please make sure that the "@redlockService" property is available in the current context.
|
|
130
|
+
|
|
131
|
+
This is the error output when using the Redlock decorator without importing the RedlockModule.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { RedlockModule } from "@anchan828/nest-sesamecare-redlock";
|
|
135
|
+
import Redis from "ioredis";
|
|
136
|
+
|
|
137
|
+
@Module({
|
|
138
|
+
imports: [
|
|
139
|
+
RedlockModule.register({
|
|
140
|
+
clients: [new Redis({ host: "localhost" })],
|
|
141
|
+
}),
|
|
142
|
+
],
|
|
143
|
+
})
|
|
144
|
+
export class AppModule {}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### What should I do with Unit tests, I don't want to use Redis.
|
|
148
|
+
|
|
149
|
+
Use `FakeRedlockService` class. Register FakeRedlockService with the provider as RedlockService.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const app = await Test.createTestingModule({
|
|
153
|
+
providers: [TestService, { provide: RedlockService, useClass: FakeRedlockService }],
|
|
154
|
+
}).compile();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Redlock } from "./redlock.decorator";
|
|
2
|
+
export { FakeRedlockService } from "./redlock.fake-service";
|
|
3
|
+
export { LockedKeysHookArgs, PreLockedKeysHookArgs, RedlockKeyFunction, RedlockModuleOptions, UnlockedKeysHookArgs, } from "./redlock.interface";
|
|
4
|
+
export { RedlockModule } from "./redlock.module";
|
|
5
|
+
export { RedlockService } from "./redlock.service";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RedlockService = exports.RedlockModule = exports.FakeRedlockService = exports.Redlock = void 0;
|
|
4
|
+
var redlock_decorator_1 = require("./redlock.decorator");
|
|
5
|
+
Object.defineProperty(exports, "Redlock", { enumerable: true, get: function () { return redlock_decorator_1.Redlock; } });
|
|
6
|
+
var redlock_fake_service_1 = require("./redlock.fake-service");
|
|
7
|
+
Object.defineProperty(exports, "FakeRedlockService", { enumerable: true, get: function () { return redlock_fake_service_1.FakeRedlockService; } });
|
|
8
|
+
var redlock_module_1 = require("./redlock.module");
|
|
9
|
+
Object.defineProperty(exports, "RedlockModule", { enumerable: true, get: function () { return redlock_module_1.RedlockModule; } });
|
|
10
|
+
var redlock_service_1 = require("./redlock.service");
|
|
11
|
+
Object.defineProperty(exports, "RedlockService", { enumerable: true, get: function () { return redlock_service_1.RedlockService; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DEFAULT_DURATION = 5000;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { Settings } from "@sesamecare-oss/redlock";
|
|
2
|
+
import { RedlockKeyFunction } from "./redlock.interface";
|
|
3
|
+
export declare function Redlock<T extends (...args: any) => any = (...args: any) => any>(key: string | string[] | RedlockKeyFunction<T>, duration?: number, settings?: Partial<Settings>): MethodDecorator;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Redlock = Redlock;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
const redlock_constants_1 = require("./redlock.constants");
|
|
6
|
+
const redlock_service_1 = require("./redlock.service");
|
|
7
|
+
function Redlock(key, duration, settings = {}) {
|
|
8
|
+
const injectRedlockService = (0, common_1.Inject)(redlock_service_1.RedlockService);
|
|
9
|
+
return (target, propertyKey, descriptor) => {
|
|
10
|
+
const serviceSymbol = "@redlockService";
|
|
11
|
+
injectRedlockService(target, serviceSymbol);
|
|
12
|
+
const originalMethod = descriptor.value;
|
|
13
|
+
descriptor.value = async function (...args) {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
15
|
+
const descriptorThis = this;
|
|
16
|
+
const redlockService = descriptorThis[serviceSymbol];
|
|
17
|
+
if (redlockService?.options?.decoratorEnabled !== undefined && !redlockService.options.decoratorEnabled) {
|
|
18
|
+
return await originalMethod.apply(descriptorThis, args);
|
|
19
|
+
}
|
|
20
|
+
const keys = getKeys(key, descriptorThis, args);
|
|
21
|
+
const useDuration = duration || redlockService.options?.duration || redlock_constants_1.DEFAULT_DURATION;
|
|
22
|
+
await redlockService.options?.decoratorHooks?.preLockKeys?.({ keys, duration: useDuration });
|
|
23
|
+
const startTime = Date.now();
|
|
24
|
+
return await redlockService
|
|
25
|
+
.using(keys, useDuration, settings, async (signal) => {
|
|
26
|
+
if (signal.aborted) {
|
|
27
|
+
throw signal.error;
|
|
28
|
+
}
|
|
29
|
+
await redlockService.options?.decoratorHooks?.lockedKeys?.({
|
|
30
|
+
keys,
|
|
31
|
+
duration: useDuration,
|
|
32
|
+
elapsedTime: Date.now() - startTime,
|
|
33
|
+
});
|
|
34
|
+
const result = await originalMethod.apply(descriptorThis, args);
|
|
35
|
+
return result;
|
|
36
|
+
})
|
|
37
|
+
.finally(async () => {
|
|
38
|
+
await redlockService.options?.decoratorHooks?.unlockedKeys?.({
|
|
39
|
+
keys,
|
|
40
|
+
duration: useDuration,
|
|
41
|
+
elapsedTime: Date.now() - startTime,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
return descriptor;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function getKeys(key, descriptorThis, args) {
|
|
49
|
+
const keys = new Set();
|
|
50
|
+
if (typeof key === "string") {
|
|
51
|
+
keys.add(key);
|
|
52
|
+
}
|
|
53
|
+
else if (Array.isArray(key)) {
|
|
54
|
+
key.forEach((k) => keys.add(k));
|
|
55
|
+
}
|
|
56
|
+
else if (typeof key === "function") {
|
|
57
|
+
[key(descriptorThis, ...args)].flat().forEach((k) => keys.add(k));
|
|
58
|
+
}
|
|
59
|
+
return Array.from(keys);
|
|
60
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ExecutionResult, Lock, RedlockAbortSignal, Settings } from "@sesamecare-oss/redlock";
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
export declare class FakeRedlockService extends EventEmitter {
|
|
4
|
+
quit(): Promise<void>;
|
|
5
|
+
acquire(keys: string[], duration: number, settings?: Partial<Settings> | undefined): Promise<Lock>;
|
|
6
|
+
release(lock: Lock, settings?: Partial<Settings> | undefined): Promise<ExecutionResult>;
|
|
7
|
+
extend(existing: Lock, duration: number, settings?: Partial<Settings> | undefined): Promise<Lock>;
|
|
8
|
+
using<T>(keys: string[], duration: number, settings: Partial<Settings>, routine?: ((signal: RedlockAbortSignal) => Promise<T>) | undefined): Promise<T>;
|
|
9
|
+
using<T>(keys: string[], duration: number, routine: (signal: RedlockAbortSignal) => Promise<T>): Promise<T>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FakeRedlockService = void 0;
|
|
4
|
+
const events_1 = require("events");
|
|
5
|
+
class FakeRedlockService extends events_1.EventEmitter {
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
7
|
+
async quit() { }
|
|
8
|
+
async acquire(keys, duration, settings) {
|
|
9
|
+
return createLockFake();
|
|
10
|
+
}
|
|
11
|
+
async release(lock, settings) {
|
|
12
|
+
return { attempts: [], start: Date.now() };
|
|
13
|
+
}
|
|
14
|
+
async extend(existing, duration, settings) {
|
|
15
|
+
return createLockFake();
|
|
16
|
+
}
|
|
17
|
+
async using(keys, duration, settingsOrRoutine, routine) {
|
|
18
|
+
const routineFunc = typeof settingsOrRoutine === "function" ? settingsOrRoutine : routine;
|
|
19
|
+
return await routineFunc?.({ aborted: false });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.FakeRedlockService = FakeRedlockService;
|
|
23
|
+
function createLockFake() {
|
|
24
|
+
let lock;
|
|
25
|
+
// eslint-disable-next-line prefer-const
|
|
26
|
+
lock = {
|
|
27
|
+
release: async () => ({ attempts: [], start: Date.now() }),
|
|
28
|
+
extend: async (duration) => lock,
|
|
29
|
+
};
|
|
30
|
+
return lock;
|
|
31
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import Redis, { Cluster } from "ioredis";
|
|
2
|
+
import { Settings } from "@sesamecare-oss/redlock";
|
|
3
|
+
export type PreLockedKeysHookArgs = {
|
|
4
|
+
keys: string[];
|
|
5
|
+
duration: number;
|
|
6
|
+
};
|
|
7
|
+
export type LockedKeysHookArgs = {
|
|
8
|
+
keys: string[];
|
|
9
|
+
duration: number;
|
|
10
|
+
elapsedTime: number;
|
|
11
|
+
};
|
|
12
|
+
export type UnlockedKeysHookArgs = {
|
|
13
|
+
keys: string[];
|
|
14
|
+
duration: number;
|
|
15
|
+
elapsedTime: number;
|
|
16
|
+
};
|
|
17
|
+
export type RedlockModuleOptions = {
|
|
18
|
+
clients: Iterable<Redis | Cluster>;
|
|
19
|
+
/**
|
|
20
|
+
* Default: true
|
|
21
|
+
* Used only with @Redlock decorator.
|
|
22
|
+
*/
|
|
23
|
+
decoratorEnabled?: boolean;
|
|
24
|
+
settings?: Partial<Settings>;
|
|
25
|
+
scripts?: {
|
|
26
|
+
readonly acquireScript?: string | ((script: string) => string);
|
|
27
|
+
readonly extendScript?: string | ((script: string) => string);
|
|
28
|
+
readonly releaseScript?: string | ((script: string) => string);
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Hooks called when using @Redlock decorator.
|
|
32
|
+
*/
|
|
33
|
+
decoratorHooks?: {
|
|
34
|
+
/**
|
|
35
|
+
* Called before redlock.using
|
|
36
|
+
*/
|
|
37
|
+
readonly preLockKeys?: (args: PreLockedKeysHookArgs) => void | Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Called first when the redlock.using callback is invoked.
|
|
40
|
+
*/
|
|
41
|
+
readonly lockedKeys?: (args: LockedKeysHookArgs) => void | Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Called after when the redlock.using callback is finished.
|
|
44
|
+
*/
|
|
45
|
+
readonly unlockedKeys?: (args: UnlockedKeysHookArgs) => void | Promise<void>;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Default duratiuon to use with Redlock decorator
|
|
49
|
+
*
|
|
50
|
+
* @type {number}
|
|
51
|
+
*/
|
|
52
|
+
duration?: number;
|
|
53
|
+
};
|
|
54
|
+
export type RedlockKeyFunction<T extends (...args: any) => any = (...args: any) => any> = (target: any, ...args: Parameters<T>) => string[] | string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var _a;
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.MODULE_OPTIONS_TOKEN = exports.ConfigurableModuleClass = void 0;
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
_a = new common_1.ConfigurableModuleBuilder().build(), exports.ConfigurableModuleClass = _a.ConfigurableModuleClass, exports.MODULE_OPTIONS_TOKEN = _a.MODULE_OPTIONS_TOKEN;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { OnApplicationShutdown } from "@nestjs/common";
|
|
2
|
+
import { RedlockModuleOptions } from "./redlock.interface";
|
|
3
|
+
import { ConfigurableModuleClass } from "./redlock.module-definition";
|
|
4
|
+
export declare class RedlockModule extends ConfigurableModuleClass implements OnApplicationShutdown {
|
|
5
|
+
private readonly options;
|
|
6
|
+
constructor(options: RedlockModuleOptions);
|
|
7
|
+
onApplicationShutdown(): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.RedlockModule = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const redlock_module_definition_1 = require("./redlock.module-definition");
|
|
18
|
+
const redlock_service_1 = require("./redlock.service");
|
|
19
|
+
let RedlockModule = class RedlockModule extends redlock_module_definition_1.ConfigurableModuleClass {
|
|
20
|
+
constructor(options) {
|
|
21
|
+
super();
|
|
22
|
+
this.options = options;
|
|
23
|
+
}
|
|
24
|
+
async onApplicationShutdown() {
|
|
25
|
+
for (const client of this.options.clients) {
|
|
26
|
+
switch (client.status) {
|
|
27
|
+
case "end":
|
|
28
|
+
continue;
|
|
29
|
+
case "ready":
|
|
30
|
+
await client.quit();
|
|
31
|
+
break;
|
|
32
|
+
default:
|
|
33
|
+
client.disconnect();
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
exports.RedlockModule = RedlockModule;
|
|
40
|
+
exports.RedlockModule = RedlockModule = __decorate([
|
|
41
|
+
(0, common_1.Module)({
|
|
42
|
+
providers: [
|
|
43
|
+
{
|
|
44
|
+
provide: redlock_service_1.RedlockService,
|
|
45
|
+
inject: [redlock_module_definition_1.MODULE_OPTIONS_TOKEN],
|
|
46
|
+
useFactory: (options) => new redlock_service_1.RedlockService(options),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
exports: [redlock_service_1.RedlockService],
|
|
50
|
+
}),
|
|
51
|
+
__param(0, (0, common_1.Inject)(redlock_module_definition_1.MODULE_OPTIONS_TOKEN)),
|
|
52
|
+
__metadata("design:paramtypes", [Object])
|
|
53
|
+
], RedlockModule);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.RedlockService = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const redlock_1 = require("@sesamecare-oss/redlock");
|
|
18
|
+
const redlock_module_definition_1 = require("./redlock.module-definition");
|
|
19
|
+
let RedlockService = class RedlockService extends redlock_1.Redlock {
|
|
20
|
+
constructor(options) {
|
|
21
|
+
super(options.clients, options.settings);
|
|
22
|
+
this.options = options;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
exports.RedlockService = RedlockService;
|
|
26
|
+
exports.RedlockService = RedlockService = __decorate([
|
|
27
|
+
(0, common_1.Injectable)(),
|
|
28
|
+
__param(0, (0, common_1.Inject)(redlock_module_definition_1.MODULE_OPTIONS_TOKEN)),
|
|
29
|
+
__metadata("design:paramtypes", [Object])
|
|
30
|
+
], RedlockService);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Redlock } from "./redlock.decorator";
|
|
2
|
+
export { FakeRedlockService } from "./redlock.fake-service";
|
|
3
|
+
export { LockedKeysHookArgs, PreLockedKeysHookArgs, RedlockKeyFunction, RedlockModuleOptions, UnlockedKeysHookArgs, } from "./redlock.interface";
|
|
4
|
+
export { RedlockModule } from "./redlock.module";
|
|
5
|
+
export { RedlockService } from "./redlock.service";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DEFAULT_DURATION = 5000;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_DURATION = 5000;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { Settings } from "@sesamecare-oss/redlock";
|
|
2
|
+
import { RedlockKeyFunction } from "./redlock.interface";
|
|
3
|
+
export declare function Redlock<T extends (...args: any) => any = (...args: any) => any>(key: string | string[] | RedlockKeyFunction<T>, duration?: number, settings?: Partial<Settings>): MethodDecorator;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Inject } from "@nestjs/common";
|
|
2
|
+
import { DEFAULT_DURATION } from "./redlock.constants";
|
|
3
|
+
import { RedlockService } from "./redlock.service";
|
|
4
|
+
export function Redlock(key, duration, settings = {}) {
|
|
5
|
+
const injectRedlockService = Inject(RedlockService);
|
|
6
|
+
return (target, propertyKey, descriptor) => {
|
|
7
|
+
const serviceSymbol = "@redlockService";
|
|
8
|
+
injectRedlockService(target, serviceSymbol);
|
|
9
|
+
const originalMethod = descriptor.value;
|
|
10
|
+
descriptor.value = async function (...args) {
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
12
|
+
const descriptorThis = this;
|
|
13
|
+
const redlockService = descriptorThis[serviceSymbol];
|
|
14
|
+
if (redlockService?.options?.decoratorEnabled !== undefined && !redlockService.options.decoratorEnabled) {
|
|
15
|
+
return await originalMethod.apply(descriptorThis, args);
|
|
16
|
+
}
|
|
17
|
+
const keys = getKeys(key, descriptorThis, args);
|
|
18
|
+
const useDuration = duration || redlockService.options?.duration || DEFAULT_DURATION;
|
|
19
|
+
await redlockService.options?.decoratorHooks?.preLockKeys?.({ keys, duration: useDuration });
|
|
20
|
+
const startTime = Date.now();
|
|
21
|
+
return await redlockService
|
|
22
|
+
.using(keys, useDuration, settings, async (signal) => {
|
|
23
|
+
if (signal.aborted) {
|
|
24
|
+
throw signal.error;
|
|
25
|
+
}
|
|
26
|
+
await redlockService.options?.decoratorHooks?.lockedKeys?.({
|
|
27
|
+
keys,
|
|
28
|
+
duration: useDuration,
|
|
29
|
+
elapsedTime: Date.now() - startTime,
|
|
30
|
+
});
|
|
31
|
+
const result = await originalMethod.apply(descriptorThis, args);
|
|
32
|
+
return result;
|
|
33
|
+
})
|
|
34
|
+
.finally(async () => {
|
|
35
|
+
await redlockService.options?.decoratorHooks?.unlockedKeys?.({
|
|
36
|
+
keys,
|
|
37
|
+
duration: useDuration,
|
|
38
|
+
elapsedTime: Date.now() - startTime,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
return descriptor;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function getKeys(key, descriptorThis, args) {
|
|
46
|
+
const keys = new Set();
|
|
47
|
+
if (typeof key === "string") {
|
|
48
|
+
keys.add(key);
|
|
49
|
+
}
|
|
50
|
+
else if (Array.isArray(key)) {
|
|
51
|
+
key.forEach((k) => keys.add(k));
|
|
52
|
+
}
|
|
53
|
+
else if (typeof key === "function") {
|
|
54
|
+
[key(descriptorThis, ...args)].flat().forEach((k) => keys.add(k));
|
|
55
|
+
}
|
|
56
|
+
return Array.from(keys);
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ExecutionResult, Lock, RedlockAbortSignal, Settings } from "@sesamecare-oss/redlock";
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
export declare class FakeRedlockService extends EventEmitter {
|
|
4
|
+
quit(): Promise<void>;
|
|
5
|
+
acquire(keys: string[], duration: number, settings?: Partial<Settings> | undefined): Promise<Lock>;
|
|
6
|
+
release(lock: Lock, settings?: Partial<Settings> | undefined): Promise<ExecutionResult>;
|
|
7
|
+
extend(existing: Lock, duration: number, settings?: Partial<Settings> | undefined): Promise<Lock>;
|
|
8
|
+
using<T>(keys: string[], duration: number, settings: Partial<Settings>, routine?: ((signal: RedlockAbortSignal) => Promise<T>) | undefined): Promise<T>;
|
|
9
|
+
using<T>(keys: string[], duration: number, routine: (signal: RedlockAbortSignal) => Promise<T>): Promise<T>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
export class FakeRedlockService extends EventEmitter {
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
4
|
+
async quit() { }
|
|
5
|
+
async acquire(keys, duration, settings) {
|
|
6
|
+
return createLockFake();
|
|
7
|
+
}
|
|
8
|
+
async release(lock, settings) {
|
|
9
|
+
return { attempts: [], start: Date.now() };
|
|
10
|
+
}
|
|
11
|
+
async extend(existing, duration, settings) {
|
|
12
|
+
return createLockFake();
|
|
13
|
+
}
|
|
14
|
+
async using(keys, duration, settingsOrRoutine, routine) {
|
|
15
|
+
const routineFunc = typeof settingsOrRoutine === "function" ? settingsOrRoutine : routine;
|
|
16
|
+
return await routineFunc?.({ aborted: false });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function createLockFake() {
|
|
20
|
+
let lock;
|
|
21
|
+
// eslint-disable-next-line prefer-const
|
|
22
|
+
lock = {
|
|
23
|
+
release: async () => ({ attempts: [], start: Date.now() }),
|
|
24
|
+
extend: async (duration) => lock,
|
|
25
|
+
};
|
|
26
|
+
return lock;
|
|
27
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import Redis, { Cluster } from "ioredis";
|
|
2
|
+
import { Settings } from "@sesamecare-oss/redlock";
|
|
3
|
+
export type PreLockedKeysHookArgs = {
|
|
4
|
+
keys: string[];
|
|
5
|
+
duration: number;
|
|
6
|
+
};
|
|
7
|
+
export type LockedKeysHookArgs = {
|
|
8
|
+
keys: string[];
|
|
9
|
+
duration: number;
|
|
10
|
+
elapsedTime: number;
|
|
11
|
+
};
|
|
12
|
+
export type UnlockedKeysHookArgs = {
|
|
13
|
+
keys: string[];
|
|
14
|
+
duration: number;
|
|
15
|
+
elapsedTime: number;
|
|
16
|
+
};
|
|
17
|
+
export type RedlockModuleOptions = {
|
|
18
|
+
clients: Iterable<Redis | Cluster>;
|
|
19
|
+
/**
|
|
20
|
+
* Default: true
|
|
21
|
+
* Used only with @Redlock decorator.
|
|
22
|
+
*/
|
|
23
|
+
decoratorEnabled?: boolean;
|
|
24
|
+
settings?: Partial<Settings>;
|
|
25
|
+
scripts?: {
|
|
26
|
+
readonly acquireScript?: string | ((script: string) => string);
|
|
27
|
+
readonly extendScript?: string | ((script: string) => string);
|
|
28
|
+
readonly releaseScript?: string | ((script: string) => string);
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Hooks called when using @Redlock decorator.
|
|
32
|
+
*/
|
|
33
|
+
decoratorHooks?: {
|
|
34
|
+
/**
|
|
35
|
+
* Called before redlock.using
|
|
36
|
+
*/
|
|
37
|
+
readonly preLockKeys?: (args: PreLockedKeysHookArgs) => void | Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Called first when the redlock.using callback is invoked.
|
|
40
|
+
*/
|
|
41
|
+
readonly lockedKeys?: (args: LockedKeysHookArgs) => void | Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Called after when the redlock.using callback is finished.
|
|
44
|
+
*/
|
|
45
|
+
readonly unlockedKeys?: (args: UnlockedKeysHookArgs) => void | Promise<void>;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Default duratiuon to use with Redlock decorator
|
|
49
|
+
*
|
|
50
|
+
* @type {number}
|
|
51
|
+
*/
|
|
52
|
+
duration?: number;
|
|
53
|
+
};
|
|
54
|
+
export type RedlockKeyFunction<T extends (...args: any) => any = (...args: any) => any> = (target: any, ...args: Parameters<T>) => string[] | string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { OnApplicationShutdown } from "@nestjs/common";
|
|
2
|
+
import { RedlockModuleOptions } from "./redlock.interface";
|
|
3
|
+
import { ConfigurableModuleClass } from "./redlock.module-definition";
|
|
4
|
+
export declare class RedlockModule extends ConfigurableModuleClass implements OnApplicationShutdown {
|
|
5
|
+
private readonly options;
|
|
6
|
+
constructor(options: RedlockModuleOptions);
|
|
7
|
+
onApplicationShutdown(): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { Inject, Module } from "@nestjs/common";
|
|
14
|
+
import { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } from "./redlock.module-definition";
|
|
15
|
+
import { RedlockService } from "./redlock.service";
|
|
16
|
+
let RedlockModule = class RedlockModule extends ConfigurableModuleClass {
|
|
17
|
+
constructor(options) {
|
|
18
|
+
super();
|
|
19
|
+
this.options = options;
|
|
20
|
+
}
|
|
21
|
+
async onApplicationShutdown() {
|
|
22
|
+
for (const client of this.options.clients) {
|
|
23
|
+
switch (client.status) {
|
|
24
|
+
case "end":
|
|
25
|
+
continue;
|
|
26
|
+
case "ready":
|
|
27
|
+
await client.quit();
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
client.disconnect();
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
RedlockModule = __decorate([
|
|
37
|
+
Module({
|
|
38
|
+
providers: [
|
|
39
|
+
{
|
|
40
|
+
provide: RedlockService,
|
|
41
|
+
inject: [MODULE_OPTIONS_TOKEN],
|
|
42
|
+
useFactory: (options) => new RedlockService(options),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
exports: [RedlockService],
|
|
46
|
+
}),
|
|
47
|
+
__param(0, Inject(MODULE_OPTIONS_TOKEN)),
|
|
48
|
+
__metadata("design:paramtypes", [Object])
|
|
49
|
+
], RedlockModule);
|
|
50
|
+
export { RedlockModule };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { Inject, Injectable } from "@nestjs/common";
|
|
14
|
+
import { Redlock } from "@sesamecare-oss/redlock";
|
|
15
|
+
import { MODULE_OPTIONS_TOKEN } from "./redlock.module-definition";
|
|
16
|
+
let RedlockService = class RedlockService extends Redlock {
|
|
17
|
+
constructor(options) {
|
|
18
|
+
super(options.clients, options.settings);
|
|
19
|
+
this.options = options;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
RedlockService = __decorate([
|
|
23
|
+
Injectable(),
|
|
24
|
+
__param(0, Inject(MODULE_OPTIONS_TOKEN)),
|
|
25
|
+
__metadata("design:paramtypes", [Object])
|
|
26
|
+
], RedlockService);
|
|
27
|
+
export { RedlockService };
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anchan828/nest-sesamecare-redlock",
|
|
3
|
+
"version": "0.2.44",
|
|
4
|
+
"description": "This is a [Nest](https://github.com/nestjs/nest) implementation of the redlock algorithm for distributed redis locks.",
|
|
5
|
+
"homepage": "https://github.com/anchan828/nest-redlock#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/anchan828/nest-redlock/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/anchan828/nest-redlock.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "anchan828 <anchan828@gmail.com>",
|
|
15
|
+
"main": "dist/cjs/index.js",
|
|
16
|
+
"types": "dist/cjs/index.d.ts",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json",
|
|
19
|
+
"copy:license": "cp ../../LICENSE ./",
|
|
20
|
+
"lint": "TIMING=1 eslint '**/*.ts'",
|
|
21
|
+
"lint:fix": "npm run lint -- --fix",
|
|
22
|
+
"test": "jest --coverage --runInBand --detectOpenHandles",
|
|
23
|
+
"test:debug": "node --inspect-brk ../../node_modules/jest/bin/jest --runInBand --logHeapUsage",
|
|
24
|
+
"test:watch": "npm run test -- --watch",
|
|
25
|
+
"watch": "tsc --watch"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@sesamecare-oss/redlock": "^1.3.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@nestjs/common": "10.4.4",
|
|
32
|
+
"@nestjs/core": "10.4.4",
|
|
33
|
+
"@nestjs/platform-express": "10.4.4",
|
|
34
|
+
"@nestjs/testing": "10.4.4",
|
|
35
|
+
"ioredis": "5.4.1",
|
|
36
|
+
"reflect-metadata": "0.2.2",
|
|
37
|
+
"rxjs": "7.8.1"
|
|
38
|
+
},
|
|
39
|
+
"volta": {
|
|
40
|
+
"node": "20.17.0"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"packageManager": "npm@10.8.3",
|
|
46
|
+
"exports": {
|
|
47
|
+
".": {
|
|
48
|
+
"import": {
|
|
49
|
+
"types": "./dist/esm/index.d.ts",
|
|
50
|
+
"default": "./dist/esm/index.js"
|
|
51
|
+
},
|
|
52
|
+
"require": {
|
|
53
|
+
"types": "./dist/cjs/index.d.ts",
|
|
54
|
+
"default": "./dist/cjs/index.js"
|
|
55
|
+
},
|
|
56
|
+
"types": "./dist/cjs/index.d.ts",
|
|
57
|
+
"default": "./dist/cjs/index.js"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|