This repository has been archived by the owner on Sep 12, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathautoupdate_client.js
executable file
·200 lines (173 loc) · 6.76 KB
/
autoupdate_client.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
// Subscribe to the `meteor_autoupdate_clientVersions` collection,
// which contains the set of acceptable client versions.
//
// A "hard code push" occurs when the running client version is not in
// the set of acceptable client versions (or the server updates the
// collection, there is a published client version marked `current` and
// the running client version is no longer in the set).
//
// When the `reload` package is loaded, a hard code push causes
// the browser to reload, so that it will load the latest client
// version from the server.
//
// A "soft code push" represents the situation when the running client
// version is in the set of acceptable versions, but there is a newer
// version available on the server.
//
// `Autoupdate.newClientAvailable` is a reactive data source which
// becomes `true` if there is a new version of the client is available on
// the server.
//
// This package doesn't implement a soft code reload process itself,
// but `newClientAvailable` could be used for example to display a
// "click to reload" link to the user.
// The client version of the client code currently running in the
// browser.
import { ClientVersions } from "./client_versions.js";
const clientArch = Meteor.isCordova ? "web.cordova" :
Meteor.isModern ? "web.browser" : "web.browser.legacy";
const autoupdateVersions =
((__meteor_runtime_config__.autoupdate || {}).versions || {})[clientArch] || {
version: "unknown",
versionRefreshable: "unknown",
versionNonRefreshable: "unknown",
assets: [],
};
export const Autoupdate = {};
// Stores acceptable client versions.
const clientVersions =
Autoupdate._clientVersions = // Used by a self-test.
new ClientVersions();
Meteor.connection.registerStore(
"meteor_autoupdate_clientVersions",
clientVersions.createStore()
);
Autoupdate.newClientAvailable = function () {
return clientVersions.newClientAvailable(
clientArch,
["versionRefreshable", "versionNonRefreshable"],
autoupdateVersions
);
};
// Set to true if the link.onload callback ever fires for any <link> node.
let knownToSupportCssOnLoad = false;
const retry = new Retry({
// Unlike the stream reconnect use of Retry, which we want to be instant
// in normal operation, this is a wacky failure. We don't want to retry
// right away, we can start slowly.
//
// A better way than timeconstants here might be to use the knowledge
// of when we reconnect to help trigger these retries. Typically, the
// server fixing code will result in a restart and reconnect, but
// potentially the subscription could have a transient error.
minCount: 0, // don't do any immediate retries
baseTimeout: 30*1000 // start with 30s
});
let failures = 0;
Autoupdate._retrySubscription = () => {
Meteor.subscribe("meteor_autoupdate_clientVersions", {
onError(error) {
Meteor._debug("autoupdate subscription failed", error);
failures++;
retry.retryLater(failures, function () {
// Just retry making the subscription, don't reload the whole
// page. While reloading would catch more cases (for example,
// the server went back a version and is now doing old-style hot
// code push), it would also be more prone to reload loops,
// which look really bad to the user. Just retrying the
// subscription over DDP means it is at least possible to fix by
// updating the server.
Autoupdate._retrySubscription();
});
},
onReady() {
// Call checkNewVersionDocument with a slight delay, so that the
// const handle declaration is guaranteed to be initialized, even if
// the added or changed callbacks are called synchronously.
const resolved = Promise.resolve();
function check(doc) {
resolved.then(() => checkNewVersionDocument(doc));
}
const stop = clientVersions.watch(check);
function checkNewVersionDocument(doc) {
if (doc._id !== clientArch) {
return;
}
if (doc.versionNonRefreshable !==
autoupdateVersions.versionNonRefreshable) {
// Non-refreshable assets have changed, so we have to reload the
// whole page rather than just replacing <link> tags.
if (stop) stop();
if (Package.reload) {
// The reload package should be provided by ddp-client, which
// is provided by the ddp package that autoupdate depends on.
Package.reload.Reload._reload();
}
return;
}
if (doc.versionRefreshable !== autoupdateVersions.versionRefreshable) {
autoupdateVersions.versionRefreshable = doc.versionRefreshable;
// Switch out old css links for the new css links. Inspired by:
// https://github.com/guard/guard-livereload/blob/master/js/livereload.js#L710
var newCss = doc.assets || [];
var oldLinks = [];
Array.prototype.forEach.call(
document.getElementsByTagName('link'),
function (link) {
if (link.className === '__meteor-css__') {
oldLinks.push(link);
}
}
);
function waitUntilCssLoads(link, callback) {
var called;
link.onload = function () {
knownToSupportCssOnLoad = true;
if (! called) {
called = true;
callback();
}
};
if (! knownToSupportCssOnLoad) {
var id = Meteor.setInterval(function () {
if (link.sheet) {
if (! called) {
called = true;
callback();
}
Meteor.clearInterval(id);
}
}, 50);
}
}
let newLinksLeftToLoad = newCss.length;
function removeOldLinks() {
if (oldLinks.length > 0 &&
--newLinksLeftToLoad < 1) {
oldLinks.splice(0).forEach(link => {
link.parentNode.removeChild(link);
});
}
}
if (newCss.length > 0) {
newCss.forEach(css => {
const newLink = document.createElement("link");
newLink.setAttribute("rel", "stylesheet");
newLink.setAttribute("type", "text/css");
newLink.setAttribute("class", "__meteor-css__");
newLink.setAttribute("href", css.url);
waitUntilCssLoads(newLink, function () {
Meteor.setTimeout(removeOldLinks, 200);
});
const head = document.getElementsByTagName("head").item(0);
head.appendChild(newLink);
});
} else {
removeOldLinks();
}
}
}
}
});
};
Autoupdate._retrySubscription();