Widget Bundle Cachingο
Widget bundles are JavaScript IIFE packages fetched from the backend via
graphql?op=GetWidgetBundle. Each bundle can be tens to hundreds of kilobytes.
On slow or high-latency networks (VPN, remote sites), transferring every bundle
on every page load is a significant bottleneck.
This page describes the IndexedDB-based cache that eliminates redundant bundle transfers while guaranteeing that the browser always runs the version the server currently has installed.
Integrity-Based Cache Modelο
The cache does not use time-to-live expiry. Instead it uses the
integrity field β a SHA-256 hash of the bundle bytes β that the backend
computes and stores in MongoDB when a widget is first downloaded or upgraded.
The same hash is returned by both:
LIST_WIDGETSβ a lightweight metadata query, always fetched withfetchPolicy: 'no-cache'at every application bootGetWidgetBundleβ the full bundle query that includes the JS code
Because LIST_WIDGETS is always fresh, the browser always knows the serverβs
current integrity hash before it decides whether to fetch a bundle. A cached
bundle is only used when its stored hash exactly matches the serverβs
current hash. Any mismatch (upgrade, redeploy, content change) is automatically
a cache miss.
Boot Flowο
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '24px', 'fontFamily': 'Arial, sans-serif'}, 'flowchart': {'rankSpacing': 90, 'nodeSpacing': 80, 'padding': 30, 'htmlLabels': true}}}%%
flowchart TD
classDef terminalStart fill:#f0f0f0,stroke:#6c757d,color:#333
classDef terminalEnd fill:#f0f0f0,stroke:#6c757d,color:#333
classDef listNode fill:#cce5ff,stroke:#004085,color:#004085
classDef idbNode fill:#e2d9f3,stroke:#6f42c1,color:#3d1a78
classDef cacheHit fill:#d4edda,stroke:#28a745,color:#155724
classDef cacheMiss fill:#fff3cd,stroke:#ffc107,color:#856404
BOOT("<span style='display:inline-block;min-width:180px;padding:10px 0;font-size:18px;text-align:center'>Browser Boot</span>"):::terminalStart
BOOT --> LIST
LIST["LIST_WIDGETS<br/>always no-cache<br/>returns integrity hash per widget"]:::listNode
LIST --> CHK
CHK{"integrity<br/>returned?"}
CHK -- No --> FETCH
CHK -- Yes --> IDB
IDB["IndexedDB<br/>.get(id)<br/>check local cache"]:::idbNode
IDB --> MATCH
MATCH{"stored hash<br/>= = =<br/>server hash?"}
MATCH -- "β match" --> HIT
MATCH -- "β miss / stale" --> FETCH
HIT["Execute cached code<br/>β zero network round-trip"]:::cacheHit
HIT --> DONE
FETCH["GetWidgetBundle<br/>fetch full bundle over network<br/>then store in IndexedDB"]:::cacheMiss
FETCH --> DONE
DONE("<span style='display:inline-block;min-width:220px;padding:12px 0;text-align:center'>Widget ready</span>"):::terminalEnd
Customising the diagramο
Appearance (fill colour, border, text colour) β edit the classDef lines at the top of the diagram source. Each class maps to one node role.
Font size, width and height β classDef properties have no effect on layout with htmlLabels: true (labels render as HTML inside a <foreignObject>, not as SVG text). Use an inline <span> in the node label instead:
MYNODE("<span style='display:inline-block;min-width:200px;padding:14px 0;font-size:20px;text-align:center'>Label</span>"):::myClass
Span property |
Controls |
|---|---|
|
Text size for that node only |
|
Minimum node width ( |
|
Top + bottom padding β node height |
Global spacing (gap between all nodes) β edit rankSpacing, nodeSpacing, and padding in the %%{init}%% line at the top of the diagram.
First Load (Cold Path)ο
When a widget has never been loaded in this browser, or its integrity hash has changed since it was last cached:
LIST_WIDGETSreturns the serverβs currentintegrityfor the widget.IndexedDB.get(id)returns nothing (or an entry with a different hash).GetWidgetBundleis called β the full bundle JS is fetched over the network.The bundle is executed immediately.
IndexedDB.put({ id, integrity, code })stores the bundle for future loads (fire-and-forget; a write failure only means the next load re-fetches).
Subsequent Loads (Warm Path)ο
When the widget is already cached and the server has not changed it:
LIST_WIDGETSreturns the sameintegrityhash as the cached entry.IndexedDB.get(id)returns the cached entry.The stored hash is compared to the serverβs hash β they match.
The cached code is executed directly.
GetWidgetBundleis not called.
The large bundle payload never crosses the network. Only the compact
LIST_WIDGETS response (metadata only, no code) is required.
Cache Invalidationο
The cache entry for a widget is invalidated (becomes a miss) in two ways:
Trigger |
Mechanism |
|---|---|
Widget upgraded on the server |
Backend changes |
Widget removed or marked stale in the frontend |
|
There is no manual cache invalidation step required after a widget upgrade. The integrity change propagates automatically on the next page load.
Security Propertiesο
The
integrityhash used for cache validation always comes from the server (LIST_WIDGETSis alwaysno-cache). A browser-local cache entry cannot promote itself β it must match what the server declares.A corrupted or tampered IndexedDB entry with a wrong hash will simply produce a cache miss and trigger a clean re-fetch.
The backend computes the
integrityhash server-side (SHA-256 of the raw bundle bytes) and stores it in MongoDB. Clients cannot influence it.
Storageο
Bundles are stored in an IndexedDB database named octopus-widget-cache,
object store bundles, keyed by widget id. Each entry holds:
Field |
Content |
|---|---|
|
Stable backend widget ID (MongoDB ObjectId string) |
|
SHA-256 hex hash of the bundle bytes |
|
Full JS bundle source string |
All IndexedDB operations are best-effort. Any failure (unavailable storage,
quota exceeded, private-browsing restrictions) is caught silently and the
system falls back to a normal GetWidgetBundle network fetch.