test-proxy-recorder 0.1.9 → 0.1.11

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
+ sequence?: number;
32
32
  recordingId: number;
33
33
  }
34
34
  interface WebSocketMessage {
@@ -28,7 +28,7 @@ interface Recording {
28
28
  response?: RecordedResponse;
29
29
  timestamp: string;
30
30
  key: string;
31
- sequence: number;
31
+ sequence?: number;
32
32
  recordingId: number;
33
33
  }
34
34
  interface WebSocketMessage {
package/dist/index.cjs CHANGED
@@ -5,9 +5,9 @@ var http = require('http');
5
5
  var https = require('https');
6
6
  var httpProxy = require('http-proxy');
7
7
  var ws = require('ws');
8
- var path = require('path');
9
8
  var crypto = require('crypto');
10
- var filenamify = require('filenamify');
9
+ var path = require('path');
10
+ var filenamify2 = require('filenamify');
11
11
 
12
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
13
 
@@ -15,9 +15,9 @@ var fs__default = /*#__PURE__*/_interopDefault(fs);
15
15
  var http__default = /*#__PURE__*/_interopDefault(http);
16
16
  var https__default = /*#__PURE__*/_interopDefault(https);
17
17
  var httpProxy__default = /*#__PURE__*/_interopDefault(httpProxy);
18
- var path__default = /*#__PURE__*/_interopDefault(path);
19
18
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
20
- var filenamify__default = /*#__PURE__*/_interopDefault(filenamify);
19
+ var path__default = /*#__PURE__*/_interopDefault(path);
20
+ var filenamify2__default = /*#__PURE__*/_interopDefault(filenamify2);
21
21
 
22
22
  // src/ProxyServer.ts
23
23
 
@@ -36,23 +36,53 @@ var Modes = {
36
36
  replay: "replay"
37
37
  };
38
38
  var JSON_INDENT_SPACES = 2;
39
+ var EXTENSION = ".mock.json";
40
+ var MAX_FILENAME_LENGTH = 255 - EXTENSION.length;
41
+ var HASH_LENGTH = 8;
42
+ function generateHash(str) {
43
+ return crypto__default.default.createHash("shake256", { outputLength: HASH_LENGTH / 2 }).update(str).digest("hex");
44
+ }
39
45
  function getRecordingPath(recordingsDir, id) {
40
- return path__default.default.join(recordingsDir, `${id}.mock.json`);
46
+ let processedId = id.replaceAll("/", "__");
47
+ if (processedId.length > MAX_FILENAME_LENGTH) {
48
+ const hash = generateHash(id);
49
+ const maxBaseLength = MAX_FILENAME_LENGTH - HASH_LENGTH - 1;
50
+ processedId = `${processedId.slice(0, maxBaseLength)}_${hash}`;
51
+ }
52
+ const sanitizedId = filenamify2__default.default(processedId, {
53
+ replacement: "_",
54
+ maxLength: 255
55
+ // Set explicit max to prevent filenamify's default truncation
56
+ });
57
+ return path__default.default.join(recordingsDir, `${sanitizedId}${EXTENSION}`);
41
58
  }
42
59
  async function loadRecordingSession(filePath) {
43
60
  const fileContent = await fs__default.default.readFile(filePath, "utf8");
44
61
  return JSON.parse(fileContent);
45
62
  }
63
+ function processRecordings(recordings) {
64
+ const keySequenceMap = /* @__PURE__ */ new Map();
65
+ return recordings.map((recording) => {
66
+ const key = recording.key;
67
+ const currentSeq = keySequenceMap.get(key) || 0;
68
+ keySequenceMap.set(key, currentSeq + 1);
69
+ return { ...recording, sequence: currentSeq };
70
+ });
71
+ }
46
72
  async function saveRecordingSession(recordingsDir, session) {
47
73
  const filePath = getRecordingPath(recordingsDir, session.id);
48
- const dirPath = path__default.default.dirname(filePath);
49
- await fs__default.default.mkdir(dirPath, { recursive: true });
74
+ await fs__default.default.mkdir(recordingsDir, { recursive: true });
75
+ const processedRecordings = processRecordings(session.recordings);
76
+ const processedSession = {
77
+ ...session,
78
+ recordings: processedRecordings
79
+ };
50
80
  await fs__default.default.writeFile(
51
81
  filePath,
52
- JSON.stringify(session, null, JSON_INDENT_SPACES)
82
+ JSON.stringify(processedSession, null, JSON_INDENT_SPACES)
53
83
  );
54
84
  console.log(
55
- `Saved ${session.recordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
85
+ `Saved ${processedRecordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
56
86
  );
57
87
  }
58
88
  function getReqID(req) {
@@ -60,10 +90,10 @@ function getReqID(req) {
60
90
  const pathname = urlParts[0];
61
91
  const query = urlParts[1] || "";
62
92
  const pathPart = pathname === "/" ? "root" : pathname.slice(1);
63
- const normalizedPath = filenamify__default.default(pathPart, { replacement: "_" });
93
+ const normalizedPath = filenamify2__default.default(pathPart, { replacement: "_" });
64
94
  const queryHash = generateQueryHash(query);
65
95
  const filename = `${req.method}_${normalizedPath}${queryHash}.json`;
66
- return filenamify__default.default(filename, { replacement: "_" });
96
+ return filenamify2__default.default(filename, { replacement: "_" });
67
97
  }
68
98
  function generateQueryHash(query) {
69
99
  if (!query) {
@@ -98,12 +128,10 @@ var ProxyServer = class {
98
128
  proxy;
99
129
  currentSession;
100
130
  recordingsDir;
101
- requestSequenceMap;
102
- // Track sequence per request key
103
- replaySequenceMap;
104
- // Track replay position per request key
105
131
  recordingIdCounter;
106
132
  // Unique ID for each recording entry
133
+ replaySessions;
134
+ // Track multiple concurrent replay sessions by recording ID
107
135
  constructor(targets, recordingsDir) {
108
136
  this.targets = targets;
109
137
  this.currentTargetIndex = 0;
@@ -114,11 +142,11 @@ var ProxyServer = class {
114
142
  this.modeTimeout = null;
115
143
  this.currentSession = null;
116
144
  this.recordingsDir = recordingsDir;
117
- this.requestSequenceMap = /* @__PURE__ */ new Map();
118
- this.replaySequenceMap = /* @__PURE__ */ new Map();
145
+ this.replaySessions = /* @__PURE__ */ new Map();
119
146
  this.proxy = httpProxy__default.default.createProxyServer({
120
147
  secure: false,
121
- changeOrigin: true
148
+ changeOrigin: true,
149
+ ws: true
122
150
  });
123
151
  this.setupProxyEventHandlers();
124
152
  }
@@ -186,6 +214,43 @@ var ProxyServer = class {
186
214
  this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
187
215
  return target;
188
216
  }
217
+ /**
218
+ * Extract recording ID from request cookie
219
+ * Used for concurrent replay session routing
220
+ * @param req The incoming HTTP request
221
+ * @returns The recording ID from cookie, or null if not found
222
+ */
223
+ getRecordingIdFromCookie(req) {
224
+ const cookies = req.headers.cookie;
225
+ if (!cookies) {
226
+ return null;
227
+ }
228
+ const match = cookies.match(/proxy-recording-id=([^;]+)/);
229
+ return match ? decodeURIComponent(match[1]) : null;
230
+ }
231
+ /**
232
+ * Get or create a replay session state for a given recording ID
233
+ * @param recordingId The recording ID to get/create session for
234
+ * @returns The replay session state
235
+ */
236
+ getOrCreateReplaySession(recordingId) {
237
+ let session = this.replaySessions.get(recordingId);
238
+ if (session) {
239
+ session.lastAccessTime = Date.now();
240
+ } else {
241
+ session = {
242
+ recordingId,
243
+ servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
244
+ loadedSession: null,
245
+ lastAccessTime: Date.now()
246
+ };
247
+ this.replaySessions.set(recordingId, session);
248
+ console.log(
249
+ `[CONCURRENT REPLAY] Created new session for recording: ${recordingId}`
250
+ );
251
+ }
252
+ return session;
253
+ }
189
254
  parseGetParams(req) {
190
255
  const url = new URL(req.url || "", `http://${req.headers.host}`);
191
256
  const mode = url.searchParams.get("mode");
@@ -202,16 +267,25 @@ var ProxyServer = class {
202
267
  let data;
203
268
  if (req.method === "GET") {
204
269
  data = this.parseGetParams(req);
205
- } else {
270
+ } else if (req.method === "POST") {
206
271
  const body = await readRequestBody(req);
207
- console.log("MODE CHANGE (POST)", body);
272
+ console.log(`MODE CHANGE (${req.method})`, body);
208
273
  data = JSON.parse(body);
274
+ } else {
275
+ return;
209
276
  }
210
277
  const { mode, id, timeout: requestTimeout } = data;
211
278
  const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
212
279
  this.clearModeTimeout();
213
280
  await this.switchMode(mode, id);
214
281
  this.setupModeTimeout(timeout);
282
+ if (mode === Modes.replay && id) {
283
+ res.setHeader(
284
+ "Set-Cookie",
285
+ `proxy-recording-id=${encodeURIComponent(id)}; HttpOnly; Path=/; SameSite=Lax`
286
+ );
287
+ console.log(`[CONCURRENT REPLAY] Set cookie for recording: ${id}`);
288
+ }
215
289
  sendJsonResponse(res, HTTP_STATUS_OK, {
216
290
  success: true,
217
291
  mode: this.mode,
@@ -226,14 +300,12 @@ var ProxyServer = class {
226
300
  }
227
301
  }
228
302
  clearModeTimeout() {
229
- if (this.modeTimeout) {
230
- clearTimeout(this.modeTimeout);
231
- this.modeTimeout = null;
232
- }
303
+ clearTimeout(this.modeTimeout || 0);
304
+ this.modeTimeout = null;
233
305
  }
234
306
  async switchMode(mode, id) {
235
- if (this.currentSession) {
236
- console.log("Switching mode, saving current session first");
307
+ console.log(`Switching to ${mode.toUpperCase()} mode`);
308
+ if (this.currentSession && this.mode === Modes.record) {
237
309
  await this.saveCurrentSession(true);
238
310
  console.log("Session saved, continuing with mode switch");
239
311
  }
@@ -243,11 +315,17 @@ var ProxyServer = class {
243
315
  break;
244
316
  }
245
317
  case Modes.record: {
318
+ if (!id) {
319
+ throw new Error("Record ID is required");
320
+ }
246
321
  this.switchToRecordMode(id);
247
322
  break;
248
323
  }
249
324
  case Modes.replay: {
250
- this.switchToReplayMode(id);
325
+ if (!id) {
326
+ throw new Error("Replay ID is required");
327
+ }
328
+ await this.switchToReplayMode(id);
251
329
  break;
252
330
  }
253
331
  default: {
@@ -264,36 +342,33 @@ var ProxyServer = class {
264
342
  console.log("Switched to transparent mode");
265
343
  }
266
344
  switchToRecordMode(id) {
267
- if (!id) {
268
- throw new Error("Record ID is required");
269
- }
270
345
  this.mode = Modes.record;
271
346
  this.recordingId = id;
272
347
  this.replayId = null;
273
348
  this.currentSession = { id, recordings: [], websocketRecordings: [] };
274
- this.requestSequenceMap.clear();
275
349
  console.log(`Switched to record mode with ID: ${id}`);
276
350
  }
277
- switchToReplayMode(id) {
278
- if (!id) {
279
- throw new Error("Replay ID is required");
280
- }
351
+ async switchToReplayMode(id) {
281
352
  this.mode = Modes.replay;
282
353
  this.replayId = id;
283
354
  this.recordingId = null;
284
355
  this.currentSession = null;
285
- this.replaySequenceMap.clear();
356
+ const session = this.replaySessions.get(id);
357
+ if (session) {
358
+ session.servedRecordingIdsByKey.clear();
359
+ console.log(`Reset served recordings tracker for session: ${id}`);
360
+ } else {
361
+ this.getOrCreateReplaySession(id);
362
+ }
286
363
  console.log(`Switched to replay mode with ID: ${id}`);
287
364
  }
288
365
  setupModeTimeout(timeout) {
289
- if (timeout && timeout > 0) {
290
- this.modeTimeout = setTimeout(async () => {
291
- console.log("Timeout reached, switching back to transparent mode");
292
- await this.saveCurrentSession(true);
293
- this.switchToTransparentMode();
294
- this.modeTimeout = null;
295
- }, timeout);
296
- }
366
+ this.modeTimeout = setTimeout(async () => {
367
+ console.log("Timeout reached, switching back to transparent mode");
368
+ await this.saveCurrentSession(true);
369
+ this.switchToTransparentMode();
370
+ this.modeTimeout = null;
371
+ }, timeout);
297
372
  }
298
373
  async saveCurrentSession(filterIncomplete = false) {
299
374
  if (!this.currentSession) {
@@ -319,9 +394,6 @@ var ProxyServer = class {
319
394
  return;
320
395
  }
321
396
  const key = getReqID(req);
322
- const currentSequence = this.requestSequenceMap.get(key) || 0;
323
- const sequence = currentSequence;
324
- this.requestSequenceMap.set(key, currentSequence + 1);
325
397
  const recordingId = this.recordingIdCounter++;
326
398
  req.__recordingId = recordingId;
327
399
  const record = {
@@ -333,13 +405,12 @@ var ProxyServer = class {
333
405
  },
334
406
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
335
407
  key,
336
- sequence,
337
408
  recordingId
338
409
  };
339
410
  this.currentSession.recordings.push(record);
340
411
  console.log(
341
412
  // eslint-disable-next-line sonarjs/no-nested-template-literals
342
- `saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, seq: ${sequence}, recordingId: ${recordingId}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
413
+ `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})`
343
414
  );
344
415
  }
345
416
  updateRequestBodySync(req, body) {
@@ -399,7 +470,7 @@ var ProxyServer = class {
399
470
  body: body || null
400
471
  };
401
472
  console.log(
402
- `Recorded: ${req.method} ${req.url} (seq: ${record.sequence}, recordingId: ${recordingId})`
473
+ `Recorded: ${req.method} ${req.url} (recordingId: ${recordingId})`
403
474
  );
404
475
  });
405
476
  }
@@ -429,46 +500,77 @@ var ProxyServer = class {
429
500
  body: body || null
430
501
  };
431
502
  console.log(
432
- `recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence}, recordingId: ${recordingId})`
503
+ `recordResponseData: Recorded response for ${req.method} ${req.url} (recordingId: ${recordingId})`
433
504
  );
434
505
  return true;
435
506
  }
436
507
  async handleReplayRequest(req, res) {
508
+ const recordingId = this.getRecordingIdFromCookie(req) || this.replayId;
509
+ if (!recordingId) {
510
+ const corsHeaders = this.getCorsHeaders(req);
511
+ res.writeHead(HTTP_STATUS_BAD_REQUEST, {
512
+ "Content-Type": "application/json",
513
+ ...corsHeaders
514
+ });
515
+ res.end(JSON.stringify({ error: "No replay session active" }));
516
+ return;
517
+ }
437
518
  const key = getReqID(req);
438
- const filePath = getRecordingPath(this.recordingsDir, this.replayId);
519
+ const filePath = getRecordingPath(this.recordingsDir, recordingId);
439
520
  try {
440
- const session = await loadRecordingSession(filePath);
521
+ const sessionState = this.getOrCreateReplaySession(recordingId);
522
+ if (!sessionState.loadedSession) {
523
+ sessionState.loadedSession = await loadRecordingSession(filePath);
524
+ console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
525
+ }
526
+ const session = sessionState.loadedSession;
527
+ if (!sessionState.servedRecordingIdsByKey.has(key)) {
528
+ sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
529
+ }
530
+ const servedForThisKey = sessionState.servedRecordingIdsByKey.get(key);
441
531
  const host = req.headers.host || "unknown";
442
- const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
532
+ const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.recordingId - b.recordingId);
443
533
  if (recordsWithKey.length === 0) {
444
- console.warn(
445
- `No recording found for ${key} at ${req.method} ${host}${req.url}, returning default response`
534
+ const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
535
+ console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
536
+ console.error(
537
+ `[REPLAY ERROR] This request was not made during recording - possible test non-determinism`
446
538
  );
447
- const defaultResponse = req.method === "GET" ? {
448
- data: [],
449
- items: [],
450
- results: [],
451
- updated_at: "0001-01-01T00:00:00Z"
452
- } : { success: true };
539
+ const errorResponse = {
540
+ error: "No recording found",
541
+ message: errorMsg,
542
+ key,
543
+ sessionId: recordingId
544
+ };
453
545
  const corsHeaders = this.getCorsHeaders(req);
454
- res.writeHead(HTTP_STATUS_OK, {
546
+ res.writeHead(HTTP_STATUS_NOT_FOUND, {
455
547
  "Content-Type": "application/json",
456
548
  ...corsHeaders
457
549
  });
458
- res.end(JSON.stringify(defaultResponse));
550
+ res.end(JSON.stringify(errorResponse));
459
551
  return;
460
552
  }
461
- const usageCount = this.replaySequenceMap.get(key) || 0;
553
+ const requestCount = servedForThisKey.size + 1;
554
+ console.log(
555
+ `[REPLAY REQUEST #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
556
+ );
462
557
  let record;
463
- if (usageCount < recordsWithKey.length) {
464
- record = recordsWithKey[usageCount];
465
- } else {
558
+ for (const rec of recordsWithKey) {
559
+ if (!servedForThisKey.has(rec.recordingId)) {
560
+ record = rec;
561
+ break;
562
+ }
563
+ }
564
+ if (!record) {
565
+ console.log(
566
+ `[REPLAY WARNING] All ${recordsWithKey.length} recordings already served for ${key} (session: ${recordingId}), reusing last one`
567
+ );
466
568
  record = recordsWithKey[recordsWithKey.length - 1];
467
569
  }
570
+ servedForThisKey.add(record.recordingId);
468
571
  console.log(
469
- `Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
572
+ `[REPLAY SERVING] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
470
573
  );
471
- this.replaySequenceMap.set(key, usageCount + 1);
472
574
  if (!record.response) {
473
575
  throw new Error(
474
576
  `No response recorded for this request: ${req.method} ${host}${req.url}`
@@ -534,6 +636,7 @@ var ProxyServer = class {
534
636
  this.proxy.web(req, res, { target });
535
637
  }
536
638
  }
639
+ // TODO: check if can handle streaming requests
537
640
  async bufferAndProxyRequest(req, res, target) {
538
641
  const chunks = [];
539
642
  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-CG-XcFDa.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-Cx_Kflfl.cjs';
3
3
  import '@playwright/test';
4
4
 
5
5
  declare class ProxyServer {
@@ -12,9 +12,8 @@ declare class ProxyServer {
12
12
  private proxy;
13
13
  private currentSession;
14
14
  private recordingsDir;
15
- private requestSequenceMap;
16
- private replaySequenceMap;
17
15
  private recordingIdCounter;
16
+ private replaySessions;
18
17
  constructor(targets: string[], recordingsDir: string);
19
18
  init(): Promise<void>;
20
19
  listen(port: number): http.Server;
@@ -29,6 +28,19 @@ declare class ProxyServer {
29
28
  private getCorsHeaders;
30
29
  private addCorsHeaders;
31
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;
32
44
  private parseGetParams;
33
45
  private handleControlRequest;
34
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-CG-XcFDa.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-Cx_Kflfl.js';
3
3
  import '@playwright/test';
4
4
 
5
5
  declare class ProxyServer {
@@ -12,9 +12,8 @@ declare class ProxyServer {
12
12
  private proxy;
13
13
  private currentSession;
14
14
  private recordingsDir;
15
- private requestSequenceMap;
16
- private replaySequenceMap;
17
15
  private recordingIdCounter;
16
+ private replaySessions;
18
17
  constructor(targets: string[], recordingsDir: string);
19
18
  init(): Promise<void>;
20
19
  listen(port: number): http.Server;
@@ -29,6 +28,19 @@ declare class ProxyServer {
29
28
  private getCorsHeaders;
30
29
  private addCorsHeaders;
31
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;
32
44
  private parseGetParams;
33
45
  private handleControlRequest;
34
46
  private clearModeTimeout;