@openclaw/crabline 0.1.10 → 0.1.11
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/dist/src/openclaw/artifact-generation.d.ts +5 -0
- package/dist/src/openclaw/artifact-generation.js +54 -5
- package/dist/src/openclaw/artifact-generation.js.map +1 -1
- package/dist/src/openclaw/private-file.d.ts +3 -1
- package/dist/src/openclaw/private-file.js +58 -4
- package/dist/src/openclaw/private-file.js.map +1 -1
- package/dist/src/openclaw/smoke-lock.js +70 -15
- package/dist/src/openclaw/smoke-lock.js.map +1 -1
- package/dist/src/openclaw.js +144 -4
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/providers/recorder.js +13 -3
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/servers/recorder.js +226 -10
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/telegram.js +24 -11
- package/dist/src/servers/telegram.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,11 +1,182 @@
|
|
|
1
1
|
import { chmod, mkdir, open } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { lock } from "proper-lockfile";
|
|
3
4
|
const pendingAppends = new Map();
|
|
5
|
+
const durableRecorderIdentities = new Map();
|
|
6
|
+
const MAX_DURABLE_RECORDER_IDENTITIES = 128;
|
|
7
|
+
const MAX_RECOVERY_VALIDATION_BYTES = 64 * 1024 * 1024;
|
|
8
|
+
const MAX_RECOVERY_SCAN_BYTES = MAX_RECOVERY_VALIDATION_BYTES + 1;
|
|
9
|
+
const RECORDER_LOCK_RETRY_MS = 100;
|
|
10
|
+
const RECORDER_LOCK_STALE_MS = 30_000;
|
|
11
|
+
const RECORDER_LOCK_UPDATE_MS = 10_000;
|
|
12
|
+
const TAIL_SCAN_CHUNK_BYTES = 64 * 1024;
|
|
4
13
|
function isManagedRecorderDirectory(directory) {
|
|
5
14
|
return (directory === path.resolve(".crabline", "servers") ||
|
|
6
15
|
directory === path.resolve("artifacts", "crabline"));
|
|
7
16
|
}
|
|
8
|
-
async function
|
|
17
|
+
async function readBufferAt(file, length, position) {
|
|
18
|
+
const buffer = Buffer.alloc(length);
|
|
19
|
+
let bytesRead = 0;
|
|
20
|
+
while (bytesRead < length) {
|
|
21
|
+
const result = await file.read(buffer, bytesRead, length - bytesRead, position + bytesRead);
|
|
22
|
+
if (result.bytesRead === 0) {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
bytesRead += result.bytesRead;
|
|
26
|
+
}
|
|
27
|
+
return buffer.subarray(0, bytesRead);
|
|
28
|
+
}
|
|
29
|
+
async function findIncompleteTailStart(file, fileSize) {
|
|
30
|
+
let position = fileSize;
|
|
31
|
+
while (position > 0) {
|
|
32
|
+
const scannedBytes = fileSize - position;
|
|
33
|
+
const remainingScanBytes = MAX_RECOVERY_SCAN_BYTES - scannedBytes;
|
|
34
|
+
if (remainingScanBytes <= 0) {
|
|
35
|
+
throw recoveryValidationLimitError();
|
|
36
|
+
}
|
|
37
|
+
const chunkStart = Math.max(0, position - Math.min(TAIL_SCAN_CHUNK_BYTES, remainingScanBytes));
|
|
38
|
+
const chunk = await readBufferAt(file, position - chunkStart, chunkStart);
|
|
39
|
+
const lastNewline = chunk.lastIndexOf(0x0a);
|
|
40
|
+
if (lastNewline >= 0) {
|
|
41
|
+
return chunkStart + lastNewline + 1;
|
|
42
|
+
}
|
|
43
|
+
if (chunk.length === 0) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
position = chunkStart;
|
|
47
|
+
}
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
function recoveryValidationLimitError() {
|
|
51
|
+
return new Error("Server recorder final record is too large to validate safely; refusing to modify it.");
|
|
52
|
+
}
|
|
53
|
+
async function openRecorderFile(filePath) {
|
|
54
|
+
try {
|
|
55
|
+
return {
|
|
56
|
+
created: true,
|
|
57
|
+
file: await open(filePath, "ax+", 0o600),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code !== "EEXIST") {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
created: false,
|
|
66
|
+
file: await open(filePath, "a+", 0o600),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function syncDirectory(directoryPath) {
|
|
71
|
+
if (process.platform === "win32") {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const directory = await open(directoryPath, "r");
|
|
75
|
+
try {
|
|
76
|
+
await directory.sync();
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await directory.close();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function syncRecorderPathAncestry(filePath, firstCreatedDirectory) {
|
|
83
|
+
if (process.platform === "win32") {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const resolvedFilePath = path.resolve(filePath);
|
|
87
|
+
let currentPath = resolvedFilePath;
|
|
88
|
+
const syncThroughPath = firstCreatedDirectory === undefined ? undefined : path.resolve(firstCreatedDirectory);
|
|
89
|
+
for (;;) {
|
|
90
|
+
const directoryPath = path.dirname(currentPath);
|
|
91
|
+
const mandatory = syncThroughPath !== undefined || currentPath === resolvedFilePath;
|
|
92
|
+
try {
|
|
93
|
+
await syncDirectory(directoryPath);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const code = error.code;
|
|
97
|
+
if (!mandatory && (code === "EACCES" || code === "EPERM")) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
if (currentPath === syncThroughPath) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (path.dirname(directoryPath) === directoryPath) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
currentPath = directoryPath;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function recorderIdentity(stats) {
|
|
112
|
+
if (stats.dev === undefined || stats.ino === undefined) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return `${stats.dev}:${stats.ino}`;
|
|
116
|
+
}
|
|
117
|
+
function rememberDurableRecorderIdentity(filePath, identity) {
|
|
118
|
+
durableRecorderIdentities.delete(filePath);
|
|
119
|
+
durableRecorderIdentities.set(filePath, identity);
|
|
120
|
+
if (durableRecorderIdentities.size > MAX_DURABLE_RECORDER_IDENTITIES) {
|
|
121
|
+
const oldestPath = durableRecorderIdentities.keys().next().value;
|
|
122
|
+
if (oldestPath !== undefined) {
|
|
123
|
+
durableRecorderIdentities.delete(oldestPath);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function recorderLockReleaseError(filePath, operationError, releaseError) {
|
|
128
|
+
return new AggregateError([operationError, releaseError], `Server recorder append and lock release both failed for "${filePath}".`, { cause: operationError });
|
|
129
|
+
}
|
|
130
|
+
function isRecorderLockContention(error) {
|
|
131
|
+
return (typeof error === "object" &&
|
|
132
|
+
error !== null &&
|
|
133
|
+
error.code === "ELOCKED");
|
|
134
|
+
}
|
|
135
|
+
async function acquireRecorderLock(filePath) {
|
|
136
|
+
while (true) {
|
|
137
|
+
try {
|
|
138
|
+
return await lock(filePath, {
|
|
139
|
+
realpath: false,
|
|
140
|
+
retries: 0,
|
|
141
|
+
stale: RECORDER_LOCK_STALE_MS,
|
|
142
|
+
update: RECORDER_LOCK_UPDATE_MS,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (!isRecorderLockContention(error)) {
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
await new Promise((resolve) => setTimeout(resolve, RECORDER_LOCK_RETRY_MS));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function withRecorderLock(filePath, operation) {
|
|
154
|
+
const release = await acquireRecorderLock(filePath);
|
|
155
|
+
let operationFailed = false;
|
|
156
|
+
let operationError;
|
|
157
|
+
let result;
|
|
158
|
+
try {
|
|
159
|
+
result = await operation();
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
operationFailed = true;
|
|
163
|
+
operationError = error;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
await release();
|
|
167
|
+
}
|
|
168
|
+
catch (releaseError) {
|
|
169
|
+
if (operationFailed) {
|
|
170
|
+
throw recorderLockReleaseError(filePath, operationError, releaseError);
|
|
171
|
+
}
|
|
172
|
+
throw releaseError;
|
|
173
|
+
}
|
|
174
|
+
if (operationFailed) {
|
|
175
|
+
throw operationError;
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
async function appendJsonLine(filePath, line, durable) {
|
|
9
180
|
const key = path.resolve(filePath);
|
|
10
181
|
const previous = pendingAppends.get(key) ?? Promise.resolve();
|
|
11
182
|
const current = previous
|
|
@@ -16,14 +187,59 @@ async function appendJsonLine(filePath, line) {
|
|
|
16
187
|
if (createdDirectory !== undefined || isManagedRecorderDirectory(path.resolve(directory))) {
|
|
17
188
|
await chmod(directory, 0o700);
|
|
18
189
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
190
|
+
await withRecorderLock(key, async () => {
|
|
191
|
+
const opened = await openRecorderFile(filePath);
|
|
192
|
+
const { file } = opened;
|
|
193
|
+
try {
|
|
194
|
+
await file.chmod(0o600);
|
|
195
|
+
const stats = await file.stat();
|
|
196
|
+
const identity = recorderIdentity(stats);
|
|
197
|
+
const needsPathDurability = opened.created ||
|
|
198
|
+
identity === undefined ||
|
|
199
|
+
durableRecorderIdentities.get(key) !== identity;
|
|
200
|
+
if (stats.size > 0) {
|
|
201
|
+
const finalByte = await readBufferAt(file, 1, stats.size - 1);
|
|
202
|
+
if (finalByte[0] !== 0x0a) {
|
|
203
|
+
const tailStart = await findIncompleteTailStart(file, stats.size);
|
|
204
|
+
const tailLength = stats.size - tailStart;
|
|
205
|
+
if (tailLength > MAX_RECOVERY_VALIDATION_BYTES) {
|
|
206
|
+
throw recoveryValidationLimitError();
|
|
207
|
+
}
|
|
208
|
+
const tail = await readBufferAt(file, tailLength, tailStart);
|
|
209
|
+
if (tail.length !== tailLength) {
|
|
210
|
+
throw new Error("Server recorder changed while repairing its final record.");
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
JSON.parse(tail.toString("utf8"));
|
|
214
|
+
await file.appendFile("\n", { encoding: "utf8" });
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
if (!(error instanceof SyntaxError)) {
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
await file.truncate(tailStart);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
await file.appendFile(line, { encoding: "utf8" });
|
|
225
|
+
if (durable || needsPathDurability) {
|
|
226
|
+
await file.sync();
|
|
227
|
+
}
|
|
228
|
+
if (needsPathDurability) {
|
|
229
|
+
const firstCreatedPath = createdDirectory ?? (opened.created ? path.resolve(filePath) : undefined);
|
|
230
|
+
await syncRecorderPathAncestry(filePath, firstCreatedPath);
|
|
231
|
+
if (identity === undefined) {
|
|
232
|
+
durableRecorderIdentities.delete(key);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
rememberDurableRecorderIdentity(key, identity);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
await file.close();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
27
243
|
});
|
|
28
244
|
pendingAppends.set(key, current);
|
|
29
245
|
try {
|
|
@@ -37,7 +253,7 @@ async function appendJsonLine(filePath, line) {
|
|
|
37
253
|
}
|
|
38
254
|
export async function recordServerEvent(params) {
|
|
39
255
|
// Observers only see events after the recorder append is durable.
|
|
40
|
-
await appendJsonLine(params.recorderPath, `${JSON.stringify(params.event)}\n
|
|
256
|
+
await appendJsonLine(params.recorderPath, `${JSON.stringify(params.event)}\n`, params.onEvent !== undefined);
|
|
41
257
|
await params.onEvent?.(params.event);
|
|
42
258
|
}
|
|
43
259
|
export async function recordCommittedServerEvent(params) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recorder.js","sourceRoot":"","sources":["../../../src/servers/recorder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,IAAI,MAAM,WAAW,CAAC;AAK7B,MAAM,cAAc,GAAG,IAAI,GAAG,EAAyB,CAAC;AAExD,SAAS,0BAA0B,CAAC,SAAiB;IACnD,OAAO,CACL,SAAS,KAAK,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,SAAS,CAAC;QAClD,SAAS,KAAK,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,UAAU,CAAC,CACpD,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,QAAgB,EAAE,IAAY;IAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnC,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAC9D,MAAM,OAAO,GAAG,QAAQ;SACrB,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC;SACf,IAAI,CAAC,KAAK,IAAI,EAAE;QACf,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACzC,MAAM,gBAAgB,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClF,IAAI,gBAAgB,KAAK,SAAS,IAAI,0BAA0B,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC;YAC1F,MAAM,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QAChC,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QAC9C,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACxB,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QACpD,CAAC;gBAAS,CAAC;YACT,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;QACrB,CAAC;IACH,CAAC,CAAC,CAAC;IACL,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAEjC,IAAI,CAAC;QACH,MAAM,OAAO,CAAC;IAChB,CAAC;YAAS,CAAC;QACT,IAAI,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,OAAO,EAAE,CAAC;YACxC,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAIvC;IACC,kEAAkE;IAClE,MAAM,cAAc,CAAC,MAAM,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/E,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAAC,MAIhD;IACC,IAAI,CAAC;QACH,MAAM,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,4FAA4F;IAC9F,CAAC;AACH,CAAC","sourcesContent":["import { chmod, mkdir, open } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { ServerRequestEvent } from \"./http.js\";\n\nexport type ServerEventObserver = (event: ServerRequestEvent) => void | Promise<void>;\n\nconst pendingAppends = new Map<string, Promise<void>>();\n\nfunction isManagedRecorderDirectory(directory: string): boolean {\n return (\n directory === path.resolve(\".crabline\", \"servers\") ||\n directory === path.resolve(\"artifacts\", \"crabline\")\n );\n}\n\nasync function appendJsonLine(filePath: string, line: string): Promise<void> {\n const key = path.resolve(filePath);\n const previous = pendingAppends.get(key) ?? Promise.resolve();\n const current = previous\n .catch(() => {})\n .then(async () => {\n const directory = path.dirname(filePath);\n const createdDirectory = await mkdir(directory, { mode: 0o700, recursive: true });\n if (createdDirectory !== undefined || isManagedRecorderDirectory(path.resolve(directory))) {\n await chmod(directory, 0o700);\n }\n const file = await open(filePath, \"a\", 0o600);\n try {\n await file.chmod(0o600);\n await file.appendFile(line, { encoding: \"utf8\" });\n } finally {\n await file.close();\n }\n });\n pendingAppends.set(key, current);\n\n try {\n await current;\n } finally {\n if (pendingAppends.get(key) === current) {\n pendingAppends.delete(key);\n }\n }\n}\n\nexport async function recordServerEvent(params: {\n event: ServerRequestEvent;\n onEvent: ServerEventObserver | undefined;\n recorderPath: string;\n}): Promise<void> {\n // Observers only see events after the recorder append is durable.\n await appendJsonLine(params.recorderPath, `${JSON.stringify(params.event)}\\n`);\n await params.onEvent?.(params.event);\n}\n\nexport async function recordCommittedServerEvent(params: {\n event: ServerRequestEvent;\n onEvent: ServerEventObserver | undefined;\n recorderPath: string;\n}): Promise<void> {\n try {\n await recordServerEvent(params);\n } catch {\n // The provider mutation already committed, so telemetry failure cannot change its response.\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"recorder.js","sourceRoot":"","sources":["../../../src/servers/recorder.ts"],"names":[],"mappings":"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","sourcesContent":["import { chmod, mkdir, open } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { lock } from \"proper-lockfile\";\nimport type { ServerRequestEvent } from \"./http.js\";\n\nexport type ServerEventObserver = (event: ServerRequestEvent) => void | Promise<void>;\n\nconst pendingAppends = new Map<string, Promise<void>>();\nconst durableRecorderIdentities = new Map<string, string>();\nconst MAX_DURABLE_RECORDER_IDENTITIES = 128;\nconst MAX_RECOVERY_VALIDATION_BYTES = 64 * 1024 * 1024;\nconst MAX_RECOVERY_SCAN_BYTES = MAX_RECOVERY_VALIDATION_BYTES + 1;\nconst RECORDER_LOCK_RETRY_MS = 100;\nconst RECORDER_LOCK_STALE_MS = 30_000;\nconst RECORDER_LOCK_UPDATE_MS = 10_000;\nconst TAIL_SCAN_CHUNK_BYTES = 64 * 1024;\n\nfunction isManagedRecorderDirectory(directory: string): boolean {\n return (\n directory === path.resolve(\".crabline\", \"servers\") ||\n directory === path.resolve(\"artifacts\", \"crabline\")\n );\n}\n\nasync function readBufferAt(\n file: Awaited<ReturnType<typeof open>>,\n length: number,\n position: number,\n): Promise<Buffer> {\n const buffer = Buffer.alloc(length);\n let bytesRead = 0;\n while (bytesRead < length) {\n const result = await file.read(buffer, bytesRead, length - bytesRead, position + bytesRead);\n if (result.bytesRead === 0) {\n break;\n }\n bytesRead += result.bytesRead;\n }\n return buffer.subarray(0, bytesRead);\n}\n\nasync function findIncompleteTailStart(\n file: Awaited<ReturnType<typeof open>>,\n fileSize: number,\n): Promise<number> {\n let position = fileSize;\n while (position > 0) {\n const scannedBytes = fileSize - position;\n const remainingScanBytes = MAX_RECOVERY_SCAN_BYTES - scannedBytes;\n if (remainingScanBytes <= 0) {\n throw recoveryValidationLimitError();\n }\n const chunkStart = Math.max(0, position - Math.min(TAIL_SCAN_CHUNK_BYTES, remainingScanBytes));\n const chunk = await readBufferAt(file, position - chunkStart, chunkStart);\n const lastNewline = chunk.lastIndexOf(0x0a);\n if (lastNewline >= 0) {\n return chunkStart + lastNewline + 1;\n }\n if (chunk.length === 0) {\n break;\n }\n position = chunkStart;\n }\n return 0;\n}\n\nfunction recoveryValidationLimitError(): Error {\n return new Error(\n \"Server recorder final record is too large to validate safely; refusing to modify it.\",\n );\n}\n\nasync function openRecorderFile(\n filePath: string,\n): Promise<{ created: boolean; file: Awaited<ReturnType<typeof open>> }> {\n try {\n return {\n created: true,\n file: await open(filePath, \"ax+\", 0o600),\n };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") {\n throw error;\n }\n return {\n created: false,\n file: await open(filePath, \"a+\", 0o600),\n };\n }\n}\n\nasync function syncDirectory(directoryPath: string): Promise<void> {\n if (process.platform === \"win32\") {\n return;\n }\n const directory = await open(directoryPath, \"r\");\n try {\n await directory.sync();\n } finally {\n await directory.close();\n }\n}\n\nasync function syncRecorderPathAncestry(\n filePath: string,\n firstCreatedDirectory?: string,\n): Promise<void> {\n if (process.platform === \"win32\") {\n return;\n }\n const resolvedFilePath = path.resolve(filePath);\n let currentPath = resolvedFilePath;\n const syncThroughPath =\n firstCreatedDirectory === undefined ? undefined : path.resolve(firstCreatedDirectory);\n for (;;) {\n const directoryPath = path.dirname(currentPath);\n const mandatory = syncThroughPath !== undefined || currentPath === resolvedFilePath;\n try {\n await syncDirectory(directoryPath);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (!mandatory && (code === \"EACCES\" || code === \"EPERM\")) {\n return;\n }\n throw error;\n }\n if (currentPath === syncThroughPath) {\n return;\n }\n if (path.dirname(directoryPath) === directoryPath) {\n return;\n }\n currentPath = directoryPath;\n }\n}\n\nfunction recorderIdentity(stats: {\n dev?: bigint | number;\n ino?: bigint | number;\n}): string | undefined {\n if (stats.dev === undefined || stats.ino === undefined) {\n return undefined;\n }\n return `${stats.dev}:${stats.ino}`;\n}\n\nfunction rememberDurableRecorderIdentity(filePath: string, identity: string): void {\n durableRecorderIdentities.delete(filePath);\n durableRecorderIdentities.set(filePath, identity);\n if (durableRecorderIdentities.size > MAX_DURABLE_RECORDER_IDENTITIES) {\n const oldestPath = durableRecorderIdentities.keys().next().value;\n if (oldestPath !== undefined) {\n durableRecorderIdentities.delete(oldestPath);\n }\n }\n}\n\nfunction recorderLockReleaseError(\n filePath: string,\n operationError: unknown,\n releaseError: unknown,\n): AggregateError {\n return new AggregateError(\n [operationError, releaseError],\n `Server recorder append and lock release both failed for \"${filePath}\".`,\n { cause: operationError },\n );\n}\n\nfunction isRecorderLockContention(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n (error as NodeJS.ErrnoException).code === \"ELOCKED\"\n );\n}\n\nasync function acquireRecorderLock(filePath: string): Promise<() => Promise<void>> {\n while (true) {\n try {\n return await lock(filePath, {\n realpath: false,\n retries: 0,\n stale: RECORDER_LOCK_STALE_MS,\n update: RECORDER_LOCK_UPDATE_MS,\n });\n } catch (error) {\n if (!isRecorderLockContention(error)) {\n throw error;\n }\n await new Promise((resolve) => setTimeout(resolve, RECORDER_LOCK_RETRY_MS));\n }\n }\n}\n\nasync function withRecorderLock<T>(filePath: string, operation: () => Promise<T>): Promise<T> {\n const release = await acquireRecorderLock(filePath);\n let operationFailed = false;\n let operationError: unknown;\n let result: T | undefined;\n try {\n result = await operation();\n } catch (error) {\n operationFailed = true;\n operationError = error;\n }\n try {\n await release();\n } catch (releaseError) {\n if (operationFailed) {\n throw recorderLockReleaseError(filePath, operationError, releaseError);\n }\n throw releaseError;\n }\n if (operationFailed) {\n throw operationError;\n }\n return result as T;\n}\n\nasync function appendJsonLine(filePath: string, line: string, durable: boolean): Promise<void> {\n const key = path.resolve(filePath);\n const previous = pendingAppends.get(key) ?? Promise.resolve();\n const current = previous\n .catch(() => {})\n .then(async () => {\n const directory = path.dirname(filePath);\n const createdDirectory = await mkdir(directory, { mode: 0o700, recursive: true });\n if (createdDirectory !== undefined || isManagedRecorderDirectory(path.resolve(directory))) {\n await chmod(directory, 0o700);\n }\n await withRecorderLock(key, async () => {\n const opened = await openRecorderFile(filePath);\n const { file } = opened;\n try {\n await file.chmod(0o600);\n const stats = await file.stat();\n const identity = recorderIdentity(stats);\n const needsPathDurability =\n opened.created ||\n identity === undefined ||\n durableRecorderIdentities.get(key) !== identity;\n if (stats.size > 0) {\n const finalByte = await readBufferAt(file, 1, stats.size - 1);\n if (finalByte[0] !== 0x0a) {\n const tailStart = await findIncompleteTailStart(file, stats.size);\n const tailLength = stats.size - tailStart;\n if (tailLength > MAX_RECOVERY_VALIDATION_BYTES) {\n throw recoveryValidationLimitError();\n }\n const tail = await readBufferAt(file, tailLength, tailStart);\n if (tail.length !== tailLength) {\n throw new Error(\"Server recorder changed while repairing its final record.\");\n }\n try {\n JSON.parse(tail.toString(\"utf8\"));\n await file.appendFile(\"\\n\", { encoding: \"utf8\" });\n } catch (error) {\n if (!(error instanceof SyntaxError)) {\n throw error;\n }\n await file.truncate(tailStart);\n }\n }\n }\n await file.appendFile(line, { encoding: \"utf8\" });\n if (durable || needsPathDurability) {\n await file.sync();\n }\n if (needsPathDurability) {\n const firstCreatedPath =\n createdDirectory ?? (opened.created ? path.resolve(filePath) : undefined);\n await syncRecorderPathAncestry(filePath, firstCreatedPath);\n if (identity === undefined) {\n durableRecorderIdentities.delete(key);\n } else {\n rememberDurableRecorderIdentity(key, identity);\n }\n }\n } finally {\n await file.close();\n }\n });\n });\n pendingAppends.set(key, current);\n\n try {\n await current;\n } finally {\n if (pendingAppends.get(key) === current) {\n pendingAppends.delete(key);\n }\n }\n}\n\nexport async function recordServerEvent(params: {\n event: ServerRequestEvent;\n onEvent: ServerEventObserver | undefined;\n recorderPath: string;\n}): Promise<void> {\n // Observers only see events after the recorder append is durable.\n await appendJsonLine(\n params.recorderPath,\n `${JSON.stringify(params.event)}\\n`,\n params.onEvent !== undefined,\n );\n await params.onEvent?.(params.event);\n}\n\nexport async function recordCommittedServerEvent(params: {\n event: ServerRequestEvent;\n onEvent: ServerEventObserver | undefined;\n recorderPath: string;\n}): Promise<void> {\n try {\n await recordServerEvent(params);\n } catch {\n // The provider mutation already committed, so telemetry failure cannot change its response.\n }\n}\n"]}
|
|
@@ -966,6 +966,28 @@ async function handleRequest(params) {
|
|
|
966
966
|
state: params.state,
|
|
967
967
|
});
|
|
968
968
|
}
|
|
969
|
+
async function serveRequest(params) {
|
|
970
|
+
let fetchResponse;
|
|
971
|
+
try {
|
|
972
|
+
fetchResponse = await handleRequest({ request: params.request, state: params.state });
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
975
|
+
fetchResponse =
|
|
976
|
+
error instanceof InvalidJsonBodyError
|
|
977
|
+
? telegramError("Bad Request: can't parse JSON object")
|
|
978
|
+
: error instanceof RequestBodyTooLargeError
|
|
979
|
+
? telegramError("Request Entity Too Large", 413)
|
|
980
|
+
: jsonResponse({ error: "internal server error", ok: false }, 500);
|
|
981
|
+
}
|
|
982
|
+
try {
|
|
983
|
+
await writeResponse(params.response, fetchResponse);
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
// A disconnected client cannot receive an error fallback. End delivery here
|
|
987
|
+
// so the Node request callback never leaks an unhandled rejection.
|
|
988
|
+
params.response.destroy();
|
|
989
|
+
}
|
|
990
|
+
}
|
|
969
991
|
export async function startTelegramServer(params = {}) {
|
|
970
992
|
const host = params.host ?? "127.0.0.1";
|
|
971
993
|
const botId = params.botId ?? 424242;
|
|
@@ -1001,17 +1023,8 @@ export async function startTelegramServer(params = {}) {
|
|
|
1001
1023
|
webhookRetryUpdateId: undefined,
|
|
1002
1024
|
};
|
|
1003
1025
|
const port = params.port ?? 0;
|
|
1004
|
-
const server = createServer(
|
|
1005
|
-
|
|
1006
|
-
await writeResponse(response, await handleRequest({ request, state }));
|
|
1007
|
-
}
|
|
1008
|
-
catch (error) {
|
|
1009
|
-
await writeResponse(response, error instanceof InvalidJsonBodyError
|
|
1010
|
-
? telegramError("Bad Request: can't parse JSON object")
|
|
1011
|
-
: error instanceof RequestBodyTooLargeError
|
|
1012
|
-
? telegramError("Request Entity Too Large", 413)
|
|
1013
|
-
: jsonResponse({ error: "internal server error", ok: false }, 500));
|
|
1014
|
-
}
|
|
1026
|
+
const server = createServer((request, response) => {
|
|
1027
|
+
void serveRequest({ request, response, state });
|
|
1015
1028
|
});
|
|
1016
1029
|
await new Promise((resolve, reject) => {
|
|
1017
1030
|
server.once("error", reject);
|