test-proxy-recorder 0.3.2 → 0.3.3

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/dist/index.cjs CHANGED
@@ -148,6 +148,8 @@ var ProxyServer = class {
148
148
  // Track multiple concurrent replay sessions by recording ID
149
149
  recordingPromises;
150
150
  // Stack of promises that resolve to completed recordings
151
+ flushPromise;
152
+ // Promise for in-progress flush operation
151
153
  constructor(targets, recordingsDir) {
152
154
  this.targets = targets;
153
155
  this.currentTargetIndex = 0;
@@ -161,6 +163,7 @@ var ProxyServer = class {
161
163
  this.recordingsDir = recordingsDir;
162
164
  this.replaySessions = /* @__PURE__ */ new Map();
163
165
  this.recordingPromises = [];
166
+ this.flushPromise = null;
164
167
  this.proxy = httpProxy__default.default.createProxyServer({
165
168
  secure: false,
166
169
  changeOrigin: true,
@@ -283,7 +286,8 @@ var ProxyServer = class {
283
286
  recordingId,
284
287
  servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
285
288
  loadedSession: null,
286
- lastAccessTime: Date.now()
289
+ lastAccessTime: Date.now(),
290
+ sortedRecordingsByKey: /* @__PURE__ */ new Map()
287
291
  };
288
292
  this.replaySessions.set(recordingId, session);
289
293
  console.log(
@@ -298,17 +302,14 @@ var ProxyServer = class {
298
302
  */
299
303
  async cleanupSession(sessionId) {
300
304
  if (this.replaySessions.has(sessionId)) {
301
- console.log(`[CLEANUP] Removing replay session: ${sessionId}`);
302
305
  this.replaySessions.delete(sessionId);
303
306
  }
304
307
  if (this.recordingId === sessionId) {
305
- console.log(`[CLEANUP] Saving and clearing active recording session: ${sessionId}`);
306
308
  await this.saveCurrentSession();
307
309
  this.currentSession = null;
308
310
  this.recordingId = null;
309
311
  }
310
312
  if (this.replayId === sessionId) {
311
- console.log(`[CLEANUP] Clearing active replay session: ${sessionId}`);
312
313
  this.replayId = null;
313
314
  }
314
315
  console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
@@ -357,7 +358,9 @@ var ProxyServer = class {
357
358
  return;
358
359
  }
359
360
  if (!mode) {
360
- throw new Error("Mode parameter is required when cleanup is not specified");
361
+ throw new Error(
362
+ "Mode parameter is required when cleanup is not specified"
363
+ );
361
364
  }
362
365
  const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
363
366
  this.clearModeTimeout();
@@ -440,12 +443,16 @@ var ProxyServer = class {
440
443
  this.replayId = id;
441
444
  this.recordingId = null;
442
445
  this.currentSession = null;
443
- const session = this.replaySessions.get(id);
444
- if (session) {
445
- session.servedRecordingIdsByKey.clear();
446
- console.log(`Reset served recordings tracker for session: ${id}`);
447
- } else {
448
- this.getOrCreateReplaySession(id);
446
+ const sessionState = this.getOrCreateReplaySession(id);
447
+ sessionState.servedRecordingIdsByKey.clear();
448
+ sessionState.sortedRecordingsByKey.clear();
449
+ const filePath = getRecordingPath(this.recordingsDir, id);
450
+ try {
451
+ sessionState.loadedSession = await loadRecordingSession(filePath);
452
+ console.log(`[REPLAY] Loaded recording session: ${id}`);
453
+ } catch (error) {
454
+ console.error(`[REPLAY ERROR] Failed to load session ${id}:`, error);
455
+ sessionState.loadedSession = null;
449
456
  }
450
457
  console.log(`Switched to replay mode with ID: ${id}`);
451
458
  }
@@ -459,21 +466,32 @@ var ProxyServer = class {
459
466
  }, timeout);
460
467
  }
461
468
  async flushPendingRecordings() {
469
+ if (this.flushPromise) {
470
+ await this.flushPromise;
471
+ return;
472
+ }
462
473
  if (this.recordingPromises.length === 0) {
463
474
  return;
464
475
  }
465
- const results = await Promise.allSettled(this.recordingPromises);
466
- if (this.currentSession) {
467
- for (const result of results) {
468
- if (result.status === "fulfilled" && result.value) {
469
- this.currentSession.recordings.push(result.value);
476
+ this.flushPromise = (async () => {
477
+ try {
478
+ const results = await Promise.allSettled(this.recordingPromises);
479
+ if (this.currentSession) {
480
+ for (const result of results) {
481
+ if (result.status === "fulfilled" && result.value) {
482
+ this.currentSession.recordings.push(result.value);
483
+ }
484
+ }
485
+ console.log(
486
+ `Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
487
+ );
470
488
  }
489
+ this.recordingPromises = [];
490
+ } finally {
491
+ this.flushPromise = null;
471
492
  }
472
- console.log(
473
- `Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
474
- );
475
- }
476
- this.recordingPromises = [];
493
+ })();
494
+ await this.flushPromise;
477
495
  }
478
496
  async saveCurrentSession() {
479
497
  if (!this.currentSession) {
@@ -523,20 +541,25 @@ var ProxyServer = class {
523
541
  );
524
542
  return recordingId;
525
543
  }
526
- async ensureSessionLoaded(recordingId, filePath) {
527
- const sessionState = this.getOrCreateReplaySession(recordingId);
528
- if (!sessionState.loadedSession) {
529
- sessionState.loadedSession = await loadRecordingSession(filePath);
530
- console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
531
- }
532
- return sessionState;
533
- }
534
544
  getServedTracker(sessionState, key) {
535
545
  if (!sessionState.servedRecordingIdsByKey.has(key)) {
536
546
  sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
537
547
  }
538
548
  return sessionState.servedRecordingIdsByKey.get(key);
539
549
  }
550
+ getSortedRecordings(sessionState, key) {
551
+ if (sessionState.sortedRecordingsByKey.has(key)) {
552
+ return sessionState.sortedRecordingsByKey.get(key);
553
+ }
554
+ const session = sessionState.loadedSession;
555
+ const sortedRecords = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
556
+ const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
557
+ const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
558
+ return aSeq - bSeq;
559
+ });
560
+ sessionState.sortedRecordingsByKey.set(key, sortedRecords);
561
+ return sortedRecords;
562
+ }
540
563
  selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
541
564
  for (const rec of recordsWithKey) {
542
565
  if (!servedForThisKey.has(rec.recordingId)) {
@@ -557,18 +580,17 @@ var ProxyServer = class {
557
580
  const key = getReqID(req);
558
581
  const filePath = getRecordingPath(this.recordingsDir, recordingId);
559
582
  try {
560
- const sessionState = await this.ensureSessionLoaded(
561
- recordingId,
562
- filePath
563
- );
564
- const session = sessionState.loadedSession;
583
+ const sessionState = this.getOrCreateReplaySession(recordingId);
584
+ if (!sessionState.loadedSession) {
585
+ const error = new Error(
586
+ `Recording session file not found: ${filePath}`
587
+ );
588
+ error.code = "ENOENT";
589
+ throw error;
590
+ }
565
591
  const servedForThisKey = this.getServedTracker(sessionState, key);
566
592
  const host = req.headers.host || "unknown";
567
- const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
568
- const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
569
- const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
570
- return aSeq - bSeq;
571
- });
593
+ const recordsWithKey = this.getSortedRecordings(sessionState, key);
572
594
  if (recordsWithKey.length === 0) {
573
595
  const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
574
596
  console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
@@ -779,7 +801,6 @@ var ProxyServer = class {
779
801
  })();
780
802
  });
781
803
  this.recordingPromises.push(recordingPromise);
782
- await recordingPromise;
783
804
  }
784
805
  handleUpgrade(req, socket, head) {
785
806
  if (this.mode === Modes.replay) {
@@ -973,7 +994,6 @@ async function setProxyMode(mode, sessionId, timeout) {
973
994
  throw new Error(`Failed to set proxy mode: ${text}`);
974
995
  }
975
996
  await response.json();
976
- console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
977
997
  } catch (error) {
978
998
  console.error(`Error setting proxy mode:`, error);
979
999
  throw error;
@@ -997,9 +1017,8 @@ async function cleanupSession(sessionId) {
997
1017
  throw new Error(`Failed to cleanup session: ${text}`);
998
1018
  }
999
1019
  await response.json();
1000
- console.log(`Session cleaned up: ${sessionId}`);
1001
1020
  } catch (error) {
1002
- console.error(`Error cleaning up session:`, error);
1021
+ console.error(`Error cleaning up session: ${sessionId}`, error);
1003
1022
  throw error;
1004
1023
  }
1005
1024
  }
@@ -1072,9 +1091,6 @@ async function setupClientSideRecording(page, sessionId, mode, url) {
1072
1091
  const harFileName = sessionId.replaceAll("/", "__");
1073
1092
  const recordingsDir = await getRecordingsDir();
1074
1093
  const harPath = path2__default.default.join(recordingsDir, `${harFileName}.har`);
1075
- console.log(
1076
- `[Client-Side Recording] Setting up HAR for session: ${sessionId}, mode: ${mode}, path: ${harPath}`
1077
- );
1078
1094
  try {
1079
1095
  await page.routeFromHAR(harPath, {
1080
1096
  url,
@@ -1107,41 +1123,27 @@ var playwrightProxy = {
1107
1123
  await page.setExtraHTTPHeaders({
1108
1124
  [RECORDING_ID_HEADER]: sessionId
1109
1125
  });
1110
- console.log(`[Setup] Setting proxy mode: ${mode}, session: ${sessionId}`);
1111
1126
  await setProxyMode(mode, sessionId, timeout);
1112
- console.log(`[Setup] Proxy mode set successfully`);
1113
1127
  if (clientSideOptions?.url) {
1114
- console.log(`[Setup] Setting up client-side recording with pattern: ${clientSideOptions.url}`);
1115
1128
  await setupClientSideRecording(
1116
1129
  page,
1117
1130
  sessionId,
1118
1131
  mode,
1119
1132
  clientSideOptions.url
1120
1133
  );
1121
- console.log(`[Setup] Client-side recording setup complete`);
1122
1134
  }
1123
1135
  const proxyPort = process.env.TEST_PROXY_RECORDER_PORT || "8100";
1124
1136
  const proxyUrl = `localhost:${proxyPort}`;
1125
- console.log(`[Setup] Registering proxy route handler for: ${proxyUrl}`);
1126
1137
  await page.route(
1127
1138
  (url) => {
1128
1139
  const urlStr = url.toString();
1129
1140
  const matches = urlStr.includes(proxyUrl);
1130
- if (matches) {
1131
- console.log(`[Route Matcher] Matched proxy request: ${urlStr}`);
1132
- }
1133
1141
  return matches;
1134
1142
  },
1135
1143
  async (route) => {
1136
1144
  try {
1137
- const url = route.request().url();
1138
- const method = route.request().method();
1139
1145
  const headers = route.request().headers();
1140
- const hadHeader = !!headers[RECORDING_ID_HEADER];
1141
1146
  headers[RECORDING_ID_HEADER] = sessionId;
1142
- console.log(
1143
- `[Route Intercept] ${method} ${url} (had header: ${hadHeader}, adding session: ${sessionId})`
1144
- );
1145
1147
  await route.continue({ headers });
1146
1148
  } catch (error) {
1147
1149
  console.error(
@@ -1154,7 +1156,6 @@ var playwrightProxy = {
1154
1156
  { times: Infinity }
1155
1157
  // Ensure the handler applies to all matching requests
1156
1158
  );
1157
- console.log(`[Setup] Proxy route handler registered`);
1158
1159
  const context = page.context();
1159
1160
  const contextId = context._guid || "default";
1160
1161
  const handlerKey = `cleanup_${contextId}`;
@@ -1162,9 +1163,6 @@ var playwrightProxy = {
1162
1163
  globalThis[handlerKey] = true;
1163
1164
  context.on("close", async () => {
1164
1165
  try {
1165
- console.log(
1166
- `[Cleanup] Browser context closed, cleaning up session: ${sessionId}`
1167
- );
1168
1166
  await cleanupSession(sessionId);
1169
1167
  } catch (error) {
1170
1168
  console.warn(
package/dist/index.d.cts CHANGED
@@ -17,6 +17,7 @@ declare class ProxyServer {
17
17
  private sequenceCounterByKey;
18
18
  private replaySessions;
19
19
  private recordingPromises;
20
+ private flushPromise;
20
21
  constructor(targets: string[], recordingsDir: string);
21
22
  init(): Promise<void>;
22
23
  listen(port: number): http.Server;
@@ -73,8 +74,8 @@ declare class ProxyServer {
73
74
  private flushPendingRecordings;
74
75
  private saveCurrentSession;
75
76
  private getRecordingIdOrError;
76
- private ensureSessionLoaded;
77
77
  private getServedTracker;
78
+ private getSortedRecordings;
78
79
  private selectReplayRecord;
79
80
  private handleReplayRequest;
80
81
  private handleReplayError;
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ declare class ProxyServer {
17
17
  private sequenceCounterByKey;
18
18
  private replaySessions;
19
19
  private recordingPromises;
20
+ private flushPromise;
20
21
  constructor(targets: string[], recordingsDir: string);
21
22
  init(): Promise<void>;
22
23
  listen(port: number): http.Server;
@@ -73,8 +74,8 @@ declare class ProxyServer {
73
74
  private flushPendingRecordings;
74
75
  private saveCurrentSession;
75
76
  private getRecordingIdOrError;
76
- private ensureSessionLoaded;
77
77
  private getServedTracker;
78
+ private getSortedRecordings;
78
79
  private selectReplayRecord;
79
80
  private handleReplayRequest;
80
81
  private handleReplayError;
package/dist/index.mjs CHANGED
@@ -136,6 +136,8 @@ var ProxyServer = class {
136
136
  // Track multiple concurrent replay sessions by recording ID
137
137
  recordingPromises;
138
138
  // Stack of promises that resolve to completed recordings
139
+ flushPromise;
140
+ // Promise for in-progress flush operation
139
141
  constructor(targets, recordingsDir) {
140
142
  this.targets = targets;
141
143
  this.currentTargetIndex = 0;
@@ -149,6 +151,7 @@ var ProxyServer = class {
149
151
  this.recordingsDir = recordingsDir;
150
152
  this.replaySessions = /* @__PURE__ */ new Map();
151
153
  this.recordingPromises = [];
154
+ this.flushPromise = null;
152
155
  this.proxy = httpProxy.createProxyServer({
153
156
  secure: false,
154
157
  changeOrigin: true,
@@ -271,7 +274,8 @@ var ProxyServer = class {
271
274
  recordingId,
272
275
  servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
273
276
  loadedSession: null,
274
- lastAccessTime: Date.now()
277
+ lastAccessTime: Date.now(),
278
+ sortedRecordingsByKey: /* @__PURE__ */ new Map()
275
279
  };
276
280
  this.replaySessions.set(recordingId, session);
277
281
  console.log(
@@ -286,17 +290,14 @@ var ProxyServer = class {
286
290
  */
287
291
  async cleanupSession(sessionId) {
288
292
  if (this.replaySessions.has(sessionId)) {
289
- console.log(`[CLEANUP] Removing replay session: ${sessionId}`);
290
293
  this.replaySessions.delete(sessionId);
291
294
  }
292
295
  if (this.recordingId === sessionId) {
293
- console.log(`[CLEANUP] Saving and clearing active recording session: ${sessionId}`);
294
296
  await this.saveCurrentSession();
295
297
  this.currentSession = null;
296
298
  this.recordingId = null;
297
299
  }
298
300
  if (this.replayId === sessionId) {
299
- console.log(`[CLEANUP] Clearing active replay session: ${sessionId}`);
300
301
  this.replayId = null;
301
302
  }
302
303
  console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
@@ -345,7 +346,9 @@ var ProxyServer = class {
345
346
  return;
346
347
  }
347
348
  if (!mode) {
348
- throw new Error("Mode parameter is required when cleanup is not specified");
349
+ throw new Error(
350
+ "Mode parameter is required when cleanup is not specified"
351
+ );
349
352
  }
350
353
  const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
351
354
  this.clearModeTimeout();
@@ -428,12 +431,16 @@ var ProxyServer = class {
428
431
  this.replayId = id;
429
432
  this.recordingId = null;
430
433
  this.currentSession = null;
431
- const session = this.replaySessions.get(id);
432
- if (session) {
433
- session.servedRecordingIdsByKey.clear();
434
- console.log(`Reset served recordings tracker for session: ${id}`);
435
- } else {
436
- this.getOrCreateReplaySession(id);
434
+ const sessionState = this.getOrCreateReplaySession(id);
435
+ sessionState.servedRecordingIdsByKey.clear();
436
+ sessionState.sortedRecordingsByKey.clear();
437
+ const filePath = getRecordingPath(this.recordingsDir, id);
438
+ try {
439
+ sessionState.loadedSession = await loadRecordingSession(filePath);
440
+ console.log(`[REPLAY] Loaded recording session: ${id}`);
441
+ } catch (error) {
442
+ console.error(`[REPLAY ERROR] Failed to load session ${id}:`, error);
443
+ sessionState.loadedSession = null;
437
444
  }
438
445
  console.log(`Switched to replay mode with ID: ${id}`);
439
446
  }
@@ -447,21 +454,32 @@ var ProxyServer = class {
447
454
  }, timeout);
448
455
  }
449
456
  async flushPendingRecordings() {
457
+ if (this.flushPromise) {
458
+ await this.flushPromise;
459
+ return;
460
+ }
450
461
  if (this.recordingPromises.length === 0) {
451
462
  return;
452
463
  }
453
- const results = await Promise.allSettled(this.recordingPromises);
454
- if (this.currentSession) {
455
- for (const result of results) {
456
- if (result.status === "fulfilled" && result.value) {
457
- this.currentSession.recordings.push(result.value);
464
+ this.flushPromise = (async () => {
465
+ try {
466
+ const results = await Promise.allSettled(this.recordingPromises);
467
+ if (this.currentSession) {
468
+ for (const result of results) {
469
+ if (result.status === "fulfilled" && result.value) {
470
+ this.currentSession.recordings.push(result.value);
471
+ }
472
+ }
473
+ console.log(
474
+ `Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
475
+ );
458
476
  }
477
+ this.recordingPromises = [];
478
+ } finally {
479
+ this.flushPromise = null;
459
480
  }
460
- console.log(
461
- `Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
462
- );
463
- }
464
- this.recordingPromises = [];
481
+ })();
482
+ await this.flushPromise;
465
483
  }
466
484
  async saveCurrentSession() {
467
485
  if (!this.currentSession) {
@@ -511,20 +529,25 @@ var ProxyServer = class {
511
529
  );
512
530
  return recordingId;
513
531
  }
514
- async ensureSessionLoaded(recordingId, filePath) {
515
- const sessionState = this.getOrCreateReplaySession(recordingId);
516
- if (!sessionState.loadedSession) {
517
- sessionState.loadedSession = await loadRecordingSession(filePath);
518
- console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
519
- }
520
- return sessionState;
521
- }
522
532
  getServedTracker(sessionState, key) {
523
533
  if (!sessionState.servedRecordingIdsByKey.has(key)) {
524
534
  sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
525
535
  }
526
536
  return sessionState.servedRecordingIdsByKey.get(key);
527
537
  }
538
+ getSortedRecordings(sessionState, key) {
539
+ if (sessionState.sortedRecordingsByKey.has(key)) {
540
+ return sessionState.sortedRecordingsByKey.get(key);
541
+ }
542
+ const session = sessionState.loadedSession;
543
+ const sortedRecords = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
544
+ const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
545
+ const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
546
+ return aSeq - bSeq;
547
+ });
548
+ sessionState.sortedRecordingsByKey.set(key, sortedRecords);
549
+ return sortedRecords;
550
+ }
528
551
  selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
529
552
  for (const rec of recordsWithKey) {
530
553
  if (!servedForThisKey.has(rec.recordingId)) {
@@ -545,18 +568,17 @@ var ProxyServer = class {
545
568
  const key = getReqID(req);
546
569
  const filePath = getRecordingPath(this.recordingsDir, recordingId);
547
570
  try {
548
- const sessionState = await this.ensureSessionLoaded(
549
- recordingId,
550
- filePath
551
- );
552
- const session = sessionState.loadedSession;
571
+ const sessionState = this.getOrCreateReplaySession(recordingId);
572
+ if (!sessionState.loadedSession) {
573
+ const error = new Error(
574
+ `Recording session file not found: ${filePath}`
575
+ );
576
+ error.code = "ENOENT";
577
+ throw error;
578
+ }
553
579
  const servedForThisKey = this.getServedTracker(sessionState, key);
554
580
  const host = req.headers.host || "unknown";
555
- const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
556
- const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
557
- const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
558
- return aSeq - bSeq;
559
- });
581
+ const recordsWithKey = this.getSortedRecordings(sessionState, key);
560
582
  if (recordsWithKey.length === 0) {
561
583
  const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
562
584
  console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
@@ -767,7 +789,6 @@ var ProxyServer = class {
767
789
  })();
768
790
  });
769
791
  this.recordingPromises.push(recordingPromise);
770
- await recordingPromise;
771
792
  }
772
793
  handleUpgrade(req, socket, head) {
773
794
  if (this.mode === Modes.replay) {
@@ -961,7 +982,6 @@ async function setProxyMode(mode, sessionId, timeout) {
961
982
  throw new Error(`Failed to set proxy mode: ${text}`);
962
983
  }
963
984
  await response.json();
964
- console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
965
985
  } catch (error) {
966
986
  console.error(`Error setting proxy mode:`, error);
967
987
  throw error;
@@ -985,9 +1005,8 @@ async function cleanupSession(sessionId) {
985
1005
  throw new Error(`Failed to cleanup session: ${text}`);
986
1006
  }
987
1007
  await response.json();
988
- console.log(`Session cleaned up: ${sessionId}`);
989
1008
  } catch (error) {
990
- console.error(`Error cleaning up session:`, error);
1009
+ console.error(`Error cleaning up session: ${sessionId}`, error);
991
1010
  throw error;
992
1011
  }
993
1012
  }
@@ -1060,9 +1079,6 @@ async function setupClientSideRecording(page, sessionId, mode, url) {
1060
1079
  const harFileName = sessionId.replaceAll("/", "__");
1061
1080
  const recordingsDir = await getRecordingsDir();
1062
1081
  const harPath = path2.join(recordingsDir, `${harFileName}.har`);
1063
- console.log(
1064
- `[Client-Side Recording] Setting up HAR for session: ${sessionId}, mode: ${mode}, path: ${harPath}`
1065
- );
1066
1082
  try {
1067
1083
  await page.routeFromHAR(harPath, {
1068
1084
  url,
@@ -1095,41 +1111,27 @@ var playwrightProxy = {
1095
1111
  await page.setExtraHTTPHeaders({
1096
1112
  [RECORDING_ID_HEADER]: sessionId
1097
1113
  });
1098
- console.log(`[Setup] Setting proxy mode: ${mode}, session: ${sessionId}`);
1099
1114
  await setProxyMode(mode, sessionId, timeout);
1100
- console.log(`[Setup] Proxy mode set successfully`);
1101
1115
  if (clientSideOptions?.url) {
1102
- console.log(`[Setup] Setting up client-side recording with pattern: ${clientSideOptions.url}`);
1103
1116
  await setupClientSideRecording(
1104
1117
  page,
1105
1118
  sessionId,
1106
1119
  mode,
1107
1120
  clientSideOptions.url
1108
1121
  );
1109
- console.log(`[Setup] Client-side recording setup complete`);
1110
1122
  }
1111
1123
  const proxyPort = process.env.TEST_PROXY_RECORDER_PORT || "8100";
1112
1124
  const proxyUrl = `localhost:${proxyPort}`;
1113
- console.log(`[Setup] Registering proxy route handler for: ${proxyUrl}`);
1114
1125
  await page.route(
1115
1126
  (url) => {
1116
1127
  const urlStr = url.toString();
1117
1128
  const matches = urlStr.includes(proxyUrl);
1118
- if (matches) {
1119
- console.log(`[Route Matcher] Matched proxy request: ${urlStr}`);
1120
- }
1121
1129
  return matches;
1122
1130
  },
1123
1131
  async (route) => {
1124
1132
  try {
1125
- const url = route.request().url();
1126
- const method = route.request().method();
1127
1133
  const headers = route.request().headers();
1128
- const hadHeader = !!headers[RECORDING_ID_HEADER];
1129
1134
  headers[RECORDING_ID_HEADER] = sessionId;
1130
- console.log(
1131
- `[Route Intercept] ${method} ${url} (had header: ${hadHeader}, adding session: ${sessionId})`
1132
- );
1133
1135
  await route.continue({ headers });
1134
1136
  } catch (error) {
1135
1137
  console.error(
@@ -1142,7 +1144,6 @@ var playwrightProxy = {
1142
1144
  { times: Infinity }
1143
1145
  // Ensure the handler applies to all matching requests
1144
1146
  );
1145
- console.log(`[Setup] Proxy route handler registered`);
1146
1147
  const context = page.context();
1147
1148
  const contextId = context._guid || "default";
1148
1149
  const handlerKey = `cleanup_${contextId}`;
@@ -1150,9 +1151,6 @@ var playwrightProxy = {
1150
1151
  globalThis[handlerKey] = true;
1151
1152
  context.on("close", async () => {
1152
1153
  try {
1153
- console.log(
1154
- `[Cleanup] Browser context closed, cleaning up session: ${sessionId}`
1155
- );
1156
1154
  await cleanupSession(sessionId);
1157
1155
  } catch (error) {
1158
1156
  console.warn(
@@ -46,7 +46,6 @@ async function setProxyMode(mode, sessionId, timeout) {
46
46
  throw new Error(`Failed to set proxy mode: ${text}`);
47
47
  }
48
48
  await response.json();
49
- console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
50
49
  } catch (error) {
51
50
  console.error(`Error setting proxy mode:`, error);
52
51
  throw error;
@@ -70,9 +69,8 @@ async function cleanupSession(sessionId) {
70
69
  throw new Error(`Failed to cleanup session: ${text}`);
71
70
  }
72
71
  await response.json();
73
- console.log(`Session cleaned up: ${sessionId}`);
74
72
  } catch (error) {
75
- console.error(`Error cleaning up session:`, error);
73
+ console.error(`Error cleaning up session: ${sessionId}`, error);
76
74
  throw error;
77
75
  }
78
76
  }
@@ -145,9 +143,6 @@ async function setupClientSideRecording(page, sessionId, mode, url) {
145
143
  const harFileName = sessionId.replaceAll("/", "__");
146
144
  const recordingsDir = await getRecordingsDir();
147
145
  const harPath = path__default.default.join(recordingsDir, `${harFileName}.har`);
148
- console.log(
149
- `[Client-Side Recording] Setting up HAR for session: ${sessionId}, mode: ${mode}, path: ${harPath}`
150
- );
151
146
  try {
152
147
  await page.routeFromHAR(harPath, {
153
148
  url,
@@ -180,41 +175,27 @@ var playwrightProxy = {
180
175
  await page.setExtraHTTPHeaders({
181
176
  [RECORDING_ID_HEADER]: sessionId
182
177
  });
183
- console.log(`[Setup] Setting proxy mode: ${mode}, session: ${sessionId}`);
184
178
  await setProxyMode(mode, sessionId, timeout);
185
- console.log(`[Setup] Proxy mode set successfully`);
186
179
  if (clientSideOptions?.url) {
187
- console.log(`[Setup] Setting up client-side recording with pattern: ${clientSideOptions.url}`);
188
180
  await setupClientSideRecording(
189
181
  page,
190
182
  sessionId,
191
183
  mode,
192
184
  clientSideOptions.url
193
185
  );
194
- console.log(`[Setup] Client-side recording setup complete`);
195
186
  }
196
187
  const proxyPort = process.env.TEST_PROXY_RECORDER_PORT || "8100";
197
188
  const proxyUrl = `localhost:${proxyPort}`;
198
- console.log(`[Setup] Registering proxy route handler for: ${proxyUrl}`);
199
189
  await page.route(
200
190
  (url) => {
201
191
  const urlStr = url.toString();
202
192
  const matches = urlStr.includes(proxyUrl);
203
- if (matches) {
204
- console.log(`[Route Matcher] Matched proxy request: ${urlStr}`);
205
- }
206
193
  return matches;
207
194
  },
208
195
  async (route) => {
209
196
  try {
210
- const url = route.request().url();
211
- const method = route.request().method();
212
197
  const headers = route.request().headers();
213
- const hadHeader = !!headers[RECORDING_ID_HEADER];
214
198
  headers[RECORDING_ID_HEADER] = sessionId;
215
- console.log(
216
- `[Route Intercept] ${method} ${url} (had header: ${hadHeader}, adding session: ${sessionId})`
217
- );
218
199
  await route.continue({ headers });
219
200
  } catch (error) {
220
201
  console.error(
@@ -227,7 +208,6 @@ var playwrightProxy = {
227
208
  { times: Infinity }
228
209
  // Ensure the handler applies to all matching requests
229
210
  );
230
- console.log(`[Setup] Proxy route handler registered`);
231
211
  const context = page.context();
232
212
  const contextId = context._guid || "default";
233
213
  const handlerKey = `cleanup_${contextId}`;
@@ -235,9 +215,6 @@ var playwrightProxy = {
235
215
  globalThis[handlerKey] = true;
236
216
  context.on("close", async () => {
237
217
  try {
238
- console.log(
239
- `[Cleanup] Browser context closed, cleaning up session: ${sessionId}`
240
- );
241
218
  await cleanupSession(sessionId);
242
219
  } catch (error) {
243
220
  console.warn(
@@ -40,7 +40,6 @@ async function setProxyMode(mode, sessionId, timeout) {
40
40
  throw new Error(`Failed to set proxy mode: ${text}`);
41
41
  }
42
42
  await response.json();
43
- console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
44
43
  } catch (error) {
45
44
  console.error(`Error setting proxy mode:`, error);
46
45
  throw error;
@@ -64,9 +63,8 @@ async function cleanupSession(sessionId) {
64
63
  throw new Error(`Failed to cleanup session: ${text}`);
65
64
  }
66
65
  await response.json();
67
- console.log(`Session cleaned up: ${sessionId}`);
68
66
  } catch (error) {
69
- console.error(`Error cleaning up session:`, error);
67
+ console.error(`Error cleaning up session: ${sessionId}`, error);
70
68
  throw error;
71
69
  }
72
70
  }
@@ -139,9 +137,6 @@ async function setupClientSideRecording(page, sessionId, mode, url) {
139
137
  const harFileName = sessionId.replaceAll("/", "__");
140
138
  const recordingsDir = await getRecordingsDir();
141
139
  const harPath = path.join(recordingsDir, `${harFileName}.har`);
142
- console.log(
143
- `[Client-Side Recording] Setting up HAR for session: ${sessionId}, mode: ${mode}, path: ${harPath}`
144
- );
145
140
  try {
146
141
  await page.routeFromHAR(harPath, {
147
142
  url,
@@ -174,41 +169,27 @@ var playwrightProxy = {
174
169
  await page.setExtraHTTPHeaders({
175
170
  [RECORDING_ID_HEADER]: sessionId
176
171
  });
177
- console.log(`[Setup] Setting proxy mode: ${mode}, session: ${sessionId}`);
178
172
  await setProxyMode(mode, sessionId, timeout);
179
- console.log(`[Setup] Proxy mode set successfully`);
180
173
  if (clientSideOptions?.url) {
181
- console.log(`[Setup] Setting up client-side recording with pattern: ${clientSideOptions.url}`);
182
174
  await setupClientSideRecording(
183
175
  page,
184
176
  sessionId,
185
177
  mode,
186
178
  clientSideOptions.url
187
179
  );
188
- console.log(`[Setup] Client-side recording setup complete`);
189
180
  }
190
181
  const proxyPort = process.env.TEST_PROXY_RECORDER_PORT || "8100";
191
182
  const proxyUrl = `localhost:${proxyPort}`;
192
- console.log(`[Setup] Registering proxy route handler for: ${proxyUrl}`);
193
183
  await page.route(
194
184
  (url) => {
195
185
  const urlStr = url.toString();
196
186
  const matches = urlStr.includes(proxyUrl);
197
- if (matches) {
198
- console.log(`[Route Matcher] Matched proxy request: ${urlStr}`);
199
- }
200
187
  return matches;
201
188
  },
202
189
  async (route) => {
203
190
  try {
204
- const url = route.request().url();
205
- const method = route.request().method();
206
191
  const headers = route.request().headers();
207
- const hadHeader = !!headers[RECORDING_ID_HEADER];
208
192
  headers[RECORDING_ID_HEADER] = sessionId;
209
- console.log(
210
- `[Route Intercept] ${method} ${url} (had header: ${hadHeader}, adding session: ${sessionId})`
211
- );
212
193
  await route.continue({ headers });
213
194
  } catch (error) {
214
195
  console.error(
@@ -221,7 +202,6 @@ var playwrightProxy = {
221
202
  { times: Infinity }
222
203
  // Ensure the handler applies to all matching requests
223
204
  );
224
- console.log(`[Setup] Proxy route handler registered`);
225
205
  const context = page.context();
226
206
  const contextId = context._guid || "default";
227
207
  const handlerKey = `cleanup_${contextId}`;
@@ -229,9 +209,6 @@ var playwrightProxy = {
229
209
  globalThis[handlerKey] = true;
230
210
  context.on("close", async () => {
231
211
  try {
232
- console.log(
233
- `[Cleanup] Browser context closed, cleaning up session: ${sessionId}`
234
- );
235
212
  await cleanupSession(sessionId);
236
213
  } catch (error) {
237
214
  console.warn(
package/dist/proxy.js CHANGED
@@ -172,6 +172,8 @@ var ProxyServer = class {
172
172
  // Track multiple concurrent replay sessions by recording ID
173
173
  recordingPromises;
174
174
  // Stack of promises that resolve to completed recordings
175
+ flushPromise;
176
+ // Promise for in-progress flush operation
175
177
  constructor(targets2, recordingsDir2) {
176
178
  this.targets = targets2;
177
179
  this.currentTargetIndex = 0;
@@ -185,6 +187,7 @@ var ProxyServer = class {
185
187
  this.recordingsDir = recordingsDir2;
186
188
  this.replaySessions = /* @__PURE__ */ new Map();
187
189
  this.recordingPromises = [];
190
+ this.flushPromise = null;
188
191
  this.proxy = httpProxy.createProxyServer({
189
192
  secure: false,
190
193
  changeOrigin: true,
@@ -307,7 +310,8 @@ var ProxyServer = class {
307
310
  recordingId,
308
311
  servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
309
312
  loadedSession: null,
310
- lastAccessTime: Date.now()
313
+ lastAccessTime: Date.now(),
314
+ sortedRecordingsByKey: /* @__PURE__ */ new Map()
311
315
  };
312
316
  this.replaySessions.set(recordingId, session);
313
317
  console.log(
@@ -322,17 +326,14 @@ var ProxyServer = class {
322
326
  */
323
327
  async cleanupSession(sessionId) {
324
328
  if (this.replaySessions.has(sessionId)) {
325
- console.log(`[CLEANUP] Removing replay session: ${sessionId}`);
326
329
  this.replaySessions.delete(sessionId);
327
330
  }
328
331
  if (this.recordingId === sessionId) {
329
- console.log(`[CLEANUP] Saving and clearing active recording session: ${sessionId}`);
330
332
  await this.saveCurrentSession();
331
333
  this.currentSession = null;
332
334
  this.recordingId = null;
333
335
  }
334
336
  if (this.replayId === sessionId) {
335
- console.log(`[CLEANUP] Clearing active replay session: ${sessionId}`);
336
337
  this.replayId = null;
337
338
  }
338
339
  console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
@@ -381,7 +382,9 @@ var ProxyServer = class {
381
382
  return;
382
383
  }
383
384
  if (!mode) {
384
- throw new Error("Mode parameter is required when cleanup is not specified");
385
+ throw new Error(
386
+ "Mode parameter is required when cleanup is not specified"
387
+ );
385
388
  }
386
389
  const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
387
390
  this.clearModeTimeout();
@@ -464,12 +467,16 @@ var ProxyServer = class {
464
467
  this.replayId = id;
465
468
  this.recordingId = null;
466
469
  this.currentSession = null;
467
- const session = this.replaySessions.get(id);
468
- if (session) {
469
- session.servedRecordingIdsByKey.clear();
470
- console.log(`Reset served recordings tracker for session: ${id}`);
471
- } else {
472
- this.getOrCreateReplaySession(id);
470
+ const sessionState = this.getOrCreateReplaySession(id);
471
+ sessionState.servedRecordingIdsByKey.clear();
472
+ sessionState.sortedRecordingsByKey.clear();
473
+ const filePath = getRecordingPath(this.recordingsDir, id);
474
+ try {
475
+ sessionState.loadedSession = await loadRecordingSession(filePath);
476
+ console.log(`[REPLAY] Loaded recording session: ${id}`);
477
+ } catch (error) {
478
+ console.error(`[REPLAY ERROR] Failed to load session ${id}:`, error);
479
+ sessionState.loadedSession = null;
473
480
  }
474
481
  console.log(`Switched to replay mode with ID: ${id}`);
475
482
  }
@@ -483,21 +490,32 @@ var ProxyServer = class {
483
490
  }, timeout);
484
491
  }
485
492
  async flushPendingRecordings() {
493
+ if (this.flushPromise) {
494
+ await this.flushPromise;
495
+ return;
496
+ }
486
497
  if (this.recordingPromises.length === 0) {
487
498
  return;
488
499
  }
489
- const results = await Promise.allSettled(this.recordingPromises);
490
- if (this.currentSession) {
491
- for (const result of results) {
492
- if (result.status === "fulfilled" && result.value) {
493
- this.currentSession.recordings.push(result.value);
500
+ this.flushPromise = (async () => {
501
+ try {
502
+ const results = await Promise.allSettled(this.recordingPromises);
503
+ if (this.currentSession) {
504
+ for (const result of results) {
505
+ if (result.status === "fulfilled" && result.value) {
506
+ this.currentSession.recordings.push(result.value);
507
+ }
508
+ }
509
+ console.log(
510
+ `Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
511
+ );
494
512
  }
513
+ this.recordingPromises = [];
514
+ } finally {
515
+ this.flushPromise = null;
495
516
  }
496
- console.log(
497
- `Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
498
- );
499
- }
500
- this.recordingPromises = [];
517
+ })();
518
+ await this.flushPromise;
501
519
  }
502
520
  async saveCurrentSession() {
503
521
  if (!this.currentSession) {
@@ -547,20 +565,25 @@ var ProxyServer = class {
547
565
  );
548
566
  return recordingId;
549
567
  }
550
- async ensureSessionLoaded(recordingId, filePath) {
551
- const sessionState = this.getOrCreateReplaySession(recordingId);
552
- if (!sessionState.loadedSession) {
553
- sessionState.loadedSession = await loadRecordingSession(filePath);
554
- console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
555
- }
556
- return sessionState;
557
- }
558
568
  getServedTracker(sessionState, key) {
559
569
  if (!sessionState.servedRecordingIdsByKey.has(key)) {
560
570
  sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
561
571
  }
562
572
  return sessionState.servedRecordingIdsByKey.get(key);
563
573
  }
574
+ getSortedRecordings(sessionState, key) {
575
+ if (sessionState.sortedRecordingsByKey.has(key)) {
576
+ return sessionState.sortedRecordingsByKey.get(key);
577
+ }
578
+ const session = sessionState.loadedSession;
579
+ const sortedRecords = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
580
+ const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
581
+ const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
582
+ return aSeq - bSeq;
583
+ });
584
+ sessionState.sortedRecordingsByKey.set(key, sortedRecords);
585
+ return sortedRecords;
586
+ }
564
587
  selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
565
588
  for (const rec of recordsWithKey) {
566
589
  if (!servedForThisKey.has(rec.recordingId)) {
@@ -581,18 +604,17 @@ var ProxyServer = class {
581
604
  const key = getReqID(req);
582
605
  const filePath = getRecordingPath(this.recordingsDir, recordingId);
583
606
  try {
584
- const sessionState = await this.ensureSessionLoaded(
585
- recordingId,
586
- filePath
587
- );
588
- const session = sessionState.loadedSession;
607
+ const sessionState = this.getOrCreateReplaySession(recordingId);
608
+ if (!sessionState.loadedSession) {
609
+ const error = new Error(
610
+ `Recording session file not found: ${filePath}`
611
+ );
612
+ error.code = "ENOENT";
613
+ throw error;
614
+ }
589
615
  const servedForThisKey = this.getServedTracker(sessionState, key);
590
616
  const host = req.headers.host || "unknown";
591
- const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
592
- const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
593
- const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
594
- return aSeq - bSeq;
595
- });
617
+ const recordsWithKey = this.getSortedRecordings(sessionState, key);
596
618
  if (recordsWithKey.length === 0) {
597
619
  const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
598
620
  console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
@@ -803,7 +825,6 @@ var ProxyServer = class {
803
825
  })();
804
826
  });
805
827
  this.recordingPromises.push(recordingPromise);
806
- await recordingPromise;
807
828
  }
808
829
  handleUpgrade(req, socket, head) {
809
830
  if (this.mode === Modes.replay) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "test-proxy-recorder",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "HTTP proxy server for recording and replaying network requests in testing. Works seamlessly with Playwright testing framework.",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",