browser-commander 0.2.0

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.
Files changed (82) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/release.yml +296 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.jscpd.json +20 -0
  6. package/.prettierignore +7 -0
  7. package/.prettierrc +10 -0
  8. package/CHANGELOG.md +32 -0
  9. package/LICENSE +24 -0
  10. package/README.md +320 -0
  11. package/bunfig.toml +3 -0
  12. package/deno.json +7 -0
  13. package/eslint.config.js +125 -0
  14. package/examples/react-test-app/index.html +25 -0
  15. package/examples/react-test-app/package.json +19 -0
  16. package/examples/react-test-app/src/App.jsx +473 -0
  17. package/examples/react-test-app/src/main.jsx +10 -0
  18. package/examples/react-test-app/src/styles.css +323 -0
  19. package/examples/react-test-app/vite.config.js +9 -0
  20. package/package.json +89 -0
  21. package/scripts/changeset-version.mjs +38 -0
  22. package/scripts/create-github-release.mjs +93 -0
  23. package/scripts/create-manual-changeset.mjs +86 -0
  24. package/scripts/format-github-release.mjs +83 -0
  25. package/scripts/format-release-notes.mjs +216 -0
  26. package/scripts/instant-version-bump.mjs +121 -0
  27. package/scripts/merge-changesets.mjs +260 -0
  28. package/scripts/publish-to-npm.mjs +126 -0
  29. package/scripts/setup-npm.mjs +37 -0
  30. package/scripts/validate-changeset.mjs +262 -0
  31. package/scripts/version-and-commit.mjs +237 -0
  32. package/src/ARCHITECTURE.md +270 -0
  33. package/src/README.md +517 -0
  34. package/src/bindings.js +298 -0
  35. package/src/browser/launcher.js +93 -0
  36. package/src/browser/navigation.js +513 -0
  37. package/src/core/constants.js +24 -0
  38. package/src/core/engine-adapter.js +466 -0
  39. package/src/core/engine-detection.js +49 -0
  40. package/src/core/logger.js +21 -0
  41. package/src/core/navigation-manager.js +503 -0
  42. package/src/core/navigation-safety.js +160 -0
  43. package/src/core/network-tracker.js +373 -0
  44. package/src/core/page-session.js +299 -0
  45. package/src/core/page-trigger-manager.js +564 -0
  46. package/src/core/preferences.js +46 -0
  47. package/src/elements/content.js +197 -0
  48. package/src/elements/locators.js +243 -0
  49. package/src/elements/selectors.js +360 -0
  50. package/src/elements/visibility.js +166 -0
  51. package/src/exports.js +121 -0
  52. package/src/factory.js +192 -0
  53. package/src/high-level/universal-logic.js +206 -0
  54. package/src/index.js +17 -0
  55. package/src/interactions/click.js +684 -0
  56. package/src/interactions/fill.js +383 -0
  57. package/src/interactions/scroll.js +341 -0
  58. package/src/utilities/url.js +33 -0
  59. package/src/utilities/wait.js +135 -0
  60. package/tests/e2e/playwright.e2e.test.js +442 -0
  61. package/tests/e2e/puppeteer.e2e.test.js +408 -0
  62. package/tests/helpers/mocks.js +542 -0
  63. package/tests/unit/bindings.test.js +218 -0
  64. package/tests/unit/browser/navigation.test.js +345 -0
  65. package/tests/unit/core/constants.test.js +72 -0
  66. package/tests/unit/core/engine-adapter.test.js +170 -0
  67. package/tests/unit/core/engine-detection.test.js +81 -0
  68. package/tests/unit/core/logger.test.js +80 -0
  69. package/tests/unit/core/navigation-safety.test.js +202 -0
  70. package/tests/unit/core/network-tracker.test.js +198 -0
  71. package/tests/unit/core/page-trigger-manager.test.js +358 -0
  72. package/tests/unit/elements/content.test.js +318 -0
  73. package/tests/unit/elements/locators.test.js +236 -0
  74. package/tests/unit/elements/selectors.test.js +302 -0
  75. package/tests/unit/elements/visibility.test.js +234 -0
  76. package/tests/unit/factory.test.js +174 -0
  77. package/tests/unit/high-level/universal-logic.test.js +299 -0
  78. package/tests/unit/interactions/click.test.js +340 -0
  79. package/tests/unit/interactions/fill.test.js +378 -0
  80. package/tests/unit/interactions/scroll.test.js +330 -0
  81. package/tests/unit/utilities/url.test.js +63 -0
  82. package/tests/unit/utilities/wait.test.js +207 -0
@@ -0,0 +1,373 @@
1
+ /**
2
+ * NetworkTracker - Track all HTTP requests and wait for network idle
3
+ *
4
+ * This module monitors all network requests on a page and provides
5
+ * methods to wait until all requests are complete (network idle).
6
+ */
7
+
8
+ /**
9
+ * Create a NetworkTracker instance for a page
10
+ * @param {Object} options - Configuration options
11
+ * @param {Object} options.page - Playwright or Puppeteer page object
12
+ * @param {string} options.engine - 'playwright' or 'puppeteer'
13
+ * @param {Function} options.log - Logger instance
14
+ * @param {number} options.idleTimeout - Time to wait after last request completes (default: 500ms)
15
+ * @param {number} options.requestTimeout - Maximum time to wait for a single request (default: 30000ms)
16
+ * @returns {Object} - NetworkTracker API
17
+ */
18
+ export function createNetworkTracker(options = {}) {
19
+ const {
20
+ page,
21
+ engine,
22
+ log,
23
+ idleTimeout = 500,
24
+ requestTimeout = 30000,
25
+ } = options;
26
+
27
+ if (!page) {
28
+ throw new Error('page is required in options');
29
+ }
30
+
31
+ // Track pending requests by URL
32
+ const pendingRequests = new Map();
33
+
34
+ // Track request start times for timeout detection
35
+ const requestStartTimes = new Map();
36
+
37
+ // Event listeners
38
+ const listeners = {
39
+ onRequestStart: [],
40
+ onRequestEnd: [],
41
+ onNetworkIdle: [],
42
+ };
43
+
44
+ // State
45
+ let isTracking = false;
46
+ let idleTimer = null;
47
+ let navigationId = 0; // Track navigation sessions
48
+
49
+ /**
50
+ * Get unique request key
51
+ */
52
+ function getRequestKey(request) {
53
+ // Use URL + method as unique key (handles redirects properly)
54
+ const url = typeof request.url === 'function' ? request.url() : request.url;
55
+ const method =
56
+ typeof request.method === 'function' ? request.method() : request.method;
57
+ return `${method}:${url}`;
58
+ }
59
+
60
+ /**
61
+ * Handle request start
62
+ */
63
+ function onRequest(request) {
64
+ const key = getRequestKey(request);
65
+ const url = typeof request.url === 'function' ? request.url() : request.url;
66
+
67
+ // Ignore data URLs and blob URLs
68
+ if (url.startsWith('data:') || url.startsWith('blob:')) {
69
+ return;
70
+ }
71
+
72
+ pendingRequests.set(key, request);
73
+ requestStartTimes.set(key, Date.now());
74
+
75
+ // Clear idle timer since we have a new request
76
+ if (idleTimer) {
77
+ clearTimeout(idleTimer);
78
+ idleTimer = null;
79
+ }
80
+
81
+ log.debug(
82
+ () =>
83
+ `📤 Request started: ${url.substring(0, 80)}... (pending: ${pendingRequests.size})`
84
+ );
85
+
86
+ // Notify listeners
87
+ listeners.onRequestStart.forEach((fn) =>
88
+ fn({ url, pendingCount: pendingRequests.size })
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Handle request completion (success or failure)
94
+ */
95
+ function onRequestEnd(request) {
96
+ const key = getRequestKey(request);
97
+ const url = typeof request.url === 'function' ? request.url() : request.url;
98
+
99
+ if (!pendingRequests.has(key)) {
100
+ return; // Request was filtered out or already completed
101
+ }
102
+
103
+ pendingRequests.delete(key);
104
+ requestStartTimes.delete(key);
105
+
106
+ log.debug(
107
+ () =>
108
+ `📥 Request ended: ${url.substring(0, 80)}... (pending: ${pendingRequests.size})`
109
+ );
110
+
111
+ // Notify listeners
112
+ listeners.onRequestEnd.forEach((fn) =>
113
+ fn({ url, pendingCount: pendingRequests.size })
114
+ );
115
+
116
+ // Check if we're now idle
117
+ checkIdle();
118
+ }
119
+
120
+ /**
121
+ * Check if network is idle and trigger idle event
122
+ */
123
+ function checkIdle() {
124
+ if (pendingRequests.size === 0 && !idleTimer) {
125
+ // Start idle timer
126
+ idleTimer = setTimeout(() => {
127
+ if (pendingRequests.size === 0) {
128
+ log.debug(() => '🌐 Network idle detected');
129
+ listeners.onNetworkIdle.forEach((fn) => fn());
130
+ }
131
+ idleTimer = null;
132
+ }, idleTimeout);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Start tracking network requests
138
+ */
139
+ function startTracking() {
140
+ if (isTracking) {
141
+ return;
142
+ }
143
+
144
+ isTracking = true;
145
+ navigationId++;
146
+
147
+ // Clear any existing state
148
+ pendingRequests.clear();
149
+ requestStartTimes.clear();
150
+ if (idleTimer) {
151
+ clearTimeout(idleTimer);
152
+ idleTimer = null;
153
+ }
154
+
155
+ // Setup event listeners based on engine
156
+ if (engine === 'playwright') {
157
+ page.on('request', onRequest);
158
+ page.on('requestfinished', onRequestEnd);
159
+ page.on('requestfailed', onRequestEnd);
160
+ } else {
161
+ // Puppeteer
162
+ page.on('request', onRequest);
163
+ page.on('requestfinished', onRequestEnd);
164
+ page.on('requestfailed', onRequestEnd);
165
+ }
166
+
167
+ log.debug(() => '🔌 Network tracking started');
168
+ }
169
+
170
+ /**
171
+ * Stop tracking network requests
172
+ */
173
+ function stopTracking() {
174
+ if (!isTracking) {
175
+ return;
176
+ }
177
+
178
+ isTracking = false;
179
+
180
+ // Remove event listeners
181
+ if (engine === 'playwright') {
182
+ page.off('request', onRequest);
183
+ page.off('requestfinished', onRequestEnd);
184
+ page.off('requestfailed', onRequestEnd);
185
+ } else {
186
+ page.off('request', onRequest);
187
+ page.off('requestfinished', onRequestEnd);
188
+ page.off('requestfailed', onRequestEnd);
189
+ }
190
+
191
+ // Clear state
192
+ pendingRequests.clear();
193
+ requestStartTimes.clear();
194
+ if (idleTimer) {
195
+ clearTimeout(idleTimer);
196
+ idleTimer = null;
197
+ }
198
+
199
+ log.debug(() => '🔌 Network tracking stopped');
200
+ }
201
+
202
+ /**
203
+ * Wait for network to become idle
204
+ * @param {Object} options - Configuration options
205
+ * @param {number} options.timeout - Maximum time to wait (default: 30000ms)
206
+ * @param {number} options.idleTime - Time network must be idle (default: idleTimeout)
207
+ * @returns {Promise<boolean>} - True if idle, false if timeout
208
+ */
209
+ async function waitForNetworkIdle(opts = {}) {
210
+ const { timeout = 30000, idleTime = idleTimeout } = opts;
211
+
212
+ const startTime = Date.now();
213
+ const currentNavId = navigationId;
214
+
215
+ // If already idle, wait for idle time
216
+ if (pendingRequests.size === 0) {
217
+ await new Promise((r) => setTimeout(r, idleTime));
218
+
219
+ // Check if still idle and no navigation happened
220
+ if (pendingRequests.size === 0 && navigationId === currentNavId) {
221
+ return true;
222
+ }
223
+ }
224
+
225
+ return new Promise((resolve) => {
226
+ let resolved = false;
227
+ let checkTimer = null;
228
+ let timeoutTimer = null;
229
+
230
+ const cleanup = () => {
231
+ if (checkTimer) {
232
+ clearInterval(checkTimer);
233
+ }
234
+ if (timeoutTimer) {
235
+ clearTimeout(timeoutTimer);
236
+ }
237
+ resolved = true;
238
+ };
239
+
240
+ // Timeout handler
241
+ timeoutTimer = setTimeout(() => {
242
+ if (!resolved) {
243
+ cleanup();
244
+ log.debug(
245
+ () =>
246
+ `⚠️ Network idle timeout after ${timeout}ms (${pendingRequests.size} pending)`
247
+ );
248
+
249
+ // Log stuck requests
250
+ if (pendingRequests.size > 0) {
251
+ const stuckRequests = [];
252
+ for (const [key, req] of pendingRequests) {
253
+ const startTime = requestStartTimes.get(key);
254
+ const duration = Date.now() - startTime;
255
+ const url = typeof req.url === 'function' ? req.url() : req.url;
256
+ stuckRequests.push(
257
+ ` ${url.substring(0, 60)}... (${duration}ms)`
258
+ );
259
+ }
260
+ log.debug(() => `⚠️ Stuck requests:\n${stuckRequests.join('\n')}`);
261
+ }
262
+
263
+ resolve(false);
264
+ }
265
+ }, timeout);
266
+
267
+ // Check periodically for idle state
268
+ checkTimer = setInterval(async () => {
269
+ if (resolved) {
270
+ return;
271
+ }
272
+
273
+ // Check for navigation change (abort wait)
274
+ if (navigationId !== currentNavId) {
275
+ cleanup();
276
+ resolve(false);
277
+ return;
278
+ }
279
+
280
+ // Check for timed out requests and remove them
281
+ const now = Date.now();
282
+ for (const [key, startTime] of requestStartTimes) {
283
+ if (now - startTime > requestTimeout) {
284
+ log.debug(() => `⚠️ Request timed out, removing: ${key}`);
285
+ pendingRequests.delete(key);
286
+ requestStartTimes.delete(key);
287
+ }
288
+ }
289
+
290
+ // Check if idle
291
+ if (pendingRequests.size === 0) {
292
+ // Wait for idle time to confirm
293
+ await new Promise((r) => setTimeout(r, idleTime));
294
+
295
+ if (
296
+ !resolved &&
297
+ pendingRequests.size === 0 &&
298
+ navigationId === currentNavId
299
+ ) {
300
+ cleanup();
301
+ resolve(true);
302
+ }
303
+ }
304
+ }, 100);
305
+ });
306
+ }
307
+
308
+ /**
309
+ * Get current pending request count
310
+ */
311
+ function getPendingCount() {
312
+ return pendingRequests.size;
313
+ }
314
+
315
+ /**
316
+ * Get list of pending request URLs
317
+ */
318
+ function getPendingUrls() {
319
+ const urls = [];
320
+ for (const [key, req] of pendingRequests) {
321
+ const url = typeof req.url === 'function' ? req.url() : req.url;
322
+ urls.push(url);
323
+ }
324
+ return urls;
325
+ }
326
+
327
+ /**
328
+ * Add event listener
329
+ */
330
+ function on(event, callback) {
331
+ if (listeners[event]) {
332
+ listeners[event].push(callback);
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Remove event listener
338
+ */
339
+ function off(event, callback) {
340
+ if (listeners[event]) {
341
+ const index = listeners[event].indexOf(callback);
342
+ if (index !== -1) {
343
+ listeners[event].splice(index, 1);
344
+ }
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Reset tracking (clear all pending requests)
350
+ * Called on navigation to start fresh
351
+ */
352
+ function reset() {
353
+ navigationId++;
354
+ pendingRequests.clear();
355
+ requestStartTimes.clear();
356
+ if (idleTimer) {
357
+ clearTimeout(idleTimer);
358
+ idleTimer = null;
359
+ }
360
+ log.debug(() => '🔄 Network tracker reset');
361
+ }
362
+
363
+ return {
364
+ startTracking,
365
+ stopTracking,
366
+ waitForNetworkIdle,
367
+ getPendingCount,
368
+ getPendingUrls,
369
+ reset,
370
+ on,
371
+ off,
372
+ };
373
+ }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * PageSession - Abstraction for page lifecycle management
3
+ *
4
+ * A PageSession represents a single "page context" - from when a page
5
+ * is loaded until navigation to a different page occurs.
6
+ *
7
+ * Key features:
8
+ * - Automatic cleanup when navigation occurs
9
+ * - Scope handlers to current page only
10
+ * - Provide context for automation logic
11
+ */
12
+
13
+ /**
14
+ * Create a PageSession factory
15
+ * @param {Object} options - Configuration options
16
+ * @param {Object} options.navigationManager - NavigationManager instance
17
+ * @param {Object} options.networkTracker - NetworkTracker instance
18
+ * @param {Function} options.log - Logger instance
19
+ * @returns {Object} - PageSession factory
20
+ */
21
+ export function createPageSessionFactory(options = {}) {
22
+ const { navigationManager, networkTracker, log } = options;
23
+
24
+ const activeSessions = new Map();
25
+
26
+ /**
27
+ * Create a new PageSession for the current page
28
+ * @param {Object} sessionOptions - Session options
29
+ * @param {string} sessionOptions.name - Session name for debugging
30
+ * @param {RegExp|Function} sessionOptions.urlPattern - URL pattern to match (optional)
31
+ * @returns {Object} - PageSession instance
32
+ */
33
+ function createSession(sessionOptions = {}) {
34
+ const { name = 'unnamed', urlPattern = null } = sessionOptions;
35
+
36
+ const sessionId = navigationManager.getSessionId();
37
+ const startUrl = navigationManager.getCurrentUrl();
38
+
39
+ // State
40
+ let isActive = true;
41
+ let cleanupCallbacks = [];
42
+ let eventListeners = [];
43
+
44
+ log.debug(
45
+ () =>
46
+ `📄 Creating page session "${name}" (id: ${sessionId}) for: ${startUrl}`
47
+ );
48
+
49
+ /**
50
+ * Check if URL matches the session pattern
51
+ */
52
+ function matchesUrl(url) {
53
+ if (!urlPattern) {
54
+ return true;
55
+ }
56
+ if (urlPattern instanceof RegExp) {
57
+ return urlPattern.test(url);
58
+ }
59
+ if (typeof urlPattern === 'function') {
60
+ return urlPattern(url);
61
+ }
62
+ return true;
63
+ }
64
+
65
+ /**
66
+ * Handle navigation - deactivate session if URL no longer matches
67
+ */
68
+ function handleUrlChange({ previousUrl, newUrl }) {
69
+ if (!isActive) {
70
+ return;
71
+ }
72
+
73
+ // If we have a URL pattern, check if we're still on a matching page
74
+ if (urlPattern && !matchesUrl(newUrl)) {
75
+ log.debug(
76
+ () => `📄 Session "${name}" ending - URL no longer matches: ${newUrl}`
77
+ );
78
+ deactivate();
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Handle navigation start - cleanup before leaving
84
+ */
85
+ async function handleBeforeNavigate() {
86
+ if (!isActive) {
87
+ return;
88
+ }
89
+
90
+ log.debug(
91
+ () => `📄 Session "${name}" cleanup triggered (navigation starting)`
92
+ );
93
+ await runCleanup();
94
+ deactivate();
95
+ }
96
+
97
+ // Subscribe to navigation events
98
+ navigationManager.on('onUrlChange', handleUrlChange);
99
+ navigationManager.on('onBeforeNavigate', handleBeforeNavigate);
100
+
101
+ /**
102
+ * Run all cleanup callbacks
103
+ */
104
+ async function runCleanup() {
105
+ for (const callback of cleanupCallbacks) {
106
+ try {
107
+ await callback();
108
+ } catch (e) {
109
+ log.debug(() => `⚠️ Session cleanup error: ${e.message}`);
110
+ }
111
+ }
112
+ cleanupCallbacks = [];
113
+
114
+ // Remove all event listeners
115
+ for (const { target, event, handler } of eventListeners) {
116
+ try {
117
+ target.off(event, handler);
118
+ } catch (e) {
119
+ // Ignore removal errors
120
+ }
121
+ }
122
+ eventListeners = [];
123
+ }
124
+
125
+ /**
126
+ * Deactivate the session
127
+ */
128
+ function deactivate() {
129
+ if (!isActive) {
130
+ return;
131
+ }
132
+
133
+ isActive = false;
134
+
135
+ // Unsubscribe from navigation events
136
+ navigationManager.off('onUrlChange', handleUrlChange);
137
+ navigationManager.off('onBeforeNavigate', handleBeforeNavigate);
138
+
139
+ // Remove from active sessions
140
+ activeSessions.delete(sessionId);
141
+
142
+ log.debug(() => `📄 Session "${name}" deactivated`);
143
+ }
144
+
145
+ /**
146
+ * Register cleanup callback
147
+ * @param {Function} callback - Cleanup callback
148
+ */
149
+ function onCleanup(callback) {
150
+ if (!isActive) {
151
+ log.debug(
152
+ () => `⚠️ Cannot register cleanup on inactive session "${name}"`
153
+ );
154
+ return;
155
+ }
156
+ cleanupCallbacks.push(callback);
157
+ }
158
+
159
+ /**
160
+ * Run code only if session is still active
161
+ * @param {Function} fn - Async function to run
162
+ * @returns {Promise<any>} - Result or null if session inactive
163
+ */
164
+ async function ifActive(fn) {
165
+ if (!isActive) {
166
+ log.debug(() => `⏭️ Skipping action - session "${name}" is inactive`);
167
+ return null;
168
+ }
169
+ return fn();
170
+ }
171
+
172
+ /**
173
+ * Add event listener that will be cleaned up with session
174
+ * @param {Object} target - Event target (page, navigationManager, etc.)
175
+ * @param {string} event - Event name
176
+ * @param {Function} handler - Event handler
177
+ */
178
+ function addEventListener(target, event, handler) {
179
+ if (!isActive) {
180
+ return;
181
+ }
182
+
183
+ target.on(event, handler);
184
+ eventListeners.push({ target, event, handler });
185
+ }
186
+
187
+ /**
188
+ * Check if session is still active
189
+ */
190
+ function checkActive() {
191
+ return isActive;
192
+ }
193
+
194
+ /**
195
+ * Get session info
196
+ */
197
+ function getInfo() {
198
+ return {
199
+ name,
200
+ sessionId,
201
+ startUrl,
202
+ isActive,
203
+ currentUrl: navigationManager.getCurrentUrl(),
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Wait for network idle within this session
209
+ */
210
+ async function waitForNetworkIdle(opts = {}) {
211
+ if (!isActive) {
212
+ return false;
213
+ }
214
+ if (!networkTracker) {
215
+ return true;
216
+ }
217
+ return networkTracker.waitForNetworkIdle(opts);
218
+ }
219
+
220
+ /**
221
+ * Wait for page to be ready within this session
222
+ */
223
+ async function waitForPageReady(opts = {}) {
224
+ if (!isActive) {
225
+ return false;
226
+ }
227
+ return navigationManager.waitForPageReady(opts);
228
+ }
229
+
230
+ /**
231
+ * Manually end the session
232
+ */
233
+ async function end() {
234
+ if (!isActive) {
235
+ return;
236
+ }
237
+
238
+ log.debug(() => `📄 Session "${name}" ending (manual)`);
239
+ await runCleanup();
240
+ deactivate();
241
+ }
242
+
243
+ const session = {
244
+ // State
245
+ get isActive() {
246
+ return isActive;
247
+ },
248
+ get sessionId() {
249
+ return sessionId;
250
+ },
251
+ get startUrl() {
252
+ return startUrl;
253
+ },
254
+ get currentUrl() {
255
+ return navigationManager.getCurrentUrl();
256
+ },
257
+
258
+ // Lifecycle
259
+ onCleanup,
260
+ end,
261
+ checkActive,
262
+ getInfo,
263
+
264
+ // Utilities
265
+ ifActive,
266
+ addEventListener,
267
+ waitForNetworkIdle,
268
+ waitForPageReady,
269
+ };
270
+
271
+ // Track active session
272
+ activeSessions.set(sessionId, session);
273
+
274
+ return session;
275
+ }
276
+
277
+ /**
278
+ * Get all active sessions
279
+ */
280
+ function getActiveSessions() {
281
+ return Array.from(activeSessions.values());
282
+ }
283
+
284
+ /**
285
+ * End all active sessions
286
+ */
287
+ async function endAllSessions() {
288
+ const sessions = Array.from(activeSessions.values());
289
+ for (const session of sessions) {
290
+ await session.end();
291
+ }
292
+ }
293
+
294
+ return {
295
+ createSession,
296
+ getActiveSessions,
297
+ endAllSessions,
298
+ };
299
+ }