test-proxy-recorder 0.3.0 → 0.3.1

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
@@ -59,7 +59,7 @@ Add to `package.json`:
59
59
  ```json
60
60
  {
61
61
  "scripts": {
62
- "proxy": "test-proxy-recorder http://localhost:8000 --port 8100 --recordings-dir ./e2e/recordings"
62
+ "proxy": "test-proxy-recorder http://localhost:8000 --port 8100 --dir ./e2e/recordings"
63
63
  }
64
64
  }
65
65
  ```
@@ -73,7 +73,7 @@ npm install --save-dev concurrently
73
73
  ```json
74
74
  {
75
75
  "scripts": {
76
- "proxy": "test-proxy-recorder http://localhost:8000 --port 8100 --recordings-dir ./e2e/recordings",
76
+ "proxy": "test-proxy-recorder http://localhost:8000 --port 8100 --dir ./e2e/recordings",
77
77
  "dev:proxy": "concurrently -n \"proxy,app\" -c \"blue,green\" \"npm run proxy\" \"INTERNAL_API_URL=http://localhost:8100 npm run dev\""
78
78
  }
79
79
  }
@@ -165,7 +165,7 @@ test-proxy-recorder <target-url> [options]
165
165
 
166
166
  - `<target-url>` - Backend API URL (positional argument, required)
167
167
  - `--port, -p <number>` - Port to listen on (default: 8080)
168
- - `--recordings-dir, -r <path>` - Directory to store recordings (default: ./recordings)
168
+ - `--dir, -d <path>` - Directory to store recordings (default: ./recordings)
169
169
  - `--help, -h` - Show help
170
170
 
171
171
  ### Examples
@@ -175,7 +175,7 @@ test-proxy-recorder <target-url> [options]
175
175
  test-proxy-recorder http://localhost:8000
176
176
 
177
177
  # Custom port and recordings directory
178
- test-proxy-recorder http://localhost:8000 --port 8100 --recordings-dir ./mocks
178
+ test-proxy-recorder http://localhost:8000 --port 8100 --dir ./mocks
179
179
 
180
180
  # Multiple targets (experimental)
181
181
  test-proxy-recorder http://localhost:8000 http://localhost:9000 --port 8100
package/dist/index.cjs CHANGED
@@ -60,13 +60,23 @@ async function loadRecordingSession(filePath) {
60
60
  return JSON.parse(fileContent);
61
61
  }
62
62
  function processRecordings(recordings) {
63
- const keySequenceMap = /* @__PURE__ */ new Map();
64
- return recordings.map((recording) => {
63
+ const recordingsByKey = /* @__PURE__ */ new Map();
64
+ for (const recording of recordings) {
65
65
  const key = recording.key;
66
- const currentSeq = keySequenceMap.get(key) || 0;
67
- keySequenceMap.set(key, currentSeq + 1);
68
- return { ...recording, sequence: currentSeq };
69
- });
66
+ if (!recordingsByKey.has(key)) {
67
+ recordingsByKey.set(key, []);
68
+ }
69
+ recordingsByKey.get(key).push(recording);
70
+ }
71
+ const processedRecordings = [];
72
+ for (const [_key, keyRecordings] of recordingsByKey) {
73
+ keyRecordings.sort((a, b) => a.recordingId - b.recordingId);
74
+ keyRecordings.forEach((recording, index) => {
75
+ processedRecordings.push({ ...recording, sequence: index });
76
+ });
77
+ }
78
+ processedRecordings.sort((a, b) => a.recordingId - b.recordingId);
79
+ return processedRecordings;
70
80
  }
71
81
  async function saveRecordingSession(recordingsDir, session) {
72
82
  const filePath = getRecordingPath(recordingsDir, session.id);
@@ -129,19 +139,25 @@ var ProxyServer = class {
129
139
  recordingsDir;
130
140
  recordingIdCounter;
131
141
  // Unique ID for each recording entry
142
+ sequenceCounterByKey;
143
+ // Sequence counter per key (endpoint)
132
144
  replaySessions;
133
145
  // Track multiple concurrent replay sessions by recording ID
146
+ recordingPromises;
147
+ // Stack of promises that resolve to completed recordings
134
148
  constructor(targets, recordingsDir) {
135
149
  this.targets = targets;
136
150
  this.currentTargetIndex = 0;
137
151
  this.mode = Modes.transparent;
138
152
  this.recordingId = null;
139
153
  this.recordingIdCounter = 0;
154
+ this.sequenceCounterByKey = /* @__PURE__ */ new Map();
140
155
  this.replayId = null;
141
156
  this.modeTimeout = null;
142
157
  this.currentSession = null;
143
158
  this.recordingsDir = recordingsDir;
144
159
  this.replaySessions = /* @__PURE__ */ new Map();
160
+ this.recordingPromises = [];
145
161
  this.proxy = httpProxy__default.default.createProxyServer({
146
162
  secure: false,
147
163
  changeOrigin: true,
@@ -167,7 +183,7 @@ var ProxyServer = class {
167
183
  }
168
184
  setupProxyEventHandlers() {
169
185
  this.proxy.on("error", this.handleProxyError.bind(this));
170
- this.proxy.on("proxyRes", this.handleProxyResponse.bind(this));
186
+ this.proxy.on("proxyRes", this.addCorsHeaders.bind(this));
171
187
  }
172
188
  handleProxyError(err, req, res) {
173
189
  console.error("Proxy error:", err);
@@ -183,12 +199,6 @@ var ProxyServer = class {
183
199
  }
184
200
  res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
185
201
  }
186
- handleProxyResponse(proxyRes, req) {
187
- this.addCorsHeaders(proxyRes, req);
188
- if (this.mode === Modes.record && this.recordingId) {
189
- this.recordResponse(req, proxyRes);
190
- }
191
- }
192
202
  /**
193
203
  * Get CORS headers for a given request
194
204
  * @param req The incoming HTTP request
@@ -282,18 +292,20 @@ var ProxyServer = class {
282
292
  }
283
293
  return { mode, id, timeout };
284
294
  }
295
+ async parseControlRequest(req) {
296
+ if (req.method === "GET") {
297
+ return this.parseGetParams(req);
298
+ }
299
+ if (req.method === "POST") {
300
+ const body = await readRequestBody(req);
301
+ console.log(`MODE CHANGE (${req.method})`, body);
302
+ return JSON.parse(body);
303
+ }
304
+ throw new Error("Unsupported control method");
305
+ }
285
306
  async handleControlRequest(req, res) {
286
307
  try {
287
- let data;
288
- if (req.method === "GET") {
289
- data = this.parseGetParams(req);
290
- } else if (req.method === "POST") {
291
- const body = await readRequestBody(req);
292
- console.log(`MODE CHANGE (${req.method})`, body);
293
- data = JSON.parse(body);
294
- } else {
295
- return;
296
- }
308
+ const data = await this.parseControlRequest(req);
297
309
  const { mode, id, timeout: requestTimeout } = data;
298
310
  const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
299
311
  this.clearModeTimeout();
@@ -326,7 +338,7 @@ var ProxyServer = class {
326
338
  async switchMode(mode, id) {
327
339
  console.log(`Switching to ${mode.toUpperCase()} mode`);
328
340
  if (this.currentSession && this.mode === Modes.record) {
329
- await this.saveCurrentSession(true);
341
+ await this.saveCurrentSession();
330
342
  console.log("Session saved, continuing with mode switch");
331
343
  }
332
344
  switch (mode) {
@@ -366,6 +378,8 @@ var ProxyServer = class {
366
378
  this.recordingId = id;
367
379
  this.replayId = null;
368
380
  this.currentSession = { id, recordings: [], websocketRecordings: [] };
381
+ this.recordingIdCounter = 0;
382
+ this.sequenceCounterByKey.clear();
369
383
  console.log(`Switched to record mode with ID: ${id}`);
370
384
  }
371
385
  async switchToReplayMode(id) {
@@ -385,146 +399,39 @@ var ProxyServer = class {
385
399
  setupModeTimeout(timeout) {
386
400
  this.modeTimeout = setTimeout(async () => {
387
401
  console.log("Timeout reached, switching back to transparent mode");
388
- await this.saveCurrentSession(true);
402
+ await this.saveCurrentSession();
389
403
  this.switchToTransparentMode();
390
404
  this.modeTimeout = null;
391
405
  }, timeout);
392
406
  }
393
- async saveCurrentSession(filterIncomplete = false) {
394
- if (!this.currentSession) {
407
+ async flushPendingRecordings() {
408
+ if (this.recordingPromises.length === 0) {
395
409
  return;
396
410
  }
397
- if (filterIncomplete) {
398
- const incompleteCount = this.currentSession.recordings.filter(
399
- (r) => !r.response
400
- ).length;
401
- if (incompleteCount > 0) {
402
- this.currentSession.recordings = this.currentSession.recordings.filter(
403
- (r) => r.response
404
- );
411
+ const results = await Promise.allSettled(this.recordingPromises);
412
+ if (this.currentSession) {
413
+ for (const result of results) {
414
+ if (result.status === "fulfilled" && result.value) {
415
+ this.currentSession.recordings.push(result.value);
416
+ }
405
417
  }
406
- }
407
- console.log(
408
- `Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
409
- );
410
- await saveRecordingSession(this.recordingsDir, this.currentSession);
411
- }
412
- saveRequestRecordSync(req, body) {
413
- if (!this.currentSession) {
414
- return;
415
- }
416
- const key = getReqID(req);
417
- const recordingId = this.recordingIdCounter++;
418
- req.__recordingId = recordingId;
419
- const record = {
420
- request: {
421
- method: req.method,
422
- url: req.url,
423
- headers: req.headers,
424
- body: body || null
425
- },
426
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
427
- key,
428
- recordingId
429
- };
430
- this.currentSession.recordings.push(record);
431
- console.log(
432
- // eslint-disable-next-line sonarjs/no-nested-template-literals
433
- `saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, recordingId: ${recordingId}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
434
- );
435
- }
436
- updateRequestBodySync(req, body) {
437
- if (!this.currentSession) {
438
- return;
439
- }
440
- const recordingId = req.__recordingId;
441
- if (recordingId === void 0) {
442
- console.error(
443
- `updateRequestBodySync: No recording ID found on request ${req.method} ${req.url}`
444
- );
445
- return;
446
- }
447
- const record = this.currentSession.recordings.find(
448
- (r) => r.recordingId === recordingId
449
- );
450
- if (!record) {
451
- console.error(
452
- `updateRequestBodySync: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
418
+ console.log(
419
+ `Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
453
420
  );
454
- return;
455
421
  }
456
- record.request.body = body || null;
457
- console.log(
458
- `updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars, recordingId: ${recordingId})`
459
- );
422
+ this.recordingPromises = [];
460
423
  }
461
- async recordResponse(req, proxyRes) {
424
+ async saveCurrentSession() {
462
425
  if (!this.currentSession) {
463
426
  return;
464
427
  }
465
- const recordingId = req.__recordingId;
466
- if (recordingId === void 0) {
467
- console.error(
468
- `recordResponse: No recording ID found on request ${req.method} ${req.url}`
469
- );
470
- return;
471
- }
472
- const record = this.currentSession.recordings.find(
473
- (r) => r.recordingId === recordingId
474
- );
475
- if (!record) {
476
- console.error(
477
- `recordResponse: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
478
- );
479
- return;
480
- }
481
- const chunks = [];
482
- proxyRes.on("data", (chunk) => {
483
- chunks.push(chunk);
484
- });
485
- proxyRes.on("end", async () => {
486
- const body = Buffer.concat(chunks).toString("utf8");
487
- record.response = {
488
- statusCode: proxyRes.statusCode,
489
- headers: proxyRes.headers,
490
- body: body || null
491
- };
492
- console.log(
493
- `Recorded: ${req.method} ${req.url} (recordingId: ${recordingId})`
494
- );
495
- });
496
- }
497
- async recordResponseData(req, proxyRes, body) {
498
- if (!this.currentSession) {
499
- return false;
500
- }
501
- const recordingId = req.__recordingId;
502
- if (recordingId === void 0) {
503
- console.error(
504
- `recordResponseData: No recording ID found on request ${req.method} ${req.url}`
505
- );
506
- return false;
507
- }
508
- const record = this.currentSession.recordings.find(
509
- (r) => r.recordingId === recordingId
510
- );
511
- if (!record) {
512
- console.error(
513
- `recordResponseData: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
514
- );
515
- return false;
516
- }
517
- record.response = {
518
- statusCode: proxyRes.statusCode,
519
- headers: proxyRes.headers,
520
- body: body || null
521
- };
428
+ await this.flushPendingRecordings();
522
429
  console.log(
523
- `recordResponseData: Recorded response for ${req.method} ${req.url} (recordingId: ${recordingId})`
430
+ `Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
524
431
  );
525
- return true;
432
+ await saveRecordingSession(this.recordingsDir, this.currentSession);
526
433
  }
527
- async handleReplayRequest(req, res) {
434
+ getRecordingIdOrError(req, res) {
528
435
  const recordingId = this.getRecordingIdFromRequest(req) || this.replayId;
529
436
  if (!recordingId) {
530
437
  const corsHeaders = this.getCorsHeaders(req);
@@ -533,21 +440,50 @@ var ProxyServer = class {
533
440
  ...corsHeaders
534
441
  });
535
442
  res.end(JSON.stringify({ error: "No replay session active" }));
536
- return;
443
+ return null;
537
444
  }
445
+ return recordingId;
446
+ }
447
+ async ensureSessionLoaded(recordingId, filePath) {
448
+ const sessionState = this.getOrCreateReplaySession(recordingId);
449
+ if (!sessionState.loadedSession) {
450
+ sessionState.loadedSession = await loadRecordingSession(filePath);
451
+ console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
452
+ }
453
+ return sessionState;
454
+ }
455
+ getServedTracker(sessionState, key) {
456
+ if (!sessionState.servedRecordingIdsByKey.has(key)) {
457
+ sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
458
+ }
459
+ return sessionState.servedRecordingIdsByKey.get(key);
460
+ }
461
+ selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
462
+ for (const rec of recordsWithKey) {
463
+ if (!servedForThisKey.has(rec.recordingId)) {
464
+ return rec;
465
+ }
466
+ }
467
+ if (recordsWithKey.length > 0) {
468
+ console.log(
469
+ `[REPLAY WARNING] All ${recordsWithKey.length} recordings already served for ${key} (session: ${recordingId}), reusing last one`
470
+ );
471
+ return recordsWithKey[recordsWithKey.length - 1];
472
+ }
473
+ return null;
474
+ }
475
+ async handleReplayRequest(req, res) {
476
+ const recordingId = this.getRecordingIdOrError(req, res);
477
+ if (!recordingId) return;
538
478
  const key = getReqID(req);
539
479
  const filePath = getRecordingPath(this.recordingsDir, recordingId);
540
480
  try {
541
- const sessionState = this.getOrCreateReplaySession(recordingId);
542
- if (!sessionState.loadedSession) {
543
- sessionState.loadedSession = await loadRecordingSession(filePath);
544
- console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
545
- }
481
+ const sessionState = await this.ensureSessionLoaded(
482
+ recordingId,
483
+ filePath
484
+ );
546
485
  const session = sessionState.loadedSession;
547
- if (!sessionState.servedRecordingIdsByKey.has(key)) {
548
- sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
549
- }
550
- const servedForThisKey = sessionState.servedRecordingIdsByKey.get(key);
486
+ const servedForThisKey = this.getServedTracker(sessionState, key);
551
487
  const host = req.headers.host || "unknown";
552
488
  const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
553
489
  const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
@@ -576,30 +512,23 @@ var ProxyServer = class {
576
512
  }
577
513
  const requestCount = servedForThisKey.size + 1;
578
514
  console.log(
579
- `[replay request #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
515
+ `[replay request #${requestCount}] ${req.method} ${req.url} (key: ${key}, session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
580
516
  );
581
- let record;
582
- for (const rec of recordsWithKey) {
583
- if (!servedForThisKey.has(rec.recordingId)) {
584
- record = rec;
585
- break;
586
- }
587
- }
588
- if (!record) {
589
- console.log(
590
- `[REPLAY WARNING] All ${recordsWithKey.length} recordings already served for ${key} (session: ${recordingId}), reusing last one`
517
+ const record = this.selectReplayRecord(
518
+ recordsWithKey,
519
+ servedForThisKey,
520
+ key,
521
+ recordingId
522
+ );
523
+ if (!record || !record.response) {
524
+ throw new Error(
525
+ `No response recorded for this request: ${req.method} ${host}${req.url}`
591
526
  );
592
- record = recordsWithKey[recordsWithKey.length - 1];
593
527
  }
594
528
  servedForThisKey.add(record.recordingId);
595
529
  console.log(
596
530
  `[replay serving] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
597
531
  );
598
- if (!record.response) {
599
- throw new Error(
600
- `No response recorded for this request: ${req.method} ${host}${req.url}`
601
- );
602
- }
603
532
  const { statusCode, headers, body } = record.response;
604
533
  const responseHeaders = {
605
534
  ...headers,
@@ -654,82 +583,124 @@ var ProxyServer = class {
654
583
  const target = this.getTarget();
655
584
  console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
656
585
  if (this.mode === Modes.record) {
657
- this.saveRequestRecordSync(req, null);
658
- await this.bufferAndProxyRequest(req, res, target);
586
+ await this.recordAndProxyRequest(req, res, target);
659
587
  } else {
660
588
  this.proxy.web(req, res, { target });
661
589
  }
662
590
  }
663
- // TODO: check if can handle streaming requests
664
- async bufferAndProxyRequest(req, res, target) {
665
- const chunks = [];
666
- req.on("data", (chunk) => {
667
- chunks.push(chunk);
668
- });
669
- try {
670
- await new Promise((resolve, reject) => {
671
- req.on("end", () => resolve());
672
- req.on("error", (err) => reject(err));
673
- setTimeout(
674
- () => reject(new Error("Request buffering timeout")),
675
- 3e4
676
- );
677
- });
678
- } catch (error) {
679
- console.error("Error buffering request:", error);
591
+ // Note: streaming requests are buffered before proxying; streaming passthrough is not yet implemented
592
+ async recordAndProxyRequest(req, res, target) {
593
+ if (!this.currentSession) {
594
+ return;
680
595
  }
681
- const body = Buffer.concat(chunks).toString("utf8");
682
- this.updateRequestBodySync(req, body);
683
- const targetUrl = new URL(target);
684
- const isHttps = targetUrl.protocol === "https:";
685
- const requestModule = isHttps ? https__default.default : http__default.default;
686
- const defaultPort = isHttps ? 443 : 80;
687
- const proxyReq = requestModule.request(
688
- {
689
- hostname: targetUrl.hostname,
690
- port: targetUrl.port || defaultPort,
691
- path: req.url,
692
- method: req.method,
693
- headers: req.headers
694
- },
695
- (proxyRes) => {
696
- this.addCorsHeaders(proxyRes, req);
697
- const responseChunks = [];
698
- proxyRes.on("data", (chunk) => {
699
- responseChunks.push(chunk);
700
- });
701
- proxyRes.on("end", async () => {
702
- const responseBody = Buffer.concat(responseChunks);
703
- const recorded = await this.recordResponseData(
704
- req,
705
- proxyRes,
706
- responseBody.toString("utf8")
707
- );
708
- const responseHeaders = {
709
- ...proxyRes.headers,
710
- ...this.getCorsHeaders(req)
711
- };
712
- res.writeHead(proxyRes.statusCode || 200, responseHeaders);
713
- res.end(responseBody);
714
- if (recorded) {
715
- console.log(`Recorded: ${req.method} ${req.url}`);
596
+ const key = getReqID(req);
597
+ const recordingId = this.recordingIdCounter++;
598
+ const sequence = this.sequenceCounterByKey.get(key) || 0;
599
+ this.sequenceCounterByKey.set(key, sequence + 1);
600
+ const recordingPromise = new Promise((resolve) => {
601
+ (async () => {
602
+ try {
603
+ const chunks = [];
604
+ req.on("data", (chunk) => {
605
+ chunks.push(chunk);
606
+ });
607
+ try {
608
+ await new Promise((resolveBuffer, rejectBuffer) => {
609
+ req.on("end", () => resolveBuffer());
610
+ req.on("error", (err) => rejectBuffer(err));
611
+ setTimeout(
612
+ () => rejectBuffer(new Error("Request buffering timeout")),
613
+ 3e4
614
+ );
615
+ });
616
+ } catch (error) {
617
+ console.error("Error buffering request:", error);
716
618
  }
717
- });
718
- proxyRes.on("error", (err) => {
719
- console.error("Proxy response error:", err);
720
- if (!res.headersSent) {
619
+ const requestBody = Buffer.concat(chunks).toString("utf8");
620
+ const targetUrl = new URL(target);
621
+ const isHttps = targetUrl.protocol === "https:";
622
+ const requestModule = isHttps ? https__default.default : http__default.default;
623
+ const defaultPort = isHttps ? 443 : 80;
624
+ const proxyReq = requestModule.request(
625
+ {
626
+ hostname: targetUrl.hostname,
627
+ port: targetUrl.port || defaultPort,
628
+ path: req.url,
629
+ method: req.method,
630
+ headers: req.headers
631
+ },
632
+ (proxyRes) => {
633
+ this.addCorsHeaders(proxyRes, req);
634
+ const responseChunks = [];
635
+ proxyRes.on("data", (chunk) => {
636
+ responseChunks.push(chunk);
637
+ });
638
+ proxyRes.on("end", async () => {
639
+ try {
640
+ const responseBody = Buffer.concat(responseChunks);
641
+ const responseBodyStr = responseBody.toString("utf8");
642
+ const recording = {
643
+ request: {
644
+ method: req.method,
645
+ url: req.url,
646
+ headers: req.headers,
647
+ body: requestBody || null
648
+ },
649
+ response: {
650
+ statusCode: proxyRes.statusCode,
651
+ headers: proxyRes.headers,
652
+ body: responseBodyStr || null
653
+ },
654
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
655
+ key,
656
+ recordingId,
657
+ sequence
658
+ };
659
+ const responseHeaders = {
660
+ ...proxyRes.headers,
661
+ ...this.getCorsHeaders(req)
662
+ };
663
+ res.writeHead(proxyRes.statusCode || 200, responseHeaders);
664
+ res.end(responseBody);
665
+ console.log(
666
+ `Recorded: ${req.method} ${req.url} (recordingId: ${recordingId}, sequence: ${sequence})`
667
+ );
668
+ resolve(recording);
669
+ } catch (error) {
670
+ console.error("Error completing recording:", error);
671
+ resolve(null);
672
+ }
673
+ });
674
+ proxyRes.on("error", (err) => {
675
+ console.error("Proxy response error:", err);
676
+ if (!res.headersSent) {
677
+ this.handleProxyError(err, req, res);
678
+ }
679
+ resolve(null);
680
+ });
681
+ }
682
+ );
683
+ proxyReq.on("error", (err) => {
721
684
  this.handleProxyError(err, req, res);
685
+ resolve(null);
686
+ });
687
+ if (chunks.length > 0) {
688
+ proxyReq.write(Buffer.concat(chunks));
722
689
  }
723
- });
724
- }
725
- );
726
- proxyReq.on("error", (err) => {
727
- this.handleProxyError(err, req, res);
690
+ proxyReq.end();
691
+ } catch (error) {
692
+ console.error("Error in recordAndProxyRequest:", error);
693
+ try {
694
+ this.handleProxyError(error, req, res);
695
+ } catch (error_) {
696
+ console.error("Failed to handle proxy error:", error_);
697
+ }
698
+ resolve(null);
699
+ }
700
+ })();
728
701
  });
729
- if (chunks.length > 0) {
730
- proxyReq.write(Buffer.concat(chunks));
731
- }
732
- proxyReq.end();
702
+ this.recordingPromises.push(recordingPromise);
703
+ await recordingPromise;
733
704
  }
734
705
  handleUpgrade(req, socket, head) {
735
706
  if (this.mode === Modes.replay) {
@@ -991,7 +962,7 @@ var playwrightProxy = {
991
962
  try {
992
963
  await setProxyMode(Modes.replay, sessionId);
993
964
  console.log(
994
- `[Cleanup] Switched to transparent mode for session: ${sessionId}`
965
+ `[Cleanup] Switched to replay mode for session: ${sessionId}`
995
966
  );
996
967
  } catch (error) {
997
968
  console.error("[Cleanup] Error during page close cleanup:", error);
package/dist/index.d.cts CHANGED
@@ -14,13 +14,14 @@ declare class ProxyServer {
14
14
  private currentSession;
15
15
  private recordingsDir;
16
16
  private recordingIdCounter;
17
+ private sequenceCounterByKey;
17
18
  private replaySessions;
19
+ private recordingPromises;
18
20
  constructor(targets: string[], recordingsDir: string);
19
21
  init(): Promise<void>;
20
22
  listen(port: number): http.Server;
21
23
  private setupProxyEventHandlers;
22
24
  private handleProxyError;
23
- private handleProxyResponse;
24
25
  /**
25
26
  * Get CORS headers for a given request
26
27
  * @param req The incoming HTTP request
@@ -56,6 +57,7 @@ declare class ProxyServer {
56
57
  */
57
58
  private getOrCreateReplaySession;
58
59
  private parseGetParams;
60
+ private parseControlRequest;
59
61
  private handleControlRequest;
60
62
  private clearModeTimeout;
61
63
  private switchMode;
@@ -63,17 +65,18 @@ declare class ProxyServer {
63
65
  private switchToRecordMode;
64
66
  private switchToReplayMode;
65
67
  private setupModeTimeout;
68
+ private flushPendingRecordings;
66
69
  private saveCurrentSession;
67
- private saveRequestRecordSync;
68
- private updateRequestBodySync;
69
- private recordResponse;
70
- private recordResponseData;
70
+ private getRecordingIdOrError;
71
+ private ensureSessionLoaded;
72
+ private getServedTracker;
73
+ private selectReplayRecord;
71
74
  private handleReplayRequest;
72
75
  private handleReplayError;
73
76
  private handleRequest;
74
77
  private handleCorsPreflightRequest;
75
78
  private handleProxyRequest;
76
- private bufferAndProxyRequest;
79
+ private recordAndProxyRequest;
77
80
  private handleUpgrade;
78
81
  private handleRecordWebSocket;
79
82
  private handleReplayWebSocket;