@bitfab/sdk 0.61.3 → 0.61.4

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-TZSTKVZF.js → chunk-2ZZBNQVA.js} +6 -6
  2. package/dist/{chunk-TZSTKVZF.js.map → chunk-2ZZBNQVA.js.map} +1 -1
  3. package/dist/{chunk-7BU5LUMR.js → chunk-3GSEECNP.js} +26 -2
  4. package/dist/{chunk-7BU5LUMR.js.map → chunk-3GSEECNP.js.map} +1 -1
  5. package/dist/{chunk-OWXP3YXA.js → chunk-BRSQCK3F.js} +21 -3
  6. package/dist/chunk-BRSQCK3F.js.map +1 -0
  7. package/dist/{chunk-34UFYAE6.js → chunk-FL6BNPSO.js} +176 -10
  8. package/dist/chunk-FL6BNPSO.js.map +1 -0
  9. package/dist/{chunk-65QKCHI2.js → chunk-THGHTVB6.js} +27 -2
  10. package/dist/chunk-THGHTVB6.js.map +1 -0
  11. package/dist/{http-VTIALK4P.js → http-J2DYDKWJ.js} +2 -2
  12. package/dist/{http-ZWENQXQA.js → http-RH3SW32W.js} +2 -2
  13. package/dist/index.cjs +214 -9
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +13 -2
  16. package/dist/index.d.ts +13 -2
  17. package/dist/index.js +3 -3
  18. package/dist/node.cjs +214 -9
  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-YQG3OGDN.js → replay-CZWFAAIY.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-34UFYAE6.js.map +0 -1
  28. package/dist/chunk-65QKCHI2.js.map +0 -1
  29. package/dist/chunk-OWXP3YXA.js.map +0 -1
  30. /package/dist/{http-VTIALK4P.js.map → http-J2DYDKWJ.js.map} +0 -0
  31. /package/dist/{http-ZWENQXQA.js.map → http-RH3SW32W.js.map} +0 -0
  32. /package/dist/{replay-YQG3OGDN.js.map → replay-CZWFAAIY.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -42,7 +42,7 @@ var __version__, __packageName__;
42
42
  var init_version_generated = __esm({
43
43
  "src/version.generated.ts"() {
44
44
  "use strict";
45
- __version__ = "0.61.3";
45
+ __version__ = "0.61.4";
46
46
  __packageName__ = "@bitfab/sdk";
47
47
  }
48
48
  });
@@ -2277,7 +2277,7 @@ function warnDroppedExternalTrace(error) {
2277
2277
  function experimentIdWireFields(experimentId) {
2278
2278
  return { experimentId, testRunId: experimentId };
2279
2279
  }
2280
- var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, REPLAY_COMPLETE_REQUEST_TIMEOUT_MS, OTLP_TRACES_ENDPOINT, RETRYABLE_STATUSES, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, liveHttpClients, carrierSeq, HttpClient;
2280
+ var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, REPLAY_COMPLETE_REQUEST_TIMEOUT_MS, REPLAY_INTERRUPT_REQUEST_TIMEOUT_MS, REPLAY_HEARTBEAT_REQUEST_TIMEOUT_MS, OTLP_TRACES_ENDPOINT, RETRYABLE_STATUSES, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, liveHttpClients, carrierSeq, HttpClient;
2281
2281
  var init_http = __esm({
2282
2282
  "src/http.ts"() {
2283
2283
  "use strict";
@@ -2296,6 +2296,8 @@ var init_http = __esm({
2296
2296
  init_warnOnce();
2297
2297
  REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
2298
2298
  REPLAY_COMPLETE_REQUEST_TIMEOUT_MS = 12e4;
2299
+ REPLAY_INTERRUPT_REQUEST_TIMEOUT_MS = 5e3;
2300
+ REPLAY_HEARTBEAT_REQUEST_TIMEOUT_MS = 5e3;
2299
2301
  OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
2300
2302
  RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
2301
2303
  EXIT_FLUSH_TIMEOUT_MS = 5e3;
@@ -3108,6 +3110,28 @@ var init_http = __esm({
3108
3110
  { timeout: REPLAY_COMPLETE_REQUEST_TIMEOUT_MS }
3109
3111
  );
3110
3112
  }
3113
+ async interruptReplay(experimentId) {
3114
+ try {
3115
+ await this.request(
3116
+ "/api/sdk/replay/interrupt",
3117
+ experimentIdWireFields(experimentId),
3118
+ { timeout: REPLAY_INTERRUPT_REQUEST_TIMEOUT_MS }
3119
+ );
3120
+ return true;
3121
+ } catch {
3122
+ return false;
3123
+ }
3124
+ }
3125
+ async heartbeatReplay(experimentId) {
3126
+ try {
3127
+ await this.request(
3128
+ "/api/sdk/replay/heartbeat",
3129
+ experimentIdWireFields(experimentId),
3130
+ { timeout: REPLAY_HEARTBEAT_REQUEST_TIMEOUT_MS }
3131
+ );
3132
+ } catch {
3133
+ }
3134
+ }
3111
3135
  async getReseedSource(traceId) {
3112
3136
  return this.get(
3113
3137
  `/api/sdk/traces/${encodeURIComponent(traceId)}/reseedSource`
@@ -3611,6 +3635,114 @@ var init_integrationFixture = __esm({
3611
3635
  }
3612
3636
  });
3613
3637
 
3638
+ // src/replayHeartbeat.ts
3639
+ function startReplayHeartbeat(send) {
3640
+ let stopped = false;
3641
+ let inFlight;
3642
+ const timer = setInterval(() => {
3643
+ if (stopped || inFlight !== void 0) {
3644
+ return;
3645
+ }
3646
+ inFlight = send().catch(() => void 0).finally(() => {
3647
+ inFlight = void 0;
3648
+ });
3649
+ }, REPLAY_HEARTBEAT_INTERVAL_MS);
3650
+ unrefTimer(timer);
3651
+ return {
3652
+ stop: async () => {
3653
+ stopped = true;
3654
+ clearInterval(timer);
3655
+ await inFlight;
3656
+ }
3657
+ };
3658
+ }
3659
+ var REPLAY_HEARTBEAT_INTERVAL_MS;
3660
+ var init_replayHeartbeat = __esm({
3661
+ "src/replayHeartbeat.ts"() {
3662
+ "use strict";
3663
+ init_unrefTimer();
3664
+ REPLAY_HEARTBEAT_INTERVAL_MS = 6e4;
3665
+ }
3666
+ });
3667
+
3668
+ // src/replayInterrupt.ts
3669
+ function signalCapableProcess() {
3670
+ if (typeof process === "undefined" || typeof process.on !== "function") {
3671
+ return void 0;
3672
+ }
3673
+ return process;
3674
+ }
3675
+ function describeReplayInterrupt(experimentId, markedInterrupted) {
3676
+ if (markedInterrupted) {
3677
+ return `[replay] Experiment ${experimentId} interrupted.`;
3678
+ }
3679
+ 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.`;
3680
+ }
3681
+ function reportInterrupted(experimentId, markedInterrupted) {
3682
+ try {
3683
+ console.error(describeReplayInterrupt(experimentId, markedInterrupted));
3684
+ } catch {
3685
+ }
3686
+ }
3687
+ function installReplayInterruptHandlers(target) {
3688
+ const host = signalCapableProcess();
3689
+ if (host === void 0) {
3690
+ return { remove: () => {
3691
+ }, interrupted: () => false };
3692
+ }
3693
+ const handlers = /* @__PURE__ */ new Map();
3694
+ const removeHandlers = () => {
3695
+ for (const [signal, handler] of handlers) {
3696
+ host.removeListener(signal, handler);
3697
+ }
3698
+ handlers.clear();
3699
+ };
3700
+ let interrupting = false;
3701
+ const handleSignal = async (signal) => {
3702
+ const hostHandlesSignal = host.listenerCount(signal) > 1;
3703
+ const markedInterrupted = await target.interrupt();
3704
+ removeHandlers();
3705
+ if (target.onInterrupt !== void 0) {
3706
+ try {
3707
+ target.onInterrupt({
3708
+ experimentId: target.experimentId,
3709
+ experimentUrl: target.experimentUrl,
3710
+ signal,
3711
+ markedInterrupted
3712
+ });
3713
+ } catch {
3714
+ }
3715
+ return;
3716
+ }
3717
+ reportInterrupted(target.experimentId, markedInterrupted);
3718
+ if (!hostHandlesSignal) {
3719
+ host.kill(host.pid, signal);
3720
+ }
3721
+ };
3722
+ for (const signal of REPLAY_INTERRUPT_SIGNALS) {
3723
+ const handler = () => {
3724
+ if (interrupting) {
3725
+ return;
3726
+ }
3727
+ interrupting = true;
3728
+ void handleSignal(signal);
3729
+ };
3730
+ handlers.set(signal, handler);
3731
+ host.on(signal, handler);
3732
+ }
3733
+ return { remove: removeHandlers, interrupted: () => interrupting };
3734
+ }
3735
+ var REPLAY_INTERRUPT_SIGNALS;
3736
+ var init_replayInterrupt = __esm({
3737
+ "src/replayInterrupt.ts"() {
3738
+ "use strict";
3739
+ REPLAY_INTERRUPT_SIGNALS = [
3740
+ "SIGINT",
3741
+ "SIGTERM"
3742
+ ];
3743
+ }
3744
+ });
3745
+
3614
3746
  // src/selectiveReplay.ts
3615
3747
  function replayNodeKey(node) {
3616
3748
  return JSON.stringify([node.traceFunctionKey, node.spanName]);
@@ -4520,7 +4652,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4520
4652
  const includeDbBranchLease = dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true;
4521
4653
  let session;
4522
4654
  if (options?.resume === void 0) {
4523
- const started2 = await httpClient.startReplay(
4655
+ const started = await httpClient.startReplay(
4524
4656
  traceFunctionKey,
4525
4657
  // limit is meaningless with explicit traceIds (the ID list determines
4526
4658
  // the count), so it's omitted from the request entirely.
@@ -4543,7 +4675,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4543
4675
  options?.judgeAssertions,
4544
4676
  options?.experimentalSelectiveReplay
4545
4677
  );
4546
- if (started2.items.length === 0) {
4678
+ if (started.items.length === 0) {
4547
4679
  try {
4548
4680
  console.warn(
4549
4681
  `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
@@ -4552,10 +4684,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4552
4684
  }
4553
4685
  }
4554
4686
  session = {
4555
- experimentId: started2.experimentId,
4556
- experimentUrl: started2.experimentUrl,
4687
+ experimentId: started.experimentId,
4688
+ experimentUrl: started.experimentUrl,
4557
4689
  attempts: requestedAttempts,
4558
- work: expandReplayWork(started2.items, requestedAttempts),
4690
+ work: expandReplayWork(started.items, requestedAttempts),
4559
4691
  finished: [],
4560
4692
  includeDbBranchLease,
4561
4693
  dbBranchSettings: resolveDbBranchSettings(options?.dbBranch)
@@ -4570,9 +4702,68 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4570
4702
  options.dbBranch
4571
4703
  );
4572
4704
  }
4573
- const { experimentId, experimentUrl, attempts } = session;
4705
+ const { experimentId } = session;
4706
+ const fullExperimentUrl = `${serviceUrl}${session.experimentUrl}`;
4707
+ const heartbeat = startReplayHeartbeat(
4708
+ () => httpClient.heartbeatReplay(experimentId)
4709
+ );
4710
+ const interruptReplay = async () => {
4711
+ await heartbeat.stop();
4712
+ return httpClient.interruptReplay(experimentId);
4713
+ };
4714
+ const interruptHandlers = installReplayInterruptHandlers({
4715
+ experimentId,
4716
+ experimentUrl: fullExperimentUrl,
4717
+ interrupt: interruptReplay,
4718
+ onInterrupt: options?.onInterrupt
4719
+ });
4720
+ let completeReturned = false;
4721
+ const markCompleteReturned = () => {
4722
+ completeReturned = true;
4723
+ void heartbeat.stop();
4724
+ };
4725
+ try {
4726
+ return await runReplaySession(
4727
+ httpClient,
4728
+ fn,
4729
+ session,
4730
+ fullExperimentUrl,
4731
+ maxConcurrency,
4732
+ interruptHandlers.remove,
4733
+ markCompleteReturned,
4734
+ interruptHandlers.interrupted,
4735
+ options,
4736
+ registeredOverrides
4737
+ );
4738
+ } catch (error) {
4739
+ if (!completeReturned && !interruptHandlers.interrupted()) {
4740
+ await interruptReplay();
4741
+ }
4742
+ throw error;
4743
+ } finally {
4744
+ interruptHandlers.remove();
4745
+ await heartbeat.stop();
4746
+ }
4747
+ }
4748
+ function throwIfInterrupted(interrupted, items, experimentId, experimentUrl) {
4749
+ if (!interrupted()) {
4750
+ return;
4751
+ }
4752
+ const cause = new BitfabError(
4753
+ `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}.`,
4754
+ experimentUrl
4755
+ );
4756
+ throw new ReplayError(
4757
+ cause.message,
4758
+ items,
4759
+ experimentId,
4760
+ experimentUrl,
4761
+ cause
4762
+ );
4763
+ }
4764
+ async function runReplaySession(httpClient, fn, session, fullExperimentUrl, maxConcurrency, stopInterruptingOnSignal, markCompleteReturned, interrupted, options, registeredOverrides) {
4765
+ const { experimentId, attempts } = session;
4574
4766
  const mockStrategy = options?.mock ?? "marked";
4575
- const fullExperimentUrl = `${serviceUrl}${experimentUrl}`;
4576
4767
  try {
4577
4768
  options?.onExperimentStart?.({
4578
4769
  experimentId,
@@ -4722,7 +4913,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4722
4913
  );
4723
4914
  const resultItems = [...carriedOverItems, ...ranItems];
4724
4915
  if (options?.dryRun === true) {
4916
+ throwIfInterrupted(
4917
+ interrupted,
4918
+ resultItems,
4919
+ experimentId,
4920
+ fullExperimentUrl
4921
+ );
4922
+ stopInterruptingOnSignal();
4725
4923
  await httpClient.completeReplay(experimentId).catch(() => void 0);
4924
+ markCompleteReturned();
4726
4925
  const dryResult = {
4727
4926
  items: resultItems,
4728
4927
  experimentId,
@@ -4747,12 +4946,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4747
4946
  ranItems[index].traceId = readBack;
4748
4947
  }
4749
4948
  }
4949
+ throwIfInterrupted(interrupted, resultItems, experimentId, fullExperimentUrl);
4950
+ stopInterruptingOnSignal();
4750
4951
  const completeResult = await preserveReplayFailure(
4751
4952
  () => httpClient.completeReplay(experimentId),
4752
4953
  resultItems,
4753
4954
  experimentId,
4754
4955
  fullExperimentUrl
4755
4956
  );
4957
+ markCompleteReturned();
4756
4958
  const serverTraceIds = completeResult.traceIds;
4757
4959
  const replayTokens = completeResult.tokens;
4758
4960
  if (serverTraceIds !== void 0) {
@@ -4856,6 +5058,8 @@ var init_replay = __esm({
4856
5058
  init_mockOverride();
4857
5059
  init_randomUuid();
4858
5060
  init_replayContext();
5061
+ init_replayHeartbeat();
5062
+ init_replayInterrupt();
4859
5063
  init_selectiveReplayRuntime();
4860
5064
  init_serialize();
4861
5065
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
@@ -11632,6 +11836,7 @@ init_replay();
11632
11836
  // src/replayRegistry.ts
11633
11837
  init_errors();
11634
11838
  init_replay();
11839
+ init_replayInterrupt();
11635
11840
  function defineReplayRegistry(registry) {
11636
11841
  return registry;
11637
11842
  }