Skip to content

Commit 340d960

Browse files
Improve resource caching (#973)
1 parent d13b87c commit 340d960

File tree

7 files changed

+175
-22
lines changed

7 files changed

+175
-22
lines changed

Diff for: @plotly/webpack-dash-dynamic-import/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@plotly/webpack-dash-dynamic-import",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "Webpack Plugin for Dynamic Import in Dash",
55
"repository": {
66
"type": "git",

Diff for: @plotly/webpack-dash-dynamic-import/src/index.js

+53-10
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,72 @@
1-
const resolveImportSource = `\
1+
const fs = require('fs');
2+
3+
function getFingerprint() {
4+
const package = fs.readFileSync('./package.json');
5+
const packageJson = JSON.parse(package);
6+
7+
const timestamp = Math.round(Date.now() / 1000);
8+
const version = packageJson.version.replace(/[.]/g, '_');
9+
10+
return `"v${version}m${timestamp}"`;
11+
}
12+
13+
const resolveImportSource = () => `\
14+
const getCurrentScript = function() {
15+
let script = document.currentScript;
16+
if (!script) {
17+
/* Shim for IE11 and below */
18+
/* Do not take into account async scripts and inline scripts */
19+
const scripts = Array.from(document.getElementsByTagName('script')).filter(function(s) { return !s.async && !s.text && !s.textContent; });
20+
script = scripts.slice(-1)[0];
21+
}
22+
23+
return script;
24+
};
25+
26+
const isLocalScript = function(script) {
27+
return /\/_dash-components-suite\//.test(script.src);
28+
};
29+
230
Object.defineProperty(__webpack_require__, 'p', {
331
get: (function () {
4-
let script = document.currentScript;
5-
if (!script) {
6-
/* Shim for IE11 and below */
7-
/* Do not take into account async scripts and inline scripts */
8-
const scripts = Array.from(document.getElementsByTagName('script')).filter(function(s) { return !s.async && !s.text && !s.textContent; });
9-
script = scripts.slice(-1)[0];
10-
}
32+
let script = getCurrentScript();
1133
1234
var url = script.src.split('/').slice(0, -1).join('/') + '/';
1335
1436
return function() {
1537
return url;
1638
};
1739
})()
18-
});`
40+
});
41+
42+
const __jsonpScriptSrc__ = jsonpScriptSrc;
43+
jsonpScriptSrc = function(chunkId) {
44+
let script = getCurrentScript();
45+
let isLocal = isLocalScript(script);
46+
47+
let src = __jsonpScriptSrc__(chunkId);
48+
49+
if(!isLocal) {
50+
return src;
51+
}
52+
53+
const srcFragments = src.split('/');
54+
const fileFragments = srcFragments.slice(-1)[0].split('.');
55+
56+
fileFragments.splice(1, 0, ${getFingerprint()});
57+
srcFragments.splice(-1, 1, fileFragments.join('.'))
58+
59+
return srcFragments.join('/');
60+
};
61+
`
1962

2063
class WebpackDashDynamicImport {
2164
apply(compiler) {
2265
compiler.hooks.compilation.tap('WebpackDashDynamicImport', compilation => {
2366
compilation.mainTemplate.hooks.requireExtensions.tap('WebpackDashDynamicImport > RequireExtensions', (source, chunk, hash) => {
2467
return [
2568
source,
26-
resolveImportSource
69+
resolveImportSource()
2770
]
2871
});
2972
});

Diff for: CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
55
## Unreleased
66
### Added
77
- [#964](https://github.com/plotly/dash/pull/964) Adds support for preventing
8-
updates in clientside functions.
8+
updates in clientside functions.
99
- Reject all updates with `throw window.dash_clientside.PreventUpdate;`
1010
- Reject a single output by returning `window.dash_clientside.no_update`
1111
- [#899](https://github.com/plotly/dash/pull/899) Add support for async dependencies and components
12+
- [#973](https://github.com/plotly/dash/pull/973) Adds support for resource caching and adds a fallback caching mechanism through etag
1213

1314
## [1.4.1] - 2019-10-17
1415
### Fixed

Diff for: dash/dash.py

+29-5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import dash_renderer
2525

2626
from .dependencies import Input, Output, State
27+
from .fingerprint import build_fingerprint, check_fingerprint
2728
from .resources import Scripts, Css
2829
from .development.base_component import Component, ComponentRegistry
2930
from . import exceptions
@@ -541,12 +542,14 @@ def _relative_url_path(relative_package_path="", namespace=""):
541542

542543
modified = int(os.stat(module_path).st_mtime)
543544

544-
return "{}_dash-component-suites/{}/{}?v={}&m={}".format(
545+
return "{}_dash-component-suites/{}/{}".format(
545546
self.config.requests_pathname_prefix,
546547
namespace,
547-
relative_package_path,
548-
importlib.import_module(namespace).__version__,
549-
modified,
548+
build_fingerprint(
549+
relative_package_path,
550+
importlib.import_module(namespace).__version__,
551+
modified,
552+
),
550553
)
551554

552555
srcs = []
@@ -676,6 +679,10 @@ def _generate_meta_html(self):
676679

677680
# Serve the JS bundles for each package
678681
def serve_component_suites(self, package_name, path_in_package_dist):
682+
path_in_package_dist, has_fingerprint = check_fingerprint(
683+
path_in_package_dist
684+
)
685+
679686
if package_name not in self.registered_paths:
680687
raise exceptions.DependencyException(
681688
"Error loading dependency.\n"
@@ -711,11 +718,28 @@ def serve_component_suites(self, package_name, path_in_package_dist):
711718
package.__path__,
712719
)
713720

714-
return flask.Response(
721+
response = flask.Response(
715722
pkgutil.get_data(package_name, path_in_package_dist),
716723
mimetype=mimetype,
717724
)
718725

726+
if has_fingerprint:
727+
# Fingerprinted resources are good forever (1 year)
728+
# No need for ETag as the fingerprint changes with each build
729+
response.cache_control.max_age = 31536000 # 1 year
730+
else:
731+
# Non-fingerprinted resources are given an ETag that
732+
# will be used / check on future requests
733+
response.add_etag()
734+
tag = response.get_etag()[0]
735+
736+
request_etag = flask.request.headers.get('If-None-Match')
737+
738+
if '"{}"'.format(tag) == request_etag:
739+
response = flask.Response(None, status=304)
740+
741+
return response
742+
719743
def index(self, *args, **kwargs): # pylint: disable=unused-argument
720744
scripts = self._generate_scripts_html()
721745
css = self._generate_css_dist_html()

Diff for: dash/fingerprint.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import re
2+
3+
build_regex = re.compile(r'^(?P<filename>[\w@-]+)(?P<extension>.*)$')
4+
5+
check_regex = re.compile(
6+
r'^(?P<filename>.*)[.]v[\w-]+m[0-9a-fA-F]+(?P<extension>(?:(?:(?<![.])[.])?[\w])+)$'
7+
)
8+
9+
10+
def build_fingerprint(path, version, hash_value):
11+
res = build_regex.match(path)
12+
13+
return '{}.v{}m{}{}'.format(
14+
res.group('filename'),
15+
str(version).replace('.', '_'),
16+
hash_value,
17+
res.group('extension'),
18+
)
19+
20+
21+
def check_fingerprint(path):
22+
# Check if the resource has a fingerprint
23+
res = check_regex.match(path)
24+
25+
# Resolve real resource name from fingerprinted resource path
26+
return (
27+
res.group('filename') + res.group('extension')
28+
if res is not None
29+
else path,
30+
res is not None,
31+
)

Diff for: tests/unit/test_fingerprint.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
2+
from dash.fingerprint import build_fingerprint, check_fingerprint
3+
4+
version = 1
5+
hash_value = 1
6+
7+
valid_resources = [
8+
{'path': '[email protected]', 'fingerprint': '[email protected]'},
9+
{'path': '[email protected]', 'fingerprint': '[email protected]_1_1m1234567890abcdef.8.6.min.js', 'version': '1.1.1', 'hash': '1234567890abcdef' },
10+
{'path': '[email protected]', 'fingerprint': '[email protected]_1_1-alpha_1m1234567890abcdef.8.6.min.js', 'version': '1.1.1-alpha.1', 'hash': '1234567890abcdef' },
11+
{'path': 'dash.plotly.js', 'fingerprint': 'dash.v1m1.plotly.js'},
12+
{'path': 'dash.plotly.j_s', 'fingerprint': 'dash.v1m1.plotly.j_s'},
13+
{'path': 'dash.plotly.css', 'fingerprint': 'dash.v1m1.plotly.css'},
14+
{'path': 'dash.plotly.xxx.yyy.zzz', 'fingerprint': 'dash.v1m1.plotly.xxx.yyy.zzz'}
15+
]
16+
17+
valid_fingerprints = [
18+
'[email protected]_1_2m1571771240.8.6.min.js',
19+
'dash.plotly.v1_1_1m1234567890.js',
20+
'dash.plotly.v1_1_1m1234567890.j_s',
21+
'dash.plotly.v1_1_1m1234567890.css',
22+
'dash.plotly.v1_1_1m1234567890.xxx.yyy.zzz',
23+
'dash.plotly.v1_1_1-alpha1m1234567890.js',
24+
'dash.plotly.v1_1_1-alpha_3m1234567890.js',
25+
'dash.plotly.v1_1_1m1234567890123.js',
26+
'dash.plotly.v1_1_1m4bc3.js'
27+
]
28+
29+
invalid_fingerprints = [
30+
'dash.plotly.v1_1_1m1234567890..js',
31+
'dash.plotly.v1_1_1m1234567890.',
32+
'dash.plotly.v1_1_1m1234567890..',
33+
'dash.plotly.v1_1_1m1234567890.js.',
34+
'dash.plotly.v1_1_1m1234567890.j-s'
35+
]
36+
37+
def test_fingerprint():
38+
for resource in valid_resources:
39+
# The fingerprint matches expectations
40+
fingerprint = build_fingerprint(resource.get('path'), resource.get('version', version), resource.get('hash', hash_value))
41+
assert fingerprint == resource.get('fingerprint')
42+
43+
(original_path, has_fingerprint) = check_fingerprint(fingerprint)
44+
# The inverse operation returns that the fingerprint was valid and the original path
45+
assert has_fingerprint
46+
assert original_path == resource.get('path')
47+
48+
for resource in valid_fingerprints:
49+
(_, has_fingerprint) = check_fingerprint(resource)
50+
assert has_fingerprint
51+
52+
for resource in invalid_fingerprints:
53+
(_, has_fingerprint) = check_fingerprint(resource)
54+
assert not has_fingerprint

Diff for: tests/unit/test_resources.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def test_external(mocker):
3737
mocker.patch("dash_core_components._js_dist")
3838
mocker.patch("dash_html_components._js_dist")
3939
dcc._js_dist = _monkey_patched_js_dist # noqa: W0212
40-
dcc.__version__ = 1
40+
dcc.__version__ = "1.0.0"
4141

4242
app = dash.Dash(
4343
__name__, assets_folder="tests/assets", assets_ignore="load_after.+.js"
@@ -66,7 +66,7 @@ def test_internal(mocker):
6666
mocker.patch("dash_core_components._js_dist")
6767
mocker.patch("dash_html_components._js_dist")
6868
dcc._js_dist = _monkey_patched_js_dist # noqa: W0212,
69-
dcc.__version__ = 1
69+
dcc.__version__ = "1.0.0"
7070

7171
app = dash.Dash(
7272
__name__, assets_folder="tests/assets", assets_ignore="load_after.+.js"
@@ -83,10 +83,10 @@ def test_internal(mocker):
8383

8484
assert resource == [
8585
"/_dash-component-suites/"
86-
"dash_core_components/external_javascript.js?v=1&m=1",
86+
"dash_core_components/external_javascript.v1_0_0m1.js",
8787
"/_dash-component-suites/"
88-
"dash_core_components/external_css.css?v=1&m=1",
89-
"/_dash-component-suites/" "dash_core_components/fake_dcc.js?v=1&m=1",
88+
"dash_core_components/external_css.v1_0_0m1.css",
89+
"/_dash-component-suites/" "dash_core_components/fake_dcc.v1_0_0m1.js",
9090
]
9191

9292
assert (

0 commit comments

Comments
 (0)