@orkestrel/browser 0.0.3 → 0.0.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.
@@ -46,7 +46,7 @@ var BROWSER_RESULT_LIMIT_SENTINEL_PREFIX = "[[ORKESTREL_BROWSER_RESULT_LIMIT]]";
46
46
  * throw is recognized only at the message START, not wherever the substring
47
47
  * happens to occur.
48
48
  */
49
- var BROWSER_RESULT_LIMIT_PATTERN = new RegExp(`(?:Uncaught )?Error: \\[\\[ORKESTREL_BROWSER_RESULT_LIMIT\\]\\](\\d+)`);
49
+ var BROWSER_RESULT_LIMIT_PATTERN = new RegExp(`^(?:Uncaught )?Error: \\[\\[ORKESTREL_BROWSER_RESULT_LIMIT\\]\\](\\d+)`);
50
50
  /** Poll interval in milliseconds while waiting for a selector to appear. */
51
51
  var BROWSER_WAIT_POLL_INTERVAL_MS = 100;
52
52
  /**
@@ -497,9 +497,11 @@ var CDPClient = class {
497
497
  #subscriptions = /* @__PURE__ */ new Map();
498
498
  #sessionSubscriptions = /* @__PURE__ */ new Map();
499
499
  #connected = false;
500
+ #active = false;
500
501
  #wired = false;
501
502
  #timeout;
502
503
  #connecting;
504
+ #closing;
503
505
  #closeRequested = false;
504
506
  constructor(options) {
505
507
  this.#transport = options.transport;
@@ -509,29 +511,16 @@ var CDPClient = class {
509
511
  return this.#connected;
510
512
  }
511
513
  async connect() {
514
+ if (this.#closing !== void 0) await this.#closing;
512
515
  if (this.#connected) return;
513
516
  if (this.#connecting !== void 0) return this.#connecting;
514
517
  this.#closeRequested = false;
515
- const attempt = (async () => {
516
- if (!this.#wired) {
517
- this.#transport.emitter.on("message", (data) => this.#onMessage(data));
518
- this.#transport.emitter.on("close", () => this.#onClose());
519
- this.#transport.emitter.on("error", (error) => this.#onError(error));
520
- this.#wired = true;
521
- }
522
- await this.#transport.start();
523
- if (this.#closeRequested) {
524
- this.#closeRequested = false;
525
- await this.#transport.close();
526
- throw new CDPConnectionError("CDP client was closed while connecting", { method: "connect" });
527
- }
528
- this.#connected = true;
529
- })();
518
+ const attempt = this.#start();
530
519
  this.#connecting = attempt;
531
520
  try {
532
521
  await attempt;
533
522
  } finally {
534
- this.#connecting = void 0;
523
+ if (this.#connecting === attempt) this.#connecting = void 0;
535
524
  }
536
525
  }
537
526
  /**
@@ -633,18 +622,56 @@ var CDPClient = class {
633
622
  * transport is closed deterministically once `start()` resolves.
634
623
  */
635
624
  async close() {
625
+ const active = this.#closing;
626
+ if (active !== void 0) {
627
+ await active;
628
+ return;
629
+ }
630
+ const attempt = this.#stop();
631
+ this.#closing = attempt;
632
+ try {
633
+ await attempt;
634
+ } finally {
635
+ if (this.#closing === attempt) this.#closing = void 0;
636
+ }
637
+ }
638
+ async #start() {
639
+ if (!this.#wired) {
640
+ this.#transport.emitter.on("message", (data) => this.#onMessage(data));
641
+ this.#transport.emitter.on("close", () => this.#onClose());
642
+ this.#transport.emitter.on("error", (error) => this.#onError(error));
643
+ this.#wired = true;
644
+ }
645
+ await this.#transport.start();
646
+ this.#active = true;
647
+ if (this.#closeRequested) {
648
+ this.#closeRequested = false;
649
+ try {
650
+ await this.#transport.close();
651
+ } finally {
652
+ this.#active = false;
653
+ }
654
+ throw new CDPConnectionError("CDP client was closed while connecting", { method: "connect" });
655
+ }
656
+ this.#connected = true;
657
+ }
658
+ async #stop() {
636
659
  if (this.#connecting !== void 0) {
637
660
  this.#closeRequested = true;
638
661
  await this.#connecting.catch(() => void 0);
639
662
  }
640
- if (!this.#connected) return;
663
+ if (!this.#connected && !this.#active) return;
641
664
  this.#connected = false;
642
665
  for (const [id, entry] of this.#pending) {
643
666
  clearTimeout(entry.timer);
644
667
  entry.reject(new CDPConnectionError("CDP connection closed", { method: entry.method }));
645
668
  this.#pending.delete(id);
646
669
  }
647
- await this.#transport.close();
670
+ try {
671
+ await this.#transport.close();
672
+ } finally {
673
+ this.#active = false;
674
+ }
648
675
  }
649
676
  #nextId() {
650
677
  this.#messageId += 1;
@@ -652,6 +679,7 @@ var CDPClient = class {
652
679
  }
653
680
  #onClose() {
654
681
  this.#connected = false;
682
+ this.#active = false;
655
683
  for (const [id, entry] of this.#pending) {
656
684
  clearTimeout(entry.timer);
657
685
  entry.reject(new CDPConnectionError("CDP connection closed", { method: entry.method }));
@@ -659,7 +687,8 @@ var CDPClient = class {
659
687
  }
660
688
  }
661
689
  #onError(error) {
662
- if (!this.#connected) for (const [id, entry] of this.#pending) {
690
+ this.#connected = false;
691
+ for (const [id, entry] of this.#pending) {
663
692
  clearTimeout(entry.timer);
664
693
  entry.reject(new CDPConnectionError(`CDP connection failed: ${String(error)}`, {
665
694
  method: entry.method,
@@ -699,17 +728,22 @@ var CDPClient = class {
699
728
  const rawParams = parsed["params"];
700
729
  const params = (0, _orkestrel_contract.isRecord)(rawParams) ? rawParams : Object.freeze({});
701
730
  const globalHandlers = this.#subscriptions.get(method);
702
- if (globalHandlers !== void 0) for (const handler of [...globalHandlers]) handler(params);
731
+ if (globalHandlers !== void 0) this.#dispatch(globalHandlers, params);
703
732
  const eventSessionId = parsed["sessionId"];
704
733
  if ((0, _orkestrel_contract.isString)(eventSessionId)) {
705
734
  const sessionMap = this.#sessionSubscriptions.get(eventSessionId);
706
735
  if (sessionMap !== void 0) {
707
736
  const sessionHandlers = sessionMap.get(method);
708
- if (sessionHandlers !== void 0) for (const handler of [...sessionHandlers]) handler(params);
737
+ if (sessionHandlers !== void 0) this.#dispatch(sessionHandlers, params);
709
738
  }
710
739
  }
711
740
  }
712
741
  }
742
+ #dispatch(handlers, params) {
743
+ for (const handler of [...handlers]) try {
744
+ handler(params);
745
+ } catch {}
746
+ }
713
747
  };
714
748
  //#endregion
715
749
  //#region src/core/BrowserCodegen.ts
@@ -719,11 +753,17 @@ var BrowserCodegen = class {
719
753
  #actions = [];
720
754
  #started = false;
721
755
  #destroyed = false;
756
+ #starting;
757
+ #stopping;
758
+ #shutdown;
722
759
  #emitter;
723
760
  constructor(client, sessionId, options) {
724
761
  this.#client = client;
725
762
  this.#sessionId = sessionId;
726
- this.#emitter = new _orkestrel_emitter.Emitter({ on: options?.on });
763
+ this.#emitter = new _orkestrel_emitter.Emitter({
764
+ on: options?.on,
765
+ error: options?.error
766
+ });
727
767
  }
728
768
  get emitter() {
729
769
  return this.#emitter;
@@ -732,9 +772,56 @@ var BrowserCodegen = class {
732
772
  return this.#started;
733
773
  }
734
774
  async start() {
775
+ if (this.#destroyed) return;
776
+ if (this.#stopping !== void 0) await this.#stopping;
735
777
  if (this.#destroyed) return;
736
778
  if (this.#started) return;
737
- this.#started = true;
779
+ const active = this.#starting;
780
+ if (active !== void 0) {
781
+ await active;
782
+ return;
783
+ }
784
+ const attempt = this.#begin();
785
+ this.#starting = attempt;
786
+ try {
787
+ await attempt;
788
+ } finally {
789
+ if (this.#starting === attempt) this.#starting = void 0;
790
+ }
791
+ }
792
+ async stop() {
793
+ await this.#starting?.catch(() => void 0);
794
+ if (!this.#started) return this.actions();
795
+ const active = this.#stopping;
796
+ if (active !== void 0) return await active;
797
+ const attempt = this.#end();
798
+ this.#stopping = attempt;
799
+ try {
800
+ return await attempt;
801
+ } finally {
802
+ if (this.#stopping === attempt) this.#stopping = void 0;
803
+ }
804
+ }
805
+ actions() {
806
+ return normalizeCodegenActions(this.#actions);
807
+ }
808
+ script(options) {
809
+ return compileCodegenScript(this.actions(), options);
810
+ }
811
+ clear() {
812
+ this.#actions = [];
813
+ this.#emitter.emit("clear");
814
+ }
815
+ destroy() {
816
+ const active = this.#shutdown;
817
+ if (active !== void 0) return active;
818
+ if (this.#destroyed) return Promise.resolve();
819
+ this.#destroyed = true;
820
+ const shutdown = this.#destroy();
821
+ this.#shutdown = shutdown;
822
+ return shutdown;
823
+ }
824
+ async #begin() {
738
825
  this.#client.subscribe("Runtime.bindingCalled", this.#onBindingCalled, this.#sessionId);
739
826
  this.#client.subscribe("Page.frameNavigated", this.#onFrameNavigated, this.#sessionId);
740
827
  try {
@@ -745,36 +832,25 @@ var BrowserCodegen = class {
745
832
  expression: BROWSER_CODEGEN_SOURCE,
746
833
  awaitPromise: true
747
834
  }, this.#sessionId);
835
+ if (this.#destroyed) {
836
+ this.#unsubscribe();
837
+ return;
838
+ }
748
839
  } catch (error) {
749
- this.#client.unsubscribe("Runtime.bindingCalled", this.#onBindingCalled, this.#sessionId);
750
- this.#client.unsubscribe("Page.frameNavigated", this.#onFrameNavigated, this.#sessionId);
751
- this.#started = false;
840
+ this.#unsubscribe();
752
841
  throw error;
753
842
  }
843
+ this.#started = true;
754
844
  this.#emitter.emit("start");
755
845
  }
756
- async stop() {
757
- if (!this.#started) return this.actions();
758
- this.#client.unsubscribe("Runtime.bindingCalled", this.#onBindingCalled, this.#sessionId);
759
- this.#client.unsubscribe("Page.frameNavigated", this.#onFrameNavigated, this.#sessionId);
846
+ async #end() {
847
+ this.#unsubscribe();
760
848
  this.#started = false;
761
849
  const snapshot = this.actions();
762
850
  this.#emitter.emit("stop", snapshot);
763
851
  return snapshot;
764
852
  }
765
- actions() {
766
- return normalizeCodegenActions(this.#actions);
767
- }
768
- script(options) {
769
- return compileCodegenScript(this.actions(), options);
770
- }
771
- clear() {
772
- this.#actions = [];
773
- this.#emitter.emit("clear");
774
- }
775
- async destroy() {
776
- if (this.#destroyed) return;
777
- this.#destroyed = true;
853
+ async #destroy() {
778
854
  try {
779
855
  await this.stop();
780
856
  } catch {}
@@ -794,6 +870,10 @@ var BrowserCodegen = class {
794
870
  this.#actions.push(action);
795
871
  this.#emitter.emit("action", action);
796
872
  };
873
+ #unsubscribe() {
874
+ this.#client.unsubscribe("Runtime.bindingCalled", this.#onBindingCalled, this.#sessionId);
875
+ this.#client.unsubscribe("Page.frameNavigated", this.#onFrameNavigated, this.#sessionId);
876
+ }
797
877
  };
798
878
  //#endregion
799
879
  //#region src/core/BrowserPage.ts
@@ -805,16 +885,26 @@ var BrowserPage = class {
805
885
  #url = "about:blank";
806
886
  #closed = false;
807
887
  #codegen;
808
- #destroyHandler;
888
+ #codegenStart;
889
+ #navigation;
890
+ #closing;
891
+ #releasing;
892
+ #loadEvent;
893
+ #loadTimer;
894
+ #loadResolve;
895
+ #loadReject;
896
+ #destroyHandler = (params) => {
897
+ if (!(0, _orkestrel_contract.isString)(params["targetId"]) || params["targetId"] !== this.#targetId) return;
898
+ this.#closed = true;
899
+ this.#release().catch(() => void 0);
900
+ };
901
+ #loadHandler = () => this.#resolveLoad();
809
902
  constructor(client, targetId, sessionId, writer, url) {
810
903
  this.#client = client;
811
904
  this.#targetId = targetId;
812
905
  this.#sessionId = sessionId;
813
906
  this.#writer = writer;
814
907
  if (url !== void 0) this.#url = url;
815
- this.#destroyHandler = (params) => {
816
- if ((0, _orkestrel_contract.isString)(params["targetId"]) && params["targetId"] === this.#targetId) this.#closed = true;
817
- };
818
908
  this.#client.subscribe("Target.targetDestroyed", this.#destroyHandler);
819
909
  }
820
910
  get url() {
@@ -824,46 +914,41 @@ var BrowserPage = class {
824
914
  return this.#closed;
825
915
  }
826
916
  async title() {
917
+ this.#assertOpen();
827
918
  const result = await this.#evaluate("document.title");
828
- return typeof result === "string" ? result : "";
919
+ return this.#requireString(result, "Document title");
829
920
  }
830
921
  async navigate(url, options) {
831
- const timeout = options?.timeout ?? 3e4;
832
- const condition = options?.condition ?? "load";
833
- const wait = this.#waitForLoadEvent(condition, timeout);
834
- const waitOutcome = wait.promise.then(() => ({ ok: true }), (error) => ({
835
- ok: false,
836
- error
837
- }));
922
+ this.#assertOpen();
923
+ while (this.#navigation !== void 0) await this.#navigation.catch(() => void 0);
924
+ this.#assertOpen();
925
+ const navigation = this.#navigate(url, options);
926
+ this.#navigation = navigation;
838
927
  try {
839
- const result = await this.#client.send("Page.navigate", { url }, this.#sessionId, timeout);
840
- if ((0, _orkestrel_contract.isRecord)(result) && (0, _orkestrel_contract.isString)(result["errorText"])) throw new BrowserError(`Navigation failed: ${result["errorText"]}`);
841
- const outcome = await waitOutcome;
842
- if (!outcome.ok) throw outcome.error;
843
- } catch (error) {
844
- wait.cancel();
845
- await this.#stopLoading(Math.min(timeout, BROWSER_STOP_LOADING_TIMEOUT_MS));
846
- throw error;
928
+ await navigation;
929
+ } finally {
930
+ if (this.#navigation === navigation) this.#navigation = void 0;
847
931
  }
848
- const currentUrl = await this.#evaluate("location.href");
849
- this.#url = typeof currentUrl === "string" ? currentUrl : url;
850
932
  }
851
933
  async content() {
934
+ this.#assertOpen();
852
935
  const [title, html, text, currentUrl] = await Promise.all([
853
936
  this.#evaluate("document.title"),
854
937
  this.#evaluate(guardEvaluateExpression("document.documentElement.outerHTML", BROWSER_RESULT_LIMIT)),
855
938
  this.#evaluate(guardEvaluateExpression("document.body ? document.body.innerText : \"\"", BROWSER_RESULT_LIMIT)),
856
939
  this.#evaluate("location.href")
857
940
  ]);
858
- if (typeof currentUrl === "string") this.#url = currentUrl;
941
+ const url = this.#requireString(currentUrl, "Document URL");
942
+ this.#url = url;
859
943
  return {
860
- url: this.#url,
861
- title: typeof title === "string" ? title : "",
862
- html: typeof html === "string" ? html : "",
863
- text: typeof text === "string" ? text : ""
944
+ url,
945
+ title: this.#requireString(title, "Document title"),
946
+ html: this.#requireString(html, "Document HTML"),
947
+ text: this.#requireString(text, "Document text")
864
948
  };
865
949
  }
866
950
  async screenshot(options) {
951
+ this.#assertOpen();
867
952
  const format = options?.type ?? "png";
868
953
  const params = { format };
869
954
  if (options?.quality !== void 0 && format === "jpeg") params["quality"] = options.quality;
@@ -871,9 +956,9 @@ var BrowserPage = class {
871
956
  const metrics = await this.#client.send("Page.getLayoutMetrics", void 0, this.#sessionId);
872
957
  if ((0, _orkestrel_contract.isRecord)(metrics) && (0, _orkestrel_contract.isRecord)(metrics["contentSize"])) {
873
958
  const contentSize = metrics["contentSize"];
874
- const width = typeof contentSize["width"] === "number" ? contentSize["width"] : 0;
875
- const height = typeof contentSize["height"] === "number" ? contentSize["height"] : 0;
876
- if (width > 0 && height > 0) {
959
+ const width = contentSize["width"];
960
+ const height = contentSize["height"];
961
+ if (typeof width === "number" && typeof height === "number" && width > 0 && height > 0) {
877
962
  params["clip"] = {
878
963
  x: 0,
879
964
  y: 0,
@@ -895,11 +980,13 @@ var BrowserPage = class {
895
980
  };
896
981
  }
897
982
  async click(selector, options) {
983
+ this.#assertOpen();
898
984
  const timeout = options?.timeout ?? 3e4;
899
985
  await this.#waitForSelector(selector, timeout);
900
986
  await this.#evaluate(`(() => { const el = document.querySelector(${JSON.stringify(selector)}); if (el) { el.scrollIntoView({ block: 'center' }); el.click(); } else { throw new Error('Element not found: ' + ${JSON.stringify(selector)}); } })()`);
901
987
  }
902
988
  async fill(selector, value, options) {
989
+ this.#assertOpen();
903
990
  const timeout = options?.timeout ?? 3e4;
904
991
  await this.#waitForSelector(selector, timeout);
905
992
  await this.#evaluate(`(() => {
@@ -917,6 +1004,7 @@ var BrowserPage = class {
917
1004
  })()`);
918
1005
  }
919
1006
  async select(selector, values, options) {
1007
+ this.#assertOpen();
920
1008
  const timeout = options?.timeout ?? 3e4;
921
1009
  await this.#waitForSelector(selector, timeout);
922
1010
  const valuesJson = JSON.stringify([...values]);
@@ -931,9 +1019,11 @@ var BrowserPage = class {
931
1019
  })()`);
932
1020
  }
933
1021
  async evaluate(expression) {
1022
+ this.#assertOpen();
934
1023
  return this.#evaluate(guardEvaluateExpression(expression, BROWSER_RESULT_LIMIT));
935
1024
  }
936
1025
  async wait(selector, options) {
1026
+ this.#assertOpen();
937
1027
  const timeout = options?.timeout ?? 3e4;
938
1028
  await this.#waitForSelector(selector, timeout);
939
1029
  }
@@ -941,6 +1031,7 @@ var BrowserPage = class {
941
1031
  return (await this.frames()).find((frame) => frame.name === name || frame.url === name);
942
1032
  }
943
1033
  async frames() {
1034
+ this.#assertOpen();
944
1035
  const result = await this.#client.send("Page.getFrameTree", void 0, this.#sessionId);
945
1036
  if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isRecord)(result["frameTree"])) return [];
946
1037
  const frames = [];
@@ -948,15 +1039,88 @@ var BrowserPage = class {
948
1039
  return frames;
949
1040
  }
950
1041
  async codegen(options) {
1042
+ this.#assertOpen();
951
1043
  if (this.#codegen !== void 0) return this.#codegen;
1044
+ const active = this.#codegenStart;
1045
+ if (active !== void 0) return await active;
1046
+ const attempt = this.#startCodegen(options);
1047
+ this.#codegenStart = attempt;
1048
+ try {
1049
+ return await attempt;
1050
+ } finally {
1051
+ if (this.#codegenStart === attempt) this.#codegenStart = void 0;
1052
+ }
1053
+ }
1054
+ destroy() {
1055
+ const active = this.#closing;
1056
+ if (active !== void 0) return active;
1057
+ this.#closed = true;
1058
+ const closing = this.#destroy();
1059
+ this.#closing = closing;
1060
+ return closing;
1061
+ }
1062
+ close() {
1063
+ const active = this.#closing;
1064
+ if (active !== void 0) return active;
1065
+ if (this.#closed) {
1066
+ const closing = this.#release();
1067
+ this.#closing = closing;
1068
+ return closing;
1069
+ }
1070
+ this.#closed = true;
1071
+ const closing = this.#close();
1072
+ this.#closing = closing;
1073
+ return closing;
1074
+ }
1075
+ async #navigate(url, options) {
1076
+ const timeout = options?.timeout ?? 3e4;
1077
+ const condition = options?.condition ?? "load";
1078
+ const wait = this.#waitForLoadEvent(condition, timeout);
1079
+ wait.catch(() => void 0);
1080
+ try {
1081
+ const result = await this.#client.send("Page.navigate", { url }, this.#sessionId, timeout);
1082
+ if ((0, _orkestrel_contract.isRecord)(result) && (0, _orkestrel_contract.isString)(result["errorText"])) throw new BrowserError(`Navigation failed: ${result["errorText"]}`);
1083
+ await wait;
1084
+ } catch (error) {
1085
+ this.#cancelLoad();
1086
+ await this.#stopLoading(Math.min(timeout, BROWSER_STOP_LOADING_TIMEOUT_MS));
1087
+ throw error;
1088
+ }
1089
+ const currentUrl = await this.#evaluate("location.href");
1090
+ this.#url = this.#requireString(currentUrl, "Navigation URL");
1091
+ }
1092
+ async #startCodegen(options) {
952
1093
  const codegen = new BrowserCodegen(this.#client, this.#sessionId, options);
953
1094
  await codegen.start();
1095
+ if (this.#closed) {
1096
+ await codegen.destroy();
1097
+ throw new BrowserError("Browser page is closed");
1098
+ }
954
1099
  this.#codegen = codegen;
955
1100
  return codegen;
956
1101
  }
957
- async close() {
958
- if (this.#closed) return;
959
- this.#closed = true;
1102
+ async #destroy() {
1103
+ await this.#release();
1104
+ try {
1105
+ await this.#client.send("Target.detachFromTarget", { sessionId: this.#sessionId });
1106
+ } catch {}
1107
+ }
1108
+ async #close() {
1109
+ await this.#release();
1110
+ try {
1111
+ await this.#client.send("Target.closeTarget", { targetId: this.#targetId });
1112
+ } catch {}
1113
+ }
1114
+ #release() {
1115
+ const active = this.#releasing;
1116
+ if (active !== void 0) return active;
1117
+ const release = this.#releaseResources();
1118
+ this.#releasing = release;
1119
+ return release;
1120
+ }
1121
+ async #releaseResources() {
1122
+ this.#cancelLoad();
1123
+ await this.#codegenStart?.catch(() => void 0);
960
1124
  if (this.#codegen !== void 0) {
961
1125
  try {
962
1126
  await this.#codegen.destroy();
@@ -964,9 +1128,6 @@ var BrowserPage = class {
964
1128
  this.#codegen = void 0;
965
1129
  }
966
1130
  this.#client.unsubscribe("Target.targetDestroyed", this.#destroyHandler);
967
- try {
968
- await this.#client.send("Target.closeTarget", { targetId: this.#targetId });
969
- } catch {}
970
1131
  }
971
1132
  #flattenFrameTree(node, out) {
972
1133
  const frame = node["frame"];
@@ -1025,35 +1186,43 @@ var BrowserPage = class {
1025
1186
  }
1026
1187
  #waitForLoadEvent(condition, timeout) {
1027
1188
  const eventName = condition === "domcontentloaded" ? "Page.domContentEventFired" : "Page.loadEventFired";
1028
- let settled = false;
1029
- let timer;
1030
- let handler;
1031
- const promise = new Promise((resolve, reject) => {
1032
- timer = setTimeout(() => {
1033
- if (settled) return;
1034
- settled = true;
1035
- this.#client.unsubscribe(eventName, handler, this.#sessionId);
1036
- reject(new BrowserError(`Navigation timeout after ${timeout}ms`));
1037
- }, timeout);
1038
- handler = () => {
1039
- if (settled) return;
1040
- settled = true;
1041
- clearTimeout(timer);
1042
- this.#client.unsubscribe(eventName, handler, this.#sessionId);
1043
- resolve();
1044
- };
1045
- this.#client.subscribe(eventName, handler, this.#sessionId);
1046
- });
1047
- const cancel = () => {
1048
- if (settled) return;
1049
- settled = true;
1050
- clearTimeout(timer);
1051
- this.#client.unsubscribe(eventName, handler, this.#sessionId);
1052
- };
1053
- return {
1054
- promise,
1055
- cancel
1056
- };
1189
+ const deferred = Promise.withResolvers();
1190
+ this.#loadEvent = eventName;
1191
+ this.#loadResolve = deferred.resolve;
1192
+ this.#loadReject = deferred.reject;
1193
+ this.#loadTimer = setTimeout(() => {
1194
+ this.#rejectLoad(new BrowserError(`Navigation timeout after ${timeout}ms`));
1195
+ }, timeout);
1196
+ this.#client.subscribe(eventName, this.#loadHandler, this.#sessionId);
1197
+ return deferred.promise;
1198
+ }
1199
+ #resolveLoad() {
1200
+ const resolve = this.#loadResolve;
1201
+ this.#clearLoad();
1202
+ resolve?.();
1203
+ }
1204
+ #rejectLoad(error) {
1205
+ const reject = this.#loadReject;
1206
+ this.#clearLoad();
1207
+ reject?.(error);
1208
+ }
1209
+ #cancelLoad() {
1210
+ this.#rejectLoad(new BrowserError("Navigation cancelled"));
1211
+ }
1212
+ #clearLoad() {
1213
+ if (this.#loadTimer !== void 0) clearTimeout(this.#loadTimer);
1214
+ if (this.#loadEvent !== void 0) this.#client.unsubscribe(this.#loadEvent, this.#loadHandler, this.#sessionId);
1215
+ this.#loadEvent = void 0;
1216
+ this.#loadTimer = void 0;
1217
+ this.#loadResolve = void 0;
1218
+ this.#loadReject = void 0;
1219
+ }
1220
+ #requireString(value, field) {
1221
+ if (typeof value === "string") return value;
1222
+ throw new BrowserError(`${field} failed: no string value returned`);
1223
+ }
1224
+ #assertOpen() {
1225
+ if (this.#closed) throw new BrowserError("Browser page is closed");
1057
1226
  }
1058
1227
  };
1059
1228
  //#endregion
@@ -1064,6 +1233,10 @@ var BrowserContext = class {
1064
1233
  #viewport;
1065
1234
  #writer;
1066
1235
  #pages = /* @__PURE__ */ new Map();
1236
+ #creating = /* @__PURE__ */ new Set();
1237
+ #syncing;
1238
+ #shutdown;
1239
+ #closed = false;
1067
1240
  constructor(client, id, viewport, writer) {
1068
1241
  this.#client = client;
1069
1242
  this.#id = id;
@@ -1075,78 +1248,165 @@ var BrowserContext = class {
1075
1248
  }
1076
1249
  page(index) {
1077
1250
  const i = index ?? 0;
1078
- const values = [...this.#pages.values()];
1079
- return i >= 0 && i < values.length ? values[i] : void 0;
1251
+ const pages = [...this.#pages.values()];
1252
+ return i >= 0 && i < pages.length ? pages[i] : void 0;
1080
1253
  }
1081
1254
  pages() {
1082
1255
  return [...this.#pages.values()];
1083
1256
  }
1084
1257
  async create(options) {
1085
- const createParams = { url: "about:blank" };
1086
- if (this.#id !== void 0) createParams["browserContextId"] = this.#id;
1087
- const result = await this.#client.send("Target.createTarget", createParams);
1088
- if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["targetId"])) throw new Error("Failed to create new browser target");
1258
+ if (this.#closed) throw new BrowserError("Browser context is closed");
1259
+ const attempt = this.#create(options);
1260
+ this.#creating.add(attempt);
1261
+ try {
1262
+ return await attempt;
1263
+ } finally {
1264
+ this.#creating.delete(attempt);
1265
+ }
1266
+ }
1267
+ async sync(targets) {
1268
+ while (this.#syncing !== void 0) await this.#syncing;
1269
+ if (this.#closed) throw new BrowserError("Browser context is closed");
1270
+ const attempt = this.#sync(targets);
1271
+ this.#syncing = attempt;
1272
+ try {
1273
+ await attempt;
1274
+ } finally {
1275
+ if (this.#syncing === attempt) this.#syncing = void 0;
1276
+ }
1277
+ }
1278
+ destroy() {
1279
+ const active = this.#shutdown;
1280
+ if (active !== void 0) return active;
1281
+ this.#closed = true;
1282
+ const shutdown = this.#destroyResources();
1283
+ this.#shutdown = shutdown;
1284
+ return shutdown;
1285
+ }
1286
+ close() {
1287
+ const active = this.#shutdown;
1288
+ if (active !== void 0) return active;
1289
+ this.#closed = true;
1290
+ const shutdown = this.#closeResources();
1291
+ this.#shutdown = shutdown;
1292
+ return shutdown;
1293
+ }
1294
+ async #create(options) {
1295
+ const result = await this.#client.send("Target.createTarget", {
1296
+ url: "about:blank",
1297
+ ...this.#id === void 0 ? {} : { browserContextId: this.#id }
1298
+ });
1299
+ if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["targetId"])) throw new BrowserError("Failed to create new browser target");
1089
1300
  const targetId = result["targetId"];
1090
- const attachResult = await this.#client.send("Target.attachToTarget", {
1301
+ let page;
1302
+ try {
1303
+ const viewport = options?.viewport ?? this.#viewport;
1304
+ page = await this.#attach(targetId, options?.url ?? "about:blank", viewport);
1305
+ if (options?.url !== void 0 && options.url !== "about:blank") await page.navigate(options.url, { timeout: options.timeout });
1306
+ if (this.#closed) throw new BrowserError("Browser context closed during page creation");
1307
+ this.#pages.set(targetId, page);
1308
+ return page;
1309
+ } catch (error) {
1310
+ if (page !== void 0) await page.close();
1311
+ else await this.#closeTarget(targetId);
1312
+ throw error;
1313
+ }
1314
+ }
1315
+ async #sync(targets) {
1316
+ const pageTargets = targets.filter((target) => target.type === "page");
1317
+ const targetIds = new Set(pageTargets.map((target) => target.id));
1318
+ for (const [id, page] of this.#pages) {
1319
+ if (targetIds.has(id)) continue;
1320
+ await page.destroy();
1321
+ this.#pages.delete(id);
1322
+ }
1323
+ for (const target of pageTargets) {
1324
+ if (this.#closed || this.#pages.has(target.id)) continue;
1325
+ try {
1326
+ const page = await this.#reattach(target.id, target.url, this.#viewport);
1327
+ if (this.#closed) {
1328
+ await page.destroy();
1329
+ continue;
1330
+ }
1331
+ this.#pages.set(target.id, page);
1332
+ } catch {}
1333
+ }
1334
+ }
1335
+ async #attach(targetId, url, viewport) {
1336
+ let sessionId;
1337
+ try {
1338
+ sessionId = await this.#openSession(targetId);
1339
+ await this.#enableSession(sessionId);
1340
+ if (viewport !== void 0) await this.#applyViewport(sessionId, viewport);
1341
+ return new BrowserPage(this.#client, targetId, sessionId, this.#writer, url);
1342
+ } catch (error) {
1343
+ if (sessionId !== void 0) await this.#detachSession(sessionId);
1344
+ throw error;
1345
+ }
1346
+ }
1347
+ async #reattach(targetId, url, viewport) {
1348
+ let sessionId;
1349
+ try {
1350
+ sessionId = await this.#openSession(targetId);
1351
+ await this.#enableSession(sessionId);
1352
+ if (viewport !== void 0) await this.#tryViewport(sessionId, viewport);
1353
+ return new BrowserPage(this.#client, targetId, sessionId, this.#writer, url);
1354
+ } catch (error) {
1355
+ if (sessionId !== void 0) await this.#detachSession(sessionId);
1356
+ throw error;
1357
+ }
1358
+ }
1359
+ async #destroyResources() {
1360
+ await this.#settle();
1361
+ for (const page of this.#pages.values()) await page.destroy();
1362
+ this.#pages.clear();
1363
+ }
1364
+ async #closeResources() {
1365
+ await this.#settle();
1366
+ for (const page of this.#pages.values()) await page.close();
1367
+ this.#pages.clear();
1368
+ if (this.#id === void 0) return;
1369
+ try {
1370
+ await this.#client.send("Target.disposeBrowserContext", { browserContextId: this.#id });
1371
+ } catch {}
1372
+ }
1373
+ async #settle() {
1374
+ await Promise.allSettled([...this.#creating]);
1375
+ await this.#syncing?.catch(() => void 0);
1376
+ }
1377
+ async #openSession(targetId) {
1378
+ const result = await this.#client.send("Target.attachToTarget", {
1091
1379
  targetId,
1092
1380
  flatten: true
1093
1381
  });
1094
- if (!(0, _orkestrel_contract.isRecord)(attachResult) || !(0, _orkestrel_contract.isString)(attachResult["sessionId"])) throw new Error("Failed to attach to browser target");
1095
- const sessionId = attachResult["sessionId"];
1382
+ if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["sessionId"])) throw new BrowserError("Failed to attach to browser target");
1383
+ return result["sessionId"];
1384
+ }
1385
+ async #enableSession(sessionId) {
1096
1386
  await this.#client.send("Page.enable", void 0, sessionId);
1097
1387
  await this.#client.send("Runtime.enable", void 0, sessionId);
1098
- const viewport = options?.viewport ?? this.#viewport;
1099
- if (viewport !== void 0) await this.#client.send("Emulation.setDeviceMetricsOverride", {
1388
+ }
1389
+ async #applyViewport(sessionId, viewport) {
1390
+ await this.#client.send("Emulation.setDeviceMetricsOverride", {
1100
1391
  width: viewport.width,
1101
1392
  height: viewport.height,
1102
1393
  deviceScaleFactor: 1,
1103
1394
  mobile: false
1104
1395
  }, sessionId);
1105
- const page = new BrowserPage(this.#client, targetId, sessionId, this.#writer, options?.url);
1106
- if (options?.url !== void 0 && options.url !== "about:blank") await page.navigate(options.url, { timeout: options.timeout });
1107
- this.#pages.set(targetId, page);
1108
- return page;
1109
1396
  }
1110
- async sync(targets) {
1111
- const pageTargets = targets.filter((t) => t.type === "page");
1112
- const targetIds = new Set(pageTargets.map((t) => t.id));
1113
- for (const [id, page] of this.#pages) {
1114
- if (targetIds.has(id)) continue;
1115
- try {
1116
- await page.close();
1117
- } catch {}
1118
- this.#pages.delete(id);
1119
- }
1120
- for (const target of pageTargets) {
1121
- if (this.#pages.has(target.id)) continue;
1122
- try {
1123
- const attachResult = await this.#client.send("Target.attachToTarget", {
1124
- targetId: target.id,
1125
- flatten: true
1126
- });
1127
- if (!(0, _orkestrel_contract.isRecord)(attachResult) || !(0, _orkestrel_contract.isString)(attachResult["sessionId"])) continue;
1128
- const sessionId = attachResult["sessionId"];
1129
- await this.#client.send("Page.enable", void 0, sessionId);
1130
- await this.#client.send("Runtime.enable", void 0, sessionId);
1131
- if (this.#viewport !== void 0) try {
1132
- await this.#client.send("Emulation.setDeviceMetricsOverride", {
1133
- width: this.#viewport.width,
1134
- height: this.#viewport.height,
1135
- deviceScaleFactor: 1,
1136
- mobile: false
1137
- }, sessionId);
1138
- } catch {}
1139
- this.#pages.set(target.id, new BrowserPage(this.#client, target.id, sessionId, this.#writer, target.url));
1140
- } catch {}
1141
- }
1397
+ async #tryViewport(sessionId, viewport) {
1398
+ try {
1399
+ await this.#applyViewport(sessionId, viewport);
1400
+ } catch {}
1142
1401
  }
1143
- async close() {
1144
- for (const page of this.#pages.values()) try {
1145
- await page.close();
1402
+ async #closeTarget(targetId) {
1403
+ try {
1404
+ await this.#client.send("Target.closeTarget", { targetId });
1146
1405
  } catch {}
1147
- this.#pages.clear();
1148
- if (this.#id !== void 0) try {
1149
- await this.#client.send("Target.disposeBrowserContext", { browserContextId: this.#id });
1406
+ }
1407
+ async #detachSession(sessionId) {
1408
+ try {
1409
+ await this.#client.send("Target.detachFromTarget", { sessionId });
1150
1410
  } catch {}
1151
1411
  }
1152
1412
  };