PageSourceSearch

https://dimeindustries.com/wp-content/themes/dime-custom/assets/js/locator-area-data.js?ver=1791436698

js dimeindustries.com collected 2026-10-10 01:05:21 UTC 19,758 bytes, 432 lines download raw bytes

1(() => {
2  'use strict';
3
4  if (window.dimeLocatorAreaData) return;
5
6  const installed = new WeakSet();
7  const controllers = new WeakMap();
8  // Leave room above the current catalog; prepareCatalog still rejects any
9  // truncated response rather than silently hiding retailers from an area.
10  const catalogLimit = 5000;
11  const listPageSize = 30;
12  const cacheLimit = 3;
13  const cacheLifetime = 5 * 60 * 1000;
14  const requestTimeout = 12000;
15  const coordinateKeys = ['lat', 'lng', 'radius', 'limit', 'list_limit', 'list_offset'];
16  const withoutCoordinates = params => {
17    const copy = { ...params };
18    coordinateKeys.forEach(key => delete copy[key]);
19    return copy;
20  };
21  const canonical = value => {
22    if (Array.isArray(value)) return value.map(canonical);
23    if (value && typeof value === 'object') {
24      return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]));
25    }
26    return value;
27  };
28  const isZeroCoordinate = value => value !== null && value !== undefined && value !== '' && Number(value) === 0;
29  const handleIgnoredRequest = promise => {
30    const then = promise.then.bind(promise);
31    then(undefined, () => {});
32    // StoreRocket's event handlers sometimes use .then(trackSearch) without a
33    // rejection callback. Keep native Promise rejection for our await/catch
34    // callers, but consume ignored child chains just as its Deferred did.
35    promise.then = (fulfilled, rejected) => handleIgnoredRequest(then(fulfilled, rejected));
36    return promise;
37  };
38
39  const prepareCatalog = response => {
40    const results = response?.results;
41    if (!results || !Array.isArray(results.locations) || typeof results.html?.locations !== 'string') {
42      throw new Error('The retailer catalog response was incomplete.');
43    }
44
45    // Parse inert HTML once, keeping strings rather than a large detached DOM.
46    // StoreRocket binds list actions to data-index, not the retailer's ID.
47    const template = document.createElement('template');
48    template.innerHTML = results.html.locations;
49    const rows = new Map();
50    template.content.querySelectorAll('.storerocket-result[data-index]').forEach(row => {
51      const index = Number(row.getAttribute('data-index'));
52      if (!Number.isInteger(index) || index < 0 || rows.has(index)) {
53        throw new Error('The retailer catalog contained invalid list indexes.');
54      }
55      rows.set(index, row.outerHTML);
56    });
57    const total = Number(results.list?.total ?? results.locations.length);
58    if (rows.size !== results.locations.length || total > rows.size ||
59        results.locations.some((_, index) => !rows.has(index))) {
60      throw new Error('The retailer catalog was truncated; the map cannot show a partial area.');
61    }
62    return {
63      response: { ...response, results: { ...results, html: { ...results.html, locations: '' } } },
64      rows,
65    };
66  };
67
68  const rowsForPage = (catalog, indexes, start, end) => indexes.slice(start, end).map((originalIndex, offset) =>
69    catalog.rows.get(originalIndex).replace(/\bdata-index\s*=\s*(["'])\d+\1/i, `data-index="${start + offset}"`)
70  ).join('');
71
72  const responseInBounds = (catalog, bounds, pages, state = null) => {
73    const locations = [];
74    const indexes = [];
75    catalog.response.results.locations.forEach((location, originalIndex) => {
76      if (state) {
77        const region = String(location.state || '').trim().toLowerCase();
78        const country = String(location.country || '').trim().toLowerCase();
79        if (state.type === 'country') {
80          if (!['ca', 'can', 'canada'].includes(country)) return;
81        } else if (![state.code.toLowerCase(), state.name.toLowerCase()].includes(region) ||
82            !['us', 'usa', 'united states', 'united states of america'].includes(country)) return;
83      }
84      if (location.lat === null || location.lat === '' || location.lng === null || location.lng === '') return;
85      const lat = Number(location.lat);
86      const lng = Number(location.lng);
87      if (!Number.isFinite(lat) || !Number.isFinite(lng) || Math.abs(lat) > 90 || Math.abs(lng) > 180 ||
88          (!state && !bounds.contains({ lat, lng }))) return;
89      // The provider decorates results in place; never hand it cached objects.
90      locations.push({ ...location });
91      indexes.push(originalIndex);
92    });
93    const rendered = pages ? Math.min(listPageSize, locations.length) : locations.length;
94    if (pages) pages.set(locations, { catalog, indexes });
95    return {
96      ...catalog.response,
97      results: {
98        ...catalog.response.results,
99        locations,
100        html: { ...catalog.response.results.html, locations: rowsForPage(catalog, indexes, 0, rendered) },
101        // Every map location remains available. Only the sidebar's initial
102        // DOM is bounded; further rows use the provider's accessible button.
103        list: rendered < locations.length ? { total: locations.length, rendered } : null,
104      },
105    };
106  };
107
108  const install = (instance, getArea) => {
109    if (!instance || installed.has(instance) || typeof instance.fetchLocations !== 'function' || typeof getArea !== 'function') return;
110    installed.add(instance);
111    const originalFetch = instance.fetchLocations;
112    const originalUpdate = instance.updateMap;
113    const originalMore = instance.fetchMoreResults;
114    let pages = typeof originalMore === 'function' && typeof instance.appendMoreResults === 'function'
115      ? new WeakMap() : null;
116    const caches = new Map();
117    // LRU eviction only limits reuse; an evicted request may still be pending.
118    // Keep a separate set so closing the locator can abort every catalog load.
119    const pendingCatalogs = new Set();
120    const pendingPages = new Set();
121    let latestPage = null;
122    let releaseGeneration = 0;
123    let latestOperation = null;
124    let fetchingOperation = null;
125
126    const evict = (entry, abort = false) => {
127      if (entry && caches.get(entry.key) === entry) caches.delete(entry.key);
128      if (abort && entry?.pending) entry.controller?.abort();
129    };
130    const rememberCache = entry => {
131      caches.delete(entry.key);
132      caches.set(entry.key, entry);
133      while (caches.size > cacheLimit) caches.delete(caches.keys().next().value);
134    };
135
136    const fetchNativePage = target => {
137      if (target.fetchingMoreResults || !target.listWindow) return;
138      if (!target.listRequestParams || typeof target.listRequestParams !== 'object') return originalMore.call(target);
139      const scope = target.selector?.querySelectorAll ? target.selector : null;
140      const page = {
141        generation: releaseGeneration, providerGeneration: target.generation, locations: target.locations, listWindow: target.listWindow,
142        offset: Number(target.listRenderedCount) || 0, request: null,
143        buttons: [...(scope?.querySelectorAll('.storerocket-show-more') || [])],
144        wrapper: scope?.querySelector('.storerocket-show-more-wrap') || null,
145      };
146      const owns = () => latestPage === page && page.generation === releaseGeneration;
147      const valid = () => owns() && target.generation === page.providerGeneration &&
148        target.locations === page.locations && target.listWindow === page.listWindow &&
149        (Number(target.listRenderedCount) || 0) === page.offset;
150      latestPage = page;
151      pendingPages.add(page);
152      target.fetchingMoreResults = true;
153      page.buttons.forEach(button => { button.disabled = true; button.classList.add('is-loading'); });
154
155      // Reuse the provider's public request builder rather than duplicating its
156      // account/embed/filter serializer. Its isolated receiver prevents the
157      // main-map spinner and listRequestParams writes during sidebar paging.
158      // The vendor's own fetchMoreResults has unguarded global DOM callbacks;
159      // owning this completion keeps late responses out after close/reopen.
160      const receiver = Object.create(target);
161      receiver.showLoadingSpinner = () => {};
162      receiver.installation = Object.create(target.installation || null);
163      receiver.installation.contentPending = () => {};
164      let request;
165      try {
166        request = originalFetch.call(receiver, { ...target.listRequestParams, list_offset: page.offset });
167        page.request = request;
168      } catch (error) { request = Promise.reject(error); }
169      const result = Promise.resolve(request).then(response => {
170        if (!valid()) return;
171        const html = response?.results?.html?.locations;
172        if (html) target.appendMoreResults(html, response.results.list);
173        else page.wrapper?.remove();
174      }, () => {
175        // Leave the existing button in place for retry; never clear newer UI.
176      }).finally(() => {
177        if (owns()) {
178          latestPage = null;
179          target.fetchingMoreResults = false;
180          page.buttons.forEach(button => { button.disabled = false; button.classList.remove('is-loading'); });
181        }
182        pendingPages.delete(page);
183        page.request = page.locations = page.listWindow = page.wrapper = null;
184        page.buttons = [];
185      });
186      return handleIgnoredRequest(result);
187    };
188
189    if (pages) {
190      instance.fetchMoreResults = function () {
191        const page = pages.get(this.locations);
192        if (!page) return fetchNativePage(this);
193        if (this.fetchingMoreResults || !this.listWindow) return;
194        const start = Math.max(0, Number(this.listRenderedCount) || 0);
195        const end = Math.min(start + listPageSize, page.indexes.length);
196        if (start >= end) return;
197        this.fetchingMoreResults = true;
198        try {
199          // appendMoreResults retains native row decoration, data-index click
200          // handling and the keyboard-focusable Show more button, without a
201          // new request, map rebuild, or globally indexed off-screen rows.
202          return this.appendMoreResults(rowsForPage(page.catalog, page.indexes, start, end), {
203            total: page.indexes.length,
204            rendered: end,
205          });
206        } finally { this.fetchingMoreResults = false; }
207      };
208    }
209
210    const discardResponse = () => ({ results: { locations: [], html: { locations: '' }, list: null } });
211    const protectFetch = (request, operation) => {
212      if (!operation) return request;
213      return Promise.resolve(request).catch(error => {
214        operation.fail(error);
215        // The provider never forwards rejection to updateMap's Deferred.
216        // Resolve its internal chain only after making its commit guard false.
217        return discardResponse();
218      });
219    };
220
221    if (typeof originalUpdate === 'function') {
222      instance.updateMap = function (options, isCurrent = () => true) {
223        const target = this;
224        const operationGeneration = releaseGeneration;
225        let resolveResult;
226        let rejectResult;
227        const result = new Promise((resolve, reject) => {
228          resolveResult = resolve;
229          rejectResult = reject;
230        });
231        let timer;
232        const operation = {
233          valid: true,
234          settled: false,
235          catalogEntry: null,
236          nativeRequest: null,
237          isCurrent: () => valid(),
238          cancel() {
239            if (this.settled) return;
240            this.valid = false;
241            this.settled = true;
242            clearTimeout(timer);
243            target._markerRun = (target._markerRun || 0) + 1;
244            this.nativeRequest?.abort?.();
245            this.nativeRequest = null;
246            this.catalogEntry = null;
247            resolveResult();
248          },
249          fail(error) {
250            if (this.settled) return;
251            this.valid = false;
252            this.settled = true;
253            clearTimeout(timer);
254            this.nativeRequest?.abort?.();
255            if (latestOperation === this) {
256              target._markerRun = (target._markerRun || 0) + 1;
257              target.hideLoadingSpinner?.();
258              target.hideGeocodingSpinner?.();
259              if (this.catalogEntry?.pending) evict(this.catalogEntry, true);
260            }
261            this.nativeRequest = null;
262            this.catalogEntry = null;
263            rejectResult(error instanceof Error ? error : new Error('The retailer request could not complete.'));
264          },
265          complete(value) {
266            if (this.settled) return;
267            this.settled = true;
268            clearTimeout(timer);
269            if (latestOperation === this && !valid()) target.hideLoadingSpinner?.();
270            this.nativeRequest = null;
271            this.catalogEntry = null;
272            resolveResult(value);
273          },
274        };
275        const previous = latestOperation;
276        latestOperation = operation;
277        previous?.cancel();
278        const valid = () => {
279          if (!operation.valid || latestOperation !== operation || operationGeneration !== releaseGeneration) return false;
280          try { return typeof isCurrent !== 'function' || isCurrent(); }
281          catch (error) { operation.fail(error); return false; }
282        };
283        timer = setTimeout(() => operation.fail(new Error('The retailer request timed out.')), requestTimeout);
284        const previousFetchOperation = fetchingOperation;
285        fetchingOperation = operation;
286        try {
287          Promise.resolve(originalUpdate.call(target, options, valid))
288            .then(value => operation.complete(value), error => operation.fail(error));
289        } catch (error) {
290          operation.fail(error);
291        } finally {
292          fetchingOperation = operationGeneration === releaseGeneration ? previousFetchOperation : null;
293        }
294        return handleIgnoredRequest(result);
295      };
296
297    }
298
299    const invalidate = () => {
300      if (!latestOperation || latestOperation.settled) return false;
301      latestOperation.cancel();
302      // StoreRocket schedules markers in batches after committing results.
303      // Its render loop checks _markerRun, not the request validity callback.
304      // cancel() stops that work immediately, before the next fetch commits.
305      instance.hideLoadingSpinner?.();
306      instance.hideGeocodingSpinner?.();
307      return true;
308    };
309    const release = () => {
310      ++releaseGeneration;
311      const operation = latestOperation;
312      latestOperation = null;
313      fetchingOperation = null;
314      const wasPending = operation && !operation.settled;
315      operation?.cancel();
316      if (operation) {
317        operation.nativeRequest = null;
318        operation.catalogEntry = null;
319      }
320      // Also stop any provider marker batches whose request has already settled.
321      if (!wasPending) instance._markerRun = (instance._markerRun || 0) + 1;
322      caches.clear();
323      if (pages) pages = new WeakMap();
324      latestPage = null;
325      instance.fetchingMoreResults = false;
326      const sidebarRequests = [...pendingPages];
327      pendingPages.clear();
328      sidebarRequests.forEach(page => {
329        page.request?.abort?.();
330        page.request = page.locations = page.listWindow = page.wrapper = null;
331        page.buttons = [];
332      });
333      const requests = [...pendingCatalogs];
334      pendingCatalogs.clear();
335      requests.forEach(entry => entry.controller?.abort());
336      instance.hideLoadingSpinner?.();
337      instance.hideGeocodingSpinner?.();
338      return true;
339    };
340    controllers.set(instance, { invalidate, release });
341
342    instance.fetchLocations = function (params) {
343      const operation = fetchingOperation;
344      const requestGeneration = releaseGeneration;
345      const area = getArea();
346      const zeroCenter = isZeroCoordinate(this.lat) || isZeroCoordinate(this.lng);
347      // Keep normal searches, selected retailers and supported local radii on
348      // the provider's native path, including native distance sorting/paging.
349      if (!area?.bounds || typeof area.bounds.contains !== 'function' || params?.location ||
350          (!area.state && !zeroCenter && Number(area.radius) <= Number(area.maxRadius ?? 100))) {
351        const request = originalFetch.apply(this, arguments);
352        if (operation) operation.nativeRequest = request;
353        return protectFetch(request, operation);
354      }
355
356      const request = withoutCoordinates(params);
357      const key = JSON.stringify(canonical({
358        request,
359        account: this.userId,
360        api: this.apiBaseUrl,
361        filters: this.filters,
362        lockedFilters: this.lockedFilters,
363        filterOperator: this.filterOperator,
364        units: this.units,
365        embedFilterOptions: this.embedFilterOptions,
366      }));
367      let cache = caches.get(key);
368      if (cache && !cache.pending && Date.now() >= cache.expiresAt) { evict(cache); cache = null; }
369      if (!cache) {
370        // Let StoreRocket construct its complete, scoped request. Its public
371        // radius endpoint caps at 100 miles; use an unbounded, filtered catalog
372        // only for larger views (and its falsy-coordinate edge case at zero).
373        const providerRequest = originalFetch.call(this, { ...request });
374        Promise.resolve(providerRequest).catch(() => {});
375        const fullParams = { ...this.listRequestParams, list_limit: catalogLimit };
376        // Parameter construction is synchronous; this metadata-only jqXHR
377        // need not download a second nationwide response alongside fetch().
378        providerRequest?.abort?.();
379        const url = new URL(`${this.apiBaseUrl.replace(/\/$/, '')}/user/${encodeURIComponent(this.userId)}/locations`);
380        Object.entries(fullParams).forEach(([name, value]) => {
381          if (value !== undefined && value !== null) url.searchParams.set(name, String(value));
382        });
383        const entry = {
384          key, params: fullParams, pending: true, expiresAt: 0, promise: null,
385          controller: typeof window.AbortController === 'function' ? new window.AbortController() : null,
386        };
387        cache = entry;
388        rememberCache(entry);
389        pendingCatalogs.add(entry);
390        entry.promise = window.fetch(url.href, { credentials: 'omit', ...(entry.controller ? { signal: entry.controller.signal } : {}) })
391          .then(response => {
392            if (requestGeneration !== releaseGeneration) return null;
393            if (!response.ok) throw new Error(`The retailer catalog could not load (${response.status}).`);
394            return response.json();
395          })
396          .then(response => requestGeneration === releaseGeneration ? prepareCatalog(response) : null)
397          .then(catalog => {
398            entry.pending = false;
399            pendingCatalogs.delete(entry);
400            if (requestGeneration !== releaseGeneration) return null;
401            entry.expiresAt = Date.now() + cacheLifetime;
402            return catalog;
403          })
404          .catch(error => {
405            entry.pending = false;
406            pendingCatalogs.delete(entry);
407            evict(entry);
408            if (requestGeneration !== releaseGeneration) return null;
409            throw error;
410          });
411      } else {
412        rememberCache(cache);
413        this.installation?.contentPending?.();
414        this.showLoadingSpinner?.();
415      }
416      this.listRequestParams = { ...cache.params };
417      if (operation) operation.catalogEntry = cache;
418      return protectFetch(cache.promise.then(catalog => {
419        // A rapid wheel reversal can supersede a request before its shared
420        // catalog finishes. Do not build even a small DOM payload for it.
421        if (requestGeneration !== releaseGeneration || !catalog || (operation && !operation.isCurrent())) return discardResponse();
422        return responseInBounds(catalog, area.bounds, pages, area.state);
423      }), operation);
424    };
425  };
426
427  window.dimeLocatorAreaData = {
428    install,
429    invalidate: instance => controllers.get(instance)?.invalidate() || false,
430    release: instance => controllers.get(instance)?.release() || false,
431  };
432})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.