PageSourceSearch

https://www.cafepress.co.uk/scripts/section.js?30ee96d925107cd1f2b86141b0ad71ffab79167c

js cafepress.co.uk collected 2026-09-24 23:46:32 UTC 7,844 bytes, 207 lines download raw bytes

1/**
2 * SectionStore — per-section SWR store backed by localStorage.
3 *
4 * Loaded globally via script_footer.php in the order api.js -> section.js ->
5 * init.js, so it runs on every page after PAApi is available and before init.js
6 * consumes it. Designed to hydrate per-visitor data (cart / favorite / customer)
7 * out of the FPC-cached HTML: the page renders instantly from cache and this
8 * store revalidates each section in the background.
9 *
10 * On load it auto-fires one background sync (via PAApi.syncSections) for the
11 * registered sections (cart / favorite / customer). The HTTP call itself lives
12 * in api.js; this module only owns storage, subscriptions, and cross-tab relay.
13 *
14 * Storage layout:
15 *   localStorage["pa_section_<name>"] = JSON.stringify({ version, data })
16 *
17 * Server contract (POST /rest/web/sections, issued by PAApi.syncSections):
18 *   request:  { sections: ["cart","favorite","customer"], versions: { cart: "v", ... } }
19 *   response: { sections: { "<changed>": { version, data }, ... } }
20 * A section absent from the response means "not modified" — the local copy is kept.
21 *
22 * Cross-tab: writing localStorage in one tab fires a `storage` event in OTHER
23 * tabs (not the writing tab), so those tabs re-fire onUpdated without hitting
24 * the server. The writing tab fires onUpdated explicitly inside writeLocal().
25 */
26window.SectionStore = (function () {
27    var KEY_PREFIX = 'pa_section_';
28
29    // name -> array of callbacks
30    var listeners = {};
31    // sections this store syncs on every page load. SectionStore loads before
32    // init.js and auto-fires one sync for these on construction.
33    var registered = ['cart', 'favorite', 'customer'];
34    // single in-flight sync promise
35    var inFlight = null;
36
37    function key(name) { return KEY_PREFIX + name; }
38
39    function readLocal(name) {
40        try {
41            var raw = localStorage.getItem(key(name));
42            if (!raw) return null;
43            var parsed = JSON.parse(raw);
44            if (!parsed || typeof parsed !== 'object') return null;
45            return parsed; // { version, data }
46        } catch (e) {
47            return null;
48        }
49    }
50
51    function writeLocal(name, version, data) {
52        try {
53            localStorage.setItem(key(name), JSON.stringify({ version: version, data: data }));
54        } catch (e) { /* quota / disabled — ignore */ }
55        fire(name, data);
56    }
57
58    function localVersion(name) {
59        var entry = readLocal(name);
60        return entry ? (entry.version || null) : null;
61    }
62
63    function fire(name, data) {
64        var cbs = listeners[name];
65        if (!cbs) return;
66        cbs.slice().forEach(function (cb) {
67            try { cb(data); } catch (e) { /* swallow listener errors */ }
68        });
69    }
70
71    /**
72     * GET a section's data. Returns the cached value synchronously (stale-while-
73     * revalidate). Returns null on first ever load (no cache yet); a background
74     * sync is kicked so onUpdated will fire once fresh data arrives.
75     */
76    function get(name) {
77        var entry = readLocal(name);
78        kickSync();
79        return entry ? entry.data : null;
80    }
81
82    /**
83     * Subscribe to per-section updates. Returns an unsubscribe function.
84     */
85    function onUpdated(name, cb) {
86        if (typeof cb !== 'function') return function () {};
87        if (!listeners[name]) listeners[name] = [];
88        listeners[name].push(cb);
89        return function () { offUpdated(name, cb); };
90    }
91
92    function offUpdated(name, cb) {
93        var cbs = listeners[name];
94        if (!cbs) return;
95        var i = cbs.indexOf(cb);
96        if (i !== -1) cbs.splice(i, 1);
97    }
98
99    /**
100     * Force the next sync to re-fetch this section (drops the stored version,
101     * keeps the data so get() still returns something). Call from a same-session
102     * write endpoint (e.g. add-to-cart) so the next page load revalidates
103     * immediately, regardless of CDC lag.
104     */
105    function invalidate(name) {
106        var entry = readLocal(name);
107        if (entry) {
108            try {
109                localStorage.setItem(key(name), JSON.stringify({ version: null, data: entry.data }));
110            } catch (e) { /* ignore */ }
111        }
112        kickSync([name]);
113    }
114
115    /**
116     * Write a section directly from a write-endpoint response (no server round
117     * trip). Optional version; if omitted the local version is cleared so the
118     * next sync still revalidates against the server.
119     */
120    function setLocal(name, data, version) {
121        writeLocal(name, (typeof version === 'undefined' ? null : version), data);
122    }
123
124    /**
125     * Drop the cached cart badge to empty immediately after checkout. Server-side
126     * checkout clears the cart and bumps the section version, but localStorage can
127     * still hold the pre-order count until the next sync — which FPC pages surface
128     * instantly via mini-cart.js. Call before redirecting to confirm_order (or
129     * upsell hops that precede it) so the next page does not flash a stale qty.
130     */
131    function resetCartAfterCheckout() {
132        writeLocal('cart', null, { count: 0, cart_item_qty: 0, subtotal: '0.00', items: [] });
133        kickSync(['cart']);
134    }
135
136    /**
137     * Single-flight sync. If names is omitted, syncs all registered sections.
138     * Concurrent calls share one in-flight request.
139     */
140    function sync(names) {
141        return kickSync(names);
142    }
143
144    function kickSync(forceNames) {
145        if (inFlight) return inFlight;
146        var names = (forceNames && forceNames.length) ? forceNames.slice() : registered.slice();
147        if (!names.length) return Promise.resolve({ sections: {} });
148
149        var versions = {};
150        names.forEach(function (n) { versions[n] = localVersion(n); });
151
152        // The HTTP call lives in api.js (PAApi.syncSections) so timeout / abort /
153        // error normalization are shared with the rest of the client. api.js is
154        // loaded before section.js, so PAApi is guaranteed to exist here.
155        inFlight = window.PAApi.syncSections(names, versions).then(function (res) {
156            inFlight = null;
157            var changed = (res && res.sections) || {};
158            Object.keys(changed).forEach(function (n) {
159                var payload = changed[n];
160                if (payload && typeof payload === 'object' && 'data' in payload) {
161                    writeLocal(n, payload.version || null, payload.data);
162                }
163            });
164            return res;
165        }).catch(function () {
166            inFlight = null;
167            return { sections: {} };
168        });
169        return inFlight;
170    }
171
172    /**
173     * Register sections to sync on every page load. Idempotent.
174     */
175    function register(names) {
176        names.forEach(function (n) {
177            if (registered.indexOf(n) === -1) registered.push(n);
178        });
179    }
180
181    // Cross-tab: another tab wrote a section's localStorage -> re-fire here.
182    if (typeof window !== 'undefined') {
183        window.addEventListener('storage', function (e) {
184            if (!e || !e.key || e.key.indexOf(KEY_PREFIX) !== 0) return;
185            var name = e.key.slice(KEY_PREFIX.length);
186            var entry = readLocal(name);
187            if (entry) fire(name, entry.data);
188        });
189    }
190
191    // Auto-sync on load: SectionStore is loaded before init.js, so fire one
192    // background sync for the registered sections immediately. It does not
193    // block — callers read localStorage via get() (stale-while-revalidate) and
194    // onUpdated fires when fresh data lands.
195    kickSync();
196
197    return {
198        get: get,
199        sync: sync,
200        register: register,
201        onUpdated: onUpdated,
202        offUpdated: offUpdated,
203        invalidate: invalidate,
204        setLocal: setLocal,
205        resetCartAfterCheckout: resetCartAfterCheckout,
206    };
207})();

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.