@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.
@@ -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 appendJsonLine(filePath, line) {
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
- const file = await open(filePath, "a", 0o600);
20
- try {
21
- await file.chmod(0o600);
22
- await file.appendFile(line, { encoding: "utf8" });
23
- }
24
- finally {
25
- await file.close();
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(async (request, response) => {
1005
- try {
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);