@fairfox/polly 0.14.0 → 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 +454 -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 +234 -9
  29. package/dist/tools/verify/src/cli.js.map +4 -4
  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,301 @@ 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;
81
+ return this.dbPromise;
179
82
  }
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);
188
- }
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
166
  }
414
167
  }
415
168
 
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
- }
465
- }
466
-
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
230
  }
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
- }
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
236
  }
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
- }
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
256
  }
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
- }
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
- }
970
- }
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
- }
267
+ isConnected() {
268
+ return typeof chrome !== "undefined" && !!chrome.runtime;
990
269
  }
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;
302
+ console.warn("[SyncAdapter] Failed to broadcast state update:", error);
1110
303
  }
1111
304
  }
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);
1132
- }
1133
- }
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
 
1201
338
  // src/shared/lib/state.ts
1202
339
  import { effect, signal } from "@preact/signals";
1203
340
  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
341
  function $sharedState(key, initialValue, options = {}) {
1235
342
  const sig = createState(key, initialValue, {
1236
343
  ...options,
1237
- sync: true,
1238
- persist: true
344
+ enableSync: true,
345
+ enablePersist: true
1239
346
  });
1240
347
  const entry = stateRegistry.get(key);
1241
348
  if (entry) {
@@ -1246,15 +353,15 @@ function $sharedState(key, initialValue, options = {}) {
1246
353
  function $syncedState(key, initialValue, options = {}) {
1247
354
  return createState(key, initialValue, {
1248
355
  ...options,
1249
- sync: true,
1250
- persist: false
356
+ enableSync: true,
357
+ enablePersist: false
1251
358
  });
1252
359
  }
1253
360
  function $persistedState(key, initialValue, options = {}) {
1254
361
  const sig = createState(key, initialValue, {
1255
362
  ...options,
1256
- sync: false,
1257
- persist: true
363
+ enableSync: false,
364
+ enablePersist: true
1258
365
  });
1259
366
  const entry = stateRegistry.get(key);
1260
367
  if (entry) {
@@ -1284,17 +391,28 @@ function deepEqual(a, b) {
1284
391
  }
1285
392
  return true;
1286
393
  }
394
+ function resolveAdapters(options) {
395
+ if (options.storage || options.sync) {
396
+ return {
397
+ storage: options.storage || (options.enablePersist ? createStorageAdapter() : null),
398
+ sync: options.sync || (options.enableSync ? createSyncAdapter() : null)
399
+ };
400
+ }
401
+ if (options.bus) {
402
+ return {
403
+ storage: options.bus.adapters.storage,
404
+ sync: options.enableSync ? createSyncAdapter() : null
405
+ };
406
+ }
407
+ return {
408
+ storage: options.enablePersist ? createStorageAdapter() : null,
409
+ sync: options.enableSync ? createSyncAdapter() : null
410
+ };
411
+ }
1287
412
  function createState(key, initialValue, options) {
1288
413
  if (stateRegistry.has(key)) {
1289
414
  return stateRegistry.get(key)?.signal;
1290
415
  }
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
416
  const sig = signal(initialValue);
1299
417
  if (options.verify) {
1300
418
  const mirror = JSON.parse(JSON.stringify(initialValue));
@@ -1306,15 +424,9 @@ Use state in the content script instead, or use $state() for local-only state.`)
1306
424
  loaded: Promise.resolve(),
1307
425
  updating: false
1308
426
  };
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);
427
+ const adapters = resolveAdapters(options);
428
+ if (options.enablePersist && adapters.storage) {
429
+ entry.loaded = loadFromStorage(key, sig, entry, adapters.storage, options.validator);
1318
430
  }
1319
431
  entry.loaded.then(() => {
1320
432
  let debounceTimer = null;
@@ -1340,11 +452,11 @@ Use state in the content script instead, or use $state() for local-only state.`)
1340
452
  }
1341
453
  entry.clock++;
1342
454
  const doUpdate = () => {
1343
- if (options.persist) {
1344
- persistToStorage(key, value, entry.clock, getBus());
455
+ if (options.enablePersist && adapters.storage) {
456
+ persistToStorage(key, value, entry.clock, adapters.storage);
1345
457
  }
1346
- if (options.sync) {
1347
- broadcastUpdate(key, value, entry.clock, getBus());
458
+ if (options.enableSync && adapters.sync) {
459
+ broadcastUpdate(key, value, entry.clock, adapters.sync);
1348
460
  }
1349
461
  };
1350
462
  if (options.debounceMs) {
@@ -1356,46 +468,40 @@ Use state in the content script instead, or use $state() for local-only state.`)
1356
468
  }
1357
469
  });
1358
470
  });
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)
471
+ if (options.enableSync && adapters.sync) {
472
+ if (adapters.sync.connect) {
473
+ adapters.sync.connect();
474
+ }
475
+ adapters.sync.onMessage((message) => {
476
+ if (message.key !== key)
477
+ return;
478
+ const oldClock = entry.clock;
479
+ entry.clock = Math.max(entry.clock, message.clock);
480
+ if (message.clock > oldClock) {
481
+ if (options.validator && !options.validator(message.value)) {
482
+ console.warn(`[Polly] State "${key}": Received invalid value from sync (clock: ${message.clock})`, message.value);
1367
483
  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
484
  }
1380
- return;
1381
- });
1382
- }
485
+ if (deepEqual(entry.signal.value, message.value)) {
486
+ return;
487
+ }
488
+ applyUpdate(entry, message.value, message.clock);
489
+ }
490
+ });
1383
491
  }
1384
492
  stateRegistry.set(key, entry);
1385
493
  return sig;
1386
494
  }
1387
- async function loadFromStorage(key, sig, entry, bus, validator) {
1388
- if (!bus)
1389
- return;
495
+ async function loadFromStorage(key, sig, entry, storage, validator) {
1390
496
  try {
1391
- const result = await bus.adapters.storage.get([key, `${key}:clock`]);
497
+ const result = await storage.get([key, `${key}:clock`]);
1392
498
  if (result[key] !== undefined) {
1393
499
  const storedValue = result[key];
1394
500
  if (validator) {
1395
501
  if (validator(storedValue)) {
1396
502
  sig.value = storedValue;
1397
503
  } else {
1398
- console.warn(`[web-ext] State "${key}": Stored value failed validation, using initial value`, storedValue);
504
+ console.warn(`[Polly] State "${key}": Stored value failed validation, using initial value`, storedValue);
1399
505
  }
1400
506
  } else {
1401
507
  sig.value = storedValue;
@@ -1405,33 +511,28 @@ async function loadFromStorage(key, sig, entry, bus, validator) {
1405
511
  entry.clock = result[`${key}:clock`];
1406
512
  }
1407
513
  } catch (error) {
1408
- console.warn(`[web-ext] Failed to load state from storage: ${key}`, error);
514
+ console.warn(`[Polly] Failed to load state from storage: ${key}`, error);
1409
515
  }
1410
516
  }
1411
- function persistToStorage(key, value, clock, bus) {
1412
- if (!bus)
1413
- return;
517
+ function persistToStorage(key, value, clock, storage) {
1414
518
  try {
1415
- bus.adapters.storage.set({
519
+ storage.set({
1416
520
  [key]: value,
1417
521
  [`${key}:clock`]: clock
1418
522
  });
1419
523
  } catch (error) {
1420
- console.warn(`Failed to persist state to storage: ${key}`, error);
524
+ console.warn(`[Polly] Failed to persist state to storage: ${key}`, error);
1421
525
  }
1422
526
  }
1423
- function broadcastUpdate(key, value, clock, bus) {
1424
- if (!bus)
1425
- return;
527
+ function broadcastUpdate(key, value, clock, sync) {
1426
528
  try {
1427
- bus.broadcast({
1428
- type: "STATE_SYNC",
529
+ sync.broadcast({
1429
530
  key,
1430
531
  value,
1431
532
  clock
1432
533
  });
1433
534
  } catch (error) {
1434
- console.warn(`Failed to broadcast state update: ${key}`, error);
535
+ console.warn(`[Polly] Failed to broadcast state update: ${key}`, error);
1435
536
  }
1436
537
  }
1437
538
  function applyUpdate(entry, value, clock) {
@@ -1456,4 +557,4 @@ export {
1456
557
  $persistedState
1457
558
  };
1458
559
 
1459
- //# debugId=30E85DBFA071F76364756E2164756E21
560
+ //# debugId=44A9C265AE724F4564756E2164756E21