@nocobase/lock-manager 2.1.0-beta.2 → 2.1.0-beta.21
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 +201 -661
- package/README.md +99 -0
- package/lib/lock-manager.d.ts +12 -1
- package/lib/lock-manager.js +60 -8
- package/package.json +4 -4
- package/src/__tests__/lock-manager.test.ts +66 -0
- package/src/lock-manager.ts +90 -11
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# NocoBase
|
|
2
|
+
|
|
3
|
+
<video width="100%" controls>
|
|
4
|
+
<source src="https://github.com/user-attachments/assets/4d11a87b-00e2-48f3-9bf7-389d21072d13" type="video/mp4">
|
|
5
|
+
</video>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://trendshift.io/repositories/4112" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4112" alt="nocobase%2Fnocobase | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
|
9
|
+
<a href="https://www.producthunt.com/posts/nocobase?embed=true&utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-nocobase" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=456520&theme=light&period=weekly&topic_id=267" alt="NocoBase - Scalability-first, open-source no-code platform | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
## What is NocoBase
|
|
13
|
+
|
|
14
|
+
NocoBase is the most extensible AI-powered no-code platform.
|
|
15
|
+
Total control. Infinite extensibility. AI collaboration.
|
|
16
|
+
Enable your team to adapt quickly and cut costs dramatically.
|
|
17
|
+
No years of development. No millions wasted.
|
|
18
|
+
Deploy NocoBase in minutes — and take control of everything.
|
|
19
|
+
|
|
20
|
+
Homepage:
|
|
21
|
+
https://www.nocobase.com/
|
|
22
|
+
|
|
23
|
+
Online Demo:
|
|
24
|
+
https://demo.nocobase.com/new
|
|
25
|
+
|
|
26
|
+
Documents:
|
|
27
|
+
https://docs.nocobase.com/
|
|
28
|
+
|
|
29
|
+
Forum:
|
|
30
|
+
https://forum.nocobase.com/
|
|
31
|
+
|
|
32
|
+
Use Cases:
|
|
33
|
+
https://www.nocobase.com/en/blog/tags/customer-stories
|
|
34
|
+
|
|
35
|
+
## Release Notes
|
|
36
|
+
|
|
37
|
+
Our [blog](https://www.nocobase.com/en/blog/timeline) is regularly updated with release notes and provides a weekly summary.
|
|
38
|
+
|
|
39
|
+
## Distinctive features
|
|
40
|
+
|
|
41
|
+
### 1. Data model-driven, not form/table–driven
|
|
42
|
+
|
|
43
|
+
Instead of being constrained by forms or tables, NocoBase adopts a data model–driven approach, separating data structure from user interface to unlock unlimited possibilities.
|
|
44
|
+
|
|
45
|
+
- UI and data structure are fully decoupled
|
|
46
|
+
- Multiple blocks and actions can be created for the same table or record in any quantity or form
|
|
47
|
+
- Supports the main database, external databases, and third-party APIs as data sources
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
### 2. AI employees, integrated into your business systems
|
|
52
|
+
Unlike standalone AI demos, NocoBase allows you to embed AI capabilities seamlessly into your interfaces, workflows, and data context, making AI truly useful in real business scenarios.
|
|
53
|
+
|
|
54
|
+
- Define AI employees for roles such as translator, analyst, researcher, or assistant
|
|
55
|
+
- Seamless AI–human collaboration in interfaces and workflows
|
|
56
|
+
- Ensure AI usage is secure, transparent, and customizable for your business needs
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
### 3. What you see is what you get, incredibly easy to use
|
|
61
|
+
|
|
62
|
+
While enabling the development of complex business systems, NocoBase keeps the experience simple and intuitive.
|
|
63
|
+
|
|
64
|
+
- One-click switch between usage mode and configuration mode
|
|
65
|
+
- Pages serve as a canvas to arrange blocks and actions, similar to Notion
|
|
66
|
+
- Configuration mode is designed for ordinary users, not just programmers
|
|
67
|
+
|
|
68
|
+

|
|
69
|
+
|
|
70
|
+
### 4. Everything is a plugin, designed for extension
|
|
71
|
+
Adding more no-code features will never cover every business case. NocoBase is built for extension through its plugin-based microkernel architecture.
|
|
72
|
+
|
|
73
|
+
- All functionalities are plugins, similar to WordPress
|
|
74
|
+
- Plugins are ready to use upon installation
|
|
75
|
+
- Pages, blocks, actions, APIs, and data sources can all be extended through custom plugins
|
|
76
|
+
|
|
77
|
+

|
|
78
|
+
|
|
79
|
+
## Installation
|
|
80
|
+
|
|
81
|
+
NocoBase supports three installation methods:
|
|
82
|
+
|
|
83
|
+
- <a target="_blank" href="https://docs.nocobase.com/welcome/getting-started/installation/docker-compose">Installing With Docker (👍Recommended)</a>
|
|
84
|
+
|
|
85
|
+
Suitable for no-code scenarios, no code to write. When upgrading, just download the latest image and reboot.
|
|
86
|
+
|
|
87
|
+
- <a target="_blank" href="https://docs.nocobase.com/welcome/getting-started/installation/create-nocobase-app">Installing from create-nocobase-app CLI</a>
|
|
88
|
+
|
|
89
|
+
The business code of the project is completely independent and supports low-code development.
|
|
90
|
+
|
|
91
|
+
- <a target="_blank" href="https://docs.nocobase.com/welcome/getting-started/installation/git-clone">Installing from Git source code</a>
|
|
92
|
+
|
|
93
|
+
If you want to experience the latest unreleased version, or want to participate in the contribution, you need to make changes and debug on the source code, it is recommended to choose this installation method, which requires a high level of development skills, and if the code has been updated, you can git pull the latest code.
|
|
94
|
+
|
|
95
|
+
## How NocoBase works
|
|
96
|
+
|
|
97
|
+
<video width="100%" controls>
|
|
98
|
+
<source src="https://github.com/user-attachments/assets/8d183b44-9bb5-4792-b08f-bc08fe8dfaaf" type="video/mp4">
|
|
99
|
+
</video>
|
package/lib/lock-manager.d.ts
CHANGED
|
@@ -7,9 +7,20 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
export type Releaser = () => void | Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* A lock handle returned by {@link ILockAdapter.tryAcquire}.
|
|
12
|
+
*
|
|
13
|
+
* **Important**: the underlying mutex is already held when this object is
|
|
14
|
+
* returned. The caller MUST invoke one of `acquire()`, `runExclusive()`, or
|
|
15
|
+
* `release()` promptly; if none is called the lock will be held indefinitely.
|
|
16
|
+
*
|
|
17
|
+
* Use `release()` to abandon the lock without executing any work (e.g. when an
|
|
18
|
+
* early-return or error prevents the caller from proceeding).
|
|
19
|
+
*/
|
|
10
20
|
export interface ILock {
|
|
11
21
|
acquire(ttl: number): Releaser | Promise<Releaser>;
|
|
12
22
|
runExclusive<T>(fn: () => Promise<T>, ttl: number): Promise<T>;
|
|
23
|
+
release(): void | Promise<void>;
|
|
13
24
|
}
|
|
14
25
|
export interface ILockAdapter {
|
|
15
26
|
connect(): Promise<void>;
|
|
@@ -41,6 +52,6 @@ export declare class LockManager {
|
|
|
41
52
|
close(): Promise<void>;
|
|
42
53
|
acquire(key: string, ttl?: number): Promise<Releaser>;
|
|
43
54
|
runExclusive<T>(key: string, fn: () => Promise<T>, ttl?: number): Promise<T>;
|
|
44
|
-
tryAcquire(key: string): Promise<ILock>;
|
|
55
|
+
tryAcquire(key: string, timeout?: number): Promise<ILock>;
|
|
45
56
|
}
|
|
46
57
|
export default LockManager;
|
package/lib/lock-manager.js
CHANGED
|
@@ -97,17 +97,69 @@ const _LocalLockAdapter = class _LocalLockAdapter {
|
|
|
97
97
|
clearTimeout(timer);
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
-
async tryAcquire(key) {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
async tryAcquire(key, timeout = 0) {
|
|
101
|
+
const mutex = this.getLock(key);
|
|
102
|
+
let preAcquiredRelease;
|
|
103
|
+
if (timeout === 0) {
|
|
104
|
+
if (mutex.isLocked()) {
|
|
105
|
+
throw new LockAcquireError("lock is locked");
|
|
106
|
+
}
|
|
107
|
+
preAcquiredRelease = await mutex.acquire();
|
|
108
|
+
} else {
|
|
109
|
+
try {
|
|
110
|
+
preAcquiredRelease = await (0, import_async_mutex.withTimeout)(mutex, timeout).acquire();
|
|
111
|
+
} catch (e) {
|
|
112
|
+
throw new LockAcquireError("lock acquire timed out", { cause: e });
|
|
113
|
+
}
|
|
104
114
|
}
|
|
115
|
+
let preAcquiredConsumed = false;
|
|
116
|
+
const getRelease = /* @__PURE__ */ __name(async () => {
|
|
117
|
+
const rawRelease = !preAcquiredConsumed ? (preAcquiredConsumed = true, preAcquiredRelease) : await mutex.acquire();
|
|
118
|
+
let released = false;
|
|
119
|
+
return () => {
|
|
120
|
+
if (!released) {
|
|
121
|
+
released = true;
|
|
122
|
+
return rawRelease();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}, "getRelease");
|
|
105
126
|
return {
|
|
127
|
+
release: /* @__PURE__ */ __name(async () => {
|
|
128
|
+
const release = await getRelease();
|
|
129
|
+
await release();
|
|
130
|
+
}, "release"),
|
|
106
131
|
acquire: /* @__PURE__ */ __name(async (ttl) => {
|
|
107
|
-
|
|
132
|
+
const release = await getRelease();
|
|
133
|
+
const timer = setTimeout(() => {
|
|
134
|
+
if (mutex.isLocked()) {
|
|
135
|
+
release();
|
|
136
|
+
}
|
|
137
|
+
}, ttl);
|
|
138
|
+
return () => {
|
|
139
|
+
release();
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
};
|
|
108
142
|
}, "acquire"),
|
|
109
143
|
runExclusive: /* @__PURE__ */ __name(async (fn, ttl) => {
|
|
110
|
-
|
|
144
|
+
const release = await getRelease();
|
|
145
|
+
let timer;
|
|
146
|
+
try {
|
|
147
|
+
timer = setTimeout(() => {
|
|
148
|
+
if (mutex.isLocked()) {
|
|
149
|
+
release();
|
|
150
|
+
}
|
|
151
|
+
}, ttl);
|
|
152
|
+
return await fn();
|
|
153
|
+
} catch (e) {
|
|
154
|
+
if (e === import_async_mutex.E_CANCELED) {
|
|
155
|
+
throw new LockAbortError("Lock aborted", { cause: import_async_mutex.E_CANCELED });
|
|
156
|
+
} else {
|
|
157
|
+
throw e;
|
|
158
|
+
}
|
|
159
|
+
} finally {
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
release();
|
|
162
|
+
}
|
|
111
163
|
}, "runExclusive")
|
|
112
164
|
};
|
|
113
165
|
}
|
|
@@ -155,9 +207,9 @@ const _LockManager = class _LockManager {
|
|
|
155
207
|
const client = await this.getAdapter();
|
|
156
208
|
return client.runExclusive(key, fn, ttl);
|
|
157
209
|
}
|
|
158
|
-
async tryAcquire(key) {
|
|
210
|
+
async tryAcquire(key, timeout = 0) {
|
|
159
211
|
const client = await this.getAdapter();
|
|
160
|
-
return client.tryAcquire(key);
|
|
212
|
+
return client.tryAcquire(key, timeout);
|
|
161
213
|
}
|
|
162
214
|
};
|
|
163
215
|
__name(_LockManager, "LockManager");
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/lock-manager",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.21",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
6
|
"devDependencies": {
|
|
7
|
-
"@nocobase/utils": "2.1.0-beta.
|
|
7
|
+
"@nocobase/utils": "2.1.0-beta.21",
|
|
8
8
|
"async-mutex": "^0.5.0"
|
|
9
9
|
},
|
|
10
|
-
"gitHead": "
|
|
10
|
+
"gitHead": "324bd82f33fca58e98711688a17ceb65c186b65e"
|
|
11
11
|
}
|
|
@@ -165,5 +165,71 @@ describe('lock manager', () => {
|
|
|
165
165
|
await r2();
|
|
166
166
|
expect(order).toEqual([1, 2, 3, 4]);
|
|
167
167
|
});
|
|
168
|
+
|
|
169
|
+
it('tryAcquire: only one concurrent caller wins (TOCTOU race)', async () => {
|
|
170
|
+
// Simulate two nodes calling tryAcquire at the same time (single-process,
|
|
171
|
+
// like createMockCluster). Only one should succeed; the other must throw.
|
|
172
|
+
const results: Array<'acquired' | 'skipped'> = [];
|
|
173
|
+
await Promise.all(
|
|
174
|
+
[0, 1].map(async () => {
|
|
175
|
+
try {
|
|
176
|
+
const lock = await lockManager.tryAcquire('race-test');
|
|
177
|
+
const release = await lock.acquire(1000);
|
|
178
|
+
results.push('acquired');
|
|
179
|
+
await release();
|
|
180
|
+
} catch (e) {
|
|
181
|
+
if (e instanceof LockAcquireError) {
|
|
182
|
+
results.push('skipped');
|
|
183
|
+
} else {
|
|
184
|
+
throw e;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
expect(results.filter((r) => r === 'acquired').length).toBe(1);
|
|
190
|
+
expect(results.filter((r) => r === 'skipped').length).toBe(1);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('tryAcquire: waits up to timeout ms before throwing when lock is held', async () => {
|
|
194
|
+
// Acquire the lock manually so it is held during the tryAcquire call.
|
|
195
|
+
const holdRelease = await lockManager.acquire('wait-test');
|
|
196
|
+
|
|
197
|
+
// Release the lock after 50 ms — tryAcquire with timeout=1000 should
|
|
198
|
+
// succeed because the lock becomes available well within the window.
|
|
199
|
+
// Using a large margin (50ms release vs 1000ms timeout) to avoid flakiness
|
|
200
|
+
// on slow CI machines under load.
|
|
201
|
+
setTimeout(() => holdRelease(), 50);
|
|
202
|
+
const lock = await lockManager.tryAcquire('wait-test', 1000);
|
|
203
|
+
const release = await lock.acquire(1000);
|
|
204
|
+
await release();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('tryAcquire: throws LockAcquireError when timeout expires before lock is released', async () => {
|
|
208
|
+
const holdRelease = await lockManager.acquire('timeout-test');
|
|
209
|
+
// Lock is held; tryAcquire with a very short timeout should fail.
|
|
210
|
+
await expect(lockManager.tryAcquire('timeout-test', 50)).rejects.toThrowError(LockAcquireError);
|
|
211
|
+
await holdRelease();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('tryAcquire: subsequent call succeeds after previous lock is released', async () => {
|
|
215
|
+
const lock1 = await lockManager.tryAcquire('seq-test');
|
|
216
|
+
const release = await lock1.acquire(1000);
|
|
217
|
+
await release();
|
|
218
|
+
// After release, a second tryAcquire on the same key should succeed
|
|
219
|
+
const lock2 = await lockManager.tryAcquire('seq-test');
|
|
220
|
+
const release2 = await lock2.acquire(1000);
|
|
221
|
+
await release2();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('tryAcquire: release() abandons the pre-acquired lock without executing work', async () => {
|
|
225
|
+
// Acquire the lock via tryAcquire, then immediately release it.
|
|
226
|
+
const lock = await lockManager.tryAcquire('cancel-test');
|
|
227
|
+
await lock.release();
|
|
228
|
+
|
|
229
|
+
// The lock should now be free; a subsequent tryAcquire must succeed.
|
|
230
|
+
const lock2 = await lockManager.tryAcquire('cancel-test');
|
|
231
|
+
const r = await lock2.acquire(1000);
|
|
232
|
+
await r();
|
|
233
|
+
});
|
|
168
234
|
});
|
|
169
235
|
});
|
package/src/lock-manager.ts
CHANGED
|
@@ -8,13 +8,24 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Registry } from '@nocobase/utils';
|
|
11
|
-
import { Mutex, MutexInterface, E_CANCELED } from 'async-mutex';
|
|
11
|
+
import { Mutex, MutexInterface, E_CANCELED, withTimeout } from 'async-mutex';
|
|
12
12
|
|
|
13
13
|
export type Releaser = () => void | Promise<void>;
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* A lock handle returned by {@link ILockAdapter.tryAcquire}.
|
|
17
|
+
*
|
|
18
|
+
* **Important**: the underlying mutex is already held when this object is
|
|
19
|
+
* returned. The caller MUST invoke one of `acquire()`, `runExclusive()`, or
|
|
20
|
+
* `release()` promptly; if none is called the lock will be held indefinitely.
|
|
21
|
+
*
|
|
22
|
+
* Use `release()` to abandon the lock without executing any work (e.g. when an
|
|
23
|
+
* early-return or error prevents the caller from proceeding).
|
|
24
|
+
*/
|
|
15
25
|
export interface ILock {
|
|
16
26
|
acquire(ttl: number): Releaser | Promise<Releaser>;
|
|
17
27
|
runExclusive<T>(fn: () => Promise<T>, ttl: number): Promise<T>;
|
|
28
|
+
release(): void | Promise<void>;
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
export interface ILockAdapter {
|
|
@@ -87,17 +98,85 @@ class LocalLockAdapter implements ILockAdapter {
|
|
|
87
98
|
}
|
|
88
99
|
}
|
|
89
100
|
|
|
90
|
-
async tryAcquire(key: string) {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
async tryAcquire(key: string, timeout = 0) {
|
|
102
|
+
const mutex = this.getLock(key);
|
|
103
|
+
let preAcquiredRelease: Releaser;
|
|
104
|
+
|
|
105
|
+
if (timeout === 0) {
|
|
106
|
+
// Non-blocking: throw immediately if the lock is already held.
|
|
107
|
+
// mutex.acquire() is called synchronously (before any await boundary) so
|
|
108
|
+
// that _locked=true is set atomically within the current JS execution
|
|
109
|
+
// slice, preventing TOCTOU races in single-process cluster simulations
|
|
110
|
+
// (e.g. tests using createMockCluster).
|
|
111
|
+
if (mutex.isLocked()) {
|
|
112
|
+
throw new LockAcquireError('lock is locked');
|
|
113
|
+
}
|
|
114
|
+
preAcquiredRelease = (await mutex.acquire()) as Releaser;
|
|
115
|
+
} else {
|
|
116
|
+
// Blocking with timeout: wait up to `timeout` ms for the lock, then
|
|
117
|
+
// throw. withTimeout() from async-mutex handles queue cleanup properly
|
|
118
|
+
// when the timeout fires before the lock is acquired.
|
|
119
|
+
try {
|
|
120
|
+
preAcquiredRelease = (await withTimeout(mutex, timeout).acquire()) as Releaser;
|
|
121
|
+
} catch (e) {
|
|
122
|
+
throw new LockAcquireError('lock acquire timed out', { cause: e });
|
|
123
|
+
}
|
|
94
124
|
}
|
|
125
|
+
|
|
126
|
+
let preAcquiredConsumed = false;
|
|
127
|
+
|
|
128
|
+
const getRelease = async (): Promise<Releaser> => {
|
|
129
|
+
const rawRelease: Releaser = !preAcquiredConsumed
|
|
130
|
+
? ((preAcquiredConsumed = true), preAcquiredRelease)
|
|
131
|
+
: ((await mutex.acquire()) as Releaser);
|
|
132
|
+
// Idempotency guard: prevents double-release when both the TTL auto-
|
|
133
|
+
// release timer and the caller-facing releaser (or finally block) fire.
|
|
134
|
+
let released = false;
|
|
135
|
+
return () => {
|
|
136
|
+
if (!released) {
|
|
137
|
+
released = true;
|
|
138
|
+
return (rawRelease as () => void | Promise<void>)();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
95
143
|
return {
|
|
96
|
-
|
|
97
|
-
|
|
144
|
+
release: async (): Promise<void> => {
|
|
145
|
+
const release = await getRelease();
|
|
146
|
+
await release();
|
|
147
|
+
},
|
|
148
|
+
acquire: async (ttl: number): Promise<Releaser> => {
|
|
149
|
+
const release = await getRelease();
|
|
150
|
+
const timer: ReturnType<typeof setTimeout> = setTimeout(() => {
|
|
151
|
+
if (mutex.isLocked()) {
|
|
152
|
+
release();
|
|
153
|
+
}
|
|
154
|
+
}, ttl);
|
|
155
|
+
return () => {
|
|
156
|
+
release();
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
};
|
|
98
159
|
},
|
|
99
|
-
runExclusive: async (fn: () => Promise<
|
|
100
|
-
|
|
160
|
+
runExclusive: async <T>(fn: () => Promise<T>, ttl: number): Promise<T> => {
|
|
161
|
+
const release = await getRelease();
|
|
162
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
163
|
+
try {
|
|
164
|
+
timer = setTimeout(() => {
|
|
165
|
+
if (mutex.isLocked()) {
|
|
166
|
+
release();
|
|
167
|
+
}
|
|
168
|
+
}, ttl);
|
|
169
|
+
return await fn();
|
|
170
|
+
} catch (e) {
|
|
171
|
+
if (e === E_CANCELED) {
|
|
172
|
+
throw new LockAbortError('Lock aborted', { cause: E_CANCELED });
|
|
173
|
+
} else {
|
|
174
|
+
throw e;
|
|
175
|
+
}
|
|
176
|
+
} finally {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
release();
|
|
179
|
+
}
|
|
101
180
|
},
|
|
102
181
|
};
|
|
103
182
|
}
|
|
@@ -160,9 +239,9 @@ export class LockManager {
|
|
|
160
239
|
return client.runExclusive(key, fn, ttl);
|
|
161
240
|
}
|
|
162
241
|
|
|
163
|
-
public async tryAcquire(key: string) {
|
|
242
|
+
public async tryAcquire(key: string, timeout = 0) {
|
|
164
243
|
const client = await this.getAdapter();
|
|
165
|
-
return client.tryAcquire(key);
|
|
244
|
+
return client.tryAcquire(key, timeout);
|
|
166
245
|
}
|
|
167
246
|
}
|
|
168
247
|
|