├── .gitignore ├── History.md ├── LICENSE.md ├── README.md ├── fallback.litcoffee ├── package.js ├── proxy.litcoffee ├── server.litcoffee ├── smart.json ├── variable.litcoffee ├── worker-boot.js ├── worker-client.litcoffee ├── worker-packages.js └── worker-server.litcoffee /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | ## v0.1.2 2 | 3 | Update for Meteor 0.6.5.1. 4 | 5 | Use asset for worker Javascript files. 6 | 7 | Fallback for when the browser database is unavailable. (#6) 8 | 9 | 10 | ## v0.1.1 11 | 12 | Update for Meteor 0.6.5. 13 | 14 | Fix subscription status. 15 | 16 | Ensure Offline.supported is false if we're using browser-msg and 17 | BrowserMsg.supported is false. 18 | 19 | 20 | ## v0.1.0 21 | 22 | Implement Offline.subscriptions, subscriptionLoaded, and 23 | subscriptionStatus. 24 | 25 | Clean up the interaction between the client and the agent for 26 | subscribing to subscriptions and reporting subscription status back to 27 | the client. 28 | 29 | #8, #7, #4. 30 | 31 | 32 | ## v0.0.5 33 | 34 | Implement dead window detection for the shared web worker, fixing #19. 35 | 36 | Use canonical-stringify v1.1 37 | 38 | 39 | ## v0.0.4 40 | 41 | Fix problem with confusing Meteorite. Move common code used by both 42 | the offline data client and the shared web worker agent into 43 | offline-common, and the app to build the shared web worker into 44 | offline-worker. #20. 45 | 46 | 47 | ## v0.0.3 48 | 49 | Use a shared web worker when supported for the agent. #17 50 | 51 | 52 | ## v0.0.2 53 | 54 | * Fix "Match error: not a transaction" in iOS. #16 55 | 56 | 57 | ## v0.0.1 58 | 59 | * Initial release. 60 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | Copyright (C) 2013 Andrew Wilcox 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # On Hold 2 | 3 | This project is currently on hold, awaiting time for me (or someone) 4 | to redesign to address the problems with the current approach. 5 | 6 | I'd like the thank the many supporters of the project. While I 7 | regret the first attempt didn't result in a workable implementation, 8 | I hope that post the 1.0 Meteor release we'll be able to revisit 9 | and come up with a new design. 10 | 11 | 12 | ## Meteor Offline Data 13 | 14 | Home of the Meteor offline data project, implementing an "Offline 15 | Collection" which wraps a `Meteor.Collection`: 16 | 17 | * Data from the server is stored persistently in the browser database, 18 | making it available to the application even if the application starts 19 | up offline. 20 | 21 | * Changes made by the user are also saved in the browser database, 22 | preserving them if the browser is closed and reopened. The next time 23 | the application goes online the changes are sent up to the server. 24 | 25 | * Updates are reactively shared across browser windows open on the same 26 | application, even while offline. 27 | 28 | 29 | See the 30 | [two minute video introducing Offline Data](http://vimeo.com/64803472) 31 | 32 | 33 | ## A Big Problem 34 | 35 | The architecture used by Offline Data avoids three-way merges. 36 | 37 | In Offline Data, windows need to share updates between themselves to 38 | give a good user experience when offline. (Otherwise if the user 39 | comes back to a different window without realizing it, they'll be 40 | alarmed when something they entered has appeared to have vanished). 41 | 42 | In standard Meteor, each browser window has its own connection to the 43 | server. If we did this *and* had windows sharing updates as well, 44 | we'd have to deal with three-way merges. We'd have updates coming 45 | from other windows, and updates coming from the server, and we'd need 46 | to merge them correctly. 47 | 48 | The approach taken by Offline Data is to have *one* connection to the 49 | server, shared by the windows open on an application. Now updates 50 | flow in a linear fashion. Windows talk to the agent, which talks to 51 | the server. This is easy to figure out how to do right, and is easy 52 | to debug when something strange happens. We don't have obscure race 53 | conditions to avoid, and we don't need to be proving proofs to ensure 54 | that we're not going to occasionally mess up a three-way merge. 55 | 56 | The easy way to do this is with a shared web worker. We put the agent 57 | in the shared web worker, which is shared between windows, the shared 58 | web worker talks to the server, and everything works. 59 | 60 | For browsers which don't support shared web workers, the browser 61 | windows elect one of their number to act as the agent and make the 62 | connection to the server. It's more complicated than just using a 63 | shared web worker because the window acting as the agent might be 64 | closed and another window has to take over. But the end result is the 65 | same. 66 | 67 | This approach of having one browser window make the connection to the 68 | server on behalf of the other windows doesn't work on iOS because iOS 69 | suppresses timer events in inactive tabs. But that's OK, because iOS 70 | supports shared web workers. 71 | 72 | Until now. iOS 7 [has dropped support for shared web 73 | workers](http://caniuse.com/sharedworkers). 74 | 75 | This is a big problem for the current architecture of Offline Data. 76 | 77 | So what's next? Maybe we change the architecture. Currently, Offline 78 | Data runs on standard DDP. The server doesn't know or care that the 79 | client is storing data offline. On the server, it's plain unmodifed 80 | Meteor collections. But we could add server support for offline data, use timestamps or whatever, extend DDP if we need to. 81 | 82 | Or maybe there will be some clever way to get the "one connection to 83 | the server" agent working in iOS 7. I don't know yet. 84 | 85 | 86 | ## Version 87 | 88 | 0.1.2 89 | 90 | Use 0.1.2 with Meteor 0.6.5.1 or 0.6.5 only. 91 | 92 | (Offline data is closely tied to Meteor's internal livedata 93 | implementation, and the shared web worker loads Meteor 0.6.5.1 code 94 | modified to run in the web worker environment, so new releases of 95 | Meteor require a new release of offline data). 96 | 97 | For Meteor 0.6.4.1, use Offline Data version 0.1.0. 98 | 99 | 100 | Current gaps: 101 | 102 | * [no support yet for IE and Firefox](https://github.com/awwx/meteor-offline-data/issues/5) 103 | 104 | * [no support for accounts](https://github.com/awwx/meteor-offline-data/issues/2) 105 | 106 | See the [issues list](https://github.com/awwx/meteor-offline-data/issues) 107 | for the full known TODO list. 108 | 109 | 110 | ## Community Funded 111 | 112 | Development of the Offline Data package is funded by contributors through 113 | [Gittip](https://www.gittip.com/awwx/). 114 | 115 | 116 | ## Offline Data Package 117 | 118 | Offline data is a client side package which runs on top of standard, 119 | unmodified Meteor. 120 | 121 | On the server, collections are standard Meteor collections and methods 122 | are standard Meteor methods: the server doesn't know or care that the 123 | client is persisting data for offline use or not. (In fact some 124 | clients could be using the offline package and other clients might 125 | not; it's all the same to the server). 126 | 127 | On the client, an "Offline Collection" is used in place of a standard 128 | `Meteor.Collection` for collections that the developer wants to 129 | persist in the browser's database for offline usage. Regular Meteor 130 | collections can still be used, so an application can choose to make 131 | some collections available for offline use while leaving others in 132 | memory. 133 | 134 | When using offline collections the major architectural differences to 135 | the application are that offline collections and subscriptions are 136 | reactively shared across browser windows open on the same application, 137 | and method calls can complete in a different instance of the 138 | application. 139 | 140 | 141 | ## Limitations 142 | 143 | * Unnamed subscriptions (such as published by the `autopublish` 144 | package) are not supported. You will need to use 145 | [Meteor.publish](http://docs.meteor.com/#meteor_publish) with a 146 | non-null `name` argument to publish subscriptions for use by offline 147 | collections. 148 | 149 | 150 | ## Example 151 | 152 | The Meteor "todos" example, 153 | [modified to use offline-data](https://github.com/awwx/meteor-offline-todos#readme). 154 | 155 | 156 | ## Offline Subscriptions 157 | 158 | In standard Meteor, subscriptions are dynamic: you start a 159 | subscription with `Meteor.subscribe`, and you can later stop the 160 | subscription by calling `stop` on the subscription handle. Each call 161 | to `Meteor.subscribe` creates a new subscription, and it can only last 162 | for as long as the window is loaded. 163 | 164 | In offline data, subscriptions are shared across browser windows, and 165 | persist across reloads. A window declares the subscriptions it wants 166 | the agent to subscribe to, but there isn't a one-to-one correspondence 167 | between the window's declared subscriptions and the agent's Meteor 168 | subscriptions. If the window requests a subscription that is already 169 | being subscribed to, the existing subscription is reused instead of 170 | starting a new subscription. And the agent's Meteor subscription may 171 | need to be restarted if the agent window changes or the application is 172 | reloaded. 173 | 174 | Another difference is that *not* subscribing to a subscription 175 | actively causes the documents unique to that subscription to be 176 | deleted. This is because the only way to tell that document persisted 177 | in the browser was deleted while the client was offline is to wait for 178 | subscriptions to become ready, and to see what documents we got from 179 | the server. (Any documents we *didn't* receive can and should now be 180 | deleted on the client, since we now know they were deleted on the 181 | server while we were offline). 182 | 183 | Thus we don't want to subscribe for example to "lists" and then later 184 | subscribe to "tasks"... we could end up deleting all our task 185 | documents and then reloading them. Instead we want to subscribe to "lists" and "tasks" together, 186 | 187 | 188 | ## Offline API 189 | 190 | **Offline.persistent** 191 | *client* 192 | 193 | This constant is `true` if data can be stored offline in this browser 194 | (for example, the browser supports the Web SQL Database). 195 | 196 | When `false`, the Offline Data API falls back to using Meteor 197 | collections, and stores data in memory only. 198 | 199 | 200 | **Offline.subscriptions([ 201 |
  [name, arg1, arg2, ... ] 202 |
  [name, arg1, arg2, ... ] 203 |
  ... 204 |
])** 205 | *client* 206 | 207 | Specifies the set of subscriptions to subscribe to. Any subscriptions 208 | not listed in any window's subscription set are unsubscribed. 209 | 210 | Thus calling `subscriptions` *replaces* the set of subscriptions 211 | subscribed to, instead of adding to them. 212 | 213 | In standard Meteor a common pattern is to *select* a set of documents 214 | to retrieve from the server and display: 215 | 216 | ``` 217 | Meteor.subscribe("projects"); 218 | Deps.autorun(function () { 219 | Meteor.subscribe("tasks", Session.get("currentProjectId")); 220 | }); 221 | ``` 222 | 223 | With offline data it is common to subscribe to a larger set of 224 | documents that we want to have *available* while offline, 225 | 226 | ``` 227 | Offline.subscriptions([["projects"], ["tasks"]]); 228 | ``` 229 | 230 | and then display a particular subset: 231 | 232 | ``` 233 | Tasks.find({projectId: Session.get("currentProjectId")}) 234 | ``` 235 | 236 | 237 |
238 | **Offline.subscriptionLoaded(name, [, arg1, arg2, ...])** 239 | *client* 240 | 241 | Returns `true` or `false` indicating whether the documents for a 242 | subscription have been loaded. A reactive data source. 243 | 244 | For a new subscription, the subscription is "loaded" when it becomes 245 | ready. However the "loaded" status persists across reloads of the 246 | application, and so a loaded subscription will still show as loaded 247 | even if the application starts up offline. 248 | 249 | A subscription will transition to not being loaded if it is 250 | unsubscribed, or if the offline agent's Meteor subscription reports an 251 | error (through the Meteor.subscribe onError callback). 252 | 253 | 254 |
255 | **Offline.subscriptionStatus(name, [, arg1, arg2, ...])** 256 | *client* 257 | 258 | Returns an object describing the dynamic status of the Meteor 259 | subscription made by the offline agent. A reactive data source. 260 | 261 | The object will contain a `status` field which 262 | can be one of the strings `unsubscribed`, `subscribing`, `error`, or 263 | `ready`. When the status is "error" the object will also contain an 264 | `error` field with the subscription error. 265 | 266 | (The subscription error will be an object containing the same fields 267 | as the 268 | [`Meteor.Error`](http://docs.meteor.com/#meteor_error) 269 | object returned for the Meteor subscription, but will not be an 270 | instance of the `Meteor.Error` class because of EJSON serialization). 271 | 272 | It's normal for the status to transition from `ready` back to 273 | `subscribing` if the agent window changes. 274 | 275 | If a subscription is loaded but not ready, that means the client has a 276 | complete set of old documents (from the last time we were online and 277 | got synced up), but hasn't received the latest updates from the server 278 | yet. 279 | 280 | 281 |
282 | **Offline.methods(methods)** *client* 283 | 284 | Define client stubs for methods that can be called offline. 285 | 286 | On the server, use `Meteor.methods` as usual to define the methods. 287 | 288 | In standard Meteor, if an application temporarily doesn't have a 289 | connection to the server, method calls will be queued up in memory. 290 | Meteor will automatically keep trying to reconnect and will send the 291 | method calls to the server when it is connected again. However, 292 | undelivered method calls will be lost if the browser window is closed, or 293 | if on a mobile device the window is unloaded to save memory when 294 | switching between windows or applications. 295 | 296 | Offline methods are saved persistently in the browser's database, and 297 | will be delivered when the browser goes online -- even if the 298 | application was closed or unloaded in the meantime. 299 | 300 | In Meteor, the collection modification methods 301 | (collection.insert, collection.update, 302 | collection.remove) are translated into method calls 303 | internally, and so this is the mechanism by which changes to offline 304 | collections are persisted (if needed) until the application has a 305 | connection. 306 | 307 | 308 |
309 | **Offline.call(name, param1, param2, ...)** *client* 310 | 311 | Calls an offline method. 312 | 313 | There is no `asyncCallback` argument because it is quite normal for an 314 | offline method to be started in one instance of the application while 315 | offline, have the window be closed or unloaded, and then for the method 316 | call to complete in another instance of the application when the 317 | browser is online again. (This is how changes the user makes to 318 | collections are saved until the application goes online again). 319 | 320 | Instead you can listen for method complete events. 321 | 322 | *TODO: `Offline.apply`, and doing something for `wait`.* 323 | 324 | 325 |
326 | **Offline.methodCompleted([ name, ] callback)** *client* 327 | 328 | To be implemented. 329 | 330 | Registers a callback to be called when an offline method `name` has 331 | completed: the method has returned a result and the server's writes 332 | have replaced the stub's writes in the local cache. If `name` is not 333 | specified then the callback is called for all offline methods. 334 | 335 | The callback is called with the name, parameters as an array, and 336 | error or result returned: 337 | 338 | callback(name, params, error, result) 339 | 340 | Note that method completion is broadcast to all listening windows. 341 | 342 | *TODO: This is a straightforward conversion of the Meteor method 343 | completion API to support resumed applications, but it would be good 344 | to walk through some use cases to see if this is what we really 345 | want.* 346 | 347 | 348 |
349 | **new Offline.Collection(name)** *client* 350 | 351 | Creates and returns an offline collection. `name` is the name of a 352 | regular `Meteor.Collection` on the server. 353 | 354 | (The server doesn't know or care if a client is using a collection as 355 | an offline collection or not). 356 | 357 | 358 |
359 | **offlineCollection.find** 360 |
**offlineCollection.findOne** 361 | 362 | These work the same as the `Meteor.Collection` methods. 363 | 364 |
365 | **offlineCollection.insert(doc)** 366 |
**offlineCollection.update(selector, modifier, [options])** 367 |
**offlineCollection.remove(selector)** 368 | 369 | These methods work the same as their corresponding `Meteor.Collection` 370 | methods, except for the lack of a callback since the methods may 371 | complete in a later instance of the application. 372 | 373 | *TODO: Naturally we'd like to have an API to get notified on method 374 | completion; it would be helpful to think of a use case to help see 375 | what the API could look like.* 376 | 377 | There is no `allow` or `deny` methods on an offline collection as 378 | these are server-only methods. 379 | 380 |
381 | **Offline.resetDatabase()** 382 | 383 | Clears the browser database. 384 | 385 | 386 | ## Offline Functionality Not Included 387 | 388 | There's other functionality that might be useful or important for an 389 | offline application, but isn't part of the offline-data package. 390 | 391 | 392 | ### Conflict resolution 393 | 394 | Conflict resolution can become more important when some updates are 395 | delayed in time by applications being offline. A conflict resolution 396 | strategy might involve for example adding a last modified timestamp to 397 | server documents, and then accepting, rejecting, or perhaps merging 398 | updates in a server method. 399 | 400 | 401 | ### Incremental Loading of Large Data Sets 402 | 403 | The offline-data package makes a standard Meteor subscription to 404 | receive data, which means that just like with a regular Meteor 405 | application all the documents in the subscription are sent down to the 406 | client each time the application is opened. For larger data sets 407 | (with some kind of support on the server to keep track of versioning) 408 | it would be nice to only need to download new and changed documents. 409 | 410 | 411 | ## Architecture 412 | 413 | Offline subscriptions are made from the "offline agent" in the client, 414 | which connects to the server on behalf of the browser windows. This 415 | allows updates from the server to be delivered to the browser over one 416 | connection, instead of redundantly delivered to every browser window; 417 | and as offline collections are shared across browser windows, ensures 418 | that the browser sees a consistent view of updates from the server. 419 | 420 | In browsers which support 421 | [shared web workers](http://caniuse.com/#feat=sharedworkers), 422 | the agent runs inside of a shared web worker. Otherwise, the browser 423 | windows cooperatively elect one of their number to act as the agent 424 | for the other windows. 425 | 426 | (In iOS, timeout and interval events are not delivered to tabs other 427 | than the active tab, which would make it hard for a tab to act as the 428 | agent for the other tabs when it wasn't the active tab; but iOS Safari 429 | does support shared web workers. The Android browser doesn't support 430 | shared web workers, but timer events are delivered to all tabs and so 431 | there's no problem on Android having one tab act as the agent for the 432 | other tabs.) 433 | 434 | While in theory it might be possible for individual browser windows 435 | not to make any connection to the server at all and to channel all 436 | communication through the agent, the offline-data package is designed 437 | to run on top of standard Meteor and so browser windows do each have 438 | their own livedata connection to the server. 439 | 440 | Communication for regular (non-offline) Meteor collections, Meteor 441 | methods, and the hot code reload notification go through the 442 | individual window's livedata connection as usual, in the same way as 443 | when the offline-data packages isn't being used. 444 | -------------------------------------------------------------------------------- /fallback.litcoffee: -------------------------------------------------------------------------------- 1 | return if Offline.persistent 2 | 3 | 4 | subscriptionKey = (subscription) -> 5 | stringify(subscription) 6 | 7 | 8 | {connection, name, args} -> handle 9 | 10 | currentSubscriptions = {} 11 | 12 | 13 | Offline.subscriptions = (subscriptions) -> 14 | subscriptions = _.map( 15 | subscriptions, 16 | ((subscriptionArray) -> 17 | stringify({ 18 | name: subscriptionArray[0] 19 | args: subscriptionArray[1..] 20 | }) 21 | ) 22 | ) 23 | 24 | for subscription in currentSubscriptions 25 | unless subscription in subscriptions 26 | subscription.stop() 27 | delete currentSubscriptions[subscription] 28 | 29 | for subscription in subscriptions 30 | unless currentSubscriptions[subscription] 31 | {name, args} = EJSON.parse(subscription) 32 | currentSubscriptions[subscription] = 33 | Meteor.subscribe(name, args...) 34 | 35 | return 36 | 37 | 38 | Offline.Collection = Meteor.Collection 39 | 40 | 41 | Offline.subscriptionLoaded = (name, args...) -> 42 | 43 | 44 | connection name -> OfflineConnection 45 | 46 | offlineConnections = {} 47 | 48 | 49 | class OfflineConnection 50 | 51 | constructor: (@connectionName, @meteorConnection) -> 52 | if offlineConnections[@connectionName]? 53 | throw new Error( 54 | "an OfflineConnection has already been constructed for this connection: #{@connectionName}" 55 | ) 56 | offlineConnections[@connectionName] = this 57 | @currentSubscriptions = {} 58 | @subscriptionStatusVariables = {} 59 | 60 | 61 | subscriptions: (subscriptions) -> 62 | unless _.isArray(subscriptions) 63 | throw new Error('`subscriptions` argument should be an array') 64 | for subscription in subscriptions 65 | unless _.isArray(subscription) 66 | throw new Error('each individual subscription should be an array') 67 | unless subscription.length > 0 68 | throw new Error('a subscription should include at least the subscription name') 69 | unless _.isString(subscription[0]) 70 | throw new Error('the subscription name should be a string') 71 | 72 | subscriptions = _.map(subscriptions, 73 | (array) -> {name: array[0], args: array[1..]} 74 | ) 75 | 76 | subscriptionKeys = _.map(subscriptions, subscriptionKey) 77 | 78 | for key, handle of @currentSubscriptions 79 | unless key in subscriptionKeys 80 | handle.stop() 81 | delete @currentSubscriptions[key] 82 | @setSubscriptionStatus( 83 | EJSON.parse(key), 84 | {status: 'stopped', loaded: false} 85 | ) 86 | 87 | for subscription in subscriptions 88 | do (subscription) => 89 | key = subscriptionKey(subscription) 90 | unless @currentSubscriptions[key] 91 | @currentSubscriptions[key] = 92 | @meteorConnection.subscribe( 93 | subscription.name, 94 | subscription.args..., 95 | { 96 | onError: (err) => 97 | @setSubscriptionStatus( 98 | subscription, 99 | { 100 | status: 'error' 101 | loaded: false 102 | error: err 103 | } 104 | ) 105 | return 106 | onReady: => 107 | @setSubscriptionStatus( 108 | subscription, 109 | { 110 | status: 'ready' 111 | loaded: true 112 | } 113 | ) 114 | } 115 | ) 116 | 117 | return 118 | 119 | 120 | subscriptionStatusVariable: (subscription) -> 121 | @subscriptionStatusVariables[stringify(subscription)] or= 122 | Variable({ 123 | status: 'unsubscribed' 124 | loaded: false 125 | }) 126 | 127 | 128 | subscriptionStatus: (name, args...) -> 129 | @subscriptionStatusVariable({name, args})() 130 | 131 | 132 | setSubscriptionStatus: (subscription, status) -> 133 | @subscriptionStatusVariable(subscription).set(status) 134 | 135 | 136 | subscriptionLoaded: (name, args...) -> 137 | isolateValue(=> @subscriptionStatus(name, args...).loaded) 138 | 139 | 140 | call: (args...) -> 141 | @meteorConnection.call(args...) 142 | 143 | 144 | Offline._defaultOfflineConnection = defaultOfflineConnection = 145 | new OfflineConnection('/', Meteor.connection) 146 | 147 | 148 | Offline.subscriptions = (subscriptions) -> 149 | defaultOfflineConnection.subscriptions(subscriptions) 150 | 151 | 152 | Offline.subscriptionStatus = (name, args...) -> 153 | defaultOfflineConnection.subscriptionStatus(name, args...) 154 | 155 | 156 | Offline.subscriptionLoaded = (name, args...) -> 157 | defaultOfflineConnection.subscriptionLoaded(name, args...) 158 | 159 | 160 | Offline.methods = (methods) -> 161 | defaultOfflineConnection.methods(methods) 162 | 163 | 164 | Offline.call = (args...) -> 165 | defaultOfflineConnection.call(args...) 166 | 167 | 168 | Offline.resetDatabase = -> 169 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: "offline data" 3 | }); 4 | 5 | 6 | Package.on_use(function (api) { 7 | api.use( 8 | [ 9 | 'coffeescript', 10 | 'livedata', 11 | 'underscore', 12 | 'offline-common' 13 | ], 14 | ['client', 'server'] 15 | ); 16 | 17 | api.use( 18 | [ 19 | 'ejson', 20 | 'minimongo', 21 | 'isolate-value', 22 | 'canonical-stringify', 23 | 'variable', 24 | 'mongo-livedata' 25 | ], 26 | 'client' 27 | ); 28 | 29 | api.use( 30 | [ 31 | 'webapp' 32 | ], 33 | 'server' 34 | ); 35 | 36 | api.add_files([ 37 | 'worker-boot.js', 38 | 'worker-packages.js' 39 | ], 'client', {isAsset: true}); 40 | 41 | api.export('Offline', ['client', 'server']); 42 | 43 | api.add_files('worker-server.litcoffee', 'server'); 44 | api.add_files('worker-client.litcoffee', 'client'); 45 | 46 | api.add_files('proxy.litcoffee', 'client'); 47 | api.add_files('fallback.litcoffee', 'client'); 48 | 49 | api.add_files('server.litcoffee', 'server'); 50 | }); 51 | -------------------------------------------------------------------------------- /proxy.litcoffee: -------------------------------------------------------------------------------- 1 | return unless Offline.persistent 2 | 3 | database = Offline._database 4 | {thisWindowId} = Offline._windows 5 | messageAgent = Offline._messageAgent 6 | 7 | 8 | debug = Meteor.settings?.public?.offlineData?.debug?.proxy 9 | 10 | log = (args...) -> 11 | Meteor._debug "offline-data proxy:", args... 12 | return 13 | 14 | 15 | if Offline._usingSharedWebWorker 16 | 17 | broadcastUpdate = -> 18 | log "broadcast update" if debug 19 | messageAgent 'update' 20 | return 21 | 22 | else 23 | 24 | broadcastUpdate = -> 25 | log "broadcast update" if debug 26 | broadcast 'update' 27 | return 28 | 29 | 30 | 31 | Subscription status: connecting, ready, error, stopped 32 | 33 | 34 | subscriptionStatus = {} 35 | 36 | 37 | subscriptionStatusVariable = (subscription) -> 38 | subscriptionStatus[stringify(subscription)] or= 39 | Variable({ 40 | status: 'unsubscribed' 41 | loaded: false 42 | }) 43 | 44 | 45 | getSubscriptionStatus = (subscription) -> 46 | subscriptionStatusVariable(subscription)() 47 | 48 | 49 | setSubscriptionStatus = (subscription, status) -> 50 | log "set subscription status", stringify(subscription), stringify(status) if debug 51 | subscriptionStatusVariable(subscription).set(status) 52 | 53 | 54 | addWindowSubscription = (connection, name, args) -> 55 | database.transaction((tx) -> 56 | database.addWindowSubscription( 57 | tx, 58 | thisWindowId, 59 | connection, 60 | name, 61 | args 62 | ) 63 | ) 64 | .then(-> 65 | messageAgent 'windowSubscriptionsUpdated' 66 | ) 67 | 68 | 69 | https://github.com/meteor/meteor/blob/release/0.6.5/packages/livedata/livedata_common.js#L7 70 | 71 | TODO sessionData (need an example of how it's used) 72 | 73 | TODO can unblock get called in the client? is it treated as a no-op? 74 | 75 | class MethodInvocation 76 | 77 | constructor: (options) -> 78 | @isSimulation = options.isSimulation 79 | @userId = options.userId 80 | 81 | setUserId: (userId) -> 82 | throw new Error("accounts are not yet supported for offline data") 83 | 84 | 85 | connection name -> OfflineConnection 86 | 87 | offlineConnections = {} 88 | 89 | 90 | class OfflineConnection 91 | 92 | constructor: (@connectionName) -> 93 | if offlineConnections[@connectionName]? 94 | throw new Error( 95 | "an OfflineConnection has already been constructed for this connection: #{@connectionName}" 96 | ) 97 | offlineConnections[@connectionName] = this 98 | @_methodHandlers = {} 99 | @_offlineCollections = {} 100 | @_initialized = new Result() 101 | @_initialize() 102 | 103 | 104 | _initialize: -> 105 | Context.withContext "initialize offline connection #{@connectionName}", => 106 | Meteor.startup => 107 | database.transaction((tx) => 108 | database.readDocs(tx, @connectionName) 109 | ) 110 | .then((connectionDocs) => 111 | for collectionName, docs of connectionDocs 112 | for docId, doc of docs 113 | updateLocal @connectionName, collectionName, docId, doc 114 | return 115 | ) 116 | .then(=> 117 | @_initialized.complete() 118 | ) 119 | return 120 | 121 | 122 | _addCollection: (offlineCollection) -> 123 | name = offlineCollection._collectionName 124 | if @_offlineCollections[name]? 125 | throw new Error("already have an offline collection for: #{name}") 126 | @_offlineCollections[name] = offlineCollection 127 | return 128 | 129 | 130 | registerStore: (name, wrappedStore) -> 131 | return wrappedStore 132 | 133 | 134 | userId: -> 135 | return null 136 | 137 | 138 | TODO is setUserId defined on the client? 139 | 140 | setUserId: (userId) -> 141 | throw new Error('not implemented yet') 142 | 143 | 144 | subscriptions: (subscriptions) -> 145 | unless _.isArray(subscriptions) 146 | throw new Error('`subscriptions` argument should be an array') 147 | for subscription in subscriptions 148 | unless _.isArray(subscription) 149 | throw new Error('each individual subscription should be an array') 150 | unless subscription.length > 0 151 | throw new Error('a subscription should include at least the subscription name') 152 | unless _.isString(subscription[0]) 153 | throw new Error('the subscription name should be a string') 154 | 155 | database.transaction((tx) => 156 | database.setWindowSubscriptions( 157 | tx, 158 | thisWindowId, 159 | @connectionName, 160 | subscriptions 161 | ) 162 | ) 163 | .then(-> 164 | messageAgent 'windowSubscriptionsUpdated' 165 | ) 166 | 167 | 168 | subscriptionStatus: (name, args...) -> 169 | getSubscriptionStatus({connection: @connectionName, name, args}) 170 | 171 | 172 | subscriptionLoaded: (name, args...) -> 173 | isolateValue(=> @subscriptionStatus(name, args...).loaded) 174 | 175 | 176 | https://github.com/meteor/meteor/blob/release/0.6.5/packages/livedata/livedata_connection.js#L561 177 | 178 | methods: (methods) -> 179 | _.each methods, (func, name) => 180 | if @_methodHandlers[name] 181 | throw new Error("A method named '" + name + "' is already defined") 182 | @_methodHandlers[name] = func 183 | return 184 | 185 | 186 | _saveOriginals: -> 187 | for name, offlineCollection of @_offlineCollections 188 | offlineCollection._saveOriginals() 189 | return 190 | 191 | 192 | _writeChanges: (tx, methodId) -> 193 | writes = [] 194 | for name, offlineCollection of @_offlineCollections 195 | writes.push offlineCollection._writeMethodChanges(tx, methodId) 196 | return Result.join(writes) 197 | 198 | 199 | https://github.com/meteor/meteor/blob/release/0.6.5/packages/livedata/livedata_connection.js#L634 200 | 201 | _runStub: (methodId, alreadyInSimulation, name, args) -> 202 | stub = @_methodHandlers[name] 203 | return unless stub 204 | 205 | # TODO sessionData 206 | 207 | invocation = new MethodInvocation({ 208 | isSimulation: true 209 | userId: @userId() 210 | setUserId: (userId) => @setUserId(userId) 211 | }) 212 | 213 | if alreadyInSimulation 214 | try 215 | ret = DDP._CurrentInvocation.withValue(invocation, -> 216 | return stub.apply(invocation, EJSON.clone(args)) 217 | ) 218 | catch e 219 | exception = e 220 | if exception 221 | return Result.failed(exception) 222 | else 223 | return Result.completed(ret) 224 | 225 | # Not already in simulation... run the method stub in 226 | # a database transaction. 227 | 228 | database.transaction((tx) => 229 | processUpdatesInTx(tx) 230 | .then(=> 231 | @_saveOriginals() 232 | try 233 | ret = DDP._CurrentInvocation.withValue(invocation, -> 234 | return stub.apply(invocation, EJSON.clone(args)) 235 | ) 236 | catch e 237 | exception = e 238 | return @_writeChanges(tx, methodId) 239 | ) 240 | ) 241 | .then(=> 242 | broadcastUpdate() 243 | if exception 244 | return Result.failed(exception) 245 | else 246 | return Result.completed(ret) 247 | ) 248 | 249 | 250 | https://github.com/meteor/meteor/blob/release/0.6.5/packages/livedata/livedata_connection.js#L570 251 | 252 | call: (name, args...) -> 253 | if args.length and typeof args[args.length - 1] is 'function' 254 | callback = args.pop() 255 | return @apply(name, args, callback) 256 | 257 | 258 | https://github.com/meteor/meteor/blob/release/0.6.5/packages/livedata/livedata_connection.js#L588 259 | 260 | apply: (name, args, options, callback) -> 261 | if not callback and typeof options is 'function' 262 | callback = options 263 | options = {} 264 | 265 | if callback 266 | callback = Meteor.bindEnvironment callback, (e) -> 267 | Meteor._debug("Exception while delivering result of invoking '" + 268 | name + "'", e, e?.stack) 269 | 270 | methodId = Random.id() 271 | 272 | enclosing = DDP._CurrentInvocation.get() 273 | alreadyInSimulation = enclosing and enclosing.isSimulation 274 | 275 | @_runStub(methodId, alreadyInSimulation, name, args) 276 | .onFailure((exception) => 277 | unless exception.expected 278 | Meteor._debug( 279 | "Exception while simulating the effect of invoking '" + 280 | name + "'", exception, exception?.stack 281 | ) 282 | return 283 | ) 284 | .always(=> 285 | return if alreadyInSimulation 286 | database.transaction((tx) => 287 | database.addQueuedMethod( 288 | tx, @connectionName, methodId, name, args 289 | ) 290 | ) 291 | .then(=> 292 | messageAgent 'newQueuedMethod' 293 | return 294 | ) 295 | ) 296 | 297 | return 298 | 299 | 300 | offlineConnectionFor = (connectionName) -> 301 | offlineConnections[connectionName] or 302 | new OfflineConnection(connectionName) 303 | 304 | 305 | Offline._defaultOfflineConnection = defaultOfflineConnection = 306 | new OfflineConnection('/') 307 | 308 | 309 | Offline.subscriptions = (subscriptions) -> 310 | defaultOfflineConnection.subscriptions(subscriptions) 311 | 312 | 313 | Offline.subscriptionStatus = (name, args...) -> 314 | defaultOfflineConnection.subscriptionStatus(name, args...) 315 | 316 | 317 | Offline.subscriptionLoaded = (name, args...) -> 318 | defaultOfflineConnection.subscriptionLoaded(name, args...) 319 | 320 | 321 | Offline.methods = (methods) -> 322 | defaultOfflineConnection.methods(methods) 323 | 324 | 325 | connectionName -> collectionName -> LocalCollection 326 | 327 | Offline._localCollections = localCollections = {} 328 | 329 | 330 | getLocalCollection = (connectionName, collectionName) -> 331 | (localCollections[connectionName] or= {})[collectionName] or= 332 | new LocalCollection() 333 | 334 | 335 | updateLocal = (connectionName, collectionName, docId, doc) -> 336 | localCollection = getLocalCollection(connectionName, collectionName) 337 | if doc? 338 | if doc._id isnt docId 339 | throw new Error("oops, document id doesn't match") 340 | if localCollection.findOne(docId)? 341 | localCollection.update(docId, doc) 342 | else 343 | localCollection.insert(doc) 344 | else 345 | localCollection.remove(docId) 346 | return 347 | 348 | 349 | connection name -> collection name -> OfflineCollection 350 | 351 | offlineCollections = {} 352 | 353 | 354 | class OfflineCollection 355 | 356 | constructor: (@_collectionName, options = {}) -> 357 | 358 | @_connectionName = options.connectionName ? '/' 359 | offlineConnection = offlineConnectionFor(@_connectionName) 360 | 361 | offlineConnection._addCollection(this) 362 | 363 | @_localCollection = getLocalCollection(@_connectionName, @_collectionName) 364 | 365 | driver = 366 | open: (_name) => 367 | unless _name is @_collectionName 368 | throw new Error( 369 | "oops, driver is being called with the wrong name 370 | for this collection: #{_name}" 371 | ) 372 | return @_localCollection 373 | 374 | @_collection = new Meteor.Collection( 375 | @_collectionName, 376 | {connection: offlineConnection, _driver: driver} 377 | ) 378 | 379 | find: (args...) -> 380 | @_localCollection.find(args...) 381 | 382 | findOne: (args...) -> 383 | @_localCollection.findOne(args...) 384 | 385 | _saveOriginals: -> 386 | @_localCollection.saveOriginals() 387 | 388 | _writeDoc: (tx, docId) -> 389 | doc = @_localCollection.findOne(docId) 390 | if doc? 391 | database.writeDoc(tx, @_connectionName, @_collectionName, doc) 392 | else 393 | database.deleteDoc(tx, @_connectionName, @_collectionName, docId) 394 | 395 | _writeMethodChanges: (tx, methodId) -> 396 | originals = @_localCollection.retrieveOriginals() 397 | writes = [] 398 | for docId of originals 399 | writes.push @_writeDoc(tx, docId) 400 | writes.push database.addDocumentWrittenByStub( 401 | tx, @_connectionName, methodId, @_collectionName, docId 402 | ) 403 | writes.push database.addUpdate tx, { 404 | update: 'documentUpdated', 405 | connectionName: @_connectionName, 406 | collectionName: @_collectionName, 407 | docId, 408 | doc: @_localCollection.findOne(docId) 409 | } 410 | return Result.join(writes) 411 | 412 | insert: (doc, callback) -> 413 | if callback? 414 | Meteor._debug "Warning: the insert `callback` argument will not called for an Offline collection" 415 | return @_collection.insert(doc) 416 | 417 | update: (selector, modifier, options, callback) -> 418 | if typeof(options) is 'function' or typeof(callback) is 'function' 419 | Meteor._debug "Warning: the update `callback` argument will not called for an Offline collection" 420 | if typeof(options) is 'function' 421 | options = undefined 422 | return @_collection.update(selector, modifier, options) 423 | 424 | remove: (selector, callback) -> 425 | if callback? 426 | Meteor._debug "Warning: the remove `callback` argument will not called for an Offline collection" 427 | return @_collection.remove(selector) 428 | 429 | 430 | ## Updates 431 | 432 | All windows listen for updates from the agent window. 433 | 434 | processDocumentUpdated = (update) -> 435 | {connectionName, collectionName, docId, doc} = update 436 | updateLocal connectionName, collectionName, docId, doc 437 | return 438 | 439 | processUpdate = (update) -> 440 | log "process update", stringify(update) if debug 441 | switch update.update 442 | when 'documentUpdated' 443 | processDocumentUpdated(update) 444 | when 'subscriptionStatus' 445 | setSubscriptionStatus update.subscription, update.status 446 | else 447 | throw new Error "unknown update: " + stringify(update) 448 | 449 | return 450 | 451 | 452 | TODO getting called a lot 453 | 454 | processUpdatesInTx = (tx) -> 455 | database.pullUpdatesForWindow(tx, thisWindowId) 456 | .then((updates) -> 457 | database.removeUpdatesProcessedByAllWindows(tx) 458 | .then(-> 459 | processUpdate(update) for update in updates 460 | return 461 | ) 462 | ) 463 | 464 | processUpdates = -> 465 | database.transaction((tx) -> 466 | processUpdatesInTx(tx) 467 | ) 468 | return 469 | 470 | 471 | Meteor.startup -> 472 | database.transaction((tx) -> 473 | database.readSubscriptions(tx) 474 | ) 475 | .then((subscriptions) -> 476 | log "startup subscriptions", stringify(subscriptions) if debug 477 | for subscription in subscriptions 478 | setSubscriptionStatus( 479 | _.pick(subscription, ['connection', 'name', 'args']), 480 | Offline._subscriptionStatus(subscription) 481 | ) 482 | return 483 | ) 484 | 485 | 486 | Offline.Collection = OfflineCollection 487 | 488 | 489 | if Offline._usingSharedWebWorker 490 | 491 | Offline._sharedWebWorker.addMessageHandler 'update', (data) -> 492 | processUpdates() 493 | return 494 | 495 | else 496 | 497 | broadcast.listen 'update', -> 498 | processUpdates() 499 | return 500 | -------------------------------------------------------------------------------- /server.litcoffee: -------------------------------------------------------------------------------- 1 | This allows us to invoke a method on the server without running the 2 | stub on the client. (We've already run the stub using the offline 3 | algorithm). 4 | 5 | Meteor.methods 6 | '/awwx/offline-data/apply': (methodId, name, params) -> 7 | return Meteor.apply name, params 8 | 9 | 10 | For convenience on the server, Offline.methods etc. delegate to their 11 | Meteor counterpart. 12 | 13 | Offline.methods = (methods) -> 14 | Meteor.methods methods 15 | -------------------------------------------------------------------------------- /smart.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "offline-data", 3 | 4 | "description": "Offline data support", 5 | 6 | "homepage": "https://github.com/awwx/meteor-offline-data#readme", 7 | 8 | "author": "Andrew Wilcox", 9 | 10 | "version": "0.1.2", 11 | 12 | "git": "https://github.com/awwx/meteor-offline-data.git", 13 | 14 | "packages": { 15 | "offline-common": { 16 | "git": "https://github.com/awwx/meteor-offline-common.git", 17 | "tag": "v0.1.2" 18 | }, 19 | "isolate-value": "2.0.2", 20 | "variable": "1.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /variable.litcoffee: -------------------------------------------------------------------------------- 1 | Variable = (initialValue) -> 2 | 3 | value = initialValue 4 | dep = new Deps.Dependency() 5 | 6 | fn = -> 7 | dep.depend() 8 | return value 9 | 10 | fn.set = (newValue) -> 11 | unless EJSON.equals(newValue, value) 12 | value = newValue 13 | dep.changed() 14 | return 15 | 16 | return fn 17 | -------------------------------------------------------------------------------- /worker-boot.js: -------------------------------------------------------------------------------- 1 | // This code is run first in the shared web worker environment. 2 | 3 | (function () { 4 | 5 | var global = self; 6 | 7 | 8 | var WebWorker = {}; 9 | global.WebWorker = WebWorker; 10 | 11 | 12 | WebWorker.id = (''+Math.random()).substr(2); 13 | 14 | 15 | 16 | // List of ports from browser windows connecting to us. 17 | 18 | WebWorker.ports = []; 19 | 20 | // List of the windowId of each window connecting to us, in the same 21 | // order as the ports list. 22 | 23 | WebWorker.windowIds = []; 24 | 25 | // There's no console log in the shared web worker environment, so 26 | // we'll have Meteor._debug call the log function here. 27 | 28 | // Store any startup errors or debug messages until we have a 29 | // connection from a window to pass the log messages to. Make 30 | // global because sometimes it can be helpful to be able to view in 31 | // the browser debugger if startup errors prevent getting to the 32 | // point of listening to window connections. 33 | 34 | WebWorker.logs = []; 35 | 36 | 37 | // Log a debug message. Pass on to connected windows if we have any 38 | // yet, otherwise store in `logs` until we have a connection. 39 | 40 | WebWorker.log = function (msg) { 41 | if (WebWorker.ports.length > 0) { 42 | for (var i = 0; i < WebWorker.ports.length; ++i) { 43 | WebWorker.ports[i].postMessage({msg: 'log', log: [msg]}); 44 | } 45 | } 46 | else { 47 | WebWorker.logs.push(msg); 48 | } 49 | }; 50 | 51 | 52 | // Handle uncaught exceptions. 53 | // https://developer.mozilla.org/en-US/docs/Web/API/window.onerror 54 | 55 | global.onerror = function(errorMessage, url, lineNumber) { 56 | WebWorker.log("error: " + errorMessage + " " + url + ":" + lineNumber); 57 | }; 58 | 59 | 60 | // Call `fn`, and log any uncaught exceptions thrown. 61 | 62 | // var catcherr = function (fn) { 63 | // try { 64 | // return fn(); 65 | // } 66 | // catch (error) { 67 | // log("error: " + (error != null ? error.stack : '')); 68 | // } 69 | // }; 70 | 71 | 72 | // Handlers for messages from windows. 73 | // msg -> handler 74 | 75 | var messageHandlers = {}; 76 | 77 | 78 | WebWorker.addMessageHandler = function (msg, callback) { 79 | messageHandlers[msg] = callback; 80 | }; 81 | 82 | 83 | // Not dynamically including the Meteor runtime config in this 84 | // JavaScript source allows it to be served statically. Instead, 85 | // connecting windows send a "boot" message with the config, and we 86 | // delay loading the package code until we get the config. 87 | 88 | var booted = false; 89 | 90 | WebWorker.addMessageHandler('boot', function (port, data) { 91 | var i = WebWorker.ports.indexOf(port); 92 | if (i !== -1) 93 | WebWorker.windowIds[i] = data.windowId; 94 | 95 | if (booted) 96 | return; 97 | 98 | global.__meteor_runtime_config__ = data.__meteor_runtime_config__; 99 | 100 | // We will at least hear about syntax errors now because of the 101 | // onerror handler above, but there doesn't seem to be a way to 102 | // get the line number of the syntax error (the lineNumber 103 | // reported is the this importScripts line here, not the line of 104 | // the syntax error). 105 | 106 | importScripts( 107 | __meteor_runtime_config__.offlineDataWorker.urls['worker-packages.js'] 108 | ); 109 | booted = true; 110 | 111 | Package.meteor.Meteor._start(); 112 | }); 113 | 114 | 115 | // Incoming message from a window. 116 | 117 | var onmessage = function (port, event) { 118 | var data = event.data; 119 | 120 | if (WebWorker.ports.indexOf(port) === -1) { 121 | WebWorker.log( 122 | 'Message ' + data.msg + ' received from "dead" window: ' + 123 | data.windowId 124 | ); 125 | return; 126 | } 127 | 128 | var handler = messageHandlers[data.msg]; 129 | 130 | if (handler) { 131 | handler(port, data); 132 | } 133 | else { 134 | WebWorker.log("Error: Unknown message type: " + data.msg); 135 | } 136 | }; 137 | 138 | 139 | global.onconnect = function (event) { 140 | var port = event.ports[0]; 141 | 142 | if (WebWorker.logs.length > 0) { 143 | port.postMessage({log: WebWorker.logs}); 144 | WebWorker.logs = []; 145 | } 146 | 147 | port.onmessage = function (event) { 148 | onmessage(port, event); 149 | }; 150 | 151 | WebWorker.ports.push(port); 152 | }; 153 | 154 | // Weirdly, shared web workers have no way of detecting when the 155 | // windows they're communicating with have closed. (!) We make 156 | // do by pinging windows to see if they're still alive, and 157 | // `windowIsDead` here gets called when we don't get a response. 158 | 159 | WebWorker.windowIsDead = function (windowId) { 160 | var i = WebWorker.windowIds.indexOf(windowId); 161 | if (i !== -1) { 162 | WebWorker.ports.splice(i, 1); 163 | WebWorker.windowIds.splice(i, 1); 164 | } 165 | }; 166 | 167 | })(); 168 | -------------------------------------------------------------------------------- /worker-client.litcoffee: -------------------------------------------------------------------------------- 1 | TODO not supporting unit tests with shared web worker yet. 2 | 3 | if (not Offline?.persistent or 4 | not @SharedWorker? or 5 | Meteor.settings?.public?.offlineData?.disableWorker or 6 | @Tinytest?) 7 | Offline._usingSharedWebWorker = false 8 | return 9 | 10 | 11 | Offline._usingSharedWebWorker = true 12 | 13 | 14 | Reference the boot script with the same "cache busting" URL that the 15 | appcache generates for the app manifest. 16 | 17 | url = __meteor_runtime_config__.offlineDataWorker.urls['worker-boot.js'] 18 | hash = url.substr(url.lastIndexOf('?') + 1) 19 | worker = new SharedWorker(url, hash) 20 | 21 | 22 | messageHandlers = {} 23 | 24 | 25 | Offline._sharedWebWorker = Worker = {} 26 | 27 | 28 | Worker.addMessageHandler = (msg, callback) -> 29 | messageHandlers[msg] = callback 30 | return 31 | 32 | 33 | Worker.addMessageHandler 'log', (data) -> 34 | for entry in data.log 35 | Meteor._debug "worker log: #{entry}" 36 | return 37 | 38 | 39 | worker.port.onmessage = (event) -> 40 | handler = messageHandlers[event.data?.msg] 41 | if handler? 42 | handler(event.data) 43 | else 44 | Meteor._debug( 45 | "unknown message received from shared web worker: " + 46 | JSON.stringify(event.data) 47 | ) 48 | 49 | 50 | Worker.post = (data) -> 51 | worker.port.postMessage( 52 | _.extend(data, {windowId: Offline._windows.thisWindowId}) 53 | ) 54 | return 55 | 56 | 57 | Worker.addMessageHandler 'ping', (data) -> 58 | Worker.post 59 | msg: 'pong' 60 | return 61 | 62 | 63 | Worker.post { 64 | msg: 'boot' 65 | __meteor_runtime_config__ 66 | } 67 | -------------------------------------------------------------------------------- /worker-server.litcoffee: -------------------------------------------------------------------------------- 1 | files = [ 2 | 'worker-boot.js' 3 | 'worker-packages.js' 4 | ] 5 | 6 | 7 | Map filename to the file's url in the appcache manifest (which 8 | includes the "cache busting" hash as the url's query parameter). 9 | 10 | urls = {} 11 | 12 | for file in files 13 | entry = _.find( 14 | WebApp.clientProgram.manifest, 15 | (entry) -> entry.url is "/packages/offline-data/#{file}" 16 | ) 17 | unless entry? 18 | throw new Error("#{file} not found in WebApp.clientProgram.manifest") 19 | urls[file] = entry.url + "?" + entry.hash 20 | 21 | 22 | Save in the runtime config delivered to the client, so the client 23 | can look up the url to use. 24 | 25 | __meteor_runtime_config__.offlineDataWorker = {urls} 26 | --------------------------------------------------------------------------------