@nvae/llmswitch 0.2.0 → 0.4.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 +1 -1
- package/dist/adapters/claude.js +8 -5
- package/dist/adapters/codex.js +10 -4
- package/dist/bridge/manager.js +241 -146
- package/dist/bridge/runtime.js +199 -0
- package/dist/bridge/server.js +207 -80
- package/dist/bridge/state.js +345 -69
- package/dist/bridge/translate-response.js +205 -80
- package/dist/bridge/transport.js +439 -0
- package/dist/commands/bridge-cmd.js +13 -11
- package/dist/commands/prompts.js +47 -51
- package/dist/store/profiles.js +2 -2
- package/dist/types.js +20 -3
- package/dist/utils/fetch-models.js +45 -56
- package/dist/utils/proxy.js +14 -37
- package/package.json +5 -3
package/dist/bridge/state.js
CHANGED
|
@@ -1,29 +1,70 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, existsSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
4
|
import { atomicWriteFile, ensureDir } from "../utils/fs.js";
|
|
4
5
|
import { getAppConfigRoot } from "../utils/paths.js";
|
|
5
6
|
import { DEFAULT_BRIDGE_HOST, DEFAULT_BRIDGE_PORT, emptyUpstreams, } from "./types.js";
|
|
7
|
+
const STATE_VERSION = 2;
|
|
8
|
+
const MAX_PID = 2_147_483_647;
|
|
9
|
+
const LOCK_STALE_MS = 120_000;
|
|
10
|
+
const LOCK_DEFAULT_TIMEOUT_MS = 5_000;
|
|
11
|
+
export class BridgeStateConflictError extends Error {
|
|
12
|
+
constructor(expected, actual) {
|
|
13
|
+
super(`Bridge 状态版本冲突:期望 revision ${expected},实际 ${actual}`);
|
|
14
|
+
this.name = "BridgeStateConflictError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class BridgeLockTimeoutError extends Error {
|
|
18
|
+
constructor(message = "获取 bridge 锁超时") {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "BridgeLockTimeoutError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
6
23
|
export function getBridgeDir() {
|
|
7
24
|
return join(getAppConfigRoot(), "bridge");
|
|
8
25
|
}
|
|
9
26
|
export function getBridgeStatePath() {
|
|
10
27
|
return join(getBridgeDir(), "state.json");
|
|
11
28
|
}
|
|
29
|
+
/** @deprecated v1 single-object upstream file; read once for migration only. */
|
|
12
30
|
export function getBridgeUpstreamPath() {
|
|
13
31
|
return join(getBridgeDir(), "upstream.json");
|
|
14
32
|
}
|
|
33
|
+
/** @deprecated PID is diagnostic only; identity lives in state.instance. */
|
|
15
34
|
export function getBridgePidPath() {
|
|
16
35
|
return join(getBridgeDir(), "bridge.pid");
|
|
17
36
|
}
|
|
37
|
+
export function getBridgeLockPath() {
|
|
38
|
+
return join(getBridgeDir(), "state.lock");
|
|
39
|
+
}
|
|
40
|
+
export function getTransactionsDir() {
|
|
41
|
+
return join(getAppConfigRoot(), "transactions");
|
|
42
|
+
}
|
|
43
|
+
// --- tokens -----------------------------------------------------------------
|
|
44
|
+
/** 32 random bytes as unpadded base64url. */
|
|
45
|
+
export function generateBridgeToken() {
|
|
46
|
+
return randomBytes(32).toString("base64url");
|
|
47
|
+
}
|
|
48
|
+
export function isValidBridgeToken(token) {
|
|
49
|
+
return typeof token === "string" && /^[A-Za-z0-9_-]{43}$/.test(token);
|
|
50
|
+
}
|
|
51
|
+
/** Constant-time token comparison; length mismatch is a fast, safe reject. */
|
|
52
|
+
export function constantTimeTokenEqual(a, b) {
|
|
53
|
+
if (typeof a !== "string" || typeof b !== "string")
|
|
54
|
+
return false;
|
|
55
|
+
const bufA = Buffer.from(a, "utf8");
|
|
56
|
+
const bufB = Buffer.from(b, "utf8");
|
|
57
|
+
if (bufA.length !== bufB.length)
|
|
58
|
+
return false;
|
|
59
|
+
return timingSafeEqual(bufA, bufB);
|
|
60
|
+
}
|
|
61
|
+
// --- upstream normalization -------------------------------------------------
|
|
18
62
|
function isLegacyUpstream(raw) {
|
|
19
63
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
20
64
|
return false;
|
|
21
65
|
const row = raw;
|
|
22
66
|
return typeof row.baseUrl === "string" && !("codex" in row) && !("claude" in row);
|
|
23
67
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Normalize disk/runtime upstream payloads (legacy single object → per-tool map).
|
|
26
|
-
*/
|
|
27
68
|
export function normalizeBridgeUpstreams(raw) {
|
|
28
69
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
29
70
|
return emptyUpstreams();
|
|
@@ -37,89 +78,324 @@ export function normalizeBridgeUpstreams(raw) {
|
|
|
37
78
|
claude: row.claude ?? null,
|
|
38
79
|
};
|
|
39
80
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!
|
|
43
|
-
return
|
|
44
|
-
|
|
45
|
-
return normalizeBridgeUpstreams(JSON.parse(readFileSync(path, "utf8")));
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return emptyUpstreams();
|
|
49
|
-
}
|
|
81
|
+
/** Legacy upstreams cannot authenticate until reapplied. */
|
|
82
|
+
function markUpstreamMigrationRequired(upstream) {
|
|
83
|
+
if (!upstream)
|
|
84
|
+
return null;
|
|
85
|
+
return { ...upstream, clientToken: null, migrationRequired: true };
|
|
50
86
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
87
|
+
// --- persistence ------------------------------------------------------------
|
|
88
|
+
function isValidPid(pid) {
|
|
89
|
+
return (typeof pid === "number" &&
|
|
90
|
+
Number.isInteger(pid) &&
|
|
91
|
+
pid >= 1 &&
|
|
92
|
+
pid <= MAX_PID);
|
|
57
93
|
}
|
|
58
|
-
|
|
59
|
-
return
|
|
94
|
+
function defaultListener() {
|
|
95
|
+
return {
|
|
96
|
+
bindHost: process.env.LLM_SWITCH_BRIDGE_HOST || DEFAULT_BRIDGE_HOST,
|
|
97
|
+
advertiseHost: process.env.LLM_SWITCH_BRIDGE_HOST || DEFAULT_BRIDGE_HOST,
|
|
98
|
+
port: Number(process.env.LLM_SWITCH_BRIDGE_PORT) || DEFAULT_BRIDGE_PORT,
|
|
99
|
+
allowRemote: false,
|
|
100
|
+
};
|
|
60
101
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
current[tool] = upstream;
|
|
64
|
-
writeBridgeUpstreams(current);
|
|
65
|
-
const state = readBridgeStateRaw();
|
|
66
|
-
writeBridgeState({
|
|
102
|
+
function withFlatAliases(state) {
|
|
103
|
+
return {
|
|
67
104
|
...state,
|
|
68
|
-
|
|
69
|
-
|
|
105
|
+
port: state.listener.port,
|
|
106
|
+
pid: state.instance?.pid ?? null,
|
|
107
|
+
host: state.listener.bindHost,
|
|
108
|
+
};
|
|
70
109
|
}
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
110
|
+
function parseInstance(raw) {
|
|
111
|
+
if (!raw || typeof raw !== "object")
|
|
112
|
+
return null;
|
|
113
|
+
const row = raw;
|
|
114
|
+
if (typeof row.id !== "string" ||
|
|
115
|
+
!isValidBridgeToken(row.controlToken) ||
|
|
116
|
+
!isValidPid(row.pid)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
id: row.id,
|
|
121
|
+
controlToken: row.controlToken,
|
|
122
|
+
pid: row.pid,
|
|
123
|
+
startedAt: typeof row.startedAt === "string" ? row.startedAt : "",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function parseListener(raw) {
|
|
127
|
+
const fallback = defaultListener();
|
|
128
|
+
if (!raw || typeof raw !== "object")
|
|
129
|
+
return fallback;
|
|
130
|
+
const row = raw;
|
|
131
|
+
return {
|
|
132
|
+
bindHost: typeof row.bindHost === "string" ? row.bindHost : fallback.bindHost,
|
|
133
|
+
advertiseHost: typeof row.advertiseHost === "string"
|
|
134
|
+
? row.advertiseHost
|
|
135
|
+
: fallback.advertiseHost,
|
|
136
|
+
port: typeof row.port === "number" ? row.port : fallback.port,
|
|
137
|
+
allowRemote: row.allowRemote === true,
|
|
78
138
|
};
|
|
79
|
-
|
|
80
|
-
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Read the one-time v1 migration state: legacy upstream.json, flagged so it can
|
|
142
|
+
* be inspected but never authenticate a client until reapplied.
|
|
143
|
+
*/
|
|
144
|
+
function readLegacyMigrationState() {
|
|
145
|
+
const legacyPath = getBridgeUpstreamPath();
|
|
146
|
+
if (!existsSync(legacyPath))
|
|
147
|
+
return null;
|
|
81
148
|
try {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
};
|
|
149
|
+
const upstreams = normalizeBridgeUpstreams(JSON.parse(readFileSync(legacyPath, "utf8")));
|
|
150
|
+
return withFlatAliases({
|
|
151
|
+
version: STATE_VERSION,
|
|
152
|
+
revision: 0,
|
|
153
|
+
listener: defaultListener(),
|
|
154
|
+
instance: null,
|
|
155
|
+
upstreams: {
|
|
156
|
+
codex: markUpstreamMigrationRequired(upstreams.codex),
|
|
157
|
+
claude: markUpstreamMigrationRequired(upstreams.claude),
|
|
158
|
+
},
|
|
159
|
+
pending: null,
|
|
160
|
+
});
|
|
94
161
|
}
|
|
95
162
|
catch {
|
|
96
|
-
return
|
|
163
|
+
return null;
|
|
97
164
|
}
|
|
98
165
|
}
|
|
99
166
|
export function readBridgeState() {
|
|
100
|
-
|
|
167
|
+
const path = getBridgeStatePath();
|
|
168
|
+
if (!existsSync(path)) {
|
|
169
|
+
const legacy = readLegacyMigrationState();
|
|
170
|
+
if (legacy)
|
|
171
|
+
return legacy;
|
|
172
|
+
return withFlatAliases({
|
|
173
|
+
version: STATE_VERSION,
|
|
174
|
+
revision: 0,
|
|
175
|
+
listener: defaultListener(),
|
|
176
|
+
instance: null,
|
|
177
|
+
upstreams: emptyUpstreams(),
|
|
178
|
+
pending: null,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
183
|
+
const pending = raw.pending && typeof raw.pending === "object"
|
|
184
|
+
? {
|
|
185
|
+
revision: Number(raw.pending.revision) || 0,
|
|
186
|
+
upstreams: normalizeBridgeUpstreams(raw.pending.upstreams),
|
|
187
|
+
transactionId: raw.pending
|
|
188
|
+
.transactionId,
|
|
189
|
+
}
|
|
190
|
+
: null;
|
|
191
|
+
return withFlatAliases({
|
|
192
|
+
version: STATE_VERSION,
|
|
193
|
+
revision: typeof raw.revision === "number" ? raw.revision : 0,
|
|
194
|
+
listener: parseListener(raw.listener),
|
|
195
|
+
instance: parseInstance(raw.instance),
|
|
196
|
+
upstreams: normalizeBridgeUpstreams(raw.upstreams),
|
|
197
|
+
pending: pending,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return withFlatAliases({
|
|
202
|
+
version: STATE_VERSION,
|
|
203
|
+
revision: 0,
|
|
204
|
+
listener: defaultListener(),
|
|
205
|
+
instance: null,
|
|
206
|
+
upstreams: emptyUpstreams(),
|
|
207
|
+
pending: null,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
101
210
|
}
|
|
102
|
-
|
|
211
|
+
function persistState(next) {
|
|
103
212
|
ensureDir(getBridgeDir());
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
213
|
+
try {
|
|
214
|
+
chmodSync(getBridgeDir(), 0o700);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// best effort; Windows relies on user-dir ACLs
|
|
218
|
+
}
|
|
219
|
+
const payload = {
|
|
220
|
+
version: STATE_VERSION,
|
|
221
|
+
revision: next.revision,
|
|
222
|
+
listener: next.listener,
|
|
223
|
+
instance: next.instance,
|
|
224
|
+
upstreams: {
|
|
225
|
+
codex: next.upstreams.codex,
|
|
226
|
+
claude: next.upstreams.claude,
|
|
227
|
+
},
|
|
228
|
+
pending: next.pending,
|
|
229
|
+
};
|
|
230
|
+
atomicWriteFile(getBridgeStatePath(), JSON.stringify(payload, null, 2) + "\n");
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Persist state with an incremented revision. Rejects stale writes: the input
|
|
234
|
+
* revision must equal the on-disk revision (compare-and-set).
|
|
235
|
+
*/
|
|
236
|
+
export function writeBridgeState(state) {
|
|
237
|
+
const current = readBridgeState();
|
|
238
|
+
if (existsSync(getBridgeStatePath()) && state.revision !== current.revision) {
|
|
239
|
+
throw new BridgeStateConflictError(current.revision, state.revision);
|
|
240
|
+
}
|
|
241
|
+
const next = withFlatAliases({
|
|
242
|
+
version: STATE_VERSION,
|
|
243
|
+
revision: current.revision + 1,
|
|
244
|
+
listener: state.listener,
|
|
245
|
+
instance: state.instance,
|
|
246
|
+
upstreams: normalizeBridgeUpstreams(state.upstreams),
|
|
247
|
+
pending: state.pending,
|
|
248
|
+
});
|
|
249
|
+
persistState(next);
|
|
250
|
+
return next;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Atomically mutate state under compare-and-set. `expectedRevision` defaults to
|
|
254
|
+
* the current on-disk revision.
|
|
255
|
+
*/
|
|
256
|
+
export function updateBridgeState(mutate, expectedRevision) {
|
|
257
|
+
const current = readBridgeState();
|
|
258
|
+
if (expectedRevision !== undefined && expectedRevision !== current.revision) {
|
|
259
|
+
throw new BridgeStateConflictError(current.revision, expectedRevision);
|
|
260
|
+
}
|
|
261
|
+
const mutated = mutate(current);
|
|
262
|
+
const next = withFlatAliases({
|
|
263
|
+
version: STATE_VERSION,
|
|
264
|
+
revision: current.revision + 1,
|
|
265
|
+
listener: mutated.listener,
|
|
266
|
+
instance: mutated.instance,
|
|
267
|
+
upstreams: normalizeBridgeUpstreams(mutated.upstreams),
|
|
268
|
+
pending: mutated.pending,
|
|
269
|
+
});
|
|
270
|
+
persistState(next);
|
|
271
|
+
return next;
|
|
272
|
+
}
|
|
273
|
+
// --- upstream facade (compat) ----------------------------------------------
|
|
274
|
+
export function readBridgeUpstreams() {
|
|
275
|
+
return readBridgeState().upstreams;
|
|
276
|
+
}
|
|
277
|
+
export function readBridgeUpstream(tool = "codex") {
|
|
278
|
+
return readBridgeUpstreams()[tool];
|
|
279
|
+
}
|
|
280
|
+
export function writeBridgeUpstream(tool, upstream) {
|
|
281
|
+
return updateBridgeState((current) => ({
|
|
282
|
+
...current,
|
|
283
|
+
upstreams: { ...current.upstreams, [tool]: upstream },
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
export function writeBridgeUpstreams(upstreams) {
|
|
287
|
+
return updateBridgeState((current) => ({ ...current, upstreams }));
|
|
288
|
+
}
|
|
289
|
+
// --- URLs -------------------------------------------------------------------
|
|
290
|
+
function hostForUrl(host) {
|
|
291
|
+
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
292
|
+
}
|
|
293
|
+
/** Codex-facing base URL (includes /v1), using the connectable advertise host. */
|
|
117
294
|
export function bridgeBaseUrl(state) {
|
|
118
295
|
const s = state || readBridgeState();
|
|
119
|
-
return `http://${s.
|
|
296
|
+
return `http://${hostForUrl(s.listener.advertiseHost)}:${s.listener.port}/v1`;
|
|
120
297
|
}
|
|
121
298
|
/** Claude-facing root URL (no /v1; client appends /v1/messages). */
|
|
122
299
|
export function bridgeRootUrl(state) {
|
|
123
300
|
const s = state || readBridgeState();
|
|
124
|
-
return `http://${s.
|
|
301
|
+
return `http://${hostForUrl(s.listener.advertiseHost)}:${s.listener.port}`;
|
|
302
|
+
}
|
|
303
|
+
function pidAlive(pid) {
|
|
304
|
+
try {
|
|
305
|
+
process.kill(pid, 0);
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
return err.code === "EPERM";
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function hasPendingJournal(transactionId) {
|
|
313
|
+
if (!transactionId)
|
|
314
|
+
return false;
|
|
315
|
+
return existsSync(join(getTransactionsDir(), `${transactionId}.json`));
|
|
316
|
+
}
|
|
317
|
+
function readLockRecord(path) {
|
|
318
|
+
try {
|
|
319
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Acquire the exclusive bridge lock (global lock in the design's lock order).
|
|
327
|
+
* Reclaims a stale lock when: (a) the owner PID is dead and an associated
|
|
328
|
+
* transaction journal exists (immediate journal recovery), or (b) the owner PID
|
|
329
|
+
* is dead and the lock is older than two minutes. Never steals a live lock.
|
|
330
|
+
*/
|
|
331
|
+
export function acquireBridgeLock(options = {}) {
|
|
332
|
+
ensureDir(getBridgeDir());
|
|
333
|
+
const lockPath = getBridgeLockPath();
|
|
334
|
+
const deadline = Date.now() + (options.timeoutMs ?? LOCK_DEFAULT_TIMEOUT_MS);
|
|
335
|
+
const lockId = randomBytes(8).toString("hex");
|
|
336
|
+
const record = {
|
|
337
|
+
lockId,
|
|
338
|
+
pid: process.pid,
|
|
339
|
+
instanceId: options.instanceId ?? null,
|
|
340
|
+
transactionId: options.transactionId ?? null,
|
|
341
|
+
createdAt: new Date().toISOString(),
|
|
342
|
+
};
|
|
343
|
+
const serialized = JSON.stringify(record) + "\n";
|
|
344
|
+
for (;;) {
|
|
345
|
+
try {
|
|
346
|
+
writeFileSync(lockPath, serialized, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
347
|
+
return makeLock(lockPath, lockId);
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
if (err.code !== "EEXIST")
|
|
351
|
+
throw err;
|
|
352
|
+
}
|
|
353
|
+
const existing = readLockRecord(lockPath);
|
|
354
|
+
if (existing && reclaimable(existing, lockPath)) {
|
|
355
|
+
// Atomically take over by replacing via temp rename.
|
|
356
|
+
const tmp = `${lockPath}.${lockId}.tmp`;
|
|
357
|
+
writeFileSync(tmp, serialized, { encoding: "utf8", mode: 0o600 });
|
|
358
|
+
renameSync(tmp, lockPath);
|
|
359
|
+
const confirmed = readLockRecord(lockPath);
|
|
360
|
+
if (confirmed?.lockId === lockId)
|
|
361
|
+
return makeLock(lockPath, lockId);
|
|
362
|
+
}
|
|
363
|
+
if (Date.now() >= deadline) {
|
|
364
|
+
throw new BridgeLockTimeoutError();
|
|
365
|
+
}
|
|
366
|
+
sleepSync(25);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function reclaimable(record, lockPath) {
|
|
370
|
+
if (isValidPid(record.pid) && pidAlive(record.pid))
|
|
371
|
+
return false;
|
|
372
|
+
// Dead owner with a pending journal: recover immediately.
|
|
373
|
+
if (hasPendingJournal(record.transactionId))
|
|
374
|
+
return true;
|
|
375
|
+
// Orphan lock without a journal: only after the stale age.
|
|
376
|
+
let age = Number.POSITIVE_INFINITY;
|
|
377
|
+
try {
|
|
378
|
+
age = Date.now() - statSync(lockPath).mtimeMs;
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
age = Number.POSITIVE_INFINITY;
|
|
382
|
+
}
|
|
383
|
+
const recordedAge = record.createdAt
|
|
384
|
+
? Date.now() - Date.parse(record.createdAt)
|
|
385
|
+
: Number.POSITIVE_INFINITY;
|
|
386
|
+
return Math.max(age, recordedAge) > LOCK_STALE_MS;
|
|
387
|
+
}
|
|
388
|
+
function makeLock(lockPath, lockId) {
|
|
389
|
+
return {
|
|
390
|
+
release() {
|
|
391
|
+
const current = readLockRecord(lockPath);
|
|
392
|
+
if (current?.lockId === lockId) {
|
|
393
|
+
rmSync(lockPath, { force: true });
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function sleepSync(ms) {
|
|
399
|
+
const shared = new Int32Array(new SharedArrayBuffer(4));
|
|
400
|
+
Atomics.wait(shared, 0, 0, ms);
|
|
125
401
|
}
|