@bitfab/sdk 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-7BU5LUMR.js → chunk-2ZUNCC2Q.js} +30 -3
  2. package/dist/{chunk-7BU5LUMR.js.map → chunk-2ZUNCC2Q.js.map} +1 -1
  3. package/dist/{chunk-34UFYAE6.js → chunk-44KG6B5D.js} +178 -11
  4. package/dist/chunk-44KG6B5D.js.map +1 -0
  5. package/dist/{chunk-TZSTKVZF.js → chunk-NISRFSN2.js} +6 -6
  6. package/dist/{chunk-TZSTKVZF.js.map → chunk-NISRFSN2.js.map} +1 -1
  7. package/dist/{chunk-OWXP3YXA.js → chunk-Y4IGLP2Z.js} +28 -4
  8. package/dist/chunk-Y4IGLP2Z.js.map +1 -0
  9. package/dist/{chunk-65QKCHI2.js → chunk-ZPVWVXWC.js} +31 -3
  10. package/dist/chunk-ZPVWVXWC.js.map +1 -0
  11. package/dist/{http-ZWENQXQA.js → http-JPHWWKL5.js} +2 -2
  12. package/dist/{http-VTIALK4P.js → http-VKYDN6PO.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-YQG3OGDN.js → replay-FP3L3X7Q.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-JPHWWKL5.js.map} +0 -0
  31. /package/dist/{http-ZWENQXQA.js.map → http-VKYDN6PO.js.map} +0 -0
  32. /package/dist/{replay-YQG3OGDN.js.map → replay-FP3L3X7Q.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.5";
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;
@@ -2883,7 +2885,7 @@ var init_http = __esm({
2883
2885
  * Start a replay session by fetching historical traces.
2884
2886
  * Blocking call - creates an experiment and returns lightweight item references.
2885
2887
  */
2886
- async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetIds, graderIds, dbBranchSettings, attempts, includeOriginalMetadata, onlyWithAssertions, judgeAssertions, experimentalSelectiveReplay) {
2888
+ async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetIds, graderIds, dbBranchSettings, attempts, includeOriginalMetadata, onlyWithAssertions, judgeAssertions, experimentalSelectiveReplay, notes) {
2887
2889
  const payload = { traceFunctionKey };
2888
2890
  if (experimentalSelectiveReplay !== void 0) {
2889
2891
  payload.experimentalSelectiveReplay = experimentalSelectiveReplay;
@@ -2897,6 +2899,9 @@ var init_http = __esm({
2897
2899
  if (name !== void 0) {
2898
2900
  payload.name = name;
2899
2901
  }
2902
+ if (notes !== void 0) {
2903
+ payload.notes = notes;
2904
+ }
2900
2905
  if (codeChangeDescription !== void 0) {
2901
2906
  payload.codeChangeDescription = codeChangeDescription;
2902
2907
  }
@@ -3108,6 +3113,28 @@ var init_http = __esm({
3108
3113
  { timeout: REPLAY_COMPLETE_REQUEST_TIMEOUT_MS }
3109
3114
  );
3110
3115
  }
3116
+ async interruptReplay(experimentId) {
3117
+ try {
3118
+ await this.request(
3119
+ "/api/sdk/replay/interrupt",
3120
+ experimentIdWireFields(experimentId),
3121
+ { timeout: REPLAY_INTERRUPT_REQUEST_TIMEOUT_MS }
3122
+ );
3123
+ return true;
3124
+ } catch {
3125
+ return false;
3126
+ }
3127
+ }
3128
+ async heartbeatReplay(experimentId) {
3129
+ try {
3130
+ await this.request(
3131
+ "/api/sdk/replay/heartbeat",
3132
+ experimentIdWireFields(experimentId),
3133
+ { timeout: REPLAY_HEARTBEAT_REQUEST_TIMEOUT_MS }
3134
+ );
3135
+ } catch {
3136
+ }
3137
+ }
3111
3138
  async getReseedSource(traceId) {
3112
3139
  return this.get(
3113
3140
  `/api/sdk/traces/${encodeURIComponent(traceId)}/reseedSource`
@@ -3611,6 +3638,114 @@ var init_integrationFixture = __esm({
3611
3638
  }
3612
3639
  });
3613
3640
 
3641
+ // src/replayHeartbeat.ts
3642
+ function startReplayHeartbeat(send) {
3643
+ let stopped = false;
3644
+ let inFlight;
3645
+ const timer = setInterval(() => {
3646
+ if (stopped || inFlight !== void 0) {
3647
+ return;
3648
+ }
3649
+ inFlight = send().catch(() => void 0).finally(() => {
3650
+ inFlight = void 0;
3651
+ });
3652
+ }, REPLAY_HEARTBEAT_INTERVAL_MS);
3653
+ unrefTimer(timer);
3654
+ return {
3655
+ stop: async () => {
3656
+ stopped = true;
3657
+ clearInterval(timer);
3658
+ await inFlight;
3659
+ }
3660
+ };
3661
+ }
3662
+ var REPLAY_HEARTBEAT_INTERVAL_MS;
3663
+ var init_replayHeartbeat = __esm({
3664
+ "src/replayHeartbeat.ts"() {
3665
+ "use strict";
3666
+ init_unrefTimer();
3667
+ REPLAY_HEARTBEAT_INTERVAL_MS = 6e4;
3668
+ }
3669
+ });
3670
+
3671
+ // src/replayInterrupt.ts
3672
+ function signalCapableProcess() {
3673
+ if (typeof process === "undefined" || typeof process.on !== "function") {
3674
+ return void 0;
3675
+ }
3676
+ return process;
3677
+ }
3678
+ function describeReplayInterrupt(experimentId, markedInterrupted) {
3679
+ if (markedInterrupted) {
3680
+ return `[replay] Experiment ${experimentId} interrupted.`;
3681
+ }
3682
+ 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.`;
3683
+ }
3684
+ function reportInterrupted(experimentId, markedInterrupted) {
3685
+ try {
3686
+ console.error(describeReplayInterrupt(experimentId, markedInterrupted));
3687
+ } catch {
3688
+ }
3689
+ }
3690
+ function installReplayInterruptHandlers(target) {
3691
+ const host = signalCapableProcess();
3692
+ if (host === void 0) {
3693
+ return { remove: () => {
3694
+ }, interrupted: () => false };
3695
+ }
3696
+ const handlers = /* @__PURE__ */ new Map();
3697
+ const removeHandlers = () => {
3698
+ for (const [signal, handler] of handlers) {
3699
+ host.removeListener(signal, handler);
3700
+ }
3701
+ handlers.clear();
3702
+ };
3703
+ let interrupting = false;
3704
+ const handleSignal = async (signal) => {
3705
+ const hostHandlesSignal = host.listenerCount(signal) > 1;
3706
+ const markedInterrupted = await target.interrupt();
3707
+ removeHandlers();
3708
+ if (target.onInterrupt !== void 0) {
3709
+ try {
3710
+ target.onInterrupt({
3711
+ experimentId: target.experimentId,
3712
+ experimentUrl: target.experimentUrl,
3713
+ signal,
3714
+ markedInterrupted
3715
+ });
3716
+ } catch {
3717
+ }
3718
+ return;
3719
+ }
3720
+ reportInterrupted(target.experimentId, markedInterrupted);
3721
+ if (!hostHandlesSignal) {
3722
+ host.kill(host.pid, signal);
3723
+ }
3724
+ };
3725
+ for (const signal of REPLAY_INTERRUPT_SIGNALS) {
3726
+ const handler = () => {
3727
+ if (interrupting) {
3728
+ return;
3729
+ }
3730
+ interrupting = true;
3731
+ void handleSignal(signal);
3732
+ };
3733
+ handlers.set(signal, handler);
3734
+ host.on(signal, handler);
3735
+ }
3736
+ return { remove: removeHandlers, interrupted: () => interrupting };
3737
+ }
3738
+ var REPLAY_INTERRUPT_SIGNALS;
3739
+ var init_replayInterrupt = __esm({
3740
+ "src/replayInterrupt.ts"() {
3741
+ "use strict";
3742
+ REPLAY_INTERRUPT_SIGNALS = [
3743
+ "SIGINT",
3744
+ "SIGTERM"
3745
+ ];
3746
+ }
3747
+ });
3748
+
3614
3749
  // src/selectiveReplay.ts
3615
3750
  function replayNodeKey(node) {
3616
3751
  return JSON.stringify([node.traceFunctionKey, node.spanName]);
@@ -4520,7 +4655,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4520
4655
  const includeDbBranchLease = dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true;
4521
4656
  let session;
4522
4657
  if (options?.resume === void 0) {
4523
- const started2 = await httpClient.startReplay(
4658
+ const started = await httpClient.startReplay(
4524
4659
  traceFunctionKey,
4525
4660
  // limit is meaningless with explicit traceIds (the ID list determines
4526
4661
  // the count), so it's omitted from the request entirely.
@@ -4541,9 +4676,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4541
4676
  options?.adaptInputs !== void 0,
4542
4677
  options?.onlyWithAssertions,
4543
4678
  options?.judgeAssertions,
4544
- options?.experimentalSelectiveReplay
4679
+ options?.experimentalSelectiveReplay,
4680
+ options?.notes
4545
4681
  );
4546
- if (started2.items.length === 0) {
4682
+ if (started.items.length === 0) {
4547
4683
  try {
4548
4684
  console.warn(
4549
4685
  `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
@@ -4552,10 +4688,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4552
4688
  }
4553
4689
  }
4554
4690
  session = {
4555
- experimentId: started2.experimentId,
4556
- experimentUrl: started2.experimentUrl,
4691
+ experimentId: started.experimentId,
4692
+ experimentUrl: started.experimentUrl,
4557
4693
  attempts: requestedAttempts,
4558
- work: expandReplayWork(started2.items, requestedAttempts),
4694
+ work: expandReplayWork(started.items, requestedAttempts),
4559
4695
  finished: [],
4560
4696
  includeDbBranchLease,
4561
4697
  dbBranchSettings: resolveDbBranchSettings(options?.dbBranch)
@@ -4570,9 +4706,68 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4570
4706
  options.dbBranch
4571
4707
  );
4572
4708
  }
4573
- const { experimentId, experimentUrl, attempts } = session;
4709
+ const { experimentId } = session;
4710
+ const fullExperimentUrl = `${serviceUrl}${session.experimentUrl}`;
4711
+ const heartbeat = startReplayHeartbeat(
4712
+ () => httpClient.heartbeatReplay(experimentId)
4713
+ );
4714
+ const interruptReplay = async () => {
4715
+ await heartbeat.stop();
4716
+ return httpClient.interruptReplay(experimentId);
4717
+ };
4718
+ const interruptHandlers = installReplayInterruptHandlers({
4719
+ experimentId,
4720
+ experimentUrl: fullExperimentUrl,
4721
+ interrupt: interruptReplay,
4722
+ onInterrupt: options?.onInterrupt
4723
+ });
4724
+ let completeReturned = false;
4725
+ const markCompleteReturned = () => {
4726
+ completeReturned = true;
4727
+ void heartbeat.stop();
4728
+ };
4729
+ try {
4730
+ return await runReplaySession(
4731
+ httpClient,
4732
+ fn,
4733
+ session,
4734
+ fullExperimentUrl,
4735
+ maxConcurrency,
4736
+ interruptHandlers.remove,
4737
+ markCompleteReturned,
4738
+ interruptHandlers.interrupted,
4739
+ options,
4740
+ registeredOverrides
4741
+ );
4742
+ } catch (error) {
4743
+ if (!completeReturned && !interruptHandlers.interrupted()) {
4744
+ await interruptReplay();
4745
+ }
4746
+ throw error;
4747
+ } finally {
4748
+ interruptHandlers.remove();
4749
+ await heartbeat.stop();
4750
+ }
4751
+ }
4752
+ function throwIfInterrupted(interrupted, items, experimentId, experimentUrl) {
4753
+ if (!interrupted()) {
4754
+ return;
4755
+ }
4756
+ const cause = new BitfabError(
4757
+ `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}.`,
4758
+ experimentUrl
4759
+ );
4760
+ throw new ReplayError(
4761
+ cause.message,
4762
+ items,
4763
+ experimentId,
4764
+ experimentUrl,
4765
+ cause
4766
+ );
4767
+ }
4768
+ async function runReplaySession(httpClient, fn, session, fullExperimentUrl, maxConcurrency, stopInterruptingOnSignal, markCompleteReturned, interrupted, options, registeredOverrides) {
4769
+ const { experimentId, attempts } = session;
4574
4770
  const mockStrategy = options?.mock ?? "marked";
4575
- const fullExperimentUrl = `${serviceUrl}${experimentUrl}`;
4576
4771
  try {
4577
4772
  options?.onExperimentStart?.({
4578
4773
  experimentId,
@@ -4722,7 +4917,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4722
4917
  );
4723
4918
  const resultItems = [...carriedOverItems, ...ranItems];
4724
4919
  if (options?.dryRun === true) {
4920
+ throwIfInterrupted(
4921
+ interrupted,
4922
+ resultItems,
4923
+ experimentId,
4924
+ fullExperimentUrl
4925
+ );
4926
+ stopInterruptingOnSignal();
4725
4927
  await httpClient.completeReplay(experimentId).catch(() => void 0);
4928
+ markCompleteReturned();
4726
4929
  const dryResult = {
4727
4930
  items: resultItems,
4728
4931
  experimentId,
@@ -4747,12 +4950,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4747
4950
  ranItems[index].traceId = readBack;
4748
4951
  }
4749
4952
  }
4953
+ throwIfInterrupted(interrupted, resultItems, experimentId, fullExperimentUrl);
4954
+ stopInterruptingOnSignal();
4750
4955
  const completeResult = await preserveReplayFailure(
4751
4956
  () => httpClient.completeReplay(experimentId),
4752
4957
  resultItems,
4753
4958
  experimentId,
4754
4959
  fullExperimentUrl
4755
4960
  );
4961
+ markCompleteReturned();
4756
4962
  const serverTraceIds = completeResult.traceIds;
4757
4963
  const replayTokens = completeResult.tokens;
4758
4964
  if (serverTraceIds !== void 0) {
@@ -4856,6 +5062,8 @@ var init_replay = __esm({
4856
5062
  init_mockOverride();
4857
5063
  init_randomUuid();
4858
5064
  init_replayContext();
5065
+ init_replayHeartbeat();
5066
+ init_replayInterrupt();
4859
5067
  init_selectiveReplayRuntime();
4860
5068
  init_serialize();
4861
5069
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
@@ -11632,6 +11840,7 @@ init_replay();
11632
11840
  // src/replayRegistry.ts
11633
11841
  init_errors();
11634
11842
  init_replay();
11843
+ init_replayInterrupt();
11635
11844
  function defineReplayRegistry(registry) {
11636
11845
  return registry;
11637
11846
  }