@testmuai/playwright-bindings 0.1.17 → 0.1.19-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { ClipboardStore, installClipboard, pasteViaPage, runClipboardQuery } from './clipboard.js';
2
2
  import * as configure from '../configure.js';
3
3
  import { log } from '../log.js';
4
+ import { SSEWireParser } from './sseParser.js';
4
5
  // ── State ──
5
6
  let networkEntries = [];
6
7
  let networkSequence = 0;
@@ -14,14 +15,29 @@ let wsActive = false;
14
15
  let wsConfigured = false; // true once WS capture was started this run
15
16
  let wsSeq = 0; // global monotonic across all connections
16
17
  let wsConnCounter = 0; // connection-id allocator
18
+ // SSE capture (CDP-based, Chromium-only). CDP sessions are NOT auto-managed
19
+ // like page.on — keep a page->CDPSession registry and detach each on stop.
20
+ let sseConnections = [];
21
+ let sseActive = false;
22
+ let sseConfigured = false; // true once SSE capture was started this run
23
+ let sseSeq = 0; // global monotonic across all connections
24
+ let sseConnCounter = 0; // connection-id allocator
25
+ const sseMsgCounter = new Map(); // connectionId -> per-conn messageIndex
26
+ const sseSessions = new Map(); // the page->session registry
27
+ const sseTasks = new Set(); // pending scheduled tasks (response/popup)
17
28
  let pageRef = null;
18
29
  let contextRef = null;
30
+ let pageListenerActive = false; // true once context.on('page') is wired this run
31
+ let extraPages = []; // tabs opened mid-run that got capture listeners
19
32
  const MAX_NETWORK = 5000;
20
33
  const MAX_BODY = 65536;
21
34
  const MAX_CONSOLE = 50000;
22
35
  const MAX_WS_CONN = 256;
23
36
  const MAX_WS_FRAMES = 2000;
24
37
  const WS_PRESERVE_FIRST_N = 20;
38
+ const MAX_SSE_CONN = 256;
39
+ const MAX_SSE_MSGS = 2000;
40
+ const SSE_PRESERVE_FIRST_N = 20;
25
41
  // Raw-byte budget for a binary frame so its base64 stays <= MAX_BODY and decodes cleanly
26
42
  // (multiple of 3 → no mid-stream base64 padding). Math.floor(65536/4)*3 = 49152. Matches Python.
27
43
  const MAX_WS_RAW_BUDGET = Math.floor(MAX_BODY / 4) * 3;
@@ -47,6 +63,19 @@ export async function startCapture(page, context) {
47
63
  startConsoleCapture(page);
48
64
  if (devtools.websocket)
49
65
  startWebSocketCapture(page);
66
+ if (devtools.sse) {
67
+ // SSE rides a CDP session (Chromium-only); attach is async. The
68
+ // non-Chromium hard-fail lives in session.ts run() BEFORE this runs.
69
+ await startSSECaptureInitial(page);
70
+ }
71
+ // Page-based captures (network/console/websocket/sse) only listen on the page
72
+ // they were attached to. Re-arm them on tabs opened later (window.open,
73
+ // target=_blank) via the context's 'page' event — otherwise those tabs are
74
+ // silently uncaptured. (performance/clipboard ride context init scripts and
75
+ // already cover every page.)
76
+ if (devtools.network || devtools.console || devtools.websocket || devtools.sse) {
77
+ attachContextPageListener(context);
78
+ }
50
79
  if (devtools.performance)
51
80
  await installPerformance(context);
52
81
  const enabled = Object.entries(devtools).filter(([, v]) => v).map(([k]) => k);
@@ -54,14 +83,86 @@ export async function startCapture(page, context) {
54
83
  log.test(`DevTools capture started: ${enabled.join(', ')}`);
55
84
  }
56
85
  export function stopCapture(page) {
86
+ detachContextPageListener();
57
87
  if (networkActive)
58
88
  stopNetworkCapture(page);
59
89
  if (consoleActive)
60
90
  stopConsoleCapture(page);
61
91
  if (wsActive)
62
92
  stopWebSocketCapture(page);
93
+ // SSE CDP sessions are detached asynchronously by stopSSECapture(); this sync
94
+ // stop only flips sseActive off (so handlers short-circuit). The context
95
+ // 'page' listener is removed above, so no new popup-attach task is scheduled
96
+ // before stopSSECapture() drains and detaches.
97
+ sseActive = false;
98
+ // The per-type stops above only detach the initial page; also detach the
99
+ // tabs opened mid-run so no listener outlives the capture session.
100
+ detachExtraPages();
101
+ }
102
+ // ── New-page propagation — capture tabs opened after startCapture ──
103
+ function attachContextPageListener(context) {
104
+ if (pageListenerActive || !context)
105
+ return;
106
+ try {
107
+ context.on('page', onNewPage);
108
+ pageListenerActive = true;
109
+ }
110
+ catch { /* ignore */ }
111
+ }
112
+ function detachContextPageListener() {
113
+ if (pageListenerActive && contextRef) {
114
+ try {
115
+ contextRef.removeListener('page', onNewPage);
116
+ }
117
+ catch { /* ignore */ }
118
+ }
119
+ pageListenerActive = false;
120
+ }
121
+ function detachExtraPages() {
122
+ // Removing a listener that was never attached (only network enabled, say) is
123
+ // a safe no-op.
124
+ for (const p of extraPages) {
125
+ detachNetworkListeners(p);
126
+ detachConsoleListeners(p);
127
+ detachWebSocketListeners(p);
128
+ }
129
+ extraPages = [];
130
+ }
131
+ function onNewPage(page) {
132
+ // Attach the currently-active capture listeners to a newly opened page.
133
+ // Attach-only — never resets accumulated state (which the start* helpers do).
134
+ // Each capture type is guarded independently so one failure can't skip the rest.
135
+ let attached = false;
136
+ for (const [active, attach] of [
137
+ [networkActive, attachNetworkListeners],
138
+ [consoleActive, attachConsoleListeners],
139
+ [wsActive, attachWebSocketListeners],
140
+ ]) {
141
+ if (!active)
142
+ continue;
143
+ try {
144
+ attach(page);
145
+ attached = true;
146
+ }
147
+ catch { /* ignore */ }
148
+ }
149
+ // SSE attach is async (own CDP session) — schedule it as a tracked task.
150
+ // The session is registered in sseSessions and detached by stopSSECapture.
151
+ if (sseActive) {
152
+ try {
153
+ schedule(attachSseToPage(page));
154
+ }
155
+ catch { /* ignore */ }
156
+ }
157
+ if (attached && !extraPages.includes(page))
158
+ extraPages.push(page);
63
159
  }
64
160
  export function resetCapture() {
161
+ // Remove any live listeners on the (possibly reused) context/tabs before
162
+ // dropping our refs, so a later startCapture can't double-register them.
163
+ // detachContextPageListener() also clears pageListenerActive.
164
+ detachContextPageListener();
165
+ detachExtraPages();
65
166
  networkEntries = [];
66
167
  networkSequence = 0;
67
168
  networkActive = false;
@@ -75,6 +176,17 @@ export function resetCapture() {
75
176
  wsConnCounter = 0;
76
177
  wsById.clear();
77
178
  wsFrameCounter.clear();
179
+ // SSE: best-effort sync reset for test isolation. Live CDP sessions should be
180
+ // detached via stopSSECapture() in the async teardown; here we drop refs so a
181
+ // fresh run starts clean.
182
+ sseConnections = [];
183
+ sseActive = false;
184
+ sseConfigured = false;
185
+ sseSeq = 0;
186
+ sseConnCounter = 0;
187
+ sseMsgCounter.clear();
188
+ sseSessions.clear();
189
+ sseTasks.clear();
78
190
  pendingRequests.clear();
79
191
  pageRef = null;
80
192
  contextRef = null;
@@ -84,13 +196,15 @@ function startNetworkCapture(page) {
84
196
  networkEntries = [];
85
197
  networkSequence = 0;
86
198
  networkActive = true;
199
+ attachNetworkListeners(page);
200
+ }
201
+ function attachNetworkListeners(page) {
87
202
  page.on('request', onRequest);
88
203
  page.on('response', onResponse);
89
204
  page.on('requestfinished', onRequestFinished);
90
205
  page.on('requestfailed', onRequestFailed);
91
206
  }
92
- function stopNetworkCapture(page) {
93
- networkActive = false;
207
+ function detachNetworkListeners(page) {
94
208
  try {
95
209
  page.removeListener('request', onRequest);
96
210
  page.removeListener('response', onResponse);
@@ -99,13 +213,27 @@ function stopNetworkCapture(page) {
99
213
  }
100
214
  catch { /* ignore */ }
101
215
  }
216
+ function stopNetworkCapture(page) {
217
+ networkActive = false;
218
+ detachNetworkListeners(page);
219
+ }
102
220
  function onRequest(request) {
103
221
  if (!networkActive)
104
222
  return;
105
223
  const u = new URL(request.url());
106
224
  const qp = {};
107
225
  u.searchParams.forEach((v, k) => { qp[k] = v; });
108
- let requestBody = request.postData() ?? null;
226
+ // request.postData() decodes the body via Buffer.toString('utf-8'), which
227
+ // substitutes invalid bytes (it won't throw the way Python's post_data does),
228
+ // but guard anyway so a future Playwright change can't let an error escape
229
+ // this event listener — parity with the Python binding.
230
+ let requestBody;
231
+ try {
232
+ requestBody = request.postData() ?? null;
233
+ }
234
+ catch {
235
+ requestBody = null;
236
+ }
109
237
  if (requestBody && requestBody.length > MAX_BODY) {
110
238
  requestBody = requestBody.slice(0, MAX_BODY);
111
239
  }
@@ -147,7 +275,14 @@ async function onResponse(response) {
147
275
  entry.responseStatus = response.status();
148
276
  entry.responseHeaders = await response.allHeaders();
149
277
  const ct = entry.responseHeaders['content-type'] ?? '';
150
- if (TEXT_TYPES.some(t => ct.includes(t))) {
278
+ // SSE responses (text/event-stream) are long-lived streams whose body
279
+ // never resolves until the stream closes — reading it here would hang the
280
+ // capture. SSE is captured separately via CDP, so skip the body read.
281
+ // Match before the generic 'text' substring (which catches event-stream).
282
+ if (ct.includes('text/event-stream')) {
283
+ entry.responseBody = null;
284
+ }
285
+ else if (TEXT_TYPES.some(t => ct.includes(t))) {
151
286
  try {
152
287
  let body = await response.text();
153
288
  if (body.length > MAX_BODY)
@@ -184,17 +319,23 @@ function startConsoleCapture(page) {
184
319
  consoleEntries = [];
185
320
  consoleSequence = 0;
186
321
  consoleActive = true;
322
+ attachConsoleListeners(page);
323
+ }
324
+ function attachConsoleListeners(page) {
187
325
  page.on('console', onConsoleMessage);
188
326
  page.on('pageerror', onPageError);
189
327
  }
190
- function stopConsoleCapture(page) {
191
- consoleActive = false;
328
+ function detachConsoleListeners(page) {
192
329
  try {
193
330
  page.removeListener('console', onConsoleMessage);
194
331
  page.removeListener('pageerror', onPageError);
195
332
  }
196
333
  catch { /* ignore */ }
197
334
  }
335
+ function stopConsoleCapture(page) {
336
+ consoleActive = false;
337
+ detachConsoleListeners(page);
338
+ }
198
339
  async function onConsoleMessage(msg) {
199
340
  if (!consoleActive)
200
341
  return;
@@ -259,15 +400,21 @@ function startWebSocketCapture(page) {
259
400
  wsConnCounter = 0;
260
401
  wsById.clear();
261
402
  wsFrameCounter.clear();
403
+ attachWebSocketListeners(page);
404
+ }
405
+ function attachWebSocketListeners(page) {
262
406
  page.on('websocket', onWebSocket);
263
407
  }
264
- function stopWebSocketCapture(page) {
265
- wsActive = false;
408
+ function detachWebSocketListeners(page) {
266
409
  try {
267
410
  page.removeListener('websocket', onWebSocket);
268
411
  }
269
412
  catch { /* ignore */ }
270
413
  }
414
+ function stopWebSocketCapture(page) {
415
+ wsActive = false;
416
+ detachWebSocketListeners(page);
417
+ }
271
418
  function onWebSocket(ws) {
272
419
  if (!wsActive)
273
420
  return;
@@ -392,6 +539,254 @@ function reindexWsById(removed) {
392
539
  wsById.set(k, v - 1);
393
540
  }
394
541
  }
542
+ // ── SSE capture (CDP, Chromium-only) ──
543
+ function schedule(p) {
544
+ // Track a scheduled task so teardown can drain it; remove on completion.
545
+ sseTasks.add(p);
546
+ void p.finally(() => sseTasks.delete(p));
547
+ }
548
+ function normSseEvent(name) {
549
+ // CDP eventName "" or "message" -> null (default event type).
550
+ return !name || name === 'message' ? null : name;
551
+ }
552
+ function isEventStream(resp) {
553
+ const mime = (resp.mimeType ?? '').split(';')[0].trim().toLowerCase();
554
+ if (mime === 'text/event-stream')
555
+ return true;
556
+ const headers = resp.headers ?? {};
557
+ const ct = headers['content-type'] ?? headers['Content-Type'] ?? '';
558
+ return ct.toLowerCase().includes('text/event-stream');
559
+ }
560
+ function allocSseConn(url, transport) {
561
+ if (sseConnections.length >= MAX_SSE_CONN)
562
+ evictOldestClosedSse();
563
+ let domain = '';
564
+ let path = '/';
565
+ try {
566
+ const u = new URL(url || '');
567
+ domain = u.hostname;
568
+ path = u.pathname || '/';
569
+ }
570
+ catch { /* malformed */ }
571
+ const conn = {
572
+ connectionId: `sse_${sseConnCounter}`,
573
+ url: url || '', domain, path,
574
+ openedTsMs: Date.now(),
575
+ closedTsMs: null,
576
+ error: null,
577
+ transport,
578
+ messages: [],
579
+ droppedMessages: 0,
580
+ };
581
+ sseConnCounter++;
582
+ sseConnections.push(conn);
583
+ return conn;
584
+ }
585
+ function appendSseMessage(conn, msg) {
586
+ // per-connection ring with first-N preserved; messageIndex stays monotonic via
587
+ // sseMsgCounter (NOT messages.length, which repeats after eviction).
588
+ if (conn.messages.length >= MAX_SSE_MSGS) {
589
+ if (SSE_PRESERVE_FIRST_N < conn.messages.length) {
590
+ conn.messages.splice(SSE_PRESERVE_FIRST_N, 1);
591
+ conn.droppedMessages++;
592
+ }
593
+ }
594
+ conn.messages.push(msg);
595
+ }
596
+ function evictOldestClosedSse() {
597
+ // Session request-maps hold object refs (not indices), so no re-index needed;
598
+ // just drop the evicted conn's message counter.
599
+ for (let i = 0; i < sseConnections.length; i++) {
600
+ if (sseConnections[i].closedTsMs !== null) {
601
+ sseMsgCounter.delete(sseConnections[i].connectionId);
602
+ sseConnections.splice(i, 1);
603
+ return;
604
+ }
605
+ }
606
+ if (sseConnections.length) {
607
+ sseMsgCounter.delete(sseConnections[0].connectionId);
608
+ sseConnections.splice(0, 1);
609
+ }
610
+ }
611
+ function emitSse(conn, ev, transport) {
612
+ if (!sseActive)
613
+ return;
614
+ try {
615
+ let data = ev.data ?? '';
616
+ const truncated = Boolean(ev.truncated) || data.length > MAX_BODY;
617
+ data = data.slice(0, MAX_BODY);
618
+ const cid = conn.connectionId;
619
+ const midx = sseMsgCounter.get(cid) ?? 0;
620
+ sseMsgCounter.set(cid, midx + 1);
621
+ appendSseMessage(conn, {
622
+ connectionId: cid,
623
+ data,
624
+ event: ev.event,
625
+ id: ev.id,
626
+ retry: ev.retry,
627
+ transport,
628
+ truncated,
629
+ tsMs: Date.now(),
630
+ seq: sseSeq,
631
+ messageIndex: midx,
632
+ });
633
+ sseSeq++;
634
+ }
635
+ catch { /* ignore */ }
636
+ }
637
+ /**
638
+ * Create a CDP session on `page`, wire SSE handlers, register it in the
639
+ * page->session registry, and return it. Handlers close over per-session request
640
+ * maps (requestId collides across CDP targets).
641
+ */
642
+ export async function startSSECapture(page) {
643
+ const cdp = await page.context().newCDPSession(page);
644
+ await cdp.send('Network.enable');
645
+ const streams = new Map();
646
+ const native = new Map();
647
+ const getNativeConn = (rid, url = '') => {
648
+ let conn = native.get(rid);
649
+ if (!conn) {
650
+ conn = allocSseConn(url, 'eventsource');
651
+ native.set(rid, conn);
652
+ }
653
+ else if (url && !conn.url) {
654
+ conn.url = url;
655
+ try {
656
+ const u = new URL(url);
657
+ conn.domain = u.hostname;
658
+ conn.path = u.pathname || '/';
659
+ }
660
+ catch { /* malformed */ }
661
+ }
662
+ return conn;
663
+ };
664
+ const decodeB64 = (b64) => Buffer.from(b64, 'base64');
665
+ cdp.on('Network.eventSourceMessageReceived', (p) => {
666
+ if (!sseActive)
667
+ return;
668
+ try {
669
+ const conn = getNativeConn(p.requestId);
670
+ emitSse(conn, {
671
+ event: normSseEvent(p.eventName),
672
+ data: p.data ?? '',
673
+ id: p.eventId || null,
674
+ retry: null,
675
+ }, 'eventsource');
676
+ }
677
+ catch { /* ignore */ }
678
+ });
679
+ const onResponse = async (p) => {
680
+ if (!sseActive)
681
+ return;
682
+ try {
683
+ const resp = p.response ?? {};
684
+ if (!isEventStream(resp))
685
+ return;
686
+ const rid = p.requestId;
687
+ const rtype = p.type ?? 'Other';
688
+ if (rtype === 'EventSource') {
689
+ // Native EventSource is pre-parsed; never stream it (double-count).
690
+ getNativeConn(rid, resp.url ?? '');
691
+ return;
692
+ }
693
+ const transport = rtype.toLowerCase();
694
+ const conn = allocSseConn(resp.url ?? '', transport);
695
+ // NOT-yet-primed: dataReceived arriving during the streamResourceContent await
696
+ // is queued (not fed) so bufferedData (earlier bytes) parses first — ordering
697
+ // must not rely on event-loop scheduling (a buffered burst on connect would
698
+ // otherwise corrupt the parse).
699
+ const entry = { conn, parser: new SSEWireParser(), dec: new TextDecoder('utf-8'), transport, primed: false, pending: [] };
700
+ streams.set(rid, entry);
701
+ let buffered;
702
+ try {
703
+ const res = await cdp.send('Network.streamResourceContent', { requestId: rid });
704
+ buffered = res?.bufferedData;
705
+ }
706
+ catch {
707
+ buffered = undefined;
708
+ }
709
+ if (buffered) {
710
+ for (const ev of entry.parser.feed(entry.dec.decode(decodeB64(buffered), { stream: true })))
711
+ emitSse(conn, ev, transport);
712
+ }
713
+ for (const chunk of entry.pending) {
714
+ for (const ev of entry.parser.feed(entry.dec.decode(decodeB64(chunk), { stream: true })))
715
+ emitSse(conn, ev, transport);
716
+ }
717
+ entry.pending = [];
718
+ entry.primed = true;
719
+ }
720
+ catch { /* ignore */ }
721
+ };
722
+ cdp.on('Network.responseReceived', (p) => schedule(onResponse(p)));
723
+ cdp.on('Network.dataReceived', (p) => {
724
+ if (!sseActive)
725
+ return;
726
+ try {
727
+ const st = streams.get(p.requestId);
728
+ if (!st || !p.data)
729
+ return;
730
+ if (!st.primed) {
731
+ st.pending.push(p.data);
732
+ return;
733
+ }
734
+ for (const ev of st.parser.feed(st.dec.decode(decodeB64(p.data), { stream: true })))
735
+ emitSse(st.conn, ev, st.transport);
736
+ }
737
+ catch { /* ignore */ }
738
+ });
739
+ const markClosed = (rid, error) => {
740
+ try {
741
+ const conn = streams.get(rid)?.conn ?? native.get(rid);
742
+ if (!conn)
743
+ return;
744
+ conn.closedTsMs = Date.now();
745
+ if (error)
746
+ conn.error = String(error);
747
+ }
748
+ catch { /* ignore */ }
749
+ };
750
+ cdp.on('Network.loadingFinished', (p) => markClosed(p.requestId));
751
+ cdp.on('Network.loadingFailed', (p) => markClosed(p.requestId, p.errorText));
752
+ sseSessions.set(page, cdp);
753
+ return cdp;
754
+ }
755
+ async function startSSECaptureInitial(page) {
756
+ sseConnections = [];
757
+ sseActive = true;
758
+ sseConfigured = true;
759
+ sseSeq = 0;
760
+ sseConnCounter = 0;
761
+ sseMsgCounter.clear();
762
+ sseSessions.clear();
763
+ await startSSECapture(page);
764
+ }
765
+ async function attachSseToPage(page) {
766
+ // Popup attach: a tab opened after startCapture gets its own CDP session.
767
+ try {
768
+ await startSSECapture(page);
769
+ }
770
+ catch { /* ignore */ }
771
+ }
772
+ /**
773
+ * Async SSE teardown: stop handlers, drain pending tasks, detach every CDP
774
+ * session. The context 'page' listener is removed by the sync stopCapture
775
+ * BEFORE this runs, so no new popup-attach task can be scheduled mid-teardown.
776
+ */
777
+ export async function stopSSECapture() {
778
+ sseActive = false;
779
+ const pending = [...sseTasks];
780
+ if (pending.length)
781
+ await Promise.allSettled(pending);
782
+ for (const cdp of [...sseSessions.values()]) {
783
+ try {
784
+ await cdp.detach();
785
+ }
786
+ catch { /* ignore */ }
787
+ }
788
+ sseSessions.clear();
789
+ }
395
790
  // ── Performance ──
396
791
  const WEB_VITALS_INIT_SCRIPT =
397
792
  // Vendored web-vitals v4 IIFE
@@ -489,6 +884,43 @@ function buildWebSocketAPI() {
489
884
  get all() { return [...wsConnections]; },
490
885
  };
491
886
  }
887
+ /**
888
+ * Build a query API object for SSE — injected as `sse` into the user's code,
889
+ * alongside `network`. Light filters; the query code does the rest. Mirror of
890
+ * the Python _ReadOnlySSELog.
891
+ */
892
+ function buildSSEAPI() {
893
+ const matchUrl = (c, url, urlGlob) => {
894
+ if (url && c.url !== url)
895
+ return false;
896
+ if (urlGlob && !globToRegExp(urlGlob).test(c.url))
897
+ return false;
898
+ return true;
899
+ };
900
+ return {
901
+ connections(filter) {
902
+ let results = [...sseConnections];
903
+ if (filter)
904
+ results = results.filter(c => matchUrl(c, filter.url, filter.urlGlob));
905
+ return results;
906
+ },
907
+ messages(filter) {
908
+ const out = [];
909
+ for (const c of sseConnections) {
910
+ if (filter && !matchUrl(c, filter.url, filter.urlGlob))
911
+ continue;
912
+ for (const m of c.messages) {
913
+ if (filter?.event !== undefined && m.event !== filter.event)
914
+ continue;
915
+ out.push(m);
916
+ }
917
+ }
918
+ out.sort((a, b) => a.seq - b.seq); // global merge order across connections
919
+ return out;
920
+ },
921
+ get all() { return [...sseConnections]; },
922
+ };
923
+ }
492
924
  /**
493
925
  * Build a query API object for console — injected as `consoleLog` into the user's code.
494
926
  */
@@ -534,19 +966,24 @@ function buildStorageAPI(data) {
534
966
  };
535
967
  }
536
968
  export async function devtoolsNetworkQuery(codeJs) {
537
- const network = buildNetworkAPI();
538
969
  try {
539
- // Pass the `websocket` arg only when WS capture was configured, so the no-WS
540
- // call stays the original single-arg form.
541
- let result;
970
+ // Build args dynamically, gated on the PERSISTENT configured flags (never
971
+ // .length eviction can splice connections to 0 mid-session, which would
972
+ // drop the object and silently pass + regress WS). websocket and sse are
973
+ // independent: a query can use network only, +ws, +sse, or all three. When
974
+ // neither was configured this is the exact single-arg form (byte-identical).
975
+ const names = ['network'];
976
+ const values = [buildNetworkAPI()];
542
977
  if (wsConfigured) {
543
- const fn = new Function('network', 'websocket', `"use strict";\n${codeJs}`);
544
- result = fn(network, buildWebSocketAPI());
978
+ names.push('websocket');
979
+ values.push(buildWebSocketAPI());
545
980
  }
546
- else {
547
- const fn = new Function('network', `"use strict";\n${codeJs}`);
548
- result = fn(network);
981
+ if (sseConfigured) {
982
+ names.push('sse');
983
+ values.push(buildSSEAPI());
549
984
  }
985
+ const fn = new Function(...names, `"use strict";\n${codeJs}`);
986
+ const result = fn(...values);
550
987
  return result != null ? String(result) : '';
551
988
  }
552
989
  catch (e) {