test-proxy-recorder 0.3.2 → 0.3.4

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
@@ -131,8 +131,7 @@ function sendJsonResponse(res, statusCode, data) {
131
131
 
132
132
  // src/ProxyServer.ts
133
133
  var ProxyServer = class {
134
- targets;
135
- currentTargetIndex;
134
+ target;
136
135
  mode;
137
136
  recordingId;
138
137
  replayId;
@@ -148,9 +147,10 @@ var ProxyServer = class {
148
147
  // Track multiple concurrent replay sessions by recording ID
149
148
  recordingPromises;
150
149
  // Stack of promises that resolve to completed recordings
151
- constructor(targets, recordingsDir) {
152
- this.targets = targets;
153
- this.currentTargetIndex = 0;
150
+ flushPromise;
151
+ // Promise for in-progress flush operation
152
+ constructor(target, recordingsDir) {
153
+ this.target = target;
154
154
  this.mode = Modes.transparent;
155
155
  this.recordingId = null;
156
156
  this.recordingIdCounter = 0;
@@ -161,6 +161,7 @@ var ProxyServer = class {
161
161
  this.recordingsDir = recordingsDir;
162
162
  this.replaySessions = /* @__PURE__ */ new Map();
163
163
  this.recordingPromises = [];
164
+ this.flushPromise = null;
164
165
  this.proxy = httpProxy__default.default.createProxyServer({
165
166
  secure: false,
166
167
  changeOrigin: true,
@@ -222,9 +223,7 @@ var ProxyServer = class {
222
223
  Object.assign(proxyRes.headers, corsHeaders);
223
224
  }
224
225
  getTarget() {
225
- const target = this.targets[this.currentTargetIndex];
226
- this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
227
- return target;
226
+ return this.target;
228
227
  }
229
228
  /**
230
229
  * Extract recording ID from custom HTTP header
@@ -283,7 +282,8 @@ var ProxyServer = class {
283
282
  recordingId,
284
283
  servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
285
284
  loadedSession: null,
286
- lastAccessTime: Date.now()
285
+ lastAccessTime: Date.now(),
286
+ sortedRecordingsByKey: /* @__PURE__ */ new Map()
287
287
  };
288
288
  this.replaySessions.set(recordingId, session);
289
289
  console.log(
@@ -298,17 +298,14 @@ var ProxyServer = class {
298
298
  */
299
299
  async cleanupSession(sessionId) {
300
300
  if (this.replaySessions.has(sessionId)) {
301
- console.log(`[CLEANUP] Removing replay session: ${sessionId}`);
302
301
  this.replaySessions.delete(sessionId);
303
302
  }
304
303
  if (this.recordingId === sessionId) {
305
- console.log(`[CLEANUP] Saving and clearing active recording session: ${sessionId}`);
306
304
  await this.saveCurrentSession();
307
305
  this.currentSession = null;
308
306
  this.recordingId = null;
309
307
  }
310
308
  if (this.replayId === sessionId) {
311
- console.log(`[CLEANUP] Clearing active replay session: ${sessionId}`);
312
309
  this.replayId = null;
313
310
  }
314
311
  console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
@@ -357,7 +354,9 @@ var ProxyServer = class {
357
354
  return;
358
355
  }
359
356
  if (!mode) {
360
- throw new Error("Mode parameter is required when cleanup is not specified");
357
+ throw new Error(
358
+ "Mode parameter is required when cleanup is not specified"
359
+ );
361
360
  }
362
361
  const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
363
362
  this.clearModeTimeout();
@@ -440,12 +439,16 @@ var ProxyServer = class {
440
439
  this.replayId = id;
441
440
  this.recordingId = null;
442
441
  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);
442
+ const sessionState = this.getOrCreateReplaySession(id);
443
+ sessionState.servedRecordingIdsByKey.clear();
444
+ sessionState.sortedRecordingsByKey.clear();
445
+ const filePath = getRecordingPath(this.recordingsDir, id);
446
+ try {
447
+ sessionState.loadedSession = await loadRecordingSession(filePath);
448
+ console.log(`[REPLAY] Loaded recording session: ${id}`);
449
+ } catch (error) {
450
+ console.error(`[REPLAY ERROR] Failed to load session ${id}:`, error);
451
+ sessionState.loadedSession = null;
449
452
  }
450
453
  console.log(`Switched to replay mode with ID: ${id}`);
451
454
  }
@@ -459,21 +462,32 @@ var ProxyServer = class {
459
462
  }, timeout);
460
463
  }
461
464
  async flushPendingRecordings() {
465
+ if (this.flushPromise) {
466
+ await this.flushPromise;
467
+ return;
468
+ }
462
469
  if (this.recordingPromises.length === 0) {
463
470
  return;
464
471
  }
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);
472
+ this.flushPromise = (async () => {
473
+ try {
474
+ const results = await Promise.allSettled(this.recordingPromises);
475
+ if (this.currentSession) {
476
+ for (const result of results) {
477
+ if (result.status === "fulfilled" && result.value) {
478
+ this.currentSession.recordings.push(result.value);
479
+ }
480
+ }
481
+ console.log(
482
+ `Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
483
+ );
470
484
  }
485
+ this.recordingPromises = [];
486
+ } finally {
487
+ this.flushPromise = null;
471
488
  }
472
- console.log(
473
- `Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
474
- );
475
- }
476
- this.recordingPromises = [];
489
+ })();
490
+ await this.flushPromise;
477
491
  }
478
492
  async saveCurrentSession() {
479
493
  if (!this.currentSession) {
@@ -523,20 +537,25 @@ var ProxyServer = class {
523
537
  );
524
538
  return recordingId;
525
539
  }
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
540
  getServedTracker(sessionState, key) {
535
541
  if (!sessionState.servedRecordingIdsByKey.has(key)) {
536
542
  sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
537
543
  }
538
544
  return sessionState.servedRecordingIdsByKey.get(key);
539
545
  }
546
+ getSortedRecordings(sessionState, key) {
547
+ if (sessionState.sortedRecordingsByKey.has(key)) {
548
+ return sessionState.sortedRecordingsByKey.get(key);
549
+ }
550
+ const session = sessionState.loadedSession;
551
+ const sortedRecords = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
552
+ const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
553
+ const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
554
+ return aSeq - bSeq;
555
+ });
556
+ sessionState.sortedRecordingsByKey.set(key, sortedRecords);
557
+ return sortedRecords;
558
+ }
540
559
  selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
541
560
  for (const rec of recordsWithKey) {
542
561
  if (!servedForThisKey.has(rec.recordingId)) {
@@ -557,18 +576,17 @@ var ProxyServer = class {
557
576
  const key = getReqID(req);
558
577
  const filePath = getRecordingPath(this.recordingsDir, recordingId);
559
578
  try {
560
- const sessionState = await this.ensureSessionLoaded(
561
- recordingId,
562
- filePath
563
- );
564
- const session = sessionState.loadedSession;
579
+ const sessionState = this.getOrCreateReplaySession(recordingId);
580
+ if (!sessionState.loadedSession) {
581
+ const error = new Error(
582
+ `Recording session file not found: ${filePath}`
583
+ );
584
+ error.code = "ENOENT";
585
+ throw error;
586
+ }
565
587
  const servedForThisKey = this.getServedTracker(sessionState, key);
566
588
  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
- });
589
+ const recordsWithKey = this.getSortedRecordings(sessionState, key);
572
590
  if (recordsWithKey.length === 0) {
573
591
  const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
574
592
  console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
@@ -779,7 +797,6 @@ var ProxyServer = class {
779
797
  })();
780
798
  });
781
799
  this.recordingPromises.push(recordingPromise);
782
- await recordingPromise;
783
800
  }
784
801
  handleUpgrade(req, socket, head) {
785
802
  if (this.mode === Modes.replay) {
@@ -938,7 +955,7 @@ var ProxyServer = class {
938
955
  logServerStartup(port) {
939
956
  console.log(`Proxy server running on http://localhost:${port}`);
940
957
  console.log(`Mode: ${this.mode}`);
941
- console.log(`Targets: ${this.targets.join(", ")}`);
958
+ console.log(`Target: ${this.target}`);
942
959
  console.log(
943
960
  `Control endpoint: http://localhost:${port}${CONTROL_ENDPOINT}`
944
961
  );
@@ -973,7 +990,6 @@ async function setProxyMode(mode, sessionId, timeout) {
973
990
  throw new Error(`Failed to set proxy mode: ${text}`);
974
991
  }
975
992
  await response.json();
976
- console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
977
993
  } catch (error) {
978
994
  console.error(`Error setting proxy mode:`, error);
979
995
  throw error;
@@ -997,9 +1013,8 @@ async function cleanupSession(sessionId) {
997
1013
  throw new Error(`Failed to cleanup session: ${text}`);
998
1014
  }
999
1015
  await response.json();
1000
- console.log(`Session cleaned up: ${sessionId}`);
1001
1016
  } catch (error) {
1002
- console.error(`Error cleaning up session:`, error);
1017
+ console.error(`Error cleaning up session: ${sessionId}`, error);
1003
1018
  throw error;
1004
1019
  }
1005
1020
  }
@@ -1072,9 +1087,6 @@ async function setupClientSideRecording(page, sessionId, mode, url) {
1072
1087
  const harFileName = sessionId.replaceAll("/", "__");
1073
1088
  const recordingsDir = await getRecordingsDir();
1074
1089
  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
1090
  try {
1079
1091
  await page.routeFromHAR(harPath, {
1080
1092
  url,
@@ -1107,41 +1119,27 @@ var playwrightProxy = {
1107
1119
  await page.setExtraHTTPHeaders({
1108
1120
  [RECORDING_ID_HEADER]: sessionId
1109
1121
  });
1110
- console.log(`[Setup] Setting proxy mode: ${mode}, session: ${sessionId}`);
1111
1122
  await setProxyMode(mode, sessionId, timeout);
1112
- console.log(`[Setup] Proxy mode set successfully`);
1113
1123
  if (clientSideOptions?.url) {
1114
- console.log(`[Setup] Setting up client-side recording with pattern: ${clientSideOptions.url}`);
1115
1124
  await setupClientSideRecording(
1116
1125
  page,
1117
1126
  sessionId,
1118
1127
  mode,
1119
1128
  clientSideOptions.url
1120
1129
  );
1121
- console.log(`[Setup] Client-side recording setup complete`);
1122
1130
  }
1123
1131
  const proxyPort = process.env.TEST_PROXY_RECORDER_PORT || "8100";
1124
1132
  const proxyUrl = `localhost:${proxyPort}`;
1125
- console.log(`[Setup] Registering proxy route handler for: ${proxyUrl}`);
1126
1133
  await page.route(
1127
1134
  (url) => {
1128
1135
  const urlStr = url.toString();
1129
1136
  const matches = urlStr.includes(proxyUrl);
1130
- if (matches) {
1131
- console.log(`[Route Matcher] Matched proxy request: ${urlStr}`);
1132
- }
1133
1137
  return matches;
1134
1138
  },
1135
1139
  async (route) => {
1136
1140
  try {
1137
- const url = route.request().url();
1138
- const method = route.request().method();
1139
1141
  const headers = route.request().headers();
1140
- const hadHeader = !!headers[RECORDING_ID_HEADER];
1141
1142
  headers[RECORDING_ID_HEADER] = sessionId;
1142
- console.log(
1143
- `[Route Intercept] ${method} ${url} (had header: ${hadHeader}, adding session: ${sessionId})`
1144
- );
1145
1143
  await route.continue({ headers });
1146
1144
  } catch (error) {
1147
1145
  console.error(
@@ -1154,7 +1152,6 @@ var playwrightProxy = {
1154
1152
  { times: Infinity }
1155
1153
  // Ensure the handler applies to all matching requests
1156
1154
  );
1157
- console.log(`[Setup] Proxy route handler registered`);
1158
1155
  const context = page.context();
1159
1156
  const contextId = context._guid || "default";
1160
1157
  const handlerKey = `cleanup_${contextId}`;
@@ -1162,9 +1159,6 @@ var playwrightProxy = {
1162
1159
  globalThis[handlerKey] = true;
1163
1160
  context.on("close", async () => {
1164
1161
  try {
1165
- console.log(
1166
- `[Cleanup] Browser context closed, cleaning up session: ${sessionId}`
1167
- );
1168
1162
  await cleanupSession(sessionId);
1169
1163
  } catch (error) {
1170
1164
  console.warn(
package/dist/index.d.cts CHANGED
@@ -4,8 +4,7 @@ export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording
4
4
  import '@playwright/test';
5
5
 
6
6
  declare class ProxyServer {
7
- private targets;
8
- private currentTargetIndex;
7
+ private target;
9
8
  private mode;
10
9
  private recordingId;
11
10
  private replayId;
@@ -17,7 +16,8 @@ declare class ProxyServer {
17
16
  private sequenceCounterByKey;
18
17
  private replaySessions;
19
18
  private recordingPromises;
20
- constructor(targets: string[], recordingsDir: string);
19
+ private flushPromise;
20
+ constructor(target: string, recordingsDir: string);
21
21
  init(): Promise<void>;
22
22
  listen(port: number): http.Server;
23
23
  private setupProxyEventHandlers;
@@ -73,8 +73,8 @@ declare class ProxyServer {
73
73
  private flushPendingRecordings;
74
74
  private saveCurrentSession;
75
75
  private getRecordingIdOrError;
76
- private ensureSessionLoaded;
77
76
  private getServedTracker;
77
+ private getSortedRecordings;
78
78
  private selectReplayRecord;
79
79
  private handleReplayRequest;
80
80
  private handleReplayError;
package/dist/index.d.ts CHANGED
@@ -4,8 +4,7 @@ export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording
4
4
  import '@playwright/test';
5
5
 
6
6
  declare class ProxyServer {
7
- private targets;
8
- private currentTargetIndex;
7
+ private target;
9
8
  private mode;
10
9
  private recordingId;
11
10
  private replayId;
@@ -17,7 +16,8 @@ declare class ProxyServer {
17
16
  private sequenceCounterByKey;
18
17
  private replaySessions;
19
18
  private recordingPromises;
20
- constructor(targets: string[], recordingsDir: string);
19
+ private flushPromise;
20
+ constructor(target: string, recordingsDir: string);
21
21
  init(): Promise<void>;
22
22
  listen(port: number): http.Server;
23
23
  private setupProxyEventHandlers;
@@ -73,8 +73,8 @@ declare class ProxyServer {
73
73
  private flushPendingRecordings;
74
74
  private saveCurrentSession;
75
75
  private getRecordingIdOrError;
76
- private ensureSessionLoaded;
77
76
  private getServedTracker;
77
+ private getSortedRecordings;
78
78
  private selectReplayRecord;
79
79
  private handleReplayRequest;
80
80
  private handleReplayError;
package/dist/index.mjs CHANGED
@@ -119,8 +119,7 @@ function sendJsonResponse(res, statusCode, data) {
119
119
 
120
120
  // src/ProxyServer.ts
121
121
  var ProxyServer = class {
122
- targets;
123
- currentTargetIndex;
122
+ target;
124
123
  mode;
125
124
  recordingId;
126
125
  replayId;
@@ -136,9 +135,10 @@ var ProxyServer = class {
136
135
  // Track multiple concurrent replay sessions by recording ID
137
136
  recordingPromises;
138
137
  // Stack of promises that resolve to completed recordings
139
- constructor(targets, recordingsDir) {
140
- this.targets = targets;
141
- this.currentTargetIndex = 0;
138
+ flushPromise;
139
+ // Promise for in-progress flush operation
140
+ constructor(target, recordingsDir) {
141
+ this.target = target;
142
142
  this.mode = Modes.transparent;
143
143
  this.recordingId = null;
144
144
  this.recordingIdCounter = 0;
@@ -149,6 +149,7 @@ var ProxyServer = class {
149
149
  this.recordingsDir = recordingsDir;
150
150
  this.replaySessions = /* @__PURE__ */ new Map();
151
151
  this.recordingPromises = [];
152
+ this.flushPromise = null;
152
153
  this.proxy = httpProxy.createProxyServer({
153
154
  secure: false,
154
155
  changeOrigin: true,
@@ -210,9 +211,7 @@ var ProxyServer = class {
210
211
  Object.assign(proxyRes.headers, corsHeaders);
211
212
  }
212
213
  getTarget() {
213
- const target = this.targets[this.currentTargetIndex];
214
- this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
215
- return target;
214
+ return this.target;
216
215
  }
217
216
  /**
218
217
  * Extract recording ID from custom HTTP header
@@ -271,7 +270,8 @@ var ProxyServer = class {
271
270
  recordingId,
272
271
  servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
273
272
  loadedSession: null,
274
- lastAccessTime: Date.now()
273
+ lastAccessTime: Date.now(),
274
+ sortedRecordingsByKey: /* @__PURE__ */ new Map()
275
275
  };
276
276
  this.replaySessions.set(recordingId, session);
277
277
  console.log(
@@ -286,17 +286,14 @@ var ProxyServer = class {
286
286
  */
287
287
  async cleanupSession(sessionId) {
288
288
  if (this.replaySessions.has(sessionId)) {
289
- console.log(`[CLEANUP] Removing replay session: ${sessionId}`);
290
289
  this.replaySessions.delete(sessionId);
291
290
  }
292
291
  if (this.recordingId === sessionId) {
293
- console.log(`[CLEANUP] Saving and clearing active recording session: ${sessionId}`);
294
292
  await this.saveCurrentSession();
295
293
  this.currentSession = null;
296
294
  this.recordingId = null;
297
295
  }
298
296
  if (this.replayId === sessionId) {
299
- console.log(`[CLEANUP] Clearing active replay session: ${sessionId}`);
300
297
  this.replayId = null;
301
298
  }
302
299
  console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
@@ -345,7 +342,9 @@ var ProxyServer = class {
345
342
  return;
346
343
  }
347
344
  if (!mode) {
348
- throw new Error("Mode parameter is required when cleanup is not specified");
345
+ throw new Error(
346
+ "Mode parameter is required when cleanup is not specified"
347
+ );
349
348
  }
350
349
  const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
351
350
  this.clearModeTimeout();
@@ -428,12 +427,16 @@ var ProxyServer = class {
428
427
  this.replayId = id;
429
428
  this.recordingId = null;
430
429
  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);
430
+ const sessionState = this.getOrCreateReplaySession(id);
431
+ sessionState.servedRecordingIdsByKey.clear();
432
+ sessionState.sortedRecordingsByKey.clear();
433
+ const filePath = getRecordingPath(this.recordingsDir, id);
434
+ try {
435
+ sessionState.loadedSession = await loadRecordingSession(filePath);
436
+ console.log(`[REPLAY] Loaded recording session: ${id}`);
437
+ } catch (error) {
438
+ console.error(`[REPLAY ERROR] Failed to load session ${id}:`, error);
439
+ sessionState.loadedSession = null;
437
440
  }
438
441
  console.log(`Switched to replay mode with ID: ${id}`);
439
442
  }
@@ -447,21 +450,32 @@ var ProxyServer = class {
447
450
  }, timeout);
448
451
  }
449
452
  async flushPendingRecordings() {
453
+ if (this.flushPromise) {
454
+ await this.flushPromise;
455
+ return;
456
+ }
450
457
  if (this.recordingPromises.length === 0) {
451
458
  return;
452
459
  }
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);
460
+ this.flushPromise = (async () => {
461
+ try {
462
+ const results = await Promise.allSettled(this.recordingPromises);
463
+ if (this.currentSession) {
464
+ for (const result of results) {
465
+ if (result.status === "fulfilled" && result.value) {
466
+ this.currentSession.recordings.push(result.value);
467
+ }
468
+ }
469
+ console.log(
470
+ `Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
471
+ );
458
472
  }
473
+ this.recordingPromises = [];
474
+ } finally {
475
+ this.flushPromise = null;
459
476
  }
460
- console.log(
461
- `Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
462
- );
463
- }
464
- this.recordingPromises = [];
477
+ })();
478
+ await this.flushPromise;
465
479
  }
466
480
  async saveCurrentSession() {
467
481
  if (!this.currentSession) {
@@ -511,20 +525,25 @@ var ProxyServer = class {
511
525
  );
512
526
  return recordingId;
513
527
  }
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
528
  getServedTracker(sessionState, key) {
523
529
  if (!sessionState.servedRecordingIdsByKey.has(key)) {
524
530
  sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
525
531
  }
526
532
  return sessionState.servedRecordingIdsByKey.get(key);
527
533
  }
534
+ getSortedRecordings(sessionState, key) {
535
+ if (sessionState.sortedRecordingsByKey.has(key)) {
536
+ return sessionState.sortedRecordingsByKey.get(key);
537
+ }
538
+ const session = sessionState.loadedSession;
539
+ const sortedRecords = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
540
+ const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
541
+ const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
542
+ return aSeq - bSeq;
543
+ });
544
+ sessionState.sortedRecordingsByKey.set(key, sortedRecords);
545
+ return sortedRecords;
546
+ }
528
547
  selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
529
548
  for (const rec of recordsWithKey) {
530
549
  if (!servedForThisKey.has(rec.recordingId)) {
@@ -545,18 +564,17 @@ var ProxyServer = class {
545
564
  const key = getReqID(req);
546
565
  const filePath = getRecordingPath(this.recordingsDir, recordingId);
547
566
  try {
548
- const sessionState = await this.ensureSessionLoaded(
549
- recordingId,
550
- filePath
551
- );
552
- const session = sessionState.loadedSession;
567
+ const sessionState = this.getOrCreateReplaySession(recordingId);
568
+ if (!sessionState.loadedSession) {
569
+ const error = new Error(
570
+ `Recording session file not found: ${filePath}`
571
+ );
572
+ error.code = "ENOENT";
573
+ throw error;
574
+ }
553
575
  const servedForThisKey = this.getServedTracker(sessionState, key);
554
576
  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
- });
577
+ const recordsWithKey = this.getSortedRecordings(sessionState, key);
560
578
  if (recordsWithKey.length === 0) {
561
579
  const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
562
580
  console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
@@ -767,7 +785,6 @@ var ProxyServer = class {
767
785
  })();
768
786
  });
769
787
  this.recordingPromises.push(recordingPromise);
770
- await recordingPromise;
771
788
  }
772
789
  handleUpgrade(req, socket, head) {
773
790
  if (this.mode === Modes.replay) {
@@ -926,7 +943,7 @@ var ProxyServer = class {
926
943
  logServerStartup(port) {
927
944
  console.log(`Proxy server running on http://localhost:${port}`);
928
945
  console.log(`Mode: ${this.mode}`);
929
- console.log(`Targets: ${this.targets.join(", ")}`);
946
+ console.log(`Target: ${this.target}`);
930
947
  console.log(
931
948
  `Control endpoint: http://localhost:${port}${CONTROL_ENDPOINT}`
932
949
  );
@@ -961,7 +978,6 @@ async function setProxyMode(mode, sessionId, timeout) {
961
978
  throw new Error(`Failed to set proxy mode: ${text}`);
962
979
  }
963
980
  await response.json();
964
- console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
965
981
  } catch (error) {
966
982
  console.error(`Error setting proxy mode:`, error);
967
983
  throw error;
@@ -985,9 +1001,8 @@ async function cleanupSession(sessionId) {
985
1001
  throw new Error(`Failed to cleanup session: ${text}`);
986
1002
  }
987
1003
  await response.json();
988
- console.log(`Session cleaned up: ${sessionId}`);
989
1004
  } catch (error) {
990
- console.error(`Error cleaning up session:`, error);
1005
+ console.error(`Error cleaning up session: ${sessionId}`, error);
991
1006
  throw error;
992
1007
  }
993
1008
  }
@@ -1060,9 +1075,6 @@ async function setupClientSideRecording(page, sessionId, mode, url) {
1060
1075
  const harFileName = sessionId.replaceAll("/", "__");
1061
1076
  const recordingsDir = await getRecordingsDir();
1062
1077
  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
1078
  try {
1067
1079
  await page.routeFromHAR(harPath, {
1068
1080
  url,
@@ -1095,41 +1107,27 @@ var playwrightProxy = {
1095
1107
  await page.setExtraHTTPHeaders({
1096
1108
  [RECORDING_ID_HEADER]: sessionId
1097
1109
  });
1098
- console.log(`[Setup] Setting proxy mode: ${mode}, session: ${sessionId}`);
1099
1110
  await setProxyMode(mode, sessionId, timeout);
1100
- console.log(`[Setup] Proxy mode set successfully`);
1101
1111
  if (clientSideOptions?.url) {
1102
- console.log(`[Setup] Setting up client-side recording with pattern: ${clientSideOptions.url}`);
1103
1112
  await setupClientSideRecording(
1104
1113
  page,
1105
1114
  sessionId,
1106
1115
  mode,
1107
1116
  clientSideOptions.url
1108
1117
  );
1109
- console.log(`[Setup] Client-side recording setup complete`);
1110
1118
  }
1111
1119
  const proxyPort = process.env.TEST_PROXY_RECORDER_PORT || "8100";
1112
1120
  const proxyUrl = `localhost:${proxyPort}`;
1113
- console.log(`[Setup] Registering proxy route handler for: ${proxyUrl}`);
1114
1121
  await page.route(
1115
1122
  (url) => {
1116
1123
  const urlStr = url.toString();
1117
1124
  const matches = urlStr.includes(proxyUrl);
1118
- if (matches) {
1119
- console.log(`[Route Matcher] Matched proxy request: ${urlStr}`);
1120
- }
1121
1125
  return matches;
1122
1126
  },
1123
1127
  async (route) => {
1124
1128
  try {
1125
- const url = route.request().url();
1126
- const method = route.request().method();
1127
1129
  const headers = route.request().headers();
1128
- const hadHeader = !!headers[RECORDING_ID_HEADER];
1129
1130
  headers[RECORDING_ID_HEADER] = sessionId;
1130
- console.log(
1131
- `[Route Intercept] ${method} ${url} (had header: ${hadHeader}, adding session: ${sessionId})`
1132
- );
1133
1131
  await route.continue({ headers });
1134
1132
  } catch (error) {
1135
1133
  console.error(
@@ -1142,7 +1140,6 @@ var playwrightProxy = {
1142
1140
  { times: Infinity }
1143
1141
  // Ensure the handler applies to all matching requests
1144
1142
  );
1145
- console.log(`[Setup] Proxy route handler registered`);
1146
1143
  const context = page.context();
1147
1144
  const contextId = context._guid || "default";
1148
1145
  const handlerKey = `cleanup_${contextId}`;
@@ -1150,9 +1147,6 @@ var playwrightProxy = {
1150
1147
  globalThis[handlerKey] = true;
1151
1148
  context.on("close", async () => {
1152
1149
  try {
1153
- console.log(
1154
- `[Cleanup] Browser context closed, cleaning up session: ${sessionId}`
1155
- );
1156
1150
  await cleanupSession(sessionId);
1157
1151
  } catch (error) {
1158
1152
  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
@@ -16,8 +16,8 @@ function parseCliArgs() {
16
16
  program.name("dev-proxy").description(
17
17
  "Development proxy server with recording and replay capabilities"
18
18
  ).argument(
19
- "<targets...>",
20
- "Target API service URLs (e.g., http://localhost:3000)"
19
+ "<target>",
20
+ "Target API service URL (e.g., http://localhost:3000)"
21
21
  ).option(
22
22
  "-p, --port <number>",
23
23
  "Port number for the proxy server",
@@ -29,18 +29,18 @@ function parseCliArgs() {
29
29
  ).action(() => {
30
30
  });
31
31
  program.parse();
32
- const targets2 = program.args;
32
+ const target2 = program.args[0];
33
33
  const options = program.opts();
34
34
  const port2 = Number.parseInt(options.port, 10);
35
35
  if (Number.isNaN(port2) || port2 < 1025 || port2 > 65535) {
36
36
  console.error("Error: Invalid port number. Must be between 1 and 65535");
37
37
  process.exit(1);
38
38
  }
39
- if (targets2.length === 0) {
39
+ if (!target2) {
40
40
  program.help();
41
41
  }
42
42
  const recordingsDir2 = path.resolve(process.cwd(), options.dir);
43
- return { targets: targets2, port: port2, recordingsDir: recordingsDir2 };
43
+ return { target: target2, port: port2, recordingsDir: recordingsDir2 };
44
44
  }
45
45
 
46
46
  // src/constants.ts
@@ -155,8 +155,7 @@ function sendJsonResponse(res, statusCode, data) {
155
155
 
156
156
  // src/ProxyServer.ts
157
157
  var ProxyServer = class {
158
- targets;
159
- currentTargetIndex;
158
+ target;
160
159
  mode;
161
160
  recordingId;
162
161
  replayId;
@@ -172,9 +171,10 @@ var ProxyServer = class {
172
171
  // Track multiple concurrent replay sessions by recording ID
173
172
  recordingPromises;
174
173
  // Stack of promises that resolve to completed recordings
175
- constructor(targets2, recordingsDir2) {
176
- this.targets = targets2;
177
- this.currentTargetIndex = 0;
174
+ flushPromise;
175
+ // Promise for in-progress flush operation
176
+ constructor(target2, recordingsDir2) {
177
+ this.target = target2;
178
178
  this.mode = Modes.transparent;
179
179
  this.recordingId = null;
180
180
  this.recordingIdCounter = 0;
@@ -185,6 +185,7 @@ var ProxyServer = class {
185
185
  this.recordingsDir = recordingsDir2;
186
186
  this.replaySessions = /* @__PURE__ */ new Map();
187
187
  this.recordingPromises = [];
188
+ this.flushPromise = null;
188
189
  this.proxy = httpProxy.createProxyServer({
189
190
  secure: false,
190
191
  changeOrigin: true,
@@ -246,9 +247,7 @@ var ProxyServer = class {
246
247
  Object.assign(proxyRes.headers, corsHeaders);
247
248
  }
248
249
  getTarget() {
249
- const target = this.targets[this.currentTargetIndex];
250
- this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
251
- return target;
250
+ return this.target;
252
251
  }
253
252
  /**
254
253
  * Extract recording ID from custom HTTP header
@@ -307,7 +306,8 @@ var ProxyServer = class {
307
306
  recordingId,
308
307
  servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
309
308
  loadedSession: null,
310
- lastAccessTime: Date.now()
309
+ lastAccessTime: Date.now(),
310
+ sortedRecordingsByKey: /* @__PURE__ */ new Map()
311
311
  };
312
312
  this.replaySessions.set(recordingId, session);
313
313
  console.log(
@@ -322,17 +322,14 @@ var ProxyServer = class {
322
322
  */
323
323
  async cleanupSession(sessionId) {
324
324
  if (this.replaySessions.has(sessionId)) {
325
- console.log(`[CLEANUP] Removing replay session: ${sessionId}`);
326
325
  this.replaySessions.delete(sessionId);
327
326
  }
328
327
  if (this.recordingId === sessionId) {
329
- console.log(`[CLEANUP] Saving and clearing active recording session: ${sessionId}`);
330
328
  await this.saveCurrentSession();
331
329
  this.currentSession = null;
332
330
  this.recordingId = null;
333
331
  }
334
332
  if (this.replayId === sessionId) {
335
- console.log(`[CLEANUP] Clearing active replay session: ${sessionId}`);
336
333
  this.replayId = null;
337
334
  }
338
335
  console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
@@ -381,7 +378,9 @@ var ProxyServer = class {
381
378
  return;
382
379
  }
383
380
  if (!mode) {
384
- throw new Error("Mode parameter is required when cleanup is not specified");
381
+ throw new Error(
382
+ "Mode parameter is required when cleanup is not specified"
383
+ );
385
384
  }
386
385
  const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
387
386
  this.clearModeTimeout();
@@ -464,12 +463,16 @@ var ProxyServer = class {
464
463
  this.replayId = id;
465
464
  this.recordingId = null;
466
465
  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);
466
+ const sessionState = this.getOrCreateReplaySession(id);
467
+ sessionState.servedRecordingIdsByKey.clear();
468
+ sessionState.sortedRecordingsByKey.clear();
469
+ const filePath = getRecordingPath(this.recordingsDir, id);
470
+ try {
471
+ sessionState.loadedSession = await loadRecordingSession(filePath);
472
+ console.log(`[REPLAY] Loaded recording session: ${id}`);
473
+ } catch (error) {
474
+ console.error(`[REPLAY ERROR] Failed to load session ${id}:`, error);
475
+ sessionState.loadedSession = null;
473
476
  }
474
477
  console.log(`Switched to replay mode with ID: ${id}`);
475
478
  }
@@ -483,21 +486,32 @@ var ProxyServer = class {
483
486
  }, timeout);
484
487
  }
485
488
  async flushPendingRecordings() {
489
+ if (this.flushPromise) {
490
+ await this.flushPromise;
491
+ return;
492
+ }
486
493
  if (this.recordingPromises.length === 0) {
487
494
  return;
488
495
  }
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);
496
+ this.flushPromise = (async () => {
497
+ try {
498
+ const results = await Promise.allSettled(this.recordingPromises);
499
+ if (this.currentSession) {
500
+ for (const result of results) {
501
+ if (result.status === "fulfilled" && result.value) {
502
+ this.currentSession.recordings.push(result.value);
503
+ }
504
+ }
505
+ console.log(
506
+ `Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
507
+ );
494
508
  }
509
+ this.recordingPromises = [];
510
+ } finally {
511
+ this.flushPromise = null;
495
512
  }
496
- console.log(
497
- `Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
498
- );
499
- }
500
- this.recordingPromises = [];
513
+ })();
514
+ await this.flushPromise;
501
515
  }
502
516
  async saveCurrentSession() {
503
517
  if (!this.currentSession) {
@@ -547,20 +561,25 @@ var ProxyServer = class {
547
561
  );
548
562
  return recordingId;
549
563
  }
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
564
  getServedTracker(sessionState, key) {
559
565
  if (!sessionState.servedRecordingIdsByKey.has(key)) {
560
566
  sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
561
567
  }
562
568
  return sessionState.servedRecordingIdsByKey.get(key);
563
569
  }
570
+ getSortedRecordings(sessionState, key) {
571
+ if (sessionState.sortedRecordingsByKey.has(key)) {
572
+ return sessionState.sortedRecordingsByKey.get(key);
573
+ }
574
+ const session = sessionState.loadedSession;
575
+ const sortedRecords = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
576
+ const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
577
+ const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
578
+ return aSeq - bSeq;
579
+ });
580
+ sessionState.sortedRecordingsByKey.set(key, sortedRecords);
581
+ return sortedRecords;
582
+ }
564
583
  selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
565
584
  for (const rec of recordsWithKey) {
566
585
  if (!servedForThisKey.has(rec.recordingId)) {
@@ -581,18 +600,17 @@ var ProxyServer = class {
581
600
  const key = getReqID(req);
582
601
  const filePath = getRecordingPath(this.recordingsDir, recordingId);
583
602
  try {
584
- const sessionState = await this.ensureSessionLoaded(
585
- recordingId,
586
- filePath
587
- );
588
- const session = sessionState.loadedSession;
603
+ const sessionState = this.getOrCreateReplaySession(recordingId);
604
+ if (!sessionState.loadedSession) {
605
+ const error = new Error(
606
+ `Recording session file not found: ${filePath}`
607
+ );
608
+ error.code = "ENOENT";
609
+ throw error;
610
+ }
589
611
  const servedForThisKey = this.getServedTracker(sessionState, key);
590
612
  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
- });
613
+ const recordsWithKey = this.getSortedRecordings(sessionState, key);
596
614
  if (recordsWithKey.length === 0) {
597
615
  const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
598
616
  console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
@@ -683,16 +701,16 @@ var ProxyServer = class {
683
701
  res.end();
684
702
  }
685
703
  async handleProxyRequest(req, res) {
686
- const target = this.getTarget();
687
- console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
704
+ const target2 = this.getTarget();
705
+ console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target2}`);
688
706
  if (this.mode === Modes.record) {
689
- await this.recordAndProxyRequest(req, res, target);
707
+ await this.recordAndProxyRequest(req, res, target2);
690
708
  } else {
691
- this.proxy.web(req, res, { target });
709
+ this.proxy.web(req, res, { target: target2 });
692
710
  }
693
711
  }
694
712
  // Note: streaming requests are buffered before proxying; streaming passthrough is not yet implemented
695
- async recordAndProxyRequest(req, res, target) {
713
+ async recordAndProxyRequest(req, res, target2) {
696
714
  if (!this.currentSession) {
697
715
  return;
698
716
  }
@@ -720,7 +738,7 @@ var ProxyServer = class {
720
738
  console.error("Error buffering request:", error);
721
739
  }
722
740
  const requestBody = Buffer.concat(chunks).toString("utf8");
723
- const targetUrl = new URL(target);
741
+ const targetUrl = new URL(target2);
724
742
  const isHttps = targetUrl.protocol === "https:";
725
743
  const requestModule = isHttps ? https : http;
726
744
  const defaultPort = isHttps ? 443 : 80;
@@ -803,22 +821,21 @@ var ProxyServer = class {
803
821
  })();
804
822
  });
805
823
  this.recordingPromises.push(recordingPromise);
806
- await recordingPromise;
807
824
  }
808
825
  handleUpgrade(req, socket, head) {
809
826
  if (this.mode === Modes.replay) {
810
827
  this.handleReplayWebSocket(req, socket);
811
828
  return;
812
829
  }
813
- const target = this.getTarget();
814
- console.log(`[${this.mode}] WebSocket upgrade ${req.url} -> ${target}`);
830
+ const target2 = this.getTarget();
831
+ console.log(`[${this.mode}] WebSocket upgrade ${req.url} -> ${target2}`);
815
832
  if (this.mode === Modes.record) {
816
- this.handleRecordWebSocket(req, socket, head, target);
833
+ this.handleRecordWebSocket(req, socket, head, target2);
817
834
  } else {
818
- this.proxy.ws(req, socket, head, { target });
835
+ this.proxy.ws(req, socket, head, { target: target2 });
819
836
  }
820
837
  }
821
- handleRecordWebSocket(req, clientSocket, head, target) {
838
+ handleRecordWebSocket(req, clientSocket, head, target2) {
822
839
  const url = req.url || "/";
823
840
  const key = `WS_${url.replaceAll("/", "_")}`;
824
841
  const wsRecording = {
@@ -830,7 +847,7 @@ var ProxyServer = class {
830
847
  if (this.currentSession) {
831
848
  this.currentSession.websocketRecordings.push(wsRecording);
832
849
  }
833
- const backendWsUrl = `${target.replace("http", "ws")}${url}`;
850
+ const backendWsUrl = `${target2.replace("http", "ws")}${url}`;
834
851
  const backendWs = new WebSocket(backendWsUrl);
835
852
  const wss = new WebSocketServer({ noServer: true });
836
853
  backendWs.on("open", () => {
@@ -962,7 +979,7 @@ var ProxyServer = class {
962
979
  logServerStartup(port2) {
963
980
  console.log(`Proxy server running on http://localhost:${port2}`);
964
981
  console.log(`Mode: ${this.mode}`);
965
- console.log(`Targets: ${this.targets.join(", ")}`);
982
+ console.log(`Target: ${this.target}`);
966
983
  console.log(
967
984
  `Control endpoint: http://localhost:${port2}${CONTROL_ENDPOINT}`
968
985
  );
@@ -970,8 +987,8 @@ var ProxyServer = class {
970
987
  };
971
988
 
972
989
  // src/proxy.ts
973
- var { targets, port, recordingsDir } = parseCliArgs();
974
- var proxy = new ProxyServer(targets, recordingsDir);
990
+ var { target, port, recordingsDir } = parseCliArgs();
991
+ var proxy = new ProxyServer(target, recordingsDir);
975
992
  await proxy.init();
976
993
  proxy.listen(port);
977
994
  console.log(`Recordings will be saved to: ${recordingsDir}`);
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.4",
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",