@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.
Files changed (34) hide show
  1. package/README.md +159 -0
  2. package/dist/cjs/index.d.ts +5 -0
  3. package/dist/cjs/index.js +11 -0
  4. package/dist/cjs/redlock.constants.d.ts +1 -0
  5. package/dist/cjs/redlock.constants.js +4 -0
  6. package/dist/cjs/redlock.decorator.d.ts +3 -0
  7. package/dist/cjs/redlock.decorator.js +60 -0
  8. package/dist/cjs/redlock.fake-service.d.ts +10 -0
  9. package/dist/cjs/redlock.fake-service.js +31 -0
  10. package/dist/cjs/redlock.interface.d.ts +54 -0
  11. package/dist/cjs/redlock.interface.js +2 -0
  12. package/dist/cjs/redlock.module-definition.d.ts +2 -0
  13. package/dist/cjs/redlock.module-definition.js +6 -0
  14. package/dist/cjs/redlock.module.d.ts +8 -0
  15. package/dist/cjs/redlock.module.js +53 -0
  16. package/dist/cjs/redlock.service.d.ts +6 -0
  17. package/dist/cjs/redlock.service.js +30 -0
  18. package/dist/esm/index.d.ts +5 -0
  19. package/dist/esm/index.js +4 -0
  20. package/dist/esm/redlock.constants.d.ts +1 -0
  21. package/dist/esm/redlock.constants.js +1 -0
  22. package/dist/esm/redlock.decorator.d.ts +3 -0
  23. package/dist/esm/redlock.decorator.js +57 -0
  24. package/dist/esm/redlock.fake-service.d.ts +10 -0
  25. package/dist/esm/redlock.fake-service.js +27 -0
  26. package/dist/esm/redlock.interface.d.ts +54 -0
  27. package/dist/esm/redlock.interface.js +1 -0
  28. package/dist/esm/redlock.module-definition.d.ts +2 -0
  29. package/dist/esm/redlock.module-definition.js +2 -0
  30. package/dist/esm/redlock.module.d.ts +8 -0
  31. package/dist/esm/redlock.module.js +50 -0
  32. package/dist/esm/redlock.service.d.ts +6 -0
  33. package/dist/esm/redlock.service.js +27 -0
  34. package/package.json +60 -0
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # @anchan828/nest-sesamecare-redlock
2
+
3
+ ![npm](https://img.shields.io/npm/v/@anchan828/nest-sesamecare-redlock.svg)
4
+ ![NPM](https://img.shields.io/npm/l/@anchan828/nest-sesamecare-redlock.svg)
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,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_DURATION = void 0;
4
+ exports.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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ import { RedlockModuleOptions } from "./redlock.interface";
2
+ export declare const ConfigurableModuleClass: import("@nestjs/common").ConfigurableModuleCls<RedlockModuleOptions, "register", "create", {}>, MODULE_OPTIONS_TOKEN: string | symbol;
@@ -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,6 @@
1
+ import { Redlock } from "@sesamecare-oss/redlock";
2
+ import { RedlockModuleOptions } from "./redlock.interface";
3
+ export declare class RedlockService extends Redlock {
4
+ readonly options: RedlockModuleOptions;
5
+ constructor(options: RedlockModuleOptions);
6
+ }
@@ -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,4 @@
1
+ export { Redlock } from "./redlock.decorator";
2
+ export { FakeRedlockService } from "./redlock.fake-service";
3
+ export { RedlockModule } from "./redlock.module";
4
+ 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,2 @@
1
+ import { RedlockModuleOptions } from "./redlock.interface";
2
+ export declare const ConfigurableModuleClass: import("@nestjs/common").ConfigurableModuleCls<RedlockModuleOptions, "register", "create", {}>, MODULE_OPTIONS_TOKEN: string | symbol;
@@ -0,0 +1,2 @@
1
+ import { ConfigurableModuleBuilder } from "@nestjs/common";
2
+ export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder().build();
@@ -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,6 @@
1
+ import { Redlock } from "@sesamecare-oss/redlock";
2
+ import { RedlockModuleOptions } from "./redlock.interface";
3
+ export declare class RedlockService extends Redlock {
4
+ readonly options: RedlockModuleOptions;
5
+ constructor(options: RedlockModuleOptions);
6
+ }
@@ -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
+ }