bitfab 0.61.3 → 0.61.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.
Files changed (32) hide show
  1. package/dist/{chunk-6IMUONDN.js → chunk-4TRH3X3S.js} +31 -3
  2. package/dist/chunk-4TRH3X3S.js.map +1 -0
  3. package/dist/{chunk-IZUZ5NBB.js → chunk-7OXXBUGQ.js} +178 -11
  4. package/dist/chunk-7OXXBUGQ.js.map +1 -0
  5. package/dist/{chunk-PAUXHTNF.js → chunk-KOANTEGQ.js} +6 -6
  6. package/dist/{chunk-PAUXHTNF.js.map → chunk-KOANTEGQ.js.map} +1 -1
  7. package/dist/{chunk-HVEZHLVQ.js → chunk-R5RMFRHX.js} +30 -3
  8. package/dist/{chunk-HVEZHLVQ.js.map → chunk-R5RMFRHX.js.map} +1 -1
  9. package/dist/{chunk-WOHIAKZF.js → chunk-VD5PS6WE.js} +28 -4
  10. package/dist/chunk-VD5PS6WE.js.map +1 -0
  11. package/dist/{http-BYICBOWP.js → http-CR2SEMK4.js} +2 -2
  12. package/dist/{http-3IYU5Z54.js → http-SK2QESXJ.js} +2 -2
  13. package/dist/index.cjs +220 -11
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +24 -4
  16. package/dist/index.d.ts +24 -4
  17. package/dist/index.js +3 -3
  18. package/dist/node.cjs +220 -11
  19. package/dist/node.cjs.map +1 -1
  20. package/dist/node.d.cts +1 -1
  21. package/dist/node.d.ts +1 -1
  22. package/dist/node.js +3 -3
  23. package/dist/{replay-SXHX5BBU.js → replay-YSR7FD4I.js} +3 -3
  24. package/dist/replayCli.js +2 -2
  25. package/dist/seedCli.js +2 -2
  26. package/package.json +1 -1
  27. package/dist/chunk-6IMUONDN.js.map +0 -1
  28. package/dist/chunk-IZUZ5NBB.js.map +0 -1
  29. package/dist/chunk-WOHIAKZF.js.map +0 -1
  30. /package/dist/{http-3IYU5Z54.js.map → http-CR2SEMK4.js.map} +0 -0
  31. /package/dist/{http-BYICBOWP.js.map → http-SK2QESXJ.js.map} +0 -0
  32. /package/dist/{replay-SXHX5BBU.js.map → replay-YSR7FD4I.js.map} +0 -0
@@ -4,8 +4,9 @@ import {
4
4
  flushTraces,
5
5
  gitRunner,
6
6
  replayContextReady,
7
- runWithReplayContext
8
- } from "./chunk-6IMUONDN.js";
7
+ runWithReplayContext,
8
+ unrefTimer
9
+ } from "./chunk-4TRH3X3S.js";
9
10
  import {
10
11
  runWithCaptureSuppressed,
11
12
  warnOnce
@@ -437,6 +438,101 @@ function fallbackUuidV4() {
437
438
  });
438
439
  }
439
440
 
441
+ // src/replayHeartbeat.ts
442
+ var REPLAY_HEARTBEAT_INTERVAL_MS = 6e4;
443
+ function startReplayHeartbeat(send) {
444
+ let stopped = false;
445
+ let inFlight;
446
+ const timer = setInterval(() => {
447
+ if (stopped || inFlight !== void 0) {
448
+ return;
449
+ }
450
+ inFlight = send().catch(() => void 0).finally(() => {
451
+ inFlight = void 0;
452
+ });
453
+ }, REPLAY_HEARTBEAT_INTERVAL_MS);
454
+ unrefTimer(timer);
455
+ return {
456
+ stop: async () => {
457
+ stopped = true;
458
+ clearInterval(timer);
459
+ await inFlight;
460
+ }
461
+ };
462
+ }
463
+
464
+ // src/replayInterrupt.ts
465
+ var REPLAY_INTERRUPT_SIGNALS = [
466
+ "SIGINT",
467
+ "SIGTERM"
468
+ ];
469
+ function signalCapableProcess() {
470
+ if (typeof process === "undefined" || typeof process.on !== "function") {
471
+ return void 0;
472
+ }
473
+ return process;
474
+ }
475
+ function describeReplayInterrupt(experimentId, markedInterrupted) {
476
+ if (markedInterrupted) {
477
+ return `[replay] Experiment ${experimentId} interrupted.`;
478
+ }
479
+ return `[replay] Experiment ${experimentId} stopped, but Bitfab could not mark it interrupted. It will be marked interrupted within 15 minutes. A resume in the next two minutes may be refused as still running.`;
480
+ }
481
+ function reportInterrupted(experimentId, markedInterrupted) {
482
+ try {
483
+ console.error(describeReplayInterrupt(experimentId, markedInterrupted));
484
+ } catch {
485
+ }
486
+ }
487
+ function installReplayInterruptHandlers(target) {
488
+ const host = signalCapableProcess();
489
+ if (host === void 0) {
490
+ return { remove: () => {
491
+ }, interrupted: () => false };
492
+ }
493
+ const handlers = /* @__PURE__ */ new Map();
494
+ const removeHandlers = () => {
495
+ for (const [signal, handler] of handlers) {
496
+ host.removeListener(signal, handler);
497
+ }
498
+ handlers.clear();
499
+ };
500
+ let interrupting = false;
501
+ const handleSignal = async (signal) => {
502
+ const hostHandlesSignal = host.listenerCount(signal) > 1;
503
+ const markedInterrupted = await target.interrupt();
504
+ removeHandlers();
505
+ if (target.onInterrupt !== void 0) {
506
+ try {
507
+ target.onInterrupt({
508
+ experimentId: target.experimentId,
509
+ experimentUrl: target.experimentUrl,
510
+ signal,
511
+ markedInterrupted
512
+ });
513
+ } catch {
514
+ }
515
+ return;
516
+ }
517
+ reportInterrupted(target.experimentId, markedInterrupted);
518
+ if (!hostHandlesSignal) {
519
+ host.kill(host.pid, signal);
520
+ }
521
+ };
522
+ for (const signal of REPLAY_INTERRUPT_SIGNALS) {
523
+ const handler = () => {
524
+ if (interrupting) {
525
+ return;
526
+ }
527
+ interrupting = true;
528
+ void handleSignal(signal);
529
+ };
530
+ handlers.set(signal, handler);
531
+ host.on(signal, handler);
532
+ }
533
+ return { remove: removeHandlers, interrupted: () => interrupting };
534
+ }
535
+
440
536
  // src/selectiveReplay.ts
441
537
  function replayNodeKey(node) {
442
538
  return JSON.stringify([node.traceFunctionKey, node.spanName]);
@@ -1357,7 +1453,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
1357
1453
  const includeDbBranchLease = dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true;
1358
1454
  let session;
1359
1455
  if (options?.resume === void 0) {
1360
- const started2 = await httpClient.startReplay(
1456
+ const started = await httpClient.startReplay(
1361
1457
  traceFunctionKey,
1362
1458
  // limit is meaningless with explicit traceIds (the ID list determines
1363
1459
  // the count), so it's omitted from the request entirely.
@@ -1378,9 +1474,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
1378
1474
  options?.adaptInputs !== void 0,
1379
1475
  options?.onlyWithAssertions,
1380
1476
  options?.judgeAssertions,
1381
- options?.experimentalSelectiveReplay
1477
+ options?.experimentalSelectiveReplay,
1478
+ options?.notes
1382
1479
  );
1383
- if (started2.items.length === 0) {
1480
+ if (started.items.length === 0) {
1384
1481
  try {
1385
1482
  console.warn(
1386
1483
  `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
@@ -1389,10 +1486,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
1389
1486
  }
1390
1487
  }
1391
1488
  session = {
1392
- experimentId: started2.experimentId,
1393
- experimentUrl: started2.experimentUrl,
1489
+ experimentId: started.experimentId,
1490
+ experimentUrl: started.experimentUrl,
1394
1491
  attempts: requestedAttempts,
1395
- work: expandReplayWork(started2.items, requestedAttempts),
1492
+ work: expandReplayWork(started.items, requestedAttempts),
1396
1493
  finished: [],
1397
1494
  includeDbBranchLease,
1398
1495
  dbBranchSettings: resolveDbBranchSettings(options?.dbBranch)
@@ -1407,9 +1504,68 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
1407
1504
  options.dbBranch
1408
1505
  );
1409
1506
  }
1410
- const { experimentId, experimentUrl, attempts } = session;
1507
+ const { experimentId } = session;
1508
+ const fullExperimentUrl = `${serviceUrl}${session.experimentUrl}`;
1509
+ const heartbeat = startReplayHeartbeat(
1510
+ () => httpClient.heartbeatReplay(experimentId)
1511
+ );
1512
+ const interruptReplay = async () => {
1513
+ await heartbeat.stop();
1514
+ return httpClient.interruptReplay(experimentId);
1515
+ };
1516
+ const interruptHandlers = installReplayInterruptHandlers({
1517
+ experimentId,
1518
+ experimentUrl: fullExperimentUrl,
1519
+ interrupt: interruptReplay,
1520
+ onInterrupt: options?.onInterrupt
1521
+ });
1522
+ let completeReturned = false;
1523
+ const markCompleteReturned = () => {
1524
+ completeReturned = true;
1525
+ void heartbeat.stop();
1526
+ };
1527
+ try {
1528
+ return await runReplaySession(
1529
+ httpClient,
1530
+ fn,
1531
+ session,
1532
+ fullExperimentUrl,
1533
+ maxConcurrency,
1534
+ interruptHandlers.remove,
1535
+ markCompleteReturned,
1536
+ interruptHandlers.interrupted,
1537
+ options,
1538
+ registeredOverrides
1539
+ );
1540
+ } catch (error) {
1541
+ if (!completeReturned && !interruptHandlers.interrupted()) {
1542
+ await interruptReplay();
1543
+ }
1544
+ throw error;
1545
+ } finally {
1546
+ interruptHandlers.remove();
1547
+ await heartbeat.stop();
1548
+ }
1549
+ }
1550
+ function throwIfInterrupted(interrupted, items, experimentId, experimentUrl) {
1551
+ if (!interrupted()) {
1552
+ return;
1553
+ }
1554
+ const cause = new BitfabError(
1555
+ `Experiment ${experimentId} was interrupted, so it was not completed or graded. Resume it by passing { resume: "${experimentId}" } to replay, or with bitfab-replay --resume ${experimentId}.`,
1556
+ experimentUrl
1557
+ );
1558
+ throw new ReplayError(
1559
+ cause.message,
1560
+ items,
1561
+ experimentId,
1562
+ experimentUrl,
1563
+ cause
1564
+ );
1565
+ }
1566
+ async function runReplaySession(httpClient, fn, session, fullExperimentUrl, maxConcurrency, stopInterruptingOnSignal, markCompleteReturned, interrupted, options, registeredOverrides) {
1567
+ const { experimentId, attempts } = session;
1411
1568
  const mockStrategy = options?.mock ?? "marked";
1412
- const fullExperimentUrl = `${serviceUrl}${experimentUrl}`;
1413
1569
  try {
1414
1570
  options?.onExperimentStart?.({
1415
1571
  experimentId,
@@ -1559,7 +1715,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
1559
1715
  );
1560
1716
  const resultItems = [...carriedOverItems, ...ranItems];
1561
1717
  if (options?.dryRun === true) {
1718
+ throwIfInterrupted(
1719
+ interrupted,
1720
+ resultItems,
1721
+ experimentId,
1722
+ fullExperimentUrl
1723
+ );
1724
+ stopInterruptingOnSignal();
1562
1725
  await httpClient.completeReplay(experimentId).catch(() => void 0);
1726
+ markCompleteReturned();
1563
1727
  const dryResult = {
1564
1728
  items: resultItems,
1565
1729
  experimentId,
@@ -1584,12 +1748,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
1584
1748
  ranItems[index].traceId = readBack;
1585
1749
  }
1586
1750
  }
1751
+ throwIfInterrupted(interrupted, resultItems, experimentId, fullExperimentUrl);
1752
+ stopInterruptingOnSignal();
1587
1753
  const completeResult = await preserveReplayFailure(
1588
1754
  () => httpClient.completeReplay(experimentId),
1589
1755
  resultItems,
1590
1756
  experimentId,
1591
1757
  fullExperimentUrl
1592
1758
  );
1759
+ markCompleteReturned();
1593
1760
  const serverTraceIds = completeResult.traceIds;
1594
1761
  const replayTokens = completeResult.tokens;
1595
1762
  if (serverTraceIds !== void 0) {
@@ -1702,4 +1869,4 @@ export {
1702
1869
  sleepForReplayPersistence,
1703
1870
  replay
1704
1871
  };
1705
- //# sourceMappingURL=chunk-IZUZ5NBB.js.map
1872
+ //# sourceMappingURL=chunk-7OXXBUGQ.js.map