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