@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,1265 @@
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/state.ts
1028
+ import { effect, signal } from "@preact/signals";
1029
+ var stateRegistry = new Map;
1030
+ function getCurrentContext() {
1031
+ if (typeof chrome !== "undefined") {
1032
+ if (typeof chrome.devtools !== "undefined") {
1033
+ return "devtools";
1034
+ }
1035
+ if (typeof chrome.runtime !== "undefined") {
1036
+ try {
1037
+ if (typeof self !== "undefined" && "ServiceWorkerGlobalScope" in self) {
1038
+ return "background";
1039
+ }
1040
+ } catch {}
1041
+ }
1042
+ }
1043
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
1044
+ if (typeof chrome === "undefined" || typeof chrome.runtime === "undefined") {
1045
+ return "page";
1046
+ }
1047
+ if (typeof window.location !== "undefined" && window.location.protocol === "chrome-extension:") {
1048
+ const path = window.location.pathname;
1049
+ if (path.includes("/popup"))
1050
+ return "popup";
1051
+ if (path.includes("/options"))
1052
+ return "options";
1053
+ if (path.includes("/offscreen"))
1054
+ return "offscreen";
1055
+ }
1056
+ return "content";
1057
+ }
1058
+ return "background";
1059
+ }
1060
+ function $sharedState(key, initialValue, options = {}) {
1061
+ return createState(key, initialValue, {
1062
+ ...options,
1063
+ sync: true,
1064
+ persist: true
1065
+ });
1066
+ }
1067
+ function $syncedState(key, initialValue, options = {}) {
1068
+ return createState(key, initialValue, {
1069
+ ...options,
1070
+ sync: true,
1071
+ persist: false
1072
+ });
1073
+ }
1074
+ function $persistedState(key, initialValue, options = {}) {
1075
+ return createState(key, initialValue, {
1076
+ ...options,
1077
+ sync: false,
1078
+ persist: true
1079
+ });
1080
+ }
1081
+ function $state(initialValue) {
1082
+ return signal(initialValue);
1083
+ }
1084
+ function deepEqual(a, b) {
1085
+ if (a === b)
1086
+ return true;
1087
+ if (a == null || b == null)
1088
+ return false;
1089
+ if (typeof a !== "object" || typeof b !== "object")
1090
+ return false;
1091
+ const keysA = Object.keys(a);
1092
+ const keysB = Object.keys(b);
1093
+ if (keysA.length !== keysB.length)
1094
+ return false;
1095
+ for (const key of keysA) {
1096
+ if (!keysB.includes(key))
1097
+ return false;
1098
+ if (!deepEqual(a[key], b[key]))
1099
+ return false;
1100
+ }
1101
+ return true;
1102
+ }
1103
+ function createState(key, initialValue, options) {
1104
+ if (stateRegistry.has(key)) {
1105
+ return stateRegistry.get(key)?.signal;
1106
+ }
1107
+ const currentContext = getCurrentContext();
1108
+ if (currentContext === "page" && (options.sync || options.persist)) {
1109
+ const stateFn = options.sync && options.persist ? "$sharedState" : options.persist ? "$persistedState" : "$syncedState";
1110
+ throw new Error(`[web-ext] ${stateFn}() is not available in page context.
1111
+ Page scripts are execution contexts for content scripts and should not maintain state.
1112
+ Use state in the content script instead, or use $state() for local-only state.`);
1113
+ }
1114
+ const sig = signal(initialValue);
1115
+ const entry = {
1116
+ signal: sig,
1117
+ clock: 0,
1118
+ loaded: Promise.resolve(),
1119
+ updating: false
1120
+ };
1121
+ let bus = null;
1122
+ const getBus = () => {
1123
+ if (!bus && typeof chrome !== "undefined") {
1124
+ bus = options.bus || getMessageBus(currentContext);
1125
+ }
1126
+ return bus;
1127
+ };
1128
+ if (options.persist) {
1129
+ entry.loaded = loadFromStorage(key, sig, entry, getBus(), options.validator);
1130
+ }
1131
+ entry.loaded.then(() => {
1132
+ let debounceTimer = null;
1133
+ let previousValue = sig.value;
1134
+ let isFirstRun = true;
1135
+ effect(() => {
1136
+ if (entry.updating)
1137
+ return;
1138
+ const value = sig.value;
1139
+ if (isFirstRun) {
1140
+ isFirstRun = false;
1141
+ return;
1142
+ }
1143
+ if (deepEqual(value, previousValue)) {
1144
+ return;
1145
+ }
1146
+ previousValue = value;
1147
+ entry.clock++;
1148
+ const doUpdate = () => {
1149
+ if (options.persist) {
1150
+ persistToStorage(key, value, entry.clock, getBus());
1151
+ }
1152
+ if (options.sync) {
1153
+ broadcastUpdate(key, value, entry.clock, getBus());
1154
+ }
1155
+ };
1156
+ if (options.debounceMs) {
1157
+ if (debounceTimer)
1158
+ clearTimeout(debounceTimer);
1159
+ debounceTimer = setTimeout(doUpdate, options.debounceMs);
1160
+ } else {
1161
+ doUpdate();
1162
+ }
1163
+ });
1164
+ });
1165
+ if (options.sync) {
1166
+ const messageBus = getBus();
1167
+ if (messageBus) {
1168
+ if (currentContext === "popup" || currentContext === "options" || currentContext === "devtools") {
1169
+ messageBus.connect(currentContext);
1170
+ }
1171
+ messageBus.on("STATE_SYNC", async (payload) => {
1172
+ if (payload.key !== key)
1173
+ return;
1174
+ const oldClock = entry.clock;
1175
+ entry.clock = Math.max(entry.clock, payload.clock);
1176
+ if (payload.clock > oldClock) {
1177
+ if (options.validator && !options.validator(payload.value)) {
1178
+ console.warn(`[web-ext] State "${key}": Received invalid value from sync (clock: ${payload.clock})`, payload.value);
1179
+ return;
1180
+ }
1181
+ if (deepEqual(entry.signal.value, payload.value)) {
1182
+ return;
1183
+ }
1184
+ applyUpdate(entry, payload.value, payload.clock);
1185
+ }
1186
+ return;
1187
+ });
1188
+ }
1189
+ }
1190
+ stateRegistry.set(key, entry);
1191
+ return sig;
1192
+ }
1193
+ async function loadFromStorage(key, sig, entry, bus, validator) {
1194
+ if (!bus)
1195
+ return;
1196
+ try {
1197
+ const result = await bus.adapters.storage.get([key, `${key}:clock`]);
1198
+ if (result[key] !== undefined) {
1199
+ const storedValue = result[key];
1200
+ if (validator) {
1201
+ if (validator(storedValue)) {
1202
+ sig.value = storedValue;
1203
+ } else {
1204
+ console.warn(`[web-ext] State "${key}": Stored value failed validation, using initial value`, storedValue);
1205
+ }
1206
+ } else {
1207
+ sig.value = storedValue;
1208
+ }
1209
+ }
1210
+ if (result[`${key}:clock`] !== undefined) {
1211
+ entry.clock = result[`${key}:clock`];
1212
+ }
1213
+ } catch (error) {
1214
+ console.warn(`[web-ext] Failed to load state from storage: ${key}`, error);
1215
+ }
1216
+ }
1217
+ function persistToStorage(key, value, clock, bus) {
1218
+ if (!bus)
1219
+ return;
1220
+ try {
1221
+ bus.adapters.storage.set({
1222
+ [key]: value,
1223
+ [`${key}:clock`]: clock
1224
+ });
1225
+ } catch (error) {
1226
+ console.warn(`Failed to persist state to storage: ${key}`, error);
1227
+ }
1228
+ }
1229
+ function broadcastUpdate(key, value, clock, bus) {
1230
+ if (!bus)
1231
+ return;
1232
+ try {
1233
+ bus.broadcast({
1234
+ type: "STATE_SYNC",
1235
+ key,
1236
+ value,
1237
+ clock
1238
+ });
1239
+ } catch (error) {
1240
+ console.warn(`Failed to broadcast state update: ${key}`, error);
1241
+ }
1242
+ }
1243
+ function applyUpdate(entry, value, clock) {
1244
+ entry.updating = true;
1245
+ entry.signal.value = value;
1246
+ entry.clock = clock;
1247
+ entry.updating = false;
1248
+ }
1249
+ function getStateByKey(key) {
1250
+ const entry = stateRegistry.get(key);
1251
+ return entry?.signal;
1252
+ }
1253
+ function clearStateRegistry() {
1254
+ stateRegistry.clear();
1255
+ }
1256
+ export {
1257
+ getStateByKey,
1258
+ clearStateRegistry,
1259
+ $syncedState,
1260
+ $state,
1261
+ $sharedState,
1262
+ $persistedState
1263
+ };
1264
+
1265
+ //# debugId=054D660650837F1D64756E2164756E21