@nocobase/lock-manager 2.1.39 → 2.1.41

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.
@@ -19,6 +19,8 @@ export type Releaser = () => void | Promise<void>;
19
19
  */
20
20
  export interface ILock {
21
21
  acquire(ttl: number): Releaser | Promise<Releaser>;
22
+ /** Extend the currently held lease from now, when supported by the adapter. */
23
+ extend?(ttl: number): void | Promise<void>;
22
24
  runExclusive<T>(fn: () => Promise<T>, ttl: number): Promise<T>;
23
25
  release(): void | Promise<void>;
24
26
  }
@@ -112,43 +112,63 @@ const _LocalLockAdapter = class _LocalLockAdapter {
112
112
  throw new LockAcquireError("lock acquire timed out", { cause: e });
113
113
  }
114
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");
115
+ let reservation = preAcquiredRelease;
116
+ let activeLease;
117
+ const releaseLease = /* @__PURE__ */ __name(async (lease) => {
118
+ if (activeLease !== lease) {
119
+ return;
120
+ }
121
+ activeLease = void 0;
122
+ if (lease.timer) {
123
+ clearTimeout(lease.timer);
124
+ }
125
+ await lease.release();
126
+ }, "releaseLease");
127
+ const resetTimer = /* @__PURE__ */ __name((ttl) => {
128
+ if (!activeLease) {
129
+ throw new LockAcquireError("lock is not active");
130
+ }
131
+ const lease = activeLease;
132
+ if (lease.timer) {
133
+ clearTimeout(lease.timer);
134
+ }
135
+ lease.timer = setTimeout(() => {
136
+ releaseLease(lease).catch(() => void 0);
137
+ }, ttl);
138
+ }, "resetTimer");
126
139
  return {
127
140
  release: /* @__PURE__ */ __name(async () => {
128
- const release = await getRelease();
129
- await release();
141
+ if (reservation) {
142
+ const releaseReservation = reservation;
143
+ reservation = void 0;
144
+ await releaseReservation();
145
+ return;
146
+ }
147
+ if (activeLease) {
148
+ await releaseLease(activeLease);
149
+ }
130
150
  }, "release"),
131
151
  acquire: /* @__PURE__ */ __name(async (ttl) => {
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
- };
152
+ const rawRelease = reservation || await mutex.acquire();
153
+ reservation = void 0;
154
+ const lease = { release: rawRelease };
155
+ activeLease = lease;
156
+ resetTimer(ttl);
157
+ return () => releaseLease(lease);
142
158
  }, "acquire"),
159
+ extend: /* @__PURE__ */ __name(async (ttl) => {
160
+ resetTimer(ttl);
161
+ }, "extend"),
143
162
  runExclusive: /* @__PURE__ */ __name(async (fn, ttl) => {
144
- const release = await getRelease();
145
- let timer;
163
+ const release = await (async () => {
164
+ const rawRelease = reservation || await mutex.acquire();
165
+ reservation = void 0;
166
+ const lease = { release: rawRelease };
167
+ activeLease = lease;
168
+ resetTimer(ttl);
169
+ return () => releaseLease(lease);
170
+ })();
146
171
  try {
147
- timer = setTimeout(() => {
148
- if (mutex.isLocked()) {
149
- release();
150
- }
151
- }, ttl);
152
172
  return await fn();
153
173
  } catch (e) {
154
174
  if (e === import_async_mutex.E_CANCELED) {
@@ -157,8 +177,7 @@ const _LocalLockAdapter = class _LocalLockAdapter {
157
177
  throw e;
158
178
  }
159
179
  } finally {
160
- clearTimeout(timer);
161
- release();
180
+ await release();
162
181
  }
163
182
  }, "runExclusive")
164
183
  };
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@nocobase/lock-manager",
3
- "version": "2.1.39",
3
+ "version": "2.1.41",
4
4
  "main": "lib/index.js",
5
5
  "license": "Apache-2.0",
6
6
  "devDependencies": {
7
- "@nocobase/utils": "2.1.39",
7
+ "@nocobase/utils": "2.1.41",
8
8
  "async-mutex": "^0.5.0"
9
9
  },
10
- "gitHead": "ddbf88ac2a5de1ad94ff4779618cdf1b81aae037"
10
+ "gitHead": "91c01d0941d758d97d8000d0c8b8a2a48af6c1ae"
11
11
  }
@@ -149,6 +149,7 @@ describe('lock manager', () => {
149
149
  await release();
150
150
  const lock = await lockManager.tryAcquire('test');
151
151
  expect(lock.acquire).toBeTypeOf('function');
152
+ expect(lock.extend).toBeTypeOf('function');
152
153
  expect(lock.runExclusive).toBeTypeOf('function');
153
154
 
154
155
  const order = [];
@@ -221,6 +222,19 @@ describe('lock manager', () => {
221
222
  await release2();
222
223
  });
223
224
 
225
+ it('tryAcquire: extends an active lease', async () => {
226
+ const lock = await lockManager.tryAcquire('extend-test');
227
+ const release = await lock.acquire(500);
228
+ await sleep(100);
229
+ await lock.extend(500);
230
+ await sleep(200);
231
+ await expect(lockManager.tryAcquire('extend-test')).rejects.toThrowError(LockAcquireError);
232
+ await release();
233
+
234
+ const nextLock = await lockManager.tryAcquire('extend-test');
235
+ await nextLock.release();
236
+ });
237
+
224
238
  it('tryAcquire: release() abandons the pre-acquired lock without executing work', async () => {
225
239
  // Acquire the lock via tryAcquire, then immediately release it.
226
240
  const lock = await lockManager.tryAcquire('cancel-test');
@@ -24,6 +24,8 @@ export type Releaser = () => void | Promise<void>;
24
24
  */
25
25
  export interface ILock {
26
26
  acquire(ttl: number): Releaser | Promise<Releaser>;
27
+ /** Extend the currently held lease from now, when supported by the adapter. */
28
+ extend?(ttl: number): void | Promise<void>;
27
29
  runExclusive<T>(fn: () => Promise<T>, ttl: number): Promise<T>;
28
30
  release(): void | Promise<void>;
29
31
  }
@@ -123,49 +125,71 @@ class LocalLockAdapter implements ILockAdapter {
123
125
  }
124
126
  }
125
127
 
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>)();
128
+ let reservation: Releaser | undefined = preAcquiredRelease;
129
+ let activeLease:
130
+ | {
131
+ release: Releaser;
132
+ timer?: ReturnType<typeof setTimeout>;
139
133
  }
140
- };
134
+ | undefined;
135
+
136
+ const releaseLease = async (lease: NonNullable<typeof activeLease>): Promise<void> => {
137
+ if (activeLease !== lease) {
138
+ return;
139
+ }
140
+ activeLease = undefined;
141
+ if (lease.timer) {
142
+ clearTimeout(lease.timer);
143
+ }
144
+ await lease.release();
145
+ };
146
+
147
+ const resetTimer = (ttl: number) => {
148
+ if (!activeLease) {
149
+ throw new LockAcquireError('lock is not active');
150
+ }
151
+ const lease = activeLease;
152
+ if (lease.timer) {
153
+ clearTimeout(lease.timer);
154
+ }
155
+ lease.timer = setTimeout(() => {
156
+ releaseLease(lease).catch(() => undefined);
157
+ }, ttl);
141
158
  };
142
159
 
143
160
  return {
144
161
  release: async (): Promise<void> => {
145
- const release = await getRelease();
146
- await release();
162
+ if (reservation) {
163
+ const releaseReservation = reservation;
164
+ reservation = undefined;
165
+ await releaseReservation();
166
+ return;
167
+ }
168
+ if (activeLease) {
169
+ await releaseLease(activeLease);
170
+ }
147
171
  },
148
172
  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
- };
173
+ const rawRelease = reservation || ((await mutex.acquire()) as Releaser);
174
+ reservation = undefined;
175
+ const lease = { release: rawRelease };
176
+ activeLease = lease;
177
+ resetTimer(ttl);
178
+ return () => releaseLease(lease);
179
+ },
180
+ extend: async (ttl: number): Promise<void> => {
181
+ resetTimer(ttl);
159
182
  },
160
183
  runExclusive: async <T>(fn: () => Promise<T>, ttl: number): Promise<T> => {
161
- const release = await getRelease();
162
- let timer: ReturnType<typeof setTimeout>;
184
+ const release = await (async () => {
185
+ const rawRelease = reservation || ((await mutex.acquire()) as Releaser);
186
+ reservation = undefined;
187
+ const lease = { release: rawRelease };
188
+ activeLease = lease;
189
+ resetTimer(ttl);
190
+ return () => releaseLease(lease);
191
+ })();
163
192
  try {
164
- timer = setTimeout(() => {
165
- if (mutex.isLocked()) {
166
- release();
167
- }
168
- }, ttl);
169
193
  return await fn();
170
194
  } catch (e) {
171
195
  if (e === E_CANCELED) {
@@ -174,8 +198,7 @@ class LocalLockAdapter implements ILockAdapter {
174
198
  throw e;
175
199
  }
176
200
  } finally {
177
- clearTimeout(timer);
178
- release();
201
+ await release();
179
202
  }
180
203
  },
181
204
  };