dflockd-client 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -13
- package/dist/client.cjs +289 -6
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +85 -3
- package/dist/client.d.ts +85 -3
- package/dist/client.js +282 -6
- package/dist/client.js.map +1 -1
- package/package.json +2 -1
package/dist/client.d.cts
CHANGED
|
@@ -6,6 +6,8 @@ declare class AcquireTimeoutError extends Error {
|
|
|
6
6
|
declare class LockError extends Error {
|
|
7
7
|
constructor(message: string);
|
|
8
8
|
}
|
|
9
|
+
type ShardingStrategy = (key: string, numServers: number) => number;
|
|
10
|
+
declare function stableHashShard(key: string, numServers: number): number;
|
|
9
11
|
declare function acquire(sock: net.Socket, key: string, acquireTimeoutS: number, leaseTtlS?: number): Promise<{
|
|
10
12
|
token: string;
|
|
11
13
|
lease: number;
|
|
@@ -21,20 +23,39 @@ declare function waitForLock(sock: net.Socket, key: string, waitTimeoutS: number
|
|
|
21
23
|
lease: number;
|
|
22
24
|
}>;
|
|
23
25
|
declare function release(sock: net.Socket, key: string, token: string): Promise<void>;
|
|
26
|
+
declare function semAcquire(sock: net.Socket, key: string, acquireTimeoutS: number, limit: number, leaseTtlS?: number): Promise<{
|
|
27
|
+
token: string;
|
|
28
|
+
lease: number;
|
|
29
|
+
}>;
|
|
30
|
+
declare function semRenew(sock: net.Socket, key: string, token: string, leaseTtlS?: number): Promise<number>;
|
|
31
|
+
declare function semEnqueue(sock: net.Socket, key: string, limit: number, leaseTtlS?: number): Promise<{
|
|
32
|
+
status: "acquired" | "queued";
|
|
33
|
+
token: string | null;
|
|
34
|
+
lease: number | null;
|
|
35
|
+
}>;
|
|
36
|
+
declare function semWaitForLock(sock: net.Socket, key: string, waitTimeoutS: number): Promise<{
|
|
37
|
+
token: string;
|
|
38
|
+
lease: number;
|
|
39
|
+
}>;
|
|
40
|
+
declare function semRelease(sock: net.Socket, key: string, token: string): Promise<void>;
|
|
24
41
|
interface DistributedLockOptions {
|
|
25
42
|
key: string;
|
|
26
43
|
acquireTimeoutS?: number;
|
|
27
44
|
leaseTtlS?: number;
|
|
45
|
+
/** @deprecated Use `servers` instead. */
|
|
28
46
|
host?: string;
|
|
47
|
+
/** @deprecated Use `servers` instead. */
|
|
29
48
|
port?: number;
|
|
49
|
+
servers?: Array<[host: string, port: number]>;
|
|
50
|
+
shardingStrategy?: ShardingStrategy;
|
|
30
51
|
renewRatio?: number;
|
|
31
52
|
}
|
|
32
53
|
declare class DistributedLock {
|
|
33
54
|
readonly key: string;
|
|
34
55
|
readonly acquireTimeoutS: number;
|
|
35
56
|
readonly leaseTtlS: number | undefined;
|
|
36
|
-
readonly
|
|
37
|
-
readonly
|
|
57
|
+
readonly servers: Array<[string, number]>;
|
|
58
|
+
readonly shardingStrategy: ShardingStrategy;
|
|
38
59
|
readonly renewRatio: number;
|
|
39
60
|
token: string | null;
|
|
40
61
|
lease: number;
|
|
@@ -42,6 +63,7 @@ declare class DistributedLock {
|
|
|
42
63
|
private renewTimer;
|
|
43
64
|
private closed;
|
|
44
65
|
constructor(opts: DistributedLockOptions);
|
|
66
|
+
private pickServer;
|
|
45
67
|
/** Acquire the lock. Returns `true` on success, `false` on timeout. */
|
|
46
68
|
acquire(): Promise<boolean>;
|
|
47
69
|
/** Release the lock and close the connection. */
|
|
@@ -74,5 +96,65 @@ declare class DistributedLock {
|
|
|
74
96
|
private startRenew;
|
|
75
97
|
private stopRenew;
|
|
76
98
|
}
|
|
99
|
+
interface DistributedSemaphoreOptions {
|
|
100
|
+
key: string;
|
|
101
|
+
limit: number;
|
|
102
|
+
acquireTimeoutS?: number;
|
|
103
|
+
leaseTtlS?: number;
|
|
104
|
+
/** @deprecated Use `servers` instead. */
|
|
105
|
+
host?: string;
|
|
106
|
+
/** @deprecated Use `servers` instead. */
|
|
107
|
+
port?: number;
|
|
108
|
+
servers?: Array<[host: string, port: number]>;
|
|
109
|
+
shardingStrategy?: ShardingStrategy;
|
|
110
|
+
renewRatio?: number;
|
|
111
|
+
}
|
|
112
|
+
declare class DistributedSemaphore {
|
|
113
|
+
readonly key: string;
|
|
114
|
+
readonly limit: number;
|
|
115
|
+
readonly acquireTimeoutS: number;
|
|
116
|
+
readonly leaseTtlS: number | undefined;
|
|
117
|
+
readonly servers: Array<[string, number]>;
|
|
118
|
+
readonly shardingStrategy: ShardingStrategy;
|
|
119
|
+
readonly renewRatio: number;
|
|
120
|
+
token: string | null;
|
|
121
|
+
lease: number;
|
|
122
|
+
private sock;
|
|
123
|
+
private renewTimer;
|
|
124
|
+
private closed;
|
|
125
|
+
constructor(opts: DistributedSemaphoreOptions);
|
|
126
|
+
private pickServer;
|
|
127
|
+
/** Acquire a semaphore slot. Returns `true` on success, `false` on timeout. */
|
|
128
|
+
acquire(): Promise<boolean>;
|
|
129
|
+
/** Release the semaphore slot and close the connection. */
|
|
130
|
+
release(): Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* Two-phase step 1: connect and join the FIFO queue.
|
|
133
|
+
* Returns `"acquired"` (fast-path, slot granted immediately) or `"queued"`.
|
|
134
|
+
* If acquired immediately, the renew loop starts automatically.
|
|
135
|
+
*/
|
|
136
|
+
enqueue(): Promise<"acquired" | "queued">;
|
|
137
|
+
/**
|
|
138
|
+
* Two-phase step 2: block until a semaphore slot is granted.
|
|
139
|
+
* Returns `true` if granted, `false` on timeout.
|
|
140
|
+
* If already acquired during `enqueue()`, returns `true` immediately.
|
|
141
|
+
*/
|
|
142
|
+
wait(timeoutS?: number): Promise<boolean>;
|
|
143
|
+
/**
|
|
144
|
+
* Run `fn` while holding a semaphore slot, then release automatically.
|
|
145
|
+
*
|
|
146
|
+
* ```ts
|
|
147
|
+
* const sem = new DistributedSemaphore({ key: "my-resource", limit: 5 });
|
|
148
|
+
* await sem.withLock(async () => {
|
|
149
|
+
* // critical section (up to 5 concurrent holders)
|
|
150
|
+
* });
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
withLock<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
154
|
+
/** Close the underlying socket (idempotent). */
|
|
155
|
+
close(): Promise<void>;
|
|
156
|
+
private startRenew;
|
|
157
|
+
private stopRenew;
|
|
158
|
+
}
|
|
77
159
|
|
|
78
|
-
export { AcquireTimeoutError, DistributedLock, type DistributedLockOptions, LockError, acquire, enqueue, release, renew, waitForLock };
|
|
160
|
+
export { AcquireTimeoutError, DistributedLock, type DistributedLockOptions, DistributedSemaphore, type DistributedSemaphoreOptions, LockError, type ShardingStrategy, acquire, enqueue, release, renew, semAcquire, semEnqueue, semRelease, semRenew, semWaitForLock, stableHashShard, waitForLock };
|
package/dist/client.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ declare class AcquireTimeoutError extends Error {
|
|
|
6
6
|
declare class LockError extends Error {
|
|
7
7
|
constructor(message: string);
|
|
8
8
|
}
|
|
9
|
+
type ShardingStrategy = (key: string, numServers: number) => number;
|
|
10
|
+
declare function stableHashShard(key: string, numServers: number): number;
|
|
9
11
|
declare function acquire(sock: net.Socket, key: string, acquireTimeoutS: number, leaseTtlS?: number): Promise<{
|
|
10
12
|
token: string;
|
|
11
13
|
lease: number;
|
|
@@ -21,20 +23,39 @@ declare function waitForLock(sock: net.Socket, key: string, waitTimeoutS: number
|
|
|
21
23
|
lease: number;
|
|
22
24
|
}>;
|
|
23
25
|
declare function release(sock: net.Socket, key: string, token: string): Promise<void>;
|
|
26
|
+
declare function semAcquire(sock: net.Socket, key: string, acquireTimeoutS: number, limit: number, leaseTtlS?: number): Promise<{
|
|
27
|
+
token: string;
|
|
28
|
+
lease: number;
|
|
29
|
+
}>;
|
|
30
|
+
declare function semRenew(sock: net.Socket, key: string, token: string, leaseTtlS?: number): Promise<number>;
|
|
31
|
+
declare function semEnqueue(sock: net.Socket, key: string, limit: number, leaseTtlS?: number): Promise<{
|
|
32
|
+
status: "acquired" | "queued";
|
|
33
|
+
token: string | null;
|
|
34
|
+
lease: number | null;
|
|
35
|
+
}>;
|
|
36
|
+
declare function semWaitForLock(sock: net.Socket, key: string, waitTimeoutS: number): Promise<{
|
|
37
|
+
token: string;
|
|
38
|
+
lease: number;
|
|
39
|
+
}>;
|
|
40
|
+
declare function semRelease(sock: net.Socket, key: string, token: string): Promise<void>;
|
|
24
41
|
interface DistributedLockOptions {
|
|
25
42
|
key: string;
|
|
26
43
|
acquireTimeoutS?: number;
|
|
27
44
|
leaseTtlS?: number;
|
|
45
|
+
/** @deprecated Use `servers` instead. */
|
|
28
46
|
host?: string;
|
|
47
|
+
/** @deprecated Use `servers` instead. */
|
|
29
48
|
port?: number;
|
|
49
|
+
servers?: Array<[host: string, port: number]>;
|
|
50
|
+
shardingStrategy?: ShardingStrategy;
|
|
30
51
|
renewRatio?: number;
|
|
31
52
|
}
|
|
32
53
|
declare class DistributedLock {
|
|
33
54
|
readonly key: string;
|
|
34
55
|
readonly acquireTimeoutS: number;
|
|
35
56
|
readonly leaseTtlS: number | undefined;
|
|
36
|
-
readonly
|
|
37
|
-
readonly
|
|
57
|
+
readonly servers: Array<[string, number]>;
|
|
58
|
+
readonly shardingStrategy: ShardingStrategy;
|
|
38
59
|
readonly renewRatio: number;
|
|
39
60
|
token: string | null;
|
|
40
61
|
lease: number;
|
|
@@ -42,6 +63,7 @@ declare class DistributedLock {
|
|
|
42
63
|
private renewTimer;
|
|
43
64
|
private closed;
|
|
44
65
|
constructor(opts: DistributedLockOptions);
|
|
66
|
+
private pickServer;
|
|
45
67
|
/** Acquire the lock. Returns `true` on success, `false` on timeout. */
|
|
46
68
|
acquire(): Promise<boolean>;
|
|
47
69
|
/** Release the lock and close the connection. */
|
|
@@ -74,5 +96,65 @@ declare class DistributedLock {
|
|
|
74
96
|
private startRenew;
|
|
75
97
|
private stopRenew;
|
|
76
98
|
}
|
|
99
|
+
interface DistributedSemaphoreOptions {
|
|
100
|
+
key: string;
|
|
101
|
+
limit: number;
|
|
102
|
+
acquireTimeoutS?: number;
|
|
103
|
+
leaseTtlS?: number;
|
|
104
|
+
/** @deprecated Use `servers` instead. */
|
|
105
|
+
host?: string;
|
|
106
|
+
/** @deprecated Use `servers` instead. */
|
|
107
|
+
port?: number;
|
|
108
|
+
servers?: Array<[host: string, port: number]>;
|
|
109
|
+
shardingStrategy?: ShardingStrategy;
|
|
110
|
+
renewRatio?: number;
|
|
111
|
+
}
|
|
112
|
+
declare class DistributedSemaphore {
|
|
113
|
+
readonly key: string;
|
|
114
|
+
readonly limit: number;
|
|
115
|
+
readonly acquireTimeoutS: number;
|
|
116
|
+
readonly leaseTtlS: number | undefined;
|
|
117
|
+
readonly servers: Array<[string, number]>;
|
|
118
|
+
readonly shardingStrategy: ShardingStrategy;
|
|
119
|
+
readonly renewRatio: number;
|
|
120
|
+
token: string | null;
|
|
121
|
+
lease: number;
|
|
122
|
+
private sock;
|
|
123
|
+
private renewTimer;
|
|
124
|
+
private closed;
|
|
125
|
+
constructor(opts: DistributedSemaphoreOptions);
|
|
126
|
+
private pickServer;
|
|
127
|
+
/** Acquire a semaphore slot. Returns `true` on success, `false` on timeout. */
|
|
128
|
+
acquire(): Promise<boolean>;
|
|
129
|
+
/** Release the semaphore slot and close the connection. */
|
|
130
|
+
release(): Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* Two-phase step 1: connect and join the FIFO queue.
|
|
133
|
+
* Returns `"acquired"` (fast-path, slot granted immediately) or `"queued"`.
|
|
134
|
+
* If acquired immediately, the renew loop starts automatically.
|
|
135
|
+
*/
|
|
136
|
+
enqueue(): Promise<"acquired" | "queued">;
|
|
137
|
+
/**
|
|
138
|
+
* Two-phase step 2: block until a semaphore slot is granted.
|
|
139
|
+
* Returns `true` if granted, `false` on timeout.
|
|
140
|
+
* If already acquired during `enqueue()`, returns `true` immediately.
|
|
141
|
+
*/
|
|
142
|
+
wait(timeoutS?: number): Promise<boolean>;
|
|
143
|
+
/**
|
|
144
|
+
* Run `fn` while holding a semaphore slot, then release automatically.
|
|
145
|
+
*
|
|
146
|
+
* ```ts
|
|
147
|
+
* const sem = new DistributedSemaphore({ key: "my-resource", limit: 5 });
|
|
148
|
+
* await sem.withLock(async () => {
|
|
149
|
+
* // critical section (up to 5 concurrent holders)
|
|
150
|
+
* });
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
withLock<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
154
|
+
/** Close the underlying socket (idempotent). */
|
|
155
|
+
close(): Promise<void>;
|
|
156
|
+
private startRenew;
|
|
157
|
+
private stopRenew;
|
|
158
|
+
}
|
|
77
159
|
|
|
78
|
-
export { AcquireTimeoutError, DistributedLock, type DistributedLockOptions, LockError, acquire, enqueue, release, renew, waitForLock };
|
|
160
|
+
export { AcquireTimeoutError, DistributedLock, type DistributedLockOptions, DistributedSemaphore, type DistributedSemaphoreOptions, LockError, type ShardingStrategy, acquire, enqueue, release, renew, semAcquire, semEnqueue, semRelease, semRenew, semWaitForLock, stableHashShard, waitForLock };
|
package/dist/client.js
CHANGED
|
@@ -52,6 +52,24 @@ function connect(host, port) {
|
|
|
52
52
|
sock.on("error", reject);
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
+
var CRC32_TABLE = new Uint32Array(256);
|
|
56
|
+
for (let i = 0; i < 256; i++) {
|
|
57
|
+
let c = i;
|
|
58
|
+
for (let j = 0; j < 8; j++) {
|
|
59
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
60
|
+
}
|
|
61
|
+
CRC32_TABLE[i] = c;
|
|
62
|
+
}
|
|
63
|
+
function crc32(buf) {
|
|
64
|
+
let crc = 4294967295;
|
|
65
|
+
for (let i = 0; i < buf.length; i++) {
|
|
66
|
+
crc = CRC32_TABLE[(crc ^ buf[i]) & 255] ^ crc >>> 8;
|
|
67
|
+
}
|
|
68
|
+
return (crc ^ 4294967295) >>> 0;
|
|
69
|
+
}
|
|
70
|
+
function stableHashShard(key, numServers) {
|
|
71
|
+
return (crc32(Buffer.from(key, "utf-8")) >>> 0) % numServers;
|
|
72
|
+
}
|
|
55
73
|
async function acquire(sock, key, acquireTimeoutS, leaseTtlS) {
|
|
56
74
|
const arg = leaseTtlS == null ? String(acquireTimeoutS) : `${acquireTimeoutS} ${leaseTtlS}`;
|
|
57
75
|
sock.write(encodeLines("l", key, arg));
|
|
@@ -119,12 +137,79 @@ async function release(sock, key, token) {
|
|
|
119
137
|
throw new LockError(`release failed: '${resp}'`);
|
|
120
138
|
}
|
|
121
139
|
}
|
|
140
|
+
async function semAcquire(sock, key, acquireTimeoutS, limit, leaseTtlS) {
|
|
141
|
+
const arg = leaseTtlS == null ? `${acquireTimeoutS} ${limit}` : `${acquireTimeoutS} ${limit} ${leaseTtlS}`;
|
|
142
|
+
sock.write(encodeLines("sl", key, arg));
|
|
143
|
+
const resp = await readline(sock);
|
|
144
|
+
if (resp === "timeout") {
|
|
145
|
+
throw new AcquireTimeoutError(key);
|
|
146
|
+
}
|
|
147
|
+
if (!resp.startsWith("ok ")) {
|
|
148
|
+
throw new LockError(`sem_acquire failed: '${resp}'`);
|
|
149
|
+
}
|
|
150
|
+
const parts = resp.split(" ");
|
|
151
|
+
if (parts.length < 2) {
|
|
152
|
+
throw new LockError(`bad ok response: '${resp}'`);
|
|
153
|
+
}
|
|
154
|
+
const token = parts[1];
|
|
155
|
+
const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 30;
|
|
156
|
+
return { token, lease };
|
|
157
|
+
}
|
|
158
|
+
async function semRenew(sock, key, token, leaseTtlS) {
|
|
159
|
+
const arg = leaseTtlS == null ? token : `${token} ${leaseTtlS}`;
|
|
160
|
+
sock.write(encodeLines("sn", key, arg));
|
|
161
|
+
const resp = await readline(sock);
|
|
162
|
+
if (!resp.startsWith("ok")) {
|
|
163
|
+
throw new LockError(`sem_renew failed: '${resp}'`);
|
|
164
|
+
}
|
|
165
|
+
const parts = resp.split(" ");
|
|
166
|
+
if (parts.length >= 2 && /^\d+$/.test(parts[1])) {
|
|
167
|
+
return parseInt(parts[1], 10);
|
|
168
|
+
}
|
|
169
|
+
return -1;
|
|
170
|
+
}
|
|
171
|
+
async function semEnqueue(sock, key, limit, leaseTtlS) {
|
|
172
|
+
const arg = leaseTtlS == null ? String(limit) : `${limit} ${leaseTtlS}`;
|
|
173
|
+
sock.write(encodeLines("se", key, arg));
|
|
174
|
+
const resp = await readline(sock);
|
|
175
|
+
if (resp.startsWith("acquired ")) {
|
|
176
|
+
const parts = resp.split(" ");
|
|
177
|
+
const token = parts[1];
|
|
178
|
+
const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 30;
|
|
179
|
+
return { status: "acquired", token, lease };
|
|
180
|
+
}
|
|
181
|
+
if (resp === "queued") {
|
|
182
|
+
return { status: "queued", token: null, lease: null };
|
|
183
|
+
}
|
|
184
|
+
throw new LockError(`sem_enqueue failed: '${resp}'`);
|
|
185
|
+
}
|
|
186
|
+
async function semWaitForLock(sock, key, waitTimeoutS) {
|
|
187
|
+
sock.write(encodeLines("sw", key, String(waitTimeoutS)));
|
|
188
|
+
const resp = await readline(sock);
|
|
189
|
+
if (resp === "timeout") {
|
|
190
|
+
throw new AcquireTimeoutError(key);
|
|
191
|
+
}
|
|
192
|
+
if (!resp.startsWith("ok ")) {
|
|
193
|
+
throw new LockError(`sem_wait failed: '${resp}'`);
|
|
194
|
+
}
|
|
195
|
+
const parts = resp.split(" ");
|
|
196
|
+
const token = parts[1];
|
|
197
|
+
const lease = parts.length >= 3 ? parseInt(parts[2], 10) : 30;
|
|
198
|
+
return { token, lease };
|
|
199
|
+
}
|
|
200
|
+
async function semRelease(sock, key, token) {
|
|
201
|
+
sock.write(encodeLines("sr", key, token));
|
|
202
|
+
const resp = await readline(sock);
|
|
203
|
+
if (resp !== "ok") {
|
|
204
|
+
throw new LockError(`sem_release failed: '${resp}'`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
122
207
|
var DistributedLock = class {
|
|
123
208
|
key;
|
|
124
209
|
acquireTimeoutS;
|
|
125
210
|
leaseTtlS;
|
|
126
|
-
|
|
127
|
-
|
|
211
|
+
servers;
|
|
212
|
+
shardingStrategy;
|
|
128
213
|
renewRatio;
|
|
129
214
|
token = null;
|
|
130
215
|
lease = 0;
|
|
@@ -135,14 +220,26 @@ var DistributedLock = class {
|
|
|
135
220
|
this.key = opts.key;
|
|
136
221
|
this.acquireTimeoutS = opts.acquireTimeoutS ?? 10;
|
|
137
222
|
this.leaseTtlS = opts.leaseTtlS;
|
|
138
|
-
|
|
139
|
-
|
|
223
|
+
if (opts.servers) {
|
|
224
|
+
if (opts.servers.length === 0) {
|
|
225
|
+
throw new LockError("servers list must not be empty");
|
|
226
|
+
}
|
|
227
|
+
this.servers = opts.servers;
|
|
228
|
+
} else {
|
|
229
|
+
this.servers = [[opts.host ?? DEFAULT_HOST, opts.port ?? DEFAULT_PORT]];
|
|
230
|
+
}
|
|
231
|
+
this.shardingStrategy = opts.shardingStrategy ?? stableHashShard;
|
|
140
232
|
this.renewRatio = opts.renewRatio ?? 0.5;
|
|
141
233
|
}
|
|
234
|
+
pickServer() {
|
|
235
|
+
const idx = this.shardingStrategy(this.key, this.servers.length);
|
|
236
|
+
return this.servers[idx];
|
|
237
|
+
}
|
|
142
238
|
/** Acquire the lock. Returns `true` on success, `false` on timeout. */
|
|
143
239
|
async acquire() {
|
|
144
240
|
this.closed = false;
|
|
145
|
-
|
|
241
|
+
const [host, port] = this.pickServer();
|
|
242
|
+
this.sock = await connect(host, port);
|
|
146
243
|
try {
|
|
147
244
|
const result = await acquire(
|
|
148
245
|
this.sock,
|
|
@@ -178,7 +275,8 @@ var DistributedLock = class {
|
|
|
178
275
|
*/
|
|
179
276
|
async enqueue() {
|
|
180
277
|
this.closed = false;
|
|
181
|
-
|
|
278
|
+
const [host, port] = this.pickServer();
|
|
279
|
+
this.sock = await connect(host, port);
|
|
182
280
|
try {
|
|
183
281
|
const result = await enqueue(this.sock, this.key, this.leaseTtlS);
|
|
184
282
|
if (result.status === "acquired") {
|
|
@@ -274,14 +372,192 @@ var DistributedLock = class {
|
|
|
274
372
|
}
|
|
275
373
|
}
|
|
276
374
|
};
|
|
375
|
+
var DistributedSemaphore = class {
|
|
376
|
+
key;
|
|
377
|
+
limit;
|
|
378
|
+
acquireTimeoutS;
|
|
379
|
+
leaseTtlS;
|
|
380
|
+
servers;
|
|
381
|
+
shardingStrategy;
|
|
382
|
+
renewRatio;
|
|
383
|
+
token = null;
|
|
384
|
+
lease = 0;
|
|
385
|
+
sock = null;
|
|
386
|
+
renewTimer = null;
|
|
387
|
+
closed = false;
|
|
388
|
+
constructor(opts) {
|
|
389
|
+
this.key = opts.key;
|
|
390
|
+
this.limit = opts.limit;
|
|
391
|
+
this.acquireTimeoutS = opts.acquireTimeoutS ?? 10;
|
|
392
|
+
this.leaseTtlS = opts.leaseTtlS;
|
|
393
|
+
if (opts.servers) {
|
|
394
|
+
if (opts.servers.length === 0) {
|
|
395
|
+
throw new LockError("servers list must not be empty");
|
|
396
|
+
}
|
|
397
|
+
this.servers = opts.servers;
|
|
398
|
+
} else {
|
|
399
|
+
this.servers = [[opts.host ?? DEFAULT_HOST, opts.port ?? DEFAULT_PORT]];
|
|
400
|
+
}
|
|
401
|
+
this.shardingStrategy = opts.shardingStrategy ?? stableHashShard;
|
|
402
|
+
this.renewRatio = opts.renewRatio ?? 0.5;
|
|
403
|
+
}
|
|
404
|
+
pickServer() {
|
|
405
|
+
const idx = this.shardingStrategy(this.key, this.servers.length);
|
|
406
|
+
return this.servers[idx];
|
|
407
|
+
}
|
|
408
|
+
/** Acquire a semaphore slot. Returns `true` on success, `false` on timeout. */
|
|
409
|
+
async acquire() {
|
|
410
|
+
this.closed = false;
|
|
411
|
+
const [host, port] = this.pickServer();
|
|
412
|
+
this.sock = await connect(host, port);
|
|
413
|
+
try {
|
|
414
|
+
const result = await semAcquire(
|
|
415
|
+
this.sock,
|
|
416
|
+
this.key,
|
|
417
|
+
this.acquireTimeoutS,
|
|
418
|
+
this.limit,
|
|
419
|
+
this.leaseTtlS
|
|
420
|
+
);
|
|
421
|
+
this.token = result.token;
|
|
422
|
+
this.lease = result.lease;
|
|
423
|
+
} catch (err) {
|
|
424
|
+
await this.close();
|
|
425
|
+
if (err instanceof AcquireTimeoutError) return false;
|
|
426
|
+
throw err;
|
|
427
|
+
}
|
|
428
|
+
this.startRenew();
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
/** Release the semaphore slot and close the connection. */
|
|
432
|
+
async release() {
|
|
433
|
+
try {
|
|
434
|
+
this.stopRenew();
|
|
435
|
+
if (this.sock && this.token) {
|
|
436
|
+
await semRelease(this.sock, this.key, this.token);
|
|
437
|
+
}
|
|
438
|
+
} finally {
|
|
439
|
+
await this.close();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Two-phase step 1: connect and join the FIFO queue.
|
|
444
|
+
* Returns `"acquired"` (fast-path, slot granted immediately) or `"queued"`.
|
|
445
|
+
* If acquired immediately, the renew loop starts automatically.
|
|
446
|
+
*/
|
|
447
|
+
async enqueue() {
|
|
448
|
+
this.closed = false;
|
|
449
|
+
const [host, port] = this.pickServer();
|
|
450
|
+
this.sock = await connect(host, port);
|
|
451
|
+
try {
|
|
452
|
+
const result = await semEnqueue(this.sock, this.key, this.limit, this.leaseTtlS);
|
|
453
|
+
if (result.status === "acquired") {
|
|
454
|
+
this.token = result.token;
|
|
455
|
+
this.lease = result.lease ?? 0;
|
|
456
|
+
this.startRenew();
|
|
457
|
+
}
|
|
458
|
+
return result.status;
|
|
459
|
+
} catch (err) {
|
|
460
|
+
await this.close();
|
|
461
|
+
throw err;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Two-phase step 2: block until a semaphore slot is granted.
|
|
466
|
+
* Returns `true` if granted, `false` on timeout.
|
|
467
|
+
* If already acquired during `enqueue()`, returns `true` immediately.
|
|
468
|
+
*/
|
|
469
|
+
async wait(timeoutS) {
|
|
470
|
+
if (this.token !== null) {
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
if (!this.sock) {
|
|
474
|
+
throw new LockError("not connected; call enqueue() first");
|
|
475
|
+
}
|
|
476
|
+
const timeout = timeoutS ?? this.acquireTimeoutS;
|
|
477
|
+
try {
|
|
478
|
+
const result = await semWaitForLock(this.sock, this.key, timeout);
|
|
479
|
+
this.token = result.token;
|
|
480
|
+
this.lease = result.lease;
|
|
481
|
+
} catch (err) {
|
|
482
|
+
await this.close();
|
|
483
|
+
if (err instanceof AcquireTimeoutError) return false;
|
|
484
|
+
throw err;
|
|
485
|
+
}
|
|
486
|
+
this.startRenew();
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Run `fn` while holding a semaphore slot, then release automatically.
|
|
491
|
+
*
|
|
492
|
+
* ```ts
|
|
493
|
+
* const sem = new DistributedSemaphore({ key: "my-resource", limit: 5 });
|
|
494
|
+
* await sem.withLock(async () => {
|
|
495
|
+
* // critical section (up to 5 concurrent holders)
|
|
496
|
+
* });
|
|
497
|
+
* ```
|
|
498
|
+
*/
|
|
499
|
+
async withLock(fn) {
|
|
500
|
+
const ok = await this.acquire();
|
|
501
|
+
if (!ok) {
|
|
502
|
+
throw new AcquireTimeoutError(this.key);
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
return await fn();
|
|
506
|
+
} finally {
|
|
507
|
+
await this.release();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/** Close the underlying socket (idempotent). */
|
|
511
|
+
async close() {
|
|
512
|
+
if (this.closed) return;
|
|
513
|
+
this.closed = true;
|
|
514
|
+
this.stopRenew();
|
|
515
|
+
if (this.sock) {
|
|
516
|
+
this.sock.destroy();
|
|
517
|
+
this.sock = null;
|
|
518
|
+
}
|
|
519
|
+
this.token = null;
|
|
520
|
+
}
|
|
521
|
+
// -- internals --
|
|
522
|
+
startRenew() {
|
|
523
|
+
const interval = Math.max(1, this.lease * this.renewRatio) * 1e3;
|
|
524
|
+
const loop = async () => {
|
|
525
|
+
if (!this.sock || !this.token) return;
|
|
526
|
+
try {
|
|
527
|
+
await semRenew(this.sock, this.key, this.token, this.leaseTtlS);
|
|
528
|
+
} catch {
|
|
529
|
+
console.error(
|
|
530
|
+
`semaphore lost (renew failed): key=${this.key} token=${this.token}`
|
|
531
|
+
);
|
|
532
|
+
this.token = null;
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
this.renewTimer = setTimeout(loop, interval);
|
|
536
|
+
};
|
|
537
|
+
this.renewTimer = setTimeout(loop, interval);
|
|
538
|
+
}
|
|
539
|
+
stopRenew() {
|
|
540
|
+
if (this.renewTimer != null) {
|
|
541
|
+
clearTimeout(this.renewTimer);
|
|
542
|
+
this.renewTimer = null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
};
|
|
277
546
|
export {
|
|
278
547
|
AcquireTimeoutError,
|
|
279
548
|
DistributedLock,
|
|
549
|
+
DistributedSemaphore,
|
|
280
550
|
LockError,
|
|
281
551
|
acquire,
|
|
282
552
|
enqueue,
|
|
283
553
|
release,
|
|
284
554
|
renew,
|
|
555
|
+
semAcquire,
|
|
556
|
+
semEnqueue,
|
|
557
|
+
semRelease,
|
|
558
|
+
semRenew,
|
|
559
|
+
semWaitForLock,
|
|
560
|
+
stableHashShard,
|
|
285
561
|
waitForLock
|
|
286
562
|
};
|
|
287
563
|
//# sourceMappingURL=client.js.map
|