@fitchmultz/pi-subagents 0.48.0 → 0.49.1
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/CHANGELOG.md +20 -0
- package/agents/worker.md +2 -2
- package/dist/extension/build-info.js +1 -1
- package/dist/pi-intercom/bridge-config.js +1 -1
- package/dist/pi-intercom/bridge-mailbox.js +3 -55
- package/dist/pi-intercom/bridge-presence.js +5 -1
- package/dist/pi-intercom/bridge-routes.js +25 -6
- package/dist/pi-intercom/bridge-session.js +60 -45
- package/dist/pi-intercom/bridge-storage.js +73 -0
- package/dist/pi-intercom/bridge-wake-owner.js +57 -0
- package/dist/pi-intercom/bridge-wake-state.js +293 -0
- package/dist/pi-intercom/bridge-wake.js +190 -99
- package/dist/pi-intercom/bridge.js +10 -2
- package/docs/intercom-bridge.md +56 -25
- package/package.json +1 -1
- package/scripts/intercom-bridge-wake-preflight.mjs +112 -0
- package/scripts/intercom-remote.mjs +39 -5
- package/src/pi-intercom/bridge-config.ts +1 -1
- package/src/pi-intercom/bridge-mailbox.ts +3 -59
- package/src/pi-intercom/bridge-presence.ts +7 -1
- package/src/pi-intercom/bridge-routes.ts +38 -8
- package/src/pi-intercom/bridge-session.ts +64 -61
- package/src/pi-intercom/bridge-storage.ts +83 -0
- package/src/pi-intercom/bridge-wake-owner.ts +58 -0
- package/src/pi-intercom/bridge-wake-state.ts +321 -0
- package/src/pi-intercom/bridge-wake.ts +224 -130
- package/src/pi-intercom/bridge.ts +11 -2
|
@@ -9,9 +9,20 @@ import { transcriptOptions, type TranscriptOptions } from "./transcript.ts";
|
|
|
9
9
|
|
|
10
10
|
export type MessagingOperation =
|
|
11
11
|
| { readonly route: "GET /v1/list" | "POST /v1/register" }
|
|
12
|
-
| {
|
|
12
|
+
| {
|
|
13
|
+
readonly route: "GET /v1/inbox";
|
|
14
|
+
readonly after: number;
|
|
15
|
+
readonly limit: number;
|
|
16
|
+
readonly waitMs: number;
|
|
17
|
+
}
|
|
13
18
|
| { readonly route: "POST /v1/ack"; readonly ids: readonly string[] }
|
|
14
|
-
| {
|
|
19
|
+
| {
|
|
20
|
+
readonly route: "POST /v1/send";
|
|
21
|
+
readonly to: string;
|
|
22
|
+
readonly message: string;
|
|
23
|
+
readonly passive?: boolean;
|
|
24
|
+
readonly messageId?: string;
|
|
25
|
+
}
|
|
15
26
|
| {
|
|
16
27
|
readonly route: "POST /v1/ask";
|
|
17
28
|
readonly to: string;
|
|
@@ -73,7 +84,7 @@ const fields = {
|
|
|
73
84
|
"GET /v1/inbox": [],
|
|
74
85
|
"POST /v1/register": [],
|
|
75
86
|
"POST /v1/ack": ["ids"],
|
|
76
|
-
"POST /v1/send": ["to", "message"],
|
|
87
|
+
"POST /v1/send": ["to", "message", "passive", "messageId"],
|
|
77
88
|
"POST /v1/ask": ["to", "message", "timeoutMs"],
|
|
78
89
|
"POST /v1/reply": ["replyTo", "message"],
|
|
79
90
|
"POST /v1/start": ["name"],
|
|
@@ -134,22 +145,29 @@ function queryFor(req: IncomingMessage, allowed: readonly string[]): URLSearchPa
|
|
|
134
145
|
return query;
|
|
135
146
|
}
|
|
136
147
|
function inboxQuery(req: IncomingMessage): Extract<Operation, { readonly route: "GET /v1/inbox" }> {
|
|
137
|
-
const query = queryFor(req, ["after", "limit"]);
|
|
148
|
+
const query = queryFor(req, ["after", "limit", "waitMs"]);
|
|
138
149
|
const after = query.get("after") ?? "0";
|
|
139
150
|
const limit = query.get("limit") ?? String(INBOX_COUNT);
|
|
151
|
+
const waitMs = query.get("waitMs") ?? "0";
|
|
140
152
|
if (
|
|
141
153
|
!/^(0|[1-9]\d{0,15})$/.test(after) ||
|
|
142
154
|
!Number.isSafeInteger(Number(after)) ||
|
|
143
155
|
!/^[1-9]\d{0,2}$/.test(limit) ||
|
|
144
|
-
Number(limit) > INBOX_COUNT
|
|
156
|
+
Number(limit) > INBOX_COUNT ||
|
|
157
|
+
!/^(0|[1-9]\d{0,3}|10000)$/.test(waitMs)
|
|
145
158
|
) {
|
|
146
159
|
throw new Fault(
|
|
147
160
|
400,
|
|
148
161
|
"invalid_input",
|
|
149
|
-
"after must be a nonnegative safe integer cursor; limit must be 1..256.",
|
|
162
|
+
"after must be a nonnegative safe integer cursor; limit must be 1..256; waitMs must be 0..10000.",
|
|
150
163
|
);
|
|
151
164
|
}
|
|
152
|
-
return {
|
|
165
|
+
return {
|
|
166
|
+
route: "GET /v1/inbox",
|
|
167
|
+
after: Number(after),
|
|
168
|
+
limit: Number(limit),
|
|
169
|
+
waitMs: Number(waitMs),
|
|
170
|
+
};
|
|
153
171
|
}
|
|
154
172
|
function pageNumber(value: string | null, fallback: number, min: number, max: number): number {
|
|
155
173
|
const number = value === null ? fallback : Number(value);
|
|
@@ -288,7 +306,7 @@ function messagingOperation(
|
|
|
288
306
|
}
|
|
289
307
|
return { route, ids: [...new Set(input.ids.map((id) => label(id, "message ID")))] };
|
|
290
308
|
case "POST /v1/send":
|
|
291
|
-
return
|
|
309
|
+
return send(input);
|
|
292
310
|
case "POST /v1/ask":
|
|
293
311
|
return {
|
|
294
312
|
route,
|
|
@@ -305,6 +323,18 @@ function messagingOperation(
|
|
|
305
323
|
return nativeControl(route, input);
|
|
306
324
|
}
|
|
307
325
|
}
|
|
326
|
+
function send(input: Readonly<Record<string, unknown>>): MessagingOperation {
|
|
327
|
+
if (input.passive !== undefined && typeof input.passive !== "boolean") {
|
|
328
|
+
throw new Fault(400, "invalid_input", "passive must be a boolean.");
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
route: "POST /v1/send",
|
|
332
|
+
message: text(input.message),
|
|
333
|
+
to: label(input.to, "to").trim(),
|
|
334
|
+
passive: input.passive,
|
|
335
|
+
messageId: input.messageId === undefined ? undefined : label(input.messageId, "message ID"),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
308
338
|
export async function operationFor(
|
|
309
339
|
req: IncomingMessage,
|
|
310
340
|
route: Route,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
3
|
import { IntercomClient } from "./broker/client.ts";
|
|
3
4
|
import { resolveSessionProjectId } from "./session-targets.ts";
|
|
4
5
|
import type { Message, SendResult, SessionInfo } from "./types.ts";
|
|
@@ -18,14 +19,13 @@ const IDLE_TIMEOUT = 5 * 60 * 1000;
|
|
|
18
19
|
const SEND_TIMEOUT = 8000;
|
|
19
20
|
const LIST_TIMEOUT = 5000;
|
|
20
21
|
|
|
21
|
-
interface SendOptions {
|
|
22
|
+
interface SendOptions extends Pick<Message, "replyTo" | "expectsReply" | "delivery"> {
|
|
22
23
|
readonly messageId?: string;
|
|
23
|
-
readonly replyTo?: string;
|
|
24
|
-
readonly expectsReply?: boolean;
|
|
25
24
|
}
|
|
26
25
|
export interface SessionOwner {
|
|
27
26
|
readonly stateDir?: string;
|
|
28
27
|
readonly notifications?: boolean;
|
|
28
|
+
readonly mailboxChanged?: () => void;
|
|
29
29
|
readonly allowed: (identity: Identity) => boolean;
|
|
30
30
|
readonly lost: (fp: string, count?: number) => void;
|
|
31
31
|
readonly retired: (fp: string, generation: string) => void;
|
|
@@ -43,6 +43,7 @@ export class Session {
|
|
|
43
43
|
});
|
|
44
44
|
private readonly requests = new Set<AbortController>();
|
|
45
45
|
private readonly inbox: Mailbox;
|
|
46
|
+
private inboxChanged = new AbortController();
|
|
46
47
|
private readonly replying = new Set<string>();
|
|
47
48
|
private pending?: WaitingAsk;
|
|
48
49
|
private readonly controls = new ControlReplies();
|
|
@@ -76,9 +77,7 @@ export class Session {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
guard(signal?: AbortSignal): void {
|
|
79
|
-
|
|
80
|
-
cancelled(signal);
|
|
81
|
-
}
|
|
80
|
+
signal?.throwIfAborted();
|
|
82
81
|
if (this.closed) {
|
|
83
82
|
throw this.closeReason;
|
|
84
83
|
}
|
|
@@ -267,12 +266,22 @@ export class Session {
|
|
|
267
266
|
);
|
|
268
267
|
return;
|
|
269
268
|
}
|
|
270
|
-
if (result
|
|
271
|
-
|
|
269
|
+
if (result !== "duplicate" || !this.inbox.persistent) {
|
|
270
|
+
if (result !== "stored") {
|
|
271
|
+
this.owner.lost(this.identity.fingerprint256);
|
|
272
|
+
}
|
|
273
|
+
this.inboxChanged.abort();
|
|
274
|
+
this.inboxChanged = new AbortController();
|
|
272
275
|
}
|
|
273
|
-
|
|
276
|
+
log(result === "lost" ? "inbox_overflow" : "received", {
|
|
277
|
+
...this.fields(),
|
|
278
|
+
peer: from.id,
|
|
279
|
+
messageId: message.id,
|
|
280
|
+
result: result === "lost" && this.inbox.persistent ? "storage_failed" : result,
|
|
281
|
+
});
|
|
274
282
|
if (result !== "lost") {
|
|
275
283
|
this.resolveReply(from, message);
|
|
284
|
+
this.owner.mailboxChanged?.();
|
|
276
285
|
}
|
|
277
286
|
this.presence();
|
|
278
287
|
}
|
|
@@ -284,18 +293,6 @@ export class Session {
|
|
|
284
293
|
pending.resolve({ from, message });
|
|
285
294
|
return true;
|
|
286
295
|
}
|
|
287
|
-
private recordReceive(
|
|
288
|
-
peer: string,
|
|
289
|
-
messageId: string,
|
|
290
|
-
result: "stored" | "duplicate" | "lost",
|
|
291
|
-
): void {
|
|
292
|
-
log(result === "lost" ? "inbox_overflow" : "received", {
|
|
293
|
-
...this.fields(),
|
|
294
|
-
peer,
|
|
295
|
-
messageId,
|
|
296
|
-
result: result === "lost" && this.inbox.persistent ? "storage_failed" : result,
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
296
|
private presence(): void {
|
|
300
297
|
if (this.closed) {
|
|
301
298
|
return;
|
|
@@ -393,22 +390,6 @@ export class Session {
|
|
|
393
390
|
const waiting = new WaitingAsk();
|
|
394
391
|
this.pending = waiting;
|
|
395
392
|
const { signal } = context;
|
|
396
|
-
const abort = () => {
|
|
397
|
-
try {
|
|
398
|
-
cancelled(signal);
|
|
399
|
-
} catch (error) {
|
|
400
|
-
waiting.reject(
|
|
401
|
-
error instanceof Error
|
|
402
|
-
? error
|
|
403
|
-
: new Fault(
|
|
404
|
-
499,
|
|
405
|
-
"client_aborted",
|
|
406
|
-
"HTTP client disconnected; delivery may be unknown.",
|
|
407
|
-
),
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
412
393
|
try {
|
|
413
394
|
await this.connect(signal);
|
|
414
395
|
const peer = await this.target(operation.to, signal);
|
|
@@ -424,16 +405,39 @@ export class Session {
|
|
|
424
405
|
return { ...waiting.receipt, replied: true, reply: answer };
|
|
425
406
|
} finally {
|
|
426
407
|
waiting.clear();
|
|
427
|
-
|
|
428
|
-
if (this.pending === waiting) {
|
|
429
|
-
this.pending = undefined;
|
|
430
|
-
}
|
|
408
|
+
this.pending = undefined;
|
|
431
409
|
}
|
|
432
410
|
}
|
|
433
|
-
private
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
411
|
+
private async readInbox(
|
|
412
|
+
operation: Extract<Operation, { readonly route: "GET /v1/inbox" }>,
|
|
413
|
+
context: RequestContext,
|
|
414
|
+
): Promise<UnknownRecord> {
|
|
415
|
+
const read = (): ReturnType<Mailbox["page"]> & UnknownRecord => ({
|
|
416
|
+
...this.inbox.page(operation.after, operation.limit),
|
|
417
|
+
...this.inbox.status(this.owner.losses(this.identity.fingerprint256)),
|
|
418
|
+
});
|
|
419
|
+
const deadline = performance.now() + operation.waitMs;
|
|
420
|
+
const losses = read().lostMessages;
|
|
421
|
+
for (;;) {
|
|
422
|
+
// Subscribe to the post-commit signal before rereading the authoritative cursor page.
|
|
423
|
+
const signal = AbortSignal.any([this.inboxChanged.signal, context.signal]);
|
|
424
|
+
const page = read();
|
|
425
|
+
const remaining = deadline - performance.now();
|
|
426
|
+
if (page.messages.length > 0 || page.lostMessages !== losses || remaining <= 0) {
|
|
427
|
+
return page;
|
|
428
|
+
}
|
|
429
|
+
log("inbox_wait", { ...this.fields(), requestId: context.requestId, after: operation.after });
|
|
430
|
+
try {
|
|
431
|
+
// A nonmatching commit must rearm the same cursor, not start parallel readers.
|
|
432
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
433
|
+
await delay(remaining, undefined, { signal });
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (!signal.aborted) {
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
this.guard(context.signal);
|
|
440
|
+
}
|
|
437
441
|
}
|
|
438
442
|
private async answer(
|
|
439
443
|
replyTo: string,
|
|
@@ -461,6 +465,7 @@ export class Session {
|
|
|
461
465
|
}
|
|
462
466
|
const data = { ...(await this.send(peer, message, context, { replyTo })) };
|
|
463
467
|
this.inbox.remove(replyTo);
|
|
468
|
+
this.owner.mailboxChanged?.();
|
|
464
469
|
this.presence();
|
|
465
470
|
return data;
|
|
466
471
|
} finally {
|
|
@@ -478,32 +483,30 @@ export class Session {
|
|
|
478
483
|
await this.connect(context.signal);
|
|
479
484
|
}
|
|
480
485
|
if (operation.route === "POST /v1/ack") {
|
|
481
|
-
|
|
486
|
+
const result = this.inbox.ack(operation.ids);
|
|
487
|
+
this.owner.mailboxChanged?.();
|
|
488
|
+
this.presence();
|
|
489
|
+
return result;
|
|
482
490
|
}
|
|
483
|
-
return
|
|
484
|
-
...this.inbox.page(operation.after, operation.limit),
|
|
485
|
-
...this.inbox.status(this.owner.losses(this.identity.fingerprint256)),
|
|
486
|
-
};
|
|
491
|
+
return this.readInbox(operation, context);
|
|
487
492
|
}
|
|
488
493
|
await this.connect(context.signal);
|
|
489
494
|
switch (operation.route) {
|
|
490
495
|
case "POST /v1/register":
|
|
491
496
|
return { sessionId: this.client.sessionId, name: this.identity.name };
|
|
492
|
-
case "GET /v1/list":
|
|
493
|
-
const sessions = await this.peers(context.signal);
|
|
497
|
+
case "GET /v1/list":
|
|
494
498
|
return {
|
|
495
499
|
sessionId: this.client.sessionId,
|
|
496
|
-
sessions: bridgeSessionList(
|
|
500
|
+
sessions: bridgeSessionList(await this.peers(context.signal)),
|
|
497
501
|
};
|
|
502
|
+
case "POST /v1/send": {
|
|
503
|
+
const peer = await this.target(operation.to, context.signal);
|
|
504
|
+
const receipt = await this.send(peer.id, operation.message, context, {
|
|
505
|
+
messageId: operation.messageId,
|
|
506
|
+
delivery: operation.passive === true ? "passive" : "steer",
|
|
507
|
+
});
|
|
508
|
+
return { ...receipt };
|
|
498
509
|
}
|
|
499
|
-
case "POST /v1/send":
|
|
500
|
-
return {
|
|
501
|
-
...(await this.send(
|
|
502
|
-
(await this.target(operation.to, context.signal)).id,
|
|
503
|
-
operation.message,
|
|
504
|
-
context,
|
|
505
|
-
)),
|
|
506
|
-
};
|
|
507
510
|
case "POST /v1/reply":
|
|
508
511
|
return this.answer(operation.replyTo, operation.message, context);
|
|
509
512
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, realpathSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import { isRecord } from "../shared/unknown.ts";
|
|
5
|
+
import { label } from "./bridge-protocol.ts";
|
|
6
|
+
|
|
7
|
+
export function stateDirectory(value: unknown): string | undefined {
|
|
8
|
+
if (value === undefined) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const path = label(value, "stateDir", 4096);
|
|
12
|
+
const stat = lstatSync(path);
|
|
13
|
+
if (
|
|
14
|
+
!isAbsolute(path) ||
|
|
15
|
+
!stat.isDirectory() ||
|
|
16
|
+
stat.uid !== process.getuid?.() ||
|
|
17
|
+
(stat.mode & 0o777) !== 0o700
|
|
18
|
+
) {
|
|
19
|
+
throw new Error("stateDir must be an absolute owned directory with mode 0700, not a symlink.");
|
|
20
|
+
}
|
|
21
|
+
return realpathSync(path);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function privateFile(path: string): void {
|
|
25
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
26
|
+
try {
|
|
27
|
+
const stat = fstatSync(fd);
|
|
28
|
+
if (
|
|
29
|
+
!stat.isFile() ||
|
|
30
|
+
stat.uid !== process.getuid?.() ||
|
|
31
|
+
(stat.mode & 0o777) !== 0o600 ||
|
|
32
|
+
stat.nlink !== 1
|
|
33
|
+
) {
|
|
34
|
+
throw new Error("Storage files must be owned regular files with mode 0600 and one link.");
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
closeSync(fd);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Native SQLite FULL commits own durability; never replace an unreadable existing database. */
|
|
42
|
+
export function privateDatabase(
|
|
43
|
+
stateDir: string,
|
|
44
|
+
name: string,
|
|
45
|
+
): { readonly db: DatabaseSync; readonly created: boolean } {
|
|
46
|
+
stateDirectory(stateDir);
|
|
47
|
+
const path = join(stateDir, name);
|
|
48
|
+
let created = false;
|
|
49
|
+
try {
|
|
50
|
+
const fd = openSync(
|
|
51
|
+
path,
|
|
52
|
+
constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
|
|
53
|
+
0o600,
|
|
54
|
+
);
|
|
55
|
+
closeSync(fd);
|
|
56
|
+
created = true;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (!isRecord(error) || error.code !== "EEXIST") {
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
privateFile(path);
|
|
63
|
+
for (const suffix of ["-journal", "-wal", "-shm"]) {
|
|
64
|
+
try {
|
|
65
|
+
lstatSync(`${path}${suffix}`);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (isRecord(error) && error.code === "ENOENT") {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
privateFile(`${path}${suffix}`);
|
|
73
|
+
}
|
|
74
|
+
const db = new DatabaseSync(path, { enableDoubleQuotedStringLiterals: false });
|
|
75
|
+
try {
|
|
76
|
+
db.exec(`PRAGMA journal_mode=DELETE; PRAGMA synchronous=FULL; PRAGMA secure_delete=ON;
|
|
77
|
+
PRAGMA cache_size=-1024; PRAGMA busy_timeout=1000;`);
|
|
78
|
+
return { db, created };
|
|
79
|
+
} catch (error) {
|
|
80
|
+
db.close();
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { privateDatabase, privateFile } from "./bridge-storage.ts";
|
|
6
|
+
import { isRecord } from "../shared/unknown.ts";
|
|
7
|
+
|
|
8
|
+
export const WAKE_OWNER_FILE = /^wake-owner-[0-9a-f-]{36}\.lock$/;
|
|
9
|
+
|
|
10
|
+
/** SQLite's OS lock survives pauses and is released on exit, without PID reuse or stale leases. */
|
|
11
|
+
export class WakeOwner {
|
|
12
|
+
readonly name = `wake-owner-${randomUUID()}.lock`;
|
|
13
|
+
private readonly db: DatabaseSync;
|
|
14
|
+
private readonly path: string;
|
|
15
|
+
|
|
16
|
+
constructor(stateDir: string) {
|
|
17
|
+
this.path = join(stateDir, this.name);
|
|
18
|
+
this.db = privateDatabase(stateDir, this.name).db;
|
|
19
|
+
try {
|
|
20
|
+
this.db.exec("BEGIN EXCLUSIVE");
|
|
21
|
+
} catch (error) {
|
|
22
|
+
this.db.close();
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
close(): void {
|
|
28
|
+
this.db.close();
|
|
29
|
+
rmSync(this.path, { force: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function stoppedWakeOwner(stateDir: string, name: string): boolean {
|
|
34
|
+
if (!WAKE_OWNER_FILE.test(name)) {
|
|
35
|
+
throw new Error("Invalid wake owner.");
|
|
36
|
+
}
|
|
37
|
+
const path = join(stateDir, name);
|
|
38
|
+
try {
|
|
39
|
+
privateFile(path);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (isRecord(error) && error.code === "ENOENT") {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
const db = new DatabaseSync(path);
|
|
47
|
+
try {
|
|
48
|
+
db.exec("PRAGMA busy_timeout=0; BEGIN EXCLUSIVE");
|
|
49
|
+
return true;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (isRecord(error) && error.errcode === 5) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
} finally {
|
|
56
|
+
db.close();
|
|
57
|
+
}
|
|
58
|
+
}
|