@trim21/personal-pi-extensions 0.0.271 → 0.0.272

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.271",
3
+ "version": "0.0.272",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -34,10 +34,12 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
34
34
  import { homedir } from "node:os";
35
35
  import { delimiter, dirname, join, resolve } from "node:path";
36
36
 
37
- import { type ExtensionAPI, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
37
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
38
38
  import { Type } from "typebox";
39
39
  import { Value } from "typebox/value";
40
40
 
41
+ import { createSeqState } from "./lib/seq-state.js";
42
+
41
43
  interface GhResult {
42
44
  stdout: string;
43
45
  stderr: string;
@@ -380,8 +382,9 @@ export function stepsDetail(
380
382
  }));
381
383
  }
382
384
 
383
- /** In-flight dedup map to avoid concurrent fetches of the same log. */
384
- const inflightLogs = new Map<string, Promise<string>>();
385
+ // 模块级串行状态:同一资源(如 CI 日志)的请求排队执行,配合函数内部的
386
+ // 缓存检查避免重复网络请求。闭包状态不与其他扩展共享,key 无需全局前缀。
387
+ const seq = createSeqState();
385
388
 
386
389
  async function getJobLog(
387
390
  runId: string,
@@ -393,13 +396,10 @@ async function getJobLog(
393
396
  ): Promise<string> {
394
397
  const cacheDir = join(homedir(), ".cache", "pi", "ci-logs", runId);
395
398
  const cacheFile = join(cacheDir, `${jobId}.log`);
396
- const key = `${runId}:${jobId}`;
397
-
398
- // Check in-flight dedup map
399
- const inflight = inflightLogs.get(key);
400
- if (inflight) return inflight;
401
399
 
402
- const fetchAndCache = async (): Promise<string> => {
400
+ // 同一 runId:jobId 的请求串行执行:后一个进入时缓存已写入,直接命中缓存,
401
+ // 不会重复发网络请求;串行也保证不会有两个并发写同一 cache 文件。
402
+ return seq.execute(`${runId}:${jobId}`, async () => {
403
403
  // Check file cache
404
404
  try {
405
405
  return await readFile(cacheFile, "utf8");
@@ -424,20 +424,10 @@ async function getJobLog(
424
424
 
425
425
  // Write to cache
426
426
  await mkdir(cacheDir, { recursive: true });
427
- await withFileMutationQueue(cacheFile, async () => {
428
- await writeFile(cacheFile, log);
429
- });
427
+ await writeFile(cacheFile, log);
430
428
 
431
429
  return log;
432
- };
433
-
434
- const promise = fetchAndCache();
435
- inflightLogs.set(key, promise);
436
- try {
437
- return await promise;
438
- } finally {
439
- inflightLogs.delete(key);
440
- }
430
+ });
441
431
  }
442
432
 
443
433
  async function resolveRepo(
@@ -1061,9 +1051,7 @@ export async function writeLogFile(
1061
1051
 
1062
1052
  const target = resolve(cwd ?? process.cwd(), outputFile);
1063
1053
  await mkdir(dirname(target), { recursive: true });
1064
- await withFileMutationQueue(target, async () => {
1065
- await writeFile(target, content);
1066
- });
1054
+ await writeFile(target, content);
1067
1055
 
1068
1056
  const lines = content.split("\n").length;
1069
1057
  const bytes = Buffer.byteLength(content, "utf8");
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 按 key 串行化并发任务:同一 key 的调用排队执行,前一个 settle(无论成败)
3
+ * 后才执行下一个。key 由调用方自定义(如资源 id),每个 `createSeqState()`
4
+ * 返回独立的闭包状态,不同扩展之间不串扰。
5
+ */
6
+
7
+ export interface SeqState {
8
+ execute<T>(key: string, fn: () => Promise<T>): Promise<T>;
9
+ }
10
+
11
+ export function createSeqState(): SeqState {
12
+ const tails = new Map<string, Promise<unknown>>();
13
+
14
+ return {
15
+ execute<T>(key: string, fn: () => Promise<T>): Promise<T> {
16
+ const previous = tails.get(key) ?? Promise.resolve();
17
+
18
+ // 等前一个任务 settle 后执行本任务。previous 恒为 resolve(tail 吞掉了错误),
19
+ // 失败不会阻塞后续任务。
20
+ const run = previous.then(() => fn());
21
+
22
+ // 本任务的完成占位:永远 resolve,作为下一个任务的等待点。
23
+ // eslint-disable-next-line unicorn/no-useless-undefined -- 显式返回 undefined:吞掉前序错误并归一化为成功占位
24
+ const tail = run.catch(() => undefined);
25
+
26
+ tails.set(key, tail);
27
+ void tail.finally(() => {
28
+ if (tails.get(key) === tail) tails.delete(key);
29
+ });
30
+
31
+ return run;
32
+ },
33
+ };
34
+ }