@fairfox/polly 0.1.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 (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/cli/polly.ts +564 -0
  4. package/dist/background/api-client.d.ts +7 -0
  5. package/dist/background/context-menu.d.ts +7 -0
  6. package/dist/background/index.d.ts +31 -0
  7. package/dist/background/index.js +1309 -0
  8. package/dist/background/index.js.map +25 -0
  9. package/dist/background/log-store.d.ts +22 -0
  10. package/dist/background/message-router.d.ts +30 -0
  11. package/dist/background/message-router.js +1300 -0
  12. package/dist/background/message-router.js.map +24 -0
  13. package/dist/background/offscreen-manager.d.ts +10 -0
  14. package/dist/index.d.ts +18 -0
  15. package/dist/index.js +1471 -0
  16. package/dist/index.js.map +27 -0
  17. package/dist/shared/adapters/chrome/context-menus.chrome.d.ts +8 -0
  18. package/dist/shared/adapters/chrome/offscreen.chrome.d.ts +6 -0
  19. package/dist/shared/adapters/chrome/runtime.chrome.d.ts +13 -0
  20. package/dist/shared/adapters/chrome/storage.chrome.d.ts +8 -0
  21. package/dist/shared/adapters/chrome/tabs.chrome.d.ts +16 -0
  22. package/dist/shared/adapters/chrome/window.chrome.d.ts +6 -0
  23. package/dist/shared/adapters/context-menus.adapter.d.ts +22 -0
  24. package/dist/shared/adapters/fetch.adapter.d.ts +6 -0
  25. package/dist/shared/adapters/index.d.ts +34 -0
  26. package/dist/shared/adapters/index.js +298 -0
  27. package/dist/shared/adapters/index.js.map +18 -0
  28. package/dist/shared/adapters/logger.adapter.d.ts +44 -0
  29. package/dist/shared/adapters/offscreen.adapter.d.ts +20 -0
  30. package/dist/shared/adapters/runtime.adapter.d.ts +66 -0
  31. package/dist/shared/adapters/storage.adapter.d.ts +29 -0
  32. package/dist/shared/adapters/tabs.adapter.d.ts +39 -0
  33. package/dist/shared/adapters/window.adapter.d.ts +14 -0
  34. package/dist/shared/lib/context-helpers.d.ts +64 -0
  35. package/dist/shared/lib/context-helpers.js +1086 -0
  36. package/dist/shared/lib/context-helpers.js.map +24 -0
  37. package/dist/shared/lib/context-specific-helpers.d.ts +160 -0
  38. package/dist/shared/lib/errors.d.ts +67 -0
  39. package/dist/shared/lib/errors.js +94 -0
  40. package/dist/shared/lib/errors.js.map +10 -0
  41. package/dist/shared/lib/handler-execution-tracker.d.ts +24 -0
  42. package/dist/shared/lib/message-bus.d.ts +233 -0
  43. package/dist/shared/lib/message-bus.js +1033 -0
  44. package/dist/shared/lib/message-bus.js.map +23 -0
  45. package/dist/shared/lib/state.d.ts +102 -0
  46. package/dist/shared/lib/state.js +1265 -0
  47. package/dist/shared/lib/state.js.map +24 -0
  48. package/dist/shared/lib/test-helpers.d.ts +133 -0
  49. package/dist/shared/lib/test-helpers.js +136 -0
  50. package/dist/shared/lib/test-helpers.js.map +10 -0
  51. package/dist/shared/state/app-state.d.ts +8 -0
  52. package/dist/shared/state/app-state.js +1272 -0
  53. package/dist/shared/state/app-state.js.map +25 -0
  54. package/dist/shared/types/messages.d.ts +341 -0
  55. package/dist/shared/types/messages.js +25 -0
  56. package/dist/shared/types/messages.js.map +10 -0
  57. package/package.json +110 -0
@@ -0,0 +1,1086 @@
1
+ // src/shared/adapters/chrome/context-menus.chrome.ts
2
+ class ChromeContextMenusAdapter {
3
+ async create(createProperties) {
4
+ return new Promise((resolve, reject) => {
5
+ chrome.contextMenus.create(createProperties, () => {
6
+ if (chrome.runtime.lastError) {
7
+ reject(new Error(chrome.runtime.lastError.message));
8
+ } else {
9
+ resolve();
10
+ }
11
+ });
12
+ });
13
+ }
14
+ async update(id, updateProperties) {
15
+ await chrome.contextMenus.update(id, updateProperties);
16
+ }
17
+ async remove(id) {
18
+ await chrome.contextMenus.remove(id);
19
+ }
20
+ async removeAll() {
21
+ await chrome.contextMenus.removeAll();
22
+ }
23
+ onClicked(callback) {
24
+ chrome.contextMenus.onClicked.addListener(callback);
25
+ }
26
+ }
27
+
28
+ // src/shared/adapters/chrome/offscreen.chrome.ts
29
+ class ChromeOffscreenAdapter {
30
+ async createDocument(parameters) {
31
+ await chrome.offscreen.createDocument({
32
+ url: parameters.url,
33
+ reasons: parameters.reasons,
34
+ justification: parameters.justification
35
+ });
36
+ }
37
+ async closeDocument() {
38
+ await chrome.offscreen.closeDocument();
39
+ }
40
+ async hasDocument() {
41
+ const existingContexts = await chrome.runtime.getContexts({
42
+ contextTypes: ["OFFSCREEN_DOCUMENT"]
43
+ });
44
+ return existingContexts.length > 0;
45
+ }
46
+ }
47
+
48
+ // src/shared/adapters/chrome/runtime.chrome.ts
49
+ class ChromeRuntimeAdapter {
50
+ messageListeners = new Map;
51
+ static listenerCount = 0;
52
+ sendMessage(message) {
53
+ return chrome.runtime.sendMessage(message);
54
+ }
55
+ onMessage(callback) {
56
+ const wrappedCallback = (message, sender, sendResponse) => {
57
+ const mappedSender = {
58
+ ...sender.tab && {
59
+ tab: {
60
+ id: sender.tab.id ?? 0,
61
+ url: sender.tab.url ?? "",
62
+ title: sender.tab.title ?? ""
63
+ }
64
+ },
65
+ ...sender.frameId !== undefined && { frameId: sender.frameId },
66
+ ...sender.url && { url: sender.url }
67
+ };
68
+ return callback(message, mappedSender, sendResponse);
69
+ };
70
+ this.messageListeners.set(callback, wrappedCallback);
71
+ chrome.runtime.onMessage.addListener(wrappedCallback);
72
+ ChromeRuntimeAdapter.listenerCount++;
73
+ if (ChromeRuntimeAdapter.listenerCount > 1) {
74
+ console.warn(`⚠️ WARNING: ${ChromeRuntimeAdapter.listenerCount} chrome.runtime.onMessage listeners registered!
75
+
76
+ Multiple listeners will cause message handlers to execute multiple times.
77
+ This is usually caused by:
78
+ 1. Creating both MessageBus and MessageRouter with separate listeners
79
+ 2. Calling createBackground() multiple times
80
+ 3. Calling getMessageBus('background') after createBackground()
81
+
82
+ Fix: In background scripts, use createBackground() ONCE at startup.
83
+ Do not call getMessageBus('background') separately.`);
84
+ }
85
+ }
86
+ removeMessageListener(callback) {
87
+ const wrappedCallback = this.messageListeners.get(callback);
88
+ if (wrappedCallback) {
89
+ chrome.runtime.onMessage.removeListener(wrappedCallback);
90
+ this.messageListeners.delete(callback);
91
+ ChromeRuntimeAdapter.listenerCount = Math.max(0, ChromeRuntimeAdapter.listenerCount - 1);
92
+ }
93
+ }
94
+ connect(name) {
95
+ const port = chrome.runtime.connect({ name });
96
+ return new ChromePortAdapter(port);
97
+ }
98
+ onConnect(callback) {
99
+ chrome.runtime.onConnect.addListener((port) => {
100
+ callback(new ChromePortAdapter(port));
101
+ });
102
+ }
103
+ getURL(path) {
104
+ return chrome.runtime.getURL(path);
105
+ }
106
+ getId() {
107
+ return chrome.runtime.id;
108
+ }
109
+ openOptionsPage() {
110
+ chrome.runtime.openOptionsPage();
111
+ }
112
+ }
113
+
114
+ class ChromePortAdapter {
115
+ port;
116
+ listeners = {
117
+ message: new Set,
118
+ disconnect: new Set
119
+ };
120
+ constructor(port) {
121
+ this.port = port;
122
+ this.port.onMessage.addListener((message) => {
123
+ for (const callback of this.listeners.message) {
124
+ callback(message);
125
+ }
126
+ });
127
+ this.port.onDisconnect.addListener(() => {
128
+ for (const callback of this.listeners.disconnect) {
129
+ callback();
130
+ }
131
+ });
132
+ }
133
+ get name() {
134
+ return this.port.name;
135
+ }
136
+ postMessage(message) {
137
+ this.port.postMessage(message);
138
+ }
139
+ onMessage(callback) {
140
+ this.listeners.message.add(callback);
141
+ }
142
+ onDisconnect(callback) {
143
+ this.listeners.disconnect.add(callback);
144
+ }
145
+ disconnect() {
146
+ this.port.disconnect();
147
+ }
148
+ }
149
+
150
+ // src/shared/adapters/chrome/storage.chrome.ts
151
+ class ChromeStorageAdapter {
152
+ async get(keys) {
153
+ return await chrome.storage.local.get(keys);
154
+ }
155
+ async set(items) {
156
+ await chrome.storage.local.set(items);
157
+ }
158
+ async remove(keys) {
159
+ await chrome.storage.local.remove(keys);
160
+ }
161
+ async clear() {
162
+ await chrome.storage.local.clear();
163
+ }
164
+ onChanged(callback) {
165
+ chrome.storage.onChanged.addListener((changes, areaName) => {
166
+ const mappedChanges = {};
167
+ for (const [key, change] of Object.entries(changes)) {
168
+ mappedChanges[key] = {
169
+ oldValue: change.oldValue,
170
+ newValue: change.newValue
171
+ };
172
+ }
173
+ callback(mappedChanges, areaName);
174
+ });
175
+ }
176
+ }
177
+
178
+ // src/shared/adapters/chrome/tabs.chrome.ts
179
+ class ChromeTabsAdapter {
180
+ async query(queryInfo) {
181
+ return chrome.tabs.query(queryInfo);
182
+ }
183
+ async get(tabId) {
184
+ return chrome.tabs.get(tabId);
185
+ }
186
+ async sendMessage(tabId, message) {
187
+ return chrome.tabs.sendMessage(tabId, message);
188
+ }
189
+ async reload(tabId, reloadProperties) {
190
+ await chrome.tabs.reload(tabId, reloadProperties);
191
+ }
192
+ onRemoved(callback) {
193
+ chrome.tabs.onRemoved.addListener(callback);
194
+ }
195
+ onUpdated(callback) {
196
+ chrome.tabs.onUpdated.addListener(callback);
197
+ }
198
+ onActivated(callback) {
199
+ chrome.tabs.onActivated.addListener(callback);
200
+ }
201
+ async create(createProperties) {
202
+ return chrome.tabs.create(createProperties);
203
+ }
204
+ }
205
+
206
+ // src/shared/adapters/chrome/window.chrome.ts
207
+ class ChromeWindowAdapter {
208
+ postMessage(message, targetOrigin) {
209
+ window.postMessage(message, targetOrigin);
210
+ }
211
+ addEventListener(type, listener) {
212
+ window.addEventListener(type, listener);
213
+ }
214
+ removeEventListener(type, listener) {
215
+ window.removeEventListener(type, listener);
216
+ }
217
+ }
218
+
219
+ // src/shared/adapters/fetch.adapter.ts
220
+ class BrowserFetchAdapter {
221
+ fetch(input, init) {
222
+ return fetch(input, init);
223
+ }
224
+ }
225
+
226
+ // src/shared/adapters/logger.adapter.ts
227
+ class MessageLoggerAdapter {
228
+ runtime;
229
+ sourceContext;
230
+ options;
231
+ constructor(runtime, sourceContext, options) {
232
+ this.runtime = runtime;
233
+ this.sourceContext = sourceContext;
234
+ this.options = options;
235
+ }
236
+ debug(message, context) {
237
+ this.sendLog("debug", message, context);
238
+ }
239
+ info(message, context) {
240
+ this.sendLog("info", message, context);
241
+ }
242
+ warn(message, context) {
243
+ this.sendLog("warn", message, context);
244
+ }
245
+ error(message, error, context) {
246
+ this.sendLog("error", message, context, error);
247
+ }
248
+ log(level, message, context) {
249
+ this.sendLog(level, message, context);
250
+ }
251
+ sendLog(level, message, context, error) {
252
+ if (this.options?.consoleMirror) {
253
+ const consoleMethod = console[level] || console.log;
254
+ consoleMethod(`[${this.sourceContext}]`, message, context || "", error || "");
255
+ }
256
+ const logMessage = {
257
+ type: "LOG",
258
+ level,
259
+ message,
260
+ context,
261
+ error: error?.message,
262
+ stack: error?.stack,
263
+ source: this.sourceContext,
264
+ timestamp: Date.now()
265
+ };
266
+ this.runtime.sendMessage(logMessage).catch((sendError) => {
267
+ if (this.options?.fallbackToConsole !== false) {
268
+ console[level](`[${this.sourceContext}] ${message}`, context || "", error || "");
269
+ console.warn("Failed to send log message:", sendError);
270
+ }
271
+ });
272
+ }
273
+ }
274
+
275
+ // src/shared/adapters/index.ts
276
+ function createChromeAdapters(context, options) {
277
+ const runtime2 = new ChromeRuntimeAdapter;
278
+ return {
279
+ runtime: runtime2,
280
+ storage: new ChromeStorageAdapter,
281
+ tabs: new ChromeTabsAdapter,
282
+ window: new ChromeWindowAdapter,
283
+ offscreen: new ChromeOffscreenAdapter,
284
+ contextMenus: new ChromeContextMenusAdapter,
285
+ fetch: new BrowserFetchAdapter,
286
+ logger: new MessageLoggerAdapter(runtime2, context, {
287
+ ...options?.consoleMirror !== undefined && { consoleMirror: options.consoleMirror },
288
+ fallbackToConsole: true
289
+ })
290
+ };
291
+ }
292
+
293
+ // src/shared/types/messages.ts
294
+ var ALL_CONTEXTS = [
295
+ "background",
296
+ "content",
297
+ "page",
298
+ "devtools",
299
+ "popup",
300
+ "options",
301
+ "sidepanel",
302
+ "offscreen"
303
+ ];
304
+ var defaultSettings = {
305
+ theme: "auto",
306
+ autoSync: true,
307
+ debugMode: false,
308
+ notifications: true,
309
+ apiEndpoint: "https://api.example.com",
310
+ refreshInterval: 60000
311
+ };
312
+
313
+ // src/shared/lib/errors.ts
314
+ class ExtensionError extends Error {
315
+ code;
316
+ context;
317
+ constructor(message, code, context) {
318
+ super(message);
319
+ this.code = code;
320
+ this.context = context;
321
+ this.name = this.constructor.name;
322
+ Error.captureStackTrace?.(this, this.constructor);
323
+ }
324
+ }
325
+
326
+ class TimeoutError extends ExtensionError {
327
+ timeoutMs;
328
+ constructor(message, timeoutMs, context) {
329
+ super(message, "TIMEOUT_ERROR", { ...context, timeoutMs });
330
+ this.timeoutMs = timeoutMs;
331
+ }
332
+ }
333
+
334
+ class ConnectionError extends ExtensionError {
335
+ constructor(message, context) {
336
+ super(message, "CONNECTION_ERROR", context);
337
+ }
338
+ }
339
+
340
+ class MessageRouterError extends ExtensionError {
341
+ constructor(message, context) {
342
+ super(message, "MESSAGE_ROUTER_ERROR", context);
343
+ }
344
+ }
345
+
346
+ class HandlerError extends ExtensionError {
347
+ messageType;
348
+ constructor(message, messageType, context) {
349
+ super(message, "HANDLER_ERROR", { ...context, messageType });
350
+ this.messageType = messageType;
351
+ }
352
+ }
353
+
354
+ class APIError extends ExtensionError {
355
+ statusCode;
356
+ constructor(message, statusCode, context) {
357
+ super(message, "API_ERROR", { ...context, statusCode });
358
+ this.statusCode = statusCode;
359
+ }
360
+ }
361
+
362
+ class OffscreenError extends ExtensionError {
363
+ constructor(message, context) {
364
+ super(message, "OFFSCREEN_ERROR", context);
365
+ }
366
+ }
367
+
368
+ class ErrorHandler {
369
+ logger;
370
+ constructor(logger2) {
371
+ this.logger = logger2;
372
+ }
373
+ throw(error) {
374
+ this.logger.error(error.message, error, error.context);
375
+ throw error;
376
+ }
377
+ reject(error) {
378
+ this.logger.error(error.message, error, error.context);
379
+ return error;
380
+ }
381
+ wrap(error, message, code, context) {
382
+ const originalError = error instanceof Error ? error : new Error(String(error));
383
+ const wrappedError = new ExtensionError(`${message}: ${originalError.message}`, code, {
384
+ ...context,
385
+ originalError: originalError.message,
386
+ originalStack: originalError.stack
387
+ });
388
+ if (originalError.stack) {
389
+ wrappedError.stack = originalError.stack;
390
+ }
391
+ this.logger.error(wrappedError.message, wrappedError, wrappedError.context);
392
+ return wrappedError;
393
+ }
394
+ }
395
+
396
+ // src/shared/lib/context-specific-helpers.ts
397
+ function createContentScriptHelpers() {
398
+ return {
399
+ getPageInfo() {
400
+ return {
401
+ url: window.location.href,
402
+ title: document.title,
403
+ host: window.location.host,
404
+ pathname: window.location.pathname,
405
+ readyState: document.readyState
406
+ };
407
+ },
408
+ queryElements(selector) {
409
+ const elements = document.querySelectorAll(selector);
410
+ return Array.from(elements).map((el) => ({
411
+ tagName: el.tagName,
412
+ id: el.id,
413
+ className: el.className,
414
+ textContent: el.textContent?.slice(0, 100) || ""
415
+ }));
416
+ },
417
+ getPageMetadata() {
418
+ const metadata = {};
419
+ const metaTags = document.querySelectorAll("meta");
420
+ for (const tag of Array.from(metaTags)) {
421
+ const name = tag.getAttribute("name") || tag.getAttribute("property");
422
+ const content = tag.getAttribute("content");
423
+ if (name && content) {
424
+ metadata[name] = content;
425
+ }
426
+ }
427
+ return metadata;
428
+ },
429
+ injectCSS(css) {
430
+ const styleId = `ext-injected-${Date.now()}`;
431
+ const style = document.createElement("style");
432
+ style.id = styleId;
433
+ style.textContent = css;
434
+ document.head.appendChild(style);
435
+ },
436
+ removeCSS(styleId) {
437
+ const style = document.getElementById(styleId);
438
+ if (style) {
439
+ style.remove();
440
+ }
441
+ }
442
+ };
443
+ }
444
+ function createDevToolsHelpers() {
445
+ return {
446
+ get inspectedTabId() {
447
+ return chrome.devtools?.inspectedWindow?.tabId;
448
+ },
449
+ evalInPage(code) {
450
+ return new Promise((resolve, reject) => {
451
+ if (!chrome.devtools?.inspectedWindow) {
452
+ reject(new Error("DevTools inspectedWindow not available"));
453
+ return;
454
+ }
455
+ chrome.devtools.inspectedWindow.eval(code, (result, error) => {
456
+ if (error) {
457
+ reject(new Error(error.isException ? error.value : "Execution error"));
458
+ } else {
459
+ resolve(result);
460
+ }
461
+ });
462
+ });
463
+ },
464
+ getPageResource(url) {
465
+ return new Promise((resolve, reject) => {
466
+ if (!chrome.devtools?.inspectedWindow) {
467
+ reject(new Error("DevTools inspectedWindow not available"));
468
+ return;
469
+ }
470
+ chrome.devtools.inspectedWindow.getResources((resources) => {
471
+ const resource = resources.find((r) => r.url === url);
472
+ if (!resource) {
473
+ reject(new Error(`Resource not found: ${url}`));
474
+ return;
475
+ }
476
+ resource.getContent((content, encoding) => {
477
+ if (encoding === "base64") {
478
+ resolve(atob(content));
479
+ } else {
480
+ resolve(content);
481
+ }
482
+ });
483
+ });
484
+ });
485
+ },
486
+ reloadInspectedPage(options = {}) {
487
+ if (!chrome.devtools?.inspectedWindow) {
488
+ console.warn("DevTools inspectedWindow not available");
489
+ return;
490
+ }
491
+ chrome.devtools.inspectedWindow.reload(options);
492
+ }
493
+ };
494
+ }
495
+ function createPopupHelpers(adapters) {
496
+ return {
497
+ async getCurrentTab() {
498
+ const tabs2 = await adapters.tabs.query({ active: true, currentWindow: true });
499
+ return tabs2[0];
500
+ },
501
+ closePopup() {
502
+ window.close();
503
+ },
504
+ setDimensions(width, height) {
505
+ document.body.style.width = `${width}px`;
506
+ document.body.style.height = `${height}px`;
507
+ }
508
+ };
509
+ }
510
+ function createOptionsHelpers(adapters) {
511
+ return {
512
+ openInNewTab(path) {
513
+ adapters.tabs.create({ url: path });
514
+ },
515
+ showSaveConfirmation(message = "Settings saved!", duration = 3000) {
516
+ const notification = document.createElement("div");
517
+ notification.textContent = message;
518
+ notification.style.cssText = `
519
+ position: fixed;
520
+ top: 20px;
521
+ right: 20px;
522
+ background: #4caf50;
523
+ color: white;
524
+ padding: 12px 24px;
525
+ border-radius: 4px;
526
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
527
+ z-index: 10000;
528
+ animation: slideIn 0.3s ease;
529
+ `;
530
+ document.body.appendChild(notification);
531
+ setTimeout(() => {
532
+ notification.style.animation = "slideOut 0.3s ease";
533
+ setTimeout(() => notification.remove(), 300);
534
+ }, duration);
535
+ },
536
+ showError(message, duration = 5000) {
537
+ const notification = document.createElement("div");
538
+ notification.textContent = message;
539
+ notification.style.cssText = `
540
+ position: fixed;
541
+ top: 20px;
542
+ right: 20px;
543
+ background: #f44336;
544
+ color: white;
545
+ padding: 12px 24px;
546
+ border-radius: 4px;
547
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
548
+ z-index: 10000;
549
+ animation: slideIn 0.3s ease;
550
+ `;
551
+ document.body.appendChild(notification);
552
+ setTimeout(() => {
553
+ notification.style.animation = "slideOut 0.3s ease";
554
+ setTimeout(() => notification.remove(), 300);
555
+ }, duration);
556
+ }
557
+ };
558
+ }
559
+ function createSidePanelHelpers(adapters) {
560
+ return {
561
+ async getCurrentTab() {
562
+ const tabs2 = await adapters.tabs.query({ active: true, currentWindow: true });
563
+ return tabs2[0];
564
+ },
565
+ isVisible() {
566
+ return document.visibilityState === "visible";
567
+ },
568
+ setWidth(width) {
569
+ document.body.style.width = `${width}px`;
570
+ }
571
+ };
572
+ }
573
+ function createBackgroundHelpers(adapters) {
574
+ return {
575
+ async getAllTabs() {
576
+ return adapters.tabs.query({});
577
+ },
578
+ getExtensionViews(type) {
579
+ return chrome.extension.getViews(type ? { type } : undefined);
580
+ },
581
+ openOptionsPage() {
582
+ adapters.runtime.openOptionsPage();
583
+ },
584
+ setBadge(text, color = "#f44336") {
585
+ chrome.action.setBadgeText({ text });
586
+ chrome.action.setBadgeBackgroundColor({ color });
587
+ },
588
+ clearBadge() {
589
+ chrome.action.setBadgeText({ text: "" });
590
+ }
591
+ };
592
+ }
593
+
594
+ // src/shared/lib/handler-execution-tracker.ts
595
+ class HandlerExecutionTracker {
596
+ executions = new Map;
597
+ isDevelopment;
598
+ constructor() {
599
+ this.isDevelopment = typeof process !== "undefined" && (process.env?.NODE_ENV === "development" || process.env?.NODE_ENV === "test");
600
+ }
601
+ track(messageId, handlerType) {
602
+ if (!this.isDevelopment)
603
+ return;
604
+ let handlerCounts = this.executions.get(messageId);
605
+ if (!handlerCounts) {
606
+ handlerCounts = new Map;
607
+ this.executions.set(messageId, handlerCounts);
608
+ }
609
+ const count = (handlerCounts.get(handlerType) || 0) + 1;
610
+ handlerCounts.set(handlerType, count);
611
+ if (count > 1) {
612
+ const error = new Error(`\uD83D\uDD34 DOUBLE EXECUTION DETECTED
613
+
614
+ Handler "${handlerType}" executed ${count} times for message ${messageId}.
615
+
616
+ This indicates multiple chrome.runtime.onMessage listeners are registered.
617
+ Common causes:
618
+ 1. Both MessageBus and MessageRouter registered listeners
619
+ 2. createBackground() called multiple times
620
+ 3. Handler registered in multiple places
621
+
622
+ Fix: Ensure only ONE listener is registered. In background scripts,
623
+ use createBackground() instead of getMessageBus().
624
+ `);
625
+ console.error(error);
626
+ console.error("Execution trace for message:", messageId);
627
+ console.error(Array.from(handlerCounts.entries()));
628
+ throw error;
629
+ }
630
+ setTimeout(() => {
631
+ this.executions.delete(messageId);
632
+ }, 5000);
633
+ }
634
+ reset() {
635
+ this.executions.clear();
636
+ }
637
+ getExecutionCount(messageId, handlerType) {
638
+ return this.executions.get(messageId)?.get(handlerType) || 0;
639
+ }
640
+ }
641
+ var globalExecutionTracker = new HandlerExecutionTracker;
642
+
643
+ // src/shared/lib/message-bus.ts
644
+ function isRoutedMessage(value) {
645
+ if (typeof value !== "object" || value === null)
646
+ return false;
647
+ if (!("id" in value) || !("source" in value) || !("targets" in value) || !("payload" in value)) {
648
+ return false;
649
+ }
650
+ return typeof value.id === "string" && typeof value.source === "string" && Array.isArray(value.targets) && typeof value.payload === "object" && value.payload !== null;
651
+ }
652
+ function isRoutedResponse(value) {
653
+ if (typeof value !== "object" || value === null)
654
+ return false;
655
+ if (!("id" in value) || !("success" in value)) {
656
+ return false;
657
+ }
658
+ return typeof value.id === "string" && typeof value.success === "boolean";
659
+ }
660
+
661
+ class MessageBus {
662
+ context;
663
+ adapters;
664
+ helpers;
665
+ pendingRequests = new Map;
666
+ handlers = new Map;
667
+ port = null;
668
+ errorHandler;
669
+ userErrorHandlers = [];
670
+ messageListener = null;
671
+ constructor(context, adapters, options) {
672
+ this.context = context;
673
+ this.adapters = adapters || createChromeAdapters(context);
674
+ this.errorHandler = new ErrorHandler(this.adapters.logger);
675
+ this.helpers = this.createContextHelpers();
676
+ if (!options?.skipListenerSetup) {
677
+ this.setupListeners();
678
+ }
679
+ }
680
+ async send(payload, options) {
681
+ const id = crypto.randomUUID();
682
+ let targets;
683
+ if (options?.target) {
684
+ if (Array.isArray(options.target)) {
685
+ targets = options.target;
686
+ } else {
687
+ targets = [options.target];
688
+ }
689
+ } else {
690
+ const inferredTarget = this.inferTarget(payload.type);
691
+ if (!inferredTarget) {
692
+ throw new Error(`Message type "${payload.type}" is not a framework message. Please provide explicit 'target' option.`);
693
+ }
694
+ targets = Array.isArray(inferredTarget) ? inferredTarget : [inferredTarget];
695
+ }
696
+ const message = {
697
+ id,
698
+ source: this.context,
699
+ targets,
700
+ ...options?.tabId !== undefined && { tabId: options.tabId },
701
+ timestamp: Date.now(),
702
+ payload
703
+ };
704
+ return new Promise((resolve, reject) => {
705
+ const timeoutMs = options?.timeout || 5000;
706
+ const timeout = setTimeout(() => {
707
+ this.pendingRequests.delete(id);
708
+ const error = new TimeoutError(`Message timeout: ${payload.type}`, timeoutMs, {
709
+ messageType: payload.type,
710
+ targets
711
+ });
712
+ this.notifyErrorHandlers(error);
713
+ reject(this.errorHandler.reject(error));
714
+ }, timeoutMs);
715
+ this.pendingRequests.set(id, {
716
+ resolve: (value) => {
717
+ clearTimeout(timeout);
718
+ resolve(value);
719
+ },
720
+ reject: (error) => {
721
+ clearTimeout(timeout);
722
+ reject(error);
723
+ },
724
+ timestamp: Date.now(),
725
+ timeout
726
+ });
727
+ this.sendMessage(message);
728
+ });
729
+ }
730
+ broadcast(payload) {
731
+ const message = {
732
+ id: crypto.randomUUID(),
733
+ source: this.context,
734
+ targets: ALL_CONTEXTS,
735
+ timestamp: Date.now(),
736
+ payload
737
+ };
738
+ this.sendMessage(message);
739
+ }
740
+ on(type, handler) {
741
+ const existing = this.handlers.get(type) || [];
742
+ existing.push(handler);
743
+ this.handlers.set(type, existing);
744
+ }
745
+ registerHandlers(handlers) {
746
+ for (const [type, handler] of Object.entries(handlers)) {
747
+ if (handler) {
748
+ const existing = this.handlers.get(type) || [];
749
+ existing.push(handler);
750
+ this.handlers.set(type, existing);
751
+ }
752
+ }
753
+ }
754
+ onError(handler) {
755
+ this.userErrorHandlers.push(handler);
756
+ }
757
+ async sendToBackground(payload, options) {
758
+ return this.send(payload, { ...options, target: "background" });
759
+ }
760
+ async sendToContentScript(tabId, payload, options) {
761
+ return this.send(payload, { ...options, target: "content", tabId });
762
+ }
763
+ async sendToAllTabs(payload, options) {
764
+ const tabs2 = await this.adapters.tabs.query({});
765
+ return Promise.all(tabs2.map((tab) => tab.id ? this.sendToContentScript(tab.id, payload, options) : Promise.resolve(undefined)));
766
+ }
767
+ async sendToPopup(payload, options) {
768
+ return this.send(payload, { ...options, target: "popup" });
769
+ }
770
+ async sendToOptions(payload, options) {
771
+ return this.send(payload, { ...options, target: "options" });
772
+ }
773
+ async sendToDevTools(payload, options) {
774
+ return this.send(payload, { ...options, target: "devtools" });
775
+ }
776
+ async sendToSidePanel(payload, options) {
777
+ return this.send(payload, { ...options, target: "sidepanel" });
778
+ }
779
+ connect(name) {
780
+ if (this.port) {
781
+ console.warn(`[${this.context}] Port already connected: ${this.port.name}`);
782
+ return;
783
+ }
784
+ this.port = this.adapters.runtime.connect(name);
785
+ this.port.onMessage((message) => {
786
+ if (isRoutedMessage(message) || isRoutedResponse(message)) {
787
+ this.handleMessage(message);
788
+ }
789
+ });
790
+ this.port.onDisconnect(() => {
791
+ this.adapters.logger.warn("Port disconnected", {
792
+ context: this.context,
793
+ portName: name
794
+ });
795
+ this.port = null;
796
+ for (const [id, pending] of this.pendingRequests.entries()) {
797
+ const error = new ConnectionError("Port disconnected", {
798
+ context: this.context,
799
+ portName: name,
800
+ requestId: id
801
+ });
802
+ this.notifyErrorHandlers(error);
803
+ pending.reject(this.errorHandler.reject(error));
804
+ clearTimeout(pending.timeout);
805
+ this.pendingRequests.delete(id);
806
+ }
807
+ });
808
+ }
809
+ disconnect() {
810
+ if (this.port) {
811
+ this.port.disconnect();
812
+ this.port = null;
813
+ }
814
+ }
815
+ destroy() {
816
+ this.disconnect();
817
+ this.handlers.clear();
818
+ for (const pending of this.pendingRequests.values()) {
819
+ clearTimeout(pending.timeout);
820
+ }
821
+ this.pendingRequests.clear();
822
+ if (this.messageListener) {
823
+ this.adapters.runtime.removeMessageListener(this.messageListener);
824
+ }
825
+ }
826
+ setupListeners() {
827
+ this.messageListener = (message, sender, sendResponse) => {
828
+ if (isRoutedMessage(message) || isRoutedResponse(message)) {
829
+ this.handleMessage(message, sender).then((response) => sendResponse(response)).catch((error) => {
830
+ sendResponse({ success: false, error: error.message });
831
+ });
832
+ }
833
+ return true;
834
+ };
835
+ this.adapters.runtime.onMessage(this.messageListener);
836
+ if (this.context === "content" || this.context === "page") {
837
+ this.adapters.window.addEventListener("message", (event) => {
838
+ if (event.source !== window)
839
+ return;
840
+ if (event.data?.__extensionMessage) {
841
+ this.handleMessage(event.data.message);
842
+ }
843
+ });
844
+ }
845
+ }
846
+ async handleMessage(message, _sender) {
847
+ if ("success" in message) {
848
+ const pending = this.pendingRequests.get(message.id);
849
+ if (pending) {
850
+ this.pendingRequests.delete(message.id);
851
+ clearTimeout(pending.timeout);
852
+ if (message.success) {
853
+ pending.resolve(message.data ?? undefined);
854
+ } else {
855
+ const error = new HandlerError(message.error || "Unknown error", "unknown", {
856
+ messageId: message.id
857
+ });
858
+ this.notifyErrorHandlers(error);
859
+ pending.reject(this.errorHandler.reject(error));
860
+ }
861
+ }
862
+ return;
863
+ }
864
+ if (!message.targets.includes(this.context)) {
865
+ if (this.context === "background") {
866
+ return;
867
+ }
868
+ return;
869
+ }
870
+ const handlers = this.handlers.get(message.payload.type);
871
+ if (!handlers || handlers.length === 0) {
872
+ if (message.targets.length === 1) {
873
+ console.warn(`[${this.context}] No handler for message type: ${message.payload.type}`);
874
+ }
875
+ return { success: false, error: "No handler" };
876
+ }
877
+ if (message.targets.length > 1) {
878
+ try {
879
+ globalExecutionTracker.track(message.id, message.payload.type);
880
+ await Promise.all(handlers.map((handler) => handler(message.payload, message)));
881
+ return { success: true, data: undefined, timestamp: Date.now() };
882
+ } catch (error) {
883
+ return {
884
+ success: false,
885
+ error: error instanceof Error ? error.message : "Unknown error",
886
+ timestamp: Date.now()
887
+ };
888
+ }
889
+ }
890
+ if (message.payload.type === "LOG") {
891
+ try {
892
+ globalExecutionTracker.track(message.id, message.payload.type);
893
+ await Promise.all(handlers.map((handler) => handler(message.payload, message)));
894
+ const response = {
895
+ id: message.id,
896
+ success: true,
897
+ timestamp: Date.now()
898
+ };
899
+ this.sendResponse(message, response);
900
+ return response;
901
+ } catch (error) {
902
+ const response = {
903
+ id: message.id,
904
+ success: false,
905
+ error: error instanceof Error ? error.message : "Unknown error",
906
+ timestamp: Date.now()
907
+ };
908
+ this.sendResponse(message, response);
909
+ return response;
910
+ }
911
+ }
912
+ try {
913
+ globalExecutionTracker.track(message.id, message.payload.type);
914
+ const handler = handlers[0];
915
+ if (!handler) {
916
+ throw new Error(`Handler not found for ${message.payload.type}`);
917
+ }
918
+ const data = await handler(message.payload, message);
919
+ const response = {
920
+ id: message.id,
921
+ success: true,
922
+ data,
923
+ timestamp: Date.now()
924
+ };
925
+ this.sendResponse(message, response);
926
+ return response;
927
+ } catch (error) {
928
+ const response = {
929
+ id: message.id,
930
+ success: false,
931
+ error: error instanceof Error ? error.message : "Unknown error",
932
+ timestamp: Date.now()
933
+ };
934
+ this.sendResponse(message, response);
935
+ return response;
936
+ }
937
+ }
938
+ sendMessage(message) {
939
+ if (this.context === "content" && message.targets.includes("page")) {
940
+ this.adapters.window.postMessage({ __extensionMessage: true, message }, "*");
941
+ } else if (this.context === "page") {
942
+ this.adapters.window.postMessage({ __extensionMessage: true, message }, "*");
943
+ } else if (this.port) {
944
+ this.port.postMessage(message);
945
+ } else {
946
+ this.adapters.runtime.sendMessage(message);
947
+ }
948
+ }
949
+ sendResponse(request, response) {
950
+ if (this.context === "content" && request.source === "page") {
951
+ this.adapters.window.postMessage({ __extensionMessage: true, message: response }, "*");
952
+ } else if (this.context === "page" && request.source === "content") {
953
+ this.adapters.window.postMessage({ __extensionMessage: true, message: response }, "*");
954
+ } else if (this.port && (this.context === "devtools" || this.context === "content")) {
955
+ this.port.postMessage(response);
956
+ } else {
957
+ this.adapters.runtime.sendMessage(response);
958
+ }
959
+ }
960
+ inferTarget(type) {
961
+ const handlers = {
962
+ DOM_QUERY: "content",
963
+ DOM_UPDATE: "content",
964
+ DOM_INSERT: "content",
965
+ DOM_REMOVE: "content",
966
+ PAGE_EVAL: "page",
967
+ PAGE_GET_VAR: "page",
968
+ PAGE_CALL_FN: "page",
969
+ PAGE_SET_VAR: "page",
970
+ API_REQUEST: "background",
971
+ API_BATCH: "background",
972
+ CLIPBOARD_WRITE: "offscreen",
973
+ CLIPBOARD_WRITE_HTML: "offscreen",
974
+ CLIPBOARD_WRITE_RICH: "offscreen",
975
+ CLIPBOARD_READ: "offscreen",
976
+ CONTEXT_MENU_CREATE: "background",
977
+ CONTEXT_MENU_REMOVE: "background",
978
+ STATE_SYNC: ALL_CONTEXTS,
979
+ TAB_QUERY: "background",
980
+ TAB_GET_CURRENT: "background",
981
+ TAB_RELOAD: "background",
982
+ LOG: "background",
983
+ LOGS_GET: "background",
984
+ LOGS_CLEAR: "background",
985
+ LOGS_EXPORT: "background"
986
+ };
987
+ function isHandlerKey(key) {
988
+ return key in handlers;
989
+ }
990
+ if (isHandlerKey(type)) {
991
+ return handlers[type];
992
+ }
993
+ return;
994
+ }
995
+ createContextHelpers() {
996
+ switch (this.context) {
997
+ case "content":
998
+ return createContentScriptHelpers();
999
+ case "devtools":
1000
+ return createDevToolsHelpers();
1001
+ case "popup":
1002
+ return createPopupHelpers(this.adapters);
1003
+ case "options":
1004
+ return createOptionsHelpers(this.adapters);
1005
+ case "sidepanel":
1006
+ return createSidePanelHelpers(this.adapters);
1007
+ case "background":
1008
+ return createBackgroundHelpers(this.adapters);
1009
+ default:
1010
+ return {};
1011
+ }
1012
+ }
1013
+ notifyErrorHandlers(error) {
1014
+ for (const handler of this.userErrorHandlers) {
1015
+ try {
1016
+ handler(error, this);
1017
+ } catch (handlerError) {
1018
+ console.error(`[${this.context}] Error in error handler:`, handlerError);
1019
+ }
1020
+ }
1021
+ }
1022
+ }
1023
+ function getMessageBus(context, adapters, options) {
1024
+ return new MessageBus(context, adapters, options);
1025
+ }
1026
+
1027
+ // src/shared/lib/context-helpers.ts
1028
+ function createContext(context, config = {}) {
1029
+ const {
1030
+ onInit,
1031
+ onError,
1032
+ waitForDOM = true,
1033
+ logPrefix = `[${context.charAt(0).toUpperCase() + context.slice(1)}]`
1034
+ } = config;
1035
+ const bus = getMessageBus(context);
1036
+ if (onError) {
1037
+ bus.onError(onError);
1038
+ }
1039
+ const initialize = async () => {
1040
+ try {
1041
+ if (onInit) {
1042
+ await onInit(bus);
1043
+ }
1044
+ } catch (error) {
1045
+ const err = error instanceof Error ? error : new Error(String(error));
1046
+ console.error(`${logPrefix} Initialization failed:`, err);
1047
+ if (onError) {
1048
+ onError(err, bus);
1049
+ } else {
1050
+ throw err;
1051
+ }
1052
+ }
1053
+ };
1054
+ const contextsWithDOM = ["popup", "options", "devtools", "sidepanel", "content"];
1055
+ if (typeof window !== "undefined" && contextsWithDOM.includes(context) && waitForDOM) {
1056
+ if (document.readyState === "loading") {
1057
+ document.addEventListener("DOMContentLoaded", () => {
1058
+ initialize().catch((err) => {
1059
+ console.error(`${logPrefix} Uncaught initialization error:`, err);
1060
+ });
1061
+ });
1062
+ } else {
1063
+ initialize().catch((err) => {
1064
+ console.error(`${logPrefix} Uncaught initialization error:`, err);
1065
+ });
1066
+ }
1067
+ } else {
1068
+ initialize().catch((err) => {
1069
+ console.error(`${logPrefix} Uncaught initialization error:`, err);
1070
+ });
1071
+ }
1072
+ return bus;
1073
+ }
1074
+ function runInContext(context, contexts, fn) {
1075
+ const targetContexts = Array.isArray(contexts) ? contexts : [contexts];
1076
+ if (targetContexts.includes(context)) {
1077
+ const bus = getMessageBus(context);
1078
+ Promise.resolve(fn(bus)).catch(() => {});
1079
+ }
1080
+ }
1081
+ export {
1082
+ runInContext,
1083
+ createContext
1084
+ };
1085
+
1086
+ //# debugId=255894C34406A3FE64756E2164756E21