├── README.md ├── app ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── packages │ ├── platforms │ ├── release │ └── versions ├── autoupdate-change.html ├── autoupdate-change.js ├── mobile-config.js ├── packages │ └── autoupdate │ │ ├── autoupdate_client.js │ │ ├── autoupdate_cordova.js │ │ ├── autoupdate_server.js │ │ └── package.js ├── run-as-mobile-app.sh └── run-as-web-app.sh └── remote-server ├── .meteor ├── .finished-upgraders ├── .gitignore ├── .id ├── packages ├── platforms ├── release └── versions ├── remote-test-server.js └── run.sh /README.md: -------------------------------------------------------------------------------- 1 | # This is now outdated after Meteor 1.3 2 | 3 | You can track the following issue [here](https://github.com/meteor/meteor/issues/3815#issuecomment-200954639) to test/see new workarounds. 4 | 5 | ## Meteor App With Different Meteor Server 6 | 7 | A lot of people wanted to know how to develop a Meteor app (web or mobile) that uses a different Meteor server backend. This is how. 8 | 9 | For mobile, I only tested using Android, though this should theoretically work for iOS as well. If you don't know already [here's how to get mobile set up for Meteor](https://www.meteor.com/tutorials/blaze/running-on-mobile). 10 | 11 | There's two basic Meteor apps in here. 12 | 13 | To demonstrate how this works, the `remote-server` Meteor app will act as the remote Meteor server (running on `localhost:4000`) and the `app` will be your web or mobile app (running on `localhost:3000`), which will connect to your `remote-server` Meteor server. Also, I demonstrate user authentication using `accounts-password` to the `remote-server`. 14 | 15 | ## Getting Started 16 | 17 | Open up two terminals. 18 | 19 | ``` 20 | git clone https://github.com/rclai/meteor-app-with-remote-server.git 21 | cd meteor-app-with-remote-server 22 | ``` 23 | 24 | #### Terminal 1: Starting the "remote" server. 25 | 26 | ``` 27 | cd remote-server 28 | sh run.sh 29 | ``` 30 | 31 | #### Terminal 2: Starting your client/mobile app. 32 | 33 | `cd app` 34 | 35 | If you want to run as a web app: 36 | 37 | `sh run-as-web-app.sh` 38 | 39 | If you want to run as a mobile app: 40 | 41 | Take your machine's IP address (by using `ifconfig` or equivalent), and set it in the `--mobile-server` flag in the `run-as-mobile-app.sh` file, making sure it is port __4000__. 42 | 43 | Modify `packages/autoupdate/autoupdate_cordova.js` line 15, and change `rootUrl` to use your IP address, making sure it is port __3000__. 44 | 45 | Make sure your phone is connected to the same network as your machine (WiFi connection most likely, unless your phone uses an Ethernet cable, which would be mind-blowing). 46 | 47 | Finally: 48 | 49 | `sh run-as-mobile-app.sh` 50 | 51 | Test out hot code reload and see it just work. 52 | 53 | ## How Does This Work? 54 | 55 | I copied the [Meteor `autoupdate`](https://github.com/meteor/meteor/tree/master/packages/autoupdate) core package and modified it to make sure it uses the `app`'s DDP connection because by default, Meteor uses the same URL as `DDP_DEFAULT_CONNECTION_URL` for handling hot code reloads. 56 | 57 | For Cordova, I also had to make sure that the download URL pointed to my machine's IP address with port 3000 (See [here](https://github.com/rclai/meteor-app-with-remote-server/blob/master/app/packages/autoupdate/autoupdate_cordova.js#L15)). 58 | 59 | Eventually Meteor will hopefully give you the ability to pass in an explicit autoupdate URL. Here's the [issue](https://github.com/meteor/meteor/issues/3815). 60 | -------------------------------------------------------------------------------- /app/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | -------------------------------------------------------------------------------- /app/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /app/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | kaii8agk0rogtm9o7u 8 | -------------------------------------------------------------------------------- /app/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-platform 8 | accounts-password 9 | -------------------------------------------------------------------------------- /app/.meteor/platforms: -------------------------------------------------------------------------------- 1 | android 2 | browser 3 | server 4 | -------------------------------------------------------------------------------- /app/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.1.0.2 2 | -------------------------------------------------------------------------------- /app/.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.0 2 | accounts-password@1.1.1 3 | autoupdate@1.2.1 4 | base64@1.0.3 5 | binary-heap@1.0.3 6 | blaze@2.1.2 7 | blaze-tools@1.0.3 8 | boilerplate-generator@1.0.3 9 | callback-hook@1.0.3 10 | check@1.0.5 11 | ddp@1.1.0 12 | deps@1.0.7 13 | ejson@1.0.6 14 | email@1.0.6 15 | fastclick@1.0.3 16 | geojson-utils@1.0.3 17 | html-tools@1.0.4 18 | htmljs@1.0.4 19 | http@1.1.0 20 | id-map@1.0.3 21 | jquery@1.11.3_2 22 | json@1.0.3 23 | launch-screen@1.0.2 24 | livedata@1.0.13 25 | localstorage@1.0.3 26 | logging@1.0.7 27 | meteor@1.1.6 28 | meteor-platform@1.2.2 29 | minifiers@1.1.5 30 | minimongo@1.0.8 31 | mobile-status-bar@1.0.3 32 | mongo@1.1.0 33 | npm-bcrypt@0.7.8_2 34 | observe-sequence@1.0.6 35 | ordered-dict@1.0.3 36 | random@1.0.3 37 | reactive-dict@1.1.0 38 | reactive-var@1.0.5 39 | reload@1.1.3 40 | retry@1.0.3 41 | routepolicy@1.0.5 42 | service-configuration@1.0.4 43 | session@1.1.0 44 | sha@1.0.3 45 | spacebars@1.0.6 46 | spacebars-compiler@1.0.6 47 | srp@1.0.3 48 | templating@1.1.1 49 | tracker@1.0.7 50 | ui@1.0.6 51 | underscore@1.0.3 52 | url@1.0.4 53 | webapp@1.2.0 54 | webapp-hashing@1.0.3 55 | -------------------------------------------------------------------------------- /app/autoupdate-change.html: -------------------------------------------------------------------------------- 1 | 2 | Remote Meteor Server 3 | 4 | 5 | 6 | {{> hello}} 7 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /app/autoupdate-change.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isClient) { 2 | Test = new Mongo.Collection('test'); 3 | 4 | Meteor.subscribe('test') 5 | 6 | Template.hello.helpers({ 7 | test: function () { 8 | return Test.findOne(); 9 | } 10 | }); 11 | 12 | Template.hello.events({ 13 | 'click button.login': function () { 14 | Meteor.loginWithPassword('bob', 'test'); 15 | }, 16 | 'click button.logout': function () { 17 | Meteor.logout(); 18 | } 19 | }); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/mobile-config.js: -------------------------------------------------------------------------------- 1 | // Used in order to not get browser cross browser origin errors 2 | // Typically you want to get more specific than this 3 | App.accessRule("*"); 4 | -------------------------------------------------------------------------------- /app/packages/autoupdate/autoupdate_client.js: -------------------------------------------------------------------------------- 1 | // Subscribe to the `meteor_autoupdate_clientVersions` collection, 2 | // which contains the set of acceptable client versions. 3 | // 4 | // A "hard code push" occurs when the running client version is not in 5 | // the set of acceptable client versions (or the server updates the 6 | // collection, there is a published client version marked `current` and 7 | // the running client version is no longer in the set). 8 | // 9 | // When the `reload` package is loaded, a hard code push causes 10 | // the browser to reload, so that it will load the latest client 11 | // version from the server. 12 | // 13 | // A "soft code push" represents the situation when the running client 14 | // version is in the set of acceptable versions, but there is a newer 15 | // version available on the server. 16 | // 17 | // `Autoupdate.newClientAvailable` is a reactive data source which 18 | // becomes `true` if there is a new version of the client is available on 19 | // the server. 20 | // 21 | // This package doesn't implement a soft code reload process itself, 22 | // but `newClientAvailable` could be used for example to display a 23 | // "click to reload" link to the user. 24 | 25 | // The client version of the client code currently running in the 26 | // browser. 27 | var autoupdateVersion = __meteor_runtime_config__.autoupdateVersion || "unknown"; 28 | var autoupdateVersionRefreshable = 29 | __meteor_runtime_config__.autoupdateVersionRefreshable || "unknown"; 30 | 31 | var LocalDDPConnection = DDP.connect('http://localhost:3000'); 32 | 33 | // The collection of acceptable client versions. 34 | ClientVersions = new Mongo.Collection("meteor_autoupdate_clientVersions", { connection: LocalDDPConnection }); 35 | 36 | Autoupdate = {}; 37 | 38 | Autoupdate.newClientAvailable = function () { 39 | return !! ClientVersions.findOne({ 40 | _id: "version", 41 | version: {$ne: autoupdateVersion} }) || 42 | !! ClientVersions.findOne({ 43 | _id: "version-refreshable", 44 | version: {$ne: autoupdateVersionRefreshable} }); 45 | }; 46 | Autoupdate._ClientVersions = ClientVersions; // Used by a self-test 47 | 48 | var knownToSupportCssOnLoad = false; 49 | 50 | var retry = new Retry({ 51 | // Unlike the stream reconnect use of Retry, which we want to be instant 52 | // in normal operation, this is a wacky failure. We don't want to retry 53 | // right away, we can start slowly. 54 | // 55 | // A better way than timeconstants here might be to use the knowledge 56 | // of when we reconnect to help trigger these retries. Typically, the 57 | // server fixing code will result in a restart and reconnect, but 58 | // potentially the subscription could have a transient error. 59 | minCount: 0, // don't do any immediate retries 60 | baseTimeout: 30*1000 // start with 30s 61 | }); 62 | var failures = 0; 63 | 64 | Autoupdate._retrySubscription = function () { 65 | LocalDDPConnection.subscribe("meteor_autoupdate_clientVersions", { 66 | onError: function (error) { 67 | Meteor._debug("autoupdate subscription failed:", error); 68 | failures++; 69 | retry.retryLater(failures, function () { 70 | // Just retry making the subscription, don't reload the whole 71 | // page. While reloading would catch more cases (for example, 72 | // the server went back a version and is now doing old-style hot 73 | // code push), it would also be more prone to reload loops, 74 | // which look really bad to the user. Just retrying the 75 | // subscription over DDP means it is at least possible to fix by 76 | // updating the server. 77 | Autoupdate._retrySubscription(); 78 | }); 79 | }, 80 | onReady: function () { 81 | if (Package.reload) { 82 | var checkNewVersionDocument = function (doc) { 83 | var self = this; 84 | if (doc._id === 'version-refreshable' && 85 | doc.version !== autoupdateVersionRefreshable) { 86 | autoupdateVersionRefreshable = doc.version; 87 | // Switch out old css links for the new css links. Inspired by: 88 | // https://github.com/guard/guard-livereload/blob/master/js/livereload.js#L710 89 | var newCss = (doc.assets && doc.assets.allCss) || []; 90 | var oldLinks = []; 91 | _.each(document.getElementsByTagName('link'), function (link) { 92 | if (link.className === '__meteor-css__') { 93 | oldLinks.push(link); 94 | } 95 | }); 96 | 97 | var waitUntilCssLoads = function (link, callback) { 98 | var executeCallback = _.once(callback); 99 | link.onload = function () { 100 | knownToSupportCssOnLoad = true; 101 | executeCallback(); 102 | }; 103 | if (! knownToSupportCssOnLoad) { 104 | var id = Meteor.setInterval(function () { 105 | if (link.sheet) { 106 | executeCallback(); 107 | Meteor.clearInterval(id); 108 | } 109 | }, 50); 110 | } 111 | }; 112 | 113 | var removeOldLinks = _.after(newCss.length, function () { 114 | _.each(oldLinks, function (oldLink) { 115 | oldLink.parentNode.removeChild(oldLink); 116 | }); 117 | }); 118 | 119 | var attachStylesheetLink = function (newLink) { 120 | document.getElementsByTagName("head").item(0).appendChild(newLink); 121 | 122 | waitUntilCssLoads(newLink, function () { 123 | Meteor.setTimeout(removeOldLinks, 200); 124 | }); 125 | }; 126 | 127 | if (newCss.length !== 0) { 128 | _.each(newCss, function (css) { 129 | var newLink = document.createElement("link"); 130 | newLink.setAttribute("rel", "stylesheet"); 131 | newLink.setAttribute("type", "text/css"); 132 | newLink.setAttribute("class", "__meteor-css__"); 133 | newLink.setAttribute("href", Meteor._relativeToSiteRootUrl(css.url)); 134 | attachStylesheetLink(newLink); 135 | }); 136 | } else { 137 | removeOldLinks(); 138 | } 139 | 140 | } 141 | else if (doc._id === 'version' && doc.version !== autoupdateVersion) { 142 | handle && handle.stop(); 143 | Package.reload.Reload._reload(); 144 | } 145 | }; 146 | 147 | var handle = ClientVersions.find().observe({ 148 | added: checkNewVersionDocument, 149 | changed: checkNewVersionDocument 150 | }); 151 | } 152 | } 153 | }); 154 | }; 155 | Autoupdate._retrySubscription(); 156 | -------------------------------------------------------------------------------- /app/packages/autoupdate/autoupdate_cordova.js: -------------------------------------------------------------------------------- 1 | var DEBUG_TAG = 'METEOR CORDOVA DEBUG (autoupdate_cordova.js) '; 2 | var log = function (msg) { 3 | console.log(DEBUG_TAG + msg); 4 | }; 5 | 6 | // This constant was picked by testing on iOS 7.1 7 | // We limit the number of concurrent downloads because iOS gets angry on the 8 | // application when a certain limit is exceeded and starts timing-out the 9 | // connections in 1-2 minutes which makes the whole HCP really slow. 10 | var MAX_NUM_CONCURRENT_DOWNLOADS = 30; 11 | var MAX_RETRY_COUNT = 5; 12 | 13 | var autoupdateVersionCordova = __meteor_runtime_config__.autoupdateVersionCordova || "unknown"; 14 | 15 | var rootUrl = 'http://10.10.10.125:3000/'; 16 | var LocalDDPConnection = DDP.connect(rootUrl); 17 | 18 | // The collection of acceptable client versions. 19 | ClientVersions = new Mongo.Collection("meteor_autoupdate_clientVersions", { connection: LocalDDPConnection }); 20 | 21 | Autoupdate = {}; 22 | 23 | Autoupdate.newClientAvailable = function () { 24 | return !! ClientVersions.findOne({ 25 | _id: 'version-cordova', 26 | version: {$ne: autoupdateVersionCordova} 27 | }); 28 | }; 29 | 30 | var writeFile = function (directoryPath, fileName, content, cb) { 31 | var fail = function (err) { 32 | cb(new Error("Failed to write file: ", err), null); 33 | }; 34 | 35 | window.resolveLocalFileSystemURL(directoryPath, function (dirEntry) { 36 | var success = function (fileEntry) { 37 | fileEntry.createWriter(function (writer) { 38 | writer.onwrite = function (evt) { 39 | var result = evt.target.result; 40 | cb(null, result); 41 | }; 42 | writer.onerror = fail; 43 | writer.write(content); 44 | }, fail); 45 | }; 46 | 47 | dirEntry.getFile(fileName, { 48 | create: true, 49 | exclusive: false 50 | }, success, fail); 51 | }, fail); 52 | }; 53 | 54 | var restartServer = function (location) { 55 | log('restartServer with location ' + location); 56 | var fail = function (err) { log("Unexpected error in restartServer: " + err.message) }; 57 | var httpd = cordova && cordova.plugins && cordova.plugins.CordovaUpdate; 58 | 59 | if (! httpd) { 60 | fail(new Error('no httpd')); 61 | return; 62 | } 63 | 64 | var startServer = function (cordovajsRoot) { 65 | httpd.startServer({ 66 | 'www_root' : location, 67 | 'cordovajs_root': cordovajsRoot 68 | }, function (url) { 69 | Package.reload.Reload._reload(); 70 | }, fail); 71 | }; 72 | 73 | httpd.getCordovajsRoot(function (cordovajsRoot) { 74 | startServer(cordovajsRoot); 75 | }, fail); 76 | }; 77 | 78 | var hasCalledReload = false; 79 | var updating = false; 80 | var localPathPrefix = null; 81 | 82 | var onNewVersion = function () { 83 | var ft = new FileTransfer(); 84 | var urlPrefix = rootUrl + '__cordova'; 85 | 86 | HTTP.get(urlPrefix + '/manifest.json', function (err, res) { 87 | if (err || ! res.data) { 88 | log('Failed to download the manifest ' + (err && err.message) + ' ' + (res && res.content)); 89 | return; 90 | } 91 | 92 | updating = true; 93 | ensureLocalPathPrefix(_.bind(downloadNewVersion, null, res.data)); 94 | }); 95 | }; 96 | 97 | var downloadNewVersion = function (program) { 98 | var urlPrefix = rootUrl + '__cordova'; 99 | var manifest = _.clone(program.manifest); 100 | var version = program.version; 101 | var ft = new FileTransfer(); 102 | 103 | manifest.push({ url: '/index.html?' + Random.id() }); 104 | 105 | var versionPrefix = localPathPrefix + version; 106 | 107 | var queue = []; 108 | _.each(manifest, function (item) { 109 | if (! item.url) return; 110 | 111 | var url = item.url; 112 | url = url.replace(/\?.+$/, ''); 113 | 114 | queue.push(url); 115 | }); 116 | 117 | var afterAllFilesDownloaded = _.after(queue.length, function () { 118 | var wroteManifest = function (err) { 119 | if (err) { 120 | log("Failed to write manifest.json: " + err); 121 | // XXX do something smarter? 122 | return; 123 | } 124 | 125 | // success! downloaded all sources and saved the manifest 126 | // save the version string for atomicity 127 | writeFile(localPathPrefix, 'version', version, function (err) { 128 | if (err) { 129 | log("Failed to write version: " + err); 130 | return; 131 | } 132 | 133 | // don't call reload twice! 134 | if (! hasCalledReload) { 135 | var location = uriToPath(localPathPrefix + version); 136 | restartServer(location); 137 | } 138 | }); 139 | }; 140 | 141 | writeFile(versionPrefix, 'manifest.json', 142 | JSON.stringify(program, undefined, 2), wroteManifest); 143 | }); 144 | 145 | var downloadUrl = function (url) { 146 | console.log(DEBUG_TAG + "start downloading " + url); 147 | // Add a cache buster to ensure that we don't cache an old asset. 148 | var uri = encodeURI(urlPrefix + url + '?' + Random.id()); 149 | 150 | // Try to download the file a few times. 151 | var tries = 0; 152 | var tryDownload = function () { 153 | ft.download(uri, versionPrefix + encodeURI(url), function (entry) { 154 | if (entry) { 155 | console.log(DEBUG_TAG + "done downloading " + url); 156 | // start downloading next queued url 157 | if (queue.length) 158 | downloadUrl(queue.shift()); 159 | afterAllFilesDownloaded(); 160 | } 161 | }, function (err) { 162 | // It failed, try again if we have tried less than 5 times. 163 | if (tries++ < MAX_RETRY_COUNT) { 164 | log("Download error, will retry (#" + tries + "): " + uri); 165 | tryDownload(); 166 | } else { 167 | log('Download failed: ' + JSON.stringify(err) + ", source=" + err.source + ", target=" + err.target); 168 | } 169 | }); 170 | }; 171 | 172 | tryDownload(); 173 | }; 174 | 175 | _.times(Math.min(MAX_NUM_CONCURRENT_DOWNLOADS, queue.length), function () { 176 | var nextUrl = queue.shift(); 177 | // XXX defer the next download so iOS doesn't rate limit us on concurrent 178 | // downloads 179 | Meteor.setTimeout(downloadUrl.bind(null, nextUrl), 50); 180 | }); 181 | }; 182 | 183 | var retry = new Retry({ 184 | minCount: 0, // don't do any immediate retries 185 | baseTimeout: 30*1000 // start with 30s 186 | }); 187 | var failures = 0; 188 | 189 | Autoupdate._retrySubscription = function () { 190 | var appId = __meteor_runtime_config__.appId; 191 | LocalDDPConnection.subscribe("meteor_autoupdate_clientVersions", appId, { 192 | onError: function (err) { 193 | Meteor._debug("autoupdate subscription failed:", err); 194 | failures++; 195 | retry.retryLater(failures, function () { 196 | // Just retry making the subscription, don't reload the whole 197 | // page. While reloading would catch more cases (for example, 198 | // the server went back a version and is now doing old-style hot 199 | // code push), it would also be more prone to reload loops, 200 | // which look really bad to the user. Just retrying the 201 | // subscription over DDP means it is at least possible to fix by 202 | // updating the server. 203 | Autoupdate._retrySubscription(); 204 | }); 205 | } 206 | }); 207 | if (Package.reload) { 208 | var checkNewVersionDocument = function (doc) { 209 | var self = this; 210 | if (doc.version !== autoupdateVersionCordova) { 211 | onNewVersion(); 212 | } 213 | }; 214 | 215 | var handle = ClientVersions.find({ 216 | _id: 'version-cordova' 217 | }).observe({ 218 | added: checkNewVersionDocument, 219 | changed: checkNewVersionDocument 220 | }); 221 | } 222 | }; 223 | 224 | Meteor.startup(function () { 225 | clearAutoupdateCache(autoupdateVersionCordova); 226 | }); 227 | Meteor.startup(Autoupdate._retrySubscription); 228 | 229 | 230 | // A helper that removes old directories left from previous autoupdates 231 | var clearAutoupdateCache = function (currentVersion) { 232 | ensureLocalPathPrefix(function () { 233 | // Try to clean up our cache directory, make sure to scan the directory 234 | // *before* loading the actual app. This ordering will prevent race 235 | // conditions when the app code tries to download a new version before 236 | // the old-cache removal has scanned the cache folder. 237 | listDirectory(localPathPrefix, {dirsOnly: true}, function (err, names) { 238 | // Couldn't get the list of dirs or risking to get into a race with an 239 | // on-going update to disk. 240 | if (err || updating) { 241 | return; 242 | } 243 | 244 | _.each(names, function (name) { 245 | // Skip the folder with the latest version 246 | if (name === currentVersion) 247 | return; 248 | 249 | // remove everything else, as we don't want to keep too much cache 250 | // around on disk 251 | removeDirectory(localPathPrefix + name + '/', function (err) { 252 | if (err) { 253 | log('Failed to remove an old cache folder ' 254 | + name + ':' + err.message); 255 | } else { 256 | log('Successfully removed an old cache folder ' + name); 257 | } 258 | }); 259 | }); 260 | }); 261 | }) 262 | }; 263 | 264 | // Cordova File plugin helpers 265 | var listDirectory = function (url, options, cb) { 266 | if (typeof options === 'function') 267 | cb = options, options = {}; 268 | 269 | var fail = function (err) { cb(err); }; 270 | window.resolveLocalFileSystemURL(url, function (entry) { 271 | var reader = entry.createReader(); 272 | reader.readEntries(function (entries) { 273 | var names = []; 274 | _.each(entries, function (entry) { 275 | if (! options.dirsOnly || entry.isDirectory) 276 | names.push(entry.name); 277 | }); 278 | cb(null, names); 279 | }, fail); 280 | }, fail); 281 | }; 282 | 283 | var removeDirectory = function (url, cb) { 284 | var fail = function (err) { 285 | cb(err); 286 | }; 287 | window.resolveLocalFileSystemURL(url, function (entry) { 288 | entry.removeRecursively(function () { cb(); }, fail); 289 | }, fail); 290 | }; 291 | 292 | var uriToPath = function (uri) { 293 | return decodeURI(uri).replace(/^file:\/\//g, ''); 294 | }; 295 | 296 | var ensureLocalPathPrefix = function (cb) { 297 | if (! localPathPrefix) { 298 | if (! cordova.file.dataDirectory) { 299 | // Since ensureLocalPathPrefix function is always called on 300 | // Meteor.startup, all Cordova plugins should be ready. 301 | // XXX Experiments have shown that it is not always the case, even when 302 | // the cordova.file symbol is attached, properties like dataDirectory 303 | // still can be null. Poll until we are sure the property is attached. 304 | console.log(DEBUG_TAG + 'cordova.file.dataDirectory is null, retrying in 20ms'); 305 | Meteor.setTimeout(_.bind(ensureLocalPathPrefix, null, cb), 20); 306 | } else { 307 | localPathPrefix = cordova.file.dataDirectory + 'meteor/'; 308 | cb(); 309 | } 310 | } else { 311 | cb(); 312 | } 313 | }; 314 | -------------------------------------------------------------------------------- /app/packages/autoupdate/autoupdate_server.js: -------------------------------------------------------------------------------- 1 | // Publish the current client versions to the client. When a client 2 | // sees the subscription change and that there is a new version of the 3 | // client available on the server, it can reload. 4 | // 5 | // By default there are two current client versions. The refreshable client 6 | // version is identified by a hash of the client resources seen by the browser 7 | // that are refreshable, such as CSS, while the non refreshable client version 8 | // is identified by a hash of the rest of the client assets 9 | // (the HTML, code, and static files in the `public` directory). 10 | // 11 | // If the environment variable `AUTOUPDATE_VERSION` is set it will be 12 | // used as the client id instead. You can use this to control when 13 | // the client reloads. For example, if you want to only force a 14 | // reload on major changes, you can use a custom AUTOUPDATE_VERSION 15 | // which you only change when something worth pushing to clients 16 | // immediately happens. 17 | // 18 | // The server publishes a `meteor_autoupdate_clientVersions` 19 | // collection. There are two documents in this collection, a document 20 | // with _id 'version' which represnets the non refreshable client assets, 21 | // and a document with _id 'version-refreshable' which represents the 22 | // refreshable client assets. Each document has a 'version' field 23 | // which is equivalent to the hash of the relevant assets. The refreshable 24 | // document also contains a list of the refreshable assets, so that the client 25 | // can swap in the new assets without forcing a page refresh. Clients can 26 | // observe changes on these documents to detect when there is a new 27 | // version available. 28 | // 29 | // In this implementation only two documents are present in the collection 30 | // the current refreshable client version and the current nonRefreshable client 31 | // version. Developers can easily experiment with different versioning and 32 | // updating models by forking this package. 33 | 34 | var Future = Npm.require("fibers/future"); 35 | 36 | Autoupdate = {}; 37 | 38 | // The collection of acceptable client versions. 39 | ClientVersions = new Mongo.Collection("meteor_autoupdate_clientVersions", 40 | { connection: null }); 41 | 42 | // The client hash includes __meteor_runtime_config__, so wait until 43 | // all packages have loaded and have had a chance to populate the 44 | // runtime config before using the client hash as our default auto 45 | // update version id. 46 | 47 | // Note: Tests allow people to override Autoupdate.autoupdateVersion before 48 | // startup. 49 | Autoupdate.autoupdateVersion = null; 50 | Autoupdate.autoupdateVersionRefreshable = null; 51 | Autoupdate.autoupdateVersionCordova = null; 52 | Autoupdate.appId = __meteor_runtime_config__.appId = process.env.APP_ID; 53 | 54 | var syncQueue = new Meteor._SynchronousQueue(); 55 | 56 | // updateVersions can only be called after the server has fully loaded. 57 | var updateVersions = function (shouldReloadClientProgram) { 58 | // Step 1: load the current client program on the server and update the 59 | // hash values in __meteor_runtime_config__. 60 | if (shouldReloadClientProgram) { 61 | WebAppInternals.reloadClientPrograms(); 62 | } 63 | 64 | // If we just re-read the client program, or if we don't have an autoupdate 65 | // version, calculate it. 66 | if (shouldReloadClientProgram || Autoupdate.autoupdateVersion === null) { 67 | Autoupdate.autoupdateVersion = 68 | process.env.AUTOUPDATE_VERSION || 69 | WebApp.calculateClientHashNonRefreshable(); 70 | } 71 | // If we just recalculated it OR if it was set by (eg) test-in-browser, 72 | // ensure it ends up in __meteor_runtime_config__. 73 | __meteor_runtime_config__.autoupdateVersion = 74 | Autoupdate.autoupdateVersion; 75 | 76 | Autoupdate.autoupdateVersionRefreshable = 77 | __meteor_runtime_config__.autoupdateVersionRefreshable = 78 | process.env.AUTOUPDATE_VERSION || 79 | WebApp.calculateClientHashRefreshable(); 80 | 81 | Autoupdate.autoupdateVersionCordova = 82 | __meteor_runtime_config__.autoupdateVersionCordova = 83 | process.env.AUTOUPDATE_VERSION || 84 | WebApp.calculateClientHashCordova(); 85 | 86 | // Step 2: form the new client boilerplate which contains the updated 87 | // assets and __meteor_runtime_config__. 88 | if (shouldReloadClientProgram) { 89 | WebAppInternals.generateBoilerplate(); 90 | } 91 | 92 | // XXX COMPAT WITH 0.8.3 93 | if (! ClientVersions.findOne({current: true})) { 94 | // To ensure apps with version of Meteor prior to 0.9.0 (in 95 | // which the structure of documents in `ClientVersions` was 96 | // different) also reload. 97 | ClientVersions.insert({current: true}); 98 | } 99 | 100 | if (! ClientVersions.findOne({_id: "version"})) { 101 | ClientVersions.insert({ 102 | _id: "version", 103 | version: Autoupdate.autoupdateVersion 104 | }); 105 | } else { 106 | ClientVersions.update("version", { $set: { 107 | version: Autoupdate.autoupdateVersion 108 | }}); 109 | } 110 | 111 | if (! ClientVersions.findOne({_id: "version-cordova"})) { 112 | ClientVersions.insert({ 113 | _id: "version-cordova", 114 | version: Autoupdate.autoupdateVersionCordova, 115 | refreshable: false 116 | }); 117 | } else { 118 | ClientVersions.update("version-cordova", { $set: { 119 | version: Autoupdate.autoupdateVersionCordova 120 | }}); 121 | } 122 | 123 | // Use `onListening` here because we need to use 124 | // `WebAppInternals.refreshableAssets`, which is only set after 125 | // `WebApp.generateBoilerplate` is called by `main` in webapp. 126 | WebApp.onListening(function () { 127 | if (! ClientVersions.findOne({_id: "version-refreshable"})) { 128 | ClientVersions.insert({ 129 | _id: "version-refreshable", 130 | version: Autoupdate.autoupdateVersionRefreshable, 131 | assets: WebAppInternals.refreshableAssets 132 | }); 133 | } else { 134 | ClientVersions.update("version-refreshable", { $set: { 135 | version: Autoupdate.autoupdateVersionRefreshable, 136 | assets: WebAppInternals.refreshableAssets 137 | }}); 138 | } 139 | }); 140 | }; 141 | 142 | Meteor.publish( 143 | "meteor_autoupdate_clientVersions", 144 | function (appId) { 145 | // `null` happens when a client doesn't have an appId and passes 146 | // `undefined` to `Meteor.subscribe`. `undefined` is translated to 147 | // `null` as JSON doesn't have `undefined. 148 | check(appId, Match.OneOf(String, undefined, null)); 149 | 150 | // Don't notify clients using wrong appId such as mobile apps built with a 151 | // different server but pointing at the same local url 152 | if (Autoupdate.appId && appId && Autoupdate.appId !== appId) 153 | return []; 154 | 155 | return ClientVersions.find(); 156 | }, 157 | {is_auto: true} 158 | ); 159 | 160 | Meteor.startup(function () { 161 | updateVersions(false); 162 | }); 163 | 164 | var fut = new Future(); 165 | 166 | // We only want 'refresh' to trigger 'updateVersions' AFTER onListen, 167 | // so we add a queued task that waits for onListen before 'refresh' can queue 168 | // tasks. Note that the `onListening` callbacks do not fire until after 169 | // Meteor.startup, so there is no concern that the 'updateVersions' calls from 170 | // 'refresh' will overlap with the `updateVersions` call from Meteor.startup. 171 | 172 | syncQueue.queueTask(function () { 173 | fut.wait(); 174 | }); 175 | 176 | WebApp.onListening(function () { 177 | fut.return(); 178 | }); 179 | 180 | var enqueueVersionsRefresh = function () { 181 | syncQueue.queueTask(function () { 182 | updateVersions(true); 183 | }); 184 | }; 185 | 186 | // Listen for the special {refresh: 'client'} message, which signals that a 187 | // client asset has changed. 188 | process.on('message', Meteor.bindEnvironment(function (m) { 189 | if (m && m.refresh === 'client') { 190 | enqueueVersionsRefresh(); 191 | } 192 | })); 193 | 194 | // Another way to tell the process to refresh: send SIGHUP signal 195 | process.on('SIGHUP', Meteor.bindEnvironment(function () { 196 | enqueueVersionsRefresh(); 197 | })); 198 | -------------------------------------------------------------------------------- /app/packages/autoupdate/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: "Update the client when new client code is available", 3 | version: '1.2.1', 4 | name: 'autoupdate' 5 | }); 6 | 7 | Cordova.depends({ 8 | 'org.apache.cordova.file': '1.3.3', 9 | 'org.apache.cordova.file-transfer': '0.5.0' 10 | }); 11 | 12 | Package.onUse(function (api) { 13 | api.use('webapp', 'server'); 14 | api.use(['tracker', 'retry'], 'client'); 15 | api.use(['ddp', 'mongo', 'underscore'], ['client', 'server']); 16 | api.use('tracker', 'client'); 17 | api.use('reload', 'client', {weak: true}); 18 | api.use(['http', 'random'], 'web.cordova'); 19 | 20 | api.export('Autoupdate'); 21 | api.addFiles('autoupdate_server.js', 'server'); 22 | api.addFiles('autoupdate_client.js', 'web.browser'); 23 | api.addFiles('autoupdate_cordova.js', 'web.cordova'); 24 | }); 25 | -------------------------------------------------------------------------------- /app/run-as-mobile-app.sh: -------------------------------------------------------------------------------- 1 | meteor run android-device --mobile-server 10.10.10.125:4000 2 | -------------------------------------------------------------------------------- /app/run-as-web-app.sh: -------------------------------------------------------------------------------- 1 | export DDP_DEFAULT_CONNECTION_URL=http://localhost:4000 2 | meteor run 3 | -------------------------------------------------------------------------------- /remote-server/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | -------------------------------------------------------------------------------- /remote-server/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /remote-server/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1mzrqxnm56d30b61sl 8 | -------------------------------------------------------------------------------- /remote-server/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-platform 8 | accounts-password 9 | -------------------------------------------------------------------------------- /remote-server/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /remote-server/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.1.0.2 2 | -------------------------------------------------------------------------------- /remote-server/.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.0 2 | accounts-password@1.1.1 3 | autoupdate@1.2.1 4 | base64@1.0.3 5 | binary-heap@1.0.3 6 | blaze@2.1.2 7 | blaze-tools@1.0.3 8 | boilerplate-generator@1.0.3 9 | callback-hook@1.0.3 10 | check@1.0.5 11 | ddp@1.1.0 12 | deps@1.0.7 13 | ejson@1.0.6 14 | email@1.0.6 15 | fastclick@1.0.3 16 | geojson-utils@1.0.3 17 | html-tools@1.0.4 18 | htmljs@1.0.4 19 | http@1.1.0 20 | id-map@1.0.3 21 | jquery@1.11.3_2 22 | json@1.0.3 23 | launch-screen@1.0.2 24 | livedata@1.0.13 25 | localstorage@1.0.3 26 | logging@1.0.7 27 | meteor@1.1.6 28 | meteor-platform@1.2.2 29 | minifiers@1.1.5 30 | minimongo@1.0.8 31 | mobile-status-bar@1.0.3 32 | mongo@1.1.0 33 | npm-bcrypt@0.7.8_2 34 | observe-sequence@1.0.6 35 | ordered-dict@1.0.3 36 | random@1.0.3 37 | reactive-dict@1.1.0 38 | reactive-var@1.0.5 39 | reload@1.1.3 40 | retry@1.0.3 41 | routepolicy@1.0.5 42 | service-configuration@1.0.4 43 | session@1.1.0 44 | sha@1.0.3 45 | spacebars@1.0.6 46 | spacebars-compiler@1.0.6 47 | srp@1.0.3 48 | templating@1.1.1 49 | tracker@1.0.7 50 | ui@1.0.6 51 | underscore@1.0.3 52 | url@1.0.4 53 | webapp@1.2.0 54 | webapp-hashing@1.0.3 55 | -------------------------------------------------------------------------------- /remote-server/remote-test-server.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Meteor.startup(function () { 3 | // Creating one test account 4 | if (! Meteor.users.findOne()) { 5 | Accounts.createUser({ 6 | username: 'bob', 7 | password: 'test' 8 | }); 9 | } 10 | }); 11 | 12 | Meteor.publish('test', function () { 13 | if (this.userId) { 14 | this.added('test', 'test', { data: 'You will see this if you are logged in!' }); 15 | } 16 | this.ready(); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /remote-server/run.sh: -------------------------------------------------------------------------------- 1 | meteor run --port 4000 2 | --------------------------------------------------------------------------------