@parlor/agent-host 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +16 -2
- package/dist/acp.d.ts +165 -0
- package/dist/acp.d.ts.map +1 -0
- package/dist/acp.js +626 -0
- package/dist/acp.js.map +1 -0
- package/dist/adapter-launch.d.ts +7 -0
- package/dist/adapter-launch.d.ts.map +1 -0
- package/dist/adapter-launch.js +35 -0
- package/dist/adapter-launch.js.map +1 -0
- package/dist/agents.d.ts +32 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +132 -0
- package/dist/agents.js.map +1 -0
- package/dist/assertion.d.ts +9 -0
- package/dist/assertion.d.ts.map +1 -0
- package/dist/assertion.js +22 -0
- package/dist/assertion.js.map +1 -0
- package/dist/control.d.ts +106 -0
- package/dist/control.d.ts.map +1 -0
- package/dist/control.js +32 -0
- package/dist/control.js.map +1 -0
- package/dist/daemon.d.ts +50 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +543 -0
- package/dist/daemon.js.map +1 -0
- package/dist/dev-host.d.ts +5 -0
- package/dist/dev-host.d.ts.map +1 -0
- package/dist/dev-host.js +29 -0
- package/dist/dev-host.js.map +1 -0
- package/dist/device-list.d.ts +6 -0
- package/dist/device-list.d.ts.map +1 -0
- package/dist/device-list.js +30 -0
- package/dist/device-list.js.map +1 -0
- package/dist/embedded.d.ts +6 -0
- package/dist/embedded.d.ts.map +1 -0
- package/dist/embedded.js +3 -0
- package/dist/embedded.js.map +1 -0
- package/dist/fold.d.ts +38 -0
- package/dist/fold.d.ts.map +1 -0
- package/dist/fold.js +165 -0
- package/dist/fold.js.map +1 -0
- package/dist/grants.d.ts +54 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +157 -0
- package/dist/grants.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/json-file.d.ts +6 -0
- package/dist/json-file.d.ts.map +1 -0
- package/dist/json-file.js +35 -0
- package/dist/json-file.js.map +1 -0
- package/dist/lines.d.ts +5 -0
- package/dist/lines.d.ts.map +1 -0
- package/dist/lines.js +9 -0
- package/dist/lines.js.map +1 -0
- package/dist/link-device.d.ts +28 -0
- package/dist/link-device.d.ts.map +1 -0
- package/dist/link-device.js +40 -0
- package/dist/link-device.js.map +1 -0
- package/dist/link.d.ts +52 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +277 -0
- package/dist/link.js.map +1 -0
- package/dist/lock.d.ts +7 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +105 -0
- package/dist/lock.js.map +1 -0
- package/dist/log.d.ts +3 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +18 -0
- package/dist/log.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +520 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp-proxy.d.ts +33 -0
- package/dist/mcp-proxy.d.ts.map +1 -0
- package/dist/mcp-proxy.js +317 -0
- package/dist/mcp-proxy.js.map +1 -0
- package/dist/naming.d.ts +7 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/naming.js +42 -0
- package/dist/naming.js.map +1 -0
- package/dist/pairing.d.ts +68 -0
- package/dist/pairing.d.ts.map +1 -0
- package/dist/pairing.js +312 -0
- package/dist/pairing.js.map +1 -0
- package/dist/paths.d.ts +19 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +23 -0
- package/dist/paths.js.map +1 -0
- package/dist/policy.d.ts +37 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +60 -0
- package/dist/policy.js.map +1 -0
- package/dist/pool.d.ts +34 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +112 -0
- package/dist/pool.js.map +1 -0
- package/dist/prompter.d.ts +33 -0
- package/dist/prompter.d.ts.map +1 -0
- package/dist/prompter.js +98 -0
- package/dist/prompter.js.map +1 -0
- package/dist/release-main.d.ts +2 -0
- package/dist/release-main.d.ts.map +1 -0
- package/dist/release-main.js +50 -0
- package/dist/release-main.js.map +1 -0
- package/dist/release.d.ts +22 -0
- package/dist/release.d.ts.map +1 -0
- package/dist/release.js +81 -0
- package/dist/release.js.map +1 -0
- package/dist/revoke.d.ts +10 -0
- package/dist/revoke.d.ts.map +1 -0
- package/dist/revoke.js +23 -0
- package/dist/revoke.js.map +1 -0
- package/dist/self.d.ts +7 -0
- package/dist/self.d.ts.map +1 -0
- package/dist/self.js +10 -0
- package/dist/self.js.map +1 -0
- package/dist/service.d.ts +97 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +205 -0
- package/dist/service.js.map +1 -0
- package/dist/shell-path.d.ts +10 -0
- package/dist/shell-path.d.ts.map +1 -0
- package/dist/shell-path.js +64 -0
- package/dist/shell-path.js.map +1 -0
- package/dist/skeleton-kit.d.ts +8 -0
- package/dist/skeleton-kit.d.ts.map +1 -0
- package/dist/skeleton-kit.js +143 -0
- package/dist/skeleton-kit.js.map +1 -0
- package/dist/spec-kit.d.ts +102 -0
- package/dist/spec-kit.d.ts.map +1 -0
- package/dist/spec-kit.js +372 -0
- package/dist/spec-kit.js.map +1 -0
- package/dist/testing/fake-agent.d.ts +65 -0
- package/dist/testing/fake-agent.d.ts.map +1 -0
- package/dist/testing/fake-agent.js +238 -0
- package/dist/testing/fake-agent.js.map +1 -0
- package/dist/trust.d.ts +20 -0
- package/dist/trust.d.ts.map +1 -0
- package/dist/trust.js +27 -0
- package/dist/trust.js.map +1 -0
- package/dist/turn.d.ts +32 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +650 -0
- package/dist/turn.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/package.json +57 -3
package/dist/link.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { CHAT_ACTIVITY_TAG, CHAT_TAG, HOST_PROTOCOL, } from "@parlor/chat";
|
|
2
|
+
import { MAX_STREAM_BODY_BYTES } from "@parlor/chat/server";
|
|
3
|
+
import { createActionClient, createPluginClient, } from "@parlor/client";
|
|
4
|
+
import { createSyncClient } from "@parlor/sync/client";
|
|
5
|
+
import { logEvent } from "./log.js";
|
|
6
|
+
import { revokeGrant } from "./revoke.js";
|
|
7
|
+
export const RECONNECT_MAX_MS = 4_000;
|
|
8
|
+
export const ACTIVE_POLL_MS = 2_000;
|
|
9
|
+
export const IDLE_POLL_MS = 15_000;
|
|
10
|
+
const USER_ACTIVE_WINDOW_MS = 2 * 60_000;
|
|
11
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
12
|
+
const RETRY_BASE_MS = 250;
|
|
13
|
+
const MAX_BATCH_BYTES = (MAX_STREAM_BODY_BYTES * 3) / 4;
|
|
14
|
+
class LinkDown extends Error {
|
|
15
|
+
name = "LinkDown";
|
|
16
|
+
}
|
|
17
|
+
function coalesce(frames) {
|
|
18
|
+
const merged = [];
|
|
19
|
+
for (const frame of frames) {
|
|
20
|
+
const last = merged.at(-1);
|
|
21
|
+
if (last?.type === "delta" &&
|
|
22
|
+
frame.type === "delta" &&
|
|
23
|
+
last.itemId === frame.itemId &&
|
|
24
|
+
last.offset + last.text.length === frame.offset) {
|
|
25
|
+
merged[merged.length - 1] = { ...last, text: last.text + frame.text };
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
merged.push(frame);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return merged;
|
|
32
|
+
}
|
|
33
|
+
function batches(frames) {
|
|
34
|
+
const out = [];
|
|
35
|
+
let current = [];
|
|
36
|
+
let bytes = 0;
|
|
37
|
+
for (const frame of frames) {
|
|
38
|
+
const size = JSON.stringify(frame).length;
|
|
39
|
+
if (current.length > 0 && bytes + size > MAX_BATCH_BYTES) {
|
|
40
|
+
out.push(current);
|
|
41
|
+
current = [];
|
|
42
|
+
bytes = 0;
|
|
43
|
+
}
|
|
44
|
+
current.push(frame);
|
|
45
|
+
bytes += size;
|
|
46
|
+
}
|
|
47
|
+
if (current.length > 0)
|
|
48
|
+
out.push(current);
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
52
|
+
class RequestTimeout extends Error {
|
|
53
|
+
name = "RequestTimeout";
|
|
54
|
+
constructor(ms) {
|
|
55
|
+
super(`The app did not answer within ${ms} ms.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function untilHeaders(inner, ms) {
|
|
59
|
+
return async (input, init) => {
|
|
60
|
+
const own = new AbortController();
|
|
61
|
+
const timer = setTimeout(() => own.abort(new RequestTimeout(ms)), ms);
|
|
62
|
+
const signal = init?.signal ? AbortSignal.any([init.signal, own.signal]) : own.signal;
|
|
63
|
+
try {
|
|
64
|
+
return await inner(input, { ...init, signal });
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function adaptivePoll(advertised, busy) {
|
|
72
|
+
let activeUntil = 0;
|
|
73
|
+
const idle = () => advertised && !busy() && Date.now() >= activeUntil;
|
|
74
|
+
return {
|
|
75
|
+
pollEveryMs: () => (idle() ? IDLE_POLL_MS : ACTIVE_POLL_MS),
|
|
76
|
+
touch: () => {
|
|
77
|
+
const wasIdle = idle();
|
|
78
|
+
activeUntil = Date.now() + USER_ACTIVE_WINDOW_MS;
|
|
79
|
+
return wasIdle;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function createAppLink(grant, deps) {
|
|
84
|
+
const origin = grant.appOrigin.replace(/\/+$/, "");
|
|
85
|
+
let welcome;
|
|
86
|
+
let mode = "idle";
|
|
87
|
+
const modeNow = () => mode;
|
|
88
|
+
let revoked = false;
|
|
89
|
+
let sync;
|
|
90
|
+
let heartbeat;
|
|
91
|
+
const streams = new Map();
|
|
92
|
+
let admit = () => undefined;
|
|
93
|
+
let refuse = () => undefined;
|
|
94
|
+
const welcomed = new Promise((resolve, reject) => {
|
|
95
|
+
admit = resolve;
|
|
96
|
+
refuse = reject;
|
|
97
|
+
});
|
|
98
|
+
welcomed.catch(() => undefined);
|
|
99
|
+
const signedFetch = async (input, init) => {
|
|
100
|
+
if (mode === "closed")
|
|
101
|
+
throw new LinkDown("This app's link is closed.");
|
|
102
|
+
const request = new Request(input, init);
|
|
103
|
+
const method = request.method === "GET" ? "GET" : "POST";
|
|
104
|
+
const assertion = await deps.keys.assert(origin, { method, url: request.url });
|
|
105
|
+
const headers = new Headers(request.headers);
|
|
106
|
+
headers.set("authorization", `ParlorHost ${assertion}`);
|
|
107
|
+
const response = await deps.fetch(new Request(request, { headers }));
|
|
108
|
+
logEvent("http_request", {
|
|
109
|
+
method,
|
|
110
|
+
path: new URL(request.url).pathname,
|
|
111
|
+
status: response.status,
|
|
112
|
+
});
|
|
113
|
+
if (response.status === 401 && !revoked && modeNow() !== "closed") {
|
|
114
|
+
revoked = true;
|
|
115
|
+
queueMicrotask(() => deps.onRevoked(grant));
|
|
116
|
+
}
|
|
117
|
+
return response;
|
|
118
|
+
};
|
|
119
|
+
const timedFetch = untilHeaders(signedFetch, REQUEST_TIMEOUT_MS);
|
|
120
|
+
const plugin = createPluginClient(createActionClient({ baseUrl: origin, fetch: timedFetch }), "chat");
|
|
121
|
+
const drained = async (turnId) => {
|
|
122
|
+
if (turnId === undefined)
|
|
123
|
+
return;
|
|
124
|
+
while (true) {
|
|
125
|
+
const posting = streams.get(turnId)?.posting;
|
|
126
|
+
if (!posting)
|
|
127
|
+
return;
|
|
128
|
+
await posting;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const invoke = async (name, input, options) => {
|
|
132
|
+
if (name !== "hostHello")
|
|
133
|
+
await welcomed;
|
|
134
|
+
if (name === "hostAppend" || name === "hostFinish") {
|
|
135
|
+
await drained(input.turnId);
|
|
136
|
+
}
|
|
137
|
+
return plugin.invoke(name, input, options);
|
|
138
|
+
};
|
|
139
|
+
const calls = {
|
|
140
|
+
invoke,
|
|
141
|
+
call: async (name, input, options) => (await invoke(name, input, options)).data,
|
|
142
|
+
};
|
|
143
|
+
const post = (path, body) => timedFetch(`${origin}/_parlor/chat/${path}`, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { "content-type": "application/json" },
|
|
146
|
+
body: JSON.stringify(body),
|
|
147
|
+
});
|
|
148
|
+
const flush = async (turnId, stream) => {
|
|
149
|
+
while (stream.pending.length > 0) {
|
|
150
|
+
if (mode === "closed")
|
|
151
|
+
break;
|
|
152
|
+
const frames = coalesce(stream.pending);
|
|
153
|
+
stream.pending = [];
|
|
154
|
+
for (const chunk of batches(frames)) {
|
|
155
|
+
await post("stream", { turnId, frames: chunk }).catch(() => undefined);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
stream.posting = undefined;
|
|
159
|
+
if (stream.pending.length === 0)
|
|
160
|
+
streams.delete(turnId);
|
|
161
|
+
};
|
|
162
|
+
const api = {
|
|
163
|
+
calls,
|
|
164
|
+
stream(batch) {
|
|
165
|
+
if (mode === "closed" || batch.frames.length === 0)
|
|
166
|
+
return;
|
|
167
|
+
let stream = streams.get(batch.turnId);
|
|
168
|
+
if (!stream) {
|
|
169
|
+
stream = { pending: [], posting: undefined };
|
|
170
|
+
streams.set(batch.turnId, stream);
|
|
171
|
+
}
|
|
172
|
+
stream.pending.push(...batch.frames);
|
|
173
|
+
if (!stream.posting)
|
|
174
|
+
stream.posting = flush(batch.turnId, stream);
|
|
175
|
+
},
|
|
176
|
+
async heartbeat() {
|
|
177
|
+
await welcomed;
|
|
178
|
+
await post("heartbeat", {});
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
const close = () => {
|
|
182
|
+
mode = "closed";
|
|
183
|
+
sync?.stop();
|
|
184
|
+
sync = undefined;
|
|
185
|
+
if (heartbeat)
|
|
186
|
+
clearInterval(heartbeat);
|
|
187
|
+
heartbeat = undefined;
|
|
188
|
+
refuse(new LinkDown("This app's link is closed."));
|
|
189
|
+
};
|
|
190
|
+
const hello = async () => {
|
|
191
|
+
for (let attempt = 0;; attempt += 1) {
|
|
192
|
+
if (mode !== "serving" && mode !== "draining")
|
|
193
|
+
return undefined;
|
|
194
|
+
try {
|
|
195
|
+
return await plugin.call("hostHello", {
|
|
196
|
+
protocols: { min: 1, max: HOST_PROTOCOL },
|
|
197
|
+
hostVersion: deps.hostVersion,
|
|
198
|
+
name: deps.hostName,
|
|
199
|
+
platform: deps.platform,
|
|
200
|
+
agents: [],
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
if (revoked || modeNow() === "closed")
|
|
205
|
+
return undefined;
|
|
206
|
+
await sleep(Math.min(RETRY_BASE_MS * 2 ** attempt, RECONNECT_MAX_MS));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const link = {
|
|
211
|
+
grant,
|
|
212
|
+
api,
|
|
213
|
+
get protocol() {
|
|
214
|
+
return welcome?.protocol;
|
|
215
|
+
},
|
|
216
|
+
get mode() {
|
|
217
|
+
return mode;
|
|
218
|
+
},
|
|
219
|
+
start() {
|
|
220
|
+
if (mode !== "idle")
|
|
221
|
+
return;
|
|
222
|
+
mode = "serving";
|
|
223
|
+
void hello().then((answer) => {
|
|
224
|
+
if (!answer || modeNow() === "closed")
|
|
225
|
+
return;
|
|
226
|
+
if (!answer.accepted) {
|
|
227
|
+
close();
|
|
228
|
+
logEvent("link_closed", { appOrigin: origin, reason: "incompatible" });
|
|
229
|
+
deps.onIncompatible(grant, answer);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
welcome = answer;
|
|
233
|
+
admit();
|
|
234
|
+
const polling = adaptivePoll(answer.userActivity === true, () => deps.busy?.() ?? false);
|
|
235
|
+
sync = createSyncClient({
|
|
236
|
+
request: (path, init) => signedFetch(`${origin}${path}`, init),
|
|
237
|
+
coalesceMs: 0,
|
|
238
|
+
reconnectMaxMs: RECONNECT_MAX_MS,
|
|
239
|
+
pollEveryMs: polling.pollEveryMs,
|
|
240
|
+
onEvents: (events) => {
|
|
241
|
+
const heads = events.flatMap((event) => event.tags.map((tag) => tag[0]));
|
|
242
|
+
if (heads.some((head) => head === CHAT_TAG || head === CHAT_ACTIVITY_TAG)) {
|
|
243
|
+
if (polling.touch())
|
|
244
|
+
sync?.reconnectNow();
|
|
245
|
+
}
|
|
246
|
+
if (modeNow() === "serving" && heads.includes(CHAT_TAG))
|
|
247
|
+
deps.onWake(link);
|
|
248
|
+
},
|
|
249
|
+
onUnauthorized: () => undefined,
|
|
250
|
+
onError: () => undefined,
|
|
251
|
+
});
|
|
252
|
+
sync.start();
|
|
253
|
+
heartbeat = setInterval(() => void api.heartbeat().catch(() => undefined), answer.heartbeatMs);
|
|
254
|
+
heartbeat.unref();
|
|
255
|
+
if (modeNow() === "serving")
|
|
256
|
+
deps.onWake(link);
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
drain() {
|
|
260
|
+
if (mode === "serving")
|
|
261
|
+
mode = "draining";
|
|
262
|
+
},
|
|
263
|
+
async stop(options = {}) {
|
|
264
|
+
if (options.revoke && !grant.dev) {
|
|
265
|
+
await revokeGrant({
|
|
266
|
+
appOrigin: origin,
|
|
267
|
+
appName: grant.appName,
|
|
268
|
+
assert: (request) => deps.keys.assert(origin, request),
|
|
269
|
+
fetch: deps.fetch,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
close();
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
return link;
|
|
276
|
+
}
|
|
277
|
+
//# sourceMappingURL=link.js.map
|
package/dist/link.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link.js","sourceRoot":"","sources":["../src/link.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n CHAT_ACTIVITY_TAG,\n CHAT_TAG,\n HOST_PROTOCOL,\n type HostFrame,\n type HostStreamBatch,\n type HostWelcome,\n} from \"@parlor/chat\";\nimport { type ChatHostActionMap, MAX_STREAM_BODY_BYTES } from \"@parlor/chat/server\";\nimport {\n createActionClient,\n createPluginClient,\n type PluginClient,\n} from \"@parlor/client\";\nimport type { TurnId } from \"@parlor/core\";\nimport { createSyncClient, type SyncClient } from \"@parlor/sync/client\";\n\nimport type { AppGrant, HostKeys } from \"./grants.js\";\nimport { logEvent } from \"./log.js\";\nimport { revokeGrant } from \"./revoke.js\";\n\nexport const RECONNECT_MAX_MS = 4_000;\nexport const ACTIVE_POLL_MS = 2_000;\nexport const IDLE_POLL_MS = 15_000;\n\nconst USER_ACTIVE_WINDOW_MS = 2 * 60_000;\nconst REQUEST_TIMEOUT_MS = 10_000;\n\nexport interface HostApi {\n readonly calls: PluginClient<ChatHostActionMap>;\n /** At most one post in flight per turn; frames produced meanwhile coalesce into the next. */\n stream(batch: HostStreamBatch): void;\n heartbeat(): Promise<void>;\n}\n\nexport interface AppLinkDeps {\n readonly keys: HostKeys;\n readonly hostVersion: string;\n readonly hostName: string;\n readonly platform: string;\n readonly fetch: typeof fetch;\n /** A turn may be waiting: claim until not_found, then ask for running turns' signals. */\n readonly onWake: (link: AppLink) => void;\n /** Whether a turn runs on this link, which keeps a poll deployment's `/sync` at the active interval. */\n readonly busy?: (() => boolean) | undefined;\n /** Any 401, or the stream closed for revocation. */\n readonly onRevoked: (grant: AppGrant) => void;\n /** No shared protocol: shown locally (\"update Parlor Host\"); the link stays down. */\n readonly onIncompatible: (\n grant: AppGrant,\n welcome: Extract<HostWelcome, { readonly accepted: false }>,\n ) => void;\n}\n\n/**\n * `idle` until started; `serving` wakes the host for turns; `draining` stays up for the\n * turns it runs but wakes nobody (a newer service took over the claiming); `closed` is final.\n */\nexport type LinkMode = \"idle\" | \"serving\" | \"draining\" | \"closed\";\n\n/** The host's whole conversation with one app, each request carrying a fresh assertion. */\nexport interface AppLink {\n readonly grant: AppGrant;\n readonly api: HostApi;\n /** Set once `hostHello` was welcomed; every other call waits for it. */\n readonly protocol: number | undefined;\n readonly mode: LinkMode;\n start(): void;\n /** Stops waking the host and keeps everything else, so running turns finish on this link. */\n drain(): void;\n /** With `revoke`, a refused revoke throws and leaves the link running. */\n stop(options?: { readonly revoke?: boolean | undefined }): Promise<void>;\n}\n\ntype Accepted = Extract<HostWelcome, { readonly accepted: true }>;\n\nconst RETRY_BASE_MS = 250;\nconst MAX_BATCH_BYTES = (MAX_STREAM_BODY_BYTES * 3) / 4;\n\nclass LinkDown extends Error {\n override readonly name = \"LinkDown\";\n}\n\ninterface TurnStream {\n pending: HostFrame[];\n posting: Promise<void> | undefined;\n}\n\nfunction coalesce(frames: readonly HostFrame[]): HostFrame[] {\n const merged: HostFrame[] = [];\n for (const frame of frames) {\n const last = merged.at(-1);\n if (\n last?.type === \"delta\" &&\n frame.type === \"delta\" &&\n last.itemId === frame.itemId &&\n last.offset + last.text.length === frame.offset\n ) {\n merged[merged.length - 1] = { ...last, text: last.text + frame.text };\n } else {\n merged.push(frame);\n }\n }\n return merged;\n}\n\nfunction batches(frames: readonly HostFrame[]): HostFrame[][] {\n const out: HostFrame[][] = [];\n let current: HostFrame[] = [];\n let bytes = 0;\n for (const frame of frames) {\n const size = JSON.stringify(frame).length;\n if (current.length > 0 && bytes + size > MAX_BATCH_BYTES) {\n out.push(current);\n current = [];\n bytes = 0;\n }\n current.push(frame);\n bytes += size;\n }\n if (current.length > 0) out.push(current);\n return out;\n}\n\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nclass RequestTimeout extends Error {\n override readonly name = \"RequestTimeout\";\n constructor(ms: number) {\n super(`The app did not answer within ${ms} ms.`);\n }\n}\n\nfunction untilHeaders(inner: typeof fetch, ms: number): typeof fetch {\n return async (input, init) => {\n const own = new AbortController();\n const timer = setTimeout(() => own.abort(new RequestTimeout(ms)), ms);\n const signal = init?.signal ? AbortSignal.any([init.signal, own.signal]) : own.signal;\n try {\n return await inner(input, { ...init, signal });\n } finally {\n clearTimeout(timer);\n }\n };\n}\n\nfunction adaptivePoll(advertised: boolean, busy: () => boolean) {\n let activeUntil = 0;\n const idle = () => advertised && !busy() && Date.now() >= activeUntil;\n return {\n pollEveryMs: () => (idle() ? IDLE_POLL_MS : ACTIVE_POLL_MS),\n touch: () => {\n const wasIdle = idle();\n activeUntil = Date.now() + USER_ACTIVE_WINDOW_MS;\n return wasIdle;\n },\n };\n}\n\nexport function createAppLink(grant: AppGrant, deps: AppLinkDeps): AppLink {\n const origin = grant.appOrigin.replace(/\\/+$/, \"\");\n let welcome: Accepted | undefined;\n let mode: LinkMode = \"idle\";\n const modeNow = (): LinkMode => mode;\n let revoked = false;\n let sync: SyncClient | undefined;\n let heartbeat: NodeJS.Timeout | undefined;\n const streams = new Map<TurnId, TurnStream>();\n\n let admit: () => void = () => undefined;\n let refuse: (error: Error) => void = () => undefined;\n const welcomed = new Promise<void>((resolve, reject) => {\n admit = resolve;\n refuse = reject;\n });\n welcomed.catch(() => undefined);\n\n const signedFetch: typeof fetch = async (input, init) => {\n if (mode === \"closed\") throw new LinkDown(\"This app's link is closed.\");\n const request = new Request(input, init);\n const method = request.method === \"GET\" ? \"GET\" : \"POST\";\n const assertion = await deps.keys.assert(origin, { method, url: request.url });\n const headers = new Headers(request.headers);\n headers.set(\"authorization\", `ParlorHost ${assertion}`);\n const response = await deps.fetch(new Request(request, { headers }));\n logEvent(\"http_request\", {\n method,\n path: new URL(request.url).pathname,\n status: response.status,\n });\n if (response.status === 401 && !revoked && modeNow() !== \"closed\") {\n revoked = true;\n queueMicrotask(() => deps.onRevoked(grant));\n }\n return response;\n };\n\n const timedFetch = untilHeaders(signedFetch, REQUEST_TIMEOUT_MS);\n\n const plugin = createPluginClient<ChatHostActionMap>(\n createActionClient({ baseUrl: origin, fetch: timedFetch }),\n \"chat\",\n );\n\n const drained = async (turnId: TurnId | undefined) => {\n if (turnId === undefined) return;\n while (true) {\n const posting = streams.get(turnId)?.posting;\n if (!posting) return;\n await posting;\n }\n };\n\n const invoke: PluginClient<ChatHostActionMap>[\"invoke\"] = async (\n name,\n input,\n options,\n ) => {\n if (name !== \"hostHello\") await welcomed;\n if (name === \"hostAppend\" || name === \"hostFinish\") {\n await drained((input as { readonly turnId?: TurnId }).turnId);\n }\n return plugin.invoke(name, input, options);\n };\n const calls: PluginClient<ChatHostActionMap> = {\n invoke,\n call: async (name, input, options) => (await invoke(name, input, options)).data,\n };\n\n const post = (path: string, body: unknown) =>\n timedFetch(`${origin}/_parlor/chat/${path}`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n\n const flush = async (turnId: TurnId, stream: TurnStream) => {\n while (stream.pending.length > 0) {\n if (mode === \"closed\") break;\n const frames = coalesce(stream.pending);\n stream.pending = [];\n for (const chunk of batches(frames)) {\n await post(\"stream\", { turnId, frames: chunk }).catch(() => undefined);\n }\n }\n stream.posting = undefined;\n if (stream.pending.length === 0) streams.delete(turnId);\n };\n\n const api: HostApi = {\n calls,\n stream(batch) {\n if (mode === \"closed\" || batch.frames.length === 0) return;\n let stream = streams.get(batch.turnId);\n if (!stream) {\n stream = { pending: [], posting: undefined };\n streams.set(batch.turnId, stream);\n }\n stream.pending.push(...batch.frames);\n if (!stream.posting) stream.posting = flush(batch.turnId, stream);\n },\n async heartbeat() {\n await welcomed;\n await post(\"heartbeat\", {});\n },\n };\n\n const close = () => {\n mode = \"closed\";\n sync?.stop();\n sync = undefined;\n if (heartbeat) clearInterval(heartbeat);\n heartbeat = undefined;\n refuse(new LinkDown(\"This app's link is closed.\"));\n };\n\n const hello = async (): Promise<HostWelcome | undefined> => {\n for (let attempt = 0; ; attempt += 1) {\n if (mode !== \"serving\" && mode !== \"draining\") return undefined;\n try {\n return await plugin.call(\"hostHello\", {\n protocols: { min: 1, max: HOST_PROTOCOL },\n hostVersion: deps.hostVersion,\n name: deps.hostName,\n platform: deps.platform,\n agents: [],\n });\n } catch {\n if (revoked || modeNow() === \"closed\") return undefined;\n await sleep(Math.min(RETRY_BASE_MS * 2 ** attempt, RECONNECT_MAX_MS));\n }\n }\n };\n\n const link: AppLink = {\n grant,\n api,\n get protocol() {\n return welcome?.protocol;\n },\n get mode() {\n return mode;\n },\n start() {\n if (mode !== \"idle\") return;\n mode = \"serving\";\n void hello().then((answer) => {\n if (!answer || modeNow() === \"closed\") return;\n if (!answer.accepted) {\n close();\n logEvent(\"link_closed\", { appOrigin: origin, reason: \"incompatible\" });\n deps.onIncompatible(grant, answer);\n return;\n }\n welcome = answer;\n admit();\n const polling = adaptivePoll(\n answer.userActivity === true,\n () => deps.busy?.() ?? false,\n );\n sync = createSyncClient({\n request: (path, init) => signedFetch(`${origin}${path}`, init),\n coalesceMs: 0,\n reconnectMaxMs: RECONNECT_MAX_MS,\n pollEveryMs: polling.pollEveryMs,\n onEvents: (events) => {\n const heads = events.flatMap((event) => event.tags.map((tag) => tag[0]));\n if (heads.some((head) => head === CHAT_TAG || head === CHAT_ACTIVITY_TAG)) {\n if (polling.touch()) sync?.reconnectNow();\n }\n if (modeNow() === \"serving\" && heads.includes(CHAT_TAG)) deps.onWake(link);\n },\n onUnauthorized: () => undefined,\n onError: () => undefined,\n });\n sync.start();\n heartbeat = setInterval(\n () => void api.heartbeat().catch(() => undefined),\n answer.heartbeatMs,\n );\n heartbeat.unref();\n if (modeNow() === \"serving\") deps.onWake(link);\n });\n },\n drain() {\n if (mode === \"serving\") mode = \"draining\";\n },\n async stop(options = {}) {\n if (options.revoke && !grant.dev) {\n await revokeGrant({\n appOrigin: origin,\n appName: grant.appName,\n assert: (request) => deps.keys.assert(origin, request),\n fetch: deps.fetch,\n });\n }\n close();\n },\n };\n return link;\n}\n"]}
|
package/dist/lock.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const LOCK_TOKEN_ENV = "PARLOR_HOST_LOCK_TOKEN";
|
|
2
|
+
export declare function holdsLock(path: string, token: string): Promise<boolean>;
|
|
3
|
+
/** A lock is held until its owner dies or its pid is taken by a process that started after the lock was written. */
|
|
4
|
+
export declare function withFileLock<T>(path: string, work: (token: string) => Promise<T>): Promise<T>;
|
|
5
|
+
/** Runs `work` under the lock `inherited` names when this process's parent holds it, else under a lock of its own. */
|
|
6
|
+
export declare function withInheritedLock<T>(path: string, inherited: string | undefined, work: () => Promise<T>): Promise<T>;
|
|
7
|
+
//# sourceMappingURL=lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../src/lock.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,cAAc,2BAA2B,CAAC;AA6EvD,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAG7E;AAED,oHAAoH;AACpH,wBAAsB,YAAY,CAAC,CAAC,EAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAQZ;AAED,sHAAsH;AACtH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACrB,OAAO,CAAC,CAAC,CAAC,CAGZ"}
|
package/dist/lock.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
const run = promisify(execFile);
|
|
6
|
+
export const LOCK_TOKEN_ENV = "PARLOR_HOST_LOCK_TOKEN";
|
|
7
|
+
const RETRY_MS = 20;
|
|
8
|
+
const UNWRITTEN_MS = 1_000;
|
|
9
|
+
const ACQUIRE_TIMEOUT_MS = 120_000;
|
|
10
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
function alive(pid) {
|
|
12
|
+
try {
|
|
13
|
+
process.kill(pid, 0);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
return error.code === "EPERM";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function startedAt(pid) {
|
|
21
|
+
try {
|
|
22
|
+
const { stdout } = await run("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
23
|
+
env: { ...process.env, LC_ALL: "C" },
|
|
24
|
+
});
|
|
25
|
+
const started = Date.parse(stdout.trim());
|
|
26
|
+
return Number.isNaN(started) ? undefined : started;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** `ps` reports whole seconds, rounded down, so a real owner never reads as started after its own lock. */
|
|
33
|
+
async function reusedPid(pid, lockedAt) {
|
|
34
|
+
const started = await startedAt(pid);
|
|
35
|
+
return started !== undefined && started > lockedAt;
|
|
36
|
+
}
|
|
37
|
+
async function stale(path, verdicts) {
|
|
38
|
+
try {
|
|
39
|
+
const [text, { mtimeMs }] = await Promise.all([readFile(path, "utf8"), stat(path)]);
|
|
40
|
+
const age = Date.now() - mtimeMs;
|
|
41
|
+
const pid = Number.parseInt(text, 10);
|
|
42
|
+
if (!Number.isInteger(pid))
|
|
43
|
+
return age > UNWRITTEN_MS;
|
|
44
|
+
if (!alive(pid))
|
|
45
|
+
return true;
|
|
46
|
+
const key = `${pid}:${mtimeMs}`;
|
|
47
|
+
let reused = verdicts.get(key);
|
|
48
|
+
if (reused === undefined) {
|
|
49
|
+
reused = await reusedPid(pid, mtimeMs);
|
|
50
|
+
verdicts.set(key, reused);
|
|
51
|
+
}
|
|
52
|
+
return reused;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function acquire(path, token) {
|
|
59
|
+
const deadline = Date.now() + ACQUIRE_TIMEOUT_MS;
|
|
60
|
+
const verdicts = new Map();
|
|
61
|
+
for (;;) {
|
|
62
|
+
try {
|
|
63
|
+
const file = await open(path, "wx", 0o600);
|
|
64
|
+
await file.writeFile(`${process.pid}\n${token}\n`);
|
|
65
|
+
await file.close();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code !== "EEXIST")
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
if (await stale(path, verdicts)) {
|
|
73
|
+
await rename(path, `${path}.stale.${process.pid}`).catch(() => undefined);
|
|
74
|
+
await rm(`${path}.stale.${process.pid}`, { force: true });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (Date.now() > deadline)
|
|
78
|
+
throw new Error(`Could not take the lock ${path}.`);
|
|
79
|
+
await sleep(RETRY_MS);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const tokenOf = (text) => text.split("\n")[1];
|
|
83
|
+
export async function holdsLock(path, token) {
|
|
84
|
+
const text = await readFile(path, "utf8").catch(() => undefined);
|
|
85
|
+
return text !== undefined && tokenOf(text) === token;
|
|
86
|
+
}
|
|
87
|
+
/** A lock is held until its owner dies or its pid is taken by a process that started after the lock was written. */
|
|
88
|
+
export async function withFileLock(path, work) {
|
|
89
|
+
const token = randomUUID();
|
|
90
|
+
await acquire(path, token);
|
|
91
|
+
try {
|
|
92
|
+
return await work(token);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
if (await holdsLock(path, token))
|
|
96
|
+
await rm(path, { force: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Runs `work` under the lock `inherited` names when this process's parent holds it, else under a lock of its own. */
|
|
100
|
+
export async function withInheritedLock(path, inherited, work) {
|
|
101
|
+
if (inherited && (await holdsLock(path, inherited)))
|
|
102
|
+
return work();
|
|
103
|
+
return withFileLock(path, work);
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=lock.js.map
|
package/dist/lock.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../src/lock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAEhC,MAAM,CAAC,MAAM,cAAc,GAAG,wBAAwB,CAAC;AACvD,MAAM,QAAQ,GAAG,EAAE,CAAC;AACpB,MAAM,YAAY,GAAG,KAAK,CAAC;AAC3B,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAEnC,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAEhF,SAAS,KAAK,CAAC,GAAW;IACxB,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAQ,KAA+B,CAAC,IAAI,KAAK,OAAO,CAAC;IAC3D,CAAC;AACH,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,GAAW;IAClC,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE;YACvE,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE;SACrC,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QAC1C,OAAO,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,2GAA2G;AAC3G,KAAK,UAAU,SAAS,CAAC,GAAW,EAAE,QAAgB;IACpD,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;IACrC,OAAO,OAAO,KAAK,SAAS,IAAI,OAAO,GAAG,QAAQ,CAAC;AACrD,CAAC;AAED,KAAK,UAAU,KAAK,CAAC,IAAY,EAAE,QAA8B;IAC/D,IAAI,CAAC;QACH,MAAM,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACpF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC;QACjC,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACtC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC;YAAE,OAAO,GAAG,GAAG,YAAY,CAAC;QACtD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QAC7B,MAAM,GAAG,GAAG,GAAG,GAAG,IAAI,OAAO,EAAE,CAAC;QAChC,IAAI,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YACvC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC5B,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,IAAY,EAAE,KAAa;IAChD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,kBAAkB,CAAC;IACjD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAmB,CAAC;IAC5C,SAAS,CAAC;QACR,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;YAC3C,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC;YACnD,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YACnB,OAAO;QACT,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,KAAK,CAAC;QACtE,CAAC;QACD,IAAI,MAAM,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC;YAChC,MAAM,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,UAAU,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YAC1E,MAAM,EAAE,CAAC,GAAG,IAAI,UAAU,OAAO,CAAC,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAC1D,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,GAAG,CAAC,CAAC;QAC/E,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;IACxB,CAAC;AACH,CAAC;AAED,MAAM,OAAO,GAAG,CAAC,IAAY,EAAsB,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAE1E,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY,EAAE,KAAa;IACzD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IACjE,OAAO,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC;AACvD,CAAC;AAED,oHAAoH;AACpH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAY,EACZ,IAAmC;IAEnC,MAAM,KAAK,GAAG,UAAU,EAAE,CAAC;IAC3B,MAAM,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC3B,IAAI,CAAC;QACH,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;YAAS,CAAC;QACT,IAAI,MAAM,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC;YAAE,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpE,CAAC;AACH,CAAC;AAED,sHAAsH;AACtH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAAY,EACZ,SAA6B,EAC7B,IAAsB;IAEtB,IAAI,SAAS,IAAI,CAAC,MAAM,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QAAE,OAAO,IAAI,EAAE,CAAC;IACnE,OAAO,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AAClC,CAAC","sourcesContent":["import { execFile } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport { open, readFile, rename, rm, stat } from \"node:fs/promises\";\nimport { promisify } from \"node:util\";\n\nconst run = promisify(execFile);\n\nexport const LOCK_TOKEN_ENV = \"PARLOR_HOST_LOCK_TOKEN\";\nconst RETRY_MS = 20;\nconst UNWRITTEN_MS = 1_000;\nconst ACQUIRE_TIMEOUT_MS = 120_000;\n\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nfunction alive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\nasync function startedAt(pid: number): Promise<number | undefined> {\n try {\n const { stdout } = await run(\"ps\", [\"-o\", \"lstart=\", \"-p\", String(pid)], {\n env: { ...process.env, LC_ALL: \"C\" },\n });\n const started = Date.parse(stdout.trim());\n return Number.isNaN(started) ? undefined : started;\n } catch {\n return undefined;\n }\n}\n\n/** `ps` reports whole seconds, rounded down, so a real owner never reads as started after its own lock. */\nasync function reusedPid(pid: number, lockedAt: number): Promise<boolean> {\n const started = await startedAt(pid);\n return started !== undefined && started > lockedAt;\n}\n\nasync function stale(path: string, verdicts: Map<string, boolean>): Promise<boolean> {\n try {\n const [text, { mtimeMs }] = await Promise.all([readFile(path, \"utf8\"), stat(path)]);\n const age = Date.now() - mtimeMs;\n const pid = Number.parseInt(text, 10);\n if (!Number.isInteger(pid)) return age > UNWRITTEN_MS;\n if (!alive(pid)) return true;\n const key = `${pid}:${mtimeMs}`;\n let reused = verdicts.get(key);\n if (reused === undefined) {\n reused = await reusedPid(pid, mtimeMs);\n verdicts.set(key, reused);\n }\n return reused;\n } catch {\n return false;\n }\n}\n\nasync function acquire(path: string, token: string): Promise<void> {\n const deadline = Date.now() + ACQUIRE_TIMEOUT_MS;\n const verdicts = new Map<string, boolean>();\n for (;;) {\n try {\n const file = await open(path, \"wx\", 0o600);\n await file.writeFile(`${process.pid}\\n${token}\\n`);\n await file.close();\n return;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n }\n if (await stale(path, verdicts)) {\n await rename(path, `${path}.stale.${process.pid}`).catch(() => undefined);\n await rm(`${path}.stale.${process.pid}`, { force: true });\n continue;\n }\n if (Date.now() > deadline) throw new Error(`Could not take the lock ${path}.`);\n await sleep(RETRY_MS);\n }\n}\n\nconst tokenOf = (text: string): string | undefined => text.split(\"\\n\")[1];\n\nexport async function holdsLock(path: string, token: string): Promise<boolean> {\n const text = await readFile(path, \"utf8\").catch(() => undefined);\n return text !== undefined && tokenOf(text) === token;\n}\n\n/** A lock is held until its owner dies or its pid is taken by a process that started after the lock was written. */\nexport async function withFileLock<T>(\n path: string,\n work: (token: string) => Promise<T>,\n): Promise<T> {\n const token = randomUUID();\n await acquire(path, token);\n try {\n return await work(token);\n } finally {\n if (await holdsLock(path, token)) await rm(path, { force: true });\n }\n}\n\n/** Runs `work` under the lock `inherited` names when this process's parent holds it, else under a lock of its own. */\nexport async function withInheritedLock<T>(\n path: string,\n inherited: string | undefined,\n work: () => Promise<T>,\n): Promise<T> {\n if (inherited && (await holdsLock(path, inherited))) return work();\n return withFileLock(path, work);\n}\n"]}
|
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAKA,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAG9C;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,CAOvF"}
|
package/dist/log.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
let file;
|
|
4
|
+
export function openHostLog(path) {
|
|
5
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
6
|
+
file = path;
|
|
7
|
+
}
|
|
8
|
+
export function logEvent(event, fields) {
|
|
9
|
+
if (file === undefined)
|
|
10
|
+
return;
|
|
11
|
+
try {
|
|
12
|
+
appendFileSync(file, `${JSON.stringify({ at: Date.now(), event, ...fields })}\n`, {
|
|
13
|
+
mode: 0o600,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=log.js.map
|
package/dist/log.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,IAAI,IAAwB,CAAC;AAE7B,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3D,IAAI,GAAG,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAa,EAAE,MAAyC;IAC/E,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO;IAC/B,IAAI,CAAC;QACH,cAAc,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE;YAChF,IAAI,EAAE,KAAK;SACZ,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;AACZ,CAAC","sourcesContent":["import { appendFileSync, mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nlet file: string | undefined;\n\nexport function openHostLog(path: string): void {\n mkdirSync(dirname(path), { recursive: true, mode: 0o700 });\n file = path;\n}\n\nexport function logEvent(event: string, fields: Readonly<Record<string, unknown>>): void {\n if (file === undefined) return;\n try {\n appendFileSync(file, `${JSON.stringify({ at: Date.now(), event, ...fields })}\\n`, {\n mode: 0o600,\n });\n } catch {}\n}\n"]}
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
|