vibe-coding-master 0.3.4 → 0.3.5

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,58 +1,70 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  export function createNodeFileSystemAdapter() {
4
+ const runFileOperation = createFileOperationRunner();
4
5
  return {
5
6
  async pathExists(targetPath) {
6
7
  try {
7
- await fs.access(targetPath);
8
+ await runFileOperation(() => fs.access(targetPath));
8
9
  return true;
9
10
  }
10
- catch {
11
- return false;
11
+ catch (error) {
12
+ if (isMissingPathError(error)) {
13
+ return false;
14
+ }
15
+ throw error;
12
16
  }
13
17
  },
14
18
  async ensureDir(targetPath) {
15
- await fs.mkdir(targetPath, { recursive: true });
19
+ await runFileOperation(() => fs.mkdir(targetPath, { recursive: true }));
16
20
  },
17
21
  async readDir(targetPath) {
18
- return fs.readdir(targetPath);
22
+ return runFileOperation(() => fs.readdir(targetPath));
19
23
  },
20
24
  async readText(targetPath) {
21
- return fs.readFile(targetPath, "utf8");
25
+ return runFileOperation(() => fs.readFile(targetPath, "utf8"));
22
26
  },
23
27
  async readTextTail(targetPath, maxBytes) {
24
- const handle = await fs.open(targetPath, "r");
25
- try {
26
- const stat = await handle.stat();
27
- const length = Math.min(Math.max(0, Math.floor(maxBytes)), stat.size);
28
- const position = stat.size - length;
29
- const buffer = Buffer.alloc(length);
30
- await handle.read(buffer, 0, length, position);
31
- return buffer.toString("utf8");
32
- }
33
- finally {
34
- await handle.close();
35
- }
28
+ return runFileOperation(async () => {
29
+ const handle = await fs.open(targetPath, "r");
30
+ try {
31
+ const stat = await handle.stat();
32
+ const length = Math.min(Math.max(0, Math.floor(maxBytes)), stat.size);
33
+ const position = stat.size - length;
34
+ const buffer = Buffer.alloc(length);
35
+ await handle.read(buffer, 0, length, position);
36
+ return buffer.toString("utf8");
37
+ }
38
+ finally {
39
+ await handle.close();
40
+ }
41
+ });
36
42
  },
37
43
  async writeText(targetPath, content) {
38
- await fs.mkdir(path.dirname(targetPath), { recursive: true });
39
- await fs.writeFile(targetPath, content, "utf8");
44
+ await runFileOperation(async () => {
45
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
46
+ await fs.writeFile(targetPath, content, "utf8");
47
+ });
40
48
  },
41
49
  async appendText(targetPath, content) {
42
- await fs.mkdir(path.dirname(targetPath), { recursive: true });
43
- await fs.appendFile(targetPath, content, "utf8");
50
+ await runFileOperation(async () => {
51
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
52
+ await fs.appendFile(targetPath, content, "utf8");
53
+ });
44
54
  },
45
55
  async readJson(targetPath) {
46
- return JSON.parse(await fs.readFile(targetPath, "utf8"));
56
+ return JSON.parse(await runFileOperation(() => fs.readFile(targetPath, "utf8")));
47
57
  },
48
58
  async writeJson(targetPath, value) {
49
59
  await this.writeText(targetPath, `${JSON.stringify(value, null, 2)}\n`);
50
60
  },
51
61
  async writeJsonAtomic(targetPath, value) {
52
- await fs.mkdir(path.dirname(targetPath), { recursive: true });
53
- const tempPath = `${targetPath}.${process.pid}.tmp`;
54
- await fs.writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
55
- await fs.rename(tempPath, targetPath);
62
+ await runFileOperation(async () => {
63
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
64
+ const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
65
+ await fs.writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
66
+ await fs.rename(tempPath, targetPath);
67
+ });
56
68
  },
57
69
  async ensureFile(targetPath, content, options = {}) {
58
70
  if (!options.overwrite && await this.pathExists(targetPath)) {
@@ -62,13 +74,75 @@ export function createNodeFileSystemAdapter() {
62
74
  return true;
63
75
  },
64
76
  async removePath(targetPath, options = {}) {
65
- await fs.rm(targetPath, {
77
+ await runFileOperation(() => fs.rm(targetPath, {
66
78
  recursive: options.recursive ?? false,
67
79
  force: options.force ?? false
68
- });
80
+ }));
81
+ }
82
+ };
83
+ }
84
+ const DEFAULT_FILE_OPERATION_CONCURRENCY = 32;
85
+ const OPEN_FILE_RETRY_DELAYS_MS = [25, 50, 100, 200, 400, 800];
86
+ function createFileOperationRunner(maxConcurrent = DEFAULT_FILE_OPERATION_CONCURRENCY) {
87
+ let active = 0;
88
+ const waiters = [];
89
+ async function acquire() {
90
+ if (active < maxConcurrent) {
91
+ active += 1;
92
+ return;
93
+ }
94
+ await new Promise((resolve) => waiters.push(resolve));
95
+ }
96
+ function release() {
97
+ const next = waiters.shift();
98
+ if (next) {
99
+ next();
100
+ return;
101
+ }
102
+ active = Math.max(0, active - 1);
103
+ }
104
+ return async (operation) => {
105
+ await acquire();
106
+ try {
107
+ return await retryOpenFileLimit(operation);
108
+ }
109
+ finally {
110
+ release();
69
111
  }
70
112
  };
71
113
  }
114
+ async function retryOpenFileLimit(operation) {
115
+ let attempt = 0;
116
+ for (;;) {
117
+ try {
118
+ return await operation();
119
+ }
120
+ catch (error) {
121
+ const delayMs = OPEN_FILE_RETRY_DELAYS_MS[attempt];
122
+ if (!isOpenFileLimitError(error) || delayMs === undefined) {
123
+ throw error;
124
+ }
125
+ attempt += 1;
126
+ await delay(delayMs);
127
+ }
128
+ }
129
+ }
130
+ function isOpenFileLimitError(error) {
131
+ const code = getErrorCode(error);
132
+ return code === "EMFILE" || code === "ENFILE";
133
+ }
134
+ function isMissingPathError(error) {
135
+ const code = getErrorCode(error);
136
+ return code === "ENOENT" || code === "ENOTDIR";
137
+ }
138
+ function getErrorCode(error) {
139
+ return typeof error === "object" && error !== null && "code" in error
140
+ ? String(error.code)
141
+ : undefined;
142
+ }
143
+ function delay(ms) {
144
+ return new Promise((resolve) => setTimeout(resolve, ms));
145
+ }
72
146
  export function resolveRepoPath(repoRoot, repoRelativePath) {
73
147
  if (path.isAbsolute(repoRelativePath)) {
74
148
  return repoRelativePath;
@@ -60,6 +60,11 @@ export function registerTranslationRoutes(app, deps) {
60
60
  await deps.translationService.clearSession(request.params.sessionId);
61
61
  return { ok: true };
62
62
  });
63
+ app.post("/api/translation/sessions/:sessionId/stop", async (request) => {
64
+ await requireCurrentProject(deps.projectService);
65
+ await deps.translationService.stopSession(request.params.sessionId);
66
+ return { ok: true };
67
+ });
63
68
  app.post("/api/translation/sessions/:sessionId/retry/:translationId", async (request) => {
64
69
  await requireCurrentProject(deps.projectService);
65
70
  return deps.translationService.retryTranslation(request.params.sessionId, request.params.translationId);
@@ -47,14 +47,14 @@ export function createWeixinIlinkChannel(options = {}) {
47
47
  async function fetchJson(input) {
48
48
  const controller = input.timeoutMs !== undefined ? new AbortController() : undefined;
49
49
  const timeout = controller ? setTimeout(() => controller.abort(), input.timeoutMs) : undefined;
50
- const signal = combineSignals(controller?.signal, input.signal);
50
+ const combinedSignal = combineSignals(controller?.signal, input.signal);
51
51
  const url = new URL(input.endpoint, ensureTrailingSlash(input.requestBaseUrl));
52
52
  try {
53
53
  const response = await fetchImpl(url, {
54
54
  method: input.method,
55
55
  headers: input.jsonHeaders === false ? buildIlinkAppHeaders() : buildJsonHeaders(input.token),
56
56
  body: input.body === undefined ? undefined : JSON.stringify(input.body),
57
- signal
57
+ signal: combinedSignal.signal
58
58
  });
59
59
  const rawText = await response.text();
60
60
  if (!response.ok) {
@@ -66,6 +66,7 @@ export function createWeixinIlinkChannel(options = {}) {
66
66
  if (timeout) {
67
67
  clearTimeout(timeout);
68
68
  }
69
+ combinedSignal.cleanup();
69
70
  }
70
71
  }
71
72
  async function post(input) {
@@ -285,10 +286,22 @@ function randomWechatUin() {
285
286
  }
286
287
  function combineSignals(first, second) {
287
288
  if (!first) {
288
- return second;
289
+ return {
290
+ signal: second,
291
+ cleanup() { }
292
+ };
289
293
  }
290
294
  if (!second) {
291
- return first;
295
+ return {
296
+ signal: first,
297
+ cleanup() { }
298
+ };
299
+ }
300
+ if (first === second) {
301
+ return {
302
+ signal: first,
303
+ cleanup() { }
304
+ };
292
305
  }
293
306
  const controller = new AbortController();
294
307
  const abort = () => controller.abort();
@@ -297,7 +310,13 @@ function combineSignals(first, second) {
297
310
  if (first.aborted || second.aborted) {
298
311
  controller.abort();
299
312
  }
300
- return controller.signal;
313
+ return {
314
+ signal: controller.signal,
315
+ cleanup() {
316
+ first.removeEventListener("abort", abort);
317
+ second.removeEventListener("abort", abort);
318
+ }
319
+ };
301
320
  }
302
321
  function stringOrUndefined(value) {
303
322
  return typeof value === "string" ? value : undefined;
@@ -25,28 +25,52 @@ export function createGatewayService(deps) {
25
25
  const now = deps.now ?? (() => new Date().toISOString());
26
26
  let pollAbort = null;
27
27
  let pollLoopPromise = null;
28
+ let pollStartingPromise = null;
28
29
  let qrLogin = null;
29
30
  let lastFailedTranslation = null;
30
31
  function isRunning() {
31
32
  return Boolean(pollAbort && !pollAbort.signal.aborted);
32
33
  }
33
34
  async function ensurePolling() {
34
- const settings = await deps.settings.loadSettings();
35
- if (!settings.binding.token || isRunning()) {
35
+ if (isRunning()) {
36
36
  return;
37
37
  }
38
- pollAbort = new AbortController();
39
- pollLoopPromise = pollLoop(pollAbort.signal).finally(() => {
40
- pollAbort = null;
41
- pollLoopPromise = null;
38
+ if (pollStartingPromise) {
39
+ await pollStartingPromise;
40
+ return;
41
+ }
42
+ pollStartingPromise = (async () => {
43
+ const settings = await deps.settings.loadSettings();
44
+ if (!settings.binding.token || isRunning()) {
45
+ return;
46
+ }
47
+ const controller = new AbortController();
48
+ pollAbort = controller;
49
+ const loop = pollLoop(controller.signal).finally(() => {
50
+ if (pollAbort === controller) {
51
+ pollAbort = null;
52
+ }
53
+ if (pollLoopPromise === loop) {
54
+ pollLoopPromise = null;
55
+ }
56
+ });
57
+ pollLoopPromise = loop;
58
+ })().finally(() => {
59
+ pollStartingPromise = null;
42
60
  });
61
+ await pollStartingPromise;
43
62
  }
44
63
  async function stopPolling() {
64
+ await pollStartingPromise?.catch(() => undefined);
45
65
  if (!pollAbort) {
46
66
  return;
47
67
  }
48
- pollAbort.abort();
68
+ const controller = pollAbort;
69
+ controller.abort();
49
70
  await pollLoopPromise?.catch(() => undefined);
71
+ if (pollAbort === controller) {
72
+ pollAbort = null;
73
+ }
50
74
  }
51
75
  function toAccount(settings) {
52
76
  if (!settings.binding.token) {
@@ -643,7 +667,7 @@ export function createGatewayService(deps) {
643
667
  await ensurePolling();
644
668
  },
645
669
  stop() {
646
- void stopPolling();
670
+ return stopPolling();
647
671
  },
648
672
  async getStatus() {
649
673
  const settings = await syncDesktopContext(await deps.settings.loadSettings());
@@ -111,7 +111,7 @@ export async function createServer(deps, options = {}) {
111
111
  await deps.gatewayService.start();
112
112
  });
113
113
  app.addHook("onClose", async () => {
114
- deps.gatewayService.stop();
114
+ await deps.gatewayService.stop();
115
115
  });
116
116
  if (options.staticDir) {
117
117
  await app.register(fastifyStatic, {
@@ -703,6 +703,10 @@ export function createTranslationService(deps) {
703
703
  });
704
704
  return () => {
705
705
  state.listeners.delete(listener);
706
+ if (state.listeners.size === 0 && state.unsubscribeTranscript) {
707
+ state.unsubscribeTranscript();
708
+ state.unsubscribeTranscript = undefined;
709
+ }
706
710
  };
707
711
  },
708
712
  async clearSession(sessionId) {