@chromahq/core 1.0.52 → 1.0.54

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.
@@ -263,6 +263,139 @@ function getNonceService() {
263
263
  return nonceServiceInstance;
264
264
  }
265
265
 
266
+ const _PopupVisibilityService = class _PopupVisibilityService {
267
+ /**
268
+ * Private constructor - use PopupVisibilityService.instance instead.
269
+ */
270
+ constructor() {
271
+ /** Number of currently connected ports (popup views) */
272
+ this.connectedPortCount = 0;
273
+ /** Listeners for visibility changes */
274
+ this.listeners = /* @__PURE__ */ new Set();
275
+ /** Timestamp of last visibility change */
276
+ this.lastVisibilityChangeAt = 0;
277
+ }
278
+ /**
279
+ * Get the singleton instance of the service.
280
+ */
281
+ static get instance() {
282
+ if (!_PopupVisibilityService._instance) {
283
+ _PopupVisibilityService._instance = new _PopupVisibilityService();
284
+ }
285
+ return _PopupVisibilityService._instance;
286
+ }
287
+ /**
288
+ * Reset the singleton instance (primarily for testing).
289
+ * @internal
290
+ */
291
+ static resetInstance() {
292
+ if (_PopupVisibilityService._instance) {
293
+ _PopupVisibilityService._instance.listeners.clear();
294
+ }
295
+ _PopupVisibilityService._instance = null;
296
+ }
297
+ // ─────────────────────────────────────────────────────────────────────────────
298
+ // Public API
299
+ // ─────────────────────────────────────────────────────────────────────────────
300
+ /**
301
+ * Check if the popup (or any extension view) is currently visible.
302
+ *
303
+ * @returns true if at least one port is connected (popup is open)
304
+ */
305
+ isPopupVisible() {
306
+ return this.connectedPortCount > 0;
307
+ }
308
+ /**
309
+ * Get the number of connected ports.
310
+ *
311
+ * @returns The current count of connected extension views
312
+ */
313
+ getConnectedPortCount() {
314
+ return this.connectedPortCount;
315
+ }
316
+ /**
317
+ * Get the timestamp of the last visibility change.
318
+ *
319
+ * @returns Unix timestamp in milliseconds, or 0 if never changed
320
+ */
321
+ getLastVisibilityChangeAt() {
322
+ return this.lastVisibilityChangeAt;
323
+ }
324
+ /**
325
+ * Register a callback to be notified when visibility changes.
326
+ *
327
+ * @param callback - Function to call when visibility changes
328
+ * @returns Unsubscribe function to remove the listener
329
+ */
330
+ onVisibilityChange(callback) {
331
+ this.listeners.add(callback);
332
+ return () => {
333
+ this.listeners.delete(callback);
334
+ };
335
+ }
336
+ // ─────────────────────────────────────────────────────────────────────────────
337
+ // Internal API (called by BridgeRuntime)
338
+ // ─────────────────────────────────────────────────────────────────────────────
339
+ /**
340
+ * Called when a port connects (popup opens).
341
+ * @internal
342
+ */
343
+ onPortConnected() {
344
+ const wasVisible = this.isPopupVisible();
345
+ this.connectedPortCount++;
346
+ if (!wasVisible && this.isPopupVisible()) {
347
+ this.lastVisibilityChangeAt = Date.now();
348
+ this.notifyListeners(true);
349
+ }
350
+ }
351
+ /**
352
+ * Called when a port disconnects (popup closes).
353
+ * @internal
354
+ */
355
+ onPortDisconnected() {
356
+ const wasVisible = this.isPopupVisible();
357
+ this.connectedPortCount = Math.max(0, this.connectedPortCount - 1);
358
+ if (wasVisible && !this.isPopupVisible()) {
359
+ this.lastVisibilityChangeAt = Date.now();
360
+ this.notifyListeners(false);
361
+ }
362
+ }
363
+ /**
364
+ * Sync the port count with the actual connected ports set.
365
+ * Called by BridgeRuntime to ensure consistency.
366
+ * @internal
367
+ */
368
+ syncPortCount(count) {
369
+ const wasVisible = this.isPopupVisible();
370
+ this.connectedPortCount = count;
371
+ const isNowVisible = this.isPopupVisible();
372
+ if (wasVisible !== isNowVisible) {
373
+ this.lastVisibilityChangeAt = Date.now();
374
+ this.notifyListeners(isNowVisible);
375
+ }
376
+ }
377
+ // ─────────────────────────────────────────────────────────────────────────────
378
+ // Private Methods
379
+ // ─────────────────────────────────────────────────────────────────────────────
380
+ /**
381
+ * Notify all registered listeners of a visibility change.
382
+ */
383
+ notifyListeners(isVisible) {
384
+ this.listeners.forEach((callback) => {
385
+ try {
386
+ callback(isVisible);
387
+ } catch (error) {
388
+ console.error("[PopupVisibilityService] Listener error:", error);
389
+ }
390
+ });
391
+ }
392
+ };
393
+ _PopupVisibilityService._instance = null;
394
+ let PopupVisibilityService = _PopupVisibilityService;
395
+ function getPopupVisibilityService() {
396
+ return PopupVisibilityService.instance;
397
+ }
398
+
266
399
  const DEFAULT_PORT_NAME$1 = "chroma-bridge";
267
400
  const earlyPorts = [];
268
401
  let listenerSetup = false;
@@ -517,6 +650,7 @@ const _BridgeRuntimeManager = class _BridgeRuntimeManager {
517
650
  */
518
651
  setupMessageHandler(port) {
519
652
  this.connectedPorts.add(port);
653
+ PopupVisibilityService.instance.onPortConnected();
520
654
  if (this.keepAlive && this.connectedPorts.size === 1) {
521
655
  this.startKeepAlive();
522
656
  }
@@ -552,6 +686,7 @@ const _BridgeRuntimeManager = class _BridgeRuntimeManager {
552
686
  });
553
687
  port.onDisconnect.addListener(() => {
554
688
  this.connectedPorts.delete(port);
689
+ PopupVisibilityService.instance.onPortDisconnected();
555
690
  const runtimeErrorMessage = chrome.runtime.lastError?.message;
556
691
  if (runtimeErrorMessage) {
557
692
  this.logger.warn(`\u{1F4F4} Port disconnected with error: ${runtimeErrorMessage}`);
@@ -1830,12 +1965,77 @@ class Scheduler {
1830
1965
  this.logger.info("Scheduler initialized");
1831
1966
  this.alarm.onTrigger(this.execute.bind(this));
1832
1967
  this.timeout.onTrigger(this.execute.bind(this));
1968
+ this.setupPopupVisibilityListener();
1969
+ }
1970
+ /**
1971
+ * Setup listener for popup visibility changes.
1972
+ * When popup closes, pause all jobs with requiresPopup.
1973
+ * When popup opens, resume those jobs.
1974
+ */
1975
+ setupPopupVisibilityListener() {
1976
+ const visibilityService = PopupVisibilityService.instance;
1977
+ this.popupVisibilityUnsubscribe = visibilityService.onVisibilityChange((isVisible) => {
1978
+ if (isVisible) {
1979
+ this.resumePopupDependentJobs();
1980
+ } else {
1981
+ this.pausePopupDependentJobs();
1982
+ }
1983
+ });
1984
+ }
1985
+ /**
1986
+ * Pause all jobs that have requiresPopup: true
1987
+ */
1988
+ pausePopupDependentJobs() {
1989
+ const jobs = this.registry.listAll();
1990
+ let pausedCount = 0;
1991
+ for (const job of jobs) {
1992
+ if (job.options?.requiresPopup && !this.registry.getContext(job.id)?.isPaused()) {
1993
+ this.logger.debug(`Pausing popup-dependent job: ${job.id}`);
1994
+ this.alarm.cancel(job.id);
1995
+ this.timeout.cancel(job.id);
1996
+ this.registry.pause(job.id);
1997
+ pausedCount++;
1998
+ }
1999
+ }
2000
+ if (pausedCount > 0) {
2001
+ this.logger.info(`Paused ${pausedCount} popup-dependent jobs (popup closed)`);
2002
+ }
2003
+ }
2004
+ /**
2005
+ * Resume all jobs that have requiresPopup: true
2006
+ */
2007
+ resumePopupDependentJobs() {
2008
+ const jobs = this.registry.listAll();
2009
+ let resumedCount = 0;
2010
+ for (const job of jobs) {
2011
+ if (job.options?.requiresPopup && this.registry.getContext(job.id)?.isPaused()) {
2012
+ this.logger.debug(`Resuming popup-dependent job: ${job.id}`);
2013
+ this.registry.resume(job.id);
2014
+ this.schedule(job.id, job.options);
2015
+ resumedCount++;
2016
+ }
2017
+ }
2018
+ if (resumedCount > 0) {
2019
+ this.logger.info(`Resumed ${resumedCount} popup-dependent jobs (popup opened)`);
2020
+ }
1833
2021
  }
1834
2022
  schedule(id, options) {
1835
2023
  const context = this.registry.getContext(id);
1836
2024
  if (!context || context.isStopped()) {
1837
2025
  return;
1838
2026
  }
2027
+ if (options?.requiresPopup) {
2028
+ const isPopupVisible = PopupVisibilityService.instance.isPopupVisible();
2029
+ if (!isPopupVisible) {
2030
+ this.logger.debug(
2031
+ `Job ${id} requires popup but popup is not visible, pausing instead of scheduling`
2032
+ );
2033
+ if (!context.isPaused()) {
2034
+ this.registry.pause(id);
2035
+ }
2036
+ return;
2037
+ }
2038
+ }
1839
2039
  const when = this.getScheduleTime(options);
1840
2040
  const now = Date.now();
1841
2041
  if (when <= now) {
@@ -1886,6 +2086,7 @@ class Scheduler {
1886
2086
  async execute(id) {
1887
2087
  const job = this.registry.resolve(id);
1888
2088
  const context = this.registry.getContext(id);
2089
+ const options = this.registry.meta(id);
1889
2090
  if (!job || !context) {
1890
2091
  this.logger.debug(`Job ${id} not found or no context`);
1891
2092
  return;
@@ -1894,6 +2095,14 @@ class Scheduler {
1894
2095
  this.logger.debug(`Job ${id} is paused or stopped, skipping execution`);
1895
2096
  return;
1896
2097
  }
2098
+ if (options?.requiresPopup) {
2099
+ const isPopupVisible = PopupVisibilityService.instance.isPopupVisible();
2100
+ if (!isPopupVisible) {
2101
+ this.logger.debug(`Job ${id} requires popup but popup closed, pausing job`);
2102
+ this.registry.pause(id);
2103
+ return;
2104
+ }
2105
+ }
1897
2106
  try {
1898
2107
  this.registry.updateState(id, JobState.RUNNING);
1899
2108
  this.logger.info(`Executing job ${id}`);
@@ -1902,7 +2111,6 @@ class Scheduler {
1902
2111
  await jobInstance.handle.bind(jobInstance).call(jobInstance, context);
1903
2112
  if (!context.isStopped() && !context.isPaused()) {
1904
2113
  this.registry.updateState(id, JobState.COMPLETED);
1905
- const options = this.registry.meta(id);
1906
2114
  if (options?.cron || options?.recurring) {
1907
2115
  this.registry.updateState(id, JobState.SCHEDULED);
1908
2116
  this.schedule(id, options);
@@ -1911,7 +2119,6 @@ class Scheduler {
1911
2119
  } catch (error) {
1912
2120
  this.logger.error(`Job ${id} execution failed:`, error);
1913
2121
  context.fail(error);
1914
- const options = this.registry.meta(id);
1915
2122
  if (options?.cron || options?.recurring) {
1916
2123
  this.logger.info(`Rescheduling failed recurring job ${id}`);
1917
2124
  this.registry.updateState(id, JobState.SCHEDULED);
@@ -1942,6 +2149,10 @@ class Scheduler {
1942
2149
  */
1943
2150
  shutdown() {
1944
2151
  this.logger.info("Shutting down scheduler...");
2152
+ if (this.popupVisibilityUnsubscribe) {
2153
+ this.popupVisibilityUnsubscribe();
2154
+ this.popupVisibilityUnsubscribe = void 0;
2155
+ }
1945
2156
  this.alarm.clear();
1946
2157
  this.timeout.clear();
1947
2158
  this.registry.clear();
@@ -2489,6 +2700,7 @@ class BootstrapBuilder {
2489
2700
  exports.JobRegistry = JobRegistry;
2490
2701
  exports.JobState = JobState;
2491
2702
  exports.NonceService = NonceService;
2703
+ exports.PopupVisibilityService = PopupVisibilityService;
2492
2704
  exports.Scheduler = Scheduler;
2493
2705
  exports.arePortsClaimed = arePortsClaimed;
2494
2706
  exports.bootstrap = bootstrap;
@@ -2496,6 +2708,7 @@ exports.claimEarlyPorts = claimEarlyPorts;
2496
2708
  exports.container = container;
2497
2709
  exports.create = create;
2498
2710
  exports.getNonceService = getNonceService;
2711
+ exports.getPopupVisibilityService = getPopupVisibilityService;
2499
2712
  exports.isEarlyListenerSetup = isEarlyListenerSetup;
2500
2713
  exports.setupEarlyListener = setupEarlyListener;
2501
- //# sourceMappingURL=boot-B33Lf5tn.js.map
2714
+ //# sourceMappingURL=boot-CcvC3GTE.js.map