pipe-kan 0.20.3 → 0.20.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.
package/dist/pipe-kan.js CHANGED
@@ -261,6 +261,10 @@ function flagsToJql(flags) {
261
261
  function emptyList(text) {
262
262
  return /no result found/i.test(text);
263
263
  }
264
+ function rateLimited(text) {
265
+ return /\b429\b/.test(text);
266
+ }
267
+ var RETRY_LIMIT = 4;
264
268
  var LIST_PAGE = 100;
265
269
  function issueKey(issue) {
266
270
  if (!issue || typeof issue !== "object" || !("key" in issue))
@@ -319,6 +323,10 @@ function resolveJiraBin(bin = process.env.JIRA_BIN ?? "jira", pathVar = process.
319
323
  }
320
324
  function createJiraCli(opts = {}) {
321
325
  const bin = opts.bin ?? "jira";
326
+ const retryDelayMs = opts.retryDelayMs ?? 1000;
327
+ function fmt(args) {
328
+ return args.filter((arg) => arg !== "--raw").map((arg, i, all) => all[i - 1] === "-q" && arg.length > 48 ? `${arg.slice(0, 48)}…` : arg).join(" ");
329
+ }
322
330
  function run(args) {
323
331
  return new Promise((resolveRun, reject) => {
324
332
  const env = { ...process.env };
@@ -339,16 +347,36 @@ function createJiraCli(opts = {}) {
339
347
  child.on("close", (code) => resolveRun({ code: code ?? 1, stdout, stderr }));
340
348
  });
341
349
  }
350
+ async function runRetry(args) {
351
+ let result = await run(args);
352
+ for (let attempt = 0;attempt < RETRY_LIMIT; attempt++) {
353
+ if (result.code === 0 || !rateLimited(result.stderr || result.stdout))
354
+ return result;
355
+ const delay = retryDelayMs * 2 ** attempt;
356
+ console.log(`jira 429 retry ${delay}ms ${fmt(args)}`);
357
+ if (retryDelayMs) {
358
+ await new Promise((resolve) => setTimeout(resolve, delay));
359
+ }
360
+ result = await run(args);
361
+ }
362
+ return result;
363
+ }
342
364
  async function listOnce(args) {
343
- const result = await run(args);
365
+ const started = Date.now();
366
+ const result = await runRetry(args);
344
367
  if (result.code !== 0) {
345
368
  const text = result.stderr || result.stdout || "jira issue list failed";
346
- if (emptyList(text))
369
+ if (emptyList(text)) {
370
+ console.log(`jira ${fmt(args)} -> 0 (${Date.now() - started}ms)`);
347
371
  return [];
372
+ }
373
+ console.log(`jira ${fmt(args)} fail (${Date.now() - started}ms)`);
348
374
  throw new Error(text);
349
375
  }
350
376
  const parsed = JSON.parse(result.stdout);
351
- return Array.isArray(parsed) ? parsed : [];
377
+ const page = Array.isArray(parsed) ? parsed : [];
378
+ console.log(`jira ${fmt(args)} -> ${page.length} (${Date.now() - started}ms)`);
379
+ return page;
352
380
  }
353
381
  async function listAll(args) {
354
382
  if (hasPaginate(args)) {
@@ -377,6 +405,7 @@ function createJiraCli(opts = {}) {
377
405
  if (page.length < LIST_PAGE)
378
406
  break;
379
407
  }
408
+ console.log(`jira ${fmt(args)} ${issues.length} total`);
380
409
  return JSON.stringify(issues);
381
410
  }
382
411
  return {
@@ -407,7 +436,7 @@ function createJiraCli(opts = {}) {
407
436
  ]);
408
437
  },
409
438
  async move(key, status) {
410
- const result = await run(["issue", "move", key, status]);
439
+ const result = await runRetry(["issue", "move", key, status]);
411
440
  if (result.code !== 0) {
412
441
  return {
413
442
  ok: false,
@@ -417,12 +446,12 @@ function createJiraCli(opts = {}) {
417
446
  return { ok: true };
418
447
  },
419
448
  async open(key) {
420
- const result = await run(["open", key, "--no-browser"]);
449
+ const result = await runRetry(["open", key, "--no-browser"]);
421
450
  return result.stdout.trim().split(`
422
451
  `).pop() ?? `/browse/${key}`;
423
452
  },
424
453
  async view(key) {
425
- const result = await run(["issue", "view", key, "--raw"]);
454
+ const result = await runRetry(["issue", "view", key, "--raw"]);
426
455
  if (result.code !== 0) {
427
456
  throw new Error((result.stderr || result.stdout || "jira issue view failed").trim());
428
457
  }
@@ -581,19 +610,6 @@ function stampMissingEpic(board, epic) {
581
610
  }
582
611
  return board;
583
612
  }
584
- function stampRawParent(raw, epic) {
585
- if (!Array.isArray(raw))
586
- return [];
587
- return raw.map((issue) => {
588
- if (!issue || typeof issue !== "object")
589
- return issue;
590
- const current = issue;
591
- const fields = current.fields;
592
- if (!fields || typeof fields !== "object" || fields.parent)
593
- return issue;
594
- return { ...current, fields: { ...fields, parent: { key: epic } } };
595
- });
596
- }
597
613
  function cardsOf(raw) {
598
614
  if (!Array.isArray(raw))
599
615
  return [];
@@ -642,34 +658,46 @@ function createApp(opts) {
642
658
  async refresh(next) {
643
659
  if (next !== undefined)
644
660
  flags = next;
645
- const [issues, epics] = await Promise.all([
646
- cli.list(flags),
647
- cli.listEpics()
648
- ]);
649
- const nextPayload = JSON.parse(issues);
650
- const nextEpics = JSON.parse(epics);
651
- const keys = issuesToBoard(nextEpics).epics.map((epic) => epic.key);
652
- let nextChildren = [];
653
- let nextHasCache = false;
654
- let nextError;
661
+ console.log("Refresh");
655
662
  try {
656
- nextChildren = JSON.parse(await cli.listChildren(keys));
657
- const cards = cardsOf(nextChildren);
658
- if (cards.length > 0 && !cards.some((card) => card.epic)) {
659
- nextChildren = (await Promise.all(keys.map(async (key) => stampRawParent(JSON.parse(await cli.listEpic(key)), key)))).flat();
663
+ const issues = await cli.list(flags);
664
+ const epics = await cli.listEpics();
665
+ const nextPayload = JSON.parse(issues);
666
+ const nextEpics = JSON.parse(epics);
667
+ const keys = issuesToBoard(nextEpics).epics.map((epic) => epic.key);
668
+ const issueCount = Array.isArray(nextPayload) ? nextPayload.length : 0;
669
+ console.log(`Refresh listed ${issueCount} issues, ${keys.length} epics`);
670
+ let nextChildren = [];
671
+ let nextHasCache = false;
672
+ let nextError;
673
+ try {
674
+ nextChildren = JSON.parse(await cli.listChildren(keys));
675
+ const cards = cardsOf(nextChildren);
676
+ if (cards.length > 0 && !cards.some((card) => card.epic)) {
677
+ console.log(`Refresh children missing Epic keys; skip ${keys.length} per-Epic lists`);
678
+ nextChildren = [];
679
+ nextHasCache = false;
680
+ } else {
681
+ nextHasCache = true;
682
+ console.log(`Refresh children ${cards.length}`);
683
+ }
684
+ } catch (err) {
685
+ nextHasCache = false;
686
+ nextChildren = [];
687
+ nextError = err instanceof Error ? err.message : "Epic children list failed";
688
+ console.log("Refresh children failed", nextError);
660
689
  }
661
- nextHasCache = true;
690
+ payload = nextPayload;
691
+ epicsPayload = nextEpics;
692
+ childrenRaw = nextChildren;
693
+ hasChildrenCache = nextHasCache;
694
+ childrenError = nextError;
695
+ return app.board();
662
696
  } catch (err) {
663
- nextHasCache = false;
664
- nextChildren = [];
665
- nextError = err instanceof Error ? err.message : "Epic children list failed";
666
- }
667
- payload = nextPayload;
668
- epicsPayload = nextEpics;
669
- childrenRaw = nextChildren;
670
- hasChildrenCache = nextHasCache;
671
- childrenError = nextError;
672
- return app.board();
697
+ const message = err instanceof Error ? err.message : "Refresh failed";
698
+ console.error("Refresh failed", message);
699
+ return { ...app.board(), error: message };
700
+ }
673
701
  },
674
702
  async children(epic) {
675
703
  if (hasChildrenCache) {
@@ -883,6 +911,14 @@ function readBody(req) {
883
911
  function pathOf(req) {
884
912
  return new URL(req.url ?? "/", "http://127.0.0.1");
885
913
  }
914
+ function reply(req, res, work) {
915
+ readBody(req).then(work).catch((err) => {
916
+ const message = err instanceof Error ? err.message : "request failed";
917
+ console.error(message);
918
+ if (!res.headersSent)
919
+ json(res, 500, { error: message });
920
+ });
921
+ }
886
922
  function handleAppApi(req, res, app) {
887
923
  const url = pathOf(req);
888
924
  const method = (req.method ?? "GET").toUpperCase();
@@ -891,15 +927,14 @@ function handleAppApi(req, res, app) {
891
927
  return true;
892
928
  }
893
929
  if (url.pathname === "/api/refresh" && method === "POST") {
894
- readBody(req).then(async (text) => {
930
+ reply(req, res, async (text) => {
895
931
  const body = text ? JSON.parse(text) : {};
896
- const board = await app.refresh(body.flags);
897
- json(res, 200, board);
932
+ json(res, 200, await app.refresh(body.flags));
898
933
  });
899
934
  return true;
900
935
  }
901
936
  if (url.pathname === "/api/move" && method === "POST") {
902
- readBody(req).then(async (text) => {
937
+ reply(req, res, async (text) => {
903
938
  const body = text ? JSON.parse(text) : {};
904
939
  const result = await app.move(String(body.key ?? ""), String(body.status ?? ""));
905
940
  json(res, result.ok ? 200 : 409, result);
@@ -907,14 +942,14 @@ function handleAppApi(req, res, app) {
907
942
  return true;
908
943
  }
909
944
  if (url.pathname === "/api/epic" && method === "POST") {
910
- readBody(req).then(async (text) => {
945
+ reply(req, res, async (text) => {
911
946
  const body = text ? JSON.parse(text) : {};
912
947
  json(res, 200, await app.children(String(body.key ?? "")));
913
948
  });
914
949
  return true;
915
950
  }
916
951
  if (url.pathname === "/api/open" && method === "POST") {
917
- readBody(req).then(async (text) => {
952
+ reply(req, res, async (text) => {
918
953
  const body = text ? JSON.parse(text) : {};
919
954
  json(res, 200, await app.open(String(body.key ?? "")));
920
955
  });