flashwright 1.64.1-flashwright.1 → 1.64.1-flashwright.2

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.
package/README.md CHANGED
@@ -13,4 +13,6 @@ npx playwright install chromium
13
13
 
14
14
  Without the test runner, `playwright` is `flashwright` and `playwright-core` is `flashwright-core`. See the [project page](https://github.com/xcodebeast/flashwright) for benchmarks, speed options and the differences from Playwright.
15
15
 
16
+ Re-running tests, like an agent checking its work? Outside CI, a warm session keeps the browser between runs, so one test re-runs in about 0.1 s instead of 0.7 s with Playwright ([warm session](https://github.com/xcodebeast/flashwright#warm-session); `FLASHWRIGHT_SESSION=0` turns it off).
17
+
16
18
  Flashwright is an independent fork and is not affiliated with Microsoft. Apache-2.0, like Playwright.
package/cli.js CHANGED
@@ -24,16 +24,14 @@ if (nodeModule.enableCompileCache && !process.env.NODE_DISABLE_COMPILE_CACHE) {
24
24
  process.env.NODE_COMPILE_CACHE = directory;
25
25
  }
26
26
 
27
- // Flashwright: compile Playwright's bundles with code caches that include their lazily compiled
28
- // functions (see codeCache.ts).
29
- require('./lib/codeCache').installCodeCache(process.argv[2] === 'test' ? 'runner' : 'cli');
30
-
31
- // Flashwright: start the first test worker while the CLI loads.
32
- require('./lib/runner/workerStandby').prespawnWorkerForTestCommand(process.argv);
33
- // Flashwright: `cli` commands only forward to a browser session; skip loading every command.
34
- if (process.argv[2] === 'cli') {
35
- require('playwright-core/lib/coreBundle').runCliClient();
36
- } else {
37
- const { program } = require('./lib/program');
38
- program.parse(process.argv);
39
- }
27
+ // Flashwright: runs the command here, or in a runner process that the warm session prepared
28
+ // ahead of time (see session.ts). `cli` commands only forward to a browser session.
29
+ require('./lib/runner/session').runCli(process.argv, {
30
+ // Compiles Playwright's bundles with code caches that include their lazily compiled functions.
31
+ installCodeCache: role => require('./lib/codeCache').installCodeCache(role),
32
+ // Starts the first test workers while the CLI loads.
33
+ prespawnWorkers: argv => require('./lib/runner/workerStandby').prespawnWorkerForTestCommand(argv),
34
+ retargetWorkers: argv => require('./lib/runner/workerStandby').retargetStandbys(argv),
35
+ loadProgram: () => require('./lib/program').program,
36
+ runCliClient: () => require('playwright-core/lib/coreBundle').runCliClient(),
37
+ });
@@ -37,6 +37,7 @@ var import_fs = __toESM(require("fs"));
37
37
  var import_path = __toESM(require("path"));
38
38
  var import_common = require("../common");
39
39
  var import_runner = require("../runner");
40
+ var import_session = require("../runner/session");
40
41
  const { gracefullyProcessExitDoNotHang } = require("playwright-core/lib/coreBundle").utils;
41
42
  async function runTests(args, opts) {
42
43
  const cliOverrides = overridesFromOptions(opts);
@@ -90,7 +91,7 @@ async function runTests(args, opts) {
90
91
  }
91
92
  const status = await import_runner.testRunner.runAllTestsWithConfig(config, options);
92
93
  const exitCode = status === "interrupted" ? 130 : status === "passed" ? 0 : 1;
93
- gracefullyProcessExitDoNotHang(exitCode);
94
+ gracefullyProcessExitDoNotHang(exitCode, () => (0, import_session.flushSessionBrowsers)(exitCode));
94
95
  }
95
96
  async function runTestServerAction(opts) {
96
97
  const host = opts.host;
package/lib/index.js CHANGED
@@ -213,6 +213,7 @@ const playwrightFixtures = {
213
213
  playwright._defaultLaunchOptions = options;
214
214
  await use(options);
215
215
  playwright._defaultLaunchOptions = void 0;
216
+ await (0, import_speculativeBrowser.returnSpeculativeBrowser)();
216
217
  }, { scope: "worker", auto: true, box: true }],
217
218
  browser: [async ({ playwright, browserName, _browserOptions, connectOptions, _recycleContext }, use, workerInfo) => {
218
219
  if (!["chromium", "firefox", "webkit"].includes(browserName))
@@ -239,6 +240,8 @@ const playwrightFixtures = {
239
240
  if (process.env.PLAYWRIGHT_DASHBOARD)
240
241
  await browser.bind(`worker-${workerInfo.parallelIndex}`);
241
242
  await use(browser);
243
+ if (await (0, import_speculativeBrowser.handBrowsersToSession)(browser, true))
244
+ return;
242
245
  if (browser.contexts().length)
243
246
  await browser.close({ reason: "Test ended." });
244
247
  else
@@ -1896,6 +1896,7 @@ var import_fs3 = __toESM(require("fs"));
1896
1896
  var import_child_process = __toESM(require("child_process"));
1897
1897
  var import_events = require("events");
1898
1898
  var import_workerStandby = require("./workerStandby");
1899
+ var import_session = require("./session");
1899
1900
  var debug3 = require("playwright-core/lib/utilsBundle").debug;
1900
1901
  var { assert } = require("playwright-core/lib/coreBundle").iso;
1901
1902
  var { monotonicTime: monotonicTime3, timeOrigin } = require("playwright-core/lib/coreBundle").iso;
@@ -1944,7 +1945,10 @@ var ProcessHost = class extends import_events.EventEmitter {
1944
1945
  });
1945
1946
  this.process.on("error", (e) => {
1946
1947
  });
1947
- this.process.on("message", (message) => {
1948
+ (0, import_session.trackSessionWorker)(this.process);
1949
+ this.process.on("message", (message, handle) => {
1950
+ if ((0, import_session.receiveWorkerMessage)(this.process, message, handle))
1951
+ return;
1948
1952
  if (debug3.enabled("pw:test:protocol"))
1949
1953
  debug3("pw:test:protocol")("\u25C0 RECV " + JSON.stringify(message));
1950
1954
  if (message.method === "__env_produced__") {
@@ -1,5 +1,5 @@
1
1
  # packages/playwright/lib/runner/index.js
2
- # total: 321.2 KB
2
+ # total: 321.4 KB
3
3
 
4
4
  ## Inlined (49)
5
5
  1.3 KB packages/playwright/src/isomorphic/events.ts
@@ -35,7 +35,7 @@
35
35
  1.7 KB packages/playwright/src/runner/lastRun.ts
36
36
  1.6 KB packages/playwright/src/runner/loaderHost.ts
37
37
  13.3 KB packages/playwright/src/runner/loadUtils.ts
38
- 6.3 KB packages/playwright/src/runner/processHost.ts
38
+ 6.5 KB packages/playwright/src/runner/processHost.ts
39
39
  7.9 KB packages/playwright/src/runner/projectUtils.ts
40
40
  5.5 KB packages/playwright/src/runner/rebase.ts
41
41
  3.4 KB packages/playwright/src/runner/reporters.ts
@@ -52,12 +52,13 @@
52
52
  13.0 KB packages/playwright/src/runner/watchMode.ts
53
53
  2.1 KB packages/playwright/src/runner/workerHost.ts
54
54
 
55
- ## External (9)
55
+ ## External (10)
56
56
  ../common
57
57
  ../loader/loaderProcessEntry.js
58
58
  ../transform/babelBundle
59
59
  ../util
60
60
  ../worker/workerProcessEntry.js
61
+ ./session
61
62
  ./workerStandby
62
63
  playwright-core
63
64
  playwright-core/lib/coreBundle
@@ -0,0 +1,723 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // packages/playwright/src/runner/session.ts
31
+ var session_exports = {};
32
+ __export(session_exports, {
33
+ flushSessionBrowsers: () => flushSessionBrowsers,
34
+ receiveWorkerMessage: () => receiveWorkerMessage,
35
+ requestSessionBrowser: () => requestSessionBrowser,
36
+ runCli: () => runCli,
37
+ sessionStandbyEnv: () => sessionStandbyEnv,
38
+ trackSessionWorker: () => trackSessionWorker
39
+ });
40
+ module.exports = __toCommonJS(session_exports);
41
+ var import_child_process = __toESM(require("child_process"));
42
+ var import_crypto = __toESM(require("crypto"));
43
+ var import_fs = __toESM(require("fs"));
44
+ var import_inspector = __toESM(require("inspector"));
45
+ var import_net = __toESM(require("net"));
46
+ var import_os = __toESM(require("os"));
47
+ var import_path = __toESM(require("path"));
48
+ var import_url = __toESM(require("url"));
49
+ var kIdleTimeout = (Number(process.env.FLASHWRIGHT_SESSION_TIMEOUT) || 5 * 60) * 1e3;
50
+ var kVolatileEnv = /* @__PURE__ */ new Set(["_", "PWD", "OLDPWD", "SHLVL", "COLUMNS", "LINES"]);
51
+ var kInteractiveArgs = ["--ui", "--ui-host", "--ui-port", "--debug", "--list", "-h", "--help"];
52
+ var kBrowserMessage = "__flashwright_browser__";
53
+ var kBrowserRequest = "__flashwright_browser_request__";
54
+ var kReturnBrowser = "__flashwright_return_browser__";
55
+ var kBrowserReturned = "__flashwright_browser_returned__";
56
+ var kMaxBrowsers = 8;
57
+ function runCli(argv, hooks) {
58
+ if (argv[2] === "cli") {
59
+ hooks.installCodeCache("cli");
60
+ hooks.runCliClient();
61
+ return;
62
+ }
63
+ if (process.env.FLASHWRIGHT_SESSION_ROLE === "warm") {
64
+ runWarmProcess(hooks).catch((error) => {
65
+ process.stderr.write(`Flashwright session: ${error.stack || error}
66
+ `);
67
+ process.exit(1);
68
+ });
69
+ return;
70
+ }
71
+ if (argv[2] === "test" && sessionRequested() && !argv.some((arg) => kInteractiveArgs.includes(arg.split("=")[0]))) {
72
+ const request = createRequest(argv);
73
+ runInSession(request).then((outcome) => runLocally(argv, hooks, outcome === "start" ? request : void 0), () => runLocally(argv, hooks));
74
+ return;
75
+ }
76
+ runLocally(argv, hooks);
77
+ }
78
+ function runLocally(argv, hooks, startSessionWith) {
79
+ hooks.installCodeCache(argv[2] === "test" ? "runner" : "cli");
80
+ if (startSessionWith)
81
+ startSession(startSessionWith);
82
+ if (argv[2] === "test")
83
+ hooks.prespawnWorkers(argv);
84
+ hooks.loadProgram().parse(argv);
85
+ }
86
+ function sessionRequested() {
87
+ if (process.env.PWDEBUG || process.env.PWTEST_WATCH || import_inspector.default.url() || process.platform === "win32")
88
+ return false;
89
+ const value = process.env.FLASHWRIGHT_SESSION;
90
+ if (value)
91
+ return value !== "0" && value !== "false";
92
+ return !process.env.CI;
93
+ }
94
+ function buildStamp() {
95
+ const files = ["program.js", "common/index.js", "runner/index.js", "runner/session.js", "worker/workerProcessEntry.js"].map((file) => import_path.default.join(__dirname, "..", file));
96
+ try {
97
+ files.push(require.resolve("playwright-core/lib/coreBundle"));
98
+ } catch {
99
+ }
100
+ return files.map((file) => {
101
+ try {
102
+ const stat = import_fs.default.statSync(file);
103
+ return `${file}:${stat.size}:${stat.mtimeMs}`;
104
+ } catch {
105
+ return file;
106
+ }
107
+ }).join("|");
108
+ }
109
+ function terminalProfile() {
110
+ return {
111
+ stdout: process.stdout.isTTY ? `${process.stdout.columns || 0}x${process.stdout.rows || 0}` : "false",
112
+ stdinTTY: !!process.stdin.isTTY
113
+ };
114
+ }
115
+ function stableEnv(env) {
116
+ const result = {};
117
+ for (const [key, value] of Object.entries(env)) {
118
+ if (value !== void 0 && !kVolatileEnv.has(key) && !key.startsWith("FLASHWRIGHT_SESSION_"))
119
+ result[key] = value;
120
+ }
121
+ return result;
122
+ }
123
+ function createRequest(argv) {
124
+ return {
125
+ type: "run",
126
+ argv,
127
+ cwd: process.cwd(),
128
+ env: stableEnv(process.env),
129
+ execPath: process.execPath,
130
+ execArgv: process.execArgv,
131
+ cliPath: argv[1],
132
+ build: buildStamp(),
133
+ profile: terminalProfile()
134
+ };
135
+ }
136
+ function warmKey(request) {
137
+ const env = Object.entries(stableEnv(request.env)).sort(([a], [b]) => a < b ? -1 : 1);
138
+ return import_crypto.default.createHash("sha1").update(JSON.stringify([env, request.execPath, request.execArgv, request.cliPath, request.build, request.profile])).digest("hex");
139
+ }
140
+ function sessionSocketPath(cwd) {
141
+ const uid = import_os.default.userInfo().uid;
142
+ const directory = import_path.default.join(import_os.default.tmpdir(), `fw-${uid}`);
143
+ try {
144
+ import_fs.default.mkdirSync(directory, { mode: 448 });
145
+ } catch {
146
+ }
147
+ try {
148
+ const stat = import_fs.default.statSync(directory);
149
+ if (!stat.isDirectory() || stat.uid !== uid || stat.mode & 63)
150
+ return;
151
+ } catch {
152
+ return;
153
+ }
154
+ const id = import_crypto.default.createHash("sha1").update([import_path.default.join(__dirname, ".."), process.execPath, cwd].join("\0")).digest("hex").slice(0, 16);
155
+ const socketFile = import_path.default.join(directory, `${id}.sock`);
156
+ return Buffer.byteLength(socketFile) < 100 ? socketFile : void 0;
157
+ }
158
+ function runInSession(request) {
159
+ const socketFile = sessionSocketPath(request.cwd);
160
+ if (!socketFile)
161
+ return Promise.resolve("local");
162
+ return new Promise((resolve) => {
163
+ const socket = import_net.default.connect(socketFile);
164
+ let started = false;
165
+ let exited = false;
166
+ let buffer = "";
167
+ const lost = () => {
168
+ process.stderr.write("Flashwright session: the connection to the test run was lost.\n");
169
+ process.exit(1);
170
+ };
171
+ socket.on("error", () => {
172
+ if (started)
173
+ lost();
174
+ else
175
+ resolve("start");
176
+ });
177
+ socket.on("close", () => {
178
+ if (!started)
179
+ resolve("local");
180
+ else if (!exited)
181
+ lost();
182
+ });
183
+ socket.on("connect", () => socket.write(JSON.stringify(request) + "\n"));
184
+ socket.on("data", (chunk) => {
185
+ buffer += chunk.toString();
186
+ let newline;
187
+ while ((newline = buffer.indexOf("\n")) !== -1) {
188
+ const message = JSON.parse(buffer.slice(0, newline));
189
+ buffer = buffer.slice(newline + 1);
190
+ if (message.type === "cold") {
191
+ socket.destroy();
192
+ resolve(message.stale ? "start" : "local");
193
+ } else if (message.type === "started") {
194
+ started = true;
195
+ let interrupted = false;
196
+ for (const signal of ["SIGINT", "SIGTERM"]) {
197
+ process.on(signal, () => {
198
+ if (interrupted)
199
+ process.exit(130);
200
+ interrupted = true;
201
+ socket.write(JSON.stringify({ type: "signal", signal }) + "\n");
202
+ });
203
+ }
204
+ } else if (message.type === "stdout" || message.type === "stderr") {
205
+ (message.type === "stdout" ? process.stdout : process.stderr).write(Buffer.from(message.data, "base64"));
206
+ } else if (message.type === "exit") {
207
+ exited = true;
208
+ socket.destroy();
209
+ const code = message.code ?? (message.signal === "SIGINT" ? 130 : 1);
210
+ process.stdout.write("", () => process.stderr.write("", () => process.exit(code)));
211
+ }
212
+ }
213
+ });
214
+ });
215
+ }
216
+ function startSession(request) {
217
+ const socketFile = sessionSocketPath(request.cwd);
218
+ if (!socketFile)
219
+ return;
220
+ const child = import_child_process.default.spawn(process.execPath, [__filename, "--flashwright-session", socketFile, request.build], {
221
+ detached: true,
222
+ stdio: ["ignore", "ignore", "ignore", "ipc"],
223
+ env: { ...process.env, FLASHWRIGHT_SESSION_ROLE: "session" }
224
+ });
225
+ child.on("error", () => {
226
+ });
227
+ child.unref();
228
+ child.channel?.unref?.();
229
+ child.send({ method: "request", params: request });
230
+ joinSession(child, false);
231
+ }
232
+ var sessionChannel;
233
+ var offersBrowsers = false;
234
+ var lastRequestId = 0;
235
+ var standbyRequests = /* @__PURE__ */ new Map();
236
+ var workerParts = /* @__PURE__ */ new Map();
237
+ var pendingSends = /* @__PURE__ */ new Set();
238
+ var sessionWorkers = /* @__PURE__ */ new Set();
239
+ var sessionStandbys = /* @__PURE__ */ new Set();
240
+ function joinSession(channel, offers) {
241
+ sessionChannel = channel;
242
+ offersBrowsers = offers;
243
+ process.env.FLASHWRIGHT_SESSION_HANDOFF = "1";
244
+ }
245
+ function sessionStandbyEnv() {
246
+ return offersBrowsers && sessionChannel?.connected ? { FLASHWRIGHT_SESSION_OFFER: "1" } : {};
247
+ }
248
+ function requestSessionBrowser(child) {
249
+ if (!sessionChannel)
250
+ return;
251
+ sessionStandbys.add(child);
252
+ child.once("exit", () => sessionStandbys.delete(child));
253
+ if (!offersBrowsers || !sessionChannel.connected)
254
+ return;
255
+ const id = String(++lastRequestId);
256
+ standbyRequests.set(id, child);
257
+ child.once("exit", () => standbyRequests.delete(id));
258
+ sessionChannel.send({ method: kBrowserRequest, params: { id } });
259
+ }
260
+ function receiveWorkerMessage(child, message, handle) {
261
+ if (message?.method !== kBrowserMessage)
262
+ return false;
263
+ const browser = collectBrowserPart(workerParts, message, handle);
264
+ if (browser)
265
+ forwardBrowser(browser);
266
+ return true;
267
+ }
268
+ function trackSessionWorker(child) {
269
+ if (!sessionChannel)
270
+ return;
271
+ sessionStandbys.delete(child);
272
+ sessionWorkers.add(child);
273
+ child.once("disconnect", () => sessionWorkers.delete(child));
274
+ }
275
+ function forwardBrowser(browser) {
276
+ if (!sessionChannel?.connected || browser.read.readableLength) {
277
+ killBrowser(browser);
278
+ return;
279
+ }
280
+ pendingSends.add(sendBrowser(sessionChannel, browser));
281
+ }
282
+ async function flushSessionBrowsers(exitCode) {
283
+ if (!sessionChannel)
284
+ return;
285
+ await Promise.all([...sessionStandbys].map(returnStandbyBrowser));
286
+ const deadline = Date.now() + 2e3;
287
+ while (sessionWorkers.size && Date.now() < deadline)
288
+ await new Promise((resolve) => setTimeout(resolve, 5));
289
+ await Promise.all(pendingSends);
290
+ for (const parts of workerParts.values())
291
+ killBrowser(parts);
292
+ workerParts.clear();
293
+ if (outputBytes && exitCode !== void 0 && sessionChannel.connected)
294
+ await new Promise((resolve) => sessionChannel.send({ type: "exiting", code: exitCode, ...outputBytes }, void 0, resolve));
295
+ if (sessionChannel.connected)
296
+ sessionChannel.disconnect();
297
+ }
298
+ var outputBytes;
299
+ function countOutputBytes() {
300
+ outputBytes = { stdout: 0, stderr: 0 };
301
+ for (const name of ["stdout", "stderr"]) {
302
+ const stream = process[name];
303
+ const write = stream.write;
304
+ stream.write = function(chunk, encoding, callback) {
305
+ outputBytes[name] += typeof chunk === "string" ? Buffer.byteLength(chunk, typeof encoding === "string" ? encoding : "utf8") : chunk.length;
306
+ return write.apply(this, arguments);
307
+ };
308
+ }
309
+ }
310
+ function returnStandbyBrowser(child) {
311
+ if (!child.connected)
312
+ return Promise.resolve();
313
+ return new Promise((resolve) => {
314
+ const done = () => {
315
+ clearTimeout(timer);
316
+ child.off("message", onMessage);
317
+ resolve();
318
+ };
319
+ const timer = setTimeout(done, 2e3);
320
+ const onMessage = (message, handle) => {
321
+ if (message?.method === kBrowserReturned)
322
+ done();
323
+ else if (message?.method === kBrowserMessage)
324
+ receiveWorkerMessage(child, message, handle);
325
+ };
326
+ child.on("message", onMessage);
327
+ child.once("exit", done);
328
+ child.send({ method: kReturnBrowser });
329
+ });
330
+ }
331
+ function onSessionMessage(message, handle) {
332
+ if (message?.method !== kBrowserMessage)
333
+ return;
334
+ const child = standbyRequests.get(message.params.id);
335
+ if (message.params.part !== "write")
336
+ standbyRequests.delete(message.params.id);
337
+ if (child?.connected) {
338
+ child.send(message, handle);
339
+ return;
340
+ }
341
+ const browser = collectBrowserPart(workerParts, message, handle);
342
+ if (browser)
343
+ forwardBrowser(browser);
344
+ }
345
+ function collectBrowserPart(parts, message, handle) {
346
+ const { id, part, metadata } = message.params;
347
+ if (!part || !handle)
348
+ return;
349
+ const browser = parts.get(id) ?? { id };
350
+ browser[part] = handle;
351
+ if (metadata)
352
+ browser.metadata = metadata;
353
+ parts.set(id, browser);
354
+ if (!browser.write || !browser.read || !browser.metadata)
355
+ return;
356
+ parts.delete(id);
357
+ return browser;
358
+ }
359
+ function sendBrowser(channel, browser) {
360
+ const { id, metadata } = browser;
361
+ return new Promise((resolve) => {
362
+ channel.send({ method: kBrowserMessage, params: { id, part: "write", metadata } }, browser.write, (error) => {
363
+ if (error) {
364
+ killBrowser(browser);
365
+ resolve();
366
+ return;
367
+ }
368
+ channel.send({ method: kBrowserMessage, params: { id, part: "read" } }, browser.read, () => {
369
+ channel.send({ method: kBrowserMessage, params: { id, sent: true } }, void 0, () => resolve());
370
+ });
371
+ });
372
+ });
373
+ }
374
+ function killBrowser(browser) {
375
+ browser.write?.destroy();
376
+ browser.read?.destroy();
377
+ if (!browser.metadata)
378
+ return;
379
+ try {
380
+ process.kill(-browser.metadata.pid, "SIGKILL");
381
+ } catch {
382
+ }
383
+ const directories = browser.metadata.tempDirectories;
384
+ setTimeout(() => {
385
+ for (const directory of directories)
386
+ import_fs.default.rm(directory, { recursive: true, force: true, maxRetries: 5 }, () => {
387
+ });
388
+ }, 500).unref();
389
+ }
390
+ function runSession(socketFile, build) {
391
+ const warmProcesses = [];
392
+ const browsers = [];
393
+ const waiting = [];
394
+ let startingRunKey;
395
+ let lastRequest;
396
+ let idleTimer;
397
+ let activeRuns = 0;
398
+ const shutdown = () => {
399
+ for (const warm of warmProcesses.splice(0))
400
+ warm.child.kill();
401
+ for (const browser of browsers.splice(0))
402
+ killBrowser(browser);
403
+ server.close();
404
+ try {
405
+ import_fs.default.unlinkSync(socketFile);
406
+ } catch {
407
+ }
408
+ process.exit(0);
409
+ };
410
+ const resetIdleTimer = () => {
411
+ clearTimeout(idleTimer);
412
+ idleTimer = setTimeout(() => {
413
+ if (!activeRuns && !startingRunKey)
414
+ shutdown();
415
+ }, kIdleTimeout);
416
+ };
417
+ const runsThatMayHandOver = (key) => (startingRunKey === key ? 1 : 0) + warmProcesses.filter((warm) => warm.running && warm.connected && warm.key === key).length;
418
+ const answer = (warm, id) => {
419
+ let browser;
420
+ for (const candidate of browsers) {
421
+ if (candidate.key === warm.key && (!browser || (candidate.metadata.lastUsed ?? 0) > (browser.metadata.lastUsed ?? 0)))
422
+ browser = candidate;
423
+ }
424
+ if (browser)
425
+ remove(browser);
426
+ if (!warm.child.connected) {
427
+ if (browser)
428
+ browsers.push(browser);
429
+ return;
430
+ }
431
+ if (!browser) {
432
+ warm.child.send({ method: kBrowserMessage, params: { id, none: true } });
433
+ return;
434
+ }
435
+ browser.read.removeAllListeners("data");
436
+ browser.read.removeAllListeners("close");
437
+ browser.read.pause();
438
+ void sendBrowser(warm.child, { ...browser, id });
439
+ };
440
+ const onBrowserRequest = (warm, id) => {
441
+ if (browsers.some((browser) => browser.key === warm.key) || warm.running || !runsThatMayHandOver(warm.key))
442
+ answer(warm, id);
443
+ else
444
+ waiting.push({ warm, id });
445
+ };
446
+ const answerWaiting = (predicate) => {
447
+ for (const entry of waiting.filter(predicate)) {
448
+ waiting.splice(waiting.indexOf(entry), 1);
449
+ answer(entry.warm, entry.id);
450
+ }
451
+ };
452
+ const onBrowserReturned = (key, browser) => {
453
+ if (browser.read.readableLength) {
454
+ killBrowser(browser);
455
+ return;
456
+ }
457
+ const pooled = { ...browser, key };
458
+ browser.read.on("data", () => {
459
+ remove(pooled);
460
+ killBrowser(pooled);
461
+ });
462
+ browser.read.on("close", () => {
463
+ remove(pooled);
464
+ killBrowser(pooled);
465
+ });
466
+ browsers.push(pooled);
467
+ const entry = waiting.find((entry2) => entry2.warm.key === key);
468
+ if (entry) {
469
+ waiting.splice(waiting.indexOf(entry), 1);
470
+ answer(entry.warm, entry.id);
471
+ }
472
+ const sameKind = browsers.filter((browser2) => browser2.key === key).sort((a, b) => (a.metadata.lastUsed ?? 0) - (b.metadata.lastUsed ?? 0));
473
+ for (const extra of sameKind.slice(0, Math.max(0, sameKind.length - kMaxBrowsers))) {
474
+ remove(extra);
475
+ killBrowser(extra);
476
+ }
477
+ };
478
+ const remove = (browser) => {
479
+ const index = browsers.indexOf(browser);
480
+ if (index !== -1)
481
+ browsers.splice(index, 1);
482
+ };
483
+ const spawnWarm = (request) => {
484
+ const key = warmKey(request);
485
+ if (warmProcesses.some((warm2) => warm2.key === key && !warm2.running))
486
+ return;
487
+ const standbys = browsers.filter((browser) => browser.key === key).length;
488
+ const child = import_child_process.default.spawn(request.execPath, [...request.execArgv, request.cliPath, ...request.argv.slice(2)], {
489
+ cwd: request.cwd,
490
+ env: { ...warmEnv(request), FLASHWRIGHT_SESSION_ROLE: "warm", FLASHWRIGHT_SESSION_STANDBYS: String(standbys) },
491
+ stdio: ["ignore", "pipe", "pipe", "ipc"]
492
+ });
493
+ const warm = { child, key, ready: Promise.resolve(false), running: false, connected: true, parts: /* @__PURE__ */ new Map() };
494
+ warm.ready = new Promise((resolve) => {
495
+ child.on("message", (message, handle2) => {
496
+ if (message?.type === "warm") {
497
+ resolve(true);
498
+ } else if (message?.type === "exiting") {
499
+ warm.exiting?.(message);
500
+ } else if (message?.method === kBrowserRequest) {
501
+ onBrowserRequest(warm, message.params.id);
502
+ } else if (message?.method === kBrowserMessage) {
503
+ const browser = collectBrowserPart(warm.parts, message, handle2);
504
+ if (browser)
505
+ onBrowserReturned(key, browser);
506
+ }
507
+ });
508
+ child.once("exit", () => resolve(false));
509
+ });
510
+ warmProcesses.push(warm);
511
+ child.once("disconnect", () => {
512
+ warm.connected = false;
513
+ for (const parts2 of warm.parts.values())
514
+ killBrowser(parts2);
515
+ warm.parts.clear();
516
+ answerWaiting((entry) => entry.warm === warm || !runsThatMayHandOver(entry.warm.key));
517
+ });
518
+ child.once("exit", () => {
519
+ const index = warmProcesses.indexOf(warm);
520
+ if (index !== -1)
521
+ warmProcesses.splice(index, 1);
522
+ });
523
+ };
524
+ const handle = async (socket, request) => {
525
+ resetIdleTimer();
526
+ if (request.build !== build) {
527
+ socket.end(JSON.stringify({ type: "cold", stale: true }) + "\n");
528
+ shutdown();
529
+ return;
530
+ }
531
+ lastRequest = request;
532
+ const key = warmKey(request);
533
+ const index = warmProcesses.findIndex((warm2) => warm2.key === key && !warm2.running);
534
+ const warm = index === -1 ? void 0 : warmProcesses[index];
535
+ if (!warm || !await warm.ready) {
536
+ warm?.child.kill();
537
+ socket.end(JSON.stringify({ type: "cold" }) + "\n");
538
+ if (!startingRunKey)
539
+ spawnWarm(request);
540
+ return;
541
+ }
542
+ activeRuns++;
543
+ warm.running = true;
544
+ answerWaiting((entry) => entry.warm === warm);
545
+ const send = (message) => {
546
+ if (!socket.destroyed)
547
+ socket.write(JSON.stringify(message) + "\n");
548
+ };
549
+ send({ type: "started" });
550
+ const relayed = { stdout: 0, stderr: 0 };
551
+ let exiting;
552
+ let finished = false;
553
+ const finish = (code, signal) => {
554
+ if (finished)
555
+ return;
556
+ finished = true;
557
+ send({ type: "exit", code, signal });
558
+ socket.end();
559
+ };
560
+ const finishIfRelayed = () => {
561
+ if (exiting && relayed.stdout >= exiting.stdout && relayed.stderr >= exiting.stderr)
562
+ finish(exiting.code, null);
563
+ };
564
+ warm.exiting = (message) => {
565
+ exiting = message;
566
+ finishIfRelayed();
567
+ };
568
+ for (const name of ["stdout", "stderr"]) {
569
+ warm.child[name].on("data", (data) => {
570
+ send({ type: name, data: data.toString("base64") });
571
+ relayed[name] += data.length;
572
+ finishIfRelayed();
573
+ });
574
+ }
575
+ let buffer = "";
576
+ socket.on("data", (chunk) => {
577
+ buffer += chunk.toString();
578
+ let newline;
579
+ while ((newline = buffer.indexOf("\n")) !== -1) {
580
+ const message = JSON.parse(buffer.slice(0, newline));
581
+ buffer = buffer.slice(newline + 1);
582
+ if (message.type === "signal")
583
+ warm.child.kill(message.signal);
584
+ }
585
+ });
586
+ socket.on("close", () => {
587
+ if (warm.child.exitCode === null && warm.child.signalCode === null)
588
+ warm.child.kill("SIGINT");
589
+ });
590
+ warm.child.once("exit", (code, signal) => {
591
+ activeRuns--;
592
+ resetIdleTimer();
593
+ if (warm.child.stdout.readableEnded && warm.child.stderr.readableEnded)
594
+ finish(code, signal);
595
+ else
596
+ warm.child.once("close", () => finish(code, signal));
597
+ spawnWarm(lastRequest ?? request);
598
+ });
599
+ warm.child.send(request);
600
+ };
601
+ const listen = () => server.listen(socketFile, () => import_fs.default.chmodSync(socketFile, 384));
602
+ const server = import_net.default.createServer((socket) => {
603
+ let buffer = "";
604
+ const onData = (chunk) => {
605
+ buffer += chunk.toString();
606
+ const newline = buffer.indexOf("\n");
607
+ if (newline === -1)
608
+ return;
609
+ socket.off("data", onData);
610
+ const request = JSON.parse(buffer.slice(0, newline));
611
+ handle(socket, request).catch(() => socket.destroy());
612
+ };
613
+ socket.on("data", onData);
614
+ socket.on("error", () => {
615
+ });
616
+ });
617
+ server.on("error", (error) => {
618
+ if (error.code !== "EADDRINUSE")
619
+ process.exit(0);
620
+ const probe = import_net.default.connect(socketFile);
621
+ probe.on("connect", () => process.exit(0));
622
+ probe.on("error", () => {
623
+ try {
624
+ import_fs.default.unlinkSync(socketFile);
625
+ } catch {
626
+ }
627
+ listen();
628
+ });
629
+ });
630
+ listen();
631
+ resetIdleTimer();
632
+ process.on("SIGTERM", shutdown);
633
+ const parts = /* @__PURE__ */ new Map();
634
+ process.on("message", (message, handle2) => {
635
+ if (message?.method === "request") {
636
+ lastRequest = message.params;
637
+ startingRunKey = warmKey(message.params);
638
+ } else if (message?.method === kBrowserMessage && startingRunKey) {
639
+ const browser = collectBrowserPart(parts, message, handle2);
640
+ if (browser)
641
+ onBrowserReturned(startingRunKey, browser);
642
+ }
643
+ });
644
+ process.on("disconnect", () => {
645
+ for (const browser of parts.values())
646
+ killBrowser(browser);
647
+ parts.clear();
648
+ startingRunKey = void 0;
649
+ resetIdleTimer();
650
+ if (lastRequest)
651
+ spawnWarm(lastRequest);
652
+ });
653
+ }
654
+ function warmEnv(request) {
655
+ const env = { ...request.env };
656
+ const injected = [];
657
+ if (request.profile.stdout !== "false") {
658
+ for (const [key, value] of [["PLAYWRIGHT_FORCE_TTY", request.profile.stdout], ["FORCE_COLOR", "1"]]) {
659
+ if (env[key] === void 0) {
660
+ env[key] = value;
661
+ injected.push(key);
662
+ }
663
+ }
664
+ }
665
+ env.FLASHWRIGHT_SESSION_INJECTED = injected.join(",");
666
+ return env;
667
+ }
668
+ async function runWarmProcess(hooks) {
669
+ joinSession(process, true);
670
+ process.on("message", onSessionMessage);
671
+ hooks.installCodeCache("runner");
672
+ const program = hooks.loadProgram();
673
+ require(import_path.default.join(__dirname, "index.js"));
674
+ const packageRoot = import_path.default.join(__dirname, "..", "..");
675
+ require(import_path.default.join(packageRoot, "test.js"));
676
+ await import(import_url.default.pathToFileURL(import_path.default.join(packageRoot, "test.mjs")).href).catch(() => {
677
+ });
678
+ await import(import_url.default.pathToFileURL(import_path.default.join(import_path.default.dirname(process.argv[1]), "index.mjs")).href).catch(() => {
679
+ });
680
+ for (const key of (process.env.FLASHWRIGHT_SESSION_INJECTED || "").split(",").filter(Boolean))
681
+ delete process.env[key];
682
+ delete process.env.FLASHWRIGHT_SESSION_INJECTED;
683
+ delete process.env.FLASHWRIGHT_SESSION_ROLE;
684
+ hooks.prespawnWorkers(process.argv);
685
+ const request = await new Promise((resolve) => {
686
+ const onMessage = (message) => {
687
+ if (message?.type !== "run")
688
+ return;
689
+ process.off("message", onMessage);
690
+ resolve(message);
691
+ };
692
+ process.on("message", onMessage);
693
+ process.send({ type: "warm" });
694
+ });
695
+ process.channel?.unref();
696
+ process.chdir(request.cwd);
697
+ for (const key of Object.keys(process.env)) {
698
+ if (!(key in request.env) && key !== "FLASHWRIGHT_SESSION_HANDOFF")
699
+ delete process.env[key];
700
+ }
701
+ Object.assign(process.env, request.env);
702
+ process.argv = request.argv;
703
+ if (request.profile.stdout !== "false") {
704
+ const [columns, rows] = request.profile.stdout.split("x").map(Number);
705
+ Object.assign(process.stdout, { isTTY: true, columns, rows });
706
+ }
707
+ if (request.profile.stdinTTY)
708
+ Object.assign(process.stdin, { isTTY: true });
709
+ countOutputBytes();
710
+ hooks.retargetWorkers(request.argv);
711
+ program.parse(request.argv);
712
+ }
713
+ if (process.argv[2] === "--flashwright-session")
714
+ runSession(process.argv[3], process.argv[4]);
715
+ // Annotate the CommonJS export names for ESM import in node:
716
+ 0 && (module.exports = {
717
+ flushSessionBrowsers,
718
+ receiveWorkerMessage,
719
+ requestSessionBrowser,
720
+ runCli,
721
+ sessionStandbyEnv,
722
+ trackSessionWorker
723
+ });
@@ -0,0 +1,8 @@
1
+ # packages/playwright/lib/runner/session.js
2
+ # total: 25.0 KB
3
+
4
+ ## Inlined (1)
5
+ 23.4 KB packages/playwright/src/runner/session.ts
6
+
7
+ ## External (1)
8
+ playwright-core/lib/coreBundle
@@ -36,6 +36,7 @@ __export(workerStandby_exports, {
36
36
  prespawnWorkerForTestCommand: () => prespawnWorkerForTestCommand,
37
37
  recordWorkerCount: () => recordWorkerCount,
38
38
  recordWorkerRestart: () => recordWorkerRestart,
39
+ retargetStandbys: () => retargetStandbys,
39
40
  takeStandby: () => takeStandby,
40
41
  workerEntryScript: () => workerEntryScript,
41
42
  workerEnv: () => workerEnv
@@ -46,6 +47,7 @@ var import_crypto = __toESM(require("crypto"));
46
47
  var import_fs = __toESM(require("fs"));
47
48
  var import_os = __toESM(require("os"));
48
49
  var import_path = __toESM(require("path"));
50
+ var import_session = require("./session");
49
51
  var standbys = [];
50
52
  var enabled = false;
51
53
  var hintKey;
@@ -78,7 +80,8 @@ function prespawnWorkerForTestCommand(argv) {
78
80
  cpus = import_os.default.availableParallelism?.() ?? import_os.default.cpus().length;
79
81
  const hint = readWorkerCountHints()[hintKey];
80
82
  hadHint = !!hint;
81
- const count = Math.max(1, Math.min(hint?.workers || 1, cpus));
83
+ const sessionBrowsers = Number(process.env.FLASHWRIGHT_SESSION_STANDBYS) || 0;
84
+ const count = Math.max(1, Math.min(Math.max(hint?.workers || 1, sessionBrowsers), cpus));
82
85
  keepSpare = !!hint?.restarts && count < cpus;
83
86
  for (let i = 0; i < count + (keepSpare ? 1 : 0); i++)
84
87
  spawnStandby();
@@ -89,7 +92,7 @@ function prespawnWorkerForTestCommand(argv) {
89
92
  }
90
93
  function spawnStandby(waitsForNextPhase = false) {
91
94
  const entryScript = workerEntryScript();
92
- const env = { ...workerEnv({}), PLAYWRIGHT_TEST: "1", FLASHWRIGHT_SPECULATIVE_BROWSER: waitsForNextPhase ? "next-phase" : "1" };
95
+ const env = { ...workerEnv({}), PLAYWRIGHT_TEST: "1", FLASHWRIGHT_SPECULATIVE_BROWSER: waitsForNextPhase ? "next-phase" : "1", ...(0, import_session.sessionStandbyEnv)() };
93
96
  const child = import_child_process.default.fork(entryScript, {
94
97
  detached: false,
95
98
  env,
@@ -97,6 +100,7 @@ function spawnStandby(waitsForNextPhase = false) {
97
100
  });
98
101
  child.on("error", () => {
99
102
  });
103
+ (0, import_session.requestSessionBrowser)(child);
100
104
  const ready = new Promise((resolve) => {
101
105
  const onMessage = (message) => {
102
106
  if (message?.method === "ready") {
@@ -128,6 +132,10 @@ function ensureStandbysForConfig(configuredWorkers) {
128
132
  if (enabled && !hadHint)
129
133
  ensureStandbys(Math.min(configuredWorkers, cpus));
130
134
  }
135
+ function retargetStandbys(argv) {
136
+ if (enabled)
137
+ hintKey = workerCountHintKey(argv);
138
+ }
131
139
  function recordWorkerCount(count) {
132
140
  workerCount = Math.max(workerCount, count);
133
141
  }
@@ -191,6 +199,7 @@ function writeWorkerCountHint() {
191
199
  prespawnWorkerForTestCommand,
192
200
  recordWorkerCount,
193
201
  recordWorkerRestart,
202
+ retargetStandbys,
194
203
  takeStandby,
195
204
  workerEntryScript,
196
205
  workerEnv
@@ -1,7 +1,8 @@
1
1
  # packages/playwright/lib/runner/workerStandby.js
2
- # total: 7.2 KB
2
+ # total: 7.6 KB
3
3
 
4
4
  ## Inlined (1)
5
- 5.5 KB packages/playwright/src/runner/workerStandby.ts
5
+ 5.9 KB packages/playwright/src/runner/workerStandby.ts
6
6
 
7
- ## External (0)
7
+ ## External (1)
8
+ ./session
@@ -28,9 +28,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var speculativeBrowser_exports = {};
30
30
  __export(speculativeBrowser_exports, {
31
+ handBrowsersToSession: () => handBrowsersToSession,
31
32
  launchSpeculativeBrowser: () => launchSpeculativeBrowser,
32
33
  prepareSpeculativeContext: () => prepareSpeculativeContext,
33
34
  rememberRecycledContext: () => rememberRecycledContext,
35
+ returnSpeculativeBrowser: () => returnSpeculativeBrowser,
34
36
  takeSpeculativeBrowser: () => takeSpeculativeBrowser
35
37
  });
36
38
  module.exports = __toCommonJS(speculativeBrowser_exports);
@@ -41,11 +43,100 @@ var import_util = require("util");
41
43
  const speculativeOptions = { handleSIGINT: false, headless: true, ...process.env.FLASHWRIGHT_ISOLATION === "strict" ? {} : { recycleContexts: true } };
42
44
  const speculativeKey = /* @__PURE__ */ Symbol.for("flashwright.speculativeBrowser");
43
45
  function launchSpeculativeBrowser() {
44
- const playwright = require("playwright-core");
45
- const launch = playwright.chromium.launch(speculativeOptions).catch(() => void 0);
46
+ const offered = process.env.FLASHWRIGHT_SESSION_OFFER ? receiveSessionBrowser() : Promise.resolve();
47
+ const launch = offered.then(() => require("playwright-core").chromium.launch(speculativeOptions)).catch(() => void 0);
46
48
  globalThis[speculativeKey] = launch;
47
49
  return launch;
48
50
  }
51
+ const kBrowserMessage = "__flashwright_browser__";
52
+ const offerKey = /* @__PURE__ */ Symbol.for("flashwright.browserOffer");
53
+ let offeredLastUsed = 0;
54
+ const kOfferTimeoutAfterInit = 500;
55
+ function receiveSessionBrowser() {
56
+ return new Promise((resolve) => {
57
+ let answered = false;
58
+ let timer;
59
+ const answer = () => {
60
+ answered = true;
61
+ clearTimeout(timer);
62
+ resolve();
63
+ };
64
+ const parts = {};
65
+ const onMessage = (message, handle) => {
66
+ if (message?.method === "__init__" && !answered)
67
+ timer = setTimeout(answer, kOfferTimeoutAfterInit);
68
+ if (message?.method !== kBrowserMessage || message.params.sent)
69
+ return;
70
+ if (message.params.none) {
71
+ process.off("message", onMessage);
72
+ answer();
73
+ return;
74
+ }
75
+ parts[message.params.part] = handle;
76
+ parts.metadata ??= message.params.metadata;
77
+ if (!parts.write || !parts.read || !parts.metadata)
78
+ return;
79
+ process.off("message", onMessage);
80
+ if (answered) {
81
+ void sendToRunner(parts);
82
+ return;
83
+ }
84
+ globalThis[offerKey] = parts;
85
+ offeredLastUsed = parts.metadata.lastUsed ?? 0;
86
+ process.once("exit", () => {
87
+ if (globalThis[offerKey] === parts)
88
+ killHandedOverBrowser(parts.metadata);
89
+ });
90
+ answer();
91
+ };
92
+ process.on("message", onMessage);
93
+ });
94
+ }
95
+ async function handBrowsersToSession(browser, ranTests) {
96
+ if (!process.env.FLASHWRIGHT_SESSION_HANDOFF || !process.send)
97
+ return false;
98
+ const offer = globalThis[offerKey];
99
+ if (offer) {
100
+ delete globalThis[offerKey];
101
+ await sendToRunner(offer);
102
+ }
103
+ if (!browser)
104
+ return false;
105
+ const serverBrowser = browser._connection.toImpl?.(browser);
106
+ const handoff = await serverBrowser?.handoffForSession?.().catch(() => void 0);
107
+ if (!handoff)
108
+ return false;
109
+ handoff.metadata.lastUsed = ranTests ? Date.now() : offeredLastUsed;
110
+ await sendToRunner(handoff);
111
+ return true;
112
+ }
113
+ async function sendToRunner(browser) {
114
+ const id = `${process.pid}-${browser.metadata.pid}`;
115
+ const send = (params, handle) => new Promise((resolve) => {
116
+ if (!process.connected)
117
+ return resolve(false);
118
+ process.send({ method: kBrowserMessage, params }, handle, void 0, (error) => resolve(!error));
119
+ });
120
+ if (await send({ id, part: "write", metadata: browser.metadata }, browser.write) && await send({ id, part: "read" }, browser.read)) {
121
+ await send({ id, sent: true }, void 0);
122
+ return;
123
+ }
124
+ browser.write.destroy();
125
+ browser.read.destroy();
126
+ killHandedOverBrowser(browser.metadata);
127
+ }
128
+ function killHandedOverBrowser(metadata) {
129
+ try {
130
+ process.kill(-metadata.pid, "SIGKILL");
131
+ } catch {
132
+ }
133
+ for (const directory of metadata.tempDirectories) {
134
+ try {
135
+ import_fs.default.rmSync(directory, { recursive: true, force: true, maxRetries: 5 });
136
+ } catch {
137
+ }
138
+ }
139
+ }
49
140
  function prepareSpeculativeContext() {
50
141
  const launch = globalThis[speculativeKey];
51
142
  const hint = speculativeOptions.recycleContexts ? readContextHint() : void 0;
@@ -100,6 +191,13 @@ function isPlainJSON(value) {
100
191
  return value.every(isPlainJSON);
101
192
  return typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype && Object.values(value).every(isPlainJSON);
102
193
  }
194
+ async function returnSpeculativeBrowser() {
195
+ const launch = globalThis[speculativeKey];
196
+ if (!launch || !process.env.FLASHWRIGHT_SESSION_HANDOFF)
197
+ return;
198
+ delete globalThis[speculativeKey];
199
+ await handBrowsersToSession(await launch, false);
200
+ }
103
201
  async function takeSpeculativeBrowser(browserName, options) {
104
202
  const launch = globalThis[speculativeKey];
105
203
  if (!launch)
@@ -121,8 +219,10 @@ async function takeSpeculativeBrowser(browserName, options) {
121
219
  }
122
220
  // Annotate the CommonJS export names for ESM import in node:
123
221
  0 && (module.exports = {
222
+ handBrowsersToSession,
124
223
  launchSpeculativeBrowser,
125
224
  prepareSpeculativeContext,
126
225
  rememberRecycledContext,
226
+ returnSpeculativeBrowser,
127
227
  takeSpeculativeBrowser
128
228
  });
@@ -1865,11 +1865,100 @@ var import_path4 = __toESM(require("path"));
1865
1865
  var speculativeOptions = { handleSIGINT: false, headless: true, ...process.env.FLASHWRIGHT_ISOLATION === "strict" ? {} : { recycleContexts: true } };
1866
1866
  var speculativeKey = /* @__PURE__ */ Symbol.for("flashwright.speculativeBrowser");
1867
1867
  function launchSpeculativeBrowser() {
1868
- const playwright = require("playwright-core");
1869
- const launch = playwright.chromium.launch(speculativeOptions).catch(() => void 0);
1868
+ const offered = process.env.FLASHWRIGHT_SESSION_OFFER ? receiveSessionBrowser() : Promise.resolve();
1869
+ const launch = offered.then(() => require("playwright-core").chromium.launch(speculativeOptions)).catch(() => void 0);
1870
1870
  globalThis[speculativeKey] = launch;
1871
1871
  return launch;
1872
1872
  }
1873
+ var kBrowserMessage = "__flashwright_browser__";
1874
+ var offerKey = /* @__PURE__ */ Symbol.for("flashwright.browserOffer");
1875
+ var offeredLastUsed = 0;
1876
+ var kOfferTimeoutAfterInit = 500;
1877
+ function receiveSessionBrowser() {
1878
+ return new Promise((resolve) => {
1879
+ let answered = false;
1880
+ let timer;
1881
+ const answer = () => {
1882
+ answered = true;
1883
+ clearTimeout(timer);
1884
+ resolve();
1885
+ };
1886
+ const parts = {};
1887
+ const onMessage = (message, handle) => {
1888
+ if (message?.method === "__init__" && !answered)
1889
+ timer = setTimeout(answer, kOfferTimeoutAfterInit);
1890
+ if (message?.method !== kBrowserMessage || message.params.sent)
1891
+ return;
1892
+ if (message.params.none) {
1893
+ process.off("message", onMessage);
1894
+ answer();
1895
+ return;
1896
+ }
1897
+ parts[message.params.part] = handle;
1898
+ parts.metadata ??= message.params.metadata;
1899
+ if (!parts.write || !parts.read || !parts.metadata)
1900
+ return;
1901
+ process.off("message", onMessage);
1902
+ if (answered) {
1903
+ void sendToRunner(parts);
1904
+ return;
1905
+ }
1906
+ globalThis[offerKey] = parts;
1907
+ offeredLastUsed = parts.metadata.lastUsed ?? 0;
1908
+ process.once("exit", () => {
1909
+ if (globalThis[offerKey] === parts)
1910
+ killHandedOverBrowser(parts.metadata);
1911
+ });
1912
+ answer();
1913
+ };
1914
+ process.on("message", onMessage);
1915
+ });
1916
+ }
1917
+ async function handBrowsersToSession(browser, ranTests) {
1918
+ if (!process.env.FLASHWRIGHT_SESSION_HANDOFF || !process.send)
1919
+ return false;
1920
+ const offer = globalThis[offerKey];
1921
+ if (offer) {
1922
+ delete globalThis[offerKey];
1923
+ await sendToRunner(offer);
1924
+ }
1925
+ if (!browser)
1926
+ return false;
1927
+ const serverBrowser = browser._connection.toImpl?.(browser);
1928
+ const handoff = await serverBrowser?.handoffForSession?.().catch(() => void 0);
1929
+ if (!handoff)
1930
+ return false;
1931
+ handoff.metadata.lastUsed = ranTests ? Date.now() : offeredLastUsed;
1932
+ await sendToRunner(handoff);
1933
+ return true;
1934
+ }
1935
+ async function sendToRunner(browser) {
1936
+ const id = `${process.pid}-${browser.metadata.pid}`;
1937
+ const send = (params, handle) => new Promise((resolve) => {
1938
+ if (!process.connected)
1939
+ return resolve(false);
1940
+ process.send({ method: kBrowserMessage, params }, handle, void 0, (error) => resolve(!error));
1941
+ });
1942
+ if (await send({ id, part: "write", metadata: browser.metadata }, browser.write) && await send({ id, part: "read" }, browser.read)) {
1943
+ await send({ id, sent: true }, void 0);
1944
+ return;
1945
+ }
1946
+ browser.write.destroy();
1947
+ browser.read.destroy();
1948
+ killHandedOverBrowser(browser.metadata);
1949
+ }
1950
+ function killHandedOverBrowser(metadata) {
1951
+ try {
1952
+ process.kill(-metadata.pid, "SIGKILL");
1953
+ } catch {
1954
+ }
1955
+ for (const directory of metadata.tempDirectories) {
1956
+ try {
1957
+ import_fs4.default.rmSync(directory, { recursive: true, force: true, maxRetries: 5 });
1958
+ } catch {
1959
+ }
1960
+ }
1961
+ }
1873
1962
  function prepareSpeculativeContext() {
1874
1963
  const launch = globalThis[speculativeKey];
1875
1964
  const hint = speculativeOptions.recycleContexts ? readContextHint() : void 0;
@@ -1894,6 +1983,13 @@ function readContextHint() {
1894
1983
  const key = process.env.FLASHWRIGHT_CONTEXT_HINT_KEY;
1895
1984
  return key ? readContextHints()[key] : void 0;
1896
1985
  }
1986
+ async function returnSpeculativeBrowser() {
1987
+ const launch = globalThis[speculativeKey];
1988
+ if (!launch || !process.env.FLASHWRIGHT_SESSION_HANDOFF)
1989
+ return;
1990
+ delete globalThis[speculativeKey];
1991
+ await handBrowsersToSession(await launch, false);
1992
+ }
1897
1993
 
1898
1994
  // packages/playwright/src/worker/workerProcessEntry.ts
1899
1995
  var kSpareIdleTime = 100;
@@ -1901,6 +1997,10 @@ if (process.env.FLASHWRIGHT_SPECULATIVE_BROWSER) {
1901
1997
  let initialized = false;
1902
1998
  process.on("message", (message) => initialized ||= message?.method === "__init__");
1903
1999
  const idleTime = process.env.FLASHWRIGHT_SPECULATIVE_BROWSER === "next-phase" ? 0 : kSpareIdleTime;
2000
+ process.on("message", (message) => {
2001
+ if (message?.method === "__flashwright_return_browser__" && !initialized)
2002
+ void returnSpeculativeBrowser().finally(() => process.send?.({ method: "__flashwright_browser_returned__" }));
2003
+ });
1904
2004
  void launchSpeculativeBrowser().then(() => setTimeout(() => {
1905
2005
  if (initialized)
1906
2006
  return;
@@ -1,8 +1,8 @@
1
1
  # packages/playwright/lib/worker/workerProcessEntry.js
2
- # total: 76.4 KB
2
+ # total: 79.7 KB
3
3
 
4
4
  ## Inlined (9)
5
- 1.4 KB packages/playwright/src/speculativeBrowser.ts
5
+ 4.6 KB packages/playwright/src/speculativeBrowser.ts
6
6
  3.7 KB packages/playwright/src/util.ts
7
7
  8.7 KB packages/playwright/src/worker/fixtureRunner.ts
8
8
  21.3 KB packages/playwright/src/worker/testInfo.ts
@@ -10,7 +10,7 @@
10
10
  5.4 KB packages/playwright/src/worker/timeoutManager.ts
11
11
  0.3 KB packages/playwright/src/worker/util.ts
12
12
  21.3 KB packages/playwright/src/worker/workerMain.ts
13
- 0.7 KB packages/playwright/src/worker/workerProcessEntry.ts
13
+ 1.0 KB packages/playwright/src/worker/workerProcessEntry.ts
14
14
 
15
15
  ## External (7)
16
16
  ../codeCache
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flashwright",
3
- "version": "1.64.1-flashwright.1",
3
+ "version": "1.64.1-flashwright.2",
4
4
  "description": "Flashwright: a faster drop-in replacement for Playwright (fork of Playwright 1.64.1)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,6 +22,7 @@
22
22
  "./lib/common": "./lib/common/index.js",
23
23
  "./lib/program": "./lib/program.js",
24
24
  "./lib/runner/workerStandby": "./lib/runner/workerStandby.js",
25
+ "./lib/runner/session": "./lib/runner/session.js",
25
26
  "./lib/codeCache": "./lib/codeCache.js",
26
27
  "./lib/runner": "./lib/runner/index.js",
27
28
  "./lib/transform/babelBundle": "./lib/transform/babelBundle.js",
@@ -47,7 +48,7 @@
47
48
  },
48
49
  "license": "Apache-2.0",
49
50
  "dependencies": {
50
- "playwright-core": "npm:flashwright-core@1.64.1-flashwright.1"
51
+ "playwright-core": "npm:flashwright-core@1.64.1-flashwright.2"
51
52
  },
52
53
  "bugs": {
53
54
  "url": "https://github.com/xcodebeast/flashwright/issues"