test-proxy-recorder 0.1.8 → 0.1.10

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
@@ -102,7 +102,7 @@ Create `e2e/global-teardown.ts`:
102
102
  import { setProxyMode } from 'test-proxy-recorder';
103
103
 
104
104
  async function globalTeardown() {
105
- await setProxyMode('transparent');
105
+ await setProxyMode('transparent').catch(err => { console.error(err) });
106
106
  }
107
107
 
108
108
  export default globalTeardown;
@@ -247,7 +247,7 @@ Create `e2e/global-teardown.ts`:
247
247
  import { setProxyMode } from 'test-proxy-recorder';
248
248
 
249
249
  async function globalTeardown() {
250
- await setProxyMode('transparent');
250
+ await setProxyMode('transparent').catch(err => { console.error(err) });
251
251
  }
252
252
 
253
253
  export default globalTeardown;
@@ -28,7 +28,7 @@ interface Recording {
28
28
  response?: RecordedResponse;
29
29
  timestamp: string;
30
30
  key: string;
31
- sequence: number;
31
+ recordingId: number;
32
32
  }
33
33
  interface WebSocketMessage {
34
34
  direction: 'client-to-server' | 'server-to-client';
@@ -28,7 +28,7 @@ interface Recording {
28
28
  response?: RecordedResponse;
29
29
  timestamp: string;
30
30
  key: string;
31
- sequence: number;
31
+ recordingId: number;
32
32
  }
33
33
  interface WebSocketMessage {
34
34
  direction: 'client-to-server' | 'server-to-client';
package/dist/index.cjs CHANGED
@@ -6,6 +6,7 @@ var https = require('https');
6
6
  var httpProxy = require('http-proxy');
7
7
  var ws = require('ws');
8
8
  var path = require('path');
9
+ var crypto = require('crypto');
9
10
  var filenamify = require('filenamify');
10
11
 
11
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -15,6 +16,7 @@ var http__default = /*#__PURE__*/_interopDefault(http);
15
16
  var https__default = /*#__PURE__*/_interopDefault(https);
16
17
  var httpProxy__default = /*#__PURE__*/_interopDefault(httpProxy);
17
18
  var path__default = /*#__PURE__*/_interopDefault(path);
19
+ var crypto__default = /*#__PURE__*/_interopDefault(crypto);
18
20
  var filenamify__default = /*#__PURE__*/_interopDefault(filenamify);
19
21
 
20
22
  // src/ProxyServer.ts
@@ -53,7 +55,6 @@ async function saveRecordingSession(recordingsDir, session) {
53
55
  `Saved ${session.recordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
54
56
  );
55
57
  }
56
- var QUERY_HASH_LENGTH = 8;
57
58
  function getReqID(req) {
58
59
  const urlParts = req.url.split("?");
59
60
  const pathname = urlParts[0];
@@ -68,7 +69,7 @@ function generateQueryHash(query) {
68
69
  if (!query) {
69
70
  return "";
70
71
  }
71
- const hash = Buffer.from(query).toString("base64").replaceAll(/[^a-zA-Z0-9]/g, "").slice(0, Math.max(0, QUERY_HASH_LENGTH));
72
+ const hash = crypto__default.default.createHash("md5").update(query).digest("hex").slice(0, 16);
72
73
  return `_${hash}`;
73
74
  }
74
75
 
@@ -97,24 +98,25 @@ var ProxyServer = class {
97
98
  proxy;
98
99
  currentSession;
99
100
  recordingsDir;
100
- requestSequenceMap;
101
- // Track sequence per request key
102
- replaySequenceMap;
103
- // Track replay position per request key
101
+ recordingIdCounter;
102
+ // Unique ID for each recording entry
103
+ replaySessions;
104
+ // Track multiple concurrent replay sessions by recording ID
104
105
  constructor(targets, recordingsDir) {
105
106
  this.targets = targets;
106
107
  this.currentTargetIndex = 0;
107
108
  this.mode = Modes.transparent;
108
109
  this.recordingId = null;
110
+ this.recordingIdCounter = 0;
109
111
  this.replayId = null;
110
112
  this.modeTimeout = null;
111
113
  this.currentSession = null;
112
114
  this.recordingsDir = recordingsDir;
113
- this.requestSequenceMap = /* @__PURE__ */ new Map();
114
- this.replaySequenceMap = /* @__PURE__ */ new Map();
115
+ this.replaySessions = /* @__PURE__ */ new Map();
115
116
  this.proxy = httpProxy__default.default.createProxyServer({
116
117
  secure: false,
117
- changeOrigin: true
118
+ changeOrigin: true,
119
+ ws: true
118
120
  });
119
121
  this.setupProxyEventHandlers();
120
122
  }
@@ -182,6 +184,43 @@ var ProxyServer = class {
182
184
  this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
183
185
  return target;
184
186
  }
187
+ /**
188
+ * Extract recording ID from request cookie
189
+ * Used for concurrent replay session routing
190
+ * @param req The incoming HTTP request
191
+ * @returns The recording ID from cookie, or null if not found
192
+ */
193
+ getRecordingIdFromCookie(req) {
194
+ const cookies = req.headers.cookie;
195
+ if (!cookies) {
196
+ return null;
197
+ }
198
+ const match = cookies.match(/proxy-recording-id=([^;]+)/);
199
+ return match ? decodeURIComponent(match[1]) : null;
200
+ }
201
+ /**
202
+ * Get or create a replay session state for a given recording ID
203
+ * @param recordingId The recording ID to get/create session for
204
+ * @returns The replay session state
205
+ */
206
+ getOrCreateReplaySession(recordingId) {
207
+ let session = this.replaySessions.get(recordingId);
208
+ if (session) {
209
+ session.lastAccessTime = Date.now();
210
+ } else {
211
+ session = {
212
+ recordingId,
213
+ servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
214
+ loadedSession: null,
215
+ lastAccessTime: Date.now()
216
+ };
217
+ this.replaySessions.set(recordingId, session);
218
+ console.log(
219
+ `[CONCURRENT REPLAY] Created new session for recording: ${recordingId}`
220
+ );
221
+ }
222
+ return session;
223
+ }
185
224
  parseGetParams(req) {
186
225
  const url = new URL(req.url || "", `http://${req.headers.host}`);
187
226
  const mode = url.searchParams.get("mode");
@@ -198,16 +237,25 @@ var ProxyServer = class {
198
237
  let data;
199
238
  if (req.method === "GET") {
200
239
  data = this.parseGetParams(req);
201
- } else {
240
+ } else if (req.method === "POST") {
202
241
  const body = await readRequestBody(req);
203
- console.log("MODE CHANGE (POST)", body);
242
+ console.log(`MODE CHANGE (${req.method})`, body);
204
243
  data = JSON.parse(body);
244
+ } else {
245
+ return;
205
246
  }
206
247
  const { mode, id, timeout: requestTimeout } = data;
207
248
  const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
208
249
  this.clearModeTimeout();
209
250
  await this.switchMode(mode, id);
210
251
  this.setupModeTimeout(timeout);
252
+ if (mode === Modes.replay && id) {
253
+ res.setHeader(
254
+ "Set-Cookie",
255
+ `proxy-recording-id=${encodeURIComponent(id)}; HttpOnly; Path=/; SameSite=Lax`
256
+ );
257
+ console.log(`[CONCURRENT REPLAY] Set cookie for recording: ${id}`);
258
+ }
211
259
  sendJsonResponse(res, HTTP_STATUS_OK, {
212
260
  success: true,
213
261
  mode: this.mode,
@@ -222,14 +270,12 @@ var ProxyServer = class {
222
270
  }
223
271
  }
224
272
  clearModeTimeout() {
225
- if (this.modeTimeout) {
226
- clearTimeout(this.modeTimeout);
227
- this.modeTimeout = null;
228
- }
273
+ clearTimeout(this.modeTimeout || 0);
274
+ this.modeTimeout = null;
229
275
  }
230
276
  async switchMode(mode, id) {
231
- if (this.currentSession) {
232
- console.log("Switching mode, saving current session first");
277
+ console.log(`Switching to ${mode.toUpperCase()} mode`);
278
+ if (this.currentSession && this.mode === Modes.record) {
233
279
  await this.saveCurrentSession(true);
234
280
  console.log("Session saved, continuing with mode switch");
235
281
  }
@@ -239,11 +285,17 @@ var ProxyServer = class {
239
285
  break;
240
286
  }
241
287
  case Modes.record: {
288
+ if (!id) {
289
+ throw new Error("Record ID is required");
290
+ }
242
291
  this.switchToRecordMode(id);
243
292
  break;
244
293
  }
245
294
  case Modes.replay: {
246
- this.switchToReplayMode(id);
295
+ if (!id) {
296
+ throw new Error("Replay ID is required");
297
+ }
298
+ await this.switchToReplayMode(id);
247
299
  break;
248
300
  }
249
301
  default: {
@@ -260,36 +312,33 @@ var ProxyServer = class {
260
312
  console.log("Switched to transparent mode");
261
313
  }
262
314
  switchToRecordMode(id) {
263
- if (!id) {
264
- throw new Error("Record ID is required");
265
- }
266
315
  this.mode = Modes.record;
267
316
  this.recordingId = id;
268
317
  this.replayId = null;
269
318
  this.currentSession = { id, recordings: [], websocketRecordings: [] };
270
- this.requestSequenceMap.clear();
271
319
  console.log(`Switched to record mode with ID: ${id}`);
272
320
  }
273
- switchToReplayMode(id) {
274
- if (!id) {
275
- throw new Error("Replay ID is required");
276
- }
321
+ async switchToReplayMode(id) {
277
322
  this.mode = Modes.replay;
278
323
  this.replayId = id;
279
324
  this.recordingId = null;
280
325
  this.currentSession = null;
281
- this.replaySequenceMap.clear();
326
+ const session = this.replaySessions.get(id);
327
+ if (session) {
328
+ session.servedRecordingIdsByKey.clear();
329
+ console.log(`Reset served recordings tracker for session: ${id}`);
330
+ } else {
331
+ this.getOrCreateReplaySession(id);
332
+ }
282
333
  console.log(`Switched to replay mode with ID: ${id}`);
283
334
  }
284
335
  setupModeTimeout(timeout) {
285
- if (timeout && timeout > 0) {
286
- this.modeTimeout = setTimeout(async () => {
287
- console.log("Timeout reached, switching back to transparent mode");
288
- await this.saveCurrentSession(true);
289
- this.switchToTransparentMode();
290
- this.modeTimeout = null;
291
- }, timeout);
292
- }
336
+ this.modeTimeout = setTimeout(async () => {
337
+ console.log("Timeout reached, switching back to transparent mode");
338
+ await this.saveCurrentSession(true);
339
+ this.switchToTransparentMode();
340
+ this.modeTimeout = null;
341
+ }, timeout);
293
342
  }
294
343
  async saveCurrentSession(filterIncomplete = false) {
295
344
  if (!this.currentSession) {
@@ -315,6 +364,8 @@ var ProxyServer = class {
315
364
  return;
316
365
  }
317
366
  const key = getReqID(req);
367
+ const recordingId = this.recordingIdCounter++;
368
+ req.__recordingId = recordingId;
318
369
  const record = {
319
370
  request: {
320
371
  method: req.method,
@@ -324,44 +375,57 @@ var ProxyServer = class {
324
375
  },
325
376
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
326
377
  key,
327
- sequence: -1
328
- // Temporary, will be set when response arrives
378
+ recordingId
329
379
  };
330
380
  this.currentSession.recordings.push(record);
331
381
  console.log(
332
382
  // eslint-disable-next-line sonarjs/no-nested-template-literals
333
- `saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
383
+ `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})`
334
384
  );
335
385
  }
336
386
  updateRequestBodySync(req, body) {
337
387
  if (!this.currentSession) {
338
388
  return;
339
389
  }
340
- const key = getReqID(req);
341
- const record = this.currentSession.recordings.findLast(
342
- (r) => r.key === key && !r.response
390
+ const recordingId = req.__recordingId;
391
+ if (recordingId === void 0) {
392
+ console.error(
393
+ `updateRequestBodySync: No recording ID found on request ${req.method} ${req.url}`
394
+ );
395
+ return;
396
+ }
397
+ const record = this.currentSession.recordings.find(
398
+ (r) => r.recordingId === recordingId
343
399
  );
344
400
  if (!record) {
345
401
  console.error(
346
- `updateRequestBodySync: Could not find request record for ${req.method} ${req.url}`
402
+ `updateRequestBodySync: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
347
403
  );
348
404
  return;
349
405
  }
350
406
  record.request.body = body || null;
351
407
  console.log(
352
- `updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars)`
408
+ `updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars, recordingId: ${recordingId})`
353
409
  );
354
410
  }
355
411
  async recordResponse(req, proxyRes) {
356
412
  if (!this.currentSession) {
357
413
  return;
358
414
  }
359
- const key = getReqID(req);
360
- const record = this.currentSession.recordings.findLast(
361
- (r) => r.key === key && !r.response
415
+ const recordingId = req.__recordingId;
416
+ if (recordingId === void 0) {
417
+ console.error(
418
+ `recordResponse: No recording ID found on request ${req.method} ${req.url}`
419
+ );
420
+ return;
421
+ }
422
+ const record = this.currentSession.recordings.find(
423
+ (r) => r.recordingId === recordingId
362
424
  );
363
425
  if (!record) {
364
- console.error("Request record not found for response:", key);
426
+ console.error(
427
+ `recordResponse: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
428
+ );
365
429
  return;
366
430
  }
367
431
  const chunks = [];
@@ -375,34 +439,28 @@ var ProxyServer = class {
375
439
  headers: proxyRes.headers,
376
440
  body: body || null
377
441
  };
378
- console.log(`Recorded: ${req.method} ${req.url}`);
442
+ console.log(
443
+ `Recorded: ${req.method} ${req.url} (recordingId: ${recordingId})`
444
+ );
379
445
  });
380
446
  }
381
447
  async recordResponseData(req, proxyRes, body) {
382
448
  if (!this.currentSession) {
383
449
  return false;
384
450
  }
385
- const key = getReqID(req);
386
- const record = this.currentSession.recordings.findLast(
387
- (r) => r.key === key && !r.response
388
- );
389
- if (!record) {
390
- const host = req.headers.host || "unknown";
391
- const recordsWithKey = this.currentSession.recordings.filter(
392
- (r) => r.key === key
393
- );
451
+ const recordingId = req.__recordingId;
452
+ if (recordingId === void 0) {
394
453
  console.error(
395
- `Request record not found for response: ${key} at ${req.method} ${host}${req.url}`
396
- );
397
- console.error(
398
- ` Total recordings: ${this.currentSession.recordings.length}, with this key: ${recordsWithKey.length}`
454
+ `recordResponseData: No recording ID found on request ${req.method} ${req.url}`
399
455
  );
456
+ return false;
457
+ }
458
+ const record = this.currentSession.recordings.find(
459
+ (r) => r.recordingId === recordingId
460
+ );
461
+ if (!record) {
400
462
  console.error(
401
- ` Records with key:`,
402
- recordsWithKey.map((r) => ({
403
- seq: r.sequence,
404
- hasResponse: !!r.response
405
- }))
463
+ `recordResponseData: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
406
464
  );
407
465
  return false;
408
466
  }
@@ -411,34 +469,78 @@ var ProxyServer = class {
411
469
  headers: proxyRes.headers,
412
470
  body: body || null
413
471
  };
414
- record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
415
- const currentSequence = this.requestSequenceMap.get(key) || 0;
416
- record.sequence = currentSequence;
417
- this.requestSequenceMap.set(key, currentSequence + 1);
418
472
  console.log(
419
- `recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence})`
473
+ `recordResponseData: Recorded response for ${req.method} ${req.url} (recordingId: ${recordingId})`
420
474
  );
421
475
  return true;
422
476
  }
423
477
  async handleReplayRequest(req, res) {
478
+ const recordingId = this.getRecordingIdFromCookie(req) || this.replayId;
479
+ if (!recordingId) {
480
+ const corsHeaders = this.getCorsHeaders(req);
481
+ res.writeHead(HTTP_STATUS_BAD_REQUEST, {
482
+ "Content-Type": "application/json",
483
+ ...corsHeaders
484
+ });
485
+ res.end(JSON.stringify({ error: "No replay session active" }));
486
+ return;
487
+ }
424
488
  const key = getReqID(req);
425
- const filePath = getRecordingPath(this.recordingsDir, this.replayId);
489
+ const filePath = getRecordingPath(this.recordingsDir, recordingId);
426
490
  try {
427
- const session = await loadRecordingSession(filePath);
491
+ const sessionState = this.getOrCreateReplaySession(recordingId);
492
+ if (!sessionState.loadedSession) {
493
+ sessionState.loadedSession = await loadRecordingSession(filePath);
494
+ console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
495
+ }
496
+ const session = sessionState.loadedSession;
497
+ if (!sessionState.servedRecordingIdsByKey.has(key)) {
498
+ sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
499
+ }
500
+ const servedForThisKey = sessionState.servedRecordingIdsByKey.get(key);
428
501
  const host = req.headers.host || "unknown";
429
- const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
502
+ const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.recordingId - b.recordingId);
430
503
  if (recordsWithKey.length === 0) {
431
- throw new Error(
432
- `No recording found for ${key} at ${req.method} ${host}${req.url}`
504
+ const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
505
+ console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
506
+ console.error(
507
+ `[REPLAY ERROR] This request was not made during recording - possible test non-determinism`
508
+ );
509
+ const errorResponse = {
510
+ error: "No recording found",
511
+ message: errorMsg,
512
+ key,
513
+ sessionId: recordingId
514
+ };
515
+ const corsHeaders = this.getCorsHeaders(req);
516
+ res.writeHead(HTTP_STATUS_NOT_FOUND, {
517
+ "Content-Type": "application/json",
518
+ ...corsHeaders
519
+ });
520
+ res.end(JSON.stringify(errorResponse));
521
+ return;
522
+ }
523
+ const requestCount = servedForThisKey.size + 1;
524
+ console.log(
525
+ `[REPLAY REQUEST #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
526
+ );
527
+ let record;
528
+ for (const rec of recordsWithKey) {
529
+ if (!servedForThisKey.has(rec.recordingId)) {
530
+ record = rec;
531
+ break;
532
+ }
533
+ }
534
+ if (!record) {
535
+ console.log(
536
+ `[REPLAY WARNING] All ${recordsWithKey.length} recordings already served for ${key} (session: ${recordingId}), reusing last one`
433
537
  );
538
+ record = recordsWithKey[recordsWithKey.length - 1];
434
539
  }
435
- const usageCount = this.replaySequenceMap.get(key) || 0;
436
- const recordIndex = usageCount % recordsWithKey.length;
437
- const record = recordsWithKey[recordIndex];
540
+ servedForThisKey.add(record.recordingId);
438
541
  console.log(
439
- `Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
542
+ `[REPLAY SERVING] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
440
543
  );
441
- this.replaySequenceMap.set(key, usageCount + 1);
442
544
  if (!record.response) {
443
545
  throw new Error(
444
546
  `No response recorded for this request: ${req.method} ${host}${req.url}`
@@ -504,6 +606,7 @@ var ProxyServer = class {
504
606
  this.proxy.web(req, res, { target });
505
607
  }
506
608
  }
609
+ // TODO: check if can handle streaming requests
507
610
  async bufferAndProxyRequest(req, res, target) {
508
611
  const chunks = [];
509
612
  req.on("data", (chunk) => {
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import http from 'node:http';
2
- export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-CBjvm5rb.cjs';
2
+ export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-CjM3evKb.cjs';
3
3
  import '@playwright/test';
4
4
 
5
5
  declare class ProxyServer {
@@ -12,8 +12,8 @@ declare class ProxyServer {
12
12
  private proxy;
13
13
  private currentSession;
14
14
  private recordingsDir;
15
- private requestSequenceMap;
16
- private replaySequenceMap;
15
+ private recordingIdCounter;
16
+ private replaySessions;
17
17
  constructor(targets: string[], recordingsDir: string);
18
18
  init(): Promise<void>;
19
19
  listen(port: number): http.Server;
@@ -28,6 +28,19 @@ declare class ProxyServer {
28
28
  private getCorsHeaders;
29
29
  private addCorsHeaders;
30
30
  private getTarget;
31
+ /**
32
+ * Extract recording ID from request cookie
33
+ * Used for concurrent replay session routing
34
+ * @param req The incoming HTTP request
35
+ * @returns The recording ID from cookie, or null if not found
36
+ */
37
+ private getRecordingIdFromCookie;
38
+ /**
39
+ * Get or create a replay session state for a given recording ID
40
+ * @param recordingId The recording ID to get/create session for
41
+ * @returns The replay session state
42
+ */
43
+ private getOrCreateReplaySession;
31
44
  private parseGetParams;
32
45
  private handleControlRequest;
33
46
  private clearModeTimeout;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import http from 'node:http';
2
- export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-CBjvm5rb.js';
2
+ export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-CjM3evKb.js';
3
3
  import '@playwright/test';
4
4
 
5
5
  declare class ProxyServer {
@@ -12,8 +12,8 @@ declare class ProxyServer {
12
12
  private proxy;
13
13
  private currentSession;
14
14
  private recordingsDir;
15
- private requestSequenceMap;
16
- private replaySequenceMap;
15
+ private recordingIdCounter;
16
+ private replaySessions;
17
17
  constructor(targets: string[], recordingsDir: string);
18
18
  init(): Promise<void>;
19
19
  listen(port: number): http.Server;
@@ -28,6 +28,19 @@ declare class ProxyServer {
28
28
  private getCorsHeaders;
29
29
  private addCorsHeaders;
30
30
  private getTarget;
31
+ /**
32
+ * Extract recording ID from request cookie
33
+ * Used for concurrent replay session routing
34
+ * @param req The incoming HTTP request
35
+ * @returns The recording ID from cookie, or null if not found
36
+ */
37
+ private getRecordingIdFromCookie;
38
+ /**
39
+ * Get or create a replay session state for a given recording ID
40
+ * @param recordingId The recording ID to get/create session for
41
+ * @returns The replay session state
42
+ */
43
+ private getOrCreateReplaySession;
31
44
  private parseGetParams;
32
45
  private handleControlRequest;
33
46
  private clearModeTimeout;