├── .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 |
--------------------------------------------------------------------------------