@trops/dash-core 0.1.165 → 0.1.167

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.
@@ -4512,7 +4512,7 @@ var settingsController_1 = settingsController$1;
4512
4512
  * window.mainApi.myService.getData({ ...pc, cache: true, forceRefresh: true }); // bypass
4513
4513
  */
4514
4514
 
4515
- const cache = new Map(); // key → { data, timestamp, ttl }
4515
+ const cache$1 = new Map(); // key → { data, timestamp, ttl }
4516
4516
  const inflight = new Map(); // key → Promise
4517
4517
 
4518
4518
  function stableHash(obj) {
@@ -4528,13 +4528,13 @@ const responseCache$1 = {
4528
4528
  async get(key, fetcher, options = {}) {
4529
4529
  const { ttl = 30000, forceRefresh = false } = options;
4530
4530
 
4531
- if (!forceRefresh && cache.has(key)) {
4532
- const entry = cache.get(key);
4531
+ if (!forceRefresh && cache$1.has(key)) {
4532
+ const entry = cache$1.get(key);
4533
4533
  if (Date.now() - entry.timestamp < entry.ttl) {
4534
4534
  console.log(`[responseCache] HIT ${key}`);
4535
4535
  return entry.data;
4536
4536
  }
4537
- cache.delete(key);
4537
+ cache$1.delete(key);
4538
4538
  }
4539
4539
 
4540
4540
  if (!forceRefresh && inflight.has(key)) {
@@ -4548,7 +4548,7 @@ const responseCache$1 = {
4548
4548
  try {
4549
4549
  const data = await promise;
4550
4550
  if (data && !data.error) {
4551
- cache.set(key, { data, timestamp: Date.now(), ttl });
4551
+ cache$1.set(key, { data, timestamp: Date.now(), ttl });
4552
4552
  }
4553
4553
  return data;
4554
4554
  } finally {
@@ -4586,25 +4586,25 @@ const responseCache$1 = {
4586
4586
  },
4587
4587
 
4588
4588
  invalidate(key) {
4589
- cache.delete(key);
4589
+ cache$1.delete(key);
4590
4590
  },
4591
4591
 
4592
4592
  invalidatePrefix(prefix) {
4593
- for (const k of cache.keys()) {
4594
- if (k.startsWith(prefix)) cache.delete(k);
4593
+ for (const k of cache$1.keys()) {
4594
+ if (k.startsWith(prefix)) cache$1.delete(k);
4595
4595
  }
4596
4596
  },
4597
4597
 
4598
4598
  clear() {
4599
- cache.clear();
4599
+ cache$1.clear();
4600
4600
  inflight.clear();
4601
4601
  },
4602
4602
 
4603
4603
  stats() {
4604
4604
  return {
4605
- entries: cache.size,
4605
+ entries: cache$1.size,
4606
4606
  inflight: inflight.size,
4607
- keys: [...cache.keys()],
4607
+ keys: [...cache$1.keys()],
4608
4608
  };
4609
4609
  },
4610
4610
  };
@@ -45758,6 +45758,208 @@ const webSocketController$1 = {
45758
45758
 
45759
45759
  var webSocketController_1 = webSocketController$1;
45760
45760
 
45761
+ /**
45762
+ * extractionCacheController.js
45763
+ *
45764
+ * LRU cache with TTL for theme-from-URL extraction results.
45765
+ * Caches palette results keyed by URL to avoid re-scanning recently visited sites.
45766
+ *
45767
+ * - Default TTL: 24 hours
45768
+ * - Max entries: 50 (LRU eviction)
45769
+ * - In-memory only — cleared on app restart
45770
+ * - Supports force refresh to bypass cache
45771
+ */
45772
+
45773
+ const DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours
45774
+ const MAX_ENTRIES = 50;
45775
+
45776
+ // Map preserves insertion order — we use this for LRU tracking
45777
+ const cache = new Map(); // url → { data, timestamp, ttl }
45778
+
45779
+ /**
45780
+ * Get a cached result for the given URL, or run the fetcher and cache the result.
45781
+ * @param {string} url - The URL key
45782
+ * @param {Function} fetcher - Async function that produces the extraction result
45783
+ * @param {Object} [options]
45784
+ * @param {number} [options.ttl=86400000] - Time-to-live in milliseconds
45785
+ * @param {boolean} [options.forceRefresh=false] - Bypass cache and re-extract
45786
+ * @returns {Promise<any>} The extraction result
45787
+ */
45788
+ async function get(url, fetcher, options = {}) {
45789
+ const { ttl = DEFAULT_TTL, forceRefresh = false } = options;
45790
+ const key = url.toLowerCase().replace(/\/+$/, ""); // normalize
45791
+
45792
+ if (!forceRefresh && cache.has(key)) {
45793
+ const entry = cache.get(key);
45794
+ if (Date.now() - entry.timestamp < entry.ttl) {
45795
+ // Move to end (most recently used)
45796
+ cache.delete(key);
45797
+ cache.set(key, entry);
45798
+ console.log(`[extractionCache] HIT ${key}`);
45799
+ return entry.data;
45800
+ }
45801
+ // Expired
45802
+ cache.delete(key);
45803
+ }
45804
+
45805
+ console.log(`[extractionCache] ${forceRefresh ? "REFRESH" : "MISS"} ${key}`);
45806
+ const data = await fetcher();
45807
+
45808
+ // Store result
45809
+ cache.set(key, { data, timestamp: Date.now(), ttl });
45810
+
45811
+ // LRU eviction — remove oldest entries if over limit
45812
+ while (cache.size > MAX_ENTRIES) {
45813
+ const oldestKey = cache.keys().next().value;
45814
+ cache.delete(oldestKey);
45815
+ console.log(`[extractionCache] EVICT ${oldestKey}`);
45816
+ }
45817
+
45818
+ return data;
45819
+ }
45820
+
45821
+ /**
45822
+ * Check if a URL has a valid (non-expired) cache entry.
45823
+ * @param {string} url
45824
+ * @returns {boolean}
45825
+ */
45826
+ function has(url) {
45827
+ const key = url.toLowerCase().replace(/\/+$/, "");
45828
+ if (!cache.has(key)) return false;
45829
+ const entry = cache.get(key);
45830
+ if (Date.now() - entry.timestamp >= entry.ttl) {
45831
+ cache.delete(key);
45832
+ return false;
45833
+ }
45834
+ return true;
45835
+ }
45836
+
45837
+ /** Clear all cached entries. */
45838
+ function clear() {
45839
+ cache.clear();
45840
+ console.log("[extractionCache] CLEARED");
45841
+ }
45842
+
45843
+ /**
45844
+ * Remove a single URL from the cache.
45845
+ * @param {string} url
45846
+ */
45847
+ function invalidate(url) {
45848
+ const key = url.toLowerCase().replace(/\/+$/, "");
45849
+ cache.delete(key);
45850
+ }
45851
+
45852
+ /** Get cache statistics. */
45853
+ function stats() {
45854
+ return {
45855
+ entries: cache.size,
45856
+ maxEntries: MAX_ENTRIES,
45857
+ keys: [...cache.keys()],
45858
+ };
45859
+ }
45860
+
45861
+ const extractionCacheController$1 = { get, has, clear, invalidate, stats };
45862
+
45863
+ var extractionCacheController_1 = extractionCacheController$1;
45864
+
45865
+ /**
45866
+ * themeFromUrlErrors.js
45867
+ *
45868
+ * Typed error classes for the Theme from URL extraction pipeline.
45869
+ * Used across dash-core (controller), dash-electron (IPC handler),
45870
+ * and dash-react (UI error mapping).
45871
+ */
45872
+
45873
+ const ERROR_TYPES = {
45874
+ URL_UNREACHABLE: "URL_UNREACHABLE",
45875
+ URL_TIMEOUT: "URL_TIMEOUT",
45876
+ EXTRACTION_FAILED: "EXTRACTION_FAILED",
45877
+ NO_COLORS_FOUND: "NO_COLORS_FOUND",
45878
+ FAVICON_FETCH_FAILED: "FAVICON_FETCH_FAILED",
45879
+ };
45880
+
45881
+ class ThemeExtractionError extends Error {
45882
+ /**
45883
+ * @param {string} message - Developer-facing error message
45884
+ * @param {Object} options
45885
+ * @param {string} options.type - Machine-readable error type (from ERROR_TYPES)
45886
+ * @param {string} options.userMessage - User-facing message for UI display
45887
+ * @param {Error} [options.cause] - Original error that triggered this one
45888
+ */
45889
+ constructor(message, { type, userMessage, cause } = {}) {
45890
+ super(message);
45891
+ this.name = "ThemeExtractionError";
45892
+ this.type = type;
45893
+ this.userMessage = userMessage || "Something went wrong extracting colors.";
45894
+ this.cause = cause || null;
45895
+ }
45896
+ }
45897
+
45898
+ class UrlUnreachableError extends ThemeExtractionError {
45899
+ constructor(message, { cause } = {}) {
45900
+ super(message || "URL is unreachable", {
45901
+ type: ERROR_TYPES.URL_UNREACHABLE,
45902
+ userMessage: "Couldn't reach that URL. Check the address.",
45903
+ cause,
45904
+ });
45905
+ this.name = "UrlUnreachableError";
45906
+ }
45907
+ }
45908
+
45909
+ class UrlTimeoutError extends ThemeExtractionError {
45910
+ constructor(message, { cause } = {}) {
45911
+ super(message || "URL load timed out", {
45912
+ type: ERROR_TYPES.URL_TIMEOUT,
45913
+ userMessage: "The site took too long to load. Try a simpler page.",
45914
+ cause,
45915
+ });
45916
+ this.name = "UrlTimeoutError";
45917
+ }
45918
+ }
45919
+
45920
+ class ExtractionFailedError extends ThemeExtractionError {
45921
+ constructor(message, { cause } = {}) {
45922
+ super(message || "Color extraction failed", {
45923
+ type: ERROR_TYPES.EXTRACTION_FAILED,
45924
+ userMessage: "Failed to extract colors from this site.",
45925
+ cause,
45926
+ });
45927
+ this.name = "ExtractionFailedError";
45928
+ }
45929
+ }
45930
+
45931
+ class NoColorsFoundError extends ThemeExtractionError {
45932
+ constructor(message, { cause } = {}) {
45933
+ super(message || "No usable colors found", {
45934
+ type: ERROR_TYPES.NO_COLORS_FOUND,
45935
+ userMessage: "No usable colors found. Try a more styled page.",
45936
+ cause,
45937
+ });
45938
+ this.name = "NoColorsFoundError";
45939
+ }
45940
+ }
45941
+
45942
+ class FaviconFetchError extends ThemeExtractionError {
45943
+ constructor(message, { cause } = {}) {
45944
+ super(message || "Favicon fetch failed", {
45945
+ type: ERROR_TYPES.FAVICON_FETCH_FAILED,
45946
+ userMessage: "Couldn't fetch the site's favicon.",
45947
+ cause,
45948
+ });
45949
+ this.name = "FaviconFetchError";
45950
+ }
45951
+ }
45952
+
45953
+ var themeFromUrlErrors$1 = {
45954
+ ERROR_TYPES,
45955
+ ThemeExtractionError,
45956
+ UrlUnreachableError,
45957
+ UrlTimeoutError,
45958
+ ExtractionFailedError,
45959
+ NoColorsFoundError,
45960
+ FaviconFetchError,
45961
+ };
45962
+
45761
45963
  /**
45762
45964
  * clientFactories.js
45763
45965
  *
@@ -48126,7 +48328,8 @@ const {
48126
48328
  } = events$8;
48127
48329
 
48128
48330
  const themeFromUrlApi$2 = {
48129
- extractFromUrl: (url) => ipcRenderer$4.invoke(THEME_EXTRACT_FROM_URL, { url }),
48331
+ extractFromUrl: (url, { forceRefresh = false } = {}) =>
48332
+ ipcRenderer$4.invoke(THEME_EXTRACT_FROM_URL, { url, forceRefresh }),
48130
48333
  mapPaletteToTheme: (palette, overrides) =>
48131
48334
  ipcRenderer$4.invoke(THEME_MAP_PALETTE_TO_THEME, { palette, overrides }),
48132
48335
  };
@@ -48508,6 +48711,10 @@ const themeRegistryController = themeRegistryController$1;
48508
48711
  const themeFromUrlController = themeFromUrlController_1;
48509
48712
  const paletteToThemeMapper = paletteToThemeMapper_1;
48510
48713
  const webSocketController = webSocketController_1;
48714
+ const extractionCacheController = extractionCacheController_1;
48715
+
48716
+ // --- Errors ---
48717
+ const themeFromUrlErrors = themeFromUrlErrors$1;
48511
48718
 
48512
48719
  // --- Utils ---
48513
48720
  const clientCache = requireClientCache();
@@ -48584,6 +48791,7 @@ var electron = {
48584
48791
  themeFromUrlController,
48585
48792
  paletteToThemeMapper,
48586
48793
  webSocketController,
48794
+ extractionCacheController,
48587
48795
 
48588
48796
  // Controller functions (flat) — spread for convenient destructuring
48589
48797
  ...controllers,
@@ -48629,6 +48837,9 @@ var electron = {
48629
48837
  clientCache,
48630
48838
  responseCache,
48631
48839
 
48840
+ // Errors
48841
+ themeFromUrlErrors,
48842
+
48632
48843
  // Schema
48633
48844
  dashboardConfigValidator,
48634
48845
  dashboardConfigUtils,