@fairfox/polly 0.14.1 → 0.15.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 (32) hide show
  1. package/dist/src/background/index.js +342 -3
  2. package/dist/src/background/index.js.map +7 -4
  3. package/dist/src/background/message-router.js +342 -3
  4. package/dist/src/background/message-router.js.map +7 -4
  5. package/dist/src/index.js +402 -99
  6. package/dist/src/index.js.map +8 -5
  7. package/dist/src/shared/adapters/index.d.ts +3 -0
  8. package/dist/src/shared/adapters/index.js +356 -4
  9. package/dist/src/shared/adapters/index.js.map +7 -4
  10. package/dist/src/shared/lib/adapter-factory.d.ts +80 -0
  11. package/dist/src/shared/lib/context-helpers.js +342 -3
  12. package/dist/src/shared/lib/context-helpers.js.map +7 -4
  13. package/dist/src/shared/lib/message-bus.js +342 -3
  14. package/dist/src/shared/lib/message-bus.js.map +7 -4
  15. package/dist/src/shared/lib/state.d.ts +5 -1
  16. package/dist/src/shared/lib/state.js +274 -1173
  17. package/dist/src/shared/lib/state.js.map +6 -19
  18. package/dist/src/shared/lib/storage-adapter.d.ts +42 -0
  19. package/dist/src/shared/lib/sync-adapter.d.ts +79 -0
  20. package/dist/src/shared/state/app-state.js +294 -1173
  21. package/dist/src/shared/state/app-state.js.map +6 -18
  22. package/dist/tools/analysis/src/extract/handlers.d.ts +48 -1
  23. package/dist/tools/analysis/src/types/core.d.ts +20 -0
  24. package/dist/tools/teach/src/cli.js +376 -7
  25. package/dist/tools/teach/src/cli.js.map +4 -4
  26. package/dist/tools/teach/src/index.js +232 -7
  27. package/dist/tools/teach/src/index.js.map +3 -3
  28. package/dist/tools/verify/src/cli.js +232 -7
  29. package/dist/tools/verify/src/cli.js.map +3 -3
  30. package/dist/tools/visualize/src/cli.js +232 -7
  31. package/dist/tools/visualize/src/cli.js.map +3 -3
  32. package/package.json +1 -1
@@ -48,1194 +48,321 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
48
48
  throw Error('Dynamic require of "' + x + '" is not supported');
49
49
  });
50
50
 
51
- // src/shared/lib/constraints.ts
52
- var exports_constraints = {};
53
- __export(exports_constraints, {
54
- registerConstraints: () => registerConstraints,
55
- registerConstraint: () => registerConstraint,
56
- isRuntimeConstraintsEnabled: () => isRuntimeConstraintsEnabled,
57
- getRegisteredConstraints: () => getRegisteredConstraints,
58
- clearConstraints: () => clearConstraints,
59
- checkPreconditions: () => checkPreconditions,
60
- checkPostconditions: () => checkPostconditions
61
- });
62
- function registerConstraint(field, messageType, constraint) {
63
- if (!registry.has(field)) {
64
- registry.set(field, new Map);
65
- }
66
- registry.get(field)?.set(messageType, constraint);
67
- }
68
- function registerConstraints(field, constraints) {
69
- for (const [messageType, constraint] of Object.entries(constraints)) {
70
- const runtimeConstraint = {
71
- message: constraint.message
72
- };
73
- if (typeof constraint.requires === "function") {
74
- runtimeConstraint.requires = constraint.requires;
75
- }
76
- if (typeof constraint.ensures === "function") {
77
- runtimeConstraint.ensures = constraint.ensures;
78
- }
79
- if (runtimeConstraint.requires || runtimeConstraint.ensures) {
80
- registerConstraint(field, messageType, runtimeConstraint);
81
- }
82
- }
83
- }
84
- function executeConstraint(predicate, state, messageType, field, customMessage, constraintType = "Precondition") {
85
- try {
86
- const result = predicate(state);
87
- if (!result) {
88
- const message = customMessage || `${constraintType} failed for ${messageType} on field ${field}`;
89
- throw new Error(message);
90
- }
91
- } catch (error) {
92
- if (error instanceof Error && error.message.includes(`${constraintType} failed`)) {
93
- throw error;
94
- }
95
- const message = customMessage || `${constraintType} check error for ${messageType} on field ${field}`;
96
- throw new Error(`${message}: ${error}`);
97
- }
98
- }
99
- function checkPreconditions(messageType, state) {
100
- for (const [field, constraints] of registry) {
101
- const constraint = constraints.get(messageType);
102
- if (constraint?.requires) {
103
- executeConstraint(constraint.requires, state, messageType, field, constraint.message, "Precondition");
104
- }
105
- }
106
- }
107
- function checkPostconditions(messageType, state) {
108
- for (const [field, constraints] of registry) {
109
- const constraint = constraints.get(messageType);
110
- if (constraint?.ensures) {
111
- executeConstraint(constraint.ensures, state, messageType, field, constraint.message, "Postcondition");
112
- }
113
- }
114
- }
115
- function clearConstraints() {
116
- registry.clear();
117
- }
118
- function getRegisteredConstraints() {
119
- return new Map(registry);
120
- }
121
- function isRuntimeConstraintsEnabled() {
122
- if (typeof process !== "undefined" && process.env) {
123
- return process.env["POLLY_RUNTIME_CONSTRAINTS"] === "true";
124
- }
125
- if (typeof Bun !== "undefined" && Bun.env) {
126
- return Bun.env["POLLY_RUNTIME_CONSTRAINTS"] === "true";
127
- }
128
- return false;
129
- }
130
- var registry;
131
- var init_constraints = __esm(() => {
132
- registry = new Map;
51
+ // src/shared/lib/storage-adapter.ts
52
+ var exports_storage_adapter = {};
53
+ __export(exports_storage_adapter, {
54
+ createStorageAdapter: () => createStorageAdapter,
55
+ MemoryStorageAdapter: () => MemoryStorageAdapter,
56
+ IndexedDBAdapter: () => IndexedDBAdapter,
57
+ ChromeStorageAdapter: () => ChromeStorageAdapter
133
58
  });
134
59
 
135
- // src/shared/adapters/chrome/context-menus.chrome.ts
136
- class ChromeContextMenusAdapter {
137
- async create(createProperties) {
138
- return new Promise((resolve, reject) => {
139
- chrome.contextMenus.create(createProperties, () => {
140
- if (chrome.runtime.lastError) {
141
- reject(new Error(chrome.runtime.lastError.message));
142
- } else {
143
- resolve();
60
+ class IndexedDBAdapter {
61
+ dbName;
62
+ storeName = "state";
63
+ dbPromise = null;
64
+ constructor(dbName = "polly-state") {
65
+ this.dbName = dbName;
66
+ }
67
+ getDB() {
68
+ if (this.dbPromise)
69
+ return this.dbPromise;
70
+ this.dbPromise = new Promise((resolve, reject) => {
71
+ const request = indexedDB.open(this.dbName, 1);
72
+ request.onerror = () => reject(request.error);
73
+ request.onsuccess = () => resolve(request.result);
74
+ request.onupgradeneeded = (event) => {
75
+ const db = event.target.result;
76
+ if (!db.objectStoreNames.contains(this.storeName)) {
77
+ db.createObjectStore(this.storeName);
144
78
  }
145
- });
146
- });
147
- }
148
- async update(id, updateProperties) {
149
- await chrome.contextMenus.update(id, updateProperties);
150
- }
151
- async remove(id) {
152
- await chrome.contextMenus.remove(id);
153
- }
154
- async removeAll() {
155
- await chrome.contextMenus.removeAll();
156
- }
157
- onClicked(callback) {
158
- chrome.contextMenus.onClicked.addListener(callback);
159
- }
160
- }
161
-
162
- // src/shared/adapters/chrome/offscreen.chrome.ts
163
- class ChromeOffscreenAdapter {
164
- async createDocument(parameters) {
165
- await chrome.offscreen.createDocument({
166
- url: parameters.url,
167
- reasons: parameters.reasons,
168
- justification: parameters.justification
169
- });
170
- }
171
- async closeDocument() {
172
- await chrome.offscreen.closeDocument();
173
- }
174
- async hasDocument() {
175
- const existingContexts = await chrome.runtime.getContexts({
176
- contextTypes: ["OFFSCREEN_DOCUMENT"]
79
+ };
177
80
  });
178
- return existingContexts.length > 0;
179
- }
180
- }
181
-
182
- // src/shared/adapters/chrome/runtime.chrome.ts
183
- class ChromeRuntimeAdapter {
184
- messageListeners = new Map;
185
- static listenerCount = 0;
186
- sendMessage(message) {
187
- return chrome.runtime.sendMessage(message);
81
+ return this.dbPromise;
188
82
  }
189
- onMessage(callback) {
190
- const wrappedCallback = (message, sender, sendResponse) => {
191
- const mappedSender = {
192
- ...sender.tab && {
193
- tab: {
194
- id: sender.tab.id ?? 0,
195
- url: sender.tab.url ?? "",
196
- title: sender.tab.title ?? ""
83
+ async get(keys) {
84
+ try {
85
+ const db = await this.getDB();
86
+ const result = {};
87
+ await Promise.all(keys.map((key) => new Promise((resolve, reject) => {
88
+ const transaction = db.transaction([this.storeName], "readonly");
89
+ const store = transaction.objectStore(this.storeName);
90
+ const request = store.get(key);
91
+ request.onerror = () => reject(request.error);
92
+ request.onsuccess = () => {
93
+ if (request.result !== undefined) {
94
+ result[key] = request.result;
197
95
  }
198
- },
199
- ...sender.frameId !== undefined && { frameId: sender.frameId },
200
- ...sender.url && { url: sender.url }
201
- };
202
- return callback(message, mappedSender, sendResponse);
203
- };
204
- this.messageListeners.set(callback, wrappedCallback);
205
- chrome.runtime.onMessage.addListener(wrappedCallback);
206
- ChromeRuntimeAdapter.listenerCount++;
207
- if (ChromeRuntimeAdapter.listenerCount > 1) {
208
- console.warn(`⚠️ WARNING: ${ChromeRuntimeAdapter.listenerCount} chrome.runtime.onMessage listeners registered!
209
-
210
- Multiple listeners will cause message handlers to execute multiple times.
211
- This is usually caused by:
212
- 1. Creating both MessageBus and MessageRouter with separate listeners
213
- 2. Calling createBackground() multiple times
214
- 3. Calling getMessageBus('background') after createBackground()
215
-
216
- Fix: In background scripts, use createBackground() ONCE at startup.
217
- Do not call getMessageBus('background') separately.`);
96
+ resolve();
97
+ };
98
+ })));
99
+ return result;
100
+ } catch (error) {
101
+ console.warn("[Polly] IndexedDB get failed:", error);
102
+ return {};
218
103
  }
219
104
  }
220
- removeMessageListener(callback) {
221
- const wrappedCallback = this.messageListeners.get(callback);
222
- if (wrappedCallback) {
223
- chrome.runtime.onMessage.removeListener(wrappedCallback);
224
- this.messageListeners.delete(callback);
225
- ChromeRuntimeAdapter.listenerCount = Math.max(0, ChromeRuntimeAdapter.listenerCount - 1);
105
+ async set(items) {
106
+ try {
107
+ const db = await this.getDB();
108
+ await Promise.all(Object.entries(items).map(([key, value]) => new Promise((resolve, reject) => {
109
+ const transaction = db.transaction([this.storeName], "readwrite");
110
+ const store = transaction.objectStore(this.storeName);
111
+ const request = store.put(value, key);
112
+ request.onerror = () => reject(request.error);
113
+ request.onsuccess = () => resolve();
114
+ })));
115
+ } catch (error) {
116
+ console.warn("[Polly] IndexedDB set failed:", error);
226
117
  }
227
118
  }
228
- connect(name) {
229
- const port = chrome.runtime.connect({ name });
230
- return new ChromePortAdapter(port);
231
- }
232
- onConnect(callback) {
233
- chrome.runtime.onConnect.addListener((port) => {
234
- callback(new ChromePortAdapter(port));
235
- });
236
- }
237
- getURL(path) {
238
- return chrome.runtime.getURL(path);
239
- }
240
- getId() {
241
- return chrome.runtime.id;
242
- }
243
- openOptionsPage() {
244
- chrome.runtime.openOptionsPage();
245
- }
246
- }
247
-
248
- class ChromePortAdapter {
249
- port;
250
- listeners = {
251
- message: new Set,
252
- disconnect: new Set
253
- };
254
- constructor(port) {
255
- this.port = port;
256
- this.port.onMessage.addListener((message) => {
257
- for (const callback of this.listeners.message) {
258
- callback(message);
259
- }
260
- });
261
- this.port.onDisconnect.addListener(() => {
262
- for (const callback of this.listeners.disconnect) {
263
- callback();
264
- }
265
- });
266
- }
267
- get name() {
268
- return this.port.name;
269
- }
270
- postMessage(message) {
271
- this.port.postMessage(message);
272
- }
273
- onMessage(callback) {
274
- this.listeners.message.add(callback);
275
- }
276
- onDisconnect(callback) {
277
- this.listeners.disconnect.add(callback);
278
- }
279
- disconnect() {
280
- this.port.disconnect();
119
+ async remove(keys) {
120
+ try {
121
+ const db = await this.getDB();
122
+ await Promise.all(keys.map((key) => new Promise((resolve, reject) => {
123
+ const transaction = db.transaction([this.storeName], "readwrite");
124
+ const store = transaction.objectStore(this.storeName);
125
+ const request = store.delete(key);
126
+ request.onerror = () => reject(request.error);
127
+ request.onsuccess = () => resolve();
128
+ })));
129
+ } catch (error) {
130
+ console.warn("[Polly] IndexedDB remove failed:", error);
131
+ }
281
132
  }
282
133
  }
283
134
 
284
- // src/shared/adapters/chrome/storage.chrome.ts
285
135
  class ChromeStorageAdapter {
286
136
  async get(keys) {
287
- if (keys === null) {
288
- return await chrome.storage.local.get();
137
+ if (typeof chrome === "undefined" || !chrome.storage) {
138
+ return {};
139
+ }
140
+ try {
141
+ return await chrome.storage.local.get(keys);
142
+ } catch (error) {
143
+ console.warn("[Polly] Chrome storage get failed:", error);
144
+ return {};
289
145
  }
290
- return await chrome.storage.local.get(keys);
291
146
  }
292
147
  async set(items) {
293
- await chrome.storage.local.set(items);
148
+ if (typeof chrome === "undefined" || !chrome.storage) {
149
+ return;
150
+ }
151
+ try {
152
+ await chrome.storage.local.set(items);
153
+ } catch (error) {
154
+ console.warn("[Polly] Chrome storage set failed:", error);
155
+ }
294
156
  }
295
157
  async remove(keys) {
296
- await chrome.storage.local.remove(keys);
297
- }
298
- async clear() {
299
- await chrome.storage.local.clear();
300
- }
301
- onChanged(callback) {
302
- chrome.storage.onChanged.addListener((changes, areaName) => {
303
- const mappedChanges = {};
304
- for (const [key, change] of Object.entries(changes)) {
305
- mappedChanges[key] = {
306
- oldValue: change.oldValue,
307
- newValue: change.newValue
308
- };
309
- }
310
- callback(mappedChanges, areaName);
311
- });
312
- }
313
- }
314
-
315
- // src/shared/adapters/chrome/tabs.chrome.ts
316
- class ChromeTabsAdapter {
317
- async query(queryInfo) {
318
- return chrome.tabs.query(queryInfo);
319
- }
320
- async get(tabId) {
321
- return chrome.tabs.get(tabId);
322
- }
323
- async sendMessage(tabId, message) {
324
- return chrome.tabs.sendMessage(tabId, message);
325
- }
326
- async reload(tabId, reloadProperties) {
327
- if (reloadProperties) {
328
- await chrome.tabs.reload(tabId, reloadProperties);
329
- } else {
330
- await chrome.tabs.reload(tabId);
158
+ if (typeof chrome === "undefined" || !chrome.storage) {
159
+ return;
331
160
  }
332
- }
333
- onRemoved(callback) {
334
- chrome.tabs.onRemoved.addListener(callback);
335
- }
336
- onUpdated(callback) {
337
- chrome.tabs.onUpdated.addListener(callback);
338
- }
339
- onActivated(callback) {
340
- chrome.tabs.onActivated.addListener(callback);
341
- }
342
- async create(createProperties) {
343
- return chrome.tabs.create(createProperties);
344
- }
345
- }
346
-
347
- // src/shared/adapters/chrome/window.chrome.ts
348
- class ChromeWindowAdapter {
349
- postMessage(message, targetOrigin) {
350
- window.postMessage(message, targetOrigin);
351
- }
352
- addEventListener(type, listener) {
353
- window.addEventListener(type, listener);
354
- }
355
- removeEventListener(type, listener) {
356
- window.removeEventListener(type, listener);
357
- }
358
- }
359
-
360
- // src/shared/adapters/fetch.adapter.ts
361
- class BrowserFetchAdapter {
362
- fetch(input, init) {
363
- return fetch(input, init);
364
- }
365
- }
366
-
367
- // src/shared/adapters/logger.adapter.ts
368
- class MessageLoggerAdapter {
369
- runtime;
370
- sourceContext;
371
- options;
372
- constructor(runtime, sourceContext, options) {
373
- this.runtime = runtime;
374
- this.sourceContext = sourceContext;
375
- this.options = options;
376
- }
377
- debug(message, context) {
378
- this.sendLog("debug", message, context);
379
- }
380
- info(message, context) {
381
- this.sendLog("info", message, context);
382
- }
383
- warn(message, context) {
384
- this.sendLog("warn", message, context);
385
- }
386
- error(message, error, context) {
387
- this.sendLog("error", message, context, error);
388
- }
389
- log(level, message, context) {
390
- this.sendLog(level, message, context);
391
- }
392
- sendLog(level, message, context, error) {
393
- if (this.options?.consoleMirror) {
394
- const consoleMethod = console[level] || console.log;
395
- consoleMethod(`[${this.sourceContext}]`, message, context || "", error || "");
161
+ try {
162
+ await chrome.storage.local.remove(keys);
163
+ } catch (error) {
164
+ console.warn("[Polly] Chrome storage remove failed:", error);
396
165
  }
397
- const logMessage = {
398
- type: "LOG",
399
- level,
400
- message,
401
- context,
402
- error: error?.message,
403
- stack: error?.stack,
404
- source: this.sourceContext,
405
- timestamp: Date.now()
406
- };
407
- this.runtime.sendMessage(logMessage).catch((sendError) => {
408
- if (this.options?.fallbackToConsole !== false) {
409
- console[level](`[${this.sourceContext}] ${message}`, context || "", error || "");
410
- console.warn("Failed to send log message:", sendError);
411
- }
412
- });
413
- }
414
- }
415
-
416
- // src/shared/adapters/index.ts
417
- function createChromeAdapters(context, options) {
418
- const runtime2 = new ChromeRuntimeAdapter;
419
- return {
420
- runtime: runtime2,
421
- storage: new ChromeStorageAdapter,
422
- tabs: new ChromeTabsAdapter,
423
- window: new ChromeWindowAdapter,
424
- offscreen: new ChromeOffscreenAdapter,
425
- contextMenus: new ChromeContextMenusAdapter,
426
- fetch: new BrowserFetchAdapter,
427
- logger: new MessageLoggerAdapter(runtime2, context, {
428
- ...options?.consoleMirror !== undefined && { consoleMirror: options.consoleMirror },
429
- fallbackToConsole: true
430
- })
431
- };
432
- }
433
-
434
- // src/shared/types/messages.ts
435
- var ALL_CONTEXTS = [
436
- "background",
437
- "content",
438
- "page",
439
- "devtools",
440
- "popup",
441
- "options",
442
- "sidepanel",
443
- "offscreen"
444
- ];
445
- var defaultSettings = {
446
- theme: "auto",
447
- autoSync: true,
448
- debugMode: false,
449
- notifications: true,
450
- apiEndpoint: "https://api.example.com",
451
- refreshInterval: 60000
452
- };
453
-
454
- // src/shared/lib/errors.ts
455
- class ExtensionError extends Error {
456
- code;
457
- context;
458
- constructor(message, code, context) {
459
- super(message);
460
- this.code = code;
461
- this.context = context;
462
- this.name = this.constructor.name;
463
- Error.captureStackTrace?.(this, this.constructor);
464
166
  }
465
167
  }
466
168
 
467
- class TimeoutError extends ExtensionError {
468
- timeoutMs;
469
- constructor(message, timeoutMs, context) {
470
- super(message, "TIMEOUT_ERROR", { ...context, timeoutMs });
471
- this.timeoutMs = timeoutMs;
169
+ class MemoryStorageAdapter {
170
+ storage = new Map;
171
+ async get(keys) {
172
+ const result = {};
173
+ for (const key of keys) {
174
+ const value = this.storage.get(key);
175
+ if (value !== undefined) {
176
+ result[key] = value;
177
+ }
178
+ }
179
+ return result;
472
180
  }
473
- }
474
-
475
- class ConnectionError extends ExtensionError {
476
- constructor(message, context) {
477
- super(message, "CONNECTION_ERROR", context);
181
+ async set(items) {
182
+ for (const [key, value] of Object.entries(items)) {
183
+ this.storage.set(key, value);
184
+ }
478
185
  }
479
- }
480
-
481
- class MessageRouterError extends ExtensionError {
482
- constructor(message, context) {
483
- super(message, "MESSAGE_ROUTER_ERROR", context);
186
+ async remove(keys) {
187
+ for (const key of keys) {
188
+ this.storage.delete(key);
189
+ }
484
190
  }
485
191
  }
486
-
487
- class HandlerError extends ExtensionError {
488
- messageType;
489
- constructor(message, messageType, context) {
490
- super(message, "HANDLER_ERROR", { ...context, messageType });
491
- this.messageType = messageType;
192
+ function createStorageAdapter() {
193
+ if (typeof chrome !== "undefined" && chrome.storage && chrome.runtime) {
194
+ return new ChromeStorageAdapter;
492
195
  }
493
- }
494
-
495
- class APIError extends ExtensionError {
496
- statusCode;
497
- constructor(message, statusCode, context) {
498
- super(message, "API_ERROR", { ...context, statusCode });
499
- this.statusCode = statusCode;
196
+ if (typeof indexedDB !== "undefined") {
197
+ return new IndexedDBAdapter;
500
198
  }
199
+ return new MemoryStorageAdapter;
501
200
  }
502
201
 
503
- class OffscreenError extends ExtensionError {
504
- constructor(message, context) {
505
- super(message, "OFFSCREEN_ERROR", context);
506
- }
507
- }
202
+ // src/shared/lib/sync-adapter.ts
203
+ var exports_sync_adapter = {};
204
+ __export(exports_sync_adapter, {
205
+ createSyncAdapter: () => createSyncAdapter,
206
+ NoOpSyncAdapter: () => NoOpSyncAdapter,
207
+ ChromeRuntimeSyncAdapter: () => ChromeRuntimeSyncAdapter,
208
+ BroadcastChannelSyncAdapter: () => BroadcastChannelSyncAdapter
209
+ });
508
210
 
509
- class ErrorHandler {
510
- logger;
511
- constructor(logger2) {
512
- this.logger = logger2;
513
- }
514
- throw(error) {
515
- this.logger.error(error.message, error, error.context);
516
- throw error;
517
- }
518
- reject(error) {
519
- this.logger.error(error.message, error, error.context);
520
- return error;
521
- }
522
- wrap(error, message, code, context) {
523
- const originalError = error instanceof Error ? error : new Error(String(error));
524
- const wrappedError = new ExtensionError(`${message}: ${originalError.message}`, code, {
525
- ...context,
526
- originalError: originalError.message,
527
- originalStack: originalError.stack
528
- });
529
- if (originalError.stack) {
530
- wrappedError.stack = originalError.stack;
531
- }
532
- this.logger.error(wrappedError.message, wrappedError, wrappedError.context);
533
- return wrappedError;
211
+ class NoOpSyncAdapter {
212
+ broadcast(_message) {}
213
+ onMessage(_callback) {
214
+ return () => {};
534
215
  }
535
216
  }
536
217
 
537
- // src/shared/lib/context-specific-helpers.ts
538
- function createContentScriptHelpers() {
539
- return {
540
- getPageInfo() {
541
- return {
542
- url: window.location.href,
543
- title: document.title,
544
- host: window.location.host,
545
- pathname: window.location.pathname,
546
- readyState: document.readyState
547
- };
548
- },
549
- queryElements(selector) {
550
- const elements = document.querySelectorAll(selector);
551
- return Array.from(elements).map((el) => ({
552
- tagName: el.tagName,
553
- id: el.id,
554
- className: el.className,
555
- textContent: el.textContent?.slice(0, 100) || ""
556
- }));
557
- },
558
- getPageMetadata() {
559
- const metadata = {};
560
- const metaTags = document.querySelectorAll("meta");
561
- for (const tag of Array.from(metaTags)) {
562
- const name = tag.getAttribute("name") || tag.getAttribute("property");
563
- const content = tag.getAttribute("content");
564
- if (name && content) {
565
- metadata[name] = content;
566
- }
567
- }
568
- return metadata;
569
- },
570
- injectCSS(css) {
571
- const styleId = `ext-injected-${Date.now()}`;
572
- const style = document.createElement("style");
573
- style.id = styleId;
574
- style.textContent = css;
575
- document.head.appendChild(style);
576
- },
577
- removeCSS(styleId) {
578
- const style = document.getElementById(styleId);
579
- if (style) {
580
- style.remove();
581
- }
582
- }
583
- };
584
- }
585
- function createDevToolsHelpers() {
586
- return {
587
- get inspectedTabId() {
588
- return chrome.devtools?.inspectedWindow?.tabId;
589
- },
590
- evalInPage(code) {
591
- return new Promise((resolve, reject) => {
592
- if (!chrome.devtools?.inspectedWindow) {
593
- reject(new Error("DevTools inspectedWindow not available"));
594
- return;
595
- }
596
- chrome.devtools.inspectedWindow.eval(code, (result, error) => {
597
- if (error) {
598
- reject(new Error(error.isException ? error.value : "Execution error"));
599
- } else {
600
- resolve(result);
601
- }
602
- });
603
- });
604
- },
605
- getPageResource(url) {
606
- return new Promise((resolve, reject) => {
607
- if (!chrome.devtools?.inspectedWindow) {
608
- reject(new Error("DevTools inspectedWindow not available"));
609
- return;
610
- }
611
- chrome.devtools.inspectedWindow.getResources((resources) => {
612
- const resource = resources.find((r) => r.url === url);
613
- if (!resource) {
614
- reject(new Error(`Resource not found: ${url}`));
615
- return;
616
- }
617
- resource.getContent((content, encoding) => {
618
- if (encoding === "base64") {
619
- resolve(atob(content));
620
- } else {
621
- resolve(content);
622
- }
218
+ class ChromeRuntimeSyncAdapter {
219
+ listeners = [];
220
+ port = null;
221
+ constructor() {
222
+ if (typeof chrome !== "undefined" && chrome.runtime) {
223
+ chrome.runtime.onMessage.addListener((message, _sender, _sendResponse) => {
224
+ if (message.type === "STATE_SYNC") {
225
+ this.listeners.forEach((listener) => {
226
+ listener(message);
623
227
  });
624
- });
228
+ }
625
229
  });
626
- },
627
- reloadInspectedPage(options = {}) {
628
- if (!chrome.devtools?.inspectedWindow) {
629
- console.warn("DevTools inspectedWindow not available");
630
- return;
631
- }
632
- chrome.devtools.inspectedWindow.reload(options);
633
- }
634
- };
635
- }
636
- function createPopupHelpers(adapters) {
637
- return {
638
- async getCurrentTab() {
639
- const tabs2 = await adapters.tabs.query({ active: true, currentWindow: true });
640
- return tabs2[0];
641
- },
642
- closePopup() {
643
- window.close();
644
- },
645
- setDimensions(width, height) {
646
- document.body.style.width = `${width}px`;
647
- document.body.style.height = `${height}px`;
648
- }
649
- };
650
- }
651
- function createOptionsHelpers(adapters) {
652
- return {
653
- openInNewTab(path) {
654
- adapters.tabs.create({ url: path });
655
- },
656
- showSaveConfirmation(message = "Settings saved!", duration = 3000) {
657
- const notification = document.createElement("div");
658
- notification.textContent = message;
659
- notification.style.cssText = `
660
- position: fixed;
661
- top: 20px;
662
- right: 20px;
663
- background: #4caf50;
664
- color: white;
665
- padding: 12px 24px;
666
- border-radius: 4px;
667
- box-shadow: 0 2px 8px rgba(0,0,0,0.2);
668
- z-index: 10000;
669
- animation: slideIn 0.3s ease;
670
- `;
671
- document.body.appendChild(notification);
672
- setTimeout(() => {
673
- notification.style.animation = "slideOut 0.3s ease";
674
- setTimeout(() => notification.remove(), 300);
675
- }, duration);
676
- },
677
- showError(message, duration = 5000) {
678
- const notification = document.createElement("div");
679
- notification.textContent = message;
680
- notification.style.cssText = `
681
- position: fixed;
682
- top: 20px;
683
- right: 20px;
684
- background: #f44336;
685
- color: white;
686
- padding: 12px 24px;
687
- border-radius: 4px;
688
- box-shadow: 0 2px 8px rgba(0,0,0,0.2);
689
- z-index: 10000;
690
- animation: slideIn 0.3s ease;
691
- `;
692
- document.body.appendChild(notification);
693
- setTimeout(() => {
694
- notification.style.animation = "slideOut 0.3s ease";
695
- setTimeout(() => notification.remove(), 300);
696
- }, duration);
697
- }
698
- };
699
- }
700
- function createSidePanelHelpers(adapters) {
701
- return {
702
- async getCurrentTab() {
703
- const tabs2 = await adapters.tabs.query({ active: true, currentWindow: true });
704
- return tabs2[0];
705
- },
706
- isVisible() {
707
- return document.visibilityState === "visible";
708
- },
709
- setWidth(width) {
710
- document.body.style.width = `${width}px`;
711
- }
712
- };
713
- }
714
- function createBackgroundHelpers(adapters) {
715
- return {
716
- async getAllTabs() {
717
- return adapters.tabs.query({});
718
- },
719
- getExtensionViews(type) {
720
- return chrome.extension.getViews(type && (type === "popup" || type === "tab") ? { type } : undefined);
721
- },
722
- openOptionsPage() {
723
- adapters.runtime.openOptionsPage();
724
- },
725
- setBadge(text, color = "#f44336") {
726
- chrome.action.setBadgeText({ text });
727
- chrome.action.setBadgeBackgroundColor({ color });
728
- },
729
- clearBadge() {
730
- chrome.action.setBadgeText({ text: "" });
731
230
  }
732
- };
733
- }
734
-
735
- // src/shared/lib/handler-execution-tracker.ts
736
- class HandlerExecutionTracker {
737
- executions = new Map;
738
- isDevelopment;
739
- constructor() {
740
- this.isDevelopment = typeof process !== "undefined" && (process.env?.NODE_ENV === "development" || process.env?.NODE_ENV === "test");
741
231
  }
742
- track(messageId, handlerType) {
743
- if (!this.isDevelopment)
232
+ broadcast(message) {
233
+ if (typeof chrome === "undefined" || !chrome.runtime) {
234
+ console.warn("[SyncAdapter] chrome.runtime not available");
744
235
  return;
745
- let handlerCounts = this.executions.get(messageId);
746
- if (!handlerCounts) {
747
- handlerCounts = new Map;
748
- this.executions.set(messageId, handlerCounts);
749
- }
750
- const count = (handlerCounts.get(handlerType) || 0) + 1;
751
- handlerCounts.set(handlerType, count);
752
- if (count > 1) {
753
- const error = new Error(`\uD83D\uDD34 DOUBLE EXECUTION DETECTED
754
-
755
- Handler "${handlerType}" executed ${count} times for message ${messageId}.
756
-
757
- This indicates multiple chrome.runtime.onMessage listeners are registered.
758
- Common causes:
759
- 1. Both MessageBus and MessageRouter registered listeners
760
- 2. createBackground() called multiple times
761
- 3. Handler registered in multiple places
762
-
763
- Fix: Ensure only ONE listener is registered. In background scripts,
764
- use createBackground() instead of getMessageBus().
765
- `);
766
- console.error(error);
767
- console.error("Execution trace for message:", messageId);
768
- console.error(Array.from(handlerCounts.entries()));
769
- throw error;
770
236
  }
771
- setTimeout(() => {
772
- this.executions.delete(messageId);
773
- }, 5000);
774
- }
775
- reset() {
776
- this.executions.clear();
777
- }
778
- getExecutionCount(messageId, handlerType) {
779
- return this.executions.get(messageId)?.get(handlerType) || 0;
780
- }
781
- }
782
- var globalExecutionTracker = new HandlerExecutionTracker;
783
-
784
- // src/shared/lib/message-bus.ts
785
- function isRoutedMessage(value) {
786
- if (typeof value !== "object" || value === null)
787
- return false;
788
- if (!("id" in value) || !("source" in value) || !("targets" in value) || !("payload" in value)) {
789
- return false;
790
- }
791
- return typeof value.id === "string" && typeof value.source === "string" && Array.isArray(value.targets) && typeof value.payload === "object" && value.payload !== null;
792
- }
793
- function isRoutedResponse(value) {
794
- if (typeof value !== "object" || value === null)
795
- return false;
796
- if (!("id" in value) || !("success" in value)) {
797
- return false;
798
- }
799
- return typeof value.id === "string" && typeof value.success === "boolean";
800
- }
801
-
802
- class MessageBus {
803
- context;
804
- adapters;
805
- helpers;
806
- pendingRequests = new Map;
807
- handlers = new Map;
808
- port = null;
809
- errorHandler;
810
- userErrorHandlers = [];
811
- stateAccessor = null;
812
- messageListener = null;
813
- constructor(context, adapters, options) {
814
- this.context = context;
815
- this.adapters = adapters || createChromeAdapters(context);
816
- this.errorHandler = new ErrorHandler(this.adapters.logger);
817
- this.helpers = this.createContextHelpers();
818
- if (!options?.skipListenerSetup) {
819
- this.setupListeners();
237
+ try {
238
+ chrome.runtime.sendMessage({
239
+ type: "STATE_SYNC",
240
+ key: message.key,
241
+ value: message.value,
242
+ clock: message.clock
243
+ });
244
+ } catch (error) {
245
+ console.warn("[SyncAdapter] Failed to broadcast state update:", error);
820
246
  }
821
247
  }
822
- async send(payload, options) {
823
- const id = crypto.randomUUID();
824
- let targets;
825
- if (options?.target) {
826
- if (Array.isArray(options.target)) {
827
- targets = options.target;
828
- } else {
829
- targets = [options.target];
830
- }
831
- } else {
832
- const inferredTarget = this.inferTarget(payload.type);
833
- if (!inferredTarget) {
834
- throw new Error(`Message type "${payload.type}" is not a framework message. Please provide explicit 'target' option.`);
248
+ onMessage(callback) {
249
+ this.listeners.push(callback);
250
+ return () => {
251
+ const index = this.listeners.indexOf(callback);
252
+ if (index > -1) {
253
+ this.listeners.splice(index, 1);
835
254
  }
836
- targets = Array.isArray(inferredTarget) ? inferredTarget : [inferredTarget];
837
- }
838
- const message = {
839
- id,
840
- source: this.context,
841
- targets,
842
- ...options?.tabId !== undefined && { tabId: options.tabId },
843
- timestamp: Date.now(),
844
- payload
845
- };
846
- return new Promise((resolve, reject) => {
847
- const timeoutMs = options?.timeout || 5000;
848
- const timeout = setTimeout(() => {
849
- this.pendingRequests.delete(id);
850
- const error = new TimeoutError(`Message timeout: ${payload.type}`, timeoutMs, {
851
- messageType: payload.type,
852
- targets
853
- });
854
- this.notifyErrorHandlers(error);
855
- reject(this.errorHandler.reject(error));
856
- }, timeoutMs);
857
- this.pendingRequests.set(id, {
858
- resolve: (value) => {
859
- clearTimeout(timeout);
860
- resolve(value);
861
- },
862
- reject: (error) => {
863
- clearTimeout(timeout);
864
- reject(error);
865
- },
866
- timestamp: Date.now(),
867
- timeout
868
- });
869
- this.sendMessage(message);
870
- });
871
- }
872
- broadcast(payload) {
873
- const message = {
874
- id: crypto.randomUUID(),
875
- source: this.context,
876
- targets: ALL_CONTEXTS,
877
- timestamp: Date.now(),
878
- payload
879
255
  };
880
- this.sendMessage(message);
881
- }
882
- on(type, handler) {
883
- const existing = this.handlers.get(type) || [];
884
- existing.push(handler);
885
- this.handlers.set(type, existing);
886
- }
887
- registerHandlers(handlers) {
888
- for (const [type, handler] of Object.entries(handlers)) {
889
- if (handler) {
890
- const existing = this.handlers.get(type) || [];
891
- existing.push(handler);
892
- this.handlers.set(type, existing);
893
- }
894
- }
895
- }
896
- onError(handler) {
897
- this.userErrorHandlers.push(handler);
898
- }
899
- setStateAccessor(accessor) {
900
- this.stateAccessor = accessor;
901
- }
902
- async sendToBackground(payload, options) {
903
- return this.send(payload, { ...options, target: "background" });
904
- }
905
- async sendToContentScript(tabId, payload, options) {
906
- return this.send(payload, { ...options, target: "content", tabId });
907
- }
908
- async sendToAllTabs(payload, options) {
909
- const tabs2 = await this.adapters.tabs.query({});
910
- return Promise.all(tabs2.map((tab) => tab.id ? this.sendToContentScript(tab.id, payload, options) : Promise.resolve(undefined)));
911
- }
912
- async sendToPopup(payload, options) {
913
- return this.send(payload, { ...options, target: "popup" });
914
- }
915
- async sendToOptions(payload, options) {
916
- return this.send(payload, { ...options, target: "options" });
917
- }
918
- async sendToDevTools(payload, options) {
919
- return this.send(payload, { ...options, target: "devtools" });
920
256
  }
921
- async sendToSidePanel(payload, options) {
922
- return this.send(payload, { ...options, target: "sidepanel" });
923
- }
924
- connect(name) {
925
- if (this.port) {
926
- console.warn(`[${this.context}] Port already connected: ${this.port.name}`);
927
- return;
928
- }
929
- this.port = this.adapters.runtime.connect(name);
930
- this.port.onMessage((message) => {
931
- if (isRoutedMessage(message) || isRoutedResponse(message)) {
932
- this.handleMessage(message);
933
- }
934
- });
935
- this.port.onDisconnect(() => {
936
- this.adapters.logger.warn("Port disconnected", {
937
- context: this.context,
938
- portName: name
939
- });
940
- this.port = null;
941
- for (const [id, pending] of this.pendingRequests.entries()) {
942
- const error = new ConnectionError("Port disconnected", {
943
- context: this.context,
944
- portName: name,
945
- requestId: id
946
- });
947
- this.notifyErrorHandlers(error);
948
- pending.reject(this.errorHandler.reject(error));
949
- clearTimeout(pending.timeout);
950
- this.pendingRequests.delete(id);
951
- }
952
- });
257
+ connect() {
258
+ return Promise.resolve();
953
259
  }
954
260
  disconnect() {
261
+ this.listeners = [];
955
262
  if (this.port) {
956
263
  this.port.disconnect();
957
264
  this.port = null;
958
265
  }
959
266
  }
960
- destroy() {
961
- this.disconnect();
962
- this.handlers.clear();
963
- for (const pending of this.pendingRequests.values()) {
964
- clearTimeout(pending.timeout);
965
- }
966
- this.pendingRequests.clear();
967
- if (this.messageListener) {
968
- this.adapters.runtime.removeMessageListener(this.messageListener);
969
- }
267
+ isConnected() {
268
+ return typeof chrome !== "undefined" && !!chrome.runtime;
970
269
  }
971
- setupListeners() {
972
- this.messageListener = (message, sender, sendResponse) => {
973
- if (isRoutedMessage(message) || isRoutedResponse(message)) {
974
- this.handleMessage(message, sender).then((response) => sendResponse(response)).catch((error) => {
975
- sendResponse({ success: false, error: error.message });
976
- });
977
- }
978
- return true;
979
- };
980
- this.adapters.runtime.onMessage(this.messageListener);
981
- if (this.context === "content" || this.context === "page") {
982
- this.adapters.window.addEventListener("message", (event) => {
983
- if (event.source !== window)
984
- return;
985
- if (event.data?.__extensionMessage) {
986
- this.handleMessage(event.data.message);
987
- }
988
- });
989
- }
990
- }
991
- async handleMessage(message, _sender) {
992
- if ("success" in message) {
993
- const pending = this.pendingRequests.get(message.id);
994
- if (pending) {
995
- this.pendingRequests.delete(message.id);
996
- clearTimeout(pending.timeout);
997
- if (message.success) {
998
- pending.resolve(message.data ?? undefined);
999
- } else {
1000
- const error = new HandlerError(message.error || "Unknown error", "unknown", {
1001
- messageId: message.id
270
+ }
271
+
272
+ class BroadcastChannelSyncAdapter {
273
+ channel = null;
274
+ listeners = [];
275
+ constructor(channelName = "polly-sync") {
276
+ if (typeof BroadcastChannel !== "undefined") {
277
+ this.channel = new BroadcastChannel(channelName);
278
+ this.channel.onmessage = (event) => {
279
+ if (event.data.type === "STATE_SYNC") {
280
+ this.listeners.forEach((listener) => {
281
+ listener(event.data);
1002
282
  });
1003
- this.notifyErrorHandlers(error);
1004
- pending.reject(this.errorHandler.reject(error));
1005
283
  }
1006
- }
1007
- return;
284
+ };
285
+ } else {
286
+ console.warn("[SyncAdapter] BroadcastChannel not available");
1008
287
  }
1009
- if (!message.targets.includes(this.context)) {
1010
- if (this.context === "background") {
1011
- return;
1012
- }
288
+ }
289
+ broadcast(message) {
290
+ if (!this.channel) {
291
+ console.warn("[SyncAdapter] BroadcastChannel not initialized");
1013
292
  return;
1014
293
  }
1015
- const handlers = this.handlers.get(message.payload.type);
1016
- if (!handlers || handlers.length === 0) {
1017
- if (message.targets.length === 1) {
1018
- console.warn(`[${this.context}] No handler for message type: ${message.payload.type}`);
1019
- }
1020
- return { success: false, error: "No handler" };
1021
- }
1022
- if (message.targets.length > 1) {
1023
- try {
1024
- globalExecutionTracker.track(message.id, message.payload.type);
1025
- await Promise.all(handlers.map((handler) => handler(message.payload, message)));
1026
- return { success: true, data: undefined, timestamp: Date.now() };
1027
- } catch (error) {
1028
- return {
1029
- success: false,
1030
- error: error instanceof Error ? error.message : "Unknown error",
1031
- timestamp: Date.now()
1032
- };
1033
- }
1034
- }
1035
- if (message.payload.type === "LOG") {
1036
- try {
1037
- globalExecutionTracker.track(message.id, message.payload.type);
1038
- await Promise.all(handlers.map((handler) => handler(message.payload, message)));
1039
- const response = {
1040
- id: message.id,
1041
- success: true,
1042
- timestamp: Date.now()
1043
- };
1044
- this.sendResponse(message, response);
1045
- return response;
1046
- } catch (error) {
1047
- const response = {
1048
- id: message.id,
1049
- success: false,
1050
- error: error instanceof Error ? error.message : "Unknown error",
1051
- timestamp: Date.now()
1052
- };
1053
- this.sendResponse(message, response);
1054
- return response;
1055
- }
1056
- }
1057
294
  try {
1058
- globalExecutionTracker.track(message.id, message.payload.type);
1059
- if (this.stateAccessor) {
1060
- try {
1061
- const { checkPreconditions: checkPreconditions2, isRuntimeConstraintsEnabled: isRuntimeConstraintsEnabled2 } = await Promise.resolve().then(() => (init_constraints(), exports_constraints));
1062
- if (isRuntimeConstraintsEnabled2()) {
1063
- const currentState = this.stateAccessor();
1064
- checkPreconditions2(message.payload.type, currentState);
1065
- }
1066
- } catch (error) {
1067
- if (error instanceof Error) {
1068
- throw error;
1069
- }
1070
- if (error && typeof error === "object" && "code" in error && error.code === "MODULE_NOT_FOUND") {} else {
1071
- throw error;
1072
- }
1073
- }
1074
- }
1075
- const handler = handlers[0];
1076
- if (!handler) {
1077
- throw new Error(`Handler not found for ${message.payload.type}`);
1078
- }
1079
- const data = await handler(message.payload, message);
1080
- if (this.stateAccessor) {
1081
- try {
1082
- const { checkPostconditions: checkPostconditions2, isRuntimeConstraintsEnabled: isRuntimeConstraintsEnabled2 } = await Promise.resolve().then(() => (init_constraints(), exports_constraints));
1083
- if (isRuntimeConstraintsEnabled2()) {
1084
- const currentState = this.stateAccessor();
1085
- checkPostconditions2(message.payload.type, currentState);
1086
- }
1087
- } catch (error) {
1088
- if (error instanceof Error && error.message.includes("Postcondition")) {
1089
- console.error(`[${this.context}] Postcondition failed:`, error.message);
1090
- }
1091
- }
1092
- }
1093
- const response = {
1094
- id: message.id,
1095
- success: true,
1096
- data,
1097
- timestamp: Date.now()
1098
- };
1099
- this.sendResponse(message, response);
1100
- return response;
295
+ this.channel.postMessage({
296
+ type: "STATE_SYNC",
297
+ key: message.key,
298
+ value: message.value,
299
+ clock: message.clock
300
+ });
1101
301
  } catch (error) {
1102
- const response = {
1103
- id: message.id,
1104
- success: false,
1105
- error: error instanceof Error ? error.message : "Unknown error",
1106
- timestamp: Date.now()
1107
- };
1108
- this.sendResponse(message, response);
1109
- return response;
1110
- }
1111
- }
1112
- sendMessage(message) {
1113
- if (this.context === "content" && message.targets.includes("page")) {
1114
- this.adapters.window.postMessage({ __extensionMessage: true, message }, "*");
1115
- } else if (this.context === "page") {
1116
- this.adapters.window.postMessage({ __extensionMessage: true, message }, "*");
1117
- } else if (this.port) {
1118
- this.port.postMessage(message);
1119
- } else {
1120
- this.adapters.runtime.sendMessage(message);
1121
- }
1122
- }
1123
- sendResponse(request, response) {
1124
- if (this.context === "content" && request.source === "page") {
1125
- this.adapters.window.postMessage({ __extensionMessage: true, message: response }, "*");
1126
- } else if (this.context === "page" && request.source === "content") {
1127
- this.adapters.window.postMessage({ __extensionMessage: true, message: response }, "*");
1128
- } else if (this.port && (this.context === "devtools" || this.context === "content")) {
1129
- this.port.postMessage(response);
1130
- } else {
1131
- this.adapters.runtime.sendMessage(response);
302
+ console.warn("[SyncAdapter] Failed to broadcast state update:", error);
1132
303
  }
1133
304
  }
1134
- inferTarget(type) {
1135
- const handlers = {
1136
- DOM_QUERY: "content",
1137
- DOM_UPDATE: "content",
1138
- DOM_INSERT: "content",
1139
- DOM_REMOVE: "content",
1140
- PAGE_EVAL: "page",
1141
- PAGE_GET_VAR: "page",
1142
- PAGE_CALL_FN: "page",
1143
- PAGE_SET_VAR: "page",
1144
- API_REQUEST: "background",
1145
- API_BATCH: "background",
1146
- CLIPBOARD_WRITE: "offscreen",
1147
- CLIPBOARD_WRITE_HTML: "offscreen",
1148
- CLIPBOARD_WRITE_RICH: "offscreen",
1149
- CLIPBOARD_READ: "offscreen",
1150
- CONTEXT_MENU_CREATE: "background",
1151
- CONTEXT_MENU_REMOVE: "background",
1152
- STATE_SYNC: ALL_CONTEXTS,
1153
- TAB_QUERY: "background",
1154
- TAB_GET_CURRENT: "background",
1155
- TAB_RELOAD: "background",
1156
- LOG: "background",
1157
- LOGS_GET: "background",
1158
- LOGS_CLEAR: "background",
1159
- LOGS_EXPORT: "background"
305
+ onMessage(callback) {
306
+ this.listeners.push(callback);
307
+ return () => {
308
+ const index = this.listeners.indexOf(callback);
309
+ if (index > -1) {
310
+ this.listeners.splice(index, 1);
311
+ }
1160
312
  };
1161
- function isHandlerKey(key) {
1162
- return key in handlers;
1163
- }
1164
- if (isHandlerKey(type)) {
1165
- return handlers[type];
1166
- }
1167
- return;
1168
313
  }
1169
- createContextHelpers() {
1170
- switch (this.context) {
1171
- case "content":
1172
- return createContentScriptHelpers();
1173
- case "devtools":
1174
- return createDevToolsHelpers();
1175
- case "popup":
1176
- return createPopupHelpers(this.adapters);
1177
- case "options":
1178
- return createOptionsHelpers(this.adapters);
1179
- case "sidepanel":
1180
- return createSidePanelHelpers(this.adapters);
1181
- case "background":
1182
- return createBackgroundHelpers(this.adapters);
1183
- default:
1184
- return {};
1185
- }
314
+ connect() {
315
+ return Promise.resolve();
1186
316
  }
1187
- notifyErrorHandlers(error) {
1188
- for (const handler of this.userErrorHandlers) {
1189
- try {
1190
- handler(error, this);
1191
- } catch (handlerError) {
1192
- console.error(`[${this.context}] Error in error handler:`, handlerError);
1193
- }
317
+ disconnect() {
318
+ this.listeners = [];
319
+ if (this.channel) {
320
+ this.channel.close();
321
+ this.channel = null;
1194
322
  }
1195
323
  }
324
+ isConnected() {
325
+ return this.channel !== null;
326
+ }
1196
327
  }
1197
- function getMessageBus(context, adapters, options) {
1198
- return new MessageBus(context, adapters, options);
328
+ function createSyncAdapter() {
329
+ if (typeof chrome !== "undefined" && chrome.runtime) {
330
+ return new ChromeRuntimeSyncAdapter;
331
+ }
332
+ if (typeof BroadcastChannel !== "undefined") {
333
+ return new BroadcastChannelSyncAdapter;
334
+ }
335
+ return new NoOpSyncAdapter;
1199
336
  }
1200
337
 
338
+ // src/shared/types/messages.ts
339
+ var ALL_CONTEXTS = [
340
+ "background",
341
+ "content",
342
+ "page",
343
+ "devtools",
344
+ "popup",
345
+ "options",
346
+ "sidepanel",
347
+ "offscreen"
348
+ ];
349
+ var defaultSettings = {
350
+ theme: "auto",
351
+ autoSync: true,
352
+ debugMode: false,
353
+ notifications: true,
354
+ apiEndpoint: "https://api.example.com",
355
+ refreshInterval: 60000
356
+ };
357
+
1201
358
  // src/shared/lib/state.ts
1202
359
  import { effect, signal } from "@preact/signals";
1203
360
  var stateRegistry = new Map;
1204
- function getCurrentContext() {
1205
- if (typeof chrome !== "undefined") {
1206
- if (typeof chrome.devtools !== "undefined") {
1207
- return "devtools";
1208
- }
1209
- if (typeof chrome.runtime !== "undefined") {
1210
- try {
1211
- if (typeof self !== "undefined" && "ServiceWorkerGlobalScope" in self) {
1212
- return "background";
1213
- }
1214
- } catch {}
1215
- }
1216
- }
1217
- if (typeof window !== "undefined" && typeof document !== "undefined") {
1218
- if (typeof chrome === "undefined" || typeof chrome.runtime === "undefined") {
1219
- return "page";
1220
- }
1221
- if (typeof window.location !== "undefined" && window.location.protocol === "chrome-extension:") {
1222
- const path = window.location.pathname;
1223
- if (path.includes("/popup"))
1224
- return "popup";
1225
- if (path.includes("/options"))
1226
- return "options";
1227
- if (path.includes("/offscreen"))
1228
- return "offscreen";
1229
- }
1230
- return "content";
1231
- }
1232
- return "background";
1233
- }
1234
361
  function $sharedState(key, initialValue, options = {}) {
1235
362
  const sig = createState(key, initialValue, {
1236
363
  ...options,
1237
- sync: true,
1238
- persist: true
364
+ enableSync: true,
365
+ enablePersist: true
1239
366
  });
1240
367
  const entry = stateRegistry.get(key);
1241
368
  if (entry) {
@@ -1246,15 +373,15 @@ function $sharedState(key, initialValue, options = {}) {
1246
373
  function $syncedState(key, initialValue, options = {}) {
1247
374
  return createState(key, initialValue, {
1248
375
  ...options,
1249
- sync: true,
1250
- persist: false
376
+ enableSync: true,
377
+ enablePersist: false
1251
378
  });
1252
379
  }
1253
380
  function $persistedState(key, initialValue, options = {}) {
1254
381
  const sig = createState(key, initialValue, {
1255
382
  ...options,
1256
- sync: false,
1257
- persist: true
383
+ enableSync: false,
384
+ enablePersist: true
1258
385
  });
1259
386
  const entry = stateRegistry.get(key);
1260
387
  if (entry) {
@@ -1284,17 +411,28 @@ function deepEqual(a, b) {
1284
411
  }
1285
412
  return true;
1286
413
  }
414
+ function resolveAdapters(options) {
415
+ if (options.storage || options.sync) {
416
+ return {
417
+ storage: options.storage || (options.enablePersist ? createStorageAdapter() : null),
418
+ sync: options.sync || (options.enableSync ? createSyncAdapter() : null)
419
+ };
420
+ }
421
+ if (options.bus) {
422
+ return {
423
+ storage: options.bus.adapters.storage,
424
+ sync: options.enableSync ? createSyncAdapter() : null
425
+ };
426
+ }
427
+ return {
428
+ storage: options.enablePersist ? createStorageAdapter() : null,
429
+ sync: options.enableSync ? createSyncAdapter() : null
430
+ };
431
+ }
1287
432
  function createState(key, initialValue, options) {
1288
433
  if (stateRegistry.has(key)) {
1289
434
  return stateRegistry.get(key)?.signal;
1290
435
  }
1291
- const currentContext = getCurrentContext();
1292
- if (currentContext === "page" && (options.sync || options.persist)) {
1293
- const stateFn = options.sync && options.persist ? "$sharedState" : options.persist ? "$persistedState" : "$syncedState";
1294
- throw new Error(`[web-ext] ${stateFn}() is not available in page context.
1295
- Page scripts are execution contexts for content scripts and should not maintain state.
1296
- Use state in the content script instead, or use $state() for local-only state.`);
1297
- }
1298
436
  const sig = signal(initialValue);
1299
437
  if (options.verify) {
1300
438
  const mirror = JSON.parse(JSON.stringify(initialValue));
@@ -1306,15 +444,9 @@ Use state in the content script instead, or use $state() for local-only state.`)
1306
444
  loaded: Promise.resolve(),
1307
445
  updating: false
1308
446
  };
1309
- let bus = null;
1310
- const getBus = () => {
1311
- if (!bus && typeof chrome !== "undefined") {
1312
- bus = options.bus || getMessageBus(currentContext);
1313
- }
1314
- return bus;
1315
- };
1316
- if (options.persist) {
1317
- entry.loaded = loadFromStorage(key, sig, entry, getBus(), options.validator);
447
+ const adapters = resolveAdapters(options);
448
+ if (options.enablePersist && adapters.storage) {
449
+ entry.loaded = loadFromStorage(key, sig, entry, adapters.storage, options.validator);
1318
450
  }
1319
451
  entry.loaded.then(() => {
1320
452
  let debounceTimer = null;
@@ -1340,11 +472,11 @@ Use state in the content script instead, or use $state() for local-only state.`)
1340
472
  }
1341
473
  entry.clock++;
1342
474
  const doUpdate = () => {
1343
- if (options.persist) {
1344
- persistToStorage(key, value, entry.clock, getBus());
475
+ if (options.enablePersist && adapters.storage) {
476
+ persistToStorage(key, value, entry.clock, adapters.storage);
1345
477
  }
1346
- if (options.sync) {
1347
- broadcastUpdate(key, value, entry.clock, getBus());
478
+ if (options.enableSync && adapters.sync) {
479
+ broadcastUpdate(key, value, entry.clock, adapters.sync);
1348
480
  }
1349
481
  };
1350
482
  if (options.debounceMs) {
@@ -1356,46 +488,40 @@ Use state in the content script instead, or use $state() for local-only state.`)
1356
488
  }
1357
489
  });
1358
490
  });
1359
- if (options.sync) {
1360
- const messageBus = getBus();
1361
- if (messageBus) {
1362
- if (currentContext === "popup" || currentContext === "options" || currentContext === "devtools") {
1363
- messageBus.connect(currentContext);
1364
- }
1365
- messageBus.on("STATE_SYNC", async (payload) => {
1366
- if (payload.key !== key)
491
+ if (options.enableSync && adapters.sync) {
492
+ if (adapters.sync.connect) {
493
+ adapters.sync.connect();
494
+ }
495
+ adapters.sync.onMessage((message) => {
496
+ if (message.key !== key)
497
+ return;
498
+ const oldClock = entry.clock;
499
+ entry.clock = Math.max(entry.clock, message.clock);
500
+ if (message.clock > oldClock) {
501
+ if (options.validator && !options.validator(message.value)) {
502
+ console.warn(`[Polly] State "${key}": Received invalid value from sync (clock: ${message.clock})`, message.value);
1367
503
  return;
1368
- const oldClock = entry.clock;
1369
- entry.clock = Math.max(entry.clock, payload.clock);
1370
- if (payload.clock > oldClock) {
1371
- if (options.validator && !options.validator(payload.value)) {
1372
- console.warn(`[web-ext] State "${key}": Received invalid value from sync (clock: ${payload.clock})`, payload.value);
1373
- return;
1374
- }
1375
- if (deepEqual(entry.signal.value, payload.value)) {
1376
- return;
1377
- }
1378
- applyUpdate(entry, payload.value, payload.clock);
1379
504
  }
1380
- return;
1381
- });
1382
- }
505
+ if (deepEqual(entry.signal.value, message.value)) {
506
+ return;
507
+ }
508
+ applyUpdate(entry, message.value, message.clock);
509
+ }
510
+ });
1383
511
  }
1384
512
  stateRegistry.set(key, entry);
1385
513
  return sig;
1386
514
  }
1387
- async function loadFromStorage(key, sig, entry, bus, validator) {
1388
- if (!bus)
1389
- return;
515
+ async function loadFromStorage(key, sig, entry, storage, validator) {
1390
516
  try {
1391
- const result = await bus.adapters.storage.get([key, `${key}:clock`]);
517
+ const result = await storage.get([key, `${key}:clock`]);
1392
518
  if (result[key] !== undefined) {
1393
519
  const storedValue = result[key];
1394
520
  if (validator) {
1395
521
  if (validator(storedValue)) {
1396
522
  sig.value = storedValue;
1397
523
  } else {
1398
- console.warn(`[web-ext] State "${key}": Stored value failed validation, using initial value`, storedValue);
524
+ console.warn(`[Polly] State "${key}": Stored value failed validation, using initial value`, storedValue);
1399
525
  }
1400
526
  } else {
1401
527
  sig.value = storedValue;
@@ -1405,33 +531,28 @@ async function loadFromStorage(key, sig, entry, bus, validator) {
1405
531
  entry.clock = result[`${key}:clock`];
1406
532
  }
1407
533
  } catch (error) {
1408
- console.warn(`[web-ext] Failed to load state from storage: ${key}`, error);
534
+ console.warn(`[Polly] Failed to load state from storage: ${key}`, error);
1409
535
  }
1410
536
  }
1411
- function persistToStorage(key, value, clock, bus) {
1412
- if (!bus)
1413
- return;
537
+ function persistToStorage(key, value, clock, storage) {
1414
538
  try {
1415
- bus.adapters.storage.set({
539
+ storage.set({
1416
540
  [key]: value,
1417
541
  [`${key}:clock`]: clock
1418
542
  });
1419
543
  } catch (error) {
1420
- console.warn(`Failed to persist state to storage: ${key}`, error);
544
+ console.warn(`[Polly] Failed to persist state to storage: ${key}`, error);
1421
545
  }
1422
546
  }
1423
- function broadcastUpdate(key, value, clock, bus) {
1424
- if (!bus)
1425
- return;
547
+ function broadcastUpdate(key, value, clock, sync) {
1426
548
  try {
1427
- bus.broadcast({
1428
- type: "STATE_SYNC",
549
+ sync.broadcast({
1429
550
  key,
1430
551
  value,
1431
552
  clock
1432
553
  });
1433
554
  } catch (error) {
1434
- console.warn(`Failed to broadcast state update: ${key}`, error);
555
+ console.warn(`[Polly] Failed to broadcast state update: ${key}`, error);
1435
556
  }
1436
557
  }
1437
558
  function applyUpdate(entry, value, clock) {
@@ -1463,4 +584,4 @@ export {
1463
584
  currentTab
1464
585
  };
1465
586
 
1466
- //# debugId=1ED38906FED5B38C64756E2164756E21
587
+ //# debugId=FBE4935174EC258C64756E2164756E21